# 工作流增删改查接口说明（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)://<host>:<port>/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<T>`：

```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`）
> - 工作目录文件（`<data-dir>/workflow-runs/{workflowId}/{runId}/`）
> - 版本快照目录（`<data-dir>/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`） |
