baa-conductor

git clone 

commit
293d471
parent
48d41ff
author
jiaozhiwang
date
2026-03-27 11:55:58 +0800 CST
docs: add BUG-015/016/017 with FIX cards — SSE stream, custom headers, buffered SSE parsing
7 files changed,  +410, -6
A bugs/BUG-015-sse-stream-open-timeout.md
+63, -0
 1@@ -0,0 +1,63 @@
 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)全套逻辑已实现但无法使用
A bugs/BUG-016-custom-headers-not-forwarded.md
+53, -0
 1@@ -0,0 +1,53 @@
 2+# BUG-016: browser/request 的自定义 headers 未透传到上游
 3+
 4+## 现象
 5+
 6+通过 `POST /v1/browser/request` 发送带 `headers` 字段的请求时,自定义 header 没有被附加到插件侧的 fetch 请求中。
 7+
 8+具体场景:ChatGPT 的 `/backend-api/conversation` 端点需要 `openai-sentinel-chat-requirements-token` header,但插件侧的 fetch 只携带了浏览器自动附加的 cookie 和标准 header,缺少 sentinel token,导致 ChatGPT 返回 422。
 9+
10+## 触发路径
11+
12+```
13+conductor POST /v1/browser/request {
14+  platform: "chatgpt",
15+  path: "/backend-api/conversation",
16+  headers: {"openai-sentinel-chat-requirements-token": "gAAAA..."},
17+  body: {action: "next", messages: [...]}
18+}
19+→ conductor 将 headers 字段放入 WS 消息
20+→ Firefox 插件收到 api_request
21+→ 插件用 fetch() 发请求,但没有从 WS 消息中读取 headers 并附加
22+→ ChatGPT 返回 422: Input should be a valid dictionary
23+```
24+
25+## 根因分析
26+
27+需要排查 controller.js 中处理 `api_request` 的代码:
28+1. 是否从 WS 消息中读取了 `message.headers` 字段
29+2. 是否在 `fetch(url, { headers: ... })` 中合并了自定义 headers
30+3. 如果只透传了 cookie(通过 `credentials: "include"`),自定义 header 会丢失
31+
32+## 修复方向
33+
34+在插件侧的 api_request 处理中,从 WS 消息读取 `message.headers`(一个 key-value 对象),合并到 fetch 的 headers 中:
35+
36+```javascript
37+const customHeaders = message.headers || {};
38+const fetchHeaders = {
39+  "Content-Type": "application/json",
40+  ...customHeaders
41+};
42+const response = await fetch(url, { method, headers: fetchHeaders, body, credentials: "include" });
43+```
44+
45+注意安全:不应允许覆盖 `Cookie`、`Host`、`Origin` 等浏览器管控的 header。可以维护一个黑名单过滤。
46+
47+## 严重度
48+
49+Medium — 影响 ChatGPT 对话发送和其他需要自定义 header 的 API 调用
50+
51+## 影响
52+
53+- ChatGPT `/backend-api/conversation` 无法使用(需要 sentinel token header)
54+- 任何需要非标准 header 的 API 调用都无法通过 browser/request 完成
A bugs/BUG-017-buffered-sse-raw-text.md
+50, -0
 1@@ -0,0 +1,50 @@
 2+# BUG-017: buffered 模式对 SSE 端点返回原始文本而非解析后的 JSON
 3+
 4+## 现象
 5+
 6+通过 `POST /v1/browser/request` 以 `responseMode: "buffered"` 请求 Claude completion 端点时,`response` 字段返回的是原始 SSE 文本字符串,而不是解析后的 JSON 对象:
 7+
 8+```
 9+"response": "event: completion\ndata: {\"type\":\"completion\",\"completion\":\" 远程\", ...}\n\nevent: completion\ndata: {\"type\":\"completion\",\"completion\":\"控制\", ...}\n\n..."
10+```
11+
12+调用方需要自己解析 SSE 格式文本来提取 completion 内容,增加了使用复杂度。
13+
14+## 根因
15+
16+插件侧对 buffered 请求,不管上游 Content-Type 是 `application/json` 还是 `text/event-stream`,都用 `response.text()` 读取并原样返回。对于 JSON 端点(如 `/api/organizations`)这没问题,但对于 SSE 端点(如 completion),返回的是 SSE 格式的文本。
17+
18+## 严重度
19+
20+Low — 功能可用(调用方自行解析),但体验不好
21+
22+## 建议修复方向
23+
24+有两种方案:
25+
26+### 方案 A(推荐):插件侧检测 Content-Type 并解析
27+
28+当 buffered 响应的 Content-Type 是 `text/event-stream` 时,插件侧解析 SSE 文本,提取所有 `data:` 行,返回结构化结果:
29+
30+```json
31+{
32+  "type": "api_response",
33+  "id": "...",
34+  "ok": true,
35+  "status": 200,
36+  "body": {
37+    "content_type": "text/event-stream",
38+    "events": [
39+      {"event": "completion", "data": {"type": "completion", "completion": " 远程", ...}},
40+      {"event": "completion", "data": {"type": "completion", "completion": "控制", ...}}
41+    ],
42+    "full_text": "远程控制成功"
43+  }
44+}
45+```
46+
47+### 方案 B:保持原样,在文档中说明
48+
49+在 describe/business 的 browser/request 合约中说明:buffered 模式请求 SSE 端点时,response 字段是原始 SSE 文本,调用方需自行解析。
50+
51+方案 A 更友好但改动大,方案 B 零改动。首版可以先用方案 B,后续再优化。
A bugs/FIX-BUG-015.md
+112, -0
  1@@ -0,0 +1,112 @@
  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+### 1. 检测 responseMode
 30+
 31+当收到 WS 消息 `type: "api_request"` 时,读取 `message.responseMode`(或 `message.response_mode`)。如果值为 `"sse"`,走流式处理路径。
 32+
 33+### 2. 流式 fetch + ReadableStream 解析
 34+
 35+```javascript
 36+// 伪代码
 37+const response = await fetch(url, { method, headers, body, credentials: "include" });
 38+
 39+// 发 stream_open
 40+wsSend({
 41+  type: "stream_open",
 42+  id: requestId,
 43+  streamId: streamId,
 44+  status: response.status,
 45+  meta: { headers: Object.fromEntries(response.headers.entries()) }
 46+});
 47+
 48+// 逐行读取 SSE
 49+const reader = response.body.getReader();
 50+const decoder = new TextDecoder();
 51+let buffer = "";
 52+let seq = 0;
 53+
 54+while (true) {
 55+  const { done, value } = await reader.read();
 56+  if (done) break;
 57+
 58+  buffer += decoder.decode(value, { stream: true });
 59+  const lines = buffer.split("\n");
 60+  buffer = lines.pop(); // 保留未完成的行
 61+
 62+  for (const line of lines) {
 63+    if (line.startsWith("data: ")) {
 64+      seq++;
 65+      wsSend({
 66+        type: "stream_event",
 67+        id: requestId,
 68+        streamId: streamId,
 69+        seq: seq,
 70+        event: "message",
 71+        data: line.slice(6),
 72+        raw: line
 73+      });
 74+    }
 75+  }
 76+}
 77+
 78+// 发 stream_end
 79+wsSend({
 80+  type: "stream_end",
 81+  id: requestId,
 82+  streamId: streamId,
 83+  status: response.status
 84+});
 85+```
 86+
 87+### 3. 错误处理
 88+
 89+fetch 失败或 reader 异常时发 `stream_error`:
 90+
 91+```javascript
 92+wsSend({
 93+  type: "stream_error",
 94+  id: requestId,
 95+  streamId: streamId,
 96+  code: "fetch_error",
 97+  message: error.message,
 98+  status: null
 99+});
100+```
101+
102+### 4. 取消支持
103+
104+收到 `type: "request_cancel"` 时,调用 `reader.cancel()` 并发 `stream_end`。
105+
106+## 验收标准
107+
108+1. `responseMode: "sse"` 请求 Claude completion 端点,conductor 返回 `text/event-stream` 响应
109+2. 响应中包含正确的 `stream_open`、多个 `stream_event`(每个带递增 seq)、`stream_end`
110+3. `stream_event.data` 包含 Claude 的原始 SSE data 字段内容
111+4. 流中途断开或出错时正确返回 `stream_error`
112+5. `responseMode: "buffered"` 行为不受影响
113+6. `pnpm typecheck` 和 `pnpm test` 通过
A bugs/FIX-BUG-016.md
+66, -0
 1@@ -0,0 +1,66 @@
 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+### 1. 读取 headers
