fix: sync canvas generation results immediately
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-13 20:36:08 +08:00
parent 1b1ff65de8
commit 86b1ee9eeb

View File

@ -784,6 +784,135 @@ function formatMessageDateLabel(timestamp: number): string {
}).format(new Date(timestamp));
}
function sortCanvasMessagesByCreatedAt(messages: ChatMessage[]): ChatMessage[] {
return [...messages].sort((a, b) => {
const timeA = getMessageTimestamp(a) ?? 0;
const timeB = getMessageTimestamp(b) ?? 0;
return timeA - timeB;
});
}
function buildCanvasMessageFingerprint(message: ChatMessage): string {
const parts = Array.isArray(message.parts) ? message.parts : [];
const partFingerprint = parts
.slice(0, 8)
.map((part: any) => {
if (!part) return "part:null";
if (part.type === "text") {
const text = String(part.text || "").trim();
return `text:${text.length}:${text.slice(0, 80)}`;
}
const url =
typeof part.url === "string" && part.url.trim()
? part.url.trim()
: typeof part.data === "string" && part.data.trim()
? part.data.trim()
: "";
return `${String(part.type || "part")}:${String(
part.mediaType || part.mimeType || part.contentType || ""
)}:${url.slice(0, 120)}`;
})
.join("|");
const attachmentFingerprint = (((message as any).attachments || []) as any[])
.slice(0, 6)
.map((attachment) => {
const url = String(attachment?.url || "").trim();
return `${String(attachment?.contentType || "")}:${url.slice(0, 120)}`;
})
.join("|");
return [
message.id,
message.role,
String(getMessageTimestamp(message) ?? 0),
partFingerprint,
attachmentFingerprint,
].join("::");
}
function buildCanvasHistoryMessagesSignature(
rawChatId: string,
messages: ChatMessage[],
): string {
const recentFingerprint = messages
.slice(-12)
.map((message) => buildCanvasMessageFingerprint(message))
.join("||");
return `${rawChatId}:${messages.length}:${recentFingerprint}`;
}
function hasCanvasImageMarkdown(parts?: Array<any>) {
if (!parts || parts.length === 0) return false;
return parts.some((part) => {
if (!part) return false;
if (
part?.type === "text" &&
typeof part.text === "string" &&
(part.text.includes("![thumb|") ||
part.text.includes("![thumb|||") ||
part.text.includes("![Generated Image"))
) {
return true;
}
if (part?.type === "image" && typeof part.url === "string" && part.url.length > 0) {
return true;
}
if (
part?.type === "file" &&
typeof part.url === "string" &&
((typeof part.mediaType === "string" && part.mediaType.startsWith("image/")) ||
/\.(png|jpe?g|webp|gif|bmp|svg)(\?|$)/i.test(part.url))
) {
return true;
}
return false;
});
}
function hasCanvasLikelyGenerationErrorMessage(message?: ChatMessage | null) {
if (!message?.parts || message.parts.length === 0) return false;
const text = message.parts
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
.map((part: any) => part.text)
.join("\n")
.trim();
if (!text) return false;
return /失败|超时|重试|error|failed/i.test(text);
}
function getCanvasAssistantMessagesAfterTargetUser(
dbMessages: ChatMessage[],
targetUserMessageId?: string | null,
) {
if (!Array.isArray(dbMessages) || dbMessages.length === 0) {
return [] as ChatMessage[];
}
let targetUserIndex = -1;
if (targetUserMessageId) {
targetUserIndex = dbMessages.findIndex(
(message) => message.role === "user" && message.id === targetUserMessageId,
);
}
if (targetUserIndex < 0) {
targetUserIndex = dbMessages.findLastIndex((message) => message.role === "user");
}
if (targetUserIndex < 0) {
return dbMessages.filter((message) => message.role === "assistant");
}
return dbMessages
.slice(targetUserIndex + 1)
.filter((message) => message.role === "assistant");
}
const CANVAS_IMAGE_ANALYSIS_TITLE = "图片分析";
const CANVAS_IMAGE_DONE_SUMMARY = "已完成本次生成,如需微调可继续告诉我。";
const CANVAS_AGENT_SECTION_TITLES = new Set([
@ -5091,6 +5220,78 @@ export function CanvasChatPanel({
}
}
},
onFinish: async () => {
if (!rawChatId) return;
const syncMessagesFromDatabase = (dbMessages: ChatMessage[]) => {
const historySignature = buildCanvasHistoryMessagesSignature(rawChatId, 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()));
});
requestAnimationFrame(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
});
};
const reloadMessagesOnce = async () => {
const response = await fetch(`/api/messages?chatId=${rawChatId}&_t=${Date.now()}`, {
cache: "no-store",
});
if (!response.ok) {
return { hasResult: false, hasError: false };
}
const data = await response.json().catch(() => ({}));
if (!Array.isArray(data?.messages)) {
return { hasResult: false, hasError: false };
}
const dbMessages = data.messages as ChatMessage[];
syncMessagesFromDatabase(dbMessages);
const assistantMessages = getCanvasAssistantMessagesAfterTargetUser(dbMessages);
const hasResult = assistantMessages.some((message) => {
if (hasCanvasImageMarkdown((message as any).parts)) return true;
const text = ((message as any).parts || [])
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
.map((part: any) => String(part.text))
.join("\n");
return extractAssistantImageUrls(text).length > 0 || extractVideoUrls(text).length > 0;
});
const hasError = assistantMessages.some((message) =>
hasCanvasLikelyGenerationErrorMessage(message),
);
return { hasResult, hasError };
};
try {
await new Promise((resolve) => window.setTimeout(resolve, 450));
let { hasResult, hasError } = await reloadMessagesOnce();
if (!hasResult && !hasError) {
for (let attempt = 0; attempt < 6; attempt += 1) {
await new Promise((resolve) => window.setTimeout(resolve, 500));
const next = await reloadMessagesOnce();
hasResult = next.hasResult;
hasError = next.hasError;
if (hasResult || hasError) {
break;
}
}
}
} catch (error) {
console.error("[CanvasChat] Failed to sync messages after finish:", error);
}
},
onError: (error) => {
clearStreamImageGenerationStateForChat(activeChatId);
setAgentProgressStateForChat(activeChatId, {
@ -5195,8 +5396,7 @@ export function CanvasChatPanel({
if ((status === "streaming" || status === "submitted") && !shouldForceHydrate) return;
const historyMessages = historyData.messages || [];
const lastMessageId = historyMessages.at(-1)?.id || "none";
const signature = `${rawChatId}:${historyMessages.length}:${lastMessageId}`;
const signature = buildCanvasHistoryMessagesSignature(rawChatId, historyMessages);
if (loadedHistorySignatureRef.current === signature) return;