---
name: flowchart-generator
description: >
  流程图生成器技能 (V10.5.4)。当用户上传业务文档（Word/PDF/Excel/图片/VSDX）需要梳理业务流程，或仅提供流程名称要求基于行业知识设计流程图，或已有 Draw.io 流程图仅需生成配套流程说明 Excel 时，触发此技能。默认生成 Draw.io 横向泳道流程图，支持纵向。支持 VSDX (Visio) 文件导入。流程图生成后**仅在用户确认流程图无误后**，配套生成 Excel 流程说明文档（含严格格式的活动操作说明）。Excel 生成采用 JSON 数据 + 固定 Runner 脚本方案，无需每次编写 Python 代码。
  This skill should be used when the user wants to analyze business documents or design a flowchart based on process name/description, generating a Draw.io swimlane flowchart in XML format. Also supports importing Visio (.vsdx) files.
agent_created: true
---

# 流程图生成器 (Flowchart Generator) — V10.5.4

> **发布日期**：2026-06-07 | **库版本**：drawio_lib.py V1.2.5 | **Excel 库**：flow_desc_xlsx.py V1.0 | **Excel Runner**：run_flow_desc.py V1.0
> 
> V10.5.4 新增**场景五**：已有 Draw.io 流程图，仅需生成流程说明 Excel。触发时机章节补充该场景的完整处理流程（解析 drawio → 合并预览 → AI 生成内容 → JSON+Runner 输出）。
> V10.5.3 新增 **JSON + 固定 Runner 方案**：Excel 生成从"每次编写 Python 脚本"改为"生成 data.json → 运行 run_flow_desc.py"，节省 15-25s 时间、~1000 token，并彻底消除 ASCII 双引号/JSON 转义/隐式拼接三个踩坑（坑1、坑2、坑4）。禁止字符扫描内置于 Runner 自动执行。
> V10.5.2 新增**脚本生成规范与踩坑规避**：包含 4 个常见踩坑的解决方案，文本禁止字符规范（~#/*&），以及生成脚本后强制验证流程。
> V10.5.1 新增**活动操作说明内容质量强制规范**：6.3.1 节定义执行类（6要素）和决策类（4要素）的模板化格式，附带好/差示例对比。强化 G5 门禁不可跳过规则。
> V10.5.0 新增**流程说明 Excel 生成**
> V10.4.2 新增**多行布局机制**
> V10.4.1 修复**协同框同伴节点级联右移bug**
> V10.4.0 新增 **VSDX 直接转换器（vsdx2drawio.py）**

## 技能目的

解析用户提供的业务文档（Word、PDF、Excel、VSDX 等格式），理解其中描述的业务运作逻辑，提取角色（泳道）与活动步骤，最终输出一份可直接导入 Draw.io 的**横向泳道流程图 XML 文件**。

---

## ⚠️ 强制门禁清单（Mandatory Gates）

> **本技能所有生成任务必须严格遵守以下门禁。每个门禁是执行流程中的"检查点"，未通过前不得进入下一步。违反任一门禁视为严重流程违规。**

| 门禁 | 名称 | 触发时机 | 强制动作 | 禁止动作 | 违反后果 |
|------|------|---------|---------|---------|---------|
| **G1** | 角色清单确认 | 解析/提取完成后、写生成脚本之前 | 向用户展示泳道清单表格，等待用户明确确认（"确认"/"继续"） | **严禁**在未获确认前写任何生成脚本或调用 `fc.layout()` | 输出文件无效，必须重做 |
| **G2** | 活动归属确认 | 角色清单确认后、写生成脚本之前 | 向用户展示活动→泳道归属表，等待确认 | **严禁**跳过归属确认直接生成 | 泳道分配错误，必须重做 |
| **G3** | 驳回/循环目标确认 | 活动归属确认后、写生成脚本之前 | 向用户展示驳回返回目标和循环目标，等待确认 | **严禁**自行推断驳回/循环目标 | 流程逻辑错误，必须重做 |
| **G4** | 协同活动识别确认 | 活动归属确认后、写生成脚本之前 | 向用户展示协同活动分组，等待确认 | **严禁**自行决定协同框分组 | 协同框错误，必须重做 |
| **G5** | 流程说明确认 | **⚠️ 必须在用户明确确认流程图效果无误之后** | 先询问用户是否生成 Excel 流程说明；若是→展示流程说明预览（WORD→原文摘要，其他含已有Drawio场景→AI生成内容），等待用户确认内容后生成 | **严禁**在用户确认流程图之前生成 Excel；**严禁**跳过 G5 内容确认直接输出 Excel；**严禁**用一句话概括活动说明 | ①流程未确认就生成Excel，必须重新来过；②活动描述过于简单未达质量标准，需重写 |

> **执行纪律**：每次开始生成任务时，AI 必须首先读取本章节，确认所有门禁要求。不得以"我记得规则"为由跳过本章节阅读。

---

## 泳道类型定义 ⚠️ 核心概念

**横向泳道** 与 **纵向泳道** 是两个独立的维度概念，必须严格区分：

| 术语 | 含义 | 视觉特征 |
|------|------|---------|
| **横向泳道** | 不同泳道**纵向（上下）排列** | 角色标签在泳道**最左侧**；活动按**从左往右**时序推进 |
| **纵向泳道** | 不同泳道**横向（左右）排列** | 角色标签在泳道**最顶端**；活动按**从上往下**时序推进 |

### ⚠️ 单层 lc 容器结构（V10.0 修正）

Draw.io 泳道图采用**单层结构**——不再设独立 `pool` 节点。唯一容器 `lc` 同时承载标题栏和泳道。

| 版本 | 结构 | 结果 |
|------|------|------|
| V9.6 | Pool(`swimlane;fillColor=#F5F5F5`) + lanes 直接子节点（parent=pool） | Pool 有 children 时标题栏被覆盖 |
| V9.7 | Pool 空容器 + LC容器（兄弟）+ lanes（parent="lc"） | lc 的 `horizontal=1;startSize=20` 产生第二个 Pool 外观 |
| **V10.0** | **单层 lc 容器（parent="1"）直接承载标题 value + 泳道 children** | ✅ 标题栏清晰，无冗余节点 |

> **核心原则**：`lc` 即唯一的顶层容器，兼作 Pool（标题栏）和泳道父节点。**不需要**独立 `pool` 节点。

**对应的 XML 结构**：

| 元素 | 完整样式 | parent | value |
|------|---------|--------|-------|
| **lc（标题栏+泳道父容器）** | `swimlane;html=1;childLayout=stackLayout;resizeParent=1;resizeParentMax=0;horizontal=1;startSize=30;horizontalStack=0;whiteSpace=wrap;fontSize=15;` + **`vertex="1"`** | `1`（根） | `"流程名称"` |
| **lane（横向→标签在左）** | `swimlane;html=1;startSize=30;horizontal=0;whiteSpace=wrap;fillColor=...;strokeColor=...;fontSize=12;fontStyle=1;` | lc | `"角色名"` |
| **lane（纵向→标签在上）** | `swimlane;html=1;startSize=30;horizontal=1;whiteSpace=wrap;fillColor=...;strokeColor=...;` | lc | `"角色名"` |

**定位规则**：
- lc：`x=40, y=40`（画布边距），`w=画布宽−80`（默认 1020），`h=startSize(40)+Σ(lane H)`
- lane：`width=lc 的 width`（统一宽度），`y=累计高度+startSize`（相对于 lc，首条 y=40）
- 禁止 lane 的 width > lc 的 width，禁止 lane 的 width 单独设置不同值

**跨泳道边 waypoints 规则（V10.0 新增）**：
- 当决策节点有**多条出边**（如大额/小额）通往不同泳道时，必须加入 `<Array as="points">` 在公共折点汇聚后再分叉，避免线条在垂直通道中重叠

**跨泳道边 parent 规则**：
- 跨 lane 边：`parent="lc"`（lane 挂在 lc 下，LCA=lc）
- 同 lane 边：`parent="lane_X"`（挂在对应 lane 下）

### ⚠️ Pool 容器渲染规则（踩坑总结）

> 以下规则均为实战中反复踩坑后提炼，**生成任何 drawio 文件时必须逐条落实**。

| # | 规则 | 说明 | 踩坑历史 |
|---|------|------|---------|
| **R1** | **lc 必须带 `vertex="1"`** | `vertex="1"` 是 draw.io 将 mxCell 识别为可渲染 shape 容器的**必需属性**。缺失则整个 Pool 不渲染，所有子泳道消失或错位。 | 2026-06-02：生成"费用报销流程"，Pool 不显示。对比正确文件发现只差 `vertex="1"` |
| **R2** | **`horizontal=1` 是正确的** | lc 容器 `horizontal=1` + `horizontalStack=0` + `childLayout=stackLayout` 组合下，标题栏在**顶部**横向展开，不是左侧竖条。 | 之前误判 `horizontal=1` → `horizontal=0`，回退后发现正确文件本身就用 `horizontal=1` |
| **R3** | **lc 必须有 `value`** | `value` 为标题栏文字内容。缺失则标题栏空白（不影响 Pool 渲染，但无标题）。 | — |
| **R4** | **lc 的 parent 必须为 `"1"`（根）** | lc 是顶层容器，直接挂在根节点下。禁止挂在其他节点下。 | — |
| **R5** | **禁止独立 pool 节点** | 不要创建 `shape=pool` 或额外的 swimlane 兄弟节点与 lc 并存。单层结构：一个 lc 即全部。 | V9.6/V9.7 历史问题 |

**lc 节点的完整属性清单**：

```
必需属性：vertex="1"  parent="1"  id="lc"  value="流程名称"
样式属性：swimlane  html=1  childLayout=stackLayout  resizeParent=1
          resizeParentMax=0  horizontal=1  startSize=30
          horizontalStack=0  whiteSpace=wrap  fontSize=15
```

**自检口诀**：生成后逐一核对 — `vertex` 有吗？`horizontal=1` 对吗？`value` 填了吗？`parent="1"` 吗？有没有多余的 pool 节点？

---

## 触发时机

当用户提出以下类似请求时触发本技能：

**场景一：有业务文档**
- "帮我根据这个文件生成流程图"
- "把这个 Word/PDF/Excel 里的流程整理成 Draw.io 格式"
- "分析文档，生成泳道图"
- "把业务流程图导出成 drawio 文件"

**场景二：无文档，仅流程描述**（新增）
- "帮我画一个 XX 流程图"
- "XX 流程一般是怎么做的？生成一个泳道图"
- "帮我设计一个 XX 业务的流程图"
- 用户给出了流程名称，可能附带了该流程的基本内容、意义或价值描述

**场景三：上传了图片格式的流程图**（V10.2.9 新增）
- 用户上传了一张 PNG/JPG/截图格式的现有流程图，要求解析并重新绘制
- "分析这张流程图，帮我重新画成 Draw.io 格式"
- "把这个图片里的流程图导入成 drawio 文件"
- 图片可能是手绘、Visio 导出、PPT 截图、或系统截图

**场景四：上传了 Visio (.vsdx) 格式的流程图**（V10.3.0 新增）
- 用户上传了 .vsdx 文件，要求导入转换为 Draw.io 格式
- "导入这个 Visio 流程图""把这个 vsdx 转成 drawio"
- "将 Visio 泳道图导入成 Draw.io 格式"
- 前提要求：用户提供的 Visio 文件必须使用**真正的 Cross-Functional Flowchart 泳道**（CFF Container/Swimlane master），而非手工画线分隔的"伪泳道"

**场景五：已有 Draw.io 流程图，仅需生成流程说明 Excel**（V10.5.4 新增）
- 用户已有 .drawio 流程图文件，要求基于流程图内容编写配套的流程说明文档
- "根据这个流程图，帮我编写流程说明"
- "读取这个 drawio 文件，生成 Excel 流程说明"
- "这个流程图已经有了，给我出一份配套的流程文档"

> **场景五的处理流程**：
> ```
> 读取 drawio XML → 解析泳道/节点/边/驳回/循环/协同
>     ↓
> 一次性展示：角色清单 + 活动归属 + 驳回/循环/协同（合并预览，无需 G1/G2/G3 分步确认）
>     ↓ 用户确认
> AI 自主生成流程说明内容（6/4要素模板，与"其他场景"规则相同）
>     ↓ 用户确认
> 生成 data.json → run_flow_desc.py → Excel
> ```
> ⚠️ **场景五不生成流程图**（用户已有），仅生成流程说明 Excel。内容生成采用 AI 自主生成模式（非 WORD 导入的原文提取模式）。

> **默认方向**：无特殊说明时，默认生成**横向泳道图**。若用户明确指定"纵向"，则生成纵向泳道图。

---

## ⚠️ 强制规则：所有场景必须用库模式生成

**无论场景一（有文档）/ 场景二（仅名称）/ 场景三（图片）/ 场景四（VSDX），生成流程图时必须调用 `scripts/drawio_lib.py` 库，禁止手写 XML。**

| 禁止行为 | 说明 |
|---------|------|
| ❌ 手写 XML 拼接 | 禁止在生成脚本中手动拼接 `<mxCell>` 字符串 |
| ❌ 跳过库直接输出文件 | 必须通过 `Flowchart` / `add_lane` / `add_node` / `add_edge` / `add_reject` / `add_loop` / `add_collab` API 构建 |
| ❌ 手写检查清单 3.5/3.6 | 库已将 30+ 条规则编码进 `layout()` 和 `_build_xml()`，生成脚本只需描述业务语义 |

**库模式优势**：
- `rounded=0` + `edgeStyle=orthogonalEdgeStyle`：库强制写入，无需逐边检查
- 零 waypoint：库不生成 `<Array as="points">`，正交路由自行计算
- 坐标递推：`_lay_nodes_x()` 自动计算，无需人工推算 x 序列
- 驳回三角：`add_reject()` 一行完成，自动计算位置和锚点
- 循环圈：`add_loop()` 一行完成
- 协同框：`add_collab()` 一行完成，自动计算包围盒和根坐标

**生成脚本模板**（所有场景通用）：

```python
import sys; sys.path.insert(0, 'scripts')
from drawio_lib import *

fc = Flowchart('流程名称')
# 1. 加泳道
lane_a = fc.add_lane('角色A')
# 2. 加节点
n1 = fc.add_node('活动1', NodeType.ACTIVITY, lane_a)
# 3. 加边（按主链顺序，库自动递推x）
fc.add_edge(n1.id, n2.id)
# 4. 加驳回/循环/协同
fc.add_reject(dec_node, tgt_node, '1')
fc.add_loop(src_node, tgt_node, '1')
fc.add_collab([n1.id, n2.id])
# 5. 输出
xml = fc.to_xml()
with open('输出文件.drawio', 'w', newline='\n') as f:
    f.write(xml)
```

---

## 执行流程

### ⚠️ 通用规则：开始/结束节点

**每个流程图必须自动添加"开始"和"结束"图符，不得用流程中的第一个或最后一个活动充当。**

- **开始节点**：椭圆，标签"开始"，位于流程最前端（第一个泳道的最左侧），是流程的唯一起始点
- **结束节点**：椭圆，标签"结束"，位于流程最末端（最后一个泳道的最右侧），是流程的唯一终止点
- 开始和结束节点**不属于任何具体角色**，放在第一个泳道和最后一个泳道中仅出于布局考虑
- 即使用户文档中没有明确写出"开始""结束"，也必须自动添加
- 开始/结束节点宽度：**80px**，高度：**60px**（与活动节点一致）

---

### 场景一：用户提供了业务文档

#### 第一步：读取并理解文档

1. 使用文件读取工具打开用户提供的文档（支持 .docx、.pdf、.xlsx、.xls、.csv 等格式）。
2. 全文通读，重点识别以下信息：
   - **角色/岗位**：负责执行某项活动的人员、岗位或系统（即泳道主体）
   - **活动/步骤**：具体发生的动作、操作、决策
   - **流程顺序**：步骤之间的先后关系、分支、汇聚
   - **决策判断**：有条件分支的地方（如"审批通过/不通过"）

---

### 场景二：用户仅提供了流程名称和描述（无文档）新增

#### 第一步：分析流程需求

1. 基于用户给出的流程名称和描述，结合对该业务领域的**公开知识和行业最佳实践**，分析该流程通常涉及的内容。
2. 识别：
   - **可能涉及的角色/岗位**：哪些岗位或人员通常参与该流程
   - **核心活动步骤**：该流程的典型环节和行为
   - **关键决策点**：通常需要审批、判断的环节
   - **流程价值/意义**：用户可能提到的业务目标，用于验证流程设计的合理性
