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
83 lines
3.4 KiB
TypeScript
83 lines
3.4 KiB
TypeScript
/**
|
|
* 一封信 → 调用入参 —— 测试台起单前那两步的**接缝**。
|
|
*
|
|
* 两步各自有测试(channel 的抽键、trigger 的装配),但**串起来**是新的:
|
|
* 通道抽出来的键叫 `keys.customerCode`,触发器的映射也得照这个名字取 ——
|
|
* 中间任何一头改了字段名,单还是能起,只是入参悄悄变空,流走到 G1 才莫名其妙地停。
|
|
* 所以这条测试盯的就是这个名字对不对得上。
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
import { evaluate as evalSfeel } from '@sfeel';
|
|
import { extractFields } from '@modules/channel';
|
|
import { assembleInput, scopeOf } from '@modules/trigger';
|
|
import type { TriggerConfig } from '@modules/trigger';
|
|
|
|
/** 与种子里 `ch-rfq-mail` 同一套抽键规则 */
|
|
const EXTRACT = [
|
|
{ key: 'customerCode', regex: '\\bC\\d{3}\\b', source: 'both' as const },
|
|
{ key: 'rfqRef', regex: 'RFQ-[A-Z0-9]+(?:-[A-Z0-9]+)*', source: 'both' as const },
|
|
];
|
|
|
|
/** 与种子里「客户RFQ询价」同一套装配映射 */
|
|
const TRIGGER = {
|
|
id: 'trg-rfq-mail',
|
|
name: '客户RFQ询价',
|
|
processPackageId: 'unified-quote',
|
|
enabled: true,
|
|
spec: { kind: 'event', eventType: 'data', rule: 'true' },
|
|
mapping: [
|
|
{ source: { from: 'expression', sfeel: 'keys.customerCode' }, target: 'customerCode' },
|
|
{ source: { from: 'expression', sfeel: 'keys.rfqRef' }, target: 'rfqRef' },
|
|
{ source: { from: 'expression', sfeel: 'subject' }, target: 'subject' },
|
|
{ source: { from: 'expression', sfeel: 'body' }, target: 'body' },
|
|
],
|
|
} as TriggerConfig;
|
|
|
|
const evaluator = (expression: string, scope: Readonly<Record<string, unknown>>): unknown => {
|
|
try {
|
|
return evalSfeel(expression, scope);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
function assemble(subject: string, body = '正文'): ReturnType<typeof assembleInput> {
|
|
const keys = extractFields({ subject, body, from: 'buyer@x.com', to: ['rfq@we.com'] }, EXTRACT);
|
|
const scope = scopeOf({
|
|
type: 'data',
|
|
keys,
|
|
source: 'channel',
|
|
occurredAt: '2026-07-29T00:00:00.000Z',
|
|
payload: { subject, body, from: 'buyer@x.com', channelId: 'ch-rfq-mail' },
|
|
});
|
|
return assembleInput(TRIGGER, scope, ['customerCode', 'rfqRef'], evaluator);
|
|
}
|
|
|
|
describe('一封信 → 调用入参', () => {
|
|
it('主题里带客户码与询价单号:两个必填都装得上', () => {
|
|
const out = assemble('【询价】C014 / RFQ-2607-001 工控主板 BOM 12 项');
|
|
expect(out.kind).toBe('ready');
|
|
if (out.kind !== 'ready') return;
|
|
expect(out.data['customerCode']).toBe('C014');
|
|
expect(out.data['rfqRef']).toBe('RFQ-2607-001');
|
|
expect(out.data['subject']).toContain('工控主板');
|
|
});
|
|
|
|
it('正文里带也算 —— 抽键两处都看(source: both)', () => {
|
|
const out = assemble('询价', '客户 C014,单号 RFQ-2607-002,请报价');
|
|
expect(out.kind).toBe('ready');
|
|
if (out.kind !== 'ready') return;
|
|
expect(out.data['rfqRef']).toBe('RFQ-2607-002');
|
|
});
|
|
|
|
it('抽不全:如实列出缺哪个,但**带着已有的照样能起** —— 宽进,收敛在活动', () => {
|
|
const out = assemble('随便问问,有没有便宜的板子');
|
|
expect(out.kind).toBe('partial');
|
|
if (out.kind !== 'partial') return;
|
|
expect(out.fields).toEqual(expect.arrayContaining(['customerCode', 'rfqRef']));
|
|
// 缺的键不以 undefined 占位 —— 下游按「键在不在」判缺不缺
|
|
expect('customerCode' in out.data).toBe(false);
|
|
expect(out.data['subject'], '抽得到的照带').toBeDefined();
|
|
});
|
|
});
|