You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

57 KiB

Popiart-node 节点系统交互增强实现方案

1. 背景与目标

本实现方案对应 Popiart-node 节点系统交互增强 PRD,目标是在不重构现有节点系统、不新增依赖、不改变工作流 JSON 主结构的前提下,增强节点画布的低门槛操作体验。

本次增强聚焦两类能力:

  1. 节点左右两侧增加轻量 + 操作入口。

    • 右侧 + 用于添加下游节点,并自动连接当前节点输出。
    • 左侧 + 用于添加输入节点,并自动连接到当前节点输入。
    • 候选菜单复用现有 ConnectionDropMenu 的菜单数据与自动连线逻辑。
  2. 图片和视频生成节点底部前置常用生成参数。

    • 图片生成节点前置 ModelAspect RatioResolutionGoogle SearchImage SearchCost
    • 视频生成节点前置 Model、可识别的核心 schema 参数、CostAdvanced
    • 高级参数继续复用 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 支持 settingsPanelsettingsExpanded,用于把参数面板挂在节点底部。
  • 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 新增文件

建议新增:

src/components/connectionMenuOptions.tsx

或如果希望更偏业务逻辑:

src/utils/connectionMenuOptions.tsx

考虑菜单项包含 React icon,建议放在 src/components/connectionMenuOptions.tsx

5.3 导出类型

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 等私有常量。
  • 从新文件导入:
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 已经拥有 nodesedgesaddNodeonConnectscreenToFlowPositionconnectionDrop 等上下文。
  • 可以复用现有 handleMenuSelect 的自动连接逻辑。
  • 不需要改每个节点组件。

6.3 新增组件

建议新增:

src/components/NodeQuickAddControls.tsx

职责:

  • 接收当前节点位置、尺寸、类型、可用 handle 类型。
  • 渲染左侧和右侧 +
  • 点击时回调给 WorkflowCanvas

接口建议:

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 中已有:

import { ViewportPortal } from "@xyflow/react";

可在 <ReactFlow> 内部或附近渲染:

<ViewportPortal>
  {quickAddNode && (
    <NodeQuickAddControls
      nodeId={quickAddNode.id}
      nodeType={quickAddNode.type as NodeType}
      position={quickAddNode.position}
      width={nodeWidth}
      height={nodeHeight}
      showInput={hasTargetHandles}
      showOutput={hasSourceHandles}
      onOpen={(side) => openQuickAddMenu(quickAddNode, side)}
    />
  )}
</ViewportPortal>

6.5 显示规则

显示条件:

  • 单节点选中。
  • 或 hover 某个节点。
  • 拖拽节点时隐藏。
  • 正在打开菜单时保持显示。
  • 多选时首版建议隐藏,避免批量语义复杂。

获取 hover:

  • BaseNode 已经调用 setHoveredNodeId
  • workflowStore 中已有 hoveredNodeId
  • WorkflowCanvas 可读取:
const hoveredNodeId = useWorkflowStore((state) => state.hoveredNodeId);

选择逻辑:

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:
    • 左侧:添加输入
    • 右侧:添加下一步

样式建议:

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
"

位置建议:

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),可复用或抽出到共享文件。

建议新增:

src/utils/nodeHandleRegistry.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
  • switchconditionalSwitch 等动态输出节点,首版可按当前类型特殊处理:
    • switch:有输入时显示左侧,右侧如果没有 switch outputs 则隐藏。
    • conditionalSwitch:左侧 text,右侧可暂不支持或取默认 rule output。

6.8 打开 Quick Add 菜单

复用现有 connectionDrop 状态最省事。

当前 connectionDrop 结构:

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 默认策略

按节点类型判断:

  • imageInputannotationnanoBananavideoFrameGrabglbViewer 输出 image
  • promptarraypromptConstructorllmGenerate 输出 text
  • generateVideovideoInputvideoStitchvideoTrimeaseCurve 输出 video
  • generateAudioaudioInput 输出 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 新节点位置

推荐固定偏移:

const QUICK_ADD_X_OFFSET = 360;
const QUICK_ADD_Y_OFFSET = 0;

右侧:

