im_wower
·
2026-03-28
BUG-022-final-message-claude-missing.md
1# BUG-022: final-message.js 缺少 Claude 平台支持,有机回复永远不会被捕获
2
3## 状态
4
5- 已修复(2026-03-28,代码已合入主线)
6
7## 当前代码结论
8
9- `isRelevantStreamUrl()` 现在已补齐 Claude `/completion` 识别
10- `observeSse()` 和 `observeNetwork()` 都已显式支持 Claude final-message 提取
11- Claude 的空 `completion` 事件不会再把元数据误拼进 `raw_text`
12- 当前自动化验证覆盖了 SSE 和 buffered/network 两条 Claude relay 路径
13
14> 提交者:Claude(代码审查 + 实测验证)
15> 日期:2026-03-27
16
17## 现象
18
19用户在 mini 的 Firefox 上打开 Claude 对话,发消息请求 AI 输出 baa 代码块。Claude 正常回复了,但 conductor 的 instruction_ingest 没有收到任何 Claude 平台的 final_message。recent_ingests 全是 Gemini,0 条 Claude。
20
21## 根因
22
23`plugins/baa-firefox/final-message.js` 的 `isRelevantStreamUrl` 函数缺少 Claude 分支:
24
25```javascript
26function isRelevantStreamUrl(platform, url) {
27 if (platform === "chatgpt") {
28 return lower.includes("/conversation");
29 }
30 if (platform === "gemini") {
31 return lower.includes("streamgenerate") || ...;
32 }
33 return false; // ← Claude 走到这里,永远 false
34}
35```
36
37`observeSse` 第一行就返回 null:
38```javascript
39if (!isRelevantStreamUrl(state.platform, detail.url)) {
40 return null; // Claude 的 SSE 永远不会被处理
41}
42```
43
44同时,`observeSse` 最终提取候选时也没有 Claude 分支:
45```javascript
46const finalCandidate = state.platform === "chatgpt"
47 ? extractChatgptCandidateFromText(...)
48 : extractGeminiCandidateFromText(...); // Claude 走 Gemini 分支,无法提取
49```
50
51## 缺失的三样东西
52
531. `isRelevantStreamUrl` 缺少 Claude case:URL 包含 `/completion`
542. `extractClaudeCandidateFromText` 不存在:需要解析 Claude SSE 格式
553. `observeSse` 的候选提取缺少 Claude 分支
56
57## Claude SSE 格式
58
59```
60event: completion
61data: {"type":"completion","id":"chatcompl_xxx","completion":" hello","stop_reason":null,...}
62
63event: completion
64data: {"type":"completion","id":"chatcompl_xxx","completion":" world","stop_reason":"end_turn",...}
65```
66
67- `completion` 字段拼接 = 完整回复文本
68- `id` 字段 = assistant message id
69- URL 格式:`/api/organizations/{org}/chat_conversations/{conv_id}/completion`
70 → conv_id 可从 URL 提取
71
72## 修复
73
74### 1. isRelevantStreamUrl 加 Claude
75
76```javascript
77if (platform === "claude") {
78 return lower.includes("/completion");
79}
80```
81
82### 2. 新增 extractClaudeCandidateFromText
83
84```javascript
85function extractClaudeConversationIdFromUrl(url) {
86 const match = String(url || "").match(
87 /\/chat_conversations\/([a-f0-9-]+)\/completion/i
88 );
89 return match?.[1] || null;
90}
91
92function extractClaudeCandidateFromText(text, context) {
93 let fullText = "";
94 let assistantMessageId = null;
95
96 for (const line of String(text || "").split("\n")) {
97 const trimmed = line.trim();
98 if (!trimmed.startsWith("data:")) continue;
99 const payload = parseJson(trimmed.slice(5).trimStart());
100 if (!payload || payload.type !== "completion") continue;
101 if (typeof payload.completion === "string") {
102 fullText += payload.completion;
103 }
104 if (!assistantMessageId && payload.id) {
105 assistantMessageId = String(payload.id);
106 }
107 }
108
109 const rawText = normalizeMessageText(fullText);
110 if (!rawText) return null;
111
112 return {
113 assistantMessageId: assistantMessageId || null,
114 conversationId: extractClaudeConversationIdFromUrl(context.url)
115 || extractClaudeConversationIdFromUrl(context.pageUrl),
116 rawText
117 };
118}
119```
120
121### 3. observeSse 加 Claude 分支
122
123```javascript
124let finalCandidate;
125if (state.platform === "chatgpt") {
126 finalCandidate = extractChatgptCandidateFromText(fullText, context);
127} else if (state.platform === "claude") {
128 finalCandidate = extractClaudeCandidateFromText(fullText, context);
129} else {
130 finalCandidate = extractGeminiCandidateFromText(fullText, context);
131}
132```
133
134## 严重度
135
136**Critical** — Claude 是 Phase 1 的主要平台,没有这个修复整个 BAA 指令系统的端到端闭环对 Claude 完全不工作
137
138## 验收
139
1401. 用户在 Firefox 上的 Claude 对话中发消息
1412. Claude 回复后,conductor 的 `instruction_ingest.recent_ingests` 出现 `platform: "claude"` 的记录
1423. 如果回复包含 baa 代码块,`status` 应为 `"executed"` 而非 `"ignored_no_instructions"`