// ==UserScript== // @name ChatGPT Route Checker // @namespace chatgpt-route-checker // @version 6.0.6 // @description 每輪自動比對 ChatGPT 請求模型與服務端路由標注,提供可收合、可拖曳的狀態面板 // @author Yat-mo // @match https://chatgpt.com/* // @match https://chat.openai.com/* // @run-at document-start // @sandbox raw // @grant none // @noframes // ==/UserScript== (() => { "use strict"; const VERSION = "6.0.6"; const POST_RESPONSE_CONFIRMATION_MS = 15000; const INSTANCE_KEY = "__CHATGPT_ROUTE_CHECKER_V6__"; const HOST_ID = "__chatgpt_route_checker_v6_host__"; if (window[INSTANCE_KEY]) { console.info(`[Route Checker] v${VERSION} 已在執行`); return; } window[INSTANCE_KEY] = true; const nativeFetch = typeof window.fetch === "function" ? window.fetch.bind(window) : null; const NativeXHR = window.XMLHttpRequest; const nativeXHROpen = NativeXHR?.prototype?.open; const nativeXHRSend = NativeXHR?.prototype?.send; const nativeBeacon = typeof navigator.sendBeacon === "function" ? navigator.sendBeacon.bind(navigator) : null; const STORAGE = Object.freeze({ minimized: "__route_checker_v6_minimized__", strict: "__route_checker_v6_strict__", anchor: "__route_checker_v6_anchor__" }); function readBoolean(key, fallback) { try { const value = localStorage.getItem(key); return value === null ? fallback : value === "1"; } catch { return fallback; } } function writeBoolean(key, value) { try { localStorage.setItem(key, value ? "1" : "0"); } catch {} } function readJSON(key, fallback) { try { const value = localStorage.getItem(key); return value ? JSON.parse(value) : fallback; } catch { return fallback; } } function writeJSON(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch {} } function validAnchor(value) { if (!value || typeof value !== "object") return null; const horizontal = value.horizontal === "left" || value.horizontal === "right" ? value.horizontal : null; const vertical = value.vertical === "top" || value.vertical === "bottom" ? value.vertical : null; const x = Number(value.x); const y = Number(value.y); if (!horizontal || !vertical || !Number.isFinite(x) || !Number.isFinite(y)) { return null; } return { horizontal, vertical, x: Math.max(8, x), y: Math.max(8, y) }; } const defaultAnchor = Object.freeze({ horizontal: "right", vertical: "bottom", x: 18, y: 18 }); const state = { active: false, generationId: 0, turnKey: null, conversationId: null, lastRequestAt: 0, requestModel: null, thinkingEffort: null, requestError: null, serverModel: null, serverSource: null, assistantModel: null, resolvedModel: null, requestedExperience: null, responseThinkingEffort: null, domModel: null, startedAt: null, updatedAt: null, firstEvidenceAt: null, responseComplete: false, confirmationComplete: false, domBaselineNode: null, telemetrySeen: 0, telemetryScanned: 0, uiMinimized: readBoolean(STORAGE.minimized, true), strictMode: readBoolean(STORAGE.strict, false), detailsOpen: false, toastMessage: null, anchor: validAnchor(readJSON(STORAGE.anchor, null)) || { ...defaultAnchor } }; const oldHost = document.getElementById(HOST_ID); oldHost?.remove(); const host = document.createElement("div"); host.id = HOST_ID; host.style.cssText = [ "position:fixed", "z-index:2147483647", "display:block", "width:max-content", "height:max-content", "max-width:calc(100vw - 16px)", "max-height:calc(100vh - 16px)", "isolation:isolate", "contain:layout style", "pointer-events:auto" ].join(";"); const shadow = host.attachShadow({ mode: "open" }); shadow.innerHTML = `
`; const app = shadow.getElementById("app"); function esc(value) { return String(value).replace(/[&<>"']/g, character => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]); } function normalizeModel(value) { return String(value || "").trim().toLowerCase(); } function parseModelFamily(value) { const slug = normalizeModel(value); if (!slug) return null; const gpt = slug.match(/(?:^|-)gpt-(\d+)(o)?(?:[-.](\d+))?/i); if (gpt) { return { kind: "gpt", major: gpt[1], variant: gpt[2] ? "o" : (gpt[3] || null), key: `gpt-${gpt[1]}${gpt[2] ? "o" : gpt[3] ? `-${gpt[3]}` : ""}` }; } const oSeries = slug.match(/(?:^|-)o(\d+)(?:[-.](\d+))?/i); if (oSeries) { return { kind: "o", major: oSeries[1], variant: oSeries[2] || null, key: `o${oSeries[1]}${oSeries[2] ? `-${oSeries[2]}` : ""}` }; } return { kind: "other", major: slug, variant: null, key: slug }; } function sameModelFamily(left, right) { const a = parseModelFamily(left); const b = parseModelFamily(right); if (!a || !b || a.kind !== b.kind || a.major !== b.major) { return false; } if (a.key === b.key) return true; if (a.kind === "gpt" && (!a.variant || !b.variant)) { return true; } return false; } function routeTier(value) { const slug = normalizeModel(value); if (!slug) return null; if (/(?:^|[-_])(nano)(?:$|[-_])/.test(slug)) return 0; if (/(?:^|[-_])(mini|lite)(?:$|[-_])/.test(slug)) return 1; if (/(?:^|[-_])(instant|fast)(?:$|[-_])/.test(slug)) return 2; if (/(?:^|[-_])(thinking|reasoning)(?:$|[-_])/.test(slug)) return 4; if (/(?:^|[-_])(pro|max)(?:$|[-_])/.test(slug)) return 5; return null; } function effortTier(value) { const effort = String(value || "") .trim() .toLowerCase() .replace(/[\s_-]+/g, ""); return ({ none: 0, disabled: 0, minimal: 1, low: 2, medium: 3, high: 4, xhigh: 5, extrahigh: 5, max: 6, maximum: 6 })[effort] ?? null; } function isAutoModel(value) { const slug = normalizeModel(value); return ( slug === "auto" || /(?:^|[-_])(auto|automatic)(?:$|[-_])/.test(slug) ); } function observedModel() { if (state.serverModel) { return { model: state.serverModel, source: state.serverSource || "server", primary: true }; } if (state.assistantModel) { return { model: state.assistantModel, source: "assistant", primary: false }; } if (state.resolvedModel) { return { model: state.resolvedModel, source: "resolved", primary: false }; } if (state.domModel) { return { model: state.domModel, source: "dom", primary: false }; } return null; } function modelEvidence() { const evidence = []; function add(field, model, source, primary = false) { if (typeof model !== "string" || !model.trim()) return; evidence.push({ field, model, source, primary }); } add( "server_ste_metadata.model_slug", state.serverModel, state.serverSource || "server", true ); add( "assistant.metadata.model_slug", state.assistantModel, "assistant" ); add( "resolved_model_slug", state.resolvedModel, "resolved" ); add( "DOM data-message-model-slug", state.domModel, "dom" ); return evidence; } function conflictingEvidence(requestedModel = state.requestModel) { const requested = normalizeModel(requestedModel); if (!requested) return []; return modelEvidence().filter( item => normalizeModel(item.model) !== requested ); } function evidenceSummary(items) { return items .map(item => `${sourceLabel(item.source)}: ${item.model}`) .join(";"); } function sourceLabel(source) { return ({ stream: "服務端串流標注", telemetry: "遙測標注", server: "服務端標注", assistant: "助理訊息 metadata", resolved: "resolved model", dom: "頁面 DOM" })[source] || "未知來源"; } function routeStatus() { if (!state.active) { return { key: "idle", tone: "idle", icon: "idle", mini: "等待檢測", title: "等待下一輪", description: "發送訊息後,這裡會自動比對本輪模型路由。", resolved: false }; } if (state.requestError) { return { key: "request-error", tone: "danger", icon: "danger", mini: "請求失敗", title: "這輪請求未完成", description: "網路請求失敗,因此無法完成路由比對。", resolved: true }; } if (!state.requestModel) { return { key: "reading-request", tone: "info", icon: "checking", mini: "檢測中", title: "正在讀取請求", description: "已偵測到新一輪訊息,正在解析請求模型。", resolved: false }; } const observed = observedModel(); if (!observed) { return { key: "waiting-server", tone: "info", icon: "checking", mini: state.responseComplete ? "正在確認" : "檢測中", title: state.responseComplete ? "正在確認路由標注" : "正在等待路由標注", description: state.responseComplete ? "正文回覆已結束,仍在等待稍後送達的路由遙測標注。" : "請求模型已取得,等待服務端回傳本輪模型資料。", resolved: false }; } const requested = normalizeModel(state.requestModel); const routed = normalizeModel(observed.model); const requestedEffortTier = effortTier(state.thinkingEffort); const responseEffortTier = effortTier(state.responseThinkingEffort); const conflicts = conflictingEvidence(state.requestModel); const secondaryConflicts = conflicts.filter(item => !item.primary); if (!observed.primary) { return { key: "waiting-primary", tone: "info", icon: "checking", mini: "正在確認", title: "正在確認路由標注", description: `已取得${sourceLabel(observed.source)},收到 server_ste_metadata.model_slug 後才會給出判定。`, resolved: false }; } if ( requestedEffortTier !== null && responseEffortTier !== null && responseEffortTier < requestedEffortTier ) { return { key: "effort-downgrade", tone: "danger", icon: "danger", mini: "疑似降智", title: "思考強度低於請求", description: `請求為 ${state.thinkingEffort},回傳標注為 ${state.responseThinkingEffort}。`, resolved: true }; } if (isAutoModel(requested) && observed.primary) { return { key: "automatic", tone: "info", icon: "route", mini: "自動路由", title: "本輪使用自動路由", description: `系統選擇了 ${observed.model},自動模式沒有固定模型可供精確比對。`, resolved: true }; } if (state.strictMode && conflicts.length) { return { key: "strict-evidence-conflict", tone: "danger", icon: "danger", mini: "標注衝突", title: "嚴格比對未通過", description: `${evidenceSummary(conflicts)},與請求模型 ${state.requestModel} 不同。`, resolved: true }; } if ( observed.primary && requested === routed && secondaryConflicts.length ) { return { key: "secondary-evidence-conflict", tone: "warn", icon: "warn", mini: "標注有差異", title: "交叉驗證發現不同標注", description: `主要服務端標注與請求相同,但${evidenceSummary(secondaryConflicts)}。`, resolved: true }; } if (requested === routed) { return { key: "exact-match", tone: "ok", icon: "ok", mini: "路由正常", title: "路由完全一致", description: `請求模型與${sourceLabel(observed.source)}完全相同。`, resolved: true }; } const sameFamily = sameModelFamily(requested, routed); const requestedTier = routeTier(requested) ?? (requestedEffortTier !== null && requestedEffortTier >= 2 ? 4 : null); const routedTier = routeTier(routed); const explicitLowerTier = sameFamily && requestedTier !== null && routedTier !== null && routedTier < requestedTier; if (state.strictMode) { return { key: "strict-difference", tone: "danger", icon: "danger", mini: "疑似降智", title: "嚴格比對未通過", description: "請求模型與服務端 model slug 並非逐字一致。", resolved: true }; } if (explicitLowerTier) { return { key: "explicit-downgrade", tone: "danger", icon: "danger", mini: "疑似降智", title: "偵測到較低路由層級", description: "請求與回覆屬於同系列,但服務端標注含有較低層級的路由詞。", resolved: true }; } if (sameFamily) { return { key: "family-variant", tone: "warn", icon: "warn", mini: "同系列變體", title: "同系列,內部路由不同", description: "模型系列一致,但內部 slug 不同。這值得留意,尚不能單憑此項判定降智。", resolved: true }; } return { key: "model-mismatch", tone: "danger", icon: "danger", mini: "疑似降智", title: "模型路由不一致", description: "請求模型與服務端標注屬於不同模型系列。", resolved: true }; } function icon(name) { const common = `viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"`; const icons = { brand: ` `, idle: ` `, ok: ` `, warn: ` `, danger: ` `, route: ` `, chevronUp: ``, chevronDown: ``, collapse: ` `, copy: ` `, refresh: ` `, swap: ` `, info: ` ` }; return icons[name] || icons.info; } function statusIcon(status) { if (status.icon === "checking") { return ''; } return icon(status.icon); } function modelDisplay(value) { return value ? `
${esc(value)}
` : '
讀取中
'; } function fieldRow(key, value) { const hasValue = value !== null && value !== undefined && value !== ""; const missingLabel = !state.active ? "等待訊息" : state.confirmationComplete ? "本輪未提供" : "正在確認"; return `
${esc(key)}
${hasValue ? esc(value) : missingLabel}
`; } function crossCheckLabel() { const evidence = modelEvidence(); const conflicts = conflictingEvidence(); const hasPrimaryEvidence = evidence.some(item => item.primary); if (!state.requestModel || !evidence.length) { return null; } if (!hasPrimaryEvidence) { return "正在確認主要服務端標注"; } if (conflicts.length) { return `衝突:${evidenceSummary(conflicts)}`; } return "目前所有標注一致"; } function detectionLatency() { if (!state.startedAt || !state.firstEvidenceAt) return null; const milliseconds = Math.max(0, state.firstEvidenceAt - state.startedAt); return milliseconds < 1000 ? `${milliseconds} ms` : `${(milliseconds / 1000).toFixed(2)} s`; } function formatTime(timestamp) { if (!timestamp) return null; try { return new Date(timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } catch { return null; } } function miniModel(status) { const observed = observedModel(); if (status.key === "idle") return `Route Checker v${VERSION}`; if (!status.resolved) return state.requestModel || "正在取得模型資料"; return observed?.model || state.requestModel || `Route Checker v${VERSION}`; } let renderQueued = false; let lastRenderedMode = null; function scheduleRender() { if (renderQueued) return; renderQueued = true; requestAnimationFrame(() => { renderQueued = false; render(); }); } function render() { if (drag?.moved) { renderAfterDrag = true; return; } mountHost(); syncTheme(); const status = routeStatus(); const observed = observedModel(); const mode = state.uiMinimized ? "pill" : "card"; const animateClass = lastRenderedMode && lastRenderedMode !== mode ? " materialize" : ""; lastRenderedMode = mode; if (state.uiMinimized) { app.innerHTML = ` `; } else { const observedSource = observed ? sourceLabel(observed.source) : "等待服務端資料"; const time = formatTime(state.updatedAt || state.startedAt); app.innerHTML = `
${icon("brand")} Route Checker CHATGPT · V${VERSION}
${state.toastMessage ? `
${esc(state.toastMessage)}
` : ""}
${statusIcon(status)} ${esc(status.title)} ${esc(status.description)}
請求模型
${modelDisplay(state.requestModel)}
${esc(state.thinkingEffort ? `effort: ${state.thinkingEffort}` : "request.model")}
觀察到的模型
${modelDisplay(observed?.model)}
${esc(observedSource)}
嚴格比對
開啟後,只要 model slug 不完全一致就會標紅。
${state.detailsOpen ? `
${fieldRow("request.model", state.requestModel)} ${fieldRow("request.thinking_effort", state.thinkingEffort)} ${fieldRow("server_ste_metadata.model_slug", state.serverModel)} ${fieldRow("assistant.metadata.model_slug", state.assistantModel)} ${fieldRow("resolved_model_slug", state.resolvedModel)} ${fieldRow("requested_model_experience", state.requestedExperience)} ${fieldRow("response.thinking_effort", state.responseThinkingEffort)} ${fieldRow("DOM data-message-model-slug", state.domModel)} ${fieldRow("判定來源", observedSource)} ${fieldRow("首個路由資料耗時", detectionLatency())} ${fieldRow( "回覆擷取狀態", state.responseComplete ? state.confirmationComplete ? "確認完成" : "正在確認" : "接收中" )} ${fieldRow("遙測封包", `${state.telemetryScanned}/${state.telemetrySeen}`)} ${fieldRow("交叉驗證", crossCheckLabel())}
` : ""}

