From ff8bf20680f384db422f65589aad998d1001b60c Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 12 Jun 2026 11:21:05 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(ai):=20=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E5=BE=85=E5=93=8D=E5=BA=94=E6=B0=94=E6=B3=A1=E6=89=BF=E8=BD=BD?= =?UTF-8?q?=E5=8F=91=E9=80=81=E5=A4=B1=E8=B4=A5=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/AIChatPanel.tsx | 6 + .../ai/aiChatPayloadDispatch.test.ts | 104 ++++++++++++++++ .../components/ai/aiChatPayloadDispatch.ts | 111 ++++++++++++++---- .../src/components/ai/useAIChatLocalTools.ts | 2 + 4 files changed, 197 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/AIChatPanel.tsx b/frontend/src/components/AIChatPanel.tsx index caf39d4..3f7b43a 100644 --- a/frontend/src/components/AIChatPanel.tsx +++ b/frontend/src/components/AIChatPanel.tsx @@ -301,8 +301,10 @@ export const AIChatPanel: React.FC = ({ messages: allMessages, tools: availableTools, addAIChatMessage, + updateAIChatMessage, setSending, nextMessageId: genId, + pendingAssistantMessageId: connectingMsg.id, jvmPlanContext: retryJVMPlanContext, jvmDiagnosticPlanContext: retryJVMDiagnosticPlanContext, }); @@ -319,6 +321,7 @@ export const AIChatPanel: React.FC = ({ getCurrentJVMPlanContext, getCurrentJVMDiagnosticPlanContext, resetToolCallState, + updateAIChatMessage, ]); useAIChatStreamSubscription({ @@ -433,8 +436,10 @@ export const AIChatPanel: React.FC = ({ messages: allMessages, tools: availableTools, addAIChatMessage, + updateAIChatMessage, setSending, nextMessageId: genId, + pendingAssistantMessageId: connectingMsg.id, jvmPlanContext: currentJVMPlanContext, jvmDiagnosticPlanContext: currentJVMDiagnosticPlanContext, unavailableContent: '❌ AI Service 未就绪', @@ -459,6 +464,7 @@ export const AIChatPanel: React.FC = ({ getCurrentJVMPlanContext, getCurrentJVMDiagnosticPlanContext, loadingModels, + updateAIChatMessage, ]); const handleKeyDown = useCallback((e: React.KeyboardEvent) => { diff --git a/frontend/src/components/ai/aiChatPayloadDispatch.test.ts b/frontend/src/components/ai/aiChatPayloadDispatch.test.ts index db4d9b6..4109bd6 100644 --- a/frontend/src/components/ai/aiChatPayloadDispatch.test.ts +++ b/frontend/src/components/ai/aiChatPayloadDispatch.test.ts @@ -78,6 +78,47 @@ describe('aiChatPayloadDispatch', () => { expect(onNonStreamSuccess).toHaveBeenCalled(); }); + it('settles the pending assistant message when falling back to non-stream send', async () => { + const AIChatSend = vi.fn().mockResolvedValue({ + success: true, + content: 'done', + reasoning_content: 'thinking', + }); + const addAIChatMessage = vi.fn(); + const updateAIChatMessage = vi.fn(); + const setSending = vi.fn(); + + (globalThis as any).window = { + go: { + aiservice: { + Service: { AIChatSend }, + }, + }, + }; + + const result = await dispatchAIChatPayload({ + sid: 'session-1', + messages: [{ role: 'user', content: 'hello' }], + tools: [], + addAIChatMessage, + updateAIChatMessage, + setSending, + nextMessageId: () => 'msg-send', + pendingAssistantMessageId: 'assistant-connecting', + }); + + expect(result).toBe('send'); + expect(addAIChatMessage).not.toHaveBeenCalled(); + expect(updateAIChatMessage).toHaveBeenCalledWith('session-1', 'assistant-connecting', expect.objectContaining({ + content: 'done', + thinking: 'thinking', + reasoning_content: 'thinking', + loading: false, + phase: 'idle', + })); + expect(setSending).toHaveBeenCalledWith(false); + }); + it('emits the unavailable message when the AI service is missing', async () => { const addAIChatMessage = vi.fn(); const setSending = vi.fn(); @@ -101,6 +142,33 @@ describe('aiChatPayloadDispatch', () => { expect(setSending).toHaveBeenCalledWith(false); }); + it('settles the pending assistant message when the AI service is missing', async () => { + const addAIChatMessage = vi.fn(); + const updateAIChatMessage = vi.fn(); + const setSending = vi.fn(); + (globalThis as any).window = {}; + + const result = await dispatchAIChatPayload({ + sid: 'session-1', + messages: [{ role: 'user', content: 'hello' }], + tools: [], + addAIChatMessage, + updateAIChatMessage, + setSending, + nextMessageId: () => 'msg-unavailable', + pendingAssistantMessageId: 'assistant-connecting', + }); + + expect(result).toBe('unavailable'); + expect(addAIChatMessage).not.toHaveBeenCalled(); + expect(updateAIChatMessage).toHaveBeenCalledWith('session-1', 'assistant-connecting', expect.objectContaining({ + content: '❌ AI Service 未就绪', + loading: false, + phase: 'idle', + })); + expect(setSending).toHaveBeenCalledWith(false); + }); + it('sanitizes thrown errors and preserves the raw error when the cleaned text changes', async () => { const AIChatSend = vi.fn().mockRejectedValue(new Error('502 Bad Gateway')); const addAIChatMessage = vi.fn(); @@ -131,4 +199,40 @@ describe('aiChatPayloadDispatch', () => { })); expect(setSending).toHaveBeenCalledWith(false); }); + + it('settles the pending assistant message when streaming startup throws', async () => { + const AIChatStream = vi.fn().mockRejectedValue(new Error('502 Bad Gateway')); + const addAIChatMessage = vi.fn(); + const updateAIChatMessage = vi.fn(); + const setSending = vi.fn(); + + (globalThis as any).window = { + go: { + aiservice: { + Service: { AIChatStream }, + }, + }, + }; + + const result = await dispatchAIChatPayload({ + sid: 'session-1', + messages: [{ role: 'user', content: 'hello' }], + tools: [], + addAIChatMessage, + updateAIChatMessage, + setSending, + nextMessageId: () => 'msg-error', + pendingAssistantMessageId: 'assistant-connecting', + }); + + expect(result).toBe('error'); + expect(addAIChatMessage).not.toHaveBeenCalled(); + expect(updateAIChatMessage).toHaveBeenCalledWith('session-1', 'assistant-connecting', expect.objectContaining({ + content: '❌ 发送失败: HTTP 502: 502 Bad Gateway', + rawError: '502 Bad Gateway', + loading: false, + phase: 'idle', + })); + expect(setSending).toHaveBeenCalledWith(false); + }); }); diff --git a/frontend/src/components/ai/aiChatPayloadDispatch.ts b/frontend/src/components/ai/aiChatPayloadDispatch.ts index d429977..14660be 100644 --- a/frontend/src/components/ai/aiChatPayloadDispatch.ts +++ b/frontend/src/components/ai/aiChatPayloadDispatch.ts @@ -16,8 +16,14 @@ interface DispatchAIChatPayloadOptions { messages: any[]; tools: AIChatToolDefinition[]; addAIChatMessage: (sid: string, message: AIChatMessage) => void; + updateAIChatMessage?: ( + sid: string, + messageId: string, + patch: Partial, + ) => void; setSending: (sending: boolean) => void; nextMessageId: () => string; + pendingAssistantMessageId?: string; jvmPlanContext?: JVMAIPlanContext; jvmDiagnosticPlanContext?: JVMDiagnosticPlanContext; unavailableContent?: string; @@ -27,13 +33,53 @@ interface DispatchAIChatPayloadOptions { const getAIChatService = (): AIChatService | undefined => (window as any)?.go?.aiservice?.Service; +const settleAssistantMessage = ({ + sid, + patch, + addAIChatMessage, + updateAIChatMessage, + nextMessageId, + pendingAssistantMessageId, +}: { + sid: string; + patch: Partial; + addAIChatMessage: (sid: string, message: AIChatMessage) => void; + updateAIChatMessage?: ( + sid: string, + messageId: string, + patch: Partial, + ) => void; + nextMessageId: () => string; + pendingAssistantMessageId?: string; +}) => { + const settledPatch: Partial = { + ...patch, + loading: false, + phase: 'idle', + }; + + if (pendingAssistantMessageId && updateAIChatMessage) { + updateAIChatMessage(sid, pendingAssistantMessageId, settledPatch); + return; + } + + addAIChatMessage(sid, { + id: nextMessageId(), + role: 'assistant', + timestamp: Date.now(), + ...settledPatch, + } as AIChatMessage); +}; + export const dispatchAIChatPayload = async ({ sid, messages, tools, addAIChatMessage, + updateAIChatMessage, setSending, nextMessageId, + pendingAssistantMessageId, jvmPlanContext, jvmDiagnosticPlanContext, unavailableContent, @@ -51,16 +97,20 @@ export const dispatchAIChatPayload = async ({ const rawError = result?.error || '未知错误'; const cleanError = sanitizeErrorMsg(rawError); - addAIChatMessage(sid, { - id: nextMessageId(), - role: 'assistant', - content: result?.success ? result.content : `❌ ${cleanError}`, - thinking: result?.success ? result.reasoning_content : undefined, - reasoning_content: result?.success ? result.reasoning_content : undefined, - rawError: !result?.success && cleanError !== rawError ? rawError : undefined, - timestamp: Date.now(), - jvmPlanContext, - jvmDiagnosticPlanContext, + settleAssistantMessage({ + sid, + addAIChatMessage, + updateAIChatMessage, + nextMessageId, + pendingAssistantMessageId, + patch: { + content: result?.success ? result.content : `❌ ${cleanError}`, + thinking: result?.success ? result.reasoning_content : undefined, + reasoning_content: result?.success ? result.reasoning_content : undefined, + rawError: !result?.success && cleanError !== rawError ? rawError : undefined, + jvmPlanContext, + jvmDiagnosticPlanContext, + }, }); setSending(false); if (result?.success) { @@ -69,14 +119,19 @@ export const dispatchAIChatPayload = async ({ return 'send'; } - if (unavailableContent) { - addAIChatMessage(sid, { - id: nextMessageId(), - role: 'assistant', - content: unavailableContent, - timestamp: Date.now(), - jvmPlanContext, - jvmDiagnosticPlanContext, + const resolvedUnavailableContent = unavailableContent || (pendingAssistantMessageId ? '❌ AI Service 未就绪' : ''); + if (resolvedUnavailableContent) { + settleAssistantMessage({ + sid, + addAIChatMessage, + updateAIChatMessage, + nextMessageId, + pendingAssistantMessageId, + patch: { + content: resolvedUnavailableContent, + jvmPlanContext, + jvmDiagnosticPlanContext, + }, }); } setSending(false); @@ -84,14 +139,18 @@ export const dispatchAIChatPayload = async ({ } catch (error: any) { const rawError = error?.message || String(error); const cleanError = sanitizeErrorMsg(rawError); - addAIChatMessage(sid, { - id: nextMessageId(), - role: 'assistant', - content: `❌ 发送失败: ${cleanError}`, - rawError: cleanError !== rawError ? rawError : undefined, - timestamp: Date.now(), - jvmPlanContext, - jvmDiagnosticPlanContext, + settleAssistantMessage({ + sid, + addAIChatMessage, + updateAIChatMessage, + nextMessageId, + pendingAssistantMessageId, + patch: { + content: `❌ 发送失败: ${cleanError}`, + rawError: cleanError !== rawError ? rawError : undefined, + jvmPlanContext, + jvmDiagnosticPlanContext, + }, }); setSending(false); return 'error'; diff --git a/frontend/src/components/ai/useAIChatLocalTools.ts b/frontend/src/components/ai/useAIChatLocalTools.ts index 926c59b..aaa22b7 100644 --- a/frontend/src/components/ai/useAIChatLocalTools.ts +++ b/frontend/src/components/ai/useAIChatLocalTools.ts @@ -217,8 +217,10 @@ export const useAIChatLocalTools = ({ messages: allMessages, tools: chainTools, addAIChatMessage: (sessionId, message) => useStore.getState().addAIChatMessage(sessionId, message), + updateAIChatMessage, setSending, nextMessageId, + pendingAssistantMessageId: chainConnectingMsg.id, jvmPlanContext: inheritedJVMPlanContext, jvmDiagnosticPlanContext: inheritedJVMDiagnosticPlanContext, });