feat(agent): 创作能力 - 接通专业节点发现与插件执行基础

This commit is contained in:
王志伟 committed 2026-09-09 13:01:30 +08:00
1 parent a594c8266f
commit 757d1dd2c7
29 files changed
+617 -88

No files matched your search

@@ -1,5 +1,7 @@
import { hashCanvasAgentSnapshot, type CanvasAgentOp, type CanvasAgentSnapshot } from "./canvas-agent-ops";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
import { getNodeDefinition } from "./node-registry";
import { isPluginEffectivelyEnabled } from "@/stores/use-plugin-store";
export type CanvasAgentResource = {
nodeId: string;
@@ -151,6 +153,8 @@ export function validateCanvasAgentOps(snapshot: CanvasAgentSnapshot, ops: Canva
};
ops.forEach((op, index) => {
if (op.type === "add_node") {
const definition = getNodeDefinition(op.nodeType || CanvasNodeType.Text);
if (!definition || (definition.plugin && !isPluginEffectivelyEnabled(definition.plugin.pluginId))) issues.push({ index, severity: "error", message: "节点未注册或插件不可用,请重新发现能力" });
validateNodeNumbers(issues, index, op as unknown as Record<string, unknown>);
if (op.id && liveNodeIds.has(op.id)) issues.push({ index, severity: "error", message: `新增节点 id「${op.id}」重复` });
if (op.id) liveNodeIds.add(op.id);
+12 -9
View File
@@ -1,12 +1,14 @@
import { nanoid } from "nanoid";
import { getNodeSpec } from "@/constant/canvas";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type CanvasNodeMetadata, type ViewportTransform } from "@/types/canvas";
import { getNodeDefinition } from "./node-registry";
import { isPluginEffectivelyEnabled } from "@/stores/use-plugin-store";
import { CanvasNodeType, type CanvasNodeTypeId, type CanvasConnection, type CanvasNodeData, type CanvasNodeMetadata, type ViewportTransform } from "@/types/canvas";
export type CanvasAgentOp =
| { type: "add_node"; id?: string; nodeType?: CanvasNodeType; title?: string; position?: { x: number; y: number }; x?: number; y?: number; width?: number; height?: number; metadata?: CanvasNodeMetadata }
| { type: "add_node"; id?: string; nodeType?: CanvasNodeTypeId; title?: string; position?: { x: number; y: number }; x?: number; y?: number; width?: number; height?: number; metadata?: CanvasNodeMetadata }
| { type: "update_node"; id: string; patch?: Partial<CanvasNodeData>; metadata?: CanvasNodeMetadata }
| { type: "delete_node"; id?: string; ids?: string[]; nodeType?: CanvasNodeType }
| { type: "delete_node"; id?: string; ids?: string[]; nodeType?: CanvasNodeTypeId }
| { type: "delete_connections"; id?: string; ids?: string[]; all?: boolean }
| { type: "connect_nodes"; id?: string; fromNodeId: string; toNodeId: string; fromHandleId?: string; toHandleId?: string }
| { type: "set_viewport"; viewport: ViewportTransform }
@@ -320,8 +322,10 @@ export function applyCanvasAgentOps(snapshot: CanvasAgentSnapshot, ops?: CanvasA
(Array.isArray(ops) ? ops : []).forEach((op, index) => {
if (!op?.type) return;
if (op.type === "add_node") {
const nodeType = Object.values(CanvasNodeType).includes(op.nodeType as CanvasNodeType) ? op.nodeType! : CanvasNodeType.Text;
const spec = getNodeSpec(nodeType);
const nodeType = op.nodeType || CanvasNodeType.Text;
const definition = getNodeDefinition(nodeType);
if (!definition || (definition.plugin && !isPluginEffectivelyEnabled(definition.plugin.pluginId))) throw new Error(`节点类型未注册或插件不可用:${nodeType}`);
const spec = getNodeSpec(nodeType as CanvasNodeType) || { title: definition.defaultTitle, ...definition.defaultSize, metadata: definition.defaultMetadata };
const node: CanvasNodeData = {
id: op.id || `${nodeType}-${Date.now()}-${index}`,
type: nodeType,
@@ -329,7 +333,7 @@ export function applyCanvasAgentOps(snapshot: CanvasAgentSnapshot, ops?: CanvasA
position: op.position || { x: op.x ?? index * 36, y: op.y ?? index * 36 },
width: op.width || spec.width,
height: op.height || spec.height,
metadata: { ...spec.metadata, ...op.metadata },
metadata: { ...spec.metadata, ...op.metadata, ...(definition.plugin ? { pluginId: definition.plugin.pluginId, pluginNodeId: nodeType } : {}) },
};
nodes = [...nodes, node];
selectedNodeIds = [node.id];
@@ -390,7 +394,7 @@ function opLabel(type: string) {
return type;
}
function canvasNodeTypeLabel(type?: CanvasNodeType) {
function canvasNodeTypeLabel(type?: CanvasNodeTypeId) {
if (type === CanvasNodeType.Image) return "图片节点";
if (type === CanvasNodeType.Video) return "视频节点";
if (type === CanvasNodeType.Audio) return "音频节点";
@@ -399,8 +403,7 @@ function canvasNodeTypeLabel(type?: CanvasNodeType) {
if (type === CanvasNodeType.Frame) return "背板";
if (type === CanvasNodeType.Drawing) return "绘图节点";
if (type === CanvasNodeType.Skill) return "技能节点";
if (type === CanvasNodeType.MediaConversion) return "转换节点";
return "文本节点";
return type ? `${getNodeDefinition(type)?.label || type}节点` : "文本节点";
}
function generationModeLabel(mode?: "text" | "image" | "video" | "audio") {
@@ -0,0 +1,52 @@
import type { CanvasAgentOp, CanvasAgentSnapshot } from "./canvas-agent-ops";
import { createStoryboardRow } from "./canvas-project-domain";
import type { StoryboardRow } from "@/types/canvas";
const textFields = ["plotDescription", "dialogue", "videoMotionPrompt", "imageGenerationPrompt", "camera", "motion", "shotSize", "emotion", "lightingAndAtmosphere", "audioEffects", "narrativeIntent", "viewerPOV", "performanceBlocking", "timeBeats", "continuityOut", "negativePrompt"] as const;
export function readAgentStoryboard(snapshot: CanvasAgentSnapshot, nodeId: string) {
const node = snapshot.nodes.find((item) => item.id === nodeId && item.type === "script");
if (!node) throw new Error("未找到当前画布的分镜脚本节点,请先查找真实节点");
return { node, rows: node.metadata?.storyboard?.rows || [] };
}
export function buildAgentStoryboardOperations(snapshot: CanvasAgentSnapshot, input: Record<string, unknown>): CanvasAgentOp[] {
if (typeof input.nodeId !== "string") throw new Error("需要分镜节点 nodeId");
const { node, rows } = readAgentStoryboard(snapshot, input.nodeId);
const action = input.action;
const rowId = typeof input.rowId === "string" ? input.rowId : "";
const index = rows.findIndex((row) => row.id === rowId);
let next = rows.slice();
if (action === "remove") {
if (index < 0) throw new Error("分镜行不存在,请重新读取分镜表");
// Removing a row never deletes its existing generated assets.
next.splice(index, 1);
} else if (action === "append" || action === "update") {
if (action === "update" && index < 0) throw new Error("分镜行不存在,请重新读取分镜表");
const raw = input.patch;
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("需要分镜内容 patch");
const data = raw as Record<string, unknown>;
const patch: Partial<StoryboardRow> = {};
for (const [key, value] of Object.entries(data)) {
if (key === "durationSeconds") {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new Error("分镜时长必须是大于零的数字");
patch.durationSeconds = value;
} else if (textFields.includes(key as typeof textFields[number])) {
if (typeof value !== "string" || value.length > 20_000) throw new Error(`分镜字段 ${key} 需要不超过 20000 字符的文本`);
patch[key as typeof textFields[number]] = value;
} else throw new Error(`不能通过分镜编辑修改字段 ${key};已有素材关联与任务状态由系统维护`);
}
if (!Object.keys(patch).length) throw new Error("未提供需要修改的分镜内容");
if (action === "append") {
if (rows.length >= 100) throw new Error("单个分镜表最多 100 行");
if (!patch.videoMotionPrompt?.trim() && !patch.plotDescription?.trim()) throw new Error("新增镜头需要画面描述或视频提示词");
next.push(createStoryboardRow(rows.length + 1, patch));
} else next[index] = { ...rows[index], ...patch };
} else throw new Error("分镜操作必须是 append、update 或 remove");
next = next.map((row, i) => ({ ...row, shotNumber: i + 1 }));
return [{ type: "update_node", id: node.id, metadata: { storyboard: {
rows: next,
visibleColumns: node.metadata?.storyboard?.visibleColumns || ["shotNumber", "durationSeconds", "videoMotionPrompt", "dialogue", "assets"],
referenceNodeIds: node.metadata?.storyboard?.referenceNodeIds || [],
} } }];
}
+23 -5
View File
@@ -4,10 +4,14 @@ import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
import { CanvasNodeType, type CanvasNodeData, type CanvasNodeMetadata } from "@/types/canvas";
import type { AiConfig } from "@/stores/use-config-store";
import type { CanvasAgentOp, CanvasAgentSnapshot } from "./canvas-agent-ops";
import { createShortDramaPipeline } from "./canvas-short-drama";
import { createStoryboardRow } from "./canvas-project-domain";
export type CanvasWorkflowNodeKind =
| "text"
| "script"
| "styleboard"
| "story_input"
| "image"
| "video"
| "audio"
@@ -28,6 +32,7 @@ export type CanvasWorkflowNodeInput = {
runGeneration?: boolean;
width?: number;
height?: number;
shots?: Array<{ durationSeconds: number; videoMotionPrompt: string; dialogue?: string }>;
};
export type CanvasWorkflowInput = {
@@ -102,6 +107,18 @@ export function buildCanvasWorkflowOps(input: CanvasWorkflowInput, snapshot: Can
],
} : {}),
};
if (["styleboard", "story_input", "script"].includes(node.kind)) {
const template = createShortDramaPipeline({ x: 0, y: 0 });
const source = template.nodes[node.kind === "styleboard" ? 0 : node.kind === "story_input" ? 1 : 2];
Object.assign(metadata, source.metadata, { content: node.kind === "styleboard" ? "" : node.content || "", composerContent: prompt });
if (node.kind === "script") {
if (node.shots && (!Array.isArray(node.shots) || node.shots.length > 100)) throw new Error("分镜最多 100 行");
metadata.storyboard = { ...source.metadata!.storyboard!, rows: (node.shots || []).map((shot, rowIndex) => {
if (!Number.isFinite(shot.durationSeconds) || shot.durationSeconds <= 0 || typeof shot.videoMotionPrompt !== "string" || !shot.videoMotionPrompt.trim()) throw new Error("每条分镜需要有效时长和视频提示词");
return createStoryboardRow(rowIndex + 1, { id: `${id}:shot:${rowIndex + 1}`, durationSeconds: shot.durationSeconds, videoMotionPrompt: shot.videoMotionPrompt, dialogue: typeof shot.dialogue === "string" ? shot.dialogue : "" });
}) };
}
}
ops.push({ type: "add_node", id, nodeType: type, title: node.title, position, width: size.width, height: size.height, metadata });
});
@@ -113,20 +130,20 @@ export function buildCanvasWorkflowOps(input: CanvasWorkflowInput, snapshot: Can
const key = `${edge.from}\0${edge.to}`;
if (edgeKeys.has(key)) continue;
edgeKeys.add(key);
ops.push({ type: "connect_nodes", fromNodeId: ids.get(edge.from)!, toNodeId: ids.get(edge.to)! });
ops.push({ type: "connect_nodes", fromNodeId: ids.get(edge.from)!, toNodeId: ids.get(edge.to)!, ...(input.nodes.find((node) => node.ref === edge.to)?.kind === "script" ? { toHandleId: "storyboard:context" } : {}) });
}
for (const node of input.nodes) {
for (const referenceRef of node.referenceRefs || []) {
const key = `${referenceRef}\0${node.ref}`;
if (edgeKeys.has(key)) continue;
edgeKeys.add(key);
ops.push({ type: "connect_nodes", fromNodeId: ids.get(referenceRef)!, toNodeId: ids.get(node.ref)! });
ops.push({ type: "connect_nodes", fromNodeId: ids.get(referenceRef)!, toNodeId: ids.get(node.ref)!, ...(node.kind === "script" ? { toHandleId: "storyboard:context" } : {}) });
}
for (const referenceNodeId of node.referenceNodeIds || []) {
const key = `${referenceNodeId}\0${node.ref}`;
if (edgeKeys.has(key)) continue;
edgeKeys.add(key);
ops.push({ type: "connect_nodes", fromNodeId: referenceNodeId, toNodeId: ids.get(node.ref)! });
ops.push({ type: "connect_nodes", fromNodeId: referenceNodeId, toNodeId: ids.get(node.ref)!, ...(node.kind === "script" ? { toHandleId: "storyboard:context" } : {}) });
}
}
const targetIds = input.nodes.map((node) => ids.get(node.ref)!);
@@ -177,7 +194,8 @@ function nodeTypeForWorkflowKind(kind: CanvasWorkflowNodeKind) {
if (kind === "image" || kind === "character_cards" || kind === "character_three_view") return CanvasNodeType.Image;
if (kind === "video" || kind === "storyboard_video") return CanvasNodeType.Video;
if (kind === "audio") return CanvasNodeType.Audio;
return CanvasNodeType.Text;
if (["text", "styleboard", "story_input"].includes(kind)) return CanvasNodeType.Text;
throw new Error(`未知工作流节点类型:${kind}`);
}
function nodeSize(type: CanvasNodeType, kind: CanvasWorkflowNodeKind, width?: number, height?: number) {
@@ -187,7 +205,7 @@ function nodeSize(type: CanvasNodeType, kind: CanvasWorkflowNodeKind, width?: nu
: kind === "storyboard_video"
? { width: 640, height: 360 }
: defaults;
return { width: width || preferred.width, height: height || preferred.height };
return { width: width || (kind === "styleboard" ? 360 : kind === "story_input" ? 420 : preferred.width), height: height || (kind === "styleboard" ? 220 : kind === "story_input" ? 260 : preferred.height) };
}
function workflowKindForNode(kind: CanvasWorkflowNodeKind): CanvasNodeMetadata["workflowKind"] {
@@ -13,6 +13,12 @@ const FALLBACK_MIN_SIZE = { width: 220, height: 160 } as const;
/** 批量注册节点定义 */
export function registerNodeDefinitions(defs: CanvasNodeDefinition[], ownerId = "builtin") {
const ids = new Set<string>();
for (const def of defs) {
const owner = ownerByType.get(def.type);
if (ids.has(def.type) || (owner && owner !== ownerId)) throw new Error(`节点类型重复或归属冲突:${def.type}`);
ids.add(def.type);
}
for (const def of defs) {
definitions.set(def.type, def);
ownerByType.set(def.type, ownerId);