Scope
本文单独研究当前项目的 workflow node 系统,不覆盖完整执行引擎和编辑器布局逻辑。关注点是:
- node 的共享数据模型
- 前端内建节点定义与注册
- 节点在编辑器里的渲染与属性配置
- 服务端如何把
node.type分派到具体执行实现 - 插件节点、客户端节点与
agent_run这类特殊节点的扩展路径
目标是让后续 AI agent 能在不重扫全仓的情况下新增节点、修改节点属性、接插件节点或排查节点执行问题。
Main Responsibilities
- 用统一的
WorkflowNode数据结构承载所有节点实例。 - 用
NodeTypeDefinition描述节点类型的编辑器元数据。 - 前端根据 definition 决定:
- 节点分类与搜索
- icon / label / description
- 属性面板表单结构
- handles、动态 handles、默认 outputs
- 特殊渲染或 custom view
- 服务端根据
node.type和node.data决定:- 内建节点执行函数
- 插件节点执行方式
- 是否要转给客户端执行
Core Data Model
共享 node 实例定义位于 packages/shared/src/types/workflow.ts:
WorkflowNode
- id
- type
- label
- position
- data: Record<string, unknown>
- inputFields?
- outputs?
- nodeState?
- breakpoint?
- nodeColor?
- composite?
关键结论:
type是字符串,不是 enum。- 真正的节点业务参数基本都放在
data。 inputFields/outputs既出现在 node 顶层,也可能在编辑器快照阶段复制到data中做执行态预览。composite用于 loop / sub workflow 这类复合节点的子节点关系、隐藏节点、scope boundary 等。
这套设计本质上是:
运行态实例
WorkflowNode保持宽松,编辑器能力和执行能力再分别通过 definition 与 dispatch 补上。
Node Type Definition Model
节点类型元数据同样定义在 packages/shared/src/types/workflow.ts 中,核心是 NodeTypeDefinition 相关字段族。
可验证的能力包括:
typelabelcategoryicondescriptionpropertiesoutputsallowInputFieldsallowedInputFieldTypeshandlessingletoncustomView
NodeProperty 支撑的属性类型包括:
texttextareanumberselectcheckboxcodeconditionsarrayoutput_fieldsagentsqliteknowledge-base
因此 node 系统的本质不是“每个节点写死一套表单”,而是:
NodeTypeDefinition驱动编辑器生成大部分通用表单- 少数节点再用 custom view 或额外逻辑补足
Frontend Definition Registry
前端 registry 位于 packages/web/src/lib/workflow-nodes/registry.ts。
主流程:
definitions/*
-> registry.ts: allNodeDefinitions
-> getAllNodeDefinitions()
-> getNodeDefinitionsByCategory()
-> getNodeDefinition(type)
-> i18n.ts 进行本地化包装
-> 组件侧使用 localized definitions
内建节点来源:
flowControlNodesaiNodesinteractionNodesdisplayNodesutilsNodesstringNodessqliteNodesknowledgeBaseNodesLOCAL_BRIDGE_WORKFLOW_NODES
插件节点来源:
registerPluginNodeDefinitions(nodes)- 与内建定义合并后参与查找与渲染
Built-in Node Categories
从 definitions/* 可以确认当前节点体系至少包含这些大类:
- Flow Control
startendrun_coderun_pythontoastswitchset_variableget_variabledelete_variablelooploop_breaksub_workflow
- AI
agent_run
- Interaction
alertpromptform
- Display
table_displaygallery_previewcode_rendermarkdownsticky_note
- Utilities / String / SQLite / Knowledge Base
这说明 node 分类主要是编辑器导航语义,不直接影响服务端 dispatch。
Definition Example
packages/web/src/lib/workflow-nodes/definitions/ai.ts 中的 agent_run 很典型:
- 用
properties定义 agent、prompt、cwd、additionalDirectories、permissionMode - 用
outputs定义result与usage
packages/web/src/lib/workflow-nodes/definitions/flow-control.ts 中的 switch 和 loop 更能体现 definition 的表达力:
switchproperties里有conditionshandles.dynamicSource根据conditions动态生成 source handles
set_variable- 属性变更后 outputs 会被派生更新
loop- 使用共享常量定义 loop root/body/break 的复合关系
Frontend Rendering And Editing
节点渲染主入口在 packages/web/src/components/workflow/workflow-node.tsx。
关键机制:
- 根据
nodeData.nodeType || type找 definition - 使用
useLocalizedNodeDefinition()得到翻译后的 label / category / properties - 根据 definition 决定:
- handles
- 动态 source handles
- custom view
- 日志展示方式
- property mode / variable input / preset 输出展示
如果 definition 声明了插件 custom view:
plugin-workflow-custom-view路径会接管一部分节点体渲染
属性编辑主入口在 packages/web/src/components/workflow/workflow-properties-panel.tsx。
关键点:
- 面板本身依赖
useLocalizedNodeDefinition(node.type) - 大多数字段由
properties描述驱动 - 某些节点会有额外派生逻辑
- 例如
set_variable的 outputs 由createSetVariableOutputs()根据变量路径生成 - JSON preset 会把执行结果回填为可复用输出模板
- 例如
Property View Runtime Sizing
property view 模式下,节点的真实显示高度不是只由持久化的 WorkflowNode.data.nodeHeight 决定。
关键链路:
workflow-canvas.tsx
-> useCanvasData()
-> rfNodes: width / height / initialWidth / initialHeight / measured / style
-> ReactFlow nodes
-> workflow-node.tsx: WorkflowNodeComponent()
-> measuredPropertyHeight
-> displayNodeHeight
-> workflow:update-node-runtime-size
-> workflow-canvas.tsx: runtimeNodeSizesRef + canvasNodes
要点:
use-workflow-canvas-data.ts会基于getWorkflowNodeSize()生成 React Flow node 的初始width、height、initialWidth、initialHeight、measured和style。workflow-node.tsx在 property view 下会测量属性面板真实内容高度,写入组件本地的measuredPropertyHeight,并用它生成displayNodeHeight。- 仅调用
useUpdateNodeInternals()不足以让所有下游逻辑看到真实高度;minimap、导出、分组 overlay 等逻辑会读取 React Flow node dimensions 或本地canvasNodes。 - 因此 property view 的动态高度需要同步为运行时尺寸,而不是写回 workflow 持久数据。用户手动 resize 仍通过
nodeWidth/nodeHeight持久化。
已验证的风险点:
- 画布导出:
use-workflow-canvas-export.ts需要使用 React Flow instance 的getNodesBounds(),并合并实际 DOM bounds,避免只按静态 node size 裁剪图片。 - minimap:React Flow minimap 读取节点 dimensions;如果
canvasNodes没有运行时高度,minimap 会显示过小。 - 合并成组:
use-workflow-group-operations.ts创建 group 时计算 bounds;workflow-group-node.tsx渲染 overlay 时还会根据childNodes二次计算 bounds。两处都不能只依赖getWorkflowNodeSize()。 - 组 overlay 数据源在
workflow-canvas.tsx的groupOverlayItems。这里必须优先使用当前canvasNodes的width/height/measured,否则 property view 高节点会超出蓝色组背景。
调试点:
[WorkflowGroupBoundsDebug] merge request
-> 合并点击瞬间从 DOM / runtime cache 得到的 node bounds
[WorkflowGroupBoundsDebug] create group bounds
-> use-workflow-group-operations.ts 里创建 group 时算出的持久 bounds
[WorkflowGroupBoundsDebug] render group bounds
-> workflow-group-node.tsx 最终渲染 overlay 时使用的 bounds 和 childNodes
排查时优先比较 create group bounds.bounds 与 render group bounds.renderedBounds。如果创建时正确但渲染时变小,问题通常在 groupOverlayItems 或 WorkflowGroupOverlay 二次计算;如果创建时就偏小,问题通常在合并事件传入的 DOM/runtime bounds。
Dynamic Handles And Graph Semantics
动态 handles 是 node 系统和画布系统的关键接缝。
已验证的调用链:
definition.handles.dynamicSource
-> use-workflow-edge-operations.ts
-> workflow-node-size.ts
-> workflow-node.tsx
-> use-workflow-editor-state.ts
用途:
- 根据属性数组长度动态生成 source handles
- 典型节点:
switch - 同时影响:
- 连接数量和 handle id
- 画布尺寸估算
- legacy sourceHandle 兼容与归一化
这意味着:
修改 definition 的 handles 不是纯 UI 改动,会连带影响边连接语义和已有 workflow 兼容逻辑。
Localized Definition Layer
前端并不直接把 registry.ts 中的原始定义丢给组件。
packages/web/src/lib/workflow-nodes/i18n.ts 负责:
- 翻译
label - 翻译
category - 翻译
description - 翻译 properties / array fields / options / handles 文本
所以:
- 原始 definitions 里可以存 i18n key
- 组件里应优先消费 localized hooks,而不是直接读 raw registry
Plugin Node Path
插件节点的服务端主入口在 packages/server/src/services/plugin.ts。
已确认的能力:
- 插件 manifest 可声明
workflowNodes - 可从
workflow.js/ CommonJS workflow module 加载节点定义 - 可为节点类型绑定 handler
getWorkflowNodes(pluginId, locale?)返回节点定义给前端getWorkflowNodeDefinitionByType(nodeType)允许执行前做 definition 查询canExecuteWorkflowNode(nodeType)判断该 type 是否由插件运行时支持requiresClientExecution(nodeType)决定是否必须走客户端
前端插件节点接入链:
workflow-editor.tsx
-> 拉取插件节点定义
-> registerPluginNodeDefinitions(allNodes)
-> registry 合并
-> 画布/节点选择器/属性面板可见
所以插件节点要同时满足两件事:
- 前端拿到 definition,才能编辑和显示
- 服务端拿到 handler 或执行声明,才能运行
Client Node Bridge
客户端节点桥接由两部分组成:
- 识别:
packages/server/src/services/execution-node-helpers.tsisClientPluginNode(node)getClientPluginId(node)
- 执行桥接:
packages/server/src/services/client-node-manager.ts
执行链:
ExecutionManager.dispatchNode()
-> isClientPluginNode(node) / pluginService.requiresClientExecution(node.type)
-> executeClientNode()
-> clientNodeManager.request(...)
-> WS channel 'workflow:client-node'
-> web use-workflow-editor-execution.ts
-> electronAPI.clientPlugins.executeNode(...)
-> client_node_response
-> ClientNodeManager.handleResponse()
关键约束:
- 这类节点必须依赖具体客户端连接
- 有 5 分钟超时与 30 秒断线重连宽限
- 如果客户端掉线且未恢复,节点执行直接失败
Server Dispatch Model
服务端真正把 node.type 映射到实现的是 packages/server/src/services/execution-manager.ts。
主流程:
executeNode()
-> 解析变量 / dry-run / stepInput
-> dispatchNode()
-> switch(node.type)
-> 内建节点函数 or 插件节点 or client node
-> 写回 step.output / session.context / execution data
内建映射是显式 switch:
start/endsqlite_*kb_*run_code/run_pythonswitchvariable_aggregateset_variable/get_variable/delete_variablesub_workflowloopagent_runalert/prompt/form
默认分支逻辑:
if client plugin node
-> executeClientNode()
else if pluginService.canExecuteWorkflowNode(node.type)
-> 插件执行 or 客户端插件执行
else
-> throw Unsupported node type
这意味着当前 node 系统是“三段式执行模型”:
- 内建
switch - 服务端插件 handler
- 客户端插件桥接
Special Case: agent_run
agent_run 是最特殊的内建节点之一。
前端 definition:
- 在
ai.ts里声明属性和输出
服务端执行:
execution-manager.ts->executeAgentRun()packages/server/src/services/execution-agent-runner.ts
其特点:
- 并不是普通 plugin node
- 会解析 agent preset / runtime / permission mode / sandbox dirs
- 最终调用 agent runtime 执行 prompt
- 输出包含
result、usage和 runtime 元信息
已确认的风险:
resolveWorkflowAgentWorkspaceId()当前直接取workspaceService.getAll()[0]?.id ?? 'default'- 说明
agent_run的 workspace 归属依然不是从 workflow session 明确传入,而是依赖全局第一个 workspace
这和前面 execution scope 的研究结论是同一类设计缺口。
Files To Read Next
packages/shared/src/types/workflow.ts- node 实例模型与 definition 类型
packages/web/src/lib/workflow-nodes/registry.ts- 内建/插件 definition 合并入口
packages/web/src/lib/workflow-nodes/definitions/*- 新增节点时首先参考
packages/web/src/components/workflow/workflow-node.tsx- 节点渲染与 definition 消费方式
packages/web/src/components/workflow/workflow-properties-panel.tsx- 属性面板与特殊派生逻辑
packages/server/src/services/execution-manager.tsnode.type到执行实现的最终 dispatch
packages/server/src/services/plugin.ts- 插件节点定义加载与 handler 执行
packages/server/src/services/client-node-manager.ts- 客户端节点请求/响应桥接
packages/server/src/services/execution-agent-runner.tsagent_run的特殊执行路径
Open Questions / Risks
- definition 与执行实现是分离维护的。
- 新增内建节点时,如果只加前端 definition 不加
dispatchNode(),运行时会直接Unsupported node type。
- 新增内建节点时,如果只加前端 definition 不加
- 插件节点存在前后端双注册要求。
- 只注册前端 definition 会“能看到不能执行”。
- 只注册服务端 handler 会“能执行但编辑器不可见”。
- 客户端节点依赖具体连接。
- 不适合无 UI 或纯后台调度场景。
agent_run仍有 workspace 归属不严格的问题。- 动态 handles 与 composite 节点改动有较高兼容性风险。
- 会影响 edge id、画布尺寸、legacy handle 迁移和执行可达性。