---
name: JeecgBoot BPM
slug: jeecgboot-bpm
category: Automation
description: JeecgBoot BPM generates Flowable BPMN 2.0 workflows from natural language and can create or edit processes in JeecgBoot. Use it for approval flows, OA forms with workflows, and changes to existing process nodes or listeners.
github: "https://github.com/jeecgboot/skills/tree/main/jeecg-bpmn"
language: Python
stars: 225
forks: 67
install: "npx degit https://github.com/jeecgboot/skills/tree/main/jeecg-bpmn ~/.claude/skills/jeecg-bpmn"
installs_to: ~/.claude/skills/jeecg-bpmn
source_path: jeecg-bpmn/SKILL.md
collection_size: 14
category_size: 1754
collection_url: "https://dirskills.com/collections/jeecgboot/skills"
added: 2026-09-03T06:05:04.532Z
last_synced: 2026-09-03T06:05:04.532Z
canonical_url: "https://dirskills.com/skills/jeecgboot-bpm"
---

# JeecgBoot BPM

JeecgBoot BPM generates Flowable BPMN 2.0 workflows from natural language and can create or edit processes in JeecgBoot. Use it for approval flows, OA forms with workflows, and changes to existing process nodes or listeners.

**Install:**

```bash
npx degit https://github.com/jeecgboot/skills/tree/main/jeecg-bpmn ~/.claude/skills/jeecg-bpmn
```

## README

# JeecgBoot BPM 流程自动生成器

将自然语言的流程描述转换为 Flowable BPMN 2.0 XML，并通过 API 在 JeecgBoot 系统中自动创建流程。

## 临时配置文件规则（强制）

所有传给脚本的 `--config <xxx.json>` 必须写到 **`{系统临时目录}/{SKILL_NAME}/`** 下，由操作系统自动清理；skill 与脚本均不主动删除该目录或文件。

```python
import tempfile, os, json

SKILL_NAME = "<SKILL_NAME>"               # 请替换为实际的技能名称
skill_dir = os.path.join(tempfile.gettempdir(), SKILL_NAME)
os.makedirs(skill_dir, exist_ok=True)          # 确保目录存在，不主动检查

config_path = os.path.join(skill_dir, 'sk_audit_create.json')   # 示例文件名
with open(config_path, 'w', encoding='utf-8') as f:
    json.dump(cfg, f, ensure_ascii=False, indent=2)
```

`tempfile.gettempdir()` 自动适配：Windows `%TEMP%`、Linux `/tmp`、macOS `/var/folders/.../T`（注意 macOS 并非 `/tmp`）。  
文件名建议使用 **`<表名>_<步骤>.json`**（如 `sk_audit_create.json`），无需重复技能前缀，因路径已包含技能名称，便于排错。

** 禁止：**

- 写到 `<skill>/tmp/` 或当前工作目录（污染 skill / 用户项目）
- 硬编码 `/tmp`、`C:\Temp` 或任何固定路径（不跨平台）
- 每步完成后主动 `rm` / `Remove-Item`（操作系统会清理，属多余 tool call）
- 主动 `os.path.exists()` 检查（其本身即为一次 tool call）  
  （使用 `os.makedirs(…, exist_ok=True)` 满足需求，不算主动检查）

**临时文件可能被操作系统异步清理**，但仍遵循 **乐观调用 + 报错补救**：仅当脚本返回 `FileNotFoundError` 或 `配置文件不存在` 时，使用相同内容、**在相同的 `{系统临时目录}/{SKILL名称}/` 路径下重写**（重写前仍需 `os.makedirs(skill_dir, exist_ok=True)` 确保目录存在），切勿更换路径或回退至 skill 目录。

## 介绍组件时的完整性要求

