Codex accounts: 6h auto-refresh, manual refresh, manual switch (#48)
Auto refresh - New refreshCodexAccount(index) calls https://auth.openai.com/oauth/token with grant_type=refresh_token and persists rotated tokens back to ~/.codex-accounts/{N}/auth.json. JWT's subscription_active_until reflects the latest plan state OpenAI hands back. - startCodexAccountRefreshLoop() runs every 6h (first run delayed 60s after boot to keep startup snappy). Hooked into main()/shutdown() alongside the existing claude token refresh loop. Manual controls (Settings → 계정 → Codex) - "갱신" button per row: forces an immediate refresh for that account. - "전체 갱신" button: refreshes all codex accounts in sequence. - "전환" button: writes data/codex-rotation-state.json so the next codex spawn picks the chosen account. Active account marked with a green- bordered card and "사용중" badge. Endpoints - POST /api/settings/accounts/codex/{i}/refresh - POST /api/settings/accounts/codex/refresh-all - PUT /api/settings/accounts/codex/current { index } - GET /api/settings/accounts now also returns codexCurrentIndex. Why JWTs cache subscription state at issue-time. When a Pro plan lapses or a user upgrades/downgrades, the dashboard kept showing stale data until the user logged in again. Periodic refresh + explicit "갱신" button keeps the displayed state honest, and the manual switch unblocks the user when rotation lands on a known-bad account. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -381,6 +381,7 @@ export interface FastModeSnapshot {
|
||||
export async function fetchAccounts(): Promise<{
|
||||
claude: ClaudeAccountSummary[];
|
||||
codex: CodexAccountSummary[];
|
||||
codexCurrentIndex?: number;
|
||||
}> {
|
||||
return fetchJson('/api/settings/accounts');
|
||||
}
|
||||
@@ -417,6 +418,64 @@ export async function updateModels(
|
||||
return (await response.json()) as ModelConfigSnapshot;
|
||||
}
|
||||
|
||||
export async function refreshCodexAccount(
|
||||
index: number,
|
||||
): Promise<CodexAccountSummary> {
|
||||
const response = await fetch(
|
||||
`/api/settings/accounts/codex/${index}/refresh`,
|
||||
{ method: 'POST' },
|
||||
);
|
||||
if (!response.ok) {
|
||||
let msg = `refresh failed: ${response.status}`;
|
||||
try {
|
||||
const payload = (await response.json()) as { error?: string };
|
||||
if (payload.error) msg = payload.error;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
const json = (await response.json()) as { account: CodexAccountSummary };
|
||||
return json.account;
|
||||
}
|
||||
|
||||
export async function refreshAllCodexAccounts(): Promise<{
|
||||
refreshed: number[];
|
||||
failed: Array<{ index: number; error: string }>;
|
||||
}> {
|
||||
const response = await fetch('/api/settings/accounts/codex/refresh-all', {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`refresh-all failed: ${response.status}`);
|
||||
}
|
||||
return (await response.json()) as {
|
||||
refreshed: number[];
|
||||
failed: Array<{ index: number; error: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
export async function setCurrentCodexAccount(
|
||||
index: number,
|
||||
): Promise<{ codexCurrentIndex: number }> {
|
||||
const response = await fetch('/api/settings/accounts/codex/current', {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ index }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
let msg = `switch failed: ${response.status}`;
|
||||
try {
|
||||
const payload = (await response.json()) as { error?: string };
|
||||
if (payload.error) msg = payload.error;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
return (await response.json()) as { codexCurrentIndex: number };
|
||||
}
|
||||
|
||||
export async function fetchFastMode(): Promise<FastModeSnapshot> {
|
||||
return fetchJson('/api/settings/fast-mode');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user