import fs from 'node:fs/promises'; import path from 'node:path'; import { CODEX_USAGE_URL, MANAGEMENT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, resolveAuthDir, resolveManagementBaseUrl, resolveManagementKey } from './codex-quota-config.js'; import { authEntryAccountId, authEntryName, authEntryPlan, extractQuotaWindows, maskAccount, normalizePlan, planFromFileName, safeId } from './codex-quota-normalize.js'; export async function readJsonFile(filePath) { const raw = await fs.readFile(filePath, 'utf8'); return JSON.parse(raw); } export function safeErrorMessage(error) { const status = error?.statusCode || error?.status; if (status) { return `HTTP ${status}`; } if (error?.name === 'AbortError') { return '请求超时'; } return '查询失败'; } export async function requestCodexUsage(credential) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); try { const response = await fetch(CODEX_USAGE_URL, { method: 'GET', signal: controller.signal, headers: { Authorization: `Bearer ${credential.access_token}`, 'Chatgpt-Account-Id': credential.account_id, 'Content-Type': 'application/json', 'User-Agent': 'codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal' } }); const text = await response.text(); let body = null; try { body = text ? JSON.parse(text) : null; } catch { body = null; } if (!response.ok) { const error = new Error('Codex quota request failed'); error.statusCode = response.status; throw error; } return body; } finally { clearTimeout(timeout); } } export async function managementJson(baseUrl, managementKey, route, options = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), MANAGEMENT_TIMEOUT_MS); try { const headers = { 'X-Management-Key': managementKey, ...(options.headers || {}) }; const response = await fetch(`${baseUrl}${route}`, { ...options, signal: controller.signal, headers }); const text = await response.text(); let body = null; try { body = text ? JSON.parse(text) : null; } catch { body = null; } if (!response.ok) { const error = new Error(body?.error || `CLIProxyAPI management HTTP ${response.status}`); error.statusCode = response.status; throw error; } return body; } finally { clearTimeout(timeout); } } export async function managementApiCall(baseUrl, managementKey, authIndex, accountId) { const response = await managementJson(baseUrl, managementKey, '/v0/management/api-call', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ authIndex, method: 'GET', url: CODEX_USAGE_URL, header: { Authorization: 'Bearer $TOKEN$', 'Chatgpt-Account-Id': accountId, 'Content-Type': 'application/json', 'User-Agent': 'codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal' } }) }); const statusCode = Number(response?.status_code ?? response?.statusCode ?? 0); const rawBody = response?.body ?? response?.bodyText ?? ''; let body = null; try { body = rawBody ? JSON.parse(rawBody) : null; } catch { body = null; } if (statusCode < 200 || statusCode >= 300) { const error = new Error(body?.error?.message || body?.error || `Codex quota HTTP ${statusCode || 'unknown'}`); error.statusCode = statusCode || 502; throw error; } return body; } export function baseAccount(fileName, credential) { const email = credential.email || fileName.replace(/^codex-/, '').replace(/-[^-]+\.json$/, ''); return { id: safeId(credential.account_id, credential.email, fileName), label: maskAccount(email), plan: normalizePlan(credential.plan_type || credential.planType, planFromFileName(fileName)), disabled: Boolean(credential.disabled), status: 'ok', windows: [] }; } export function baseAccountFromAuthEntry(entry) { const name = authEntryName(entry); const email = entry?.email || entry?.account || entry?.label || name.replace(/^codex-/, '').replace(/-[^-]+\.json$/i, ''); return { id: safeId(entry?.auth_index || entry?.authIndex, entry?.id, email, name), label: maskAccount(email), plan: normalizePlan(authEntryPlan(entry), planFromFileName(name)), disabled: Boolean(entry?.disabled), status: 'ok', windows: [] }; } export async function quotaForFile(authDir, fileName) { const filePath = path.join(authDir, fileName); const credential = await readJsonFile(filePath); const account = baseAccount(fileName, credential); if (account.disabled) { return { ...account, status: 'disabled', error: '已停用' }; } if (!credential.access_token || !credential.account_id) { return { ...account, status: 'failed', error: '凭证缺少额度查询信息' }; } try { const usage = await requestCodexUsage(credential); return { ...account, plan: normalizePlan(usage?.plan_type ?? usage?.planType, account.plan), status: 'ok', windows: extractQuotaWindows(usage) }; } catch (error) { return { ...account, status: 'failed', error: safeErrorMessage(error) }; } } export async function quotaForManagementEntry(baseUrl, managementKey, entry) { const account = baseAccountFromAuthEntry(entry); if (account.disabled) { return { ...account, status: 'disabled', error: '宸插仠鐢?' }; } const authIndex = String(entry?.auth_index || entry?.authIndex || '').trim(); if (!authIndex) { return { ...account, status: 'failed', error: 'missing auth_index' }; } const accountId = authEntryAccountId(entry); if (!accountId) { return { ...account, status: 'failed', error: 'missing account_id' }; } try { const usage = await managementApiCall(baseUrl, managementKey, authIndex, accountId); return { ...account, plan: normalizePlan(usage?.plan_type ?? usage?.planType, account.plan), status: 'ok', windows: extractQuotaWindows(usage) }; } catch (error) { return { ...account, status: 'failed', error: safeErrorMessage(error) }; } } export async function getCodexQuotaFromManagement() { const managementKey = await resolveManagementKey(); if (!managementKey) { return null; } const baseUrl = await resolveManagementBaseUrl(); const payload = await managementJson(baseUrl, managementKey, '/v0/management/auth-files'); const entries = (Array.isArray(payload?.files) ? payload.files : []) .filter((entry) => { const provider = String(entry?.provider || entry?.type || '').trim().toLowerCase(); const name = authEntryName(entry).toLowerCase(); return provider === 'codex' || name.startsWith('codex-'); }); const accounts = await Promise.all( entries.map((entry) => quotaForManagementEntry(baseUrl, managementKey, entry)) ); return { provider: 'cliproxyapi', source: 'cliproxyapi-management', accounts }; } export async function getCodexQuota() { try { const managed = await getCodexQuotaFromManagement(); if (managed) { return managed; } } catch (error) { console.warn(`[quota] CLIProxyAPI management quota fallback: ${safeErrorMessage(error)}`); } const authDir = await resolveAuthDir(); let files = []; try { files = (await fs.readdir(authDir)) .filter((fileName) => /^codex-.+\.json$/i.test(fileName)) .sort((a, b) => a.localeCompare(b)); } catch (error) { error.statusCode = 500; throw error; } const accounts = await Promise.all( files.map(async (fileName) => { try { return await quotaForFile(authDir, fileName); } catch { return { id: safeId(fileName), label: maskAccount(fileName.replace(/^codex-/, '').replace(/\.json$/, '')), plan: normalizePlan(planFromFileName(fileName)), disabled: false, status: 'failed', error: '凭证读取失败', windows: [] }; } }) ); return { provider: 'cliproxyapi', accounts }; }