> **重要：** 当用户要求介绍流程设计器各组件时，必须包含以下内容，不可遗漏：
>
> 1. **会签节点**：串行/并行两种模式；全部通过/一人通过/半数通过/按比例/自定义 5种通过规则；指定人员/角色/审批角色/部门/岗位/职级/表单字段/流程变量 8种审批人类型
> 2. **条件表达式**：系统内置流程变量（`result`、`applyUserId`、`applyDate` 等）；13种条件运算符；多条件组合用法（AND/OR）
> 3. **监听器**：执行监听器/任务监听器/全局事件监听器三种类型；系统预置监听器（ProcessEndListener必需、TaskSkipApprovalListener、TaskCreatedAutoSubmitListener等）；taskExtendJson 节点行为控制字段说明

## 性能规范与已验证规律

> **⚠️ 禁止预防性读取参考文档。** 执行任务前不要为了"以防万用"而读取 references/ 下的文档。只在遇到具体问题时按需读取，且使用 offset/limit 指定行范围。
>
> **⚠️ 对外部 API 响应结构，先用小脚本探测，再写主逻辑。** 但下方速查表中**已验证的数据不需要重新探测**。
>
> **⚠️ 用不熟悉的 Python 模块前，必须先 `dir()` 查 exports。** 但下方速查表中**已验证的模块不需要重新 dir()**。
>
> **⚠️ 禁止对 API 响应的 `result` 直接做 `[:]` 切片。** JeecgBoot API 的 `result` 格式不统一：分页接口返回 dict `{"records": [...], "total": N}`，全量接口返回 list `[...]`，写操作返回 string。对 dict 做切片 → `KeyError: slice(None, 5, None)`。**强制规则：取值前必须根据「API 响应速查」表确定 result 类型，分页接口统一用 `.get('result', {}).get('records', [])`，全量接口用 `isinstance(result, list)` 判后再切片。**

### 模块导入（固定模式，直接复用）

```python
import os, pathlib, sys
_SKILLS_DIR = pathlib.Path.home() / '.claude' / 'skills'
sys.path.insert(0, str(_SKILLS_DIR / 'jeecg-desform' / 'scripts'))  # desform_creator, desform_utils
sys.path.insert(0, str(_SKILLS_DIR / 'jeecg-bpmn'    / 'scripts'))  # bpmn_creator, bpmn_oa
sys.path.insert(0, str(_SKILLS_DIR / 'jeecg-system'  / 'scripts'))  # system_utils
os.chdir(str(_SKILLS_DIR / 'jeecg-bpmn' / 'scripts'))
import desform_utils as du; du.init_api(API_BASE, TOKEN)  # ⚠ 必须初始化，否则 ValueError: unknown url type
import desform_creator as dc  # 无需初始化
import bpmn_creator as bc     # 无需初始化，各函数直接传 api_base/token
# system_utils 需要: from system_utils import init_api, ...; init_api(API_BASE, TOKEN)
```

### 函数返回值速查

| 函数 | 返回类型 | 正确取值 |
|------|---------|---------|
| `dc.create_form(...)` | tuple `(form_id, title_field_model)` | `result[0]` |
| `dc.get_form_id(code)` | tuple `(form_id, index)` | `result[0]` |
| `bc.get_desform_fields(api_base, token, code)` | dict `{label: {model, key, type}}` | `fields.get('薪资', {}).get('model')` |
| `bc.authorize_form(...)` | dict（**不是** tuple） | `r = bc.authorize_form(...)` |
| `du.get_form_fields(code)` | list `[{name, model, type}]` | 返回表单字段列表。**注意：不存在 `du.get_form_detail()`** |

### API 响应速查

