# workflow-orchestrator **Repository Path**: bingbingyihao/workflow-orchestrator ## Basic Information - **Project Name**: workflow-orchestrator - **Description**: No description available - **Primary Language**: JavaScript - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-12 - **Last Updated**: 2026-09-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # workflow-orchestrator **工作流编排组件**:拖拽建图 → 实时校验 → 模拟运行 → 输出一份标准 `DSL JSON`。 仓库里只保留**一个页面**(`src/views/EditorView.vue`)作为组件的演示壳,没有首页、没有路由、没有示例广场——因为它交付的是组件,不是站点。 界面为**亮色 SaaS 风格**:浅灰画布 + 白色卡片 + 细描边 + 统一自绘控件;原生控件全部归一化,下拉框与确认弹窗为自绘。 > 画布刻意用浅灰(`#eef1f6`)而非纯白 —— 拖拽时的 `grab` 光标在白底上会「消失」。 ``` 编排(本项目) 只读预览(复用 workflow-preview-player) 拖拽 / 连线 / 属性表单 / 校验 ──► DSL JSON ──► 结果快照 · 逐步回放 · 节点检视 ``` 界面截图见 `docs/screenshots/`:`editor.png`(编排态)、`editor-property.png`(选中节点后的属性面板)、`preview-replay.png`(预览回放态)。 ## 快速开始 ```bash npm install npm run dev # http://127.0.0.1:5174/ 打开即编排器 ``` 打开后就是一张可直接编辑的工作流(默认载入内置示例;若存在本地草稿则优先恢复): - 左栏「节点库」拖拽 / 点击添加节点; - 节点右侧端口拖到另一个节点左侧端口 = 连线; - 选中节点,右栏按类型改配置;**右栏左边框可拖动调整宽度**(双击复位、方向键微调、宽度会记住); - 底栏选「失败节点 / 条件分支」后点「模拟运行 · 预览 / 回放」,切到只读放映态看结果快照与逐步回放; - 顶栏「载入示例」下拉可切换 4 份内置示例。 ```bash npm run build # 构建到 dist/ npm run test:model # 模型层测试(纯 Node,22 项断言) npm run smoke # 真实浏览器端到端冒烟(需先 npm run dev,30 项断言) npm run format # Prettier ``` ## 页面结构 只有一条主线,两态切换(`v-show` 保活,来回切不丢画布状态): | 区域 | 内容 | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | 顶栏 | 品牌标识 · 载入示例(自绘下拉)· 操作提示 · 「编排 / 预览 / 回放」分段切换 | | 编排态 | 工具栏(撤销/重做 · 自动布局/适应/缩放 · 校验/导入/导出/复制 JSON/新建)+ 三栏(节点库 / 画布 / 属性·校验)+ 状态栏(统计 · 状态图例 · 模拟运行参数) | | 预览态 | 只读画布 + 结果快照 / 逐步回放 + 节点检视侧栏 | | 分隔条 | 右栏左边框可拖拽调宽:280–640px(上限随窗口自适应,始终给画布留 ≥560px),双击复位,`←/→` 微调(`Shift` 加速),`Home/End` 到两端,宽度写入 `localStorage` | ## 编排能力 | 能力 | 说明 | | ----------- | ----------------------------------------------------------------------------------------------------------- | | 添加节点 | 节点库拖拽到画布(落点即位置)或点击(排到最右一列) | | 连线 | 端口拖拽成边;自环 / 连到 start / 从 end 连出 / 重复连线会被拒绝 | | 选中 | 点节点、点连线、`Shift` + 拖拽框选 | | 拖动 | 吸附 12px 网格;多选整组拖动;一次拖拽进一次历史 | | 删除 | `Delete` / `Backspace`;节点删除级联删边;节点卡片悬停也有复制/删除 | | 属性表单 | 按类型动态渲染(见下表);改 `id` 自动同步引用它的连线 | | 自动布局 | DAG 最长路径分层重排;「适应」把整图放进屏幕 | | 校验 | 错误:缺入口、成环、id 重复、悬挂连线;提醒:孤立节点、不可达、条件分支缺标签、配置不完整。点击条目定位节点 | | 撤销 / 重做 | 快照栈,上限 60 步 | | 导入 / 导出 | 导入兼容 `source/target` 命名、自动补 id、忽略坏边并告警;导出标准 DSL JSON | | 草稿 | 改动自动写 `localStorage`,重开自动恢复 | | 模拟运行 | 按拓扑序跑图,可指定失败节点与条件分支走向,生成预览用运行记录 | ### 节点类型与表单字段 `start / end / llm / skill / http / code / condition`,字段定义在 `src/orchestrator/nodeSchema.js`: | 类型 | 字段 | | ----------- | ------------------------------------------------- | | `start` | 触发入参(变量名 + 类型) | | `end` | 输出字段 | | `llm` | 模型、提示词、系统提示词、温度、最大 Token | | `skill` | 技能标识、技能参数(键值对) | | `http` | 请求方法、URL、请求头(键值对)、请求体模板、超时 | | `code` | 代码片段、输出变量名 | | `condition` | 判断字段、分支列表(分支名 + 运算符 + 比较值) | 每个节点还可编辑:标题、`id`、类型(切换时合并新类型默认配置)、`inputs`。 ### 快捷键 | 快捷键 | 作用 | | --------------------------- | ------------------ | | `Ctrl/⌘ + Z` / `Ctrl/⌘ + Y` | 撤销 / 重做 | | `Ctrl/⌘ + D` | 复制选中节点 | | `Ctrl/⌘ + S` | 导出 DSL | | `Delete` / `Backspace` | 删除选中元素 | | `Shift` + 拖拽 | 框选 | | `Esc` | 取消选中 / 关弹窗 | | 滚轮 / 捏合 | 缩放;拖拽空白平移 | ## 视觉规范(亮色 SaaS) 设计取向:**浅灰底 + 白色卡片 + 细描边 + 克制的强调色**。分三层落地。 ### 1. 设计令牌(`src/styles/global.css`) | 类别 | 令牌 | | ------------------ | ------------------------------------------------------------------------------------------------------------ | | 表面层级 | `--c-bg #eef1f6` → `--c-surface #ffffff`(面板)→ `--c-surface-2 #f7f9fc`(输入框)→ `--c-surface-3 #eef1f6` | | 画布 | `--c-canvas #eef1f6` + 径向渐变 `#f6f8fc → #e8ecf3`(**刻意不是纯白**) | | 描边 | `--c-border #e2e7ef`、`--c-border-strong #ccd5e0` | | 文本 | `--c-text #1b2230`、`--c-text-2 #5b6675`、`--c-text-3 #7d8a9c`(对比度约 15.9 / 5.8 / 3.5 : 1) | | 强调色 | `--c-primary #2f6fed`、`--c-danger #dc3d43`、`--c-success #16a34a`、`--c-warn #b7791f` | | 圆角 / 阴影 / 焦点 | `--r-xs…lg`、`--sh-1/2/3`、`--ring` | 组件只引用令牌,不写死颜色;节点类型色与运行状态色也按白底重新取值(`preview/workflowDsl.js`)。 ### 2. 原生控件归一化 - `select`:`appearance: none` + 自绘 SVG 细箭头(抹平各平台差异),展开面板靠 `color-scheme: light` 跟随; - `textarea`:**去掉 resize 手柄**(`resize: none`),靠行高与滚动条控制,避免拖拽撑破布局; - `input[type=number]` 隐藏上下箭头;`input[type=search]` 隐藏原生清除按钮; - 滚动条:细圆角 + `background-clip: content-box` 做出内缩效果;`::selection` 用主色半透明; - 焦点:仅 `:focus-visible` 出现光环,鼠标点击不闪; - 层级质感:白色卡片 + 极轻阴影 + 浅灰画布表达浮起,而不是靠加粗边框。 ### 为什么画布不是纯白 编辑区用 `#eef1f6` 浅灰(带一点径向渐变纵深)。除了层次更好看,更实际的原因是:画布与节点都是 `cursor: grab`, 纯白背景会让白色手型光标在拖动时「找不到鼠标」。浅灰底 + 网格点让光标始终有对比参照。 ### 3. 自绘控件(`src/orchestrator/components/ui/`) - `` 取代 `` 写样式时用 `background-color` 而不是 `background` 简写,否则会连同自绘箭头一起覆盖(预览层倍速下拉踩过)。 ## 目录结构 ``` workflow-orchestrator/ ├── index.html ├── vite.config.js ├── .prettierrc.cjs / .editorconfig / .gitignore ├── docs/screenshots/ # 界面截图 ├── scripts/ │ ├── model-test.mjs # 模型层测试(纯 Node,22 项) │ └── smoke.mjs # 真实浏览器冒烟(30 项) └── src/ ├── orchestrator/ # ★ 编排组件(本项目主体) │ ├── editorModel.js # 纯函数图模型:增删改 / 布局 / 拓扑序 / 语义校验 / DSL 导入导出 │ ├── mockRunner.js # 模拟运行器:拓扑序执行 → 运行记录(失败传导 / 分支选择 / 可复现) │ ├── nodeSchema.js # 节点配置表单 schema(字段 → 控件) │ ├── useWorkflowEditor.js # 状态机:dsl + 选中 + 历史栈 + 草稿持久化 │ ├── index.js # 统一出口 │ └── components/ │ ├── WorkflowEditor.vue # 主组件:工具栏 + 三栏 + 状态栏 + 可拖拽分隔条 + 导入弹窗 │ ├── EditorCanvas.vue # 可写画布(Vue Flow 封装:拖拽 / 连线 / 框选 / 删除上报) │ ├── EditorNodeView.vue # 节点卡片(端口 / 选中态 / 悬停快捷操作) │ ├── NodePalette.vue # 节点库 │ ├── PropertyPanel.vue # 属性面板(节点 / 连线 / 工作流信息) │ ├── ValidationPanel.vue # 校验结果面板 │ └── ui/ │ ├── SelectMenu.vue # 自绘下拉选择器 │ └── ConfirmDialog.vue # 自绘确认弹窗 ├── preview/ # 只读预览组件(来自 workflow-preview-player,契约不变) │ ├── theme.css # 预览层主题令牌(继承宿主 --c-*,缺省亮色兜底) │ ├── workflowDsl.js # 节点元数据 / DSL 归一化 / 自动布局 / DSL→Vue Flow │ ├── replay.js # 运行记录归一化 / 播放步骤 / 进度状态推导 │ └── components/ │ ├── WorkflowPreview.vue # 预览主组件:工具栏 + 画布 + 快照/回放 + 节点检视 │ ├── FlowCanvas.vue # 只读 Vue Flow 画布 │ └── WorkflowNodeView.vue # 只读节点卡片(状态呼吸动画) ├── samples/sampleWorkflows.js# 内置示例(默认载入 + 顶栏示例菜单) ├── views/EditorView.vue # 唯一页面:编排 / 预览两态切换(组件用法示例) ├── App.vue / main.js # 极薄的应用壳(无路由) ├── styles/global.css # 设计令牌 + 原生控件归一化 └── utils/download.js ``` ## 数据契约 ### 工作流 DSL ```jsonc { "version": 1, "name": "智能客服:意图路由与分流", "description": "可选描述", "nodes": [ { "id": "understand", "type": "llm", // start | end | llm | skill | http | code | condition "title": "意图理解", // 展示名 "config": { "prompt": "..." }, // 类型化表单编辑 "inputs": { "query": "..." }, // 只读展示 "position": { "x": 0, "y": 0 }, // 缺省时自动分层布局 }, ], "edges": [ { "id": "e2", "from": "understand", "to": "route" }, { "id": "e4", "from": "route", "to": "complaint_api", "condition": "intent == 投诉" }, ], } ``` - `condition` 只是连线上的分支标签,是否走到由运行记录的 `takenEdges` 决定; - 兼容 `edge.from/to` 与 `edge.source/target`,`id` 缺失自动补; - 引用不存在节点的边会被忽略并告警,坏数据不白屏。 ### 运行记录(快照 / 回放) ```jsonc { "meta": { "status": "success", "inputs": {}, "outputs": {} }, "nodes": [ { "nodeId": "understand", "status": "success", // pending | running | success | failed | skipped "seq": 2, // 执行序号,回放按它排序 "input": {}, "output": {}, "errorMessage": "可选", "durationMs": 420, "takenEdges": ["e4"], // 实际流经的边 }, ], } ``` ## 组件 API ### ``(主组件) ```vue ``` | Props | 类型 | 默认 | 说明 | | --------------- | ------- | ------ | ---------------------------------- | | `initialDsl` | Object | `null` | 初始 DSL;为空则新建 `start → end` | | `allowSimulate` | Boolean | `true` | 是否显示模拟运行控件 | Events:`preview({ dsl, run })`、`change(dsl)`。 `defineExpose`:`getDsl()`、`getRawDsl()`、`simulate()`、`getLastRun()`、`fitView()`、`loadDsl(input)`、`resetWorkflow()`、`undo()`、`redo()`、`refresh()`。 ### `` / `` ```vue ``` ### `useWorkflowEditor(initialDsl)` 状态:`dsl / selectedNode / selectedEdge / stats / validation / canUndo / canRedo`; 动作:`addNode / updateNode / updateNodeConfig / moveNodes / deleteSelection / duplicateNode / changeNodeId / connect / updateEdge / deleteEdge / updateMeta / autoLayout / loadDsl / resetWorkflow / exportDsl / run / undo / redo`。 ### `editorModel.js`(纯函数) `emptyWorkflow / createNode / addNode / withNodePatch / withNodeConfig / withNodePosition / withEdge / withoutNode / withoutEdge / renameNodeId / duplicateNode / applyLayout / suggestPosition / topologicalOrder / reachableFromStart / graphStats / neighborSummary / validateGraph / toEditorDsl / toPlainDsl / toDslJson` ### `simulateRun(dsl, options)` ```js simulateRun(dsl, { seed: 20240912, // 同 seed ⇒ 同结果 failAt: 'parse', // 指定节点失败,演示失败传导 branchMode: 'first', // first | last | random }) ``` ## 主题定制 - **编排层**:所有颜色走 `src/styles/global.css` 的 `--c-*` 令牌。换主色 / 换底色只改令牌,组件不用动。 - **预览层**:样式统一使用 `--wp-*`,取值来自 `src/preview/theme.css` —— 优先继承宿主的 `--c-*`,宿主未定义时用内置亮色兜底。因此预览组件既能跟随本工程主题,也能被单独拷进别的工程直接使用;若宿主有自己的变量体系,在 `theme.css` 里做一次映射即可。 - **布局宽度**:右栏宽度存于 `localStorage` 的 `workflow-orchestrator:layout:v1`,改默认值调整 `RIGHT_WIDTH_DEFAULT`。 ## 与参考项目的关系 | 部分 | 来源 | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `src/preview/**` | 复用 [workflow-preview-player](https://gitee.com/bingbingyihao/workflow-preview-player) 的只读预览组件;数据契约不变,整体拷入即可用 | | 预览层的改动 | ① 归一化时保留节点 `title`(原实现只从 `config.title` 取名,中文节点名不显示);② 抽出 `theme.css` 令牌层并改为亮色;③ 节点卡片 / 状态栏 / 播放按钮的亮色质感;④ 倍速下拉适配全局 select 样式 | | `src/orchestrator/**` | 本项目实现:画布编辑、属性表单、校验、撤销栈、模拟运行器、自绘控件、可拖拽分隔条 | | `src/views`、`src/App.vue` | 只保留单页演示壳(已删掉参考项目里的首页 / 路由 / 示例广场) | 预览层依然只读、无业务依赖:它不知道编排器存在,只认 DSL 与运行记录。 ## 质量保障 - `npm run test:model`:22 项纯 Node 断言 —— 图模型纯函数、布局、DAG 校验、模拟运行(失败传导 / 分支 / 可复现)、DSL 解析与导出往返; - `npm run smoke`:30 项真实浏览器断言 —— 打开即编排器 → 加节点 → 改标题 → 自绘下拉改类型并自动关闭 → 撤销/重做 → 端口拉线 → 校验 → 模拟运行 → 预览快照 → 逐步回放 → 节点检视 → 切回编排保留状态 → 示例菜单切换 → 新建确认弹窗 → **右栏拖拽调宽 / 持久化 / 双击复位** → **亮色主题、`color-scheme: light`、textarea 无 resize、无原生 select、编辑区为浅灰非纯白**,并断言全程无 `pageerror` / `console.error`。 ## 后续可扩展 - 节点类型 / 表单字段插件化(`NODE_TYPES` + `NODE_FORMS` 已是数据驱动,加配置即可); - 左栏也支持拖拽调宽(复用同一套分隔条逻辑)、节点分组、对齐辅助线; - 亮 / 暗双主题切换(令牌已分层,补一套 dark 值即可); - 用真实后端执行接口替换 `simulateRun`,`run` 结构不用改; - 多人协同(把 `commit` 的动作包成可序列化操作日志即可)。