• Licoy

    最近看到很多帖子都说codex的5x、20x等等的额度降了,我实际体感也是一样用的飞快,不过也可能是我一直都是Ultra+Fast的模式来走的,所以用的比较快,用论坛里面一个其他的脚本改了一下,然后找几个朋友测试了一下:他是从5x升级到20x的,但是已经用了一个多周期了,额度都重置了,但是用原来的脚本显示周估算额度不到4000$,但是强制7天计算就有13000$

    图片

    脚本如下:```
    // ==UserScript==
    // @name Codex Quota Compass (Visual Edition)
    // @namespace http://tampermonkey.net/
    // @version 1.10
    // @description 人性化展示 Codex 配额,支持强制周窗口(最近 7 天)计算,强化 Credits 标注及历史合计(Token 以 M 为单位)
    // @author Jun Zhao
    // @match https://chatgpt.com/codex/cloud/settings/analytics*
    // @grant GM_addStyle
    // ==/UserScript==

    (function () {
    "use strict";

    const CONFIG = {
    USD_PER_CREDIT: 40 / 1000,
    WEEK_SECONDS: 7 24 3600,
    MODE_KEY: "codex-compass-window-mode",
    };

    const fmtNum = (n) => {
    if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
    if (n >= 1e3) return (n / 1e3).toFixed(2) + "K";
    return n.toLocaleString();
    };

    const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
    const tokenTotal = (obj = {}) =>
    num(obj.text_total_tokens) ||
    num(obj.cached_text_input_tokens) +
    num(obj.uncached_text_input_tokens) +
    num(obj.text_output_tokens);

    const ymdUTC = (tsSec) =>
    new Date(tsSec * 1000).toISOString().split("T")[0];
    const ymdDaysAgo = (days) =>
    new Date(Date.now() - days * 86400000).toISOString().split("T")[0];
    const addDaysYmd = (ymd, delta) => {
    const d = new Date(${ymd}T00:00:00Z);
    if (Number.isNaN(d.getTime())) return ymdDaysAgo(-delta);
    d.setUTCDate(d.getUTCDate() + delta);
    return d.toISOString().split("T")[0];
    };
    const rollingWeekStart = (dailyList) => {
    const dates = (dailyList || [])
    .map((d) => String(d.date || ""))
    .filter(Boolean)
    .sort();
    const latest = dates[dates.length - 1];
    return latest ? addDaysYmd(latest, -6) : ymdDaysAgo(6);
    };

    const getMode = () => {
    const v = localStorage.getItem(CONFIG.MODE_KEY);
    return v === "weekly" ? "weekly" : "auto";
    };
    const setMode = (mode) => localStorage.setItem(CONFIG.MODE_KEY, mode);

    const windowSecs = (w) =>
    num(w?.limit_window_seconds) ||
    num(w?.window_seconds) ||
    num(w?.window_minutes) * 60;

    const windowKind = (w, hint) => {
    if (!w) return null;
    const secs = windowSecs(w);
    if (secs >= 6 * 86400) return "weekly";
    if (secs > 0 && secs <= 12 * 3600) return "5h";
    if (hint === "secondary") return "weekly";
    if (hint === "primary") return "5h";
    return "unknown";
    };

    const pickWindows = (usage) => {
    const rl = usage?.rate_limit || {};
    const primary = rl.primary_window || rl.primary || null;
    const secondary = rl.secondary_window || rl.secondary || null;
    const weekly =
    (windowKind(secondary, "secondary") === "weekly" ? secondary : null) ||
    (windowKind(primary) === "weekly" ? primary : null);
    const fiveHour =
    (windowKind(primary, "primary") === "5h" ? primary : null) ||
    (windowKind(secondary) === "5h" ? secondary : null);
    return { primary, secondary, weekly, fiveHour };
    };

    const resolveView = (mode, windows, dailyList) => {
    if (mode === "weekly") {
    const meter = windows.weekly || windows.secondary || null;
    const kind = windowKind(meter, meter === windows.secondary ? "secondary" : undefined);
    const isFiveHour = kind === "5h";
    return {
    meter: isFiveHour ? null : meter,
    cycleStartDate: rollingWeekStart(dailyList),
    kind: "weekly-forced",
    canExtrapolate: !isFiveHour && !!meter && num(meter?.used_percent) > 0,
    title: "周窗口 · 最近7天",
    };
    }

    const meter =
    windows.weekly ||
    windows.secondary ||
    windows.fiveHour ||
    windows.primary;
    const kind =
    windowKind(
    meter,
    meter === windows.secondary
    ? "secondary"
    : meter === windows.primary
    ? "primary"
    : undefined,
    ) || "auto";
    const secs = windowSecs(meter) || CONFIG.WEEK_SECONDS;
    const resetAt = num(meter?.reset_at);
    const cycleStartDate = resetAt
    ? ymdUTC(resetAt - secs)
    : ymdDaysAgo(6);
    return {
    meter,
    cycleStartDate,
    kind,
    canExtrapolate: !!meter && num(meter?.used_percent) > 0,
    title: kind === "weekly" ? "周窗口" : kind === "5h" ? "5小时窗口" : "自动窗口",
    };
    };

    GM_addStyle(`
    #codex-compass-root {
    position: fixed; top: 5%; left: 50%; transform: translateX(-50%);
    width: 720px; max-height: 90vh; background: #fff;
    border-radius: 12px; box-shadow: 0 10px 50px rgba(0,0,0,0.3);
    z-index: 10001; padding: 24px; display: none;
    flex-direction: column; border: 1px solid #e5e5e5; color: #333;
    font-family: -apple-system, system-ui, sans-serif;
    }
    .compass-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; gap: 12px; }
    .compass-header-left { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
    .compass-header-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
    .compass-title { font-size: 18px; font-weight: 600; }
    .compass-meta { font-size: 12px; color: #888; font-weight: 400; }
    .compass-close { cursor: pointer; font-size: 28px; color: #999; line-height: 1; }
    .compass-toggle { display: inline-flex; border: 1px solid #d1f2e1; border-radius: 8px; overflow: hidden; }
    .compass-toggle button {
    border: none; background: #fff; padding: 6px 12px; cursor: pointer;
    color: #666; font-size: 12px; font-weight: 600; line-height: 1.2;
    }
    .compass-toggle button.active { background: #10a37f; color: #fff; }
    .compass-notice {
    font-size: 12px; color: #8a6d3b; background: #fff8e6;
    border: 1px solid #f0e0b2; border-radius: 8px; padding: 8px 10px;
    margin-bottom: 12px; line-height: 1.45;
    }
    .compass-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
    .compass-card { background: #f9f9f9; padding: 12px; border-radius: 8px; border: 1px solid #eee; }
    .compass-card.highlight { background: #eefaf5; border-color: #d1f2e1; }
    .compass-card.muted .card-value { color: #999; }
    .card-label { font-size: 12px; color: #666; margin-bottom: 4px; }
    .card-value { font-size: 15px; font-weight: bold; color: #10a37f; white-space: nowrap; }
    .card-sub { font-size: 11px; color: #999; margin-top: 4px; font-weight: 400; }

    .table-container {
    max-height: 220px; overflow-y: auto; border: 1px solid #eee;
    border-radius: 6px; margin-bottom: 15px;
    }
    .compass-table { width: 100%; border-collapse: collapse; font-size: 12px; }
    .compass-table thead { position: sticky; top: 0; background: #f5f5f5; z-index: 1; }
    .compass-table th { text-align: left; padding: 8px; border-bottom: 1px solid #eee; color: #666; }
    .compass-table td { padding: 8px; border-bottom: 1px solid #f0f0f0; }
    .compass-footer-row { background: #fafafa; font-weight: bold; position: sticky; bottom: 0; border-top: 2px solid #eee; }

    #codex-compass-btn {
    position: fixed; bottom: 20px; right: 20px; z-index: 10000;
    padding: 10px 20px; background: #10a37f; color: white;
    border: none; border-radius: 8px; cursor: pointer; font-weight: 600;
    }
    `);

    async function apiGet(path, token) {
    const res = await fetch(path, {
    headers: { Authorization: Bearer ${token} },
    });
    return res.json();
    }

    let lastSnapshot = null;

    const showPanel = (snapshot) => {
    lastSnapshot = snapshot;
    const root = document.getElementById("codex-compass-root");
    const mode = getMode();
    const { windows, dailyList } = snapshot;
    const view = resolveView(mode, windows, dailyList);
    const meter = view.meter;

    const currentCycleList = [];
    const historyList = [];
    dailyList.forEach((item) => {
    if (String(item.date) >= view.cycleStartDate) {
    currentCycleList.push(item);
    } else {
    historyList.push(item);
    }
    });

    const getStats = (list) => {
    const credits = list.reduce(
    (sum, d) => sum + (d.totals?.credits || 0),
    0,
    );
    const turns = list.reduce((sum, d) => sum + (d.totals?.turns || 0), 0);
    const tokens = list.reduce((sum, d) => sum + tokenTotal(d.totals), 0);
    return { credits, turns, tokens, usd: credits * CONFIG.USD_PER_CREDIT };
    };

    const currentStats = getStats(currentCycleList);
    const historyStats = getStats(historyList);

    const usedPercent = num(meter?.used_percent);
    const ratio = usedPercent / 100;
    const canEst = view.canExtrapolate && ratio > 0;
    const estCredits = canEst ? currentStats.credits / ratio : 0;
    const weekValue = estCredits * CONFIG.USD_PER_CREDIT;

    const weeklyPct = windows.weekly
    ? ${num(windows.weekly.used_percent)}%
    : "无";
    const fivePct = windows.fiveHour
    ? ${num(windows.fiveHour.used_percent)}%
    : "无";

    let historyRangeTitle = "⏳ 历史记录 (本周期外)";
    if (historyList.length > 0) {
    const sortedHistory = [...historyList].sort(
    (a, b) => String(a.date).localeCompare(String(b.date)),
    );
    const startStr = sortedHistory[0].date;
    const endStr = sortedHistory[sortedHistory.length - 1].date;
    historyRangeTitle = ⏳ 历史记录 (本周期外 ${startStr} 至 ${endStr});
    }

    const isWeeklyView = mode === "weekly" || view.kind === "weekly";
    const usedLabel = isWeeklyView ? "本周已用" : "本窗口已用";
    const estLabel = isWeeklyView ? "推算周额度" : "推算总额";
    const valueLabel = isWeeklyView ? "周价值 (估算)" : "窗口价值 (估算)";

    let notice = "";
    if (mode === "auto" && view.kind === "5h") {
    notice = 当前自动选中的是 <b>5 小时窗口</b>,明细可能只有 1 天。点右上角「周窗口」按最近 7 天重算。;
    } else if (mode === "weekly") {
    notice = 已强制按 <b>最近 7 个自然日</b> 汇总 Credits(${view.cycleStartDate} 起,${currentCycleList.length} 天)。占用率使用周窗口百分比。若 API 周重置晚于这 7 天起点,总额可能含上一周用量。;
    if (!canEst) {
    notice += 当前没有可用的周占用率,推算周额度 / 周价值不外推。;
    }
    }

    const renderTable = (list, stats) => `
    <div class="table-container">
    <table class="compass-table">
    <thead>
    <tr><th>日期</th><th>Credits</th><th>Tokens</th><th>金额</th><th>轮数</th></tr>
    </thead>
    <tbody>
    ${[...list]
    .reverse()
    .map(
    (row) => `
    <tr>
    <td>${row.date}</td>
    <td style="font-family:monospace">${(row.totals?.credits || 0).toFixed(3)}</td>
    <td style="font-family:monospace">${fmtNum(tokenTotal(row.totals))}</td>
    <td>$ ${((row.totals?.credits || 0) * CONFIG.USD_PER_CREDIT).toFixed(2)}</td>
    <td>${row.totals?.turns || 0}</td>
    </tr>
    `,
    )
    .join("")}
    </tbody>
    <tfoot>
    <tr class="compass-footer-row">
    <td>合计</td>
    <td>${stats.credits.toFixed(3)}</td>
    <td style="font-family:monospace">${fmtNum(stats.tokens)}</td>
    <td style="color:#10a37f">$ ${stats.usd.toFixed(2)}</td>
    <td>${stats.turns}</td>
    </tr>
    </tfoot>
    </table>
    </div>
    `;

    root.innerHTML = `
    <div class="compass-header">
    <div class="compass-header-left">
    <div class="compass-title">📊 Codex 配额深度分析 (V1.10)</div>
    <div class="compass-meta">计算窗口:${view.title} · 5小时 ${fivePct} · 周 ${weeklyPct}</div>
    </div>
    <div class="compass-header-right">
    <div class="compass-toggle" id="compass-window-toggle">
    <button type="button" data-mode="auto" class="${mode === "auto" ? "active" : ""}">自动</button>
    <button type="button" data-mode="weekly" class="${mode === "weekly" ? "active" : ""}">周窗口</button>
    </div>
    <div class="compass-close" id="compass-close-btn">&times;</div>
    </div>
    </div>
    ${notice ? <div class="compass-notice">${notice}</div> : ""}
    <div class="compass-grid">
    <div class="compass-card">
    <div class="card-label">已用比例</div>
    <div class="card-value">${usedPercent || 0}%</div>
    <div class="card-sub">${view.title}</div>
    </div>
    <div class="compass-card">
    <div class="card-label">${usedLabel}</div>
    <div class="card-value">${currentStats.credits.toFixed(1)} <span style="font-size:10px; font-weight:normal">Credits</span></div>
    <div class="card-sub">${currentCycleList.length} 天</div>
    </div>
    <div class="compass-card highlight ${canEst ? "" : "muted"}">
    <div class="card-label">${estLabel}</div>
    <div class="card-value">${canEst ? estCredits.toFixed(1) : "—"} <span style="font-size:10px; font-weight:normal">${canEst ? "Credits" : ""}</span></div>
    </div>
    <div class="compass-card ${canEst ? "" : "muted"}">
    <div class="card-label">${valueLabel}</div>
    <div class="card-value">${canEst ? "$ " + weekValue.toFixed(2) : "—"}</div>
    </div>
    </div>

    <div style="font-weight:600; margin-bottom:8px; font-size:13px;">📅 ${isWeeklyView ? "本周明细" : "本周期明细"} (始于 ${view.cycleStartDate})</div>
    ${renderTable(currentCycleList, currentStats)}

    ${
    historyList.length > 0
    ? `
    <div style="font-weight:600; margin-bottom:8px; font-size:13px; color: #666; margin-top: 5px;">${historyRangeTitle}</div>
    ${renderTable(historyList, historyStats)}
    `
    : ""
    }
    `;

    root.style.display = "flex";

    if (!root.dataset.bound) {
    root.dataset.bound = "1";
    root.addEventListener("click", (e) => {
    if (e.target.closest("#compass-close-btn")) {
    root.style.display = "none";
    return;
    }
    const modeBtn = e.target.closest("#compass-window-toggle [data-mode]");
    if (!modeBtn) return;
    e.preventDefault();
    e.stopPropagation();
    const next = modeBtn.getAttribute("data-mode");
    if (next !== "auto" && next !== "weekly") return;
    if (next === getMode()) return;
    setMode(next);
    if (lastSnapshot) showPanel(lastSnapshot);
    });
    }
    };

    const run = async () => {
    const btn = document.getElementById("codex-compass-btn");
    const bootstrapData =
    document.getElementById("client-bootstrap")?.textContent;
    const token = bootstrapData?.match(
    /[\w-]{30,}\.[\w-]{30,}\.[\w-]{30,}/,
    )?.[0];
    if (!token) return alert("令牌获取失败,请确保已登录。");

    btn.innerText = "分析中...";
    try {
    const usage = await apiGet("/backend-api/wham/usage", token);
    const windows = pickWindows(usage);

    const endDate = new Date(Date.now() + 86400000)
    .toISOString()
    .split("T")[0];
    const startDate = new Date(Date.now() - 30 * 86400000)
    .toISOString()
    .split("T")[0];

    const dailyData = await apiGet(
    /backend-api/wham/analytics/daily-workspace-usage-counts?start_date=${startDate}&end_date=${endDate}&group_by=day,
    token,
    );

    showPanel({
    windows,
    dailyList: dailyData.data || [],
    });
    } catch (e) {
    alert("错误: " + e.message);
    } finally {
    btn.innerText = "📊 运行用量分析";
    }
    };

    const btn = document.createElement("button");
    btn.id = "codex-compass-btn";
    btn.innerText = "📊 运行用量分析";
    btn.onclick = run;
    document.body.appendChild(btn);

    const root = document.createElement("div");
    root.id = "codex-compass-root";
    document.body.appendChild(root);
    })();

    主楼
  • zaoan

    https://chatgpt.com/codex/cloud/settings/analytics

    大佬,之前的脚本可以发下吗?想参考一下呢

    #1
  • Licoy

    之前的脚本和这个一样,只不过就是没有右上角的 自动、周窗口 的切换

    #2
  • Hiskens

    佬 为什么我用下来credits都是0,额度没计算

    图片

    #3

发表回复

登录后回复