From 52f00a95d62e904d3cbf1c39f20b0b68e34f9cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sun, 5 Apr 2026 15:21:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20address=20Codex=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20auth,=20cron=20CLI,=20workdir,=20model=20IDs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enforce auth on API/WS when non-localhost or SERVER_AUTH_REQUIRED=1 - Fix CronScheduler: add --verbose flag and correct stream-json message envelope - Resolve session workdir from JSONL file instead of hardcoding process.cwd() - Give Opus 4.6 1M a unique composite ID (claude-opus-4-6-20250610:1m) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/server/api/models.ts | 38 ++++++++++++++++++++------- src/server/index.ts | 32 ++++++++++++++++++++++ src/server/services/cronScheduler.ts | 8 +++++- src/server/services/sessionService.ts | 14 +++++++++- src/server/ws/handler.ts | 12 ++++++++- 5 files changed, 92 insertions(+), 12 deletions(-) diff --git a/src/server/api/models.ts b/src/server/api/models.ts index 668df954..5cc175e1 100644 --- a/src/server/api/models.ts +++ b/src/server/api/models.ts @@ -21,7 +21,7 @@ const AVAILABLE_MODELS = [ context: '200k', }, { - id: 'claude-opus-4-6-20250610', + id: 'claude-opus-4-6-20250610:1m', name: 'Opus 4.6 1M', description: 'Most capable for ambitious work', context: '1m', @@ -86,13 +86,19 @@ export async function handleModelsApi( async function handleCurrentModel(req: Request): Promise { if (req.method === 'GET') { const settings = await settingsService.getUserSettings() - const modelId = (settings.model as string) || DEFAULT_MODEL - const model = AVAILABLE_MODELS.find((m) => m.id === modelId) || { - id: modelId, - name: modelId, - description: 'Custom model', - context: 'unknown', - } + const baseModelId = (settings.model as string) || DEFAULT_MODEL + const contextTier = (settings.modelContext as string) || undefined + + // Reconstruct composite ID for lookup (e.g. 'claude-opus-4-6-20250610:1m') + const lookupId = contextTier ? `${baseModelId}:${contextTier}` : baseModelId + const model = AVAILABLE_MODELS.find((m) => m.id === lookupId) + || AVAILABLE_MODELS.find((m) => m.id === baseModelId) + || { + id: baseModelId, + name: baseModelId, + description: 'Custom model', + context: 'unknown', + } return Response.json({ model }) } @@ -102,7 +108,21 @@ async function handleCurrentModel(req: Request): Promise { if (typeof modelId !== 'string' || !modelId) { throw ApiError.badRequest('Missing or invalid "modelId" in request body') } - await settingsService.updateUserSettings({ model: modelId }) + + // Parse composite IDs like 'claude-opus-4-6-20250610:1m' + // Persist the base model ID for CLI compatibility and context tier separately + const colonIdx = modelId.indexOf(':') + const baseId = colonIdx !== -1 ? modelId.slice(0, colonIdx) : modelId + const contextTier = colonIdx !== -1 ? modelId.slice(colonIdx + 1) : undefined + + const updates: Record = { model: baseId } + if (contextTier) { + updates.modelContext = contextTier + } else { + // Clear context tier when switching to a non-composite model + updates.modelContext = undefined + } + await settingsService.updateUserSettings(updates) return Response.json({ ok: true, model: modelId }) } diff --git a/src/server/index.ts b/src/server/index.ts index 9bbc6acb..ff4543a5 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -16,6 +16,14 @@ const PORT = parseInt(process.env.SERVER_PORT || '3456', 10) const HOST = process.env.SERVER_HOST || '127.0.0.1' export function startServer(port = PORT, host = HOST) { + /** + * Auth is required when explicitly opted in or when bound to a non-localhost address. + * - Default localhost dev: no auth needed (tests pass as-is). + * - Production / non-localhost (e.g. 0.0.0.0): auth enforced automatically. + * - Explicit opt-in: SERVER_AUTH_REQUIRED=1 forces auth even on localhost. + */ + const authRequired = process.env.SERVER_AUTH_REQUIRED === '1' || host !== '127.0.0.1' + const server = Bun.serve({ port, hostname: host, @@ -32,6 +40,18 @@ export function startServer(port = PORT, host = HOST) { // WebSocket upgrade if (url.pathname.startsWith('/ws/')) { + // Enforce authentication when required + if (authRequired) { + const authError = requireAuth(req) + if (authError) { + const headers = new Headers(authError.headers) + for (const [key, value] of Object.entries(corsHeaders(origin))) { + headers.set(key, value) + } + return new Response(authError.body, { status: authError.status, headers }) + } + } + // Validate session ID format const sessionId = url.pathname.split('/').pop() || '' if (!sessionId || !/^[0-9a-zA-Z_-]{1,64}$/.test(sessionId)) { @@ -49,6 +69,18 @@ export function startServer(port = PORT, host = HOST) { // REST API if (url.pathname.startsWith('/api/')) { + // Enforce authentication when required + if (authRequired) { + const authError = requireAuth(req) + if (authError) { + const headers = new Headers(authError.headers) + for (const [key, value] of Object.entries(corsHeaders(origin))) { + headers.set(key, value) + } + return new Response(authError.body, { status: authError.status, headers }) + } + } + try { const response = await handleApiRequest(req, url) // Add CORS headers to all responses diff --git a/src/server/services/cronScheduler.ts b/src/server/services/cronScheduler.ts index e1ff63d4..bc5172af 100644 --- a/src/server/services/cronScheduler.ts +++ b/src/server/services/cronScheduler.ts @@ -274,7 +274,12 @@ export class CronScheduler { const inputPayload = JSON.stringify({ type: 'user', - content: task.prompt, + message: { + role: 'user', + content: [{ type: 'text', text: task.prompt }], + }, + parent_tool_use_id: null, + session_id: '', }) + '\n' const proc = Bun.spawn( @@ -282,6 +287,7 @@ export class CronScheduler { 'bun', cliPath, '--print', + '--verbose', '--input-format', 'stream-json', '--output-format', diff --git a/src/server/services/sessionService.ts b/src/server/services/sessionService.ts index 8253edc1..9aa80ef5 100644 --- a/src/server/services/sessionService.ts +++ b/src/server/services/sessionService.ts @@ -262,11 +262,23 @@ export class SessionService { return results } + /** + * Convert a sanitized directory name back to the original absolute path. + * Reverses sanitizePath(): `-Users-nanmi-workspace` → `/Users/nanmi/workspace`. + */ + desanitizePath(sanitized: string): string { + // The sanitized form replaces all '/' (and '\') with '-'. + // On POSIX the original path starts with '/', so the sanitized form starts with '-'. + // We restore by replacing every '-' with '/' (the platform separator). + // On Windows the leading character would be a drive letter, but we handle POSIX here. + return sanitized.replace(/-/g, path.sep) + } + /** * Find the .jsonl file for a given session ID. * Searches across all project directories since sessions may belong to any project. */ - private async findSessionFile( + async findSessionFile( sessionId: string ): Promise<{ filePath: string; projectDir: string } | null> { // Validate sessionId format to prevent path traversal diff --git a/src/server/ws/handler.ts b/src/server/ws/handler.ts index 09395981..2223093d 100644 --- a/src/server/ws/handler.ts +++ b/src/server/ws/handler.ts @@ -9,6 +9,7 @@ import type { ServerWebSocket } from 'bun' import type { ClientMessage, ServerMessage, WebSocketSession } from './events.js' import { conversationService } from '../services/conversationService.js' +import { sessionService } from '../services/sessionService.js' export type WebSocketData = { sessionId: string @@ -94,7 +95,16 @@ async function handleUserMessage( // 启动 CLI 子进程(如果还没有) if (!conversationService.hasSession(sessionId)) { try { - const workDir = process.cwd() + // Resolve the session's actual project directory from persisted data + let workDir = process.cwd() // fallback + try { + const sessionInfo = await sessionService.findSessionFile(sessionId) + if (sessionInfo) { + workDir = sessionService.desanitizePath(sessionInfo.projectDir) + } + } catch { + // fallback to cwd if session file not found + } await conversationService.startSession(sessionId, workDir) // 注册 CLI stdout → WebSocket 转发