01问题:审批逻辑与业务代码的耦合
审批流的常规实现方式是把判定条件直接写进业务函数:设备价值超过阈值时创建审批单并中断写入。
// 反模式:审批逻辑侵入业务代码
async function createEquipment(data) {
if (data.value > 100000) {
await createApprovalRequest(data);
return { status: "pending_approval" };
}
await insertEquipment(data);
return { status: "created" };
}
这一写法带来三项代价,且三项都随系统规模放大。
- 规则变更需要改业务代码。阈值调整、审批层级增加,均落在业务函数的修改范围内。
- 审批检查被重复实现。设备、采购、工单各有写入入口,各自写一遍判定。
- 插件启停需要改业务代码。停用审批流意味着回退业务函数的实现。
02拦截模式:业务代码不涉及审批
EAMX 把审批从业务函数中移出:业务代码只负责写入与返回结果,审批插件通过 lifecycleHandler 注册到业务事件上完成门槛判定。
// equipment 业务代码——不涉及审批
async function createEquipment(data, ctx) {
const result = await db.insert(equipmentsTable).values(data);
return { id: result.id, status: "created" };
}
// approval 插件——独立于业务代码
lifecycleHandler("equipment:beforeCreate", async ({ payload, ctx, abort }) => {
if (payload.value > 100000) {
const needsApproval = await checkApprovalGate(ctx, {
entityType: "Equipment", threshold: 100000,
});
if (needsApproval) {
await createApprovalRequest(ctx, payload);
abort("设备价值超过阈值,已提交审批");
}
}
});
业务代码对审批的存在没有感知;调用 abort 时由事件总线中断主流程,并返回原因。
03emit 与 intercept 两种模式
事件总线提供两种订阅方式,差别在失败处理与数据修改权限上。
表 1 · emit(观察)与 intercept(拦截)的差别
| 属性 | emit(观察) | intercept(拦截) |
| 执行方式 | 并行,Promise.allSettled | 串行,for-of |
| 失败处理 | 记录日志,不中断主流程 | 抛出 AbortError,中断主流程 |
| 数据修改 | 不可修改 payload | 经 ctx.intercept(modified) 修改 payload |
| 典型用例 | 通知、日志、缓存失效 | 审批、校验、权限注入 |
依据:packages/core/src/lifecycle-bus.ts 的双模式实现,共 79 行。
串行是拦截模式的必要条件:多个拦截器按注册顺序执行,任一环节终止则后续环节不再执行。
04审批单的状态流转
门槛判定通过后创建审批单,主流程中断;审批单的流转由批准与驳回两个接口触发,结果以事件形式广播。
equipment:beforeCreate → 门槛判定 → 需要审批
├─ 创建审批单 → abort(中断主流程)→ approval:created → 通知主管
├─ POST /api/approval/:id/approve → approval:approved → 重新执行 createEquipment
└─ POST /api/approval/:id/reject → approval:rejected → 通知申请人
表 2 · 审批单状态与后续去向
| 状态 | 进入条件 | 后续去向 |
| 待审批 | 门槛判定通过,审批单已创建 | 批准或驳回 |
| 已通过 | 批准接口返回成功 | 订阅 approval:approved 的业务插件重新执行原动作 |
| 已驳回 | 驳回接口返回成功 | 审批单保留原参数与意见,申请人修改后重提 |
驳回不删除审批单;重新提交时门槛判定再次执行,审批路径按新参数重新确定。
审批人亦可改派其他审批人复核,改派记录保留在审批单上。
05与统一执行管道的对齐
审批插件的拦截点挂在业务事件上,写操作本身仍在统一执行管道内完成:参数校验、权限裁决、预演、人工确认、幂等键与审计。
高危动作在动作定义上标注 confirm,未携带确认一律拒绝执行。
这条判定与调用通道无关:Web 控制台、内嵌 AI 助手与经 MCP 接入的外部 Agent 走同一条管道。
因此「Agent 不绕过审批」并非一项约定,而是结构性的结果:它没有可以绕过的路径。
06插件注册:一份清单与零侵入
审批插件以一份清单完成注册,包含依赖、权限、路由前缀与拦截点;启用或停用审批流即启用或停用这份清单。
export const approvalPlugin: PluginManifest = {
id: "approval", name: "审批流", tier: 1,
dependencies: ["auth"],
permissions: [
{ action: "approve", subject: "WorkOrder" },
{ action: "approve", subject: "PurchaseRequest" },
],
routePrefix: "/api/approval",
createRouter: () => { /* CRUD routes */ },
lifecycleHandlers: {
"equipment:beforeCreate": equipmentCreateGate,
"purchase:beforeSubmit": purchaseSubmitGate,
},
};
07核心文件与边界
表 3 · 核心文件
| 文件 | 行数 | 职责 |
packages/core/src/lifecycle-bus.ts | 79 | emit / intercept 双模式事件总线 |
packages/core/src/types.ts | — | LifecycleEvent 类型定义 |
其一,审批仍由人完成。引擎负责判定与流转,不承诺免于人工审批。
其二,规则变更落在配置上。适用实体与阈值由 checkApprovalGate 的入参决定。
其三,拦截器需可重复执行。拦截点内的判定会被多次触发,拦截器应在终止主流程之前完成审批单的创建。
08作者与依据
作者
EAMX 产品团队 · 产品与解决方案
依据
packages/core/src/lifecycle-bus.ts 与审批插件清单的实现记录
数据来源
事件总线双模式实现(79 行);审批门槛判定与插件注册代码
引用标准
ISO 55000 系列仅为方法论依据,不构成认证;认证对象是组织,而非软件
更新日期
2026-09-28