- commit
- d04922a
- parent
- 293d471
- author
- codex@macbookpro
- date
- 2026-03-27 13:42:38 +0800 CST
docs: sync progress docs and bug status from code review
12 files changed,
+441,
-283
+228,
-0
1@@ -0,0 +1,228 @@
2+# 当前代码进度核对(2026-03-27)
3+
4+## 结论摘要
5+
6+- 当前“功能代码基线”按已提交代码看是 `main@25be868`,主题是 `restore managed firefox shell tabs on startup`。
7+- 当前浏览器桥接主线已经完成到“Firefox 本地 WS bridge + Claude 代发 + 结构化 action_result + shell_runtime + 登录态持久化”的阶段。
8+- 代码和自动化测试都表明:`/describe/business`、`/describe/control`、`GET /v1/browser`、`POST /v1/browser/actions`、`POST /v1/browser/request`、`POST /v1/browser/request/cancel` 已经形成正式主链路。
9+- 目前不应再把系统描述成“只有 Claude 专用页面路径”;当前是“通用 browser surface 已落地,但正式 relay 仍只有 Claude 接通,其它平台主要停留在空壳页和元数据链路”。
10+- 当前仍不能写成“全部收尾完成”。剩余未闭项主要是:真实 Firefox 手工 smoke 未完成、`BUG-011` 到 `BUG-014` 仍待修、`BUG-014` 当前代码仍然存在、风控状态仍是进程内内存态。
11+
12+## 本次核对依据
13+
14+### 代码核对范围
15+
16+- `../apps/conductor-daemon/src/local-api.ts`
17+- `../apps/conductor-daemon/src/firefox-bridge.ts`
18+- `../apps/conductor-daemon/src/firefox-ws.ts`
19+- `../apps/conductor-daemon/src/browser-request-policy.ts`
20+- `../apps/conductor-daemon/src/browser-types.ts`
21+- `../plugins/baa-firefox/controller.js`
22+- `../scripts/runtime/verify-mini.sh`
23+
24+### 本次实际执行的验证
25+
26+- `pnpm -C /Users/george/code/baa-conductor -F @baa-conductor/conductor-daemon build`
27+ - 结果:通过
28+- `node --test /Users/george/code/baa-conductor/apps/conductor-daemon/src/index.test.js`
29+ - 结果:`25/25` 通过
30+- `node --test /Users/george/code/baa-conductor/tests/browser/browser-control-e2e-smoke.test.mjs`
31+ - 结果:`3/3` 通过
32+
33+## 当前已完成功能
34+
35+### 1. 本地 API 和两层 describe 已落地
36+
37+- 代码里已经固定保留两层 describe:
38+ - `/describe/business`
39+ - `/describe/control`
40+- 浏览器正式接口已经收口到:
41+ - `GET /v1/browser`
42+ - `POST /v1/browser/actions`
43+ - `POST /v1/browser/request`
44+ - `POST /v1/browser/request/cancel`
45+- 证据:
46+ - `../apps/conductor-daemon/src/local-api.ts:3230-3250`
47+ - `../apps/conductor-daemon/src/local-api.ts:4700-4875`
48+ - `../apps/conductor-daemon/src/index.test.js:1734-1795`
49+ - `../apps/conductor-daemon/src/index.test.js:1840-1955`
50+
51+### 2. `GET /v1/browser` 已经是统一读面
52+
53+- 当前读面不只是 bridge 在线状态,还包含:
54+ - `bridge`
55+ - `current_client`
56+ - `claude`
57+ - `records`
58+ - `policy`
59+ - `summary`
60+- `current_client` 已带:
61+ - `shell_runtime`
62+ - `last_action_result`
63+- merged records 已带活跃态和持久化态合并视图。
64+- 证据:
65+ - `../apps/conductor-daemon/src/local-api.ts:2747-2825`
66+ - `../apps/conductor-daemon/src/browser-types.ts:103-150`
67+ - `../apps/conductor-daemon/src/index.test.js:1840-1858`
68+ - `../tests/browser/browser-control-e2e-smoke.test.mjs:387-404`
69+
70+### 3. 通用 browser request/cancel/SSE 主链路已落地
71+
72+- `POST /v1/browser/request` 已支持 buffered JSON 和正式 SSE。
73+- SSE 事件类型已经明确实现为:
74+ - `stream_open`
75+ - `stream_event`
76+ - `stream_end`
77+ - `stream_error`
78+- `POST /v1/browser/request/cancel` 已接通取消链路。
79+- 证据:
80+ - `../apps/conductor-daemon/src/local-api.ts:4717-4789`
81+ - `../apps/conductor-daemon/src/local-api.ts:4792-4875`
82+ - `../apps/conductor-daemon/src/firefox-bridge.ts:701-739`
83+ - `../apps/conductor-daemon/src/firefox-bridge.ts:792-917`
84+ - `../tests/browser/browser-control-e2e-smoke.test.mjs:513-645`
85+ - `../apps/conductor-daemon/src/index.test.js:1894-1955`
86+
87+### 4. 浏览器风控策略已在 `conductor-daemon` 内实现
88+
89+- 默认策略已实装,不只是文档约定:
90+ - 抖动
91+ - 平台限流
92+ - 单 `client/platform` 并发
93+ - 失败退避
94+ - 熔断
95+ - stream open/idle timeout
96+ - stream buffer 上限
97+- 证据:
98+ - `../apps/conductor-daemon/src/browser-request-policy.ts:11-145`
99+ - `../apps/conductor-daemon/src/local-api.ts:2810-2825`
100+
101+### 5. 登录态元数据持久化已经落地,而且不暴露原始凭证
102+
103+- 当前代码会持久化:
104+ - `account`
105+ - `credential_fingerprint`
106+ - endpoint metadata
107+ - freshness / status
108+- 当前代码不会把原始 `cookie` / header 值暴露到 `GET /v1/browser`。
109+- 断连和重启后,持久化记录仍可读。
110+- 证据:
111+ - `../apps/conductor-daemon/src/firefox-ws.ts:1330-1365`
112+ - `../apps/conductor-daemon/src/index.test.js:3434-3560`
113+ - `../tests/browser/browser-control-e2e-smoke.test.mjs:833-977`
114+
115+### 6. 浏览器登录态 aging 已落地
116+
117+- 浏览器登录态会从 `fresh -> stale -> lost` 演进。
118+- 这部分不只在文档里,自动化测试已覆盖。
119+- 证据:
120+ - `../apps/conductor-daemon/src/index.test.js:3575-3650`
121+ - `../tests/browser/browser-control-e2e-smoke.test.mjs:996-1076`
122+
123+### 7. 结构化 `action_result` 和 `shell_runtime` 已接入主线
124+
125+- 插件侧已经回传结构化 `action_result`:
126+ - `accepted`
127+ - `completed`
128+ - `failed`
129+ - `reason`
130+ - `target`
131+ - `result`
132+ - `results`
133+ - `shell_runtime`
134+- `conductor-daemon` 已消费并挂到:
135+ - `current_client.last_action_result`
136+ - `current_client.shell_runtime`
137+ - `records[].live.shell_runtime`
138+- 证据:
139+ - `../plugins/baa-firefox/controller.js:3218-3284`
140+ - `../apps/conductor-daemon/src/firefox-ws.ts:1268-1328`
141+ - `../apps/conductor-daemon/src/local-api.ts:4328-4375`
142+ - `../apps/conductor-daemon/src/index.test.js:1849-1877`
143+ - `../tests/browser/browser-control-e2e-smoke.test.mjs:428-512`
144+
145+### 8. Firefox 插件空壳页模型和启动恢复逻辑已存在
146+
147+- 插件管理动作已支持:
148+ - `plugin_status`
149+ - `ws_reconnect`
150+ - `controller_reload`
151+ - `tab_open`
152+ - `tab_focus`
153+ - `tab_reload`
154+ - `tab_restore`
155+- `desired / actual / drift` 模型已在插件侧维护。
156+- `2026-03-27` 的代码跟进已经补上:
157+ - 启动时自动恢复缺失的 desired shell tabs
158+ - 识别平台根页并纳入受管理 shell 集合
159+- 证据:
160+ - `../plugins/baa-firefox/controller.js:3053-3100`
161+ - `../plugins/baa-firefox/controller.js:3120-3146`
162+ - `../plugins/baa-firefox/controller.js:3366-3455`
163+ - `../plugins/baa-firefox/controller.js:4401-4425`
164+
165+### 9. 自动化和运维入口已具备
166+
167+- 根脚本已经提供:
168+ - `pnpm test`
169+ - `pnpm smoke`
170+ - `pnpm verify:mini`
171+- `verify-mini.sh` 当前是正式 on-node 静态 + 运行态检查入口。
172+- 证据:
173+ - `../package.json:5-13`
174+ - `../scripts/runtime/verify-mini.sh`
175+
176+## 当前未完成 / 待复核
177+
178+### 1. 真实 Firefox 手工 smoke 仍未完成
179+
180+- 代码和任务文档里都没有新增“真实 Firefox.app 上手动关 tab -> tab_restore -> WS 重连 -> 状态恢复”的实测结论。
181+- 当前自动化 smoke 是 bridge / relay / persistence 层面的模拟 E2E,不等于真实 Firefox 桌面手工验收。
182+
183+### 2. `BUG-011` 到 `BUG-014` 仍然是 open backlog
184+
185+- 当前不能写成“bug backlog 已清空”。
186+- 尤其 `BUG-014` 目前从代码可直接确认仍存在:
187+ - `../plugins/baa-firefox/controller.js:3384-3389` 的 `ws_reconnect` 仍通过 `setTimeout` 延后真正重连
188+ - `../plugins/baa-firefox/controller.js:3254-3256` 里 `completed` 默认仍会按 `true` 回传
189+- 这意味着 `ws_reconnect` 的 `action_result.completed` 语义现在仍然偏早。
190+
191+### 3. 正式 browser relay 仍然只有 Claude 接通
192+
193+- `chatgpt` 和 `gemini` 在插件侧已有平台定义、空壳页和元数据路径。
194+- 但从 `local-api.ts` 的正式说明和测试覆盖看,真正走通 request / SSE / legacy wrapper 的还是 Claude。
195+- 因此当前应表述为:
196+ - Claude:正式 relay 已可用
197+ - ChatGPT / Gemini:元数据和 shell runtime 已有,通用 relay 未见同等级落地验证
198+
199+### 4. 风控状态仍是进程内内存态
200+
201+- 默认策略本身已实现。
202+- 但策略运行态计数并未持久化,进程重启后限流 / 退避 / 熔断状态会重置。
203+
204+### 5. 启动自动恢复逻辑目前主要靠代码核对,不是现成自动化用例
205+
206+- 这次 `25be868` 的启动恢复改动已经在 `controller.js` 中存在。
207+- 但仓库里当前没有直接覆盖“浏览器启动后自动后台 `tab_restore`”的独立测试文件。
208+- 因此这部分目前更适合写成:
209+ - “代码已落地”
210+ - “建议继续补专项自动化或真实 Firefox 手工验收”
211+
212+## 建议给 Claude 重点复核的点
213+
214+1. 复核 `../plugins/baa-firefox/controller.js:3090-3100`、`3366-3455`、`4401-4425`
215+ - 目标:确认启动自动恢复和根页 adopt 逻辑确实能把 desired/actual 调和起来
216+2. 复核 `../plugins/baa-firefox/controller.js:3384-3389` 与 `3218-3256`
217+ - 目标:确认 `BUG-014` 的判断是否准确
218+3. 复核 `../apps/conductor-daemon/src/local-api.ts:2747-2825`、`4328-4375`、`4717-4875`
219+ - 目标:确认当前正式 browser 读写面和 action_result/stream/cancel 的外部合同描述是否准确
220+4. 复核 `../apps/conductor-daemon/src/browser-request-policy.ts:117-145`
221+ - 目标:确认默认风控参数已经实装,而不是仅在文档里声明
222+5. 复核 `../apps/conductor-daemon/src/index.test.js` 和 `../tests/browser/browser-control-e2e-smoke.test.mjs`
223+ - 目标:确认本文写的“自动化已验证范围”没有夸大
224+
225+## 适合对外同步的简版结论
226+
227+如果只写一段给外部协作者看,可以用下面这版:
228+
229+> 当前代码已经完成单节点 `mini` 主接口收口,以及 Firefox 本地 bridge 下的 Claude browser relay 主链路。`GET /v1/browser`、`POST /v1/browser/actions`、`POST /v1/browser/request`、`POST /v1/browser/request/cancel`、正式 SSE、结构化 `action_result`、`shell_runtime`、登录态元数据持久化都已落地,并已通过 `conductor-daemon build`、`index.test.js`(25/25)和 browser-control e2e smoke(3/3)。当前剩余缺口主要是:真实 Firefox 手工 smoke 未完成、`BUG-011`~`BUG-014` 仍待修、正式 relay 仍只有 Claude 接通、以及风控运行态仍是进程内内存态。
+13,
-0
1@@ -0,0 +1,13 @@
2+# PROGRESS
3+
4+这个目录用于沉淀“按当前代码实际状态核对后的进度文档”,方便继续交给 Claude 或人工复核。
5+
6+当前最新报告:
7+
8+- [`2026-03-27-current-code-progress.md`](./2026-03-27-current-code-progress.md)
9+
10+阅读建议:
11+
12+1. 先读最新报告里的“结论摘要”和“当前未完成 / 待复核”
13+2. 再按报告里的代码路径和测试文件逐项抽查
14+3. 如果后续代码继续推进,新增同日期报告,保留旧报告做时间点对照
+35,
-61
1@@ -1,63 +1,37 @@
2 # BUG-015: SSE 流式模式 stream_open_timeout
3
4-## 现象
5-
6-通过 `POST /v1/browser/request` 以 `responseMode: "sse"` 向 Claude completion 端点发请求时,conductor 返回 `stream_open_timeout`:
7-
8-```json
9-{
10- "error": "stream_open_timeout",
11- "message": "Browser stream did not open within 10000ms.",
12- "partial": {"buffered_bytes": 0, "event_count": 0, "last_seq": 0, "opened": false}
13-}
14-```
15-
16-同样的请求用 `responseMode: "buffered"` 能正常拿到 200 响应和完整 SSE 文本。
17-
18-## 触发路径
19-
20-```
21-conductor → firefox-bridge sendApiRequest(responseMode=sse)
22-→ WS → Firefox 插件
23-→ 插件用浏览器 fetch() 发起请求
24-→ 插件应该回传 stream_open / stream_event / stream_end
25-→ 但 conductor 侧 10 秒内没收到 stream_open
26-→ FirefoxBridgeApiStreamSession openTimer 触发 → stream_open_timeout
27-```
28-
29-## 根因分析
30-
31-最可能的原因是 Firefox 插件的 controller.js 中,SSE 拦截逻辑只在 buffered 模式下工作(直接返回完整 response),没有实现 stream_open / stream_event / stream_end 的逐事件回传给 conductor WS。
32-
33-需要排查:
34-1. controller.js 中是否有处理 `responseMode: "sse"` 的分支
35-2. 是否有发送 `stream_open`、`stream_event`、`stream_end` WS 消息的代码路径
36-3. 如果没有,需要在插件侧实现 SSE 流拦截:用 ReadableStream reader 逐行解析 SSE,每收到一个 `data:` 行就回传一个 `stream_event`
37-
38-## 复现
39-
40-```bash
41-# 先创建对话拿到 conv uuid,然后:
42-curl -s https://conductor.makefile.so/v1/browser/request \
43- -X POST -H "Content-Type: application/json" \
44- -H "Authorization: Bearer $TOKEN" \
45- -d '{
46- "platform": "claude",
47- "path": "/api/organizations/{org}/chat_conversations/{conv}/completion",
48- "method": "POST",
49- "responseMode": "sse",
50- "body": {"prompt": "hello", "model": "claude-sonnet-4-6", "timezone": "Asia/Shanghai", "attachments": [], "files": [], "rendering_mode": "raw"}
51- }'
52-# 返回 stream_error: stream_open_timeout
53-# 改为 "responseMode": "buffered" 则正常返回 200
54-```
55-
56-## 严重度
57-
58-High — SSE 是需求文档(FIREFOX_BRIDGE_CONTROL_REQUIREMENTS)定义的首批正式能力
59-
60-## 影响
61-
62-- 所有流式 API 调用(Claude completion、ChatGPT conversation)只能用 buffered 模式
63-- buffered 模式需要等完整响应返回才能读取,延迟高且无法做流式 UI
64-- conductor 侧的 stream session(seq tracking、buffer overflow、idle timeout)全套逻辑已实现但无法使用
65+## 状态
66+
67+- `已关闭(2026-03-27 代码核对)`
68+
69+## 当前代码核对结论
70+
71+原报告里的核心判断是“Firefox 插件还没有实现正式 SSE 流式回传”。这个结论与当前代码不一致。
72+
73+当前代码已经具备完整的 SSE 主链路:
74+
75+- `plugins/baa-firefox/controller.js`
76+ - 收到 `api_request` 时,已经按 `response_mode === "sse"` 走单独分支
77+ - `handlePageSse(...)` 已经把页面侧事件转成 `stream_open` / `stream_event` / `stream_end` / `stream_error`
78+- `plugins/baa-firefox/page-interceptor.js`
79+ - 已实现 `streamProxyResponse(...)`
80+ - 会用 `ReadableStream.getReader()` 逐块读取响应体
81+ - 会在页面内发出 open / chunk / done / error 事件
82+- 自动化验证已覆盖这条链路:
83+ - `tests/browser/browser-control-e2e-smoke.test.mjs`
84+ - `apps/conductor-daemon/src/index.test.js`
85+
86+## 为什么关闭
87+
88+- 这张卡的根因描述是“插件侧缺少 SSE 实现”
89+- 按当前代码核对,这个实现已经存在,且有自动化覆盖
90+- 因此这张卡不再适合作为 open bug 保留
91+
92+## 保留说明
93+
94+- 如果线上环境仍然出现 `stream_open_timeout`
95+- 应当新开一张 bug,按新的复现条件排查:
96+ - 实际请求路径
97+ - 页面注入是否成功
98+ - 是否命中了特定平台 / 特定响应格式 / 特定页面环境
99+- 不再沿用本卡“插件尚未实现 SSE”的旧根因
+23,
-42
1@@ -1,53 +1,34 @@
2 # BUG-016: browser/request 的自定义 headers 未透传到上游
3
4-## 现象
5+## 状态
6
7-通过 `POST /v1/browser/request` 发送带 `headers` 字段的请求时,自定义 header 没有被附加到插件侧的 fetch 请求中。
8+- `已关闭(2026-03-27 代码核对)`
9
10-具体场景:ChatGPT 的 `/backend-api/conversation` 端点需要 `openai-sentinel-chat-requirements-token` header,但插件侧的 fetch 只携带了浏览器自动附加的 cookie 和标准 header,缺少 sentinel token,导致 ChatGPT 返回 422。
11+## 当前代码核对结论
12
13-## 触发路径
14+原报告里的核心判断是“自定义 headers 没有进入插件侧真实 fetch”。这个结论与当前代码不一致。
15
16-```
17-conductor POST /v1/browser/request {
18- platform: "chatgpt",
19- path: "/backend-api/conversation",
20- headers: {"openai-sentinel-chat-requirements-token": "gAAAA..."},
21- body: {action: "next", messages: [...]}
22-}
23-→ conductor 将 headers 字段放入 WS 消息
24-→ Firefox 插件收到 api_request
25-→ 插件用 fetch() 发请求,但没有从 WS 消息中读取 headers 并附加
26-→ ChatGPT 返回 422: Input should be a valid dictionary
27-```
28+当前链路已经具备 header 透传:
29
30-## 根因分析
31+- `apps/conductor-daemon/src/firefox-bridge.ts`
32+ - `apiRequest(...)` / `streamRequest(...)` 已把 `headers` 放进 `api_request`
33+- `apps/conductor-daemon/src/index.test.js`
34+ - 已断言 bridge 发出的 `api_request` 消息里包含传入的 `headers`
35+- `plugins/baa-firefox/page-interceptor.js`
36+ - 在 `__baa_proxy_request__` 处理里,会把 `detail.headers` 合并进页面内 `fetch(...)`
37+ - 同时会过滤 `cookie`、`host`、`origin`、`referer`、`sec-*` 等 forbidden headers
38
39-需要排查 controller.js 中处理 `api_request` 的代码:
40-1. 是否从 WS 消息中读取了 `message.headers` 字段
41-2. 是否在 `fetch(url, { headers: ... })` 中合并了自定义 headers
42-3. 如果只透传了 cookie(通过 `credentials: "include"`),自定义 header 会丢失
43+## 为什么关闭
44
45-## 修复方向
46+- 这张卡描述的是“headers 完全未透传”
47+- 按当前代码核对,这个描述已经不成立
48+- 因此不再作为 open bug 保留
49
50-在插件侧的 api_request 处理中,从 WS 消息读取 `message.headers`(一个 key-value 对象),合并到 fetch 的 headers 中:
51+## 保留说明
52
53-```javascript
54-const customHeaders = message.headers || {};
55-const fetchHeaders = {
56- "Content-Type": "application/json",
57- ...customHeaders
58-};
59-const response = await fetch(url, { method, headers: fetchHeaders, body, credentials: "include" });
60-```
61-
62-注意安全:不应允许覆盖 `Cookie`、`Host`、`Origin` 等浏览器管控的 header。可以维护一个黑名单过滤。
63-
64-## 严重度
65-
66-Medium — 影响 ChatGPT 对话发送和其他需要自定义 header 的 API 调用
67-
68-## 影响
69-
70-- ChatGPT `/backend-api/conversation` 无法使用(需要 sentinel token header)
71-- 任何需要非标准 header 的 API 调用都无法通过 browser/request 完成
72+- 如果特定 header 仍在线上环境失效
73+- 更可能是:
74+ - 命中了 forbidden header 过滤
75+ - header 名被上游要求大小写或格式约束
76+ - 某个平台对页面内 fetch header 有额外限制
77+- 这类情况应按具体 header / 平台 / 请求路径新开 bug,不再沿用“完全未透传”这一版描述
+24,
-5
1@@ -1,5 +1,9 @@
2 # BUG-017: buffered 模式对 SSE 端点返回原始文本而非解析后的 JSON
3
4+## 状态
5+
6+- `待修复(2026-03-27 代码核对确认)`
7+
8 ## 现象
9
10 通过 `POST /v1/browser/request` 以 `responseMode: "buffered"` 请求 Claude completion 端点时,`response` 字段返回的是原始 SSE 文本字符串,而不是解析后的 JSON 对象:
11@@ -10,9 +14,24 @@
12
13 调用方需要自己解析 SSE 格式文本来提取 completion 内容,增加了使用复杂度。
14
15-## 根因
16+## 当前代码核对结论
17+
18+这张 bug 当前仍然成立,而且根因可以从现有代码直接看出来:
19+
20+- `plugins/baa-firefox/page-interceptor.js`
21+ - buffered 路径会直接 `await response.text()`
22+ - 然后把 `body: responseBody` 原样发回 `__baa_proxy_response__`
23+- `plugins/baa-firefox/controller.js`
24+ - `handlePageProxyResponse(...)` 会把这个 `body` 继续作为普通 buffered body 回传
25+- `apps/conductor-daemon/src/local-api.ts`
26+ - `parseBrowserProxyBody(...)` 只会尝试 `JSON.parse(...)`
27+ - SSE 文本不是合法 JSON,因此最终仍会以原始字符串返回
28+
29+补充说明:
30
31-插件侧对 buffered 请求,不管上游 Content-Type 是 `application/json` 还是 `text/event-stream`,都用 `response.text()` 读取并原样返回。对于 JSON 端点(如 `/api/organizations`)这没问题,但对于 SSE 端点(如 completion),返回的是 SSE 格式的文本。
32+- 当前 legacy Claude helper `sendClaudePrompt(...)` 会单独调用 `parseClaudeSseText(...)`
33+- 但 generic `POST /v1/browser/request` 的 buffered 路径不会做这层解析
34+- 所以这张 bug 的影响范围是“generic browser/request buffered + SSE 端点”,不是所有 Claude 相关接口都受影响
35
36 ## 严重度
37
38@@ -22,9 +41,9 @@ Low — 功能可用(调用方自行解析),但体验不好
39
40 有两种方案:
41
42-### 方案 A(推荐):插件侧检测 Content-Type 并解析
43+### 方案 A(推荐):在代理 buffered 路径解析 SSE 文本
44
45-当 buffered 响应的 Content-Type 是 `text/event-stream` 时,插件侧解析 SSE 文本,提取所有 `data:` 行,返回结构化结果:
46+当 buffered 响应的 Content-Type 是 `text/event-stream` 时,在页面代理层或 controller buffered 收口层解析 SSE 文本,提取所有 `data:` 行,返回结构化结果:
47
48 ```json
49 {
50@@ -47,4 +66,4 @@ Low — 功能可用(调用方自行解析),但体验不好
51
52 在 describe/business 的 browser/request 合约中说明:buffered 模式请求 SSE 端点时,response 字段是原始 SSE 文本,调用方需自行解析。
53
54-方案 A 更友好但改动大,方案 B 零改动。首版可以先用方案 B,后续再优化。
55+方案 A 更友好;方案 B 可以作为短期兼容说明,但不应再把当前行为写成“已经返回结构化 JSON”。
+21,
-102
1@@ -1,112 +1,31 @@
2 # FIX-BUG-015: 插件侧实现 SSE 流式回传
3
4-## 关联 Bug
5-
6-BUG-015-sse-stream-open-timeout.md
7-
8-## 目标
9-
10-让 `POST /v1/browser/request` 的 `responseMode: "sse"` 在 Firefox 插件侧正确工作,实现 SSE 流拦截和逐事件回传。
11-
12-## 修改文件
13-
14-`plugins/baa-firefox/controller.js`
15-
16-## 背景
17-
18-conductor 侧的 SSE 基础设施已完整实现:
19-- `FirefoxBridgeApiStreamSession`:seq tracking、buffer overflow 保护、idle timeout、open timeout
20-- `firefox-ws.ts`:`handleStreamOpen`、`handleStreamEvent`、`handleStreamEnd`、`handleStreamError`
21-- `local-api.ts`:SSE HTTP response 生成(`text/event-stream`)
22-
23-缺失的是插件侧:收到 `api_request` 且 `responseMode === "sse"` 时,需要用流式方式回传而不是等 buffered 响应。
24-
25-## 修改方案
26-
27-在 controller.js 中处理 api_request 的代码路径里,加入 `responseMode === "sse"` 分支:
28+## 状态
29
30-### 1. 检测 responseMode
31+- `无需执行(2026-03-27 代码核对)`
32
33-当收到 WS 消息 `type: "api_request"` 时,读取 `message.responseMode`(或 `message.response_mode`)。如果值为 `"sse"`,走流式处理路径。
34-
35-### 2. 流式 fetch + ReadableStream 解析
36-
37-```javascript
38-// 伪代码
39-const response = await fetch(url, { method, headers, body, credentials: "include" });
40-
41-// 发 stream_open
42-wsSend({
43- type: "stream_open",
44- id: requestId,
45- streamId: streamId,
46- status: response.status,
47- meta: { headers: Object.fromEntries(response.headers.entries()) }
48-});
49-
50-// 逐行读取 SSE
51-const reader = response.body.getReader();
52-const decoder = new TextDecoder();
53-let buffer = "";
54-let seq = 0;
55-
56-while (true) {
57- const { done, value } = await reader.read();
58- if (done) break;
59-
60- buffer += decoder.decode(value, { stream: true });
61- const lines = buffer.split("\n");
62- buffer = lines.pop(); // 保留未完成的行
63-
64- for (const line of lines) {
65- if (line.startsWith("data: ")) {
66- seq++;
67- wsSend({
68- type: "stream_event",
69- id: requestId,
70- streamId: streamId,
71- seq: seq,
72- event: "message",
73- data: line.slice(6),
74- raw: line
75- });
76- }
77- }
78-}
79-
80-// 发 stream_end
81-wsSend({
82- type: "stream_end",
83- id: requestId,
84- streamId: streamId,
85- status: response.status
86-});
87-```
88-
89-### 3. 错误处理
90+## 关联 Bug
91
92-fetch 失败或 reader 异常时发 `stream_error`:
93+BUG-015-sse-stream-open-timeout.md
94
95-```javascript
96-wsSend({
97- type: "stream_error",
98- id: requestId,
99- streamId: streamId,
100- code: "fetch_error",
101- message: error.message,
102- status: null
103-});
104-```
105+## 结论
106
107-### 4. 取消支持
108+这张 fix 卡对应的工作,按当前代码核对已经存在:
109
110-收到 `type: "request_cancel"` 时,调用 `reader.cancel()` 并发 `stream_end`。
111+- `plugins/baa-firefox/controller.js`
112+ - 已有 `response_mode === "sse"` 分支
113+ - 已有 `handlePageSse(...)`
114+- `plugins/baa-firefox/page-interceptor.js`
115+ - 已有 `streamProxyResponse(...)`
116+ - 已有 `ReadableStream` 逐块读取和事件转发
117+- `tests/browser/browser-control-e2e-smoke.test.mjs`
118+ - 已覆盖 `stream_open` / `stream_event` / `stream_end`
119
120-## 验收标准
121+## 现在该怎么做
122
123-1. `responseMode: "sse"` 请求 Claude completion 端点,conductor 返回 `text/event-stream` 响应
124-2. 响应中包含正确的 `stream_open`、多个 `stream_event`(每个带递增 seq)、`stream_end`
125-3. `stream_event.data` 包含 Claude 的原始 SSE data 字段内容
126-4. 流中途断开或出错时正确返回 `stream_error`
127-5. `responseMode: "buffered"` 行为不受影响
128-6. `pnpm typecheck` 和 `pnpm test` 通过
129+- 不再按“补 SSE 实现”开工
130+- 如果线上仍能复现 `stream_open_timeout`
131+- 应新开基于真实复现条件的 bug,重点排查:
132+ - 页面注入是否失效
133+ - 请求是否走到了错误标签页
134+ - 特定平台 / 特定 content-type / 特定页面环境下的兼容问题
+15,
-56
1@@ -1,66 +1,25 @@
2 # FIX-BUG-016: 插件侧透传自定义 headers
3
4-## 关联 Bug
5-
6-BUG-016-custom-headers-not-forwarded.md
7-
8-## 目标
9-
10-让 `POST /v1/browser/request` 的 `headers` 字段在插件侧 fetch 时正确透传。
11-
12-## 修改文件
13-
14-`plugins/baa-firefox/controller.js`
15+## 状态
16
17-## 修改方案
18+- `无需执行(2026-03-27 代码核对)`
19
20-### 1. 读取 headers
21-
22-在 api_request 消息处理中,读取 `message.headers`:
23-
24-```javascript
25-const customHeaders = {};
26-if (message.headers && typeof message.headers === "object") {
27- for (const [key, value] of Object.entries(message.headers)) {
28- if (typeof key === "string" && typeof value === "string") {
29- customHeaders[key] = value;
30- }
31- }
32-}
33-```
34-
35-### 2. 安全过滤
36-
37-不允许覆盖浏览器管控的 header:
38+## 关联 Bug
39
40-```javascript
41-const BLOCKED_HEADERS = new Set([
42- "cookie", "host", "origin", "referer",
43- "sec-fetch-dest", "sec-fetch-mode", "sec-fetch-site",
44- "connection", "upgrade"
45-]);
46+BUG-016-custom-headers-not-forwarded.md
47
48-for (const key of Object.keys(customHeaders)) {
49- if (BLOCKED_HEADERS.has(key.toLowerCase())) {
50- delete customHeaders[key];
51- }
52-}
53-```
54+## 结论
55
56-### 3. 合并到 fetch
57+这张 fix 卡对应的“header 透传实现”按当前代码核对已经存在:
58
59-```javascript
60-const fetchHeaders = {
61- "Content-Type": "application/json",
62- ...customHeaders
63-};
64-const response = await fetch(url, { method, headers: fetchHeaders, body, credentials: "include" });
65-```
66+- bridge 层会把 `headers` 放入 `api_request`
67+- 页面注入层会把 `detail.headers` 合并进真实 `fetch(...)`
68+- forbidden headers 过滤也已经存在
69
70-## 验收标准
71+## 现在该怎么做
72
73-1. 发送带 `headers: {"X-Custom": "test"}` 的请求,上游能收到该 header
74-2. 发送带 `headers: {"Cookie": "evil"}` 的请求,Cookie header 被过滤不透传
75-3. ChatGPT `/backend-api/conversation` 配合 sentinel token header 能成功发送对话
76-4. 不带 headers 字段的请求行为不变
77-5. `pnpm typecheck` 和 `pnpm test` 通过
78+- 不再按“补 header 透传实现”开工
79+- 如果线上仍复现 header 问题,应针对具体 header 新开 bug,重点排查:
80+ - 是否命中 forbidden header 过滤
81+ - 是否被页面环境或上游接口额外限制
82+ - 是否只在某个平台 / 某条路径出现
+27,
-11
1@@ -10,11 +10,18 @@ buffered 模式请求 SSE 端点时,插件侧解析 SSE 文本,返回结构
2
3 ## 修改文件
4
5-`plugins/baa-firefox/controller.js`
6+- `plugins/baa-firefox/page-interceptor.js`
7+- 如需在 bridge 收口层兜底,可同时调整 `plugins/baa-firefox/controller.js`
8+- 测试建议同步更新 `tests/browser/browser-control-e2e-smoke.test.mjs`
9
10 ## 修改方案
11
12-在 api_request 的 buffered 响应路径中,检测 `response.headers.get("content-type")`:
13+当前代码的真实问题点在页面代理层 buffered 路径,而不只是 controller。推荐优先在 `page-interceptor.js` 的 `__baa_proxy_request__` buffered 分支处理:
14+
15+- 当前这里会直接 `await response.text()` 后把原始字符串放进 `body`
16+- 当 `content-type` 是 `text/event-stream` 时,应该先解析,再把结构化结果塞进 `body`
17+
18+伪代码:
19
20 ```javascript
21 const contentType = response.headers.get("content-type") || "";
22@@ -38,13 +45,20 @@ if (contentType.includes("text/event-stream")) {
23 }
24 }
25
26- wsSend({
27- type: "api_response",
28- id: requestId,
29- ok: true,
30+ emit("__baa_proxy_response__", {
31+ id,
32+ platform: pageRule.platform,
33+ url,
34+ method,
35+ ok: response.ok,
36 status: response.status,
37- body: { content_type: "text/event-stream", events, full_text: fullText }
38- });
39+ body: {
40+ content_type: "text/event-stream",
41+ events,
42+ full_text: fullText,
43+ raw
44+ }
45+ }, pageRule);
46 } else {
47 // 现有 JSON/text 处理逻辑
48 }
49@@ -52,6 +66,8 @@ if (contentType.includes("text/event-stream")) {
50
51 ## 验收标准
52
53-1. buffered 模式请求 Claude completion,`response.body` 包含 `events` 数组和 `full_text` 字段
54-2. buffered 模式请求 JSON 端点(如 `/api/organizations`),行为不变
55-3. `pnpm typecheck` 和 `pnpm test` 通过
56+1. buffered 模式请求 Claude completion,`response.body` 不再是原始 SSE 字符串,而是结构化对象
57+2. 结构化对象至少包含 `events` 和 `full_text`
58+3. buffered 模式请求普通 JSON 端点(如 `/api/organizations`)行为不变
59+4. legacy Claude helper 行为不回退
60+5. `pnpm typecheck` 和 `pnpm test` 通过
+17,
-3
1@@ -12,6 +12,8 @@
2 1. `BUG-008` — 已随 `BUG-010` 一并修复
3 2. `BUG-009` — 已修复
4 3. `BUG-010` — 已修复
5+4. `BUG-015` — 按当前代码核对,SSE 流式回传主链路已落地,不再作为“插件缺少 SSE 实现”的 open bug 保留
6+5. `BUG-016` — 按当前代码核对,自定义 headers 已进入 bridge -> plugin -> page fetch 链路,不再作为“headers 完全未透传”的 open bug 保留
7
8 ## 待修复
9
10@@ -21,11 +23,23 @@
11 | BUG-012 | `BUG-012-*.md` | browser-request-policy waiter 死锁 | Medium | FIX-BUG-012.md |
12 | BUG-013 | `BUG-013-*.md` | stream session timer 未清除 | Low | FIX-BUG-013.md |
13 | BUG-014 | `BUG-014-*.md` | ws_reconnect 提前报 completed=true | Low-Medium | FIX-BUG-014.md |
14-| BUG-015 | `BUG-015-*.md` | SSE 流式模式 stream_open_timeout | High | FIX-BUG-015.md |
15-| BUG-016 | `BUG-016-*.md` | browser/request 自定义 headers 未透传 | Medium | FIX-BUG-016.md |
16 | BUG-017 | `BUG-017-*.md` | buffered 模式 SSE 端点返回原始文本 | Low | FIX-BUG-017.md |
17
18-修复优先级:BUG-015 > BUG-011 > BUG-016 > BUG-012 > BUG-014 > BUG-017 > BUG-013
19+修复优先级:BUG-011 > BUG-012 > BUG-014 > BUG-017 > BUG-013
20+
21+## 当前代码核对结论(2026-03-27)
22+
23+- `BUG-015` 的“插件侧缺少 SSE 实现”结论与当前代码不一致:
24+ - `plugins/baa-firefox/controller.js` 已有 `response_mode === "sse"` 分支
25+ - `plugins/baa-firefox/page-interceptor.js` 已实现 `streamProxyResponse(...)`
26+ - 现有测试已覆盖 `stream_open` / `stream_event` / `stream_end`
27+- `BUG-016` 的“自定义 headers 完全未透传”结论与当前代码不一致:
28+ - `firefox-bridge.ts` 已把 `headers` 放入 `api_request`
29+ - `page-interceptor.js` 会把 `detail.headers` 合并进实际 `fetch(...)`
30+ - 同时保留 forbidden header 过滤
31+- `BUG-017` 仍然成立:
32+ - buffered 模式请求 SSE 端点时,当前仍回原始 SSE 文本
33+- 如果 `BUG-015` / `BUG-016` 仍在线上环境复现,应以新的复现条件重新开卡,不再沿用“缺少实现”这一版根因描述
34
35 ## 优化建议
36
+15,
-2
1@@ -2,11 +2,12 @@
2
3 ## 当前时间
4
5-- `2026-03-26`
6+- `2026-03-27`
7
8 ## 当前代码基线
9
10-- 主线基线:`main@07895cd`
11+- 浏览器控制主链路收口基线:`main@07895cd`
12+- 最近功能代码提交:`main@25be868`(启动时自动恢复受管 Firefox shell tabs)
13 - 任务文档已统一收口到 `tasks/`
14 - 当前活动任务见 `tasks/TASK_OVERVIEW.md`
15 - `T-S001` 到 `T-S025` 已经完成
16@@ -15,6 +16,7 @@
17
18 - `已完成`:`T-S001` 到 `T-S025`
19 - `当前 TODO`:无高优先级主线任务
20+- `待处理缺陷`:`BUG-011` 到 `BUG-014`(见 `bugs/README.md`)
21 - `低优先级 TODO`:`4318/status-api` 兼容层删旧与解耦
22
23 当前新的主需求文档:
24@@ -62,10 +64,13 @@
25
26 1. `T-S023`:已完成,通用 browser request / cancel / SSE 链路和首版风控策略已接入主线
27 2. `T-S024`:已完成,README / docs / smoke / 状态视图已同步到正式口径
28+3. `T-S025`:已完成,`shell_runtime`、结构化 `action_result` 和控制读面已接入主线
29+4. `2026-03-27` 跟进修复:Firefox 插件管理页启动、浏览器重开或扩展重载后,会自动恢复之前明确启用过、但当前缺失的 shell tab;平台根页也会被收进受管理 shell 集合
30
31 当前策略:
32
33 - 当前没有高优先级主线 blocker
34+- 当前 bug backlog 单独留在 `bugs/` 目录,不把它和主线需求任务混写
35 - 当前不把大文件拆分当作主线 blocker
36 - 以下重构工作顺延到下一轮专门重构任务:
37 - `apps/conductor-daemon/src/local-api.ts`
38@@ -74,6 +79,12 @@
39 - `apps/conductor-daemon/src/index.ts`
40 - `apps/conductor-daemon/src/index.test.js`
41
42+## 当前缺陷 backlog
43+
44+- 当前待修:`BUG-011`、`BUG-012`、`BUG-013`、`BUG-014`
45+- 对应修复卡:`FIX-BUG-011.md`、`FIX-BUG-012.md`、`FIX-BUG-013.md`、`FIX-BUG-014.md`
46+- 当前没有 bug fix 正在主线开发中;如需继续推进,直接从 `bugs/` 目录对应 fix 卡开工
47+
48 ## 低优先级 TODO
49
50 下面这些继续保留,但不再是当前最高优先级:
51@@ -109,6 +120,7 @@
52 - `T-S023`:通用 browser request / cancel / SSE 链路、`stream_*` 事件模型和首版浏览器风控策略已经接入 `conductor` 与 Firefox bridge
53 - `T-S024`:README、API / Firefox / runtime 文档、browser smoke 和任务状态视图已同步到正式主线口径
54 - `T-S025`:`shell_runtime`、结构化 `action_result` 和控制读面已接入主线;唯一残余风险是当前机器缺少 `Firefox.app`,因此未完成真实 Firefox 手工 smoke
55+- `2026-03-27` 跟进修复:Firefox 插件现在会在管理页启动、浏览器重开或扩展重载后自动执行一次后台 `tab_restore`,并会把用户手工打开的 Claude `new`、ChatGPT 根页和 Gemini `app` 收进受管理 shell 集合
56 - 根级 `pnpm smoke` 已进主线,覆盖 runtime public-api compatibility、legacy absence、codexd e2e 和 browser-control e2e smoke
57
58 ## 4318 依赖盘点与结论
59@@ -140,5 +152,6 @@
60 - `status-api` 的终局已经先收口到“保留为 opt-in 兼容层”;真正删除它之前,还要先清 `4318` 调用方并拆掉当前构建时复用
61 - 风控状态当前仍是进程内内存态;`conductor` 重启后,限流、退避和熔断计数会重置
62 - 正式 browser HTTP relay 当前仍只支持 Claude;其它平台目前只有空壳页和元数据链路,没有接入通用 request / SSE 合同
63+- `BUG-011` 到 `BUG-014` 当前都还没有进入“已修复”状态;其中 `BUG-014` 直接影响 `ws_reconnect` 的 `action_result.completed` 语义
64 - 当前机器未发现 `Firefox.app`,因此尚未执行“手动关 tab -> tab_restore -> WS 重连后状态回报恢复”的真实 Firefox 手工 smoke;这是当前唯一环境型残余风险
65 - 当前多个源码文件已超过常规体量,但拆分重构已明确延后到浏览器桥接主线收口之后再做
+11,
-0
1@@ -27,6 +27,8 @@
2 - `2026-03-26`:代码闭环已接通;`shell_runtime`、结构化 `action_result`、`/v1/browser` 与 `/describe/control` 已同步落地。
3 - `2026-03-26`:自动化验证已通过,包括 `apps/conductor-daemon/src/index.test.js` 和 `tests/browser/browser-control-e2e-smoke.test.mjs`。
4 - `2026-03-26`:真实 Firefox 手工 smoke 在当前机器上受阻;未发现 `Firefox.app`(已检查 `/Applications` 与 `~/Applications`),因此无法在本环境完成“真实 Firefox 手工验收”。
5+- `2026-03-27`:后续代码跟进已补上“插件管理页启动 / 浏览器重开 / 扩展重载后自动恢复 desired shell tabs”,并支持把平台根页收进受管理 shell 集合。
6+- `2026-03-27`:`BUG-014` 已登记;当前 `ws_reconnect` 仍会在真正断开 / 重连前回报 `completed=true`,需要后续单独修复。
7
8 ## 建议分支名
9
10@@ -167,6 +169,14 @@
11 - `current_client.last_action_result`
12 - `claude.shell_runtime`
13 - `records[].live.shell_runtime`
14+- Firefox 插件现在会在管理页启动、浏览器重开或扩展重载后自动执行一次后台 `tab_restore`,恢复之前明确启用过、但当前缺失的 shell tab;如果用户手工打开 Claude `new`、ChatGPT 根页或 Gemini `app`,插件也会把它们纳入受管理 shell 集合。
15+
16+## 后续代码跟进
17+
18+- `2026-03-27`:提交 `25be868` 已补上启动时的受管 shell tab 自动恢复逻辑,收口了“插件重启后 desired 仍在、actual 丢失但没有自动调和”的空窗。
19+- `2026-03-27`:同一轮跟进里,`verify-mini.sh` 的 wrapper 调用也改成数组参数拼装,降低空参数场景下的脚本调用风险。
20+- `2026-03-27`:当前没有新增手工 smoke 结果;环境阻塞仍然是本机缺少可启动的 `Firefox.app`。
21+- `2026-03-27`:当前仍需后续单独处理 `BUG-014`,不应把它误判为已随本轮启动恢复修复一并关闭。
22
23 ## 自动化验证
24
25@@ -184,6 +194,7 @@
26 - 状态回报恢复
27 - 结果:当前环境阻塞,未执行
28 - 阻塞原因:本机未发现可启动的 `Firefox.app`(已检查 `/Applications`、`~/Applications` 和 Spotlight `org.mozilla.firefox` bundle id),因此无法在这台机器上完成真实 Firefox 手工 smoke。
29+- 截至 `2026-03-27`:该状态未变化;本轮只补了启动恢复代码和文档同步,没有新增真实 Firefox 手工验收结果。
30
31 ## 交付要求
32
+12,
-1
1@@ -9,12 +9,14 @@
2 - `control-api.makefile.so`、Cloudflare Worker、D1 只剩迁移期 legacy 兼容残留和依赖盘点用途
3 - `baa-hand` / `baa-shell` 只保留为接口语义参考,不再作为主系统维护
4 - 当前任务卡都放在本目录
5-- 当前任务基线:`main@07895cd`
6+- 浏览器控制主链路收口基线:`main@07895cd`
7+- 最近功能代码提交:`main@25be868`(启动时自动恢复受管 Firefox shell tabs)
8
9 ## 状态分类
10
11 - `已完成`:`T-S001` 到 `T-S025`
12 - `当前 TODO`:无高优先级主线任务
13+- `待处理缺陷`:`BUG-011` 到 `BUG-014`(见 `../bugs/README.md`)
14 - `低优先级 TODO`:`4318/status-api` 兼容层删旧与解耦
15
16 当前新的主需求文档:
17@@ -56,6 +58,7 @@
18
19 - 根级 `pnpm smoke`,覆盖 repo 内可自举的 runtime compatibility / legacy absence / codexd e2e / browser-control e2e smoke
20 - 根级 `pnpm verify:mini`,作为 `mini` 节点 on-node 静态+运行态检查入口
21+- `2026-03-27` 跟进修复:Firefox 插件管理页启动、浏览器重开或扩展重载后,会自动恢复之前明确启用过、但当前缺失的 shell tab;平台根页也会被收进受管理 shell 集合
22
23 说明:
24
25@@ -64,6 +67,7 @@
26 ## 当前活动任务
27
28 - 当前没有高优先级活动任务卡;如需继续推进,直接新开后续任务
29+- 当前可直接执行的缺陷修复卡位于 `../bugs/`:`FIX-BUG-011.md`、`FIX-BUG-012.md`、`FIX-BUG-013.md`、`FIX-BUG-014.md`
30
31 ## 当前主线收口情况
32
33@@ -79,6 +83,12 @@
34 8. [`T-S024.md`](./T-S024.md):已完成,README / docs / smoke / 状态视图已经同步到正式口径
35 9. [`T-S025.md`](./T-S025.md):已完成,`shell_runtime`、结构化 `action_result` 和控制读面已接入;唯一剩余风险是当前机器缺少 `Firefox.app`,未完成真实手工 smoke
36
37+最近代码跟进:
38+
39+- `2026-03-27`:Firefox 插件管理页启动、浏览器重开或扩展重载后,会自动恢复之前明确启用过、但当前缺失的 shell tab
40+- `2026-03-27`:如果用户手工打开 Claude `new`、ChatGPT 根页或 Gemini `app` 这类平台根页,插件会把它们纳入受管理 shell 集合
41+- `2026-03-27`:`verify-mini.sh` 的 wrapper 调用已收口为数组参数组装,避免空参数场景下的命令拼接问题
42+
43 建议并行关系:
44
45 - `T-S017` 与 `T-S018` 已并行完成
46@@ -92,6 +102,7 @@
47
48 - 风控状态当前仍是进程内内存态;`conductor` 重启后,限流、退避和熔断计数会重置
49 - 正式 browser HTTP relay 当前仍只支持 Claude;其它平台目前只有空壳页和元数据链路,没有接入通用 request / SSE 合同
50+- `BUG-011` 到 `BUG-014` 当前都还是待修状态;其中 `BUG-014` 直接影响 `ws_reconnect` 的 `completed` 语义
51 - runtime smoke 仍依赖仓库根已有 `state/`、`runs/`、`worktrees/`、`logs/launchd/`、`logs/codexd/`、`tmp/` 等本地运行目录;这是现有脚本前提,不是本轮功能回归
52 - 当前机器未发现 `Firefox.app`,因此尚未执行“手动关 tab -> `tab_restore` -> WS 重连后状态回报恢复”的真实 Firefox 手工 smoke;这是当前唯一残余风险
53