Archived
人兜底也要问**行标识**:主键问不到就写不进台账
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -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": []
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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<Record<string, unknown>> = {}): 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)])];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user