- commit
- a2d48c4
- parent
- 4aea341
- author
- im_wower
- date
- 2026-04-01 23:05:25 +0800 CST
Merge branch 'fix/bug-037-claude-relay-observer'
4 files changed,
+72,
-8
Raw patch view.
1diff --git a/plugins/baa-firefox/controller.js b/plugins/baa-firefox/controller.js
2index 4ba7953bfcc4f94229ca7fe53a6108c2ba16ed5d..1664b68fd3eeda510e8b5ad6501f60a000506530 100644
3--- a/plugins/baa-firefox/controller.js
4+++ b/plugins/baa-firefox/controller.js
5@@ -1399,10 +1399,6 @@ function createPlatformMap(factory) {
6 }
7
8 function createFinalMessageRelayObserver(platform) {
9- if (platform !== "chatgpt" && platform !== "gemini") {
10- return null;
11- }
12-
13 return FINAL_MESSAGE_HELPERS?.createRelayState(platform) || null;
14 }
15
16diff --git a/plugins/baa-firefox/controller.test.cjs b/plugins/baa-firefox/controller.test.cjs
17index 01dabd952b971e99e4d7a51c49dd2848f11af527..340f3330764f27746386be6375050cdd0493c7ab 100644
18--- a/plugins/baa-firefox/controller.test.cjs
19+++ b/plugins/baa-firefox/controller.test.cjs
20@@ -76,6 +76,7 @@ function createControllerHarness(options = {}) {
21 Response,
22 URL,
23 URLSearchParams,
24+ BAAFinalMessage: options.finalMessageHelpers || null,
25 WebSocket: FakeWebSocket,
26 crypto: globalThis.crypto,
27 browser: {
28@@ -156,6 +157,23 @@ function createControllerHarness(options = {}) {
29 };
30 }
31
32+test("controller creates final message relay observers for every supported platform, including Claude", () => {
33+ const createRelayStateCalls = [];
34+ const { api } = createControllerHarness({
35+ finalMessageHelpers: {
36+ createRelayState(platform) {
37+ createRelayStateCalls.push(platform);
38+ return { platform };
39+ }
40+ }
41+ });
42+
43+ assert.deepEqual(createRelayStateCalls, ["claude", "chatgpt", "gemini"]);
44+ assert.deepEqual(api.state.finalMessageRelayObservers.claude, { platform: "claude" });
45+ assert.deepEqual(api.state.finalMessageRelayObservers.chatgpt, { platform: "chatgpt" });
46+ assert.deepEqual(api.state.finalMessageRelayObservers.gemini, { platform: "gemini" });
47+});
48+
49 function parseGeminiRequestTuple(reqBody) {
50 const params = new URLSearchParams(reqBody);
51 const outer = JSON.parse(params.get("f.req"));
52diff --git a/plugins/baa-firefox/final-message.js b/plugins/baa-firefox/final-message.js
53index 474d7e6568710834b5cbc02104cfdd31512dfcc0..21676aba8cd73759817bfbe0e404cb499d6f1ea4 100644
54--- a/plugins/baa-firefox/final-message.js
55+++ b/plugins/baa-firefox/final-message.js
56@@ -39,6 +39,12 @@
57 }
58 }
59
60+ function splitSseBlocks(text) {
61+ return String(text || "")
62+ .split(/\r?\n\r?\n+/u)
63+ .filter((block) => block.trim());
64+ }
65+
66 function simpleHash(input) {
67 const text = String(input || "");
68 let hash = 2166136261;
69@@ -191,7 +197,7 @@
70 function parseSseChunkPayload(chunk) {
71 const source = String(chunk || "");
72 const dataLines = source
73- .split("\n")
74+ .split(/\r?\n/u)
75 .filter((line) => line.startsWith("data:"))
76 .map((line) => line.slice(5).trimStart());
77 const payloadText = dataLines.join("\n").trim();
78@@ -479,7 +485,7 @@
79 }
80
81 let merged = null;
82- for (const block of String(text || "").split(/\n\n+/u)) {
83+ for (const block of splitSseBlocks(text)) {
84 merged = mergeCandidates(merged, extractChatgptCandidateFromChunk(block, context));
85 }
86 return merged;
87@@ -826,7 +832,7 @@
88 function extractClaudeMetadataFromText(text, context) {
89 let merged = null;
90
91- for (const block of String(text || "").split(/\n\n+/u)) {
92+ for (const block of splitSseBlocks(text)) {
93 const parsedChunk = parseClaudeSsePayload(block);
94 if (!parsedChunk) continue;
95 merged = mergeCandidates(merged, buildClaudeCandidate(null, parsedChunk.payload, context));
96@@ -841,7 +847,7 @@
97 let metadata = null;
98 let matched = false;
99
100- for (const block of String(text || "").split(/\n\n+/u)) {
101+ for (const block of splitSseBlocks(text)) {
102 const parsedChunk = parseClaudeSsePayload(block);
103 if (!parsedChunk) continue;
104
105diff --git a/plugins/baa-firefox/final-message.test.cjs b/plugins/baa-firefox/final-message.test.cjs
106index c4094d10b16374bb8a1e62c38a31ec4b51659cb8..c70afd2fe3831aeabb31a7893821ec15f6a59eea 100644
107--- a/plugins/baa-firefox/final-message.test.cjs
108+++ b/plugins/baa-firefox/final-message.test.cjs
109@@ -294,6 +294,50 @@ test("observeSse relays Claude text_delta fallback after an aborted SSE stream",
110 assert.equal(state.activeStream, null);
111 });
112
113+test("observeSse relays Claude text_delta fallback when a CRLF-delimited stream arrives as one buffered chunk", () => {
114+ const state = createRelayState("claude");
115+ const meta = {
116+ observedAt: 1743206400000,
117+ pageUrl: "https://claude.ai/chat/conv-claude-crlf"
118+ };
119+ const url = "https://claude.ai/api/organizations/org-1/chat_conversations/conv-claude-crlf/completion";
120+ const chunk = [
121+ "event: message_start",
122+ 'data: {"type":"message_start","message":{"uuid":"msg-claude-crlf"}}',
123+ "",
124+ "event: content_block_delta",
125+ 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"Alpha"}}',
126+ "",
127+ "event: content_block_delta",
128+ 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" beta"}}',
129+ "",
130+ "event: message_stop",
131+ 'data: {"type":"message_stop"}',
132+ ""
133+ ].join("\r\n");
134+
135+ assert.equal(
136+ observeSse(state, {
137+ chunk,
138+ url
139+ }, meta),
140+ null
141+ );
142+
143+ const relay = observeSse(state, {
144+ error: "The operation was aborted.",
145+ url
146+ }, meta);
147+
148+ assert.ok(relay);
149+ assert.equal(relay.payload.platform, "claude");
150+ assert.equal(relay.payload.conversation_id, "conv-claude-crlf");
151+ assert.equal(relay.payload.assistant_message_id, "msg-claude-crlf");
152+ assert.equal(relay.payload.raw_text, "Alpha beta");
153+ assert.equal(relay.payload.observed_at, 1743206400000);
154+ assert.equal(state.activeStream, null);
155+});
156+
157 test("observeSse ignores Claude thinking-only protocol chunks", () => {
158 const state = createRelayState("claude");
159 const meta = {