// token_inject.js —— 注入到 Codex 页面的 Token 用量 UI 脚本,以字符串形式导出。 // 由 voice_cdp.js 经 CDP Runtime.evaluate 注入(复用语音 CDP 连接,无需独立端口)。 // // 职责(仅 UI 层): // 1. 在输入框下方工具栏内创建 token 用量条(优先放在模型选择左侧) // 2. 读写 localStorage["td-token-daily"](按日期分桶,跨天自动清零) // 3. 暴露 window.__tdTokenAdd(usage) 给主进程调用来累加并触发动画 // 4. 暴露 window.__tdTokenGet() / __tdTokenReset() 给主进程调用来读取/重置 // 5. 页面内兜底解析 fetch/XHR usage 与 Codex DOM token 文本 // // Token 抓取优先由 voice_cdp.js 在 CDP 层面通过 Network.responseReceived 完成, // 页面内 fetch/XHR 解析只作为兜底。 // // 注意:脚本体用普通模板字符串包裹;内部避免出现 ${ 与裸反引号。 const TOKEN_INJECT_SCRIPT = ` (() => { const VER = "td-token-v18"; if (window.__tdTokenInstalled === VER) return; if (window.__tdTokenObserver) { try { window.__tdTokenObserver.disconnect(); } catch {} } const oldBar = document.getElementById("td-token-bar"); if (oldBar) oldBar.remove(); const oldRow = document.getElementById("td-token-center-row"); if (oldRow) oldRow.remove(); ["td-token-style-v4", "td-token-style-v5", "td-token-style-v6", "td-token-style-v7", "td-token-style-v8", "td-token-style-v9", "td-token-style-v10", "td-token-style-v11", "td-token-style-v12", "td-token-style-v13", "td-token-style-v14", "td-token-style-v15", "td-token-style-v16", "td-token-style-v17", "td-token-style-v18"].forEach((id) => { const el = document.getElementById(id); if (el) el.remove(); }); window.__tdTokenInstalled = VER; // ============================================================================ // 1. 持久化 // ============================================================================ const STORE_KEY = "td-token-daily"; function todayKey() { const d = new Date(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return d.getFullYear() + "-" + m + "-" + day; } function loadDaily() { try { const raw = localStorage.getItem(STORE_KEY); if (!raw) return blankDaily(); const d = JSON.parse(raw); if (d.date !== todayKey()) return blankDaily(); return normalizeDaily(d); } catch { return blankDaily(); } } function blankDaily() { return { date: todayKey(), total: 0, input: 0, output: 0, cached: 0, estimate: 0, unknown: 0, lastSource: "" }; } function cleanNum(n) { n = Number(n); return Number.isFinite(n) && n > 0 ? Math.round(n) : 0; } function normalizeDaily(d) { const out = blankDaily(); out.date = d && d.date ? d.date : todayKey(); out.input = cleanNum(d && d.input); out.output = cleanNum(d && d.output); out.cached = cleanNum(d && d.cached); out.estimate = cleanNum(d && (d.estimate || d.estimated)); out.unknown = cleanNum(d && d.unknown); out.total = cleanNum(d && d.total) || out.input + out.output + out.cached + out.estimate + out.unknown; const known = out.input + out.output + out.cached + out.estimate + out.unknown; if (out.total > known) out.unknown += out.total - known; out.lastSource = String((d && (d.lastSource || d.source)) || ""); if (out.unknown && !out.lastSource) out.lastSource = "legacy"; return out; } function saveDaily(data) { try { localStorage.setItem(STORE_KEY, JSON.stringify(normalizeDaily(data))); } catch {} } let daily = loadDaily(); // ============================================================================ // 2. 创建 UI // ============================================================================ const BAR_ID = "td-token-bar"; const STYLE_ID = "td-token-style-v18"; const TOKEN_CAP = 5000000000; if (!document.getElementById(STYLE_ID)) { const st = document.createElement("style"); st.id = STYLE_ID; st.textContent = \` #td-token-bar{ display:inline-block;position:relative; flex:0 0 auto;width:228px;max-width:32vw;height:28px; margin:0 6px;padding:0;border-radius:999px; font:11px system-ui,-apple-system,sans-serif; user-select:none;-webkit-app-region:no-drag; pointer-events:none;opacity:.96;transition:opacity .18s,filter .18s; background:transparent;border:0; vertical-align:middle;box-sizing:border-box; } #td-token-bar:hover{opacity:1;filter:saturate(1.08)} .td-tk-track{ position:absolute;left:0;right:0;top:50%;height:28px; border-radius:8px;transform:translateY(-50%); background:#fff;overflow:hidden; box-shadow:0 1px 0 rgba(255,255,255,.92) inset,0 0 0 1px rgba(15,23,42,.10),0 5px 16px rgba(15,23,42,.20); } .td-tk-segments{ position:absolute;left:18px;right:72px;top:50%; display:grid;grid-template-columns:repeat(8,1fr);gap:3px; height:12px;transform:translateY(-50%); } .td-tk-seg{ position:relative;overflow:hidden;border-radius:3px;transform:skewX(-7deg); background:linear-gradient(180deg,rgba(138,217,255,.22),rgba(23,103,204,.22)); box-shadow:0 0 0 1px rgba(54,157,239,.18) inset; } .td-tk-seg::before{ content:"";position:absolute;left:0;top:0;bottom:0;width:var(--td-tk-seg-fill,0%); border-radius:inherit;background:linear-gradient(180deg,#52c8ff 0%,#2673dd 58%,#1554c5 100%); box-shadow:0 0 9px rgba(42,163,255,.72),0 1px 0 rgba(255,255,255,.45) inset; transition:width .8s cubic-bezier(.25,.8,.25,1); } .td-tk-segments.beat .td-tk-seg::before{animation:tdTkBeat .55s ease} .td-tk-segments::after{ content:"";position:absolute;left:-30px;top:-3px;bottom:-3px;width:24px; pointer-events:none;opacity:0;border-radius:999px; background:linear-gradient(90deg,transparent,rgba(255,255,255,.9),transparent); transform:skewX(-12deg); } .td-tk-segments.beat::after{animation:tdTkSweep .7s ease} .td-tk-segments.beat + .td-tk-badge{animation:tdTkBadgeBeat .62s ease} @keyframes tdTkBeat{ 0%{filter:brightness(1)} 45%{filter:brightness(1.26)} 100%{filter:brightness(1)} } @keyframes tdTkSweep{ 0%{left:-30px;opacity:0} 18%{opacity:.9} 100%{left:calc(100% + 30px);opacity:0} } @keyframes tdTkBadgeBeat{ 0%{transform:translateY(-50%) scale(1)} 45%{transform:translateY(-50%) scale(1.08);filter:brightness(1.18)} 100%{transform:translateY(-50%) scale(1)} } .td-tk-badge{ position:absolute;right:18px;top:50%;z-index:2; display:inline-flex;align-items:center;gap:3px;height:18px;padding:0 7px 0 5px; border-radius:7px;transform:translateY(-50%); color:#fff;font-size:11px;font-weight:800;letter-spacing:0;white-space:nowrap; background:linear-gradient(180deg,#63c8ff 0%,#2c77df 64%,#1f60c9 100%); box-shadow:0 0 12px rgba(53,159,246,.75),0 1px 0 rgba(255,255,255,.45) inset; } .td-tk-check{ display:inline-flex;align-items:center;justify-content:center;width:12px;height:12px; border-radius:50%;background:#fff;color:#266bd5;font-size:10px;line-height:1; } .td-tk-cap{ position:absolute;right:7px;bottom:4px;width:10px;height:12px;border-radius:3px 3px 4px 4px; background:linear-gradient(180deg,#dcffff 0%,#61ddfb 100%); box-shadow:0 0 8px rgba(82,222,255,.75);opacity:.95; } .td-tk-cap::before{ content:"";position:absolute;left:2px;right:2px;top:-3px;height:3px;border-radius:2px 2px 0 0; background:#cfffff; } .td-tk-delta{ position:absolute;right:38px;top:-8px;z-index:3; font-size:10px;font-weight:700;white-space:nowrap;color:#fff; padding:1px 5px;border-radius:999px;background:#2b7ee7; opacity:0;transform:translateY(2px);transition:none; } .td-tk-delta.show{ animation:tdTkDeltaIn 1.6s ease forwards; } @keyframes tdTkDeltaIn{ 0%{opacity:0;transform:translateY(2px)} 20%{opacity:1;transform:translateY(0)} 85%{opacity:1;transform:translateY(0)} 100%{opacity:0;transform:translateY(-4px)} } @media (max-width:640px){#td-token-bar{width:186px;max-width:44vw;margin:0 4px}.td-tk-segments{left:14px;right:58px;gap:2px}.td-tk-badge{right:12px;font-size:10px;padding:0 5px;height:17px}.td-tk-cap{display:none}} @media (prefers-color-scheme:dark){ .td-tk-track{background:#fff;box-shadow:0 1px 0 rgba(255,255,255,.92) inset,0 0 0 1px rgba(255,255,255,.2),0 5px 16px rgba(0,0,0,.36)} } \`; (document.head || document.documentElement).appendChild(st); } function buildBar() { const div = document.createElement("div"); div.id = BAR_ID; div.innerHTML = '
' + '' + '' + '
✓0 token
' + ''; return div; } // ============================================================================ // 3. 格式化 + 颜色 + 渲染 // ============================================================================ function fmtTokens(n) { if (n < 1000) return String(n); if (n < 1000000) return (n / 1000).toFixed(1) + "K"; if (n < 1000000000) return (n / 1000000).toFixed(2) + "M"; return (n / 1000000000).toFixed(2) + "B"; } function fmtBadgeTokens(n) { n = cleanNum(n); if (n < 1000) return n + " token"; if (n < 1000000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k token"; if (n < 1000000000) return (n / 1000000).toFixed(1).replace(/\.0$/, "") + "M token"; return (n / 1000000000).toFixed(1).replace(/\.0$/, "") + "B token"; } function fmtDelta(n) { if (n < 1000) return "+" + n; if (n < 1000000) return "+" + (n / 1000).toFixed(1) + "K"; if (n < 1000000000) return "+" + (n / 1000000).toFixed(2) + "M"; return "+" + (n / 1000000000).toFixed(2) + "B"; } function fillColor(total) { return "linear-gradient(180deg,#52c8ff 0%,#2673dd 58%,#1554c5 100%)"; } function progressPct(total) { total = cleanNum(total); if (!total) return 0; return Math.max(0.8, Math.min(100, (total / TOKEN_CAP) * 100)); } // 5B 上限下,低于几十 MB 的线性变化肉眼几乎不可见;视觉分段采用对数映射,实际百分比仍保留在线程提示中。 function visualProgressPct(total) { total = cleanNum(total); if (!total) return 0; const scale = 100000; const pct = Math.log1p(total / scale) / Math.log1p(TOKEN_CAP / scale) * 100; return Math.max(2.4, Math.min(100, pct)); } function paintSegments(pct) { const segs = Array.from(document.querySelectorAll(".td-tk-seg")); if (!segs.length) return; const span = 100 / segs.length; segs.forEach((seg, i) => { const start = i * span; const fill = Math.max(0, Math.min(100, ((pct - start) / span) * 100)); seg.style.setProperty("--td-tk-seg-fill", fill + "%"); }); } let beatTimer = null; function triggerBeat() { const segments = document.getElementById("td-tk-segments"); if (!segments) return; render(); segments.classList.remove("beat"); void segments.offsetWidth; segments.classList.add("beat"); clearTimeout(beatTimer); beatTimer = setTimeout(() => { segments.classList.remove("beat"); }, 650); } function render() { daily = normalizeDaily(daily); const bar = document.getElementById(BAR_ID); const badge = document.getElementById("td-tk-badge-text"); const pct = visualProgressPct(daily.total); paintSegments(pct); if (badge) badge.textContent = fmtBadgeTokens(daily.total); if (bar) { bar.title = usageTitle(daily); } } function usageTitle(d) { d = normalizeDaily(d); const parts = ["今日 " + fmtTokens(d.total), "额度 5B", "进度 " + progressPct(d.total).toFixed(4).replace(/0+$/,"").replace(/\\.$/,"") + "%"]; if (d.input) parts.push("输入 " + fmtTokens(d.input)); if (d.output) parts.push("输出 " + fmtTokens(d.output)); if (d.cached) parts.push("缓存 " + fmtTokens(d.cached)); if (d.estimate) parts.push("估算 " + fmtTokens(d.estimate)); if (d.unknown) parts.push("未知 " + fmtTokens(d.unknown)); if (d.lastSource) parts.push("来源 " + d.lastSource); return parts.join(" · "); } // ============================================================================ // 4. 动画:主进程通过 Runtime.evaluate 调用此函数触发 // ============================================================================ let lastNetworkAt = 0; let lastPageUsageAt = 0; let estimatedSinceUsage = 0; let recentRealAdds = []; let lastSessionTotal = null; function showDelta(total) { const delta = document.getElementById("td-tk-delta"); if (!delta || total <= 0) return; delta.textContent = fmtDelta(total); delta.classList.remove("show"); void delta.offsetWidth; delta.classList.add("show"); } function normalizeUsagePayload(payload, source) { if (typeof payload === "number" || typeof payload === "string") { return { total: cleanNum(payload), input: 0, output: 0, cached: 0, estimate: 0, source: source || "manual" }; } const u = payload && typeof payload === "object" ? payload : {}; const input = cleanNum(u.input || u.input_tokens || u.prompt_tokens || u.promptTokenCount || u.prompt_token_count || u.inputTokenCount); const output = cleanNum(u.output || u.output_tokens || u.completion_tokens || u.candidatesTokenCount || u.candidates_token_count || u.outputTokenCount); const reasoning = cleanNum(u.reasoning_tokens || u.thoughtsTokenCount || u.thoughts_token_count); const cached = cleanNum(u.cached || u.cached_tokens || u.cache_creation_input_tokens || 0) + cleanNum(u.cache_read_input_tokens || 0) + cleanNum(u.cachedContentTokenCount || 0) + cleanNum(u.cached_content_token_count || 0); const estimate = cleanNum(u.estimate || u.estimated); const unknown = cleanNum(u.unknown); const total = cleanNum(u.total || u.total_tokens || u.totalTokenCount || u.total_token_count || u.token_count) || input + output + reasoning + cached + estimate + unknown; return { total, input, output: output + reasoning, cached, estimate, unknown, source: String(source || u.source || "manual") }; } function animateTokenAdd(payload, source) { const usage = normalizeUsagePayload(payload, source); if (usage.total <= 0) return; const src = usage.source || "manual"; const now = Date.now(); if (src === "network" || src === "page") { recentRealAdds = recentRealAdds.filter((item) => now - item.at < 15000); if (recentRealAdds.some((item) => item.total === usage.total && item.source !== src)) return; recentRealAdds.push({ total: usage.total, source: src, at: now }); } if (src === "network") lastNetworkAt = now; if (src === "page") lastPageUsageAt = now; if (src === "dom" && (now - lastNetworkAt < 12000 || now - lastPageUsageAt < 12000)) return; if (src === "page" && now - lastNetworkAt < 8000) return; if (src === "network" || src === "page") { daily = loadDaily(); const subtract = Math.min(estimatedSinceUsage || daily.estimate, daily.total); if (subtract > 0) { daily.total = Math.max(0, daily.total - subtract); daily.estimate = Math.max(0, daily.estimate - subtract); saveDaily(daily); estimatedSinceUsage = 0; } } daily = loadDaily(); daily.total += usage.total; daily.input += usage.input; daily.output += usage.output; daily.cached += usage.cached; daily.unknown += usage.unknown; if (src === "estimate") { const est = usage.estimate || usage.total; daily.estimate += est; estimatedSinceUsage += est; } daily.lastSource = src; saveDaily(daily); // 本地 session 已接管时,Network 只保留兜底记账,不重复播放动画。 if (lastSessionTotal === null || src === "session") triggerBeat(); const shouldShowDelta = src !== "estimate" && src !== "dom"; if (shouldShowDelta && (lastSessionTotal === null || src === "session")) showDelta(usage.total); } function usageTotal(u) { return u ? cleanNum(u.total) : 0; } function betterUsage(a, b) { return usageTotal(b) > usageTotal(a) ? b : a; } function hasOwn(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); } function looksLikeUsage(usage) { if (!usage || typeof usage !== "object") return false; const keys = [ "input_tokens", "prompt_tokens", "output_tokens", "completion_tokens", "total_tokens", "promptTokenCount", "prompt_token_count", "inputTokenCount", "outputTokenCount", "candidatesTokenCount", "candidates_token_count", "totalTokenCount", "total_token_count", "token_count", "cache_creation_input_tokens", "cache_read_input_tokens", "cachedContentTokenCount", "cached_content_token_count", "reasoning_tokens", "thoughtsTokenCount", "thoughts_token_count" ]; if (keys.some((key) => hasOwn(usage, key))) return true; return hasOwn(usage, "total") && (hasOwn(usage, "input") || hasOwn(usage, "output") || hasOwn(usage, "cached")); } function extractUsageFromRawUsage(usage) { if (!usage || typeof usage !== "object") return null; if (!looksLikeUsage(usage)) return null; const input = cleanNum(usage.input_tokens || usage.prompt_tokens || usage.promptTokenCount || usage.prompt_token_count || usage.inputTokenCount); const output = cleanNum(usage.output_tokens || usage.completion_tokens || usage.candidatesTokenCount || usage.candidates_token_count || usage.outputTokenCount); const reasoning = cleanNum(usage.reasoning_tokens || usage.thoughtsTokenCount || usage.thoughts_token_count); const details = usage.input_tokens_details || usage.prompt_tokens_details || usage.inputTokenDetails || usage.promptTokenDetails || {}; const cached = cleanNum(details.cached_tokens) + cleanNum(usage.cache_creation_input_tokens) + cleanNum(usage.cache_read_input_tokens) + cleanNum(usage.cachedContentTokenCount) + cleanNum(usage.cached_content_token_count); const total = cleanNum(usage.total_tokens || usage.totalTokenCount || usage.total_token_count || usage.token_count || usage.total) || input + output + reasoning + cached; if (!total) return null; return { total, input, output: output + reasoning, cached }; } function extractUsageFromObject(obj) { if (!obj || typeof obj !== "object") return null; const direct = extractUsageFromRawUsage(obj.usage) || extractUsageFromRawUsage(obj.usage_metadata) || extractUsageFromRawUsage(obj.usageMetadata) || extractUsageFromRawUsage(obj); if (direct) return direct; let best = null; for (const val of Object.values(obj)) { if (!val || typeof val !== "object") continue; if (Array.isArray(val)) { for (const item of val) best = betterUsage(best, extractUsageFromObject(item)); } else { best = betterUsage(best, extractUsageFromObject(val)); } } return best; } function extractUsageFromText(text) { let best = null; const take = (obj) => { best = betterUsage(best, extractUsageFromObject(obj)); }; try { take(JSON.parse(text)); } catch {} const lines = String(text || "").split(String.fromCharCode(13)).join("").split(String.fromCharCode(10)); for (const line of lines) { const s = line.trim(); if (!s.startsWith("data:")) continue; const payload = s.slice(5).trim(); if (!payload || payload === "[DONE]") continue; try { take(JSON.parse(payload)); } catch {} } return best; } function installNetworkFallback() { if (window.__tdTokenNetFallbackInstalled === VER) return; window.__tdTokenNetFallbackInstalled = VER; const API_URL_RE = new RegExp("chat/completions|/responses|/messages|conversation|codex", "i"); const TEXT_CT_RE = new RegExp("json|text|event-stream", "i"); const rawFetch = window.fetch; if (typeof rawFetch === "function") { window.fetch = async function(...args) { const res = await rawFetch.apply(this, args); try { const url = String((args[0] && args[0].url) || args[0] || ""); if (API_URL_RE.test(url)) { const ct = (res.headers && res.headers.get && res.headers.get("content-type")) || ""; if (TEXT_CT_RE.test(ct)) { res.clone().text().then((txt) => { const usage = extractUsageFromText(txt); if (usage && usage.total > 0) animateTokenAdd(usage, "page"); }).catch(() => {}); } } } catch {} return res; }; } const XHR = window.XMLHttpRequest; if (XHR && XHR.prototype) { const rawOpen = XHR.prototype.open; const rawSend = XHR.prototype.send; XHR.prototype.open = function(method, url, ...rest) { try { this.__tdTokenUrl = String(url || ""); } catch {} return rawOpen.call(this, method, url, ...rest); }; XHR.prototype.send = function(...args) { try { this.addEventListener("loadend", () => { try { const url = this.__tdTokenUrl || ""; const ct = this.getResponseHeader && (this.getResponseHeader("content-type") || ""); if (!API_URL_RE.test(url)) return; if (!TEXT_CT_RE.test(ct)) return; const txt = typeof this.responseText === "string" ? this.responseText : ""; const usage = extractUsageFromText(txt); if (usage && usage.total > 0) animateTokenAdd(usage, "page"); } catch {} }); } catch {} return rawSend.apply(this, args); }; } } function parseTokenText(text) { const matches = String(text || "").matchAll(new RegExp("([0-9.,]+) *([KkMm]?) *tokens?", "gi")); let best = 0; for (const m of matches) { let num = parseFloat(String(m[1]).replace(/,/g, "")); if (!isFinite(num) || num <= 0) continue; const unit = String(m[2] || "").toLowerCase(); if (unit === "k") num *= 1000; else if (unit === "m") num *= 1000000; best = Math.max(best, Math.round(num)); } return best; } let domTokenBaseline = 0; let domTokenBest = 0; let domScanTimer = null; function scanDomTokenText() { const text = document.body ? document.body.innerText || "" : ""; const best = parseTokenText(text); if (best <= 0) return; if (!domTokenBaseline) { domTokenBaseline = best; domTokenBest = best; return; } if (best > domTokenBest) { const delta = best - domTokenBest; domTokenBest = best; if (delta > 0 && delta < 2000000) animateTokenAdd(delta, "dom"); } } function primeEstimateBaseline() { const raw = document.body ? document.body.innerText || "" : ""; const best = parseTokenText(raw); if (best > 0) { domTokenBaseline = best; domTokenBest = best; } } function scheduleDomScan() { clearTimeout(domScanTimer); domScanTimer = setTimeout(() => { scanDomTokenText(); }, 700); } const EDITOR_SELECTOR = "DIV.ProseMirror, [contenteditable='true'], [contenteditable=''], textarea"; const TOOLBAR_SELECTOR = "[class*='_footer_'], [class*='composer-footer'], footer.composer, [class*='footer'][class*='composer']"; function isVisible(el) { if (!el || !el.isConnected) return false; const rect = el.getBoundingClientRect(); if (!rect.width || !rect.height) return false; const style = window.getComputedStyle(el); return style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || 1) > 0; } function toolbarForEditor(editor) { let parent = editor; for (let i = 0; i < 8 && parent && parent !== document.body && parent !== document.documentElement; i++) { const toolbars = Array.from(parent.querySelectorAll(TOOLBAR_SELECTOR)).filter((toolbar) => ( isVisible(toolbar) && toolbar.querySelectorAll("button, [role='button']").length >= 2 )); if (toolbars.length) { const editorRect = editor.getBoundingClientRect(); toolbars.sort((a, b) => { const aRect = a.getBoundingClientRect(); const bRect = b.getBoundingClientRect(); return Math.abs(aRect.top - editorRect.bottom) - Math.abs(bRect.top - editorRect.bottom); }); return toolbars[0]; } parent = parent.parentElement; } return null; } function toolbarScore(editor, toolbar) { const editorRect = editor.getBoundingClientRect(); const toolbarRect = toolbar.getBoundingClientRect(); const model = findModelControl(toolbar); // 主会话 composer 通常位于可视区更靠下的位置,并且具备模型选择控件。 return (model ? 1000000 : 0) + Math.round(toolbarRect.bottom * 1000) + Math.round(editorRect.bottom); } function findToolbar() { const candidates = []; for (const editor of document.querySelectorAll(EDITOR_SELECTOR)) { if (!isVisible(editor)) continue; const toolbar = toolbarForEditor(editor); if (toolbar) candidates.push({ editor, toolbar }); } if (!candidates.length) return null; candidates.sort((a, b) => toolbarScore(b.editor, b.toolbar) - toolbarScore(a.editor, a.toolbar)); return candidates[0].toolbar; } function visibleText(el) { return String((el && (el.getAttribute("aria-label") || el.title || el.textContent)) || "").trim(); } function findModelControl(toolbar) { const controls = Array.from(toolbar.querySelectorAll("button, [role='button'], [aria-haspopup], [data-testid*='model'], [class*='model']")); const modelLike = controls.find((el) => /model|模型|gpt|claude|codex|o[0-9]|gemini|deepseek|glm|kimi/i.test(visibleText(el))); if (modelLike) return modelLike; const buttons = Array.from(toolbar.querySelectorAll("button, [role='button']")); return buttons.length > 1 ? buttons[buttons.length - 2] : buttons[0] || null; } function placeInToolbar(bar, toolbar) { toolbar = toolbar || findToolbar(); if (!toolbar) return false; const model = findModelControl(toolbar); if (model && model.parentElement && toolbar.contains(model.parentElement)) { model.parentElement.insertBefore(bar, model); } else if (model && toolbar.contains(model)) { toolbar.insertBefore(bar, model); } else { toolbar.appendChild(bar); } return true; } function mountBar() { const toolbar = findToolbar(); if (!toolbar) return false; const bar = document.getElementById(BAR_ID) || buildBar(); placeInToolbar(bar, toolbar); render(); return true; } function maybeRelocate() { const bar = document.getElementById(BAR_ID); const toolbar = findToolbar(); if (!toolbar) return false; if (!bar) return mountBar(); if (!toolbar.contains(bar)) return placeInToolbar(bar, toolbar); const model = findModelControl(toolbar); const expectedParent = model && model.parentElement && toolbar.contains(model.parentElement) ? model.parentElement : toolbar; if (bar.parentElement !== expectedParent || (model && bar.nextElementSibling !== model)) { return placeInToolbar(bar, toolbar); } return true; } // ============================================================================ // 5. MutationObserver:页面重绘后补回悬浮条 // ============================================================================ function tryMount() { maybeRelocate(); } let mo = null; let relocateTimer = null; function scheduleRelocate() { if (relocateTimer) return; relocateTimer = setTimeout(() => { relocateTimer = null; tryMount(); }, 40); } function startObserver() { if (mo) return; installNetworkFallback(); mo = new MutationObserver((mutations) => { scheduleRelocate(); scheduleDomScan(); }); window.__tdTokenObserver = mo; const root = document.body || document.documentElement; if (root) mo.observe(root, { childList: true, subtree: true }); tryMount(); primeEstimateBaseline(); scheduleDomScan(); } function ensureStarted() { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", startObserver, { once: true }); } else { startObserver(); } } ensureStarted(); // ============================================================================ // 7. 对外 API(主进程通过 Runtime.evaluate 调用) // ============================================================================ window.__tdTokenAdd = (usage, source) => { try { const u = normalizeUsagePayload(usage, source || "external"); if (!u.total) return "bad"; animateTokenAdd(u, u.source); return "ok"; } catch { return "err"; } }; window.__tdTokenGet = () => { const d = loadDaily(); return JSON.stringify(d); }; window.__tdTokenSet = (usage) => { try { const next = normalizeUsagePayload(usage, "session"); const previousSessionTotal = lastSessionTotal; lastSessionTotal = next.total; daily = normalizeDaily({ ...next, date: todayKey(), lastSource: "session" }); saveDaily(daily); render(); const delta = previousSessionTotal === null ? 0 : next.total - previousSessionTotal; if (delta > 0) { triggerBeat(); showDelta(delta); } return "ok"; } catch { return "err"; } }; window.__tdTokenReset = () => { daily = blankDaily(); estimatedSinceUsage = 0; recentRealAdds = []; lastSessionTotal = 0; saveDaily(daily); render(); return "ok"; }; })(); `; module.exports = { TOKEN_INJECT_SCRIPT };