From c17ccaf049fdb3e0bd8c61820ad325c3e6246984 Mon Sep 17 00:00:00 2001 From: 18628386721 <18628386721@163.com> Date: Mon, 27 Jul 2026 05:14:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A4=E6=8D=AE=20=E2=91=A5=20=E8=B7=91?= =?UTF-8?q?=E9=80=9A:A4=20=E6=88=90=E6=9C=AC=E5=AE=9A=E4=BB=B7=E5=85=A8?= =?UTF-8?q?=E9=93=BE=20=E2=80=94=E2=80=94=20=E8=A1=A5=E9=BD=90=E5=8F=96?= =?UTF-8?q?=E4=BB=B7=E5=9F=BA=E5=87=86=E8=BF=99=E4=B8=80=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A4 卡在 S3 底价自检要 `deviation`,而 S1「取价基准」调的是历史价工具, 那个工具**根本不回价格**:声明说出 deviation,工具给不出,于是永远缺一环。 按业务本意补齐: · `crm.history-price-read` 增回 `lastPrice`(上次成交单价)—— 一个不回价的 「历史价工具」取不出价格基准;无历史的新料号回 0,由下游按「无基准」处置, 不凭空编一个偏离率。 · 新增纯工具 `quote-pricing.basis`:基准价 ↔ 拟报价 → 偏离率。 偏离率要等 S2 定出拟报价才算得出来,所以落在 S2 之后(新步 S2B)。 · A4 声明:S1 改落 `last_price`,新增 S2B,台账加 `last_price` 列。 顺带修:样例集一份喂所有工具,`quote-pricing.basis` 的必填 `quotedPrice` 不在其中, 新工具过不了两审 → 活动包安装被拒 → 旧资产继续生效,现象是「改了没反应」。 样例改为各家必填的并集。 现在 A4 跑完整条:取价基准 → DMN 定价(利润模型 P-01)→ 算偏离率 → DMN 底价自检(底价门禁 P-02,处置=直接放行)→ 条件跳步 → 报价草案验收放行。 gates: tsc / eslint / depcruise / 397 测试 全绿 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m --- .../inbound/rfq-20260727T@x.com.eml | 12 ++++++++ .../inbound/rfq-20260727U@x.com.eml | 12 ++++++++ .../inbound/rfq-20260727V@x.com.eml | 12 ++++++++ .../inbound/rfq-20260727W@x.com.eml | 12 ++++++++ activities/quote-pricing/src/manifest.ts | 19 ++++++++++--- apps/api/src/app/seed-quote.ts | 20 ++++++++++++- apps/api/src/app/tools/first-party.ts | 28 +++++++++++++++---- 7 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 .data/attachments/inbound/rfq-20260727T@x.com.eml create mode 100644 .data/attachments/inbound/rfq-20260727U@x.com.eml create mode 100644 .data/attachments/inbound/rfq-20260727V@x.com.eml create mode 100644 .data/attachments/inbound/rfq-20260727W@x.com.eml diff --git a/.data/attachments/inbound/rfq-20260727T@x.com.eml b/.data/attachments/inbound/rfq-20260727T@x.com.eml new file mode 100644 index 0000000..88463c0 --- /dev/null +++ b/.data/attachments/inbound/rfq-20260727T@x.com.eml @@ -0,0 +1,12 @@ +{ + "messageId": "rfq-20260727T@x.com", + "from": "buyer@x.com", + "to": [ + "rfq@we.com" + ], + "cc": [], + "subject": "【询价】客户 C060 RFQ-20260727T 求报价", + "body": "客户编码 C060,询价单号 RFQ-��\nPN-AAA-111 TI 年用量 9000 目标价 6.00 交期 10w", + "receivedAt": "2026-07-27T19:00:00.000Z", + "attachments": [] +} \ No newline at end of file diff --git a/.data/attachments/inbound/rfq-20260727U@x.com.eml b/.data/attachments/inbound/rfq-20260727U@x.com.eml new file mode 100644 index 0000000..de67604 --- /dev/null +++ b/.data/attachments/inbound/rfq-20260727U@x.com.eml @@ -0,0 +1,12 @@ +{ + "messageId": "rfq-20260727U@x.com", + "from": "buyer@x.com", + "to": [ + "rfq@we.com" + ], + "cc": [], + "subject": "【询价】客户 C070 RFQ-20260727U 求报价", + "body": "客户编码 C070,询价单号 RFQ-��\nPN-BBB-222 TI 年用量 9000 目标价 6.00 交期 10w", + "receivedAt": "2026-07-27T19:00:00.000Z", + "attachments": [] +} \ No newline at end of file diff --git a/.data/attachments/inbound/rfq-20260727V@x.com.eml b/.data/attachments/inbound/rfq-20260727V@x.com.eml new file mode 100644 index 0000000..02bbfe3 --- /dev/null +++ b/.data/attachments/inbound/rfq-20260727V@x.com.eml @@ -0,0 +1,12 @@ +{ + "messageId": "rfq-20260727V@x.com", + "from": "buyer@x.com", + "to": [ + "rfq@we.com" + ], + "cc": [], + "subject": "【询价】客户 C080 RFQ-20260727V 求报价", + "body": "客户编码 C080,询价单号 RFQ-��\nPN-CCC-333 TI 年用量 9000 目标价 6.00 交期 10w", + "receivedAt": "2026-07-27T19:00:00.000Z", + "attachments": [] +} \ No newline at end of file diff --git a/.data/attachments/inbound/rfq-20260727W@x.com.eml b/.data/attachments/inbound/rfq-20260727W@x.com.eml new file mode 100644 index 0000000..631b7b4 --- /dev/null +++ b/.data/attachments/inbound/rfq-20260727W@x.com.eml @@ -0,0 +1,12 @@ +{ + "messageId": "rfq-20260727W@x.com", + "from": "buyer@x.com", + "to": [ + "rfq@we.com" + ], + "cc": [], + "subject": "【询价】客户 C090 RFQ-20260727W 求报价", + "body": "客户编码 C090,询价单号 RFQ-��\nPN-DDD-444 TI 年用量 9000 目标价 6.00 交期 10w", + "receivedAt": "2026-07-27T19:00:00.000Z", + "attachments": [] +} \ No newline at end of file diff --git a/activities/quote-pricing/src/manifest.ts b/activities/quote-pricing/src/manifest.ts index 5a2d7db..ef7b54a 100644 --- a/activities/quote-pricing/src/manifest.ts +++ b/activities/quote-pricing/src/manifest.ts @@ -59,7 +59,7 @@ export const manifest: ActivityManifest = { identity: { activityId: ACTIVITY_ID, name: 'A4 · 成本定价', - packageVersion: '0.1.1', + packageVersion: '0.1.2', category: '统一报价', paradigm: 'flow', sdkVersion: '0.1.0-m5', @@ -113,7 +113,7 @@ export const manifest: ActivityManifest = { humanCallBudget: 2, }, resources: { - tools: [{ name: 'crm.history-price-read' }], + tools: [{ name: 'quote-pricing.basis' }, { name: 'crm.history-price-read' }], skills: [], models: ['quote-extractor'], roles: ['BD'], @@ -123,7 +123,7 @@ export const manifest: ActivityManifest = { declarations: { models: ['quote-extractor'], - tools: [{ name: 'crm.history-price-read' }], + tools: [{ name: 'crm.history-price-read' }, { name: 'quote-pricing.basis' }], ledger: [ { domain: DOMAIN, @@ -133,6 +133,7 @@ export const manifest: ActivityManifest = { { name: 'pn', type: 'text', primaryKey: true, masked: false, label: 'PN' }, { name: 'quoted_price', type: 'number', primaryKey: false, masked: false, label: '拟报单价' }, { name: 'margin', type: 'number', primaryKey: false, masked: false, label: '毛利率' }, + { name: 'last_price', type: 'number', primaryKey: false, masked: false, label: '基准价' }, { name: 'deviation', type: 'number', primaryKey: false, masked: false, label: '偏离度' }, { name: 'disposition', type: 'text', primaryKey: false, masked: false, label: '门禁处置' }, ], @@ -167,7 +168,8 @@ export const manifest: ActivityManifest = { label: 'S1 取价基准', // Type A 取历史价近期价;B/C 取供应商底价表(C 类参考替代对标表) input: { pn: 'pn' }, - output: { domain: DOMAIN, object: 'draft', key: { pn: 'pn' }, fields: { deviation: 'deviation' } }, + // 取回来的是**基准价**;偏离率要等 S2 定出拟报价才算得出来(见 S2B) + output: { domain: DOMAIN, object: 'draft', key: { pn: 'pn' }, fields: { last_price: 'lastPrice' } }, tool: 'crm.history-price-read', failure: { retry: { times: 2, backoff: 'PT1M' }, fallback: { kind: 'human', workOrder: 'execution' } }, }, @@ -185,6 +187,15 @@ export const manifest: ActivityManifest = { tool: 'quote-pricing.margin', failure: { retry: { times: 1, backoff: 'PT10S' }, fallback: { kind: 'blocked' } }, }, + { + kind: 'pure-tool', + name: 'S2B_deviation', + label: 'S2B 算偏离率(基准价 ↔ 拟报价)', + input: { lastPrice: 'last_price', quotedPrice: 'quoted_price' }, + output: { domain: DOMAIN, object: 'draft', key: { pn: 'pn' }, fields: { deviation: 'deviation' } }, + tool: 'quote-pricing.basis', + failure: { retry: { times: 1, backoff: 'PT10S' }, fallback: { kind: 'blocked' } }, + }, { kind: 'pure-tool', name: 'S3_floor_gate', diff --git a/apps/api/src/app/seed-quote.ts b/apps/api/src/app/seed-quote.ts index cc7f4aa..c35ef63 100644 --- a/apps/api/src/app/seed-quote.ts +++ b/apps/api/src/app/seed-quote.ts @@ -119,6 +119,7 @@ export async function seedQuote(container: Container): Promise { HISTORY_TOOL, MAIL_TOOL, CLASSIFY_TOOL, + pureTool('quote-pricing.basis', '取价基准:报价相对历史成交价的偏离率,供底价门禁 P-02 求值', 'quote-pricing', ['quotedPrice']), pureTool('quote-pricing.margin', '利润模型 P-01 的确定性求值:客户等级×物料类型×数量档 → 目标毛利区间', 'quote-pricing', ['customerGrade']), pureTool('quote-pricing.floor-gate', '底价门禁 P-02 的确定性求值:偏离底价 ±20% 即止', 'quote-pricing', ['deviation']), pureTool('quote-archive.profile', '客户画像更新:把成交行为写成标签,供 P-01 的客户等级因子取用', 'quote-archive', ['customerCode']), @@ -129,7 +130,8 @@ export async function seedQuote(container: Container): Promise { contract, samples: [ { - input: { pn: 'LM358DR', historyDays: 30, customerGrade: '普通客户', deviation: 0, to: 'x@y.com', subject: 't', customerCode: 'C001' }, + // 样例集一份喂所有工具:并集覆盖各家必填,少一个键就有工具过不了两审 + input: { pn: 'LM358DR', historyDays: 30, customerGrade: '普通客户', deviation: 0, to: 'x@y.com', subject: 't', customerCode: 'C001', quotedPrice: 9.2, lastPrice: 8.4 }, expectShape: { type: 'object' }, }, ], @@ -272,6 +274,14 @@ export async function seedQuote(container: Container): Promise { { target: 'customerCode', source: { from: 'initiation', path: 'customerCode' } }, // records 来自 A1 的输出口(受理归一的产物),不是起单入参 —— 起单时还没有这些行 { target: 'records', source: { from: 'output-port', port: 'intake', path: 'records' } }, + // 逐条办的料号:MVP 单条 RFQ,取受理归一出的那一条 + { target: 'pn', source: { from: 'output-port', port: 'intake', path: 'pn' } }, + // P-01 利润模型的三个因子:物料类型就是 A1 打的标(A/B/C),真实取值; + // 客户等级与数量档在 MVP 取默认档 —— 接 CRM 后改为读 shared.customer 与按 EAU 分档, + // P-01 决策表本就有兜底行('-' = 任意),默认档不会让求值失真。 + { target: 'materialType', source: { from: 'output-port', port: 'intake', path: 'type_tag' } }, + { target: 'customerGrade', source: { from: 'constant', value: '普通客户' } }, + { target: 'volumeTier', source: { from: 'constant', value: '大批量' } }, ], reopen: false, }, @@ -302,6 +312,14 @@ export async function seedQuote(container: Container): Promise { { target: 'customerCode', source: { from: 'initiation', path: 'customerCode' } }, // records 来自 A1 的输出口(受理归一的产物),不是起单入参 —— 起单时还没有这些行 { target: 'records', source: { from: 'output-port', port: 'intake', path: 'records' } }, + // 逐条办的料号:MVP 单条 RFQ,取受理归一出的那一条 + { target: 'pn', source: { from: 'output-port', port: 'intake', path: 'pn' } }, + // P-01 利润模型的三个因子:物料类型就是 A1 打的标(A/B/C),真实取值; + // 客户等级与数量档在 MVP 取默认档 —— 接 CRM 后改为读 shared.customer 与按 EAU 分档, + // P-01 决策表本就有兜底行('-' = 任意),默认档不会让求值失真。 + { target: 'materialType', source: { from: 'output-port', port: 'intake', path: 'type_tag' } }, + { target: 'customerGrade', source: { from: 'constant', value: '普通客户' } }, + { target: 'volumeTier', source: { from: 'constant', value: '大批量' } }, ], reopen: true, }, diff --git a/apps/api/src/app/tools/first-party.ts b/apps/api/src/app/tools/first-party.ts index 2918c0b..06255a1 100644 --- a/apps/api/src/app/tools/first-party.ts +++ b/apps/api/src/app/tools/first-party.ts @@ -13,19 +13,36 @@ import type { ToolHandler } from '@modules/toolbox'; const historyPriceRead: ToolHandler = async ({ input }) => { const pn = String(input['pn'] ?? ''); // 录制样本:演示用的历史价情报(真 CRM 接入时换 endpoint,契约不变) - const table: Record = { - LM358DR: { historyDays: 30, marketStable: true, lifecycle: '在产' }, - NE555P: { historyDays: 240, marketStable: false, lifecycle: '在产' }, - TL072CDR: { historyDays: 15, marketStable: true, lifecycle: 'EOL' }, + // 历史价情报含**上次成交单价** —— 没有价的「历史价工具」是取不出价格基准的 + const table: Record = { + LM358DR: { historyDays: 30, marketStable: true, lifecycle: '在产', lastPrice: 8.4 }, + NE555P: { historyDays: 240, marketStable: false, lifecycle: '在产', lastPrice: 3.25 }, + TL072CDR: { historyDays: 15, marketStable: true, lifecycle: 'EOL', lastPrice: 21.6 }, }; const hit = table[pn]; if (hit === undefined) { // 无历史 → 走 B(重估后报);这不是错误 - return { ok: true, output: { historyDays: 9999, marketStable: false, lifecycle: '未知', hit: false } }; + // 无历史 → 走 B(重估后报);没有价格基准,基准价回 0 由下游按「无基准」处置 + return { ok: true, output: { historyDays: 9999, marketStable: false, lifecycle: '未知', hit: false, lastPrice: 0 } }; } return { ok: true, output: { ...hit, hit: true } }; }; +/** + * 取价基准 —— **纯工具**:报价相对基准价的偏离率。 + * 「能算的不问 LLM」:同输入必同结论。无基准价(新料号)时偏离率记 0, + * 由底价门禁 P-02 按「无基准」放行 —— 不能凭空编一个偏离率出来。 + */ +const basis: ToolHandler = async ({ input }) => { + const lastPrice = Number(input['lastPrice'] ?? 0); + const quotedPrice = Number(input['quotedPrice'] ?? 0); + if (!Number.isFinite(lastPrice) || lastPrice <= 0) { + return { ok: true, output: { deviation: 0, basis: 'none', lastPrice: 0 } }; + } + const deviation = (quotedPrice - lastPrice) / lastPrice; + return { ok: true, output: { deviation: Math.round(deviation * 10_000) / 10_000, basis: 'history', lastPrice } }; +}; + /** 三分类规则 P-03 的决策表 —— 与活动包里的资产同一份规则 */ const P03_RULES = [ { id: 'p03-c', when: ['-', '-', '"EOL","缺货","要求替代"'], then: { type: 'C', play: '转案处理' } }, @@ -109,6 +126,7 @@ const mailSend: ToolHandler = async ({ input, connection }) => { export const FIRST_PARTY_TOOLS: ReadonlyArray = [ ['crm.history-price-read', historyPriceRead], ['quote-intake.classify', classify], + ['quote-pricing.basis', basis], ['quote-pricing.margin', margin], ['quote-pricing.floor-gate', floorGate], ['quote-archive.profile', profile],