From 651ab936b70d4d6a5e817c9a273349c9e947598b 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: Wed, 15 Apr 2026 14:18:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=81=9C=E6=AD=A2=E7=94=9F=E6=88=90?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E6=98=BE=E7=A4=BA=E9=94=99=E8=AF=AF=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=20&=20GrowthBook=20clientKey=20=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ws/handler.ts 增加 sessionStopRequested 追踪,用户主动停止时返回 message_complete 而非 error - growthbook.ts 在 isGrowthBookEnabled() 中先检查 clientKey 是否存在 --- src/server/ws/handler.ts | 38 ++++++++++++++++++++++++---- src/services/analytics/growthbook.ts | 2 ++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/server/ws/handler.ts b/src/server/ws/handler.ts index 6975e215..eed6b2d5 100644 --- a/src/server/ws/handler.ts +++ b/src/server/ws/handler.ts @@ -32,6 +32,12 @@ const sessionSlashCommands = new Map>() +/** + * Track sessions where user requested stop — suppress the CLI_ERROR that + * follows an interrupt so the frontend doesn't show "处理过程中发生错误". + */ +const sessionStopRequested = new Set() + /** * Track user message count and title state per session for auto-title generation. */ @@ -147,15 +153,15 @@ export const handleWebSocket = { sessionSlashCommands.delete(sessionId) sessionTitleState.delete(sessionId) - // Schedule delayed cleanup: if the client doesn't reconnect within 5 minutes, + // Schedule delayed cleanup: if the client doesn't reconnect within 30 seconds, // stop the CLI subprocess to avoid leaking resources. const cleanupTimer = setTimeout(() => { sessionCleanupTimers.delete(sessionId) if (!activeSessions.has(sessionId)) { - console.log(`[WS] Session ${sessionId} not reconnected after 5 min, stopping CLI subprocess`) + console.log(`[WS] Session ${sessionId} not reconnected after 30s, stopping CLI subprocess`) conversationService.stopSession(sessionId) } - }, 5 * 60 * 1000) + }, 30_000) sessionCleanupTimers.set(sessionId, cleanupTimer) }, @@ -175,6 +181,9 @@ async function handleUserMessage( const { sessionId } = ws.data let workDir = os.homedir() + // Clear any stale stop flag from a previous turn + sessionStopRequested.delete(sessionId) + // Send thinking status sendMessage(ws, { type: 'status', state: 'thinking', verb: 'Thinking' }) @@ -306,9 +315,19 @@ function handleStopGeneration(ws: ServerWebSocket) { const { sessionId } = ws.data console.log(`[WS] Stop generation requested for session: ${sessionId}`) - // 向 CLI 子进程发送中断信号 + sessionStopRequested.add(sessionId) + if (conversationService.hasSession(sessionId)) { + // First try graceful interrupt via SDK control message conversationService.sendInterrupt(sessionId) + + // Force-kill if still running after 3 seconds + setTimeout(() => { + if (conversationService.hasSession(sessionId)) { + console.log(`[WS] Force-killing CLI subprocess for session: ${sessionId}`) + conversationService.stopSession(sessionId) + } + }, 3_000) } sendMessage(ws, { type: 'status', state: 'idle' }) @@ -587,6 +606,13 @@ function translateCliMessage(cliMsg: any, sessionId: string): ServerMessage[] { } if (cliMsg.is_error) { + // If the user requested stop, this "error" is just the interrupt + // result — don't show it as an error in the chat UI. + if (sessionStopRequested.has(sessionId)) { + sessionStopRequested.delete(sessionId) + return [{ type: 'message_complete', usage }] + } + const resultMessage = (typeof cliMsg.result === 'string' && cliMsg.result) || (Array.isArray(cliMsg.errors) && cliMsg.errors.length > 0 @@ -603,6 +629,8 @@ function translateCliMessage(cliMsg: any, sessionId: string): ServerMessage[] { ] } + // Clear stop flag on successful completion too + sessionStopRequested.delete(sessionId) return [{ type: 'message_complete', usage }] } @@ -717,7 +745,7 @@ async function getRuntimeSettings(): Promise<{ // Otherwise the CLI should use ANTHROPIC_MODEL from env (set by syncToSettings). // Default Anthropic model should be overridden by the provider's model. const baseModel = (userSettings.model as string) || '' - if (baseModel && baseModel !== 'claude-sonnet-4-6-20250514') { + if (baseModel && baseModel !== 'claude-sonnet-4-6') { // User explicitly selected a different model — pass it through model = baseModel if (modelContext) model += `:${modelContext}` diff --git a/src/services/analytics/growthbook.ts b/src/services/analytics/growthbook.ts index c71bba84..8041478a 100644 --- a/src/services/analytics/growthbook.ts +++ b/src/services/analytics/growthbook.ts @@ -420,6 +420,8 @@ function syncRemoteEvalToDisk(): void { * Check if GrowthBook operations should be enabled */ function isGrowthBookEnabled(): boolean { + // No clientKey available in this fork — skip GrowthBook entirely. + if (!getGrowthBookClientKey()) return false // GrowthBook depends on 1P event logging. return is1PEventLoggingEnabled() }