Skip to content

Instantly share code, notes, and snippets.

@BlueSkyXN
Last active May 31, 2026 14:43
Show Gist options
  • Select an option

  • Save BlueSkyXN/528e810b98affcecca170e6b9d53d7da to your computer and use it in GitHub Desktop.

Select an option

Save BlueSkyXN/528e810b98affcecca170e6b9d53d7da to your computer and use it in GitHub Desktop.
codex-quota-compass.js
(async () => {
'use strict';
// ============================================================
// codex-quota-compass.js(发布版)
// ============================================================
// 用法:在 https://chatgpt.com/codex/cloud/settings/analytics#usage
// 或任意 chatgpt.com 页面打开 DevTools Console,粘贴运行。
//
// 安全:
// - 不打印 accessToken / cookie。
// - 不返回 /usage 原始响应,避免泄露 email / user_id。
// - MANUAL_ACCESS_TOKEN 默认留空;只有自动取不到 token 时,才在自己电脑临时填写。
//
// 说明:
// - /backend-api/wham/usage 的 reset_at 是 epoch seconds,不受时区影响。
// - daily-workspace-usage-counts 只接受 YYYY-MM-DD,且 group_by 只支持 day/week/month。
// - 本脚本默认把用量日期桶按 UTC 解释;同时输出 UTC / 本地时区诊断。
const CONFIG = {
DATE_BUCKET_MODE: 'utc', // 推荐:'utc';可改为 'local' 对比
USD_PER_CREDIT: 40 / 1000, // 1000 credits = 40 USD
ROLLING_DAYS: 30,
// 不建议使用。只有自动取不到 accessToken 时,才在自己电脑临时填写 Bearer 后面的内容。
// 发布脚本、截图、复制输出前,必须保持为空。
MANUAL_ACCESS_TOKEN: '',
USAGE_PATH: '/backend-api/wham/usage',
DAILY_USAGE_PATH:
'/backend-api/wham/analytics/daily-workspace-usage-counts',
};
if (location.hostname !== 'chatgpt.com') {
throw new Error('请在 chatgpt.com 页面运行,例如 Codex Usage / Analytics 页面。');
}
// ============================================================
// 基础工具
// ============================================================
const DAY_MS = 24 * 60 * 60 * 1000;
const n = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
const round = (v, d = 2) => Number(Number(v).toFixed(d));
const pad2 = (x) => String(x).padStart(2, '0');
const last = (arr) => (arr.length ? arr[arr.length - 1] : undefined);
const fmtLocal = (ms) => new Date(ms).toLocaleString();
const fmtUTC = (ms) =>
new Date(ms).toISOString().replace('T', ' ').replace('.000Z', ' UTC');
const ymdUTC = (x) => {
const d = new Date(x);
return [
d.getUTCFullYear(),
pad2(d.getUTCMonth() + 1),
pad2(d.getUTCDate()),
].join('-');
};
const ymdLocal = (x) => {
const d = new Date(x);
d.setMinutes(d.getMinutes() - d.getTimezoneOffset());
return d.toISOString().slice(0, 10);
};
const ymdForApi = (ms) =>
CONFIG.DATE_BUCKET_MODE === 'utc' ? ymdUTC(ms) : ymdLocal(ms);
const addDaysLocalMs = (ms, days) => {
const d = new Date(ms);
d.setDate(d.getDate() + days);
return d.getTime();
};
const addDaysForApi = (ms, days) =>
CONFIG.DATE_BUCKET_MODE === 'utc'
? ms + days * DAY_MS
: addDaysLocalMs(ms, days);
const firstDayOfMonthUTC = (ms) => {
const d = new Date(ms);
return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-01`;
};
const firstDayOfMonthLocal = (ms) => {
const d = new Date(ms);
return ymdLocal(new Date(d.getFullYear(), d.getMonth(), 1).getTime());
};
const firstDayOfMonthForApi = (ms) =>
CONFIG.DATE_BUCKET_MODE === 'utc'
? firstDayOfMonthUTC(ms)
: firstDayOfMonthLocal(ms);
const utcOffsetLabel = (ms) => {
const offsetMinutes = -new Date(ms).getTimezoneOffset();
const sign = offsetMinutes >= 0 ? '+' : '-';
const abs = Math.abs(offsetMinutes);
return `UTC${sign}${pad2(Math.floor(abs / 60))}:${pad2(abs % 60)}`;
};
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 stripBearer = (s) =>
String(s || '')
.replace(/^Bearer\s+/i, '')
.trim();
const looksLikeJwt = (s) =>
typeof s === 'string' && s.length > 100 && s.split('.').length >= 3;
// ============================================================
// Auth:自动从 session 中找 accessToken,不打印敏感信息
// ============================================================
function findAccessToken(obj, depth = 0) {
if (!obj || typeof obj !== 'object' || depth > 8) return '';
for (const [key, value] of Object.entries(obj)) {
if (
typeof value === 'string' &&
/access/i.test(key) &&
looksLikeJwt(value)
) {
return value;
}
if (value && typeof value === 'object') {
const found = findAccessToken(value, depth + 1);
if (found) return found;
}
}
return '';
}
async function getAccessToken() {
const manual = stripBearer(CONFIG.MANUAL_ACCESS_TOKEN);
if (manual) return manual;
try {
const res = await fetch('/api/auth/session', {
credentials: 'include',
headers: { accept: 'application/json' },
});
if (!res.ok) return '';
return findAccessToken(await res.json());
} catch {
return '';
}
}
const accessToken = await getAccessToken();
const headers = {
accept: 'application/json',
};
if (accessToken) {
headers.authorization = `Bearer ${accessToken}`;
}
async function apiGet(path) {
const res = await fetch(path, {
method: 'GET',
credentials: 'include',
headers,
});
if (!res.ok) {
const text = await res.text().catch(() => '');
if (res.status === 401) {
throw new Error(
[
`HTTP 401 Unauthorized: ${path}`,
'',
'没有拿到有效 Authorization。',
'处理方式:',
'1. 先确认你已经登录 chatgpt.com,并在同一个页面运行脚本。',
'2. 刷新 Codex Usage / Analytics 页面后重试。',
'3. 仍失败时,可在 Network 面板找到成功的 /backend-api/wham/usage 请求,',
' 复制 Authorization: Bearer 后面的 token,只在自己电脑临时填到 CONFIG.MANUAL_ACCESS_TOKEN。',
'',
'不要把 token、Cookie、填过 token 的脚本或截图发给任何人。',
].join('\n')
);
}
throw new Error(
`HTTP ${res.status} ${res.statusText}: ${path}\n${text.slice(0, 800)}`
);
}
return res.json();
}
// ============================================================
// /usage 解析:刷新周期、重置时间、已用百分比
// ============================================================
function parseWindow(label, w) {
const usedPercent = n(w?.used_percent);
const windowSeconds = n(w?.limit_window_seconds);
const resetAfterSeconds = n(w?.reset_after_seconds);
const resetAtSeconds = n(w?.reset_at);
const resetAtMs = resetAtSeconds * 1000;
const windowStartMs = resetAtMs - windowSeconds * 1000;
const serverNowMs = resetAtMs - resetAfterSeconds * 1000;
return {
名称: label,
已用百分比: usedPercent,
已用比例小数: round(usedPercent / 100, 4),
窗口秒数: windowSeconds,
窗口天数: round(windowSeconds / 86400, 4),
本轮开始_UTC: fmtUTC(windowStartMs),
本轮开始_本地: fmtLocal(windowStartMs),
下次重置_UTC: fmtUTC(resetAtMs),
下次重置_本地: fmtLocal(resetAtMs),
后端当前_UTC: fmtUTC(serverNowMs),
后端当前_本地: fmtLocal(serverNowMs),
距离重置小时: round(resetAfterSeconds / 3600, 2),
_windowStartMs: windowStartMs,
_resetAtMs: resetAtMs,
_serverNowMs: serverNowMs,
};
}
function collectWindows(usage) {
const windows = [];
if (usage?.rate_limit?.primary_window) {
windows.push(
parseWindow('主限制 - 5小时窗口', usage.rate_limit.primary_window)
);
}
if (usage?.rate_limit?.secondary_window) {
windows.push(
parseWindow('主限制 - 7天窗口', usage.rate_limit.secondary_window)
);
}
for (const item of usage?.additional_rate_limits ?? []) {
const name = item.limit_name || item.metered_feature || '额外限制';
if (item?.rate_limit?.primary_window) {
windows.push(
parseWindow(`${name} - 5小时窗口`, item.rate_limit.primary_window)
);
}
if (item?.rate_limit?.secondary_window) {
windows.push(
parseWindow(`${name} - 7天窗口`, item.rate_limit.secondary_window)
);
}
}
return windows;
}
// ============================================================
// daily-workspace-usage-counts 解析
// ============================================================
function parseDailyRows(json) {
return (json.data ?? [])
.slice()
.sort((a, b) => String(a.date).localeCompare(String(b.date)))
.map((d) => {
const t = d.totals ?? {};
const credits = n(t.credits);
return {
日期桶: d.date,
Credits: round(credits, 6),
折算USD: round(credits * CONFIG.USD_PER_CREDIT, 2),
用户数: n(t.users),
线程数: n(t.threads),
轮数: n(t.turns),
Token总量: tokenTotal(t),
缓存输入Token: n(t.cached_text_input_tokens),
非缓存输入Token: n(t.uncached_text_input_tokens),
输出Token: n(t.text_output_tokens),
客户端数量: Array.isArray(d.clients) ? d.clients.length : 0,
客户端Credits: (d.clients ?? [])
.map((c) => `${c.client_id ?? 'UNKNOWN'}:${round(n(c.credits), 2)}`)
.join(' | '),
};
});
}
function summarizeClients(json) {
const map = new Map();
for (const day of json.data ?? []) {
for (const c of day.clients ?? []) {
const id = c.client_id ?? 'UNKNOWN';
const row = map.get(id) ?? {
客户端: id,
Credits: 0,
折算USD: 0,
线程数: 0,
轮数: 0,
Token总量: 0,
缓存输入Token: 0,
非缓存输入Token: 0,
输出Token: 0,
};
const credits = n(c.credits);
row.Credits += credits;
row.折算USD += credits * CONFIG.USD_PER_CREDIT;
row.线程数 += n(c.threads);
row.轮数 += n(c.turns);
row.Token总量 += tokenTotal(c);
row.缓存输入Token += n(c.cached_text_input_tokens);
row.非缓存输入Token += n(c.uncached_text_input_tokens);
row.输出Token += n(c.text_output_tokens);
map.set(id, row);
}
}
return [...map.values()]
.map((r) => ({
...r,
Credits: round(r.Credits, 6),
折算USD: round(r.折算USD, 2),
}))
.sort((a, b) => b.Credits - a.Credits);
}
async function fetchDailyUsage(startDate, endExclusiveDate) {
const qs = new URLSearchParams({
start_date: startDate,
end_date: endExclusiveDate,
group_by: 'day',
});
const url = `${CONFIG.DAILY_USAGE_PATH}?${qs}`;
const json = await apiGet(url);
return {
url: location.origin + url,
rows: parseDailyRows(json),
clients: summarizeClients(json),
};
}
function summarizeRows(rangeName, rows, startDate, endExclusiveDate) {
const credits = rows.reduce((s, r) => s + n(r.Credits), 0);
return {
范围: rangeName,
日期桶口径:
CONFIG.DATE_BUCKET_MODE === 'utc' ? 'UTC日期桶' : '本地日期桶',
API_start_date: startDate,
API_end_date_排他: endExclusiveDate,
返回日期桶数: rows.length,
首个返回日期桶: rows[0]?.日期桶 ?? '',
最后返回日期桶: last(rows)?.日期桶 ?? '',
累计Credits: round(credits, 6),
累计折算USD: round(credits * CONFIG.USD_PER_CREDIT, 2),
累计Token: rows.reduce((s, r) => s + n(r.Token总量), 0),
累计线程数: rows.reduce((s, r) => s + n(r.线程数), 0),
累计轮数: rows.reduce((s, r) => s + n(r.轮数), 0),
};
}
function publicWindowRow(w) {
const { _windowStartMs, _resetAtMs, _serverNowMs, ...visible } = w;
return visible;
}
function printTimezoneDiagnostics({
apiNowMs,
windowStartMs,
resetAtMs,
sinceResetStartDate,
monthStartDate,
rollingStartDate,
endExclusiveDate,
}) {
const browserTimeZone =
Intl.DateTimeFormat().resolvedOptions().timeZone || '未知';
console.log('0)时区诊断:刷新周期与用量日期桶');
console.table([
{ 项目: '浏览器本地时区', 值: browserTimeZone },
{ 项目: '浏览器UTC偏移', 值: utcOffsetLabel(apiNowMs) },
{
项目: '当前脚本日期桶模式',
值: CONFIG.DATE_BUCKET_MODE === 'utc' ? 'UTC日期桶' : '本地日期桶',
},
{ 项目: '浏览器当前时间_本地', 值: fmtLocal(Date.now()) },
{ 项目: '浏览器当前时间_UTC', 值: fmtUTC(Date.now()) },
{ 项目: '后端当前时间_本地', 值: fmtLocal(apiNowMs) },
{ 项目: '后端当前时间_UTC', 值: fmtUTC(apiNowMs) },
{
项目: '浏览器时间与后端时间差_秒',
值: round((Date.now() - apiNowMs) / 1000, 2),
},
{ 项目: '7天窗口开始_本地', 值: fmtLocal(windowStartMs) },
{ 项目: '7天窗口开始_UTC', 值: fmtUTC(windowStartMs) },
{ 项目: '下次重置时间_本地', 值: fmtLocal(resetAtMs) },
{ 项目: '下次重置时间_UTC', 值: fmtUTC(resetAtMs) },
{ 项目: '7天窗口开始日期_本地口径', 值: ymdLocal(windowStartMs) },
{ 项目: '7天窗口开始日期_UTC口径', 值: ymdUTC(windowStartMs) },
{ 项目: '后端当前日期_本地口径', 值: ymdLocal(apiNowMs) },
{ 项目: '后端当前日期_UTC口径', 值: ymdUTC(apiNowMs) },
{ 项目: '本月月初_本地口径', 值: firstDayOfMonthLocal(apiNowMs) },
{ 项目: '本月月初_UTC口径', 值: firstDayOfMonthUTC(apiNowMs) },
{ 项目: 'API_start_date_上次重置至今', 值: sinceResetStartDate },
{ 项目: 'API_start_date_本月初至今', 值: monthStartDate },
{ 项目: `API_start_date_近${CONFIG.ROLLING_DAYS}天`, 值: rollingStartDate },
{ 项目: 'API_end_date_排他', 值: endExclusiveDate },
]);
}
function buildWeeklyEstimate({
mainSecondary,
sinceResetRows,
sinceResetSummary,
sinceResetStartDate,
}) {
const usedPercent = n(mainSecondary.已用百分比);
const usedRatio = usedPercent / 100;
const includedCredits = n(sinceResetSummary.累计Credits);
const resetDayRow = sinceResetRows.find(
(r) => r.日期桶 === sinceResetStartDate
);
const resetDayCredits = n(resetDayRow?.Credits);
const excludedCredits = Math.max(0, includedCredits - resetDayCredits);
if (usedRatio <= 0) {
return {
依据: '主限制 - 7天窗口 secondary_window',
已用百分比: usedPercent,
说明: '已用比例为 0,无法反推总额度。',
};
}
const totalWithResetDay = includedCredits / usedRatio;
const totalWithoutResetDay = excludedCredits / usedRatio;
const remainingWithResetDay = Math.max(
0,
totalWithResetDay - includedCredits
);
const remainingWithoutResetDay = Math.max(
0,
totalWithoutResetDay - excludedCredits
);
return {
依据: '主限制 - 7天窗口 secondary_window',
已用百分比: usedPercent,
已用比例小数: round(usedRatio, 4),
剩余比例小数: round(1 - usedRatio, 4),
说明:
'used_percent 表示已经用掉的比例;例如 45 = 已用 45%,不是剩余 45%。',
日期桶口径:
CONFIG.DATE_BUCKET_MODE === 'utc' ? 'UTC日期桶' : '本地日期桶',
包含重置日_已用Credits: round(includedCredits, 6),
包含重置日_已用折算USD: round(
includedCredits * CONFIG.USD_PER_CREDIT,
2
),
重置日整天Credits: round(resetDayCredits, 6),
重置日整天折算USD: round(
resetDayCredits * CONFIG.USD_PER_CREDIT,
2
),
排除重置日_已用Credits: round(excludedCredits, 6),
排除重置日_已用折算USD: round(
excludedCredits * CONFIG.USD_PER_CREDIT,
2
),
反推周总Credits_包含重置日: round(totalWithResetDay, 2),
反推周总USD_包含重置日: round(
totalWithResetDay * CONFIG.USD_PER_CREDIT,
2
),
反推周总Credits_排除重置日: round(totalWithoutResetDay, 2),
反推周总USD_排除重置日: round(
totalWithoutResetDay * CONFIG.USD_PER_CREDIT,
2
),
剩余Credits_包含重置日口径: round(remainingWithResetDay, 2),
剩余USD_包含重置日口径: round(
remainingWithResetDay * CONFIG.USD_PER_CREDIT,
2
),
剩余Credits_排除重置日口径: round(remainingWithoutResetDay, 2),
剩余USD_排除重置日口径: round(
remainingWithoutResetDay * CONFIG.USD_PER_CREDIT,
2
),
误差说明:
'daily analytics 只能按天聚合,不能切到具体小时分钟;实际值通常介于“排除重置日”和“包含重置日”之间。used_percent 也是整数,存在四舍五入或截断误差。',
};
}
// ============================================================
// 主流程
// ============================================================
const usage = await apiGet(CONFIG.USAGE_PATH);
const windows = collectWindows(usage);
if (!usage?.rate_limit?.secondary_window) {
throw new Error(
'没有找到 usage.rate_limit.secondary_window,无法反推主 7 天窗口。'
);
}
const mainSecondary = parseWindow(
'主限制 - 7天窗口',
usage.rate_limit.secondary_window
);
const apiNowMs = mainSecondary._serverNowMs || Date.now();
const apiTodayDate = ymdForApi(apiNowMs);
// daily-workspace-usage-counts 的 end_date 是排他边界。
// 要尽量包含当前日期桶,所以传“后端当前日期 + 1天”。
const endExclusiveDate = ymdForApi(addDaysForApi(apiNowMs, 1));
// 7天窗口:用 UTC 日期桶取 windowStart 所在日期。
// 注意:因为接口只能按天,windowStart 当天只能整天纳入。
const sinceResetStartDate = ymdForApi(mainSecondary._windowStartMs);
// 本月初至今:按 DATE_BUCKET_MODE 取月初。
const monthStartDate = firstDayOfMonthForApi(apiNowMs);
// 近 N 天:包含今天,往前 N-1 个日期桶。
const rollingStartDate = ymdForApi(
addDaysForApi(apiNowMs, -(CONFIG.ROLLING_DAYS - 1))
);
printTimezoneDiagnostics({
apiNowMs,
windowStartMs: mainSecondary._windowStartMs,
resetAtMs: mainSecondary._resetAtMs,
sinceResetStartDate,
monthStartDate,
rollingStartDate,
endExclusiveDate,
});
const sinceReset = await fetchDailyUsage(
sinceResetStartDate,
endExclusiveDate
);
const sinceResetSummary = summarizeRows(
`上次重置至今近似 ${sinceResetStartDate} ~ ${apiTodayDate}`,
sinceReset.rows,
sinceResetStartDate,
endExclusiveDate
);
const weeklyEstimate = buildWeeklyEstimate({
mainSecondary,
sinceResetRows: sinceReset.rows,
sinceResetSummary,
sinceResetStartDate,
});
const monthToDate = await fetchDailyUsage(monthStartDate, endExclusiveDate);
const monthToDateSummary = summarizeRows(
`本月初至今 ${monthStartDate} ~ ${apiTodayDate}`,
monthToDate.rows,
monthStartDate,
endExclusiveDate
);
const rolling = await fetchDailyUsage(rollingStartDate, endExclusiveDate);
const rollingSummary = summarizeRows(
`近${CONFIG.ROLLING_DAYS}天 ${rollingStartDate} ~ ${apiTodayDate}`,
rolling.rows,
rollingStartDate,
endExclusiveDate
);
// ============================================================
// 输出
// ============================================================
console.log('1)限制窗口概览:刷新周期 UTC / 本地对照');
console.table(windows.map(publicWindowRow));
console.log('2)主 7 天窗口:上次重置至今,按 daily analytics 近似');
console.log('GET', sinceReset.url);
console.table([sinceResetSummary]);
console.log('3)用 used_percent 反推周额度');
console.table([weeklyEstimate]);
console.log('4)上次重置至今每日明细');
console.table(sinceReset.rows);
console.log('4.1)上次重置至今客户端汇总');
console.table(sinceReset.clients);
console.log('5)本月初至今汇总');
console.log('GET', monthToDate.url);
console.table([monthToDateSummary]);
console.log('6)本月初至今日明细');
console.table(monthToDate.rows);
console.log('6.1)本月初至今客户端汇总');
console.table(monthToDate.clients);
console.log(`7)近${CONFIG.ROLLING_DAYS}天汇总`);
console.log('GET', rolling.url);
console.table([rollingSummary]);
console.log(`8)近${CONFIG.ROLLING_DAYS}天每日明细`);
console.table(rolling.rows);
console.log(`8.1)近${CONFIG.ROLLING_DAYS}天客户端汇总`);
console.table(rolling.clients);
console.log(
'说明:end_date 是排他边界;如果今天没有返回,通常是 daily analytics 尚未刷新或当天暂无统计。'
);
return {
配置: {
日期桶模式: CONFIG.DATE_BUCKET_MODE,
USD_PER_CREDIT: CONFIG.USD_PER_CREDIT,
ROLLING_DAYS: CONFIG.ROLLING_DAYS,
},
时区诊断: {
浏览器本地时区:
Intl.DateTimeFormat().resolvedOptions().timeZone || '未知',
浏览器UTC偏移: utcOffsetLabel(apiNowMs),
后端当前_UTC: fmtUTC(apiNowMs),
后端当前_本地: fmtLocal(apiNowMs),
七天窗口开始_UTC: fmtUTC(mainSecondary._windowStartMs),
七天窗口开始_本地: fmtLocal(mainSecondary._windowStartMs),
下次重置_UTC: fmtUTC(mainSecondary._resetAtMs),
下次重置_本地: fmtLocal(mainSecondary._resetAtMs),
API_start_date_上次重置至今: sinceResetStartDate,
API_start_date_本月初至今: monthStartDate,
[`API_start_date_近${CONFIG.ROLLING_DAYS}天`]: rollingStartDate,
API_end_date_排他: endExclusiveDate,
},
限制窗口概览: windows.map(publicWindowRow),
主7天窗口_上次重置至今: {
汇总: sinceResetSummary,
反推周额度: weeklyEstimate,
每日明细: sinceReset.rows,
客户端汇总: sinceReset.clients,
},
本月初至今: {
汇总: monthToDateSummary,
每日明细: monthToDate.rows,
客户端汇总: monthToDate.clients,
},
[`近${CONFIG.ROLLING_DAYS}天`]: {
汇总: rollingSummary,
每日明细: rolling.rows,
客户端汇总: rolling.clients,
},
};
})();
@zzulpc
Copy link
Copy Markdown

zzulpc commented May 20, 2026

似乎获取不了点数和金额了

@XinSSS
Copy link
Copy Markdown

XinSSS commented May 31, 2026

@zzulpc 是的, 我也获取不到了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment