烧饼论坛 LZ.SB
登录

Codex Quota Compass

626
  • 咪喵
    UID 7627Lv.1 初来乍到

    claude fable 5.1 写的 codex 额度查询 感觉还挺准的
    // ==UserScript==
    // @name Codex Quota Compass (Snapshot Delta)
    // @namespace https://chatgpt.com/codex/
    // @version 3.0.0
    // @description 读取 Codex 用量接口,用多次快照的「消耗增量 ÷ 百分比增量」推算周额度,自动识别重置并给出误差区间。
    // @author Claude Fable 5.1 Medium
    // @match https://chatgpt.com/codex/cloud/settings/analytics*
    // @icon https://chatgpt.com/favicon.ico
    // @run-at document-idle
    // @grant GM_addStyle
    // ==/UserScript==

    (() => {
    'use strict';

    const APP = {
    buttonId: 'cqc-api-launcher',
    panelId: 'cqc-api-panel',
    storeKey: 'cqc_snapshots_v3',
    usdPerCredit: 0.04,
    historyDays: 30,
    autoIntervalMs: 10 * 60 * 1000,
    maxSnapshotsPerSegment: 600,
    maxSegments: 30,
    // 百分比跨度低于这个值时,取整误差会把结果放大得太厉害,标记为 “不可靠”
    reliableDeltaPct: 10,
    };

    let latestPayload = null;
    let autoTimer = null;

    GM_addStyle(#${APP.buttonId} { position: fixed; right: 20px; bottom: 20px; z-index: 2147483645; min-height: 42px; padding: 0 17px; border: 0; border-radius: 12px; background: #10a37f; color: #fff; cursor: pointer; box-shadow: 0 10px 32px rgba(0,0,0,.22); font: 650 13px/1 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif; } #${APP.buttonId}:hover { background:#0d8f70; transform:translateY(-1px); } #${APP.buttonId}:disabled { opacity:.65; cursor:wait; transform:none; } #${APP.panelId} { position: fixed; inset: 4vh auto auto 50%; transform: translateX(-50%); z-index: 2147483646; display: none; flex-direction: column; width: min(900px, calc(100vw - 28px)); max-height: 92vh; overflow: hidden; color:#202123; background:#fff; border:1px solid #e5e7eb; border-radius:16px; box-shadow:0 24px 90px rgba(0,0,0,.28); font: 13px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif; } #${APP.panelId} * { box-sizing:border-box; } #${APP.panelId} button { font:inherit; } .cqc-head { display:flex; align-items:center; gap:12px; padding:16px 18px; border-bottom:1px solid #eceef1; } .cqc-brand { min-width:0; flex:1; } .cqc-title { font-size:17px; font-weight:750; } .cqc-subtitle { margin-top:2px; color:#6b7280; font-size:11px; } .cqc-head-actions { display:flex; gap:7px; flex-wrap:wrap; justify-content:flex-end; } .cqc-action { padding:7px 11px; border:1px solid #dfe2e7; border-radius:9px; background:#fff; color:#374151; cursor:pointer; } .cqc-action:hover { background:#f7f7f8; } .cqc-action.danger { color:#b42318; } .cqc-close { width:32px; height:32px; padding:0; font-size:22px; line-height:1; } .cqc-body { padding:16px 18px 20px; overflow:auto; } .cqc-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:10px; } .cqc-card { min-width:0; padding:12px; border:1px solid #e7e9ed; border-radius:12px; background:#fafafa; } .cqc-card.accent { background:#effaf6; border-color:#ccecdf; } .cqc-card.warn { background:#fff9df; border-color:#ffe08a; } .cqc-label { color:#6b7280; font-size:11px; } .cqc-value { margin-top:4px; color:#111827; font-size:19px; font-weight:750; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .cqc-card.accent .cqc-value { color:#0b8b69; } .cqc-card.warn .cqc-value { color:#9a6700; } .cqc-note { margin-top:3px; color:#8b93a1; font-size:10px; white-space:normal; } .cqc-progress { height:6px; margin-top:9px; overflow:hidden; border-radius:999px; background:#e6e8eb; } .cqc-progress i { display:block; height:100%; width:var(--width); border-radius:inherit; background:var(--color,#10a37f); } .cqc-notice { margin:12px 0 0; padding:9px 11px; border:1px solid #ffe08a; border-radius:10px; background:#fff9df; color:#9a6700; font-size:11px; line-height:1.5; } .cqc-notice.ok { border-color:#ccecdf; background:#effaf6; color:#0b6b52; } .cqc-section { margin-top:16px; } .cqc-section-title { display:flex; justify-content:space-between; gap:10px; margin:0 0 8px; font-weight:700; } .cqc-section-title small { color:#8b93a1; font-weight:400; } .cqc-table-wrap { max-height:260px; overflow:auto; border:1px solid #e7e9ed; border-radius:10px; } .cqc-table { width:100%; border-collapse:collapse; font-size:12px; } .cqc-table th { position:sticky; top:0; z-index:1; padding:8px 10px; color:#6b7280; background:#f7f7f8; border-bottom:1px solid #e7e9ed; text-align:left; font-weight:650; } .cqc-table td { padding:8px 10px; border-bottom:1px solid #f0f1f3; } .cqc-table tbody tr:hover { background:#fafafa; } .cqc-table tfoot td { position:sticky; bottom:0; background:#f7f7f8; border-top:2px solid #dfe2e7; border-bottom:0; font-weight:750; } .cqc-num { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-variant-numeric:tabular-nums; } .cqc-tag { display:inline-block; padding:1px 6px; border-radius:6px; font-size:10px; background:#e6e8eb; color:#374151; } .cqc-tag.ok { background:#d1fae5; color:#065f46; } .cqc-tag.warn { background:#fef3c7; color:#92400e; } .cqc-empty { padding:36px 20px; text-align:center; color:#6b7280; } .cqc-empty strong { display:block; margin-bottom:7px; color:#202123; font-size:15px; } .cqc-error { color:#b42318; white-space:pre-wrap; word-break:break-word; } .cqc-foot { padding:9px 18px; border-top:1px solid #eceef1; color:#8b93a1; background:#fafafa; font-size:10px; } @media (max-width:760px) { .cqc-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } #${APP.panelId} { inset:10px 10px auto; width:auto; max-height:calc(100vh - 20px); transform:none; } } @media (prefers-color-scheme:dark) { #${APP.panelId} { color:#ececf1; background:#202123; border-color:#3b3d43; } .cqc-head,.cqc-foot { border-color:#35373d; } .cqc-foot,.cqc-table th,.cqc-table tfoot td { background:#27282d; } .cqc-card { background:#27282d; border-color:#3b3d43; } .cqc-card.accent { background:#153b32; border-color:#236c58; } .cqc-card.warn { background:#403817; border-color:#6d5b1e; } .cqc-value { color:#f7f7f8; } .cqc-action { color:#e7e7eb; background:#2b2c31; border-color:#44464d; } .cqc-action:hover,.cqc-table tbody tr:hover { background:#303137; } .cqc-table-wrap,.cqc-table th,.cqc-table td { border-color:#3b3d43; } .cqc-progress { background:#41434a; } .cqc-notice { color:#ffd978; background:#403817; border-color:#6d5b1e; } .cqc-notice.ok { color:#8fe3c5; background:#153b32; border-color:#236c58; } .cqc-tag { background:#3b3d43; color:#e7e7eb; } .cqc-empty strong { color:#f7f7f8; } });

    // ---------- 基础工具 ----------

    const number = (value) => (Number.isFinite(Number(value)) ? Number(value) : 0);

    function escapeHtml(value) {
    return String(value ??'')
    .replaceAll('&', '&')
    .replaceAll('<', '<')
    .replaceAll('>','>')
    .replaceAll('"','"')
    .replaceAll("'",''');
    }

    function tokenTotal(totals = {}) {
    return number(totals.text_total_tokens)
    || number(totals.cached_text_input_tokens)
    + number(totals.uncached_text_input_tokens)
    + number(totals.text_output_tokens);
    }

    function formatNumber(value, digits = 2) {
    const amount = number(value);
    if (amount>= 1_000_000) return ${(amount / 1_000_000).toFixed(digits)}M;
    if (amount>= 1_000) return ${(amount / 1_000).toFixed(digits)}K;
    return amount.toLocaleString('zh-CN', {maximumFractionDigits: digits});
    }

    function localDate(date) {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2,'0');
    const day = String(date.getDate()).padStart(2,'0');
    return ${year}-${month}-${day};
    }

    function formatTime(ms) {
    if (!Number.isFinite(ms)) return '—';
    return new Intl.DateTimeFormat('zh-CN', {
    month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
    }).format(new Date(ms));
    }

    function resetText(windowData) {
    if (!windowData?.reset_at) return '未提供重置时间';
    return formatTime(number(windowData.reset_at) * 1000);
    }

    function getAccessToken() {
    const bootstrap = document.getElementById('client-bootstrap')?.textContent || '';
    return bootstrap.match(/[\w-]{30,}.[\w-]{30,}.[\w-]{30,}/)?.[0] || '';
    }

    async function apiGet(path, token) {
    if (!path.startsWith('/backend-api/wham/')) throw new Error(' 已阻止非预期接口地址。');
    const headers = token ? { Authorization: Bearer ${token} } : {};
    const response = await fetch(path, { headers, credentials:'same-origin'});
    if (!response.ok) {
    const detail = await response.text().catch(() => '');
    throw new Error(用量接口返回 ${response.status}${detail ? :${detail.slice(0, 160)} : ''});
    }
    return response.json();
    }

    function getStats(list) {
    const stats = list.reduce((sum, item) => {
    sum.credits += number(item.totals?.credits);
    sum.turns += number(item.totals?.turns);
    sum.tokens += tokenTotal(item.totals);
    return sum;
    }, {credits: 0, turns: 0, tokens: 0});
    stats.usd = stats.credits * APP.usdPerCredit;
    return stats;
    }

    // ---------- 快照存储 ----------
    //
    // 核心思路:单次「累计消耗 ÷ 已用百分比」会把重置之前的用量算进去(日粒度的每日明细无法切开一天)。
    // 改为记录多次快照,只用同一周期内两次快照之间的「消耗增量 ÷ 百分比增量」,重置前的用量自然被抵消。
    //
    // segment = 一个连续的周额度周期(两次重置之间)
    // anchorDate:本周期累计消耗的起算日期(只要固定不变,快照之间做差就正确)
    // snapshots:[{t, pct, cum, resetAt}]
    // pct = 周额度 used_percent;cum = anchorDate 起至今的 credits 合计

    function loadStore() {
    try {
    const store = JSON.parse(localStorage.getItem(APP.storeKey) || 'null');
    if (store && Array.isArray(store.segments)) return store;
    } catch {/* 忽略损坏的本地数据 */}
    return {segments: [] };
    }

    function saveStore(store) {
    if (store.segments.length> APP.maxSegments) {
    store.segments = store.segments.slice(-APP.maxSegments);
    }
    localStorage.setItem(APP.storeKey, JSON.stringify(store));
    }

    function sumCreditsFrom(dailyList, anchorDate) {
    return dailyList.reduce((sum, item) => (
    String(item.date) >= anchorDate ? sum + number(item.totals?.credits) : sum
    ), 0);
    }

    function newSegment(now, secondary, reason) {
    // 往前多算两天,避免时区差异导致 "今天" 的日期在接口里落到昨天 / 明天
    const anchor = localDate(new Date(now.getTime() - 2 * 86_400_000));
    return {
    id: now.getTime(),
    anchorDate: anchor,
    startedAt: now.getTime(),
    resetAt: number(secondary.reset_at),
    windowSeconds: number(secondary.limit_window_seconds),
    reason,
    snapshots: [],
    };
    }

    function recordSnapshot(store, secondary, dailyList, now, fetchStartDate) {
    const pct = number(secondary.used_percent);
    const resetAt = number(secondary.reset_at);
    let segment = store.segments[store.segments.length - 1] || null;
    const last = segment?.snapshots[segment.snapshots.length - 1] || null;
    let event = null;

    if (!segment) {
      segment = newSegment(now, secondary, '首次记录');
      store.segments.push(segment);
      event = '开始记录第一个周期。';
    } else if (last && pct < last.pct - 1e-9) {
      // 已用百分比下降,只可能是发生了重置
      segment = newSegment(now, secondary, `重置:${last.pct}% → ${pct}%`);
      store.segments.push(segment);
      event = `检测到周额度重置(${last.pct}% → ${pct}%),已开启新周期。`;
    } else if (last && resetAt > last.resetAt + 3600 && pct <= last.pct + 1e-9) {
      // reset_at 向后跳而百分比没有增长:滚动重置锚点被重新计算(OpenAI 近期的已知行为),同样视为新周期
      segment = newSegment(now, secondary, `重置时间被重新锚定:${formatTime(last.resetAt * 1000)} → ${formatTime(resetAt * 1000)}`);
      store.segments.push(segment);
      event = '检测到重置时间被重新锚定,已开启新周期。';
    } else if (segment.anchorDate < fetchStartDate) {
      // 30 天窗口已经覆盖不到起算日,累计值不再可比
      segment = newSegment(now, secondary, '历史窗口不足,重新起算');
      store.segments.push(segment);
      event = '周期跨度超过历史数据窗口,已重新起算。';
    }
    
    if (resetAt > 0 && Math.abs(resetAt - segment.resetAt) > 60) segment.resetAt = resetAt;
    
    const cum = sumCreditsFrom(dailyList, segment.anchorDate);
    const prev = segment.snapshots[segment.snapshots.length - 1] || null;
    const unchanged = prev && prev.pct === pct && Math.abs(prev.cum - cum) < 1e-6;
    if (!unchanged) {
      segment.snapshots.push({ t: now.getTime(), pct, cum, resetAt });
      if (segment.snapshots.length > APP.maxSnapshotsPerSegment) {
        // 保留首尾,稀释中间
        const keep = [segment.snapshots[0]];
        const step = Math.ceil(segment.snapshots.length / (APP.maxSnapshotsPerSegment - 2));
        for (let i = 1; i < segment.snapshots.length - 1; i += step) keep.push(segment.snapshots[i]);
        keep.push(segment.snapshots[segment.snapshots.length - 1]);
        segment.snapshots = keep;
      }
    }
    
    saveStore(store);
    return { segment, event };
    

    }

    // ---------- 额度推算 ----------

    function estimateSegment(segment) {
    const pts = segment.snapshots;
    if (!pts || pts.length < 2) return null;

    const first = pts[0];
    const last = pts[pts.length - 1];
    const deltaPct = last.pct - first.pct;
    const deltaCredits = last.cum - first.cum;
    if (deltaPct <= 0 || deltaCredits <= 0) return null;
    
    // 用全部快照做最小二乘拟合:cum = k * pct + b,k * 100 就是总额度
    let n = 0; let sp = 0; let sc = 0; let spp = 0; let spc = 0;
    for (const p of pts) { n += 1; sp += p.pct; sc += p.cum; spp += p.pct * p.pct; spc += p.pct * p.cum; }
    const denom = n * spp - sp * sp;
    const slope = denom > 0 ? (n * spc - sp * sc) / denom : deltaCredits / deltaPct;
    const total = (slope > 0 ? slope : deltaCredits / deltaPct) * 100;
    
    // 接口给的是整数百分比时,两端各有 ±0.5 的取整误差,跨度不确定 ±1
    const integerPct = pts.every((p) => Number.isInteger(p.pct));
    const rounding = integerPct ? 1 : 0;
    const low = deltaCredits / (deltaPct + rounding) * 100;
    const high = deltaPct - rounding > 0 ? deltaCredits / (deltaPct - rounding) * 100 : Number.POSITIVE_INFINITY;
    
    return {
      total, low, high, deltaPct, deltaCredits, points: n,
      reliable: deltaPct >= APP.reliableDeltaPct,
      integerPct,
    };
    

    }

    function combineEstimates(segments) {
    const usable = segments
    .map((segment) => ({segment, est: estimateSegment(segment) }))
    .filter((item) => item.est && Number.isFinite(item.est.total));
    if (!usable.length) return null;
    // 用百分比跨度加权:跨度越大,取整误差越小,越可信
    let weight = 0; let acc = 0;
    for (const { est} of usable) {weight += est.deltaPct; acc += est.total * est.deltaPct;}
    return {total: acc / weight, count: usable.length, totalDeltaPct: weight};
    }

    // ---------- 渲染 ----------

    function usageCard(label, windowData) {
    const used = Math.min(100, Math.max(0, number(windowData?.used_percent)));
    const remaining = Math.max(0, 100 - used);
    const color = remaining <= 10 ?'#e5484d': remaining <= 30 ?'#f59e0b':'#10a37f';
    return <div class="cqc-card"> <div class="cqc-label">${escapeHtml(label)}</div> <div class="cqc-value">${remaining.toFixed(1)}% 剩余</div> <div class="cqc-note">已用 ${used.toFixed(1)}% · 重置 ${escapeHtml(resetText(windowData))}</div> <div class="cqc-progress"><i style="--width:${remaining}%;--color:${color}"></i></div> </div>;
    }

    function estimateCard(est, combined, secondary) {
    if (!est && !combined) {
    return <div class="cqc-card warn"><div class="cqc-label">周额度推算</div><div class="cqc-value">需要更多快照</div><div class="cqc-note">保持此页面打开,脚本每 ${APP.autoIntervalMs / 60000} 分钟自动记录一次;用过 Codex 后再来看。</div></div>;
    }
    const primary = est?.reliable ? est : (combined || est);
    const total = primary.total;
    const pct = number(secondary?.used_percent);
    const usedCredits = total * pct / 100;
    const remainCredits = total * (100 - pct) / 100;
    const cls = est?.reliable ? 'accent' : 'warn';
    const range = est && Number.isFinite(est.high)
    ? 区间 ${est.low.toFixed(0)}~${est.high.toFixed(0)}
    : est ? 下限 ${est.low.toFixed(0)},上限未知 : '';
    const note = est?.reliable
    ? ${range} · 本周期跨度 ${est.deltaPct.toFixed(est.integerPct ? 0 : 1)} 点 · 约 $${(total * APP.usdPerCredit).toFixed(0)}
    : combined
    ? 本周期跨度仅 ${est ? est.deltaPct.toFixed(est.integerPct ? 0 : 1) : 0} 点,改用 ${combined.count} 个周期的加权值 · ${range}
    : ${range} · 跨度仅 ${est.deltaPct.toFixed(est.integerPct ? 0 : 1)} 点,误差很大,等跨度 ≥ ${APP.reliableDeltaPct} 点再看;
    return <div class="cqc-card ${cls}"><div class="cqc-label">周额度推算</div><div class="cqc-value">${total.toFixed(0)} Credits</div><div class="cqc-note">${escapeHtml(note)}</div></div> <div class="cqc-card ${cls}"><div class="cqc-label">按推算折算</div><div class="cqc-value">剩 ${remainCredits.toFixed(0)}</div><div class="cqc-note">已用约 ${usedCredits.toFixed(0)} Credits($${(usedCredits * APP.usdPerCredit).toFixed(2)})</div></div>;
    }

    function renderSnapshotTable(segment) {
    const pts = segment.snapshots;
    const first = pts[0];
    const rows = [...pts].reverse().map((p) => <tr> <td>${escapeHtml(formatTime(p.t))}</td> <td class="cqc-num">${p.pct}%</td> <td class="cqc-num">${p.cum.toFixed(3)}</td> <td class="cqc-num">${(p.pct - first.pct).toFixed(1)}</td> <td class="cqc-num">${(p.cum - first.cum).toFixed(3)}</td> </tr>).join('');
    return <div class="cqc-table-wrap"><table class="cqc-table"> <thead><tr><th>时间</th><th>周额度已用</th><th>累计 Credits</th><th>Δ 百分比</th><th>Δ Credits</th></tr></thead> <tbody>${rows || '<tr><td colspan="5">暂无快照</td></tr>'}</tbody> </table></div>;
    }

    function renderSegmentTable(segments) {
    const rows = [...segments].reverse().map((segment) => {
    const est = estimateSegment(segment);
    const pts = segment.snapshots;
    const end = pts.length ? pts[pts.length - 1].t : segment.startedAt;
    const tag = !est ? '<span class="cqc-tag"> 数据不足 </span>'
    : est.reliable ? '<span class="cqc-tag ok">可靠 </span>':'<span class="cqc-tag warn"> 跨度小</span>';
    return <tr> <td>${escapeHtml(formatTime(segment.startedAt))} → ${escapeHtml(formatTime(end))}</td> <td>${escapeHtml(segment.reason || '')}</td> <td class="cqc-num">${pts.length}</td> <td class="cqc-num">${est ? est.deltaPct.toFixed(1) : '—'}</td> <td class="cqc-num">${est ? est.deltaCredits.toFixed(1) : '—'}</td> <td class="cqc-num">${est ? est.total.toFixed(0) : '—'}</td> <td>${tag}</td> </tr>;
    }).join('');
    return <div class="cqc-table-wrap"><table class="cqc-table"> <thead><tr><th>周期</th><th>起因</th><th>快照</th><th>Δ%</th><th>Δ Credits</th><th>推算总额</th><th></th></tr></thead> <tbody>${rows}</tbody> </table></div>;
    }

    function renderDailyTable(list, stats) {
    const rows = [...list]
    .sort((a, b) => String(b.date).localeCompare(String(a.date)))
    .map((row) => {
    const credits = number(row.totals?.credits);
    return <tr> <td>${escapeHtml(row.date)}</td> <td class="cqc-num">${credits.toFixed(3)}</td> <td class="cqc-num">${formatNumber(tokenTotal(row.totals))}</td> <td class="cqc-num">$${(credits * APP.usdPerCredit).toFixed(2)}</td> <td class="cqc-num">${number(row.totals?.turns).toLocaleString('zh-CN')}</td> </tr>;
    }).join('');

    return `<div class="cqc-table-wrap"><table class="cqc-table">
      <thead><tr><th>日期</th><th>Credits</th><th>Tokens</th><th>估算金额</th><th>轮数</th></tr></thead>
      <tbody>${rows || '<tr><t
    
  • 收藏 1支持反对打赏

6 条回复

  • 咪喵
    UID 7627Lv.1 初来乍到
    #1

    强是真的强 消耗也是真的快啊
    Snipaste_2026-09-02_09-52-39.webp

  • Couleur
    UID 2115Lv.2 新手上路
    #2

    这么快就用上肥波 5.1 啦

  • 咪喵
    UID 7627Lv.1 初来乍到
    #3

    @Couleur #2 cursor 里面 官方的用不起 马圣是真好 20 一个月的都给体验 fable 5.1 a 畜那狗 20 根本就不让你用

  • Couleur
    UID 2115Lv.2 新手上路
    #4

    @咪喵 #3 A 畜人人喊打

  • 咪喵
    UID 7627Lv.1 初来乍到
    #5

    @Couleur #4 把我谷歌号封完了 a 畜

  • 阿里云
    UID 5289Lv.7 熟悉面孔
    #6

    这是我看到的论坛第一长贴

发表回复