fix(*): 生成任务 - 修复任务可见性、分镜重试与角色视频引用

This commit is contained in:
ddCat committed 2026-07-27 15:29:36 +08:00
1 parent dd4af80b98
commit f5a9a0f0e1
13 files changed
+204 -53

No files matched your search

@@ -79,7 +79,7 @@ export async function executeVideoGeneration({
startGenerationRequest(videoId, nodeId, nodeId, controller);
try {
const result = await runBackendCanvasGenerationTask({ projectId, nodeId: videoId, mode: "video", prompt: effectivePrompt, config: generationConfig, referenceImages: generationContext.referenceImages, referenceVideos: generationContext.referenceVideos, referenceAudios: generationContext.referenceAudios, signal: controller.signal, metadata: { sourceNodeId: nodeId, resolvedCharacterVersions: generationContext.resolvedCharacterVersions, ...videoGenerationMetadata }, onTaskCreated: (task) => bindGenerationTask(videoId, task) });
const result = await runBackendCanvasGenerationTask({ projectId, nodeId: videoId, mode: "video", prompt: effectivePrompt, config: generationConfig, referenceImages: generationContext.referenceImages, referenceVideos: generationContext.referenceVideos, referenceAudios: generationContext.referenceAudios, signal: controller.signal, metadata: { sourceNodeId: nodeId, resolvedCharacterVersions: generationContext.resolvedCharacterVersions, resolvedCharacterVoices: generationContext.resolvedCharacterVoices, ...videoGenerationMetadata }, onTaskCreated: (task) => bindGenerationTask(videoId, task) });
if (!result.video?.dataUrl) throw new Error("后端任务没有返回视频");
const video = await storeGeneratedVideo({ url: result.video.dataUrl, mimeType: result.video.mimeType || "video/mp4" });
const videoSize = fitNodeSize(video.width || spec.width, video.height || spec.height, VIDEO_NODE_MAX_WIDTH, VIDEO_NODE_MAX_HEIGHT);
+12 -1
View File
@@ -1291,7 +1291,18 @@ function InfiniteCanvasPage() {
setHoveredNodeId((current) => (current === nodeId ? null : current));
hideNodeToolbar();
}, [hideNodeToolbar]);
const retryCanvasNode = useCallback((node: CanvasNodeData) => { void handleRetryNode(node); }, [handleRetryNode]);
const retryCanvasNode = useCallback((node: CanvasNodeData) => {
if (node.type === CanvasNodeType.Script) {
const prompt = (node.metadata?.composerContent || node.metadata?.prompt || "").trim();
if (!prompt) {
message.warning("分镜脚本缺少剧情内容,无法重试");
return;
}
void generateScriptRows(node.id, prompt);
return;
}
void handleRetryNode(node);
}, [generateScriptRows, handleRetryNode, message]);
const openCanvasNodeTaskDetails = useCallback((node: CanvasNodeData) => { void openNodeTaskDetails(node); }, [openNodeTaskDetails]);
const openCanvasNodeVersions = useCallback((node: CanvasNodeData) => setVersionCompareRootId(node.metadata?.versionOfNodeId || node.id), []);
const viewCanvasNodeImage = useCallback((node: CanvasNodeData) => setPreviewNodeId(node.id), []);
@@ -103,11 +103,15 @@ export function useCanvasConnectionController({
setContextMenu(null);
}, [connectionsRef, message, nodesRef, setConnections, setContextMenu, setNodes]);
const createConnectedNode = useCallback(async (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Config | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing, pending: PendingConnectionCreate) => {
const createConnectedNode = useCallback(async (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Config | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing, pending: PendingConnectionCreate) => {
const storyboardRow = type === CanvasNodeType.Video ? storyboardRowFromHandle(nodesRef.current, pending.connection.nodeId, pending.connection.handleId) : undefined;
const videoPrompt = storyboardRow ? (storyboardRow.videoMotionPrompt || storyboardRow.plotDescription).trim() : "";
const sourceNode = pending.connection.handleType === "source" ? nodesRef.current.find((node) => node.id === pending.connection.nodeId) : undefined;
const scriptPrompt = type === CanvasNodeType.Script && sourceNode?.type === CanvasNodeType.Text ? (sourceNode.metadata?.content || sourceNode.metadata?.prompt || "").trim() : "";
const metadata = type === CanvasNodeType.Config
? { model: effectiveConfig.imageModel || effectiveConfig.model, size: effectiveConfig.size, count: getGenerationCount(effectiveConfig.canvasImageCount || effectiveConfig.count) }
: type === CanvasNodeType.Script && scriptPrompt
? { prompt: scriptPrompt, composerContent: scriptPrompt }
: type === CanvasNodeType.Video && storyboardRow
? { prompt: videoPrompt, composerContent: videoPrompt, generationMode: "video" as const, videoEditOperation: "text_to_video" as const, workflowKind: "shot" as const, workflowTitle: `镜头 ${storyboardRow.shotNumber} 视频`, shotIndex: storyboardRow.shotNumber, seconds: String(storyboardRow.durationSeconds), status: NODE_STATUS_IDLE }
: undefined;
@@ -119,9 +123,9 @@ export function useCanvasConnectionController({
return;
}
if (type === CanvasNodeType.Drawing) {
const sourceNode = nodesRef.current.find((node) => node.id === pending.connection.nodeId);
const sourceUrl = sourceNode?.type === CanvasNodeType.Image ? sourceNode.metadata?.content : "";
if (pending.connection.handleType !== "source" || !sourceNode || !sourceUrl || !newNode.metadata?.drawingId) {
const drawingSourceNode = nodesRef.current.find((node) => node.id === pending.connection.nodeId);
const sourceUrl = drawingSourceNode?.type === CanvasNodeType.Image ? drawingSourceNode.metadata?.content : "";
if (pending.connection.handleType !== "source" || !drawingSourceNode || !sourceUrl || !newNode.metadata?.drawingId) {
message.error("只有已有图片内容的输出连线可以创建绘图");
return;
}
@@ -130,11 +134,11 @@ export function useCanvasConnectionController({
try {
const saved = await createCanvasDrawingFromImage(projectId, newNode.metadata.drawingId, {
url: sourceUrl,
storageKey: sourceNode.metadata?.storageKey,
name: sourceNode.title || "来源图片",
mimeType: sourceNode.metadata?.mimeType,
storageKey: drawingSourceNode.metadata?.storageKey,
name: drawingSourceNode.title || "来源图片",
mimeType: drawingSourceNode.metadata?.mimeType,
});
newNode.title = `${sourceNode.title || "图片"} · 绘图`;
newNode.title = `${drawingSourceNode.title || "图片"} · 绘图`;
newNode.metadata = {
...newNode.metadata,
drawingRevision: saved.revision,
@@ -155,7 +159,7 @@ export function useCanvasConnectionController({
setSelectedNodeIds(new Set([newNode.id]));
setSelectedConnectionId(null);
if (type === CanvasNodeType.Drawing) setDrawingNodeId(newNode.id);
else if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
else if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Script && type !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
closeConnectionCreateMenu();
setConnecting(null);
}, [closeConnectionCreateMenu, effectiveConfig.canvasImageCount, effectiveConfig.count, effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, message, nodesRef, projectId, setConnecting, setConnections, setDialogNodeId, setDrawingNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
@@ -3,7 +3,7 @@ import { App } from "antd";
import { buildNodeGenerationContext, hydrateNodeGenerationContext } from "@/components/canvas/canvas-node-generation";
import type { CanvasNodeGenerationMode } from "@/components/canvas/canvas-node-prompt-panel";
import { buildGenerationConfig, isGenerationCanceled } from "@/lib/canvas/canvas-project-generation";
import { buildGenerationConfig, isGenerationCanceled, supportsVideoReferenceAudio } from "@/lib/canvas/canvas-project-generation";
import { isGenerationTaskCapacityError } from "@/lib/canvas/canvas-generation-batch";
import { expandSkillMentions } from "@/lib/canvas/canvas-skill-mentions";
import { generationFailureMetadata } from "@/lib/generation-error";
@@ -110,6 +110,8 @@ export function useCanvasGenerationExecutor({
buildNodeGenerationContext(nodeId, nodesRef.current, connectionsRef.current, editingTextNode ? `请根据要求修改以下文本。\n\n原文:\n${sourceTextContent}\n\n修改要求:\n${prompt}` : prompt),
projectId,
domainProjectId,
mode,
mode === "video" && supportsVideoReferenceAudio(generationConfig),
);
} catch (error) {
const errorDetails = error instanceof Error ? error.message : "生成任务准备失败";
@@ -19,6 +19,7 @@ import {
resolveStoredReferenceImages,
runBackendCanvasGenerationTask,
sourceNodeReferenceImages,
supportsVideoReferenceAudio,
} from "@/lib/canvas/canvas-project-generation";
import { expandSkillMentions } from "@/lib/canvas/canvas-skill-mentions";
import { generationFailureMetadata, unchangedModeratedPrompt } from "@/lib/generation-error";
@@ -57,11 +58,15 @@ export function useCanvasGenerationRetry({ projectId, domainProjectId, activated
return useCallback(
async (node: CanvasNodeData) => {
const retryMode = retryModeForNode(node.type);
if (!retryMode) {
message.warning("当前节点不能使用通用生成重试");
return;
}
const sourceNode = findRetrySourceNode(node.id, nodesRef.current, connectionsRef.current) || node;
const batchRoot = node.metadata?.batchRootId ? nodesRef.current.find((item) => item.id === node.metadata?.batchRootId) : null;
const savedImageMetadata = node.type === CanvasNodeType.Image ? { ...batchRoot?.metadata, ...node.metadata } : undefined;
const hasSavedImageMetadata = Boolean(savedImageMetadata?.generationType);
const retryMode: CanvasNodeGenerationMode = node.type === CanvasNodeType.Text ? "text" : node.type === CanvasNodeType.Video ? "video" : node.type === CanvasNodeType.Audio ? "audio" : "image";
let generationConfig =
hasSavedImageMetadata && savedImageMetadata
? { ...effectiveConfig, model: savedImageMetadata.model || effectiveConfig.imageModel || effectiveConfig.model, quality: savedImageMetadata.quality || effectiveConfig.quality, size: savedImageMetadata.size || effectiveConfig.size, transparentBackground: (savedImageMetadata.transparentBackground || effectiveConfig.transparentBackground) === "true" ? "true" : "false", count: "1" }
@@ -79,7 +84,7 @@ export function useCanvasGenerationRetry({ projectId, domainProjectId, activated
let rawContext: Awaited<ReturnType<typeof hydrateNodeGenerationContext>> | null;
try {
const baseContext = buildNodeGenerationContext(sourceNode.id, nodesRef.current, connectionsRef.current, retryPromptSource);
rawContext = hasSavedImageMetadata && !baseContext.characterReferences.length ? null : await hydrateNodeGenerationContext(baseContext, projectId, domainProjectId);
rawContext = hasSavedImageMetadata && !baseContext.characterReferences.length ? null : await hydrateNodeGenerationContext(baseContext, projectId, domainProjectId, retryMode, retryMode === "video" && supportsVideoReferenceAudio(generationConfig));
} catch (error) {
const failure = generationFailureMetadata(error, retryPromptSource);
message.error(failure.errorDetails);
@@ -141,7 +146,7 @@ export function useCanvasGenerationRetry({ projectId, domainProjectId, activated
}
if (node.type === CanvasNodeType.Video) {
const videoGenerationMetadata = buildVideoGenerationMetadata(node, videoContext);
const result = await runBackendCanvasGenerationTask({ projectId, nodeId: node.id, mode: "video", prompt, config: generationConfig, referenceImages: videoContext?.referenceImages || [], referenceVideos: videoContext?.referenceVideos || [], referenceAudios: videoContext?.referenceAudios || [], signal: controller.signal, metadata: { retry: true, sourceNodeId: sourceNode.id, resolvedCharacterVersions: context?.resolvedCharacterVersions || [], ...videoGenerationMetadata }, onTaskCreated: (task) => bindGenerationTask(node.id, task) });
const result = await runBackendCanvasGenerationTask({ projectId, nodeId: node.id, mode: "video", prompt, config: generationConfig, referenceImages: videoContext?.referenceImages || [], referenceVideos: videoContext?.referenceVideos || [], referenceAudios: videoContext?.referenceAudios || [], signal: controller.signal, metadata: { retry: true, sourceNodeId: sourceNode.id, resolvedCharacterVersions: context?.resolvedCharacterVersions || [], resolvedCharacterVoices: context?.resolvedCharacterVoices || [], ...videoGenerationMetadata }, onTaskCreated: (task) => bindGenerationTask(node.id, task) });
if (!result.video?.dataUrl) throw new Error("后端任务没有返回视频");
const video = await storeGeneratedVideo({ url: result.video.dataUrl, mimeType: result.video.mimeType || "video/mp4" });
const videoSize = fitNodeSize(video.width || node.width, video.height || node.height, VIDEO_NODE_MAX_WIDTH, VIDEO_NODE_MAX_HEIGHT);
@@ -204,6 +209,15 @@ export function useCanvasGenerationRetry({ projectId, domainProjectId, activated
);
}
// 生成类型必须由明确的节点契约决定,未知节点不能降级成图片任务。
function retryModeForNode(type: CanvasNodeType): CanvasNodeGenerationMode | null {
if (type === CanvasNodeType.Text) return "text";
if (type === CanvasNodeType.Image) return "image";
if (type === CanvasNodeType.Video) return "video";
if (type === CanvasNodeType.Audio) return "audio";
return null;
}
function markMissingReferences(nodeId: string, setNodes: Dispatch<SetStateAction<CanvasNodeData[]>>) {
setNodes((current) => current.map((item) => (item.id === nodeId ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, errorDetails: "参考图片已丢失,无法继续重试" } } : item)));
}