From 621022ea4f1336eee1c3a7dc15f4bfff329e1418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 16 Jul 2026 18:50:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=20=E4=BC=9A=E8=AF=9D=E5=BF=AB=E9=80=9F?= =?UTF-8?q?=E5=88=87=E6=8D=A2pollute=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../【编辑器】画布Agent对话面板-2026-07-03.md | 1 + .../useEditorAgentConversation.test.tsx | 97 +++++++++++++++++++ .../useEditorAgentConversation.ts | 17 +++- 3 files changed, 111 insertions(+), 4 deletions(-) diff --git a/docs/【编辑器】画布Agent对话面板-2026-07-03.md b/docs/【编辑器】画布Agent对话面板-2026-07-03.md index 526d9c791..a1bfa260c 100644 --- a/docs/【编辑器】画布Agent对话面板-2026-07-03.md +++ b/docs/【编辑器】画布Agent对话面板-2026-07-03.md @@ -61,6 +61,7 @@ - 桌面端对话框固定宽约 360–400px;移动端抽屉式全宽覆盖;收起态为胶囊/圆形入口按钮。 - 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。 - 快速切换会话或会话轮询刷新产生并发详情请求时,前端只允许最后发起的请求更新当前会话、消息、错误和加载态;旧响应不得覆盖用户最新选择。 +- 普通 JSON 消息请求的回包必须绑定发送时的会话:用户在等待期间切换到其他会话后,只更新原会话的列表摘要,不得把原会话的 `deltaMessages` 、错误或画布刷新副作用应用到当前面板。 - 收起对话框只是隐藏面板,不卸载当前会话 hook;普通 JSON 消息请求的等待态和外部生成任务状态必须在收起 / 重新打开之间保持一致。 ## 附件 diff --git a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx index f7e8730d4..40adc07b9 100644 --- a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx @@ -327,6 +327,103 @@ describe('useEditorAgentConversation', () => { ); }); + it('does not apply a completed message response after switching conversations', async () => { + const client = createClient(); + const onCanvasRefreshRequested = vi.fn(); + let resolveSend!: (response: EditorAgentMessageResponse) => void; + vi.mocked(client.sendMessage).mockImplementation( + () => + new Promise((resolve) => { + resolveSend = resolve; + }), + ); + const { result } = renderHook(() => + useEditorAgentConversation({ + projectId: 'project-1', + client, + onCanvasRefreshRequested, + }), + ); + + await waitFor(() => { + expect(result.current.activeConversationId).toBe('conversation-1'); + }); + + let sendPromise!: Promise; + act(() => { + sendPromise = result.current.sendMessage('在第一个会话生成图片'); + }); + await waitFor(() => { + expect(result.current.isWaiting).toBe(true); + }); + + vi.mocked(client.getConversation).mockResolvedValueOnce({ + conversationId: 'conversation-2', + projectId: 'project-1', + title: '第二个会话', + messages: [ + { + id: 20, + role: 'assistant', + text: '第二个会话原有消息', + attachments: [], + toolCall: null, + createdAt: '2026-07-03T00:02:00.000Z', + }, + ], + createdAt: '2026-07-03T00:02:00.000Z', + updatedAt: '2026-07-03T00:02:00.000Z', + }); + await act(async () => { + await result.current.selectConversation('conversation-2'); + }); + + await act(async () => { + resolveSend({ + conversation: { + conversationId: 'conversation-1', + projectId: 'project-1', + title: '第一个会话已更新', + updatedAt: '2026-07-03T00:03:00.000Z', + }, + deltaMessages: [ + { + id: 21, + role: 'assistant', + text: '第一个会话生成完成', + attachments: [], + toolCall: { + toolName: 'generate_image', + status: 'completed', + externalJobId: 'task-conversation-1', + args: {}, + displayArgs: { + stringArgs: [], + imageArgs: [], + extras: { priceMudPoints: 0 }, + }, + images: [{ imageSrc: '/conversation-1-result.png' }], + error: null, + }, + createdAt: '2026-07-03T00:03:00.000Z', + }, + ], + errorMessage: null, + }); + await sendPromise; + }); + + expect(result.current.activeConversationId).toBe('conversation-2'); + expect(result.current.messages).toHaveLength(1); + expect(result.current.messages[0]?.text).toBe('第二个会话原有消息'); + expect(onCanvasRefreshRequested).not.toHaveBeenCalled(); + expect( + result.current.conversations.find( + (conversation) => conversation.conversationId === 'conversation-1', + )?.title, + ).toBe('第一个会话已更新'); + }); + it('creates a conversation before sending when the project has no history', async () => { const client = createClient(); vi.mocked(client.listConversations).mockResolvedValueOnce([]); diff --git a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts index 747dbf69e..e5aa8e447 100644 --- a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts +++ b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts @@ -189,6 +189,7 @@ export function useEditorAgentConversation({ const applyConversationDetail = useCallback( (detail: EditorAgentConversationDetail) => { + activeConversationIdRef.current = detail.conversationId; setConversations((currentConversations) => upsertConversationSummary( currentConversations, @@ -240,6 +241,7 @@ export function useEditorAgentConversation({ setIsLoadingMessages(false); if (!normalizedProjectId) { setConversations([]); + activeConversationIdRef.current = null; setActiveConversationId(null); setMessages([]); setErrorMessage(null); @@ -259,6 +261,7 @@ export function useEditorAgentConversation({ setConversations(sortedConversations); const firstConversation = sortedConversations[0] ?? null; if (!firstConversation) { + activeConversationIdRef.current = null; setActiveConversationId(null); setMessages([]); return; @@ -413,6 +416,9 @@ export function useEditorAgentConversation({ response.conversation, ), ); + if (activeConversationIdRef.current !== conversationId) { + return; + } if (response.errorMessage) { setErrorMessage(response.errorMessage); } else { @@ -424,10 +430,12 @@ export function useEditorAgentConversation({ } const message = error instanceof Error ? error.message : '发送画布 Agent 消息失败'; - setErrorMessage(message); - setMessages((currentMessages) => - currentMessages.filter((message) => message !== optimisticMessage), - ); + if (activeConversationIdRef.current === conversationId) { + setErrorMessage(message); + setMessages((currentMessages) => + currentMessages.filter((message) => message !== optimisticMessage), + ); + } throw error; } finally { if (activeRequestAbortControllerRef.current === abortController) { @@ -532,6 +540,7 @@ export function useEditorAgentConversation({ await loadConversation(nextConversation.conversationId); return; } + activeConversationIdRef.current = null; setActiveConversationId(null); setMessages([]); } catch (error) {