#!/usr/bin/env node import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; const DEFAULT_ENDPOINT = 'https://chatgpt.com/backend-api/wham/rate-limit-reset-credits'; const PROXY_REEXEC_FLAG = 'CODEX_RATE_LIMIT_PROXY_REEXECED'; function parseArgs(argv) { const options = { authPath: process.env.CODEX_AUTH_PATH || path.join(os.homedir(), '.codex', 'auth.json'), endpoint: process.env.CODEX_RATE_LIMIT_RESET_CREDITS_URL || DEFAULT_ENDPOINT, proxy: process.env.CODEX_RATE_LIMIT_PROXY || process.env.HTTPS_PROXY || process.env.https_proxy || '', json: false, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--json') { options.json = true; } else if (arg === '--auth') { options.authPath = argv[index + 1]; index += 1; } else if (arg === '--endpoint') { options.endpoint = argv[index + 1]; index += 1; } else if (arg === '--proxy') { options.proxy = argv[index + 1]; index += 1; } else if (arg === '-h' || arg === '--help') { options.help = true; } else { throw new Error(`未知参数: ${arg}`); } } if (!options.authPath) { throw new Error('--auth 需要提供路径'); } if (!options.endpoint) { throw new Error('--endpoint 需要提供 URL'); } if (options.proxy && !options.help) { validateProxyUrl(options.proxy); } return options; } function printHelp() { console.log(`用法: node scripts/codex-rate-limit-reset-credits.mjs [--json] 选项: --json 输出 JSON,便于脚本处理 --auth 指定 Codex auth.json,默认 ~/.codex/auth.json --endpoint 指定接口 URL,默认 ${DEFAULT_ENDPOINT} --proxy 使用代理,例如 http://127.0.0.1:7890 环境变量: CODEX_AUTH_PATH CODEX_RATE_LIMIT_RESET_CREDITS_URL CODEX_RATE_LIMIT_PROXY HTTPS_PROXY / https_proxy 安全: 脚本不会打印 access_token、refresh_token、cookie 或完整唯一 ID。`); } function validateProxyUrl(proxy) { let parsed; try { parsed = new URL(proxy); } catch { throw new Error('--proxy 需要是合法 URL,例如 http://127.0.0.1:7890'); } if (!['http:', 'https:'].includes(parsed.protocol)) { throw new Error('--proxy 目前支持 http:// 或 https:// 代理'); } } function hasUseEnvProxyFlag() { const nodeOptions = process.env.NODE_OPTIONS || ''; return process.execArgv.includes('--use-env-proxy') || nodeOptions.includes('--use-env-proxy'); } function stripProxyArgs(argv) { const stripped = []; for (let index = 0; index < argv.length; index += 1) { if (argv[index] === '--proxy') { index += 1; continue; } stripped.push(argv[index]); } return stripped; } function ensureProxyEnabled(options) { if (!options.proxy || hasUseEnvProxyFlag() || process.env[PROXY_REEXEC_FLAG] === '1') { return false; } if (!process.allowedNodeEnvironmentFlags.has('--use-env-proxy')) { throw new Error('当前 Node 不支持 --use-env-proxy,无法自动启用代理'); } const env = { ...process.env, HTTPS_PROXY: options.proxy, HTTP_PROXY: options.proxy, CODEX_RATE_LIMIT_PROXY: options.proxy, [PROXY_REEXEC_FLAG]: '1', }; const result = spawnSync(process.execPath, ['--use-env-proxy', ...stripProxyArgs(process.argv.slice(1))], { stdio: 'inherit', env, }); if (result.error) { throw result.error; } process.exit(result.status ?? 1); } function readAccessToken(authPath) { const auth = JSON.parse(fs.readFileSync(authPath, 'utf8')); const token = auth?.tokens?.access?.token || auth?.tokens?.access_token; if (!token || typeof token !== 'string') { throw new Error('没有在 auth.json 中找到 access token,已尝试 tokens.access.token 和 tokens.access_token'); } return token; } function formatLocalTime(value) { if (!value) { return null; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZoneName: 'short', }).format(date); } function pickCredits(data) { for (const key of ['credits', 'rate_limit_reset_credits', 'items', 'data']) { if (Array.isArray(data?.[key])) { return data[key]; } } if (Array.isArray(data)) { return data; } return []; } function summarize(data, statusCode) { const credits = pickCredits(data); return { status_code: statusCode, available_count: data?.available_count ?? data?.availableCount ?? null, credits: credits.map((credit) => { const grantedAt = credit?.granted_at ?? credit?.grantedAt ?? null; const expiresAt = credit?.expires_at ?? credit?.expiresAt ?? null; return { status: credit?.status ?? null, title: credit?.title ?? null, granted_at_utc: grantedAt, granted_at_local: formatLocalTime(grantedAt), expires_at_utc: expiresAt, expires_at_local: formatLocalTime(expiresAt), }; }), }; } function printSummary(summary) { console.log(`状态码: ${summary.status_code}`); console.log(`available_count: ${summary.available_count ?? '未知'}`); if (!summary.credits.length) { console.log('credits: 无'); return; } console.table( summary.credits.map((credit, index) => ({ '#': index + 1, status: credit.status, title: credit.title, granted_at: credit.granted_at_local, expires_at: credit.expires_at_local, })), ); } async function fetchCredits(endpoint, accessToken, proxy) { try { return await fetch(endpoint, { headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json', }, }); } catch (error) { const hint = proxy ? '已配置代理,请检查代理地址、端口和本地代理服务是否可用' : '未配置代理;如果当前网络访问 chatgpt.com 不稳定,可使用 --proxy 或 CODEX_RATE_LIMIT_PROXY'; throw new Error(`网络请求失败: ${error.message}。${hint}`); } } async function main() { const options = parseArgs(process.argv.slice(2)); if (options.help) { printHelp(); return; } ensureProxyEnabled(options); const accessToken = readAccessToken(options.authPath); const response = await fetchCredits(options.endpoint, accessToken, options.proxy); const text = await response.text(); let data = null; if (text) { try { data = JSON.parse(text); } catch { data = null; } } if (response.status === 401) { const result = { status_code: 401, message: '401: 凭证失效或没有带对 Authorization header', }; console.log(options.json ? JSON.stringify(result, null, 2) : result.message); process.exitCode = 1; return; } if (!response.ok) { const result = { status_code: response.status, message: `请求失败: HTTP ${response.status}`, }; console.log(options.json ? JSON.stringify(result, null, 2) : result.message); process.exitCode = 1; return; } const summary = summarize(data, response.status); if (options.json) { console.log(JSON.stringify(summary, null, 2)); return; } printSummary(summary); } main().catch((error) => { console.error(`查询失败: ${error.message}`); process.exitCode = 1; });