build-flow.api/docs/x6-workflow-standard.md
xiangning f932781040 feat(rag): 集成 Microsoft Extensions AI 框架并重构对话代理服务
- 引入 Microsoft.Extensions.AI.Abstractions 依赖包
- 重构 ChatAgentService 实现,支持多轮对话历史
- 更新 IAIChatAgent 接口,统一使用 ChatMessage 消息格式
- 优化流式输出处理逻辑,提升性能表现

feat(im): 增强即时通讯会话功能支持置顶备注免打扰

- 扩展会话 DTO,新增 IsPinned、IsMuted、OtherUserRemark 属性
- 扩展 ConversationParticipant 实体,新增置顶和备注相关字段
- 实现会话置顶功能,支持按置顶时间和最后消息时间排序
- 添加会话备注功能,私聊场景支持自定义备注名称
- 完善免打扰功能,支持用户级别静音设置

feat(file): 实现文件预览服务支持多种鉴权方式

- 新增文件预览端点,支持流式返回文件内容
- 实现三种鉴权方式:授权头、JWT令牌、内部签名
- 集成 KKFileView 预览服务,支持 Office 文档在线预览
- 优化文件访问安全控制,避免直接暴露 S3 链接

feat(rag): 增强 RAG 会话管理支持知识库关联

- 扩展会话创建功能,支持关联知识库
- 新增会话更新功能,支持重命名和切换知识库
- 优化会话 DTO 结构,支持知识库 ID 关联
- 实现知识库检索增强对话功能

feat(workflow): 废弃旧工作流定义接口迁移到图形化存储

- 标记旧的节点边操作端点为废弃状态
- 实现工作流定义图形化存储迁移
- 统一使用 JSON 格式作为单一事实源
- 优化工作流定义查询性能表现

chore(deps): 更新项目依赖包版本解决版本冲突问题

- 添加 Microsoft.Extensions.AI.Abstractions 依赖
- 显式声明 OpenAI 2.11.0 版本解决 CS1705 冲突
- 更新相关依赖包至兼容版本

fix(host): 修复文件预览响应包装中间件配置问题

- 添加文件预览接口到响应绕过前缀列表
- 避免文件流被 JSON 包装和内存缓冲影响性能
- 优化文件下载和预览的中间件处理逻辑

chore(im): 修复 IM 设计时数据库上下文工厂路径问题

- 优化 ImDbContextDesignTimeFactory 路径查找逻辑
- 修复数据库迁移时连接字符串获取问题
- 更新实体映射命名空间引用
2026-07-03 15:58:58 +08:00

