Merge remote-tracking branch 'origin/main' into codex/owner/ejclaw
This commit is contained in:
@@ -256,6 +256,52 @@ export function getActiveCodexAuthPath(): string | null {
|
||||
return getCodexAuthPath();
|
||||
}
|
||||
|
||||
/** Currently active codex account index (after rotation). */
|
||||
export function getCurrentCodexAccountIndex(): number {
|
||||
return accounts.length === 0 ? 0 : currentIndex;
|
||||
}
|
||||
|
||||
/** Find the rotation-array index that owns the given auth.json path. */
|
||||
export function findCodexAccountIndexByAuthPath(
|
||||
authPath: string,
|
||||
): number | null {
|
||||
for (let i = 0; i < accounts.length; i += 1) {
|
||||
if (accounts[i]?.authPath === authPath) return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Manually switch the active codex account. Clears its rate-limit cooldown. */
|
||||
export function setCurrentCodexAccountIndex(targetIndex: number): void {
|
||||
if (accounts.length === 0) {
|
||||
throw new Error('codex token rotation: no accounts loaded');
|
||||
}
|
||||
if (
|
||||
!Number.isInteger(targetIndex) ||
|
||||
targetIndex < 0 ||
|
||||
targetIndex >= accounts.length
|
||||
) {
|
||||
throw new Error(
|
||||
`codex switch: index ${targetIndex} out of range [0..${accounts.length - 1}]`,
|
||||
);
|
||||
}
|
||||
if (targetIndex === currentIndex) return;
|
||||
const previous = currentIndex;
|
||||
currentIndex = targetIndex;
|
||||
// Clear rate-limit on the chosen account so rotation logic doesn't bounce it.
|
||||
accounts[targetIndex].rateLimitedUntil = null;
|
||||
saveCodexState();
|
||||
logger.info(
|
||||
{
|
||||
transition: 'rotation:manual-switch',
|
||||
fromIndex: previous,
|
||||
toIndex: currentIndex,
|
||||
totalAccounts: accounts.length,
|
||||
},
|
||||
`Codex switched manually to account #${currentIndex + 1}/${accounts.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getCodexAuthPath(
|
||||
accountIndex: number = currentIndex,
|
||||
): string | null {
|
||||
|
||||
@@ -59,6 +59,10 @@ import { startWebDashboardServer } from './web-dashboard-server.js';
|
||||
import { Channel, NewMessage, RegisteredGroup } from './types.js';
|
||||
import { logger } from './logger.js';
|
||||
import { initCodexTokenRotation } from './codex-token-rotation.js';
|
||||
import {
|
||||
startCodexAccountRefreshLoop,
|
||||
stopCodexAccountRefreshLoop,
|
||||
} from './settings-store.js';
|
||||
import {
|
||||
hasAvailableClaudeToken,
|
||||
initTokenRotation,
|
||||
@@ -245,6 +249,7 @@ async function main(): Promise<void> {
|
||||
initTokenRotation();
|
||||
initCodexTokenRotation();
|
||||
startTokenRefreshLoop();
|
||||
startCodexAccountRefreshLoop();
|
||||
|
||||
runtimeState.loadState();
|
||||
|
||||
@@ -254,6 +259,7 @@ async function main(): Promise<void> {
|
||||
const shutdown = async (signal: string) => {
|
||||
logger.info({ signal }, 'Shutdown signal received');
|
||||
stopTokenRefreshLoop();
|
||||
stopCodexAccountRefreshLoop();
|
||||
if (leaseRecoveryTimer) {
|
||||
clearInterval(leaseRecoveryTimer);
|
||||
leaseRecoveryTimer = null;
|
||||
|
||||
@@ -15,6 +15,12 @@ import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
findCodexAccountIndexByAuthPath,
|
||||
getActiveCodexAuthPath,
|
||||
setCurrentCodexAccountIndex as setRotationIndex,
|
||||
} from './codex-token-rotation.js';
|
||||
|
||||
export interface ClaudeAccountSummary {
|
||||
index: number;
|
||||
expiresAt: number | null;
|
||||
@@ -27,8 +33,10 @@ export interface ClaudeAccountSummary {
|
||||
export interface CodexAccountSummary {
|
||||
index: number;
|
||||
accountId: string | null;
|
||||
email: string | null;
|
||||
planType: string | null;
|
||||
subscriptionUntil: string | null;
|
||||
subscriptionLastChecked: string | null;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
@@ -43,6 +51,11 @@ export interface ModelConfigSnapshot {
|
||||
arbiter: ModelRoleConfig;
|
||||
}
|
||||
|
||||
export interface FastModeSnapshot {
|
||||
codex: boolean;
|
||||
claude: boolean;
|
||||
}
|
||||
|
||||
const ROLE_KEYS = ['OWNER', 'REVIEWER', 'ARBITER'] as const;
|
||||
type RoleKey = (typeof ROLE_KEYS)[number];
|
||||
|
||||
@@ -104,9 +117,12 @@ function readCodexAccount(index: number): CodexAccountSummary | null {
|
||||
OPENAI_API_KEY?: string;
|
||||
tokens?: { id_token?: string; access_token?: string };
|
||||
}>(file);
|
||||
const live = readCodexLiveStatus(index);
|
||||
let accountId: string | null = null;
|
||||
let email: string | null = null;
|
||||
let planType: string | null = null;
|
||||
let subscriptionUntil: string | null = null;
|
||||
let subscriptionLastChecked: string | null = null;
|
||||
const idToken = data?.tokens?.id_token;
|
||||
if (idToken && typeof idToken === 'string') {
|
||||
const parts = idToken.split('.');
|
||||
@@ -116,6 +132,7 @@ function readCodexAccount(index: number): CodexAccountSummary | null {
|
||||
Buffer.from(parts[1], 'base64').toString('utf-8'),
|
||||
) as Record<string, unknown>;
|
||||
if (typeof payload.sub === 'string') accountId = payload.sub;
|
||||
if (typeof payload.email === 'string') email = payload.email;
|
||||
const auth = payload['https://api.openai.com/auth'] as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
@@ -126,17 +143,28 @@ function readCodexAccount(index: number): CodexAccountSummary | null {
|
||||
if (typeof auth.chatgpt_subscription_active_until === 'string') {
|
||||
subscriptionUntil = auth.chatgpt_subscription_active_until;
|
||||
}
|
||||
if (typeof auth.chatgpt_subscription_last_checked === 'string') {
|
||||
subscriptionLastChecked = auth.chatgpt_subscription_last_checked;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore parse errors */
|
||||
}
|
||||
}
|
||||
}
|
||||
// Live wham/usage data, written by refreshCodexAccount, takes precedence.
|
||||
if (live) {
|
||||
if (typeof live.plan_type === 'string') planType = live.plan_type;
|
||||
if (typeof live.email === 'string') email = live.email;
|
||||
subscriptionLastChecked = live.checked_at;
|
||||
}
|
||||
return {
|
||||
index,
|
||||
accountId,
|
||||
email,
|
||||
planType,
|
||||
subscriptionUntil,
|
||||
subscriptionLastChecked,
|
||||
exists: true,
|
||||
};
|
||||
}
|
||||
@@ -149,6 +177,7 @@ export function listClaudeAccounts(): ClaudeAccountSummary[] {
|
||||
if (fs.existsSync(dir)) {
|
||||
const entries = fs.readdirSync(dir);
|
||||
const indices = entries
|
||||
.filter((e) => /^\d+$/.test(e))
|
||||
.map((e) => Number.parseInt(e, 10))
|
||||
.filter((n) => Number.isFinite(n) && n >= 1)
|
||||
.sort((a, b) => a - b);
|
||||
@@ -168,6 +197,7 @@ export function listCodexAccounts(): CodexAccountSummary[] {
|
||||
if (fs.existsSync(dir)) {
|
||||
const entries = fs.readdirSync(dir);
|
||||
const indices = entries
|
||||
.filter((e) => /^\d+$/.test(e))
|
||||
.map((e) => Number.parseInt(e, 10))
|
||||
.filter((n) => Number.isFinite(n) && n >= 1)
|
||||
.sort((a, b) => a - b);
|
||||
@@ -260,6 +290,351 @@ export function updateModelConfig(
|
||||
return getModelConfig();
|
||||
}
|
||||
|
||||
const CODEX_OAUTH_TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
||||
const CODEX_OAUTH_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
||||
const CODEX_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
|
||||
|
||||
interface CodexAuthFile {
|
||||
auth_mode?: string;
|
||||
OPENAI_API_KEY?: string | null;
|
||||
tokens?: {
|
||||
id_token?: string;
|
||||
access_token?: string;
|
||||
refresh_token?: string;
|
||||
account_id?: string;
|
||||
};
|
||||
last_refresh?: string;
|
||||
}
|
||||
|
||||
interface CodexLiveStatus {
|
||||
checked_at: string;
|
||||
plan_type?: string | null;
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
function planStatusPath(index: number): string {
|
||||
if (index === 0) {
|
||||
return path.join(os.homedir(), '.codex', 'plan-status.json');
|
||||
}
|
||||
return path.join(
|
||||
os.homedir(),
|
||||
'.codex-accounts',
|
||||
String(index),
|
||||
'plan-status.json',
|
||||
);
|
||||
}
|
||||
|
||||
function readCodexLiveStatus(index: number): CodexLiveStatus | null {
|
||||
return readJson<CodexLiveStatus>(planStatusPath(index));
|
||||
}
|
||||
|
||||
function writeCodexLiveStatus(index: number, status: CodexLiveStatus): void {
|
||||
const file = planStatusPath(index);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
const tempPath = `${file}.tmp`;
|
||||
fs.writeFileSync(tempPath, `${JSON.stringify(status, null, 2)}\n`, {
|
||||
mode: 0o600,
|
||||
});
|
||||
fs.renameSync(tempPath, file);
|
||||
}
|
||||
|
||||
async function fetchCodexLivePlanType(
|
||||
accessToken: string,
|
||||
): Promise<{ plan_type?: string; email?: string } | null> {
|
||||
try {
|
||||
const res = await fetch(CODEX_USAGE_URL, {
|
||||
method: 'GET',
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const json = (await res.json()) as {
|
||||
plan_type?: unknown;
|
||||
email?: unknown;
|
||||
};
|
||||
return {
|
||||
plan_type:
|
||||
typeof json.plan_type === 'string' ? json.plan_type : undefined,
|
||||
email: typeof json.email === 'string' ? json.email : undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readCodexAuthFile(file: string): CodexAuthFile | null {
|
||||
return readJson<CodexAuthFile>(file);
|
||||
}
|
||||
|
||||
function writeCodexAuthFile(file: string, data: CodexAuthFile): void {
|
||||
const tempPath = `${file}.tmp`;
|
||||
fs.writeFileSync(tempPath, `${JSON.stringify(data, null, 2)}\n`, {
|
||||
mode: 0o600,
|
||||
});
|
||||
fs.renameSync(tempPath, file);
|
||||
}
|
||||
|
||||
export async function refreshCodexAccount(
|
||||
index: number,
|
||||
): Promise<CodexAccountSummary> {
|
||||
const file = codexAuthPath(index);
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error(`codex auth.json not found for index ${index}`);
|
||||
}
|
||||
const data = readCodexAuthFile(file);
|
||||
const refreshToken = data?.tokens?.refresh_token;
|
||||
if (!refreshToken) {
|
||||
throw new Error(`codex account #${index} has no refresh_token`);
|
||||
}
|
||||
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_id: CODEX_OAUTH_CLIENT_ID,
|
||||
});
|
||||
|
||||
const res = await fetch(CODEX_OAUTH_TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(
|
||||
`codex refresh failed (#${index}): ${res.status} ${text.slice(0, 200)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const payload = (await res.json()) as {
|
||||
access_token?: string;
|
||||
id_token?: string;
|
||||
refresh_token?: string;
|
||||
};
|
||||
|
||||
const updated: CodexAuthFile = {
|
||||
...(data ?? {}),
|
||||
tokens: {
|
||||
...(data?.tokens ?? {}),
|
||||
id_token: payload.id_token ?? data?.tokens?.id_token,
|
||||
access_token: payload.access_token ?? data?.tokens?.access_token,
|
||||
refresh_token: payload.refresh_token ?? refreshToken,
|
||||
},
|
||||
last_refresh: new Date().toISOString(),
|
||||
};
|
||||
writeCodexAuthFile(file, updated);
|
||||
|
||||
// Hit chatgpt.com/backend-api/wham/usage to read the live plan_type
|
||||
// (the JWT id_token's chatgpt_plan_type / *_active_until / *_last_checked
|
||||
// claims are cached on OpenAI's auth0 side and don't refresh on every
|
||||
// OAuth refresh_token grant). Persist the live values to a sidecar
|
||||
// plan-status.json so readCodexAccount can prefer them.
|
||||
const accessToken =
|
||||
payload.access_token ?? data?.tokens?.access_token ?? null;
|
||||
if (accessToken) {
|
||||
const live = await fetchCodexLivePlanType(accessToken);
|
||||
if (live) {
|
||||
writeCodexLiveStatus(index, {
|
||||
checked_at: new Date().toISOString(),
|
||||
plan_type: live.plan_type ?? null,
|
||||
email: live.email ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const summary = readCodexAccount(index);
|
||||
if (!summary)
|
||||
throw new Error('failed to re-read codex account after refresh');
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* settings-store lists codex accounts in an order that includes the default
|
||||
* `~/.codex/auth.json` as index 0 plus `~/.codex-accounts/{N}` as index N.
|
||||
* The rotation array (codex-token-rotation) only loads `~/.codex-accounts/{N}`
|
||||
* when those dirs exist (it ignores `~/.codex/auth.json` in that mode), so its
|
||||
* array indices are off-by-one vs. the settings indices. Translate via path.
|
||||
*/
|
||||
export function getActiveCodexSettingsIndex(): number | null {
|
||||
const activePath = getActiveCodexAuthPath();
|
||||
if (!activePath) return null;
|
||||
// Walk the same listing order as listCodexAccounts() to find the matching
|
||||
// settings-store index.
|
||||
if (codexAuthPath(0) === activePath && fs.existsSync(activePath)) {
|
||||
return 0;
|
||||
}
|
||||
const dir = path.join(os.homedir(), '.codex-accounts');
|
||||
if (fs.existsSync(dir)) {
|
||||
const indices = fs
|
||||
.readdirSync(dir)
|
||||
.filter((e) => /^\d+$/.test(e))
|
||||
.map((e) => Number.parseInt(e, 10))
|
||||
.filter((n) => Number.isFinite(n) && n >= 1)
|
||||
.sort((a, b) => a - b);
|
||||
for (const i of indices) {
|
||||
if (codexAuthPath(i) === activePath) return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function setActiveCodexSettingsIndex(settingsIndex: number): void {
|
||||
const file = codexAuthPath(settingsIndex);
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error(
|
||||
`codex auth.json not found for settings index ${settingsIndex}`,
|
||||
);
|
||||
}
|
||||
const rotationIndex = findCodexAccountIndexByAuthPath(file);
|
||||
if (rotationIndex === null) {
|
||||
throw new Error(
|
||||
`codex switch: settings #${settingsIndex} (${file}) is not part of the rotation pool. ` +
|
||||
`Rotation only manages accounts under ~/.codex-accounts/. ` +
|
||||
`If you want to use ~/.codex/auth.json directly, remove the ~/.codex-accounts dir first.`,
|
||||
);
|
||||
}
|
||||
setRotationIndex(rotationIndex);
|
||||
}
|
||||
|
||||
export async function refreshAllCodexAccounts(): Promise<{
|
||||
refreshed: number[];
|
||||
failed: Array<{ index: number; error: string }>;
|
||||
}> {
|
||||
const refreshed: number[] = [];
|
||||
const failed: Array<{ index: number; error: string }> = [];
|
||||
const accounts = listCodexAccounts();
|
||||
for (const acc of accounts) {
|
||||
try {
|
||||
await refreshCodexAccount(acc.index);
|
||||
refreshed.push(acc.index);
|
||||
} catch (err) {
|
||||
failed.push({
|
||||
index: acc.index,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
return { refreshed, failed };
|
||||
}
|
||||
|
||||
const CODEX_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
||||
let codexRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
export function startCodexAccountRefreshLoop(): void {
|
||||
if (codexRefreshTimer) return;
|
||||
// Stagger first refresh so server boot isn't slowed.
|
||||
setTimeout(() => {
|
||||
void refreshAllCodexAccounts().catch(() => {
|
||||
/* swallow; per-account errors logged inside */
|
||||
});
|
||||
}, 60_000).unref?.();
|
||||
codexRefreshTimer = setInterval(() => {
|
||||
void refreshAllCodexAccounts().catch(() => {
|
||||
/* swallow */
|
||||
});
|
||||
}, CODEX_REFRESH_INTERVAL_MS);
|
||||
codexRefreshTimer.unref?.();
|
||||
}
|
||||
|
||||
export function stopCodexAccountRefreshLoop(): void {
|
||||
if (codexRefreshTimer) {
|
||||
clearInterval(codexRefreshTimer);
|
||||
codexRefreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function codexConfigPath(): string {
|
||||
return path.join(os.homedir(), '.codex', 'config.toml');
|
||||
}
|
||||
|
||||
function claudeSettingsPath(): string {
|
||||
return path.join(os.homedir(), '.claude', 'settings.json');
|
||||
}
|
||||
|
||||
function readCodexFastMode(): boolean {
|
||||
const file = codexConfigPath();
|
||||
if (!fs.existsSync(file)) return false;
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
// [features] section, look for fast_mode = true|false
|
||||
const featuresMatch = content.match(/\[features\][\s\S]*?(?=^\[|\Z)/m);
|
||||
const block = featuresMatch ? featuresMatch[0] : content;
|
||||
const m = block.match(/^\s*fast_mode\s*=\s*(true|false)\s*$/m);
|
||||
return m ? m[1] === 'true' : false;
|
||||
}
|
||||
|
||||
function writeCodexFastMode(value: boolean): void {
|
||||
const file = codexConfigPath();
|
||||
let content = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '';
|
||||
if (/^\s*fast_mode\s*=\s*(true|false)\s*$/m.test(content)) {
|
||||
content = content.replace(
|
||||
/^\s*fast_mode\s*=\s*(true|false)\s*$/m,
|
||||
`fast_mode = ${value}`,
|
||||
);
|
||||
} else if (/^\[features\]/m.test(content)) {
|
||||
content = content.replace(
|
||||
/^\[features\]\s*$/m,
|
||||
`[features]\nfast_mode = ${value}`,
|
||||
);
|
||||
} else {
|
||||
const trimmed = content.replace(/\s*$/, '');
|
||||
content = `${trimmed}\n\n[features]\nfast_mode = ${value}\n`;
|
||||
}
|
||||
const tempPath = `${file}.tmp`;
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(tempPath, content, { mode: 0o600 });
|
||||
fs.renameSync(tempPath, file);
|
||||
}
|
||||
|
||||
function readClaudeFastMode(): boolean {
|
||||
const file = claudeSettingsPath();
|
||||
if (!fs.existsSync(file)) return false;
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(file, 'utf-8')) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
return data.fastMode === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function writeClaudeFastMode(value: boolean): void {
|
||||
const file = claudeSettingsPath();
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
let data: Record<string, unknown> = {};
|
||||
if (fs.existsSync(file)) {
|
||||
try {
|
||||
data = JSON.parse(fs.readFileSync(file, 'utf-8')) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
} catch {
|
||||
data = {};
|
||||
}
|
||||
}
|
||||
data.fastMode = value;
|
||||
const tempPath = `${file}.tmp`;
|
||||
fs.writeFileSync(tempPath, `${JSON.stringify(data, null, 2)}\n`, {
|
||||
mode: 0o600,
|
||||
});
|
||||
fs.renameSync(tempPath, file);
|
||||
}
|
||||
|
||||
export function getFastMode(): FastModeSnapshot {
|
||||
return {
|
||||
codex: readCodexFastMode(),
|
||||
claude: readClaudeFastMode(),
|
||||
};
|
||||
}
|
||||
|
||||
export function updateFastMode(
|
||||
input: Partial<FastModeSnapshot>,
|
||||
): FastModeSnapshot {
|
||||
if (typeof input.codex === 'boolean') writeCodexFastMode(input.codex);
|
||||
if (typeof input.claude === 'boolean') writeClaudeFastMode(input.claude);
|
||||
return getFastMode();
|
||||
}
|
||||
|
||||
export function removeAccountDirectory(
|
||||
provider: 'claude' | 'codex',
|
||||
index: number,
|
||||
|
||||
@@ -48,10 +48,16 @@ import {
|
||||
} from './web-dashboard-data.js';
|
||||
import {
|
||||
addClaudeAccountFromToken,
|
||||
getActiveCodexSettingsIndex,
|
||||
getFastMode,
|
||||
getModelConfig,
|
||||
listClaudeAccounts,
|
||||
listCodexAccounts,
|
||||
refreshAllCodexAccounts,
|
||||
refreshCodexAccount,
|
||||
removeAccountDirectory,
|
||||
setActiveCodexSettingsIndex,
|
||||
updateFastMode,
|
||||
updateModelConfig,
|
||||
} from './settings-store.js';
|
||||
|
||||
@@ -1271,6 +1277,31 @@ export function createWebDashboardHandler(
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname === '/api/settings/fast-mode' &&
|
||||
(request.method === 'PUT' || request.method === 'PATCH')
|
||||
) {
|
||||
let body: unknown = null;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return jsonResponse({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
if (!body || typeof body !== 'object') {
|
||||
return jsonResponse(
|
||||
{ error: 'Body must be a JSON object' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
try {
|
||||
const next = updateFastMode(body as Record<string, unknown>);
|
||||
return jsonResponse(next);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return jsonResponse({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const accountAddMatch = url.pathname.match(
|
||||
/^\/api\/settings\/accounts\/(claude)$/,
|
||||
@@ -1313,6 +1344,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 {
|
||||
setActiveCodexSettingsIndex(idx);
|
||||
return jsonResponse({
|
||||
ok: true,
|
||||
codexCurrentIndex: getActiveCodexSettingsIndex(),
|
||||
});
|
||||
} 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(
|
||||
@@ -1511,6 +1600,7 @@ export function createWebDashboardHandler(
|
||||
return jsonResponse({
|
||||
claude: listClaudeAccounts(),
|
||||
codex: listCodexAccounts(),
|
||||
codexCurrentIndex: getActiveCodexSettingsIndex(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1518,6 +1608,10 @@ export function createWebDashboardHandler(
|
||||
return jsonResponse(getModelConfig());
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/settings/fast-mode') {
|
||||
return jsonResponse(getFastMode());
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/stream') {
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
|
||||
Reference in New Issue
Block a user