3. 如果用户提供了该流程的意义或价值描述（非必须），将其作为流程设计的指导方向，确保流程涵盖这些价值点。

> 注意：如果用户没有提到流程价值，不影响流程图的生成——仅关注流程节点本身即可。

---

### 场景三：用户上传了图片格式的流程图（V10.2.9 新增，V10.2.10 修正策略）

> ⚠️ **核心原则**：图片导入 = **业务内容提取**，不是视觉布局转录。目标是提取图片中的业务语义（角色、活动、决策、驳回路径、流程逻辑），然后**按流程图生成器自身规则重新布局**。不得复制原图的泳道方向、节点位置、间距等视觉格式参数。原图是纵向泳道 → 生成横向泳道；原图是横向 → 也按自身规则重排。

#### 图片导入的独特挑战

与 Word/PDF 文本解析不同，图片导入面临以下特殊挑战：

| 挑战 | 说明 | 典型错误 |
|------|------|---------|
| **文字识别** | 图片中的文字依赖 AI 视觉能力识别，无结构化数据支撑 | 角色名误读（"组长"不存在原文中）；决策文字失真 |
| **空间归属** | 活动框在视觉上接近某泳道但不一定属于该泳道 | 活动归属到错误角色（基于视觉临近而非逻辑归属） |
| **符号歧义** | 菱形同时用于"决策"和"路由分支"；三角可能是驳回也可能是装饰 | 路由菱形被当成决策活动处理 |
| **协同活动** | 同一活动框跨多个泳道时，AI 可能仅归入一个泳道 | 缺少多泳道副本（Bug 2 的根因） |
| **边界不清** | 泳道边界线可能模糊；箭头方向可能误判 | 连线方向错误；泳道数识别不准 |
| **布局转录陷阱** ⚠️ V10.2.10 | 试图复制原图的视觉布局（泳道方向、节点x/y位置、列对齐方式） | 生成过程冗长、结果格式不一致。正确做法：只提取业务内容，生成时按流程图生成器自身规则重新布局 |

#### 图片导入四阶段解析法

```
第一阶段：宏观结构扫描 → 第二阶段：主体提取确认 → 第三阶段：细节逐项确认 → 第四阶段：生成与验证
```

---

#### 第一阶段：宏观结构扫描（只描述，不提取）

**目标**：让用户确认你"看懂"了图的整体结构，再进入细节提取。

1. **读取图片**：使用 Read 工具查看图片内容。
2. **描述整体结构**，只输出以下信息，**不提取任何具体内容**：

```
## 图片宏观结构识别

- 泳道数量：__ 条（横向/纵向排列）
- 泳道大致区域：
  - 泳道1（顶部/左侧）：约占总高度 __%，标签在左/上
  - 泳道2：约占总高度 __%
  - ...
- 是否有协同框（红色虚线框）：是 / 否
- 是否有驳回/返回路径：是 / 否
- 是否有循环回路：是 / 否
- 活动框总数（估）：约 __ 个
- 决策菱形总数（估）：约 __ 个
- 图片清晰度：清晰 / 一般 / 模糊（文字可读 / 部分可读 / 难辨认）
```

3. **请求用户确认**宏观结构正确后，再进入第二阶段。

---

#### 第二阶段：主体提取 + 逐项确认

**原则**：每提取一类信息，立即与用户确认，不累积到末尾。

##### 2.1 角色+活动确认（合并确认点，替代原 2.1+2.2）

1. 逐行识别每个泳道标签文字，同时列出该泳道内的活动：
2. 以**一张表**呈现（角色 + 活动合并展示）：

```
## 识别到的角色与活动（图片导入）

| 泳道 | 角色名（建议） | 活动列表（编号/名称/类型） |
|-------|-------------|----------------------|
| 1     | 角色A        | 001/活动1(活动)  002/判断?(决策) |
| 2     | 角色B        | 003/活动2(活动)  [协同] |
| 3     | 角色C        | 004/活动3(活动) |

**类型说明**：活动=矩形，决策=菱形，路由=无编号菱形，协同=跨泳道
**协同活动**（自动识别）：007沟通 → 涉及泳道1+2
**驳回/循环**（初步识别）：决策002"否"→退回001；活动004→循环到001

请确认：角色名是否正确？活动归属是否准确？协同/驳回/循环识别是否有误？
```

> ⚠️ **禁止行为**：不得根据行业经验"脑补"原图中不存在的角色。角色名必须来自图中实际文字。

##### 2.2 驳回/循环目标确认（简化确认点，替代原 2.4）

基于 2.1 的初步识别，列出所有驳回和循环关系，请求逐项确认：

```
## 驳回/循环关系确认

| # | 类型 | 来源节点 | 目标节点 | 说明 |
|---|------|---------|---------|------|
| 1 | 驳回 | 决策002(否) | 活动001 | 审核不通过→退回修改 |
| 2 | 循环 | 活动004 | 活动001 | 处理完→继续监控 |

请确认以上关系是否正确。如有遗漏或错误，请在此提出。
```

> ⚠️ **库模式简化**：无需描述所有正向连线（库自动计算）。只需确认驳回/循环的"从→到"关系。

##### 2.3 特殊标注确认（原 3.2 前置）

- 原图中**黄色便签/注释框** → 询问用户是否需要纳入流程图（默认不纳入）
- 原图中**红色虚线框** → 确认是否为协同框（默认视为协同框）
- 原图中**三角/圆圈符号** → 确认是驳回三角还是循环圈

##### 2.4 放弃的节点（原 3.3 前置）

列出原图中存在但决定不纳入的节点及原因：

```
## 放弃纳入的节点
| 节点 | 原因 |
|------|------|
| IT 泳道 | [用户告知不需要] |
| 黄色便签 | [暂不解析] |
```

---

#### 第三阶段：细节逐项确认（库模式下可简化）

##### 3.1 决策节点文字确认
#### 第三阶段：细节逐项确认

##### 3.1 决策节点文字确认

列出所有决策型菱形节点的文字内容，请求逐项确认：

```
## 决策节点文字

| 编号 | 原图文字（AI 识别） | 是否为决策？ | 确认文字 |
|------|------------------|------------|---------|
| 002  | [识别结果]        | 是          | [留空等确认] |
```

##### 3.2 特殊标注确认

- 原图中**黄色便签/注释框** → 询问用户是否需要纳入流程图（默认不纳入）
- 原图中**红色虚线框** → 确认是否为协同框（默认视为协同框）
- 原图中**三角/圆圈符号** → 确认是驳回三角还是循环圈

##### 3.3 放弃的节点

列出原图中存在但决定不纳入的节点及原因：

```
## 放弃纳入的节点
| 节点 | 原因 |
|------|------|
| IT 泳道 | [用户告知不需要] |
| 黄色便签 | [暂不解析] |
```

---

#### 第四阶段：生成与验证

> ⚠️ **V10.2.20 库模式**：图片导入四阶段确认完毕后，**跳过 3.5 步（检查清单）和 3.6 步（验证表）**，直接进入"第四步：生成"——调用 `drawio_lib.py` 库 API（8-15 行代码）生成 .drawio 文件，无需手写 XML 或逐条打钩。

**图片导入 → 生成的转换原则**：
- ❌ **禁止**：复制原图的泳道方向、节点列对齐方式、间距、坐标位置
- ✅ **正确**：提取业务内容后，完全按流程图生成器自身规则（角色→活动→泳道数量→x递推→连线规则→驳回三角→协同框）重新布局
- 原图纵向泳道 → 生成横向泳道；原图横向 → 同样按规则重排。**泳道格式由生成器规则决定，不由原图决定**

**验证清单**（生成后额外检查）：
- ☐ 每个泳道的角色名与用户确认一致
- ☐ 每个协同活动在**所有涉及泳道**均有副本
- ☐ 有编号的菱形 ≠ 纯路由（需有实体活动编号和文字）
- ☐ 驳回路径均有 rej_out + rej_in 三角对
- ☐ 循环圈入口锚点正确（在上→下出，在左→右出）

---

#### 图片识别率提升策略

| 策略 | 说明 |
|------|------|
| **多轮确认** | 每阶段一个确认点，不等所有信息提取完毕再一起确认。发现一处歧义问一处 |
| **原图文字优先** | 角色名、活动名以原图中可见文字为准。AI 识别不确定时，标注"识别不确定"并请求用户告知 |
| **禁止脑补角色** | 不得根据流程内容反向推断一个不存在的角色。用户没说"有组长"就不能凭空添加 |
| **视觉位置 ≠ 逻辑归属** | 活动框在图片上的 y 坐标靠近某泳道，不代表逻辑上属于该泳道。必须结合流程逻辑和文字内容综合判断 |
| **空间重叠 = 协同信号** | 若活动框的视觉区域同时覆盖两个泳道 → 大概率是协同活动 |
| **清晰度补救** | 若图片模糊导致部分文字无法识别，明确列出无法识别的区域，请用户提供原始文字或高清版本 |
| **结构优先于细节** | 先确认泳道数和角色，再确认活动和连线。结构错了，细节全白费 |

---

### 场景四：VSDX 格式流程图导入（V10.4.0 重构）

> ⚠️ **V10.4.0 起，推荐使用 `vsdx2drawio.py` 直接转换器**，一键完成 VSDX → .drawio，跳过"生成中间 Python 脚本"步骤。

#### V10.4.0 推荐方案：vsdx2drawio.py 直接转换器

**工具链**：
```
VSDX 文件 → vsdx2drawio.py → .drawio 文件（可直接在 Draw.io 中打开）
```

**使用方法**：
```bash
cd /path/to/workbuddy/project
python vsdx2drawio.py "输入文件.vsdx" "输出文件.drawio"
```

**输出结果**：
- 直接生成可打开的 .drawio 文件
- 泳道、节点、边、协同框、子流程全部自动识别
- 开始/结束节点自动新增（VSDX 无开始节点时）
- 文本自动清洗（步骤号前缀去除、TERMINATOR 翻译）
- 配色处理：移除原图配色（泳道/节点去色），协同框保留红色描边（strokeColor=#FF0000），文档符号填充黄色（fillColor=#FFF9C4；strokeColor=#F9A825）

**与旧方案对比**：

| 方案 | 流程 | 优点 | 缺点 |
|------|------|------|------|
| **新：直接转换器** | VSDX → vsdx2drawio.py → .drawio | 一键完成，无需用户确认中间步骤 | 复杂流程可能需要事后微调 |
| 旧：解析→生成脚本 | VSDX → 解析脚本 → 用户确认 → 生成 Python 脚本 → 运行 → .drawio | 每步可确认，精确度高 | 步骤多，耗时较长 |

> 📝 **何时用旧方案**：当直接转换器输出的结果有明显错误，且错误无法通过简单参数调整修复时，可回退到"解析→生成脚本"方案，在生成脚本中精确控制每个节点和边。

---

#### VSDX 解析核心规则（适用于直接转换器和旧方案）

##### R1. 泳道识别

**识别规则**：形状宽度 > 8 英寸 且有子形状 → 判定为泳道容器

```python
for shape in shapes:
    w = getattr(shape, 'width', 0) or 0
    n_children = len(shape.child_shapes) if shape.child_shapes else 0
    if w > 8 and n_children > 0:
        role_name = shape.text.strip() or get_child_text(shape)
```

**排除规则**：
- `len(role) > 30` 的极长文本（页面标题）→ 排除
- **禁止**用 `len(role) > 10 and '流程' in role` 排除（会误杀"流程Owner及业务相关方"）
- Container/List 类型形状（NameU 含 "Container" 或 "List"）→ 排除

**泳道排序**：
- Visio y 大=上方，Draw.io lane_0=上方
- ⚠️ **强制**：解析后 `lanes.sort(key=lambda l: l['y1'], reverse=True)` 反转

##### R2. 活动节点识别与类型检测 ⚠️ 核心

**形状过滤规则**：
1. 排除泳道容器（已识别）和大标题容器
2. 排除纯连线（`width < 0.5` 英寸）
3. 排除 LinePattern="0" 的形状（文本描述，无边框）
4. 排除不在任何泳道内且 `master_page_name == "Unknown"` 的形状（游离文本）
5. 排除 `shape_y > 20` 的形状（页面顶部图例区）
6. 排除 NameU 含 "Container" 或 "List" 的形状
7. 无文本形状（`node_text` 为空）→ 排除

**节点类型检测规则（基于 `shape.NameU` 属性）**：

| `shape.NameU` 值 | 节点类型 | 说明 |
|-----------------|---------|------|
| `"Decision"` 或以 `"Decision."` 开头 | DECISION | 决策菱形 |
| `"Variable procedure"` 或 `"Predefined process"` | **SUBPROCESS** | 子流程（双侧竖线矩形） |
| `"Document"` 或以 `"Document."` 开头 | DOCUMENT | 文档符号 |
| 其他 | ACTIVITY | 普通活动 |

> ⚠️ **子流程检测**：`NameU.startswith('Variable procedure')` 或 `NameU.startswith('Predefined process')`，不是精确匹配。

##### R3. 协同活动识别（语义匹配）⚠️ V10.4.0 升级

**旧方法（V10.3.x）**：精确文本匹配——只有完全相同的文本才识别为协同活动（无法匹配"初步分析和确认需求"与"参与分析和确认需求"）

**新方法（V10.4.0）**：语义匹配——剥离前缀词后做核心词匹配

```python
COMMON_PREFIXES = ['初步', '参与', '组织', '协同', '配合', '协助', '和', '与']

def strip_prefixes(text):
    core = text
    for prefix in COMMON_PREFIXES:
        if core.startswith(prefix):
            core = core[len(prefix):]
            break  # 只剥离第一个匹配的前缀
    return core

# 按核心文本分组：{core_text: [(node_id, full_text, lane_role), ...]}
core_groups = defaultdict(list)
for node in activity_nodes:
    core = strip_prefixes(node['text'])
    lane_role = node_lane_map.get(node['id'], '')
    core_groups[core].append((node['id'], node['text'], lane_role))

# 过滤：核心文本出现在 ≥2 个不同泳道 → 协同活动
# 排除：TERMINATOR 类型（"结束"/"开始"不应被协同框包裹）
```

**协同框内活动对齐规则**：
- 协同框内所有活动必须**左对齐**（x 坐标相同）
- 对齐基准：取协同框内所有活动的最小 `vsdx_x`
- 对齐后需检查是否与左侧节点重叠（若重叠，整体右移）

##### R4. 文本清洗规则 ⚠️ V10.4.0 新增

**步骤号前缀清洗**：
```python
import re
cleaned = re.sub(r'^\d{1,2}[\.\s、．]+', '', node_text).strip()
```

**TERMINATOR 英文翻译**：
```python
if node_type == "TERMINATOR":
    if node_text.upper() in ('END', 'STOP', 'FINISH'):
        node_text = '结束'
    elif node_text.upper() in ('START', 'BEGIN'):
        node_text = '开始'
```

##### R5. 开始/结束节点规则 ⚠️ V10.4.0 新增

**VSDX 无 START 节点时**：
- ❌ **禁止**将已有活动节点改为 START 类型（会破坏语义）
- ✅ **正确**：新增独立"开始"节点，放在首个活动左侧，创建边连接

**VSDX 无 END 节点时**：
- ✅ 新增独立"结束"节点，放在最后一个活动右侧，创建边连接

**实现代码**：
```python
# 检查是否有 START 节点
start_nodes = [n for n in fc.nodes if n.type == NodeType.START]
if not start_nodes:
    # 找入度为0且有出边的节点作为第一个活动
    first_activity = ...  # 找首个活动
    if first_activity:
        start_node = fc.add_node("开始", NodeType.START, first_activity.lane)
        start_node.vsdx_x = first_activity.vsdx_x - 1.5  # 放在左侧
        fc.add_edge(start_node.id, first_activity.id)
```

##### R6. 坐标映射规则 ⚠️ V10.4.0 修复

**根因**：`drawio_lib.py` 的 `to_xml()` 会检查 `node._computed`，若边未标记已计算则调用 `layout()` 重算，覆盖手工设置的坐标。

**修复方法**：
```python
# 1. 调用 lane 布局（设置 lane.y_lc）
fc._lay_lanes()

# 2. 设置节点坐标
for node in fc.nodes:
    vx = getattr(node, 'vsdx_x', 0) or 0
    node.x = int(vx * 100)  # VSDX英寸 → Draw.io像素
    node.y_lc = 30  # 泳道body内居中（60px节点在100px泳道body中）
    node._computed = True

# 3. 边锚点：调用 _lay_edge_anchors 而非设置 e._computed=True
for e in fc.edges:
    fc._lay_edge_anchors(e)  # 自动计算 exitX/exitY/entryX/entryY/parent
    # _lay_edge_anchors 内部已设置 e._computed = True

# 4. 协同框布局 + lc 尺寸计算
fc._lay_collabs()
fc._lay_lc_size()

# 5. 防止 to_xml() 触发布局重算
fc._layout_done = True
```