| API / 操作 | 返回值 | 正确取值 |
|-----------|--------|---------|
| `saveProcess` | dict | `result['obj']` 含新ID（编辑时可能 null，按 processKey 查）。路径：`/act/designer/api/saveProcess`，Content-Type：`application/x-www-form-urlencoded` |
| `extActProcess/queryById` | dict | `result` 含流程全字段；`result['processXml']` 为 **base64 编码**的 XML，需 `base64.b64decode().decode('utf-8')` |
| `sys/sysDepart/add` | `result=null` | 新建后用 `queryDepartAndPostTreeSync` 全量查找 |
| `approvalRole/rootList` | `result.records[]`（**不是**裸数组） | `r['result']['records']`，每条 `{id, name, type, pid}` |
| `approvalRole/childList?pid=xxx` | `result.records[]`（**不是**裸数组） | `r['result']['records']` |
| `approvalRole/group/add` | `result="添加成功！"`（字符串，不是 ID） | 创建后调 `rootList` 按 name 查 ID |
| `approvalRole/role/add` | `result="添加成功！"`（字符串，不是 ID） | 创建后调 `childList` 按 name 查 ID |
| `sys/position/list` | `result.records[]` | 每条 `{id, name, code}`，用于 deptPosition 审批人 |
| `query_approval_roles()` | `{'roles': [...], 'persons': [...]}` | 用 `find_approval_role(keyword)` |
| `query_dept_positions()` | depart 树节点（`departName` 不是 `name`） | 过滤 `orgCategory=='3'` |

### 关键函数签名

| 函数 | 签名 |
|------|------|
| `du.create_form` | `(name, code, widgets, title_index=0, layout='auto', ...)` |
| `bc.edit_node_config` | `(api_base, token, process_id, node_code, node_settings)` |
| `bc.set_node_field_permissions` | `(api_base, token, process_id, node_code, form_code, field_permissions, form_type='2')` |

### 其他关键规律

- `dc.DIVIDER/USER/MONEY` 等常量**是函数不是字符串**，创建 widget 用 `dc.build_widget({'type':'money', 'name':'金额', 'required': True})`
- `build_widget` 对**所有控件类型都强制要求 `name` 字段**（含 divider：`{'type': 'divider', 'name': '---', 'text': '标题'}`）
- `build_widget` 合法 `type` 清单：基础 `input textarea number integer money date time switch slider rate color` / 选择 `radio select checkbox` / 系统 `select-user select-depart select-depart-post phone email area-linkage org-role` / 文件 `file-upload imgupload hand-sign` / 高级 `auto-number formula barcode location table-dict select-tree link-record link-field capital-money text-compose ocr map summary editor markdown` / OA `oa-approval-comments` / 布局 `tabs grid card divider text buttons`
- DesForm 字段在 `design["list"]` 下（不是 `design["fields"]`），嵌套结构需递归提取：
  ```python
  def find_fields(node, results):
      if isinstance(node, dict):
          if node.get('type') not in ('grid','text','') and node.get('model'):
              results.append(node)
          for v in node.values(): find_fields(v, results)
      elif isinstance(node, list):
          for item in node: find_fields(item, results)
  fields = []; find_fields(design, fields)
  ```
- `userTask` 含会签时 XML 子元素顺序：`extensionElements` → `incoming`/`outgoing` → `multiInstanceLoopCharacteristics`（顺序错报 `cvc-complex-type.2.4.a`）
- 条件表达式必须调 `bc.build_condition_b64()`，手写格式：外层**数组** `[{"logic":"and","conditions":[...]}]`，`flowUtil.evaluateExpression` 需**三参数** `(execution, 'b64', 'and')`
- 手工分支 + 网关组合 → 自动使用水平多行布局（`_detect_horizontal_multirow`），`W_GAP=60, MAIN_CY=330, LOWER_CY=540`
- **包含网关（inclusiveGateway）带 default flow 时**：default flow 从 split 直达 join（无中间节点），`_detect_parallel_blocks` 已支持空链检测，`calc_layout` 只对非空分支做水平展开（已修复，此前空链导致检测失败、分支垂直堆叠重叠）
- 子流程必须先于表单创建，否则表单关联冲突（修复：DELETE 子流程 formId 再重新 link_form）
- `bpmn_oa.py` 支持 `subprocess` 键一键创建子流程，自动填充 `calledElement`
- 手写子流程必须加 `"isSubProcess": True`（`bpmn_oa.py` 的 `_setup_oa_subprocess` 已自动设置）
- **system_utils 函数**：查岗位 `query_dept_positions(dept_id=None)` / 查角色 `find_approval_role(keyword)` 返回 dict 或 None / 岗位列表 `GET /sys/position/list` / **不存在** `/sys/position/rank/list` `/sys/duty/list`
- **不存在的 API（禁止尝试）**：`queryDepartTreeSync?pid=xxx` `queryIdTree` `queryTreeList` `queryMyDept` `loadNodeGroupData?groupType=deptPosition` `queryByKeywords` `sysDepart/list` `recycleBin/*`
- **审批角色查找或创建模式**（防重复 + 获取真实 ID）：
  ```python
  def find_or_create_approval_role(name, grp_id):
      def query_id():
          r = api_get(f'/sys/approvalRole/childList?pid={grp_id}')
          return next((c['id'] for c in r.get('result',{}).get('records',[]) if c['name']==name), None)
      rid = query_id()
      if not rid:
          api_post('/sys/approvalRole/role/add', {'name': name, 'pid': grp_id})
          rid = query_id()
      return rid
  ```
