From 5fc1812d5359eae2357ed0bcfdf0b36dcbee4208 Mon Sep 17 00:00:00 2001 From: Huarch Date: Fri, 5 Jun 2026 13:08:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(chat):=20=E4=BF=AE=E5=A4=8D=20abort=20?= =?UTF-8?q?=E5=90=8E=20progress=20=E4=BB=8D=E6=98=BE=E7=A4=BA=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E4=B8=AD=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/hooks/useAgentChatSession.test.tsx | 60 +++++++++++++++++++ .../chat/hooks/useAgentChatSession.ts | 32 +++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/components/chat/hooks/useAgentChatSession.test.tsx b/src/components/chat/hooks/useAgentChatSession.test.tsx index 10a239d..8e3d84d 100644 --- a/src/components/chat/hooks/useAgentChatSession.test.tsx +++ b/src/components/chat/hooks/useAgentChatSession.test.tsx @@ -353,6 +353,66 @@ describe("useAgentChatSession", () => { expect(abortAgentChat).toHaveBeenCalledWith("session-loaded"); }); + it("finalizes running progress when aborting an active prompt", async () => { + listChatSessions.mockResolvedValue([]); + jest.mocked(streamAgentChat).mockImplementationOnce( + ({ onEvent, signal }) => + new Promise((_, reject) => { + onEvent({ + type: "progress", + sessionId: "session-1", + id: "request-received", + phase: "start", + status: "running", + title: "开始分析", + startedAt: 1000, + } satisfies StreamEvent); + + signal.addEventListener("abort", () => { + reject(new Error("aborted")); + }); + }), + ); + + const { result } = renderHook(() => + useAgentChatSession({ + projectId: "project-1", + onToolCall: jest.fn(), + }), + ); + + await waitFor(() => expect(result.current.isHydrating).toBe(false)); + + act(() => { + void result.current.sendPrompt("测试中断"); + }); + + await waitFor(() => expect(result.current.isStreaming).toBe(true)); + + act(() => { + result.current.abort(); + }); + + await waitFor(() => expect(result.current.isStreaming).toBe(false)); + + expect(result.current.messages.at(-1)).toEqual( + expect.objectContaining({ + role: "assistant", + content: "⚠️ **请求已中断**", + isError: true, + progress: [ + expect.objectContaining({ + id: "request-received", + status: "completed", + durationMs: expect.any(Number), + endedAt: expect.any(Number), + }), + ], + }), + ); + expect(abortAgentChat).toHaveBeenCalledWith("session-1"); + }); + it("ignores generated session titles after the title was edited manually", async () => { listChatSessions.mockResolvedValue([]); jest.mocked(streamAgentChat).mockImplementationOnce(async ({ onEvent }) => { diff --git a/src/components/chat/hooks/useAgentChatSession.ts b/src/components/chat/hooks/useAgentChatSession.ts index cb8150a..7694f6e 100644 --- a/src/components/chat/hooks/useAgentChatSession.ts +++ b/src/components/chat/hooks/useAgentChatSession.ts @@ -130,6 +130,25 @@ const completeRunningProgress = (progress: ChatProgress[] | undefined) => }; }); +const finalizeAssistantMessageAfterAbort = (message: Message): Message => { + const completedProgress = completeRunningProgress(message.progress); + const hasVisibleOutput = + message.content.trim().length > 0 || + Boolean(message.artifacts?.length) || + Boolean(completedProgress?.length); + + if (!hasVisibleOutput) { + return message; + } + + return { + ...message, + content: message.content || "⚠️ **请求已中断**", + isError: true, + progress: completedProgress, + }; +}; + const createUserMessage = (content: string, branchRootId?: string): Message => { const id = createId(); return { @@ -605,6 +624,17 @@ export const useAgentChatSession = ({ const controller = abortRef.current; controller?.abort(); setIsStreaming(false); + const assistantMessageId = getLastAssistantMessageId(); + + if (assistantMessageId) { + setMessages((prev) => + prev.map((message) => + message.id === assistantMessageId + ? finalizeAssistantMessageAfterAbort(message) + : message, + ), + ); + } const cancelPromise = abortAgentChat(sessionIdRef.current).catch((error) => { console.error("[GlobalChatbox] Failed to abort agent session:", error); @@ -615,7 +645,7 @@ export const useAgentChatSession = ({ } }); cancelPromiseRef.current = trackedCancelPromise; - }, []); + }, [getLastAssistantMessageId]); const createSession = useCallback(() => { if (isHydrating || isStreaming) return;