🐛 fix: fix topic messages display error when switch topic quickly (#11542)

* fix Switch topic issues

* clean document

* add abortable mode
This commit is contained in:
Arvin Xu
2026-01-17 10:05:51 +08:00
committed by GitHub
parent 9c9d4b17a9
commit 371d91e091
13 changed files with 37 additions and 71 deletions

View File

@@ -48,8 +48,8 @@ const Conversation = memo(() => {
context={context}
hasInitMessages={!!messages}
messages={messages}
onMessagesChange={(messages) => {
replaceMessages(messages, { context });
onMessagesChange={(messages, ctx) => {
replaceMessages(messages, { context: ctx });
}}
operationState={operationState}
>

View File

@@ -58,8 +58,8 @@ const Conversation = memo<ConversationAreaProps>(({ mobile = false }) => {
hasInitMessages={!!messages}
// hooks={groupHooks}
messages={messages}
onMessagesChange={(messages) => {
replaceMessages(messages, { context });
onMessagesChange={(messages, ctx) => {
replaceMessages(messages, { context: ctx });
}}
operationState={operationState}
>

View File

@@ -47,8 +47,8 @@ const AgentBuilderProvider = memo<AgentBuilderProviderProps>(({ agentId, childre
context={context}
hasInitMessages={!!messages}
messages={messages}
onMessagesChange={(msgs) => {
replaceMessages(msgs, { context });
onMessagesChange={(msgs, ctx) => {
replaceMessages(msgs, { context: ctx });
}}
operationState={operationState}
>

View File

@@ -40,8 +40,8 @@ const SharedMessageList = memo<SharedMessageListProps>(({ agentId, groupId, shar
context={context}
hasInitMessages={!!messages}
messages={messages}
onMessagesChange={(messages) => {
replaceMessages(messages, { context });
onMessagesChange={(messages, ctx) => {
replaceMessages(messages, { context: ctx });
}}
>
<Flexbox flex={1}>

View File

@@ -46,8 +46,8 @@ const AgentBuilderProvider = memo<AgentBuilderProviderProps>(({ agentId, childre
context={context}
hasInitMessages={!!messages}
messages={messages}
onMessagesChange={(msgs) => {
replaceMessages(msgs, { context });
onMessagesChange={(msgs, ctx) => {
replaceMessages(msgs, { context: ctx });
}}
operationState={operationState}
>

View File

@@ -42,8 +42,9 @@ export interface ConversationProviderProps {
* Use this to sync messages back to external state (e.g., ChatStore)
*
* @param messages - The updated messages array
* @param context - The context that this data belongs to (prevents race conditions)
*/
onMessagesChange?: (messages: UIChatMessage[]) => void;
onMessagesChange?: (messages: UIChatMessage[], context: ConversationContext) => void;
/**
* External operation state (from ChatStore)
*

View File

@@ -30,7 +30,7 @@ export interface StoreUpdaterProps {
/**
* Callback when messages are fetched or changed internally
*/
onMessagesChange?: (messages: UIChatMessage[]) => void;
onMessagesChange?: (messages: UIChatMessage[], context: ConversationContext) => void;
/**
* External operation state (from ChatStore)
*/

View File

@@ -33,8 +33,10 @@ export interface State extends DataState, InputState, MessageStateState, VirtuaL
/**
* Callback when messages are fetched or changed internally
* @param messages - The updated messages array
* @param context - The context that this data belongs to (prevents race conditions)
*/
onMessagesChange?: (messages: UIChatMessage[]) => void;
onMessagesChange?: (messages: UIChatMessage[], context: ConversationContext) => void;
/**
* External operation state (from ChatStore)

View File

@@ -74,7 +74,7 @@ export const dataSlice: StateCreator<
});
// Sync changes to external store (ChatStore)
get().onMessagesChange?.(newDbMessages);
get().onMessagesChange?.(newDbMessages, get().context);
},
replaceMessages: (messages) => {
@@ -84,7 +84,7 @@ export const dataSlice: StateCreator<
set({ dbMessages: messages, displayMessages: flatList }, false, 'replaceMessages');
// Sync changes to external store (ChatStore)
get().onMessagesChange?.(messages);
get().onMessagesChange?.(messages, get().context);
},
switchMessageBranch: async (messageId, branchIndex) => {
@@ -106,7 +106,6 @@ export const dataSlice: StateCreator<
// Also skip fetch when topicId is null (new conversation state) - there's no server data,
// only local optimistic updates. Fetching would return empty array and overwrite local data.
const shouldFetch = !skipFetch && !!context.agentId && !!context.topicId;
return useClientDataSWRWithSync<UIChatMessage[]>(
shouldFetch ? ['CONVERSATION_FETCH_MESSAGES', context] : null,
@@ -116,6 +115,7 @@ export const dataSlice: StateCreator<
{
onData: (data) => {
if (!data) return;
if (!context.topicId) return;
// Parse messages using conversation-flow
const { flatList } = parse(data);
@@ -126,8 +126,9 @@ export const dataSlice: StateCreator<
messagesInit: true,
});
// Call onMessagesChange callback if provided
get().onMessagesChange?.(data);
// Call onMessagesChange callback with the request context (not current context)
// This ensures data is stored to the correct topic even if user switched topics
get().onMessagesChange?.(data, context);
},
},
);

View File

@@ -49,8 +49,8 @@ const PageAgentProvider = memo<PageAgentProviderProps>(({ pageAgentId, children
context={context}
hasInitMessages={!!messages}
messages={messages}
onMessagesChange={(msgs) => {
replaceMessages(msgs, { context });
onMessagesChange={(msgs, ctx) => {
replaceMessages(msgs, { context: ctx });
}}
operationState={operationState}
>

View File

@@ -204,8 +204,8 @@ const ThreadChat = memo(() => {
hasInitMessages={!!messages}
hooks={hooks}
messages={messages}
onMessagesChange={(msgs) => {
replaceMessages(msgs, { context });
onMessagesChange={(msgs, ctx) => {
replaceMessages(msgs, { context: ctx });
}}
operationState={operationState}
skipFetch={isCreatingNewThread}

View File

@@ -2,6 +2,8 @@ import { type DocumentItem } from '@lobechat/database/schemas';
import { lambdaClient } from '@/libs/trpc/client';
import { abortableRequest } from '../utils/abortableRequest';
export interface CreateDocumentParams {
content?: string;
editorData: string;
@@ -41,7 +43,15 @@ export class DocumentService {
return lambdaClient.document.queryDocuments.query(params);
}
async getDocumentById(id: string): Promise<DocumentItem | undefined> {
async getDocumentById(id: string, uniqueKey?: string): Promise<DocumentItem | undefined> {
if (uniqueKey) {
// Use fixed key so switching documents cancels the previous request
// This prevents race conditions where old document's data overwrites new document's editor
return abortableRequest.execute(uniqueKey, async (signal) =>
lambdaClient.document.getDocumentById.query({ id }, { signal }),
);
}
return lambdaClient.document.getDocumentById.query({ id });
}

View File

@@ -47,10 +47,6 @@ export interface CrudAction {
* Duplicate an existing page
*/
duplicatePage: (pageId: string) => Promise<{ [key: string]: any; id: string }>;
/**
* Fetch full page detail by ID and update documents array
*/
fetchPageDetail: (pageId: string) => Promise<void>;
navigateToPage: (pageId: string | null) => void;
/**
* Remove a page (deletes from documents table)
@@ -240,50 +236,6 @@ export const createCrudSlice: StateCreator<
return newPage;
},
fetchPageDetail: async (pageId) => {
try {
const document = await documentService.getDocumentById(pageId);
if (!document) {
console.warn(`[fetchPageDetail] Page not found: ${pageId}`);
return;
}
const fullPage: LobeDocument = {
content: document.content || null,
createdAt: document.createdAt ? new Date(document.createdAt) : new Date(),
editorData:
typeof document.editorData === 'string'
? JSON.parse(document.editorData)
: document.editorData || null,
fileType: document.fileType,
filename: document.title || document.filename || 'Untitled',
id: document.id,
metadata: document.metadata || {},
source: 'document',
sourceType: DocumentSourceType.EDITOR,
title: document.title || '',
totalCharCount: document.content?.length || 0,
totalLineCount: 0,
updatedAt: document.updatedAt ? new Date(document.updatedAt) : new Date(),
};
// Update document via internal dispatch
const { documents } = get();
if (documents?.some((doc) => doc.id === pageId)) {
get().internal_dispatchDocuments({
document: fullPage,
id: pageId,
type: 'updateDocument',
});
} else {
get().internal_dispatchDocuments({ document: fullPage, type: 'addDocument' });
}
} catch (error) {
console.error('[fetchPageDetail] Failed to fetch page:', error);
}
},
navigateToPage: (pageId) => {
if (!pageId) {
get().navigate?.('/page');