# 工作流增删改查接口说明(Workflow CRUD API) > 版本:v1.0 > 适用范围:平台前端、运维工具对工作流元数据与图结构进行创建、查询、修改、删除、分类绑定等管理操作。 > 实现来源:`backend/src/main/java/com/agent/management/controller/WorkflowController.java`、`service/impl/WorkflowServiceImpl.java`、`parser/WorkflowNameNormalizer.java`。 > 如需第三方系统提交运行并订阅执行流,请参考 [`external-workflow-api-spec.md`](./external-workflow-api-spec.md)。 --- ## 1. 设计要点 | 要点 | 说明 | |---|---| | 标识符 | 工作流对外暴露的唯一标识为 `name`(kebab-case),URL 路由全部按 `name` 寻址;旧数字 ID 作为字符串回退兼容(路径参数 `{name}` 若为纯数字且无 kebab-case 匹配,则按 ID 查询) | | 名称规范 | `name` 必须为 kebab-case:`^[a-z0-9]+(-[a-z0-9]+)*$`,长度 ≤ 64;非法字符在创建/重命名时抛 `BusinessException`(HTTP 400) | | 中文名 | `displayName` 承载可读性与本地化,可为任意字符;未提供时默认取 `name` | | 图结构校验 | 保存 `graphData` 时强制校验:至少 1 个 `userInput` 节点 + 至少 1 个 `output` 节点,且二者之间存在长度 ≥ 2 的有向路径(中间至少经过 1 个非输出节点) | | 部分更新 | 修改接口对 `displayName`/`description`/`graphData` 支持部分更新:字段为 `null` 时跳过,不覆盖原值 | | 版本快照 | 保存接口在持久化后自动创建一份版本快照(双轨目录:优先 `name`,回退 `id`);快照失败不影响保存结果,仅 WARN 日志 | | 分类绑定 | 工作流分类(`categoryId`)通过独立接口设置;`categoryId = null` 表示取消分类 | --- ## 2. 通用约定 ### 2.1 基础路径 ``` http(s)://:/api/workflows ``` - 默认端口:`2438`(见 `application.yml: server.port`) - 路径前缀:`/api/workflows`(与对外发布的 `/api/v1/workflows` 隔离,前者用于平台内部管理,后者需 `X-API-Key` 鉴权) ### 2.2 内容类型 | 场景 | Content-Type | |---|---| | 请求体(JSON) | `application/json` | | 响应体 | `application/json` | ### 2.3 统一响应包络 所有接口统一返回 `Result`: ```json { "code": 200, "message": "success", "data": { ... } } ``` 错误时: ```json { "code": 400, "message": "工作流名称必须为 kebab-case 格式(仅小写字母、数字、短横线,禁用大写/空格/下划线/中文): 我的流程", "data": null } ``` ### 2.4 错误码 | code | 含义 | 触发场景 | |---|---|---| | `200` | 成功 | 正常返回 | | `400` | 业务校验失败 | 名称非法、名称重复、图结构不合法、工作流不存在 | | `500` | 服务器内部错误 | 未捕获异常(堆栈已记录到日志,响应体不泄漏) | > 业务异常(`BusinessException`)默认 code = 400,HTTP 状态码仍为 200,由 `Result.code` 区分;系统异常 HTTP 状态码 = 500。 ### 2.5 标识符与字段 | 字段 | 类型 | 说明 | |---|---|---| | `id` | number | 工作流主键(内部使用,URL 不依赖) | | `name` | string | kebab-case 唯一标识,URL 路由使用此字段;旧数据迁移后默认值为 `String.valueOf(id)`(纯数字天然合法) | | `displayName` | string | 中文显示名,可为任意字符 | | `description` | string | 工作流描述(用途、输入输出、注意事项等) | | `graphData` | string | 节点和边数据,JSON 字符串;空图 = `{"nodes":[],"edges":[]}` | | `categoryId` | number | 分类 ID,可空 | | `categoryName` | string | 分类名称(仅响应体,由后端关联查询填充) | | `tags` | array | 关联标签列表(仅响应体) | | `createdAt` / `updatedAt` | string (ISO-8601) | 创建/更新时间 | --- ## 3. 接口总览 | # | 方法 | 路径 | 用途 | |---|---|---|---| | 3.1 | `GET` | `/api/workflows` | 分页查询工作流列表(支持搜索、分类过滤) | | 3.2 | `GET` | `/api/workflows/{name}` | 查询单个工作流详情 | | 3.3 | `POST` | `/api/workflows` | 创建工作流(仅元数据,图数据默认为空) | | 3.4 | `POST` | `/api/workflows/{name}/save` | 保存工作流(元数据 + 图数据,支持部分更新 + 自动版本快照) | | 3.5 | `POST` | `/api/workflows/{name}/delete` | 删除工作流 | | 3.6 | `PUT` | `/api/workflows/{name}/category` | 设置/取消工作流分类 | --- ## 4. 接口详细规范 ### 3.1 分页查询工作流列表 **请求** ``` GET /api/workflows?search={kw}&categoryId={id}&page={page}&size={size} ``` **查询参数** | 参数 | 类型 | 必填 | 默认 | 说明 | |---|---|---|---|---| | `search` | string | 否 | 空 | 关键字,匹配 `name` / `displayName` / `description`(大小写不敏感,子串匹配) | | `categoryId` | number | 否 | 空 | 分类 ID;过滤时包含子孙分类(调用 `WfCategoryService.getDescendantCategoryIds` 展开后再过滤) | | `page` | number | 否 | `1` | 页码,从 1 开始 | | `size` | number | 否 | `12` | 每页条数 | **响应** ```json { "code": 200, "message": "success", "data": { "items": [ { "id": 12, "name": "research-report", "displayName": "研究报告生成", "description": "根据主题生成结构化研究报告", "graphData": "{\"nodes\":[...],\"edges\":[...]}", "categoryId": 3, "categoryName": "研发辅助", "createdAt": "2026-07-01T08:30:00", "updatedAt": "2026-07-15T10:12:33", "tags": [ { "id": 5, "name": "报告" }, { "id": 8, "name": "LLM" } ] } ], "total": 1, "page": 1, "size": 12 } } ``` > **实现说明:** 列表先全量加载 (`WorkflowService.listWorkflows`) 再内存过滤分页,适合工作流数量 < 1000 的场景;若规模增长需切换为数据库分页。 --- ### 3.2 查询单个工作流详情 **请求** ``` GET /api/workflows/{name} ``` **路径参数** | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | `name` | string | 是 | 工作流 `name`(kebab-case);若为纯数字字符串且无 kebab-case 匹配,回退按 `id` 查询(兼容旧 URL) | **响应** ```json { "code": 200, "message": "success", "data": { "id": 12, "name": "research-report", "displayName": "研究报告生成", "description": "根据主题生成结构化研究报告", "graphData": "{\"nodes\":[...],\"edges\":[...]}", "categoryId": 3, "categoryName": "研发辅助", "createdAt": "2026-07-01T08:30:00", "updatedAt": "2026-07-15T10:12:33", "tags": [ { "id": 5, "name": "报告" } ] } } ``` **错误码** | code | 触发场景 | |---|---| | `400` | 工作流不存在:`工作流不存在: {name}` | --- ### 3.3 创建工作流 **请求** ``` POST /api/workflows Content-Type: application/json ``` **请求体** ```json { "name": "research-report", "displayName": "研究报告生成", "description": "根据主题生成结构化研究报告" } ``` **字段说明** | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `name` | string | 是 | kebab-case 唯一标识;服务端会执行 `normalize + validate`:转小写、空格/下划线转短横线、合并连续短横线、剔非法字符,最终必须匹配 `^[a-z0-9]+(-[a-z0-9]+)*$` 且长度 ≤ 64 | | `displayName` | string | 否 | 中文名;为空时默认取规范化后的 `name` | | `description` | string | 否 | 工作流描述 | **响应** ```json { "code": 200, "message": "success", "data": { "id": 13, "name": "research-report", "displayName": "研究报告生成", "description": "根据主题生成结构化研究报告", "graphData": "{\"nodes\":[],\"edges\":[]}", "categoryId": null, "categoryName": null, "createdAt": "2026-07-15T10:15:00", "updatedAt": "2026-07-15T10:15:00", "tags": [] } } ``` > 创建时 `graphData` 默认为 `{"nodes":[],"edges":[]}`(空图),不触发图结构校验;后续通过 §3.4 保存时才校验。 **错误码** | code | 触发场景 | |---|---| | `400` | 名称非法:`工作流名称必须为 kebab-case 格式(仅小写字母、数字、短横线,禁用大写/空格/下划线/中文): {input}` | | `400` | 名称重复:`工作流名称已存在: {name}` | --- ### 3.4 保存工作流(更新) **请求** ``` POST /api/workflows/{name}/save Content-Type: application/json ``` **路径参数** | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | `name` | string | 是 | 目标工作流 `name`(或旧数字 ID) | **请求体** ```json { "name": "research-report-v2", "displayName": "研究报告生成(增强版)", "description": "增加了多源检索与图表生成", "graphData": "{\"nodes\":[...],\"edges\":[...]}" } ``` **字段说明** | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `name` | string | 否 | 新名称;非空时执行规范化 + 唯一性校验(排除自身 ID),通过后更新;与原值相同则跳过唯一性校验 | | `displayName` | string | 否 | 为 `null` 时保留原值;为空串时覆盖为空串 | | `description` | string | 否 | 为 `null` 时保留原值 | | `graphData` | string | 否 | 为 `null` 时保留原图;非空时先经 `NodeTypeUtils.normalizeGraphData` 规范化,再走图结构校验 | > **部分更新语义:** 任一字段为 `null` 都不会被覆盖;如需清空 `description`,请显式传 `""`。 **响应** ```json { "code": 200, "message": "success", "data": { "id": 13, "name": "research-report-v2", "displayName": "研究报告生成(增强版)", "description": "增加了多源检索与图表生成", "graphData": "{\"nodes\":[...],\"edges\":...}", "categoryId": 3, "categoryName": "研发辅助", "createdAt": "2026-07-01T08:30:00", "updatedAt": "2026-07-15T10:20:00", "tags": [...] } } ``` **副作用:自动版本快照** 保存成功后,控制器会调用 `WorkflowVersionServiceImpl.createSnapshot(workflowId, name, graphData, name, null)` 创建一份版本快照(双轨目录:优先 `name`,回退 `id`)。快照创建失败不影响保存结果,仅输出 WARN 日志: ``` [Save] 创建版本快照失败,不影响保存: {errorMessage} ``` 快照可用于回滚(`POST /api/workflows/{name}/versions/{version}/rollback`)与历史对比。 **错误码** | code | 触发场景 | |---|---| | `400` | 工作流不存在:`工作流不存在: {name}` | | `400` | 新名称非法:`工作流名称必须为 kebab-case 格式...` | | `400` | 新名称重复:`工作流名称已存在: {name}` | | `400` | 图数据为空:`工作流图数据不能为空` | | `400` | 缺少输入节点:`工作流必须包含至少一个用户输入节点` | | `400` | 缺少输出节点:`工作流必须包含至少一个输出节点` | | `400` | 输入输出未连通:`用户输入节点与输出节点必须通过其他节点连接起来` | **图结构校验规则** 1. 至少 1 个 `userInput` 类型节点 2. 至少 1 个 `output` 类型节点 3. 从任一 `userInput` 到任一 `output` 必须存在有向路径,且路径上至少包含 1 个非输出中间节点(直接 `userInput → output` 边不满足) 4. DAG 无环(由 `DagResolver.resolve` 隐式保证) --- ### 3.5 删除工作流 **请求** ``` POST /api/workflows/{name}/delete ``` **路径参数** | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | `name` | string | 是 | 目标工作流 `name`(或旧数字 ID) | 无请求体。 **响应** ```json { "code": 200, "message": "success", "data": null } ``` **错误码** | code | 触发场景 | |---|---| | `400` | 工作流不存在:`工作流不存在: {name}` | > **注意:** 当前实现仅删除工作流记录本身,不会级联清理: > - 历史运行记录(`WorkflowRun` / `WorkflowRunNode`) > - 工作目录文件(`/workflow-runs/{workflowId}/{runId}/`) > - 版本快照目录(`/workflow-versions/{name|id}/`) > - 标签关联(`TagAssignment` 中 `entityType=workflow, entityId={id}`) > > 如需彻底清理,请手动调用运行历史与版本管理接口,或编写专门的清理脚本。 --- ### 3.6 设置/取消工作流分类 **请求** ``` PUT /api/workflows/{name}/category Content-Type: application/json ``` **路径参数** | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | `name` | string | 是 | 目标工作流 `name`(或旧数字 ID) | **请求体** ```json { "categoryId": 3 } ``` **字段说明** | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `categoryId` | number | 否 | 分类 ID;传 `null` 表示取消分类(`categoryId` 置空) | **响应** ```json { "code": 200, "message": "success", "data": null } ``` > 接口不校验 `categoryId` 是否存在(即使分类被删除,工作流 `categoryId` 仍保留原值),列表查询时 `categoryName` 关联查询失败则为 `null`。 **错误码** | code | 触发场景 | |---|---| | `400` | 工作流不存在:`工作流不存在: {name}` | --- ## 5. curl 调用示例 ### 5.1 创建工作流 ```bash curl -X POST http://localhost:2438/api/workflows \ -H "Content-Type: application/json" \ -d '{ "name": "research-report", "displayName": "研究报告生成", "description": "根据主题生成结构化研究报告" }' ``` ### 5.2 查询列表(带搜索 + 分类过滤) ```bash curl "http://localhost:2438/api/workflows?search=report&categoryId=3&page=1&size=20" ``` ### 5.3 查询单个详情 ```bash curl http://localhost:2438/api/workflows/research-report # 兼容旧数字 ID curl http://localhost:2438/api/workflows/12 ``` ### 5.4 保存(更新图结构 + 重命名) ```bash curl -X POST http://localhost:2438/api/workflows/research-report/save \ -H "Content-Type: application/json" \ -d '{ "name": "research-report-v2", "displayName": "研究报告生成(增强版)", "description": "增加了多源检索", "graphData": "{\"nodes\":[{\"id\":\"n1\",\"type\":\"userInput\",\"data\":{\"label\":\"输入\"}},{\"id\":\"n2\",\"type\":\"llm\",\"data\":{\"label\":\"生成\"}},{\"id\":\"n3\",\"type\":\"output\",\"data\":{\"label\":\"输出\"}}],\"edges\":[{\"source\":\"n1\",\"target\":\"n2\"},{\"source\":\"n2\",\"target\":\"n3\"}]}" }' ``` ### 5.5 仅修改描述(部分更新) ```bash curl -X POST http://localhost:2438/api/workflows/research-report/save \ -H "Content-Type: application/json" \ -d '{ "description": "更新后的描述,其他字段保持不变" }' ``` > 注意:`name` / `displayName` / `graphData` 未传(或为 `null`)时不会覆盖原值。 ### 5.6 删除工作流 ```bash curl -X POST http://localhost:2438/api/workflows/research-report/delete ``` ### 5.7 设置分类 ```bash curl -X PUT http://localhost:2438/api/workflows/research-report/category \ -H "Content-Type: application/json" \ -d '{ "categoryId": 3 }' ``` ### 5.8 取消分类 ```bash curl -X PUT http://localhost:2438/api/workflows/research-report/category \ -H "Content-Type: application/json" \ -d '{ "categoryId": null }' ``` --- ## 6. 名称规范化规则详解 `WorkflowNameNormalizer` 提供 `normalize` 与 `normalizeStrict` 两个方法,创建/重命名时使用 `normalizeStrict`: | 步骤 | 操作 | 示例 | |---|---|---| | 1. 转小写 + trim | `"My Workflow"` → `"my workflow"` | — | | 2. 空格/下划线转短横线 | `"my workflow_v2"` → `"my-workflow-v2"` | — | | 3. 合并连续短横线 | `"my--workflow"` → `"my-workflow"` | — | | 4. 去首尾短横线 | `"-my-workflow-"` → `"my-workflow"` | — | | 5. 剔除非法字符 | `"my-workflow中文"` → `"my-workflow"` | 中文、特殊符号被移除 | | 6. 校验最终结果 | 必须匹配 `^[a-z0-9]+(-[a-z0-9]+)*$` 且 ≤ 64 字符 | 不匹配则抛 `BusinessException` | **典型用例:** | 输入 | 规范化后 | 是否合法 | |---|---|---| | `research-report` | `research-report` | ✅ | | `Research Report` | `research-report` | ✅ | | `research_report_v2` | `research-report-v2` | ✅ | | `research--report` | `research-report` | ✅ | | `研究报告` | ``(空) | ❌ 全部被剔除 | | `report!` | `report` | ✅ | | `-invalid` | `invalid` | ✅ | | `a` | `a` | ✅ | | ``(空串) | `` | ❌ 长度为 0 | --- ## 7. 与版本管理接口的协作 保存接口(§3.4)会自动创建版本快照,相关版本管理接口(已在 `WorkflowController` 实现,本文不展开): | 接口 | 方法 | 路径 | |---|---|---| | 列出版本 | `GET` | `/api/workflows/{name}/versions` | | 查询版本内容 | `GET` | `/api/workflows/{name}/versions/{version}/content` | | 回滚到版本 | `POST` | `/api/workflows/{name}/versions/{version}/rollback` | | 修改版本说明 | `PUT` | `/api/workflows/{name}/versions/{version}/message` | 回滚操作内部会调用 `WorkflowService.updateWorkflow` 恢复 `graphData`,并触发与 §3.4 相同的图结构校验。 --- ## 8. 限制与约束 | 项目 | 限制 | 说明 | |---|---|---| | `name` 长度 | ≤ 64 字符 | `WorkflowNameNormalizer.MAX_LENGTH` | | `name` 字符集 | `[a-z0-9-]` | kebab-case 严格匹配 | | `graphData` 大小 | 无显式限制 | 受 `spring.servlet.multipart.max-request-size`(默认 500MB)间接约束;前端建议 < 1MB | | 图节点数 | 无显式限制 | 实际受 DAG 解析性能约束,建议 < 100 | | 并发更新 | 无乐观锁 | 同一工作流并发 `save` 可能后写覆盖先写;多用户协作场景需前端层面加锁 | | 删除级联 | 不清理历史运行/版本/工作目录 | 见 §3.5 注意事项 | | 名称重命名 | 允许 | 重命名后旧 `name` URL 失效;历史运行目录仍按原 `id` 寻址,不受影响 | --- ## 9. 与现有内部接口的映射关系 | 接口 | Controller 方法 | Service 方法 | 关键实现 | |---|---|---|---| | `GET /api/workflows` | `WorkflowController.list` | `WorkflowService.listWorkflows` | 内存过滤分页 + `WfCategoryService.getDescendantCategoryIds` 展开子孙分类 | | `GET /api/workflows/{name}` | `WorkflowController.get` | `WorkflowService.getWorkflow` | `name` 优先,回退 `id`(数字字符串匹配) | | `POST /api/workflows` | `WorkflowController.create` | `WorkflowService.createWorkflow` | `WorkflowNameNormalizer.normalizeStrict` + `existsByNameExcluding` 唯一性校验 | | `POST /api/workflows/{name}/save` | `WorkflowController.save` | `WorkflowService.updateWorkflow` | 部分更新 + `NodeTypeUtils.normalizeGraphData` + `validateWorkflowGraph` 图结构校验 + 自动版本快照 | | `POST /api/workflows/{name}/delete` | `WorkflowController.delete` | `WorkflowService.deleteWorkflow` | `getWorkflow` + `workflowRepo.deleteById` | | `PUT /api/workflows/{name}/category` | `WorkflowController.setCategory` | `WorkflowService.updateCategory` | 直接更新 `categoryId`(允许 `null`) |