57 KiB
Popiart-node 节点系统交互增强实现方案
1. 背景与目标
本实现方案对应 Popiart-node 节点系统交互增强 PRD,目标是在不重构现有节点系统、不新增依赖、不改变工作流 JSON 主结构的前提下,增强节点画布的低门槛操作体验。
本次增强聚焦两类能力:
-
节点左右两侧增加轻量
+操作入口。- 右侧
+用于添加下游节点,并自动连接当前节点输出。 - 左侧
+用于添加输入节点,并自动连接到当前节点输入。 - 候选菜单复用现有
ConnectionDropMenu的菜单数据与自动连线逻辑。
- 右侧
-
图片和视频生成节点底部前置常用生成参数。
- 图片生成节点前置
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.tsxsrc/components/ConnectionDropMenu.tsxsrc/store/workflowStore.tssrc/store/utils/connectedInputs.tssrc/store/utils/executionUtils.ts
当前已有能力:
WorkflowCanvas.tsx负责 React Flow 主画布、节点注册、边注册、连线校验、拖拽创建节点、菜单弹出和自动连线。ConnectionDropMenu.tsx已经按 handle 类型区分候选菜单:imagetextvideoaudio
handleMenuSelect已经实现:- 创建目标节点。
- 按 handle 类型推断新节点输入/输出 handle。
- 创建合法 edge。
- 支持多选节点批量连接。
- 支持
splitGridImmediate特殊 action。
3.2 节点 UI
相关文件:
src/components/nodes/BaseNode.tsxsrc/components/nodes/FloatingNodeHeader.tsxsrc/components/nodes/GenerateImageNode.tsxsrc/components/nodes/GenerateVideoNode.tsxsrc/components/nodes/InlineParameterPanel.tsxsrc/components/nodes/ModelParameters.tsxsrc/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.tsxsrc/components/CostDialog.tsxsrc/utils/costCalculator.ts
当前已有能力:
calculatePredictedCost(nodes)能基于全工作流节点计算预估成本。- Gemini 图片模型成本有硬编码价格表。
- 外部 provider 没有 pricing 时会以 unknown pricing 处理。
当前缺口:
CostIndicator是全局成本入口,不适合作为单节点底部小组件直接复用。- 需要抽出或新增单节点成本展示能力。
4. 总体实现策略
采用增量式实现,先打通节点 + 自动连接,再做参数底栏。
推荐里程碑:
- 抽出 ConnectionDropMenu 菜单 resolver。
- 实现节点左右
+overlay 和自动连接。 - 完善左侧输入推荐逻辑。
- 实现图片生成节点参数底栏。
- 实现视频生成节点参数底栏。
- 补测试和手动 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":
nanoBanana,Generate ImagegenerateVideo,Generate Videoannotation,AnnotatesplitGrid,Split Gridoutput,OutputoutputGallery,Output GalleryimageCompare,Image Comparerouter,Routerswitch,Switch
保留 splitGridImmediate 时建议放在 splitGrid 后面,或者只在从 handle 拖出菜单中展示,节点右侧 + 首版可不展示 action,避免“创建节点”和“立即操作”混在一起。
文本输出右侧推荐
当 connectionType === "source" 且 handleType === "text":
nanoBanana,Generate ImagegenerateVideo,Generate VideogenerateAudio,Generate AudiollmGenerate,LLM GeneratepromptConstructor,Prompt Constructorarray,Arrayoutput,如果当前系统支持文本输出展示routerswitchconditionalSwitch
如果 output 当前没有 text handle,不要强行展示,避免自动连接失败。
视频输出右侧推荐
当 connectionType === "source" 且 handleType === "video":
videoStitchvideoTrimvideoFrameGraboutputoutputGalleryrouterswitch
音频输出右侧推荐
当 connectionType === "source" 且 handleType === "audio":
generateVideovideoStitchoutputrouterswitch
左侧输入推荐
当 connectionType === "target":
图片输入:
imageInputvideoFrameGrabannotationglbViewernanoBananarouter
文本输入:
promptllmGeneratepromptConstructorarrayrouter
音频输入:
audioInputgenerateAudiorouter
视频输入:
videoInputgenerateVideovideoStitchvideoTrimrouter
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已经拥有nodes、edges、addNode、onConnect、screenToFlowPosition、connectionDrop等上下文。- 可以复用现有
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 > 0showOutput = getNodeHandles(node.type).outputs.length > 0- 对
switch、conditionalSwitch等动态输出节点,首版可按当前类型特殊处理: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.idsourceHandleId = 当前节点可用输出 handlehandleType = 输出 handle typeflowPosition = 当前节点右侧固定偏移坐标position = 按钮屏幕坐标或菜单屏幕坐标
左侧 + 打开时:
connectionType = "target"sourceNodeId = currentNode.idsourceHandleId = 当前节点输入 handlehandleType = 输入 handle typeflowPosition = 当前节点左侧固定偏移坐标
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。
如果要更快落地,首版可以默认优先级:
imagetextvideoaudio3deaseCurve
后续再优化为输入类型选择。
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
职责:
- 渲染一行或两行紧凑参数控件。
- 支持
select、checkbox、成本 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 中。
- 外部 provider 显示完整
需要注意:
- 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)。
注意:
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 提取。
Costunknown 时显示--。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_ratioaspectRatioratioresolutionsizedurationnum_framesfps
建议新增工具:
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,因此需要调整数据流。
推荐方案:
- 把 schema 获取逻辑从
ModelParameters抽成 hook:
src/hooks/useModelSchema.ts
ModelParameters继续使用该 hook。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 依赖 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 单元测试
新增:
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 组件测试
扩展:
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
手动验证路径:
- 空画布添加 Image Input。
- 选中 Image Input。
- 点击右侧
+。 - 选择 Generate Video。
- 验证新节点出现在右侧并自动连线。
- 选中 Prompt。
- 点击右侧
+。 - 选择 Generate Audio。
- 验证 text edge 正确。
- 选中 Generate Image。
- 点击左侧
+。 - 添加 Image Input 或 Prompt 并自动连接。
- 验证 Generate Image 底栏可改模型、比例、分辨率、搜索开关。
- 验证 Generate Video 底栏可改模型和常见 schema 参数。
- 验证原 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.tsxNodeCostBadge.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. 最小可行版本
如果需要最快交付一个可用版本,建议只做以下内容:
- 抽
ConnectionDropMenuoptions resolver。 - 实现右侧
+。 - 支持图片、文本、视频、音频输出添加下游节点并自动连接。
- 图片生成节点底部常驻 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 天
任务:
- 新增
src/components/connectionMenuOptions.tsx。 - 从
ConnectionDropMenu.tsx抽出所有菜单 options。 - 导出
getConnectionMenuOptions(handleType, connectionType)。 - 调整图片、文本、视频、音频输出菜单顺序,使其符合 PRD。
- 给 resolver 增加单元测试。
为什么先做:
- 这是后续
+菜单复用的前置依赖。 - 改动范围小,容易验证。
- 即便后续暂停,也不会浪费工作。
风险点:
ConnectionDropMenu当前有 keyboard navigation,抽取后要保证options长度和顺序稳定。MenuAction目前定义在ConnectionDropMenu.tsx,抽出去后注意引用路径。
交付标准:
- 原有 handle 拖拽菜单仍可弹出。
- 原有菜单点击创建节点、自动连接不回归。
- 菜单顺序符合 PRD。
第 2 阶段:右侧 + 添加下游节点
优先级:P1
建议难度:M
建议耗时:1-2 天
任务:
- 新增
NodeQuickAddControls.tsx。 - 在
WorkflowCanvas.tsx里基于 selected/hover node 渲染右侧+。 - 为当前节点选择 primary output handle。
- 点击右侧
+后复用connectionDrop状态打开菜单。 - 复用现有
handleMenuSelect创建节点并自动连接。 - 创建后新节点选中。
为什么第二步做右侧:
- 右侧“下一步”是 PRD 中最关键的体验提升。
- 相比左侧输入,右侧输出语义更明确。
- 当前代码已有从 source handle 创建下游节点的完整逻辑,复用成本低。
建议先覆盖的节点:
imageInputpromptnanoBananagenerateVideogenerateAudiovideoInputaudioInput
首版可以暂缓的节点:
switchconditionalSwitchrouter的复杂泛型路径easeCurvegenerate3d
风险点:
- 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 天
任务:
- 为视频输出节点配置 primary output handle。
- 支持右侧添加:
videoStitchvideoTrimvideoFrameGraboutputoutputGallery
- 为音频输出节点支持:
generateVideovideoStitchoutput
为什么放在右侧基础之后:
- 可以复用第 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 天
任务:
- 在
NodeQuickAddControls中开启左侧+。 - 为当前节点选择 primary input handle。
- 点击左侧
+后以connectionType = "target"打开菜单。 - 创建输入节点后自动连接到当前节点。
为什么不和右侧一起做:
- 左侧输入涉及多输入节点,复杂度明显高于右侧。
- 先让右侧链路稳定,可以减少一次性改动范围。
首版默认 input handle 优先级:
imagetextvideoaudio3deaseCurve
建议覆盖:
nanoBanana左侧添加imageInput或prompt。generateVideo左侧添加imageInput或prompt。generateAudio左侧添加prompt。llmGenerate左侧添加prompt。
风险点:
nanoBanana、generateVideo这样的多输入节点默认选哪个输入会影响用户预期。- 如果默认 image,用户想加 prompt 时需要后续优化。
- 如果直接弹输入类型选择菜单,范围会变大。
交付标准:
- 左侧
+能创建输入节点并连接。 - 自动连接 edge 合法。
- 多输入节点至少有一个默认路径可用。
第 5 阶段:左侧多输入类型选择优化
优先级:P3
建议难度:L
建议耗时:1 天
任务:
- 当当前节点有多个输入类型时,先展示输入类型选择。
- 用户选择 Image/Text/Audio/Video 后,再打开对应输入候选菜单。
- 记住用户最近选择可作为后续优化,首版不需要。
为什么放后面:
- 这是体验增强,不是首版必要路径。
- 会增加一个新状态或新菜单组件,需要更多测试。
交付标准:
- Generate Image 左侧可明确选择添加 Image Input 或 Prompt。
- Generate Video 左侧可明确选择 Image、Text、Audio 输入。
第 6 阶段:图片生成节点参数底栏
优先级:P3
建议难度:M/L
建议耗时:1.5-2.5 天
任务:
- 修改
BaseNode,增加footerPanel。 - 新增
GenerationQuickSettingsBar.tsx。 - 新增
NodeCostBadge.tsx。 - 在
GenerateImageNode.tsx中把 Gemini 常用参数放到底栏。 - Advanced 继续复用
InlineParameterPanel。 - 未选中节点时底栏保持可读。
为什么在 + 后做:
- 参数底栏会影响节点布局,是更容易产生视觉回归的部分。
- 右侧
+能先解决“下一步怎么做”的主要问题。
风险点:
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 天
任务:
- 新增
NodeCostBadge.tsx。 - 基于
calculatePredictedCost([node])显示单节点成本。 - 对 unknown pricing 显示
--。 - 对未选模型显示
Select model。
为什么可以跟图片底栏一起做:
- PRD 要求参数底栏展示成本。
- 但它是独立小组件,适合单独测试。
风险点:
calculatePredictedCost对部分节点依赖完整 node data。- 外部 provider pricing 不完整,不要误导用户。
交付标准:
- Gemini 图片模型能显示成本。
- 外部 provider unknown 显示低干扰占位。
- 成本不可算不影响运行。
第 8 阶段:视频生成节点参数底栏
优先级:P4
建议难度:L
建议耗时:2-3 天
任务:
- 新增
modelParameterClassification.ts。 - 修改
ModelParameters.tsx支持:includeNamesexcludeNamescompact
- 在
GenerateVideoNode.tsx底栏展示:- Model
- quick schema 参数
- Cost
- Advanced
- 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 天
任务:
- 从
ModelParameters.tsx抽出 schema fetching/cache/defaults。 - 新增
useModelSchema(modelId, provider)。 GenerateVideoNode和ModelParameters共用 hook。
为什么暂缓:
- 它会让设计更干净,但不是 PRD 首版必须。
- 改动较深,容易让参数面板测试大面积变化。
适合时机:
- 视频底栏已经跑通。
- include/exclude 方案暴露出重复加载或默认值副作用。
16.4 推荐版本切分
MVP 版本
目标:最快交付用户能感知的主路径增强。
包含:
- 菜单 resolver 抽取。
- 右侧
+。 - 图片输出和文本输出下游推荐。
- 图片生成节点 Gemini 常用参数底栏。
不包含:
- 左侧多输入类型选择。
- 视频 schema quick 参数。
useModelSchema抽象。- 外部 provider 图片参数前置。
推荐原因:
- 覆盖最常见路径。
- 改动边界可控。
- 视觉和数据流风险适中。
完整 PRD 版本
包含:
- 菜单 resolver。
- 左右
+。 - 全媒体类型右侧推荐。
- 左侧输入推荐。
- 图片参数底栏。
- 视频参数底栏。
- 成本 badge。
- 完整测试和手动 QA。
推荐分 4-5 个 PR,不建议一次性提交。
16.5 依赖关系图
Connection menu resolver
├─ 原 ConnectionDropMenu 继续使用
└─ NodeQuickAddControls
├─ 右侧 + 添加下游节点
│ ├─ 图片输出路径
│ ├─ 文本输出路径
│ ├─ 视频输出路径
│ └─ 音频输出路径
└─ 左侧 + 添加输入节点
└─ 多输入类型选择
BaseNode footerPanel
├─ GenerateImageNode 参数底栏
│ └─ NodeCostBadge
└─ GenerateVideoNode 参数底栏
├─ modelParameterClassification
└─ ModelParameters include/exclude/compact
16.6 最容易踩坑的地方
-
不要复制第二份菜单数据。
- 菜单数据必须抽 resolver。
- 否则后续 handle 拖拽菜单和
+菜单会不一致。
-
不要一开始就重构
ModelParameters。- 视频参数底栏之前,可以先做 include/exclude 小扩展。
- 抽 hook 放到后续。
-
不要让底栏 absolute 覆盖媒体。
- 底栏必须参与节点布局。
- 推荐改
BaseNode增加footerPanel。
-
不要在首版追求所有节点类型完美支持。
- 优先图片、文本、视频、音频主路径。
switch、conditionalSwitch、router复杂路径可以后补。
-
不要把 unknown cost 显示成 0。
- 0 会误导用户。
- 应显示
--或Unknown。
-
不要破坏原 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 最终建议
最推荐的先后顺序:
ConnectionDropMenuresolver 抽取。- 右侧
+,先支持图片和文本输出。 - 右侧
+补视频和音频输出。 - 左侧
+,先做默认输入路径。 - 图片生成节点参数底栏。
- 单节点成本 badge。
- 视频生成节点参数底栏。
- 多输入类型选择和
useModelSchema优化。
这个顺序的好处是每一步都有独立价值,而且前一步基本都是后一步的基础;如果中途要停,也不会留下大面积半成品。
2026-04-29 更新:视频节点上传 / 加号 / 提示词框
状态:已完成,可进入手动测试。
本次实现把视频节点补齐到与图片节点一致的交互路径:
videoInput节点上方增加独立 Upload 按钮,上传行为从节点内部迁移到节点上方。videoInput上传完成后隐藏下方提示词框和左侧加号,只保留右侧加号,作为素材节点使用。- 上传后点击右侧加号可创建
generateVideo节点,并自动连接video -> video。 generateVideo节点下方显示固定 composer 框,包含提示词、模型入口、Cost、Advanced 和发送按钮。Generate Video Settings右侧旧面板在选中generateVideo时不再显示,避免与 composer 设置重复。- composer 的发送按钮会保存
inputPrompt并触发regenerateNode。
涉及文件:
src/utils/videoFile.tssrc/components/VideoNodeUploadOverlay.tsxsrc/components/nodes/VideoInputNode.tsxsrc/components/nodes/GenerateVideoNode.tsxsrc/components/ImageNodeComposerPanel.tsxsrc/components/WorkflowCanvas.tsxsrc/components/nodes/ControlPanel.tsxsrc/types/nodes.tssrc/components/__tests__/VideoNodeUploadOverlay.test.tsxsrc/components/__tests__/WorkflowCanvas.test.tsxsrc/components/__tests__/ImageNodeComposerPanel.test.tsxsrc/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 更新:空图像节点尝试入口
状态:已完成,可进入手动测试。
本次实现补齐第一个空图像节点中间的两个尝试入口:
- 空
imageInput节点中间显示尝试:、图生图、图片高清。 - 点击
图生图会打开本地图片选择器。 - 选择图片后会把图片上传到当前
imageInput节点。 - 上传成功后自动在右侧创建
Generate Image/nanoBanana节点。 - 自动连接当前图片节点
image输出到新生成图片节点的image输入。 图片高清目前先作为占位按钮展示,禁用状态,后续接入高清能力时再实现真实执行逻辑。
涉及文件:
src/components/nodes/ImageInputNode.tsxsrc/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 节点中:
- 空
videoInput节点中间显示尝试:、首尾帧生成视频、首帧生成视频。 - 点击
首帧生成视频会打开图片选择器,选择一张首帧图片。 - 点击
首尾帧生成视频会打开图片选择器,选择两张首尾帧图片。 - 上传帧图片后自动在右侧创建
generateVideo节点。 - 上传的帧图片会写入新
generateVideo节点的inputImages,后续生成视频时可作为帧素材使用。 - 原有上方
Upload上传真实视频文件的能力保持不变。
涉及文件:
src/components/nodes/VideoInputNode.tsxsrc/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 节点”修正为可见节点链路:
- 点击
首帧生成视频后,只允许选择视频文件;当前videoInput节点会变成首帧视频源节点,并在右侧创建generateVideo节点。 - 点击
首尾帧生成视频后,只允许选择两个视频文件;当前节点作为首帧,自动在下方创建尾帧视频源节点,并在右侧居中创建generateVideo节点。 - 自动创建
video -> video连线:首帧连接到生成视频节点;如果有尾帧,也自动连接到同一个生成视频节点。 WorkflowCanvas的浮动标题支持读取节点label,所以自动生成的源节点会显示首帧/尾帧。
涉及文件:
src/components/nodes/VideoInputNode.tsxsrc/components/WorkflowCanvas.tsxsrc/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 更新:首尾帧视频入口改为纯建链
本次根据反馈移除了“首尾帧生成视频”点击后的即时文件选择:
- 点击
首帧生成视频不再弹上传框,只把当前视频上传节点标记为首帧,并在右侧创建/连接Generate Video节点。 - 点击
首尾帧生成视频不再要求立刻选择两个视频,只创建一个空的尾帧视频上传节点,并把首帧、尾帧都连接到右侧Generate Video。 - 视频文件由用户后续分别在
首帧/尾帧视频节点上自行上传。
验证:
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 更新:首尾帧目标节点改为视频节点
本次调整“首尾帧生成视频”的建链目标:
首帧和尾帧仍然都是空的视频上传节点,用户后续自行上传视频。- 右侧被连接的目标也改为空的
videoInput视频节点,不再创建generateVideo节点。 - 自动连线保持
video -> video,形成首帧/尾帧 -> 视频节点的纯视频节点链路。
验证:
npm.cmd run test:run -- VideoInputNode.test.tsx WorkflowCanvas.test.tsx