codexmobile-relay / server /codex-quota.js
Codex
deploy: CodexMobile Relay
90f0300
Raw
History Blame Contribute Delete
8.33 kB
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
};
}