feat(agent): 统一专业拆镜入口与真实镜头生成
This commit is contained in:
@@ -2488,6 +2488,7 @@ function InfiniteCanvasPage() {
|
||||
onSessionsChange={handleAssistantSessionsChange}
|
||||
onApplyOps={applyAgentOps}
|
||||
onApplyStyle={applyCanvasStyleAsync}
|
||||
onGenerateStoryboard={generateScriptRows}
|
||||
onStartArtCritique={(nodeId, restart) => {
|
||||
if (artCritiqueRunningRef.current) throw new Error("已有审美分析正在进行,请先完成或停止当前分析");
|
||||
setArtCritiqueStartRequest({ nodeId, id: nanoid(), restart });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, type Dispatch, type SetStateAction } from "react";
|
||||
import { useCallback, useEffect, useRef, type Dispatch, type SetStateAction } from "react";
|
||||
import { App } from "antd";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
@@ -27,7 +27,9 @@ 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 { submitStoryboardTask } from "@/services/storyboard-submission";
|
||||
import { getActiveUserScope } from "@/lib/user-scope";
|
||||
import { modelDisplayName, resolveModelRequestConfig, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import {
|
||||
CanvasNodeType,
|
||||
type CanvasConnection,
|
||||
@@ -65,6 +67,13 @@ export function useCanvasStoryboard({
|
||||
const { message, modal } = App.useApp();
|
||||
const effectiveConfig = useEffectiveConfig();
|
||||
const isAiConfigReady = useConfigStore((state) => state.isAiConfigReady);
|
||||
const storyboardRequests = useRef(new Set<string>());
|
||||
const storyboardLifetime = useRef(new AbortController());
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
storyboardLifetime.current = controller;
|
||||
return () => controller.abort();
|
||||
}, [projectId]);
|
||||
|
||||
const confirmGenerationSubmission = useCallback((count: number, model: string, taskLabel: string) => new Promise<boolean>((resolve) => {
|
||||
if (!count) return resolve(false);
|
||||
@@ -99,9 +108,9 @@ export function useCanvasStoryboard({
|
||||
const previousRows = new Map((nodesRef.current.find((node) => node.id === nodeId)?.metadata?.storyboard?.rows || []).map((row) => [row.id, row]));
|
||||
const nextRows = rows.map((row) => invalidateEditedPromptVariables(previousRows.get(row.id), row));
|
||||
setConnections((current) => current
|
||||
.filter((connection) => !connection.storyboardRowId || storyboardRowIds.has(connection.storyboardRowId))
|
||||
.filter((connection) => connection.fromNodeId !== nodeId || !connection.fromHandleId || rowIds.has(connection.fromHandleId))
|
||||
.filter((connection) => connection.toNodeId !== nodeId || !connection.toHandleId || rowIds.has(connection.toHandleId)));
|
||||
.filter((connection) => connection.fromNodeId !== nodeId && connection.toNodeId !== nodeId || !connection.storyboardRowId || storyboardRowIds.has(connection.storyboardRowId))
|
||||
.filter((connection) => connection.fromNodeId !== nodeId || !connection.fromHandleId?.startsWith("row:") || rowIds.has(connection.fromHandleId))
|
||||
.filter((connection) => connection.toNodeId !== nodeId || !connection.toHandleId?.startsWith("row:") || rowIds.has(connection.toHandleId)));
|
||||
updateScriptRows(nodeId, () => nextRows);
|
||||
}, [nodesRef, setConnections, updateScriptRows]);
|
||||
|
||||
@@ -119,9 +128,19 @@ export function useCanvasStoryboard({
|
||||
replaceScriptRows(nodeId, rows);
|
||||
}, [nodesRef, replaceScriptRows]);
|
||||
|
||||
const generateScriptRows = useCallback(async (nodeId: string, prompt: string) => {
|
||||
const generateScriptRows = useCallback(async (nodeId: string, prompt: string, callerSignal?: AbortSignal) => {
|
||||
const scriptNode = nodesRef.current.find((node) => node.id === nodeId && node.type === CanvasNodeType.Script);
|
||||
if (!scriptNode || !prompt.trim()) return;
|
||||
if (storyboardRequests.current.has(nodeId) || scriptNode.metadata?.status === NODE_STATUS_LOADING) { message.warning("当前分镜正在处理,请等待原任务完成"); return false; }
|
||||
const lifetimeSignal = storyboardLifetime.current.signal;
|
||||
const signal = callerSignal ? AbortSignal.any([lifetimeSignal, callerSignal]) : lifetimeSignal;
|
||||
const scope = getActiveUserScope();
|
||||
const originalRows = JSON.stringify(scriptNode.metadata?.storyboard?.rows || []);
|
||||
const assertCurrent = () => {
|
||||
signal.throwIfAborted();
|
||||
const current = nodesRef.current.find((node) => node.id === nodeId);
|
||||
if (getActiveUserScope() !== scope || !current || JSON.stringify(current.metadata?.storyboard?.rows || []) !== originalRows) throw new Error("画布、账号或分镜内容已变化,未覆盖现有镜头,请重新读取后处理");
|
||||
};
|
||||
let storyboardContext: ReturnType<typeof resolveStoryboardGenerationContext>;
|
||||
try {
|
||||
storyboardContext = resolveStoryboardGenerationContext(nodesRef.current);
|
||||
@@ -139,14 +158,17 @@ export function useCanvasStoryboard({
|
||||
navigateToSettings({ continueCreation: true });
|
||||
return;
|
||||
}
|
||||
storyboardRequests.current.add(nodeId);
|
||||
try {
|
||||
const skillExecution = await skillRuntime.prepare({
|
||||
profile: "shortDrama",
|
||||
prompt: expandedPrompt,
|
||||
skills: addedSkills,
|
||||
});
|
||||
assertCurrent();
|
||||
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({
|
||||
assertCurrent();
|
||||
const request = {
|
||||
projectId,
|
||||
type: "agent_storyboard_rows",
|
||||
operation: "storyboard_rows",
|
||||
@@ -154,6 +176,8 @@ export function useCanvasStoryboard({
|
||||
model: generationConfig.model,
|
||||
...(logicalModelIDForConfig(generationConfig) ? { logicalModelId: logicalModelIDForConfig(generationConfig) } : {}),
|
||||
input: {
|
||||
mode: "text",
|
||||
prompt: skillExecution.prompt,
|
||||
canvasAssets: buildStoryboardAssetCatalog(nodesRef.current),
|
||||
requirements: "输出可直接编辑并用于批量生成图片和视频的分镜表。",
|
||||
projectStyle: storyboardContext.projectStyle,
|
||||
@@ -163,14 +187,41 @@ export function useCanvasStoryboard({
|
||||
config: backendProviderConfig(generationConfig, "text"),
|
||||
metadata: { nodeId, ...skillExecution.metadata },
|
||||
},
|
||||
});
|
||||
};
|
||||
const managed = Boolean(logicalModelIDForConfig(generationConfig) || resolveModelRequestConfig(generationConfig, generationConfig.model).channelId);
|
||||
const task = managed ? await submitStoryboardTask(request, { signal, assertCurrent, confirm: (submission) => new Promise<boolean>((resolve) => {
|
||||
const quote = submission.quote;
|
||||
const dialog = modal.confirm({
|
||||
title: "确认生成分镜",
|
||||
content: `使用 ${modelDisplayName(effectiveConfig, quote.model)} 拆分镜头。${quote.estimated ? "预计" : ""}平台费用 ${quote.amountMicrocredits / 1_000_000} 积分;自定义渠道费用由渠道另计,按用量计费以实际结算为准。${(scriptNode.metadata?.storyboard?.rows || []).length ? "本次将替换现有分镜行,原图片视频保留,镜头关联需要重新核对。" : "确认后生成可编辑的分镜表。"}`,
|
||||
okText: "确认生成", cancelText: "取消", centered: true,
|
||||
onOk: () => resolve(true), onCancel: () => resolve(false),
|
||||
afterClose: () => signal.removeEventListener("abort", cancel),
|
||||
});
|
||||
const cancel = () => { dialog.destroy(); resolve(false); };
|
||||
signal.addEventListener("abort", cancel, { once: true });
|
||||
if (signal.aborted) cancel();
|
||||
}) }) : await (async () => {
|
||||
const confirmed = await confirmGenerationSubmission(1, generationConfig.model, `专业拆镜${(scriptNode.metadata?.storyboard?.rows || []).length ? "(替换现有镜头,保留原媒体)" : ""}`);
|
||||
if (!confirmed) return undefined;
|
||||
assertCurrent();
|
||||
return createGenerationTask(request);
|
||||
})();
|
||||
if (!task) {
|
||||
if (!signal.aborted && scope === getActiveUserScope()) setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, metadata: { ...node.metadata, status: scriptNode.metadata?.status || NODE_STATUS_IDLE, taskStage: undefined } } : node));
|
||||
return false;
|
||||
}
|
||||
assertCurrent();
|
||||
setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, metadata: { ...node.metadata, ...generationTaskMetadata(task), status: NODE_STATUS_LOADING } } : node));
|
||||
const completed = await waitForGenerationTask(task.id, {
|
||||
initialTask: task,
|
||||
signal,
|
||||
useTextEvents: true,
|
||||
onTaskUpdate: (next) => setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, metadata: { ...node.metadata, ...generationTaskMetadata(next), status: NODE_STATUS_LOADING } } : node)),
|
||||
onTaskUpdate: (next) => { if (!signal.aborted && scope === getActiveUserScope()) setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, metadata: { ...node.metadata, ...generationTaskMetadata(next), status: NODE_STATUS_LOADING } } : node)); },
|
||||
});
|
||||
const result = storyboardRowsFromTask(completed);
|
||||
assertCurrent();
|
||||
replaceScriptRows(nodeId, result.rows);
|
||||
setNodes((current) => current.map((node) => node.id === nodeId ? {
|
||||
...node,
|
||||
title: result.title || node.title,
|
||||
@@ -189,12 +240,20 @@ export function useCanvasStoryboard({
|
||||
message.success(`已生成 ${result.rows.length} 个镜头`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (signal.aborted || scope !== getActiveUserScope()) return false;
|
||||
const details = generationErrorMessage(error);
|
||||
setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_ERROR, errorDetails: details } } : node));
|
||||
message.error(details);
|
||||
return false;
|
||||
} finally {
|
||||
if (signal.aborted && !lifetimeSignal.aborted && scope === getActiveUserScope()) setNodes((current) => current.map((node) => {
|
||||
if (node.id !== nodeId) return node;
|
||||
const active = node.metadata?.taskStatus === "queued" || node.metadata?.taskStatus === "running";
|
||||
return { ...node, metadata: { ...node.metadata, status: active ? NODE_STATUS_LOADING : NODE_STATUS_IDLE, taskStage: active ? "已提交后台任务,等待结果" : undefined } };
|
||||
}));
|
||||
storyboardRequests.current.delete(nodeId);
|
||||
}
|
||||
}, [addedSkills, connectionsRef, effectiveConfig, isAiConfigReady, message, nodesRef, projectId, setNodes]);
|
||||
}, [addedSkills, confirmGenerationSubmission, connectionsRef, effectiveConfig, isAiConfigReady, message, modal, nodesRef, projectId, replaceScriptRows, setNodes]);
|
||||
|
||||
const ensureScriptImageNodes = useCallback((nodeId: string, rowIds: string[]) => {
|
||||
const scriptNode = nodesRef.current.find((node) => node.id === nodeId && node.type === CanvasNodeType.Script);
|
||||
|
||||
Reference in New Issue
Block a user