- **`bc.edit_node_config` 不更新 `nodeConfigJson`（已踩坑）**：该函数只做 `node.update(settings)` 后 PUT，**不同步 `nodeConfigJson` 字段**。前端读 `nodeConfigJson.formEditStatus` 时仍为 false，导致可编辑节点实际不可编辑。**凡需设置 `formEditStatus=1` 的节点，必须手动同步更新 `nodeConfigJson`**，正确写法：
  ```python
  def fix_node_form_edit(api_base, token, process_id, node_code, url):
      """设 formEditStatus=1 并同步 nodeConfigJson（edit_node_config 不做这步）"""
      r = bc.api_request(api_base, token,
          f'/act/process/extActProcessNode/list?processId={process_id}&pageNo=1&pageSize=50',
          method='GET')
      for node in (r.get('result') or {}).get('records', []):
          if node.get('processNodeCode') == node_code:
              node['formEditStatus'] = '1'
              node['modelAndView'] = url
              node['modelAndViewMobile'] = url
              try:
                  cfg = json.loads(node.get('nodeConfigJson') or '{}')
              except Exception:
                  cfg = {}
              cfg['formEditStatus'] = True          # ← 关键：必须同步
              node['nodeConfigJson'] = json.dumps(cfg, ensure_ascii=False)
              return bc.api_request(api_base, token,
                  '/act/process/extActProcessNode/edit', data=node, method='PUT')
  ```
  > `set_draft_nodes_editable` 已内置此逻辑；只有直接调 `edit_node_config` 设 formEditStatus 时需要用上述替代函数。
- **子流程节点禁止使用 `draft=True`（已踩坑）**：在被 `callActivity` 调用的子流程中，任何节点都不能设 `draft=True`。原因：`draft=True` 会为节点添加 `TaskCreatedAutoSubmitListener`，callActivity 启动子流程时该监听器立即自动提交任务，此时子流程 execution 仍处于中间态，写入 `ACT_RU_VARIABLE` 时 `EXECUTION_ID_` 无效，触发 FK 约束失败（`ACT_FK_VAR_EXE`）。子流程中需要表单可编辑的节点，改用 `fix_node_form_edit` 显式设置 `formEditStatus=1` 即可。

### 规则3：URL 中含中文参数必须用 urllib.parse.quote 编码（⚠️ 强制）

```python
# ✅ 正确
import urllib.parse
keyword = urllib.parse.quote('安全评审')
url = f'{API_BASE}/sys/approvalRole/search?keyword={keyword}'
# 或用 urlencode：params = urllib.parse.urlencode({'keyword': '安全评审'})
```

### 规则4：独立的系统数据查询必须合并到单个脚本一次执行（⚠️ 强制）

不要分多轮 Bash 调用执行独立查询，合并到一个脚本里一次运行。

### 规则5：部门/岗位查询只能用 queryDepartAndPostTreeSync（⚠️ 强制）

