fix: address Codex review findings — auth, cron CLI, workdir, model IDs

- 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) <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-04-05 15:21:55 +08:00
parent ce0908a4e0
commit 52f00a95d6
5 changed files with 92 additions and 12 deletions

View File

@ -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<Response> {
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<Response> {
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<string, unknown> = { 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 })
}

View File

@ -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<WebSocketData>({
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

View File

@ -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',

View File

@ -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

View File

@ -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 转发