> ⚠️ **禁止** `e._computed = True` 跳过边锚点计算——那样所有边会用默认 `exitX=0.0, exitY=0.0`，导致连线全部指向左上角。

##### R7. 边提取规则

**核心机制**：Visio 的连线通过 `<Connect>` 元素定义。

```python
# FromPart 含义
# 0, 9 = 连线起点（连接器从该形状出发）
# 3, 12 = 连线终点（连接器到达该形状）
```

**边标签保留**：`fc.add_edge(from_id, to_id, value=label)`

##### R8. 已验证案例（V10.4.0）

| 流程名称 | 泳道 | 节点 | 边 | 驳回 | 协同框 | 子流程 | 关键改进 |
|---------|------|------|-----|------|--------|--------|---------|
| 流程例外管理程序 | 5 | 17 | 13 | 0 | 3 | 0 | 协同框语义匹配✅；开始节点新增✅；坐标映射修复✅ |
| 流程文件清单调整 | 4 | 17 | 10 | 0 | 3 | 1 | LinePattern=0过滤✅；文本清洗✅ |
| 研发流程需求及问题管理 | 5 | 22 | 23 | 0 | 1 | 5 | 文本描述全部过滤✅；直接转换器验证✅ |

> 📝 **案例说明**：V10.4.0 使用直接转换器，驳回三角由 `drawio_lib.py` 的 `layout()` 自动计算，生成脚本中无需手动添加 `add_reject()`。

#### VSDX 导入质量保证检查清单

| 检查项 | 方法 | 通过标准 |
|--------|------|---------|
| 泳道角色与 Visio 一致 | 对比解析结果中的泳道标签 | 数量+名称完全一致 |
| 子流程正确标记 | grep `predefined_process` | 所有子流程已应用正确样式 |
| 所有连线 rounded=0 | `grep -c 'rounded=0'` | 计数 = 边总数 |
| 零 waypoint | `grep 'points'` | 无结果 |
| 协同框 parent="1" | 确认协同框 mxCell 的 parent | parent="1"（根节点） |
| 协同框内活动左对齐 | 检查协同框内活动的 x 坐标 | 全部相同 |
| 开始/结束节点存在 | 检查 .drawio 文件 | 有"开始"和"结束"节点 |
| 文本无步骤号前缀 | 检查节点标签 | 无 "08 "、"1." 等前缀 |
| 边锚点正确 | 检查边的 exitX/exitY | 非全是 0.0 |
| 协同框红色描边 | grep `strokeColor=#FF0000` | 协同框 mxCell 含 `strokeColor=#FF0000`（红色描边保留） |
| 文档符号黄色填充 | grep `fillColor=#FFF9C4` | 文档节点含 `fillColor=#FFF9C4;strokeColor=#F9A825`（黄色填充） |


### ⚠️ 库生成模式说明（所有场景通用）

**自 V10.2.20 起，所有场景强制使用 `drawio_lib.py` 库生成，以下"后续步骤"描述中的第 3.5 步（前置检查清单）和第 3.6 步（决策出口验证表）在库模式下可跳过。**

| 步骤 | 手写模式（旧，已废弃） | 库模式（当前强制） |
|------|------------------|------------------|
| 第二步 | 角色清单确认 | 角色清单确认（相同） |
| 第三步 | 构建流程结构（详细 x/y 坐标） | 构建流程结构概要（节点+驳回/循环目标，无坐标） |
| **第 3.5 步** | **逐条打钩 30+ 项检查清单** | ✅ **跳过**（库代码已编码这些规则） |
| **第 3.6 步** | **逐节点填决策出口验证表** | ✅ **跳过**（库 `add_reject()` 自动计算） |
| 第四步 | 手写 XML 拼接（100+ 行） | 调用库 API（8-15 行） |
| 第五步 | 自检清单 30+ 项逐条核验 | 基本验证（count rounded=0 / waypoint=0） |

**库模式生成脚本标准流程**（所有场景通用）：

```
⚠️ **[G1] 角色清单确认**（必须，不可跳过）
   ↓
② 流程结构概要：节点清单 + 驳回目标 + 循环目标（无需 x/y 坐标）
   ↓
③ 写生成脚本（调用 drawio_lib API，8-15 行代码）
   ↓
④ 运行脚本，验证：grep rounded=0 / waypoint / stackLayout
   ↓
⑤ 交付 .drawio 文件
```

> ⚠️ **库模式下仍必须确认的内容（对应强制门禁 G1-G4）**：角色清单[G1]、活动归属[G2]、驳回/循环目标[G3]、协同活动识别[G4]。这四项业务决策无法由代码代替。详见上方"⚠️强制门禁清单"章节。

---

### 后续步骤（三种场景通用）

#### 第二步：识别角色并请求用户确认 ⚠️ 关键步骤

**⚠️ [G1门禁] 禁止在角色未确认的情况下生成流程图。违反G1门禁（未获确认就生成）的输出文件无效，必须重做。**

1. **提取角色清单**：逐一列出所有可能执行角色（有文档从文档中提取，无文档则基于分析结果）。
2. **为每个角色列出其可能涉及的活动名称或职责**，以以下格式呈现给用户：

```
## 识别到的角色清单

| # | 角色名称 | 可能涉及的活动/职责 |
|---|---------|-------------------|
| 1 | 角色A   | 活动1、活动2、活动4 |
| 2 | 角色B   | 活动3、活动5 |
| 3 | 角色C   | 活动6、审批确认 |

请确认以上角色清单是否准确。如需调整（增删角色、修改名称等），请在此阶段提出。
```

3. **等待用户确认**。用户可能提出以下类型的调整：
   - 增加遗漏的角色
   - 删除多余的角色
   - 修改角色名称
   - 调整角色的活动归属
4. **⚠️ 一旦用户确认角色清单后，不再做任何修改。** 后续流程图中严格按已确认的角色清单生成泳道，不得增删修改。

#### 角色命名规范 ⚠️

识别和定义角色时，必须遵循以下原则：

- **禁止使用部门名称**（如"工程部""人力资源部""财务部"）。部门不是角色，不能执行活动。
- **优先使用岗位名称**：如 采购员、会计、总经理、人力资源部部长。
- **可用描述性称呼**：如 费用报销人、报销规范性审核人、部门分管领导。
- **可用虚拟小组名称**：如 招标小组、评审小组。

> 每个角色都必须是**能执行具体活动的实体**（人或小组），而非组织单元。一条泳道 = 一个能干活的人/角色。

#### 第三步：构建流程结构

基于用户确认的角色清单，将识别出的活动信息整理为结构化流程。**必须包含"开始"和"结束"节点：**

```
[开始] → [角色A: 活动1] → [角色A: 活动2] → [角色B: 决策?] → 是/否分支 → ... → [结束]
```

#### 3.1 协同活动识别（V10.2.8 新增）⚠️

在活动归属确认后，必须执行**跨泳道归属检查**，识别同一活动同时在多个泳道出现的情况：

1. **检查信号**：活动名称含"沟通""协商""协同""联合""对接"等关键词；或从原始文档/图片中可观察到同一活动框跨越多个泳道的视觉区域
2. **处理方式**：若某活动属于协同活动，在**所有相关泳道**中均创建该活动节点（同一 x 坐标，不同 y 坐标），并在生成阶段配协同框（规则2c）
3. **禁止遗漏**：不得因活动已在某一泳道创建，就跳过其他泳道的副本。协同 = 每个泳道都有 + 红色虚线框包围

**库生成模式协同框编写规范**（V1.2.4 修复同伴节点级联右移bug）：
- 协同框内**每个泳道的节点**都必须单独创建（`fc.add_node()`）
- **主节点**（有FORWARD边连接主流程）：正常创建，有入边和出边
- **同伴节点**（无FORWARD边，仅作为协同框视觉副本）：创建后**不添加任何边**（`fc.add_edge()` 不调用）
- 库 `layout()` 会自动将同伴节点X坐标对齐到主节点（`_fix_collab_nodes()` 方法，V1.2.4新增）
- 调用 `fc.add_collab([n_main.id, n_companion.id])` 创建协同框
- **禁止**：只创建一个节点然后期望库自动复制（库不会自动复制节点）

**库生成模式多行布局规范**（V1.2.5 新增行号分配机制）：
- 库 `layout()` 自动检测同一泳道内 X 坐标相同的多个节点，按 DFS 深度分配不同行号（row=0,1,2...）
- 节点 Y 坐标公式：`y_lc = 30 + row × (NODE_H + 40)`，行间距 40px
- 泳道高度自动适应：`lane.height = max((max_row + 1) × 100 + 60, 120)`
- **触发场景**：
  - 并行路径：同一泳道内两条独立路径在相同 X 位置有节点（如采购安全库存 vs 订单驱动路径）
  - 决策分支：决策节点的多个后续分支目标在同一泳道内（避免连线交叉和视觉混乱）
- 生成脚本**无需**手动设置 row 或 Y 坐标，库完全自动处理
- 行号分配依赖 FORWARD 边构建的拓扑深度（DFS），因此生成脚本必须正确添加所有 FORWARD 边

**库生成模式 API 调用规范**（V10.4.3 新增，防反复踩坑）：
- `add_lane()` 只接受标签参数，返回 Lane 对象：**必须用变量接收返回的 Lane 对象**
- `add_node()` 第三参数必须传 Lane 对象（`Lane` 类型），**禁止传字符串**
- **正确模式**：
  ```
  lane_sales = fc.add_lane("销售专员")
  n1 = fc.add_node("活动1", NodeType.ACTIVITY, lane_sales)
  ```
- **禁止**的错误模式：
  ```
  fc.add_lane("销售专员", 0)        # add_lane() 只有一个参数（label），索引自动分配
  fc.add_node("活动1", NodeType.ACTIVITY, "销售专员")  # 传字符串→lane.index 变成 str.index 方法→比较时报 TypeError
  ```

**开始/结束节点布局规则**：
- 开始节点放在第一个泳道**内容区**最左侧，x = `泳道 startSize + 30`（距角色栏 30px，横向泳道 startSize=30 时即 x=60）
- 结束节点放在最后一个泳道的最右侧位置（x 坐标最大，在所有活动节点之后）
- 开始节点连接到第一个活动节点（同泳道右侧出口 → 左侧入口）
- 最后一个活动节点连接到结束节点（方向根据泳道位置确定）
- 横向泳道中：开始→第一个活动=右出口线；最后一个活动→结束=右出口线

**异常处理规则**：
- 若某个活动能在文档中确认存在，但**无法明确找到执行角色**，则该活动放入一个**匿名/空白泳道**（泳道标签留空或标记为"—"），不得遗漏该活动。
- 若文档中角色描述模糊（如"相关人员"），保留原文描述作为泳道名称。

### ⚠️ 第 3.5 步：前置强制检查清单 — 写生成脚本前必须逐条打钩（V10.2.3 新增，V10.2.10 扩展）

> ⚠️ **库模式下此步可跳过**：库 `drawio_lib.py` 已将以下 30+ 条规则编码进 `layout()` 和 `_build_xml()`，生成脚本只需调用库 API（8-15 行），无需逐条打钩。跳过此步后直接进入"第四步：生成"。

**在写 Python 生成脚本之前，必须逐条确认以下规则，全部打钩后才能开始编写代码。禁止凭记忆写脚本——这里是每次生成时必须核对的最小稳定规则集。每一项都应从 SKILL.md 正文中找到对应规则的定义，按规则写代码，而非凭直觉。**

```
☐ 0a. 零 Waypoint — 所有 edge 不写 <Array as="points">，仅 exit/entry 锚点
☐ 0b. rounded=0 + edgeStyle=orthogonalEdgeStyle — 每条 edge style 两个属性缺一不可
☐ 1a. 同泳道串联 — exitX=1;exitY=0.5;entryX=0;entryY=0.5（右侧直线，入口左侧）
☐ 1b. 下方泳道串联 — exitX=0.5;exitY=1;entryX=0;entryY=0.5（入口走左侧）；但先执行障碍检测
☐ 1b+. 障碍检测 — 下方出口前预测垂直段 x，检查是否穿过中间泳道活动框（x 重叠 AND y 重叠）；有障碍→改 exitX=1
☐ 1c. 上方泳道串联 — exitX=1;exitY=0.5;entryX=0;entryY=0.5（均走右-左，避免折返）
☐ 1d. 下方活动出口 — 上下两个活动有先后时序关系时，下方活动的出口在右侧（exitX=1;exitY=0.5），入口走左侧（entryX=0;entryY=0.5）。上方入的边不受此规则影响
☐ 活-横. 同泳道右移 — x_next = x_prev + prev_w + PAD（仅同泳道行内串联适用）
☐ 活-竖. 跨泳道错位右移 — 跨泳道向下串联时也右移：x_next = x_prev + prev_w + PAD（错位摆放，与活-横同公式）。废弃 V10.2.12 的同x列规则
☐ 决. 决策双出口分侧 — "是"和"否"从菱形不同侧发出
☐ 决标. 决策分支标签 — 决策菱形正向出口连线 value="是"，驳回出口连线 value="否"。标签必须显式标注，与其他无文字的连接线区分开来
☐ 驳扫. 驳回全量扫描 — 列出所有含"否"/"驳回"/"不通过"返回分支的节点（含 route_ 前缀路由节点），逐节点确认 rej_out 三角存在。原则：有驳回出线 → 必有 rej_out；禁止依赖顺序记忆逐个处理
☐ 驳0. 正向出口方向确认 — 在放置驳回三角前，先确认每个驳回分支节点的正向出口方向（右/下/上），写在注释中，再按驳1规则定三角位置
☐ 驳1. 驳回三角位置 — 正向出口=下→三角在右(rotation=0▶)；正向出口=右→三角在下(rotation=90▼)
☐ 驳2. 驳回三角间距 — 距锚定节点 = nodeH/3（NODE_H=60 → 20px）。rej_in距目标顶部间距 = NODE_H/3。⚠️ 三角在决策下方时，该泳道 body 高度需 ≥ AY+DEC_H+间距+TRI_H（30+60+20+20=130），若不足则增加该泳道高度
☐ 驳3. 驳回两端独立锚定 — rej_out 和 rej_in 之间无连线；rej_out 无出边；rej_in 恰 1 条出边
☐ 驳4. rej_in 连线出口 — rotation=90(▼) → 出口走底部尖：exitX=1.0;exitY=0.5;entryX=0.5;entryY=0（三角底部尖出→目标顶部入）。⚠️ 三角禁止走左侧尖角
☐ 驳5. rej_in 位置 — 水平居中于目标活动正上方：rej_in_x = target.x + (target.w - rej_in.w)/2
☐ 驳6. 多驳回源→同一目标 — 入口三角数量 = 驳回源数量；多个 rej_in 横向平铺于目标正上方（y 相同，x 按 TRI_W 间距依次排开）
☐ 驳号. 驳回三角编号 — rej_out 和 rej_in 的 value 属性必须填数字编号（"1"/"2"...），禁止 value=""
☐ 循. 循环圈连线出口 — 圈在目标上方→下方出(exitX=0.5;exitY=1)；圈在目标左侧→右侧出(exitX=1;exitY=0.5)。必须正对目标节点，禁止套用驳回三角的 exitX=0 左侧出规则
☐ 始. 开始/结束 — 椭圆，80×60px，开始 x = startSize + 30（距角色栏30px），结束时序最末
☐ 泳. 泳道 html=1 — 每条 lane style 必须含 whiteSpace=wrap;html=1
☐ 泳横. 横向泳道 horizontal=0 — 横向泳道图的每条 lane style 必须含 horizontal=0（标签在左），纵向泳道则 horizontal=1（标签在上）
☐ 协. 协同框 — 同一活动出现在≥2个泳道 → 红色虚线框包围，**parent="1"（根节点，非lc）**，dashed=1;strokeColor=#FF0000;fillColor=none。坐标 = lc坐标 + lc偏移(LC_X,LC_Y)。在所有节点之后输出。parent="1" 脱离 lc 的 stackLayout，防止编辑时丢失
☐ lc. 容器 vertex=1 — lc 必须 vertex="1"，horizontal=1，value 填标题
☐ LCA. edge parent — 同泳道→lane_X，跨泳道→lc
☐ esc. XML 转义 — value 属性用 esc_val() 转义 <>，delete \n
☐ mxf. mxfile 模板 — `<mxfile host="Electron">`，无 XML 声明(`<?xml...?>`)，无 modified/agent/version/type 属性
☐ mx2. diagram id — `id="flow-1"` + `name="流程名称"`，不是 `id="diagram1"`
☐ mx3. 换行符 — `\n`(Unix)。`open()` 加 `newline="\n"`。f-string 内用变量 `{NL}` 代替 `\n`（禁止 `\\n`）
☐ mx4. GraphModel — dx="1200" dy="800"，去掉 host/version 属性，保留 grid/gridSize/guides 等
```

