// 引擎核心 —— 移植自 rust engine.rs(JeeflowEngineImpl,方案 §2.3 全 async)
// start → execute → jump → withdraw;spec/04-engine-ops.md
// 泛型 [R, E]:R : @spi.ProcessRepository, E : @spi.ProcessExtRepository
///|
pub(all) struct Engine[R, E] {
ctx : @spi.Ctx[R, E]
}
///|
pub fn[R, E] Engine::make(ctx : @spi.Ctx[R, E]) -> Engine[R, E] {
{ ctx, }
}
///|
pub fn[R, E] Engine::context(self : Engine[R, E]) -> @spi.Ctx[R, E] {
self.ctx
}
///|
fn[R, E : @spi.ProcessExtRepository] Engine::repo(
self : Engine[R, E]
) -> R {
self.ctx.repository()
}
///|
fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Engine::next_id(self : Engine[R, E]) -> Int64 raise @error.JeeflowError {
(self.ctx.id_generator_or_default())()
}
///|
/// 抄送知会事件(CC_CREATE / issues/102·104):逐抄送人 fire,cc_actor_id 直传事件体。
/// 无监听器装配时零副作用。
///
/// **入参一律是"实际新建的 actor 子集"**(issues/141 G2 · spec 06 §4):调用点先走
/// `ProcessRepository#create_cc_instance_if_absent` 拿到子集,子集为空整支不 fire——
/// spec §11.2 原则 1「码=事实」,重复抄送没发生"创建"就不该发码 4,严禁照旧按原始请求全量 fire。
/// 三条入口(发起 f_ccActors/办理 tf_ccActors/门面手动 createCCInstance)共用这一支(§11.7)。
pub async fn[R, E] Engine::notify_cc_create(
self : Engine[R, E],
instance_id : Int64,
cc_actors : Array[String]
) -> Unit raise @error.JeeflowError {
for actor in cc_actors {
let event = @event.ProcessEvent::make(CcCreate, instance_id).with_cc_actor_id(actor)
@event.notify(event, self.ctx.event_listeners())
}
}
///|
/// 注入用户信息变量(u_userId/u_realName/…;C25:仅 start 注入一次)
async fn[R, E] Engine::add_user_info(
self : Engine[R, E],
args : @json.FlowData,
operator : String
) -> Unit raise @error.JeeflowError {
// issues/194:spec 04:55-56「忽略大小写」——java FlowUtil.addUserInfoToArgs 用 equalsIgnoreCase
let lower = operator.to_lower()
if lower == "flow.auto" || lower == "flow.admin" {
return
}
match self.ctx.user_provider {
Some(get_user) => {
// provider 签名是 `(String) -> UserInfo? raise JeeflowError`:
// 取用户失败按旧语义吞掉(catch 到 _ => ()),不炸发起路径。
try {
match (get_user)(operator) {
Some(user) => {
args.insert_str("u_userId", user.user_id())
args.insert_str("u_realName", user.real_name())
args.insert_str("u_deptId", user.dept_id())
args.insert_str("u_deptName", user.dept_name())
args.insert_str("u_postId", user.post_id())
args.insert_str("u_postName", user.post_name())
}
None => ()
}
} catch {
_ => ()
}
}
None => ()
}
}
///|
/// autoGenTitle:"{realName}的{displayName}-{time}"(C25)
fn gen_auto_title(args : @json.FlowData, display_name : String) -> String {
let real_name = args.get_str_or("u_realName", "未知")
"\{real_name}的\{display_name}-\{@model.current_time_str()}"
}
///|
/// 参与人解析(优先级:tf_nextNodeOperator → assignee 字面量/变量 → assignmentHandler → candidateUsers)
async fn[R, E] Engine::resolve_assignee(
self : Engine[R, E],
exec : @exec.Execution,
node : @parser.NodeModel
) -> Array[String] raise @error.JeeflowError {
// 1. tf_nextNodeOperator(issues/142 B 批 · spec 06-facade §2.11「消费腿两形同判据」)
// 改前这一腿走 `exec.args.get_str(...)` ⇒ 只认逗号串:发起人用**数组**指派下一节点参与者时
// 整条失效(静默落回节点自己的 assignee),而逗号串"全空白"档(如 " , ,, ")非空 ⇒ 直接 return
// 空集合 ⇒ 建出一张零参与者待办。现在两形都过同一个适配器 parse_actors(与 cc 腿同一枚),
// 判据只在 @model.normalize_actors 那一处;解析后为空 ⇒ 视为"没指派",按优先级链落下一腿。
let next_ops = parse_actors(exec.args.get("tf_nextNodeOperator"))
if !next_ops.is_empty() {
return next_ops
}
// 2. assignee 字面量 / 变量引用
match node.assignee() {
Some(assignee) =>
if !assignee.is_empty() {
if assignee == "applicant" {
return [exec.process_instance.operator]
}
match exec.args.get_str(assignee) {
Some(v) => if !v.is_empty() { return split_actors(v) }
None => ()
}
return split_actors(assignee)
}
None => ()
}
// 3. assignmentHandler
match node.assignment_handler() {
Some(handler_name) =>
match self.ctx.find_assignment_handler(handler_name) {
Some(handler) => {
// handler 签名是 `-> String raise JeeflowError`:旧 `try?` 把 raise 折成 Result,
// Err 臂就是"这一档跳过";换成空串后下面的 !is_empty 判据走同一条落空路径。
let result = try { (handler)(exec) } catch { _ => "" }
if !result.is_empty() {
return split_actors(result)
}
}
None => ()
}
None => ()
}
// 4. candidateUsers(candidateGroups 由 OrgUserProvider 在上层装配处理)
// 逗号串切元素后同样只过 @model.normalize_actors 那一枚判据(issues/142 B 批:本栈不许
// 同时存在"过归一的腿"与"本地各写一份 trim 的腿")
let users = match node.candidate_users() {
Some(u) => u
None => ""
}
@model.normalize_comma_actors(users)
}
///|
/// 逗号串切元素(issues/142 B 批:判据不再本地各写一份 trim/is_empty —— 与数组腿、门面腿、
/// 两仓写侧同一枚 @model.normalize_actors;顺带把"同一次调用内的重复"折叠到与数组档同一答案)
fn split_actors(s : String) -> Array[String] {
@model.normalize_comma_actors(s)
}
///|
/// issues/131(案 A):串行会签簿记三件写进成员任务的变量。
/// 判据基准逐字取 java:写 ProcessInstance.java:257-259;读与推进 CountersignHandler.java:71-84,154-157。
fn seed_countersign_bookkeeping(
task : @model.ProcessTask, node : String, roster : Array[String], loop_counter : Int64
) -> Unit {
task.variables.insert_string_array("operatorList_" + node, roster)
task.variables.insert_i64("loopCounter_" + node, loop_counter)
task.variables.insert_i64("nrOfInstances_" + node, roster.length().to_int64())
}
///|
/// issues/126 案 A · 建单路径共用的到期写点(普通建单 / 串行会签首位 / 并行会签全员 /
/// 串行会签**推进出的下一位成员**——§1.8 更正后的第五处)。
/// 表达式取自任务节点属性 `expireTime`(parser 已把 properties 逐键搬进 NodeModel);
/// 变量源=**实例变量**(java `ProcessInstance.applyExpireTime` 的 `this.variables`
/// = boot2 的 `execution.getArgs()`)。
/// 刻意用 `prop_str` 取表达式:非字符串档(JSON null / 数字)一律当"没配",不会把"没配"
/// 伪造成"配了个算不出的表达式"(两档终局同为 NULL,但后者会白走一遍求值档)。
fn apply_node_expire_time(
exec : @exec.Execution, node : @parser.NodeModel, tasks : Array[@model.ProcessTask]
) -> Unit {
let expr = node.prop_str(@model.expire_time_prop)
for t in tasks {
@model.apply_expire_time(t, expr, exec.process_instance.variables)
}
}
///|
/// 执行节点(async 递归:start 沿边驱动全图)
async fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Engine::execute_node(self : Engine[R, E], exec : @exec.Execution, node : @parser.NodeModel) -> Unit raise @error.JeeflowError {
exec.current_node = Some(node)
if node.node_type is @parser.Start {
// 克隆后继节点避免遍历中修改
let next_nodes : Array[@parser.NodeModel] = []
for e in exec.process_model.get_output_edges(node.id) {
match exec.process_model.get_target_node(e) {
Some(next) => next_nodes.push(next)
None => println(unknown_edge_warning(node.id, e.target_node_id))
}
}
for next in next_nodes {
self.execute_node(exec, next)
}
} else if node.node_type is @parser.Task {
// 任务创建不触发**后置**节点拦截器(对齐 Java CreateTaskHandler / Go executeNode)——保持原样。
// 前置侧 issues/116 批次 D 打通了独立通道(ctx.pre_interceptors),"建单那一刻"从此可挂。
// ⚠️ 内置委托自动生效**不挂这里**:此刻参与者尚未进聚合、taskId 尚未分配,
// 它落在唯一落库收口 `persist_tasks` 的 save_task 之前(见 surrogate.mbt 条款 2 ⚠️)。
self.fire_pre_interceptors(exec)
self.create_task_with_assignment(exec, node)
} else if node.node_type is @parser.Custom {
// issues/142 A 批 · spec/02 §6.1 表第二行:记录类独立一支。从前它与 Task 合流,于是被
// 任务类那套「参与者解析」判据管着、建一条 DOING 待办 —— 而记录类节点本来就不该有
// 参与者、也不该有待办:执行 clazz、落一条 DONE 历史行(真落库)、令牌继续沿出边走。
self.execute_custom_node(exec, node)
} else if node.node_type is @parser.Unknown {
// 兜底守卫:parse_model 已把未知档拦在模型外(记日志+跳过节点,spec/02 类型键义务 2),
// 正常路径走不到这一臂。留着是为了「库里存过的旧定义/第三方直接喂进来的 ProcessModel」
// 也不被当记录类落一条留痕行 —— 与解析期同判据:记日志、不静默、不动令牌。
println(@parser.unknown_node_warning(node.id, node.node_type.as_str()))
} else if node.node_type is @parser.Decision {
self.fire_pre_interceptors(exec)
let target_node_name = self.evaluate_decision(exec, node)
self.fire_post_interceptors(exec)
// 选边:命名目标按 target id 匹配,否则表达式求值(C14:只收 true 边)
let mut chosen_edge : @parser.EdgeModel? = None
for edge in exec.process_model.get_output_edges(node.id) {
let edge_expr = edge.expr()
let matches = match target_node_name {
Some(target) => edge.target_node_id == target
None =>
match edge_expr {
Some(expr) => self.evaluate_expression(expr, exec)
None => false
}
}
if matches {
chosen_edge = Some(edge)
break
}
}
match chosen_edge {
Some(edge) =>
match exec.process_model.get_target_node(edge) {
Some(next) => self.execute_node(exec, next)
None => println(unknown_edge_warning(node.id, edge.target_node_id))
}
None =>
// issues/164 A:零命中(两条 expr 边全 false / 变量缺失 / 零出边 / 自定义处理器
// 未命中)⇒ 显式报错,对齐 java `DecisionModel.exec` 尾部 `NOT_FOUND_NEXT_NODE`
// (20010001,文案逐字取 WfErrEnum;python issues/161 已走同一条路)。旧形状此处
// 静默落出=令牌不前进也不报错,办理人看到 code=0"成功"、实例挂在 state=10 却没有
// 任何人的待办(僵尸实例)。Business 档 message() 逐字透出(门面出口 99999999+msg)。
raise @error.Business("decision节点无法确定下一步执行路线")
}
} else if node.node_type is @parser.Fork {
self.fire_pre_interceptors(exec)
let next_nodes : Array[@parser.NodeModel] = []
for e in exec.process_model.get_output_edges(node.id) {
match exec.process_model.get_target_node(e) {
Some(next) => next_nodes.push(next)
None => println(unknown_edge_warning(node.id, e.target_node_id))
}
}
for next in next_nodes {
self.execute_node(exec, next)
}
self.fire_post_interceptors(exec)
} else if node.node_type is @parser.Join {
self.fire_pre_interceptors(exec)
let doing_tasks = exec.process_instance.get_doing_tasks()
if doing_tasks.is_empty() || exec.is_merged {
exec.is_merged = true
let next_nodes : Array[@parser.NodeModel] = []
for e in exec.process_model.get_output_edges(node.id) {
match exec.process_model.get_target_node(e) {
Some(next) => next_nodes.push(next)
None => println(unknown_edge_warning(node.id, e.target_node_id))
}
}
for next in next_nodes {
self.execute_node(exec, next)
}
}
self.fire_post_interceptors(exec)
} else if node.node_type is @parser.End {
self.fire_pre_interceptors(exec)
// 驳回语义:args.reject=="true" → REJECT(45),否则 FINISHED(20)
let has_reject_var = match exec.args.get_str("reject") {
Some(v) => v == "true"
None => false
}
if has_reject_var {
exec.process_instance.reject()
} else {
exec.process_instance.finish()
}
exec.instance_finished = true
// 持久化
(self.repo()).update_instance(exec.process_instance)
// fire 结束事件:载荷 instanceId + state(§11.3 code 2,「state=落库后的实例状态整数」)。
// 取的就是上面 update_instance 刚落库的那个值,不在这里二次推演。
self.notify_instance_end(
exec.process_instance.instance_id, exec.process_instance.state
)
self.fire_post_interceptors(exec)
} else if node.node_type is @parser.SubProcess {
// 子流程简化处理(rust 同款:不做完整子流程展开)
()
} else {
()
}
}
///|
/// 建任务(含参与人解析;任务类零参与者照样建单,issues/142 A 批)
async fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Engine::create_task_with_assignment(self : Engine[R, E], exec : @exec.Execution, node : @parser.NodeModel) -> Unit raise @error.JeeflowError {
let actor_ids = self.resolve_assignee(exec, node)
// issues/142 A 批 · spec/02 §6.1 表第一行 + §6.2 第 3 条:任务类**零参与者照样建一行 DOING**
// (java `CreateTaskHandler.handle` 无条件建单)。旧形状是「actors 解析为空 ⇒ 一行不建、
// 令牌直接放行到下一节点」,那正是 §6.1 点名 python 曾犯的死锁黑洞:实例停在 state=10
// 却一条可办行都没有(普查现读 go/node/rust/moon 四栈同病,本轮四栈跟改,这里即 moon 那一栈)。
// 参与者为空就落一行 actor_ids=[] 的行 —— **不兜底挂当前操作人**(§6.1 硬结论 1:八栈一律不许有)。
let perform_type = @model.PerformType::from_code(node.perform_type())
let task_type = @model.TaskType::from_code(node.task_type())
if perform_type is Countersign {
let cs_type = node.countersign_type()
let cs_upper = cs_type.to_upper()
if cs_upper == "SEQUENTIAL" || cs_upper == "SERIAL" {
// 串行:只建第一人任务(issues/94);名册簿记搬到任务变量(issues/131,见下方 seed)
if actor_ids.is_empty() {
// R115 C3⑬:零参与者照样建一行 DOING(spec/02 §6.1 表第一行)——旧形状直接 return,
// 实例停在 state=10 却一条可办行都没有(死锁黑洞)。无首人 ⇒ 无会签名册可记。
let task = exec.process_instance.create_task(
node.id,
node.display_name,
actor_ids,
exec.operator,
task_type,
perform_type,
node.form_key(),
None,
)
apply_node_expire_time(exec, node, [task])
exec.new_tasks.push(task)
return
}
let tasks = exec.process_instance.create_countersign_tasks(
node.id,
node.display_name,
[actor_ids[0]],
exec.operator,
task_type,
node.form_key(),
None,
)
// issues/131(案 A,以 java 为准):簿记三件写在首位成员任务的变量上——
// java ProcessInstance.java:257-259 就是往那一条任务塞 operatorList/loopCounter/nrOfInstances。
// MoonBit 的 struct 是引用 ⇒ 改 tasks[0] 就是改聚合根里那一份(rust 的克隆要写两份,这里不用)。
// 名单必须是 JSON 数组:门禁 L2-23 按 len(值) 数成员,逗号串会被数成字符串长度。
if tasks.length() > 0 {
seed_countersign_bookkeeping(tasks[0], node.id, actor_ids, 0L)
}
// issues/126 案 A 写点②:串行会签**首位成员**按节点表达式算到期时间
apply_node_expire_time(exec, node, tasks)
// TASK_START 不在此 fire(id 尚为 0,issues/13)——统一在 persist_tasks 落库后 fire
for t in tasks {
exec.new_tasks.push(t)
}
} else {
// 并行:一次建全
let tasks = exec.process_instance.create_countersign_tasks(
node.id,
node.display_name,
actor_ids,
exec.operator,
task_type,
node.form_key(),
None,
)
// issues/126 案 A 写点③:并行会签**全员**逐条按节点表达式算到期时间
apply_node_expire_time(exec, node, tasks)
for t in tasks {
exec.new_tasks.push(t)
}
}
} else {
let task = exec.process_instance.create_task(
node.id,
node.display_name,
actor_ids,
exec.operator,
task_type,
perform_type,
node.form_key(),
None,
)
// issues/126 案 A 写点①:普通建单按节点表达式算到期时间;
// 节点没配 ⇒ 这一列保持 NULL(不写 now()、不写 ''、不写 0)
apply_node_expire_time(exec, node, [task])
exec.new_tasks.push(task)
}
}
///|
/// 记录类返回值写入流程变量的**缺省**键(`properties.val` 没配 / 配空串时用这一档)。
/// 逐字对齐 java `FlowConst.CUSTOM_RETURN_VAL` 与 python `KEY_CUSTOM_RETURN_VAL`。
pub let custom_return_val_key : String = "custom_return_val"
///|
/// 记录类返回值写入的变量键:`val` 命中用之(trim 后非空),否则缺省 `custom_return_val`
/// (对齐 java `CustomParser.java:20-22` 解析 + `CustomModel.java:46-48` 写 `execution.getArgs()`)。
pub fn custom_return_key(node : @parser.NodeModel) -> String {
let v = match node.prop_str("val") {
Some(s) => s.trim(chars=" \t\r\n").to_owned()
None => ""
}
if v.is_empty() { custom_return_val_key } else { v }
}
///|
/// 「未注册处理器」档的诊断日志文案(spec/02 §6.2 第 2 条末尾:两档日志要**分别**可诊断,
/// 不许像 c# 那样把「clazz 空串」与「未注册」合成同一条)。抽纯函数的理由同 parser:
/// println 的效果在测试里读不回来,判据留在纯函数、副作用留在调用点。
pub fn custom_handler_unregistered_warning(node_id : String, clazz : String) -> String {
"[jeeflow] custom 节点 clazz=\{clazz} 未注册处理器,跳过执行、照常落历史行并继续流转" + "(本栈无反射,注册入口:@spi.Ctx::register_custom_handler): nodeId=\{node_id}"
}
///|
/// 「clazz 空串/缺失」档的诊断日志文案(同一节点属性的另一格病灶:连按名解析的键都没有)。
pub fn custom_clazz_missing_warning(node_id : String) -> String {
"[jeeflow] custom 节点未配置 clazz(缺失或空串),不执行处理器、照常落历史行并继续流转" + ": nodeId=\{node_id}"
}
pub fn unknown_edge_warning(from : String, to : String) -> String {
"[jeeflow] WARNING 转移出边的目标节点不在模型里(该节点类型未建档,解析期已跳过),本次流转停在这条边: from=\{from}, to=\{to}"
}
///|
/// 记录类(`snaker:custom`)节点执行腿 —— issues/142 A 批,spec/02 §6.1 表第二行 + §6.2 三条硬要求。
/// 现成对照=python 本轮那条腿(`jeeflow-python/jeeflow/engine.py::_exec_custom_node`)。
///
/// 四件事,缺任一件都算违反 §6.2(「落库不建行 / 建行不落库 / 记日志但停在原地」都是半截):
/// 1. 执行 `clazz` —— MoonBit 没有反射,java 那句 `Class.forName(clazz)` 在本栈无从谈起,
/// 故与 C#/Python 同策:`Ctx::register_custom_handler(, handler)` 按名注册、引擎按名解析;
/// 2. 落一条**历史行**(task_state=20)—— 调 issues/137 B 拍板「不删」的那支
/// `ProcessInstance::create_history_task`,本单正是把它接线的那一单;
/// 3. **真落库**:走 `save_task` + `add_task_actor`,内存仓与 MySQL 仓两条建行通道各自真
/// INSERT(§6.2 第 1 条:只在聚合 `instance.tasks` 里 append 一条不算做到 —— java/c# 的
/// 历史行至今没有 INSERT 腿,正是本条要补的洞);
/// 4. 令牌沿出边继续流转(java `CustomModel` 收尾那句 `runOutTransition`)。
///
/// ⚠️ §6.1 表列的三条禁止形状,本腿逐条不犯:① 当任务类建 DOING 行;② 兜底把行挂给当前
/// 操作人、伪造一条他不该收到的待办(历史行的 actor=operator 是**留痕主体**不是待办主体:
/// 行状态 20 谁也办不动,todoList 按 task_state=10 取行,它进不去);③ 直接跳过节点不建行。
/// ⚠️ **不 fire 码 3**(PROCESS_TASK_START):spec/11 §11.2 原则 1「码=事实」,码 3 表达
/// 「新待办产生」,而本腿建行即已完成态。同一理由本腿**不进** `exec.new_tasks` —— 那是
/// 「本步新产生的待办」清单(门面出口与集成层按它推待办),落库走 `persist_history_task` 单独一支。
///
/// ⚠️ 返回值只写进**执行变量** `exec.args`(与 java 写 `execution.getArgs()`、python 写 `vars_`
/// 同口径):同一次令牌推进里下游决策边读得到;跨步骤的实例变量回写不在本条义务内。
async fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Engine::execute_custom_node(
self : Engine[R, E],
exec : @exec.Execution,
node : @parser.NodeModel
) -> Unit raise @error.JeeflowError {
// 前置拦截器与原 Task|Custom 合流时同口径(「建单那一刻」可挂,issues/116 批次 D);
// 后置仍不 fire —— 与本栈建单腿一致(刻意不触发,见上面 CreateTaskHandler 对齐注释)。
self.fire_pre_interceptors(exec)
let clazz = match node.prop_str("clazz") {
Some(s) => s.trim(chars=" \t\r\n").to_owned()
None => ""
}
match self.ctx.find_custom_handler(clazz) {
Some(handler) => {
// 处理器**自身**执行失败 ⇒ 直接外抛:§6.2 第 2 条豁免的是「解析不了」(配置形状),
// 不是「跑炸了」(业务错误)。这一支不加 try,外抛即与 java CustomModel 方法调用失败同形。
let ret = (handler)(exec)
match ret {
Json::Null => ()
_ => exec.args.insert(custom_return_key(node), ret)
}
}
// 两档日志分别可诊断(§6.2 第 2 条末尾;c# 合成一条的形状不许照抄):都记完日志照常
// 落历史行 + 续流,严禁 abort/raise 打断建单 —— 与 spec/04「节点属性配错不该把流程炸掉」同哲学。
None =>
if clazz.is_empty() {
println(custom_clazz_missing_warning(node.id))
} else {
println(custom_handler_unregistered_warning(node.id, clazz))
}
}
let history = exec.process_instance.create_history_task(
node.id, node.display_name, exec.operator, @model.TaskType::from_code(node.task_type())
)
self.persist_history_task(exec, history)
let next_nodes : Array[@parser.NodeModel] = []
for e in exec.process_model.get_output_edges(node.id) {
match exec.process_model.get_target_node(e) {
Some(next) => next_nodes.push(next)
None => println(unknown_edge_warning(node.id, e.target_node_id))
}
}
for next in next_nodes {
self.execute_node(exec, next)
}
}
///|
/// 历史行落库通道(spec/02 §6.2 第 1 条「历史行必须真落库」的那条 INSERT 腿)。
/// 建单不变量与 `persist_tasks` 同判据:`task_parent_id` =产生这一步的那个刚办结任务
/// (发起 execution 没有当前任务 ⇒ 落 0L),行级 `isFirstTaskNode` 建单时落库(回退血缘要读它)。
/// 与 `persist_tasks` 的三处刻意差别:① 入参是单条已完成行、不进 `exec.new_tasks`;
/// ② **不 fire 码 3**;③ 不做委托并入(历史行不是待办,代理人无从「替办结」)。
async fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Engine::persist_history_task(
self : Engine[R, E],
exec : @exec.Execution,
task : @model.ProcessTask
) -> Unit raise @error.JeeflowError {
if task.task_id == 0L {
task.task_id = self.next_id()
}
task.process_instance_id = exec.process_instance.instance_id
match task.parent_task_id {
None =>
task.parent_task_id = Some(
match exec.process_task {
Some(t) => t.task_id
None => 0L
},
)
_ => ()
}
task.variables.insert(
"isFirstTaskNode",
@json.bool_of(exec.process_model.is_first_task_node(task.task_name)),
)
(self.repo()).save_task(task)
if !task.actor_ids.is_empty() {
(self.repo()).add_task_actor(task.task_id, task.actor_ids)
}
}
///|
fn[R, E] Engine::evaluate_decision(self : Engine[R, E], _exec : @exec.Execution, _node : @parser.NodeModel) -> String? {
// rust 同款:命名目标路由暂未启用,交由边表达式求值
let _ = self
None
}
///|
/// 决策/条件表达式求值的**唯一口**(issues/153② · spec 06 §4.6 义务 2):
/// 门面 `highLight` 的路径补全与引擎路由决策边(本文件选边处)必须走这一支,
/// 两处判据不得各写一套——否则画布点亮的路径和令牌真正走过的路径会分叉。
/// ⚠️ 本栈两档:SPI `expression_evaluator` 注册了走 SPI;**未注册回落内置 `simple_eval`**。
/// 这与 java/go 不同(那两栈只有 SPI 一条腿,未注册时门面整档判 false 是 spec 许可的
/// 降级档;moon 有内置兜底 ⇒ 门面侧不存在"未注册 ⇒ 全判 false"那一档)。
/// 可见性本轮由包内 `fn` 抬成 `pub`(issues/153②:**仅放宽可见性,函数体逐字未动**)。
pub fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Engine::evaluate_expression(self : Engine[R, E], expr : String, exec : @exec.Execution) -> Bool raise @error.JeeflowError {
match self.ctx.expression_evaluator {
Some(eval) => {
let vars : Map[String, Json] = Map([])
for k, v in exec.args.inner() {
vars[k] = v
}
// SPI 是 `-> Json raise JeeflowError`:值即 Json。
// issues/201(audit8 R79 拍 C=求值器自身抛异常必须落一条带原因的可诊断日志,遍历宽度不钉):
// 旧形状 `catch { _ => false }` 把「SPI 抛错」与「求值为假」压成同一个 false ⇒ 表达式坏在
// 对外读数上与"这条分支没走"完全同形、零线索(本栈 162 腿的 WARN 在门面外层,够不到这里)。
// 宽度维持折 false(该边不收、其余边继续遍历),只补可归因的一句。
try {
let v = (eval)(expr, vars)
match v {
Json::True => true
Json::False => false
Json::Number(n, ..) => n != 0.0
Json::String(s) => !s.is_empty() && s != "false" && s != "0"
_ => false
}
} catch {
e => {
println(
eval_failure_warning(
exec.process_instance.instance_id, exec.process_instance.define_id, expr, e.message(),
),
)
false
}
}
}
None => self.simple_eval(expr, exec)
}
}
///|
/// issues/201 的日志文案单点(纯函数、零副作用,判据可直接断言文本;副作用那条腿由
/// `println` 落 stdout+变异读数判别,与本仓 `listener_error_warning`(162/R19)同法)。
pub fn eval_failure_warning(
instance_id : Int64,
define_id : Int64,
expr : String,
err : String,
) -> String {
"[jeeflow] WARN 决策边表达式求值失败,该边按 false 处理(分支不收、其余边继续遍历): instanceId=\{instance_id} defineId=\{define_id} expr=\{expr} err=\{err}"
}
///|
/// 简单表达式求值:比较运算 + ${var}/#var/裸名 引用 + 布尔字面量
fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Engine::simple_eval(self : Engine[R, E], expr : String, exec : @exec.Execution) -> Bool raise @error.JeeflowError {
let e = expr.trim(chars=" \t\r\n").to_owned()
let resolved = self.resolve_var_refs(e, exec)
// issues/159(spec 06 §4.6 义务 2 第三条 ⚠):${}/# 之外的**裸名**也必须代入——
// 不代入则 "amount" 与 "1000" 的字典序会把 `>` 与 `<=` 同时判真(两支一起"为真")。
let resolved = resolve_bare_names(resolved, exec)
// 比较运算(先长后短避免误切)
for op in [">=", "<=", "!=", "==", ">", "<"] {
match resolved.find(op) {
Some(pos) => {
let left = resolved[0:pos].trim(chars=" \t\r\n").to_owned()
let right = resolved[pos + op.length():].trim(chars=" \t\r\n").to_owned()
if op == "==" {
return match compare_values(left, right) { Some(0) => true; _ => false }
} else if op == "!=" {
return match compare_values(left, right) { Some(0) => false; _ => true }
}
// 排序运算(spec 06 §4.6 义务 2 ⚠):关系运算要求两侧都是数字,非数字操作数判 false——
// 字符串字典序不得漏进 > >= < <=(修前 compare_values 的字符串兜底+Some(2) 死臂
// 让 `>` 恒 false、`<=` 恒 true,正是 159 落错支的根因)。
let ordering = compare_numeric(left, right)
match ordering {
None => return false
Some(o) =>
if op == ">" {
return o > 0
} else if op == "<" {
return o < 0
} else if op == ">=" {
return o >= 0
} else {
return o <= 0
}
}
}
None => ()
}
}
// 布尔字面量
let lower = resolved.to_lower()
if lower == "true" {
true
} else if lower == "false" {
false
} else {
!resolved.is_empty() && resolved != "0" && resolved != "null"
}
}
///|
/// 排序运算专用:两侧都能解析成数值才返回序 Some(-1/0/1),任一非数字 ⇒ None
/// (spec 06 §4.6 义务 2 ⚠「非数字操作数判 false」的判定原料;==/!= 仍走 compare_values
/// 的字符串兜底,那两档 spec 不约束)。
fn compare_numeric(left : String, right : String) -> Int? {
let l = try {
Some(@string.parse_double(left))
} catch {
_ => None
}
let r = try {
Some(@string.parse_double(right))
} catch {
_ => None
}
match (l, r) {
(Some(lv), Some(rv)) =>
if lv < rv {
Some(-1)
} else if lv > rv {
Some(1)
} else {
Some(0)
}
_ => None
}
}
///|
/// 裸名代入(issues/159 · spec 06 §4.6 义务 2 第三条 ⚠「不得靠不代入变量蒙方向」):
/// `${var}`/`#var` 之外的裸标识符从 exec.args 查值代入(与 #var 同款 str/i64 双查)。
/// 查不到**原样保留**,交给排序运算的"非数字判 false"——缺失变量不得蒙 "0"
/// (蒙 0 会让缺失变量的 `<=` 边判真,重演 159 的落错支)。true/false/null 三个
/// 字面量与引号内的内容不代入。
fn resolve_bare_names(expr : String, exec : @exec.Execution) -> String {
let out = StringBuilder()
let chars = expr.to_array()
let len = chars.length()
let mut i = 0
while i < len {
let ch = chars[i]
// 引号内原样保留(等式档的字符串字面量)
if ch == '"' || ch == '\'' {
out.write_char(ch)
i = i + 1
while i < len && chars[i] != ch {
out.write_char(chars[i])
i = i + 1
}
if i < len {
out.write_char(ch)
i = i + 1
}
continue
}
let ci = ch.to_int()
let is_ident_start = (ci >= 97 && ci <= 122) || (ci >= 65 && ci <= 90) || ch == '_'
if is_ident_start {
let start = i
let mut end = i
while end < len {
let c = chars[end].to_int()
if (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || chars[end] == '_' {
end = end + 1
} else {
break
}
}
let name = expr[start:end].to_owned()
let lower = name.to_lower()
if lower == "true" || lower == "false" || lower == "null" {
for j in start.. Some(v)
None =>
match exec.args.get_i64(name) {
Some(n) => Some(n.to_string())
None => None
}
}
match value {
Some(v) => out.write_string(v)
None =>
// 查不到:原样保留(排序运算按非数字判 false;等式档按字面量比)
for j in start.. Int? {
// 两侧都能解析成数值才按数值比,任一失败 ⇒ 字符串兜底(旧语义)
// parse_double 是 `-> Double raise`:`try` 两臂必须同类型,所以成功臂自己包 Some
let l = try {
Some(@string.parse_double(left))
} catch {
_ => None
}
let r = try {
Some(@string.parse_double(right))
} catch {
_ => None
}
match (l, r) {
(Some(lv), Some(rv)) =>
if lv < rv {
Some(-1)
} else if lv > rv {
Some(1)
} else {
Some(0)
}
_ => Some(left.compare(right))
}
}
///|
/// ${var} 与 #var 引用替换(# 会签门控变量优先 gate_vars)
fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Engine::resolve_var_refs(self : Engine[R, E], expr : String, exec : @exec.Execution) -> String raise @error.JeeflowError {
let _ = self
let mut result = expr
// ${var}
let mut cont = true
while cont {
match result.find("$" + "{") {
Some(s) =>
match result[s:].find("}") {
Some(offset) => {
let var_name = result[s + 2:s + offset].to_owned()
// issues/159 连带:${} 形与 #var 同款 str/i64 双查——修前只 get_str,
// 数字档变量(宿主 JSON 数值)被代成字面 "0"("${amount} > 1000" 恒假)。
// 缺失档维持既有的 "0" 缺省(${}/# 是显式引用形,"0" 是本栈standing契约;
// 裸名形的缺失档判 false 走 resolve_bare_names,两形语义分开)。
let value = match exec.args.get_str(var_name) {
Some(v) => v
None =>
match exec.args.get_i64(var_name) {
Some(n) => n.to_string()
None => "0"
}
}
result = result[0:s].to_owned() + value + result[s + offset + 1:].to_owned()
}
None => cont = false
}
None => cont = false
}
}
// #var(会签门控变量)
let out = StringBuilder()
let mut i = 0
let chars = result.to_array()
let len = chars.length()
while i < len {
let ch = chars[i]
if ch == '#' && i + 1 < len && ((chars[i + 1].to_int() >= 97 && chars[i + 1].to_int() <= 122) || (chars[i + 1].to_int() >= 65 && chars[i + 1].to_int() <= 90) || chars[i + 1] == '_') {
let start = i + 1
let mut end = start
while end < len {
let c = chars[end].to_int()
if (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || chars[end] == '_' {
end = end + 1
} else {
break
}
}
let var_name = result[start:end].to_owned()
let value = match exec.gate_vars.get_str(var_name) {
Some(v) => Some(v)
None => exec.args.get_str(var_name)
}
let value = match value {
Some(v) => Some(v)
None =>
match exec.gate_vars.get_i64(var_name) {
Some(n) => Some(n.to_string())
None =>
match exec.args.get_i64(var_name) {
Some(n) => Some(n.to_string())
None => None
}
}
}
out.write_string(match value {
Some(v) => v
None => "0"
})
i = end
} else {
out.write_char(ch)
i = i + 1
}
}
out.to_string()
}
///|
/// 按档解析「本节点这一次该跑的拦截器」(audit8 R91,issues/234 §4.2/spec02 §3 三条)。
/// 基准=java `NodeModel.java:49-59`:节点 `properties` 声明优先,串空才回落流程根同名键,
/// 两档**不叠加**;两档都没声明才回落引擎级注册列表(=R91 之前的现状语义,向后兼容)。
/// 声明名一律查 `Ctx.named_interceptors`(键形按丙=java 类全名串),查不到 ⇒ 显式报错并带
/// 档位与 nodeId(issues/60 纪律:不静默跳过);不缓存错误 ⇒ 每节点每次真解析。
/// 同档多名的执行序=`order` 升序(与本栈注册列表同尺;java 按声明序反射实例化,那是它的
/// `order` 缺省形,本栈 `InterceptorFn` 自带 order ⇒ 以 order 为准)。
fn[R, E] Engine::resolve_interceptors(
self : Engine[R, E],
exec : @exec.Execution,
phase : String
) -> Array[@spi.InterceptorFn] raise @error.JeeflowError {
let is_pre = phase == "pre"
let prop = if is_pre { "preInterceptors" } else { "postInterceptors" }
match exec.process_model.interceptor_decl(exec.current_node, prop) {
None => if is_pre { self.ctx.pre_interceptors } else { self.ctx.interceptors }
Some(d) => {
let node_id = match exec.current_node {
Some(n) => n.id
None => "-"
}
let out : Array[@spi.InterceptorFn] = []
for piece in d.split(",") {
let name = piece.trim(chars=" \t\r\n").to_owned()
if name.is_empty() {
continue
}
match self.ctx.named_interceptor(name) {
Some(ic) => out.push(ic)
None => raise @error.Business(
"\{prop} 声明的拦截器未注册: \{name}(nodeId=\{node_id})")
}
}
out.sort_by(fn(a, b) { a.order.compare(b.order) })
out
}
}
}
///|
/// 前置拦截器派发(issues/116 批次 D 补的最小挂点;audit8 R91 起改走 `resolve_interceptors`)。
/// 历史:本函数是空实现 ⇒ `Ctx::register_interceptor` 注册的东西只在**后置**跑,
/// 建单期第三方无处可挂(issues/116 §3、issues/104 §P1)。
/// 未声明时按 order 遍历 `ctx.pre_interceptors` —— 一条**与后置数组分开**的通道:
/// `InterceptorFn` 只有一个 `run`(无 before/after 之分),复用后置数组会让
/// persist 的 PersistPostInterceptor 双触发(归档/SYNC 写两遍),故另起 pre 通道。
/// 委托自动生效不经本函数(时机对不上,见 `surrogate.mbt` 头部 ①②)。
async fn[R, E] Engine::fire_pre_interceptors(
self : Engine[R, E],
exec : @exec.Execution
) -> Unit raise @error.JeeflowError {
for interceptor in self.resolve_interceptors(exec, "pre") {
(interceptor.run)(exec)
}
}
///|
/// 后置拦截器派发(audit8 R91 起改走 `resolve_interceptors`;persist 的「声明才挂载」闸门
/// 与本案共用同一处读取,见 `persist/interceptor.mbt::persist_interceptor_declared`)。
async fn[R, E] Engine::fire_post_interceptors(self : Engine[R, E], exec : @exec.Execution) -> Unit raise @error.JeeflowError {
for interceptor in self.resolve_interceptors(exec, "post") {
(interceptor.run)(exec)
}
}
///|
/// 废弃同节点残留 DOING 会签任务(issues/94:merged 后不残留)
async fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Engine::abandon_countersign_remaining(self : Engine[R, E], exec : @exec.Execution, node_id : String) -> Unit raise @error.JeeflowError {
let abandoned : Array[@model.ProcessTask] = []
for task in exec.process_instance.tasks {
if task.task_name == node_id && task.task_state == @model.TaskState::Doing.code() {
task.task_state = @model.TaskState::Abandon.code()
abandoned.push(task.clone())
}
}
for t in abandoned {
(self.repo()).update_task(t)
}
}
///|
/// 聚合分配 ID(实例 + id=0 任务)
fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Engine::assign_ids(self : Engine[R, E], instance : @model.ProcessInstance) -> Unit raise @error.JeeflowError {
instance.instance_id = self.next_id()
for task in instance.tasks {
if task.task_id == 0L {
task.task_id = self.next_id()
task.process_instance_id = instance.instance_id
}
}
}
///|
/// 新任务落库(原片赋 id 后 fire TASK_START,issues/13 时机闭环)。
/// ⚠️ 本函数是**全引擎新任务落库的唯一收口**:7 处建任务路径(发起 / 带父实例发起 /
/// 办理推进 / 串行会签每一步 / 会签未合并提前返回 / 表达式会签 / 跳转含回退与退回发起人)
/// 都走它 —— issues/116 批次 D 的委托自动生效因此只挂这一处即覆盖全部路径。
/// 入参改收 `exec`(而非 instance+new_tasks)即为了让收口拿得到**流程模型 name**
/// (spec 06 §4.5 条款 1.1:委托查询以模型 name 为准,未带时回落 define.name)。
async fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Engine::persist_tasks(
self : Engine[R, E],
exec : @exec.Execution
) -> Unit raise @error.JeeflowError {
let instance = exec.process_instance
let process_name = self.surrogate_process_name(exec)
for task in exec.new_tasks {
if task.task_id == 0L {
task.task_id = self.next_id()
}
task.process_instance_id = instance.instance_id
// issues/121 P1 建单不变量:parent 与行级 isFirstTaskNode 必写。挂在这里是因为本函数
// 是全引擎新任务落库唯一收口(7 处建任务路径都汇入 exec.new_tasks)——与下面 116 的委托
// 并入口同型,一处即全覆盖。parent=产生这批新任务的那个刚办结任务;发起 execution 没有
// 当前任务 ⇒ 落 0L(对齐 mldong-boot2 Convert.toLong(execution.getProcessTaskId(), 0L))。
match task.parent_task_id {
None =>
task.parent_task_id = Some(
match exec.process_task {
Some(t) => t.task_id
None => 0L
},
)
_ => ()
}
// 出口现算版带「仅进行中」判定,已办结的历史行上恒 false,而血缘版回退要读那条历史行
// 决定参与者 ⇒ 判据必须建单时落库(沿用 parser 现成的 is_first_task_node,不新造算法)。
task.variables.insert(
"isFirstTaskNode",
@json.bool_of(exec.process_model.is_first_task_node(task.task_name)),
)
// 落库前并入被委托人到**参与者集合本身**(条款 2 ⚠️):下面 save_task + add_task_actor
// 带的就是含代理人的集合,不存在"事后补写打在空 taskId 上"的静默无效。
let _ = self.apply_surrogate(task, process_name)
(self.repo()).save_task(task)
if !task.actor_ids.is_empty() {
(self.repo()).add_task_actor(task.task_id, task.actor_ids)
}
// 载荷 instanceId/taskId/actors(§11.3 code 3)。⚠️ instanceId 是硬要求:本支 sourceId 是 taskId,
// 集成层 L2-30 的事件流水 sink 只能靠载荷里的 instanceId 把一条流的流水归到一个键下;
// 缺它则被拆成多个 taskId 键、顺序读不全(rust 栈同一个病)。
self.notify_task_start(instance.instance_id, task.task_id, task.actor_ids)
}
}
///|
/// 参与者集合的**形状适配器**(issues/56 E28 起为抄送腿而生,issues/142 B 批按 spec 06-facade
/// §2.11 逐字搬到任务侧):JSON 数组 / 逗号串 / 空三形态在这一层汇成元素序,随后一律过
/// `@model.normalize_actors` 那一枚判据 —— 空串/纯空白/数组里的空元素丢弃、**值取 trim 后的串**、
/// 同一次调用内的重复折叠;丢完为空 ⇒ 调用方按"没给"处理(建 cc 的三条入口不建行也不 fire 码 4,
/// 任务侧的 nextNodeOperator 腿则落回下一优先级)。这里**只切形状、不判空**(§2.11「不要再抄第二份」)。
///
/// 数组元素档(§2.11「不得被静默丢弃或串化成类型名」):
/// · 字符串 ⇒ 原样交给判据;
/// · 数字 ⇒ 收敛成字符串,**repr 原文优先**(>2^53 的雪花 id 不被 double 取整,D-M5-3),
/// 与别栈 `String.valueOf`/`str()`/`String()` 同形(rust 那腿"数字元素静默丢弃"是反面,不照抄);
/// · true/false ⇒ "true"/"false";
/// · `Json::Null` ⇒ 丢弃(本栈 Json 无 undefined 档,Null 就是 §2.11 说的 null/undefined 对应物,
/// 不许串化成 "null"/"None"/"");对象/嵌套数组等非标量 ⇒ 丢弃。
///
/// 抄送三支入口(发起 `f_ccActors`/办理 `tf_ccActors`/子流程发起腿)与任务侧
/// `f_nextNodeOperator`/`tf_nextNodeOperator` 共用这一支,两形同判据由构造成立。
pub fn parse_actors(v : Json?) -> Array[String] {
let out : Array[String] = []
match v {
None => ()
Some(Json::Array(items)) =>
for it in items {
match it {
Json::String(s) => out.push(s)
// id 字符串化走 repr 原文优先(>2^53 防 double 取整,D-M5-3)
Json::Number(n, repr~) =>
match repr {
Some(s) => out.push(s)
None => out.push(n.to_int64().to_string())
}
Json::True => out.push("true")
Json::False => out.push("false")
_ => ()
}
}
Some(Json::String(s)) => {
for part in s.split(",") {
out.push(part.to_owned())
}
}
Some(_) => ()
}
// 判据单点:空值丢弃 + trim + 同调用折叠只在 @model.normalize_actors 写一次
@model.normalize_actors(out)
}
///|
/// issues/141 G10 时这一支只挂抄送腿、旧名 `parse_cc_actors`;issues/142 B 批(spec §2.11)把它
/// 升格为通用的 `parse_actors`(任务侧 nextNodeOperator 共用同一枚),本名**保留为转发** ——
/// 已发布 mooncakes 模块(mldong/jeeflow-core,0.1.x 线)的公开成员,既有调用点(engine_ops 三处
/// 抄送入口)与既有测试不破;适配器本体不在这里复制。
pub fn parse_cc_actors(v : Json?) -> Array[String] {
parse_actors(v)
}