${icon("info")} 判定依據是前端可見的路由 metadata。紅色表示標注不一致,不能單獨證明實際推理品質下降。

`; } requestAnimationFrame(() => { applyAnchor(); clampHost(); }); } function mountHost() { const root = document.documentElement || document.body; if (root && !host.isConnected) { root.appendChild(host); applyAnchor(); syncTheme(); } } function syncTheme() { const root = document.documentElement; const explicit = String( root?.getAttribute("data-theme") || root?.getAttribute("data-color-scheme") || "" ).toLowerCase(); const classDark = root?.classList?.contains("dark"); const explicitDark = explicit.includes("dark"); const explicitLight = explicit.includes("light"); const systemDark = window.matchMedia?.("(prefers-color-scheme: dark)")?.matches; host.dataset.theme = classDark || explicitDark || (!explicitLight && systemDark) ? "dark" : "light"; } function applyAnchor() { const anchor = validAnchor(state.anchor) || { ...defaultAnchor }; const margin = 8; host.style.left = "auto"; host.style.right = "auto"; host.style.top = "auto"; host.style.bottom = "auto"; if (anchor.horizontal === "left") { host.style.left = `${Math.max(margin, anchor.x)}px`; } else { host.style.right = `${Math.max(margin, anchor.x)}px`; } if (anchor.vertical === "top") { host.style.top = `${Math.max(margin, anchor.y)}px`; } else { host.style.bottom = `${Math.max(margin, anchor.y)}px`; } } function clampHost() { if (!host.isConnected) return; const margin = 8; const rect = host.getBoundingClientRect(); let left = rect.left; let top = rect.top; const maxLeft = Math.max(margin, window.innerWidth - rect.width - margin); const maxTop = Math.max(margin, window.innerHeight - rect.height - margin); left = Math.min(maxLeft, Math.max(margin, left)); top = Math.min(maxTop, Math.max(margin, top)); if ( Math.abs(left - rect.left) > 0.5 || Math.abs(top - rect.top) > 0.5 ) { host.style.left = `${left}px`; host.style.top = `${top}px`; host.style.right = "auto"; host.style.bottom = "auto"; captureAnchor(); } } function captureAnchor() { const rect = host.getBoundingClientRect(); const horizontal = rect.left + rect.width / 2 < window.innerWidth / 2 ? "left" : "right"; const vertical = rect.top + rect.height / 2 < window.innerHeight / 2 ? "top" : "bottom"; state.anchor = { horizontal, vertical, x: Math.max(8, horizontal === "left" ? rect.left : window.innerWidth - rect.right), y: Math.max(8, vertical === "top" ? rect.top : window.innerHeight - rect.bottom) }; writeJSON(STORAGE.anchor, state.anchor); applyAnchor(); } function resetPosition() { state.anchor = { ...defaultAnchor }; writeJSON(STORAGE.anchor, state.anchor); applyAnchor(); requestAnimationFrame(clampHost); } let drag = null; let toastTimer = null; let confirmationTimer = null; let suppressNextClick = false; let renderAfterDrag = false; shadow.addEventListener("pointerdown", event => { if (event.button !== 0) return; const handle = event.target.closest("[data-drag-handle]"); if (!handle) return; const isPill = handle.classList.contains("pill"); if (!isPill && event.target.closest("button")) { return; } const rect = host.getBoundingClientRect(); drag = { pointerId: event.pointerId, handle, startX: event.clientX, startY: event.clientY, startLeft: rect.left, startTop: rect.top, width: rect.width, height: rect.height, moved: false }; handle.setPointerCapture?.(event.pointerId); }); shadow.addEventListener("pointermove", event => { if (!drag || event.pointerId !== drag.pointerId) return; const dx = event.clientX - drag.startX; const dy = event.clientY - drag.startY; if (!drag.moved && Math.hypot(dx, dy) < 5) return; drag.moved = true; suppressNextClick = true; host.dataset.dragging = "true"; const margin = 8; const maxLeft = Math.max(margin, window.innerWidth - drag.width - margin); const maxTop = Math.max(margin, window.innerHeight - drag.height - margin); const left = Math.min(maxLeft, Math.max(margin, drag.startLeft + dx)); const top = Math.min(maxTop, Math.max(margin, drag.startTop + dy)); host.style.left = `${left}px`; host.style.top = `${top}px`; host.style.right = "auto"; host.style.bottom = "auto"; event.preventDefault(); }); function finishDrag(event) { if (!drag || event.pointerId !== drag.pointerId) return; try { drag.handle.releasePointerCapture?.(event.pointerId); } catch {} const moved = drag.moved; if (moved) captureAnchor(); delete host.dataset.dragging; drag = null; if (moved) { window.setTimeout(() => { suppressNextClick = false; }, 0); } if (renderAfterDrag) { renderAfterDrag = false; scheduleRender(); } } shadow.addEventListener("pointerup", finishDrag); shadow.addEventListener("pointercancel", finishDrag); shadow.addEventListener("click", async event => { const action = event.target .closest("[data-action]") ?.dataset?.action?.trim(); if (!action) return; if (suppressNextClick) { suppressNextClick = false; event.preventDefault(); return; } if (action === "toggle-panel") { state.uiMinimized = !state.uiMinimized; writeBoolean(STORAGE.minimized, state.uiMinimized); scheduleRender(); return; } if (action === "toggle-strict") { state.strictMode = !state.strictMode; writeBoolean(STORAGE.strict, state.strictMode); scheduleRender(); return; } if (action === "toggle-details") { state.detailsOpen = !state.detailsOpen; scheduleRender(); return; } if (action === "refresh-data") { refreshVisibleData(); return; } if (action === "copy") { await copySnapshot(); } }); function snapshot() { const status = routeStatus(); const observed = observedModel(); const evidence = modelEvidence(); const conflicts = conflictingEvidence(); const confirmed = evidence.some(item => item.primary); return { checkerVersion: VERSION, checkedAt: new Date(state.updatedAt || Date.now()).toISOString(), verdict: status.title, strictMode: state.strictMode, request: { model: state.requestModel, thinkingEffort: state.thinkingEffort }, observed: observed ? { model: observed.model, source: sourceLabel(observed.source), primary: observed.primary } : null, metadata: { serverModel: state.serverModel, assistantModel: state.assistantModel, resolvedModel: state.resolvedModel, requestedExperience: state.requestedExperience, responseThinkingEffort: state.responseThinkingEffort, domModel: state.domModel }, crossCheck: { confirmed, passed: confirmed ? conflicts.length === 0 : null, evidence: evidence.map(item => ({ field: item.field, model: item.model, source: sourceLabel(item.source), primary: item.primary, matchesRequest: normalizeModel(item.model) === normalizeModel(state.requestModel) })), conflicts: conflicts.map(item => ({ field: item.field, model: item.model, source: sourceLabel(item.source) })) }, capture: { responseComplete: state.responseComplete, confirmationComplete: state.confirmationComplete, telemetrySeen: state.telemetrySeen, telemetryScanned: state.telemetryScanned, firstEvidenceAfterMs: state.startedAt && state.firstEvidenceAt ? Math.max(0, state.firstEvidenceAt - state.startedAt) : null, unavailableFields: state.confirmationComplete ? [ ["serverModel", state.serverModel], ["assistantModel", state.assistantModel], ["resolvedModel", state.resolvedModel], ["requestedExperience", state.requestedExperience], ["responseThinkingEffort", state.responseThinkingEffort], ["domModel", state.domModel] ] .filter(([, value]) => value === null || value === undefined) .map(([key]) => key) : [] } }; } async function writeClipboard(text) { if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(text); return true; } catch {} } try { const textarea = document.createElement("textarea"); textarea.value = text; textarea.setAttribute("readonly", ""); textarea.style.cssText = "position:fixed;left:-9999px;top:0"; document.documentElement.appendChild(textarea); textarea.select(); const copied = document.execCommand("copy"); textarea.remove(); return copied; } catch { return false; } } async function copySnapshot() { const copied = await writeClipboard(JSON.stringify(snapshot(), null, 2)); if (!copied) return; showToast("已複製"); } function showToast(message) { state.toastMessage = message; scheduleRender(); if (toastTimer) { window.clearTimeout(toastTimer); } toastTimer = window.setTimeout(() => { state.toastMessage = null; toastTimer = null; scheduleRender(); }, 1200); } function markEvidence() { const now = Date.now(); if (!state.firstEvidenceAt) { state.firstEvidenceAt = now; } state.updatedAt = now; } function markResponseComplete(context = {}) { if (!contextIsCurrent(context) || state.responseComplete) return; state.responseComplete = true; state.confirmationComplete = false; state.updatedAt = Date.now(); scheduleRender(); const generationId = context.generationId || state.generationId; if (confirmationTimer) { window.clearTimeout(confirmationTimer); } confirmationTimer = window.setTimeout(() => { confirmationTimer = null; if (generationId !== state.generationId) return; state.confirmationComplete = true; state.updatedAt = Date.now(); scheduleRender(); }, POST_RESPONSE_CONFIRMATION_MS); } function latestAssistantCandidate() { const nodes = document.querySelectorAll( '[data-message-author-role="assistant"]' ); if (!nodes.length) return null; const root = nodes[nodes.length - 1]; const modelNode = root.hasAttribute("data-message-model-slug") ? root : root.querySelector("[data-message-model-slug]"); return { node: root, slug: modelNode?.getAttribute("data-message-model-slug") || null }; } function applyDomCandidate(candidate, force = false) { if (!state.active) return false; if ( !candidate || !candidate.slug || (!force && candidate.node === state.domBaselineNode) ) { return false; } if (!force && state.domModel === candidate.slug) return false; state.domModel = candidate.slug; markEvidence(); return true; } function refreshDomModel(force = false) { return applyDomCandidate(latestAssistantCandidate(), force); } function assistantCandidateFromElement(element) { if (!element || element.nodeType !== 1) return null; let assistant = element.matches?.('[data-message-author-role="assistant"]') ? element : element.closest?.('[data-message-author-role="assistant"]'); if (!assistant) { const descendants = element.querySelectorAll?.( '[data-message-author-role="assistant"]' ); if (descendants?.length) { assistant = descendants[descendants.length - 1]; } } if (!assistant) return null; const modelNode = assistant.hasAttribute("data-message-model-slug") ? assistant : assistant.querySelector("[data-message-model-slug]"); return { node: assistant, slug: modelNode?.getAttribute("data-message-model-slug") || null }; } function refreshVisibleData() { const refreshed = refreshDomModel(true); state.updatedAt = Date.now(); showToast(refreshed ? "已重新掃描" : "頁面沒有可讀資料"); } function extractTurnKey(request) { try { const messages = Array.isArray(request?.messages) ? request.messages : []; for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; if (message?.author?.role === "user" && message?.id) { return message.id; } } if (request?.message?.author?.role === "user") { return request.message.id || null; } return request?.parent_message_id || null; } catch { return null; } } function resetTurn(request, turnKey) { const baseline = latestAssistantCandidate(); if (confirmationTimer) { window.clearTimeout(confirmationTimer); confirmationTimer = null; } state.active = true; state.generationId += 1; state.turnKey = turnKey || `turn-${Date.now()}`; state.conversationId = request?.conversation_id || state.conversationId || null; state.requestModel = null; state.thinkingEffort = null; state.requestError = null; state.serverModel = null; state.serverSource = null; state.assistantModel = null; state.resolvedModel = null; state.requestedExperience = null; state.responseThinkingEffort = null; state.domModel = null; state.startedAt = Date.now(); state.updatedAt = null; state.firstEvidenceAt = null; state.responseComplete = false; state.confirmationComplete = false; state.telemetrySeen = 0; state.telemetryScanned = 0; state.domBaselineNode = baseline?.node || null; } function beginOrUpdateTurn(request) { const now = Date.now(); const newTurnKey = extractTurnKey(request); const isDefinitelyNewTurn = Boolean(newTurnKey) && newTurnKey !== state.turnKey; if (!state.active || isDefinitelyNewTurn) { resetTurn(request, newTurnKey); } else if (!state.turnKey && newTurnKey) { state.turnKey = newTurnKey; } state.lastRequestAt = now; if (typeof request?.model === "string") { state.requestModel = request.model; } const thinkingEffort = request?.thinking_effort ?? request?.reasoning_effort ?? request?.model_configuration?.thinking_effort; if (thinkingEffort !== null && thinkingEffort !== undefined) { state.thinkingEffort = String(thinkingEffort); } const requestedExperience = request?.requested_model_experience ?? request?.model_configuration?.requested_model_experience; if (requestedExperience !== null && requestedExperience !== undefined) { state.requestedExperience = String(requestedExperience); } scheduleRender(); return state.generationId; } function contextIsCurrent(context) { return ( !context?.generationId || context.generationId === state.generationId ); } function applyServerMetadata(metadata, context = {}) { if ( !metadata || typeof metadata !== "object" || !contextIsCurrent(context) ) { return; } let changed = false; let modelCaptured = false; if (typeof metadata.model_slug === "string") { const canApply = context.source !== "telemetry" || !state.serverModel || state.serverSource === "telemetry"; if (canApply && state.serverModel !== metadata.model_slug) { state.serverModel = metadata.model_slug; state.serverSource = context.source || "server"; changed = true; modelCaptured = true; } } if (typeof metadata.requested_model_experience === "string") { if (state.requestedExperience !== metadata.requested_model_experience) { state.requestedExperience = metadata.requested_model_experience; changed = true; } } const responseEffort = metadata.thinking_effort ?? metadata.reasoning_effort; if (responseEffort !== null && responseEffort !== undefined) { const value = String(responseEffort); if (state.responseThinkingEffort !== value) { state.responseThinkingEffort = value; changed = true; } } if (changed) { if (modelCaptured) { markEvidence(); } else { state.updatedAt = Date.now(); } scheduleRender(); } } function scanObject(object, context = {}, depth = 0, seen = new WeakSet()) { if ( object === null || object === undefined || depth > 16 || !contextIsCurrent(context) ) { return; } if (typeof object === "string") { const text = object.trim(); if ( (text.startsWith("{") || text.startsWith("[")) && /model_slug|server_ste_metadata|resolved_model|requested_model_experience|thinking_effort|reasoning_effort/.test(text) ) { try { scanObject(JSON.parse(text), context, depth + 1, seen); } catch {} } return; } if (typeof object !== "object" || seen.has(object)) return; seen.add(object); if (object.type === "server_ste_metadata" && object.metadata) { applyServerMetadata(object.metadata, context); } if ( object.server_ste_metadata && typeof object.server_ste_metadata === "object" ) { applyServerMetadata(object.server_ste_metadata, context); } if (object.turn_analytics?.server_ste_metadata) { applyServerMetadata(object.turn_analytics.server_ste_metadata, context); } if ( ["message_stream_complete", "turn_complete", "response.completed"] .includes(String(object.type || "")) ) { markResponseComplete(context); } let changed = false; let modelCaptured = false; const assistantMetadata = object.author?.role === "assistant" ? object.metadata : object.message?.author?.role === "assistant" ? object.message?.metadata : null; if ( typeof assistantMetadata?.model_slug === "string" && state.assistantModel !== assistantMetadata.model_slug ) { state.assistantModel = assistantMetadata.model_slug; changed = true; modelCaptured = true; } const assistantEffort = assistantMetadata?.thinking_effort ?? assistantMetadata?.reasoning_effort ?? assistantMetadata?.reasoning?.effort; if (assistantEffort !== null && assistantEffort !== undefined) { const value = String(assistantEffort); if (state.responseThinkingEffort !== value) { state.responseThinkingEffort = value; changed = true; } } const resolvedModel = object.resolved_model_slug ?? object.metadata?.resolved_model_slug ?? object.message?.metadata?.resolved_model_slug ?? (typeof object.resolved_model === "string" ? object.resolved_model : object.resolved_model?.slug); if (typeof resolvedModel === "string" && state.resolvedModel !== resolvedModel) { state.resolvedModel = resolvedModel; changed = true; modelCaptured = true; } if ( typeof object.requested_model_experience === "string" && state.requestedExperience !== object.requested_model_experience ) { state.requestedExperience = object.requested_model_experience; changed = true; } if (changed) { if (modelCaptured) { markEvidence(); } else { state.updatedAt = Date.now(); } scheduleRender(); } for (const value of Object.values(object)) { scanObject(value, context, depth + 1, seen); } } function parseJSONCandidate(text, context) { if (!text) return; const value = text.trim(); if (!value) return; if (value === "[DONE]") { markResponseComplete(context); return; } try { scanObject(JSON.parse(value), context); } catch {} } function scanWholeText(text, context = {}) { if (!text || typeof text !== "string" || !contextIsCurrent(context)) { return; } try { scanObject(JSON.parse(text), context); } catch {} for (let line of text.split(/\r?\n/)) { line = line.trim(); if (!line) continue; if (line.startsWith("data:")) { line = line.slice(5).trim(); } parseJSONCandidate(line, context); } } async function watchStream(response, context) { if (!response?.body) { try { scanWholeText(await response.text(), context); } catch {} markResponseComplete(context); return; } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; let eventData = []; try { while (true) { const { value, done } = await reader.read(); if (done || !contextIsCurrent(context)) break; buffer += decoder.decode(value, { stream: true }); let newlineIndex; while ((newlineIndex = buffer.indexOf("\n")) !== -1) { let line = buffer.slice(0, newlineIndex); buffer = buffer.slice(newlineIndex + 1); if (line.endsWith("\r")) { line = line.slice(0, -1); } if (line === "") { if (eventData.length) { parseJSONCandidate(eventData.join("\n"), context); eventData = []; } continue; } if (line.startsWith("data:")) { const data = line.slice(5).trimStart(); eventData.push(data); // 單行 SSE 立即解析,不必等到下一個空行或串流結束。 parseJSONCandidate(data, context); continue; } const trimmed = line.trim(); if (trimmed.startsWith("{") || trimmed.startsWith("[")) { parseJSONCandidate(trimmed, context); } } } buffer += decoder.decode(); if (buffer.trim()) { const rest = buffer.trim(); if (rest.startsWith("data:")) { const data = rest.slice(5).trimStart(); eventData.push(data); parseJSONCandidate(data, context); } else { parseJSONCandidate(rest, context); } } if (eventData.length) { parseJSONCandidate(eventData.join("\n"), context); } } catch (error) { console.debug("[Route Checker] 串流解析中止", error); } finally { try { reader.releaseLock(); } catch {} markResponseComplete(context); } } async function bodyToText(body) { try { if (body === null || body === undefined) return ""; if (typeof body === "string") return body; if (body instanceof Blob) return await body.text(); if (body instanceof URLSearchParams) return body.toString(); if (typeof FormData !== "undefined" && body instanceof FormData) { const parts = []; for (const [key, value] of body.entries()) { if (typeof value === "string") { parts.push(`${key}=${value}`); } else if (value instanceof Blob) { parts.push(`${key}=${await value.text()}`); } } return parts.join("\n"); } if (body instanceof ArrayBuffer) { return new TextDecoder().decode(body); } if (ArrayBuffer.isView(body)) { return new TextDecoder().decode(body); } } catch {} return ""; } async function getRequestText(input, init) { if (init?.body !== null && init?.body !== undefined) { const text = await bodyToText(init.body); if (text) return text; } if (input instanceof Request) { try { return await input.clone().text(); } catch {} } return ""; } function pathnameOf(value) { try { return new URL(String(value || ""), location.href).pathname; } catch { return String(value || "").split("?")[0]; } } function isConversation(url, method) { return ( method === "POST" && /^\/backend-api\/(?:f\/)?conversation\/?$/.test(pathnameOf(url)) ); } function isTelemetry(url, method) { return ( method === "POST" && /^\/ces\/v1\/telemetry\/intake\/?$/.test(pathnameOf(url)) ); } function shouldScanTelemetry(text) { if ( !state.active || !state.startedAt || !text || typeof text !== "string" ) { return false; } if (state.turnKey && text.includes(state.turnKey)) return true; if (state.conversationId && text.includes(state.conversationId)) { return true; } // v5 會掃描所有遙測。這裡保留五分鐘窗口,避免長回答的 // server_ste_metadata 因為稍晚送達而被丟棄。 return Date.now() - state.startedAt <= 300000; } function scanTelemetryText(text) { if (!state.active) return; state.telemetrySeen += 1; if (shouldScanTelemetry(text)) { state.telemetryScanned += 1; scanWholeText(text, { source: "telemetry", generationId: state.generationId }); } scheduleRender(); } if (nativeFetch) { window.fetch = async function routeCheckerFetch(input, init = {}) { const url = typeof input === "string" || input instanceof URL ? String(input) : input?.url || ""; const method = String( init.method || input?.method || "GET" ).toUpperCase(); const conversationRequest = isConversation(url, method); const telemetryRequest = isTelemetry(url, method); let requestText = ""; let generationId = null; const requestTextPromise = conversationRequest || telemetryRequest ? getRequestText(input, init) : Promise.resolve(""); // 先啟動原始請求,讀取複本時不阻塞 ChatGPT 發送訊息。 const responsePromise = nativeFetch(input, init); void responsePromise.catch(() => {}); if (conversationRequest || telemetryRequest) { requestText = await requestTextPromise; } if (conversationRequest) { try { generationId = beginOrUpdateTurn(JSON.parse(requestText)); } catch { generationId = beginOrUpdateTurn({}); } } if (telemetryRequest) { scanTelemetryText(requestText); } let response; try { response = await responsePromise; } catch (error) { if (conversationRequest && generationId === state.generationId) { state.requestError = error?.message || "fetch failed"; state.updatedAt = Date.now(); scheduleRender(); } throw error; } if (conversationRequest) { try { void watchStream(response.clone(), { source: "stream", generationId }); } catch {} } return response; }; } if (NativeXHR && nativeXHROpen && nativeXHRSend) { NativeXHR.prototype.open = function routeCheckerOpen(method, url, ...rest) { this.__routeCheckerMethod = String(method || "GET").toUpperCase(); this.__routeCheckerUrl = String(url || ""); return nativeXHROpen.call(this, method, url, ...rest); }; NativeXHR.prototype.send = function routeCheckerSend(body) { const url = this.__routeCheckerUrl || ""; const method = this.__routeCheckerMethod || "GET"; const conversationRequest = isConversation(url, method); const telemetryRequest = isTelemetry(url, method); this.__routeCheckerGenerationId = null; void bodyToText(body).then(text => { if (conversationRequest) { try { this.__routeCheckerGenerationId = beginOrUpdateTurn(JSON.parse(text)); } catch { this.__routeCheckerGenerationId = beginOrUpdateTurn({}); } } if (telemetryRequest) { scanTelemetryText(text); } }); if (conversationRequest) { this.addEventListener("loadend", () => { const context = { source: "stream", generationId: this.__routeCheckerGenerationId || state.generationId }; try { if (!this.responseType || this.responseType === "text") { scanWholeText(this.responseText, context); } else if (this.responseType === "json") { scanObject(this.response, context); } } catch {} if ( this.status === 0 && context.generationId === state.generationId ) { state.requestError = "xhr failed"; state.updatedAt = Date.now(); scheduleRender(); } markResponseComplete(context); }, { once: true }); } return nativeXHRSend.call(this, body); }; } if (nativeBeacon) { const wrappedBeacon = function routeCheckerBeacon(url, data) { const target = String(url || ""); if (isTelemetry(target, "POST")) { void bodyToText(data).then(text => { scanTelemetryText(text); }); } return nativeBeacon(url, data); }; try { navigator.sendBeacon = wrappedBeacon; } catch { try { Object.defineProperty(navigator, "sendBeacon", { configurable: true, value: wrappedBeacon }); } catch {} } } function handleDomMutations(records) { if (!state.active) return; for (let recordIndex = records.length - 1; recordIndex >= 0; recordIndex -= 1) { const record = records[recordIndex]; if (record.type === "attributes") { if (applyDomCandidate(assistantCandidateFromElement(record.target))) { scheduleRender(); return; } continue; } const addedNodes = record.addedNodes || []; for (let nodeIndex = addedNodes.length - 1; nodeIndex >= 0; nodeIndex -= 1) { if (applyDomCandidate(assistantCandidateFromElement(addedNodes[nodeIndex]))) { scheduleRender(); return; } } } } const domObserver = new MutationObserver(handleDomMutations); domObserver.observe(document, { subtree: true, childList: true, attributes: true, attributeFilter: ["data-message-model-slug"] }); let themeObserver = null; function observeTheme() { if (!document.documentElement || themeObserver) return; themeObserver = new MutationObserver(syncTheme); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-theme", "data-color-scheme"] }); } const darkModeQuery = window.matchMedia?.("(prefers-color-scheme: dark)"); darkModeQuery?.addEventListener?.("change", syncTheme); window.addEventListener("resize", () => { applyAnchor(); requestAnimationFrame(clampHost); }, { passive: true }); document.addEventListener("readystatechange", () => { mountHost(); observeTheme(); }); window.__CHATGPT_ROUTE_CHECK_STATE__ = state; window.__CHATGPT_ROUTE_CHECKER__ = Object.freeze({ version: VERSION, snapshot, expand() { state.uiMinimized = false; writeBoolean(STORAGE.minimized, false); scheduleRender(); }, minimize() { state.uiMinimized = true; writeBoolean(STORAGE.minimized, true); scheduleRender(); }, resetPosition }); mountHost(); observeTheme(); render(); console.info(`[Route Checker] v${VERSION} 已啟動`); })();