```python
req = urllib.request.Request(f'{API_BASE}/sys/sysDepart/queryDepartAndPostTreeSync', headers=HEADERS)
result = json.loads(urllib.request.urlopen(req).read().decode())['result'] or []
def flatten(nodes, acc=None):
    if acc is None: acc = []
    for n in (nodes or []):
        if isinstance(n, dict):
            acc.append(n)
            flatten(n.get('children', []), acc)
    return acc
all_nodes  = flatten(result)
depts      = [n for n in all_nodes if str(n.get('orgCategory','')) == '2']
positions  = [n for n in all_nodes if str(n.get('orgCategory','')) == '3']
```

### 规则6：DesForm 表单编码被回收站占用时直接换编码（⚠️ 强制）

`desform/add` 返回 `"该code已存在"` 但 `desform/list` 查不到 → 回收站占用。直接加后缀 `_v2`，禁止尝试 recycleBin API（均 404）。

### 规则7：含 `${...}` 的 Python 脚本禁止用 `python -c "..."` 执行（⚠️ 强制）

**现象：** `bash: bad substitution`，Python 根本没启动。

**根因：** bash 双引号内的 `${...}` 会被当作 shell 变量展开。Python f-string 中的 `f'${{{model}}}'`（生成 DesForm URL 占位符如 `${BPM_DES_DATA_ID}`）触发 bash 的非法变量名错误。

**强制规则：凡是脚本含 `${` 的，必须写入 `.py` 文件再执行，不得用 `-c "..."`。**

```bash
# ❌ 错误 —— bash 会展开 ${...}，报 bad substitution
python -X utf8 -c "
...
f'${{{model}}}提交的申请'
"

# ✅ 正确 —— 写文件，bash 不解析文件内容
# Write tool 写入 C:\Users\25067\tmp_script.py，然后：
powershell -Command "& python -X utf8 C:\Users\25067\tmp_script.py"
powershell -Command "Remove-Item 'C:\Users\25067\tmp_script.py'"
```

### 规则8：Scenario A 子流程创建后必须立即删除其表单绑定（⚠️ 强制）

**现象：** 主流程 `link_form` 报 `"编码重复或表名已被授权流程！"`。

**根因：** `bpmn_oa._setup_oa_subprocess` 内部会将主流程的 `form_code` 关联到子流程（`extActProcessForm`），后端对 `relationCode` 有唯一约束，导致主流程随后绑定同一表单失败。

**强制规则：** 调用 `_setup_oa_subprocess` 后，主流程 `link_form` 前，必须先删除子流程的表单绑定：

```python
# _setup_oa_subprocess 执行完之后立即执行：
import urllib.parse as _up
q = _up.urlencode({'processId': sub_pid, 'pageNo': 1, 'pageSize': 10})
sub_forms = bc.api_request(API_BASE, TOKEN,
    f'/act/process/extActProcessForm/list?{q}', method='GET')
for rec in (sub_forms.get('result') or {}).get('records', []):
    bc.api_request(API_BASE, TOKEN,
        f'/act/process/extActProcessForm/delete?id={rec["id"]}', method='DELETE')
# 之后再 link_form 到主流程
```

> 此步骤不影响子流程运行——Scenario A 子流程通过 `JG_SUB_MAIN_PROCESS_ID` 共享主流程数据，无需自己独立绑定表单。

### 规则9：API 响应遍历前必须做类型检查（⚠️ 强制）

**现象：** `'str' object has no attribute 'get'` 或 `KeyError: 0`，程序崩溃。

**根因：** 部分 API 的 `result` 字段结构不固定，可能是 `dict`（含 `records` 键）、裸 `list`、或 `str`。直接用 `[0]` 或 `.get()` 导致类型错误。

**强制规则：凡是遍历 API 响应 `result` 的，必须先 `isinstance` 检查和 `print(type(result))` 确认结构。**

