feat(*): 短剧创作工作台 - 完善章节分镜、资产管理、技能运行与生成任务恢复

This commit is contained in:
ddCat
2026-08-29 23:34:46 +08:00
parent 4f07daae9e
commit 2f6832f99c
113 changed files with 7016 additions and 723 deletions

View File

@@ -4,6 +4,7 @@ import type { NodeGenerationContext } from "@/components/canvas/canvas-node-gene
import type { GenerationTask } from "@/services/api/task-center";
import type { AiConfig } from "@/stores/use-config-store";
import type { StyleExecutionPlan } from "@/lib/canvas/style-profile";
import type { SkillRuntimeMetadata } from "@/services/skill-runtime";
import type { CanvasConnection, CanvasNodeData } from "@/types/canvas";
export type CanvasGenerationExecutorDependencies = {
@@ -32,6 +33,7 @@ export type CanvasGenerationExecution = CanvasGenerationExecutorDependencies & {
controller: AbortController;
editingTextNode: boolean;
styleMetadata: { styleProfileJson?: string; styleExecutionPlan?: StyleExecutionPlan };
skillMetadata: SkillRuntimeMetadata;
taskContext?: { conversationId?: string; messageId?: string };
retryContext?: { retryOf: string; attemptGroupId: string; clientOperationId: string };
registerPendingNodeIds: (nodeIds: string[]) => void;

View File

@@ -35,6 +35,7 @@ export async function executeImageGeneration({
bindGenerationTask,
applyGenerationTaskResult,
styleMetadata,
skillMetadata,
taskContext,
retryContext,
showError,
@@ -96,6 +97,7 @@ export async function executeImageGeneration({
primaryImageId: undefined,
...generationMetadata,
...styleMetadata,
...skillMetadata,
imageBatchExpanded: count > 1 ? true : undefined,
generationErrorCode: undefined,
resourceReloadAvailable: undefined,
@@ -116,6 +118,7 @@ export async function executeImageGeneration({
batchRootId: count > 1 && !directCopiedBatch ? rootId : undefined,
...generationMetadata,
...styleMetadata,
...skillMetadata,
generationErrorCode: undefined,
resourceReloadAvailable: undefined,
failedPromptFingerprint: undefined,
@@ -176,6 +179,7 @@ export async function executeImageGeneration({
promptTemplateOperation: sourceNode?.metadata?.promptTemplateOperation,
promptTemplateVariables: sourceNode?.metadata?.promptTemplateVariables,
...styleMetadata,
...skillMetadata,
},
},
{

View File

@@ -28,6 +28,7 @@ export async function executeVideoGeneration({
applyGenerationTaskResult,
registerPendingNodeIds,
styleMetadata,
skillMetadata,
taskContext,
retryContext,
}: CanvasGenerationExecution) {
@@ -62,6 +63,7 @@ export async function executeVideoGeneration({
references: generationReferenceUrls(generationContext),
...videoGenerationMetadata,
...styleMetadata,
...skillMetadata,
},
};
registerPendingNodeIds([videoId]);
@@ -111,6 +113,7 @@ export async function executeVideoGeneration({
promptTemplateVariables: sourceNode?.metadata?.promptTemplateVariables,
...videoGenerationMetadata,
...styleMetadata,
...skillMetadata,
},
},
{
@@ -139,6 +142,7 @@ export async function executeAudioGeneration({
applyGenerationTaskResult,
registerPendingNodeIds,
taskContext,
skillMetadata,
retryContext,
}: CanvasGenerationExecution) {
const spec = NODE_DEFAULT_SIZE[CanvasNodeType.Audio];
@@ -152,7 +156,7 @@ export async function executeAudioGeneration({
position: isEmptyAudioNode ? sourceNode.position : { x: parent.x + (sourceNode?.width || spec.width) + 96, y: parent.y + ((sourceNode?.height || spec.height) - spec.height) / 2 },
width: isEmptyAudioNode ? sourceNode.width : spec.width,
height: isEmptyAudioNode ? sourceNode.height : spec.height,
metadata: { prompt: effectivePrompt, status: NODE_STATUS_LOADING, ...buildAudioGenerationMetadata(generationConfig) },
metadata: { prompt: effectivePrompt, status: NODE_STATUS_LOADING, ...buildAudioGenerationMetadata(generationConfig), ...skillMetadata },
};
registerPendingNodeIds([audioId]);
setNodes((current) =>
@@ -171,7 +175,7 @@ export async function executeAudioGeneration({
prompt: effectivePrompt,
config: generationConfig,
signal: controller.signal,
metadata: { sourceNodeId: nodeId, ...taskContext, resolvedCharacterVersions: generationContext.resolvedCharacterVersions, resolvedCharacterVoiceKey: generationContext.resolvedCharacterVoices[0]?.voiceKey },
metadata: { sourceNodeId: nodeId, ...taskContext, resolvedCharacterVersions: generationContext.resolvedCharacterVersions, resolvedCharacterVoiceKey: generationContext.resolvedCharacterVoices[0]?.voiceKey, ...skillMetadata },
},
{
bindTask: (task) => bindGenerationTask(audioId, task),

View File

@@ -27,6 +27,7 @@ export async function executeTextGeneration({
applyGenerationTaskResult,
registerPendingNodeIds,
taskContext,
skillMetadata,
retryContext,
}: CanvasGenerationExecution) {
const isConfigNode = sourceNode?.type === CanvasNodeType.Config;
@@ -50,7 +51,7 @@ export async function executeTextGeneration({
},
width: textConfig.width,
height: textConfig.height,
metadata: { prompt: effectivePrompt, status: NODE_STATUS_LOADING, fontSize: 14 },
metadata: { prompt: effectivePrompt, status: NODE_STATUS_LOADING, fontSize: 14, ...skillMetadata },
}));
setNodes((current) => [...current.map((node) => (node.id === nodeId && isConfigNode ? { ...node, metadata: { ...node.metadata, prompt: effectivePrompt, status: NODE_STATUS_LOADING, errorDetails: undefined } } : node)), ...childNodes]);
setConnections((current) => [...current, ...childIds.map((childId) => ({ id: nanoid(), fromNodeId: nodeId, toNodeId: childId }))]);
@@ -71,7 +72,7 @@ export async function executeTextGeneration({
referenceImages: generationContext.referenceImages,
referenceVideos: generationContext.referenceVideos,
signal: controller.signal,
metadata: { sourceNodeId: nodeId, ...taskContext, resolvedCharacterVersions: generationContext.resolvedCharacterVersions },
metadata: { sourceNodeId: nodeId, ...taskContext, resolvedCharacterVersions: generationContext.resolvedCharacterVersions, ...skillMetadata },
},
{
bindTask: (task) => bindGenerationTask(targetNodeId, task),

View File

@@ -1643,6 +1643,7 @@ function InfiniteCanvasPage() {
const { addScriptRow, createAndGenerateScriptVideos, createScriptActionBoards, createScriptImageNodes, createScriptVideoNodes, generateScriptImages, generateScriptRows, generateScriptVideos, removeScriptRow, replaceScriptRows, updateScriptRow } =
useCanvasStoryboard({
projectId,
addedSkills,
nodesRef,
connectionsRef,
setNodes,

View File

@@ -8,11 +8,11 @@ import { canvasGenerationRequestFingerprint, runCanvasGenerationSubmissionOnce }
import { isGenerationTaskCapacityError } from "@/lib/canvas/canvas-generation-batch";
import { buildPortraitTexturePrompt } from "@/lib/canvas/canvas-portrait-texture";
import { resolveCanvasStyleExecution } from "@/lib/canvas/canvas-style-execution";
import { expandSkillMentions } from "@/lib/canvas/canvas-skill-mentions";
import { generationErrorMessage, generationFailureMetadata } from "@/lib/generation-error";
import { modelCompatibilityError, modelGroupReferenceLimits, modelRequestOptions, type ModelRequirements } from "@/lib/model-selection";
import { navigateToSettings } from "@/lib/settings-navigation";
import type { Skill } from "@/services/api/skills";
import { skillRuntime } from "@/services/skill-runtime";
import type { GenerationTask } from "@/services/api/task-center";
import { useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
import type { Asset } from "@/stores/use-asset-store";
@@ -161,8 +161,14 @@ export function useCanvasGenerationExecutor({
return;
}
const expandedPrompt = promptOnly ? rawGenerationContext.prompt : expandSkillMentions(rawGenerationContext.prompt, addedSkills);
let effectivePrompt = expandedPrompt.trim();
let skillExecution: Awaited<ReturnType<typeof skillRuntime.prepare<"canvas">>>;
try {
skillExecution = await skillRuntime.prepare({ profile: "canvas", prompt: rawGenerationContext.prompt, skills: addedSkills });
} catch (error) {
message.error(error instanceof Error ? error.message : "技能上下文加载失败");
return;
}
let effectivePrompt = skillExecution.prompt.trim();
let styleMetadata = {};
if (mode === "image") {
try {
@@ -278,6 +284,7 @@ export function useCanvasGenerationExecutor({
controller,
editingTextNode,
styleMetadata,
skillMetadata: skillExecution.metadata,
taskContext: options?.context,
retryContext: options?.retryContext,
setNodes,

View File

@@ -23,13 +23,13 @@ import {
supportsVideoReferenceAudio,
} from "@/lib/canvas/canvas-project-generation";
import { isCanvasWorkflowProvider } from "@/lib/canvas/canvas-workflow";
import { expandSkillMentions } from "@/lib/canvas/canvas-skill-mentions";
import { buildPortraitTexturePrompt } from "@/lib/canvas/canvas-portrait-texture";
import { resolveCanvasStyleExecution } from "@/lib/canvas/canvas-style-execution";
import { generationFailureMetadata, unchangedModeratedPrompt } from "@/lib/generation-error";
import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
import { navigateToSettings } from "@/lib/settings-navigation";
import type { Skill } from "@/services/api/skills";
import { skillRuntime, type SkillRuntimeMetadata } from "@/services/skill-runtime";
import type { GenerationTask } from "@/services/api/task-center";
import { resolveImageUrl } from "@/services/image-storage";
import { resolveModelRequestConfig, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
@@ -122,7 +122,22 @@ export function useCanvasGenerationRetry({
setNodes((current) => current.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, ...failure } } : item)));
return;
}
const context = rawContext ? { ...rawContext, prompt: retryMode === "video" ? rawContext.prompt : expandSkillMentions(rawContext.prompt, addedSkills) } : null;
let skillMetadata: SkillRuntimeMetadata = {
skillIds: node.metadata?.skillIds || [],
skillVersions: node.metadata?.skillVersions || [],
skillFiles: node.metadata?.skillFiles || [],
};
let context = rawContext;
if (rawContext) {
try {
const skillExecution = await skillRuntime.prepare({ profile: "canvas", prompt: rawContext.prompt, skills: addedSkills });
context = { ...rawContext, prompt: skillExecution.prompt };
skillMetadata = skillExecution.metadata;
} catch (error) {
message.error(error instanceof Error ? error.message : "技能上下文加载失败");
return;
}
}
const prompt = (context?.characterReferences.length ? context.prompt : savedImageMetadata?.prompt || context?.prompt || "").trim();
if (!prompt) {
message.warning("找不到提示词,无法重试");
@@ -252,6 +267,7 @@ export function useCanvasGenerationRetry({
watermark: generationConfig.videoWatermark,
...videoGenerationMetadata,
...styleMetadata,
...skillMetadata,
references: videoContext ? generationReferenceUrls(videoContext) : item.metadata?.references,
},
}
@@ -277,6 +293,7 @@ export function useCanvasGenerationRetry({
promptTemplateVariables: node.metadata?.promptTemplateVariables,
...videoGenerationMetadata,
...styleMetadata,
...skillMetadata,
},
});
return;
@@ -319,7 +336,7 @@ export function useCanvasGenerationRetry({
item.id === node.id
? {
...item,
metadata: { ...item.metadata, prompt: providerPrompt, ...generationMetadata, ...styleMetadata, emotionEdit: nextEmotionEdit },
metadata: { ...item.metadata, prompt: providerPrompt, ...generationMetadata, ...styleMetadata, ...skillMetadata, emotionEdit: nextEmotionEdit },
}
: item,
),
@@ -333,7 +350,7 @@ export function useCanvasGenerationRetry({
referenceImages: [editReference, characterReference],
mask,
signal: controller.signal,
metadata: { retry: true, sourceNodeId: emotionSource.id, edit: "emotion", emotionEditMode: editPlan.mode, emotion: nextEmotionEdit, resolvedCharacterVersions: context?.resolvedCharacterVersions || [], ...styleMetadata },
metadata: { retry: true, sourceNodeId: emotionSource.id, edit: "emotion", emotionEditMode: editPlan.mode, emotion: nextEmotionEdit, resolvedCharacterVersions: context?.resolvedCharacterVersions || [], ...styleMetadata, ...skillMetadata },
});
return;
}
@@ -355,7 +372,7 @@ export function useCanvasGenerationRetry({
item.id === node.id
? {
...item,
metadata: { ...item.metadata, prompt: mediaPrompt, ...generationMetadata, ...styleMetadata },
metadata: { ...item.metadata, prompt: mediaPrompt, ...generationMetadata, ...styleMetadata, ...skillMetadata },
}
: item,
),
@@ -375,6 +392,7 @@ export function useCanvasGenerationRetry({
promptTemplateOperation: node.metadata?.promptTemplateOperation,
promptTemplateVariables: node.metadata?.promptTemplateVariables,
...styleMetadata,
...skillMetadata,
},
});
} catch (error) {

View File

@@ -24,7 +24,9 @@ import { resolveStoryboardGenerationContext } from "@/lib/canvas/canvas-storyboa
import { reconcileStoryboardTargetConnections, storyboardComposerContent, storyboardRowReferenceNodeIds } from "@/lib/canvas/canvas-storyboard-materializer";
import { generationErrorMessage } from "@/lib/generation-error";
import { navigateToSettings } from "@/lib/settings-navigation";
import type { Skill } from "@/services/api/skills";
import { createGenerationTask, waitForGenerationTask } from "@/services/api/task-center";
import { skillRuntime } from "@/services/skill-runtime";
import { modelDisplayName, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
import {
CanvasNodeType,
@@ -36,6 +38,7 @@ import {
type UseCanvasStoryboardOptions = {
projectId: string;
addedSkills: Skill[];
nodesRef: { current: CanvasNodeData[] };
connectionsRef: { current: CanvasConnection[] };
setNodes: Dispatch<SetStateAction<CanvasNodeData[]>>;
@@ -51,6 +54,7 @@ const NODE_STATUS_ERROR = "error" as const;
export function useCanvasStoryboard({
projectId,
addedSkills,
nodesRef,
connectionsRef,
setNodes,
@@ -135,13 +139,18 @@ export function useCanvasStoryboard({
navigateToSettings({ continueCreation: true });
return;
}
setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, metadata: { ...node.metadata, composerContent: prompt, status: NODE_STATUS_LOADING, taskStage: "正在创建任务", taskProgress: 0, errorDetails: undefined } } : node));
try {
const skillExecution = await skillRuntime.prepare({
profile: "shortDrama",
prompt: expandedPrompt,
skills: addedSkills,
});
setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, metadata: { ...node.metadata, composerContent: prompt, status: NODE_STATUS_LOADING, taskStage: "正在创建任务", taskProgress: 0, errorDetails: undefined, ...skillExecution.metadata } } : node));
const task = await createGenerationTask({
projectId,
type: "agent_storyboard_rows",
operation: "storyboard_rows",
prompt: expandedPrompt,
prompt: skillExecution.prompt,
model: generationConfig.model,
...(logicalModelIDForConfig(generationConfig) ? { logicalModelId: logicalModelIDForConfig(generationConfig) } : {}),
input: {
@@ -152,7 +161,7 @@ export function useCanvasStoryboard({
shotDurationSeconds,
shotCount: requestedShotCount,
config: backendProviderConfig(generationConfig, "text"),
metadata: { nodeId },
metadata: { nodeId, ...skillExecution.metadata },
},
});
setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, metadata: { ...node.metadata, ...generationTaskMetadata(task), status: NODE_STATUS_LOADING } } : node));
@@ -185,7 +194,7 @@ export function useCanvasStoryboard({
message.error(details);
return false;
}
}, [connectionsRef, effectiveConfig, isAiConfigReady, message, nodesRef, projectId, setNodes]);
}, [addedSkills, connectionsRef, effectiveConfig, isAiConfigReady, message, nodesRef, projectId, setNodes]);
const ensureScriptImageNodes = useCallback((nodeId: string, rowIds: string[]) => {
const scriptNode = nodesRef.current.find((node) => node.id === nodeId && node.type === CanvasNodeType.Script);

View File

@@ -92,8 +92,13 @@ export function useCanvasViewportController({
const handleCanvasDoubleClick = useCallback((event: MouseEvent<HTMLDivElement>) => {
event.preventDefault();
if (!fitCanvasSelection()) fitCanvasContent();
}, [fitCanvasContent, fitCanvasSelection]);
event.stopPropagation();
setSelectedNodeIds(new Set());
setSelectedConnectionId(null);
setDialogNodeId(null);
setToolbarNodeId(null);
setContextMenu({ type: "canvas", x: event.clientX, y: event.clientY, position: screenToCanvas(event.clientX, event.clientY), createOpen: true });
}, [screenToCanvas, setContextMenu, setDialogNodeId, setSelectedConnectionId, setSelectedNodeIds, setToolbarNodeId]);
const selectFocusedNode = useCallback((nodeId: string) => {
const selection = new Set([nodeId]);