baa-conductor

git clone 

commit
fe57806
parent
e89ee69
author
codex@macbookpro
date
2026-03-31 19:25:53 +0800 CST
fix: harden Gemini delivery adapter fallbacks
7 files changed,  +272, -30
M docs/firefox/README.md
+6, -6
 1@@ -17,9 +17,9 @@
 2 
 3 除此之外,live delivery 仍保留一条受限的插件侧 DOM adapter:
 4 
 5-5. `Claude` / `ChatGPT` 的 text-only `inject_message` / `send_message` 会由插件 content script 内的 delivery adapter 执行
 6+5. `Claude` / `ChatGPT` / `Gemini` 的 text-only `inject_message` / `send_message` 会由插件 content script 内的 delivery adapter 执行
 7 
 8-页面对话 UI、消息回读、DOM 自动化不是浏览器桥接的正式主能力。`browser.final_message` 只负责最终文本原样转发,不在插件里做 BAA parser。当前仍保留的 `GET /v1/browser/claude/current` 只是 Claude relay 的辅助读接口;delivery adapter 也只是 thin-plugin 交付面的受限执行层,不是通用页面自动化框架。
 9+页面对话 UI、消息回读、DOM 自动化不是浏览器桥接的正式主能力。`browser.final_message` 只负责最终文本原样转发,不在插件里做 BAA parser。当前保留的 `/v1/browser/{claude,chatgpt,gemini}/*` 只覆盖 helper / legacy 包装与辅助回读;delivery adapter 也只是 thin-plugin 交付面的受限执行层,不是通用页面自动化框架。
10 
11 ## 固定入口
12 
13@@ -92,7 +92,7 @@
14 2. 再调 `GET /describe/business`
15 3. 再调 `GET /v1/browser`,确认需要的平台记录、`status` 和 `view`
16 4. 如果目标是浏览器代发,先走 `POST /v1/browser/actions`、`POST /v1/browser/request`、`POST /v1/browser/request/cancel`
17-5. 只有在需要 Claude legacy 包装或 Claude 辅助回读时,才调 `/v1/browser/claude/*`
18+5. 只有在需要 helper / legacy 包装或辅助回读时,才调 `/v1/browser/{claude,chatgpt,gemini}/*`
19 
20 不要把这条链路当成通用公网浏览器自动化服务;正式链路依赖 `mini` 本地 Firefox 插件和本地 `/ws/firefox`。
21 
22@@ -177,7 +177,7 @@
23   - `system`
24 - `conversation_id` 允许为空;当前 replay 去重至少覆盖 `platform + assistant_message_id + raw_text`
25 - 当前 live 路径已经接到 text-only `inject / send` 闭环
26-- 但插件侧 `inject / send` 仍是 DOM adapter,当前只对 `Claude` / `ChatGPT` 做了选择器收口、page readiness 探测和有限重试
27+- 但插件侧 `inject / send` 仍是 DOM adapter,当前已对 `Claude` / `ChatGPT` / `Gemini` 做了选择器收口、page readiness 探测和有限重试;Gemini 兼容 `rich-text-field` / `rich-textarea` 的 Quill composer 变体
28 - 如果页面未 ready、关键 selector 缺失或 send 点击后没有出现确认状态,adapter 会以 `delivery.<code>` 形式明确 fail-closed,不会把“实际没发出”的场景误标成成功
29 - 超长文本当前默认只保留前 `200` 行,并在末尾追加 `超长截断`
30 - 当前交付仍按任务边界停留在单客户端、单轮 delivery 首版
31@@ -190,7 +190,7 @@
32 2. `POST /v1/browser/actions`
33 3. `POST /v1/browser/request`
34 4. `POST /v1/browser/request/cancel`
35-5. `POST` / `GET /v1/browser/claude/*`(legacy 包装与 Claude 辅助读)
36+5. `POST` / `GET /v1/browser/{claude,chatgpt,gemini}/*`(helper / legacy 包装与辅助读)
37 
38 这条链路的关键边界:
39 
40@@ -322,7 +322,7 @@
41 4. 再调 `POST /v1/browser/request` + `responseMode=sse`
42 5. 确认浏览器回传 `stream_open` / `stream_event` / `stream_end`
43 6. 如需终止 in-flight request 或流,调 `POST /v1/browser/request/cancel`
44-7. 如需 Claude 辅助回读或旧调用方兼容,再调 `/v1/browser/claude/*`
45+7. 如需 helper 辅助回读或旧调用方兼容,再调 `/v1/browser/{claude,chatgpt,gemini}/*`
46 
47 ### 4. 控制面
48 
M plans/STATUS_SUMMARY.md
+11, -10
 1@@ -6,7 +6,7 @@
 2 
 3 ## 当前代码基线
 4 
 5-- 当前主分支:`main@2d65427`
 6+- 当前主分支:`main@e89ee69`
 7 - canonical local API:`http://100.71.210.78:4317`
 8 - canonical public host:`https://conductor.makefile.so`
 9 - 活跃任务文档和近期刚完成的任务文档保留在 `tasks/` 根目录;较早已完成任务归档到 [`../tasks/archive/README.md`](../tasks/archive/README.md)