> ⚠️ **关键约束**：勾完清单前，不得开始写任何一行生成脚本。任何一条未打钩的规则 = 尚未理解/未纳入代码逻辑 = 禁止动手。

### ⚠️ 第 3.6 步：逐节点决策出口验证表（V10.2.12 新增）— 防复发关键步骤

> ⚠️ **库模式下此步可跳过**：库 `add_reject()` 自动计算三角位置和锚点，无需逐节点填验证表。

**在写生成脚本的坐标和连线代码之前，必须为每个决策节点填写以下验证表。这是防止"决/驳1/驳4+"规则连锁崩溃的最后防线。**

```
┌──────────┬────────────────┬────────────────────┬──────────────────┬──────────┐
│ 决策节点  │ 正向出口(是)    │ 驳回出口(否)        │ 三角位置(驳1)     │ 是/否    │
│          │ 出口方向+目标   │ 出口方向+目标       │ 坐标+方向        │ 不同侧?  │
├──────────┼────────────────┼────────────────────┼──────────────────┼──────────┤
│ dec_3    │ ↓右,→目标X     │ ↓→,→rej_out       │ 在源节点下方/右侧  │ ✅/❌    │
│ dec_5    │ ...            │ ...                │ ...              │ ✅/❌    │
└──────────┴────────────────┴────────────────────┴──────────────────┴──────────┘
```

**验证逻辑（逐节点，不可跳过任何一个决策）：**
1. 正向出口是"下"还是"右"？（确定 `exitX/exitY`）
2. 驳回出口必须在**不同侧**（"决"规则：是下→否右；是右→否下）
3. 驳回三角位置 = 与正向出口方向相反（"驳1"规则：是下→三角右；是右→三角下）
4. 驳回三角的连线锚点确认（"驳4+"规则：看三角在决策哪一侧）
5. 全部 ✅ 后才能写对应节点的坐标和处理边代码

**禁止行为**：
- ❌ 套用前一个决策节点的出口方向和三角位置
- ❌ 依赖"看起来差不多"的默认值
- ❌ 用 `edge_rej_out()` 函数名暗示锚点（函数名只是标签，必须看实际参数）

---

### 第四步：生成 Draw.io XML

调用脚本 `scripts/generate_drawio.py` 生成标准的 Draw.io XML，或按下方规范手工构建 XML。

#### ⚠️ 输出文件外层格式

Draw.io 文件必须是标准的 **mxfile** 格式，而非裸 mxGraphModel。裸 mxGraphModel 会导致渲染异常（菱形变形、标题栏不显示等）。**模板必须严格遵守以下格式，不得有 XML 声明或额外属性。**

```xml
<mxfile host="Electron">
  <diagram id="flow-1" name="流程名称">
    <mxGraphModel dx="1200" dy="800" grid="1" gridSize="10"
      guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1"
      pageScale="1" pageWidth="1350" pageHeight="750" math="0" shadow="0">
      <root>
        <mxCell id="0"/>
        <mxCell id="1" parent="0"/>
        <!-- 所有内容在此 -->
      </root>
    </mxGraphModel>
  </diagram>
</mxfile>
```

**禁止**直接以 `<mxGraphModel>` 作为文件根元素，必须包裹在 `<mxfile><diagram>` 内。

#### ⚠️ mxGraphModel 根元素元数据（致命规则）

Draw.io XML 的 `<mxGraphModel>` 根元素元数据直接影响文件能否正确渲染。**必须使用以下格式**：

```xml
<mxGraphModel host="WorkBuddy" version="10.0.0" ...>
```

**禁止使用**：
- `host="app.diagrams.net"` → 会导致某些版本 Draw.io 渲染异常
- `version="21.0.0"` → 版本号过高可能导致向后兼容问题

**lc 容器必须设置 `value` 属性作为标题栏文字**（V10.0 修正）：
```xml
<!-- ✅ 正确 -->
<mxCell id="lc" value="流程名称" style="swimlane;html=1;childLayout=stackLayout;resizeParent=1;horizontal=1;startSize=30;horizontalStack=0;whiteSpace=wrap;fontSize=15;" parent="1" vertex="1">

<!-- ❌ 错误：无 value，标题栏空白 -->
<mxCell id="lc" style="..." parent="1">
```

**根 mxCell 禁止设置 `agent` 属性**：
```xml
<!-- ❌ 错误 -->
<mxCell id="0" agent="flowchart-generator"/>

<!-- ✅ 正确 -->
<mxCell id="0"/>
```

> ⚠️ 这是 Python 脚本生成文件与手工 XML 文件之间的关键差异。历史踩坑：即使泳道样式、坐标、`html=1` 全部正确，元数据错误仍会导致文件空白。

**布局规范**：
- 方向：**横向**（水平泳道，Pool 容器 `horizontal=0` 标题栏在顶部）
- 活动节点宽度：**80px**，高度：**60px**（决策/开始/结束同理）
- 泳道高度：**120px**（每条泳道）
- 节点水平间距：**40px**
- 节点类型：
  - 普通活动 → 圆角矩形 `rounded=1`
  - 决策节点 → 菱形 `rhombus`
  - 开始/结束 → 椭圆 `ellipse`
  - 事件触发 → 六边形 `shape=hexagon;perimeter=hexagonPerimeter2;`（见下方事件符号规范）

#### 事件符号（六边形）规范

**用途**：表示流程的触发事件，尤其是存在多种触发情形时，用事件符号统一表达触发源。

**典型场景**：
- 流程有"主动触发"和"被动触发"两种入口 → 用一个事件符号表示，从事件引出两条路径
- 某个外部事件触发流程启动 → 事件符号紧接在"开始"节点之后

**样式**：
```xml
<mxCell id="event_1" value="事件名称"
  style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#FFF2CC;strokeColor=#D6B656;fontSize=12;fontStyle=1;"
  vertex="1" parent="lane_x">
  <mxGeometry x="120" y="30" width="80" height="60" as="geometry"/>
</mxCell>
```

**配色**：`fillColor=#FFF2CC;strokeColor=#D6B656;`（浅黄色，区别于活动节点和决策节点）

**尺寸**：80×60px，与普通活动节点一致

**连线规则**：
- 事件到后续活动：同泳道用右侧出口（`exitX=1;exitY=0.5`），跨泳道根据目标位置选择
- 事件可有**多条出边**（触发多个并行路径），每条标注触发条件
- 连接线：带箭头的有向边，条件分支在连线上标注条件文字

**泳道类型**：
- 使用 **Pool 泳道容器**，完整样式为：
  - 横向：`shape=pool;startSize=30;horizontal=0;childLayout=stackLayout;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=1;fillColor=none;strokeColor=#000000;fontSize=14;fontStyle=1;`
  - 纵向：`shape=pool;startSize=30;horizontal=0;childLayout=stackLayout;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=1;fillColor=none;strokeColor=#000000;fontSize=14;fontStyle=1;`
  - ⭐ Pool 必须设置 `value="流程名称"` 作为标题栏文字
- 泳道内部各行使用 `swimlane` 节点。
- 横向泳道：泳道行 `horizontal=0`（分隔线垂直 → 角色标签在**左侧**）
- 纵向泳道：泳道列 `horizontal=1`（分隔线水平 → 角色标签在**上方**）

**泳道角色标签位置规则** ⚠️：
- **横向泳道**：角色名称放在泳道的**左侧**（`swimlane;horizontal=0;startSize=80;whiteSpace=wrap;html=1`）
- **纵向泳道**：角色名称放在泳道的**上方**（`swimlane;horizontal=1;startSize=30;whiteSpace=wrap;html=1`）

**泳道样式必须包含 html=1 ⚠️ 致命规则**：

`html=1` 是 mxGraph 泳道启用 HTML 渲染模式的核心属性。**没有它，泳道退化为普通矩形，所有子节点不渲染**，导致 Draw.io 打开只显示一个 Pool 方框。

```xml
<!-- ✅ 正确：完整泳道样式（V10.0 单层结构，lane 挂在 lc 下） -->
<mxCell id="lane_x" value="角色名"
  style="swimlane;startSize=30;horizontal=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;fontStyle=1;"
  vertex="1" parent="lc">
  <mxGeometry x="0" y="0" width="1020" height="130" as="geometry"/>
</mxCell>

<!-- ❌ 错误：缺少 whiteSpace=wrap;html=1 → 泳道不渲染，文件空白 -->
<mxCell id="lane_x" value="角色名"
  style="swimlane;startSize=30;horizontal=0;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;fontStyle=1;"
  vertex="1" parent="lc">
</mxCell>
```

> ⚠️ **这是"流程图打开只有 Pool 方框"的首要原因**：泳道缺少 `whiteSpace=wrap;html=1` 导致子节点不渲染。**生成每个泳道 mxCell 时，必须包含这两个属性。**

**节点坐标必须在泳道内 ⚠️ 致命规则**：

所有活动/决策节点的 `mxGeometry` 坐标 (x, y) **必须相对于其父泳道（parent swimlane）**，**绝对禁止**使用 Pool 容器的绝对坐标。

```
// 正确：节点 y 坐标相对于所属泳道
lane_mdm (pool_y=120, h=120) → act_002 y=30  ✅ 在泳道内可见

// 错误：节点 y 坐标使用了 Pool 级绝对坐标
lane_mdm (pool_y=120, h=120) → act_002 y=150 ❌ 超出泳道范围，不可见！
```

**坐标计算公式**：
```python
# y 坐标 = 泳道内部垂直居中
node_y = (LANE_H - NODE_H) // 2

# x 坐标 = 从左到右排列，越靠右越大
node_x = base_x + index * (NODE_W + GAP)
```

> ⚠️ **这是最常见的"流程图打开空白"原因**：如果 y 坐标用了 `lane_idx * LANE_H + offset` 这种 Pool 级计算，则除第一行泳道外所有节点都会飘出泳道边界而不可见。

**跨泳道串联活动 x 坐标规则** ⚠️ V10.2.12 关键修正：

跨泳道串联活动分两种情况，x 坐标规则**完全不同**，必须严格区分：

### 活-横：同泳道行内右移

同一泳道内，后续活动向右偏移一个宽度 + 间距：

```
x_next = x_prev + prev_w + PAD
```

### 活-竖：跨泳道向下也右移（错位摆放）

跨泳道向**下**直接串联（非上方绕回、非水平拐弯）时，x 坐标也向右错位（与活-横同公式）：

```
x_next = x_prev + prev_w + PAD   （与活-横公式相同，错位摆放）
```

```
// 正确：错位右移，x 递增
  lane0:  [ dec_3 ]─────────┐  (是: 下出)
  lane1:           [ act_4 ]←─┘   x_4 = x_3 + 80 + 40 = 540  ✅

// 错误：同列堆叠，x 不变
  lane0:  [ act_6 ]─────────┐  (下出)
  lane1:         [ act_7 ]←─┘   x_7 = x_6 = 540  ❌ 同一列！
```

> ⚠️ **判断方法**：看目标节点相对于源节点是否直接正下方。若目标在紧邻下方泳道的同一 x 列 → 活-竖（x 不变）。若在同一泳道右侧 → 活-横（x += W+PAD）。若在上方泳道 → 看具体走势，通常需偏移。

**连线布局规则（关键，避免线条交叉重叠）**：

### 规则0：正向业务逻辑线优先 ⚠️ 首要原则

**先处理正向业务逻辑连线，再处理驳回连线。** 驳回连接点的位置完全取决于正向连线的方向，因此必须在正向连线确定后才能决定驳回点位置。

---

### 规则0a：零 Waypoint 全自由路由（V10.2.1 新增）

**所有 edge 一律不包含 `<Array as="points">` 子元素（零 waypoint）。** 只定义 `exitX`/`exitY`/`entryX`/`entryY` 锚点，由 Draw.io 正交路由引擎自行计算折弯路径。禁止手写 waypoint 坐标。

**核心原则**：
> 告诉 Draw.io "从哪里出去、从哪里进去"，其余的折弯交给引擎。人工计算 waypoint 坐标极易出错（坐标偏移、障碍判断失误），去掉后稳定性大幅提升。

**正例**（零 waypoint）：
```xml
<mxCell id="e_xxx" style="edgeStyle=orthogonalEdgeStyle;rounded=0;exitX=1;exitY=0.5;entryX=0;entryY=0.5;"
        edge="1" source="act_a" target="act_b" parent="lc">
  <mxGeometry relative="1" as="geometry"/>
</mxCell>
```

**反例**（含 waypoint，已禁止）：
```xml
<!-- ❌ 禁止：含 <Array as="points">，人工计算坐标易出错 -->
<mxCell id="e_xxx" style="edgeStyle=orthogonalEdgeStyle;..." edge="1" source="act_a" target="act_b" parent="lc">
  <mxGeometry relative="1" as="geometry">
    <Array as="points">
      <mxPoint x="500" y="200"/>
    </Array>
  </mxGeometry>
</mxCell>
```

---

### 规则0b：通用连线样式强制 `rounded=0`（V10.2.1 新增）

**所有连线（edge）的 style 属性中，必须强制包含 `rounded=0`，禁止圆角弯折。**

| 规则 | 说明 |
|------|------|
| `rounded=0` | 连线折角为直角，禁止圆角（`rounded=1`） |
| 适用范围 | **所有** edge，无例外 |
| 与 `edgeStyle` 配合 | `edgeStyle=orthogonalEdgeStyle;rounded=0`（两个属性缺一不可） |

> ⚠️ `rounded=0` 和 `edgeStyle=orthogonalEdgeStyle` 是独立属性：`edgeStyle` 控制路由方式（正交），`rounded` 控制弯折形状（直角 vs 圆角）。仅写 `edgeStyle` 不写 `rounded=0` → Draw.io 默认圆角，视觉效果不统一。

**正例**：
```xml
style="edgeStyle=orthogonalEdgeStyle;rounded=0;exitX=1;exitY=0.5;entryX=0;entryY=0.5;"
```

**反例**：
```xml
<!-- ❌ 缺 rounded=0 → 默认圆角弯折 -->
style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.5;entryX=0;entryY=0.5;"
```

---

### 规则1：横向泳道正向连线规则 — "直线优先，避免折弯"

核心原则：**在条件允许的情况下，以直线连接，避免折弯线。** 出口位置的选择以"最短路径、最少折弯"为判断标准。

> ⚠️ **适用范围**：规则 1a~1d 针对**普通活动框**（矩形/菱形/椭圆）。**不适用于驳回三角（triangle）和循环圈（ellipse）**——这两种符号有独立的连线规则（驳回三角见 V10.2.20 §驳回三角连线方位规则，循环圈见规则 2b）。

#### 1a. 同泳道串联活动

后续串联活动在**同一泳道的右侧水平方向**，出口自然选右侧，直线连接：

```
出口 = 右侧中间  →  exitX=1;exitY=0.5
入口 = 左侧中间  →  entryX=0;entryY=0.5
```

```xml
<mxCell id="e_same_lane" style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.5;entryX=0;entryY=0.5;" edge="1" source="act_a" target="act_b" parent="swim_container">
  <mxGeometry relative="1" as="geometry"/>
</mxCell>
```

#### 1b. 串联活动在下方的泳道

后续串联活动（非并列关系）在**下方泳道**时，出口选下方，近乎直线向下进入目标：

```
出口 = 下侧中间  →  exitX=0.5;exitY=1
入口 = 左侧中间  →  entryX=0;entryY=0.5
```

#### 1b-补充：障碍检测（连线禁穿规则）⚠️ V10.2.2 新增

**R-line-no-pass-through**：任何连线不得穿过任何活动框的视觉区域（含活动矩形、决策菱形、驳回/循环连接符号、开始/结束椭圆等一切可见图元）。

**触发场景**：多个活动在纵向堆叠的泳道中，全部从下方出口（`exitY=1`）指向下方泳道的目标节点时，上方活动的垂直线段可能穿过中间泳道活动框。

