# FFOA 流程图渲染器（flow-renderer）

> **能力**：AI 写 YAML → `node flow-renderer.mjs in.yaml out.svg` → 飞书风格 v0.2 垂直泳道 SVG。
>
> **零运行时依赖**：产物是纯 inline SVG，浏览器/邮件/Gitea/PDF/Word 都能嵌。

---

## 给 AI 的快速上手

> 这一段是为后续 AI 协作写的。**新 AI 接到"画一个流程图"的需求时，按以下步骤做**。

### 1. 命令（在 slot 根目录跑）

```bash
node docs/templates/flow-renderer.mjs <input.yaml> <output.svg>
```

例：

```bash
node docs/templates/flow-renderer.mjs docs/templates/business-trip.yaml docs/templates/business-trip-custom.svg
```

依赖 `backend/node_modules/js-yaml`（已通过 `createRequire` 自动找到，无需手动 install）。

### 2. 最小 YAML 模板（复制即改）

```yaml
title: 流程标题

# 左侧"动作"标签列（每行 = 一个 row 的动作名；可选，不写则不显示）
actions:
  - 提交 / 审批       # row 0 的动作
  - 行程安排          # row 1
  - ...

# 部门泳道（顺序 = 从左到右；color/stroke/text 飞书风格 header 不用，向后兼容保留）
lanes:
  - {id: applicant, name: 申请人,   color: "#FFF8E1", stroke: "#FBC02D", text: "#5D4037"}
  - {id: manager,   name: 部门主管, color: "#E1F5FE", stroke: "#0288D1", text: "#01579B"}
  - {id: hr,        name: HR 部门,  color: "#F3E5F5", stroke: "#8E24AA", text: "#4A148C"}
  - {id: finance,   name: 财务部门, color: "#E8F5E9", stroke: "#388E3C", text: "#1B5E20"}
  - {id: ceo,       name: 总经理,   color: "#FCE4EC", stroke: "#C2185B", text: "#880E4F"}
  - {id: system,    name: 系统,     color: "#ECEFF1", stroke: "#546E7A", text: "#263238"}

# 节点（row 必须按流程时间递增，相同 row 表示同时发生）
nodes:
  - {id: submit,  lane: applicant, row: 0, shape: oval, label: ① 提交申请}
  - {id: review,  lane: manager,   row: 0, shape: rect, label: ② 审批}
  - {id: notify,  lane: system,    row: 1, shape: end-oval, label: ③ 通知}

# 流转线
edges:
  - {from: submit, to: review,  label: 通过, style: approve}
  - {from: review, to: notify,                style: normal}
  - {from: review, to: submit,  label: 驳回, style: reject}

# 右侧备注（可选；按 row 网格化，每个 row 一格）
sidebar:
  title: 备注
  rows:
    - 申请人填基础信息  # row 0
    - 完成归档           # row 1

# 节点附注便签（可选；attach 到节点 id，position 默认 right）
notes:
  - {attach: review, position: bottom, text: "SLA 24h\nAI 辅助"}
```

### 3. 节点形状（9 种 BPMN 2.0 兼容）

| shape | 形态 | 用途 |
|---|---|---|
| `oval` | 椭圆 | **起始事件** |
| `end-oval` | 粗红边椭圆 | **结束事件** |
| `rect` | 圆角矩形 | **普通任务**（默认） |
| `subprocess` | 圆角矩形 + 右下 `+` | **子流程**（含嵌套流程） |
| `diamond` | 普通菱形 | **通用决策** |
| `xor` | 菱形 + 右上 `×` | **排他网关**（只能走一条分支） |
| `parallel` | 菱形 + 右上 `+` | **并行网关**（多条同时走） |
| `intermediate` | 双圆 | **中间事件**（如定时触发） |
| `data` | 文档形（右上折角） | **数据对象** |

### 4. Edge style（4 种）

| style | 视觉 | 用途 |
|---|---|---|
| `normal`（默认） | 灰色实线 | 普通流转 |
| `approve` | 灰线 + 绿色加粗 label | **通过** / **同意** 等正向 |
| `reject` | 红色虚线 + 红粗 label | **驳回** / **拒绝**（自动走顶部廊道） |
| `async` / `notify` | 蓝色虚线 + 蓝 label | **异步通知** / **消息流** |

### 5. `row` 字段语义（重要）

- `row` 是**整数**，表示节点在流程时间轴上的位置（从 0 开始）
- **同 row** = 同时发生（横向排列在不同 lane）
- **row 递增** = 时间往前推进（每个节点占一个网格行）
- **跨多 row** 跳跃 = 中间留空（OK，不必连续）

**正确**（对角下降）：
```yaml
- {id: submit,  row: 0}  # ① 起
- {id: review,  row: 0}  # ② 跟 ① 同时
- {id: book,    row: 1}  # ③ 在 ② 之后
- {id: budget,  row: 2}  # ④
```

**错误**（全堆在 row 0）：
```yaml
- {id: submit,  row: 0}
- {id: review,  row: 0}
- {id: book,    row: 0}  # ❌ 会跟 ① ② 重叠在最顶行
```

