From 3af6252394c07a3ee14a0a5d1d1579fbc91dd0b9 Mon Sep 17 00:00:00 2001 From: 18628386721 <18628386721@163.com> Date: Mon, 27 Jul 2026 05:28:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=BA=E5=85=9C=E5=BA=95=E4=B9=9F=E8=A6=81?= =?UTF-8?q?=E9=97=AE**=E8=A1=8C=E6=A0=87=E8=AF=86**:=E4=B8=BB=E9=94=AE?= =?UTF-8?q?=E9=97=AE=E4=B8=8D=E5=88=B0=E5=B0=B1=E5=86=99=E4=B8=8D=E8=BF=9B?= =?UTF-8?q?=E5=8F=B0=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A6「生成正式报价单」转人兜底时只问了出参的值(unitPrice / leadTime), 没问报价单号 —— 而 quote_no 是 formal_quote 的主键之一。结果人填完照样落不了地: 放行合并时被非空约束挡下(实测:null value in column "quote_no")。 `fallbackFields` 改为**键与值一起问**:出参声明里的主键路径,凡作用域里没有的都要问; 上游已给过的不再重复问。与前面「出参契约属于步、不属于执行者」是同一条: 这一步该产出的**行**和**值**,换谁来做都得齐。 gates: tsc / eslint / depcruise / 399 测试 全绿 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m --- .../inbound/rfq-20260727Z@x.com.eml | 12 ++++++++++ .../__tests__/step-sequence.test.ts | 22 +++++++++++++++++++ .../internal/domain/step-sequence.ts | 8 +++++-- .../internal/usecase/flow-runner.ts | 2 +- 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 .data/attachments/inbound/rfq-20260727Z@x.com.eml diff --git a/.data/attachments/inbound/rfq-20260727Z@x.com.eml b/.data/attachments/inbound/rfq-20260727Z@x.com.eml new file mode 100644 index 0000000..96fa7b8 --- /dev/null +++ b/.data/attachments/inbound/rfq-20260727Z@x.com.eml @@ -0,0 +1,12 @@ +{ + "messageId": "rfq-20260727Z@x.com", + "from": "buyer@x.com", + "to": [ + "rfq@we.com" + ], + "cc": [], + "subject": "【询价】客户 C200 RFQ-20260727Z 求报价", + "body": "客户编码 C200,询价单号 RFQ-��\nPN-FULL-001 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/modules/flow-engine/__tests__/step-sequence.test.ts b/apps/api/src/modules/flow-engine/__tests__/step-sequence.test.ts index 2ca9e76..3ca5b2b 100644 --- a/apps/api/src/modules/flow-engine/__tests__/step-sequence.test.ts +++ b/apps/api/src/modules/flow-engine/__tests__/step-sequence.test.ts @@ -32,6 +32,28 @@ describe('人兜底:出参契约属于步,不属于执行者', () => { expect(fallbackFields(llmStep())).toEqual(['pn', 'brand', 'eau']); }); + it('行标识也要问 —— 主键取不到就写不进台账', () => { + const step = flowStepSchema.parse({ + kind: 'llm', + name: 'S1', + label: '生成正式报价单', + input: {}, + model: 'm', + template: 't', + outputSchema: { type: 'object' }, + output: { + domain: 'q', + object: 'formal_quote', + key: { quote_no: 'quoteNo', pn: 'pn' }, + fields: { unit_price: 'unitPrice' }, + }, + failure: { retry: { times: 0, backoff: 'PT1S' }, fallback: { kind: 'human', workOrder: 'execution' } }, + }); + // pn 上游已给,不必再问;quoteNo 没人给过,必须问 + expect(fallbackFields(step, { pn: 'LM358DR' })).toEqual(['quoteNo', 'unitPrice']); + expect(fallbackFields(step, {})).toEqual(['quoteNo', 'pn', 'unitPrice']); + }); + it('无出参声明的步不凭空造字段', () => { const step = flowStepSchema.parse({ kind: 'llm', 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 bef86ae..18eadb5 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 @@ -133,8 +133,12 @@ export function parallelGroupComplete(position: FlowPosition, members: readonly * * 返回键名数组,与 human 步的 `card.fields` 同形:卡的渲染只有一套。 */ -export function fallbackFields(step: FlowStep): readonly string[] { - return step.output === undefined ? [] : Object.values(step.output.fields); +export function fallbackFields(step: FlowStep, scope: Readonly> = {}): readonly string[] { + if (step.output === undefined) return []; + // 出参的值要问,**行标识**也要问 —— 主键取不到就写不进台账(非空约束会在合并时挡下)。 + // 作用域里已经有的键不必再问一遍:上游给过就是给过了。 + const keys = Object.values(step.output.key).filter((path) => scope[path] === undefined); + return [...new Set([...keys, ...Object.values(step.output.fields)])]; } /** 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 6610196..0fc9cd2 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 @@ -210,7 +210,7 @@ export function createFlowRunner(deps: FlowRunnerDeps): FlowEngineService { reason: error, // 兜底的人要交的东西,和这一步**本来该产出的东西**同形: // 出参契约是步的契约,不因换谁来做而变(统一执行契约)。 - fields: fallbackFields(step), + fields: fallbackFields(step, await deps.data.view(taskId)), }, }); await deps.listeners.register({