forked from ChaiHao/BPOS-MVP
三面拆分⑭:宽进收敛 —— 起单不卡缺项,进活动受理归一,信息不全派补全单
用户定的逻辑:「调用活动,输入数据可以不定或者为空;进入活动后受理归一,
信息不全派补全单。」把收敛点从门口挪进活动 —— 三道门口闸全撤:
· 触发装配:缺必填**不再入死信**(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:
@@ -11,6 +11,7 @@ import type {
|
||||
ActivityLoader,
|
||||
Clock,
|
||||
IdGenerator,
|
||||
IntakeDesk,
|
||||
InteractionPhaseSetter,
|
||||
LandingGateway,
|
||||
ListenerProbe,
|
||||
@@ -39,10 +40,31 @@ export type TaskServiceDeps = {
|
||||
readonly events: TaskEventPublisher;
|
||||
readonly notifier: ReassignNotifier;
|
||||
readonly interaction: InteractionPhaseSetter;
|
||||
/**
|
||||
* 受理补全台(2026-07-30 宽进):缺必填时把补全卡发到负责人的任务对话流,
|
||||
* 并武装 human 监听等答复 —— 任务先「等人」,补齐了才开跑。
|
||||
*/
|
||||
readonly intake: IntakeDesk;
|
||||
readonly clock: Clock;
|
||||
readonly ids: IdGenerator;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 缺了哪些必填 —— 键从 JSON-Schema 的 required 来,标签取 properties 里的 title / label,
|
||||
* 没有就用键名。补全卡要给人看的是「客户码」,不是 customerCode。
|
||||
*/
|
||||
function missingRequired(
|
||||
inputSchema: Record<string, unknown>,
|
||||
data: Record<string, unknown>,
|
||||
): Array<{ key: string; label: string }> {
|
||||
const required = Array.isArray(inputSchema['required']) ? (inputSchema['required'] as string[]) : [];
|
||||
const props = (inputSchema['properties'] ?? {}) as Record<string, { title?: string; label?: string }>;
|
||||
return required
|
||||
.filter((key) => data[key] === undefined || data[key] === null || data[key] === '')
|
||||
.map((key) => ({ key, label: props[key]?.title ?? props[key]?.label ?? key }));
|
||||
}
|
||||
|
||||
export function createTaskService(deps: TaskServiceDeps): TaskService {
|
||||
async function require(taskId: TaskId): Promise<Task> {
|
||||
const task = await deps.store.get(taskId);
|
||||
@@ -69,9 +91,13 @@ export function createTaskService(deps: TaskServiceDeps): TaskService {
|
||||
return {
|
||||
async start(input: StartTaskInput): Promise<Task> {
|
||||
const loaded = await deps.activities.load(input.activityId);
|
||||
// 输入 data 必过 schema 校验,**缺项不得启动**(AM 12.1)
|
||||
const check = deps.schemas.validate(loaded.inputSchema, input.data);
|
||||
if (!check.ok) throw new Error(`输入不合契约,不得启动:${check.errors.join('; ')}`);
|
||||
/*
|
||||
* 受理归一(2026-07-30 宽进,替代旧 AM 12.1 的「缺项不得启动」):
|
||||
* 输入**可以不定或为空** —— 缺必填不再拒单,而是任务先落成「等人」,
|
||||
* 把缺的字段做成补全卡发给负责人;补齐(或答「没法填」)后才开跑。
|
||||
* 收敛点从门口挪进活动:门口拒单,人连差什么都不知道;补全卡上写得明明白白。
|
||||
*/
|
||||
const missing = missingRequired(loaded.inputSchema, input.data);
|
||||
|
||||
const now = deps.clock.now();
|
||||
const task: Task = {
|
||||
@@ -82,7 +108,7 @@ export function createTaskService(deps: TaskServiceDeps): TaskService {
|
||||
...(input.nodeId === undefined ? {} : { nodeId: input.nodeId }),
|
||||
ownerId: input.ownerId,
|
||||
title: input.title,
|
||||
runtime: 'running',
|
||||
runtime: missing.length > 0 ? 'awaiting-human' : 'running',
|
||||
data: input.data,
|
||||
envelope: loaded.envelope,
|
||||
caps: loaded.caps,
|
||||
@@ -99,6 +125,15 @@ export function createTaskService(deps: TaskServiceDeps): TaskService {
|
||||
});
|
||||
await engineOf(task).load(task.id, loaded.businessModel);
|
||||
await deps.interaction.setPhase(task.id, 'active');
|
||||
if (missing.length > 0) {
|
||||
await deps.memory.append({
|
||||
taskId: task.id,
|
||||
kind: 'observation',
|
||||
title: `受理归一:缺 ${missing.map((m) => m.key).join('、')} —— 已派补全单`,
|
||||
detail: { missing: missing.map((m) => m.key) },
|
||||
});
|
||||
await deps.intake.askCompletion({ taskId: task.id, ownerId: task.ownerId, title: task.title, fields: missing });
|
||||
}
|
||||
await deps.store.markProgress(task.id, now);
|
||||
return task;
|
||||
},
|
||||
@@ -222,6 +257,52 @@ export function createTaskService(deps: TaskServiceDeps): TaskService {
|
||||
title: `事件到达:${event.kind}/${event.ref}`,
|
||||
detail: event.payload,
|
||||
});
|
||||
|
||||
/*
|
||||
* 受理补全的答复(宽进):任务还在「等人」且谱一步没走 —— 这时候来的 human 事件
|
||||
* 只可能是补全卡(谱没开跑,别的卡还发不出来)。答的值**只收契约声明过的键**,
|
||||
* 合并进 data;齐了(或人答了「没法填」)就转 running,交给推进器开跑。
|
||||
* 「没法填」不硬拦:人已把话说明,带着缺口进活动,收敛是活动的事。
|
||||
*/
|
||||
if (task.runtime === 'awaiting-human' && event.kind === 'human') {
|
||||
const loaded = await deps.activities.load(task.activityId);
|
||||
const stillMissing = missingRequired(loaded.inputSchema, task.data);
|
||||
if (stillMissing.length > 0) {
|
||||
const values = (event.payload['values'] ?? {}) as Record<string, unknown>;
|
||||
const declared = new Set(stillMissing.map((m) => m.key));
|
||||
const accepted = Object.fromEntries(
|
||||
Object.entries(values).filter(([k, v]) => declared.has(k) && typeof v === 'string' && v.trim() !== ''),
|
||||
);
|
||||
const data = { ...task.data, ...accepted };
|
||||
const remain = missingRequired(loaded.inputSchema, data);
|
||||
const gaveUp = typeof event.payload['reason'] === 'string' && event.payload['reason'] !== '';
|
||||
if (remain.length === 0 || gaveUp) {
|
||||
await deps.store.update(taskId, { data, runtime: 'running' });
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'observation',
|
||||
title: remain.length === 0 ? '受理归一:补全到齐,开跑' : `受理归一:${remain.map((m) => m.key).join('、')} 没法填 —— 带缺口开跑`,
|
||||
detail: { merged: Object.keys(accepted), remain: remain.map((m) => m.key) },
|
||||
});
|
||||
} else {
|
||||
await deps.store.update(taskId, { data });
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'observation',
|
||||
title: `受理归一:收到 ${Object.keys(accepted).join('、') || '(空)'},还缺 ${remain.map((m) => m.key).join('、')}`,
|
||||
detail: { merged: Object.keys(accepted), remain: remain.map((m) => m.key) },
|
||||
});
|
||||
await deps.intake.askCompletion({ taskId, ownerId: task.ownerId, title: task.title, fields: remain });
|
||||
// 还在等人 —— 不 tick:引擎一步都不该走,谱要等补全齐了才开
|
||||
await deps.store.markProgress(taskId, deps.clock.now());
|
||||
const held = await require(taskId);
|
||||
return { task: held, verdict: { kind: 'ok' }, progressed: false };
|
||||
}
|
||||
await deps.store.markProgress(taskId, deps.clock.now());
|
||||
return this.tick(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
await engineOf(task).onEvent(taskId, event);
|
||||
await deps.store.markProgress(taskId, deps.clock.now());
|
||||
return this.tick(taskId);
|
||||
|
||||
Reference in New Issue
Block a user