From 2979446337b5fb80b65f35923b829a095fd22c4e Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 13:28:35 +0800 Subject: [PATCH] fix: isolate canvas chat sessions during streaming --- components/canvas/CanvasChatPanel.tsx | 77 +++++++++++++++++++-------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index c6ab324f1..55ffcd4bd 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -2227,6 +2227,9 @@ export function CanvasChatPanel({ const [detailModeMessageStartIndex, setDetailModeMessageStartIndex] = useState(0); const canvasId = useMemo(() => toRawCanvasChatId(chatId), [chatId]); const [activeChatId, setActiveChatId] = useState(chatId); + const activeChatIdRef = useRef(chatId); + const inFlightChatIdRef = useRef(null); + const inFlightRawChatIdRef = useRef(null); const [chatSessions, setChatSessions] = useState([]); const [isSessionMenuOpen, setIsSessionMenuOpen] = useState(false); const [selectedSkillTagId, setSelectedSkillTagId] = useState(null); @@ -2536,6 +2539,7 @@ export function CanvasChatPanel({ }, [fullscreenPreview]); useEffect(() => { + activeChatIdRef.current = activeChatId; setTypedAssistantSections({}); setActiveTypingSections({}); setSelectedSkillTagId(null); @@ -4816,16 +4820,19 @@ export function CanvasChatPanel({ }, }), onData: (dataPart: any) => { + const eventChatId = inFlightChatIdRef.current || activeChatId; + const isEventForActiveChat = eventChatId === activeChatIdRef.current; + if (dataPart.type === "data-imageGenerationStatus") { try { const payload: ImageGenerationStatusStreamPayload = typeof dataPart.data === "string" ? JSON.parse(dataPart.data) : dataPart.data; if (!payload || typeof payload !== "object") return; if (payload.phase === "completed") { - clearStreamImageGenerationStateForChat(activeChatId); + clearStreamImageGenerationStateForChat(eventChatId); return; } - setStreamImageGenerationStateForChat(activeChatId, { + setStreamImageGenerationStateForChat(eventChatId, { phase: payload.phase || "processing", providerType: payload.providerType, modelName: getAgentSafeModelName(payload.modelName), @@ -4876,7 +4883,7 @@ export function CanvasChatPanel({ if (!payload || typeof payload !== "object") return; setAgentTargetLockStateByChatId((prev) => ({ ...prev, - [activeChatId]: payload, + [eventChatId]: payload, })); } catch (e) { console.error("[CanvasChat] Failed to parse data-agentTargetLock:", e); @@ -4889,7 +4896,7 @@ export function CanvasChatPanel({ const payload: AgentProgressStreamPayload = typeof dataPart.data === "string" ? JSON.parse(dataPart.data) : dataPart.data; if (!payload || typeof payload !== "object") return; - setAgentProgressStateForChat(activeChatId, { + setAgentProgressStateForChat(eventChatId, { stage: payload.stage || "planning", currentStep: payload.currentStep || "analyze", title: payload.title, @@ -4961,8 +4968,8 @@ export function CanvasChatPanel({ }); if (hasImageGenStartMarker) { const previousState = - streamImageGenerationStateByChatId[activeChatId] || null; - setStreamImageGenerationStateForChat(activeChatId, { + streamImageGenerationStateByChatId[eventChatId] || null; + setStreamImageGenerationStateForChat(eventChatId, { phase: "processing", providerType: previousState?.providerType || @@ -4976,7 +4983,11 @@ export function CanvasChatPanel({ }); } if (hasImageGenEndMarker || incomingImageUrls.length > 0 || incomingVideoUrls.length > 0) { - clearStreamImageGenerationStateForChat(activeChatId); + clearStreamImageGenerationStateForChat(eventChatId); + } + + if (!isEventForActiveChat) { + return; } setMessages((prevMessages) => { @@ -5264,20 +5275,23 @@ export function CanvasChatPanel({ } }, onFinish: async () => { - if (!rawChatId) return; + const finishedChatId = inFlightChatIdRef.current || activeChatId; + const finishedRawChatId = + inFlightRawChatIdRef.current || toRawCanvasChatId(finishedChatId); + if (!finishedRawChatId) return; const syncMessagesFromDatabase = (dbMessages: ChatMessage[]) => { - const historySignature = buildCanvasHistoryMessagesSignature(rawChatId, dbMessages); + if (finishedChatId !== activeChatIdRef.current) { + void mutate(`/api/messages?chatId=${finishedRawChatId}`); + return; + } + + const historySignature = buildCanvasHistoryMessagesSignature(finishedRawChatId, dbMessages); loadedHistorySignatureRef.current = historySignature; processedMessageIds.current = new Set(); hasHydratedHistoryRef.current = true; - setMessages((currentMessages) => { - const mergedMap = new Map(); - currentMessages.forEach((message) => mergedMap.set(message.id, message)); - dbMessages.forEach((message) => mergedMap.set(message.id, message)); - return sortCanvasMessagesByCreatedAt(Array.from(mergedMap.values())); - }); + setMessages(sortCanvasMessagesByCreatedAt(dbMessages)); requestAnimationFrame(() => { messagesEndRef.current?.scrollIntoView({ behavior: "auto", block: "end" }); @@ -5285,7 +5299,7 @@ export function CanvasChatPanel({ }; const reloadMessagesOnce = async () => { - const response = await fetch(`/api/messages?chatId=${rawChatId}&_t=${Date.now()}`, { + const response = await fetch(`/api/messages?chatId=${finishedRawChatId}&_t=${Date.now()}`, { cache: "no-store", }); if (!response.ok) { @@ -5333,17 +5347,27 @@ export function CanvasChatPanel({ } } catch (error) { console.error("[CanvasChat] Failed to sync messages after finish:", error); + } finally { + if (inFlightChatIdRef.current === finishedChatId) { + inFlightChatIdRef.current = null; + inFlightRawChatIdRef.current = null; + } } }, onError: (error) => { - clearStreamImageGenerationStateForChat(activeChatId); - setAgentProgressStateForChat(activeChatId, { + const failedChatId = inFlightChatIdRef.current || activeChatId; + clearStreamImageGenerationStateForChat(failedChatId); + setAgentProgressStateForChat(failedChatId, { stage: "failed", currentStep: "execute", title: "本轮请求失败", message: error instanceof Error ? error.message : "请求失败,请稍后重试", }); - releaseSendGuard(activeChatId); + releaseSendGuard(failedChatId); + if (inFlightChatIdRef.current === failedChatId) { + inFlightChatIdRef.current = null; + inFlightRawChatIdRef.current = null; + } const fallbackMessage = error instanceof Error ? error.message : "生成失败"; const responseData = (error as any)?.data; @@ -5367,7 +5391,9 @@ export function CanvasChatPanel({ }); } - toast.error(fallbackMessage || "生成失败"); + if (failedChatId === activeChatIdRef.current) { + toast.error(fallbackMessage || "生成失败"); + } }, }); @@ -5408,6 +5434,8 @@ export function CanvasChatPanel({ // 强制同步当前模型,避免模型切换后瞬时发送命中旧模型 currentModelIdRef.current = selectedModelId; + inFlightChatIdRef.current = activeChatId; + inFlightRawChatIdRef.current = rawChatId; sendMessage({ role: "user", @@ -5424,7 +5452,7 @@ export function CanvasChatPanel({ if (editableDiv) { editableDiv.innerHTML = ""; } - }, [selectedModelId, status, sendMessage, clearSelection, launchAutoSendTick]); + }, [activeChatId, rawChatId, selectedModelId, status, sendMessage, clearSelection, launchAutoSendTick]); // 当历史消息加载完成后,更新消息列表(支持同一会话的增量刷新) const loadedHistorySignatureRef = useRef(""); @@ -8601,6 +8629,8 @@ export function CanvasChatPanel({ signature: sendSignature, timestamp: Date.now(), }); + inFlightChatIdRef.current = activeChatId; + inFlightRawChatIdRef.current = rawChatId; clearStreamImageGenerationStateForChat(activeChatId); clearAgentProgressStateForChat(activeChatId); @@ -8612,6 +8642,10 @@ export function CanvasChatPanel({ }); } catch (error) { releaseSendGuard(activeChatId); + if (inFlightChatIdRef.current === activeChatId) { + inFlightChatIdRef.current = null; + inFlightRawChatIdRef.current = null; + } contextPayloadRef.current = null; console.error("[CanvasChat] sendMessage failed:", error); toast.error("发送失败,请重试"); @@ -8637,6 +8671,7 @@ export function CanvasChatPanel({ activeComposerMode, agentEntryMode, agentRequiredUploadCount, + rawChatId, input, attachments, hasAgentUploadSlots,