flowPosition = {
  x: node.position.x + nodeWidth + QUICK_ADD_X_OFFSET,
  y: node.position.y + QUICK_ADD_Y_OFFSET,
}

左侧:

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

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() 控制:

const { inlineParametersEnabled } = useInlineParameters();

PRD 需要常用参数固定展示,因此建议:

  • 常用参数不再受 inlineParametersEnabled 控制。
  • Advanced 面板仍可复用 InlineParameterPanel

7.3 新增组件

建议新增:

src/components/nodes/GenerationQuickSettingsBar.tsx

职责:

  • 渲染一行或两行紧凑参数控件。
  • 支持 selectcheckbox、成本 badge、Advanced 按钮。
  • 用于图片和视频节点共用。

也可以先在 GenerateImageNode.tsx 内实现,再抽组件。考虑后续视频也要用,建议抽组件。

接口建议:

interface QuickSettingItem {
  key: string;
  label: string;
  control: React.ReactNode;
  hidden?: boolean;
}

interface GenerationQuickSettingsBarProps {
  items: QuickSettingItem[];
  cost?: React.ReactNode;
  advanced?: React.ReactNode;
  muted?: boolean;
}

样式要求:

  • 固定在节点底部,占用布局高度,不遮挡图片。
  • 未选中也可读,视觉可更弱。
  • 文本和控件不能溢出。
  • 控件高度紧凑。

示例 class:

<div className="nodrag nopan nowheel border-t border-neutral-700/70 bg-[#202020] px-2 py-1.5">
  <div className="grid grid-cols-[repeat(auto-fit,minmax(120px,1fr))] gap-1.5">
    ...
  </div>
</div>

7.4 Advanced 面板行为

建议将 InlineParameterPanel 改为只承载高级参数。

图片节点:

  • 常用参数常驻。
  • Advanced 默认折叠。
  • 点击 Advanced 展开:
    • 外部 provider 显示完整 ModelParameters
    • fallback 参数仍在 tabs 中。

需要注意:

  • Gemini 图片模型当前所有常用参数都已前置,Advanced 可只显示 fallback 或不显示。
  • 外部 provider 的完整 schema 仍需要 Advanced。

7.5 成本组件

建议新增:

src/components/nodes/NodeCostBadge.tsx

职责:

  • 输入单个节点数据。
  • 展示该节点预估成本。
  • 不弹全局成本详情,或点击时可打开 CostDialog

接口建议:

import { WorkflowNode } from "@/types";

interface NodeCostBadgeProps {
  node: WorkflowNode;
}

实现方式:

  • 首版可复用 calculatePredictedCost([node])
  • 如果返回 unknownPricingCount > 0,显示 --
  • 如果没有模型,显示 Select model
  • 如果有成本,显示 formatCost(totalCost)

注意:

  • calculatePredictedCostgenerateVideo 外部 provider 多数未知,显示 -- 是可接受的。
  • 成本不可计算不能阻塞生成。

7.6 图片节点参数展示规则

Gemini provider:

  • Model 永远显示。
  • Aspect Ratio 永远显示。
  • Resolution
    • nano-banana 不支持时隐藏或固定显示 1K
    • nano-banana-pronano-banana-2 显示。
  • Google Search
    • nano-banana-pronano-banana-2 显示。
  • Image Search
    • nano-banana-2 显示。
  • Cost
    • 可计算时显示价格。
    • 不可计算时显示 --

外部 provider:

  • Model 显示。
  • 常用 schema 参数首版可不前置,或后续基于 schema 提取。
  • Cost unknown 时显示 --
  • Advanced 展示完整 schema。

7.7 BaseNode 配合

当前 BaseNode 有:

settingsPanel?: ReactNode;
settingsExpanded?: boolean;

建议新增一个更明确的 prop:

footerPanel?: ReactNode;

渲染结构:

<div className="node-shell">
  <div ref={contentRef}>children</div>
  {footerPanel}
  {settingsPanel}
</div>

如果不想改 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

建议新增工具:

src/utils/modelParameterClassification.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:
src/hooks/useModelSchema.ts
  1. ModelParameters 继续使用该 hook。
  2. GenerateVideoNode 也使用该 hook,拿到 schema 后:
    • quick 参数渲染到底栏。
    • advanced 参数传给 ModelParameters 或新增 ParameterControls