### 6. 中文 edge label 约定

| 业务语义 | 推荐 label |
|---|---|
| 通过 / 同意 | `通过` + `style: approve` |
| 驳回 / 拒绝 | `驳回` + `style: reject` |
| 退回修改 | `退回` + `style: reject` |
| 异步通知 | `通知` + `style: async` |
| 决策 yes/no | `是` / `否`（normal style） |
| 大额/小额 | `大额` / `小额`（normal style） |

### 7. 嵌入到 HTML

```html
<img src="business-trip-custom.svg" alt="出差申请流程图">
```

或 inline 嵌入（复制 SVG 文件内容到 HTML body）。两种方式都零依赖。

### 8. sidebar 设计原则（用业务自然语言写"每步在做什么"）

业务方看流程图时想知道：**这一步在哪个系统里、用什么动词、对什么对象做了什么** — 不是"哪个员工 + 用啥工具"，也不只是字段名堆砌。

| ❌ 错（堆人名/系统名）| ❌ 错（堆字段名）| ✅ 对（自然语言动作）|
|---|---|---|
| `Sherry Ding · Excel` | `创建占位 SN · 采购单 PO 号` | `在主档创建采购单 PO，含占位 SN` |
| `Fiona · D365` | `销售订单号 · 生命周期=Reserved` | `创建销售订单 SO，绑定客户与机器人` |
| `Hongliang · Mike · SAP` | `位置=HQ · 状态(Build)=Received` | `★ 激活：占位 SN → 正式 SN（每台实例化）` |

**原则**：
- 用 **动词开头** 的自然语言短句（创建 / 更新 / 登记 / 核验 / 激活 / 执行 / 归集 / 注销）
- 表达 "**在哪做 + 对谁做 + 变成什么**"，让业务方读一行就明白意图
- **系统名可以写**——但要作为"在 SAP / 主档 / D365 里做"的业务描述的一部分，不是 `· SAP` 这种贴标签
- **不写人名**：责任人是元信息，需要时进节点 `notes`
- 显式标注 **状态对象层级**（PO 级 / 机器人级 / 订单级），尤其在层级切换点
- **用 ★ 标记关键转换点**（如占位 SN → 正式 SN 实例化、状态控制权交接），让滚动时一眼看到
- 每条 ≤ 18 中文字符（超出会自动换行，但 row 高度有限制；建议精炼到 ~15 字）

**好处**：
- 业务方滚一遍 sidebar 就读懂整条业务逻辑，不用一行一行点节点
- 状态/数据动作的"逻辑通顺"显式可见（例如 row 0-5 都是 "更新 PO" → row 6 "激活" → row 7+ "每台" 自然过渡）
- 跟节点 label（简短状态名）形成互补：label 是 "是什么状态"，sidebar 是 "怎么变到这个状态"

参考：`docs/modules/robot-manager/business-analysis/robot-lifecycle.yaml` 的 sidebar 段（28 行自然语言版）。

### 9. lane 合并（`mergedWith`）

业务上相邻 lane 属于同一部门 / 同一组（如"仓储"内含"主仓"和"分支仓"），可让两个 lane 在表头合并显示：

```yaml
lanes:
  - {id: warehouse,  name: 仓储, ...}
  - {id: branch,     name: 分支, ..., mergedWith: warehouse}
  - {id: sales,      name: 销售, ...}
```

效果：
- 表头 "仓储" 跨 warehouse + branch 两列**居中显示**
- 两 lane 之间的 vertical separator 自动去掉（不画分隔线）
- 各 lane 保留自己的背景色 / 颜色 stripe（视觉上仍能分辨子 lane）
- 节点定义不变，每个节点仍指定具体子 lane（warehouse / branch），定位精确

支持 2 个或更多 lane 合并到同一主 lane：`mergedWith: warehouse` 可以被多个子 lane 引用。

实例：`docs/modules/robot-manager/business-analysis/robot-lifecycle.yaml`（仓储 + 分支合并）。

### 10. 长流程图 — sticky 部门栏（`--split-header`）

垂直 row 多到滚动会看不到部门 lane header 时，用拆分模式：

```bash
node docs/templates/flow-renderer.mjs in.yaml --split-header out
# → out.header.svg  (1330×40 px，只画顶部部门栏)
# → out.body.svg    (1330×N   px，去掉 header 段)
```

HTML wrapper 用两个 inline SVG，sticky CSS 包外层：

```html
<div class="diagram-wrap">
  <div class="lane-sticky">    <!-- 整个部门栏滚动钉顶 -->
    <!-- inline out.header.svg -->
  </div>
  <div class="diagram-body">
    <!-- inline out.body.svg -->
  </div>
</div>
```

```css
.lane-sticky {
  position: sticky;
  top: 0;          /* 或贴在你的 page header / legend 下方 */
  z-index: 50;
  background: white;
  line-height: 0;   /* 消除 inline SVG 末尾的 baseline 空隙 */
}
.lane-sticky svg, .diagram-body svg {
  display: block; width: 100%; height: auto;
}
```

