Skip to content

规范 11 · 事件契约 ​

立案:issues/132(owner 2026-09-28 拍 A 方案「先文档后代码」,2026-09-29 授权按实情定事件集)。 本章是唯一权威:八语言引擎(java/go/python/node/php/rust/moon/csharp)与 13 个框架集成栈都按本章对齐。 在此之前,事件机制只散在 concepts/04-extensions.md(设计原理)里,编号契约章 01~10 零立约, 导致码值三套并存(java 血缘 1..4 / go·node 的 0..5 / python 字符串名)、订阅形状两样(七栈列表、python 单回调)。

11.1 职责边界(引擎只 fire) ​

  • 引擎只负责"在事实发生后 fire 事件",不保证任何消息/站内信落库,也不感知站内信、待办角标、业务回写等下游。
  • "事件 → 消息全量字段"的组装与持久化是集成层监听器的职责(见 扩展原理 §4.4)。
  • 集成层严禁在门面/业务侧"主动补发"事件来代替引擎 fire——历史上 PHP 走过这条降级路(issues/101 已作废), 补发会让引擎补齐后出现重复抄送/重复通知。引擎侧缺哪一支,走 issues 反馈,不在集成层绕过。

11.2 四条立约原则 ​

  1. 码值表达"发生了什么事实",不表达"谁触发的"。同一事实只发一次:引擎自动路径与门面手动路径共用同一个码 (手动 processInstance/createCCInstance 也要 fire CC_CREATE,因为"新增了一条抄送记录"这个事实成立)。
  2. 码粗、载荷细。不为"拒绝 / 跳转 / 退发起人"各开一号——它们共用 TASK_REJECT,靠载荷/反查得到的 submitType 区分。 号段只随事实类别增长。一个号一旦发出去,不许改语义、不许复用(4 号位的历史见 §11.6)。
  3. 只在落库之后 fire:实例/任务/cc 行的 insert、update 成功之后才 fire。 先 fire 后落库会让监听器(站内信反查、persist 回写)读到旧状态——这是 issues/121、issues/126 两轮"回写序"教训的同一族。 例外口径见 §11.4 每支的"触发时机"列,必须写到"哪一次写库之后"。
  4. 订阅形状=监听器列表。多监听器(按注册顺序逐个回调)是基线;单回调实现属缺陷,必须向列表靠,不是反过来。

11.3 码表(规范名 + 码值 + 载荷) ​

码值取 A 套整型(java 血缘 1..4 连续扩展)。规范名是权威,码值只是各栈内部的附带数值; 集成层跨语言判据一律用规范名,不得拿数字码当判据。载荷键名一律 camelCase。

码规范名事实sourceId 指向直传载荷键(必备)触发时机(写库之后)
1PROCESS_INSTANCE_START实例发起成功instanceIdinstanceId实例行 insert 之后
2PROCESS_INSTANCE_END流程自己走到终点(办结/拒绝到终态共用,靠 state 分)instanceIdinstanceId, state实例 state 落库为 20/45 这类"走到终点"的状态之后。30(撤回)/40(终止) 不由本支表达——各有专属码 8/9;一场撤回同时发 8+2 会让下游收到"流程已办结"的错通知(node 首版即此形状,已纠)
3PROCESS_TASK_START新待办生成(含会签逐人、回退复活行、子流程任务)taskIdinstanceId, taskId, actors每个任务行落库(saveTask 分到 taskId)之后,逐任务 fire
4CC_CREATE新增一条抄送记录instanceIdccActorId(逐抄送人 fire 一次)cc 行落库之后;发起 f_ccActors/办理 tf_ccActors/手动 createCCInstance 三条路径同判
5TASK_COMPLETE任务被办掉(同意/跳转/会签办理)taskIdinstanceId, taskId, operator, submitType任务行 state 更新为已完成并落库之后
6TASK_REJECT任务被退回/拒绝(含退发起人、软拒绝、跳转回退)taskIdinstanceId, taskId, operator, submitType退回动作使任务/实例落库之后(与 5 互斥:同一动作走 reject 就不再 fire complete)
7TASK_TRANSFER转办发生taskIdinstanceId, taskId, fromActor, toActor, operator任务参与者被替换并落库之后
8TASK_WITHDRAW撤回发生(实例进入 30)instanceIdinstanceId, operator撤回只发 8,不补发 2——2 的语义是"流程走到终点",撤回不是。撤回把实例 state 写 30 落库之后;被撤回任务行更新完成后 fire 一次(每轮撤回只 fire 一次,不逐任务)
9INSTANCE_TERMINATED实例被终止(40)instanceIdinstanceId, operator, reason实例 state 写 40 落库之后。只发 9,不补发 2(同 8 的理由)。实读四栈(java/csharp/moon/node)+php:门面没有"终止实例"action、聚合根 Interrupt 生产路径零调用者 ⇒ 9 当前无可达触发源,只占号;哪个收口点真把 state 写成 40,那里就必须 fire 9。缺路径的栈在 08 场景 34 按 unreachable 记账,不许为了凑绿凭空造一个门面 action(那是跨栈契约扩张,owner 未批)
10+预留超时催办 / 超时自动通过 …——本轮不发,仅占号防分叉
  • 直传键之外的字段允许监听器反查仓储得到(如任务名、流程名、发起人)。契约只钉"必须能拿到上表这些键", 具体承载形态各栈自定(java 是 ProcessEvent{eventType, sourceId, ccActorId, data},data 即载荷 map)。
  • ⚠️ 码 3/5/6/7 的 instanceId 不是"方便字段",是硬要求:这四支的 sourceId 是 taskId, 任何"按实例聚合事件流水"的消费方(集成层的 sink、审计、待办角标计数)都只能靠载荷里的 instanceId 归堆。引擎在 fire 点不填 instanceId ⇒ 一条流的流水被拆成多个 taskId 键,顺序无法复原。 java 的落点是 ProcessPublisher.KEY_INSTANCE_ID(各 fire 点 .data(FlowData.create().set(KEY_INSTANCE_ID, …))), 其余栈同构照做。
  • actors(code 3)=该待办的参与者列表;state(code 2)=落库后的实例状态整数。
  • 载荷里的 id 类字段与引擎其它出口同口径:用户 id 是字符串,实例/任务/定义 id 各栈按本栈类型系统承载, 跨栈判据只比语义不比原始类型(同 issues/124 口径)。

