// ==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 = `
`;
}
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} 已啟動`);
})();