// ==UserScript==
// @name Codex Quota Compas`预先格式化的文本`s (Accurate Usage Edition)
// @namespace http://tampermonkey.net/
// @version 2.1.0
// @description Codex 用量分析:实时百分比 + 同日 Credits/Percent 反推额度,避免统计延迟导致的错误周额度
// @author Jun Zhao + ChatGPT
// @match https://chatgpt.com/codex/cloud/settings/analytics*
// @match https://chatgpt.com/codex/*/settings/analytics*
// @match https://chatgpt.com/codex/*/settings/usage*
// @grant GM_addStyle
// ==/UserScript==
(function () {
"use strict";
if (window.__codexQuotaCompassV21) return;
window.__codexQuotaCompassV21 = true;
const CONFIG = {
USD_PER_CREDIT: 0.04,
HISTORY_DAYS: 45,
RATIO_TOLERANCE: 0.03, // 与最新样本相差 3% 内视为同一额度档位
MIN_LONG_WINDOW_SECONDS: 2 * 86400,
};
const DAY_MS = 86400000;
const n = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
const esc = (v) =>
String(v ?? "").replace(
/[&<>"']/g,
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c],
);
const dayKey = (ms) => new Date(ms).toISOString().slice(0, 10);
const addDays = (key, days) => dayKey(Date.parse(key + "T00:00:00Z") + days * DAY_MS);
const fmtNum = (value) => {
const x = n(value);
if (x >= 1e9) return (x / 1e9).toFixed(2) + "B";
if (x >= 1e6) return (x / 1e6).toFixed(2) + "M";
if (x >= 1e3) return (x / 1e3).toFixed(2) + "K";
return x.toLocaleString("zh-CN", { maximumFractionDigits: 2 });
};
const fmtCredits = (value, digits = 1) =>
Number.isFinite(Number(value))
? Number(value).toLocaleString("en-US", {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
})
: "—";
const fmtPct = (value, digits = 1) =>
Number.isFinite(Number(value)) ? `${Number(value).toFixed(digits)}%` : "—";
const fmtUSD = (credits) =>
Number.isFinite(Number(credits)) ? `$ ${(Number(credits) * CONFIG.USD_PER_CREDIT).toFixed(2)}` : "—";
const fmtTime = (epochMs) => {
if (!Number.isFinite(Number(epochMs))) return "—";
return new Date(Number(epochMs)).toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
};
const tokenTotal = (obj = {}) =>
n(obj.text_total_tokens) ||
n(obj.cached_text_input_tokens) + n(obj.uncached_text_input_tokens) + n(obj.text_output_tokens);
const median = (values) => {
const arr = values.filter(Number.isFinite).sort((a, b) => a - b);
if (!arr.length) return null;
const mid = Math.floor(arr.length / 2);
return arr.length % 2 ? arr[mid] : (arr[mid - 1] + arr[mid]) / 2;
};
GM_addStyle(`
#codex-compass-root {
position: fixed; top: 4%; left: 50%; transform: translateX(-50%);
width: min(1040px, calc(100vw - 28px)); max-height: 92vh;
background: #fff; border-radius: 14px; box-shadow: 0 12px 55px rgba(0,0,0,.32);
z-index: 2147483646; padding: 22px; display: none; flex-direction: column;
border: 1px solid #e5e5e5; color: #2f2f2f; box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}
#codex-compass-root * { box-sizing: border-box; }
.cc-header { display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:14px; }
.cc-title { font-size:19px; font-weight:700; }
.cc-subtitle { color:#777; font-size:12px; margin-top:3px; }
.cc-close { cursor:pointer; font-size:28px; color:#999; line-height:1; border:0; background:transparent; }
.cc-scroll { overflow:auto; padding-right:2px; }
.cc-grid { display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:10px; margin-bottom:14px; }
.cc-card { background:#fafafa; border:1px solid #ececec; border-radius:10px; padding:12px; min-height:78px; }
.cc-card.good { background:#eefaf5; border-color:#ccebdd; }
.cc-card.warn { background:#fff8e8; border-color:#f3dfab; }
.cc-label { font-size:12px; color:#6b6b6b; margin-bottom:5px; }
.cc-value { font-size:20px; line-height:1.2; font-weight:750; color:#0b8f70; white-space:nowrap; }
.cc-value.small { font-size:17px; }
.cc-note { margin-top:5px; color:#777; font-size:11px; line-height:1.35; }
.cc-section-title { font-size:13px; font-weight:700; margin:15px 0 7px; }
.cc-banner { border-radius:9px; padding:9px 11px; margin:8px 0; font-size:12px; line-height:1.55; }
.cc-banner.info { background:#f4f7fb; border:1px solid #dce5ef; color:#435160; }
.cc-banner.warn { background:#fff8e8; border:1px solid #f0dca5; color:#795d18; }
.cc-banner.ok { background:#effaf5; border:1px solid #ccebdd; color:#246a56; }
.cc-windows { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:8px; }
.cc-window { border:1px solid #ececec; border-radius:9px; padding:9px 10px; background:#fcfcfc; }
.cc-window.target { border-color:#a9dfcf; background:#f3fbf8; }
.cc-window-head { display:flex; justify-content:space-between; gap:8px; font-size:12px; font-weight:700; }
.cc-window-meta { color:#777; font-size:11px; margin-top:5px; line-height:1.5; }
.cc-table-wrap { max-height:285px; overflow:auto; border:1px solid #ececec; border-radius:8px; }
.cc-table { width:100%; border-collapse:collapse; font-size:11.5px; }
.cc-table thead { position:sticky; top:0; z-index:2; background:#f5f5f5; }
.cc-table th { text-align:left; color:#666; padding:8px 7px; border-bottom:1px solid #e5e5e5; white-space:nowrap; }
.cc-table td { padding:7px; border-bottom:1px solid #f0f0f0; white-space:nowrap; }
.cc-table tr.latest { background:#f6fbf9; }
.cc-muted { color:#999; }
.cc-green { color:#0b8f70; font-weight:700; }
.cc-amber { color:#a56c00; font-weight:700; }
.cc-footer { margin-top:10px; color:#777; font-size:11px; line-height:1.55; }
#codex-compass-btn {
position:fixed; bottom:20px; right:20px; z-index:2147483645;
padding:10px 16px; background:#10a37f; color:#fff; border:0; border-radius:9px;
cursor:pointer; font-weight:700; box-shadow:0 3px 14px rgba(0,0,0,.16);
}
#codex-compass-btn:disabled { opacity:.65; cursor:wait; }
@media (max-width:760px) {
.cc-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
#codex-compass-root { padding:15px; top:2%; max-height:96vh; }
}
`);
async function getToken() {
const boot = document.getElementById("client-bootstrap")?.textContent || "";
const jwt = boot.match(/eyJ[\w-]*\.[\w-]+\.[\w-]+/g);
if (jwt?.[0]) return jwt[0];
try {
const res = await fetch("/api/auth/session", {
credentials: "include",
cache: "no-store",
});
if (!res.ok) return null;
const data = await res.json();
return data?.accessToken || data?.access_token || null;
} catch (_) {
return null;
}
}
async function apiGet(path, token) {
const res = await fetch(path, {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
credentials: "include",
cache: "no-store",
});
const text = await res.text();
if (!res.ok) throw new Error(`${path.split("?")[0]} → HTTP ${res.status}: ${text.slice(0, 160)}`);
try {
return JSON.parse(text);
} catch (_) {
throw new Error(`${path.split("?")[0]} 返回了非 JSON 数据`);
}
}
const softGet = (path, token) => apiGet(path, token).catch((e) => {
console.warn("[Codex Quota Compass] optional endpoint failed:", e.message);
return null;
});
function classifyWindow(seconds) {
const s = n(seconds);
if (Math.abs(s - 5 * 3600) <= 1800) return "约 5 小时";
if (Math.abs(s - 24 * 3600) <= 3600) return "约 1 天";
if (Math.abs(s - 7 * 86400) <= 6 * 3600) return "约 7 天";
if (s >= 27 * 86400 && s <= 32 * 86400) return "约 1 个月";
if (s >= 86400) return `${(s / 86400).toFixed(1)} 天`;
return `${(s / 3600).toFixed(1)} 小时`;
}
function parseWindows(usage) {
const rate = usage?.rate_limit || {};
const rows = [];
for (const key of ["primary_window", "secondary_window"]) {
const w = rate?.[key];
if (!w) continue;
const sec = n(w.limit_window_seconds);
const resetAtMs = n(w.reset_at) * 1000;
if (!(sec > 0) || !(resetAtMs > 0)) continue;
rows.push({
key,
role: key === "primary_window" ? "Primary" : "Secondary",
usedPercent: clamp(n(w.used_percent), 0, 100),
windowSec: sec,
resetAtMs,
startAtMs: resetAtMs - sec * 1000,
label: classifyWindow(sec),
});
}
return rows;
}
// 新版后端有时把主要额度放 primary,有时旧账号仍是 secondary。
// 不再写死字段名:优先选 >=2 天的 primary;否则选最长的 >=2 天窗口;再否则选最长窗口。
function chooseAllowanceWindow(windows) {
if (!windows.length) return null;
const primaryLong = windows.find(
(w) => w.key === "primary_window" && w.windowSec >= CONFIG.MIN_LONG_WINDOW_SECONDS,
);
if (primaryLong) return primaryLong;
const long = windows
.filter((w) => w.windowSec >= CONFIG.MIN_LONG_WINDOW_SECONDS)
.sort((a, b) => b.windowSec - a.windowSec);
if (long[0]) return long[0];
return [...windows].sort((a, b) => b.windowSec - a.windowSec)[0];
}
function dailyPercent(row = {}) {
const surfaces = row.product_surface_usage_values;
if (surfaces && typeof surfaces === "object") {
const sum = Object.values(surfaces).reduce((a, v) => a + Math.max(0, n(v)), 0);
if (sum > 0) return sum;
}
for (const key of ["used_percent", "usage_percent", "percent", "total_percent"]) {
if (n(row[key]) > 0) return n(row[key]);
}
if (Array.isArray(row.models)) {
const sum = row.models.reduce(
(a, m) => a + Math.max(0, n(m.used_percent || m.usage_percent || m.percent)),
0,
);
if (sum > 0) return sum;
}
return 0;
}
function normalizeCounts(data) {
return Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : [];
}
function normalizeRows(data) {
return Array.isArray(data?.data) ? data.data : Array.isArray(data) ? data : [];
}
function mergeDaily(countsData, pctData) {
const counts = normalizeCounts(countsData);
const pctRows = normalizeRows(pctData);
const pctMap = new Map(pctRows.map((r) => [r.date, r]));
return counts
.map((row) => {
const totals = row.totals || {};
const credits = n(totals.credits);
const pct = dailyPercent(pctMap.get(row.date) || {});
const impliedAllowance = credits > 0 && pct > 0 ? (credits / pct) * 100 : null;
return {
date: row.date,
credits,
pct,
impliedAllowance,
tokens: tokenTotal(totals),
turns: n(totals.turns),
threads: n(totals.threads),
};
})
.filter((r) => r.date)
.sort((a, b) => a.date.localeCompare(b.date));
}
// 核心修复:绝不再用“延迟的周期 Credits ÷ 实时 used_percent”。
// 改为“同一天的 Credits ÷ 同一天的 allowance percent”,两边来自同一日桶。
function estimateAllowance(dailyRows, targetWindow) {
const usable = dailyRows.filter((d) => d.credits > 0 && d.pct > 0 && d.impliedAllowance > 0);
if (!usable.length) return null;
const cycleStartKey = targetWindow ? dayKey(targetWindow.startAtMs) : null;
const inWindow = cycleStartKey ? usable.filter((d) => d.date >= cycleStartKey) : [];
const pool = inWindow.length ? inWindow : usable;
const desc = [...pool].sort((a, b) => b.date.localeCompare(a.date));
const newest = desc[0].impliedAllowance;
const agreeing = desc.filter(
(d) => Math.abs(d.impliedAllowance - newest) / newest <= CONFIG.RATIO_TOLERANCE,
);
const value = median(agreeing.map((d) => d.impliedAllowance));
if (!(value > 0)) return null;
const deviations = agreeing.map((d) => Math.abs(d.impliedAllowance - value) / value);
const maxDeviation = deviations.length ? Math.max(...deviations) : 0;
const confidence = agreeing.length >= 3 && maxDeviation <= 0.02 ? "高" : agreeing.length >= 2 ? "中" : "低";
return {
credits: value,
samples: agreeing.length,
dropped: pool.length - agreeing.length,
newestDate: desc[0].date,
confidence,
source: "daily-ratio",
};
}
function stats(rows) {
return rows.reduce(
(a, r) => {
a.credits += r.credits;
a.tokens += r.tokens;
a.turns += r.turns;
a.pct += r.pct;
return a;
},
{ credits: 0, tokens: 0, turns: 0, pct: 0 },
);
}
function renderWindows(windows, target) {
if (!windows.length) return `<div class="cc-banner warn">usage 接口没有返回可识别的限流窗口。</div>`;
return `<div class="cc-windows">${windows
.map(
(w) => `
<div class="cc-window ${target?.key === w.key ? "target" : ""}">
<div class="cc-window-head">
<span>${esc(w.role)} · ${esc(w.label)}</span>
<span class="${target?.key === w.key ? "cc-green" : ""}">${fmtPct(w.usedPercent, 0)}</span>
</div>
<div class="cc-window-meta">
${target?.key === w.key ? "✓ 当前作为主额度窗口<br>" : ""}
开始:${esc(fmtTime(w.startAtMs))}<br>
重置:${esc(fmtTime(w.resetAtMs))}<br>
原始窗口:${Math.round(w.windowSec).toLocaleString("en-US")} 秒
</div>
</div>`,
)
.join("")}</div>`;
}
function renderDailyTable(rows, estimate) {
if (!rows.length) return `<div class="cc-banner warn">daily-workspace-usage-counts 没有返回日用量。</div>`;
const latest = rows[rows.length - 1]?.date;
return `
<div class="cc-table-wrap">
<table class="cc-table">
<thead>
<tr>
<th>日期 (UTC)</th>
<th>Credits</th>
<th>当日额度%</th>
<th>当日反推总额</th>
<th>Tokens</th>
<th>轮数</th>
</tr>
</thead>
<tbody>
${[...rows]
.reverse()
.map((r) => {
const agrees =
estimate?.credits && r.impliedAllowance
? Math.abs(r.impliedAllowance - estimate.credits) / estimate.credits <= CONFIG.RATIO_TOLERANCE
: false;
return `
<tr class="${r.date === latest ? "latest" : ""}">
<td>${esc(r.date)}${r.date === latest ? " <span class='cc-muted'>最新</span>" : ""}</td>
<td>${fmtCredits(r.credits, 3)}</td>
<td>${r.pct > 0 ? fmtPct(r.pct, 3) : "<span class='cc-muted'>—</span>"}</td>
<td class="${agrees ? "cc-green" : r.impliedAllowance ? "cc-amber" : "cc-muted"}">
${r.impliedAllowance ? fmtCredits(r.impliedAllowance, 1) : "—"}
</td>
<td>${fmtNum(r.tokens)}</td>
<td>${fmtCredits(r.turns, 0)}</td>
</tr>`;
})
.join("")}
</tbody>
</table>
</div>`;
}
function showPanel(data) {
const root = document.getElementById("codex-compass-root");
const {
usage,
windows,
targetWindow,
dailyRows,
allowance,
countsOk,
percentsOk,
queryStart,
queryEnd,
} = data;
const usedPct = targetWindow?.usedPercent ?? null;
const remainingPct = usedPct == null ? null : Math.max(0, 100 - usedPct);
const liveSpent = allowance && usedPct != null ? allowance.credits * (usedPct / 100) : null;
const liveRemaining = allowance && usedPct != null ? allowance.credits * (remainingPct / 100) : null;
const cycleStartKey = targetWindow ? dayKey(targetWindow.startAtMs) : null;
const cycleRows = cycleStartKey ? dailyRows.filter((d) => d.date >= cycleStartKey) : [];
const cycleStats = stats(cycleRows);
const allStats = stats(dailyRows);
const latestDailyDate = dailyRows[dailyRows.length - 1]?.date || null;
const today = dayKey(Date.now());
const lagDays = latestDailyDate ? Math.max(0, Math.round((Date.parse(today) - Date.parse(latestDailyDate)) / DAY_MS)) : null;
const mismatch = liveSpent && cycleStats.credits > 0 ? Math.abs(liveSpent - cycleStats.credits) / liveSpent : null;
let statusBanner = "";
if (!percentsOk) {
statusBanner = `<div class="cc-banner warn"><b>缺少 daily-token-usage-breakdown。</b> 当前只能准确显示实时百分比和已入账 Credits,不能可靠反推绝对额度。</div>`;
} else if (!allowance) {
statusBanner = `<div class="cc-banner warn"><b>暂时没有可配对样本。</b> 等 daily Credits 与 daily allowance% 至少有一天同时入账后,再计算绝对额度;实时 used% 仍然有效。</div>`;
} else if (lagDays > 0 || mismatch > 0.12) {
statusBanner = `<div class="cc-banner warn"><b>检测到统计延迟/口径差。</b> 日台账最新到 ${esc(latestDailyDate || "—")},因此不要再用“已入账 Credits ÷ 当前实时 used%”。上方“实时已用折算”使用同日比例测出的额度基准,不受这个错位公式直接影响。</div>`;
} else {
statusBanner = `<div class="cc-banner ok"><b>数据口径基本一致。</b> 实时百分比来自 usage;绝对额度来自同一天 Credits 与同一天 allowance% 的比值。</div>`;
}
const allowanceNote = allowance
? `${allowance.samples} 个同档样本 · 置信度 ${allowance.confidence}${allowance.dropped ? ` · 排除 ${allowance.dropped} 个异档样本` : ""}`
: "等待同日 Credits + allowance% 样本";
root.innerHTML = `
<div class="cc-header">
<div>
<div class="cc-title">📊 Codex 配额深度分析 V2.1</div>
<div class="cc-subtitle">修复:实时 used% 与延迟 Credits 不再直接相除;按真实窗口长度识别主额度</div>
</div>
<button class="cc-close" id="cc-close" aria-label="关闭">×</button>
</div>
<div class="cc-scroll">
<div class="cc-grid">
<div class="cc-card good">
<div class="cc-label">实时已用(官方)</div>
<div class="cc-value">${usedPct == null ? "—" : fmtPct(usedPct, 0)}</div>
<div class="cc-note">来自 /wham/usage · ${esc(targetWindow?.label || "未知窗口")}</div>
</div>
<div class="cc-card good">
<div class="cc-label">实时剩余(官方)</div>
<div class="cc-value">${remainingPct == null ? "—" : fmtPct(remainingPct, 0)}</div>
<div class="cc-note">百分比是当前最可信的实时用量</div>
</div>
<div class="cc-card ${allowance ? "good" : "warn"}">
<div class="cc-label">额度基准(同日实测反推)</div>
<div class="cc-value small">${allowance ? fmtCredits(allowance.credits, 1) + " Credits" : "—"}</div>
<div class="cc-note">${esc(allowanceNote)}</div>
</div>
<div class="cc-card">
<div class="cc-label">实时已用折算</div>
<div class="cc-value small">${liveSpent == null ? "—" : fmtCredits(liveSpent, 1) + " Credits"}</div>
<div class="cc-note">${liveSpent == null ? "需先测出额度基准" : `${fmtUSD(liveSpent)} · 额度基准 × 实时 used%`}</div>
</div>
<div class="cc-card">
<div class="cc-label">实时剩余折算</div>
<div class="cc-value small">${liveRemaining == null ? "—" : fmtCredits(liveRemaining, 1) + " Credits"}</div>
<div class="cc-note">${liveRemaining == null ? "需先测出额度基准" : `${fmtUSD(liveRemaining)} · 估算值,不是余额字段`}</div>
</div>
<div class="cc-card">
<div class="cc-label">本窗口已入账台账</div>
<div class="cc-value small">${fmtCredits(cycleStats.credits, 1)} Credits</div>
<div class="cc-note">最新日桶 ${esc(latestDailyDate || "—")} · 可能延迟,且窗口边界日是整日桶</div>
</div>
</div>
${statusBanner}
<div class="cc-section-title">⏱️ 官方实时限流窗口</div>
${renderWindows(windows, targetWindow)}
<div class="cc-section-title">📅 每日校准表:真正用于反推额度的数据</div>
<div class="cc-banner info">
公式:<b>当天 Credits ÷ 当天 allowance% × 100</b>。同一天的两个日桶配对,避免把“昨天的 Credits”除以“现在的 used%”。<br>
查询范围:${esc(queryStart)} ~ ${esc(queryEnd)}(UTC,end_date 取次日以覆盖当天);workspace 用量强制使用 <code>workspace_user=true</code>。
</div>
${renderDailyTable(dailyRows, allowance)}
<div class="cc-section-title">🧾 查询诊断</div>
<div class="cc-banner info">
Plan: <b>${esc(usage?.plan_type || "unknown")}</b> ·
日 Credits 接口: <b>${countsOk ? "OK" : "失败"}</b> ·
日百分比接口: <b>${percentsOk ? "OK" : "失败"}</b> ·
${latestDailyDate ? `日台账最新:<b>${esc(latestDailyDate)}</b>` : "没有日台账"}<br>
近 ${CONFIG.HISTORY_DAYS} 天已入账:${fmtCredits(allStats.credits, 3)} Credits / ${fmtNum(allStats.tokens)} Tokens / ${fmtCredits(allStats.turns, 0)} 轮。
</div>
<div class="cc-footer">
<b>解释:</b>“实时已用/剩余 %”是服务器实时限流数据;“已入账 Credits”是 Analytics 日桶,可能延迟;“额度基准/实时 Credits 折算”属于推算值。<br>
1 Credit = $${CONFIG.USD_PER_CREDIT.toFixed(2)} 仅用于价值换算,不影响额度百分比。若 OpenAI 改接口字段,面板会保留实时窗口并把不可用项显示为“—”。
</div>
</div>`;
root.style.display = "flex";
document.getElementById("cc-close").onclick = () => (root.style.display = "none");
}
async function run() {
const btn = document.getElementById("codex-compass-btn");
btn.disabled = true;
btn.textContent = "分析中…";
try {
const token = await getToken();
if (!token) throw new Error("令牌获取失败,请确认已登录 ChatGPT 后刷新页面。");
const usage = await apiGet("/backend-api/wham/usage", token);
const windows = parseWindows(usage);
const targetWindow = chooseAllowanceWindow(windows);
const today = dayKey(Date.now());
const queryStart = addDays(today, -CONFIG.HISTORY_DAYS);
const queryEnd = addDays(today, 1); // 后端日期范围通常按日桶,取次日保证覆盖今天
const range = `start_date=${encodeURIComponent(queryStart)}&end_date=${encodeURIComponent(queryEnd)}&group_by=day`;
// 关键:workspace_user=true,避免 workspace 总量和个人 used_percent 混在一起。
const [countsData, pctData] = await Promise.all([
softGet(`/backend-api/wham/analytics/daily-workspace-usage-counts?${range}&workspace_user=true`, token),
softGet(`/backend-api/wham/usage/daily-token-usage-breakdown?${range}`, token),
]);
const dailyRows = mergeDaily(countsData || { data: [] }, pctData || { data: [] });
const allowance = estimateAllowance(dailyRows, targetWindow);
showPanel({
usage,
windows,
targetWindow,
dailyRows,
allowance,
countsOk: !!countsData,
percentsOk: !!pctData,
queryStart,
queryEnd,
});
} catch (e) {
console.error("[Codex Quota Compass]", e);
alert("Codex Quota Compass 错误:\n" + (e?.message || String(e)));
} finally {
btn.disabled = false;
btn.textContent = "📊 运行用量分析 V2.1";
}
}
function mount() {
let btn = document.getElementById("codex-compass-btn");
if (!btn) {
btn = document.createElement("button");
btn.id = "codex-compass-btn";
btn.textContent = "📊 运行用量分析 V2.1";
btn.onclick = run;
document.body.appendChild(btn);
}
let root = document.getElementById("codex-compass-root");
if (!root) {
root = document.createElement("div");
root.id = "codex-compass-root";
document.body.appendChild(root);
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", mount, { once: true });
} else {
mount();
}
})();
改了一下v1.8 版本的,感觉原来哪个不太准,我一个plus号测出来30^-^,改完后测130^-^左右