diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 1e0fd3568..0d32e143c 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -2640,105 +2640,122 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF useEffect(() => { if (!hasHydratedHistoryRef.current) return; - // 只处理新消息 - messages.forEach(async (message, messageIndex) => { - if (message.role === 'assistant' && !processedMessageIds.current.has(message.id)) { - const content = message.parts?.map((p: any) => p.text || '').join('') || ''; + let cancelled = false; + + const processNewAssistantMessages = async () => { + const existingImageUrlKeys = new Set( + nodes + .filter((n) => n.type === "image" && (n.data as any)?.src) + .map((n) => normalizeMediaUrl((n.data as any).src)) + .filter(Boolean) + ); + const existingVideoUrlKeys = new Set( + nodes + .filter((n) => n.type === "video" && (n.data as any)?.src) + .map((n) => normalizeMediaUrl((n.data as any).src)) + .filter(Boolean) + ); + + for (let messageIndex = 0; messageIndex < messages.length; messageIndex += 1) { + if (cancelled) return; + const message = messages[messageIndex]; + if (message.role !== "assistant" || processedMessageIds.current.has(message.id)) { + continue; + } + + const content = message.parts?.map((p: any) => p.text || "").join("") || ""; const imageUrls = extractImageUrls(content); const videoUrls = extractVideoUrls(content); + if (imageUrls.length === 0 && videoUrls.length === 0) continue; + + processedMessageIds.current.add(message.id); const generationPrompt = findNearestUserPrompt(messages, messageIndex); - if (imageUrls.length > 0 || videoUrls.length > 0) { - processedMessageIds.current.add(message.id); + let addedImageCount = 0; + let addedVideoCount = 0; - let addedImageCount = 0; - let addedVideoCount = 0; + const newImageUrls = imageUrls.filter((url) => { + const key = normalizeMediaUrl(url); + if (!key || existingImageUrlKeys.has(key)) return false; + existingImageUrlKeys.add(key); + return true; + }); - if (imageUrls.length > 0) { - // 检查画布中是否已存在相同 URL 的图片 - const existingUrls = new Set( - nodes.filter(n => n.type === 'image' && (n.data as any)?.src) - .map(n => (n.data as any).src) - ); - - // 只添加不存在的图片 - const newUrls = imageUrls.filter(url => !existingUrls.has(url)); - - // 异步获取每张图片的尺寸并添加到画布 - for (let index = 0; index < newUrls.length; index++) { - const url = newUrls[index]; - // 获取图片原始尺寸 - const dimensions = await getImageDimensions(url); - - // 计算位置(避免重叠,根据图片宽度动态调整间距) - const existingNodes = nodes.length + index; - const spacing = 20; // 图片间距 - const x = 100 + (existingNodes % 3) * (dimensions.width + spacing); - const y = 100 + Math.floor(existingNodes / 3) * (dimensions.height + spacing); - - const nodeId = addNode({ - type: 'image', - position: { x, y }, - size: { width: dimensions.width, height: dimensions.height }, - data: { - src: url, - status: 'completed', - title: '图片', - prompt: generationPrompt || '对话生成', - metadata: { - originalWidth: dimensions.originalWidth, - originalHeight: dimensions.originalHeight, - generationPrompt: generationPrompt || undefined, - sourceMessageId: message.id, - }, + for (let index = 0; index < newImageUrls.length; index += 1) { + if (cancelled) return; + const url = newImageUrls[index]; + const dimensions = await getImageDimensions(url); + const existingNodes = nodes.length + addedImageCount + addedVideoCount; + const spacing = 20; + const x = 100 + (existingNodes % 3) * (dimensions.width + spacing); + const y = 100 + Math.floor(existingNodes / 3) * (dimensions.height + spacing); + + const nodeId = addNode({ + type: "image", + position: { x, y }, + size: { width: dimensions.width, height: dimensions.height }, + data: { + src: url, + status: "completed", + title: "图片", + prompt: generationPrompt || "对话生成", + metadata: { + originalWidth: dimensions.originalWidth, + originalHeight: dimensions.originalHeight, + generationPrompt: generationPrompt || undefined, + sourceMessageId: message.id, }, - connections: [], - }); - void autoNameGeneratedImageNode(nodeId, url); - addedImageCount += 1; - } - } + }, + connections: [], + }); + void autoNameGeneratedImageNode(nodeId, url); + addedImageCount += 1; + } - if (videoUrls.length > 0) { - const existingVideoUrls = new Set( - nodes.filter(n => n.type === 'video' && (n.data as any)?.src) - .map(n => (n.data as any).src) - ); - const newVideoUrls = videoUrls.filter(url => !existingVideoUrls.has(url)); + const newVideoUrls = videoUrls.filter((url) => { + const key = normalizeMediaUrl(url); + if (!key || existingVideoUrlKeys.has(key)) return false; + existingVideoUrlKeys.add(key); + return true; + }); - for (let index = 0; index < newVideoUrls.length; index++) { - const url = newVideoUrls[index]; - const existingNodes = nodes.length + index; - const x = 120 + (existingNodes % 2) * 420; - const y = 120 + Math.floor(existingNodes / 2) * 260; - addNode({ - type: "video", - position: { x, y }, - size: { width: 380, height: 220 }, - data: { - src: url, - status: "completed", - prompt: generationPrompt || "对话生成", - }, - connections: [], - }); - addedVideoCount += 1; - } - } + for (let index = 0; index < newVideoUrls.length; index += 1) { + if (cancelled) return; + const url = newVideoUrls[index]; + const existingNodes = nodes.length + addedImageCount + addedVideoCount; + const x = 120 + (existingNodes % 2) * 420; + const y = 120 + Math.floor(existingNodes / 2) * 260; + addNode({ + type: "video", + position: { x, y }, + size: { width: 380, height: 220 }, + data: { + src: url, + status: "completed", + prompt: generationPrompt || "对话生成", + }, + connections: [], + }); + addedVideoCount += 1; + } - if (addedImageCount > 0 || addedVideoCount > 0) { - const notice = [ - addedImageCount > 0 ? `${addedImageCount} 张图片` : "", - addedVideoCount > 0 ? `${addedVideoCount} 条视频` : "", - ] - .filter(Boolean) - .join("、"); - toast.success(`已将 ${notice} 添加到画布`); - } + if (addedImageCount > 0 || addedVideoCount > 0) { + const notice = [ + addedImageCount > 0 ? `${addedImageCount} 张图片` : "", + addedVideoCount > 0 ? `${addedVideoCount} 条视频` : "", + ] + .filter(Boolean) + .join("、"); + toast.success(`已将 ${notice} 添加到画布`); } } - }); - }, [messages, addNode, nodes]); + }; + + void processNewAssistantMessages(); + return () => { + cancelled = true; + }; + }, [messages, addNode, nodes, autoNameGeneratedImageNode]); // 为历史节点补全可编辑的原始提示词(旧数据里可能只有“对话生成”占位文案) useEffect(() => {