11.3.1 监听器形状与派发时序(issues/146 补) ​

  • 监听器形状按语言自定,规范只钉两件事:① 监听器必须能做写型副作用(站内信、归档、通知外发), 所以"查库只有异步一条路"的语言里监听器就得是异步形状(MoonBit 已改为 async 闭包); ② 单个监听器抛错只隔离自己,不回滚主流程、不中断后续监听器(§11.5 逐监听器兜底)。
  • 派发时序=落库之后,且对外承诺为"提交之后"。宿主把整轮写包进事务时,fire 必须挪到 COMMIT 之后 (boot4 用 afterCommit 事件监听器就是这个理由);门面级没有外包事务模板的栈,"落库后"与"提交后"等价。 本条按 audit8 R77 拍读法一定档=承诺由监听器侧承担,故"事务内 fire+提交后监听"是合规形状,接了整轮事务模板却尚未提供提交后钩子的栈(go/python/node/csharp/rust)补齐的是钩子通道。 事务模板回调的错误集仍按规范 12 §12.1 条 2 保持开放,不因监听器折成闭集而收紧。

11.4 明确不发的事件(写死,免得各自发明) ​

  1. 超时/催办/自动通过不发:八栈都没有时钟扫描器,发了没有触发源。号段已预留(10+)。
  2. 定义生命周期不发:saveDefine / updateDefineState / redeploy / removeDefine 不 fire 流程事件。
  3. 变量/persist 写入不发:updateInstance 级联落库、updateTask 变量写入不构成独立事实。
  4. 任务结束没有独立事件位:TASK_COMPLETE(5)就是"任务被办掉"这件事;历史上被废弃的 PROCESS_TASK_END 不再复活(§11.6)。
  5. 委托(surrogate)生效不发:委托只是改变了待办的实际接收人,TASK_START 已经覆盖。
  6. 会签一票否决时被兄弟任务置 abandon(99) 的那些行不发:它们既不是"被办掉"(5) 也不是"被退回"(6), 只是被作废;否决者本人的那一支按 submitType=20 走 6。
  7. 跳转(submitType=4)一律发 5:引擎不在 fire 点做"目标是不是上游节点"的图回溯判断; 要区分回退语义的下游自己看载荷 submitType(这与原则 2「码粗载荷细」同形)。

11.5 订阅形状与异常隔离 ​

义务口径
注册集成层向引擎注册一个或多个监听器;注册顺序=回调顺序(契约只承诺同栈内有序,不承诺跨栈注册顺序)
回调一次 fire 必须把该事件送给全部已注册监听器,不得"后注册覆盖前注册"(python 单回调即此病,本轮收口)
异常隔离逐监听器 catch:单个监听器抛异常只记日志,① 不回滚主流程,② 不中断后续监听器(issues/104 P2 口径,八栈统一)
无监听器零注册时 fire 必须安全返回(不得空指针/panic)
幂等引擎不得因监听器失败而重发;下游要自己去重