**检测方法**（零 waypoint 下预测正交路由路径）：
1. 若连线使用下方出口（`exitX=0.5;exitY=1`），预测垂直段 x = 源节点中心 x（lc 绝对坐标）
2. 遍历源泳道与目标泳道之间的**每一个中间泳道**
3. 对每个中间泳道的所有活动框，检查是否满足：**垂直段 x 落入活动框 x 范围** `[box_x, box_x+box_w]` **AND** **垂直段 y 行程覆盖活动框 y 范围** `[box_y, box_y+box_h]`
4. 仅 x 重叠无 y 重叠 → 不是障碍（如竖线在活动框左侧/右侧但不经过其 y 层）
5. **有障碍 → 出口改为右侧** (`exitX=1;exitY=0.5`)，垂直段自然右移至源节点右侧，绕开障碍

**修正示例**（物料编码申请流程）：

```
❌ 错误：act_admin(x=280)→dec_dept (下方出口, exitY=1)
   垂直段 x=320 穿过 act_rd(x[280,360]) 和 act_me(x[280,360])
✅ 正确：act_admin→dec_dept (右侧出口, exitX=1)
   垂直段 x≈400，完全绕开 act_rd 和 act_me 的 x 范围
```

> ⚠️ 障碍检测在"正向连线出口决策逻辑"中**优先于规则 1b**：先执行障碍检测，有障碍则直接用右侧出口，跳过规则 1b 的下方出口选择。

#### 1c. 串联活动在上方的泳道

后续串联活动（非并列关系）在**上方泳道**时，出口选右侧，避免从上方折返造成折弯：

```
出口 = 右侧中间  →  exitX=1;exitY=0.5
入口 = 左侧中间  →  entryX=0;entryY=0.5
```

#### 1d. 并行活动入口

一个活动触发多个并行活动时，所有并行活动的入口统一从**左侧**进入（`entryX=0;entryY=0.5`），避免从顶部堆叠。

#### 正向连线出口决策逻辑

```
if 目标在同一泳道右侧:
    exit = 右侧 (exitX=1;exitY=0.5)  ← 直线，无折弯
elif 目标在下方泳道:
    if 障碍检测(垂直段穿过中间泳道活动框):
        exit = 右侧 (exitX=1;exitY=0.5)  ← V10.2.2: 绕过障碍
    else:
        exit = 下侧 (exitX=0.5;exitY=1)  ← 近直线，最少折弯
elif 目标在上方泳道:
    exit = 右侧 (exitX=1;exitY=0.5)  ← 避免从上方折返
```

#### 1d. 下方活动出口规则（V10.2.10 新增）⚠️

**触发条件**：两个活动在垂直方向（不同泳道）有先后时序关系，上方活动 A → 下方活动 B。

**规则**：下方活动 B 的出口走**右侧**（`exitX=1;exitY=0.5`），而非下方。入口保持从上方进入（`entryX=0.5;entryY=0`）。

```
正确（下方活动右出）：                错误（下方活动下出）：
  [A]                                   [A]
   ↓ (A exit bottom)                     ↓
  [B]────→ (B exit right)               [B]
                                          ↓ (B exit bottom — 违背规则！)
                                        [C]
```

**原因**：下方活动已被占用了上方入口，若出口也走下方，视觉上形成"上下都连"的拥挤感；出口走右侧使连线自然向右流淌。

**适用范围**：所有跨泳道向下串联的边中，源节点是"下方活动"的边（即该节点刚作为跨泳道边的目标被上方活动进入）。典型场景：
- 010(lane_0)→020(lane_1) 之后，020→030 的出口走右侧
- 020(lane_1)→030(lane_2) 之后，030→040 的出口走右侧

**不适用范围**：
- 上方活动主动向下连的边（如 010→020）：上方出口仍走下侧（`exitX=0.5;exitY=1`）
- 向上方回连的边（如 040→050 上行）：出口仍按规则 1c 走右侧
- 同泳道串联边：按规则 1a 走右侧

---

### 规则2：驳回连线 — 三角形连接符号 + 动态位置

驳回（不通过/回退）活动时，**禁止**画长距离折返线。使用**三角形连接符号** + **带箭头连线**。

- **三角形尺寸**：20×20 px（宽度=高度=20px，正方形三角形）
- **三角形距活动节点间距**：**1/3 活动节点高度**（默认活动节点 60px → 间距 20px）
  - 右侧驳回三角：`三角.x = 活动框右侧(x+w) + 间距`
  - 下方驳回三角：`三角.y = 活动框下侧(y+h) + 间距`
  - 上方入口三角：`三角.y = 活动框上侧(y) - 三角高度 - 间距`
  - 上方空间不足时，将活动节点整体下移腾空
- **带箭头连线**（`endArrow=block`）：驳回发起点 ← 决策节点(否)；驳回目标点 → 目标活动
- 配对三角用**相同数字编号**（1、2、3…），多个驳回至同一目标共用同一编号

#### ⚠️ 驳回三角位置由正向连线方向决定（核心规则）

驳回连接点的位置**必须根据该活动正向连线的出口方向来确定**，确保驳回线和正向线不在同侧拥挤。

| 正向连线出口 | 驳回三角位置 | 三角旋转 | 说明 |
|-------------|------------|---------|------|
| 下方（`exitY=1`） | **右侧** | `rotation=0`▶ 或 `rotation=180`◀ | 下方已被正向占用，驳回放右侧 |
| 右侧（`exitX=1`） | **下方** | `rotation=90`▼ | 右侧已被正向占用，驳回放下方 |
| 无正向出口（末尾节点） | **右侧** | `rotation=0`▶ 或 `rotation=180`◀ | 默认放右侧 |

**出口三角 XML 示例**（正向出口=右侧 → 驳回在下，rotation=90▼）：

```xml
<mxCell id="rej_out_1" value="1" style="triangle;whiteSpace=wrap;html=1;
  fillColor=#FFB366;strokeColor=#D79B00;fontSize=14;fontStyle=1;
  rotation=90;pointerEvents=0;"
  vertex="1" parent="lane_xxx">
  <mxGeometry x="..." y="..." width="20" height="20" as="geometry"/>
</mxCell>
```

**出口三角 XML 示例**（正向出口=下方 → 驳回在右，rotation=0▶）：

```xml
<mxCell id="rej_out_1" value="1" style="triangle;whiteSpace=wrap;html=1;
  fillColor=#FFB366;strokeColor=#D79B00;fontSize=14;fontStyle=1;
  rotation=0;pointerEvents=0;"
  vertex="1" parent="lane_xxx">
  <mxGeometry x="..." y="..." width="20" height="20" as="geometry"/>
</mxCell>
```

**驳回目标点（入口三角）**：位于目标活动节点上方约 20px 处，`rotation=90`▼（三角尖朝下指向目标活动）。多个驳回指向同一目标时，横向平铺（驳6规则）。

**入口三角 XML 示例**（rotation=90▼，出口走底部尖）：

```xml
<mxCell id="rej_in" value="1" style="triangle;whiteSpace=wrap;html=1;
  fillColor=#FFB366;strokeColor=#D79B00;fontSize=14;fontStyle=1;
  rotation=90;pointerEvents=0;"
  vertex="1" parent="lane_target">
  <mxGeometry x="..." y="0" width="20" height="20" as="geometry"/>
</mxCell>
```

#### ⚠️ 决策→三角连线出口必须与三角位置一致（V7.9 新增）

决策节点到驳回出口三角的连线出口方向，**必须与三角所在侧一致**，确保决策节点的"是"(正向)和"否"(驳回)两条出线**从菱形不同侧发出**：

| 三角位置 | 决策→三角连线出口 | 正向连线出口 | 效果 |
|---------|-----------------|------------|------|
| 右侧 (`rotation=0`▶) | `exitX=1;exitY=0.5`（菱形右侧） | 下方 | 两条出线**不同侧**，互不干扰 |
| 下方 (`rotation=90`▼) | `exitX=0.5;exitY=1`（菱形下方） | 右侧 | 两条出线**不同侧**，互不干扰 |

> ⚠️ **核心原则**：决策节点的两条出线**必须从菱形不同侧发出**。若两条线同侧（如都从下方或都从右侧），会导致连线重叠、逻辑难以辨认，是严重的布局错误。

#### ⚠️ 驳回三角连线方位规则 — 非对称规则：左侧入，底部出（V10.2.20 修正，2026-06-05 二次修正）

**驳回三角的连线必须走三角形的平边（flat edge），而非尖角。** 三角的 `direction` 决定平边位置：

| 三角场景 | 三角方向 | 平边位置(视觉) | 连线锚点 | 完整连线规则 |
|---------|---------|---------------|---------|------------|
| 出口三角在**右侧** | `triangle;rotation=0`(▶右) | **左侧**（平边，尖角朝右） | 入口 `entryX=0;entryY=0.5` | 活动右出 → 三角平边入 |
| 出口三角在**下方** | `triangle;rotation=90`(▼下) | **左侧**（Draw.io实现：X=0锚点映射到可视平边） | 入口 `entryX=0;entryY=0.5` | 活动下出 → 三角左侧入 |
| 入口三角在**上方** | `triangle;rotation=90`(▼下) | **底部**（平边，尖角朝下指向目标） | 出口 `exitX=1.0;exitY=0.5` | 三角底部出 → 目标顶部入 |

> ⚠️ **关键认知（V10.2.12 系统，`direction=south/north` 术语）**：Draw.io 三角锚点映射不走直觉的"包围盒=平边"逻辑。`direction=south` 三角的可视平边在顶部，但 `entryX=0.5;entryY=0`（顶部中心锚点）实际不落在平边上；**`entryX=0;entryY=0.5`（左侧锚点）才映射到可视平边**（V10.1 实证）。同理 `direction=north` 三角的可视平边在底部，`exitX=0.5;exitY=1` 正确落在平边上。**因此三角规则是非对称的：south 走左侧入，north 走底部出。禁止用抽象几何推理替代 Draw.io 实际锚点行为。**（⚠️ 此注释基于 V10.2.12 的 `direction=south/north` 系统；V10.2.18 改用 `rotation=0/90` 系统后，锚点映射需重新实证验证，以下 V10.2.12 数值可能不准确）**

**三角在右侧时的连线示例**（rotation=0▶，平边在左）：

```xml
<!-- 决策节点(正向出口=下方) → 出口三角(右侧) — 从菱形右侧出→三角平边(左侧)入 -->
<mxCell id="e_d_rej" style="edgeStyle=orthogonalEdgeStyle;rounded=0;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;" edge="1" source="dec_1" target="rej_out_1" parent="lane_1">
  <mxGeometry relative="1" as="geometry"/>
</mxCell>
```

**三角在下方时的连线示例**（rotation=90▼，左侧入）：

```xml
<!-- 决策节点(正向出口=右侧) → 出口三角(下方) — 菱形下方出→三角左侧入 -->
<mxCell id="e_d_rej" style="edgeStyle=orthogonalEdgeStyle;rounded=0;endArrow=block;exitX=0.5;exitY=1;entryX=0;entryY=0.5;" edge="1" source="dec_3" target="rej_out_3" parent="lane_3">
  <mxGeometry relative="1" as="geometry"/>
</mxCell>
```

#### ⚠️ 驳回点间禁止连线规则（V8.1 修正）

驳回连接有**两端**，各自独立锚定，两端之间**无连线**：

```
驳回发起点（出口三角）  ←连线←  决策节点(否)      …各自独立，无连线…
驳回目标点（入口三角）  →连线→  目标活动
```

> ⚠️ **核心原则**：驳回发起点（出口三角）和驳回目标点（入口三角）是两个独立的标记点，各自通过连线连接到自己的节点（决策/目标活动）。**两点之间本身不连线。**

**驳回发起点（出口三角）→ 决策节点连线**：

```xml
<!-- 决策节点(正向出口=下方) → 出口三角(右侧) -->
<mxCell id="e_d1_r1" style="edgeStyle=orthogonalEdgeStyle;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;" edge="1" source="dec_1" target="rej_out_1" parent="lane_1">
  <mxGeometry relative="1" as="geometry"/>
</mxCell>
```

**驳回目标点（入口三角）→ 目标活动连线**：

```xml
<!-- 入口三角 → act_1 [同泳道] — 三角底部(平边)出，进入目标顶部 -->
<mxCell id="e_in_a1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" source="rej_in" target="act_1" parent="lane_0">
  <mxGeometry relative="1" as="geometry"/>
</mxCell>
```

#### ⚠️ 驳回三角节点禁令规则（V10.2 新增）

驳回三角（`rej_out_*` / `rej_in_*`）是路径的**标记符号**，不是流程处理节点。必须遵守以下禁令：

| 规则项 | 内容 | 违规示例 |
|-------|------|---------|
| R-rej-out-no-edge | `rej_out_*` 节点**禁止有任何出边**。它是驳回路径的终点标记，图中至此结束，不得连向任何节点。 | `rej_out_dept → act_collect` ❌ |
| R-rej-in-exactly-one-edge | `rej_in_*` 节点**只能有恰好一条出边**（到目标活动），且**禁止有任何入边来自 `rej_out_*`**（两端独立）。 | `rej_out → rej_in` ❌；`rej_in` 有 0 条出边 ❌ |

> ⚠️ **根因分析（录用审批流程 Bug）**：生成逻辑把 `rej_out` 当成普通活动节点，对它应用了"连接到下一步"的规则，错误地认为驳回后流程要继续走到 `act_collect`。正确语义：`rej_out` 是驳回路径的**终点标记**（图中至此结束），不是要继续连线的处理步骤。驳回路径的实际返回逻辑由 `rej_in → 目标活动` 表示，两个三角之间没有任何连线。

**生成指引**：
- 创建 `rej_out` 后：**停止**，不创建任何从 `rej_out` 出发的连线
- 创建 `rej_in` 后：仅创建 `rej_in → 目标活动` 一条出边，不创建任何入边

---

### 规则2b：循环回路 — 圆圈连接符号（V9.0 新增）

循环回路（loop-back）与驳回路径（reject）是两种不同语义的连接，用**不同符号**区分：

| 连接类型 | 语义 | 符号 | 尺寸 | 颜色 |
|---------|------|------|------|------|
| 驳回 | 一次性回退（审批不通过→回到上游） | **三角** (triangle) | 42×30 / 30×42 px | 橙黄 `#FFB366` / `#D79B00` |
| 循环 | 持续性回路（处理完→回到监控起点继续） | **圆圈** (ellipse) | 30×30 px | 浅绿 `#B9E0A5` / `#82B366` |

#### ⚠️ 循环圆圈与驳回三角共享相同的「两端独立锚定」模式

```
循环起点（出口圈）    ←连线←  源节点         …各自独立，无连线…
循环终点（入口圈）    →连线→  目标节点
```

> ⚠️ **核心原则**：循环出口圈和入口圈是两个独立的标记点，各自通过连线连接到自己的节点（源节点/目标活动）。**两点之间本身不连线。** 与驳回三角的规则完全一致，仅符号从三角换为圆、颜色从橙黄换为浅绿。

#### 圆圈符号 XML 示例

**出口圈**（位于源节点旁，无正向出口时默认右侧）：

```xml
<mxCell id="loop_out_1" value="1" style="ellipse;whiteSpace=wrap;html=1;
  fillColor=#B9E0A5;strokeColor=#82B366;fontSize=14;fontStyle=1;
  pointerEvents=0;"
  vertex="1" parent="lane_xxx">
  <mxGeometry x="..." y="..." width="30" height="30" as="geometry"/>
</mxCell>
```

**入口圈**（位于目标活动节点上方约 5px 处）：

```xml
<mxCell id="loop_in" value="1" style="ellipse;whiteSpace=wrap;html=1;
  fillColor=#B9E0A5;strokeColor=#82B366;fontSize=14;fontStyle=1;
  pointerEvents=0;"
  vertex="1" parent="lane_target">
  <mxGeometry x="..." y="0" width="30" height="30" as="geometry"/>
</mxCell>
```

#### 出口圈位置规则（对标驳回三角）

| 源节点正向出口 | 出口圈位置 | 说明 |
|-------------|----------|------|
| 下方（`exitY=1`） | **右侧** | 下方被正向占用 |
| 右侧（`exitX=1`） | **下方** | 右侧被正向占用 |
| 无正向出口（循环终点节点） | **右侧（默认）** | 节点只有循环回路，无下一节点 |

#### 入口圈位置规则

