fix: 使用独立的 appendedMessages 状态避免与 useChat 冲突

参考 lobe-chat-pro 的架构,将 data-appendMessage 消息存储在
独立的状态中,通过 mergedMessages 合并后传递给组件,
避免直接修改 useChat 的内部状态导致消息丢失

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name 2026-02-01 10:29:21 +08:00
parent 3737b1ce99
commit b3d37f815a
2 changed files with 81 additions and 38 deletions

View File

@ -3,7 +3,7 @@
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import useSWR, { useSWRConfig } from "swr";
import { unstable_serialize } from "swr/infinite";
import { ChatHeader } from "@/components/chat-header";
@ -73,6 +73,9 @@ export function Chat({
// 🆕 跟踪是否正在生成图片(用于占位图显示)
const [isGeneratingImageFlag, setIsGeneratingImageFlag] = useState(false);
// 🆕 存储通过 data-appendMessage 添加的消息,避免与 useChat 内部状态冲突
const [appendedMessages, setAppendedMessages] = useState<ChatMessage[]>([]);
// 🆕 当检测到恢复的 pendingGeneration 时,刷新页面获取最新消息
useEffect(() => {
if (pendingGeneration && !hasTriedRefreshRef.current) {
@ -320,28 +323,25 @@ export function Chat({
setIsGeneratingImageFlag(false);
}
// 🆕 立即添加消息到状态,使用 requestAnimationFrame 避免与 useChat 冲突
// 这比延迟到 onFinish 更可靠,因为用户需要立即看到对话消息
requestAnimationFrame(() => {
setMessages((prevMessages) => {
// 检查消息是否已存在
if (prevMessages.some(m => m.id === messageData.id)) {
console.log("[Chat] Message already exists, skipping:", messageData.id);
return prevMessages;
}
const newMessage: any = {
id: messageData.id,
role: messageData.role,
content: messageData.parts?.[0]?.text || "",
createdAt: new Date(),
parts: messageData.parts || [],
attachments: [],
};
console.log("[Chat] Adding message via requestAnimationFrame:", messageData.id);
return [...prevMessages, newMessage];
});
// 🆕 将消息添加到 appendedMessages 状态,而不是直接修改 useChat 的 messages
// 这样可以避免与 useChat 内部状态冲突导致消息丢失
const newMessage: ChatMessage = {
id: messageData.id,
role: messageData.role,
content: messageData.parts?.[0]?.text || "",
createdAt: new Date(),
parts: messageData.parts || [],
attachments: [],
} as ChatMessage;
setAppendedMessages((prev) => {
// 检查消息是否已存在
if (prev.some(m => m.id === messageData.id)) {
console.log("[Chat] Appended message already exists, skipping:", messageData.id);
return prev;
}
console.log("[Chat] Adding to appendedMessages:", messageData.id);
return [...prev, newMessage];
});
// 🆕 收到消息,清除持久化状态
@ -356,7 +356,28 @@ export function Chat({
// 🆕 生成完成,清除持久化状态
completeGeneration();
console.log("[Chat] onFinish called");
// 🆕 移除 router.refresh(),因为它会导致 React 错误和消息丢失
// 🆕 在流式传输完成后,将 appendedMessages 合并到 messages
// 延迟执行以确保 useChat 内部状态已稳定
setTimeout(() => {
setAppendedMessages((prevAppended) => {
if (prevAppended.length > 0) {
console.log("[Chat] Merging appendedMessages to messages:", prevAppended.length);
setMessages((prevMessages) => {
// 过滤掉已存在的消息
const newMessages = prevAppended.filter(
(appended) => !prevMessages.some((m) => m.id === appended.id)
);
if (newMessages.length > 0) {
console.log("[Chat] Actually adding new messages:", newMessages.length);
return [...prevMessages, ...newMessages];
}
return prevMessages;
});
}
return []; // 清空 appendedMessages
});
}, 100);
},
onError: (error) => {
if (error instanceof ChatSDKError) {
@ -1060,6 +1081,23 @@ export function Chat({
setMessages,
});
// 🆕 合并 useChat 的 messages 和 appendedMessages
// appendedMessages 包含通过 data-appendMessage 添加的消息
const mergedMessages = useMemo(() => {
if (appendedMessages.length === 0) {
return messages;
}
// 过滤掉已存在的消息,然后合并
const newMessages = appendedMessages.filter(
(appended) => !messages.some((m) => m.id === appended.id)
);
if (newMessages.length > 0) {
console.log("[Chat] Rendering with merged messages:", messages.length, "+", newMessages.length);
return [...messages, ...newMessages];
}
return messages;
}, [messages, appendedMessages]);
return (
<>
<div className="overscroll-behavior-contain flex h-dvh min-w-0 touch-pan-y flex-col bg-background">
@ -1073,7 +1111,7 @@ export function Chat({
chatId={id}
isArtifactVisible={isArtifactVisible}
isReadonly={isReadonly}
messages={messages}
messages={mergedMessages}
regenerate={regenerate}
sendMessage={sendMessage}
selectedModelId={currentModelId}
@ -1101,7 +1139,7 @@ export function Chat({
attachments={attachments}
chatId={id}
input={input}
messages={messages}
messages={mergedMessages}
onModelChange={setCurrentModelId}
selectedModelId={currentModelId}
selectedVisibilityType={visibilityType}
@ -1134,7 +1172,7 @@ export function Chat({
chatId={id}
input={input}
isReadonly={isReadonly}
messages={messages}
messages={mergedMessages}
regenerate={regenerate}
selectedModelId={currentModelId}
selectedVisibilityType={visibilityType}

View File

@ -220,7 +220,7 @@ function PureMessages({
// 根据模型类型设置延迟时间
const delayTime = (isQianwenVideoModel || isVideoModel) ? 60000 : (isImageGenModel || isAutoModeWithImage) ? 4000 : 6000;
// 🆕 智能自动模式的占位图逻辑(简化版)
// 🆕 智能自动模式的占位图逻辑
if (isAutoModeWithImage) {
console.log("[Messages useEffect] 智能模式检查:", {
hasImageContent,
@ -229,22 +229,27 @@ function PureMessages({
status,
});
// 🆕 简化的占位图逻辑
// 优先级:hasImageContent > isGeneratingImageFlag > isGeneratingImage > status
// 🆕 正确的占位图逻辑
// 优先级:isGeneratingImageFlag > (hasImageContent && status=ready) > isGeneratingImage > status
// 关键:isGeneratingImageFlag 有最高优先级,只有它变 false 且有图片时才隐藏
if (hasImageContent) {
// 图片已生成,延迟后隐藏占位图(无论其他标记如何)
console.log("[Messages useEffect] 图片已生成,延迟后隐藏");
if (isGeneratingImageFlag) {
// 后端正在生成图片,始终显示占位图
console.log("[Messages useEffect] isGeneratingImageFlag=true,强制显示占位图");
setShowThinkingWithDelay(true);
} else if (hasImageContent && status === "ready") {
// 生成完成(flag=false)且有图片内容,延迟后隐藏占位图
console.log("[Messages useEffect] 图片已生成且status=ready,延迟后隐藏");
const timer = setTimeout(() => {
setShowThinkingWithDelay(false);
}, delayTime);
return () => clearTimeout(timer);
} else if (isGeneratingImageFlag) {
// 后端正在生成图片
console.log("[Messages useEffect] isGeneratingImageFlag=true,显示占位图");
setShowThinkingWithDelay(true);
} else if (hasImageContent && !isGeneratingImage) {
// 有图片内容且没有生成标记(可能是历史消息),直接隐藏
console.log("[Messages useEffect] 有图片内容无生成标记,隐藏占位图");
setShowThinkingWithDelay(false);
} else if (isGeneratingImage) {
// 消息中检测到 GENERATING_IMAGE 标记(但没有图片内容)
// 消息中检测到 GENERATING_IMAGE 标记
console.log("[Messages useEffect] 检测到消息中的生成标记,显示占位图");
setShowThinkingWithDelay(true);
} else if (status === "streaming" || status === "submitted") {