From 2a16eb5d461e6c328293bd7f647cf75c4990e9a3 Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Tue, 28 Jul 2026 21:55:42 +0800 Subject: [PATCH] =?UTF-8?q?chore(release):=20=E7=89=88=E6=9C=AC=E5=8F=91?= =?UTF-8?q?=E5=B8=83=20-=20publish=20v1.0.12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++ VERSION | 2 +- backend/internal/service/service.go | 6 +-- canvas-agent/src/canvas-session.ts | 39 ++++++++--------- canvas-agent/src/config.ts | 2 +- canvas-agent/src/schemas.ts | 11 ++--- .../infinite-canvas/skills/canvas/SKILL.md | 4 +- .../canvas/canvas-assistant-panel.tsx | 42 ++++++++++--------- .../components/canvas/canvas-context-menu.tsx | 3 +- .../canvas/canvas-local-agent-panel.tsx | 1 - web/src/components/canvas/canvas-node.tsx | 3 +- web/src/components/canvas/canvas-toolbar.tsx | 9 +--- .../canvas/canvas-workspace-overlays.tsx | 5 +-- .../canvas/canvas-text-generation-executor.ts | 14 ++++--- web/src/pages/canvas/project.tsx | 19 +++++---- .../use-canvas-connection-controller.ts | 11 ++--- web/src/pages/canvas/use-canvas-director.ts | 3 +- .../pages/canvas/use-canvas-media-tools.ts | 17 ++++---- .../canvas/use-canvas-node-operations.ts | 14 +------ 19 files changed, 99 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ce741986..6be1cb4e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +## v1.0.12 + +- 画布退役生成配置节点的新建入口,文本生图、图片反推、导演台及 Agent 编排改为直接创建对应类型的生成目标节点。 +- 历史生成配置节点继续保持读取和执行兼容,直接文本目标支持按数量参数批量生成结果。 + ## v1.0.11 - 修复 PostgreSQL 请求明细分页继承 `DISTINCT` 后与创建时间排序冲突的问题,恢复管理后台列表读取。 diff --git a/VERSION b/VERSION index 7c46aa193..7c866b731 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.0.11 +v1.0.12 diff --git a/backend/internal/service/service.go b/backend/internal/service/service.go index ffdcbe34d..e2c171eba 100644 --- a/backend/internal/service/service.go +++ b/backend/internal/service/service.go @@ -1188,7 +1188,7 @@ func buildAgentStoryboardResult(task model.Task, plan agentStoryboardPlan, asset assetIDs = append(assetIDs, asset.ID) } ops = append(ops, - nodeOpWithMetadata(shotID, "config", fmt.Sprintf("镜头 %d · %s", index+1, shortTitle(shot.Title, 18)), index*360, 560, map[string]any{ + nodeOpWithMetadata(shotID, "video", fmt.Sprintf("镜头 %d · %s", index+1, shortTitle(shot.Title, 18)), index*360, 560, map[string]any{ "workflowKind": "shot", "workflowTitle": shot.Title, "workflowDescription": shotDescription(shot), @@ -1526,7 +1526,7 @@ func buildAgentResult(task model.Task) (map[string]any, []map[string]any) { ops := []map[string]any{ nodeOp("script-"+task.ID, "text", "剧本 · "+title, 0, 0, "script", task.Prompt), nodeOp("scene-"+task.ID, "text", "场景 · 主场景", 380, 0, "scene", "主场景设定、角色关系、视觉风格。"), - nodeOp("shot-"+task.ID, "config", "分镜 · 镜头 1", 760, 0, "shot", task.Prompt), + nodeOpWithMetadata("shot-"+task.ID, "video", "分镜 · 镜头 1", 760, 0, map[string]any{"workflowKind": "shot", "status": "idle", "generationMode": "video", "prompt": task.Prompt, "composerContent": task.Prompt, "videoEditOperation": "text_to_video"}), nodeOp("final-"+task.ID, "video", "成片 · 待生成", 1140, 0, "final", ""), connectOp("script-"+task.ID, "scene-"+task.ID), connectOp("scene-"+task.ID, "shot-"+task.ID), @@ -1555,7 +1555,7 @@ func buildVideoWorkflowResult(task model.Task) (map[string]any, []map[string]any ops := []map[string]any{ nodeOp("video-brief-"+task.ID, "text", "编辑需求 · "+title, 0, 0, "script", task.Prompt), nodeOpWithMetadata("video-ref-"+task.ID, "text", "参考素材组", 380, 0, map[string]any{"workflowKind": "reference_set", "status": "idle", "content": "原片、参考图、参考音频、风格板或历史版本。", "videoEditOperation": operation}), - nodeOpWithMetadata("video-shot-"+task.ID, "config", "视频任务 · "+operation, 760, 0, map[string]any{"workflowKind": "shot", "status": "idle", "generationMode": "video", "prompt": task.Prompt, "composerContent": task.Prompt, "videoEditOperation": operation}), + nodeOpWithMetadata("video-shot-"+task.ID, "video", "视频任务 · "+operation, 760, 0, map[string]any{"workflowKind": "shot", "status": "idle", "generationMode": "video", "prompt": task.Prompt, "composerContent": task.Prompt, "videoEditOperation": operation}), nodeOpWithMetadata("video-result-"+task.ID, "video", "结果版本 · 待回填", 1140, 0, map[string]any{"workflowKind": "final", "status": "idle", "videoEditOperation": operation, "versionLabel": "v1"}), connectOp("video-brief-"+task.ID, "video-ref-"+task.ID), connectOp("video-ref-"+task.ID, "video-shot-"+task.ID), diff --git a/canvas-agent/src/canvas-session.ts b/canvas-agent/src/canvas-session.ts index 181d260f0..e5f22d005 100644 --- a/canvas-agent/src/canvas-session.ts +++ b/canvas-agent/src/canvas-session.ts @@ -86,16 +86,6 @@ export class CanvasSession { input = { ops: generationFlowOps({ ...(input as Record), mode: "image" }, this.canvasState) }; tool = "canvas_apply_ops"; } - if (tool === "canvas_create_config_node") { - const data = input as Record; - const x = Number(data.x ?? nextCanvasX(this.canvasState)); - const y = Number(data.y ?? 0); - const configId = `config-${crypto.randomUUID()}`; - const mode = generationMode(data.mode); - const prompt = String(data.prompt || ""); - input = { ops: [configNodeOp(configId, data, x, y), ...(data.autoRun ? [runGenerationOp(configId, mode, prompt)] : [])] }; - tool = "canvas_apply_ops"; - } if (tool === "canvas_create_generation_flow") { input = { ops: generationFlowOps(input as Record, this.canvasState) }; tool = "canvas_apply_ops"; @@ -179,18 +169,21 @@ function textNodeOp(input: { id?: string; text?: string; title?: string; width?: return { type: "add_node", id: input.id, nodeType: "text", title: input.title, position: { x, y }, width: input.width, height: input.height, metadata: { content: input.text || "", status: "success", fontSize: 14 } }; } -function configNodeOp(id: string, input: Record, x: number, y: number) { +function generationTargetNodeOp(id: string, input: Record, x: number, y: number) { const mode = generationMode(input.mode); const prompt = String(input.prompt || ""); + const nodeType = generationNodeType(mode); return { type: "add_node", id, - nodeType: "config", + nodeType, title: String(input.title || generationTitle(mode)), position: { x, y }, width: typeof input.width === "number" ? input.width : undefined, height: typeof input.height === "number" ? input.height : undefined, metadata: cleanRecord({ + content: "", + fontSize: nodeType === "text" ? 14 : undefined, generationMode: mode, composerContent: prompt, prompt, @@ -198,6 +191,7 @@ function configNodeOp(id: string, input: Record, x: number, y: model: input.model, size: input.size, quality: input.quality, + transparentBackground: input.transparentBackground, count: input.count, seconds: input.seconds, vquality: input.vquality, @@ -217,20 +211,27 @@ function generationFlowOps(input: Record, state: CanvasSnapshot const x = Number(input.x ?? nextCanvasX(state)); const y = Number(input.y ?? 0); const textId = `text-${crypto.randomUUID()}`; - const configId = `config-${crypto.randomUUID()}`; + const targetId = `${mode}-${crypto.randomUUID()}`; const referenceNodeIds = Array.isArray(input.referenceNodeIds) ? input.referenceNodeIds.filter((id): id is string => typeof id === "string") : []; const tokens = [`@[node:${textId}]`, ...referenceNodeIds.map((id) => `@[node:${id}]`)]; - const configInput = { ...input, prompt: tokens.join("\n") }; + const targetInput = { ...input, prompt: tokens.join("\n") }; return [ textNodeOp({ id: textId, text: prompt, title: String(input.title || "提示词") }, x, y), - configNodeOp(configId, configInput, x + 420, y), - { type: "connect_nodes", fromNodeId: textId, toNodeId: configId }, - ...referenceNodeIds.map((fromNodeId) => ({ type: "connect_nodes", fromNodeId, toNodeId: configId })), - { type: "select_nodes", ids: [configId] }, - ...(input.autoRun ? [runGenerationOp(configId, mode, tokens.join("\n"))] : []), + generationTargetNodeOp(targetId, targetInput, x + 420, y), + { type: "connect_nodes", fromNodeId: textId, toNodeId: targetId }, + ...referenceNodeIds.map((fromNodeId) => ({ type: "connect_nodes", fromNodeId, toNodeId: targetId })), + { type: "select_nodes", ids: [targetId] }, + ...(input.autoRun ? [runGenerationOp(targetId, mode, tokens.join("\n"))] : []), ]; } +function generationNodeType(mode: "text" | "image" | "video" | "audio"): CanvasNodeType { + if (mode === "text") return "text"; + if (mode === "video") return "video"; + if (mode === "audio") return "audio"; + return "image"; +} + function runGenerationOp(nodeId: string, mode: "text" | "image" | "video" | "audio", prompt?: string) { return { type: "run_generation", nodeId, mode, prompt }; } diff --git a/canvas-agent/src/config.ts b/canvas-agent/src/config.ts index 63554ea2b..c4908f84f 100644 --- a/canvas-agent/src/config.ts +++ b/canvas-agent/src/config.ts @@ -7,7 +7,7 @@ export const DEFAULT_PORT = 17371; export const CONFIG_DIR = path.join(os.homedir(), ".infinite-canvas"); export const CONFIG_FILE = path.join(CONFIG_DIR, "canvas-agent.json"); export const VERSION = readPackageVersion(); -export const AGENT_PROMPT = "你正在帮助用户操作影策网页画布。需要改动画布时优先使用已配置的 infinite-canvas MCP 工具:先 canvas_get_state 读取当前画布,再根据任务使用 canvas_create_text_node、canvas_generate_text、canvas_generate_image、canvas_generate_video、canvas_generate_audio、canvas_create_generation_flow、canvas_create_config_node、canvas_run_generation、canvas_update_node、canvas_connect_nodes 等通用工具;复杂批量改动再用 canvas_apply_ops,删除连线可用 delete_connections。需要生成内容时直接调用对应生成工具,不要绑定特定业务场景。不要模拟鼠标点击,不要要求用户手动复制 JSON。"; +export const AGENT_PROMPT = "你正在帮助用户操作影策网页画布。需要改动画布时优先使用已配置的 infinite-canvas MCP 工具:先 canvas_get_state 读取当前画布,再根据任务使用 canvas_create_text_node、canvas_generate_text、canvas_generate_image、canvas_generate_video、canvas_generate_audio、canvas_create_generation_flow、canvas_run_generation、canvas_update_node、canvas_connect_nodes 等通用工具;复杂批量改动再用 canvas_apply_ops,删除连线可用 delete_connections。需要生成内容时直接调用对应生成工具,不要绑定特定业务场景。不要模拟鼠标点击,不要要求用户手动复制 JSON。"; export type CanvasWorkspaceConfig = { workspacePath: string; activeThreadId?: string; pinnedThreadIds?: string[] }; export type CanvasAgentConfig = { url: string; token: string; origins?: string[]; canvases?: Record }; diff --git a/canvas-agent/src/schemas.ts b/canvas-agent/src/schemas.ts index d8c8102e1..22e2607ab 100644 --- a/canvas-agent/src/schemas.ts +++ b/canvas-agent/src/schemas.ts @@ -3,7 +3,7 @@ import { z } from "zod"; const recordSchema = z.record(z.unknown()); const positionSchema = z.object({ x: z.number(), y: z.number() }); const viewportSchema = z.object({ x: z.number(), y: z.number(), k: z.number() }); -const nodeTypeSchema = z.enum(["image", "text", "script", "config", "video", "audio", "frame"]); +const nodeTypeSchema = z.enum(["image", "text", "script", "video", "audio", "frame"]); const generationModeSchema = z.enum(["text", "image", "video", "audio"]); const projectIdSchema = z.string().min(1).optional(); const projectCandidateSchema = z.object({ unitId: z.string().optional(), shotId: z.string().optional(), name: z.string().min(1), category: z.string().min(1), details: recordSchema.optional() }); @@ -17,7 +17,6 @@ export const toolNames = [ "canvas_create_node", "canvas_create_text_node", "canvas_create_text_nodes", - "canvas_create_config_node", "canvas_create_image_prompt_flow", "canvas_create_generation_flow", "canvas_generate_text", @@ -97,7 +96,6 @@ export const toolInputSchemas = { canvas_create_node: z.object({ nodeType: nodeTypeSchema, title: z.string().optional(), x: z.number().optional(), y: z.number().optional(), width: z.number().optional(), height: z.number().optional(), metadata: recordSchema.optional() }), canvas_create_text_node: z.object({ text: z.string().optional(), x: z.number().optional(), y: z.number().optional(), title: z.string().optional(), width: z.number().optional(), height: z.number().optional() }), canvas_create_text_nodes: z.object({ items: z.array(textNodeSchema).min(1), x: z.number().optional(), y: z.number().optional(), gap: z.number().optional(), direction: z.enum(["row", "column"]).optional() }), - canvas_create_config_node: z.object({ prompt: z.string().optional(), mode: generationModeSchema.optional(), title: z.string().optional(), x: z.number().optional(), y: z.number().optional(), width: z.number().optional(), height: z.number().optional(), autoRun: z.boolean().optional() }).merge(generationOptionsSchema), canvas_create_image_prompt_flow: z.object({ prompt: z.string(), x: z.number().optional(), y: z.number().optional(), autoRun: z.boolean().optional() }).merge(generationOptionsSchema), canvas_create_generation_flow: generationFlowSchema.extend({ mode: generationModeSchema.optional(), autoRun: z.boolean().optional() }).merge(generationOptionsSchema), canvas_generate_text: generationFlowSchema.merge(generationOptionsSchema), @@ -130,12 +128,11 @@ export const toolDescriptions: Record = { canvas_get_selection: "读取当前网页画布选中的节点。", canvas_export_snapshot: "导出当前画布快照,用于理解布局。", canvas_apply_ops: "批量操作当前网页画布。ops 支持 add_node、update_node、delete_node、delete_connections、connect_nodes、set_viewport、select_nodes、run_generation。", - canvas_create_node: "创建任意类型节点:text、script、image、config、video、audio、frame。适合创建脚本、媒体占位、配置节点、背板或自定义 metadata 节点。", + canvas_create_node: "创建任意类型节点:text、script、image、video、audio、frame。适合创建脚本、媒体占位、背板或自定义 metadata 节点。", canvas_create_text_node: "在当前画布创建单个文本节点。", canvas_create_text_nodes: "批量创建文本节点,适合生成标题、段落、脚本、说明等内容块。", - canvas_create_config_node: "创建生成配置节点,可指定 text/image/video/audio 模式和生成参数,可选择立即触发生成。", - canvas_create_image_prompt_flow: "创建提示词文本节点和图片生成配置节点,并自动连线,可选择立即触发生图。", - canvas_create_generation_flow: "创建通用生成流程:提示词文本节点、生成配置节点、参考节点连线,可用于文案、生图、视频或音频。", + canvas_create_image_prompt_flow: "创建提示词文本节点和图片目标节点并自动连线,可选择立即触发生图。", + canvas_create_generation_flow: "创建通用生成流程:提示词文本节点、对应类型的生成目标节点和参考节点连线,可用于文案、生图、视频或音频。", canvas_generate_text: "创建通用文本生成流程并立即触发生成。", canvas_generate_image: "创建通用图片生成流程并立即触发生成。", canvas_generate_video: "创建通用视频生成流程并立即触发生成。", diff --git a/plugins/infinite-canvas/skills/canvas/SKILL.md b/plugins/infinite-canvas/skills/canvas/SKILL.md index 63be462fe..682958acf 100644 --- a/plugins/infinite-canvas/skills/canvas/SKILL.md +++ b/plugins/infinite-canvas/skills/canvas/SKILL.md @@ -13,7 +13,7 @@ description: 操作影策当前网页画布,读取节点、选区、创建文 - 操作前先用 `canvas_get_state` 读取当前画布;如果用户明确提到选中内容、当前节点或“这个”,先用 `canvas_get_selection`。 - 创建单个文本内容优先用 `canvas_create_text_node`。 - 创建生成内容优先用 `canvas_generate_text`、`canvas_generate_image`、`canvas_generate_video`、`canvas_generate_audio`。 -- 需要把提示词、配置和生成节点串成流程时,使用 `canvas_create_generation_flow` 或项目已有的流程工具。 +- 需要把提示词、参考素材和生成目标节点串成流程时,使用 `canvas_create_generation_flow` 或项目已有的流程工具。 - 需要批量增删改、移动、连接节点或设置视口时,使用 `canvas_apply_ops`。 - 不要模拟鼠标点击,不要要求用户手动复制 JSON。 - 写入画布的操作会由网页侧边栏做二次确认,按当前工具结果继续推进即可。 @@ -21,7 +21,7 @@ description: 操作影策当前网页画布,读取节点、选区、创建文 ## 风格 - 页面文案和画布节点内容默认使用中文。 -- 生成节点、配置节点和提示词节点要保持结构清晰,方便用户继续编辑。 +- 生成目标节点和提示词节点要保持结构清晰,方便用户继续编辑。 - 批量创建节点时注意给节点留出间距,不要堆叠在同一个位置。 - 图片、视频、音频等媒体节点默认保留原始比例;只有用户明确要求自由变形时才改变比例。 - 生成流程尽量少而清楚,优先让用户一眼能看懂节点关系。 diff --git a/web/src/components/canvas/canvas-assistant-panel.tsx b/web/src/components/canvas/canvas-assistant-panel.tsx index 9d7d11358..8bebc6532 100644 --- a/web/src/components/canvas/canvas-assistant-panel.tsx +++ b/web/src/components/canvas/canvas-assistant-panel.tsx @@ -31,7 +31,7 @@ const ONLINE_AGENT_PROMPT = const JSON_RECORD_SCHEMA = { type: "object", additionalProperties: true }; const POSITION_SCHEMA = { type: "object", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"], additionalProperties: false }; const VIEWPORT_SCHEMA = { type: "object", properties: { x: { type: "number" }, y: { type: "number" }, k: { type: "number" } }, required: ["x", "y", "k"], additionalProperties: false }; -const NODE_TYPE_SCHEMA = { type: "string", enum: ["image", "text", "skill", "config", "video", "audio"] }; +const NODE_TYPE_SCHEMA = { type: "string", enum: ["image", "text", "skill", "video", "audio"] }; const GENERATION_MODE_SCHEMA = { type: "string", enum: ["text", "image", "video", "audio"] }; const GENERATION_OPTION_PROPERTIES = { model: { type: "string" }, @@ -94,13 +94,12 @@ const ONLINE_AGENT_TOOLS: ResponseFunctionTool[] = [ toolDefinition("canvas_get_selection", "读取当前网页画布选中的节点。", {}), toolDefinition("canvas_export_snapshot", "导出当前画布快照,用于理解布局。", {}), toolDefinition("canvas_apply_ops", "批量操作当前网页画布。ops 支持 add_node、update_node、delete_node、delete_connections、connect_nodes、set_viewport、select_nodes、run_generation。", { ops: { type: "array", items: CANVAS_OP_SCHEMA } }, ["ops"], false), - toolDefinition("canvas_create_node", "创建任意类型节点:text、image、config、video、audio。适合创建占位图、媒体占位、配置节点或自定义 metadata 节点。", { nodeType: NODE_TYPE_SCHEMA, title: { type: "string" }, x: { type: "number" }, y: { type: "number" }, width: { type: "number" }, height: { type: "number" }, metadata: JSON_RECORD_SCHEMA }, ["nodeType"]), + toolDefinition("canvas_create_node", "创建任意类型节点:text、image、video、audio。适合创建文本、媒体占位或自定义 metadata 节点。", { nodeType: NODE_TYPE_SCHEMA, title: { type: "string" }, x: { type: "number" }, y: { type: "number" }, width: { type: "number" }, height: { type: "number" }, metadata: JSON_RECORD_SCHEMA }, ["nodeType"]), toolDefinition("canvas_create_text_node", "在当前画布创建单个文本节点。", { text: { type: "string" }, x: { type: "number" }, y: { type: "number" }, title: { type: "string" }, width: { type: "number" }, height: { type: "number" } }), toolDefinition("canvas_create_text_nodes", "批量创建文本节点,适合生成标题、段落、脚本、说明等内容块。", { items: { type: "array", minItems: 1, items: { type: "object", properties: { text: { type: "string" }, title: { type: "string" }, x: { type: "number" }, y: { type: "number" }, width: { type: "number" }, height: { type: "number" } }, required: ["text"], additionalProperties: false } }, x: { type: "number" }, y: { type: "number" }, gap: { type: "number" }, direction: { type: "string", enum: ["row", "column"] } }, ["items"]), toolDefinition("canvas_create_cinematic_session", "把自然语言创作指令提交给后端影视 Agent 会话,后端拆解为剧本、场景、分镜、镜头和成片节点,并返回可写回画布的操作。", { prompt: { type: "string" } }, ["prompt"]), - toolDefinition("canvas_create_config_node", "创建生成配置节点,可指定 text/image/video/audio 模式和生成参数,可选择立即触发生成。", { prompt: { type: "string" }, mode: GENERATION_MODE_SCHEMA, title: { type: "string" }, x: { type: "number" }, y: { type: "number" }, width: { type: "number" }, height: { type: "number" }, autoRun: { type: "boolean" }, ...GENERATION_OPTION_PROPERTIES }), - toolDefinition("canvas_create_image_prompt_flow", "创建提示词文本节点和图片生成配置节点,并自动连线,可选择立即触发生图。", { prompt: { type: "string" }, x: { type: "number" }, y: { type: "number" }, autoRun: { type: "boolean" }, ...GENERATION_OPTION_PROPERTIES }, ["prompt"]), - generationToolDefinition("canvas_create_generation_flow", "创建通用生成流程:提示词文本节点、生成配置节点、参考节点连线,可用于文案、生图、视频或音频。"), + toolDefinition("canvas_create_image_prompt_flow", "创建提示词文本节点和图片目标节点并自动连线,可选择立即触发生图。", { prompt: { type: "string" }, x: { type: "number" }, y: { type: "number" }, autoRun: { type: "boolean" }, ...GENERATION_OPTION_PROPERTIES }, ["prompt"]), + generationToolDefinition("canvas_create_generation_flow", "创建通用生成流程:提示词文本节点、对应类型的生成目标节点和参考节点连线,可用于文案、生图、视频或音频。"), generationToolDefinition("canvas_generate_text", "创建通用文本生成流程并立即触发生成。", "text"), generationToolDefinition("canvas_generate_image", "创建通用图片生成流程并立即触发生成。", "image"), generationToolDefinition("canvas_generate_video", "创建通用视频生成流程并立即触发生成。", "video"), @@ -1177,7 +1176,6 @@ function onlineToolToOps(name: string, input: Record, snapshot: const nodeType = requireNodeType(input.nodeType); const x = numberOr(input.x, nextCanvasX(snapshot)); const y = numberOr(input.y, 0); - if (nodeType === CanvasNodeType.Config) return [configNodeOp(stringOptional(input.id) || `config-${nanoid()}`, { ...recordOptional(input.metadata), ...input }, x, y, config)]; return [{ type: "add_node", nodeType, title: stringOptional(input.title), position: { x, y }, width: numberOptional(input.width), height: numberOptional(input.height), metadata: recordOptional(input.metadata) as CanvasNodeData["metadata"] }]; } if (name === "canvas_create_text_node") return [textNodeOp(input, numberOr(input.x, nextCanvasX(snapshot)), numberOr(input.y, 0))]; @@ -1190,11 +1188,6 @@ function onlineToolToOps(name: string, input: Record, snapshot: return items.map((item, index) => textNodeOp({ ...item, text: requireString(item.text, "text") }, numberOr(item.x, direction === "row" ? x + index * (NODE_DEFAULT_SIZE[CanvasNodeType.Text].width + gap) : x), numberOr(item.y, direction === "row" ? y : y + index * (NODE_DEFAULT_SIZE[CanvasNodeType.Text].height + gap)))); } if (name === "canvas_create_image_prompt_flow") return generationFlowOps({ ...input, mode: "image" }, snapshot, config); - if (name === "canvas_create_config_node") { - const configId = `config-${nanoid()}`; - const mode = generationMode(input.mode); - return [configNodeOp(configId, input, numberOr(input.x, nextCanvasX(snapshot)), numberOr(input.y, 0), config), ...(input.autoRun ? [runGenerationOp(configId, mode, stringOptional(input.prompt))] : [])]; - } if (name === "canvas_create_generation_flow") return generationFlowOps(input, snapshot, config); if (name === "canvas_generate_text") return generationFlowOps({ ...input, mode: "text", autoRun: true }, snapshot, config); if (name === "canvas_generate_image") return generationFlowOps({ ...input, mode: "image", autoRun: true }, snapshot, config); @@ -1224,16 +1217,16 @@ function generationFlowOps(input: Record, snapshot: CanvasAgent const x = numberOr(input.x, nextCanvasX(snapshot)); const y = numberOr(input.y, 0); const textId = `text-${nanoid()}`; - const configId = `config-${nanoid()}`; + const targetId = `${mode}-${nanoid()}`; const referenceNodeIds = Array.isArray(input.referenceNodeIds) ? input.referenceNodeIds.filter((id): id is string => typeof id === "string") : []; const tokens = [`@[node:${textId}]`, ...referenceNodeIds.map((id) => `@[node:${id}]`)]; return [ textNodeOp({ id: textId, text: prompt, title: stringOptional(input.title) || "提示词" }, x, y), - configNodeOp(configId, { ...input, prompt: tokens.join("\n") }, x + NODE_DEFAULT_SIZE[CanvasNodeType.Text].width + 80, y, config), - { type: "connect_nodes", fromNodeId: textId, toNodeId: configId }, - ...referenceNodeIds.map((fromNodeId) => ({ type: "connect_nodes" as const, fromNodeId, toNodeId: configId })), - { type: "select_nodes", ids: [configId] }, - ...(input.autoRun ? [runGenerationOp(configId, mode, tokens.join("\n"))] : []), + generationTargetNodeOp(targetId, { ...input, prompt: tokens.join("\n") }, x + NODE_DEFAULT_SIZE[CanvasNodeType.Text].width + 80, y, config), + { type: "connect_nodes", fromNodeId: textId, toNodeId: targetId }, + ...referenceNodeIds.map((fromNodeId) => ({ type: "connect_nodes" as const, fromNodeId, toNodeId: targetId })), + { type: "select_nodes", ids: [targetId] }, + ...(input.autoRun ? [runGenerationOp(targetId, mode, tokens.join("\n"))] : []), ]; } @@ -1241,18 +1234,21 @@ function textNodeOp(input: Record, x: number, y: number): Canva return { type: "add_node", id: stringOptional(input.id), nodeType: CanvasNodeType.Text, title: stringOptional(input.title), position: { x, y }, width: numberOptional(input.width), height: numberOptional(input.height), metadata: { content: stringOptional(input.text), status: "success", fontSize: 14 } }; } -function configNodeOp(id: string, input: Record, x: number, y: number, config: AiConfig): CanvasAgentOp { +function generationTargetNodeOp(id: string, input: Record, x: number, y: number, config: AiConfig): CanvasAgentOp { const mode = generationMode(input.mode); const prompt = stringOptional(input.prompt); + const nodeType = generationNodeType(mode); return { type: "add_node", id, - nodeType: CanvasNodeType.Config, + nodeType, title: stringOptional(input.title) || generationTitle(mode), position: { x, y }, width: numberOptional(input.width), height: numberOptional(input.height), metadata: cleanRecord({ + content: "", + fontSize: nodeType === CanvasNodeType.Text ? 14 : undefined, generationMode: mode, composerContent: prompt, prompt, @@ -1274,6 +1270,13 @@ function configNodeOp(id: string, input: Record, x: number, y: }; } +function generationNodeType(mode: "text" | "image" | "video" | "audio") { + if (mode === "text") return CanvasNodeType.Text; + if (mode === "video") return CanvasNodeType.Video; + if (mode === "audio") return CanvasNodeType.Audio; + return CanvasNodeType.Image; +} + function runGenerationOp(nodeId: string, mode: "text" | "image" | "video" | "audio", prompt?: string): CanvasAgentOp { return { type: "run_generation", nodeId, mode, prompt }; } @@ -1333,7 +1336,6 @@ function toolCallLabel(name: string) { if (name === "canvas_create_node") return "创建节点"; if (name === "canvas_create_text_node") return "创建文本"; if (name === "canvas_create_text_nodes") return "批量创建文本"; - if (name === "canvas_create_config_node") return "创建生成配置"; if (name === "canvas_create_image_prompt_flow") return "创建生图流程"; if (name === "canvas_create_generation_flow") return "创建生成流程"; if (name === "canvas_generate_text") return "生成文本"; diff --git a/web/src/components/canvas/canvas-context-menu.tsx b/web/src/components/canvas/canvas-context-menu.tsx index 8ce6ef8fc..75b9c36cf 100644 --- a/web/src/components/canvas/canvas-context-menu.tsx +++ b/web/src/components/canvas/canvas-context-menu.tsx @@ -1,6 +1,6 @@ import { AnimatePresence, useReducedMotion } from "motion/react"; import { useEffect, useState, type CSSProperties, type ReactNode } from "react"; -import { ArrowLeft, Check, ChevronRight, Clapperboard, Clipboard, Copy, FolderOpen, FolderPlus, Image as ImageIcon, Layers3, Link2, Maximize2, Music2, PanelTop, Pencil, Plus, Redo2, Settings2, Tags, Trash2, Type, Undo2, Upload, UserRound, Video } from "lucide-react"; +import { ArrowLeft, Check, ChevronRight, Clapperboard, Clipboard, Copy, FolderOpen, FolderPlus, Image as ImageIcon, Layers3, Link2, Maximize2, Music2, PanelTop, Pencil, Plus, Redo2, Tags, Trash2, Type, Undo2, Upload, UserRound, Video } from "lucide-react"; import { aceternityMotion } from "@/lib/aceternity-motion"; import { SpotlightSurface } from "@/components/ui/aceternity/spotlight-surface"; @@ -259,7 +259,6 @@ function AddNodeContextMenu({ parentPosition, workspaceMode, isProjectLinked, re ...(!simpleMode ? [ { id: "director", label: "导演台", icon: , badge: "3D", onClick: onOpenDirector }, { id: "audio", label: "音频", icon: , onClick: () => onAddNode(CanvasNodeType.Audio) }, - { id: "config", label: "生成配置", icon: , onClick: () => onAddNode(CanvasNodeType.Config) }, ] : []), ]; const resourceCommands: CanvasCreateCommand[] = [ diff --git a/web/src/components/canvas/canvas-local-agent-panel.tsx b/web/src/components/canvas/canvas-local-agent-panel.tsx index 3efea4b84..bed6534a8 100644 --- a/web/src/components/canvas/canvas-local-agent-panel.tsx +++ b/web/src/components/canvas/canvas-local-agent-panel.tsx @@ -875,7 +875,6 @@ function toolName(name: string) { if (name === "canvas_create_node") return "创建节点"; if (name === "canvas_create_text_node") return "创建文本"; if (name === "canvas_create_text_nodes") return "批量创建文本"; - if (name === "canvas_create_config_node") return "创建生成配置"; if (name === "canvas_create_image_prompt_flow") return "创建生图流程"; if (name === "canvas_create_generation_flow") return "创建生成流程"; if (name === "canvas_generate_text") return "生成文本"; diff --git a/web/src/components/canvas/canvas-node.tsx b/web/src/components/canvas/canvas-node.tsx index 342cb3468..71166fcb1 100644 --- a/web/src/components/canvas/canvas-node.tsx +++ b/web/src/components/canvas/canvas-node.tsx @@ -459,6 +459,7 @@ export const CanvasNode = React.memo(function CanvasNode({ function NodeContent(props: NodeContentRendererProps) { const hasCustomContent = props.node.type === CanvasNodeType.Config || props.node.type === CanvasNodeType.Script + || Boolean(props.node.metadata?.directorSceneId) || (props.node.metadata?.workflowKind === "character" && Boolean(props.node.metadata.characterAssetId)) || (props.node.metadata?.workflowKind === "story_input" && !props.isEditingContent) || (props.node.metadata?.workflowKind === "styleboard" && !props.node.metadata.content); @@ -716,7 +717,7 @@ function SkillContent({ node, theme }: NodeContentRendererProps) { {tag} - )) : 连接到生成配置节点后生效} + )) : 连接到图片、视频、音频或文本节点后生效} ); diff --git a/web/src/components/canvas/canvas-toolbar.tsx b/web/src/components/canvas/canvas-toolbar.tsx index 3e40ffe2d..1e5186337 100644 --- a/web/src/components/canvas/canvas-toolbar.tsx +++ b/web/src/components/canvas/canvas-toolbar.tsx @@ -1,7 +1,7 @@ import { AnimatePresence, motion } from "motion/react"; import { useEffect, useRef, useState, type MouseEvent as ReactMouseEvent, type ReactNode } from "react"; import { Segmented, Switch } from "antd"; -import { CircleDot, Clapperboard, Eraser, FolderOpen, Grid2x2, Hand, Image as ImageIcon, Info, Layers3, Moon, Music2, Palette, PanelTop, Pencil, Plus, Redo2, Square, Sun, Trash2, Type, Undo2, UploadCloud, UserRound, Video, WandSparkles, X } from "lucide-react"; +import { CircleDot, Clapperboard, Eraser, FolderOpen, Grid2x2, Hand, Image as ImageIcon, Info, Layers3, Moon, Music2, Palette, PanelTop, Pencil, Plus, Redo2, Square, Sun, Trash2, Type, Undo2, UploadCloud, UserRound, Video, X } from "lucide-react"; import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler"; import { FloatingDock, type FloatingDockEntry } from "@/components/ui/aceternity/floating-dock"; @@ -29,7 +29,6 @@ export function CanvasToolbar({ onAddScript, onAddFrame, onAddDrawing, - onAddConfig, onOpenDirector, onUndo, onRedo, @@ -57,7 +56,6 @@ export function CanvasToolbar({ onAddScript: () => void; onAddFrame: () => void; onAddDrawing: () => void; - onAddConfig: () => void; onOpenDirector: () => void; onUndo: () => void; onRedo: () => void; @@ -127,7 +125,6 @@ export function CanvasToolbar({ onAddImage={() => runAddAction(onAddImage)} onAddVideo={() => runAddAction(onAddVideo)} onAddAudio={() => runAddAction(onAddAudio)} - onAddConfig={() => runAddAction(onAddConfig)} onOpenDirector={() => runAddAction(onOpenDirector)} onUpload={() => runAddAction(onUpload)} onOpenAssets={() => runAddAction(onOpenMyAssets)} @@ -171,7 +168,7 @@ export function CanvasToolbar({ ); } -function AddNodeMenu({ x, theme, workspaceMode, isProjectLinked, onAddText, onChooseStyle, onAddScript, onAddFrame, onAddDrawing, onAddImage, onAddVideo, onAddAudio, onAddConfig, onOpenDirector, onUpload, onOpenAssets, onOpenProjectCharacters }: { +function AddNodeMenu({ x, theme, workspaceMode, isProjectLinked, onAddText, onChooseStyle, onAddScript, onAddFrame, onAddDrawing, onAddImage, onAddVideo, onAddAudio, onOpenDirector, onUpload, onOpenAssets, onOpenProjectCharacters }: { x: number; theme: CanvasTheme; workspaceMode: CanvasWorkspaceMode; @@ -184,7 +181,6 @@ function AddNodeMenu({ x, theme, workspaceMode, isProjectLinked, onAddText, onCh onAddImage: () => void; onAddVideo: () => void; onAddAudio: () => void; - onAddConfig: () => void; onOpenDirector: () => void; onUpload: () => void; onOpenAssets: () => void; @@ -204,7 +200,6 @@ function AddNodeMenu({ x, theme, workspaceMode, isProjectLinked, onAddText, onCh ...(!simpleMode ? [ { id: "director", label: "导演台", icon: , badge: "3D", onClick: onOpenDirector }, { id: "audio", label: "音频", icon: , onClick: onAddAudio }, - { id: "config", label: "生成配置", icon: , onClick: onAddConfig }, ] : []), ]; const resourceCommands: CanvasCreateCommand[] = [ diff --git a/web/src/components/canvas/canvas-workspace-overlays.tsx b/web/src/components/canvas/canvas-workspace-overlays.tsx index 2d4d627e5..e9262ae56 100644 --- a/web/src/components/canvas/canvas-workspace-overlays.tsx +++ b/web/src/components/canvas/canvas-workspace-overlays.tsx @@ -1,6 +1,6 @@ import { motion, useReducedMotion } from "motion/react"; import { useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode, type RefObject } from "react"; -import { ChevronRight, Clapperboard, Image as ImageIcon, List, Music2, Pencil, Settings2, Video, WandSparkles, X } from "lucide-react"; +import { ChevronRight, Clapperboard, Image as ImageIcon, List, Music2, Pencil, Video, WandSparkles, X } from "lucide-react"; import { SpotlightSurface } from "@/components/ui/aceternity/spotlight-surface"; import { canvasThemes } from "@/lib/canvas-theme"; @@ -123,7 +123,7 @@ export function CanvasNodePanelOverlay({ node, viewport, containerRef, panelWidt ); } -export function CanvasConnectionCreateMenu({ pending, viewport, viewportSize, containerRef, canCreateDrawing, onCreate, onClose }: { pending: PendingConnectionCreate; viewport: ViewportTransform; viewportSize: { width: number; height: number }; containerRef: RefObject; canCreateDrawing: boolean; onCreate: (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Config | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing) => void; onClose: () => void }) { +export function CanvasConnectionCreateMenu({ pending, viewport, viewportSize, containerRef, canCreateDrawing, onCreate, onClose }: { pending: PendingConnectionCreate; viewport: ViewportTransform; viewportSize: { width: number; height: number }; containerRef: RefObject; canCreateDrawing: boolean; onCreate: (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing) => void; onClose: () => void }) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const reducedMotion = useReducedMotion(); const menuRef = useRef(null); @@ -175,7 +175,6 @@ export function CanvasConnectionCreateMenu({ pending, viewport, viewportSize, co {canCreateDrawing ? } title="绘图" onClick={() => onCreate(CanvasNodeType.Drawing)} /> : null} } title="视频生成" onClick={() => onCreate(CanvasNodeType.Video)} /> } title="音频参考" onClick={() => onCreate(CanvasNodeType.Audio)} /> - } title="配置节点" onClick={() => onCreate(CanvasNodeType.Config)} /> ); diff --git a/web/src/pages/canvas/canvas-text-generation-executor.ts b/web/src/pages/canvas/canvas-text-generation-executor.ts index c21f3f727..29b1195eb 100644 --- a/web/src/pages/canvas/canvas-text-generation-executor.ts +++ b/web/src/pages/canvas/canvas-text-generation-executor.ts @@ -28,20 +28,22 @@ export async function executeTextGeneration({ }: CanvasGenerationExecution) { let streamed = ""; const isConfigNode = sourceNode?.type === CanvasNodeType.Config; - const textCount = isConfigNode ? getGenerationCount(generationConfig.count) : 1; + const isDirectTextTarget = sourceNode?.type === CanvasNodeType.Text && !sourceNode.metadata?.content?.trim() && !editingTextNode; + const textCount = isConfigNode || (isDirectTextTarget && sourceNode?.metadata?.count) ? getGenerationCount(generationConfig.count) : 1; const parentConfig = NODE_DEFAULT_SIZE[isConfigNode ? CanvasNodeType.Config : CanvasNodeType.Text]; const textConfig = NODE_DEFAULT_SIZE[CanvasNodeType.Text]; const parentPosition = sourceNode?.position || { x: 0, y: 0 }; - const childIds = isConfigNode || editingTextNode ? Array.from({ length: textCount }, () => nanoid()) : []; + const generateInPlace = !isConfigNode && !editingTextNode; + const childIds = Array.from({ length: generateInPlace ? Math.max(0, textCount - 1) : textCount }, () => nanoid()); registerPendingNodeIds(childIds); - if (isConfigNode || editingTextNode) { + if (childIds.length) { const childNodes: CanvasNodeData[] = childIds.map((id, index) => ({ id, type: CanvasNodeType.Text, title: effectivePrompt.slice(0, 32) || "Generated Text", position: { x: parentPosition.x + parentConfig.width + 96, - y: parentPosition.y + parentConfig.height / 2 - textConfig.height / 2 + (index - (textCount - 1) / 2) * (textConfig.height + 36), + y: parentPosition.y + parentConfig.height / 2 - textConfig.height / 2 + (index - (childIds.length - 1) / 2) * (textConfig.height + 36), }, width: textConfig.width, height: textConfig.height, @@ -51,7 +53,7 @@ export async function executeTextGeneration({ setConnections((current) => [...current, ...childIds.map((childId) => ({ id: nanoid(), fromNodeId: nodeId, toNodeId: childId }))]); } - const textTargetIds = childIds.length ? childIds : [nodeId]; + const textTargetIds = generateInPlace ? [nodeId, ...childIds] : childIds; textTargetIds.forEach((targetNodeId) => startGenerationRequest(targetNodeId, nodeId, nodeId, controller)); const answers = await Promise.all( textTargetIds.map((targetNodeId) => { @@ -75,7 +77,7 @@ export async function executeTextGeneration({ : node.id === nodeId && isConfigNode ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_SUCCESS, errorDetails: undefined, generationErrorCode: undefined, failedPromptFingerprint: undefined } } : node.id === nodeId && !editingTextNode - ? { ...node, type: CanvasNodeType.Text, title: prompt.slice(0, 32) || "Generated Text", metadata: { ...node.metadata, content: answerByNodeId.get(node.id) || streamed, richText: undefined, status: NODE_STATUS_SUCCESS, errorDetails: undefined, generationErrorCode: undefined, failedPromptFingerprint: undefined } } + ? { ...node, type: CanvasNodeType.Text, title: effectivePrompt.slice(0, 32) || "Generated Text", metadata: { ...node.metadata, content: answerByNodeId.get(node.id) || streamed, richText: undefined, status: NODE_STATUS_SUCCESS, errorDetails: undefined, generationErrorCode: undefined, failedPromptFingerprint: undefined } } : node, ), ); diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx index 01e6058c7..d72990149 100644 --- a/web/src/pages/canvas/project.tsx +++ b/web/src/pages/canvas/project.tsx @@ -1152,15 +1152,16 @@ function InfiniteCanvasPage() { } const sourceNode = nodesRef.current.find((item) => item.id === node.id); if (!sourceNode) return; - const nodeSize = getNodeSpec(CanvasNodeType.Config); - const configNode = createCanvasNode( - CanvasNodeType.Config, + const nodeSize = getNodeSpec(CanvasNodeType.Image); + const imageNode = createCanvasNode( + CanvasNodeType.Image, { x: sourceNode.position.x + sourceNode.width + 96 + nodeSize.width / 2, y: sourceNode.position.y + sourceNode.height / 2, }, { - prompt: "", + prompt: `@[node:${sourceNode.id}]`, + composerContent: `@[node:${sourceNode.id}]`, model: effectiveConfig.imageModel || effectiveConfig.model, size: effectiveConfig.size, quality: effectiveConfig.quality, @@ -1168,16 +1169,17 @@ function InfiniteCanvasPage() { count: getGenerationCount(effectiveConfig.canvasImageCount || effectiveConfig.count), }, ); - const connection = { id: nanoid(), fromNodeId: sourceNode.id, toNodeId: configNode.id }; - const nextNodes = nodesRef.current.map((item) => (item.id === sourceNode.id ? { ...item, metadata: { ...item.metadata, content: prompt, richText: undefined, prompt, status: NODE_STATUS_SUCCESS } } : item)).concat(configNode); + imageNode.title = "图片生成"; + const connection = { id: nanoid(), fromNodeId: sourceNode.id, toNodeId: imageNode.id }; + const nextNodes = nodesRef.current.map((item) => (item.id === sourceNode.id ? { ...item, metadata: { ...item.metadata, content: prompt, richText: undefined, prompt, status: NODE_STATUS_SUCCESS } } : item)).concat(imageNode); const nextConnections = [...connectionsRef.current, connection]; nodesRef.current = nextNodes; connectionsRef.current = nextConnections; setNodes(nextNodes); setConnections(nextConnections); - setSelectedNodeIds(new Set([configNode.id])); + setSelectedNodeIds(new Set([imageNode.id])); setSelectedConnectionId(null); - setDialogNodeId(configNode.id); + setDialogNodeId(imageNode.id); }, [effectiveConfig, message], ); @@ -1554,7 +1556,6 @@ function InfiniteCanvasPage() { onAddScript={() => createNode(CanvasNodeType.Script)} onAddFrame={() => createNode(CanvasNodeType.Frame)} onAddDrawing={() => createNode(CanvasNodeType.Drawing)} - onAddConfig={() => createNode(CanvasNodeType.Config)} onOpenDirector={() => createDirectorShot()} onUndo={undoCanvas} onRedo={redoCanvas} diff --git a/web/src/pages/canvas/use-canvas-connection-controller.ts b/web/src/pages/canvas/use-canvas-connection-controller.ts index ca1e13904..31a3bdb45 100644 --- a/web/src/pages/canvas/use-canvas-connection-controller.ts +++ b/web/src/pages/canvas/use-canvas-connection-controller.ts @@ -6,8 +6,6 @@ import type { PendingConnectionCreate } from "@/components/canvas/canvas-workspa import { attachNodeToStoryboardRow, createCanvasNode, getConnectionTargetAnchor, isHiddenBatchChild, normalizeConnection, storyboardHandleAtY, storyboardRowFromHandle } from "@/lib/canvas/canvas-project-domain"; import { createCanvasDrawingFromImage } from "@/lib/canvas/canvas-drawing-storage"; import { isFrameNode, isNodeHiddenByCollapsedFrame } from "@/lib/canvas/canvas-frame"; -import { getGenerationCount } from "@/lib/canvas/canvas-project-generation"; -import { useEffectiveConfig } from "@/stores/use-config-store"; import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type ConnectionHandle, type ContextMenuState, type Position, type ViewportTransform } from "@/types/canvas"; type UseCanvasConnectionControllerOptions = { @@ -52,7 +50,6 @@ export function useCanvasConnectionController({ setDrawingNodeId, }: UseCanvasConnectionControllerOptions) { const { message } = App.useApp(); - const effectiveConfig = useEffectiveConfig(); const [connectingParams, setConnectingParams] = useState(null); const [connectionTargetNodeId, setConnectionTargetNodeId] = useState(null); const [pendingConnectionCreate, setPendingConnectionCreate] = useState(null); @@ -103,14 +100,12 @@ export function useCanvasConnectionController({ setContextMenu(null); }, [connectionsRef, message, nodesRef, setConnections, setContextMenu, setNodes]); - const createConnectedNode = useCallback(async (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Config | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing, pending: PendingConnectionCreate) => { + const createConnectedNode = useCallback(async (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | 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 + const metadata = 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 } @@ -162,7 +157,7 @@ export function useCanvasConnectionController({ 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]); + }, [closeConnectionCreateMenu, message, nodesRef, projectId, setConnecting, setConnections, setDialogNodeId, setDrawingNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]); const getConnectionDropTarget = useCallback((clientX: number, clientY: number, current: ConnectionHandle): ConnectionDropTarget => { const world = screenToCanvas(clientX, clientY); diff --git a/web/src/pages/canvas/use-canvas-director.ts b/web/src/pages/canvas/use-canvas-director.ts index b50d1d191..587a1a7a6 100644 --- a/web/src/pages/canvas/use-canvas-director.ts +++ b/web/src/pages/canvas/use-canvas-director.ts @@ -49,7 +49,7 @@ export function useCanvasDirector({ let scene = createDirectorScene(`镜头 ${shotIndex}`); const shot = scene.shots[0]; scene = { ...scene, shots: [{ ...shot, name: `镜头 ${shotIndex}` }] }; - const node = createCanvasNode(CanvasNodeType.Config, position || getCanvasCenter(), { + const node = createCanvasNode(CanvasNodeType.Video, position || getCanvasCenter(), { workflowKind: "shot", workflowTitle: `镜头 ${shotIndex}`, shotIndex, @@ -61,6 +61,7 @@ export function useCanvasDirector({ directorShotId: shot.id, }); node.title = `镜头 ${shotIndex}`; + node.height = 300; const nextNodes = [...nodesRef.current, node]; nodesRef.current = nextNodes; setNodes(nextNodes); diff --git a/web/src/pages/canvas/use-canvas-media-tools.ts b/web/src/pages/canvas/use-canvas-media-tools.ts index a0dd82c38..2a18bd152 100644 --- a/web/src/pages/canvas/use-canvas-media-tools.ts +++ b/web/src/pages/canvas/use-canvas-media-tools.ts @@ -105,26 +105,27 @@ export function useCanvasMediaTools({ } const gap = 96; const textSpec = NODE_DEFAULT_SIZE[CanvasNodeType.Text]; - const configSpec = NODE_DEFAULT_SIZE[CanvasNodeType.Config]; + const resultSpec = NODE_DEFAULT_SIZE[CanvasNodeType.Text]; const centerY = node.position.y + node.height / 2; const textNode = { ...createCanvasNode(CanvasNodeType.Text, { x: node.position.x + node.width + gap + textSpec.width / 2, y: centerY }, { content: IMAGE_PROMPT_REVERSE_PRESET, prompt: IMAGE_PROMPT_REVERSE_PRESET, status: NODE_STATUS_SUCCESS, fontSize: 14 }), title: "反推提示词", }; - const configNode = { - ...createCanvasNode(CanvasNodeType.Config, { x: textNode.position.x + textNode.width + gap + configSpec.width / 2, y: centerY }, { + const resultNode = { + ...createCanvasNode(CanvasNodeType.Text, { x: textNode.position.x + textNode.width + gap + resultSpec.width / 2, y: centerY }, { + content: "", generationMode: "text", model: effectiveConfig.textModel || effectiveConfig.model || defaultConfig.textModel, count: 1, composerContent: `参考图片:@[node:${node.id}]\n任务说明:@[node:${textNode.id}]`, }), - title: "反推提示词配置", + title: "反推提示词结果", }; - setNodes((current) => [...current, textNode, configNode]); - setConnections((current) => [...current, { id: nanoid(), fromNodeId: node.id, toNodeId: configNode.id }, { id: nanoid(), fromNodeId: textNode.id, toNodeId: configNode.id }]); - setSelectedNodeIds(new Set([configNode.id])); + setNodes((current) => [...current, textNode, resultNode]); + setConnections((current) => [...current, { id: nanoid(), fromNodeId: node.id, toNodeId: resultNode.id }, { id: nanoid(), fromNodeId: textNode.id, toNodeId: resultNode.id }]); + setSelectedNodeIds(new Set([resultNode.id])); setSelectedConnectionId(null); - setDialogNodeId(configNode.id); + setDialogNodeId(resultNode.id); setContextMenu(null); }, [effectiveConfig.model, effectiveConfig.textModel, message, setConnections, setContextMenu, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]); diff --git a/web/src/pages/canvas/use-canvas-node-operations.ts b/web/src/pages/canvas/use-canvas-node-operations.ts index 8b4535740..e90f24f15 100644 --- a/web/src/pages/canvas/use-canvas-node-operations.ts +++ b/web/src/pages/canvas/use-canvas-node-operations.ts @@ -6,9 +6,7 @@ import { nanoid } from "nanoid"; import { FRAME_HEADER_HEIGHT, getFrameChildIds, getFrameChildren, isFrameNode } from "@/lib/canvas/canvas-frame"; import { alignCanvasNodes, layoutCanvasFlow, layoutCanvasNodes, nextCanvasVersionLabel, type CanvasAlignmentMode } from "@/lib/canvas/canvas-layout"; import { createCanvasNode, removeCanvasNodes } from "@/lib/canvas/canvas-project-domain"; -import { getGenerationCount } from "@/lib/canvas/canvas-project-generation"; import { isolateCopiedNodeMetadata } from "@/lib/canvas/canvas-node-copy"; -import { useEffectiveConfig } from "@/stores/use-config-store"; import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type ContextMenuState, type Position } from "@/types/canvas"; import { cloneCanvasDrawing } from "@/lib/canvas/canvas-drawing-storage"; @@ -47,7 +45,6 @@ export function useCanvasNodeOperations({ onNodesDeleted, }: UseCanvasNodeOperationsOptions) { const { message } = App.useApp(); - const effectiveConfig = useEffectiveConfig(); const clipboardRef = useRef(null); const preferCopiedNodesRef = useRef(false); const markerWritePendingRef = useRef(false); @@ -113,18 +110,11 @@ export function useCanvasNodeOperations({ }, [selectedNodeIdsRef, setSelectedConnectionId, setSelectedNodeIds]); const createNode = useCallback((type: CanvasNodeType, position?: Position) => { - const configMetadata = type === CanvasNodeType.Config - ? { - model: effectiveConfig.imageModel || effectiveConfig.model, - size: effectiveConfig.size, - count: getGenerationCount(effectiveConfig.canvasImageCount || effectiveConfig.count), - } - : undefined; - const node = createCanvasNode(type, position || getCanvasCenter(), configMetadata); + const node = createCanvasNode(type, position || getCanvasCenter()); commitNodes([...nodesRef.current, node]); selectNodes(new Set([node.id])); if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Script && type !== CanvasNodeType.Audio && type !== CanvasNodeType.Frame && type !== CanvasNodeType.Drawing) setDialogNodeId(node.id); - }, [commitNodes, effectiveConfig.canvasImageCount, effectiveConfig.count, effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, getCanvasCenter, nodesRef, selectNodes, setDialogNodeId]); + }, [commitNodes, getCanvasCenter, nodesRef, selectNodes, setDialogNodeId]); const arrangeSelectedNodes = useCallback((mode: "row" | "column" | "grid" | "flow") => { const selected = nodesRef.current.filter((node) => selectedNodeIdsRef.current.has(node.id) && !node.metadata?.locked && !isFrameNode(node));