```python
# ❌ 错误 —— 假设 result 一定是 list
for item in r['result']:  # 实际是 dict，抛出 KeyError
    print(item['name'])

# ✅ 正确 —— 先查验结构再遍历
result = r.get('result', [])
if isinstance(result, dict):
    records = result.get('records', [])
elif isinstance(result, list):
    records = result
else:
    records = []
for item in records:
    print(item.get('name'))
```

### 规则10：复杂流程（含网关/会签/多种审批人）先 dry-run 验证 XML（⚠️ 推荐）

**现象：** 直接调用 API 创建复杂流程后，发现 taskExtendJson 配置不正确或布局错乱，需要删除重建。

**推荐流程：**
```bash
# 第一步：dry-run 只生成 XML，不调 API
python "<skill目录>/scripts/bpmn_creator.py" \
    --api-base <URL> --token <TOKEN> --config <config.json> --dry-run

# 第二步：人工或脚本检查 XML 中的关键元素
#   - taskExtendJson 的 sameMode/skipOne 值是否正确
#   - assignee/candidateUsers/candidateGroups 属性是否存在
#   - countersign/multiInstance/timer 等是否正确生成
#   - 条件表达式的 field 字段 model 是否匹配 DesForm 实际字段

# 第三步：确认无误后再正式创建（去掉 --dry-run）
python "<skill目录>/scripts/bpmn_creator.py" \
    --api-base <URL> --token <TOKEN> --config <config.json> --link-form
```

### 规则11：编辑已有流程 XML 时，正则必须兼容 `bpmn2:` 命名空间前缀（⚠️ 强制）

**现象：** 用 `<userTask` 匹配开标签，实际 XML 是 `<bpmn2:userTask`，正则命中失败，脚本报"未找到节点"。

**根因：** JeecgBoot 生成的 BPMN XML 固定使用 `bpmn2:` 命名空间前缀，所有 BPMN 元素名均带前缀（`bpmn2:userTask`、`bpmn2:endEvent`、`bpmn2:sequenceFlow` 等）。

**强制规则：凡是用正则操作已有 BPMN XML，所有元素名必须加 `(?:bpmn2:)?` 前缀。**

```python
# ❌ 错误 —— 匹配不到 <bpmn2:userTask ...>
pattern = rf'(<userTask[^>]*\bid="{node_code}"[^>]*>)'

# ✅ 正确 —— 兼容有/无 bpmn2: 前缀两种写法
pattern = rf'(<(?:bpmn2:)?userTask[^>]*\bid="{re.escape(node_code)}"[^>]*>)'
```

### 规则12：`candidateUsersExpression` 类型在 XML 中不加任何 `groupType`（⚠️ 强制）

**现象：** 写了 `groupType="candidateUsersExpression"`（无 `flowable:` 前缀）导致发布失败：
```
cvc-complex-type.3.2.2: 元素 'bpmn2:userTask' 中不允许出现属性 'groupType'
```

**根因：** BPMN 标准不允许无命名空间的自定义属性。只有带 `flowable:` 前缀的属性（`flowable:groupType`）才合法。而 `candidateUsersExpression` 类型（`bpmn_creator.py` 368行）**本身就不生成任何 groupType 属性**。

**各审批人类型 groupType 规则（来自 `bpmn_creator.py`）：**

| 类型 | XML 属性 | groupType |
|------|---------|-----------|
| `expression` / `assignee` / `candidateUsers` | `flowable:assignee` / `flowable:candidateUsers` | **无** |
| `candidateUsersExpression` | `flowable:candidateUsers="${表达式}"` | **无** |
| `role` | `flowable:candidateGroups` | `flowable:groupType="role"` |
| `approvalRole` | `flowable:candidateUsers="${flowUtil...}"` | `flowable:groupType="approvalRole"` |
| `dept` | `flowable:candidateGroups` | `flowable:groupType="dept"` |
| `deptPosition` | `flowable:candidateGroups` | `flowable:groupType="deptPosition"` |
| `position` | `flowable:candidateUsers="${oaFlowExpression...}"` | `flowable:groupType="position"` |

