fix: isolate canvas chat sessions during streaming
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-19 13:28:35 +08:00
parent 2d59e3e3c4
commit 2979446337

View File

@ -2227,6 +2227,9 @@ export function CanvasChatPanel({
const [detailModeMessageStartIndex, setDetailModeMessageStartIndex] = useState(0);
const canvasId = useMemo(() => toRawCanvasChatId(chatId), [chatId]);
const [activeChatId, setActiveChatId] = useState<string>(chatId);
const activeChatIdRef = useRef<string>(chatId);
const inFlightChatIdRef = useRef<string | null>(null);
const inFlightRawChatIdRef = useRef<string | null>(null);
const [chatSessions, setChatSessions] = useState<CanvasChatSession[]>([]);
const [isSessionMenuOpen, setIsSessionMenuOpen] = useState(false);
const [selectedSkillTagId, setSelectedSkillTagId] = useState<string | null>(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<string, ChatMessage>();
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<string>("");
@ -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,