10@@ -29,6 +29,8 @@
11   - `@conductor`
12   - `@system`
13   - `@browser.claude`
14+  - `@browser.chatgpt`
15+  - `@browser.gemini`
16 - delivery 当前已经是 proxy-first:
17   - conductor 记录最近观察到的业务页 route
18   - 优先派发 `browser.proxy_delivery`
19@@ -54,6 +56,7 @@
20 - `BUG-026` 已随 `98db481` 修复并归档,不再作为 open blocker 挂在 backlog
21 - `BUG-029` 已随 `625f808` 修复并归档,不再作为 open blocker 挂在 backlog
22 - `BUG-027`、`BUG-028`、`BUG-031`、`BUG-032`、`BUG-033`、`BUG-034`、`BUG-036` 已全部收口并归档,旧汇总中的 open 状态已改正
23+- `T-S048`、`T-S049` 已分别随 `7d8b4ce`、`57958a9` 合入 `main`,旧汇总仍把它们列为“下一波任务”;现统一改正
24 - `T-S055`~`T-S059` 已全部完成并合入 `main`,不再继续作为“下一波主线”
25 - `T-BUG-029` / `T-BUG-031` 的任务卡已完成,但旧汇总文档仍把它们写成 pending manual verification;现统一改为“建议补做浏览器复核”
26 - Artifact 静态服务已经完成,不再把它写成“下一阶段主线”
27@@ -62,12 +65,10 @@
28 
29 **当前下一波任务:**
30 
31-1. `T-S048`:Gemini 投递适配器
32-2. `T-S049`:开放 `@browser.chatgpt` / `@browser.gemini` target(依赖 `T-S048`)
33-3. `OPT-002`:executor 超时保护
34-4. `OPT-007`:renewal dispatcher 随机抖动
35-5. `OPT-008`:timed-jobs 日志改异步写入
36-6. `OPT-009`:renewal 模块重复工具函数抽取
37+1. `OPT-002`:executor 超时保护
38+2. `OPT-007`:renewal dispatcher 随机抖动
39+3. `OPT-008`:timed-jobs 日志改异步写入
40+4. `OPT-009`:renewal 模块重复工具函数抽取
41 
42 并行需要持续关注:
43 
44@@ -120,8 +121,8 @@ Phase 1(浏览器主链)、Artifact 静态服务,以及 timed-jobs + 续
45 - 现有主链已经具备完整 artifact 持久化、静态访问和插件诊断能力
46 - 已有本地对话/关联/续命任务、projector、dispatcher 和最小续命运维读接口
47 - 当前主线已无 open bug blocker
48-- 下一波回到 Gemini 投递与开放 chatgpt/gemini target 的 backlog
49-- 其余以 `OPT-*` 优化项为主
50+- `browser.chatgpt` / `browser.gemini` helper target 与 Gemini DOM delivery adapter 已在主线
51+- 当前主要以 `OPT-*` 优化项和浏览器复核为主
52 
53 之前的浏览器主链继续保持:
54 
55@@ -133,7 +134,7 @@ Phase 1(浏览器主链)、Artifact 静态服务,以及 timed-jobs + 续
56 ## 当前仍需关注
57 
58 - 风控状态当前仍是进程内内存态;`conductor` 重启后,限流、退避和熔断计数会重置
59-- `Gemini` 当前仍不是 `/v1/browser/request` 的正式支持面,正式 browser target 仍只有 `browser.claude`
60+- `Gemini` 当前仍不是 `/v1/browser/request` 的正式 raw relay 支持面;`@browser.gemini` 走 helper / proxy mix,仍需依赖最近观测到的真实请求上下文
61 - ChatGPT proxy send 仍依赖最近捕获的真实发送模板;如果 controller 刚重载且还没观察到真实发送,会退回同页 DOM fallback
62 - Claude 的 `organizationId` 当前仍依赖最近观测到的 org 上下文,不是完整的多页多 org 精确映射
63 - `proxy_delivery` 当前的成功语义是”请求已派发到目标页面上下文”,不是”下游 AI 已完整回复”
M plugins/baa-firefox/README.md
+4, -4
 1@@ -9,9 +9,9 @@ Firefox 插件的正式能力已经收口到四件事:
 2 
 3 另有一条受限的 thin-plugin delivery 附属能力:
 4 
 5-- 仅对 `Claude` / `ChatGPT` 提供 DOM adapter 方式的 text-only `inject_message` / `send_message`
 6-- adapter 会把平台选择器、page readiness、重试和 fail-closed 错误收口到统一模块
 7-- 这不是通用浏览器自动化框架,也不扩到 `Gemini`
 8+- 对 `Claude` / `ChatGPT` / `Gemini` 提供 DOM adapter 方式的 text-only `inject_message` / `send_message`
 9+- adapter 会把平台选择器、page readiness、重试和 fail-closed 错误收口到统一模块;Gemini 同时兼容 `rich-text-field` / `rich-textarea` 的 Quill composer 变体
