im_wower
·
2026-04-01
final-message.js
1(function initBaaFinalMessage(globalScope) {
2 const CHATGPT_TERMINAL_STATUSES = new Set([
3 "completed",
4 "finished",
5 "finished_successfully",
6 "incomplete",
7 "max_tokens",
8 "stopped"
9 ]);
10 const CLAUDE_SSE_PAYLOAD_TYPES = new Set([
11 "completion",
12 "content_block_delta",
13 "content_block_start",
14 "content_block_stop",
15 "message_delta",
16 "message_start",
17 "message_stop",
18 "thinking"
19 ]);
20 const RECENT_RELAY_LIMIT = 20;
21 const MAX_WALK_DEPTH = 8;
22 const MAX_WALK_NODES = 400;
23
24 function isRecord(value) {
25 return value !== null && typeof value === "object" && !Array.isArray(value);
26 }
27
28 function trimToNull(value) {
29 return typeof value === "string" && value.trim() ? value.trim() : null;
30 }
31
32 function parseJson(text) {
33 if (typeof text !== "string" || !text.trim()) return null;
34
35 try {
36 return JSON.parse(text);
37 } catch (_) {
38 return null;
39 }
40 }
41
42 function splitSseBlocks(text) {
43 return String(text || "")
44 .split(/\r?\n\r?\n+/u)
45 .filter((block) => block.trim());
46 }
47
48 function simpleHash(input) {
49 const text = String(input || "");
50 let hash = 2166136261;
51
52 for (let index = 0; index < text.length; index += 1) {
53 hash ^= text.charCodeAt(index);
54 hash = Math.imul(hash, 16777619);
55 }
56
57 return (hash >>> 0).toString(16).padStart(8, "0");
58 }
59
60 function normalizeUrlForSignature(url) {
61 const raw = trimToNull(url);
62 if (!raw) return "-";
63
64 try {
65 const parsed = new URL(raw, "https://platform.invalid/");
66 return `${parsed.origin}${parsed.pathname || "/"}${parsed.search || ""}`;
67 } catch (_) {
68 return raw;
69 }
70 }
71
72 function extractUrlPathname(url) {
73 const raw = trimToNull(url);
74 if (!raw) return "";
75
76 try {
77 return (new URL(raw, "https://platform.invalid/").pathname || "/").toLowerCase();
78 } catch (_) {
79 return raw.toLowerCase().split(/[?#]/u)[0] || "";
80 }
81 }
82
83 function normalizeMessageText(value) {
84 if (typeof value !== "string") return null;
85
86 const normalized = value
87 .replace(/\r\n?/gu, "\n")
88 .replace(/[ \t]+\n/gu, "\n")
89 .replace(/\u200b/gu, "")
90 .trim();
91
92 return normalized ? normalized : null;
93 }
94
95 function flattenTextFragments(value, depth = 0) {
96 if (depth > 5 || value == null) return [];
97
98 if (typeof value === "string") {
99 const text = normalizeMessageText(value);
100 return text ? [text] : [];
101 }
102
103 if (Array.isArray(value)) {
104 return value.flatMap((entry) => flattenTextFragments(entry, depth + 1));
105 }
106
107 if (!isRecord(value)) {
108 return [];
109 }
110
111 const out = [];
112 for (const key of ["text", "value", "content", "parts", "segments"]) {
113 if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
114 out.push(...flattenTextFragments(value[key], depth + 1));
115 }
116 return out;
117 }
118
119 function extractChatgptConversationIdFromUrl(url) {
120 const raw = trimToNull(url);
121 if (!raw) return null;
122
123 try {
124 const parsed = new URL(raw, "https://chatgpt.com/");
125 const pathname = parsed.pathname || "/";
126 const match = pathname.match(/\/c\/([^/?#]+)/u);
127 if (match?.[1]) return match[1];
128
129 return trimToNull(parsed.searchParams.get("conversation_id"));
130 } catch (_) {
131 return null;
132 }
133 }
134
135 function extractGeminiConversationIdFromUrl(url) {
136 const raw = trimToNull(url);
137 if (!raw) return null;
138
139 try {
140 const parsed = new URL(raw, "https://gemini.google.com/");
141 const pathname = parsed.pathname || "/";
142 const match = pathname.match(/\/app\/([^/?#]+)/u);
143 if (match?.[1]) return match[1];
144
145 return trimToNull(parsed.searchParams.get("conversation_id"));
146 } catch (_) {
147 return null;
148 }
149 }
150
151 function extractClaudeConversationIdFromUrl(url) {
152 const raw = trimToNull(url);
153 if (!raw) return null;
154
155 try {
156 const parsed = new URL(raw, "https://claude.ai/");
157 const pathname = parsed.pathname || "/";
158 const completionMatch = pathname.match(/\/chat_conversations\/([^/?#]+)\/completion(?:\/)?$/iu);
159 if (completionMatch?.[1]) return completionMatch[1];
160
161 const pageMatch = pathname.match(/\/chats?\/([^/?#]+)/iu);
162 if (pageMatch?.[1]) return pageMatch[1];
163
164 return trimToNull(parsed.searchParams.get("conversation_id"))
165 || trimToNull(parsed.searchParams.get("conversationId"));
166 } catch (_) {
167 return null;
168 }
169 }
170
171 function extractChatgptConversationIdFromReqBody(reqBody) {
172 const parsed = parseJson(reqBody);
173 if (!isRecord(parsed)) return null;
174 return trimToNull(parsed.conversation_id) || trimToNull(parsed.conversationId) || null;
175 }
176
177 function extractGeminiPromptFromReqBody(reqBody) {
178 if (typeof reqBody !== "string" || !reqBody) return null;
179
180 try {
181 const params = new URLSearchParams(reqBody);
182 const outerPayload = params.get("f.req");
183 if (!outerPayload) return null;
184
185 const outer = JSON.parse(outerPayload);
186 if (!Array.isArray(outer) || typeof outer[1] !== "string") return null;
187
188 const inner = JSON.parse(outer[1]);
189 if (!Array.isArray(inner) || !Array.isArray(inner[0])) return null;
190
191 return trimToNull(inner[0][0]);
192 } catch (_) {
193 return null;
194 }
195 }
196
197 function parseSseChunkPayload(chunk) {
198 const source = String(chunk || "");
199 const dataLines = source
200 .split(/\r?\n/u)
201 .filter((line) => line.startsWith("data:"))
202 .map((line) => line.slice(5).trimStart());
203 const payloadText = dataLines.join("\n").trim();
204
205 if (!payloadText || payloadText === "[DONE]") {
206 return null;
207 }
208
209 return parseJson(payloadText) || parseJson(source.trim()) || null;
210 }
211
212 function parseSseChunkEvent(chunk) {
213 for (const rawLine of String(chunk || "").split(/\r?\n/u)) {
214 const line = rawLine.trim();
215 if (!line.startsWith("event:")) continue;
216 return trimToNull(line.slice(6));
217 }
218
219 return null;
220 }
221
222 function extractChatgptMessageText(message) {
223 if (!isRecord(message)) return null;
224
225 if (Array.isArray(message.content?.parts)) {
226 const parts = message.content.parts
227 .flatMap((entry) => flattenTextFragments(entry))
228 .filter(Boolean);
229 return normalizeMessageText(parts.join("\n"));
230 }
231
232 if (typeof message.content?.text === "string") {
233 return normalizeMessageText(message.content.text);
234 }
235
236 if (typeof message.text === "string") {
237 return normalizeMessageText(message.text);
238 }
239
240 const fragments = flattenTextFragments(message.content);
241 return normalizeMessageText(fragments.join("\n"));
242 }
243
244 function normalizeCandidatePath(path) {
245 return String(path || "").replace(/^\./u, "");
246 }
247
248 function isHistoricalChatgptPath(path) {
249 return /(?:^|\.)(?:history|items|linear_conversation|mapping|message_map|message_nodes|messages|nodes|entries|turns)(?:$|[.[\]])/iu
250 .test(normalizeCandidatePath(path));
251 }
252
253 function scoreChatgptCandidatePath(path) {
254 const normalizedPath = normalizeCandidatePath(path);
255 if (!normalizedPath) {
256 return 0;
257 }
258
259 let score = 0;
260
261 if (normalizedPath === "message") {
262 score += 280;
263 }
264
265 if (normalizedPath.endsWith(".message")) {
266 score += 140;
267 }
268
269 if (/(?:^|\.)(?:assistant|completion|current|message|output|response)(?:$|[.[\]])/iu.test(normalizedPath)) {
270 score += isHistoricalChatgptPath(normalizedPath) ? 0 : 120;
271 }
272
273 if (isHistoricalChatgptPath(normalizedPath)) {
274 score -= 260;
275 }
276
277 return score;
278 }
279
280 function buildChatgptCandidate(message, envelope, path, context) {
281 if (!isRecord(message)) return null;
282
283 const role = trimToNull(message.author?.role) || trimToNull(message.role);
284 if (role !== "assistant") {
285 return null;
286 }
287
288 const rawText = extractChatgptMessageText(message);
289 if (!rawText) {
290 return null;
291 }
292
293 const status = trimToNull(message.status)?.toLowerCase() || null;
294 const metadata = isRecord(message.metadata) ? message.metadata : {};
295 const terminal = Boolean(
296 (status && CHATGPT_TERMINAL_STATUSES.has(status))
297 || message.end_turn === true
298 || metadata.is_complete === true
299 || trimToNull(metadata.finish_details?.type)
300 || trimToNull(metadata.finish_details?.stop)
301 );
302 const conversationId =
303 trimToNull(envelope?.conversation_id)
304 || trimToNull(envelope?.conversationId)
305 || trimToNull(message.conversation_id)
306 || trimToNull(message.conversationId)
307 || extractChatgptConversationIdFromReqBody(context.reqBody)
308 || extractChatgptConversationIdFromUrl(context.pageUrl || context.url)
309 || null;
310 const assistantMessageId =
311 trimToNull(message.id)
312 || trimToNull(message.message_id)
313 || trimToNull(message.messageId)
314 || trimToNull(envelope?.message_id)
315 || trimToNull(envelope?.messageId)
316 || null;
317
318 let score = rawText.length + scoreChatgptCandidatePath(path);
319 if (assistantMessageId) score += 120;
320 if (conversationId) score += 80;
321 if (terminal) score += 160;
322 if ((path || "").includes(".message")) score += 40;
323
324 return {
325 assistantMessageId,
326 conversationId,
327 path: normalizeCandidatePath(path),
328 rawText,
329 score
330 };
331 }
332
333 function collectChatgptCandidates(root, context) {
334 const candidates = [];
335 const queue = [{ value: root, path: "", depth: 0 }];
336 let walked = 0;
337
338 while (queue.length > 0 && walked < MAX_WALK_NODES) {
339 const current = queue.shift();
340 walked += 1;
341 if (!current) continue;
342
343 const { depth, path, value } = current;
344 if (depth > MAX_WALK_DEPTH || value == null) {
345 continue;
346 }
347
348 if (Array.isArray(value)) {
349 for (let index = 0; index < value.length && index < 32; index += 1) {
350 queue.push({
351 value: value[index],
352 path: `${path}[${index}]`,
353 depth: depth + 1
354 });
355 }
356 continue;
357 }
358
359 if (!isRecord(value)) {
360 continue;
361 }
362
363 const directCandidate = buildChatgptCandidate(value, value, path, context);
364 if (directCandidate) {
365 candidates.push(directCandidate);
366 }
367
368 if (isRecord(value.message)) {
369 const wrappedCandidate = buildChatgptCandidate(value.message, value, `${path}.message`, context);
370 if (wrappedCandidate) {
371 candidates.push(wrappedCandidate);
372 }
373 }
374
375 for (const [key, child] of Object.entries(value).slice(0, 40)) {
376 queue.push({
377 value: child,
378 path: path ? `${path}.${key}` : key,
379 depth: depth + 1
380 });
381 }
382 }
383
384 return candidates.sort((left, right) => compareCandidates(left, right))[0] || null;
385 }
386
387 function compareCandidates(current, next) {
388 const currentScore = Number(current?.score) || 0;
389 const nextScore = Number(next?.score) || 0;
390 if (nextScore !== currentScore) {
391 return nextScore - currentScore;
392 }
393
394 const currentTextLength = (current?.rawText || "").length;
395 const nextTextLength = (next?.rawText || "").length;
396 if (nextTextLength !== currentTextLength) {
397 return nextTextLength - currentTextLength;
398 }
399
400 const currentAssistant = trimToNull(current?.assistantMessageId) ? 1 : 0;
401 const nextAssistant = trimToNull(next?.assistantMessageId) ? 1 : 0;
402 if (nextAssistant !== currentAssistant) {
403 return nextAssistant - currentAssistant;
404 }
405
406 const currentConversation = trimToNull(current?.conversationId) ? 1 : 0;
407 const nextConversation = trimToNull(next?.conversationId) ? 1 : 0;
408 return nextConversation - currentConversation;
409 }
410
411 function pickPreferredCandidate(current, next) {
412 if (!current) return next ? { ...next } : null;
413 if (!next) return { ...current };
414 return compareCandidates(current, next) > 0 ? { ...next } : { ...current };
415 }
416
417 function candidateTextsOverlap(current, next) {
418 const currentText = normalizeMessageText(current?.rawText);
419 const nextText = normalizeMessageText(next?.rawText);
420 if (!currentText || !nextText) {
421 return false;
422 }
423
424 return currentText === nextText || currentText.includes(nextText) || nextText.includes(currentText);
425 }
426
427 function shouldMergeCandidatePair(current, next) {
428 if (!current || !next) {
429 return true;
430 }
431
432 const currentAssistant = trimToNull(current?.assistantMessageId);
433 const nextAssistant = trimToNull(next?.assistantMessageId);
434 if (currentAssistant && nextAssistant) {
435 return currentAssistant === nextAssistant;
436 }
437
438 const currentConversation = trimToNull(current?.conversationId);
439 const nextConversation = trimToNull(next?.conversationId);
440 if (currentConversation && nextConversation && currentConversation !== nextConversation) {
441 return false;
442 }
443
444 if (candidateTextsOverlap(current, next)) {
445 return true;
446 }
447
448 if (!current?.rawText || !next?.rawText) {
449 return Boolean(currentAssistant || nextAssistant || (currentConversation && nextConversation));
450 }
451
452 return false;
453 }
454
455 function mergeCandidates(current, next) {
456 if (!next) return current;
457 if (!current) return { ...next };
458
459 if (!shouldMergeCandidatePair(current, next)) {
460 return pickPreferredCandidate(current, next);
461 }
462
463 return {
464 assistantMessageId: next.assistantMessageId || current.assistantMessageId || null,
465 conversationId: next.conversationId || current.conversationId || null,
466 path: next.path || current.path || "",
467 rawText:
468 (next.rawText && next.rawText.length >= (current.rawText || "").length)
469 ? next.rawText
470 : current.rawText,
471 score: Math.max(Number(current.score) || 0, Number(next.score) || 0)
472 };
473 }
474
475 function extractChatgptCandidateFromChunk(chunk, context) {
476 const payload = parseSseChunkPayload(chunk);
477 if (!payload) return null;
478 return collectChatgptCandidates(payload, context);
479 }
480
481 function extractChatgptCandidateFromText(text, context) {
482 const parsed = parseJson(text);
483 if (parsed != null) {
484 return collectChatgptCandidates(parsed, context);
485 }
486
487 let merged = null;
488 for (const block of splitSseBlocks(text)) {
489 merged = mergeCandidates(merged, extractChatgptCandidateFromChunk(block, context));
490 }
491 return merged;
492 }
493
494 function looksLikeUrl(text) {
495 return /^https?:\/\//iu.test(text);
496 }
497
498 function looksIdLike(text) {
499 if (!text || /\s/u.test(text) || looksLikeUrl(text)) return false;
500 return /^[A-Za-z0-9:_./-]{6,120}$/u.test(text);
501 }
502
503 function looksOpaqueGeminiTextToken(text) {
504 if (!text || /\s/u.test(text) || looksLikeUrl(text)) return false;
505
506 const normalized = String(text || "").trim();
507 if (!normalized) return false;
508
509 if (/^[0-9a-f]{8,}$/iu.test(normalized)) return true;
510 if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(normalized)) {
511 return true;
512 }
513 if (/^(?:msg|req|resp|conv|conversation|assistant|candidate|turn|chatcmpl|chatcompl|cmpl|id)[-_:][A-Za-z0-9:_-]{4,}$/iu.test(normalized)) {
514 return true;
515 }
516
517 const letters = (normalized.match(/[A-Za-z]/gu) || []).length;
518 const digits = (normalized.match(/[0-9]/gu) || []).length;
519 const separators = (normalized.match(/[-_:./]/gu) || []).length;
520
521 if (letters === 0 && digits >= 6) return true;
522 if (/^[A-Za-z0-9:_./-]{16,}$/u.test(normalized) && !/[aeiou]/iu.test(normalized) && digits > 0) {
523 return true;
524 }
525 if (digits >= 6 && separators >= 2 && letters <= 6) return true;
526
527 return false;
528 }
529
530 function looksLikePromptEcho(text, prompt) {
531 const normalizedText = normalizeMessageText(text);
532 const normalizedPrompt = normalizeMessageText(prompt);
533 if (!normalizedText || !normalizedPrompt) return false;
534 if (normalizedText === normalizedPrompt) return true;
535
536 const wordCount = normalizedText.split(/\s+/u).filter(Boolean).length;
537 return wordCount >= 4 && normalizedText.length >= 32 && normalizedPrompt.includes(normalizedText);
538 }
539
540 function looksLikeGeminiProtocolFragment(text) {
541 const normalized = normalizeMessageText(text);
542 if (!normalized || !/^(?:\[|\{)/u.test(normalized)) return false;
543
544 const bracketCount = (normalized.match(/[\[\]\{\}]/gu) || []).length;
545 const digitCount = (normalized.match(/[0-9]/gu) || []).length;
546 const alphaCount = (normalized.match(/[A-Za-z]/gu) || []).length;
547
548 if (/\b(?:wrb\.fr|generic|af\.httprm|di)\b/iu.test(normalized)) return true;
549 if (bracketCount >= 6 && digitCount >= 4 && alphaCount <= Math.max(12, Math.floor(normalized.length / 6))) {
550 return true;
551 }
552
553 return false;
554 }
555
556 function readGeminiLineRoots(text) {
557 const roots = [];
558 const source = String(text || "")
559 .replace(/^\)\]\}'\s*/u, "")
560 .trim();
561
562 if (!source) {
563 return roots;
564 }
565
566 const wholePayload = parseJson(source);
567 if (wholePayload != null) {
568 roots.push(wholePayload);
569 }
570
571 for (const rawLine of source.split(/\r?\n/u)) {
572 const line = rawLine.trim();
573 if (!line || /^\d+$/u.test(line)) continue;
574
575 const parsedLine = parseJson(line);
576 if (parsedLine != null) {
577 roots.push(parsedLine);
578 }
579 }
580
581 return roots;
582 }
583
584 function collectGeminiIdCandidate(target, kind, value) {
585 const normalized = trimToNull(value);
586 if (!normalized || !looksIdLike(normalized)) return;
587 target.push({
588 kind,
589 score: kind === "message" ? 120 : 100,
590 value: normalized
591 });
592 }
593
594 function scoreGeminiTextCandidate(text, path, prompt) {
595 if (!text) return -1;
596
597 const normalizedText = normalizeMessageText(text);
598 if (!normalizedText) return -1;
599
600 if (looksLikePromptEcho(normalizedText, prompt)) return -1;
601 if (looksLikeUrl(normalizedText)) return -1;
602 if (looksOpaqueGeminiTextToken(normalizedText)) return -1;
603 if (looksLikeGeminiProtocolFragment(normalizedText)) return -1;
604 if (/^(wrb\.fr|generic|di|af\.httprm)$/iu.test(normalizedText)) return -1;
605 if (/^[\[\]{}",:0-9.\s_-]+$/u.test(normalizedText)) return -1;
606
607 const lowerPath = String(path || "").toLowerCase();
608 let score = Math.min(normalizedText.length, 220);
609
610 if (/\s/u.test(normalizedText)) score += 60;
611 if (/[A-Za-z\u4e00-\u9fff]/u.test(normalizedText)) score += 50;
612 if (/\n/u.test(normalizedText)) score += 30;
613 if (/(text|content|message|response|answer|markdown|candidate)/u.test(lowerPath)) score += 80;
614 if (/(prompt|query|request|input|user)/u.test(lowerPath)) score -= 90;
615 if (/(url|image|token|safety|metadata|source)/u.test(lowerPath)) score -= 40;
616
617 return score;
618 }
619
620 function maybeParseNestedJson(text) {
621 const normalized = trimToNull(text);
622 if (!normalized || normalized.length > 200_000) return null;
623 if (!/^(?:\[|\{)/u.test(normalized)) return null;
624 return parseJson(normalized);
625 }
626
627 function walkGeminiValue(value, context, bucket, path = "", depth = 0, state = { walked: 0 }) {
628 if (depth > MAX_WALK_DEPTH || state.walked >= MAX_WALK_NODES || value == null) {
629 return;
630 }
631
632 state.walked += 1;
633
634 if (typeof value === "string") {
635 const nested = maybeParseNestedJson(value);
636 if (nested != null) {
637 walkGeminiValue(nested, context, bucket, `${path}.$json`, depth + 1, state);
638 }
639
640 const normalized = normalizeMessageText(value);
641 if (!normalized) return;
642 if (nested != null && /^(?:\[|\{)/u.test(normalized)) return;
643
644 const score = scoreGeminiTextCandidate(normalized, path, context.prompt);
645 if (score > 0) {
646 bucket.texts.push({
647 path,
648 score,
649 text: normalized
650 });
651 }
652 return;
653 }
654
655 if (Array.isArray(value)) {
656 for (let index = 0; index < value.length && index < 40; index += 1) {
657 walkGeminiValue(value[index], context, bucket, `${path}[${index}]`, depth + 1, state);
658 }
659 return;
660 }
661
662 if (!isRecord(value)) {
663 return;
664 }
665
666 for (const [key, child] of Object.entries(value).slice(0, 40)) {
667 const nextPath = path ? `${path}.${key}` : key;
668 const lowerKey = key.toLowerCase();
669
670 if (typeof child === "string") {
671 if (/(conversation|conv|chat).*id/u.test(lowerKey)) {
672 collectGeminiIdCandidate(bucket.conversationIds, "conversation", child);
673 } else if (/(message|response|candidate|turn).*id/u.test(lowerKey)) {
674 collectGeminiIdCandidate(bucket.messageIds, "message", child);
675 } else if (lowerKey === "id") {
676 collectGeminiIdCandidate(bucket.genericIds, "generic", child);
677 }
678 }
679
680 walkGeminiValue(child, context, bucket, nextPath, depth + 1, state);
681 }
682 }
683
684 function pickBestId(primary, secondary = []) {
685 const source = [...primary, ...secondary]
686 .sort((left, right) => right.score - left.score)
687 .map((entry) => entry.value);
688
689 return trimToNull(source[0]) || null;
690 }
691
692 function extractGeminiCandidateFromText(text, context) {
693 const roots = readGeminiLineRoots(text);
694 const bucket = {
695 conversationIds: [],
696 genericIds: [],
697 messageIds: [],
698 texts: []
699 };
700 const geminiContext = {
701 pageUrl: context.pageUrl || context.url || "",
702 prompt: extractGeminiPromptFromReqBody(context.reqBody)
703 };
704
705 if (roots.length > 0) {
706 for (const root of roots) {
707 walkGeminiValue(root, geminiContext, bucket);
708 }
709 } else {
710 walkGeminiValue(String(text || ""), geminiContext, bucket);
711 }
712
713 const bestText = bucket.texts.sort((left, right) =>
714 (right.score - left.score) || (right.text.length - left.text.length)
715 )[0] || null;
716
717 if (!bestText?.text) {
718 return null;
719 }
720
721 return {
722 assistantMessageId: pickBestId(bucket.messageIds, bucket.genericIds),
723 conversationId: extractGeminiConversationIdFromUrl(geminiContext.pageUrl) || pickBestId(bucket.conversationIds),
724 rawText: bestText.text,
725 score: bestText.score
726 };
727 }
728
729 function parseClaudeSsePayload(chunk) {
730 const payload = parseSseChunkPayload(chunk);
731 if (!isRecord(payload)) return null;
732
733 const payloadType = trimToNull(payload.type)?.toLowerCase() || null;
734 const eventType = trimToNull(parseSseChunkEvent(chunk))?.toLowerCase() || null;
735 if (!payloadType && eventType !== "completion") {
736 return null;
737 }
738
739 if (payloadType && !CLAUDE_SSE_PAYLOAD_TYPES.has(payloadType) && eventType !== "completion") {
740 return null;
741 }
742
743 return {
744 eventType,
745 payload,
746 payloadType
747 };
748 }
749
750 function extractClaudeAssistantMessageId(payload) {
751 return trimToNull(payload?.id)
752 || trimToNull(payload?.uuid)
753 || trimToNull(payload?.message_id)
754 || trimToNull(payload?.messageId)
755 || trimToNull(payload?.message?.uuid)
756 || trimToNull(payload?.message?.id)
757 || trimToNull(payload?.message?.message_id)
758 || trimToNull(payload?.message?.messageId)
759 || null;
760 }
761
762 function extractClaudeConversationId(payload, context) {
763 return trimToNull(payload?.conversation_id)
764 || trimToNull(payload?.conversationId)
765 || trimToNull(payload?.conversation_uuid)
766 || trimToNull(payload?.conversationUuid)
767 || trimToNull(payload?.conversation?.id)
768 || trimToNull(payload?.conversation?.uuid)
769 || trimToNull(payload?.message?.conversation_id)
770 || trimToNull(payload?.message?.conversationId)
771 || trimToNull(payload?.message?.conversation_uuid)
772 || trimToNull(payload?.message?.conversationUuid)
773 || trimToNull(payload?.message?.conversation?.id)
774 || trimToNull(payload?.message?.conversation?.uuid)
775 || extractClaudeConversationIdFromUrl(context.url)
776 || extractClaudeConversationIdFromUrl(context.pageUrl)
777 || null;
778 }
779
780 function extractClaudeTextFragment(parsedChunk) {
781 if (!parsedChunk?.payload || !isRecord(parsedChunk.payload)) {
782 return null;
783 }
784
785 if (parsedChunk.payloadType === "completion" || (!parsedChunk.payloadType && parsedChunk.eventType === "completion")) {
786 if (typeof parsedChunk.payload.completion !== "string") {
787 return null;
788 }
789
790 return {
791 kind: "completion",
792 text: parsedChunk.payload.completion
793 };
794 }
795
796 if (parsedChunk.payloadType === "content_block_delta") {
797 const deltaType = trimToNull(parsedChunk.payload.delta?.type)?.toLowerCase() || null;
798 if (deltaType !== "text_delta" || typeof parsedChunk.payload.delta.text !== "string") {
799 return null;
800 }
801
802 return {
803 kind: "text_delta",
804 text: parsedChunk.payload.delta.text
805 };
806 }
807
808 return null;
809 }
810
811 function buildClaudeCandidate(rawText, payload, context) {
812 const assistantMessageId = extractClaudeAssistantMessageId(payload);
813 const conversationId = extractClaudeConversationId(payload, context);
814 const normalizedRawText = typeof rawText === "string" && rawText.length > 0 ? rawText : null;
815
816 if (!normalizedRawText && !assistantMessageId && !conversationId) {
817 return null;
818 }
819
820 let score = normalizedRawText ? normalizedRawText.length : 0;
821 if (assistantMessageId) score += 120;
822 if (conversationId) score += 80;
823
824 return {
825 assistantMessageId,
826 conversationId,
827 rawText: normalizedRawText,
828 score
829 };
830 }
831
832 function extractClaudeMetadataFromText(text, context) {
833 let merged = null;
834
835 for (const block of splitSseBlocks(text)) {
836 const parsedChunk = parseClaudeSsePayload(block);
837 if (!parsedChunk) continue;
838 merged = mergeCandidates(merged, buildClaudeCandidate(null, parsedChunk.payload, context));
839 }
840
841 return merged;
842 }
843
844 function extractClaudeCandidateFromText(text, context) {
845 let completionText = "";
846 let deltaText = "";
847 let metadata = null;
848 let matched = false;
849
850 for (const block of splitSseBlocks(text)) {
851 const parsedChunk = parseClaudeSsePayload(block);
852 if (!parsedChunk) continue;
853
854 matched = true;
855 metadata = mergeCandidates(metadata, buildClaudeCandidate(null, parsedChunk.payload, context));
856
857 const fragment = extractClaudeTextFragment(parsedChunk);
858 if (!fragment) {
859 continue;
860 }
861
862 if (fragment.kind === "completion") {
863 completionText += fragment.text;
864 } else {
865 deltaText += fragment.text;
866 }
867 }
868
869 if (!matched) {
870 return null;
871 }
872
873 const preferredText = normalizeMessageText(completionText) || normalizeMessageText(deltaText) || null;
874 return buildClaudeCandidate(
875 preferredText,
876 {
877 conversation_id: metadata?.conversationId,
878 id: metadata?.assistantMessageId
879 },
880 context
881 ) || metadata;
882 }
883
884 function createRelayState(platform) {
885 return {
886 activeStream: null,
887 platform,
888 recentRelayKeys: []
889 };
890 }
891
892 function isRelevantStreamUrl(platform, url) {
893 const lower = String(url || "").toLowerCase();
894 const pathname = extractUrlPathname(url);
895
896 if (platform === "chatgpt") {
897 return /^\/conversation\/?$/iu.test(pathname)
898 || /^\/(?:backend-api|backend-anon|public-api)\/conversation\/?$/iu.test(pathname)
899 || /^\/(?:backend-api|backend-anon|public-api)\/f\/conversation\/?$/iu.test(pathname);
900 }
901
902 if (platform === "claude") {
903 return lower.includes("/completion");
904 }
905
906 if (platform === "gemini") {
907 return lower.includes("streamgenerate")
908 || lower.includes("generatecontent")
909 || lower.includes("modelresponse")
910 || lower.includes("bardchatui");
911 }
912
913 return false;
914 }
915
916 function buildStreamSignature(state, detail, meta) {
917 return [
918 state.platform,
919 normalizeUrlForSignature(detail.url),
920 simpleHash(detail.reqBody || ""),
921 normalizeUrlForSignature(meta.pageUrl || "")
922 ].join("|");
923 }
924
925 function ensureActiveStream(state, detail, meta) {
926 const signature = buildStreamSignature(state, detail, meta);
927
928 if (!state.activeStream || state.activeStream.signature !== signature) {
929 state.activeStream = {
930 chunks: [],
931 latestCandidate: null,
932 pageUrl: meta.pageUrl || "",
933 reqBody: detail.reqBody || "",
934 signature,
935 url: detail.url || ""
936 };
937 }
938
939 return state.activeStream;
940 }
941
942 function extractSseCandidateFromText(platform, text, context) {
943 if (platform === "chatgpt") {
944 return extractChatgptCandidateFromText(text, context);
945 }
946
947 if (platform === "claude") {
948 return extractClaudeCandidateFromText(text, context);
949 }
950
951 return extractGeminiCandidateFromText(text, context);
952 }
953
954 function finalizeObservedSseRelay(state, stream, meta = {}) {
955 if (!state || !stream) {
956 return null;
957 }
958
959 const context = {
960 pageUrl: stream.pageUrl,
961 reqBody: stream.reqBody,
962 url: stream.url
963 };
964 const finalCandidate = extractSseCandidateFromText(
965 state.platform,
966 stream.chunks.join("\n\n"),
967 context
968 );
969 const relay = buildRelayEnvelope(
970 state.platform,
971 mergeCandidates(stream.latestCandidate, finalCandidate),
972 meta.observedAt
973 );
974
975 state.activeStream = null;
976 if (!relay || hasSeenRelay(state, relay)) {
977 return null;
978 }
979
980 return relay;
981 }
982
983 function buildRelayEnvelope(platform, candidate, observedAt) {
984 const rawText = normalizeMessageText(candidate?.rawText);
985 if (!rawText) return null;
986
987 const conversationId = trimToNull(candidate?.conversationId) || null;
988 const assistantMessageId =
989 trimToNull(candidate?.assistantMessageId)
990 || `synthetic_${simpleHash(`${platform}|${conversationId || "-"}|${rawText}`)}`;
991 const dedupeKey = `${platform}|${conversationId || "-"}|${assistantMessageId}|${rawText}`;
992
993 return {
994 dedupeKey,
995 payload: {
996 type: "browser.final_message",
997 platform,
998 conversation_id: conversationId,
999 assistant_message_id: assistantMessageId,
1000 raw_text: rawText,
1001 observed_at: Number.isFinite(observedAt) ? Math.round(observedAt) : Date.now()
1002 }
1003 };
1004 }
1005
1006 function hasSeenRelay(state, relay) {
1007 if (!relay?.dedupeKey) return false;
1008 return state.recentRelayKeys.includes(relay.dedupeKey);
1009 }
1010
1011 function rememberRelay(state, relay) {
1012 if (!relay?.dedupeKey || hasSeenRelay(state, relay)) {
1013 return false;
1014 }
1015
1016 state.recentRelayKeys.push(relay.dedupeKey);
1017 if (state.recentRelayKeys.length > RECENT_RELAY_LIMIT) {
1018 state.recentRelayKeys.splice(0, state.recentRelayKeys.length - RECENT_RELAY_LIMIT);
1019 }
1020 return true;
1021 }
1022
1023 function observeSse(state, detail, meta = {}) {
1024 if (!state || !detail || detail.source === "proxy" || !isRelevantStreamUrl(state.platform, detail.url)) {
1025 return null;
1026 }
1027
1028 const stream = ensureActiveStream(state, detail, meta);
1029 const context = {
1030 pageUrl: stream.pageUrl,
1031 reqBody: stream.reqBody,
1032 url: stream.url
1033 };
1034
1035 if (typeof detail.chunk === "string" && detail.chunk) {
1036 stream.chunks.push(detail.chunk);
1037
1038 if (state.platform === "chatgpt") {
1039 stream.latestCandidate = mergeCandidates(
1040 stream.latestCandidate,
1041 extractChatgptCandidateFromChunk(detail.chunk, context)
1042 );
1043 } else if (state.platform === "claude") {
1044 stream.latestCandidate = mergeCandidates(
1045 stream.latestCandidate,
1046 extractClaudeMetadataFromText(detail.chunk, context)
1047 );
1048 }
1049 }
1050
1051 if (detail.done === true) {
1052 return finalizeObservedSseRelay(state, stream, meta);
1053 }
1054
1055 if (detail.error) {
1056 if (!stream.latestCandidate && stream.chunks.length === 0) {
1057 state.activeStream = null;
1058 return null;
1059 }
1060
1061 return finalizeObservedSseRelay(state, stream, meta);
1062 }
1063
1064 if (detail.done !== true) {
1065 return null;
1066 }
1067
1068 return null;
1069 }
1070
1071 function observeNetwork(state, detail, meta = {}) {
1072 if (!state || !detail || detail.source === "proxy" || !isRelevantStreamUrl(state.platform, detail.url)) {
1073 return null;
1074 }
1075
1076 if (typeof detail.resBody !== "string" || !detail.resBody) {
1077 return null;
1078 }
1079
1080 const context = {
1081 pageUrl: meta.pageUrl || "",
1082 reqBody: detail.reqBody || "",
1083 url: detail.url || ""
1084 };
1085 let candidate = null;
1086 if (state.platform === "chatgpt") {
1087 candidate = extractChatgptCandidateFromText(detail.resBody, context);
1088 } else if (state.platform === "claude") {
1089 candidate = extractClaudeCandidateFromText(detail.resBody, context);
1090 } else {
1091 candidate = extractGeminiCandidateFromText(detail.resBody, context);
1092 }
1093 const relay = buildRelayEnvelope(state.platform, candidate, meta.observedAt);
1094
1095 if (!relay || hasSeenRelay(state, relay)) {
1096 return null;
1097 }
1098
1099 return relay;
1100 }
1101
1102 const api = {
1103 createRelayState,
1104 extractClaudeCandidateFromText,
1105 extractChatgptCandidateFromChunk,
1106 extractChatgptCandidateFromText,
1107 extractGeminiCandidateFromText,
1108 isRelevantStreamUrl,
1109 observeNetwork,
1110 observeSse,
1111 rememberRelay
1112 };
1113
1114 if (typeof module !== "undefined" && module.exports) {
1115 module.exports = api;
1116 }
1117
1118 globalScope.BAAFinalMessage = api;
1119})(typeof globalThis !== "undefined" ? globalThis : this);