- 位于**目标活动节点上方**（与驳回入口三角位置相同）
- 多个循环汇聚同一目标 → 共用一个入口圈，共用同一编号
- 入口圈 → 目标节点连线：`exitX=0.5;exitY=1;entryX=0.5;entryY=0`

#### 连线规则

| 连线 | 出口圈位置 | 入口锚点 | 说明 |
|------|----------|---------|------|
| 源节点 → 出口圈 | 右侧（正向出口=下） | `entryX=0;entryY=0.5`（左侧入） | 圈在右侧，从左侧进入 |
| 源节点 → 出口圈 | 下方（正向出口=右） | `entryX=0.5;entryY=0`（上方入） | 圈在下方，从上方进入 ⚠️ |
| 入口圈 → 目标节点 | 上方（默认） | `exitX=0.5;exitY=1`（下方出） | 入口圈下方→目标上方 |

> ⚠️ **循环圈入口锚点选择规则**：圈在锚定节点哪一侧，连线就从圈的**靠近节点的那一侧**进入。圈在右侧→左侧入；圈在下方→上方入。这是避免连线绕远路的基本原则。

#### 连线 XML 示例

**圈在右侧**：
```xml
<!-- act_4 → 出口圈 (同泳道, 圈在右侧→左侧入) -->
<mxCell id="e_a4_lo4" value="" style="edgeStyle=orthogonalEdgeStyle;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;" edge="1" source="act_4" target="loop_out_a4" parent="lane_1">
  <mxGeometry relative="1" as="geometry"/>
</mxCell>
```

**圈在下方**：
```xml
<!-- dec_cs → 出口圈 (同泳道, 圈在下方→上方入) -->
<mxCell id="e_cs_lo1" value="未通过" style="edgeStyle=orthogonalEdgeStyle;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" source="dec_cs" target="loop_out_1" parent="lane_zp">
  <mxGeometry relative="1" as="geometry"/>
</mxCell>
```

**入口圈 → 目标节点**：

> ⚠️ **关键差异**：循环圆圈连线**不标注文字**（`value=""`），区别于驳回三角连线通常标注"否"或条件文字。循环语义已由绿色圆圈本身传达，无需额外的文字标识。

#### 循环圆圈与锚定节点间距（V10.0 更新）

> 循环圆圈（出口圈/入口圈）与锚定活动节点之间的间距 = **1/3 活动节点高度**（默认 60px → 20px），与驳回三角间距规则统一。

| 规则项 | 内容 |
|-------|------|
| 间距标准 | 活动节点高度 × 1/3 ≈ **20px** |
| 出口圈（水平方向） | x = 活动框右侧(x+w) + 20，y 垂直居中于活动框 |
| 入口圈（垂直方向） | 若入口圈在活动框上方：y = 活动框上沿 - 圆圈高度 - 20，x 水平居中于活动框 |
| 上方空间不足 | 将锚定活动节点整体下移 20px 腾空给入口圈 |

**出口圈布局示例**：
```xml
<!-- act_4 x=840,w=80 → loop_out x=940(840+80+20) -->
<mxCell id="loop_out_a4" value="循" vertex="1" parent="lane_1">
  <mxGeometry x="940" y="45" width="30" height="30" as="geometry"/>
</mxCell>
```

**入口圈布局示例**（act_1 y=50 → loop_in y=0, 间距=20）：
```xml
<!-- 入口圈 y=0, act_1 下移至 y=50 -->
<mxCell id="loop_in" value="循" vertex="1" parent="lane_0">
  <mxGeometry x="265" y="0" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="act_1" value="填报实际进度信息" vertex="1" parent="lane_0">
  <mxGeometry x="240" y="50" width="80" height="60" as="geometry"/>
</mxCell>
```

#### ⚠️ 驳回三角 vs 循环圈 — 锚点规则对比（V10.2.8 新增，防混淆）

以下两种场景中，符号均在**目标活动正上方**，但出口锚点**完全不同**。必须严格区分，禁止套用：

| 符号类型 | 位于目标上方时的出口锚点 | 说明 |
|---------|---------------------|------|
| **驳回入口三角** rej_in (rotation=90▼) | **底部尖出** `exitX=1.0;exitY=0.5` | 三角底部尖出→目标顶部入。rotation=90三角底部=尖 |
| **驳回出口三角** rej_out (rotation=0▶/90▼) | **左侧入** `entryX=0;entryY=0.5` | 决策下出→三角左侧入。rotation=0/90三角X=0锚点映射到可视平边 |

> ⚠️ 三角锚点规则是**非对称的**：south三角走左侧，north三角走底部。原因：Draw.io三角锚点映射不遵循直觉的包围盒逻辑，X=0在south三角上正确落在平边，而Y=1在north三角上正确落在平边。

纵向泳道图（`horizontal=0`）按照横向泳道的"直线优先、避免折弯"原则对称处理：

- 同列串联：出口=下侧，入口=上侧
- 目标在右侧泳道：出口=右侧
- 目标在左侧泳道：出口=下侧（避免从左侧折返）
- 驳回点位置同横向逻辑对称：正向出口=下侧 → 驳回在右；正向出口=右侧 → 驳回在下

---

### 完整连线决策流程

```
第1步：确定所有正向连线的出口方向（遵循"直线优先、避免折弯"，V10.2.2 新增障碍检测：下方出口若穿过中间泳道活动框→改右侧出口）
第2步：识别驳回路径：根据正向出口方向，确定驳回发起点位置（正向在下→驳回在右；正向在右→驳回在下）
第3步：识别循环回路：判定哪些活动需要回到上游继续循环（如"持续跟进""调整计划"→回到"填报进度"）
第4步：检查多分支并行活动：若决策节点有 N≥2 个无逻辑依赖的并行后续活动 → 纵向堆叠排列（规则5）
第5步：检查多线汇聚：若 N≥2 条线汇聚同一节点且入口重叠 → 节点错位下移 75px（规则6）
第6步：检查终点汇聚：若"结束"接收 N≥3 条线 → 终点沉底 + 1.25H 错位（规则7）
第7步：检查出口差异化：并行活动连同一目标节点 → 右侧出口（规则8）
第8步：检查入口侧统一：所有汇聚到同一目标节点的连线 → 左侧单点入口（规则9）
第9步：生成驳回三角符号 + 循环圆圈符号，各按「两端独立锚定」模式连线（两端之间无连线）
```

**禁止**：
- 在同一边同时有入口和出口
- 正向连接线和驳回连接线在同侧拥挤
- 不必要的折弯连线（能直线则直线）
- **驳回发起点与驳回目标点之间有任何连线**（两点各自独立锚定，互不连线）

**Draw.io XML 结构模板**（参见 `references/drawio_template.md`）

---

### 规则2c：协同框 — 跨泳道协作活动标记（V10.2.6 新增，V10.2.10 修正 parent）

协同框（Collaboration Box）用于标识**同一活动由多个角色协同完成**的场景——同一活动框出现在不同泳道中，用红色虚线框将它们围起来表示协作关系。

**触发条件**：
- 同一活动（相同文字内容）出现在 ≥2 个不同泳道中
- 该活动是多角色之间的协同/沟通（如"需求沟通""招标沟通"等）

**样式规范**：

| 属性 | 值 | 说明 |
|------|-----|------|
| shape | 矩形（默认） | 不指定 shape，使用默认矩形 |
| dashed | `1` | 虚线边框 |
| strokeColor | `#FF0000` | 红色 |
| fillColor | `none` | 透明填充 |
| strokeWidth | `2` | 线宽 2px |
| whiteSpace | `wrap` | 文本换行 |
| html | `1` | HTML 渲染 |
| vertex | `"1"` | 可渲染节点 |
| **parent** | **`"1"`（根节点）** | ⚠️ V10.2.10 修正：必须挂根节点，详见下方说明 |
| pointerEvents | `0` | 不可交互（纯视觉标记） |

#### ⚠️ parent="1" 的原因：防编辑丢失（V10.2.10 修正）

**旧方案** `parent="lc"`：协同框作为 lc 的子元素，被 lc 的 `childLayout=stackLayout` 布局引擎管理。当用户在 Draw.io 中拖拽任意图形/连线时，stackLayout 会触发重算，将协同框视为需要"整理"的普通子元素，覆盖其手工指定的 x/y 坐标 → **协同框丢失或错位**。

**新方案** `parent="1"`：协同框直接挂到根节点，完全脱离 lc 的布局引擎。根节点无 stackLayout，坐标永不被动覆盖 → **任意编辑均不影响协同框**。

**坐标转换**（新方案必须）：协同框的位置需要描述各泳道中活动的包围盒，但活动的坐标是**泳道相对坐标**。改为 `parent="1"` 后，需要加上 lc 容器在根节点中的偏移量：

```
root_x = LC_X + collab_x_in_lc
root_y = LC_Y + collab_y_in_lc
```

其中 `LC_X`、`LC_Y` 是 lc 容器在 `mxGeometry` 中指定的 `x`、`y` 值（通常为 40）。

**定位规则**：
- 协同框的 x = `LC_X + min(所有协同活动 x) - padding`（建议 padding=10px）
- 协同框的 y = `LC_Y + min(所有协同活动 y + lane_top) - padding`（根节点绝对坐标）
- 协同框的 w = `max(协同活动 x + w) - min(协同活动 x) + 2 × padding`
- 协同框的 h = `max(协同活动 y + lane_top + h) - min(协同活动 y + lane_top) + 2 × padding`
- 协同框必须在**所有 lane 和活动节点之后**写入 XML（渲染在上层 + 包住活动）

**XML 示例**：
```xml
<!-- 协同框：跨 lane_bm 和 lane_bz，包围两个 007 活动
     lc 在根节点偏移: LC_X=40, LC_Y=40
     collab 在 lc 内坐标: (890, 60) → 根坐标: (930, 100) -->
<mxCell id="collab_1" value="" style="whiteSpace=wrap;html=1;dashed=1;strokeColor=#FF0000;fillColor=none;strokeWidth=2;pointerEvents=0;" vertex="1" parent="1">
  <mxGeometry x="930" y="100" width="100" height="420" as="geometry"/>
</mxCell>
```

> ⚠️ **关键**：协同框坐标是相对于**根节点 "1"** 的绝对坐标。需要做坐标转换：`root_absolute = LC_OFFSET + lc_relative`，其中 `lc_relative = lane_top + node_y_in_lane`。

**生成指引**：
1. 识别所有在不同泳道中出现相同文字的活动 → 这些是协同活动
2. 计算协同框在 lc 内的包围矩形
3. 转换到根坐标：加上 `LC_X`、`LC_Y` 偏移
4. 在脚本末尾（所有节点之后）输出协同框 XML，`parent="1"`

---

### 规则3：多分支汇聚防重叠 — 目标节点下移 + 出口改下侧

**基本原则**：**避免连线重叠导致逻辑难以辨认。** 连线清晰可读 > 布局紧凑。

**适用场景**：3 个及以上活动汇聚到同一个目标节点（不限「结束」节点，任何被多方汇聚的节点都适用）。

**处理方式**：
1. 将**目标节点下移**，放置在汇聚源节点的下方
2. 各汇聚源节点的**出口方向从右侧改为下侧**（`exitX=0.5;exitY=1`）
3. 三条（或多条）汇聚连线在垂直空间上自然分离，杜绝水平方向重叠

```
错误（重叠严重）：                    正确（下移+下侧出口）：
[活动A] →┐                           [活动A]
[活动B] →┼→ [目标]                    [活动B]
[活动C] →┘                           [活动C]
                                          ↓    ↓    ↓
                                       [  目标节点  ]
```

**出口/入口配置**：
- 汇聚源节点出口：`exitX=0.5;exitY=1`（下侧中间）
- 目标节点入口：`entryX=0.5;entryY=0`（上侧中间），或根据具体位置调整

**示例 — 判定质量结论三分支汇聚到结束**：
```xml
<!-- 三个活动的出口均改为下侧 -->
<!-- act_direct → end -->
<mxCell id="e_direct_end" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=1;entryX=0.34;entryY=0;" edge="1" source="act_direct" target="end" parent="lane_2"/>

<!-- act_repair → end -->
<mxCell id="e_repair_end" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" source="act_repair" target="end" parent="lane_2"/>

<!-- act_scrap → end -->
<mxCell id="e_scrap_end" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=1;entryX=0.66;entryY=0;" edge="1" source="act_scrap" target="end" parent="lane_2"/>
```

### 规则4：同向多连线连接点共点

**适用场景**：同一节点有 2 条及以上**同方向**连线（全部是出口，或全部是入口）。

**处理方式**：同方向的连线可**共用同一个连接点**（共点），无需分散到相邻的不同连接点上。

```
分散连接（不推荐，杂乱）：            共点连接（推荐，简洁）：
      ┌─ 路径A                          路径A ─┐
  ◇ ──┼─ 路径B                    ◇ ────────┼─ 路径B
      └─ 路径C                          路径C ─┘
```

**示例 — 决策节点三个出口共点**：
```xml
<!-- 三条出口连线全部使用 exitX=1;exitY=0.5（右侧中间同一个点） -->
<mxCell id="e_dec_qualified"  style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.5;..." source="dec_qc" target="act_direct"  parent="lc"/>
<mxCell id="e_dec_repair"     style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.5;..." source="dec_qc" target="act_repair"  parent="lc"/>
<mxCell id="e_dec_scrap"      style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.5;..." source="dec_qc" target="act_scrap"   parent="lc"/>
```

> ⚠️ 注意：共点仅适用于**同方向**连线（全出口或全入口）。一进一出不可以共点。

---

### 规则5：同泳道多分支并行活动纵向排列（V9.1 新增）

**触发条件**：横向泳道中，一个节点分支出 N 个（N ≥ 2）并行后续活动，且这些活动之间**无逻辑依赖关系**。

**核心原则**：
> 没有逻辑关系的活动，横向泳道中纵向并列摆放，不水平并列摆放。

```
错误（水平并排，连线重叠）：        正确（纵向堆叠，清晰分离）：
                                         [活动1] y=30
dec ─┬─ [活动1] ─┬─ [活动2] ─┬─ [活动3]   [活动2] y=105  (30+60+15)
     └───────────┴───────────┘            [活动3] y=180  (105+60+15)
          三条连线严重重叠
```

| 规则项 | 内容 |
|-------|------|
| 排列方式 | 并行活动在泳道内**纵向堆叠**（上下排列），不得水平并排 |
| 纵向间距 | 相邻活动框的上沿间距 = 活动框高度 × 1/4（60px × 1/4 = **15px**） |
| 泳道高度 | 若原始高度不足，按 `base + (n-1) × (nodeH + gap)` 扩展 |
| 出口方向 | 源节点出口按正向连线规则不变（通常右侧出口 `exitX=1`） |
| 入口方向 | 各并行活动统一从左侧进入（`entryX=0;entryY=0.5`） |
| 连线 parent | 同泳道 → `lane_X` |

> ⚠️ 多条出线从源节点同一点发出（如 `exitX=1;exitY=0.5`，符合规则 4 共点原则），通过正交路由分发到各自的 y 层目标。关键在于活动不水平并排，而非连线不出同一点。

**布局示例**（dec_2 三分支）：
```xml
<!-- act_4 y=30, act_5 y=105, act_6 y=180 -->
<mxCell id="act_4" value="持续跟进" vertex="1" parent="lane_1">
  <mxGeometry x="800" y="30" width="80" height="60" as="geometry"/>
</mxCell>
<mxCell id="act_5" value="直接调整" vertex="1" parent="lane_1">
  <mxGeometry x="800" y="105" width="80" height="60" as="geometry"/>
</mxCell>
<mxCell id="act_6" value="上报领导" vertex="1" parent="lane_1">
  <mxGeometry x="800" y="180" width="80" height="60" as="geometry"/>
</mxCell>
```

---

### 规则6：多线汇聚节点错位处理（V9.1 新增）

**触发条件**：N ≥ 2 条连线汇聚到**同一节点**（不限终点），入口方向重叠导致连线重复。

**核心原则**：
> 多条线汇聚到同一节点且入口重叠 → 节点下移错位。

**偏移量**：
| 规则项 | 内容 |
|-------|------|
| 打开量 | `offsetY = nodeH + nodeH × 1/4 = 60 + 15 = 75px` |
| 偏移方向 | 纵向下移（y 增大），x 保持不变 |
| 参照基准 | 同一泳道内、同一水平区域最近的并行节点 |

**布局示意**：
```
修正前（重叠）：                    修正后（错位）：
  源节点A ──→  [节点]y=30            源节点A ──→  [节点]y=105
  源节点B ──↗  （重叠）              源节点B ──↗  （错位 +75px）
```

