fix(canvas): 保护生成中与失败节点画幅尺寸,支持变体即时持久化与 2K 分辨率高亮 (#359)

Co-authored-by: Danborad <danborad@github.com>
This commit is contained in:
Danborad
2026-08-31 12:28:12 +08:00
committed by GitHub
parent 76e5a166dc
commit 8d9ab03a66
10 changed files with 127 additions and 77 deletions

View File

@@ -7,6 +7,7 @@ import { nodeSizeFromRatio } from "@/lib/canvas/canvas-node-size";
import { canvasImageReferenceLimitError, buildImageGenerationMetadata, getGenerationCount, isGenerationCanceled, runCanvasGenerationTaskToConsumer } from "@/lib/canvas/canvas-project-generation";
import { CONTENT_MODERATION_ERROR_CODE, generationFailureMetadata, type GenerationFailureMetadata } from "@/lib/generation-error";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
import type { CanvasGenerationExecution } from "./canvas-generation-executor-types";
@@ -49,7 +50,8 @@ export async function executeImageGeneration({
const count = getGenerationCount(generationConfig.count);
const isConfigNode = sourceNode?.type === CanvasNodeType.Config;
const isImageNode = sourceNode?.type === CanvasNodeType.Image;
const reuseSourceNode = canGenerateImageInPlace(sourceNode);
const isCopiedVariant = count === 1 && isImageNode && Boolean(sourceNode?.metadata?.copiedFromNodeId || sourceNode?.metadata?.versionOfNodeId || sourceNode?.title.endsWith(" Copy") || sourceNode?.title.includes(" · "));
const reuseSourceNode = canGenerateImageInPlace(sourceNode) || isCopiedVariant;
const directCopiedBatch = count > 1 && isImageNode && Boolean(sourceNode?.metadata?.content) && (Boolean(sourceNode?.metadata?.copiedFromNodeId) || sourceNode?.title.endsWith(" Copy"));
// 已有图片生成新结果并保留旧版本;参考图只来自入边,避免把旧结果误当成自身输入。
const referenceImages = generationContext.referenceImages;
@@ -87,6 +89,7 @@ export async function executeImageGeneration({
width: rootWidth,
height: rootHeight,
metadata: {
...(reuseSourceNode ? sourceNode?.metadata || {} : {}),
prompt: effectivePrompt,
status: NODE_STATUS_LOADING,
size: generationConfig.size,
@@ -95,6 +98,7 @@ export async function executeImageGeneration({
batchFailedCount: count > 1 ? 0 : undefined,
batchUsesReferenceImages: referenceImages.length > 0,
primaryImageId: undefined,
content: reuseSourceNode ? "" : undefined,
...generationMetadata,
...styleMetadata,
...skillMetadata,
@@ -128,27 +132,33 @@ export async function executeImageGeneration({
? childIds.map((childId) => ({ id: nanoid(), fromNodeId: nodeId, toNodeId: childId }))
: [...(reuseSourceNode ? [] : [{ id: nanoid(), fromNodeId: nodeId, toNodeId: rootId }]), ...childIds.map((childId) => ({ id: nanoid(), fromNodeId: rootId, toNodeId: childId }))];
setNodes((current) => {
return [
...current.map((node) => {
if (node.id !== nodeId) return node;
if (isConfigNode) return { ...node, metadata: { ...node.metadata, prompt: effectivePrompt, status: NODE_STATUS_LOADING, errorDetails: undefined } };
if (reuseSourceNode) return { ...node, position: rootNode.position, width: rootNode.width, height: rootNode.height, title: rootNode.title, metadata: { ...node.metadata, ...rootNode.metadata, errorDetails: undefined } };
if (isImageNode) return node;
return {
...node,
type: CanvasNodeType.Text,
title: prompt.slice(0, 32) || "Prompt",
width: parentConfig.width,
height: parentConfig.height,
metadata: { ...node.metadata, content: prompt, richText: undefined, prompt, status: NODE_STATUS_SUCCESS, fontSize: 14, errorDetails: undefined },
};
}),
...(reuseSourceNode || directCopiedBatch ? [] : [rootNode]),
...childNodes,
];
const nextNodes: CanvasNodeData[] = [
...canvasNodes.map((node) => {
if (node.id !== nodeId) return node;
if (isConfigNode) return { ...node, metadata: { ...node.metadata, prompt: effectivePrompt, status: NODE_STATUS_LOADING, errorDetails: undefined } };
if (reuseSourceNode) return { ...node, position: rootNode.position, width: rootNode.width, height: rootNode.height, title: rootNode.title, metadata: { ...node.metadata, ...rootNode.metadata, errorDetails: undefined } };
if (isImageNode) return node;
return {
...node,
type: CanvasNodeType.Text,
title: prompt.slice(0, 32) || "Prompt",
width: parentConfig.width,
height: parentConfig.height,
metadata: { ...node.metadata, content: prompt, richText: undefined, prompt, status: NODE_STATUS_SUCCESS, fontSize: 14, errorDetails: undefined },
};
}),
...(reuseSourceNode || directCopiedBatch ? [] : [rootNode]),
...childNodes,
];
setNodes(nextNodes);
setConnections((current) => {
const nextConnections = [...current, ...batchConnections];
if (projectId) {
useCanvasStore.getState().updateProject(projectId, { nodes: nextNodes, connections: nextConnections });
}
return nextConnections;
});
setConnections((current) => [...current, ...batchConnections]);
setSelectedNodeIds(new Set([nodeId]));
setSelectedConnectionId(null);
setDialogNodeId(nodeId);
@@ -200,7 +210,7 @@ export async function executeImageGeneration({
height: child.height,
position: { x: center.x - child.width / 2, y: center.y - child.height / 2 },
};
return current.map((node) =>
const updated = current.map((node) =>
node.id === rootId
? {
...node,
@@ -220,6 +230,8 @@ export async function executeImageGeneration({
}
: node,
);
if (projectId) useCanvasStore.getState().updateProject(projectId, { nodes: updated });
return updated;
});
}
hasSuccess = true;
@@ -231,7 +243,11 @@ export async function executeImageGeneration({
if (!representativeFailure || failure.generationErrorCode === CONTENT_MODERATION_ERROR_CODE) representativeFailure = failure;
hasFailure = true;
failureCount += 1;
setNodes((current) => current.map((node) => (node.id === targetId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_ERROR, ...failure } } : node)));
setNodes((current) => {
const next = current.map((node) => (node.id === targetId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_ERROR, ...failure } } : node));
if (projectId) useCanvasStore.getState().updateProject(projectId, { nodes: next });
return next;
});
return false;
} finally {
finishGenerationRequest(targetId, controller);
@@ -244,8 +260,8 @@ export async function executeImageGeneration({
return;
}
if (hasFailure) showError(hasSuccess ? "部分图片生成失败" : "全部图片生成失败");
setNodes((current) =>
current.map((node) => {
setNodes((current) => {
const next = current.map((node) => {
if (node.id === nodeId && isConfigNode) {
return {
...node,
@@ -268,6 +284,9 @@ export async function executeImageGeneration({
};
}
return node;
}),
);
});
if (projectId) useCanvasStore.getState().updateProject(projectId, { nodes: next });
return next;
});
}

View File

@@ -133,9 +133,11 @@ export function useCanvasGenerationExecutor({
const isPreparingEmptyImage = mode === "image" && sourceNode?.type === CanvasNodeType.Image && !sourceNode.metadata?.content;
let rawGenerationContext: Awaited<ReturnType<typeof hydrateNodeGenerationContext>>;
// 视频文本只保留输入框内容;连接的媒体仍作为结构化参考传递。
const promptOnly = mode === "video";
// AutoDL/其他声明式视频协议需要结构化参考素材;只有普通
// 模型视频接口才把提示词视为纯文本输入。
const usesWorkflowProvider = Boolean(mode !== "text" && generationConfig.taskWorkflowProvider && generationConfig.taskWorkflowProvider !== "model");
// 普通视频协议只保留输入框文本;声明式工作流还要保留连接媒体。
const promptOnly = mode === "video" && !usesWorkflowProvider;
try {
const baseContext = buildNodeGenerationContext(
nodeId,