**为什么用拆 SVG 而不是 HTML 重画 lane header**：
- 两个 SVG 共享同 `viewBox` 起点 X 与列宽 → 浏览器**像素级自然对齐**，无需 `grid-template-columns` 套算
- 字体 / 颜色 / 边框来自同一 renderer → **永远视觉一致**，无双 source of truth
- 不需要 `margin-bottom: -40px` / `pointer-events: none` 之类 CSS 黑魔法

**实例**：`docs/modules/robot-manager/business-analysis/robot-lifecycle.{header,body,html}` —— 25 节点垂直流程图。

---

## YAML schema 完整参考

```typescript
interface FlowSpec {
  title: string                              // 流程标题（当前未渲染，作为 metadata）

  actions?: string[]                         // 左侧"动作"标签列（按 row 索引）

  lanes: Array<{                             // 部门泳道（必须 ≥ 1 条）
    id: string                               // 唯一标识，nodes 通过 lane 引用
    name: string                             // 部门名（显示在 header）
    color?: string                           // 向后兼容（v4 飞书风格不用，header 统一浅灰）
    stroke?: string                          // 同上
    text?: string                            // 同上
  }>

  nodes: Array<{                             // 节点
    id: string                               // 唯一标识，edges/notes 引用
    lane: string                             // 所属 lane.id
    row: number                              // 行号（从 0 开始，整数）
    shape: 'oval' | 'end-oval' | 'rect' | 'subprocess'
         | 'diamond' | 'xor' | 'parallel' | 'intermediate' | 'data'
    label: string                            // 显示文字（含编号如 ① ② ③）
  }>

  edges: Array<{                             // 流转线
    from: string                             // 源节点 id
    to: string                               // 目标节点 id
    label?: string                           // 流转 label（如"通过"）
    style?: 'normal' | 'approve' | 'reject' | 'async' | 'notify'
  }>

  sidebar?: {                                // 右侧备注栏（可选）
    title?: string                           // 默认"备注"——见下面 sidebar 设计原则
    rows?: string[]                          // 按 row 网格化（推荐），每条 ≤ 18 中文字符
    text?: string                            // 单块文本（向后兼容）
  }

  notes?: Array<{                            // 节点附注便签（可选）
    attach: string                           // 附着的节点 id
    text: string                             // 多行用 \n
    position?: 'right' | 'left' | 'top' | 'bottom'  // 默认 right
  }>
}
```

---

## 视觉风格（飞书风格 v4）

- **整体背景**：`#F7F8FA`（浅灰）
- **Header 背景**：统一 `#F5F7FA`（不再按部门彩色，更整齐）
- **Header 文字**：`#1F2329`（飞书深灰）
- **节点颜色按类型区分**（不按部门）：
  - oval 起始 → 绿 `#E8F5E9` / `#2E7D32`
  - end-oval 结束 → 红粗边 `#FFEBEE` / `#C62828`
  - rect / subprocess 任务 → 蓝 `#E3F2FD` / `#1565C0`
  - diamond / xor / parallel 决策 → 橙 `#FFF3E0` / `#E65100`
  - intermediate 中间事件 → 白 + 橙双圆
  - data 数据 → 黄 `#FFF8E1` / `#FBC02D`

---

## 间距规则（自动派生）

```
TOP_PAD = 16            ┐
HEADER_H = 40           │ → HEADER_TOP_Y, HEADER_BOTTOM_Y
CORRIDOR_TO_HEADER = 22 │
NODE_TO_CORRIDOR = 28   │ → REJECT_CORRIDOR_Y, TOP_OFFSET
NODE_H = 44             │
ROW_GAP = 140           │ → 最后节点 y
BOTTOM_PAD = 40         ┘ → svgHeight
```

**改任一常量，所有边界自动重算**——保证任何尺寸下节点不挤靠边界。

---

## 已知局限 / TODO

- 只支持垂直泳道（部门在顶部 header，时间从上到下）
- 一个流程只有一个 reject 廊道（多条 reject 线会重叠）
- `notes` 便签是绝对定位，多 notes 之间可能重叠（未做避让）
- Edge routing 是简单 L 型 + 中间通道，复杂图（如多分支汇聚）可能有交叉
- 不支持 SVG 内置交互（hover / 点击展开）—— 由消费方加

---

## 相关工单

- 关联 **#409**（审批 + 表单 AI-first 重设计）—— 流程图渲染是阶段 0「AI Form Spec 标准」的视图层
- 关联 **#410**（agent ↔ 业务模块接入规范）—— 未来可作为 `@AgentTool` 工具暴露给 agent

---

## 文件清单

```
docs/templates/
├── README.md                       本文件（使用说明）
├── flow-renderer.mjs               核心渲染器（~360 行 ESM TypeScript via JSDoc）
├── business-trip.yaml              示例 YAML（出差申请审批）
├── business-trip-custom.svg        渲染产物
└── custom.html                     浏览器预览页面（开发调试用）
```