**适用场景**：不限终端节点，任意活动节点被多条线汇聚且入口重叠时，均可应用此规则。

> ⚠️ 此规则与规则 4（同向共点）不冲突：多条线从不同方向汇聚到同一节点，入射角度不同但入口区域重叠时，通过下移节点解决问题；若入口方向本来就分离（如一个从上方、一个从左侧），则无需错位。

---

### 规则7：多线汇聚 — 终点下沉定位（V9.3 新增）

**触发条件**："结束"节点接收 N ≥ 3 条来自不同泳道/位置的汇聚连线，且入口在左侧重叠。

**核心原则**：
> 终点置于泳池最底部，利用其下方的空间让连线穿行，避免与活动节点的连线交叉。

**定位规则**：

| 规则项 | 内容 |
|-------|------|
| 泳道位置 | "结束"节点置于**最下方角色泳道内**（该泳道增高至 270px），**不新增空泳道** |
| 垂直错位 | 距最底部活动节点的垂直错位 = **1.25 × 活动框高度** |
| 错位公式 | `offsetY = nodeH × 1.25 = 60 × 1.25 = 75px` |
| 效果 | 连线从上方活动节点下方穿过，不与其他连线交叉 |

**布局示意**：
```
修正前（结束嵌在泳道中间）：        修正后（结束沉底 + 1.25H 错位）：
  [活动A] ──→                          [活动A] ──→
  [活动B] ──→  [结束] ← 拥挤重叠        [活动B] ──→
                                              ↓         ← 下方空隙穿行
                                           [结束]       ← 独立泳道
```

---

### 规则8：并行共汇 — 出口差异化（V9.3 新增）

**触发条件**：N ≥ 2 个并行活动（同列不同泳道）同时连接到**同一个**下游节点。

**核心原则**：
> 多个并行活动的出口路径需要差异化处理，避免在垂直通道中拥堵重叠。

> 原因：若所有并行活动统一使用下方出口（`exitY=1`），所有连线会在同一个垂直通道中重叠。改为右侧出口（`exitX=1`）使各泳道的并行活动产生水平方向差异化的出口路径。

**与现有规则关系**：
> 此规则覆盖"有错位关系的活动出口从下方"的通用原则，为并行共汇场景的**差异化特例**。

**连线规则**：

| 规则项 | 内容 |
|-------|------|
| 出口方向 | 所有并行活动出口统一为**右侧**（`exitX=1`），而非下方（`exitY=1`） |
| 效果 | 各泳道并行线的出口路径先在水平方向错开，再向下绕行至目标节点 |

**Draw.io Edge 模板**：
```
style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.5;entryX=0;entryY=0.5;"
```

**布局示意**：
```
修正前（下方出口重叠）：               修正后（右侧出口差异化）：
  [exec设计]                              [exec设计] ──→ 右出
  [exec工艺]                              [exec工艺] ──→ 右出
  [exec供应链]    ↓ 垂直拥堵               [exec供应链]──→ 右出 ─→ 下行 ─→ 目标
  [exec物料]                              [exec物料] ──→ 右出
  [exec质量]                              [exec质量] ──→ 右出
```

---

### 规则9：多线汇聚 — 入口侧统一（V9.3 新增）

**触发条件**：与规则7联动。N ≥ 2 条连线汇聚到同一目标节点（含驳回线与正向线），且目标已按规则7置于最底部。

**核心原则**：
> 所有汇聚到同一节点的连线，入口方向统一为**左侧**或**上侧**，使用**单一连接点**。

> 原因：从左侧/上侧进入是标准化入口方向；单一连接点遵循规则4（同向多连线连接点共点），避免入口点散布导致视觉杂乱。

> ⚠️ 规则8（出口差异化）与规则9（入口侧统一）**不冲突**：规则8管出口端，规则9管入口端，两者分别作用于连线的两端，并存互补。

**连线规则**：

| 规则项 | 内容 |
|-------|------|
| 入口方向 | 所有连线统一从目标节点**左侧**进入（`entryX=0`） |
| 入口点 | **单一连接点**（`entryY=0.5`），不得使用多个分散的 entryY 值 |
| 禁止 | 不得从右侧（`entryX=1`）进入，不得使用多个分散连接点 |

**Draw.io Edge 模板**：
```
style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.5;entryX=0;entryY=0.5;"
```
无需添加 waypoints，正交路由器自动处理路径。

**三规则协同示意（规则7+8+9）**：
```
规则8（出口差异化）        规则7（终点下沉）        规则9（入口统一）

[exec设计] ─→ 右出                              ┌─────────────────┐
[exec工艺] ─→ 右出                              │                 │
[exec供应链]─→ 右出 ─→ ─→ ─→ ─→ ─→ ─→ ─→ ─→   │   [ 结  束 ]   │ ← 左侧单点
[exec物料] ─→ 右出                              │  (最底部泳道)    │
[exec质量] ─→ 右出                              │  1.25H 错位     │
                                                 └─────────────────┘
驳回线 ─→ 下方直出 ─→ ─→ ─→ ─→ ─→ ─→ ─→ ─→ ─→
```

---

### 第五步：输出产物

1. 将生成的 XML 保存为 `<原文件名>_flowchart.drawio` 文件，存放在与源文件相同目录或用户指定目录。
2. 向用户展示：
   - 已确认的角色清单
   - 流程摘要（步骤数量）
   - 发现的异常（如有空白泳道，说明哪些活动缺少执行角色）
   - 输出文件路径
3. **等待用户确认流程图内容无误后**，询问是否需要生成配套的流程说明文件（.xlsx）。

---

### 第六步：生成流程说明文件（.xlsx）⚠️ V10.5.0 新增

在用户确认流程图后，生成 `data.json` 数据文件，通过 `scripts/run_flow_desc.py` Runner 调用 `flow_desc_xlsx.py` 库生成配套的 Excel 流程说明文档。

> ⚠️ **[G5 门禁 — 最高优先级]** 必须满足以下**全部**条件才能生成 Excel：
> 1. 流程图已生成并在 Draw.io 中打开
> 2. **用户已明确确认流程图效果无误**（如说"OK""没问题""效果很好"等）
> 3. 用户同意生成流程说明 Excel
> 4. G5 预览内容已展示且用户已确认
>
> **严禁在以上任何条件不满足时生成 Excel。** 违反此规则将导致用户投诉"流程都没确认就生成说明"。

#### 6.0 G5 执行流程

```
流程图已生成 → 打开 drawio 文件 → 等待用户确认
   ↓ 用户确认"OK"/"效果很好"等
[G5] 询问：是否需要生成流程说明 Excel？
   ↓ 是
展示流程说明预览——按场景差异化处理：
├── WORD 导入场景 → 从原文提取内容摘要展示（直接COPY，无字数限制）
├── 已有 Drawio 场景（场景五）→ AI 自主生成内容展示（严格按6.3.1格式）
└── 其他场景 → AI 自主生成内容展示（严格按6.3.1格式）
   ↓
等待用户确认（用户可逐条修改内容）
   ↓ 用户确认
生成 data.json（纯结构化数据） → 运行 run_flow_desc.py data.json → 生成 Excel
   ↓
交付文件
```

> ⚠️ **场景五特殊说明**：用户已有 .drawio 流程图，不经过 G1/G2/G3/G4 门禁。直接从 drawio XML 解析泳道/活动/决策/驳回/循环/协同，将全部解析结果**一次性合并预览**展示给用户确认。确认后进入 G5 内容生成阶段。

#### 6.1 Excel 结构规范

一份 Excel 文件（`.xlsx`），一个 Sheet（"流程说明"），包含三个连续子表：

**子表一：流程说明**

| 项目 | 内容 |
|------|------|
| 流程目的 | 核心描述通过运行本流程要达到的业务目标，即本流程的业务价值 |
| 适用范围 | 描述该业务适应的业务场景（不同产品、不同客户、不同情形等） |
| 总体要求 | 描述与多个活动执行要求有关的要求或每个活动说明所不能覆盖的内容 |

**子表二：角色说明**

| 序号 | 角色名称 | 角色描述 | 角色职责 |
|------|---------|---------|---------|
| 1 | （取流程图中的角色） | 对角色的解释说明 | 该角色在本流程中的主要职责，结合其所承担活动清单提炼总结 |

**子表三：活动说明**

| 序号 | 活动名称 | 操作说明 | 关键输入 | 关键输出 |
|------|---------|---------|---------|---------|
| 1 | （取流程图中的活动名称） | 操作/决策类活动的详细操作描述 | 所需输入的文件或内容 | 输出的文件或内容 |

#### 6.2 场景差异化内容策略 ⚠️ 核心规则

| 场景 | 内容来源 | 字数限制 | 处理方式 |
|------|---------|---------|---------|
| **WORD 导入** | 从原文中**直接 COPY** | **无限制**（原文多长就多长） | 逐字段从 WORD 原文提取对应段落；如原文缺失或分散，通过整合方式补全 |
| **VSDX 导入** | AI 基于流程理解**自主生成** | 流程目的≤300字、适用范围≤100字、总体要求≤500字、角色描述≤100字、角色职责≤200字、活动操作说明≤300字 | 基于流程图结构 + 网络公开知识 + 业务常识推断 |
| **图片导入** | 同上 AI 自主生成 | 同上 | 同上 |
| **自主生成** | 同上 AI 自主生成 | 同上 | 同上 |

#### 6.3 活动操作说明编写规范

**执行/操作类活动**的描述结构：
1. 接收到什么需求或信息
2. 需要补充什么信息
3. 依据什么标准或要求
4. 如何逐步操作
5. 执行/操作后输出什么
6. 如何根据不同的条件连接下一步活动

**决策/控制类活动**的描述结构：
1. 接收到什么需求或信息
2. 从哪些维度和要素进行分析
3. 做决策的有关规则和要求
4. 不同决策的路由描述

#### 6.3.1 内容质量强制要求 ⚠️ V10.5.1 新增

**每条活动的操作说明必须包含完整的标签化要素，禁止一句话概括。**

##### 执行/操作类活动 — 模板（6要素）

```
[接收]：接收到xxx（来自上游活动xx的输出）
[补充]：需要补充/确认xxx信息；若缺失则xxx
[依据]：依据《xxx》标准/规范/制度执行
[操作步骤]：
  ① xxx
  ② xxx
  ③ xxx
[输出]：输出xxx（作为下游活动xx的输入）
[路由]：完成后进入"xxx"活动；若xxx则进入"xxx"
```

##### 决策/控制类活动 — 模板（4要素）

```
[接收]：接收到xxx（来自上游活动xx的输出）
[分析维度]：
  ① xxx方面：检查xxx是否满足xxx要求
  ② xxx方面：评估xxx是否xxx
[决策规则]：
  · 合格/通过条件：xxx且xxx
  · 不合格/不通过条件：xxx或xxx
[路由]：
  · 合格/通过 → 进入"xxx"
  · 不合格/不通过 → 驳回至"xxx"
```

##### ⛔ 禁止的错误写法（典型反例）

| ❌ 错误写法 | 问题 | ✅ 正确示例片段 |
|------------|------|---------------|
| "接收招标文件，确认完整性和截止时间。" | 缺补充、依据、步骤、输出、路由 | `[接收]：接收来自客户的招标邀请函... [操作步骤]：①登录平台下载文件 ②检查完整性 ③记录关键节点 ④通知团队` |
| "从技术可行性、创新性角度评审方案，给出通过或修改意见。" | 缺接收来源、分析维度、决策规则、路由 | `[接收]：接收技术方案文档（初稿）... [分析维度]：①技术可行性 ②创新性 ③合规性... [决策规则]：三项均满足→通过` |
| "根据审核意见完成修改后确认方案内容。" | 缺接收、依据、步骤、路由 | `[接收]：接收审核意见... [操作步骤]：①逐条对照修改 ②自检完整性 ③提交终稿... [路由]：完成后进入"提交投标"` |

> ⚠️ **在执行 G5 内容预览时，AI 必须逐条自检**：每条活动说明是否包含所有要素标签？标签内容是否完整具体？如发现一句话概括，必须重写。用户也可在预览阶段指出并修正。

#### 6.4 调用方式 ⚠️ V10.5.3 简化

> **V10.5.3 起，Excel 生成采用 JSON 数据 + 固定 Runner 脚本方案。不再每次编写 Python 脚本。**

**生成流程（3 步）**：

1. **生成 `data.json`**（纯结构化 JSON，无代码）：
```json
{
  "flow_name": "流程名称",
  "process": {
    "purpose": "流程目的（≤300字）",
    "scope": "适用范围（≤100字）",
    "requirements": "总体要求（≤500字）"
  },
  "roles": [
    {"name": "角色名", "description": "角色描述", "responsibility": "角色职责"}
  ],
  "activities": [
    {"name": "活动名", "operation": "操作说明（含6/4要素标签）", "inputs": "关键输入", "outputs": "关键输出"}
  ]
}
```

2. **运行 Runner**：
```bash
python scripts/run_flow_desc.py data.json
```

3. **Runner 自动执行**：禁止字符扫描 → 格式验证 → 生成 Excel → 输出统计

> ⚠️ **重要**：AI 只需生成 `data.json`（用 Write 工具写入即可，不涉及 Python 字符串转义）。Runner 内置禁止字符扫描（`~#/*&`），内容不合格会自动拒绝并报位置。**彻底消除** V10.5.2 记录的坑1（ASCII 双引号）、坑2（JSON 转义）、坑4（隐式拼接）。

#### 6.5 输出并确认

生成 Excel 后向用户展示：
- 三个子表的行数统计
- 输出文件路径
- 提醒用户核对并补充

---

## ⚠️ 生成后自检清单（必须逐条核验）

生成 Draw.io XML 后，**必须逐条对照以下清单自检**，确认无误后方可交付用户：

### 泳道格式自检

