Archived
三面拆分⑭:宽进收敛 —— 起单不卡缺项,进活动受理归一,信息不全派补全单
用户定的逻辑:「调用活动,输入数据可以不定或者为空;进入活动后受理归一,
信息不全派补全单。」把收敛点从门口挪进活动 —— 三道门口闸全撤:
· 触发装配:缺必填**不再入死信**(AssemblyResult 的 missing 改 partial,带上已装配的);
装到多少带多少照样起单。死信只留给真错误(勾了确认没指定人这类配置错误)。
· process.start:撤掉调用接口缺项闸 —— 从前缺一个字段整条流都起不来,
现实是客户来信本来就常缺客户码,拦在门外等于要求外界替系统把信息备齐。
· task.start(替代旧 AM 12.1「缺项不得启动」):缺必填的任务落成「等人」,
缺的字段做成**补全卡**(elicitation,标签取 schema 的 title)发到负责人的
任务对话流,并武装 human 监听(IntakeDesk 端口;不占活动的人卡预算)。
答复只收契约声明过的键,合并进 data;齐了或答「没法填」才转 running 开跑;
答一半就就缺的部分再派一张卡。等人期间谱一步不走、不扣步数。
· 测试台预检改成**信息**不是闸:灯灭不拦起单,文案直说
「抽不出的键由受理补全单找人收齐」。
5 条受理归一单测 + 全链实测:投一封只写「想问下工控板价格」的信(零键)→
流照起 → A1 落负责人工位、等人、补全卡在对话流 → 答卡(customerCode/rfqRef)→
任务醒、合并、谱跑 3 步到验收。修一处接线错误:监听 deadline 要 ISO-8601
时长(P2D)不是时刻,传错整个补全监听静默没装上。
trigger/task/process 三份 CONTRACT.md 与产品设计.md 同步(宽进纪律成文)。
五闸绿;契约快照随本提交入库。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -29,6 +29,8 @@
|
||||
- **只负责起单**:经调用接口调起流程包;流程包与活动内部不含任何触发逻辑
|
||||
- **同一事件流,不另设通路**:`match` 供 event 的三级消费判定使用
|
||||
- **同一事件对同一触发器至多起一单**:以事件 ID 为幂等键
|
||||
- **缺必填即起单失败入死信**,不静默丢弃
|
||||
- **宽进**(2026-07-30,用户定,替代旧「缺必填即入死信」):缺必填**照样起单** ——
|
||||
装到多少带多少(`AssemblyResult.partial`);收敛点在活动:受理归一、信息不全派补全单。
|
||||
死信只留给真错误(勾了确认没指定人、包无生效版这类配置错误)
|
||||
- **数据日历型同一行同一日期只触发一次**(键含日期值);字段值变更后可再触发
|
||||
- 命中规则求值失败按**不命中**处置 —— 规则写坏了不让事件流停摆
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('assembleInput', () => {
|
||||
it('事件触发:scope 不打底 —— 事件载荷与调用接口是两套字段名', () => {
|
||||
// 不声明 mapping 就装配不出必填字段,应入死信而不是把事件载荷整个塞给流程包
|
||||
const r = assembleInput(event(), { subject: '询价', customerCode: 'C001' }, ['customerCode'], noExpr);
|
||||
expect(r).toEqual({ kind: 'missing', fields: ['customerCode'] });
|
||||
expect(r).toMatchObject({ kind: 'partial', fields: ['customerCode'] });
|
||||
});
|
||||
|
||||
it('事件触发:mapping 声明了才装配得出来', () => {
|
||||
@@ -61,8 +61,8 @@ describe('assembleInput', () => {
|
||||
expect(r).toEqual({ kind: 'ready', data: { customerCode: 'C001' } });
|
||||
});
|
||||
|
||||
it('缺必填即 missing,列出缺哪几个', () => {
|
||||
it('缺必填即 partial,列出缺哪几个 —— 但已装上的照带', () => {
|
||||
const r = assembleInput(registration(), { rfqRef: 'RFQ-1' }, ['customerCode', 'rfqRef'], noExpr);
|
||||
expect(r).toEqual({ kind: 'missing', fields: ['customerCode'] });
|
||||
expect(r).toMatchObject({ kind: 'partial', fields: ['customerCode'] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('matchesEvent —— 流程层的系统级事件监听', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('assembleInput —— 缺必填即起单失败入死信', () => {
|
||||
describe('assembleInput —— 缺必填即 partial —— 宽进,收敛在活动', () => {
|
||||
const evalExpr = (expr: string, s: Readonly<Record<string, unknown>>): unknown => evalSfeel(expr, s);
|
||||
|
||||
it('必填齐备即 ready', () => {
|
||||
@@ -65,9 +65,9 @@ describe('assembleInput —— 缺必填即起单失败入死信', () => {
|
||||
expect(result).toEqual({ kind: 'ready', data: { customerCode: 'C001', channel: 'mail' } });
|
||||
});
|
||||
|
||||
it('缺必填即 missing,并指出缺谁', () => {
|
||||
it('缺必填即 partial,并指出缺谁', () => {
|
||||
const result = assembleInput(eventTrigger(), scopeOf({ ...scope, keys: {} }), ['customerCode'], evalExpr);
|
||||
expect(result).toEqual({ kind: 'missing', fields: ['customerCode'] });
|
||||
expect(result).toMatchObject({ kind: 'partial', fields: ['customerCode'] });
|
||||
});
|
||||
|
||||
it('常量映射不依赖事件', () => {
|
||||
|
||||
@@ -41,8 +41,11 @@ export function matchesEvent(config: TriggerConfig, scope: EventScope, evaluate:
|
||||
|
||||
export type AssemblyResult =
|
||||
| { readonly kind: 'ready'; readonly data: Record<string, unknown> }
|
||||
/** 缺必填字段则**起单失败、入死信** */
|
||||
| { readonly kind: 'missing'; readonly fields: readonly string[] };
|
||||
/**
|
||||
* 缺必填 —— **不再是终局**(2026-07-30 宽进):装到多少带多少,缺的字段列出来。
|
||||
* 收敛点在活动:受理归一、信息不全派补全单;门口只装配,不裁决。
|
||||
*/
|
||||
| { readonly kind: 'partial'; readonly data: Record<string, unknown>; readonly fields: readonly string[] };
|
||||
|
||||
export function assembleInput(
|
||||
config: TriggerConfig,
|
||||
@@ -75,7 +78,9 @@ export function assembleInput(
|
||||
}
|
||||
}
|
||||
const missing = required.filter((field) => data[field] === undefined || data[field] === null);
|
||||
return missing.length > 0 ? { kind: 'missing', fields: missing } : { kind: 'ready', data };
|
||||
// 表达式没算出来的键别以 undefined 占位 —— 下游按「键在不在」判缺不缺
|
||||
for (const key of Object.keys(data)) if (data[key] === undefined || data[key] === null) delete data[key];
|
||||
return missing.length > 0 ? { kind: 'partial', data, fields: missing } : { kind: 'ready', data };
|
||||
}
|
||||
|
||||
function pick(scope: Readonly<Record<string, unknown>>, path: string): unknown {
|
||||
|
||||
@@ -35,14 +35,11 @@ export function createTriggerService(deps: TriggerServiceDeps): TriggerService {
|
||||
): Promise<FireOutcome> {
|
||||
const required = await deps.process.requiredInputs(config.processPackageId);
|
||||
const assembled = assembleInput(config, scope, required, deps.sfeel.expression);
|
||||
if (assembled.kind === 'missing') {
|
||||
await deps.deadLetters.report({
|
||||
triggerId: config.id,
|
||||
reason: `起单失败:缺必填字段 ${assembled.fields.join('、')}`,
|
||||
detail: { scope },
|
||||
});
|
||||
return { triggerId: config.id, skipped: 'input-missing', missing: assembled.fields };
|
||||
}
|
||||
/*
|
||||
* 宽进(2026-07-30,用户定):**缺必填不再入死信** —— 装到多少带多少照样起单。
|
||||
* 收敛点在活动:进活动后受理归一,信息不全由任务侧派补全单收齐。
|
||||
* 门口只负责把流起起来;把单卡死在门外,人连「差什么」都要翻死信才知道。
|
||||
*/
|
||||
/*
|
||||
* 触发确认链路(OS 用户口述五步的第 4/5 步,2026-07-30):
|
||||
* 勾了「需人工确认」,命中**不等于起单** —— 装配好的入参存成待确认单,
|
||||
|
||||
Reference in New Issue
Block a user