693 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# X6 工作流前后端对接标准
| 项目 | 内容 |
|---|---|
| 文档状态 | 评审稿(P0),未实现 |
| 适用范围 | BuildFlow 审批流自定义流程设计器(前端 + 后端) |
| 技术栈 | 前端 Vue 3 + Vben Admin v5 + AntV X6;后端 .NET 10 + FastEndpoints + MediatR + EF Core |
| 关键决策 | ① 传统审批流(非 AI 编排)② 后端 X6 JSON 单一事实源 ③ X6 自建路线 |
> 本文是**标准/契约**文档,不是实现教程。所有事实陈述标注「基于现有代码:`路径`」。实现细节另起任务。
---
## 第 1 章 背景与现状
### 1.1 目标场景
BuildFlow 需要一个**审批流自定义流程设计器**:业务用户在前端通过拖拽节点、连线的方式编排审批流程(开始 → 审批 → 抄送 → 条件分支 → 并行 → 子流程 → 定时 → 结束),保存后由后端 `ProcessEngine` 执行。
**明确不做**:AI 智能体编排(LLM/代码/MCP/知识库节点)。
### 1.2 前端现状
基于现有代码:`build-flow.web/apps/web-antdv-next/src/`
- 技术栈:Vue 3 + TypeScript + Vue Vben Admin v5 + antdv-next + pnpm workspace。
- **当前无任何流程相关页面**(`src/views/` 下只有 dashboard / system / file-manager / im / share / demos / _core,无 workflow 目录)。
- **当前无 X6 依赖**,需全新引入 `@antv/x6` + `@antv/x6-vue-shape`。
- API 客户端约定:基于现有代码 `src/api/request.ts`,统一 `requestClient`(`responseReturn: 'data'`),后端响应信封由前端层自动解包。
### 1.3 后端现状(关系表事实源)
基于现有代码:`build-flow.api/src/modules/workflow/`
#### 1.3.1 三张关系表为唯一事实源
| 表名 | 实体 | 配置文件 |
|---|---|---|
| `wf_workflow_definitions` | `WorkflowDefinition` | `Infrastructure/Persistence/Configurations/WorkflowDefinitionConfiguration.cs` |
| `wf_workflow_nodes` | `WorkflowNode` | `WorkflowNodeConfiguration.cs` |
| `wf_workflow_edges` | `WorkflowEdge` | `WorkflowEdgeConfiguration.cs` |
**`DefinitionJson` 是冗余空壳字段**:
- 基于现有代码 `Domain/Entities/WorkflowDefinition.cs:21`:字段注释为「完整流程图的 JSON 快照(节点+边),便于导出/导入与前端渲染」。
- 基于现有代码 `UpdateWorkflowDefinitionCommandHandler.cs`:Handler 仅做 `entity.DefinitionJson = request.DefinitionJson;` 字符串覆盖,**不与 nodes/edges 表同步**,无任何一致性校验。
- 基于 `SeedData.cs`:种子数据只构造 Node/Edge 实体,**未写 DefinitionJson**。
#### 1.3.2 `ProcessEngine` 运行时只读关系表
基于现有代码 `Application/Engine/ProcessEngine.cs`(约 1191 行,方法名定位,行号会漂移):
- 节点查找 `FindNodeAsync`:`_dbContext.WorkflowNodes.FindAsync(nodeId)`。
- 出边查找 `GetOutgoingEdgesAsync`:`_dbContext.WorkflowEdges.Where(e => e.SourceNodeId == nodeId)`。
- 入边查找 `GetIncomingEdgesAsync`:`_dbContext.WorkflowEdges.Where(e => e.TargetNodeId == nodeId)`。
- 启动时加载定义全部节点(`StartAsync` 内):`_dbContext.WorkflowNodes.Where(n => n.DefinitionId == ...)`。
- Config 解析统一走 `NodeConfigParser.Parse(node.Config)`。
- **`DefinitionJson` 在引擎路径中一次都未被读取**(grep 零命中)。
#### 1.3.3 引擎对实体字段的精确依赖(迁移影响面)
| 实体 | 引擎使用的字段 | 是否影响迁移 |
|---|---|---|
| `WorkflowNode` | `Id`, `DefinitionId`, `NodeType`, `Name`, `Config` | ✅ 必须从 JSON 等价读取 |
| `WorkflowNode` | `PositionX`, `PositionY` | ❌ 引擎不读(纯画布字段) |
| `WorkflowNode` | `FormDefinitionId` | ⚠️ 引擎不直接读,但 DTO 层有 |
| `WorkflowEdge` | `SourceNodeId`, `TargetNodeId` | ✅ 拓扑遍历核心 |
| `WorkflowEdge` | `EdgeType` | ✅ `CompleteTaskAsync` 选边(Approved 优先 / Normal 回退 / Rejected 独立) |
| `WorkflowEdge` | `Condition` | ✅ Condition/ExclusiveGateway 节点条件评估 |
| `WorkflowEdge` | `Order` | ✅ Condition/ExclusiveGateway 分支评估顺序(first-match-wins) |
| `WorkflowEdge` | `Label` | ❌ 引擎不读 |
#### 1.3.4 现有接口割裂(7 套)
基于现有代码 `src/BuildFlow.Host/Endpoints/workflow/WorkflowDefinition/`(24 个文件):
- 定义级:Create / Update / Delete / Publish / Disable / GetById / GetList(7 个)
- 节点级:`CreateNode` / `UpdateNode` / `DeleteNode`(3 个,路由 `/workflow-definitions/{DefinitionId}/nodes[/{NodeId}]`)
- 边级:`CreateEdge` / `UpdateEdge` / `DeleteEdge`(3 个)
**问题**:前端维护画布需对每个节点/边单独发请求,与 X6「整图 `toJSON()` 保存」模型不匹配。
#### 1.3.5 响应信封
基于现有代码 `shared/BuildFlow.Shared/Api/ApiResponse.cs` + `ApiResponseMiddleware.cs`:
```
{ "code": 0, "data": <业务对象>, "message": "ok" }
```
- `code = 0` 表示成功;前端 `requestClient` 配 `responseReturn: 'data'` 自动解包。
- 时间统一序列化为 **UTC 毫秒时间戳**(`TimestampDateTimeConverter`)。
- 命名 CamelCase。
#### 1.3.6 现有枚举(本契约复用,不改)
基于现有代码 `Domain/Enums/`:
```csharp
// NodeType(int):Start=0, End=1, Approval=2, Cc=3, Condition=4,
// Parallel=5, SubProcess=6, Timer=7, ExclusiveGateway=8
// EdgeType(int):Normal=0, Approved=1, Rejected=2
// DefinitionStatus(int):Draft=0, Published=1, Disabled=2
```
---
## 第 2 章 目标与边界
### 2.1 目标
1. 定义**与 X6 `toJSON()`/`fromJSON()` 原生兼容**的 `definition_json` schema,作为前后端唯一对接契约。
2. 让 `definition_json` 升级为后端**唯一事实源**,`ProcessEngine` 改读 JSON。
3. 简化接口:废弃逐 Node/Edge CRUD,改为**整图保存**。
4. 前端用 X6 实现可视化设计器,`graph.toJSON()` → 直接落库,`graph.fromJSON()` → 直接渲染。
### 2.2 边界
- **本文档只定标准,不实现**。后续实现按第 8 章阶段推进。
- 不改变现有 `NodeType` / `EdgeType` / `DefinitionStatus` 枚举语义。
- 不改变响应信封、认证、权限体系。
- 不涉及流程实例运行时(`WorkflowInstance` / `WorkflowTask` / `WorkflowToken`)的存储改造——它们引用的是已发布流程定义的快照,迁移策略见第 7 章。
---
## 第 3 章 组件选型结论
### 3.1 结论:X6 自建路线
| 候选 | 结论 | 依据 |
|---|---|---|
| `@antv/xflow`(X6 官方应用级方案) | ❌ 不可用 | 仓库已声明停止维护;**仅支持 React**,Vben 是 Vue3 无法集成 |
| X6 官方审批流成品 | ❌ 不存在 | 官方 showcase 无审批流成品,无审批/抄送/条件分支节点预制模板 |
| Vue3 审批流成品(StavinLi/Workflow-Vue3 等) | ❌ 偏离方向 | 均为仿钉钉 DOM 树渲染,**不用 X6 画布**,与「引入 X6」目标冲突 |
| **X6 自建(BPMN/流程图示例 + x6-vue-shape)** | ✅ 采纳 | 贴合「引入 X6」目标;官方有 BPMN/流程图示例可参照;审批节点需自写但有参照 |
### 3.2 依赖包
| 包 | 用途 | 版本约束 |
|---|---|---|
| `@antv/x6` | X6 主包(**3.x 起插件已合并进主包**,Snapline/Stencil/Transform 等无需单独安装) | `^3.x`(X6 当前稳定主版本,遵循仓库 pnpm catalog 规则) |
| `@antv/x6-vue-shape` | Vue 组件渲染节点注册 | 与 `@antv/x6` 3.x 对齐(同主版本) |
> ⚠️ 必须用 **X6 3.x**,2.x 的插件需单独安装且 API 有差异。实现时以 `@antv/x6` 当前发布的稳定主版本为准,并加入仓库 catalog,不要固定死小版本。安装走 `build-flow.web` 的 pnpm workspace,禁止 npm/yarn。
### 3.3 参照起点
- X6 官方 BPMN 示例、流程图示例:源码位于 antvis/X6 仓库 `site/examples/showcase/practices/demo/`。
- X6 Vue 节点文档:`https://x6.antv.antgroup.com/tutorial/intermediate/vue`。
- 连接桩文档:`https://x6.antv.antgroup.com/tutorial/basic/port`。
- 旧版审批 demo(请假/人事审批,X6 1.x,仅作节点样式参考):`https://x6.antv.vision/zh/examples/showcase/practices/`。
---
## 第 4 章 前端方案(设计层面)
### 4.1 目录落点
遵循 `rag-coding-web` 技能约定,全部落在 `apps/web-antdv-next/src/`:
```
src/
├── api/core/workflow.ts # API 定义
├── router/routes/modules/workflow.ts # 路由 module
└── views/workflow/
├── list/index.vue # 流程定义列表/发布/启停
└── designer/
├── index.vue # 设计器主页面(容器)
├── graph/ # X6 Graph 初始化与配置
│ ├── createGraph.ts # Graph 工厂(grid/snapline/connecting)
│ └── ports.ts # 端口 groups/items 预设
├── shapes/ # 节点注册(每个 NodeType 一个)
│ ├── register.ts # 集中 register 所有 shape
│ ├── StartNode.vue
│ ├── ApprovalNode.vue
│ ├── ConditionNode.vue
│ └── ... # 详见 4.4
├── components/ # 设计器 UI 组件
│ ├── StencilPanel.vue # 拖拽节点面板
│ ├── Toolbar.vue # 保存/撤销/缩放/校验
│ └── NodeConfigDrawer.vue # 节点配置抽屉
└── composables/
├── useGraph.ts # Graph 生命周期管理
└── useGraphPersistence.ts # 加载/保存(toJSON/fromJSON)
```
> 上述是设计目标结构,实现时可在遵循「模块内聚」前提下调整。`packages/@core`、`packages/effects` 不改。
### 4.2 设计器能力清单
| 能力 | X6 机制 | 说明 |
|---|---|---|
| 拖拽创建节点 | `Stencil` 插件 | 分组面板,拖入后 `addNode` |
| 画布缩放 | `mousewheel` + `minScale/maxScale` | 0.5 ~ 3 |
| 对齐线 | `Snapline` 插件 | X6 3.x 内置 |
| 连线 | `connecting` 配置 | `connector: 'smooth'`、`allowLoop: false`、`validateConnection` |
| 端口显隐 | `node:mouseenter/mouseleave` 事件 | 默认隐藏,hover 显示,连接后常显 |
| 节点删除 | 卡片内删除按钮 + `node.remove()` | Start/End 节点禁止删除 |
| 边删除 | `edge:mouseenter` 显示 `button-remove` 工具 | |
| 整图保存 | `graph.toJSON()` | 一次请求覆盖整张图 |
| 整图加载 | `graph.fromJSON({nodes, edges})` | 详情接口返回的 `definitionJson` 直接喂入 |
### 4.3 端口预设(所有节点统一)
四向端口,默认隐藏,hover 显示:
```ts
const PORT_GROUPS = {
top: { position: 'top', attrs: { circle: { r: 3, magnet: true, fill: '#C2C8D5', stroke: '#C2C8D5' } } },
right: { position: 'right', attrs: { circle: { r: 3, magnet: true, fill: '#C2C8D5', stroke: '#C2C8D5' } } },
bottom: { position: 'bottom', attrs: { circle: { r: 3, magnet: true, fill: '#C2C8D5', stroke: '#C2C8D5' } } },
left: { position: 'left', attrs: { circle: { r: 3, magnet: true, fill: '#C2C8D5', stroke: '#C2C8D5' } } },
};
const PORT_ITEMS = [
{ id: 'top', group: 'top' }, { id: 'right', group: 'right' },
{ id: 'bottom', group: 'bottom' }, { id: 'left', group: 'left' },
];
```
### 4.4 节点形态
用 `@antv/x6-vue-shape` 注册 Vue 组件渲染。每个 `NodeType` 对应一个 shape 名和一个 `.vue` 组件,业务配置通过 `node.data` 承载,组件内 `node.getData()` 读取。详见第 5 章 5.5 的 shape 映射表。
> 交互复杂的节点配置(审批人规则编辑器、条件表达式编辑器)需独立子方案,本文不展开。
---
## 第 5 章 后端数据契约(★ 核心)
### 5.1 顶层结构
`definition_json` 列存 X6 原生格式。后端统一**接收/返回 `{nodes, edges}` 形式**(X6 的 `fromJSON`/`parseJSON` 同时支持 `{cells}` 和 `{nodes, edges}` 两种等价形式,本契约选用前者更易读写):
```json
{
"nodes": [ /* Node 对象数组 */ ],
"edges": [ /* Edge 对象数组 */ ]
}
```
> 空图(仅有定义壳、无节点)合法,值为 `{"nodes":[],"edges":[]}`。
### 5.2 节点标准字段表
| 字段 | 类型 | 必填 | 用途 | 后端是否消费 |
|---|---|---|---|---|
| `id` | string | ✅ | 节点唯一标识,格式见 5.7 | ✅ 拓扑遍历 |
| `shape` | string | ✅ | 节点形状名,决定渲染器,见 5.5 | ✅ 反推 NodeType |
| `x` | number | ✅ | 画布 X 坐标(左上角) | ❌ 仅渲染 |
| `y` | number | ✅ | 画布 Y 坐标(左上角) | ❌ 仅渲染 |
| `width` | number | ❌ | 宽度(注册时默认) | ❌ 仅渲染 |
| `height` | number | ❌ | 高度(注册时默认) | ❌ 仅渲染 |
| `angle` | number | ❌ | 旋转角度 | ❌ 仅渲染 |
| `visible` | boolean | ❌ | 是否可见,默认 true | ❌ 仅渲染 |
| `data` | object | ✅ | **业务数据**,见 5.5 | ✅ 引擎读取 |
| `ports` | object | ❌ | 连接桩,见 5.4 | ❌ 仅渲染(边引用 port) |
| `attrs` | object | ❌ | SVG 样式 | ❌ 仅渲染 |
| `markup` | array | ❌ | SVG 结构 | ❌ 仅渲染 |
| `label` | string\|object | ❌ | 文本标签 | ❌ 仅渲染 |
**后端消费原则**:引擎只读 `id` / `shape`(反推 NodeType)/ `data`;几何/样式字段透传不解释。
### 5.3 边标准字段表
| 字段 | 类型 | 必填 | 用途 | 后端是否消费 |
|---|---|---|---|---|
| `id` | string | ✅ | 边唯一标识 | ✅ 标识 |
| `shape` | string | ❌ | 边类型,默认 `wf-edge` | ❌ |
| `source` | `{cell, port}` | ✅ | 起点 `{cell: nodeId, port: portId}` | ✅ 取 `cell` 作 SourceNodeId |
| `target` | `{cell, port}` | ✅ | 终点 `{cell: nodeId, port: portId}` | ✅ 取 `cell` 作 TargetNodeId |
| `data` | object | ❌ | 业务数据,见 5.6 | ✅ 引擎读取 |
| `label` | string\|object | ❌ | 边标签 | ❌ 仅渲染(引擎不读) |
| `vertices` | `[{x,y}]` | ❌ | 路径控制点(弯折点) | ❌ 仅渲染 |
| `router` | string\|object | ❌ | 路由算法(normal/orth/manhattan) | ❌ 仅渲染 |
| `connector` | string\|object | ❌ | 连接器(normal/smooth/rounded) | ❌ 仅渲染 |
| `attrs` | object | ❌ | 样式 | ❌ 仅渲染 |
| `zIndex` | number | ❌ | 层级 | ❌ 仅渲染 |
**source/target 规则**:必须用 `{cell, port}` 结构;`cell` 是节点 id(对应 5.2 的 `node.id`),`port` 是端口 id(默认 `top/right/bottom/left`)。后端只取 `cell` 做拓扑,`port` 仅前端连线校验用。
### 5.4 端口 port 结构(与 4.3 一致)
```json
"ports": {
"groups": { /* top/right/bottom/left 组定义,见 4.3 */ },
"items": [
{ "id": "top", "group": "top" },
{ "id": "right", "group": "right" },
{ "id": "bottom", "group": "bottom" },
{ "id": "left", "group": "left" }
]
}
```
> 后端不解释 ports 结构,但边的 `source.port` / `target.port` 必须命中节点 `ports.items[].id`。校验在发布时做(见第 6 章)。
### 5.5 节点 data 业务契约(关键映射)
`node.shape` 决定渲染器,同时**反推 `NodeType` 枚举**。业务配置统一进 `node.data`,替代旧的 `WorkflowNode.Config`(jsonb 字符串)。
| NodeType | 枚举值 | shape 名 | `data` 业务字段(替代旧 Config) |
|---|---|---|---|
| `Start` | 0 | `wf-start` | 无(`data` 可存 `title`) |
| `End` | 1 | `wf-end` | 无(`data` 可存 `title`) |
| `Approval` | 2 | `wf-approval` | `approvalMode`(single/countersign/any), `assigneeRule`, `assigneeRules`(数组), `minApprovals`, `timeoutMinutes`, `onEnter`, `onApproved`, `onRejected`, `formDefinitionId` |
| `Cc` | 3 | `wf-cc` | `recipients`(数组), `formDefinitionId` |
| `Condition` | 4 | `wf-condition` | 无(条件表达式与顺序放在**出边**的 `data` 上,见 5.6) |
| `Parallel` | 5 | `wf-parallel` | 无 |
| `SubProcess` | 6 | `wf-subprocess` | `definitionId`(子流程 GUID 字符串) |
| `Timer` | 7 | `wf-timer` | `delayMinutes`, `targetTime`(ISO8601) |
| `ExclusiveGateway` | 8 | `wf-exclusive` | 无(同 Condition,分支信息在出边) |
> `data.title`(节点显示名,对应旧 `WorkflowNode.Name`)所有节点通用,建议必填。
**shape → NodeType 反推规则**:后端维护一张常量映射表,`shape` 缺失时拒绝发布。
### 5.6 边 data 业务契约
| `data` 字段 | 类型 | 对应旧字段 | 用途 |
|---|---|---|---|
| `edgeType` | int (枚举值 0/1/2) | `WorkflowEdge.EdgeType` | Normal=0 / Approved=1 / Rejected=2;影响 Approval 节点选边 |
| `condition` | string | `WorkflowEdge.Condition` | Condition/ExclusiveGateway 出边的条件表达式(简单文本或 JSON 树) |
| `order` | int | `WorkflowEdge.Order` | 分支评估顺序,值小优先(first-match-wins) |
> 几何信息(`vertices` / `router` / `connector`)放边**顶层**,不放 `data`;这些字段后端透传不读。
### 5.7 ID 规则
| 元素 | id 格式 | 生成方 |
|---|---|---|
| 节点 | `node_<guid>` | 前端生成(拖入时),后端保存时校验唯一 |
| 边 | `edge_<guid>` | 前端生成(连线时),后端保存时校验唯一 |
| 端口 | `top` / `right` / `bottom` / `left`(固定) | 由 shape 注册预设,不单独生成 |
> **待决项**(见第 9 章):是否允许后端在保存时重写 id。当前建议前端生成、后端透传,保证 `source.cell`/`target.cell` 引用稳定。
### 5.8 引擎字段对应表(迁移映射)
`ProcessEngine` 当前读实体字段 → 改造后从 JSON 路径读取:
| 当前实体字段 | JSON 路径 | 说明 |
|---|---|---|
| `node.Id` | `nodes[].id` | 字符串 ↔ Guid(需类型转换) |
| `node.NodeType` | 由 `nodes[].shape` 反推 | 见 5.5 映射 |
| `node.Name` | `nodes[].data.title` | |
| `node.Config`(jsonb 字符串) | `nodes[].data`(对象) | 直接是对象,免去 `NodeConfigParser` 字符串反序列化 |
| `node.DefinitionId` | 外层(图所属定义) | 加载时由 Definition 上下文注入 |
| `node.PositionX/Y` | `nodes[].x/y` | 引擎不读,仅迁移脚本用 |
| `edge.SourceNodeId` | `edges[].source.cell` | |
| `edge.TargetNodeId` | `edges[].target.cell` | |
| `edge.EdgeType` | `edges[].data.edgeType` | |
| `edge.Condition` | `edges[].data.condition` | |
| `edge.Order` | `edges[].data.order` | |
| `edge.Label` | `edges[].label` | 引擎不读 |
### 5.9 完整示例(最小可运行图)
```json
{
"nodes": [
{
"id": "node_start_001",
"shape": "wf-start",
"x": 120, "y": 120,
"data": { "title": "开始" },
"ports": { "groups": { "...": "见 5.4" }, "items": [ "..." ] }
},
{
"id": "node_approval_001",
"shape": "wf-approval",
"x": 120, "y": 280,
"data": {
"title": "部门经理审批",
"approvalMode": "single",
"assigneeRule": "role:dept-manager",
"timeoutMinutes": 1440,
"formDefinitionId": null
},
"ports": { "...": "..." }
},
{
"id": "node_end_001",
"shape": "wf-end",
"x": 120, "y": 440,
"data": { "title": "结束" },
"ports": { "...": "..." }
}
],
"edges": [
{
"id": "edge_001",
"shape": "wf-edge",
"source": { "cell": "node_start_001", "port": "bottom" },
"target": { "cell": "node_approval_001", "port": "top" },
"data": { "edgeType": 0 }
},
{
"id": "edge_002",
"shape": "wf-edge",
"source": { "cell": "node_approval_001", "port": "bottom" },
"target": { "cell": "node_end_001", "port": "top" },
"data": { "edgeType": 1 }
}
]
}
```
---
## 第 6 章 API 契约(简化接口)
### 6.1 改造后接口(最小集)
遵循现有 FastEndpoints + MediatR + 资源式路由风格(基于现有代码 `Endpoints/workflow/WorkflowDefinition/`):
| 方法 | 路由 | 说明 | 请求要点 | 响应 |
|---|---|---|---|---|
| `GET` | `/workflow-definitions` | 列表 | 分页/筛选 | `WorkflowDefinitionDto[]`(**不含** nodes/edges,含 definitionJson 摘要状态) |
| `GET` | `/workflow-definitions/{Id}` | 详情 | — | `WorkflowDefinitionDetailDto`(含完整 `definitionJson`,前端 `graph.fromJSON` 直接渲染) |
| `POST` | `/workflow-definitions` | 创建 | name, code, formDefinitionId | 初始空图 `{"nodes":[],"edges":[]}` |
| `PUT` | `/workflow-definitions/{Id}` | **整图保存** | name, description, **definitionJson**, formDefinitionId | `WorkflowDefinitionDto` |
| `POST` | `/workflow-definitions/{Id}/publish` | 发布 | — | 发布时做**图校验**(见 6.3) |
| `DELETE` | `/workflow-definitions/{Id}` | 删除 | — | 空对象(沿用现有写回 `"{}"` 约定) |
**关键变化**:`PUT` 整图保存——请求体 `definitionJson` 即 X6 `graph.toJSON()` 输出,**一次请求覆盖整张图**,后端不再做增量 Node/Edge 同步。
**DTO 字段去向(基于现有代码 `DTOs/WorkflowDefinitionDetailDto.cs`)**:
现有 `WorkflowDefinitionDetailDto` 含 `List<WorkflowNodeDto> Nodes` + `List<WorkflowEdgeDto> Edges` 两个集合字段,是「关系表事实源」时代的产物。JSON 单一事实源后这两个集合变为冗余,处理方式:
- **`Nodes` / `Edges` 集合**:置为 `null`(或空集合),**不再由后端填充**。前端设计器统一从 `DefinitionJson` 经 `graph.fromJSON()` 渲染,不读这两个集合。
- **`DefinitionJson`**:成为详情接口的**唯一权威负载**,前端拿它直接喂 `graph.fromJSON`。
- **是否删除 `Nodes`/`Edges` 字段**:为避免破坏存量前端调用,P1 阶段**保留字段置空**,P4 关系表清理时一并从 DTO 移除。
- `WorkflowNodeDto` / `WorkflowEdgeDto`:P4 阶段随关系表废弃而废弃。
### 6.2 废弃接口(过渡期保留)
以下 6 套接口标 `[Obsolete]`,过渡期内不删(避免破坏存量调用),最终在 P4 阶段移除:
- `POST/PUT/DELETE /workflow-definitions/{DefinitionId}/nodes[/{NodeId}]`
- `POST/PUT/DELETE /workflow-definitions/{DefinitionId}/edges[/{EdgeId}]`
> 过渡期:这些接口只操作关系表(旧事实源),不写 `definition_json`。一旦 P1 完成 ProcessEngine 切换,关系表不再是事实源,这些接口失去意义。
### 6.3 发布时图校验规则
`POST /publish` 在状态机 `Draft → Published` 前,对 `definition_json` 做以下校验(失败返回 `ValidationErrorResponse`)。
> 校验规则依据现有 `ProcessEngine` 的运行时强约束(基于现有代码 `Application/Engine/ProcessEngine.cs`:边选择在 `CompleteTaskAsync` 内、Condition 评估在 `ProcessConditionNodeAsync`、无匹配分支抛 `"No condition branch matched"`)。**发布期校验必须覆盖引擎运行时会抛异常的所有情形**,避免发布出运行即崩溃的流程。
**结构校验:**
1. **唯一 Start**:有且仅有一个 `shape === "wf-start"` 节点。
2. **至少一个 End**:存在 `shape === "wf-end"` 节点。
3. **无悬空边**:每条边的 `source.cell` / `target.cell` 都能在 `nodes[].id` 中找到。
4. **可达性**:从 Start 节点出发,所有 End 节点可达(DFS/BFS)。
5. **shape 合法**:所有 `shape` 在 5.5 映射表内。
6. **id 唯一**:节点 id、边 id 全局唯一;边的 `source.cell`/`target.cell` 引用有效。
**引擎强约束校验(必须,否则运行时抛 BusinessException):**
7. **Approval 节点出边完备性**:每个 `wf-approval` 节点必须至少有一条 `data.edgeType=Approved` 或 `data.edgeType=Normal` 的出边(通过路径);若业务允许驳回,还必须有一条 `data.edgeType=Rejected` 的出边。否则审批任务完成时引擎找不到匹配边直接异常。
8. **Condition / ExclusiveGateway 节点分支评估**:每个 `wf-condition` / `wf-exclusive` 节点的所有出边必须带 `data.condition` 和 `data.order`;**强烈建议提供一条兜底边**(condition 为 `true` 或空),否则当所有分支都不匹配时引擎抛 `"No condition branch matched"`。
9. **SubProcess 节点引用有效**:`wf-subprocess` 节点的 `data.definitionId` 必须指向一个已发布(`Published`)的流程定义。
> 校验失败统一返回 `ValidationErrorResponse`(沿用现有 `BuildFlow.Shared/Api/` 的校验失败信封)。
---
## 第 7 章 迁移策略(关系表 → JSON)
### 7.1 核心难点:已发布定义的不可变性与运行实例的版本隔离
> ⚠️ **本节是整个迁移方案的关键,直接决定方案是否可行。**
**现有代码的事实缺陷(必须先正视)**:
1. **WorkflowInstance 没有 Version 字段**(基于现有代码 `Domain/Entities/WorkflowInstance.cs`):实例仅存 `DefinitionId`。
2. **ProcessEngine 按 DefinitionId 实时读图**(基于现有代码 `Application/Engine/ProcessEngine.cs` 的 `StartAsync`):`WorkflowNodes.Where(n => n.DefinitionId == instance.DefinitionId)`,无版本隔离。
3. **Update 没有状态校验**(基于现有代码 `UpdateWorkflowDefinitionCommandHandler.cs`):注释说「仅 Draft 可改」,但 Handler **未强制**——`Published` 状态的定义仍可被 PUT 覆盖。
**后果**:在关系表时代,这三点就已构成隐患(发布后改节点会污染运行中实例);**迁移到 JSON 单一事实源、且 PUT 改为整图覆盖后,隐患被放大**——一个 PUT 即可让所有运行中实例读到不一致的图。
**结论**:JSON 单一事实源方案**必须配套解决版本隔离**,否则不可上线。
### 7.2 版本隔离方案:发布快照表(参照仓库已有先例 `FormDefinitionVersion`)
> 本方案沿用仓库**已有的** `FormDefinitionVersion`(`wf_form_definition_versions`)版本快照范式,命名/分层/审计接口完全对齐,不引入新模式。
**新增实体 `WorkflowDefinitionSnapshot`**(基于现有先例 `Domain/Entities/FormDefinitionVersion.cs` 的结构):
| 字段 | 类型 | 说明 |
|---|---|---|
| `Id` | Guid | 主键 |
| `DefinitionId` | Guid | 所属流程定义 ID |
| `Version` | int | 版本号(与发布时 `WorkflowDefinition.Version` 对应) |
| `DefinitionJson` | string? (jsonb) | 该版本的完整 X6 graph JSON 快照(**不可变**) |
| `Source` | string | 触发来源:`Publish`(仅发布写入,区别于 FormDefinitionVersion 的 Update/Publish 双源) |
| `ChangeSummary` | string? | 变更摘要(可选) |
| 审计 | IAuditable | CreatedBy/CreatedAt/UpdatedBy/UpdatedAt/IsDeleted/OperatorIP |
**EF 配置**(基于现有先例 `FormDefinitionVersionConfiguration.cs`):
- 表名 `wf_workflow_definition_snapshots`(对齐 `wf_form_definition_versions` 命名)。
- `DefinitionJson` → `HasColumnType("jsonb")`。
- 复合唯一索引 `HasIndex(e => new { e.DefinitionId, e.Version })`。
- 索引 `HasIndex(e => e.DefinitionId)`。
**写入时机(发布流程)**:
1. `PublishWorkflowDefinitionCommandHandler`(基于现有代码)在做 `Status=Published; Version++` **之前**,先做 6.3 全部校验。
2. 校验通过后,把当前 `definition_json` 复制成一条 `WorkflowDefinitionSnapshot` 记录(Version = 新版本号)。
3. 快照写入后,引擎运行实例只读快照表,Draft 编辑主表的 `definition_json` 不影响已发布版本。
**读取规则(ProcessEngine 改造后)**:
- 引擎不再用 `instance.DefinitionId` 查 `wf_workflow_definitions.definition_json`。
- 改为:用 `instance.DefinitionId` + **实例绑定的 Version** 定位 `wf_workflow_definition_snapshots` 的不可变快照。
- ⚠️ 这要求 `WorkflowInstance` **新增一个 `DefinitionVersion`(int)字段**,在 `StartInstance` 时从当时的 `WorkflowDefinition.Version` 拷贝写入实例(实例一旦启动版本即冻结)。
> 实体字段新增属侵入式改动,但 `WorkflowInstance.DefinitionVersion` 是方案成立的最小必要条件,无法绕开。EF Migration 需加此列并回填存量实例(用对应定义当前 Version)。
### 7.3 Draft 编辑的 Published 锁定(修复现有 Update 漏洞)
在 `UpdateWorkflowDefinitionCommandHandler` 增加状态校验(基于现有代码,当前缺失):
```csharp
// 仅 Draft 可编辑定义内容;Published/Disabled 状态拒绝整图 PUT
if (entity.Status != DefinitionStatus.Draft)
{
throw new BusinessException("Only draft workflow definitions can be edited.");
}
```
> 已发布的定义如需修改,走「复制为新 Draft → 编辑 → 重新发布」流程(产出新 Version + 新快照),原 Version 快照不变,存量实例不受影响。
### 7.4 四步迁移路线
**第一步(P1 核心):实现 JSON 读取层 + 快照机制**
- 新建 `WorkflowGraph` 领域值对象 + 解析器(`Application/Engine/WorkflowGraphParser.cs`),从 X6 JSON 解析出:节点列表、边列表、出边拓扑、节点 data。
- 新建 `WorkflowDefinitionSnapshot` 实体 + EF 配置 + Migration。
- `WorkflowInstance` 新增 `DefinitionVersion` 字段 + Migration + 回填。
- `PublishWorkflowDefinitionCommandHandler` 增加 6.3 校验 + 写快照。
- `UpdateWorkflowDefinitionCommandHandler` 增加 Draft 锁定(7.3)。
**第二步(P1):ProcessEngine 切换为读快照**
- 引擎启动实例时:按 `instance.DefinitionId + instance.DefinitionVersion` 从 `wf_workflow_definition_snapshots` 加载不可变 JSON 到内存 `WorkflowGraph`。
- 改造影响面见 7.5。
- 此阶段起,Draft 编辑主表 `definition_json` 与运行实例彻底解耦。
**第三步(P1):存量数据迁移脚本(支持 dry-run + 回滚,见 7.6)**
- 遍历 `wf_workflow_definitions`,对 `definition_json` 为空的记录,从 `wf_workflow_nodes` + `wf_workflow_edges` 序列化生成 X6 JSON:
- `PositionX/Y` → `x/y`
- 补默认 `width/height/ports`(按 shape 预设)
- `node.Config`(字符串)→ `node.data`(对象),并按 NodeType 补 `data.title = node.Name`
- `edge.SourceNodeId/TargetNodeId` → `edge.source.cell/target.cell`
- `edge.EdgeType/Condition/Order` → `edge.data.edgeType/condition/order`
- 节点/边 id 统一为 `node_<guid>` / `edge_<guid>`
- 为每个已 `Published` 的定义补写一条 `WorkflowDefinitionSnapshot` 快照(Version 取定义当前 Version)。
- 为每个运行中的 `WorkflowInstance` 回填 `DefinitionVersion`(取其定义当前 Version)。
**第四步(P4):关系表降级 → 最终移除**
- 关系表降级为只读快照(过渡期)。
- 新版本:移除 `WorkflowNodes` / `WorkflowEdges` DbSet、EF Configuration,加 Migration drop 表。
### 7.5 ProcessEngine 改造影响面(以方法名为准)
基于现有代码 `Application/Engine/ProcessEngine.cs`。下表**以方法名为准**(行号会随代码漂移,仅作辅助定位):
| 当前方法 | 当前实现(关系表) | 改造后(读 WorkflowGraph) |
|---|---|---|
| `StartAsync` 加载节点 | `_dbContext.WorkflowNodes.Where(DefinitionId==instance.DefinitionId)` | 从快照 JSON 加载 `WorkflowGraph`,找 `shape==="wf-start"` |
| `FindNodeAsync` | `_dbContext.WorkflowNodes.FindAsync(nodeId)` | 从内存 `WorkflowGraph.Nodes` 按 id 查 |
| `GetOutgoingEdgesAsync` | `_dbContext.WorkflowEdges.Where(SourceNodeId==)` | 从内存 `WorkflowGraph.Edges` 按 source.cell 过滤 |
| `GetIncomingEdgesAsync` | `_dbContext.WorkflowEdges.Where(TargetNodeId==)` | 从内存 `WorkflowGraph.Edges` 按 target.cell 过滤 |
| `ParseConfig` 包装 | 入参 `node.Config`(字符串) | 入参改为 `node.data`(对象),`NodeConfigParser` 适配 |
| Timer 节点内联查边(位于 `ProcessTimerNodeAsync` 内) | `db.WorkflowEdges.Where(SourceNodeId==)` | 同上,改读 `WorkflowGraph` |
> 改造后引擎从「每步查库」变为「启动时一次加载快照 JSON 到内存 `WorkflowGraph`,后续内存遍历」,性能与一致性都更优;且天然实现版本隔离(实例绑定 Version)。
### 7.6 迁移回滚预案
生产数据迁移必须有回滚方案:
1. **迁移前备份**:迁移脚本执行前,对 `wf_workflow_definitions.definition_json` 原值、`wf_workflow_nodes`、`wf_workflow_edges` 做全表 SQL 备份(`pg_dump` 相关表)。
2. **dry-run 模式**:脚本支持 `--dry-run`,仅输出迁移报告(将生成多少条 JSON、多少条快照、回填多少实例),不写库。
3. **分批 + 可重入**:按 `DefinitionId` 分批处理,每批独立事务;脚本可重复执行(幂等:`definition_json` 已非空则跳过)。
4. **回滚**:若迁移后引擎异常,回滚步骤:
- 恢复 `definition_json` 原值(备份还原);
- 保留关系表数据(P4 之前关系表不 drop,是安全网);
- ProcessEngine 通过特性开关回退到读关系表的旧路径(P1 改造时即应保留双读开关,详见 9.1)。
5. **关系表保留期**:`wf_workflow_nodes` / `wf_workflow_edges` 在 P4 之前**禁止 drop**,作为整个迁移期的最终回滚保障。
---
## 第 8 章 分阶段实施路线
| 阶段 | 内容 | 交付物 |
|---|---|---|
| **P0** | 文档评审(本文) | ✅ 本文档定稿 |
| **P1** | 后端契约改造 | ✅ 已实现:`WorkflowGraph`/`WorkflowShapeMap` 解析层、`WorkflowDefinitionSnapshot` 快照表(含 Migration)、`WorkflowInstance.DefinitionVersion` 字段、Published 锁定(Draft 校验)、ProcessEngine 适配层切换读快照(双读开关)、整图保存接口、`WorkflowGraphValidator` 发布校验(9 条规则)、`WorkflowGraphMigration` 存量迁移脚本(幂等,接入 DatabaseInitializer)。423 单测全通过。 |
| **P2** | 前端设计器骨架 | ✅ 已实现:X6 画布(复刻 AgentFlow 配置)+ Stencil 拖拽面板(3 分组 9 节点)+ 连线 + 端口 hover 显隐 + 整图 toJSON/fromJSON 保存加载闭环。typecheck + build 通过。 |
| **P3** | 节点配置弹窗 | ✅ 已实现:节点配置抽屉(双击打开,按 NodeType 动态渲染表单)含审批(approvalMode/assigneeRule/timeoutMinutes/钩子)、抄送(recipients)、子流程(definitionId)、定时(delayMinutes/targetTime)。 |
| **P4** | 收尾 | ⏳ 待办:列表/发布页(列表页已做,启停页待补)、关系表 drop、6 套废弃接口移除。注:6 套接口已标 `[Obsolete]`。 |
---
## 第 9 章 风险与待决项
### 9.1 风险
| 风险 | 说明 | 应对 |
|---|---|---|
| **运行实例无版本隔离(现有缺陷,致命)** | `WorkflowInstance` 无 Version 字段,引擎按 DefinitionId 实时读图;发布后改图会污染运行中实例 | **7.2 方案强制实施**:新增 `DefinitionVersion` 字段 + 发布快照表,引擎读不可变快照 |
| **Published 后可被 PUT 覆盖(现有漏洞)** | `UpdateWorkflowDefinitionCommandHandler` 无状态校验,已发布定义可被改 | **7.3 方案强制实施**:Update Handler 增加 Draft 状态校验 |
| **双源一致性(迁移期)** | P1 期间 JSON 与关系表并存 | ProcessEngine 保留特性开关(双读),默认读快照,异常可切回关系表;Draft 编辑只走 JSON |
| **存量已运行实例的 Version 回填** | 迁移期已运行的实例无 DefinitionVersion,按什么值回填? | 取其定义当前 Version 回填(接受「存量实例绑定到当前版本」的近似;P1 切换前应停止新发起,迁移完再恢复) |
| **整图 JSON 体积** | 复杂图可能较大 | 监控单图大小,必要时拆分子流程(已有 SubProcess 节点) |
| **前端节点交互复杂度** | 多人会签、条件表达式编辑器工作量大 | P3 独立子方案,工作量预期见 9.3 |
| **`NodeConfigParser` 兼容** | 当前解析字符串,改造后是对象 | 适配层兼容,或迁移完成后简化 |
### 9.2 待决项(需评审拍板)
1. **节点/边 ID 生成方**:当前建议前端生成 `node_<guid>` / `edge_<guid>`,后端透传。是否允许后端保存时重写 id?(影响 `source.cell` 引用稳定性)
2. **Published 流程的派生编辑**:7.3 锁定 Published 后,是否提供「复制已发布流程 → 新 Draft」的派生编辑能力?(建议提供,作为修改已发布流程的标准路径)
3. **shape ↔ NodeType 映射表存放位置**:建议放 Domain 层 `WorkflowShapeMap` 静态类。
4. **`WorkflowGraph` 是否强类型化**:建议实现 `WorkflowGraph`/`GraphNode`/`GraphEdge` 强类型模型(record),避免裸 `JsonDocument` 散落。是否纳入本次标准?(建议纳入 P1)
5. **P1 切换窗口**:存量实例 Version 回填存在近似(9.1),是否接受「P1 切换前暂停新发起、迁移完恢复」的停机窗口?还是要求零停机(零停机需更复杂的双写过渡)?
### 9.3 工作量预期(诚实声明,避免评审低估)
「X6 自建」意味着审批相关节点 UI **全部自写**(基于现有代码,仓库无任何 X6 前端代码可复用):
- P2 设计器骨架(画布/拖拽/连线/保存):~1-2 周
- **P3 节点配置弹窗是工作量重头**,尤其:
- 审批人规则编辑器(单人/会签/或签、角色/部门/指定人、加签)—— 复杂,~1-2 周
- 条件表达式编辑器(变量选择 + 比较 + 与/或组合)—— 复杂,~1 周
- 抄送人选择、子流程选择、定时配置 —— 较简单,~3-5 天
- P1 后端改造(含快照表/Version 字段/Migration/引擎切换/迁移脚本):~1-1.5 周
> P3 的复杂子项(审批人规则、条件表达式)建议在 P2 闭环验证后,另起详细子方案再实现,不在本文展开。
---
## 附录 A:关键文件路径清单
### 后端(`build-flow.api/`)
**Domain**
- `src/modules/workflow/Domain/Entities/WorkflowDefinition.cs` — 定义实体(DefinitionJson 字段在此)
- `src/modules/workflow/Domain/Entities/WorkflowNode.cs` — 节点实体(迁移源)
- `src/modules/workflow/Domain/Entities/WorkflowEdge.cs` — 边实体(迁移源)
- `src/modules/workflow/Domain/Entities/WorkflowInstance.cs` — **实例实体(待新增 DefinitionVersion 字段)**
- `src/modules/workflow/Domain/Entities/FormDefinitionVersion.cs` — **版本快照先例(wf_workflow_definition_snapshots 照此范式新建)**
- `WorkflowDefinitionSnapshot`(**待新增**) — 发布快照实体,参照 `FormDefinitionVersion`
- `src/modules/workflow/Domain/Enums/NodeType.cs` — 节点类型枚举(复用)
- `src/modules/workflow/Domain/Enums/EdgeType.cs` — 边类型枚举(复用)
- `src/modules/workflow/Domain/Enums/DefinitionStatus.cs` — 定义状态枚举(复用)
**Application**
- `src/modules/workflow/Application/Engine/ProcessEngine.cs` — 流程引擎(改造核心,~1191 行)
- `src/modules/workflow/Application/Engine/NodeConfigParser.cs` — Config 解析器(需适配)
- `src/modules/workflow/Application/Features/WorkflowDefinitions/Commands/UpdateWorkflowDefinitionCommand.cs` — 更新命令
- `src/modules/workflow/Application/Features/WorkflowDefinitions/Handlers/UpdateWorkflowDefinitionCommandHandler.cs` — 更新处理器(仅覆盖字符串)
- `src/modules/workflow/Application/Features/WorkflowDefinitions/DTOs/WorkflowDefinitionDto.cs`
- `src/modules/workflow/Application/Features/WorkflowDefinitions/DTOs/WorkflowDefinitionDetailDto.cs`
- `src/modules/workflow/Application/Features/WorkflowDefinitions/DTOs/WorkflowNodeDto.cs`
- `src/modules/workflow/Application/Features/WorkflowDefinitions/DTOs/WorkflowEdgeDto.cs`
**Infrastructure**
- `src/modules/workflow/Infrastructure/Persistence/WorkflowDbContext.cs` — DbSet 定义(WorkflowNodes/WorkflowEdges)
- `src/modules/workflow/Infrastructure/Persistence/Configurations/WorkflowDefinitionConfiguration.cs`
- `src/modules/workflow/Infrastructure/Persistence/Configurations/WorkflowNodeConfiguration.cs`
- `src/modules/workflow/Infrastructure/Persistence/Configurations/WorkflowEdgeConfiguration.cs`
- `src/modules/workflow/Infrastructure/Persistence/SeedData.cs` — 种子数据
**Host**
- `src/BuildFlow.Host/Endpoints/workflow/WorkflowDefinition/` — 24 个 Endpoint/Request 文件(含 6 套待废弃 Node/Edge 接口)
**Shared**
- `src/shared/BuildFlow.Shared/Api/ApiResponse.cs` — 响应信封
- `src/shared/BuildFlow.Shared/Api/ApiResponseMiddleware.cs` — 响应包裹中间件
### 前端(`build-flow.web/apps/web-antdv-next/`)
- `src/api/request.ts` — API 客户端(`requestClient`)
- `src/router/routes/modules/` — 路由 module 目录(待新增 `workflow.ts`)
- `src/views/` — 页面目录(待新增 `workflow/`)
- `src/api/core/` — API 模块目录(待新增 `workflow.ts`)
### 参考文档
- X6 官方文档:`https://x6.antv.antgroup.com/`
- X6 Vue 节点:`https://x6.antv.antgroup.com/tutorial/intermediate/vue`
- X6 连接桩:`https://x6.antv.antgroup.com/tutorial/basic/port`
- rag-coding-web 技能:BuildFlow 管理端前端开发约定