如果想减少改动:

  • 先给 ModelParameters 增加可选 prop:
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

interface ModelParametersProps {
  modelId: string;
  provider: ProviderType;
  parameters: Record<string, unknown>;
  onParametersChange: (parameters: Record<string, unknown>) => void;
  onExpandChange?: (expanded: boolean, parameterCount: number) => void;
  onInputsLoaded?: (inputs: ModelInputDef[]) => void;
  includeNames?: string[];
  excludeNames?: string[];
  compact?: boolean;
}

schema 过滤:

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 依赖 ModelParametersonInputsLoaded 来设置 inputSchema

如果底栏和 Advanced 都可能渲染 ModelParameters,避免 onInputsLoaded 重复造成不必要更新:

  • 只让一个 ModelParameters 负责 onInputsLoaded
  • 或在 handleInputsLoaded 中做浅比较,输入未变化时不更新。

建议:

  • 底栏 quick ModelParametersonInputsLoaded
  • Advanced 的 ModelParameters 不传 onInputsLoaded

8.8 验收

  • 视频生成节点未选中时,底部能看到模型和常见参数。
  • schema 包含 durationfpsaspect_ratio 等字段时能前置展示。
  • schema 不包含常见字段时,显示 Model + Cost + Advanced。
  • Advanced 展开后完整参数能力不丢失。
  • 动态输入 handle 仍正常生成。

9. 里程碑五:测试计划

9.1 单元测试

新增:

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") 包含 generateAudiollmGenerate
  • getConnectionMenuOptions("video", "source") 包含 videoStitchvideoTrimvideoFrameGrab
  • getConnectionMenuOptions("audio", "source") 包含 generateVideovideoStitchoutput
  • splitVideoParameters 能识别 common video params。
  • NodeCostBadge 对 known cost、unknown cost、no model 的展示正确。

9.2 组件测试

扩展:

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 命令

npm.cmd run test:run

如果只跑相关测试:

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 新增文件

src/components/connectionMenuOptions.tsx
src/components/NodeQuickAddControls.tsx
src/components/nodes/GenerationQuickSettingsBar.tsx
src/components/nodes/NodeCostBadge.tsx
src/utils/modelParameterClassification.ts

可选新增:

src/hooks/useModelSchema.ts
src/utils/nodeHandleRegistry.ts

10.2 修改文件

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 测试文件

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

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

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

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 图片参数前置。

这样可以最快覆盖新用户最常见路径:

上传图片 -> 右侧 + -> 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.measurednode.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 阶段的 NodeQuickAddControlsopenQuickAddMenu
  • 主要是完善 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 左侧添加 imageInputprompt
  • generateVideo 左侧添加 imageInputprompt
  • generateAudio 左侧添加 prompt
  • llmGenerate 左侧添加 prompt

风险点:

  • nanoBananagenerateVideo 这样的多输入节点默认选哪个输入会影响用户预期。
  • 如果默认 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 中有 durationfpsaspect_ratio 时,底栏可直接修改。
  • schema 识别不到时,只显示 Model + Cost + Advanced。
  • Advanced 不重复展示已前置字段。

第 9 阶段:抽 useModelSchema 进一步整理

优先级:P5
建议难度:L
建议耗时:1-2 天

任务:

  1. ModelParameters.tsx 抽出 schema fetching/cache/defaults。
  2. 新增 useModelSchema(modelId, provider)
  3. GenerateVideoNodeModelParameters 共用 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 依赖关系图

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. 不要在首版追求所有节点类型完美支持。

    • 优先图片、文本、视频、音频主路径。
    • switchconditionalSwitchrouter 复杂路径可以后补。
  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

验证:

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

验证:

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

验证:

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

验证:

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. 视频文件由用户后续分别在 首帧 / 尾帧 视频节点上自行上传。

验证:

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,形成 首帧/尾帧 -> 视频节点 的纯视频节点链路。

验证:

npm.cmd run test:run -- VideoInputNode.test.tsx WorkflowCanvas.test.tsx