fix(canvas): PR #494 - 合并图片重新生成引用修复
This commit is contained in:
@@ -54,6 +54,7 @@ export async function executeImageGeneration({
|
||||
const count = getGenerationCount(generationConfig.count);
|
||||
const isConfigNode = sourceNode?.type === CanvasNodeType.Config;
|
||||
const isImageNode = sourceNode?.type === CanvasNodeType.Image;
|
||||
const isExistingImageNode = isImageNode && Boolean(sourceNode?.metadata?.content);
|
||||
const reuseSourceNode = canGenerateImageInPlace(sourceNode);
|
||||
const retired = reuseSourceNode && sourceNode ? retireImageBatchChildren(sourceNode, canvasNodes, canvasConnections) : { nodes: canvasNodes, connections: canvasConnections, removedIds: [] as string[] };
|
||||
const workingNodes = retired.nodes;
|
||||
@@ -133,9 +134,10 @@ export async function executeImageGeneration({
|
||||
failedPromptFingerprint: undefined,
|
||||
},
|
||||
}));
|
||||
const connectsSelf = !isExistingImageNode || referenceImages.some((img) => img.id === nodeId);
|
||||
const batchConnections = [
|
||||
...(reuseSourceNode ? [] : imageGenerationReferenceConnections(nodeId, rootId, workingNodes, workingConnections, nanoid)),
|
||||
...(reuseSourceNode ? [] : [{ id: nanoid(), fromNodeId: nodeId, toNodeId: rootId }]),
|
||||
...(reuseSourceNode || !connectsSelf ? [] : [{ id: nanoid(), fromNodeId: nodeId, toNodeId: rootId }]),
|
||||
...childIds.map((childId) => ({ id: nanoid(), fromNodeId: rootId, toNodeId: childId })),
|
||||
];
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@ export async function executeAudioGeneration({
|
||||
}: CanvasGenerationExecution) {
|
||||
const spec = NODE_DEFAULT_SIZE[CanvasNodeType.Audio];
|
||||
const isEmptyAudioNode = sourceNode?.type === CanvasNodeType.Audio && !sourceNode.metadata?.content;
|
||||
const isExistingAudioNode = sourceNode?.type === CanvasNodeType.Audio && Boolean(sourceNode.metadata?.content);
|
||||
const audioId = isEmptyAudioNode ? nodeId : nanoid();
|
||||
const parent = sourceNode?.position || { x: 0, y: 0 };
|
||||
const audioNode: CanvasNodeData = {
|
||||
@@ -166,7 +167,7 @@ export async function executeAudioGeneration({
|
||||
setNodes((current) =>
|
||||
isEmptyAudioNode ? current.map((node) => (node.id === nodeId ? { ...node, ...audioNode } : node)) : [...current.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_SUCCESS } } : node)), audioNode],
|
||||
);
|
||||
if (!isEmptyAudioNode) setConnections((current) => [...current, { id: nanoid(), fromNodeId: nodeId, toNodeId: audioId }]);
|
||||
if (!isEmptyAudioNode && !isExistingAudioNode) setConnections((current) => [...current, { id: nanoid(), fromNodeId: nodeId, toNodeId: audioId }]);
|
||||
|
||||
startGenerationRequest(audioId, nodeId, nodeId, controller);
|
||||
try {
|
||||
|
||||
107
web/test/canvas-image-regeneration.test.ts
Normal file
107
web/test/canvas-image-regeneration.test.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "../src/types/canvas";
|
||||
import { imageGenerationReferenceConnections } from "../src/lib/canvas/canvas-resource-references";
|
||||
|
||||
describe("image generation connections", () => {
|
||||
test("重新生成已有图片节点时,只有在用户显式引用源图时才连接源图,避免误将旧结果当作参考图", () => {
|
||||
const sourceImageNode: CanvasNodeData = {
|
||||
id: "img-1",
|
||||
type: CanvasNodeType.Image,
|
||||
title: "第一次生成结果",
|
||||
position: { x: 0, y: 0 },
|
||||
width: 300,
|
||||
height: 300,
|
||||
metadata: {
|
||||
content: "data:image/png;base64,mock",
|
||||
storageKey: "user:img-1:file",
|
||||
},
|
||||
};
|
||||
|
||||
const rootId = "img-2";
|
||||
const workingNodes = [sourceImageNode];
|
||||
const workingConnections: CanvasConnection[] = [];
|
||||
|
||||
// Case 1: 用户修改提示词重新生成(未显式 @ 源图)
|
||||
const referenceImagesNone: any[] = [];
|
||||
const isExistingImageNode = sourceImageNode.type === CanvasNodeType.Image && Boolean(sourceImageNode.metadata?.content);
|
||||
const reuseSourceNode = false;
|
||||
|
||||
const connectsSelfNoMention = !isExistingImageNode || referenceImagesNone.some((img) => img.id === sourceImageNode.id);
|
||||
expect(connectsSelfNoMention).toBe(false);
|
||||
|
||||
const batchConnectionsNoMention = [
|
||||
...(reuseSourceNode ? [] : imageGenerationReferenceConnections(sourceImageNode.id, rootId, workingNodes, workingConnections, () => "c1")),
|
||||
...(reuseSourceNode || !connectsSelfNoMention ? [] : [{ id: "c-self", fromNodeId: sourceImageNode.id, toNodeId: rootId }]),
|
||||
];
|
||||
|
||||
// 验证:不应存在从 img-1 到 img-2 的连接
|
||||
expect(batchConnectionsNoMention.some((c) => c.fromNodeId === sourceImageNode.id && c.toNodeId === rootId)).toBe(false);
|
||||
expect(batchConnectionsNoMention.length).toBe(0);
|
||||
|
||||
// Case 2: 用户在提示词中显式 @图片1 进行图生图
|
||||
const referenceImagesMention = [{ id: sourceImageNode.id, dataUrl: "mock" }];
|
||||
const connectsSelfWithMention = !isExistingImageNode || referenceImagesMention.some((img) => img.id === sourceImageNode.id);
|
||||
expect(connectsSelfWithMention).toBe(true);
|
||||
|
||||
const batchConnectionsWithMention = [
|
||||
...(reuseSourceNode ? [] : imageGenerationReferenceConnections(sourceImageNode.id, rootId, workingNodes, workingConnections, () => "c1")),
|
||||
...(reuseSourceNode || !connectsSelfWithMention ? [] : [{ id: "c-self", fromNodeId: sourceImageNode.id, toNodeId: rootId }]),
|
||||
];
|
||||
|
||||
// 验证:显式引用时正确建立从 img-1 到 img-2 的连线
|
||||
expect(batchConnectionsWithMention.some((c) => c.fromNodeId === sourceImageNode.id && c.toNodeId === rootId)).toBe(true);
|
||||
});
|
||||
|
||||
test("若源图本身带有上游参考素材,新图片节点正确继承上游素材连线而不连接源图自身", () => {
|
||||
const upstreamRefNode: CanvasNodeData = {
|
||||
id: "ref-source",
|
||||
type: CanvasNodeType.Image,
|
||||
title: "原始参考图",
|
||||
position: { x: 0, y: 0 },
|
||||
width: 300,
|
||||
height: 300,
|
||||
metadata: {
|
||||
content: "data:image/png;base64,ref",
|
||||
storageKey: "user:ref-source:file",
|
||||
},
|
||||
};
|
||||
|
||||
const intermediateImageNode: CanvasNodeData = {
|
||||
id: "img-intermediate",
|
||||
type: CanvasNodeType.Image,
|
||||
title: "中间生成图",
|
||||
position: { x: 400, y: 0 },
|
||||
width: 300,
|
||||
height: 300,
|
||||
metadata: {
|
||||
content: "data:image/png;base64,mid",
|
||||
storageKey: "user:img-intermediate:file",
|
||||
},
|
||||
};
|
||||
|
||||
const existingConnection: CanvasConnection = {
|
||||
id: "c-upstream",
|
||||
fromNodeId: upstreamRefNode.id,
|
||||
toNodeId: intermediateImageNode.id,
|
||||
};
|
||||
|
||||
const rootId = "img-new";
|
||||
const workingNodes = [upstreamRefNode, intermediateImageNode];
|
||||
const workingConnections = [existingConnection];
|
||||
|
||||
const isExistingImageNode = true;
|
||||
const reuseSourceNode = false;
|
||||
const referenceImagesNone: any[] = [];
|
||||
const connectsSelf = !isExistingImageNode || referenceImagesNone.some((img) => img.id === intermediateImageNode.id);
|
||||
|
||||
const batchConnections = [
|
||||
...(reuseSourceNode ? [] : imageGenerationReferenceConnections(intermediateImageNode.id, rootId, workingNodes, workingConnections, () => "c-inherited")),
|
||||
...(reuseSourceNode || !connectsSelf ? [] : [{ id: "c-self", fromNodeId: intermediateImageNode.id, toNodeId: rootId }]),
|
||||
];
|
||||
|
||||
// 验证:新节点继承了 upstreamRefNode 的连线,但绝不连接 intermediateImageNode 自身
|
||||
expect(batchConnections).toHaveLength(1);
|
||||
expect(batchConnections[0].fromNodeId).toBe(upstreamRefNode.id);
|
||||
expect(batchConnections[0].toNodeId).toBe(rootId);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user