M5(3/3): trigger —— 三类触发器与起单幂等

- matchesEvent:事件触发的命中判定(S-FEEL 规则;规则写坏按不命中处置,不让事件流停摆)
- assembleInput:字段映射装配;缺必填即起单失败入死信,不静默丢弃
- 幂等:同一事件对同一触发器至多起一单;数据日历型同一行同一日期只触发一次
- 周期两型:日历表达式到点扫描 + 数据日历型(日期字段 + 偏移 + 过滤)逐行起单
- 登记触发:人工起单的统一形态,登记人即来源
- 触发器与流程包平级:配置独立存放,增删启停不改流程包版本

340 个测试通过,门禁五道全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
JCAI Main Agent
2026-07-27 02:03:58 +08:00
co-authored by Claude Opus 5
parent a8f6eef610
commit 4ad901c452
16 changed files with 3768 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
/**
* module: trigger —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
*/
import type { TriggerConfig } from '@contracts';
export type Clock = {
now(): Date;
};
export type IdGenerator = {
next(): string;
};
export type TriggerStore = {
list(): Promise<TriggerConfig[]>;
get(id: string): Promise<TriggerConfig | undefined>;
upsert(config: TriggerConfig): Promise<TriggerConfig>;
setEnabled(id: string, enabled: boolean): Promise<void>;
remove(id: string): Promise<void>;
/** 幂等:同一键至多起一单 */
claim(key: string): Promise<boolean>;
lastFiredAt(id: string): Promise<Date | undefined>;
markFired(id: string, at: Date): Promise<void>;
};
/** 起单 —— app 注入 `process`(L7 同层?否:trigger 在 process 之上同层,经 app 装配) */
export type ProcessGateway = {
start(input: {
packageId: string;
subject: string;
source: {
kind: 'periodic' | 'event' | 'registration';
ref: string;
by?: string;
};
data: Record<string, unknown>;
}): Promise<{
id: string;
}>;
/** 调用接口的必填字段 —— 装配缺项即起单失败 */
requiredInputs(packageId: string): Promise<string[]>;
};
export type Sfeel = {
condition(expression: string, scope: Readonly<Record<string, unknown>>): boolean;
expression(expression: string, scope: Readonly<Record<string, unknown>>): unknown;
};
/** 死信 —— app 注入 `message`(L2):起单失败入死信 */
export type DeadLetterSink = {
report(input: {
triggerId: string;
reason: string;
detail: Record<string, unknown>;
}): Promise<void>;
};
/** 数据日历型要读台账 —— app 注入 `ledger`(L1) */
export type LedgerScanner = {
scan(input: {
domain: string;
object: string;
dateField: string;
filter?: string;
}): Promise<Array<{
key: Record<string, string | number>;
date: string;
row: Record<string, unknown>;
}>>;
};