10+- 这不是通用浏览器自动化框架
11 
12 页面对话 UI、会话回读和 DOM 自动化仍然不是正式主能力。`browser.final_message` 只负责最终文本转发,不负责 BAA parser;delivery adapter 只服务 text-only delivery 的最小执行面。
13 
14@@ -207,4 +207,4 @@ browser.runtime.sendMessage({
15 - `GET /v1/browser` 已经会合并活跃 bridge 和持久化记录,并返回 `view`、`status`、`account`、凭证指纹和端点元数据
16 - client 断连或长时间老化后,持久化记录仍可读取,但状态会转成 `stale` / `lost`
17 - 当前正式浏览器写接口已经收口到 `POST /v1/browser/actions`、`POST /v1/browser/request`、`POST /v1/browser/request/cancel`
18-- `/v1/browser/claude/*` 只保留 legacy 包装与 Claude 辅助读
19+- `/v1/browser/{claude,chatgpt,gemini}/*` 保留给 helper / legacy 包装与辅助回读
M plugins/baa-firefox/delivery-adapters.js
+4, -0
 1@@ -68,12 +68,15 @@
 2         ".conversation-container",
 3         "chat-window",
 4         "rich-text-field",
 5+        "rich-textarea",
 6         ".ql-editor[contenteditable='true']",
 7         "[role='textbox']"
 8       ],
 9       composerSelectors: [
10         "rich-text-field .ql-editor[contenteditable='true']",
11+        "rich-textarea .ql-editor[contenteditable='true']",
12         "rich-text-field div[contenteditable='true']",
13+        "rich-textarea div[contenteditable='true']",
14         "div[contenteditable='true'][aria-label*='prompt' i]",
15         "div[contenteditable='true'][aria-label*='message' i]",
16         "div[contenteditable='true'][role='textbox']",
17@@ -83,6 +86,7 @@
18       sendButtonSelectors: [
19         "button[aria-label*='Send message' i]",
20         "button[aria-label*='send' i]",
21+        "button[mattooltip*='send' i]",
22         ".send-button",
23         ".input-area-container button:not([aria-label*='microphone' i])"
24       ],
A plugins/baa-firefox/delivery-adapters.test.cjs
+231, -0
  1@@ -0,0 +1,231 @@
  2+const assert = require("node:assert/strict");
  3+const test = require("node:test");
  4+
  5+const {
  6+  createDeliveryRuntime,
  7+  getPlatformAdapter,
  8+  listPlatformAdapters
  9+} = require("./delivery-adapters.js");
 10+
 11+function createMockElement(options = {}) {
 12+  const attributes = new Map(
 13+    Object.entries(options.attributes || {}).map(([key, value]) => [String(key).toLowerCase(), String(value)])
 14+  );
 15+
 16+  return {
 17+    disabled: options.disabled === true,
 18+    dispatchedEvents: [],
 19+    focusCalls: 0,
 20+    isContentEditable: options.isContentEditable === true,
 21+    tagName: String(options.tagName || "DIV").toUpperCase(),
 22+    textContent: options.textContent || "",
 23+    value: options.value || "",
 24+    click() {
 25+      options.onClick?.(this);
 26+    },
 27+    dispatchEvent(event) {
 28+      this.dispatchedEvents.push(event);
 29+      return true;
 30+    },
 31+    focus() {
 32+      this.focusCalls += 1;
 33+    },
 34+    getAttribute(name) {
 35+      return attributes.get(String(name || "").toLowerCase()) ?? null;
 36+    },
 37+    getBoundingClientRect() {
 38+      if (options.visible === false) {
 39+        return {
 40+          height: 0,
 41+          width: 0
 42+        };
 43+      }
 44+
 45+      return {
 46+        height: 32,
 47+        width: 120
 48+      };
 49+    }
 50+  };
 51+}
 52+
 53+function createHarness(options = {}) {
 54+  const state = {
 55+    now: 0,
 56+    sending: false,
 57+    url: options.url || "https://gemini.google.com/app/conv-gemini-smoke"
 58+  };
 59+  const selectorMap = new Map();
 60+  const document = {
 61+    body: options.pageReady === false ? null : {},
 62+    querySelectorAll(selector) {
 63+      const entry = selectorMap.get(selector);
 64+
 65+      if (typeof entry === "function") {
 66+        return entry();
 67+      }
 68+
 69+      return entry || [];
 70+    },
 71+    readyState: options.pageReady === false ? "loading" : "complete"
 72+  };
 73+  const runtime = createDeliveryRuntime({
 74+    env: {
 75+      createChangeEvent() {
 76+        return {
 77+          type: "change"
 78+        };
 79+      },
 80+      createInputEvent(data) {
 81+        return {
 82+          data,
 83+          type: "input"
 84+        };
 85+      },
 86+      document,
 87+      getComputedStyle() {
 88+        return {
 89+          display: "block",
 90+          opacity: "1",
 91+          visibility: "visible"
 92+        };
 93+      },
 94+      getLocationHref() {
 95+        return state.url;
 96+      },
 97+      now() {
 98+        return state.now;
 99+      },
100+      async sleep(ms) {
101+        state.now += Number(ms) || 0;
102+      }
103+    }
104+  });
105+
106+  return {
107+    document,
108+    registerSelector(selector, elements) {
109+      selectorMap.set(selector, elements);
110+    },
111+    runtime,
112+    state
113+  };
114+}
115+
116+test("delivery adapters register Gemini with current selector fallbacks", () => {
117+  const adapter = getPlatformAdapter("gemini");
118+
119+  assert.ok(adapter);
120+  assert.equal(adapter.platform, "gemini");
121+  assert.deepEqual(adapter.pageHosts, ["gemini.google.com"]);
122+  assert.equal(adapter.readinessSelectors.includes("rich-textarea"), true);
123+  assert.equal(adapter.composerSelectors.includes("rich-textarea .ql-editor[contenteditable='true']"), true);
124+  assert.equal(adapter.sendButtonSelectors.includes("button[mattooltip*='send' i]"), true);
125+  assert.equal(
126+    listPlatformAdapters().some((entry) => entry?.platform === "gemini"),
127+    true
128+  );
129+});
130+
131+test("Gemini inject_message works with rich-textarea Quill editor selectors", async () => {
132+  const harness = createHarness();
133+  const readyMarker = createMockElement({
134+    tagName: "rich-textarea"
135+  });
136+  const composer = createMockElement({
137+    isContentEditable: true,
138+    tagName: "div"
139+  });
140+
141+  harness.registerSelector("rich-textarea", [readyMarker]);
142+  harness.registerSelector("rich-textarea .ql-editor[contenteditable='true']", [composer]);
143+
144+  const result = await harness.runtime.handleCommand({
145+    command: "inject_message",
146+    platform: "gemini",
147+    retryAttempts: 1,
148+    text: "hello from Gemini adapter test",
149+    timeoutMs: 120
150+  });
151+
152+  assert.equal(result.ok, true);
153+  assert.equal(result.details.confirmed_by, "composer_text_match");
154+  assert.equal(composer.textContent, "hello from Gemini adapter test");
155+  assert.deepEqual(
156+    composer.dispatchedEvents.map((event) => event.type),
157+    ["input", "change"]
158+  );
159+});
160+
161+test("Gemini send_message confirms via mattooltip send button and stop icon", async () => {
162+  const harness = createHarness();
163+  const readyMarker = createMockElement({
164+    tagName: "div"
165+  });
166+  const stopIndicator = createMockElement({
167+    tagName: "mat-icon"
168+  });
169+  const composer = createMockElement({
170+    isContentEditable: true,
171+    tagName: "div",
172+    textContent: "queued gemini message"
173+  });
174+  const sendButton = createMockElement({
175+    attributes: {
176+      mattooltip: "Send message"
177+    },
178+    onClick() {
179+      composer.textContent = "";
180+      harness.state.sending = true;
181+    },
182+    tagName: "button"
183+  });
184+
185+  harness.registerSelector(".conversation-container", [readyMarker]);
186+  harness.registerSelector("rich-textarea .ql-editor[contenteditable='true']", [composer]);
187+  harness.registerSelector("button[mattooltip*='send' i]", [sendButton]);
188+  harness.registerSelector(
189+    "mat-icon[data-mat-icon-name='stop_circle']",
190+    () => (harness.state.sending ? [stopIndicator] : [])
191+  );
192+
193+  const result = await harness.runtime.handleCommand({
194+    command: "send_message",
195+    platform: "gemini",
196+    retryAttempts: 1,
197+    timeoutMs: 120
198+  });
199+
200+  assert.equal(result.ok, true);
201+  assert.equal(result.details.confirmed_by, "sending_indicator");
202+  assert.equal(harness.state.sending, true);
203+  assert.equal(composer.textContent, "");
204+});
205+
206+test("Gemini adapter fails closed on page host mismatch", async () => {
207+  const harness = createHarness({
208+    url: "https://chatgpt.com/c/conv-host-mismatch"
209+  });
210+  const readyMarker = createMockElement({
211+    tagName: "rich-textarea"
212+  });
213+  const composer = createMockElement({
214+    isContentEditable: true,
215+    tagName: "div"
216+  });
217+
218+  harness.registerSelector("rich-textarea", [readyMarker]);
219+  harness.registerSelector("rich-textarea .ql-editor[contenteditable='true']", [composer]);
220+
221+  const result = await harness.runtime.handleCommand({
222+    command: "inject_message",
223+    platform: "gemini",
224+    retryAttempts: 1,
225+    text: "should fail",
226+    timeoutMs: 120
227+  });
228+
229+  assert.equal(result.ok, false);
230+  assert.equal(result.code, "page_context_mismatch");
231+  assert.match(result.reason, /delivery\.page_context_mismatch/u);
232+});
M tasks/T-S048.md
+7, -0
 1@@ -102,3 +102,10 @@
 2 
 3 - 选择器基于 Gemini 截至 2025 年初的已知 DOM 结构,Google 可能随时更新页面结构导致选择器失效,需在 Firefox 中实际验证并按需调整。
 4 - `mat-icon[data-mat-icon-name='stop_circle']` 选择器依赖 Angular Material 实现细节,若 Gemini 前端框架迁移可能失效。
 5+
 6+### 2026-03-31 补充维护
 7+
 8+- 执行者:Codex
 9+- 维护内容:基于最新 `main@e89ee69` 重新创建任务 worktree;为 Gemini adapter 补充 `rich-textarea` / `mattooltip` 选择器 fallback,并新增 `plugins/baa-firefox/delivery-adapters.test.cjs` 覆盖 Gemini 的 inject/send/host mismatch 回归。
10+- 文档同步:更新 `plugins/baa-firefox/README.md`、`docs/firefox/README.md`、`tasks/TASK_OVERVIEW.md`、`plans/STATUS_SUMMARY.md`,纠正“Gemini adapter 未完成”与“browser target 仍只有 claude”的旧口径。
11+- 额外验证:已运行 `node --test plugins/baa-firefox/delivery-adapters.test.cjs`;当前环境未做真实 Firefox + Gemini 页面手工复核。
M tasks/TASK_OVERVIEW.md
+9, -10
 1@@ -3,7 +3,7 @@
 2 ## 当前基线
 3 
 4 - 日期:`2026-03-31`
 5-- 主分支基线:`main@2d65427`
 6+- 主分支基线:`main@e89ee69`
 7 - canonical local API:`http://100.71.210.78:4317`
 8 - canonical public host:`https://conductor.makefile.so`
 9 - 当前活跃任务卡和近期刚完成的任务卡保留在本目录;较早已完成任务归档到 [`./archive/README.md`](./archive/README.md)
10@@ -25,7 +25,7 @@
11 - ChatGPT / Claude / Gemini 都已接入 `browser.final_message` raw relay
12 - ChatGPT 新对话 / 新回复的 stale final-message replay 已抑制,不再默认把新回复判成旧消息 `duplicate_message`
13 - ChatGPT SSE abort 修复和插件重载自动刷新页面都已合入 `main`
14-- conductor 已具备 BAA Phase 1:`@conductor` / `@system` / `@browser.claude`
15+- conductor 已具备 BAA Phase 1:`@conductor` / `@system` / `@browser.claude` / `@browser.chatgpt` / `@browser.gemini`
16 - delivery 当前已经是 proxy-first:
17   - conductor 记录最近观察到的业务页 route 与回写目标
18   - 优先通过 `browser.proxy_delivery` 在目标页面上下文执行真实请求
19@@ -53,6 +53,7 @@
20 - `BUG-026` 已经随 `98db481` 收口并归档,不再继续作为 open bug 跟踪
21 - `BUG-029` 已经随 `625f808` 修复并归档,不再继续作为 open bug 跟踪
22 - `BUG-027`、`BUG-028`、`BUG-031`、`BUG-032`、`BUG-033`、`BUG-034`、`BUG-036` 已全部收口并归档,旧总览中的 open 状态已改正
23+- `T-S048`、`T-S049` 已分别随 `7d8b4ce`、`57958a9` 合入 `main`,旧总览里仍写成 backlog;现统一改正
24 - `T-S055`~`T-S059` 已全部完成并合入 `main`,旧总览中的 pending 状态已改正
25 - `T-BUG-029`、`T-BUG-031` 的任务卡已是 `已完成`,但旧文档仍把它们写成 pending manual verification;现统一改为“建议补做浏览器复核”
26 - Artifact 静态服务已经完成,不再把 `T-S039`~`T-S045` 写成“当前活跃主线”
27@@ -73,19 +74,15 @@
28 
29 | 项目 | 标题 | 类型 | 状态 | 说明 |
30 |---|---|---|---|---|
31-| [`T-S048`](./T-S048.md) | Gemini 投递适配器 | feat | backlog | 暂时后移,但仍是正式 backlog |
32-| [`T-S049`](./T-S049.md) | 开放 chatgpt/gemini target | feat | backlog | 依赖 `T-S048` |
33 | [`../bugs/OPT-002-executor-timeout.md`](../bugs/OPT-002-executor-timeout.md) | executor 超时保护 | opt | open | 仍建议优先收口,避免执行链路无限阻塞 |
34 | [`../plans/OPT-007-DISPATCHER-JITTER.md`](../plans/OPT-007-DISPATCHER-JITTER.md) | renewal dispatcher 随机抖动 | opt | open | 限流缓冲与节奏分散 |
35 | [`../bugs/OPT-008-timed-jobs-async-log-writes.md`](../bugs/OPT-008-timed-jobs-async-log-writes.md) | timed-jobs 异步日志写入 | opt | open | 低优先级性能卫生 |
36 | [`../bugs/OPT-009-renewal-duplicate-utility-functions.md`](../bugs/OPT-009-renewal-duplicate-utility-functions.md) | renewal 工具函数去重 | opt | open | 低优先级代码卫生 |
37 
38-### 暂时后移的 backlog 与参考
39+### 已完成但保留作参考
40 
41 | 任务 | 标题 | 说明 |
42 |---|---|---|
43-| [`T-S048`](./T-S048.md) | Gemini 投递适配器 | 仍保留,但暂让位于续命/定时任务主线 |
44-| [`T-S049`](./T-S049.md) | 开放 chatgpt/gemini target | 依赖 T-S048,暂后移 |
45 | [`T-S051`](./T-S051.md) | 代码文件直读映射 | 已完成,保留为后续实现参考 |
46 
47 ### 已完成
48@@ -101,6 +98,8 @@
49 | T-S045 | D1 同步串联 | ✅ |
50 | T-S046 | 日志落盘 + files/read kind | ✅ |
51 | T-S047 | 跨会话接续入口 | ✅ |
52+| T-S048 | Gemini 投递适配器 | ✅ |
53+| T-S049 | 开放 chatgpt/gemini target | ✅ |
54 | T-S050 | stagit git 静态页面 | ✅ |
55 | T-S052 | D1 数据库初始化 | ✅ |
56 | T-S053 | 插件诊断日志 | ✅ |
57@@ -173,9 +172,9 @@
58 
59 Phase 1(浏览器主链)、Artifact 静态服务,以及 timed-jobs + 续命主线都已完成收口。当前主线已经没有 open bug blocker,下一步是:
60 
61-- 回到 backlog `T-S048`、`T-S049`
62-- 并行推进 `OPT-002`
63-- 视情况收口 `OPT-007`、`OPT-008`、`OPT-009`
64+- 优先收口 `OPT-002`
65+- 并行推进 `OPT-007`
66+- 视情况收口 `OPT-008`、`OPT-009`
67 
68 ## 现在该读什么
69