基线:鉴权会话+工作组+工作台三区修复+编排工作区+产品设计文档

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 20:43:35 +08:00
co-authored by Claude Fable 5
parent 61fe181678
commit ac85ecf2ea
119 changed files with 16634 additions and 1246 deletions
@@ -0,0 +1,68 @@
/**
* 触发器输入装配(OS 3.1)—— 三类触发的入参从哪来,这里是分界线。
*/
import { describe, expect, it } from 'vitest';
import type { TriggerConfig } from '@contracts';
import { triggerConfigSchema } from '@contracts';
import { assembleInput } from '../internal/domain/matching.ts';
const noExpr = () => undefined;
const registration = (mapping: TriggerConfig['mapping'] = []): TriggerConfig =>
triggerConfigSchema.parse({
id: 'trg-reg',
name: '人工登记起单',
processPackageId: 'unified-quote',
enabled: true,
spec: { kind: 'registration' },
mapping,
});
const event = (mapping: TriggerConfig['mapping'] = []): TriggerConfig =>
triggerConfigSchema.parse({
id: 'trg-evt',
name: '邮件到达起单',
processPackageId: 'unified-quote',
enabled: true,
spec: { kind: 'event', eventType: 'data', rule: 'true' },
mapping,
});
describe('assembleInput', () => {
it('登记触发:表单填的就是入参 —— 不声明 mapping 也能起单', () => {
// 登记表单由调用接口 schema 渲染,填出来的已经是合契约的形状,不必再装配
const r = assembleInput(registration(), { customerCode: 'C001', rfqRef: 'RFQ-1' }, ['customerCode', 'rfqRef'], noExpr);
expect(r).toEqual({ kind: 'ready', data: { customerCode: 'C001', rfqRef: 'RFQ-1' } });
});
it('登记触发:声明了 mapping 就覆盖在表单之上', () => {
const r = assembleInput(
registration([{ target: 'customerCode', source: { from: 'constant', value: 'C999' } }]),
{ customerCode: 'C001', rfqRef: 'RFQ-1' },
['customerCode'],
noExpr,
);
expect(r.kind === 'ready' && r.data['customerCode']).toBe('C999');
});
it('事件触发:scope 不打底 —— 事件载荷与调用接口是两套字段名', () => {
// 不声明 mapping 就装配不出必填字段,应入死信而不是把事件载荷整个塞给流程包
const r = assembleInput(event(), { subject: '询价', customerCode: 'C001' }, ['customerCode'], noExpr);
expect(r).toEqual({ kind: 'missing', fields: ['customerCode'] });
});
it('事件触发:mapping 声明了才装配得出来', () => {
const r = assembleInput(
event([{ target: 'customerCode', source: { from: 'initiation', path: 'customerCode' } }]),
{ customerCode: 'C001' },
['customerCode'],
noExpr,
);
expect(r).toEqual({ kind: 'ready', data: { customerCode: 'C001' } });
});
it('缺必填即 missing,列出缺哪几个', () => {
const r = assembleInput(registration(), { rfqRef: 'RFQ-1' }, ['customerCode', 'rfqRef'], noExpr);
expect(r).toEqual({ kind: 'missing', fields: ['customerCode'] });
});
});
@@ -50,7 +50,15 @@ export function assembleInput(
required: readonly string[],
evaluateExpression: ExpressionEvaluator,
): AssemblyResult {
const data: Record<string, unknown> = {};
/*
* 登记触发的入参**就是登记人填的表单**(OS 3.1:「按调用接口 schema 填写输入,
* 表单由 schema 自动渲染」)—— 那份表单已按调用接口成形,不需要再装配一遍。
* 所以这里以 scope 打底,mapping(若声明了)再覆盖其上。
*
* 事件 / 周期触发不打底:它们的 scope 是事件载荷,与调用接口是两套字段名,
* 直接铺进去会把无关字段塞给流程包 —— 那正是 mapping 存在的理由。
*/
const data: Record<string, unknown> = config.spec.kind === 'registration' ? { ...scope } : {};
for (const mapping of config.mapping) {
switch (mapping.source.from) {
case 'constant':
@@ -86,7 +86,13 @@ export function createTriggerService(deps: TriggerServiceDeps): TriggerService {
async fireByRegistration(triggerId, by, data) {
const config = await deps.store.get(triggerId);
if (config === undefined || !config.enabled) return { triggerId, skipped: 'disabled' };
return fire(config, { kind: 'registration', ref: `registration:${by}`, by }, data, config.name);
/*
* 主题优先取登记人填的 `subject` —— 拿触发器名当主题的话,
* 同一个登记入口起的所有单主题全一样,清单上分不出哪条是哪条。
*/
const filled = data['subject'];
const subject = typeof filled === 'string' && filled.trim() !== '' ? filled.trim() : config.name;
return fire(config, { kind: 'registration', ref: `registration:${by}`, by }, data, subject);
},
/** 周期扫描:日历型按表达式到点;数据日历型逐行起单 */