# Popiart-node 节点系统交互增强实现方案 ## 1. 背景与目标 本实现方案对应 `Popiart-node 节点系统交互增强 PRD`,目标是在不重构现有节点系统、不新增依赖、不改变工作流 JSON 主结构的前提下,增强节点画布的低门槛操作体验。 本次增强聚焦两类能力: 1. 节点左右两侧增加轻量 `+` 操作入口。 - 右侧 `+` 用于添加下游节点,并自动连接当前节点输出。 - 左侧 `+` 用于添加输入节点,并自动连接到当前节点输入。 - 候选菜单复用现有 `ConnectionDropMenu` 的菜单数据与自动连线逻辑。 2. 图片和视频生成节点底部前置常用生成参数。 - 图片生成节点前置 `Model`、`Aspect Ratio`、`Resolution`、`Google Search`、`Image Search`、`Cost`。 - 视频生成节点前置 `Model`、可识别的核心 schema 参数、`Cost`、`Advanced`。 - 高级参数继续复用 `InlineParameterPanel` / `ModelParameters`。 ## 2. 非目标 - 不重写 React Flow、Zustand store 或节点执行系统。 - 不新增第三方依赖。 - 不改变现有工作流 JSON 的核心结构。 - 不实现后端能力不存在的 LibTV 功能,例如真实 720 全景、多角度、打光模型能力。 - 不把所有高级参数塞进节点底部,只前置高频参数。 ## 3. 当前代码基础 ### 3.1 画布与连线 相关文件: - `src/components/WorkflowCanvas.tsx` - `src/components/ConnectionDropMenu.tsx` - `src/store/workflowStore.ts` - `src/store/utils/connectedInputs.ts` - `src/store/utils/executionUtils.ts` 当前已有能力: - `WorkflowCanvas.tsx` 负责 React Flow 主画布、节点注册、边注册、连线校验、拖拽创建节点、菜单弹出和自动连线。 - `ConnectionDropMenu.tsx` 已经按 handle 类型区分候选菜单: - `image` - `text` - `video` - `audio` - `handleMenuSelect` 已经实现: - 创建目标节点。 - 按 handle 类型推断新节点输入/输出 handle。 - 创建合法 edge。 - 支持多选节点批量连接。 - 支持 `splitGridImmediate` 特殊 action。 ### 3.2 节点 UI 相关文件: - `src/components/nodes/BaseNode.tsx` - `src/components/nodes/FloatingNodeHeader.tsx` - `src/components/nodes/GenerateImageNode.tsx` - `src/components/nodes/GenerateVideoNode.tsx` - `src/components/nodes/InlineParameterPanel.tsx` - `src/components/nodes/ModelParameters.tsx` - `src/components/nodes/SettingsTabBar.tsx` 当前已有能力: - `BaseNode` 支持 `settingsPanel` 和 `settingsExpanded`,用于把参数面板挂在节点底部。 - `GenerateImageNode` 已经有 Gemini 图片参数、外部 provider 参数和 fallback 参数。 - `GenerateVideoNode` 已经有模型 schema 参数加载和动态输入 handle。 - `InlineParameterPanel` 已经提供折叠参数容器。 - `ModelParameters` 已经能从 `/api/models/[modelId]` 拉取 schema 并渲染控件。 ### 3.3 成本计算 相关文件: - `src/components/CostIndicator.tsx` - `src/components/CostDialog.tsx` - `src/utils/costCalculator.ts` 当前已有能力: - `calculatePredictedCost(nodes)` 能基于全工作流节点计算预估成本。 - Gemini 图片模型成本有硬编码价格表。 - 外部 provider 没有 pricing 时会以 unknown pricing 处理。 当前缺口: - `CostIndicator` 是全局成本入口,不适合作为单节点底部小组件直接复用。 - 需要抽出或新增单节点成本展示能力。 ## 4. 总体实现策略 采用增量式实现,先打通节点 `+` 自动连接,再做参数底栏。 推荐里程碑: 1. 抽出 ConnectionDropMenu 菜单 resolver。 2. 实现节点左右 `+` overlay 和自动连接。 3. 完善左侧输入推荐逻辑。 4. 实现图片生成节点参数底栏。 5. 实现视频生成节点参数底栏。 6. 补测试和手动 QA。 ## 5. 里程碑一:抽出 ConnectionDropMenu 菜单 resolver ### 5.1 目标 将 `ConnectionDropMenu.tsx` 内部的菜单数据和选项解析逻辑抽出为共享模块,使以下两处共用同一份菜单逻辑: - 从 handle 拖出后的 `ConnectionDropMenu`。 - 节点左右 `+` 点击后的轻量菜单。 ### 5.2 新增文件 建议新增: ```text src/components/connectionMenuOptions.tsx ``` 或如果希望更偏业务逻辑: ```text src/utils/connectionMenuOptions.tsx ``` 考虑菜单项包含 React icon,建议放在 `src/components/connectionMenuOptions.tsx`。 ### 5.3 导出类型 ```ts import { NodeType } from "@/types"; export type MenuAction = "splitGridImmediate"; export type ConnectionHandleType = | "image" | "text" | "video" | "audio" | "3d" | "easeCurve" | null; export type ConnectionMenuMode = "source" | "target"; export interface ConnectionMenuOption { type: NodeType | MenuAction; label: string; icon: React.ReactNode; isAction?: boolean; } export function getConnectionMenuOptions( handleType: ConnectionHandleType, connectionType: ConnectionMenuMode ): ConnectionMenuOption[] { // return options } ``` ### 5.4 菜单优先级 #### 图片输出右侧推荐 当 `connectionType === "source"` 且 `handleType === "image"`: 1. `nanoBanana`,Generate Image 2. `generateVideo`,Generate Video 3. `annotation`,Annotate 4. `splitGrid`,Split Grid 5. `output`,Output 6. `outputGallery`,Output Gallery 7. `imageCompare`,Image Compare 8. `router`,Router 9. `switch`,Switch 保留 `splitGridImmediate` 时建议放在 `splitGrid` 后面,或者只在从 handle 拖出菜单中展示,节点右侧 `+` 首版可不展示 action,避免“创建节点”和“立即操作”混在一起。 #### 文本输出右侧推荐 当 `connectionType === "source"` 且 `handleType === "text"`: 1. `nanoBanana`,Generate Image 2. `generateVideo`,Generate Video 3. `generateAudio`,Generate Audio 4. `llmGenerate`,LLM Generate 5. `promptConstructor`,Prompt Constructor 6. `array`,Array 7. `output`,如果当前系统支持文本输出展示 8. `router` 9. `switch` 10. `conditionalSwitch` 如果 `output` 当前没有 text handle,不要强行展示,避免自动连接失败。 #### 视频输出右侧推荐 当 `connectionType === "source"` 且 `handleType === "video"`: 1. `videoStitch` 2. `videoTrim` 3. `videoFrameGrab` 4. `output` 5. `outputGallery` 6. `router` 7. `switch` #### 音频输出右侧推荐 当 `connectionType === "source"` 且 `handleType === "audio"`: 1. `generateVideo` 2. `videoStitch` 3. `output` 4. `router` 5. `switch` #### 左侧输入推荐 当 `connectionType === "target"`: 图片输入: 1. `imageInput` 2. `videoFrameGrab` 3. `annotation` 4. `glbViewer` 5. `nanoBanana` 6. `router` 文本输入: 1. `prompt` 2. `llmGenerate` 3. `promptConstructor` 4. `array` 5. `router` 音频输入: 1. `audioInput` 2. `generateAudio` 3. `router` 视频输入: 1. `videoInput` 2. `generateVideo` 3. `videoStitch` 4. `videoTrim` 5. `router` ### 5.5 修改 ConnectionDropMenu 修改 `src/components/ConnectionDropMenu.tsx`: - 删除组件内部的 `IMAGE_TARGET_OPTIONS` 等私有常量。 - 从新文件导入: ```ts import { getConnectionMenuOptions, type ConnectionMenuOption, type MenuAction, } from "./connectionMenuOptions"; ``` - 保持原 props 不变。 - 内部 `getOptions` 改为调用共享 resolver。 ### 5.6 验收 - 原本从 handle 拖出并松开后弹出菜单的能力不回归。 - 菜单顺序符合 PRD。 - `splitGridImmediate` 如保留,原功能不回归。 - 相关测试: - `ConnectionDropMenu.test.tsx` - 新增 `connectionMenuOptions.test.tsx` ## 6. 里程碑二:实现节点左右 `+` ### 6.1 目标 当单个节点被选中或 hover 时,在节点左右两侧显示小型 `+`: - 左侧 `+`:打开输入候选菜单。 - 右侧 `+`:打开下游候选菜单。 点击菜单项后: - 创建目标节点。 - 放在当前节点左侧或右侧固定偏移位置。 - 自动建立合法 edge。 - 新节点进入选中状态。 ### 6.2 推荐实现位置 建议在 `WorkflowCanvas.tsx` 中实现 overlay,而不是在每个 node component 内实现。 原因: - `WorkflowCanvas` 已经拥有 `nodes`、`edges`、`addNode`、`onConnect`、`screenToFlowPosition`、`connectionDrop` 等上下文。 - 可以复用现有 `handleMenuSelect` 的自动连接逻辑。 - 不需要改每个节点组件。 ### 6.3 新增组件 建议新增: ```text src/components/NodeQuickAddControls.tsx ``` 职责: - 接收当前节点位置、尺寸、类型、可用 handle 类型。 - 渲染左侧和右侧 `+`。 - 点击时回调给 `WorkflowCanvas`。 接口建议: ```ts import { NodeType } from "@/types"; type QuickAddSide = "input" | "output"; interface NodeQuickAddControlsProps { nodeId: string; nodeType: NodeType; position: { x: number; y: number }; width: number; height: number; showInput: boolean; showOutput: boolean; onOpen: (side: QuickAddSide) => void; } ``` ### 6.4 Overlay 渲染方式 优先使用 React Flow 的 `ViewportPortal`,原因是 overlay 能跟随画布缩放和平移。 在 `WorkflowCanvas.tsx` 中已有: ```ts import { ViewportPortal } from "@xyflow/react"; ``` 可在 `` 内部或附近渲染: ```tsx {quickAddNode && ( openQuickAddMenu(quickAddNode, side)} /> )} ``` ### 6.5 显示规则 显示条件: - 单节点选中。 - 或 hover 某个节点。 - 拖拽节点时隐藏。 - 正在打开菜单时保持显示。 - 多选时首版建议隐藏,避免批量语义复杂。 获取 hover: - `BaseNode` 已经调用 `setHoveredNodeId`。 - `workflowStore` 中已有 `hoveredNodeId`。 - `WorkflowCanvas` 可读取: ```ts const hoveredNodeId = useWorkflowStore((state) => state.hoveredNodeId); ``` 选择逻辑: ```ts const selectedNodes = nodes.filter((n) => n.selected); const quickAddNode = selectedNodes.length === 1 ? selectedNodes[0] : hoveredNodeId ? nodes.find((n) => n.id === hoveredNodeId) : null; ``` 如果 hover 节点和 selected 节点不同,建议选中优先。 ### 6.6 按钮视觉 PRD 要求: - 24x24px 圆形按钮。 - 节点边缘外侧 20-28px。 - 垂直居中。 - 深色半透明背景。 - hover 时边框或背景增强。 - tooltip: - 左侧:`添加输入` - 右侧:`添加下一步` 样式建议: ```tsx className=" nodrag nopan absolute z-[80] w-6 h-6 rounded-full flex items-center justify-center bg-neutral-900/75 border border-neutral-600 text-neutral-300 shadow-lg hover:bg-neutral-800 hover:border-neutral-400 hover:text-white transition-colors " ``` 位置建议: ```ts const left = position.x - 28; const right = position.x + width + 28; const top = position.y + height / 2; ``` 按钮自身用 `transform: translate(-50%, -50%)` 居中。 ### 6.7 判断左右按钮是否展示 需要知道当前节点是否有输入或输出。 当前 `WorkflowCanvas.tsx` 内已有 `getNodeHandles(nodeType)`,可复用或抽出到共享文件。 建议新增: ```text src/utils/nodeHandleRegistry.ts ``` 导出: ```ts export function getNodeHandles(nodeType: string): { inputs: string[]; outputs: string[]; } export function getHandleType(handleId: string | null | undefined): HandleType | null ``` 首版也可以先不抽,把 `NodeQuickAddControls` 逻辑放在 `WorkflowCanvas`,减少迁移。 显示规则: - `showInput = getNodeHandles(node.type).inputs.length > 0` - `showOutput = getNodeHandles(node.type).outputs.length > 0` - 对 `switch`、`conditionalSwitch` 等动态输出节点,首版可按当前类型特殊处理: - `switch`:有输入时显示左侧,右侧如果没有 switch outputs 则隐藏。 - `conditionalSwitch`:左侧 text,右侧可暂不支持或取默认 rule output。 ### 6.8 打开 Quick Add 菜单 复用现有 `connectionDrop` 状态最省事。 当前 `connectionDrop` 结构: ```ts interface ConnectionDropState { position: { x: number; y: number }; flowPosition: { x: number; y: number }; handleType: "image" | "text" | "video" | "audio" | "3d" | "easeCurve" | null; connectionType: "source" | "target"; sourceNodeId: string | null; sourceHandleId: string | null; } ``` 右侧 `+` 打开时: - `connectionType = "source"` - `sourceNodeId = currentNode.id` - `sourceHandleId = 当前节点可用输出 handle` - `handleType = 输出 handle type` - `flowPosition = 当前节点右侧固定偏移坐标` - `position = 按钮屏幕坐标或菜单屏幕坐标` 左侧 `+` 打开时: - `connectionType = "target"` - `sourceNodeId = currentNode.id` - `sourceHandleId = 当前节点输入 handle` - `handleType = 输入 handle type` - `flowPosition = 当前节点左侧固定偏移坐标` ### 6.9 handle 选择策略 因为有些节点有多个输入或输出,需要一个默认策略。 #### 输出 handle 默认策略 按节点类型判断: - `imageInput`、`annotation`、`nanoBanana`、`videoFrameGrab`、`glbViewer` 输出 `image`。 - `prompt`、`array`、`promptConstructor`、`llmGenerate` 输出 `text`。 - `generateVideo`、`videoInput`、`videoStitch`、`videoTrim`、`easeCurve` 输出 `video`。 - `generateAudio`、`audioInput` 输出 `audio`。 - `generate3d` 输出 `3d`。 #### 输入 handle 默认策略 对左侧 `+`: - `nanoBanana` 优先展示一个小二级选择,或默认 `image` + 菜单中包含 `Image Input`。 - `generateVideo` 如果已有 schema: - 优先 required input。 - 否则优先 `image`,其次 `text`,再次 `audio`。 - `generateAudio` 默认 `text`。 - `llmGenerate` 默认 `text`。 - `output` 默认根据可接收的第一个 handle,建议 `image`。 为了首版简单,建议点击左侧 `+` 时,如果当前节点有多个输入类型,先打开一个小菜单选择输入类型: - Image - Text - Audio - Video 选择后再打开 ConnectionDropMenu。 如果要更快落地,首版可以默认优先级: 1. `image` 2. `text` 3. `video` 4. `audio` 5. `3d` 6. `easeCurve` 后续再优化为输入类型选择。 ### 6.10 新节点位置 推荐固定偏移: ```ts const QUICK_ADD_X_OFFSET = 360; const QUICK_ADD_Y_OFFSET = 0; ``` 右侧: ```ts flowPosition = { x: node.position.x + nodeWidth + QUICK_ADD_X_OFFSET, y: node.position.y + QUICK_ADD_Y_OFFSET, } ``` 左侧: ```ts flowPosition = { x: node.position.x - QUICK_ADD_X_OFFSET, y: node.position.y + QUICK_ADD_Y_OFFSET, } ``` 更优策略: - 用 `defaultNodeDimensions[nodeType]` 估算新节点宽高。 - 创建在当前节点左/右侧,垂直中心对齐。 - 调用现有 `findNearestFreePosition` 避免和其他节点重叠。 首版建议: - 先固定偏移。 - 如果已有 `findNearestFreePosition` 方便复用,再加避让。 ### 6.11 新节点选中 当前 `addNode` 返回 node id。 创建后应选中新节点并取消其他节点选中。 如果 `workflowStore.addNode` 已经默认选中新节点,则无需额外处理。 如果没有,使用 React Flow `setNodes`: ```ts setNodes((nodes) => nodes.map((node) => ({ ...node, selected: node.id === newNodeId, })) ); ``` 注意不要绕过 store 状态太多,最好使用已有 `onNodesChange` 或 store action。 ### 6.12 验收 - hover 图片节点时右侧显示 `+`。 - 选中图片节点时右侧显示 `+`。 - 点击右侧 `+` 后能添加并连接: - Generate Image - Generate Video - Annotate - Split Grid - Output - 点击 Prompt 节点右侧 `+` 后能添加并连接: - Generate Image - Generate Video - Generate Audio - LLM Generate - 点击生成节点左侧 `+` 后能添加合适输入节点并连接。 - 原 handle 拖拽弹菜单不回归。 ## 7. 里程碑三:图片生成节点参数底栏 ### 7.1 目标 图片生成节点底部始终展示常用参数,而不是只有启用 inline 参数时才显示。 需要展示: - Model - Aspect Ratio - Resolution - Google Search - Image Search - Cost - Advanced 入口 ### 7.2 当前状态 `GenerateImageNode.tsx` 已有: - Gemini model selector。 - Aspect ratio selector。 - Resolution selector。 - Google Search checkbox。 - Image Search checkbox。 - 外部 provider `ModelParameters`。 - Fallback tab。 - `InlineParameterPanel` 折叠容器。 但当前显示受 `useInlineParameters()` 控制: ```ts const { inlineParametersEnabled } = useInlineParameters(); ``` PRD 需要常用参数固定展示,因此建议: - 常用参数不再受 `inlineParametersEnabled` 控制。 - Advanced 面板仍可复用 `InlineParameterPanel`。 ### 7.3 新增组件 建议新增: ```text src/components/nodes/GenerationQuickSettingsBar.tsx ``` 职责: - 渲染一行或两行紧凑参数控件。 - 支持 `select`、`checkbox`、成本 badge、Advanced 按钮。 - 用于图片和视频节点共用。 也可以先在 `GenerateImageNode.tsx` 内实现,再抽组件。考虑后续视频也要用,建议抽组件。 接口建议: ```ts interface QuickSettingItem { key: string; label: string; control: React.ReactNode; hidden?: boolean; } interface GenerationQuickSettingsBarProps { items: QuickSettingItem[]; cost?: React.ReactNode; advanced?: React.ReactNode; muted?: boolean; } ``` 样式要求: - 固定在节点底部,占用布局高度,不遮挡图片。 - 未选中也可读,视觉可更弱。 - 文本和控件不能溢出。 - 控件高度紧凑。 示例 class: ```tsx
...
``` ### 7.4 Advanced 面板行为 建议将 `InlineParameterPanel` 改为只承载高级参数。 图片节点: - 常用参数常驻。 - Advanced 默认折叠。 - 点击 Advanced 展开: - 外部 provider 显示完整 `ModelParameters`。 - fallback 参数仍在 tabs 中。 需要注意: - Gemini 图片模型当前所有常用参数都已前置,Advanced 可只显示 fallback 或不显示。 - 外部 provider 的完整 schema 仍需要 Advanced。 ### 7.5 成本组件 建议新增: ```text src/components/nodes/NodeCostBadge.tsx ``` 职责: - 输入单个节点数据。 - 展示该节点预估成本。 - 不弹全局成本详情,或点击时可打开 `CostDialog`。 接口建议: ```ts import { WorkflowNode } from "@/types"; interface NodeCostBadgeProps { node: WorkflowNode; } ``` 实现方式: - 首版可复用 `calculatePredictedCost([node])`。 - 如果返回 `unknownPricingCount > 0`,显示 `--`。 - 如果没有模型,显示 `Select model`。 - 如果有成本,显示 `formatCost(totalCost)`。 注意: - `calculatePredictedCost` 对 `generateVideo` 外部 provider 多数未知,显示 `--` 是可接受的。 - 成本不可计算不能阻塞生成。 ### 7.6 图片节点参数展示规则 Gemini provider: - `Model` 永远显示。 - `Aspect Ratio` 永远显示。 - `Resolution`: - `nano-banana` 不支持时隐藏或固定显示 `1K`。 - `nano-banana-pro` 和 `nano-banana-2` 显示。 - `Google Search`: - 仅 `nano-banana-pro` 和 `nano-banana-2` 显示。 - `Image Search`: - 仅 `nano-banana-2` 显示。 - `Cost`: - 可计算时显示价格。 - 不可计算时显示 `--`。 外部 provider: - `Model` 显示。 - 常用 schema 参数首版可不前置,或后续基于 schema 提取。 - `Cost` unknown 时显示 `--`。 - `Advanced` 展示完整 schema。 ### 7.7 BaseNode 配合 当前 `BaseNode` 有: ```ts settingsPanel?: ReactNode; settingsExpanded?: boolean; ``` 建议新增一个更明确的 prop: ```ts footerPanel?: ReactNode; ``` 渲染结构: ```tsx
children
{footerPanel} {settingsPanel}
``` 如果不想改 `BaseNode` API,可以继续使用 `settingsPanel`,但会把“常驻底栏”和“高级折叠面板”概念混在一起。长期建议新增 `footerPanel`。 首版推荐: - 修改 `BaseNode` 增加 `footerPanel`。 - `footerPanel` 始终参与节点布局。 - `settingsPanel` 继续只处理 Advanced 折叠内容。 ### 7.8 验收 - 图片生成节点未选中时,底部仍可读常用参数。 - 修改 Model、Aspect Ratio、Resolution、搜索开关能立即更新节点数据。 - 参数底栏不遮挡输出图片。 - Advanced 能展开现有完整参数能力。 - 成本复用现有计算逻辑,无新增依赖。 ## 8. 里程碑四:视频生成节点参数底栏 ### 8.1 目标 视频生成节点底部固定展示: - Model - 常见视频参数 - Cost - Advanced 常见参数来自 provider schema,字段名可能不统一。 ### 8.2 常见字段识别 首版识别以下字段: - `aspect_ratio` - `aspectRatio` - `ratio` - `resolution` - `size` - `duration` - `num_frames` - `fps` 建议新增工具: ```text src/utils/modelParameterClassification.ts ``` 接口: ```ts import { ModelParameter } from "@/lib/providers/types"; export const COMMON_VIDEO_PARAMETER_NAMES = new Set([ "aspect_ratio", "aspectRatio", "ratio", "resolution", "size", "duration", "num_frames", "fps", ]); export function splitVideoParameters(schema: ModelParameter[]): { quick: ModelParameter[]; advanced: ModelParameter[]; } { // quick by name, advanced remaining } ``` ### 8.3 当前难点 `ModelParameters.tsx` 当前自己 fetch schema、自己 render 所有参数。视频底栏要先拿到 schema 并分流 quick/advanced,因此需要调整数据流。 推荐方案: 1. 把 schema 获取逻辑从 `ModelParameters` 抽成 hook: ```text src/hooks/useModelSchema.ts ``` 2. `ModelParameters` 继续使用该 hook。 3. `GenerateVideoNode` 也使用该 hook,拿到 schema 后: - quick 参数渲染到底栏。 - advanced 参数传给 `ModelParameters` 或新增 `ParameterControls`。 如果想减少改动: - 先给 `ModelParameters` 增加可选 prop: ```ts includeNames?: string[]; excludeNames?: string[]; variant?: "full" | "compact"; ``` 然后: - 底栏渲染一个 compact `ModelParameters includeNames={commonNames}`。 - Advanced 渲染一个 full `ModelParameters excludeNames={commonNames}`。 此方案改动更小,但 `ModelParameters` 会 fetch 两次。由于有 `deduplicatedFetch` 和 localStorage cache,首版可以接受。 ### 8.4 推荐实现方案 首版采用“小改动方案”: 修改 `ModelParameters.tsx`: ```ts interface ModelParametersProps { modelId: string; provider: ProviderType; parameters: Record; onParametersChange: (parameters: Record) => void; onExpandChange?: (expanded: boolean, parameterCount: number) => void; onInputsLoaded?: (inputs: ModelInputDef[]) => void; includeNames?: string[]; excludeNames?: string[]; compact?: boolean; } ``` schema 过滤: ```ts const filteredSchema = useMemo(() => { let result = schema; if (includeNames?.length) { const include = new Set(includeNames); result = result.filter((param) => include.has(param.name)); } if (excludeNames?.length) { const exclude = new Set(excludeNames); result = result.filter((param) => !exclude.has(param.name)); } return result; }, [schema, includeNames, excludeNames]); ``` 后续 `sortedSchema` 基于 `filteredSchema`。 compact 样式: - label 字号更小。 - 控件高度更低。 - 最大宽度不超过底栏容器。 ### 8.5 视频节点底栏展示规则 如果已选择模型并 schema 能识别常用参数: - 展示 Model。 - 展示 quick schema 参数。 - 展示 Cost。 - 展示 Advanced。 如果识别不到: - 只展示 Model。 - 展示 Cost。 - 展示 Advanced。 如果未选择模型: - Model 显示 `Select model...`。 - Cost 显示 `Select model` 或 `--`。 - Advanced 禁用或隐藏。 ### 8.6 Advanced 行为 Advanced 默认折叠。 展开后: - 显示完整高级参数。 - 不重复显示已经前置的 quick 参数。 - fallback 参数仍保留 tabs。 ### 8.7 动态输入 handle 注意事项 `GenerateVideoNode` 依赖 `ModelParameters` 的 `onInputsLoaded` 来设置 `inputSchema`。 如果底栏和 Advanced 都可能渲染 `ModelParameters`,避免 `onInputsLoaded` 重复造成不必要更新: - 只让一个 `ModelParameters` 负责 `onInputsLoaded`。 - 或在 `handleInputsLoaded` 中做浅比较,输入未变化时不更新。 建议: - 底栏 quick `ModelParameters` 传 `onInputsLoaded`。 - Advanced 的 `ModelParameters` 不传 `onInputsLoaded`。 ### 8.8 验收 - 视频生成节点未选中时,底部能看到模型和常见参数。 - schema 包含 `duration`、`fps`、`aspect_ratio` 等字段时能前置展示。 - schema 不包含常见字段时,显示 Model + Cost + Advanced。 - Advanced 展开后完整参数能力不丢失。 - 动态输入 handle 仍正常生成。 ## 9. 里程碑五:测试计划 ### 9.1 单元测试 新增: ```text src/components/__tests__/connectionMenuOptions.test.tsx src/components/__tests__/NodeQuickAddControls.test.tsx src/utils/__tests__/modelParameterClassification.test.ts src/components/__tests__/NodeCostBadge.test.tsx ``` 重点覆盖: - `getConnectionMenuOptions("image", "source")` 顺序符合 PRD。 - `getConnectionMenuOptions("text", "source")` 包含 `generateAudio`、`llmGenerate`。 - `getConnectionMenuOptions("video", "source")` 包含 `videoStitch`、`videoTrim`、`videoFrameGrab`。 - `getConnectionMenuOptions("audio", "source")` 包含 `generateVideo`、`videoStitch`、`output`。 - `splitVideoParameters` 能识别 common video params。 - `NodeCostBadge` 对 known cost、unknown cost、no model 的展示正确。 ### 9.2 组件测试 扩展: ```text src/components/__tests__/WorkflowCanvas.test.tsx src/components/__tests__/GenerateImageNode.test.tsx src/components/__tests__/GenerateVideoNode.test.tsx src/components/__tests__/ConnectionDropMenu.test.tsx ``` 覆盖: - 选中单个节点时显示左右 `+`。 - 多选时不显示 quick add。 - 无输出节点不显示右侧 `+`。 - 无输入节点不显示左侧 `+`。 - 点击图片节点右侧 `+` 并选择 Generate Video 后创建节点和 edge。 - 图片生成节点底部展示 Model、Aspect Ratio、Resolution。 - 视频生成节点底部展示 schema quick 参数。 ### 9.3 手动 QA 手动验证路径: 1. 空画布添加 Image Input。 2. 选中 Image Input。 3. 点击右侧 `+`。 4. 选择 Generate Video。 5. 验证新节点出现在右侧并自动连线。 6. 选中 Prompt。 7. 点击右侧 `+`。 8. 选择 Generate Audio。 9. 验证 text edge 正确。 10. 选中 Generate Image。 11. 点击左侧 `+`。 12. 添加 Image Input 或 Prompt 并自动连接。 13. 验证 Generate Image 底栏可改模型、比例、分辨率、搜索开关。 14. 验证 Generate Video 底栏可改模型和常见 schema 参数。 15. 验证原 handle 拖拽弹菜单仍可用。 ### 9.4 命令 ```powershell npm.cmd run test:run ``` 如果只跑相关测试: ```powershell npm.cmd run test:run -- ConnectionDropMenu npm.cmd run test:run -- GenerateImageNode npm.cmd run test:run -- GenerateVideoNode npm.cmd run test:run -- WorkflowCanvas ``` ## 10. 建议代码改动清单 ### 10.1 新增文件 ```text src/components/connectionMenuOptions.tsx src/components/NodeQuickAddControls.tsx src/components/nodes/GenerationQuickSettingsBar.tsx src/components/nodes/NodeCostBadge.tsx src/utils/modelParameterClassification.ts ``` 可选新增: ```text src/hooks/useModelSchema.ts src/utils/nodeHandleRegistry.ts ``` ### 10.2 修改文件 ```text src/components/ConnectionDropMenu.tsx src/components/WorkflowCanvas.tsx src/components/nodes/BaseNode.tsx src/components/nodes/GenerateImageNode.tsx src/components/nodes/GenerateVideoNode.tsx src/components/nodes/ModelParameters.tsx src/utils/costCalculator.ts ``` ### 10.3 测试文件 ```text src/components/__tests__/connectionMenuOptions.test.tsx src/components/__tests__/NodeQuickAddControls.test.tsx src/components/__tests__/GenerateImageNode.test.tsx src/components/__tests__/GenerateVideoNode.test.tsx src/components/__tests__/WorkflowCanvas.test.tsx src/utils/__tests__/modelParameterClassification.test.ts ``` ## 11. 关键实现伪代码 ### 11.1 openQuickAddMenu ```ts function openQuickAddMenu(node: WorkflowNode, side: "input" | "output") { const nodeWidth = getNodeWidth(node); const nodeHeight = getNodeHeight(node); const handle = side === "output" ? getPrimaryOutputHandle(node) : getPrimaryInputHandle(node); if (!handle) return; const flowPosition = side === "output" ? { x: node.position.x + nodeWidth + 360, y: node.position.y, } : { x: node.position.x - 360, y: node.position.y, }; const menuPosition = flowToScreenPosition({ x: side === "output" ? node.position.x + nodeWidth + 28 : node.position.x - 28, y: node.position.y + nodeHeight / 2, }); setConnectionDrop({ position: menuPosition, flowPosition, handleType: handle.handleType, connectionType: side === "output" ? "source" : "target", sourceNodeId: node.id, sourceHandleId: handle.handleId, }); } ``` ### 11.2 primary output handle ```ts function getPrimaryOutputHandle(node: WorkflowNode): { handleId: string; handleType: HandleType; } | null { switch (node.type) { case "imageInput": case "annotation": case "nanoBanana": case "videoFrameGrab": case "glbViewer": return { handleId: "image", handleType: "image" }; case "prompt": case "array": case "promptConstructor": case "llmGenerate": return { handleId: "text", handleType: "text" }; case "videoInput": case "generateVideo": case "videoStitch": case "videoTrim": case "easeCurve": return { handleId: "video", handleType: "video" }; case "audioInput": case "generateAudio": return { handleId: "audio", handleType: "audio" }; case "generate3d": return { handleId: "3d", handleType: "3d" }; default: return null; } } ``` ### 11.3 video quick parameter split ```ts const COMMON_VIDEO_PARAMETER_NAMES = new Set([ "aspect_ratio", "aspectRatio", "ratio", "resolution", "size", "duration", "num_frames", "fps", ]); function splitVideoParameters(schema: ModelParameter[]) { const quick: ModelParameter[] = []; const advanced: ModelParameter[] = []; for (const param of schema) { if (COMMON_VIDEO_PARAMETER_NAMES.has(param.name)) { quick.push(param); } else { advanced.push(param); } } return { quick, advanced }; } ``` ## 12. 风险与处理 ### 12.1 多输入节点左侧 `+` 语义不明确 风险: - `nanoBanana` 同时支持 image 和 text。 - `generateVideo` 可能支持 image、text、audio。 处理: - 首版使用默认优先级。 - 后续增加输入类型选择小菜单。 ### 12.2 动态 schema 字段不统一 风险: - 不同 provider 对比例、时长、尺寸字段命名不一致。 处理: - 首版只识别 PRD 列出的常见字段。 - 识别不到时只展示 Model + Cost + Advanced。 ### 12.3 节点底栏影响媒体显示 风险: - 常驻 footer 改变节点高度,可能影响图片/视频 aspect fit。 处理: - footer 参与布局,不使用 absolute 遮盖。 - 修改 `BaseNode` 增加 `footerPanel`。 - 保留现有 `aspectFitMedia` 逻辑。 ### 12.4 成本不可计算 风险: - 外部 provider pricing 不完整。 处理: - 显示 `--`。 - 不阻塞生成。 - 不新增成本计算系统。 ### 12.5 复用 handle menu 可能引发回归 风险: - 抽 resolver 后原 `ConnectionDropMenu` 顺序或 keyboard 行为变化。 处理: - `ConnectionDropMenu` 的渲染、键盘操作和 props 不变。 - 只替换 options 来源。 - 补测试。 ## 13. 推荐提交顺序 ### PR 1:菜单 resolver 抽取 包含: - `connectionMenuOptions.tsx` - 修改 `ConnectionDropMenu.tsx` - 菜单顺序调整 - 单测 验收: - 原拖拽菜单可用。 - 菜单顺序符合 PRD。 ### PR 2:节点左右 `+` 包含: - `NodeQuickAddControls.tsx` - 修改 `WorkflowCanvas.tsx` - 自动创建并连接 - 组件测试 验收: - 图片、文本、视频、音频基础路径可用。 ### PR 3:图片节点参数底栏 包含: - `GenerationQuickSettingsBar.tsx` - `NodeCostBadge.tsx` - 修改 `BaseNode.tsx` - 修改 `GenerateImageNode.tsx` 验收: - 图片节点底部常用参数常驻。 - Advanced 不丢功能。 ### PR 4:视频节点参数底栏 包含: - `modelParameterClassification.ts` - 修改 `ModelParameters.tsx` - 修改 `GenerateVideoNode.tsx` 验收: - 常见视频 schema 参数前置。 - 识别不到时降级合理。 ### PR 5:收尾测试与 QA 包含: - 补齐测试。 - 修复样式和边界问题。 - 手动 QA 记录。 验收: - `npm.cmd run test:run` 通过。 - PRD 验收标准全部通过。 ## 14. 最小可行版本 如果需要最快交付一个可用版本,建议只做以下内容: 1. 抽 `ConnectionDropMenu` options resolver。 2. 实现右侧 `+`。 3. 支持图片、文本、视频、音频输出添加下游节点并自动连接。 4. 图片生成节点底部常驻 Gemini 常用参数。 暂缓: - 左侧多输入类型选择。 - 视频 schema quick 参数精细拆分。 - 单节点成本弹窗。 - 外部 provider 图片参数前置。 这样可以最快覆盖新用户最常见路径: ```text 上传图片 -> 右侧 + -> Generate Video 输入 Prompt -> 右侧 + -> Generate Image 选中 Generate Image -> 直接调整模型、比例、分辨率 ``` ## 15. 完整验收清单 - [ ] 选中图片节点时,右侧出现 `+`。 - [ ] 图片节点右侧 `+` 可添加并连接 Generate Image。 - [ ] 图片节点右侧 `+` 可添加并连接 Generate Video。 - [ ] 图片节点右侧 `+` 可添加并连接 Annotate。 - [ ] 图片节点右侧 `+` 可添加并连接 Split Grid。 - [ ] 图片节点右侧 `+` 可添加并连接 Output。 - [ ] 选中 Prompt 节点时,右侧出现 `+`。 - [ ] Prompt 节点右侧 `+` 可添加并连接 Generate Image。 - [ ] Prompt 节点右侧 `+` 可添加并连接 Generate Video。 - [ ] Prompt 节点右侧 `+` 可添加并连接 Generate Audio。 - [ ] Prompt 节点右侧 `+` 可添加并连接 LLM Generate。 - [ ] 左侧 `+` 可为生成节点添加合适输入节点。 - [ ] 原 handle 拖拽弹出 ConnectionDropMenu 不回归。 - [ ] 图片生成节点底部能直接修改模型。 - [ ] 图片生成节点底部能直接修改比例。 - [ ] 图片生成节点底部能直接修改分辨率。 - [ ] 图片生成节点底部能直接修改搜索开关。 - [ ] 视频生成节点底部能直接修改模型。 - [ ] 视频生成节点底部能直接修改可识别的核心 schema 参数。 - [ ] 成本组件复用现有成本逻辑。 - [ ] 无新增依赖。 - [ ] `npm.cmd run test:run` 通过。 - [ ] 手动验证空画布到生成视频路径可完成。 ## 16. 功能难易程度与实施优先级 本章从工程实现角度评估每个功能的难度、风险和先后顺序。总体判断:本 PRD 可以增量落地,最适合先做“复用现有连接菜单 + 右侧加号”,再做“左侧输入加号”,最后做“生成节点参数底栏”。这样能先交付最明显的用户收益,同时降低对节点布局和参数系统的扰动。 ### 16.1 难度分级标准 | 等级 | 含义 | | --- | --- | | S | 很简单,主要是抽代码、调整顺序或轻量 UI,风险低 | | M | 中等,需要理解现有数据流,可能涉及多个组件,但边界清晰 | | L | 较难,需要改布局、状态同步或动态 schema,存在回归风险 | | XL | 高风险,需要跨系统重构或改变核心结构,本 PRD 不建议进入首版 | ### 16.2 功能难度总览 | 功能 | 难度 | 价值 | 风险 | 建议优先级 | 说明 | | --- | --- | --- | --- | --- | --- | | 抽出 `ConnectionDropMenu` 菜单 resolver | S | 高 | 低 | P0 | 后续左右 `+` 的基础工作,也能减少重复逻辑 | | 调整菜单推荐顺序 | S | 中 | 低 | P0 | 直接符合 PRD,改动集中 | | 右侧 `+` 添加下游节点 | M | 很高 | 中 | P1 | 用户感知最明显,复用现有自动连线逻辑即可 | | 图片输出右侧推荐链路 | M | 很高 | 中 | P1 | 覆盖“上传图片 -> 生成视频/生成图/标注/输出”主路径 | | 文本输出右侧推荐链路 | M | 高 | 中 | P1 | 覆盖“Prompt -> 生成图/视频/音频/LLM”主路径 | | 视频/音频输出右侧推荐链路 | M | 中 | 中 | P2 | 价值明确,但使用频次略低,可跟随右侧 `+` 一起做 | | 左侧 `+` 添加输入节点 | M/L | 高 | 中高 | P2 | 多输入节点语义复杂,建议在右侧稳定后做 | | 多输入类型选择菜单 | L | 中 | 中 | P3 | 可优化左侧 `+` 体验,但首版可以用默认优先级替代 | | 图片生成节点常用参数底栏 | M/L | 高 | 中 | P3 | 现有参数已有,只是从折叠面板前置;主要风险是节点布局 | | 单节点成本 badge | M | 中 | 中 | P3 | 可复用 `calculatePredictedCost([node])`,但未知成本展示要处理 | | 视频生成节点常用参数底栏 | L | 高 | 中高 | P4 | 依赖 schema 字段识别,参数过滤和动态 handle 要小心 | | `ModelParameters` 支持 include/exclude/compact | M/L | 高 | 中 | P4 | 是视频参数前置的低成本方案,但要避免 schema 重复加载副作用 | | 抽 `useModelSchema` hook | L | 中 | 中 | P5 | 更干净,但不是首版必要;可作为后续重构 | | 参数底栏与 Advanced 完整重构 | XL | 中 | 高 | 暂缓 | 容易扩大范围,不建议首版做 | ### 16.3 推荐实施顺序 #### 第 1 阶段:菜单逻辑准备 优先级:P0 建议难度:S 建议耗时:0.5-1 天 任务: 1. 新增 `src/components/connectionMenuOptions.tsx`。 2. 从 `ConnectionDropMenu.tsx` 抽出所有菜单 options。 3. 导出 `getConnectionMenuOptions(handleType, connectionType)`。 4. 调整图片、文本、视频、音频输出菜单顺序,使其符合 PRD。 5. 给 resolver 增加单元测试。 为什么先做: - 这是后续 `+` 菜单复用的前置依赖。 - 改动范围小,容易验证。 - 即便后续暂停,也不会浪费工作。 风险点: - `ConnectionDropMenu` 当前有 keyboard navigation,抽取后要保证 `options` 长度和顺序稳定。 - `MenuAction` 目前定义在 `ConnectionDropMenu.tsx`,抽出去后注意引用路径。 交付标准: - 原有 handle 拖拽菜单仍可弹出。 - 原有菜单点击创建节点、自动连接不回归。 - 菜单顺序符合 PRD。 #### 第 2 阶段:右侧 `+` 添加下游节点 优先级:P1 建议难度:M 建议耗时:1-2 天 任务: 1. 新增 `NodeQuickAddControls.tsx`。 2. 在 `WorkflowCanvas.tsx` 里基于 selected/hover node 渲染右侧 `+`。 3. 为当前节点选择 primary output handle。 4. 点击右侧 `+` 后复用 `connectionDrop` 状态打开菜单。 5. 复用现有 `handleMenuSelect` 创建节点并自动连接。 6. 创建后新节点选中。 为什么第二步做右侧: - 右侧“下一步”是 PRD 中最关键的体验提升。 - 相比左侧输入,右侧输出语义更明确。 - 当前代码已有从 source handle 创建下游节点的完整逻辑,复用成本低。 建议先覆盖的节点: - `imageInput` - `prompt` - `nanoBanana` - `generateVideo` - `generateAudio` - `videoInput` - `audioInput` 首版可以暂缓的节点: - `switch` - `conditionalSwitch` - `router` 的复杂泛型路径 - `easeCurve` - `generate3d` 风险点: - overlay 是否跟随 React Flow 缩放和平移。 - hover 和 selected 同时存在时显示哪个节点。 - 节点尺寸来源可能不稳定,需要兼容 `node.measured`、`node.style.width`、默认尺寸。 交付标准: - 图片节点右侧 `+` 可创建 Generate Image、Generate Video、Annotate、Split Grid、Output。 - Prompt 节点右侧 `+` 可创建 Generate Image、Generate Video、Generate Audio、LLM Generate。 - 原 handle 拖拽连接不受影响。 #### 第 3 阶段:补齐视频/音频输出右侧路径 优先级:P2 建议难度:M 建议耗时:0.5-1 天 任务: 1. 为视频输出节点配置 primary output handle。 2. 支持右侧添加: - `videoStitch` - `videoTrim` - `videoFrameGrab` - `output` - `outputGallery` 3. 为音频输出节点支持: - `generateVideo` - `videoStitch` - `output` 为什么放在右侧基础之后: - 可以复用第 2 阶段的 `NodeQuickAddControls` 和 `openQuickAddMenu`。 - 主要是完善 handle 映射和菜单顺序。 风险点: - `videoStitch` 输入 handle 是 `video-0` 等动态形式,现有 `handleMenuSelect` 已处理,但要手动验证。 - `output` 对 video/audio 的 target handle 和实际节点实现需要对齐。 交付标准: - Generate Video 右侧 `+` 可接 Video Trim、Frame Grab、Output。 - Generate Audio 右侧 `+` 可接 Generate Video、Video Stitch、Output。 #### 第 4 阶段:左侧 `+` 添加输入节点 优先级:P2 建议难度:M/L 建议耗时:1-2 天 任务: 1. 在 `NodeQuickAddControls` 中开启左侧 `+`。 2. 为当前节点选择 primary input handle。 3. 点击左侧 `+` 后以 `connectionType = "target"` 打开菜单。 4. 创建输入节点后自动连接到当前节点。 为什么不和右侧一起做: - 左侧输入涉及多输入节点,复杂度明显高于右侧。 - 先让右侧链路稳定,可以减少一次性改动范围。 首版默认 input handle 优先级: 1. `image` 2. `text` 3. `video` 4. `audio` 5. `3d` 6. `easeCurve` 建议覆盖: - `nanoBanana` 左侧添加 `imageInput` 或 `prompt`。 - `generateVideo` 左侧添加 `imageInput` 或 `prompt`。 - `generateAudio` 左侧添加 `prompt`。 - `llmGenerate` 左侧添加 `prompt`。 风险点: - `nanoBanana`、`generateVideo` 这样的多输入节点默认选哪个输入会影响用户预期。 - 如果默认 image,用户想加 prompt 时需要后续优化。 - 如果直接弹输入类型选择菜单,范围会变大。 交付标准: - 左侧 `+` 能创建输入节点并连接。 - 自动连接 edge 合法。 - 多输入节点至少有一个默认路径可用。 #### 第 5 阶段:左侧多输入类型选择优化 优先级:P3 建议难度:L 建议耗时:1 天 任务: 1. 当当前节点有多个输入类型时,先展示输入类型选择。 2. 用户选择 Image/Text/Audio/Video 后,再打开对应输入候选菜单。 3. 记住用户最近选择可作为后续优化,首版不需要。 为什么放后面: - 这是体验增强,不是首版必要路径。 - 会增加一个新状态或新菜单组件,需要更多测试。 交付标准: - Generate Image 左侧可明确选择添加 Image Input 或 Prompt。 - Generate Video 左侧可明确选择 Image、Text、Audio 输入。 #### 第 6 阶段:图片生成节点参数底栏 优先级:P3 建议难度:M/L 建议耗时:1.5-2.5 天 任务: 1. 修改 `BaseNode`,增加 `footerPanel`。 2. 新增 `GenerationQuickSettingsBar.tsx`。 3. 新增 `NodeCostBadge.tsx`。 4. 在 `GenerateImageNode.tsx` 中把 Gemini 常用参数放到底栏。 5. Advanced 继续复用 `InlineParameterPanel`。 6. 未选中节点时底栏保持可读。 为什么在 `+` 后做: - 参数底栏会影响节点布局,是更容易产生视觉回归的部分。 - 右侧 `+` 能先解决“下一步怎么做”的主要问题。 风险点: - `BaseNode` 当前对 `settingsPanel` 高度有 ResizeObserver 和动画逻辑,新增 footer 要避免干扰。 - 图片输出区域可能因为 footer 占位而变矮,需要检查 aspect fit。 - `inlineParametersEnabled` 当前是用户设置,PRD 要常驻,二者语义需要重新梳理。 建议策略: - 常用参数始终通过 `footerPanel` 展示。 - `InlineParameterPanel` 只用于 Advanced。 - 不删除 `useInlineParameters`,但生成节点可逐步不依赖它展示常用参数。 交付标准: - Generate Image 节点底部常驻 Model、Aspect Ratio、Resolution、搜索开关和 Cost。 - 参数控件能修改节点数据。 - Advanced 展开后原高级参数能力不丢失。 - 图片显示不被遮挡。 #### 第 7 阶段:单节点成本展示 优先级:P3 建议难度:M 建议耗时:0.5-1 天 任务: 1. 新增 `NodeCostBadge.tsx`。 2. 基于 `calculatePredictedCost([node])` 显示单节点成本。 3. 对 unknown pricing 显示 `--`。 4. 对未选模型显示 `Select model`。 为什么可以跟图片底栏一起做: - PRD 要求参数底栏展示成本。 - 但它是独立小组件,适合单独测试。 风险点: - `calculatePredictedCost` 对部分节点依赖完整 node data。 - 外部 provider pricing 不完整,不要误导用户。 交付标准: - Gemini 图片模型能显示成本。 - 外部 provider unknown 显示低干扰占位。 - 成本不可算不影响运行。 #### 第 8 阶段:视频生成节点参数底栏 优先级:P4 建议难度:L 建议耗时:2-3 天 任务: 1. 新增 `modelParameterClassification.ts`。 2. 修改 `ModelParameters.tsx` 支持: - `includeNames` - `excludeNames` - `compact` 3. 在 `GenerateVideoNode.tsx` 底栏展示: - Model - quick schema 参数 - Cost - Advanced 4. Advanced 展示排除 quick 参数后的完整 schema。 为什么难度更高: - 视频参数来自 provider schema,不同模型字段不稳定。 - `ModelParameters` 当前同时承担 schema 加载、默认值写入、输入 handle 回调,拆分前置参数要小心副作用。 - 可能触发动态 handle 重新渲染。 推荐首版策略: - 不先抽 `useModelSchema`。 - 先给 `ModelParameters` 增加 include/exclude/compact。 - 利用已有 localStorage schema cache 和 `deduplicatedFetch` 降低重复 fetch 影响。 风险点: - quick 和 advanced 两个 `ModelParameters` 同时渲染可能重复设置默认参数。 - `onInputsLoaded` 不应重复触发。 - schema 为空或模型未选时要降级。 处理: - 只让 quick 参数区域传 `onInputsLoaded`。 - Advanced 不传 `onInputsLoaded`。 - `handleInputsLoaded` 内做浅比较,避免重复 `updateNodeData`。 交付标准: - schema 中有 `duration`、`fps`、`aspect_ratio` 时,底栏可直接修改。 - schema 识别不到时,只显示 Model + Cost + Advanced。 - Advanced 不重复展示已前置字段。 #### 第 9 阶段:抽 `useModelSchema` 进一步整理 优先级:P5 建议难度:L 建议耗时:1-2 天 任务: 1. 从 `ModelParameters.tsx` 抽出 schema fetching/cache/defaults。 2. 新增 `useModelSchema(modelId, provider)`。 3. `GenerateVideoNode` 和 `ModelParameters` 共用 hook。 为什么暂缓: - 它会让设计更干净,但不是 PRD 首版必须。 - 改动较深,容易让参数面板测试大面积变化。 适合时机: - 视频底栏已经跑通。 - include/exclude 方案暴露出重复加载或默认值副作用。 ### 16.4 推荐版本切分 #### MVP 版本 目标:最快交付用户能感知的主路径增强。 包含: 1. 菜单 resolver 抽取。 2. 右侧 `+`。 3. 图片输出和文本输出下游推荐。 4. 图片生成节点 Gemini 常用参数底栏。 不包含: - 左侧多输入类型选择。 - 视频 schema quick 参数。 - `useModelSchema` 抽象。 - 外部 provider 图片参数前置。 推荐原因: - 覆盖最常见路径。 - 改动边界可控。 - 视觉和数据流风险适中。 #### 完整 PRD 版本 包含: 1. 菜单 resolver。 2. 左右 `+`。 3. 全媒体类型右侧推荐。 4. 左侧输入推荐。 5. 图片参数底栏。 6. 视频参数底栏。 7. 成本 badge。 8. 完整测试和手动 QA。 推荐分 4-5 个 PR,不建议一次性提交。 ### 16.5 依赖关系图 ```text Connection menu resolver ├─ 原 ConnectionDropMenu 继续使用 └─ NodeQuickAddControls ├─ 右侧 + 添加下游节点 │ ├─ 图片输出路径 │ ├─ 文本输出路径 │ ├─ 视频输出路径 │ └─ 音频输出路径 └─ 左侧 + 添加输入节点 └─ 多输入类型选择 BaseNode footerPanel ├─ GenerateImageNode 参数底栏 │ └─ NodeCostBadge └─ GenerateVideoNode 参数底栏 ├─ modelParameterClassification └─ ModelParameters include/exclude/compact ``` ### 16.6 最容易踩坑的地方 1. **不要复制第二份菜单数据。** - 菜单数据必须抽 resolver。 - 否则后续 handle 拖拽菜单和 `+` 菜单会不一致。 2. **不要一开始就重构 `ModelParameters`。** - 视频参数底栏之前,可以先做 include/exclude 小扩展。 - 抽 hook 放到后续。 3. **不要让底栏 absolute 覆盖媒体。** - 底栏必须参与节点布局。 - 推荐改 `BaseNode` 增加 `footerPanel`。 4. **不要在首版追求所有节点类型完美支持。** - 优先图片、文本、视频、音频主路径。 - `switch`、`conditionalSwitch`、`router` 复杂路径可以后补。 5. **不要把 unknown cost 显示成 0。** - 0 会误导用户。 - 应显示 `--` 或 `Unknown`。 6. **不要破坏原 handle 拖拽菜单。** - 这是现有核心交互。 - 每个阶段都要手动验证。 ### 16.7 推荐实际开发排期 如果一个人实现,建议按下面节奏: | 天数 | 目标 | | --- | --- | | Day 1 | 抽菜单 resolver,调整顺序,补测试 | | Day 2 | 实现右侧 `+` overlay,支持图片和文本主路径 | | Day 3 | 补视频/音频右侧路径,修自动连线边界 | | Day 4 | 实现左侧 `+` 默认输入路径 | | Day 5 | 实现 `BaseNode.footerPanel` 和图片参数底栏 | | Day 6 | 实现 `NodeCostBadge`,完善图片底栏测试和 QA | | Day 7-8 | 实现视频参数底栏和 `ModelParameters` include/exclude | | Day 9 | 补测试、修样式、完整手动验收 | 如果希望先快速上线 MVP: | 天数 | 目标 | | --- | --- | | Day 1 | 菜单 resolver + 右侧 `+` 基础 | | Day 2 | 图片/文本右侧路径完整可用 | | Day 3 | 图片参数底栏 | | Day 4 | 测试和手动 QA | ### 16.8 最终建议 最推荐的先后顺序: 1. `ConnectionDropMenu` resolver 抽取。 2. 右侧 `+`,先支持图片和文本输出。 3. 右侧 `+` 补视频和音频输出。 4. 左侧 `+`,先做默认输入路径。 5. 图片生成节点参数底栏。 6. 单节点成本 badge。 7. 视频生成节点参数底栏。 8. 多输入类型选择和 `useModelSchema` 优化。 这个顺序的好处是每一步都有独立价值,而且前一步基本都是后一步的基础;如果中途要停,也不会留下大面积半成品。 ## 2026-04-29 更新:视频节点上传 / 加号 / 提示词框 状态:已完成,可进入手动测试。 本次实现把视频节点补齐到与图片节点一致的交互路径: 1. `videoInput` 节点上方增加独立 Upload 按钮,上传行为从节点内部迁移到节点上方。 2. `videoInput` 上传完成后隐藏下方提示词框和左侧加号,只保留右侧加号,作为素材节点使用。 3. 上传后点击右侧加号可创建 `generateVideo` 节点,并自动连接 `video -> video`。 4. `generateVideo` 节点下方显示固定 composer 框,包含提示词、模型入口、Cost、Advanced 和发送按钮。 5. `Generate Video Settings` 右侧旧面板在选中 `generateVideo` 时不再显示,避免与 composer 设置重复。 6. composer 的发送按钮会保存 `inputPrompt` 并触发 `regenerateNode`。 涉及文件: - `src/utils/videoFile.ts` - `src/components/VideoNodeUploadOverlay.tsx` - `src/components/nodes/VideoInputNode.tsx` - `src/components/nodes/GenerateVideoNode.tsx` - `src/components/ImageNodeComposerPanel.tsx` - `src/components/WorkflowCanvas.tsx` - `src/components/nodes/ControlPanel.tsx` - `src/types/nodes.ts` - `src/components/__tests__/VideoNodeUploadOverlay.test.tsx` - `src/components/__tests__/WorkflowCanvas.test.tsx` - `src/components/__tests__/ImageNodeComposerPanel.test.tsx` - `src/components/__tests__/GenerateVideoNode.test.tsx` 验证: ```powershell npm.cmd run test:run -- WorkflowCanvas.test.tsx ImageNodeComposerPanel.test.tsx VideoNodeUploadOverlay.test.tsx GenerateVideoNode.test.tsx GenerateImageNode.test.tsx ``` 结果:5 个测试文件通过,109 个测试通过。测试日志里仍有已有的 React `act(...)` 警告和 jsdom 视频 `pause()` 未实现提示,不影响本次功能通过。 ## 2026-04-30 更新:空图像节点尝试入口 状态:已完成,可进入手动测试。 本次实现补齐第一个空图像节点中间的两个尝试入口: 1. 空 `imageInput` 节点中间显示 `尝试:`、`图生图`、`图片高清`。 2. 点击 `图生图` 会打开本地图片选择器。 3. 选择图片后会把图片上传到当前 `imageInput` 节点。 4. 上传成功后自动在右侧创建 `Generate Image` / `nanoBanana` 节点。 5. 自动连接当前图片节点 `image` 输出到新生成图片节点的 `image` 输入。 6. `图片高清` 目前先作为占位按钮展示,禁用状态,后续接入高清能力时再实现真实执行逻辑。 涉及文件: - `src/components/nodes/ImageInputNode.tsx` - `src/components/__tests__/ImageInputNode.test.tsx` 验证: ```powershell npm.cmd run test:run -- ImageInputNode.test.tsx WorkflowCanvas.test.tsx ImageNodeComposerPanel.test.tsx GenerateImageNode.test.tsx ``` 结果:4 个测试文件通过,95 个测试通过。测试日志里仍有已有的 React `act(...)` 警告、jsdom canvas/getContext 提示和测试环境相对 URL 提示,不影响本次功能通过。 ## 2026-04-30 更新:视频上传节点帧生成快捷入口 状态:已完成,可进入手动测试。 本次实现把参考图里的视频入口能力放到空 `videoInput` 节点中: 1. 空 `videoInput` 节点中间显示 `尝试:`、`首尾帧生成视频`、`首帧生成视频`。 2. 点击 `首帧生成视频` 会打开图片选择器,选择一张首帧图片。 3. 点击 `首尾帧生成视频` 会打开图片选择器,选择两张首尾帧图片。 4. 上传帧图片后自动在右侧创建 `generateVideo` 节点。 5. 上传的帧图片会写入新 `generateVideo` 节点的 `inputImages`,后续生成视频时可作为帧素材使用。 6. 原有上方 `Upload` 上传真实视频文件的能力保持不变。 涉及文件: - `src/components/nodes/VideoInputNode.tsx` - `src/components/__tests__/VideoInputNode.test.tsx` 验证: ```powershell npm.cmd run test:run -- VideoInputNode.test.tsx WorkflowCanvas.test.tsx ImageNodeComposerPanel.test.tsx VideoNodeUploadOverlay.test.tsx GenerateVideoNode.test.tsx ``` 结果:5 个测试文件通过,87 个测试通过。测试日志里仍有已有的 React `act(...)` 警告、jsdom 视频 `pause()` 未实现提示和测试环境相对 URL 提示,不影响本次功能通过。 ## 2026-04-30 更新:首尾帧生成视频节点自动建链修正 本次把“首尾帧生成视频”从“只创建一个隐藏输入的 Generate Video 节点”修正为可见节点链路: 1. 点击 `首帧生成视频` 后,只允许选择视频文件;当前 `videoInput` 节点会变成 `首帧` 视频源节点,并在右侧创建 `generateVideo` 节点。 2. 点击 `首尾帧生成视频` 后,只允许选择两个视频文件;当前节点作为 `首帧`,自动在下方创建 `尾帧` 视频源节点,并在右侧居中创建 `generateVideo` 节点。 3. 自动创建 `video -> video` 连线:`首帧` 连接到生成视频节点;如果有 `尾帧`,也自动连接到同一个生成视频节点。 4. `WorkflowCanvas` 的浮动标题支持读取节点 `label`,所以自动生成的源节点会显示 `首帧` / `尾帧`。 涉及文件: - `src/components/nodes/VideoInputNode.tsx` - `src/components/WorkflowCanvas.tsx` - `src/components/__tests__/VideoInputNode.test.tsx` 验证: ```powershell npm.cmd run test:run -- VideoInputNode.test.tsx WorkflowCanvas.test.tsx ImageNodeComposerPanel.test.tsx VideoNodeUploadOverlay.test.tsx GenerateVideoNode.test.tsx ``` 结果:5 个测试文件、87 个测试通过。测试日志里仍有既有的 React `act(...)` 和 jsdom `HTMLMediaElement.pause()` 警告,不影响本次功能断言。 ## 2026-04-30 更新:首尾帧视频入口改为纯建链 本次根据反馈移除了“首尾帧生成视频”点击后的即时文件选择: 1. 点击 `首帧生成视频` 不再弹上传框,只把当前视频上传节点标记为 `首帧`,并在右侧创建/连接 `Generate Video` 节点。 2. 点击 `首尾帧生成视频` 不再要求立刻选择两个视频,只创建一个空的 `尾帧` 视频上传节点,并把 `首帧`、`尾帧` 都连接到右侧 `Generate Video`。 3. 视频文件由用户后续分别在 `首帧` / `尾帧` 视频节点上自行上传。 验证: ```powershell npm.cmd run test:run -- VideoInputNode.test.tsx WorkflowCanvas.test.tsx ImageNodeComposerPanel.test.tsx VideoNodeUploadOverlay.test.tsx GenerateVideoNode.test.tsx ``` 结果:5 个测试文件、87 个测试通过。 ## 2026-04-30 更新:首尾帧目标节点改为视频节点 本次调整“首尾帧生成视频”的建链目标: 1. `首帧` 和 `尾帧` 仍然都是空的视频上传节点,用户后续自行上传视频。 2. 右侧被连接的目标也改为空的 `videoInput` 视频节点,不再创建 `generateVideo` 节点。 3. 自动连线保持 `video -> video`,形成 `首帧/尾帧 -> 视频节点` 的纯视频节点链路。 验证: ```powershell npm.cmd run test:run -- VideoInputNode.test.tsx WorkflowCanvas.test.tsx ```