Extract dashboard settings routes
This commit is contained in:
@@ -47,20 +47,7 @@ import {
|
|||||||
sanitizeScheduledTask,
|
sanitizeScheduledTask,
|
||||||
} from './web-dashboard-data.js';
|
} from './web-dashboard-data.js';
|
||||||
import { handleSimpleGetRoute } from './web-dashboard-routes.js';
|
import { handleSimpleGetRoute } from './web-dashboard-routes.js';
|
||||||
import {
|
import { handleSettingsRoute } from './web-dashboard-settings-routes.js';
|
||||||
addClaudeAccountFromToken,
|
|
||||||
getActiveCodexSettingsIndex,
|
|
||||||
getFastMode,
|
|
||||||
getModelConfig,
|
|
||||||
listClaudeAccounts,
|
|
||||||
listCodexAccounts,
|
|
||||||
refreshAllCodexAccounts,
|
|
||||||
refreshCodexAccount,
|
|
||||||
removeAccountDirectory,
|
|
||||||
setActiveCodexSettingsIndex,
|
|
||||||
updateFastMode,
|
|
||||||
updateModelConfig,
|
|
||||||
} from './settings-store.js';
|
|
||||||
|
|
||||||
const DEFAULT_STATUS_MAX_AGE_MS = 10 * 60 * 1000;
|
const DEFAULT_STATUS_MAX_AGE_MS = 10 * 60 * 1000;
|
||||||
const ROOM_MESSAGE_ID_CACHE_LIMIT = 500;
|
const ROOM_MESSAGE_ID_CACHE_LIMIT = 500;
|
||||||
@@ -1258,155 +1245,12 @@ export function createWebDashboardHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const settingsRoute = await handleSettingsRoute({
|
||||||
url.pathname === '/api/settings/models' &&
|
url,
|
||||||
(request.method === 'PUT' || request.method === 'PATCH')
|
request,
|
||||||
) {
|
jsonResponse,
|
||||||
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 = updateModelConfig(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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)$/,
|
|
||||||
);
|
|
||||||
if (accountAddMatch && request.method === 'POST') {
|
|
||||||
let body: { token?: unknown } | null = null;
|
|
||||||
try {
|
|
||||||
body = (await request.json()) as { token?: unknown };
|
|
||||||
} catch {
|
|
||||||
return jsonResponse({ error: 'Invalid JSON body' }, { status: 400 });
|
|
||||||
}
|
|
||||||
const token = typeof body?.token === 'string' ? body.token.trim() : '';
|
|
||||||
if (!token) {
|
|
||||||
return jsonResponse({ error: 'token is required' }, { status: 400 });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const result = addClaudeAccountFromToken(token);
|
|
||||||
return jsonResponse({ ok: true, ...result });
|
|
||||||
} catch (err) {
|
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
|
||||||
return jsonResponse({ error: message }, { status: 400 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const accountDelMatch = url.pathname.match(
|
|
||||||
/^\/api\/settings\/accounts\/(claude|codex)\/(\d+)$/,
|
|
||||||
);
|
|
||||||
if (accountDelMatch && request.method === 'DELETE') {
|
|
||||||
const provider = accountDelMatch[1] as 'claude' | 'codex';
|
|
||||||
const index = Number.parseInt(accountDelMatch[2], 10);
|
|
||||||
try {
|
|
||||||
removeAccountDirectory(provider, index);
|
|
||||||
return jsonResponse({ ok: true, provider, index });
|
|
||||||
} catch (err) {
|
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
|
||||||
return jsonResponse({ error: message }, { status: 400 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
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) {
|
if (settingsRoute) return settingsRoute;
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
|
||||||
return jsonResponse({ error: message }, { status: 400 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.pathname === '/api/tasks' && request.method === 'POST') {
|
if (url.pathname === '/api/tasks' && request.method === 'POST') {
|
||||||
if (!loadRoomBindings) {
|
if (!loadRoomBindings) {
|
||||||
@@ -1599,22 +1443,6 @@ export function createWebDashboardHandler(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.pathname === '/api/settings/accounts') {
|
|
||||||
return jsonResponse({
|
|
||||||
claude: listClaudeAccounts(),
|
|
||||||
codex: listCodexAccounts(),
|
|
||||||
codexCurrentIndex: getActiveCodexSettingsIndex(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.pathname === '/api/settings/models') {
|
|
||||||
return jsonResponse(getModelConfig());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.pathname === '/api/settings/fast-mode') {
|
|
||||||
return jsonResponse(getFastMode());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.pathname === '/api/stream') {
|
if (url.pathname === '/api/stream') {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
|
|||||||
184
src/web-dashboard-settings-routes.test.ts
Normal file
184
src/web-dashboard-settings-routes.test.ts
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
handleSettingsRoute,
|
||||||
|
type SettingsRouteDependencies,
|
||||||
|
} from './web-dashboard-settings-routes.js';
|
||||||
|
import type {
|
||||||
|
ClaudeAccountSummary,
|
||||||
|
CodexAccountSummary,
|
||||||
|
FastModeSnapshot,
|
||||||
|
ModelConfigSnapshot,
|
||||||
|
} from './settings-store.js';
|
||||||
|
|
||||||
|
function jsonResponse(value: unknown, init?: ResponseInit): Response {
|
||||||
|
return new Response(JSON.stringify(value), {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json; charset=utf-8',
|
||||||
|
...(init?.headers as Record<string, string> | undefined),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function request(
|
||||||
|
pathname: string,
|
||||||
|
method = 'GET',
|
||||||
|
body?: Record<string, unknown>,
|
||||||
|
): Request {
|
||||||
|
return new Request(`http://localhost${pathname}`, {
|
||||||
|
method,
|
||||||
|
body: body === undefined ? undefined : JSON.stringify(body),
|
||||||
|
headers:
|
||||||
|
body === undefined ? undefined : { 'content-type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelConfig: ModelConfigSnapshot = {
|
||||||
|
owner: { model: 'gpt-5.4', effort: 'medium' },
|
||||||
|
reviewer: { model: 'claude-sonnet', effort: 'high' },
|
||||||
|
arbiter: { model: 'gpt-5.4', effort: 'high' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fastMode: FastModeSnapshot = { codex: true, claude: false };
|
||||||
|
|
||||||
|
function makeClaudeAccount(): ClaudeAccountSummary {
|
||||||
|
return {
|
||||||
|
index: 0,
|
||||||
|
expiresAt: 1777348544000,
|
||||||
|
scopes: ['openid'],
|
||||||
|
exists: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCodexAccount(): CodexAccountSummary {
|
||||||
|
return {
|
||||||
|
index: 1,
|
||||||
|
accountId: 'acct_1',
|
||||||
|
email: 'codex@example.com',
|
||||||
|
planType: 'pro',
|
||||||
|
subscriptionUntil: null,
|
||||||
|
subscriptionLastChecked: '2026-04-28T08:00:00.000Z',
|
||||||
|
exists: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDeps(
|
||||||
|
overrides: Partial<SettingsRouteDependencies> = {},
|
||||||
|
): SettingsRouteDependencies {
|
||||||
|
return {
|
||||||
|
addClaudeAccountFromToken: () => ({ index: 2, accountId: null }),
|
||||||
|
getActiveCodexSettingsIndex: () => 1,
|
||||||
|
getFastMode: () => fastMode,
|
||||||
|
getModelConfig: () => modelConfig,
|
||||||
|
listClaudeAccounts: () => [makeClaudeAccount()],
|
||||||
|
listCodexAccounts: () => [makeCodexAccount()],
|
||||||
|
refreshAllCodexAccounts: async () => ({ refreshed: [1], failed: [] }),
|
||||||
|
refreshCodexAccount: async () => makeCodexAccount(),
|
||||||
|
removeAccountDirectory: () => undefined,
|
||||||
|
setActiveCodexSettingsIndex: () => undefined,
|
||||||
|
updateFastMode: () => fastMode,
|
||||||
|
updateModelConfig: () => modelConfig,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function route(
|
||||||
|
pathname: string,
|
||||||
|
method = 'GET',
|
||||||
|
body?: Record<string, unknown>,
|
||||||
|
deps: SettingsRouteDependencies = makeDeps(),
|
||||||
|
): Promise<Response | null> {
|
||||||
|
return handleSettingsRoute({
|
||||||
|
url: new URL(`http://localhost${pathname}`),
|
||||||
|
request: request(pathname, method, body),
|
||||||
|
jsonResponse,
|
||||||
|
deps,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('web dashboard settings routes', () => {
|
||||||
|
it('serves settings snapshots without touching the server router', async () => {
|
||||||
|
const accounts = await route('/api/settings/accounts');
|
||||||
|
expect(accounts?.status).toBe(200);
|
||||||
|
await expect(accounts?.json()).resolves.toMatchObject({
|
||||||
|
claude: [{ index: 0, exists: true }],
|
||||||
|
codex: [{ index: 1, email: 'codex@example.com' }],
|
||||||
|
codexCurrentIndex: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const models = await route('/api/settings/models');
|
||||||
|
expect(models?.status).toBe(200);
|
||||||
|
await expect(models?.json()).resolves.toEqual(modelConfig);
|
||||||
|
|
||||||
|
const mode = await route('/api/settings/fast-mode');
|
||||||
|
expect(mode?.status).toBe(200);
|
||||||
|
await expect(mode?.json()).resolves.toEqual(fastMode);
|
||||||
|
|
||||||
|
const unmatched = await route('/api/overview');
|
||||||
|
expect(unmatched).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles settings mutations through injected dependencies', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = makeDeps({
|
||||||
|
addClaudeAccountFromToken: (token) => {
|
||||||
|
calls.push(`add:${token}`);
|
||||||
|
return { index: 3, accountId: null };
|
||||||
|
},
|
||||||
|
removeAccountDirectory: (provider, index) => {
|
||||||
|
calls.push(`delete:${provider}:${index}`);
|
||||||
|
},
|
||||||
|
setActiveCodexSettingsIndex: (index) => {
|
||||||
|
calls.push(`current:${index}`);
|
||||||
|
},
|
||||||
|
updateModelConfig: (input) => {
|
||||||
|
calls.push(`models:${JSON.stringify(input)}`);
|
||||||
|
return modelConfig;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelUpdate = await route(
|
||||||
|
'/api/settings/models',
|
||||||
|
'PATCH',
|
||||||
|
{ owner: { model: 'gpt-5.5' } },
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
expect(modelUpdate?.status).toBe(200);
|
||||||
|
|
||||||
|
const add = await route(
|
||||||
|
'/api/settings/accounts/claude',
|
||||||
|
'POST',
|
||||||
|
{ token: ' claude-token ' },
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
expect(add?.status).toBe(200);
|
||||||
|
await expect(add?.json()).resolves.toMatchObject({ ok: true, index: 3 });
|
||||||
|
|
||||||
|
const del = await route(
|
||||||
|
'/api/settings/accounts/codex/4',
|
||||||
|
'DELETE',
|
||||||
|
undefined,
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
expect(del?.status).toBe(200);
|
||||||
|
|
||||||
|
const current = await route(
|
||||||
|
'/api/settings/accounts/codex/current',
|
||||||
|
'PUT',
|
||||||
|
{ index: 4 },
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
expect(current?.status).toBe(200);
|
||||||
|
await expect(current?.json()).resolves.toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
codexCurrentIndex: 1,
|
||||||
|
});
|
||||||
|
expect(calls).toEqual([
|
||||||
|
'models:{"owner":{"model":"gpt-5.5"}}',
|
||||||
|
'add:claude-token',
|
||||||
|
'delete:codex:4',
|
||||||
|
'current:4',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
285
src/web-dashboard-settings-routes.ts
Normal file
285
src/web-dashboard-settings-routes.ts
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import {
|
||||||
|
addClaudeAccountFromToken,
|
||||||
|
getActiveCodexSettingsIndex,
|
||||||
|
getFastMode,
|
||||||
|
getModelConfig,
|
||||||
|
listClaudeAccounts,
|
||||||
|
listCodexAccounts,
|
||||||
|
refreshAllCodexAccounts,
|
||||||
|
refreshCodexAccount,
|
||||||
|
removeAccountDirectory,
|
||||||
|
setActiveCodexSettingsIndex,
|
||||||
|
updateFastMode,
|
||||||
|
updateModelConfig,
|
||||||
|
type ClaudeAccountSummary,
|
||||||
|
type CodexAccountSummary,
|
||||||
|
type FastModeSnapshot,
|
||||||
|
type ModelConfigSnapshot,
|
||||||
|
} from './settings-store.js';
|
||||||
|
|
||||||
|
type JsonResponse = (
|
||||||
|
value: unknown,
|
||||||
|
init?: ResponseInit,
|
||||||
|
request?: Request,
|
||||||
|
) => Response;
|
||||||
|
|
||||||
|
export interface SettingsRouteDependencies {
|
||||||
|
addClaudeAccountFromToken: typeof addClaudeAccountFromToken;
|
||||||
|
getActiveCodexSettingsIndex: typeof getActiveCodexSettingsIndex;
|
||||||
|
getFastMode: typeof getFastMode;
|
||||||
|
getModelConfig: typeof getModelConfig;
|
||||||
|
listClaudeAccounts: typeof listClaudeAccounts;
|
||||||
|
listCodexAccounts: typeof listCodexAccounts;
|
||||||
|
refreshAllCodexAccounts: typeof refreshAllCodexAccounts;
|
||||||
|
refreshCodexAccount: typeof refreshCodexAccount;
|
||||||
|
removeAccountDirectory: typeof removeAccountDirectory;
|
||||||
|
setActiveCodexSettingsIndex: typeof setActiveCodexSettingsIndex;
|
||||||
|
updateFastMode: typeof updateFastMode;
|
||||||
|
updateModelConfig: typeof updateModelConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsRouteContext {
|
||||||
|
url: URL;
|
||||||
|
request: Request;
|
||||||
|
jsonResponse: JsonResponse;
|
||||||
|
deps?: SettingsRouteDependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSettingsRouteDependencies: SettingsRouteDependencies = {
|
||||||
|
addClaudeAccountFromToken,
|
||||||
|
getActiveCodexSettingsIndex,
|
||||||
|
getFastMode,
|
||||||
|
getModelConfig,
|
||||||
|
listClaudeAccounts,
|
||||||
|
listCodexAccounts,
|
||||||
|
refreshAllCodexAccounts,
|
||||||
|
refreshCodexAccount,
|
||||||
|
removeAccountDirectory,
|
||||||
|
setActiveCodexSettingsIndex,
|
||||||
|
updateFastMode,
|
||||||
|
updateModelConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
function readMethod(method: string): boolean {
|
||||||
|
return method === 'GET' || method === 'HEAD';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJsonObject(
|
||||||
|
request: Request,
|
||||||
|
jsonResponse: JsonResponse,
|
||||||
|
): Promise<Record<string, unknown> | Response> {
|
||||||
|
let body: unknown = null;
|
||||||
|
try {
|
||||||
|
body = await request.json();
|
||||||
|
} catch {
|
||||||
|
return jsonResponse({ error: 'Invalid JSON body' }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
||||||
|
return jsonResponse(
|
||||||
|
{ error: 'Body must be a JSON object' },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return body as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorMessage(err: unknown): string {
|
||||||
|
return err instanceof Error ? err.message : String(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleModelSettingsRoute(
|
||||||
|
request: Request,
|
||||||
|
jsonResponse: JsonResponse,
|
||||||
|
deps: SettingsRouteDependencies,
|
||||||
|
): Promise<Response | null> {
|
||||||
|
if (readMethod(request.method)) return jsonResponse(deps.getModelConfig());
|
||||||
|
if (request.method !== 'PUT' && request.method !== 'PATCH') return null;
|
||||||
|
|
||||||
|
const body = await readJsonObject(request, jsonResponse);
|
||||||
|
if (body instanceof Response) return body;
|
||||||
|
try {
|
||||||
|
return jsonResponse(deps.updateModelConfig(body));
|
||||||
|
} catch (err) {
|
||||||
|
return jsonResponse({ error: errorMessage(err) }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFastModeRoute(
|
||||||
|
request: Request,
|
||||||
|
jsonResponse: JsonResponse,
|
||||||
|
deps: SettingsRouteDependencies,
|
||||||
|
): Promise<Response | null> {
|
||||||
|
if (readMethod(request.method)) return jsonResponse(deps.getFastMode());
|
||||||
|
if (request.method !== 'PUT' && request.method !== 'PATCH') return null;
|
||||||
|
|
||||||
|
const body = await readJsonObject(request, jsonResponse);
|
||||||
|
if (body instanceof Response) return body;
|
||||||
|
try {
|
||||||
|
return jsonResponse(deps.updateFastMode(body));
|
||||||
|
} catch (err) {
|
||||||
|
return jsonResponse({ error: errorMessage(err) }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAccountsRoute(
|
||||||
|
request: Request,
|
||||||
|
jsonResponse: JsonResponse,
|
||||||
|
deps: SettingsRouteDependencies,
|
||||||
|
): Response | null {
|
||||||
|
if (!readMethod(request.method)) return null;
|
||||||
|
return jsonResponse({
|
||||||
|
claude: deps.listClaudeAccounts(),
|
||||||
|
codex: deps.listCodexAccounts(),
|
||||||
|
codexCurrentIndex: deps.getActiveCodexSettingsIndex(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClaudeAccountAddRoute(
|
||||||
|
request: Request,
|
||||||
|
jsonResponse: JsonResponse,
|
||||||
|
deps: SettingsRouteDependencies,
|
||||||
|
): Promise<Response | null> {
|
||||||
|
if (request.method !== 'POST') return null;
|
||||||
|
|
||||||
|
let body: { token?: unknown } | null = null;
|
||||||
|
try {
|
||||||
|
body = (await request.json()) as { token?: unknown };
|
||||||
|
} catch {
|
||||||
|
return jsonResponse({ error: 'Invalid JSON body' }, { status: 400 });
|
||||||
|
}
|
||||||
|
const token = typeof body?.token === 'string' ? body.token.trim() : '';
|
||||||
|
if (!token)
|
||||||
|
return jsonResponse({ error: 'token is required' }, { status: 400 });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = deps.addClaudeAccountFromToken(token);
|
||||||
|
return jsonResponse({ ok: true, ...result });
|
||||||
|
} catch (err) {
|
||||||
|
return jsonResponse({ error: errorMessage(err) }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAccountDeleteRoute(
|
||||||
|
request: Request,
|
||||||
|
accountMatch: RegExpMatchArray,
|
||||||
|
jsonResponse: JsonResponse,
|
||||||
|
deps: SettingsRouteDependencies,
|
||||||
|
): Response | null {
|
||||||
|
if (request.method !== 'DELETE') return null;
|
||||||
|
const provider = accountMatch[1] as 'claude' | 'codex';
|
||||||
|
const index = Number.parseInt(accountMatch[2], 10);
|
||||||
|
try {
|
||||||
|
deps.removeAccountDirectory(provider, index);
|
||||||
|
return jsonResponse({ ok: true, provider, index });
|
||||||
|
} catch (err) {
|
||||||
|
return jsonResponse({ error: errorMessage(err) }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCodexRefreshRoute(
|
||||||
|
request: Request,
|
||||||
|
refreshMatch: RegExpMatchArray,
|
||||||
|
jsonResponse: JsonResponse,
|
||||||
|
deps: SettingsRouteDependencies,
|
||||||
|
): Promise<Response | null> {
|
||||||
|
if (request.method !== 'POST') return null;
|
||||||
|
const index = Number.parseInt(refreshMatch[1], 10);
|
||||||
|
try {
|
||||||
|
const updated = await deps.refreshCodexAccount(index);
|
||||||
|
return jsonResponse({ ok: true, account: updated });
|
||||||
|
} catch (err) {
|
||||||
|
return jsonResponse({ error: errorMessage(err) }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCodexRefreshAllRoute(
|
||||||
|
request: Request,
|
||||||
|
jsonResponse: JsonResponse,
|
||||||
|
deps: SettingsRouteDependencies,
|
||||||
|
): Promise<Response | null> {
|
||||||
|
if (request.method !== 'POST') return null;
|
||||||
|
try {
|
||||||
|
const result = await deps.refreshAllCodexAccounts();
|
||||||
|
return jsonResponse({ ok: true, ...result });
|
||||||
|
} catch (err) {
|
||||||
|
return jsonResponse({ error: errorMessage(err) }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCodexCurrentRoute(
|
||||||
|
request: Request,
|
||||||
|
jsonResponse: JsonResponse,
|
||||||
|
deps: SettingsRouteDependencies,
|
||||||
|
): Promise<Response | null> {
|
||||||
|
if (request.method !== 'PUT') return null;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
deps.setActiveCodexSettingsIndex(idx);
|
||||||
|
return jsonResponse({
|
||||||
|
ok: true,
|
||||||
|
codexCurrentIndex: deps.getActiveCodexSettingsIndex(),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return jsonResponse({ error: errorMessage(err) }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleSettingsRoute({
|
||||||
|
url,
|
||||||
|
request,
|
||||||
|
jsonResponse,
|
||||||
|
deps = defaultSettingsRouteDependencies,
|
||||||
|
}: SettingsRouteContext): Promise<Response | null> {
|
||||||
|
if (url.pathname === '/api/settings/accounts') {
|
||||||
|
return handleAccountsRoute(request, jsonResponse, deps);
|
||||||
|
}
|
||||||
|
if (url.pathname === '/api/settings/models') {
|
||||||
|
return handleModelSettingsRoute(request, jsonResponse, deps);
|
||||||
|
}
|
||||||
|
if (url.pathname === '/api/settings/fast-mode') {
|
||||||
|
return handleFastModeRoute(request, jsonResponse, deps);
|
||||||
|
}
|
||||||
|
if (url.pathname === '/api/settings/accounts/claude') {
|
||||||
|
return handleClaudeAccountAddRoute(request, jsonResponse, deps);
|
||||||
|
}
|
||||||
|
if (url.pathname === '/api/settings/accounts/codex/refresh-all') {
|
||||||
|
return handleCodexRefreshAllRoute(request, jsonResponse, deps);
|
||||||
|
}
|
||||||
|
if (url.pathname === '/api/settings/accounts/codex/current') {
|
||||||
|
return handleCodexCurrentRoute(request, jsonResponse, deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountMatch = url.pathname.match(
|
||||||
|
/^\/api\/settings\/accounts\/(claude|codex)\/(\d+)$/,
|
||||||
|
);
|
||||||
|
if (accountMatch) {
|
||||||
|
return handleAccountDeleteRoute(request, accountMatch, jsonResponse, deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshMatch = url.pathname.match(
|
||||||
|
/^\/api\/settings\/accounts\/codex\/(\d+)\/refresh$/,
|
||||||
|
);
|
||||||
|
if (refreshMatch) {
|
||||||
|
return handleCodexRefreshRoute(request, refreshMatch, jsonResponse, deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ClaudeAccountSummary,
|
||||||
|
CodexAccountSummary,
|
||||||
|
FastModeSnapshot,
|
||||||
|
ModelConfigSnapshot,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user