// ==UserScript== // @name Codex 额度美元估算器 // @namespace codex-credit-usd-estimator // @version 0.4.0 // @description 在 Codex Analytics 页面估算每周/5小时额度折合美元。只拦截页面请求,避免 401。 // @author Anonymous // @license MIT // @match https://chatgpt.com/codex/cloud/settings/analytics* // @run-at document-start // @grant none // ==/UserScript== (() => { 'use strict'; const USD_PER_1000_CREDITS = 40; const USD_PER_CREDIT = USD_PER_1000_CREDITS / 1000; const state = { rateLimitPayload: null, dailyWorkspaceUsage: null, creditUsageEvents: null, lastUpdatedAt: null, }; function fmtNum(n, digits = 2) { if (!Number.isFinite(n)) return '—'; return n.toLocaleString(undefined, { minimumFractionDigits: digits, maximumFractionDigits: digits, }); } function fmtUsd(n) { if (!Number.isFinite(n)) return '—'; return `$${fmtNum(n, 2)}`; } function fmtCredits(n) { if (!Number.isFinite(n)) return '—'; return `${fmtNum(n, 2)} credits`; } function safeJsonParse(text) { try { return JSON.parse(text); } catch { return null; } } function isRateLimitPayload(obj) { return !!( obj && typeof obj === 'object' && obj.rate_limit && obj.rate_limit.primary_window && obj.rate_limit.secondary_window ); } function isDailyWorkspaceUsagePayload(obj) { return !!( obj && Array.isArray(obj.data) && obj.data.some((x) => x?.totals && typeof x.totals.credits === 'number') ); } function captureJsonByUrl(url, json) { if (!json || typeof json !== 'object') return; const u = String(url || ''); if (isRateLimitPayload(json)) { state.rateLimitPayload = json; state.lastUpdatedAt = new Date(); renderPanel(); return; } if ( u.includes('daily-workspace-usage-counts') && isDailyWorkspaceUsagePayload(json) ) { state.dailyWorkspaceUsage = json; state.lastUpdatedAt = new Date(); renderPanel(); return; } if (u.includes('credit-usage-events')) { state.creditUsageEvents = json; state.lastUpdatedAt = new Date(); renderPanel(); } } // 拦截 fetch const rawFetch = window.fetch; window.fetch = async function (...args) { const res = await rawFetch.apply(this, args); try { const url = typeof args[0] === 'string' ? args[0] : args[0]?.url; const clone = res.clone(); const ct = clone.headers.get('content-type') || ''; if (ct.includes('application/json')) { clone.text().then((txt) => { const json = safeJsonParse(txt); captureJsonByUrl(url, json); }); } } catch {} return res; }; // 拦截 XHR const RawXHR = window.XMLHttpRequest; function HookedXHR() { const xhr = new RawXHR(); let requestUrl = ''; const rawOpen = xhr.open; xhr.open = function (method, url, ...rest) { requestUrl = url; return rawOpen.call(xhr, method, url, ...rest); }; xhr.addEventListener('load', () => { try { const ct = xhr.getResponseHeader('content-type') || ''; if (ct.includes('application/json')) { const json = safeJsonParse(xhr.responseText); captureJsonByUrl(requestUrl, json); } } catch {} }); return xhr; } window.XMLHttpRequest = HookedXHR; function getWeeklyCreditsFromDailyPayload(payload) { if (!isDailyWorkspaceUsagePayload(payload)) return NaN; const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - 7); cutoff.setHours(0, 0, 0, 0); let sum = 0; for (const item of payload.data) { if (!item?.date || !item?.totals) continue; const d = new Date(`${item.date}T00:00:00`); if (d >= cutoff && typeof item.totals.credits === 'number') { sum += item.totals.credits; } } return sum; } function getFiveHourCreditsFromEvents(payload) { if (!payload || typeof payload !== 'object') return NaN; const now = Date.now(); const cutoffMs = now - 5 * 60 * 60 * 1000; let sum = 0; let found = false; function getTimeMs(obj) { const candidates = [ obj.created_at, obj.updated_at, obj.timestamp, obj.time, obj.createdAt, obj.updatedAt, obj.date, ]; for (const v of candidates) { if (v == null) continue; if (typeof v === 'number') { return v > 10_000_000_000 ? v : v * 1000; } if (typeof v === 'string') { const t = Date.parse(v); if (Number.isFinite(t)) return t; } } return NaN; } function walk(x) { if (!x || typeof x !== 'object') return; if (Array.isArray(x)) { x.forEach(walk); return; } const creditValue = typeof x.credits === 'number' ? x.credits : typeof x.credit === 'number' ? x.credit : typeof x.usage_credits === 'number' ? x.usage_credits : NaN; const timeMs = getTimeMs(x); if ( Number.isFinite(creditValue) && Number.isFinite(timeMs) && timeMs >= cutoffMs && timeMs <= now + 60 * 1000 ) { sum += creditValue; found = true; } for (const v of Object.values(x)) { if (v && typeof v === 'object') walk(v); } } walk(payload); return found ? sum : NaN; } function calculate() { const rate = state.rateLimitPayload?.rate_limit; const primary = rate?.primary_window; const secondary = rate?.secondary_window; const fiveHourUsedPercent = primary?.used_percent; const weeklyUsedPercent = secondary?.used_percent; const weeklyUsedCredits = getWeeklyCreditsFromDailyPayload( state.dailyWorkspaceUsage ); const fiveHourUsedCredits = getFiveHourCreditsFromEvents( state.creditUsageEvents ); const weeklyFullCredits = Number.isFinite(weeklyUsedCredits) && weeklyUsedPercent > 0 ? weeklyUsedCredits / (weeklyUsedPercent / 100) : NaN; const weeklyRemainingCredits = Number.isFinite(weeklyFullCredits) && Number.isFinite(weeklyUsedPercent) ? weeklyFullCredits * (1 - weeklyUsedPercent / 100) : NaN; const fiveHourFullCredits = Number.isFinite(fiveHourUsedCredits) && fiveHourUsedPercent > 0 ? fiveHourUsedCredits / (fiveHourUsedPercent / 100) : NaN; const fiveHourRemainingCredits = Number.isFinite(fiveHourFullCredits) && Number.isFinite(fiveHourUsedPercent) ? fiveHourFullCredits * (1 - fiveHourUsedPercent / 100) : NaN; return { fiveHourUsedPercent, weeklyUsedPercent, weeklyUsedCredits, weeklyFullCredits, weeklyRemainingCredits, fiveHourUsedCredits, fiveHourFullCredits, fiveHourRemainingCredits, weeklyUsedUsd: weeklyUsedCredits * USD_PER_CREDIT, weeklyFullUsd: weeklyFullCredits * USD_PER_CREDIT, weeklyRemainingUsd: weeklyRemainingCredits * USD_PER_CREDIT, fiveHourUsedUsd: fiveHourUsedCredits * USD_PER_CREDIT, fiveHourFullUsd: fiveHourFullCredits * USD_PER_CREDIT, fiveHourRemainingUsd: fiveHourRemainingCredits * USD_PER_CREDIT, }; } let host; let root; function ensurePanel() { if (host && root) return; host = document.createElement('div'); host.id = 'codex-credit-usd-panel-host'; host.style.position = 'fixed'; host.style.right = '18px'; host.style.bottom = '18px'; host.style.zIndex = '2147483647'; root = host.attachShadow({ mode: 'open' }); document.documentElement.appendChild(host); } function renderPanel() { if (!document.documentElement) return; ensurePanel(); const c = calculate(); const updated = state.lastUpdatedAt ? state.lastUpdatedAt.toLocaleTimeString() : '等待页面接口'; const weeklyNote = Number.isFinite(c.weeklyFullUsd) ? '周额度根据最近7天 credits ÷ 周 used_percent 反推,可能有统计延迟。' : '等待页面加载 daily-workspace-usage-counts 和 usage 接口。'; const fiveHourNote = Number.isFinite(c.fiveHourFullUsd) ? '' : c.fiveHourUsedPercent === 0 ? '5小时窗口当前是 0%,无法反推总额度;跑一点 Codex 后刷新页面就能估。' : '暂时没抓到近5小时 credit events,所以无法估算5小时总额。'; root.innerHTML = `