fix(canvas): 保护生成中与失败节点画幅尺寸,支持变体即时持久化与 2K 分辨率高亮 (#359)
Co-authored-by: Danborad <danborad@github.com>
This commit is contained in:
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user