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:
@@ -52,10 +52,16 @@ import {
|
||||
getModelConfig,
|
||||
listClaudeAccounts,
|
||||
listCodexAccounts,
|
||||
refreshAllCodexAccounts,
|
||||
refreshCodexAccount,
|
||||
removeAccountDirectory,
|
||||
updateFastMode,
|
||||
updateModelConfig,
|
||||
} from './settings-store.js';
|
||||
import {
|
||||
getCurrentCodexAccountIndex,
|
||||
setCurrentCodexAccountIndex,
|
||||
} from './codex-token-rotation.js';
|
||||
|
||||
const DEFAULT_STATUS_MAX_AGE_MS = 10 * 60 * 1000;
|
||||
const ROOM_MESSAGE_ID_CACHE_LIMIT = 500;
|
||||
@@ -1340,6 +1346,64 @@ export function createWebDashboardHandler(
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const refreshMatch = url.pathname.match(
|
||||
/^\/api\/settings\/accounts\/codex\/(\d+)\/refresh$/,
|
||||
);
|
||||
if (refreshMatch && request.method === 'POST') {
|
||||
const index = Number.parseInt(refreshMatch[1], 10);
|
||||
try {
|
||||
const updated = await refreshCodexAccount(index);
|
||||
return jsonResponse({ ok: true, account: updated });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return jsonResponse({ error: message }, { status: 400 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname === '/api/settings/accounts/codex/refresh-all' &&
|
||||
request.method === 'POST'
|
||||
) {
|
||||
try {
|
||||
const result = await refreshAllCodexAccounts();
|
||||
return jsonResponse({ ok: true, ...result });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return jsonResponse({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname === '/api/settings/accounts/codex/current' &&
|
||||
request.method === 'PUT'
|
||||
) {
|
||||
let body: { index?: unknown } | null = null;
|
||||
try {
|
||||
body = (await request.json()) as { index?: unknown };
|
||||
} catch {
|
||||
return jsonResponse({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
const idx = typeof body?.index === 'number' ? body.index : Number.NaN;
|
||||
if (!Number.isInteger(idx)) {
|
||||
return jsonResponse(
|
||||
{ error: 'index must be an integer' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
try {
|
||||
setCurrentCodexAccountIndex(idx);
|
||||
return jsonResponse({
|
||||
ok: true,
|
||||
codexCurrentIndex: getCurrentCodexAccountIndex(),
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return jsonResponse({ error: message }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/tasks' && request.method === 'POST') {
|
||||
if (!loadRoomBindings) {
|
||||
return jsonResponse(
|
||||
@@ -1538,6 +1602,7 @@ export function createWebDashboardHandler(
|
||||
return jsonResponse({
|
||||
claude: listClaudeAccounts(),
|
||||
codex: listCodexAccounts(),
|
||||
codexCurrentIndex: getCurrentCodexAccountIndex(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user