zh-aiot-workflow-canvas
v0.7.2
Published
AIoT 工作流画布组件库:基于 Vue Flow 的 Vue3 可视化编排画布,可独立调试并发布到 npm,供 zh-aiot-webui 等项目引用
Maintainers
Readme
zh-aiot-workflow-canvas
AIoT 工作流画布组件库 —— 基于 Vue 3 + TypeScript + Vue Flow(@vue-flow/core) 的可视化编排画布,对标 Dify Workflow Builder 的交互层。
作为独立包开发调试,可发布到 npm 公共仓库后供 zh-aiot-webui(Vue 3 + Vite + pnpm)等项目引用。
引擎只做节点/连线/交互;业务节点种类、工具列表、导出至 LiteFlow graphJson 均由本库在画布之上按需扩展。
一、目录结构
zh-aiot-workflow-canvas/
├── package.json # 包名 zh-aiot-workflow-canvas
├── vite.config.ts # Vite 库模式构建(ESM + UMD + d.ts + style.css)
├── tsconfig.json
├── index.html # 本地调试入口(开发服务器根页面)
├── src/ # 库源码(打包只含此目录)
│ ├── index.ts # 统一出口 + Vue 插件 install + 全局样式引入
│ ├── types.ts # 业务类型:WorkflowNode / WorkflowNodeData / Tool 等
│ ├── presets.ts # defaultNodeTypes + buildWorkflowNode() 便捷工厂
│ ├── kinds.ts # 节点种类元数据(开始/条件分支/循环/迭代/工具调用/结束/注释)
│ ├── tools.ts # 工具列表 mock(HTTP / Redis / MQ / 设备能力等)
│ ├── params.ts # 节点参数与变量引用模型(输出/上游/visibleWhen)
│ ├── liteflow.ts # 扁平画布 → LiteFlow graphJson 导出器
│ ├── history.ts # 撤销/重做历史栈(past/future + 连续编辑合并)
│ ├── api.ts # provide/inject key(WorkflowCanvasApi)
│ └── components/
│ ├── WorkflowCanvas.vue # 画布封装:v-model / 模式 / 快捷键 / 导出
│ ├── WorkflowNode.vue # Dify 式节点卡片(图标+标题+chevron+变量行)
│ ├── NodeDrawer.vue # Dify 式右侧编辑面板(400px,点节点打开)
│ ├── VarChip.vue # 变量引用 chip({x} 图标 + 节点名/字段名,隐藏 {{}} 代码)
│ ├── AnnotationNode.vue # 注释节点(无端口,双击编辑)
│ └── WorkflowToolbar.vue # Dify 式左侧操作栏(节点/注释/指针/手型)
└── playground/ # 独立调试沙盒(不参与打包)
├── main.ts
└── App.vue # 示例 AIoT 链路 + 导出验证二、本地调试
pnpm install # 安装依赖
pnpm dev # 启动 playground(默认 http://localhost:5174)
pnpm type-check # vue-tsc 类型检查
pnpm build # 库构建,产物输出 dist/调试要点:
- playground 从
../src直接引源码,改动即时热更新,与「消费方引用构建产物」隔离; - 新增节点组件 → 在
src/presets.ts的defaultNodeTypes注册 → playground 直接使用验证; - 节点
data遵循WorkflowNodeData(kind / label / sublabel / liteflow / tool 等),运行态通过改data.status即可回显「待运行/运行中/成功/失败」。
三、构建产物(dist/)
| 文件 | 说明 |
| --- | --- |
| index.js | ESM,供 Vite 工程按 import 引入 |
| index.umd.cjs | UMD,浏览器 <script> 直引场景 |
| index.d.ts | 类型声明,随包发布 |
| style.css | 已内联 @vue-flow/core 基础样式 + 组件样式,消费方只引这一份 CSS |
vue、@vue-flow/core、@iconify/vue 均作为外部依赖不打进产物,由消费方自行安装,保证单一实例。
图标说明:工具 / 节点的
icon形如ri:router-line时走 Iconify 渲染。库本身不内置图标数据, 消费方需保证图标可用 —— 外网环境可直接用@iconify/vue的在线 API;内网环境请用addIcon()/addCollection()离线注册(如配合已安装的@iconify-icons/ri按需导入), 否则图标位置为空白(不影响颜色与其余渲染)。非 Iconify 形态(emoji / 单字)按文字渲染。
四、消费方用法(以 zh-aiot-webui 为例)
安装
# 在 zh-aiot-webui 下添加依赖(需 Vue 3.5+)
pnpm add zh-aiot-workflow-canvas
# peerDependency:图标组件(^4.1.0 || ^5.0.0)
pnpm add @iconify/vue
# 可选:如需背景网格 / 缩放控制 / 小地图等装饰组件(独立子包,按需装)
pnpm add @vue-flow/background @vue-flow/controls @vue-flow/minimap基础画布
<script setup>
import { ref } from 'vue'
import { WorkflowCanvas } from 'zh-aiot-workflow-canvas'
import 'zh-aiot-workflow-canvas/style.css'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
const nodes = ref([])
const edges = ref([])
</script>
<template>
<WorkflowCanvas v-model:nodes="nodes" v-model:edges="edges" height="640px">
<Background />
<Controls />
</WorkflowCanvas>
</template>受控工具 + 自定义工具加载
<script setup>
import { ref } from 'vue'
import { WorkflowCanvas } from 'zh-aiot-workflow-canvas'
import 'zh-aiot-workflow-canvas/style.css'
const tool = ref('select') // 'select' | 'hand'
async function loadTools() {
const res = await fetch('/api/zh-aiot/workflow/tools')
return await res.json()
}
</script>
<template>
<WorkflowCanvas
v-model:nodes="nodes"
v-model:edges="edges"
v-model:tool="tool"
:tool-loader="loadTools"
height="640px"
/>
</template>导出 LiteFlow graphJson(zh-aiot-server 可解析)
<script setup>
import { ref } from 'vue'
import { WorkflowCanvas, serializeLiteflowGraph } from 'zh-aiot-workflow-canvas'
const canvasRef = ref()
function handleExport() {
const result = canvasRef.value.exportLiteflow()
if (result.ok) {
console.log(serializeLiteflowGraph(result.graph))
// 或 POST 到后端
} else {
console.error(result.message)
}
}
</script>
<template>
<WorkflowCanvas ref="canvasRef" v-model:nodes="nodes" v-model:edges="edges" />
<button @click="handleExport">导出 LiteFlow</button>
</template>也可以全量注册:
import ZhWorkflowCanvas from 'zh-aiot-workflow-canvas'
app.use(ZhWorkflowCanvas) // 全局可用 <WorkflowCanvas />五、组件 API
<WorkflowCanvas> Props
| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| nodes | Node[] | [] | 受控节点列表(支持 v-model:nodes) |
| edges | Edge[] | [] | 受控连线列表(支持 v-model:edges) |
| nodeTypes | NodeTypesObject | {} | 扩展/覆盖节点类型 |
| height | number \| string | '100%' | 画布高度 |
| fitView | boolean | true | 初始自适应视图 |
| id | string | 自动生成 | 多实例时区分 store |
| tool | 'select' \| 'hand' | 'select' | 指针/手型(受控) |
| toolLoader | ToolListLoader | defaultToolLoader | 工具列表加载器;默认提供 HTTP、Redis、MQTT,可由内部接口替换 |
<WorkflowCanvas> Slots
| 插槽 | 作用域参数 | 说明 |
| --- | --- | --- |
| default | — | Vue Flow 的背景、控制器、缩略图等装饰内容 |
| panel | { node, close } | 接管当前节点的右侧编辑面板;未传时仍渲染内置 NodePanel |
panel 只接管渲染,面板状态仍由画布管理:节点单击和新增节点会打开面板,Esc、画布空白单击及 close() 会关闭面板,切换节点会更新 node。宿主可对业务节点使用自定义面板,并让其余节点继续使用包内导出的 NodePanel:
<script setup lang="ts">
import {
NodePanel,
WorkflowCanvas,
} from 'zh-aiot-workflow-canvas'
</script>
<template>
<WorkflowCanvas v-model:nodes="nodes" v-model:edges="edges">
<template #panel="{ node, close }">
<DeviceNodePanel
v-if="isDeviceNode(node)"
:node="node"
@close="close"
/>
<NodePanel v-else :node="node" @close="close" />
</template>
</WorkflowCanvas>
</template>插槽组件位于 WorkflowCanvas 的 provide 上下文内,可以在 DeviceNodePanel 自身的 setup 中直接 inject(WORKFLOW_CANVAS_API),再调用 updateNodeData(node.id, patch) 写回;因此 NodePanel 在插槽中也仍可使用工具选择、变量插入和输出变量等完整功能。
<WorkflowCanvas> 注入 API(内部组件通过 inject(WORKFLOW_CANVAS_API) 使用)
| 方法 | 说明 |
| --- | --- |
| getTool() | 当前指针/手型 |
| setTool(tool) | 切换模式 |
| updateNodeData(id, patch) | 更新节点 data:只覆盖 patch 出现的顶层键,对象/数组整段替换;纳入 undo/redo,并触发受控 update:nodes 回传 |
| removeNode(id) | 删除节点(连同挂载连线一并清理) |
| loadTools() | 拉取工具列表 |
| getNodeOutputs(id) | 某节点的输出参数(供下游引用) |
| getUpstreamOutputs(id) | 某节点可引用的上游节点输出分组(变量引用下拉数据源) |
| exportLiteflow() | 导出 LiteFlow graphJson |
<WorkflowCanvas> Expose
| 方法 | 说明 |
| --- | --- |
| snapshot(): { nodes, edges } | 清掉运行时 key 的节点/连线快照 |
| exportLiteflow(): WorkflowLiteflowExport | 导出 LiteFlow graphJson(含校验) |
| addNodeByKind({ kind, tool? }) | 编程式添加节点 |
| addAnnotation() | 编程式添加注释节点 |
| setTool(tool) | 切换模式 |
| removeNode(id) | 删除节点 + 挂载连线 |
| undo() / redo() | 撤销 / 重做(增删节点、连线、拖拽、删除、抽屉内编辑均可回退) |
自定义面板样式约定
- 定位容器是画布根
.wf-canvas-root(position: relative)。 - 面板建议使用
position: absolute; top: 0; right: 0; bottom: 0; width: min(400px, 100%); z-index: 35;内置工具栏.wf-toolbar的z-index为30。 - 画布外层保留
wf-panel-slide进出场动画(transform+opacity,0.18s ease),单根节点的自定义面板可直接复用;宿主也可以在面板内部实现自己的动画。
快捷键(全局)
| 快捷键 | 功能 |
| --- | --- |
| V | 切换到指针模式 |
| H | 切换到手型模式 |
| N | 打开/收起「添加节点」面板 |
| A | 添加注释节点 |
| Esc | 收起节点面板 |
| Delete / Backspace | 删除选中节点/连线(可撤销) |
| ⌘/Ctrl+Z | 撤销(焦点在输入框时也可回退文本编辑) |
| ⌘/Ctrl+⇧+Z(或 Ctrl+Y) | 重做 |
字母/删除快捷键在焦点位于输入框时不拦截;撤销/重做任意焦点下均生效。抽屉内连续击键自动合并为一个撤销步。
内置节点类型(defaultNodeTypes)
| type | 组件 | 说明 |
| --- | --- | --- |
| workflow | WorkflowNode.vue | 通用工作流节点,自动按 data.kind 渲染不同形态与端口 |
| annotation | AnnotationNode.vue | 注释节点(无端口、双击编辑) |
六、节点种类(画布与导出)
| kind | 中文 | 端口特征 | LiteFlow 后端类型 |
| --- | --- | --- | --- |
| start | 用户输入 | 仅 source | 不导出(触发锚点,输入字段即 request.*) |
| timer | 定时触发器 | 仅 source | timer(config.cron) |
| condition | 条件分支 | source×2(真/假) | if |
| loop | 循环 | source×2(body/done)、target×1(back) | for(CONST) |
| iteration | 迭代 | source×2(body/done)、target×1(back) | for(LIST) |
| tool | 工具调用 | 有 source + target | 不导出(类型由选定工具决定) |
| end | 结束返回 | 仅 target | return |
| annotation | 注释 | 无端口 | 不导出 |
触发入口:两种触发方式互斥(同一流程二选一),由注册表 excludes 声明:
- 用户输入(
start):用户填写输入字段后触发,输入字段即下游可引用的request.*输出;不落库,仅作主链锚点;同画布区域最多一个; - 定时触发器(
timer):按 Cron 表达式到点触发,导出为后端timer节点(config.cron必填);同一流程可定义多个 —— 多个 timer 指向同一下游即可共用流程体,导出时各 timer 声明(含 cron)统一置于链首;可配置触发变量(名称 / 类型 / 值)。变量结构(名 + 类型)全流程定时器共用一份:任一定时器增删变量、改名、改类型会同步到所有定时器(改名联动重写全图引用);各定时器只为这套结构填写各自的值,新建定时器自动继承当前结构。导出为config.payload.<变量>(值按类型转换),运行时服务端注入节点输出data.payload,下游引用路径context.node.<id>.data.payload.<变量>。
工具调用节点可在节点内展开选择工具,或在「添加节点」面板中点击「工具调用」后在右侧侧边列选择工具。
节点卡片与编辑面板(Dify 式)
画布节点卡片(WorkflowNode.vue):白底圆角卡片,仅做展示——
- 头部:彩色图标块 + 节点标题 + 运行状态点 +
›chevron; - 描述行:
data.desc,最多 2 行截断; - 变量行:灰底圆角,
label → value;含{{nodeId.field}}的引用不显示模板代码,而是渲染为变量 chip({x}图标 + 节点名/字段名,主题蓝),普通文本与 chip 混排; - 条件分支节点:卡片上按
IF [变量chip] 运算符 [值chip]逐行展示结构化条件; - 源口内嵌
+,hover 放大。
右侧编辑面板(NodeDrawer.vue,点击节点打开,400px):
- 头部:图标 + 直接可编辑的节点标题输入框 + 删除节点 / 关闭按钮;
- 描述区:备注输入,实时回显到节点卡片描述行;
- 工具调用节点:工具分组下拉切换;
- 用户输入节点(start):输入字段编辑(变量名 + 类型下拉 string/number/boolean/object/array/json + 删除 + 添加);
- 定时触发器(timer):Cron 配置控件 —— 常用预设一键填充、秒/分/时/日/月/周六段分框编辑、表达式实时校验、下次执行时间预览(本地时区);导出时同样做 Cron 格式校验;另有触发变量编辑(名称 / 类型 / 值,值在流程内定义,导出为
config.payload,改名联动重写下游引用); - 条件分支节点(Dify 式 IF 结构化编辑器):每条条件 = 左值变量选择(上游输出 chip)+ 运算符下拉(等于/不等于/大于/包含/为空…12 种)+ 右值(字面量输入或
{x}插入变量,失焦态显示 chip、聚焦态才编辑文本);支持「添加条件」、删除单条、多条件 AND/OR 逻辑切换。数据写入config.condition.items并同步生成config.expression; - 参数区:label + 输入框 +
{x}变量按钮(内嵌输入框右上角),点击展开上游节点输出下拉,插入{{nodeId.field}}引用 token; - 底部输出变量折叠区:计数徽章 + 旋转 chevron,展开显示 name/type 行与引用语法提示。
节点参数与变量引用
- 变量引用以 chip 呈现:画布卡片和面板里所有
{{nodeId.field}}都经toValueSegments()解析为「节点名 / 字段名」chip(VarChip.vue),用户看不到模板代码;原始语法仅存于数据与悬浮提示; - 开始节点(用户输入):可编辑输入参数(名称 / 类型),输入参数即该节点的输出,供下游引用;
- 工具节点:按工具
params自动渲染参数编辑区(select / textarea / number / text),支持visibleWhen按取值动态显隐(如 redis 按数据类型展开不同输入); - 条件节点:结构化 IF 条件(
ConditionItem { variable, op, value }),非旧版裸表达式文本框;导出时自动过滤未完成(左值为空)的条件项; - 所有节点:编辑面板参数输入框内嵌
{x}按钮插入上游输出引用; - 输出参数:编辑面板底部输出变量折叠区(计数徽章 toggle),展开提示下游可引用字段。
可复用默认工具
HTTP、Redis、MQTT 是组件库的正式项目级定义,可由引用方单独选用,也可将完整加载器直接传给画布:
import {
defaultToolList,
defaultToolLoader,
httpRequestTool,
redisOperationTool,
mqttPublishTool,
} from 'zh-aiot-workflow-canvas'
const selectedTools = [httpRequestTool, mqttPublishTool]<WorkflowCanvas :tool-loader="defaultToolLoader" />mockToolList 仍用于 Playground,内部复用 defaultToolList,不会维护第二份定义。
| 工具 | 说明 | 动态输入 | | --- | --- | --- | | HTTP 请求 | GET/POST/PUT/DELETE;Body 支持 none、form-data、x-www-form-urlencoded、JSON、raw、binary | method / url / headers / query / bodyType / bodyParams / bodyTemplate / timeoutMs | | Redis 读取 | 按数据类型读取键值 | dataType → 展开 key / field / start/stop / min/max / member | | Redis 写入 | 按数据类型写入键值 | dataType → 展开 key / value / field / fieldValue / pushMode / listValue / score / zsetValue / ttlSeconds | | MQ 消息生成 | 向消息队列投递消息 | topic / tag / payloadType / payload / delayMs | | 查询设备状态 | 设备在线与属性 | deviceId | | 设备开关 | 下发开/关 | deviceId / action | | 查询历史数据 | 按时间范围查询 | deviceId / start / end | | 查询天气 | 按经纬度查天气 | longitude / latitude | | AI 规则判定 | 大模型判定 | prompt |
七、LiteFlow 导出器
import { buildLiteflowGraph, serializeLiteflowGraph, LiteflowExportError } from 'zh-aiot-workflow-canvas'buildLiteflowGraph(nodes, edges):将扁平 nodes/edges 编译为{ root: { children: [...] } }嵌套树;- 支持条件分支链、循环/迭代 closure、注释与开始节点自动跳过;触发节点(用户输入 start / 定时触发器 timer)作为流程入口,多个定时触发器的声明统一置于链首、共享流程体只编译一次;
- 开始节点直接显示和导出
request.<变量名>;定时触发器变量(含值)导出为config.payload.<变量>(按类型转换),下游引用路径为context.node.<id>.data.payload.<变量>;其他节点使用固定的code / message / data / success输出结构,业务字段位于data; - 画布内部变量引用使用
{{nodeId.field}};导出时自动转换为服务端路径:开始输入为request.field,其他节点输出为context.node.nodeId.*,循环局部变量为context.loop.field,普通模板转换为${...}; - 条件项导出为服务端读取的
leftPath/operator/rightType/rightValue;结束节点的单变量引用导出为returnPath,字面量或混合模板导出为returnValue,并附带returnLabel(当前仅支持一个返回值);定时触发器导出前校验 Cron 格式(6 段 Quartz/Spring 风格,core/cron提供parseCron / validateCron / nextCronExecutions可独立复用); - 节点
name和输出别名exports一并写入 graphJson,节点类型限定为服务端内置类型及消费方通过声明合并 +registerWorkflowTools注册的自定义类型; - 若存在游离节点或端口未连接,会抛出
LiteflowExportError(含中文错误描述)。
八、发布到 npm
# 1) 发布前按需调整 package.json 中的 "version"(遵循 semver)
# 2) 构建(发布内容为 dist/,sourcemap 已排除)
pnpm build
# 3) 发布(需 npmjs 账号,首次先 npm login;publishConfig 已指向 registry.npmjs.org)
npm publish提示:
zh-aiot-webui本地联调阶段可用pnpm link(在 webui 目录pnpm link zh-aiot-workflow-canvas)或直接"zh-aiot-workflow-canvas": "file:../zh-aiot-workflow-canvas"指向本目录,无需发布即可联调。
九、路线图(建议)
- [x] Dify 式左侧操作栏 + 节点种类面板 + 指针/手型模式 + 快捷键
- [x] 多形态业务节点(用户输入/定时触发器/条件分支/循环/迭代/工具调用/结束/注释)
- [x] 工具调用:支持注入 loader 获取内部接口工具列表(默认 mock)
- [x] 一键导出 zh-aiot-server LiteFlow 可解析的 graphJson
- [x] 节点参数编辑 + 变量引用(
{{nodeId.field}}):所有节点可引用上游输出;开始节点可编辑输入参数 - [x] 双触发入口互斥(二选一):用户输入(start)/ 定时触发器(timer,Cron,可定义多个)
- [x] 工具调用:支持注入 loader 获取内部接口工具列表(默认 mock,含 HTTP 请求、Redis 操作、MQTT 消息推送)
- [x] 工具展开改为侧边列(节点面板右侧新开一列)
- [x] 节点输出参数 chips(提示下游可引用字段)
- [x] Dify 式节点卡片(图标+标题+chevron+描述行+变量行,含
{x}引用高亮) - [x] Dify 式右侧编辑面板(可编辑标题 + 删除节点 + 描述/工具/输入字段/参数/输出变量折叠区)
- [x]
removeNodeAPI:删除节点并清理挂载连线 - [x] 撤销/重做(history 栈 + ⌘/Ctrl+Z、⇧+Z 快捷键 + 浮动按钮,覆盖增删/连线/拖拽/编辑)
- [x] 条件分支 Dify 式 IF 结构化编辑器(变量 chip + 运算符 + 值,AND/OR,多条件)
- [x] 参数引用全部以变量 chip 呈现(VarChip,卡片/面板隐藏 {{}} 代码)
- [ ] 接入后端 DSL:从工作流 JSON/YAML 还原 nodes/edges(反向导入)
- [ ] 运行态回显:SSE/WebSocket 驱动
data.status - [ ] 撤销/重做(可引入 zundo/自定义 history)
- [ ] 自动布局 / 校验 / 缩略图美化
自定义节点类型扩展
liteflow.type 使用 string,可直接填写后端支持的类型,无需声明合并或通过注册获得类型许可。画布不检查类型白名单。
1. 创建自定义工具描述符
import type { ToolDescriptor } from 'zh-aiot-workflow-canvas'
export const customDeviceTool: ToolDescriptor = {
id: 'custom-device',
name: 'Custom Device',
description: 'A custom device tool',
category: 'Custom',
color: '#10b981',
icon: 'ri:device-line',
params: [
{ key: 'deviceId', label: 'Device ID', type: 'text', required: true, allowVariable: true },
{ key: 'command', label: 'Command', type: 'textarea', required: true, allowVariable: true },
],
outputs: [
{ name: 'success', label: 'Success', type: 'boolean' },
{ name: 'data', label: 'Response Data', type: 'json' },
],
liteflow: {
type: 'custom_device', // 自定义节点类型
config: { deviceId: '', command: '' }
}
}2. 注册自定义工具
import { registerWorkflowTools } from 'zh-aiot-workflow-canvas'
import { customDeviceTool } from './custom-tools'
registerWorkflowTools([customDeviceTool])注册用于导入时恢复工具描述;通过 toolLoader 提供工具选择列表。节点自身携带 liteflow.type/config 时,无需注册即可导出。
内置节点类型
以下节点类型是内置的,无需注册即可使用:
timer- 定时触发器http- HTTP 请求mqtt- MQTT 消息redis- Redis 操作if- 条件分支switch- 多条件分支for- 循环while- 条件循环workflow- 子流程调用return- 流程返回
验证机制
画布仍会检查节点是否配置 liteflow.type,并校验流程结构、必填参数和内置节点配置。具体类型是否被后端支持,由后端决定。
