From 691c61b6b85461e6f1596f7ed5fb2be7ae2e9a38 Mon Sep 17 00:00:00 2001 From: 18628386721 <18628386721@163.com> Date: Mon, 27 Jul 2026 05:34:13 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=89=E6=8B=A9=E7=BB=84=E7=9A=84=E5=87=BA?= =?UTF-8?q?=E5=8F=A3=E9=80=89=E6=8B=A9=E8=A6=81=E8=B5=B0=E5=88=B0=E8=90=BD?= =?UTF-8?q?=E5=9C=B0:=E6=81=B0=E5=8F=91=E4=B8=80=E5=8F=A3,=E9=9A=8F?= =?UTF-8?q?=E6=94=BE=E8=A1=8C=E4=B8=80=E5=B9=B6=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A6 的输出是**选择组**(negotiation: won / revise / lost),回传协议里本来就有 `selectedPorts`,human-tool 也接住了 —— 但落地那一步硬写 `selectedPorts: {}`, 选择从未抵达。结果:放行了、台账写了,却「发射输出口:(无)」, G4 异或网关拿不到 outcome,整条流悄无声息地停住。 按 verdict 同样的路子把它串起来:人事件载荷 → 末步运行时 → 谱走完时报上去 → 落地收敛时照它发射。 gates: tsc / eslint / depcruise / 399 测试 全绿 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m --- .data/attachments/inbound/rfq-20260728A@x.com.eml | 12 ++++++++++++ apps/api/src/app/wiring/engines.ts | 3 ++- apps/api/src/modules/flow-engine/contract.ts | 2 ++ .../flow-engine/internal/domain/step-sequence.ts | 7 +++++++ .../flow-engine/internal/usecase/flow-runner.ts | 13 +++++++++++-- .../modules/task/internal/usecase/task-service.ts | 1 + apps/api/src/modules/task/port.ts | 10 +++++++++- 7 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 .data/attachments/inbound/rfq-20260728A@x.com.eml diff --git a/.data/attachments/inbound/rfq-20260728A@x.com.eml b/.data/attachments/inbound/rfq-20260728A@x.com.eml new file mode 100644 index 0000000..280147c --- /dev/null +++ b/.data/attachments/inbound/rfq-20260728A@x.com.eml @@ -0,0 +1,12 @@ +{ + "messageId": "rfq-20260728A@x.com", + "from": "buyer@x.com", + "to": [ + "rfq@we.com" + ], + "cc": [], + "subject": "【询价】客户 C210 RFQ-20260728A 求报价", + "body": "客户编码 C210,询价单号 RFQ-��\nPN-FULL-002 TI 年用量 9000 目标价 6.00 交期 10w", + "receivedAt": "2026-07-27T20:00:00.000Z", + "attachments": [] +} \ No newline at end of file diff --git a/apps/api/src/app/wiring/engines.ts b/apps/api/src/app/wiring/engines.ts index 540df99..50895af 100644 --- a/apps/api/src/app/wiring/engines.ts +++ b/apps/api/src/app/wiring/engines.ts @@ -414,6 +414,7 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring { ...(outcome.blockedReason === undefined ? {} : { reason: outcome.blockedReason }), // 谱走完时把放行裁决带上来 —— 完成只出自放行通过 ...(outcome.released === undefined ? {} : { released: outcome.released }), + ...(outcome.selectedPorts === undefined ? {} : { selectedPorts: outcome.selectedPorts }), }; }, onEvent: async (taskId, event) => { @@ -433,7 +434,7 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring { taskId: input.taskId, ownerId: input.ownerId, humanCallCap: input.humanCallCap, - selectedPorts: {}, + selectedPorts: input.selectedPorts, }); return { converged: outcome.verdict.kind === 'converged', diff --git a/apps/api/src/modules/flow-engine/contract.ts b/apps/api/src/modules/flow-engine/contract.ts index b449725..9b8424d 100644 --- a/apps/api/src/modules/flow-engine/contract.ts +++ b/apps/api/src/modules/flow-engine/contract.ts @@ -24,6 +24,8 @@ export type AdvanceOutcome = { readonly waitingFor?: { readonly kind: 'human' | 'event' | 'time'; readonly ref: string; readonly deadline: string }; /** finished 时:末步验收工单的裁决决定终态 */ readonly released?: boolean; + /** 选择组的出口选择:恰发一口,随放行一并确认 */ + readonly selectedPorts?: Record; readonly blockedReason?: string; }; diff --git a/apps/api/src/modules/flow-engine/internal/domain/step-sequence.ts b/apps/api/src/modules/flow-engine/internal/domain/step-sequence.ts index 18eadb5..78e8c86 100644 --- a/apps/api/src/modules/flow-engine/internal/domain/step-sequence.ts +++ b/apps/api/src/modules/flow-engine/internal/domain/step-sequence.ts @@ -19,6 +19,8 @@ export type StepRuntime = { readonly error?: string; /** 验收工单的放行裁决:pass / 驳回类别 —— 末步据此定终局 */ readonly verdict?: string; + /** 选择组的出口选择:恰发一口,随放行一并确认(AM 1.2) */ + readonly selectedPorts?: Record; }; export type FlowPosition = { @@ -149,3 +151,8 @@ export function lastVerdictPassed(position: FlowPosition): boolean { const last = position.steps[position.steps.length - 1]; return last?.verdict === 'pass'; } + +/** 末步随放行确认的出口选择 —— 落地收敛时按它发射 */ +export function lastSelectedPorts(position: FlowPosition): Record { + return position.steps[position.steps.length - 1]?.selectedPorts ?? {}; +} diff --git a/apps/api/src/modules/flow-engine/internal/usecase/flow-runner.ts b/apps/api/src/modules/flow-engine/internal/usecase/flow-runner.ts index 0fc9cd2..c5f527c 100644 --- a/apps/api/src/modules/flow-engine/internal/usecase/flow-runner.ts +++ b/apps/api/src/modules/flow-engine/internal/usecase/flow-runner.ts @@ -20,6 +20,7 @@ import type { FlowPosition } from '../domain/step-sequence.ts'; import { checkDeclaration, fallbackFields, + lastSelectedPorts, lastStepIsAcceptance, lastVerdictPassed, failureAction, @@ -257,7 +258,8 @@ export function createFlowRunner(deps: FlowRunnerDeps): FlowEngineService { // 末步是验收工单时,终态由其**裁决**给出 —— 完成只出自放行通过。 // 裁决记在末步的 runtime 上(resume 时落笔),谱走完在这里读出来。 const released = lastStepIsAcceptance(model.steps) && lastVerdictPassed(position); - return { position, status: 'finished', released }; + // 出口选择随放行一并确认,落地收敛时照它发射 + return { position, status: 'finished', released, selectedPorts: lastSelectedPorts(position) }; } case 'skip': { await deps.memory.append({ @@ -295,12 +297,19 @@ export function createFlowRunner(deps: FlowRunnerDeps): FlowEngineService { }); // 放行裁决落笔在这一步的运行时上:谱走完时据此定终局 const verdict = event.kind === 'human' ? String(event.payload['verdict'] ?? '') : ''; + const chosen = event.payload['selectedPorts']; + const selectedPorts = + typeof chosen === 'object' && chosen !== null ? (chosen as Record) : undefined; if (step !== undefined && event.kind === 'human') { // 人事件的载荷是**回传协议**的形状(`{mode, workOrderId, values}`), // 出参在 `values` 里;直接把整个载荷当出参写,落进去的会是协议壳而不是答案。 await writeOutput(taskId, step, replyValuesOf(event.payload)); } - const next = markStep(position, index, verdict === '' ? { state: 'done' } : { state: 'done', verdict }); + const next = markStep( + position, + index, + verdict === '' ? { state: 'done' } : { state: 'done', verdict, ...(selectedPorts === undefined ? {} : { selectedPorts }) }, + ); await deps.state.save(taskId, model, next); return { position: next, status: 'running' }; }, diff --git a/apps/api/src/modules/task/internal/usecase/task-service.ts b/apps/api/src/modules/task/internal/usecase/task-service.ts index 92cd68b..96a8b86 100644 --- a/apps/api/src/modules/task/internal/usecase/task-service.ts +++ b/apps/api/src/modules/task/internal/usecase/task-service.ts @@ -168,6 +168,7 @@ export function createTaskService(deps: TaskServiceDeps): TaskService { taskId, ownerId: task.ownerId, humanCallCap: task.caps.humanCalls, + selectedPorts: outcome.selectedPorts ?? {}, }); if (!landed.converged) { // 落地未收敛:输出口不得发射,任务受阻待人(落地失败卡已发) diff --git a/apps/api/src/modules/task/port.ts b/apps/api/src/modules/task/port.ts index 88fddf1..6785195 100644 --- a/apps/api/src/modules/task/port.ts +++ b/apps/api/src/modules/task/port.ts @@ -50,6 +50,8 @@ export type EngineOutcome = { readonly reason?: string; /** 谱走完时:放行是否通过 —— **完成只出自放行通过**,引擎不自行宣布完成 */ readonly released?: boolean; + /** 选择组的出口选择:恰发一口,随放行一并确认 */ + readonly selectedPorts?: Record; }; /** @@ -57,7 +59,13 @@ export type EngineOutcome = { * 放行通过的那一刻才落地:草稿合并入正式版、动作意向执行、收敛后才发射输出口。 */ export type LandingGateway = { - land(input: { taskId: TaskId; ownerId: StaffId; humanCallCap: number }): Promise<{ + land(input: { + taskId: TaskId; + ownerId: StaffId; + humanCallCap: number; + /** 选择组的出口选择 —— 恰发一口,由放行时的确认给定 */ + selectedPorts: Readonly>; + }): Promise<{ converged: boolean; emittedPorts: readonly string[]; ledgerRowsMerged: number;