baa-conductor


baa-conductor / plugins / baa-firefox
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);