| # | 检查项 | 正确标准 | 错误示例 |
|---|-------|---------|---------|
| 1 | 单层 lc 结构 + 标题栏 | **V10.0**：lc 单层容器（parent="1"）承载 `value="流程名称"` + lanes children。lc 使用 `swimlane;html=1;fontSize=15;startSize=40;horizontal=1`；lc **必须**带 `vertex="1"`。禁止独立 pool 节点、`shape=pool`、`fillColor=none`。👉 详见 [Pool 容器渲染规则](#-pool-容器渲染规则踩坑总结) | 存在独立 pool 节点；lc 无 value→标题栏空白；lc 无 vertex="1"→Pool消失；`shape=pool` → 透明 |
| 2 | swimlane `horizontal` | **横向泳道=0**（标签在左），**纵向泳道=1**（标签在上） | 横向泳道误用 horizontal=1 |
| 3 | swimlane `startSize` | 横向=30，纵向=30 | — |
| 4 | 泳道角色标签位置 | 横向：标签在泳道最左端；纵向：标签在泳道最顶端 | 标签跑到上方或右方 |

### 连线自检

| # | 检查项 | 正确标准 | 错误示例 |
|---|-------|---------|---------|
| 5 | 正向出口方向 | 同泳道→右；下方泳道→下；上方泳道→右 | 去下方的活动出口用了右侧 |
| 6 | **驳回三角位置** | 正向出口=下→三角在**右**(rotation=0▶)；正向出口=右→三角在**下**(rotation=90▼) |
| 6a | **驳回三角间距** | 三角距锚定活动节点 = **1/3 活动节点高度**（默认 60px→20px）；右侧三角 x=活动框右侧+20；下方三角 y=活动框下侧+20 | 三角紧贴活动框（间距=0）或间距过大（>50px） |
| 7 | **决策→三角连线出口** | 三角在右→决策出口`exitX=1;exitY=0.5`；三角在下→决策出口`exitX=0.5;exitY=1` | 三角在右但连线用了`exitX=0.5;exitY=1`(下方) |
| 8 | **决策双出口分侧** | 决策节点"是"和"否"两条出线**从菱形不同侧发出**，不可同侧 | 正向和驳回都从下方出线(exitY=1) |
| 8a | **跨泳道多出边 waypoints** | 决策节点有≥2条出边通往不同泳道时，必须加入 `<Array as="points">` 在公共折点汇聚再分叉 | 两条出边无 waypoints，线条在垂直通道中重叠 |
| 8b | **驳回三角连线入口** | **V10.2.20 非对称规则(rotation=0/90系统)**：出/入口三角 entryX=0;entryY=0.5（左侧入）；入口三角 exitX=1.0;exitY=0.5（底部尖出→目标顶部入）。禁止用抽象坐标推理替代Draw.io实际锚点行为 | 入口三角用exitX=0.5;exitY=1 → 连接点在可视左侧 |
| 9 | **驳回两点间无连线** | 驳回发起点(出口三角)↔目标点(入口三角)之间无任何连线；两端各自连接决策/目标活动 | rej_out_1→rej_in |
| 10 | 跨泳道串联错位 | B.x = A.x + A.width + 40（非对齐） | A 和 B 在同一 x 列 |
| 11 | **循环圆圈形状** | 循环回路用 `ellipse`（正圆），非 `triangle`；尺寸 30×30px | 用了三角形状或尺寸不是 30×30 |
| 12 | **循环圆圈颜色** | `fillColor=#B9E0A5;strokeColor=#82B366`（浅绿），非驳回橙黄 | 颜色与驳回三角相同 (#FFB366) |
| 13 | **循环圆圈两点间无连线** | 循环出口圈↔入口圈之间无任何连线；两端各自连接源节点/目标活动 | loop_out_1→loop_in |
| 14 | **循环圆圈编号** | 多个循环汇聚同一目标时，出口圈与入口圈共用同一编号 | 出口圈标"1"、入口圈标"2" |
| 15 | **循环连线无文字** | 循环回路连线 `value=""`，不标注文字 | value="回到监控循环" |

### 节点自检

| # | 检查项 | 正确标准 |
|---|-------|---------|
| 16 | 节点宽度 | 80px（活动、决策、开始/结束统一） |
| 17 | 节点间距 | 40px |
| 18 | **开始节点** | 必须存在，椭圆"开始"，位于流程最前端，x ≥ 120（startSize 80 + gap 40） |
| 19 | **结束节点** | 必须存在，椭圆"结束"，位于流程最末端 |
| 20 | **节点坐标泳道相对** | 所有节点的 x,y 坐标必须相对于其 parent swimlane，y 值不得超过泳道高度；禁止使用 Pool 级绝对坐标 |
| 21 | **泳道样式含 html=1** | 每条泳道的 style 必须包含 `whiteSpace=wrap;html=1;`，缺一不可 |

### 元数据自检

| # | 检查项 | 正确标准 | 错误示例 |
|---|-------|---------|---------|
| 23 | mxGraphModel version | 必须为 `version="10.0.0"` | `version="21.0.0"` |
| 24 | lc 标题栏 + lanes | lc 容器必须 `value="流程名称"` + `swimlane` + `fontSize=15` + `startSize=40` + `horizontal=1`；所有 lanes 为 lc 的子节点，width 统一 = lc 的 width（默认 1020）。禁止独立 pool 节点、lane width ≠ lc width | 存在独立 pool 节点→第二个 Pool；lc 无 value→标题栏空白；lane width 不统一→泳道错位 |

### 连线归属自检 ⚠️ 致命规则 — LCA 原则

| # | 检查项 | 正确标准 | 错误示例 |
|---|-------|---------|---------|
| 26 | Edge parent = LCA | 同泳道 edge → `parent="lane_X"`；跨泳道 edge → `parent="lc"`（V10.0：lane 挂在 lc 下，LCA=lc） | 统一 `parent="lc"`（同泳道 edge 会报错）；`parent="1"`（Root层全部报错） |

> ⚠️ **这是致命规则 — mxGraph LCA（最近公共祖先）原则**：
> mxGraph 要求 edge 的 `parent` 必须是**源节点与目标节点的最近公共祖先（Lowest Common Ancestor）**。
>
> - **同泳道**（源和目标在同一 swimlane）：LCA = 该 swimlane → `parent="lane_0"`、`parent="lane_1"` 等
> - **跨泳道**（源和目标在不同 swimlane）：LCA = **lc** → `parent="lc"`（V10.0：lc 为唯一顶层容器，lane 挂在 lc 下）
> - **绝对禁止** `parent="1"`（Root 层），因为 Root 不是泳道内节点的祖先
>
> **错误案例（V7.5）**：将所有 edge 统一设为 `parent="lc"` 会导致同泳道 edge 报错 "Could not add object for mxCell"。必须区分同泳道和跨泳道，按 LCA 分别设置。
>
> **修复方法**：逐条检查每条 edge 的 source 和 target 所在泳道：
> - source 和 target 在同一泳道 → `parent="lane_X"`
> - source 和 target 在不同泳道 → `parent="lc"`

### 连线重叠自检 ⚠️

基本原则：**避免连线重叠导致逻辑难以辨认。**

| # | 检查项 | 正确标准 | 错误示例 |
|---|-------|---------|---------|
| 27 | 多分支汇聚是否重叠 | 3+ 活动汇聚同一目标时，目标节点下移，各源节点出口从下侧出线 | 3 条水平线从左向右汇聚到结束节点（全部重叠） |
| 28 | 同向多连线共点 | 同节点 2+ 条同方向连线（全出口或全入口）共用同一连接点 | 3 个出口分别接菱形右侧上/中/下（连线杂乱） |
| 29 | **多分支并行活动纵向排列** | 决策节点之后多个并行活动在泳道内上下堆叠（间距=nodeH/4=15px），非水平并排 | 3 个活动 box 同 y 层水平排列致连线重叠 |
| 30 | **多线汇聚节点错位** | N ≥ 2 条线汇聚同一节点且入口重叠时，该节点 y 偏移 +75px（nodeH + nodeH/4） | 汇聚节点与相邻并行节点同 y 层，入线重叠 |
| 31 | **循环圆圈与锚定节点间距** | 出口圈 x = 活动框右侧 + 20px（=活动高度×1/3）；入口圈与活动框间距 = 20px | loop_out 圆圈与活动框重叠（同一 x 区域） |
| 32 | **终点下沉定位** | N≥3 条线汇聚"结束"时，结束节点置于最下方角色泳道内（该泳道增高至 270px），不新增空泳道；距该泳道内最底部活动节点错位 1.25H=75px | 结束节点嵌在泳道中间，或新增空泳道 |
| 33 | **入口侧统一（规则9）** | 所有汇聚到同一目标节点的连线，入口统一为左侧（entryX=0）+ 单一连接点（entryY=0.5）；不得从右侧进入，不得使用多个分散 entryY | 入口从右侧（entryX=1）或散布多个 entryY 值（0.2~0.8） |
| 34 | **并行共汇出口差异化（规则8）** | N≥2 个并行活动连同一目标节点时，出口改为右侧（exitX=1），而非下方；避免垂直通道拥堵 | 并行活动全部使用下方出口（exitY=1），导致连线在同一垂直通道中重叠 |
| 35 | **rej_out 无出边** | grep 所有 edge 的 source，无任何 source 以 `rej_out` 开头；`rej_out_*` 是驳回终点，不得连向任何节点 | `source="rej_out_dept"` 的 edge 存在 ❌ |
| 36 | **rej_in 恰一条出边** | 每个 `rej_in_*` 恰好一条出边（到目标活动），无入边来自 rej_out | `rej_in` 有 0 条出边 ❌；`rej_out → rej_in` 存在 ❌ |

> ⚠️ **规则 27 检查方法**：若发现 3 个及以上活动汇聚到同一个目标节点，检查：
> - 目标节点是否在源节点下方而非右侧
> - 各源节点的出口是否为下侧（`exitX=0.5;exitY=1`）
> - 三条汇聚线在垂直空间是否分离无重叠
>
> **规则 28 检查方法**：同一节点的多条同向连线是否使用相同的 `exitX/exitY`（出口）或 `entryX/entryY`（入口）。一进一出不可共点。
>
> **规则 29 检查方法**：决策/活动节点分支出的 N≥2 个并行活动，是否在泳道内纵向堆叠（不同 y 坐标）而非水平并排（同 y）。相邻活动上沿间距是否 = 15px。
>
> **规则 30 检查方法**：N≥2 条线从不同方向汇聚到同一节点时，若入口方向重叠，该节点 y 是否比同行相邻节点下移 75px。
>
> **规则 31 检查方法**：循环圆圈（出口圈/入口圈）的 x 坐标是否超出锚定活动框的范围至少 20px；出口圈 x ≥ 活动框.x + 活动框.w + 20；入口圈与上方活动框的 y 间距 ≥ 20。
>
> **规则 32 检查方法**：N≥3 条线汇聚"结束"时，(1) 结束是否在泳池最底部泳道，(2) 结束中心 y 距最底部活动节点中心 y 是否 = 75px，(3) 连线路径是否从活动节点下方空隙穿行而非从侧面拥挤。
>
> **规则 33 检查方法**：所有汇聚到同一目标节点的连线，(1) entryX 是否全部 = 0（左侧），(2) entryY 是否全部 = 0.5（单一连接点），(3) 是否存在 entryX=1（右侧）或散射 entryY 值（如 0.2/0.35/0.5/0.65/0.8）。
>
> **规则 34 检查方法**：N≥2 个并行活动连到同一目标节点时，(1) 各源节点的 exitX 是否 = 1（右侧），(2) 是否存在 exitY=1（下方）的情况，若有则修正为右侧出口。
>
> **规则 35 检查方法**：grep 所有 edge 的 `source="rej_out`，结果必须为空；若存在任何 `source` 以 `rej_out` 开头的 edge，说明生成逻辑错误地创建了从驳回出口三角出发的连线，必须删除。
>
> **规则 36 检查方法**：每个 `rej_in_*` 必须恰好有一条 `source="rej_in_*` 的 edge（出边到目标活动）；同时 grep `source="rej_out` 必须为空（无入边来自 rej_out）。
>
> **规则 37 检查方法（连线禁穿+防交叉）**：
> 1. 列出所有跨泳道连线，对每条连线的垂直段检查其 x 坐标是否落在中间泳道任何活动框的 x 范围内——若穿过，按优先级调整出口/入口方向或添加 waypoints。
> 2. 列出所有连线对，检查是否存在两条连线在非节点处相交——若交叉，调整节点位置或连线方向消除交叉。选择标准：不出现线条交叉为原则 → 折弯最少 → 总线长最短。
>
> **规则 38 检查方法（零 waypoint 策略 V10.2.1）**：grep 所有 edge 的 XML，确认无任何 `<Array as="points">` 出现。所有 edge 的 `<mxGeometry>` 内部仅有 `relative="1"` 属性，无子元素。
>
> **规则 39 检查方法（rounded=0 强制 V10.2.1）**：grep 所有 edge 的 style 属性，确认每条都包含 `rounded=0`。不得出现 `rounded=1` 或缺失 `rounded` 属性。
>
> **规则 40 检查方法（连线禁穿 V10.2.2）**：
> 1. 识别所有使用下方出口（`exitY=1`）的跨泳道连线
> 2. 对每条连线，检查其垂直段 x 是否落入中间泳道任何活动框的 x 范围 `[box_x, box_x+box_w]` 内，且 y 行程覆盖 `[box_y, box_y+box_h]`
> 3. 若有穿过 → 报错，出口必须改为右侧（`exitX=1`）

**若自检发现任何不合格项，必须修正后再交付，不得跳过。**

---

---

## ⚠️ 脚本生成规范与踩坑规避

> **本章节记录实战中反复踩坑后提炼的规范，生成任何脚本文件时必须遵守。**
> 
> **V10.5.3 更新**：Excel 流程说明的生成已采用 JSON + 固定 Runner 方案（见第六步 6.4），该方案**彻底消除**以下踩坑：
> - ~~坑1：中文文本中 ASCII 双引号~~（JSON 字符串中 `"` 已是合法语法，中文弯引号自由使用）
> - ~~坑2：Write 工具 JSON 转义~~（Write 工具写 JSON 文件无转义问题，JSON 格式原生支持 `\n`）
> - ~~坑4：Python 隐式字符串拼接~~（不再生成 Python 代码）
> 
> 以下规范仅适用于**仍需手写 Python 脚本的场景**（如图形生成脚本 gen_xxx.py、VSDX 转换脚本等）。


### 坑1：中文文本中 ASCII 双引号导致 Python SyntaxError


**现象**：Python 脚本中含 `进入"分析客户需求"`，运行报 `SyntaxError`。


**根因**：Python 字符串用 `"` 定义时，内容中的 `"` 会提前结束字符串。中文语境引用术语应使用弯引号。


**解决方案**：

- 在 Python 代码的中文文本中引用活动/流程/角色名称时，**必须使用中文弯引号** “...”（“”），**严禁使用 ASCII 双引号**

- 修复方法：用 python `u“术语”` 拼接或直接写中文弯引号 “术语”


---


### 坑2：Write 工具 content 参数中反斜杠n被 JSON 解释为换行符


**现象**：用 `Write` 工具写入含反斜杠n（意为字面量反斜杠+n）的 Python 脚本时，文件中的反斜杠n变成了真实换行符，导致 Python 语法错误。


**根因**：`Write` 工具的 `content` 参数值是一个 JSON 字符串，JSON 解析器会把反斜杠n解释为换行符（ASCII 10），而非字面量。同理反斜杠t、反斜杠引号、反斜杠反斜杠等 JSON 转义序列都会被解释。


**解决方案**：

- **生成含反斜杠转义符的源文件时，禁止使用 `Write` 工具**

- 改用以下方式写入文件：

  - **Bash heredoc**：`cat > file.py << 'EOF'`，单引号包裹分隔符，内容不解释任何转义

  - **PowerShell Here-String**：`$c = @'...'@; Set-Content file.py UTF8`，单引号不解释转义

- 写入后立即用 `python -m py_compile file.py` 或 `ast.parse()` 验证语法


---


### 坑3（潜在风险）：Write 工具写入含大量 Unicode 的文件可能乱码


**现象**：Write 工具对 Unicode 字符的编码处理可能不一致，导致写入的文件出现乱码。


**解决方案**：

- 对于含大量中文/Unicode 内容的源文件或数据文件，优先用 Bash 或 PowerShell（Set-Content -Encoding UTF8）写入

- Write 工具仅用于写入纯 ASCII 或少量中文的配置文件


---


### 坑4（潜在风险）：Python 多行字符串拼接遗漏加号导致隐式拼接


**现象**：Python 中相邻字符串字面量会自动拼接（'a' 'b' -> 'ab'），若多行字符串连接时遗漏加号，不会报错但内容会意外合并。


**解决方案**：

- 多行字符串拼接时，每行末尾显式写加号或用括号包裹整体

- 生成脚本后用 `ast.parse()` 检查


---


### 规范：文本中禁止出现的字符


**规则**：所有生成的文本（流程说明、活动描述、角色说明等）中，**禁止出现以下字符**：


| 字符 | 说明 | 替代方案 |

|------|------|----------|

| `~` | 波浪号，可能引起格式问题 | 删除或改用破折号 |

| `#` | 井号，可能被误认为标题标记 | 删除或改用中文序号（一、二、三） |

| `/` | 斜杠，可能被视为路径分隔符 | 改用中文或、及 |

| `*` | 星号，可能被解释为强调标记 | 删除或改用中文引号 |

| `&` | 和号，可能被解释为 HTML 实体 | 改用中文和、及 |


**原因**：这些字符在 Markdown、HTML、XML 或 Draw.io 标签中可能有特殊含义，导致渲染异常或解析错误。


---


### 规范：生成脚本后的验证流程


**规则**：每次生成脚本文件后，必须执行以下验证步骤：


1. **语法检查**：运行 `python -m py_compile script_file` 确认无语法错误

2. **运行测试**：运行脚本，确认能正常生成输出文件（.drawio 或 .xlsx）

3. **输出验证**：检查输出文件（.drawio 或 .xlsx）是否能正常打开，内容是否正确


**禁止**：未经验证直接交付脚本或输出文件。


---



## 参考资源

- `references/drawio_template.md`：Draw.io XML 结构说明与完整模板示例
- `scripts/generate_drawio.py`：根据结构化流程数据批量生成 Draw.io XML 的 Python 脚本
- `scripts/run_flow_desc.py`：**Excel 流程说明 Runner**（V10.5.3）—— 读取 JSON 数据 → 调用 flow_desc_xlsx.py 生成 Excel，内置禁止字符扫描
- `D:/A11.12北方院/7.流程治理机制/流程说明文件模版(参考).docx`：流程说明文件模板（第六步使用）

---

## 注意事项

- 优先从文档的**标题、章节结构、表格、编号列表**中提取流程信息。
- Excel 文件若存在多个 Sheet，逐一分析每个 Sheet，询问用户是否全部生成或只处理指定 Sheet。
- 生成的 XML 应可**直接导入 Draw.io 桌面版或在线版**（https://app.diagrams.net），无需二次修改。
- 若文档内容过长，可分段读取，但必须完整覆盖全部流程内容再生成图表。