19+
20+在 api_request 消息处理中,读取 `message.headers`:
21+
22+```javascript
23+const customHeaders = {};
24+if (message.headers && typeof message.headers === "object") {
25+  for (const [key, value] of Object.entries(message.headers)) {
26+    if (typeof key === "string" && typeof value === "string") {
27+      customHeaders[key] = value;
28+    }
29+  }
30+}
31+```
32+
33+### 2. 安全过滤
34+
35+不允许覆盖浏览器管控的 header:
36+
37+```javascript
38+const BLOCKED_HEADERS = new Set([
39+  "cookie", "host", "origin", "referer",
40+  "sec-fetch-dest", "sec-fetch-mode", "sec-fetch-site",
41+  "connection", "upgrade"
42+]);
43+
44+for (const key of Object.keys(customHeaders)) {
45+  if (BLOCKED_HEADERS.has(key.toLowerCase())) {
46+    delete customHeaders[key];
47+  }
48+}
49+```
50+
51+### 3. 合并到 fetch
52+
53+```javascript
54+const fetchHeaders = {
55+  "Content-Type": "application/json",
56+  ...customHeaders
57+};
58+const response = await fetch(url, { method, headers: fetchHeaders, body, credentials: "include" });
59+```
60+
61+## 验收标准
62+
63+1. 发送带 `headers: {"X-Custom": "test"}` 的请求,上游能收到该 header
64+2. 发送带 `headers: {"Cookie": "evil"}` 的请求,Cookie header 被过滤不透传
65+3. ChatGPT `/backend-api/conversation` 配合 sentinel token header 能成功发送对话
66+4. 不带 headers 字段的请求行为不变
67+5. `pnpm typecheck` 和 `pnpm test` 通过
A bugs/FIX-BUG-017.md
+57, -0
 1@@ -0,0 +1,57 @@
 2+# FIX-BUG-017: buffered 模式 SSE 响应解析
 3+
 4+## 关联 Bug
 5+
 6+BUG-017-buffered-sse-raw-text.md
 7+
 8+## 目标
 9+