**强制规则：手写 XML 审批人属性前，必须先查 `bpmn_creator.py` 中对应 type 的生成代码（约 363-395 行），禁止凭记忆猜测。**

### 规则13：手动调用 `saveProcess` 必须用 form-urlencoded + 正确路径（⚠️ 强制）

**现象：** 调用 `/act/process/extActProcess/saveProcess` 报"路径不存在"。

**根因：** `saveProcess` 的正确路径是 `/act/designer/api/saveProcess`，且必须用 `application/x-www-form-urlencoded` 编码，不能用 JSON。

**强制规则：手动调用 saveProcess 必须严格按以下模板，禁止猜测路径或 Content-Type。**

```python
import urllib.parse

save_data = {
    'processDefinitionId': process_id,          # 已有流程ID（新建传 '0'）
    'processName':  process_detail['processName'],
    'processkey':   process_detail['processKey'],  # ⚠ 字段名是 processkey（全小写）
    'typeid':       process_detail.get('processType', 'oa'),  # ⚠ 字段名是 typeid（全小写）
    'lowAppId': '',
    'params': '',
    'nodes': nodes_str,                          # 见下方 nodes_str 构建方式
    'processDescriptor': xml,                    # 原始 XML 字符串（非 base64）
    'realProcDefId': '',
    'startType': process_detail.get('startType', 'manual'),
}

# nodes_str 构建：只含 userTask 节点，格式 id=xxx###nodeName=xxx@@@
nodes_str = ''.join(
    f'id={n["processNodeCode"]}###nodeName={n["processNodeName"]}@@@'
    for n in node_records  # 来自 extActProcessNode/list
)

form_body = urllib.parse.urlencode(save_data).encode('utf-8')
req = urllib.request.Request(
    f'{API_BASE}/act/designer/api/saveProcess',
    data=form_body,
    headers={**HEADERS, 'Content-Type': 'application/x-www-form-urlencoded'},
    method='POST'
)
r = json.loads(urllib.request.urlopen(req).read().decode('utf-8'))
```

> 查询流程 XML：`GET /act/process/extActProcess/queryById?id={id}`，返回的 `processXml` 是 **base64 编码**，用 `base64.b64decode(xml_b64).decode('utf-8')` 解码；`processDescriptor` 传**原始 XML 字符串**。

### 规则14：编辑已有流程前必须先 Grep 确认，不得猜测（⚠️ 强制）

**现象：** 手写脚本时错误路径、错误属性名导致多轮失败，整体执行慢。

**根因：** 执行前未查阅 `bpmn_creator.py` 源码就直接猜测 API 路径和 XML 属性，导致每次报错后才发现问题，反复修改脚本。

**强制规则：编辑已有流程 XML 或调用 saveProcess 前，必须先用 Grep 查阅 `bpmn_creator.py` 的相关代码，确认后再写脚本。探查信息必须合并到单个脚本一次获取，严禁分多个探查脚本串行执行。**

```python
# 编辑流程前的必查清单（用 Grep，不要靠记忆）：
# 1. saveProcess 路径：grep "saveProcess" bpmn_creator.py → /act/designer/api/saveProcess
# 2. 目标审批人类型的 XML 生成：grep "candidateUsersExpression" bpmn_creator.py → 无 groupType
# 3. 已有 XML 的节点命名空间：先探查 XML 打印节点开标签，确认是否有 bpmn2: 前缀
```

### 规则16：条件 fieldType 必须与 DesForm 字段实际类型完全一致（⚠️ 严重）

**现象：** 条件规则配置面板中，字段和运算符正常显示，但值输入框为空（"请选择部门"占位符），"原始JSON数据"中条件值字段完全缺失。条件保存后无法生效。

**根因（双重）：**
1. 条件值字段名必须用 **`expectedValue`**，前端读的是 `expectedValue` 而非 `value`——传 `value` 时前端完全忽略，显示为空
2. `fieldType` 必须与 DesForm 字段实际类型完全一致，否则前端规范化时丢弃 `expectedValue`