11.6 码值迁移与历史包袱(各栈必读) ​

  • 4 号位:Java 自己的死码 PROCESS_TASK_END=4(引擎从不 fire,全联邦零消费)让位给 CC_CREATE, 与 PHP 参考实现(v1.3.8 CC_CREATE=4,已发版活码)对齐。1/2/3 不变。
  • Go / Node 原为 iota 起的 0..5 套(TaskComplete=4、CcCreate=5)⇒ 本轮整表重排到 A 套 1..9: CcCreate 5→4、TaskComplete 4→5,其余按 §11.3 对齐。这是破坏性变更,已发布版本里数字码不可混用, 集成层监听器一律按规范名分派。
  • Python 原为字符串名(CC_CREATE="CC_CREATE")⇒ 改整型 A 套,且 event_listener 单回调改监听器列表。
  • Java 是"手动 createCCInstance 不 fire"那一派——本案里唯一一处基准要向 go/py/node 学的地方, 按 §11.2 原则 1 补齐。
  • 收口后八栈规范名统一;旧版 InstanceEnd 合并"办结+拒绝"与 Finish/Reject 拆分的差异, 以"实例终态=PROCESS_INSTANCE_END(2)、任务退回=TASK_REJECT(6)"为准。
  • 改名兼容义务(八栈统一):成员改名时必须保留旧成员名作为同符号别名一代并打 deprecate 注释 (例:go 的 EventTaskCreate = EventProcessTaskStart 同一符号,不是同一个数字); 下一个代次再删。数字码不承诺稳定——别名保的是"源码还能编过、switch 还能命中同一支", 不是保数值。理由:引擎是已发布的库,13 个集成壳之外还有课程/外部用户在用, 直接删符号会让下游在升 pin 时整栈编译断掉,而断在监听器分派这种静默路径上最難查 (laravel 的 match 旧名、nestjs 的 WF_EVENT 数值镜像都是这一类)。 集成壳本轮一律改用规范名,不依赖别名续命;别名只留给外部消费者一个代次的迁移窗口。

11.7 与抄送(issues/127)的联动义务 ​

办理时抄送必须补齐这条腿:execute 带 tf_ccActors ⇒ 与任务更新同事务建 wf_process_cc_instance 行,落库后逐抄送人 fire CC_CREATE, 行为基准取 Java JeeflowEngineImpl(发起 f_ccActors 与办理 tf_ccActors 走同一个 notifyCcCreate)。 go/python/node 三栈此前只暴露"手动 createCCInstance",即由集成层自己补 ⇒ 属 §11.1 禁止的补发态,本轮摘除。

两条本轮钉死的边界(免得各栈按"超集更友好"各自发挥):

  1. 落点必须在引擎的执行路径内、且与任务更新同事务——放在门面/调用方不算合规: 那样直连引擎 API(不经门面)的业务方就拿不到 cc 行,且 cc 建失败时流转已经前进(回滚不成对)。
  2. 覆盖面以 Java 基准为准,只算 executeProcessTask 一条。executeAndJumpTask / jumpToEnd / rollbackToOperator 这类跳转·回退 action 带的 tf_ccActors 本轮不建 cc、不发 CC_CREATE; 要扩得先改 java 基准再逐栈传播并另立案,单栈自行放宽=跨栈分叉(go 第一轮就是这种超集)。

抄送行的写入语义(actor_id=被抄送人、create_user=发起抄送的人)与 ccList 出口投影见 规范 06 §ccList;ccList 行内 operator 语义与行主键键名的跨栈基准另见 issues/138(以 boot2 内置版实读为准: 行来自 wf_process_instance,operator=流程发起人,行主键 id)。

11.8 验收 ​

逐码值的"发/不发/载荷"可判定条目见 规范 08 场景 28–36; 镜像层跨栈格 L2-30 断言一条流从发起到办结按顺序收到的码值序列 [1,3,5,2] + 抄送支 [4] (只断"出现过"不算过——顺序与缺支正是本案的两个病灶)。


11.9 跨事件相对次序不是契约(2026-09-29 立,issues/132 §10 R2-8) ​

判据只钉两件事:每一支落在自己那次写库之后(§11.2 原则 3),以及单条流内 [1,3,5,2] 依次出现 + 抄送腿有 4。两支不同事件之间的相对次序不进契约, 因为八栈建 cc 行的时机本就不同,硬钉会把参考自己判成红:

形状栈实测序列
cc 腿排在实例落库(含码 2 flush)之后java 基准 JeeflowEngineImpl.executeProcessTask(:138-139)[1,3,5,2,4,4]
cc 腿在 prepareExecuteTask 内建,早于办结go / python / phpgo 门面端到端既有用例 [1,3,5,3,4,5,2]
发起时就把 f_ccActors 落库 ⇒ 4 排第一csharp / salvo[4,1,3,5,2]

三种都合规:4 的写库是 cc 行那次 createCcInstance,2 的写库是实例 state 那次 updateInstance,各排各的之后即满足原则 3。集成层门禁 judge_event_seq 曾多一条「4 必须早于 2」, 2026-09-29 删,并在离线自证里补了「java 形必须算过」这一正向钉子 (--selftest-l2-30 现 6 向:净样本/java 形为 True,乱序/缺 4/空 sink/名码不配为 False), 防止该约束被补回后把 boot2/3/4 三栈整片判红。

同理不承诺:3 与 4 的相对次序、多任务流里各支的交错方式。要钉的是同一事实只播一次 (撤回只 8 不补 2、一次撤回只 fire 一支 8)与码值↔规范名成对(4/5 互换必红,见 §11.6 重排史)。

jeeflow · 轻量级多语言工作流引擎