10+buffered 模式请求 SSE 端点时,插件侧解析 SSE 文本,返回结构化结果。
11+
12+## 修改文件
13+
14+`plugins/baa-firefox/controller.js`
15+
16+## 修改方案
17+
18+在 api_request 的 buffered 响应路径中,检测 `response.headers.get("content-type")`:
19+
20+```javascript
21+const contentType = response.headers.get("content-type") || "";
22+
23+if (contentType.includes("text/event-stream")) {
24+  const raw = await response.text();
25+  const events = [];
26+  let fullText = "";
27+
28+  for (const line of raw.split("\n")) {
29+    const trimmed = line.trim();
30+    if (trimmed.startsWith("data: ")) {
31+      const dataStr = trimmed.slice(6);
32+      try {
33+        const parsed = JSON.parse(dataStr);
34+        events.push(parsed);
35+        if (parsed.completion) fullText += parsed.completion;
36+      } catch {
37+        events.push({ raw: dataStr });
38+      }
39+    }
40+  }
41+
42+  wsSend({
43+    type: "api_response",
44+    id: requestId,
45+    ok: true,
46+    status: response.status,
47+    body: { content_type: "text/event-stream", events, full_text: fullText }
48+  });
49+} else {
50+  // 现有 JSON/text 处理逻辑
51+}
52+```
53+
54+## 验收标准
55+
56+1. buffered 模式请求 Claude completion,`response.body` 包含 `events` 数组和 `full_text` 字段
57+2. buffered 模式请求 JSON 端点(如 `/api/organizations`),行为不变
58+3. `pnpm typecheck` 和 `pnpm test` 通过
M bugs/README.md
+9, -6
 1@@ -20,19 +20,22 @@
 2 | BUG-011 | `BUG-011-*.md` | writeHttpResponse drain handler 永久挂起 | Medium-High | FIX-BUG-011.md |
 3 | BUG-012 | `BUG-012-*.md` | browser-request-policy waiter 死锁 | Medium | FIX-BUG-012.md |
 4 | BUG-013 | `BUG-013-*.md` | stream session timer 未清除 | Low | FIX-BUG-013.md |
 5-| BUG-014 | `BUG-014-*.md` | ws_reconnect 过早报 completed=true | Low-Medium | FIX-BUG-014.md |
 6+| BUG-014 | `BUG-014-*.md` | ws_reconnect 提前报 completed=true | Low-Medium | FIX-BUG-014.md |
 7+| BUG-015 | `BUG-015-*.md` | SSE 流式模式 stream_open_timeout | High | FIX-BUG-015.md |
 8+| BUG-016 | `BUG-016-*.md` | browser/request 自定义 headers 未透传 | Medium | FIX-BUG-016.md |
 9+| BUG-017 | `BUG-017-*.md` | buffered 模式 SSE 端点返回原始文本 | Low | FIX-BUG-017.md |
10 
11-修复优先级:BUG-011 > BUG-012 > BUG-014 > BUG-013
12+修复优先级:BUG-015 > BUG-011 > BUG-016 > BUG-012 > BUG-014 > BUG-017 > BUG-013
13 
14 ## 优化建议
15 
16-| # | 文件 | 内容 | 优先级 |
17-|---|---|---|---|
18-| OPT-001 | `OPT-001-*.md` | action_result 命名风格、test 重复定义、错误路径注释 | Low |
19+| # | 文件 | 内容 |
20+|---|---|---|
21+| OPT-001 | `OPT-001-*.md` | action_result 命名风格、test 重复定义、async 错误路径注释 |
22 
23 ## 编号规则
24 
25 - BUG-XXX:bug 报告
26 - FIX-BUG-XXX:对应修复任务卡(给 Codex 执行)
27-- OPT-XXX:优化建议(非紧急,可合并处理)
28+- OPT-XXX:优化建议(非紧急)
29 - 编号按发现顺序递增,不复用