Appearance
规范 02 · 流程定义配置项完整参考(属性字典)
流程定义 = LogicFlow JSON。同一份 JSON 在所有语言实现(Java/Go/Python/Node/PHP/Rust/MoonBit/C#)中 必须产生一致的执行结果。
本文档是流程定义所有配置项的权威参考(事实来源:Java 参考实现的
ModelParser/NodeParser系列解析器,八语言行为对齐):
- "引擎"列:✅ = 引擎已实现(解析并影响行为);「前端」= 仅设计器/发起页使用,引擎忽略; 「预留」= 键保留但当前无行为实现
- 未列出的
properties键 = 引擎不识别(不参与执行),在任务节点上原样存入节点扩展属性ext(任务变量可回显)——ext容器为任务节点专属,其余节点类型的未列出键直接丢弃 (go/python/node 无ext装配腿:未列出键不出口,ext回显以properties.field投影为准,见 规范 08 场景 43);已识别键(本表所列,含经父类继承链 或黑名单判据识别的)不得再镜像进ext(issues/231 · owner 2026-10-15 拍 A:java/csharp 判据收窄向 boot2 血统对齐)- 前端设计器属性面板字段与本表的关系见 用户指南 11 · 流程设计器
1. JSON 结构总览
json5
{
// ── 顶层(引擎解析)──
"name": "leave", // 流程编码(唯一,deploy 版本管理键)
"displayName": "请假审批", // 流程显示名称
"type": "approval", // 流程类型
"expireTime": "2026-12-31", // 流程期望完成时间
"relTableName": "biz_leave",// 关联业务表名(bizData 回显)
"persistMode": "ARCHIVE", // 业务落表模式(persist)
"postInterceptors": "...", // 流程级后置拦截器(逗号分隔)
"preInterceptors": "...", // 流程级前置拦截器(逗号分隔)
"instanceUrl": "...", // 实例详情 URL(预留)
"instanceNoClass": "...", // 业务号生成类(预留)
// ── LogicFlow 图 ──
"nodes": [ /* 见 §3 */ ],
"edges": [ /* 见 §9 */ ]
}节点(LogicFlow 标准字段):
json5
{
"id": "apply", // 节点唯一编码 → 任务名 taskName(全流程唯一)
"type": "snaker:task", // 节点类型(接受带/不带前缀两种写法,见 §3)
"x": 250, "y": 200, // 画布坐标(设计器布局,引擎不参与执行)
"text": { "value": "发起申请" }, // 显示名称 displayName(引擎读取位置)
"properties": { /* 引擎扩展属性,见 §4~§8 */ }
}兼容:
type接受snaker:task与task两种写法(设计器导出带前缀,手动编写可省略)。
2. 顶层属性(流程级)
| 字段 | 类型 | 引擎 | 说明 |
|---|---|---|---|
name | string | ✅ | 流程编码(唯一)——deploy 版本管理键(同 name 从 0 递增);bizData 表名回落 |
displayName | string | ✅ | 流程显示名称 |
type | string | ✅ | 流程类型(listByType 分组键) |
expireTime | string | ✅ | 流程期望完成时间 |
relTableName | string | ✅ | 关联业务表名——bizData 回显定位表;缺省回落 name |
persistMode | string | ✅ | 业务落表模式:ARCHIVE(缺省,流程结束归档)/ SYNC(发起 INSERT → 节点 UPDATE → 定稿),见 指南 08 persist |
preInterceptors | string | ✅ | 流程级前置拦截器注册名(逗号分隔多注册名;勿用数组形——引擎侧为单串模型,数组形各栈解析行为不一,参考实现解析报错) |
postInterceptors | string | ✅ | 流程级后置拦截器注册名(逗号分隔多注册名)——persist 的办理节点权限判定路径,必须配置 |
instanceUrl | string | 预留 | 实例详情 URL(前端用) |
instanceNoClass | string | 预留 | 业务号生成类(前端/集成方用) |
selectUserOnInitiate | int | 前端 | 发起时选择处理人(0/1) |
enableCcActors | int | 前端 | 发起页抄送开关(0/1) |
enableApplyReason | int | 前端 | 发起页申请理由开关(0/1) |
enableAttachment | int | 前端 | 发起页附件开关(0/1) |
enableFieldPerm | int | 前端 | 字段权限开关(0/1) |
3. 节点公共属性(全部节点通用)
| 字段 | 引擎 | 说明 |
|---|---|---|
id | ✅ | 节点唯一编码 → 任务名 taskName |
type | ✅ | 节点类型(8 种,见下) |
text.value | ✅ | 显示名称 displayName |
properties.name | 前端 | 唯一编码(钉钉模式设计器写入);引擎取节点 id 为节点名,此键不参与执行 |
preInterceptors | ✅ | 节点前置拦截器注册名(逗号分隔多注册名) |
postInterceptors | ✅ | 节点后置拦截器注册名(persist 模型级执行后触发) |
x / y / layout | 布局 | 画布坐标/布局(引擎不参与执行) |
节点级拦截器的优先序、回落与键形(audit8 R91 立法,2026-10-16 owner 拍丙):
- 节点声明优先、缺省回落流程级:节点
properties.preInterceptors/postInterceptors非空串 ⇒ 只跑节点声明的那几条; 空串或键缺位 ⇒ 回落流程根的同名键。两档不叠加(节点非空时流程级不参与本次挂载)。 参考实现 javaNodeModel.java:49-51/:57-59,phpNodeModel.php:41-43/:49-51,csharpModels.cs:89-91/:97-99三家同形。 - 声明了就必须被执行或显式暴露:节点声明的键解析不到 ⇒ 必须可诊断报错或留痕(带节点 id 与实得串),不得静默忽略。 本条与 §3 义务 2「未知档不得静默丢节点」同族,力度从节点
type扩到 interceptors。 - 键形统一=Java 全类名字符串(跨栈可移植的硬要求:13 栈一键部署共用同一套库与同一份流程定义)。 规范键取
com.mldong.jeeflow.*形式的全类名字符串,装配方式各栈自定——java/boot2 走Class.forName反射(NodeModel.java:68-69),其余各栈走以同一串为键的注册表(gopersist/interceptor.go:324、 nodesrc/persist.ts:402、pyjeeflow/persist.py:566、phpFlowInterceptorRegistry.php:11-24、 csharpServiceContext.cs:68-69现读均已在用该串作注册键)。 ⇒ 本条是把既成惯例写进契约;未实现节点级声明的栈(见 R91 代码腿)补齐时必须沿用同一串,不得另发明短名。
| properties.width / properties.height | 布局 | 画布尺寸——与 x/y 拼成 layout 串(引擎不参与执行) |
节点类型总览:
| type(兼容写法) | 说明 | 必填 properties |
|---|---|---|
snaker:start / start | 开始 | - |
snaker:end / end | 结束(全流程只能有一个) | - |
snaker:task / task | 任务节点 | assignee 或 assignmentHandler |
snaker:decision / decision | 条件分支 | 出边 expr |
snaker:fork / fork | 并行分支 | - |
snaker:join / join | 并行合并 | - |
snaker:custom / custom | 自定义节点 | clazz |
snaker:subProcess / subProcess | 子流程 | form |
类型键的三条义务(issues/141 G4 · 2026-09-29 立法;第 1、3 条已按 owner 2026-10-01 裁定改写)
规范名=设计器实际输出(owner 2026-10-01 拍板:「只能按照设计器做,历史旧账不管」): 子流程类型键立驼峰
snaker:subProcess——mldong-flow-designer-plus 两模式(canvas / dingtalk) 实际输出的就是它(NodeTypeEnum.subProcess = 'subProcess'),vben5-wf 原样透传无改写层, 库里全部存量子流程定义也都是驼峰。原 09-29 立的「小写snaker:subprocess+ 查表前大小写归一」 作废:归一化不做、各写法间历史兼容不做。普查现读(保留作背景;行号已按 2026-10-01 HEAD 复锚): 八栈查表全部大小写敏感、零归一化——subProcess(大写 P)只有 java/c# 认(Configuration.java:52/ModelParser.cs:134)、subprocess(全小写)只有 php/rust/moon 认(ModelParser.php:40/parser.rs:178/parser.mbt:157)、go/python/node 压根没有这一档(类型常量表各 7 档: gomodel/types.go:40-46、pythonmodel.py:37-43、nodemodel.ts:30-36)。 ⚠️ 这三栈也没有兜底臂:未知类型不进任何档,只被_find_by_type/findNodeByType静默略过—— 旧文那句「驼峰串在 go/node 落 Custom 兜底」不成立;有_ => Custom兜底的是 rust/moon 的旧形状, 已随 issues/142 A 批拆成显式Unknown(rustparser.rs:179、moonparser.mbt:161)。 补齐方向(owner 2026-10-01 二拍:「可以暂不支持,后续再单独开」)=子流程暂不进契约面: 六栈(php/rust/moon/go/python/node)不补 subprocess 档——java/c# 的执行侧也不是真实现 (java 简化壳、c# 抛 20010002 打断办理,四态见 §7),补档只会造出「看起来支持、其实不执行」; 设计器画出子流程节点时,六栈按下面义务 2 的未知档日志显式暴露(go/python/node 的日志腿已随批二 §3-5 补齐,见下)。 子流程真实现后续单独开案。(本段取代此前「缺档补齐方向=补驼峰档、另轮实施」的记法)未知档不得静默丢节点:类型不在表里时,必须记一条可诊断日志(带节点 id 与实得类型串)再决定跳过, 不允许"静默丢节点+连带丢它的出边"。 历史反面样本(09-29 快照,其中两条已被 issues/142 A 批换代,别再当现状读):php 类型表当时根本没有
custom档(七档无'custom',也无CustomNodeParser.php),而它自家共享夹具jeeflow-php/flows/08-custom-node.json:38就写着"type": "snaker:custom"⇒ 这个节点在 php 栈被无声吞掉; rust/moon 当时用_ => Custom兜底"不丢",但那等于把未知塌成记录类(违反 §6.1)。 现状(2026-10-01 复锚):php 已补custom档(Parser/CustomNodeParser.php已存在、ModelParser.php:44注册)+未知档error_logWARNING(ModelParser.php:112);rust/moon 的兜底臂已拆成 显式Unknown+诊断文案(rustparser.rs:118-127/:179,moonparser.mbt:161+engine/engine.mbt:203 println(unknown_node_warning(...)));javaModelParser.java:87、 c#ModelParser.cs:135/145亦已有日志。曾有的唯一欠账=go/python/node 三栈未知档零条日志 (09-29 横扫命中文件数:java 1/php 1/c# 2/rust 7/moon 7/go 0/python 0/node 0) 已由批二 §3-5 补齐(2026-10-01,八栈 8/8):goengine/engine_impl.go:643-646(守卫isKnownNodeType:610)、 pythonjeeflow/engine.py:696-700+model.py:55KNOWN_NODE_TYPES、nodesrc/engine.ts:757-779+model.ts:50KnownNodeTypes。三栈都没有独立解析期丢弃臂(go/python 的parse不按类型过滤、 node 连解析都没有)⇒ 日志只能落执行腿"没人认领"那一处;挂到解析/读定义出口会跟着每次 发起与办理各打一遍。 ⚠️ 两条不要顺手对齐的口径:① 未知判据必须是具名类型表(∉ 表),不能是"if/elif/switch 走完 没人认领"——snaker:start在表里却恰无执行分支,用后者会把拓扑病误报成类型病;② 日志文案不是逐字契约面 (门面 msg 才是),各家按本栈设施与形状写:java/php/c# 在解析期 ⇒ 「没有对应解析器」, go/python/node/rust/moon 在执行期或类型表 ⇒ 「不在类型表里」,两要素齐(nodeId=+实得type=原文) 即可横扫取证。义务 2 的后半:指向被丢弃节点的那条边必须「落穿停住」,严禁打崩办理(issues/143 · 2026-09-30)。 「记日志再跳过」只说了节点怎么办,没说上游那条指向它的边怎么办——而「跳过节点」若让令牌撞上 悬空引用,用户面就是「配错一个类型串 ⇒ 整条流程办不动」,与 §6.2 第 2 条「严禁抛错打断建单」 以及 spec/04「节点属性配错不该把流程炸掉」直接冲突。 统一形状=记一条可诊断日志(带 from/to)+ 令牌停在这条边:实例留 DOING、库里不产生越过它的行。 不选「越过未知节点继续往下跑」——未知档没被解析成任何模型,越过它等于用一条臆造的通路把跑不通的 定义跑成功,用户面更难发现(与 §6.2 第 1 条「查不到的留痕=没留痕」是同一把尺子的反向用法: 跑得通的假象=查不到的病灶)。 现读分派(同一病灶的三种表现): · 崩的一派=对象图形状:java(
ModelParser.java:99-109只在目标节点存在时setTarget,TransitionModel.java:29-31的 else 分支裸调 ⇒ NPE)、php(逐字同形 ⇒Call to a member function execute() on null); · 无声停住的一派=c#(Models.cs的else if (Target != null)守卫在,不崩,但既无解析期日志 也无落穿日志 ⇒ 义务 2 的「可诊断」没兑现); · 本来就停住的一派=go/python/node/moon/rust(按 id 现查目标,查不到就停;moon/rust 另有解析期 WARNING,go/python/node 至今零条日志 ⇒ 那笔属义务 2 未落地的欠账,见 issues/141 G4)。 本轮落地:javaa2d2529/phpc71d72e/c#7fa7b95,三栈各配同判据用例、改前红样在案; c# 顺带补小写subprocess档(义务 3 的 c# 一角,java 早在3d1fc98已补)。别名保留一代(一般原则):新增/统一类型键时,旧写法作为同值别名继续接受一代并标注 deprecate。 subprocess 档例外(owner 2026-10-01 裁定「历史旧账不管」):java/c# 的
wfSubProcess与 1.8.36 代补的小写subprocess别名就地保留、不删不管——尤其不得删驼峰subProcess(设计器与全部存量定义产出的正是驼峰,查表大小写敏感、未命中即丢节点); 原「先给 java/c# 补小写别名、下个代次再删」的计划随裁定作废。
发起申请节点约定(mldong 框架契约):start 后第一个任务节点必须是"发起申请"节点,
assignee = "applicant"(引擎解析为流程发起人)。引擎不自动执行该节点,由调用方 (startAndExecute)启动后自动完成,流程推进到真正的审批节点。
4. 任务节点 properties 完整字典(snaker:task)
| 字段 | 类型 | 引擎 | 说明 |
|---|---|---|---|
assignee | string | ✅ | 固定参与者:逗号分隔多人;applicant = 发起人;token 优先按流程变量 key 解析(见 参与者解析) |
assignmentHandler | string | ✅ | 动态参与者注册名(内置清单见 指南 07) |
form | string | ✅ | 表单标识 → formKey(发起页/办理页表单定位) |
taskType | int | ✅ | 0=主办 1=协办 2=记录(记录类节点历史行档;引擎枚举含三值,历史行落值各栈形状不承诺统一——见规范 06 §4.6 approvalRecord) |
performType | int/string | ✅ | 0=普通参与(多人任一完成即可)1=会签(每人独立任务);兼容字符串 '1'/'ALL'/'COUNTERSIGN' |
countersignType | string | ✅ | 会签模式:PARALLEL(并行,全部完成推进)/ SEQUENTIAL(串行,依次流转)——仅两档;"按比例"不是独立模式=PARALLEL+countersignCompletionCondition 表达式组合(写 RATIO 串按缺省落 PARALLEL) |
countersignCompletionCondition | string | ✅ | 会签完成条件表达式(如 #nrOfCompletedInstances>=2);特殊值 ONE_VOTE_VETO(忽略大小写)= 开启一票否决,见 §4.2 |
candidateUsers | string | ⚠️ | 候选人 userId 列表(逗号分隔)——不生成 actor,供 candidatePage 选人 |
candidateGroups | string | ⚠️ | 候选角色标识(逗号分隔) |
candidateHandler | string | ⚠️ | 动态候选人处理注册名 |
reminderTime | string | 预留 | 提醒时间(如 10:00)——无行为实现(到期提醒腿八栈均无) |
reminderRepeat | string | 预留 | 重复提醒间隔——无行为实现 |
expireTime | string | ✅ | 期望完成时间 |
autoExecute | string | 预留 | 自动执行配置——无行为实现(到期自动执行腿八栈均无) |
callback | string | 预留 | 回调处理注册名——无行为实现(任务完成回调腿八栈均无) |
preInterceptors | string | ✅ | 前置拦截器(逗号分隔多注册名) |
postInterceptors | string | ✅ | 后置拦截器(persist 写库/权限判定;逗号分隔多注册名) |
field | object | ✅ | 字段权限声明(见 §4.1) |
| 其他任意键 | any | ext | 不识别 → 存入节点扩展属性 ext(任务变量回显,不参与执行) |
候选人/会签键双源兼容:
candidateUsers/candidateGroups/candidateHandler与countersignType/countersignCompletionCondition既认properties顶层写法,也认旧版设计器的field扩展属性写法(field内容即 §4.1 的ext容器);两源并存时的覆盖方向不承诺统一。
4.1 字段权限(field,SYNC persist 用)
- 声明位置:任务节点
properties.field - 键格式(双兼容):
PERMISSION_{表单字段全名}(含f_前缀,前端 vben5-wf 约定,优先) 与PERMISSION_{去前缀名}(旧格式) - 值:
1=只读 /2=可编辑 /3=隐藏(缺省=可编辑) - 办理入口过滤:按任务节点权限过滤后再入流程变量——上游只读不可被下游绕过(可判定形状见 规范 08 场景 43)
- 状态列:优先
{节点ID}_{状态码}列,无则{节点ID}列
4.2 会签(countersign)行为
| 模式 | 行为 | 推进条件 |
|---|---|---|
PARALLEL | 每个参与者一个独立任务 | 全部完成 |
SEQUENTIAL | 一次一个任务,完成后流转给下一个参与者 | 全部完成 |
PARALLEL+条件表达式 | 每人一个任务(比例完成) | 完成数满足 countersignCompletionCondition |
一票否决(ONE_VOTE_VETO) | countersignCompletionCondition 设为 ONE_VOTE_VETO(忽略大小写)时,任一成员 submitType=20 会签不同意 → 节点立即推进 | 否决者提交即合并 |
会签推进/否决后的残留处理:节点合并(merged)推进下一节点时,该节点仍 DOING 的会签任务一律废弃(taskState=99),不留孤儿待办——并行会签下其余成员的待办即被清空(对齐 mldong 内置引擎 abandonProcessTask 行为)。
会签拒绝(submitType=20)的默认语义 = 软拒绝:未配置 ONE_VOTE_VETO 时,否决者任务正常完成、countersignDisagreeFlag=1 记录为流程变量(供下游节点作参考),流程不阻断,按常规模式(全部完成 / 表达式)等待推进——会签意见默认只作参考、不阻断流程。
5. 决策节点 properties(snaker:decision)
| 字段 | 位置 | 引擎 | 说明 |
|---|---|---|---|
expr | 节点 | ✅ | 节点级默认表达式(可选) |
handleClass | 节点 | ✅ | 自定义决策器处理类注册名 |
expr | 出边 | ✅ | 分支条件表达式——决策出边必填(首条满足者胜) |
text.value | 出边 | 前端 | 分支标签(钉钉模式渲染在线上;引擎解析器不读边标签) |
决策路由规则:引擎按出边顺序求值,第一条 expr 为真的边获胜(命中即停,多真边并行流转不是 spec 语义——issues/234 丙-9 立法,八引擎已加 break);表达式语法见 指南 02 §5。
默认分支(无 expr 的出边)=可选超集,不承诺统一(issues/234 丙-10 → 队列 R93,2026-10-15 owner 拍 C):
- 有兜底三栈,且选取顺序两档都许可:
边序=在同一次遍历里遇到 expr-less 出边立即取用(goengine_impl.go、nodeengine.ts;于是[e1(无 expr), e2(expr 真)]走 e1);段序=先把全部带 expr 的边跑完,全 false 再取第一条 expr-less 边(python_evaluate_decision两段循环;同一定义走 e2)。两档都合法,跨栈不保证同一条边走哪一支。 - 无兜底五引擎:java/php/csharp/rust/moon 的 expr-less 出边只在自定义决策器(
handleClass)返回的节点名匹配时才可选,表达式全 false 时不因它兜底(mldong-boot2 内置版与 java 同形)。 - 下限(不可豁免):一条定义跑完全部出边仍无命中 ⇒ 必须显式报错,对外
99999999+msg 逐字decision节点无法确定下一步执行路线(20010001 档,规范 06 §4.6 义务 2 的 issues/164 立法已覆盖此面),严禁静默停住——令牌不前进也不报错=办理人看到code=0"成功"、实例挂在 state=10 且没有任何人的待办。 - ⚠ 写给设计器/集成方:要用默认分支语义,流程定义必须按 go/node/python 三栈的形状,同一份定义部署到其余栈会在决策节点直接报错。要跨栈可携就别画无
expr的决策出边,改成一条恒真表达式(如#amount>=0)。 - 📌 更正记账:audit 卡面(C2-L1 静-1,批2 时点)记的「rust/moon/go/node 此处静默停」与「python 独家末段回退硬走第一条边」两档读数已被 issues/161/164 轮修平——2026-10-15 逐仓现读八栈全显式抛同串,唯 php 抛的是
JeeflowException('无法确定下一节点')无码(对外落code=0=失败报成功,违反框架契约),随 audit8 行为并批轮钉成同码同串。
6. 自定义节点 properties(snaker:custom)
| 字段 | 引擎 | 说明 |
|---|---|---|
clazz | ✅ | 处理器类路径(必填) |
methodName | ✅ | 方法名 |
args | ✅ | 参数变量(逗号分隔的变量 key) |
val | ✅ | 返回值写入的变量 key |
6.1 记录类型节点没有参与者是正常形态,不是缺陷(issues/141 G9 · 2026-09-29 owner 拍)
owner 原话:「这个得根据任务类型来,自定义类型这种记录类的,不会有参与人,是正常行为。」 ⇒ "参与者解析为空"要按节点类型分判,不许一刀切:
| 节点类型 | 参与者为空时的正确形状 | 禁止的形状(本轮抓到过实例) |
|---|---|---|
任务类(task/approval,含会签) | 建待办行(task_state=10),参与者可以为零(java CreateTaskHandler 无条件建单) | 因为"没人可办"就跳过建行 ⇒ 实例停在 state=10 却零可办行(死锁黑洞;python 曾如此) |
记录类(custom 这类自动执行/留痕节点) | 执行 clazz、落历史行(task_state=20)、令牌继续流转。它本来就不该有参与者,也不该有待办 | ① 当任务类建 DOING 行;② 兜底把行挂给当前操作人(伪造一条他不该收到的待办);③ 直接跳过节点不建行(丢留痕) |
三条硬结论:
- "参与者为空 ⇒ 兜底挂给当前操作人"这种写法八栈一律不许有(python 为消
demo_reset_test长期红曾这么修过,本轮撤回)。 - 记录类节点不产生待办 ⇒ 任何"按待办数/可办人数"做的跨栈对账与种子矩阵计数,都要先把它从分母里摘出去。
- 判据分型:同一格不许既钉"任务节点必须建一行"又钉"custom 节点不该有 DOING 行"。 形状改了之后种子矩阵的数值要按新形状重算——那是"跟着裁定改数值", 与"改期望值蒙过红"是两件事,报告里必须分开写清是哪一种。
6.2 记录类历史行的三条硬要求(issues/142 §5 · 2026-09-30 owner 逐条拍)
上一节把"该不该建行"定了,这一节把"建出来的行要满足什么"定死。三条都是普查实读撞出来的:
- 历史行必须真落库——"落历史行"=
wf_process_task里查得到那一行 (task_state=20、参与者=当前操作人、task_parent_id与行级首节点标记照建单不变量走), 只在聚合内存对象里 append 一条不算做到。 ⚠️ 现读基准自身违反本条:javaCustomModel.exec丢弃createHistoryTask的返回值只进instance.tasks,而persistTasks只保存exec.getProcessTaskList()、updateInstance的级联又只UPDATEtaskId != null的行(ProcessTask.create从不赋 taskId) ⇒ 这条 DONE 行永远进不了库;c# 同形。java 自家JeeflowFullTest#test08CustomNode对历史行零断言,所以这个洞测试照不出来。两栈本轮补 INSERT 腿。 clazz解析不了 ⇒ 记日志 + 照常落历史行 + 令牌继续流转,严禁抛错打断建单 (owner 09-30 拍;与spec/04那条"误配的相对档落穿→NULL、节点属性配错不该把流程炸掉" 是同一条哲学)。现读 java/c# 是显式报错(自定义模型[class=...]实例化对象失败),本轮跟改; python 已经是这个形状(记 WARNING 后继续),保持。"未注册处理器"与"clazz 为空串"要分档—— 两条都记日志继续,但日志文案要分别可诊断(c# 现在把两者合成同一个异常,覆盖面比 java 宽)。 处理器自身执行失败(反射到了、跑炸了)不在本条豁免内:照旧外抛,那是业务错误不是配错形状。 ⚠️ 豁免面边界(A 批收口补的细则——两栈当时各自解读成了一档):判据只有一句 「这一行处理器代码有没有被执行过」。- 没执行过=配错档 ⇒ 记日志继续:
clazz空串/属性缺失、clazz非空但未注册、反射实例化失败、methodName未配或在处理器上找不到、参数形状对不上导致方法一行都没跑; - 执行过=业务错档 ⇒ 照旧外抛:
IHandler.handle/HandleAsync/ 方法体内部抛出的异常 (含InvocationTargetException解包出的目标异常)。 ⇒methodName找不到属配错档(配置项在调用之前就错、处理器零执行,与clazz写错、 与 spec/04 那条到期档落穿同族;外抛并不比一条带class+method名的日志更可诊断)。
- 没执行过=配错档 ⇒ 记日志继续:
1bis. 这条 DONE 行必须写 operator 与 finish_time(A 批收口补,同一把尺子:查不到的留痕=没留痕) ——processTask/doneList(state<>10 AND operator=?)与 processInstance/approvalRecord 都按这两列取数, 已完成行不带处理人和完成时间,在用户面上就等于这条留痕没落过。 operator=触发这次流转的当前操作人(与参与者列同值,是留痕主体不是待办),finish_time=该行落库时刻。 ⚠️ 现读 java/c# 的 ProcessTask.create 只用 operator 实参填 create_user/update_user, 既不填 operator 字段也不填 finish_time ⇒ 两栈跟改;python/php 已是这个形状。 反过来 expire_time 保持 NULL:记录类节点没有到期表达式可算(expireTime 是 task 节点专属属性, CustomParser 只映射 clazz/methodName/args/val),写它就得臆造属性, 与 issues/126 owner 口径"节点没配就保持 NULL、不许赋建单时刻"同向。 ⇒ 两列写、一列不写,别顺手一起补。 3. 记录类腿不解析参与者,所以"参与者为空"这一判据对它不适用; 反过来任务类零参与者必须建 DOING 行(第 6.1 表第一行), 现读 go / node / rust / moon 四栈是"actors 为空 ⇒ 一行不建" (engine_impl.go:716、engine.ts:786、engine.rs:444、engine.mbt:295) ——那正是 6.1 点名 python 曾犯的死锁黑洞形状,本轮四栈跟改。 附带:python 任务类腿现在还兜底挂当前操作人(engine.py:724-728),按硬结论 1 一并撤掉, 改成与 java 逐字同形的"建零参与者行"。
⇒ 这条改变的是用户可感知的形状(有没有一条查得到的留痕、待办列表里多不多一条), 所以任何一栈只做满一半(落库不建行、建行不落库、记日志但停在原地)都算违反本条。
7. 子流程节点 properties(snaker:subProcess)
⚠️ 子流程暂不进契约面(owner 2026-10-01 二拍:「可以暂不支持,后续再单独开」):类型键规范名是驼峰
snaker:subProcess(§6.1 义务 1),但只有 java/c# 认这个串,且两栈的解析层只搬form/version; 执行侧四态(issues/219 · audit8C2-L3 R-6现读更正):java 简化壳(StartSubProcessHandler恒传defineId=null、子实例起不来)、c# 抛错(20010002「没有流程定义」打断本次办理)、 php/rust/moon 纯空壳(不建子实例、不报错)、boot2 唯一真实现;php/rust/moon 只认小写subprocess、go/python/node 无此档 ⇒ 六栈按未知档记日志后跳过(义务 2)。下表两字段属解析层契约(设计器输出什么、解析器读什么), 不承诺执行语义;真实现后续单独开案。
| 字段 | 引擎 | 说明 |
|---|---|---|
form | ✅ | 子流程表单标识 |
version | ✅ | 子流程版本号 |
8. 开始 / 结束 / 并行节点
snaker:start/snaker:end:无专属属性(仅公共属性)snaker:fork/snaker:join:无专属属性——fork 出边建议写text.value分支标签
9. 边属性(edges)
json5
{
"id": "e1",
"sourceNodeId": "start", // 起点节点 id
"targetNodeId": "apply", // 终点节点 id
"properties": { "expr": "amount > 1000" }, // 决策分支条件
"text": { "value": "金额>1000" } // 分支标签(决策/fork 边建议必填)
}| 字段 | 引擎 | 说明 |
|---|---|---|
sourceNodeId / targetNodeId | ✅ | 起止节点 |
pointsList / startPoint / endPoint | 布局 | 画布连线坐标——拼成边 g 布局串(引擎不参与执行) |
properties.expr | ✅ | 决策出边条件表达式 |
text.value | 前端 | 分支标签(钉钉模式渲染在线上,决策/fork 边建议必填;引擎解析器不读) |
10. 完整示例(标准骨架)
json
{
"name": "leave",
"displayName": "请假审批",
"type": "approval",
"relTableName": "biz_leave",
"persistMode": "SYNC",
"nodes": [
{ "id": "start", "type": "snaker:start", "x": 100, "y": 200, "properties": {}, "text": { "value": "开始" } },
{ "id": "apply", "type": "snaker:task", "x": 250, "y": 200,
"properties": { "form": "apply-form", "assignee": "applicant", "taskType": 0, "performType": 0 },
"text": { "value": "发起申请" } },
{ "id": "leader", "type": "snaker:task", "x": 400, "y": 200,
"properties": { "form": "leave-form", "assignee": "leader", "taskType": 0, "performType": 0,
"postInterceptors": "jeeflowPersistPostInterceptor" },
"text": { "value": "组长审批" } },
{ "id": "decision1", "type": "snaker:decision", "x": 550, "y": 200, "properties": {}, "text": { "value": "金额判断" } },
{ "id": "end", "type": "snaker:end", "x": 700, "y": 200, "properties": {}, "text": { "value": "结束" } }
],
"edges": [
{ "id": "e1", "sourceNodeId": "start", "targetNodeId": "apply", "properties": {} },
{ "id": "e2", "sourceNodeId": "apply", "targetNodeId": "leader", "properties": {} },
{ "id": "e3", "sourceNodeId": "leader", "targetNodeId": "decision1", "properties": {} },
{ "id": "e4", "sourceNodeId": "decision1", "targetNodeId": "end",
"properties": { "expr": "amount > 1000" }, "text": { "value": "金额>1000" } },
{ "id": "e5", "sourceNodeId": "decision1", "targetNodeId": "end",
"properties": {}, "text": { "value": "默认" } }
]
}11. 常见错误对照
| 错误 | 现象 | 修复 |
|---|---|---|
| 多个 end 节点 | 分支各自结束 | 汇合到同一个 end |
| 申请节点缺 assignee | 有任务行但零参与者(实例 DOING、无人可办=死锁黑洞) | 加 "assignee": "applicant" |
| 决策出边缺 expr | 各栈不一(go/node 边序兜底/python 段序兜底/java·php·csharp·rust·moon 不兜底⇒落零命中报错):见上文 §5「默认分支=可选超集」 | 要跨栈可携就每条出边补 expr |
| 决策/fork 边缺 text | 线上无标签 | 补 text.value |
| 节点 id 重复 | 任务名冲突 | 保证 id 全流程唯一 |
custom 写 customClass | 解析不出 clazz,处理器不执行(引擎记日志后流转照常、历史行照落;内置版抛错打断办理) | 用 clazz(旧文档字段名,引擎不识别) |
| 属性拼写错误 | 静默进 ext,行为不生效(仅任务节点;其余节点类型直接丢弃,go/python/node 连 ext 都不进) | 对照本表逐键核对 |