**强制规则：conditionExpression 中每个 condition 必须同时满足：**
- 字段名用 `expectedValue`（**不是** `value`）
- `fieldType` 与 DesForm 字段 `type` 完全一致

| DesForm 字段 `type` | condition 中的 `fieldType` |
|---------------------|--------------------------|
| `select-depart` | `"select-depart"` |
| `select-user` | `"select-user"` |
| `checkbox` | `"checkbox"` |
| `select` / `radio` | `"select"` / `"radio"` |
| `money` | `"money"` |
| `integer` | `"integer"` |
| `input` / `textarea` | `"input"` |
| `date` | `"date"` |

```python
# ✅ 正确 —— expectedValue + 正确 fieldType
cond = {"field": "select_depart_xxx", "fieldType": "select-depart", "fieldName": "所在部门",
        "operator": "eq", "expectedValue": "dept_id_here"}

cond = {"field": "money_xxx", "fieldType": "money", "fieldName": "报销金额",
        "operator": "lt", "expectedValue": "1000"}

# ❌ 错误 —— 用 "value" 代替 "expectedValue"，前端完全忽略，显示空白
cond = {"field": "money_xxx", "fieldType": "money", "fieldName": "报销金额",
        "operator": "lt", "value": "1000"}

# ❌ 错误 —— fieldType 与实际类型不符，前端规范化时丢弃 expectedValue
cond = {"field": "select_depart_xxx", "fieldType": "input", ...}
```

> `fieldType` 直接用 `bc.get_desform_fields()` 返回的 `info['type']`，**禁止手动映射或猜测**。

---

### 规则17：`position` 类型审批人表达式必须传三个参数（⚠️ 严重）

**现象：** 前端"职务级别"选择框为空（"请选择职务级别"占位符），"开启职级表达式"勾选但无值，职级审批人实际为空，流程无法正常流转。

**根因：** `oaFlowExpression.getApplyUserDeptPositionLevel` 需要三个参数：`(sys_org_code, applyUserId, positionId)`，传两个参数时方法签名不匹配，前端解析表达式提取 positionId 失败，显示为空。

**强制规则：**

```xml
<!-- ✅ 正确 —— 三个参数 -->
flowable:candidateUsers="${oaFlowExpression.getApplyUserDeptPositionLevel(sys_org_code, applyUserId, 'positionId')}"
flowable:groupType="position"

<!-- ❌ 错误 —— 用 execution 代替前两个参数，只有两个参数 -->
flowable:candidateUsers="${oaFlowExpression.getApplyUserDeptPositionLevel(execution, 'positionId')}"
```

对应 JSON 配置：
```json
{"type": "position", "value": "1958471111989067778"}
```
`bpmn_creator.py` 第389行已正确生成三参数形式，**手写 XML 时必须严格按此格式，禁止简化参数列表**。

---

### 规则18：探查 XML 行内容必须用 `repr()` 打印原始字符串（⚠️ 强制）

**现象：** 打印 XML 行时只用 `print(line)`，看不出行尾是否还有其他标签（如 `</bpmn2:extensionElements>`），写删除逻辑时整行删掉导致结构损坏，deploy 失败。

**根因：** `print(line)` 不显示行边界，无法判断一行内是否同时包含多个标签。

**强制规则：探查 XML 行内容时，必须用 `repr(line)` 打印，以便看清行尾是否还有其他内容。**

```python
# ❌ 错误 —— 看不出行尾有 </bpmn2:extensionElements>
for i, line in enumerate(xml.splitlines()):
    if 'TestExecutionListener' in line:
        print(f'L{i+1}: {line}')

# ✅ 正确 —— repr 显示完整内容，含行尾隐藏标签
for i, line in enumerate(xml.splitlines()):
    if 'TestExecutionListener' in line:
        print(f'L{i+1}: {repr(line)}')
# 输出: L20: '      <flowable:executionListener .../></bpmn2:extensi
