From 6ab64efb4ace74353bf6035044553970a88f4da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 23 Jul 2026 13:54:07 +0800 Subject: [PATCH] fix: prevent silent refresh from overwriting newly created conversations in EditorAgentConversation --- .../useEditorAgentConversation.test.tsx | 69 +++++++++++++++++++ .../useEditorAgentConversation.ts | 38 +++++++--- 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx index 3df8d34ea..442f01d79 100644 --- a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx @@ -264,6 +264,75 @@ describe('useEditorAgentConversation', () => { expect(result.current.isLoadingMessages).toBe(false); }); + it('does not let a silent refresh replace a newly created conversation', async () => { + const client = createClient(); + let resolveRefresh!: (detail: EditorAgentConversationDetail) => void; + let resolveCreate!: (detail: EditorAgentConversationDetail) => void; + const refreshPromise = new Promise( + (resolve) => { + resolveRefresh = resolve; + }, + ); + const createPromise = new Promise( + (resolve) => { + resolveCreate = resolve; + }, + ); + const { result } = renderHook(() => + useEditorAgentConversation({ projectId: 'project-1', client }), + ); + + await waitFor(() => { + expect(result.current.activeConversationId).toBe('conversation-1'); + }); + vi.mocked(client.getConversation).mockReturnValueOnce(refreshPromise); + vi.mocked(client.createConversation).mockReturnValueOnce(createPromise); + + let pendingRefresh!: Promise; + let pendingCreate!: Promise; + act(() => { + pendingRefresh = result.current.refreshActiveConversation(); + pendingCreate = result.current.createConversation(); + }); + + await act(async () => { + resolveCreate({ + conversationId: 'conversation-2', + projectId: 'project-1', + title: '新对话', + messages: [], + createdAt: '2026-07-03T00:01:00.000Z', + updatedAt: '2026-07-03T00:01:00.000Z', + }); + await pendingCreate; + }); + expect(result.current.activeConversationId).toBe('conversation-2'); + + await act(async () => { + resolveRefresh({ + conversationId: 'conversation-1', + projectId: 'project-1', + title: '旧对话刷新结果', + messages: [ + { + id: 10, + role: 'assistant', + text: '旧会话消息', + attachments: [], + toolCall: null, + createdAt: '2026-07-03T00:02:00.000Z', + }, + ], + createdAt: '2026-07-03T00:00:00.000Z', + updatedAt: '2026-07-03T00:02:00.000Z', + }); + await pendingRefresh; + }); + + expect(result.current.activeConversationId).toBe('conversation-2'); + expect(result.current.messages).toEqual([]); + }); + it('appends a lazily reconciled tool message delta', async () => { const client = createClient(); const pendingMessage: EditorAgentMessage = { diff --git a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts index 765ba77bf..d7ebd1f13 100644 --- a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts +++ b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts @@ -224,22 +224,34 @@ export function useEditorAgentConversation({ ); const loadConversation = useCallback( - async (conversationId: string, options: { showLoading?: boolean } = {}) => { + async ( + conversationId: string, + options: { + // `activate` may make conversationId active; `refresh` may apply only + // while that same conversationId is still active. + mode: 'activate' | 'refresh'; + showLoading?: boolean; + }, + ) => { const requestId = conversationLoadRequestIdRef.current + 1; conversationLoadRequestIdRef.current = requestId; const showLoading = options.showLoading ?? true; + const canApplyResult = () => + conversationLoadRequestIdRef.current === requestId && + (options.mode === 'activate' || + activeConversationIdRef.current === conversationId); if (showLoading) { setIsLoadingMessages(true); } setErrorMessage(null); try { const detail = await client.getConversation(conversationId); - if (conversationLoadRequestIdRef.current === requestId) { + if (canApplyResult()) { applyConversationDetail(detail); } return detail; } catch (error) { - if (conversationLoadRequestIdRef.current === requestId) { + if (canApplyResult()) { setErrorMessage( error instanceof Error ? error.message : '读取画布 Agent 会话失败', ); @@ -326,6 +338,8 @@ export function useEditorAgentConversation({ createConversationRequestIdRef.current === requestId && normalizedProjectIdRef.current === requestedProjectId ) { + conversationLoadRequestIdRef.current += 1; + setIsLoadingMessages(false); applyConversationDetail(detail); } return detail; @@ -354,7 +368,7 @@ export function useEditorAgentConversation({ if (conversationId === activeConversationId) { return null; } - return loadConversation(conversationId); + return loadConversation(conversationId, { mode: 'activate' }); }, [activeConversationId, loadConversation], ); @@ -364,7 +378,10 @@ export function useEditorAgentConversation({ if (!conversationId) { return null; } - return loadConversation(conversationId, { showLoading: false }); + return loadConversation(conversationId, { + mode: 'refresh', + showLoading: false, + }); }, [loadConversation]); const requestCanvasRefreshForMessages = useCallback( @@ -374,9 +391,9 @@ export function useEditorAgentConversation({ const toolCall = message.toolCall; return Boolean( toolCall?.externalJobId && - (toolCall.images.length > 0 || - (toolCall.videos?.length ?? 0) > 0 || - (toolCall.audios?.length ?? 0) > 0), + (toolCall.images.length > 0 || + (toolCall.videos?.length ?? 0) > 0 || + (toolCall.audios?.length ?? 0) > 0), ); }) ) { @@ -541,6 +558,7 @@ export function useEditorAgentConversation({ } const detail = await loadConversation(conversationId, { + mode: 'refresh', showLoading: false, }); const updatedMessage = detail.messages.find( @@ -596,7 +614,9 @@ export function useEditorAgentConversation({ setConversations(remainingConversations); const nextConversation = remainingConversations[0] ?? null; if (nextConversation) { - await loadConversation(nextConversation.conversationId); + await loadConversation(nextConversation.conversationId, { + mode: 'activate', + }); return; } activeConversationIdRef.current = null;