feat: 完成短剧项目空间与结构化资产创作闭环 (#38)
* feat(canvas): 短剧项目空间 - 建立项目章节与画布关联 MVP * feat(canvas): 短剧项目空间 - 完善项目资产、镜头与流程闭环 Refs #30 * fix(deploy): 本地部署 - 修复前端构建并忽略本地 PostgreSQL 配置 * feat(*): 短剧项目空间 - 完成项目、章节、资产、角色与画布创作闭环 Refs #30 --------- Co-authored-by: ddCat <ddcat666@88.com>
This commit is contained in:
@@ -15,6 +15,7 @@ export type CanvasAgentOp =
|
||||
|
||||
export type CanvasAgentSnapshot = {
|
||||
projectId: string;
|
||||
domainProjectId?: string;
|
||||
title: string;
|
||||
nodes: CanvasNodeData[];
|
||||
connections: CanvasConnection[];
|
||||
@@ -22,6 +23,15 @@ export type CanvasAgentSnapshot = {
|
||||
viewport: ViewportTransform;
|
||||
};
|
||||
|
||||
export type CanvasAgentOperationImpact = {
|
||||
operationCount: number;
|
||||
affectedNodeCount: number;
|
||||
destructiveCount: number;
|
||||
generationCount: number;
|
||||
items: string[];
|
||||
warning: string;
|
||||
};
|
||||
|
||||
export function summarizeCanvasAgentOps(ops?: CanvasAgentOp[]) {
|
||||
const counts = (Array.isArray(ops) ? ops : []).reduce<Record<string, number>>((acc, op) => {
|
||||
if (!op?.type) return acc;
|
||||
@@ -33,6 +43,73 @@ export function summarizeCanvasAgentOps(ops?: CanvasAgentOp[]) {
|
||||
.join(",");
|
||||
}
|
||||
|
||||
export function previewCanvasAgentOps(ops?: CanvasAgentOp[], snapshot?: CanvasAgentSnapshot): CanvasAgentOperationImpact {
|
||||
const safeOps = Array.isArray(ops) ? ops.filter((op) => op?.type) : [];
|
||||
const nodeById = new Map((snapshot?.nodes || []).map((node) => [node.id, node]));
|
||||
const affectedNodeIds = new Set<string>();
|
||||
let addedNodeCount = 0;
|
||||
let destructiveCount = 0;
|
||||
let generationCount = 0;
|
||||
const items: string[] = [];
|
||||
|
||||
safeOps.forEach((op) => {
|
||||
if (op.type === "add_node") {
|
||||
addedNodeCount += 1;
|
||||
items.push(`新增${canvasNodeTypeLabel(op.nodeType)}${op.title ? `「${op.title}」` : ""}`);
|
||||
return;
|
||||
}
|
||||
if (op.type === "update_node") {
|
||||
affectedNodeIds.add(op.id);
|
||||
items.push(`修改「${nodeById.get(op.id)?.title || op.id}」`);
|
||||
return;
|
||||
}
|
||||
if (op.type === "delete_node") {
|
||||
const ids = op.ids || (op.id ? [op.id] : op.nodeType ? (snapshot?.nodes || []).filter((node) => node.type === op.nodeType).map((node) => node.id) : []);
|
||||
ids.forEach((id) => affectedNodeIds.add(id));
|
||||
destructiveCount += Math.max(1, ids.length);
|
||||
const names = ids.slice(0, 3).map((id) => nodeById.get(id)?.title || id);
|
||||
items.push(ids.length ? `删除 ${ids.length} 个节点${names.length ? `:${names.join("、")}${ids.length > names.length ? "等" : ""}` : ""}` : `删除全部${canvasNodeTypeLabel(op.nodeType)}`);
|
||||
return;
|
||||
}
|
||||
if (op.type === "connect_nodes") {
|
||||
affectedNodeIds.add(op.fromNodeId);
|
||||
affectedNodeIds.add(op.toNodeId);
|
||||
items.push(`连接「${nodeById.get(op.fromNodeId)?.title || op.fromNodeId}」到「${nodeById.get(op.toNodeId)?.title || op.toNodeId}」`);
|
||||
return;
|
||||
}
|
||||
if (op.type === "delete_connections") {
|
||||
const count = op.all ? snapshot?.connections.length || 0 : op.ids?.length || (op.id ? 1 : 0);
|
||||
destructiveCount += Math.max(1, count);
|
||||
items.push(op.all ? `删除全部 ${count} 条连线` : `删除 ${count || 1} 条连线`);
|
||||
return;
|
||||
}
|
||||
if (op.type === "run_generation") {
|
||||
affectedNodeIds.add(op.nodeId);
|
||||
generationCount += 1;
|
||||
items.push(`为「${nodeById.get(op.nodeId)?.title || op.nodeId}」触发${generationModeLabel(op.mode)}生成`);
|
||||
return;
|
||||
}
|
||||
if (op.type === "select_nodes") {
|
||||
op.ids.forEach((id) => affectedNodeIds.add(id));
|
||||
items.push(`选择 ${op.ids.length} 个节点`);
|
||||
return;
|
||||
}
|
||||
if (op.type === "set_viewport") items.push("调整当前画布视图");
|
||||
});
|
||||
|
||||
const warnings = [];
|
||||
if (destructiveCount) warnings.push("包含删除操作,批准后可从最近 Agent 批次逐步撤销。");
|
||||
if (generationCount) warnings.push("生成任务可能产生模型费用,画布撤销不会取消已提交任务。");
|
||||
return {
|
||||
operationCount: safeOps.length,
|
||||
affectedNodeCount: affectedNodeIds.size + addedNodeCount,
|
||||
destructiveCount,
|
||||
generationCount,
|
||||
items: items.slice(0, 8),
|
||||
warning: warnings.join(" "),
|
||||
};
|
||||
}
|
||||
|
||||
export function applyCanvasAgentOps(snapshot: CanvasAgentSnapshot, ops?: CanvasAgentOp[]) {
|
||||
let nodes = snapshot.nodes;
|
||||
let connections = snapshot.connections;
|
||||
@@ -104,3 +181,22 @@ function opLabel(type: string) {
|
||||
if (type === "run_generation") return "触发生成";
|
||||
return type;
|
||||
}
|
||||
|
||||
function canvasNodeTypeLabel(type?: CanvasNodeType) {
|
||||
if (type === CanvasNodeType.Image) return "图片节点";
|
||||
if (type === CanvasNodeType.Video) return "视频节点";
|
||||
if (type === CanvasNodeType.Audio) return "音频节点";
|
||||
if (type === CanvasNodeType.Config) return "生成配置";
|
||||
if (type === CanvasNodeType.Script) return "分镜脚本";
|
||||
if (type === CanvasNodeType.Frame) return "背板";
|
||||
if (type === CanvasNodeType.Drawing) return "绘图节点";
|
||||
if (type === CanvasNodeType.Skill) return "技能节点";
|
||||
return "文本节点";
|
||||
}
|
||||
|
||||
function generationModeLabel(mode?: "text" | "image" | "video" | "audio") {
|
||||
if (mode === "text") return "文本";
|
||||
if (mode === "video") return "视频";
|
||||
if (mode === "audio") return "音频";
|
||||
return "图片";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user