判据 ⑥ 跑通:A4 成本定价全链 —— 补齐取价基准这一环

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 05:14:01 +08:00
co-authored by Claude Opus 5
parent 9201f6b094
commit c17ccaf049
7 changed files with 105 additions and 10 deletions
@@ -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": []
}
@@ -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": []
}
@@ -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": []
}
@@ -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": []
}
+15 -4
View File
@@ -59,7 +59,7 @@ export const manifest: ActivityManifest = {
identity: { identity: {
activityId: ACTIVITY_ID, activityId: ACTIVITY_ID,
name: 'A4 · 成本定价', name: 'A4 · 成本定价',
packageVersion: '0.1.1', packageVersion: '0.1.2',
category: '统一报价', category: '统一报价',
paradigm: 'flow', paradigm: 'flow',
sdkVersion: '0.1.0-m5', sdkVersion: '0.1.0-m5',
@@ -113,7 +113,7 @@ export const manifest: ActivityManifest = {
humanCallBudget: 2, humanCallBudget: 2,
}, },
resources: { resources: {
tools: [{ name: 'crm.history-price-read' }], tools: [{ name: 'quote-pricing.basis' }, { name: 'crm.history-price-read' }],
skills: [], skills: [],
models: ['quote-extractor'], models: ['quote-extractor'],
roles: ['BD'], roles: ['BD'],
@@ -123,7 +123,7 @@ export const manifest: ActivityManifest = {
declarations: { declarations: {
models: ['quote-extractor'], models: ['quote-extractor'],
tools: [{ name: 'crm.history-price-read' }], tools: [{ name: 'crm.history-price-read' }, { name: 'quote-pricing.basis' }],
ledger: [ ledger: [
{ {
domain: DOMAIN, domain: DOMAIN,
@@ -133,6 +133,7 @@ export const manifest: ActivityManifest = {
{ name: 'pn', type: 'text', primaryKey: true, masked: false, label: 'PN' }, { name: 'pn', type: 'text', primaryKey: true, masked: false, label: 'PN' },
{ name: 'quoted_price', type: 'number', primaryKey: false, masked: false, label: '拟报单价' }, { name: 'quoted_price', type: 'number', primaryKey: false, masked: false, label: '拟报单价' },
{ name: 'margin', 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: 'deviation', type: 'number', primaryKey: false, masked: false, label: '偏离度' },
{ name: 'disposition', type: 'text', primaryKey: false, masked: false, label: '门禁处置' }, { name: 'disposition', type: 'text', primaryKey: false, masked: false, label: '门禁处置' },
], ],
@@ -167,7 +168,8 @@ export const manifest: ActivityManifest = {
label: 'S1 取价基准', label: 'S1 取价基准',
// Type A 取历史价近期价;B/C 取供应商底价表(C 类参考替代对标表) // Type A 取历史价近期价;B/C 取供应商底价表(C 类参考替代对标表)
input: { pn: 'pn' }, 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', tool: 'crm.history-price-read',
failure: { retry: { times: 2, backoff: 'PT1M' }, fallback: { kind: 'human', workOrder: 'execution' } }, failure: { retry: { times: 2, backoff: 'PT1M' }, fallback: { kind: 'human', workOrder: 'execution' } },
}, },
@@ -185,6 +187,15 @@ export const manifest: ActivityManifest = {
tool: 'quote-pricing.margin', tool: 'quote-pricing.margin',
failure: { retry: { times: 1, backoff: 'PT10S' }, fallback: { kind: 'blocked' } }, 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', kind: 'pure-tool',
name: 'S3_floor_gate', name: 'S3_floor_gate',
+19 -1
View File
@@ -119,6 +119,7 @@ export async function seedQuote(container: Container): Promise<void> {
HISTORY_TOOL, HISTORY_TOOL,
MAIL_TOOL, MAIL_TOOL,
CLASSIFY_TOOL, CLASSIFY_TOOL,
pureTool('quote-pricing.basis', '取价基准:报价相对历史成交价的偏离率,供底价门禁 P-02 求值', 'quote-pricing', ['quotedPrice']),
pureTool('quote-pricing.margin', '利润模型 P-01 的确定性求值:客户等级×物料类型×数量档 → 目标毛利区间', 'quote-pricing', ['customerGrade']), pureTool('quote-pricing.margin', '利润模型 P-01 的确定性求值:客户等级×物料类型×数量档 → 目标毛利区间', 'quote-pricing', ['customerGrade']),
pureTool('quote-pricing.floor-gate', '底价门禁 P-02 的确定性求值:偏离底价 ±20% 即止', 'quote-pricing', ['deviation']), pureTool('quote-pricing.floor-gate', '底价门禁 P-02 的确定性求值:偏离底价 ±20% 即止', 'quote-pricing', ['deviation']),
pureTool('quote-archive.profile', '客户画像更新:把成交行为写成标签,供 P-01 的客户等级因子取用', 'quote-archive', ['customerCode']), pureTool('quote-archive.profile', '客户画像更新:把成交行为写成标签,供 P-01 的客户等级因子取用', 'quote-archive', ['customerCode']),
@@ -129,7 +130,8 @@ export async function seedQuote(container: Container): Promise<void> {
contract, contract,
samples: [ 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' }, expectShape: { type: 'object' },
}, },
], ],
@@ -272,6 +274,14 @@ export async function seedQuote(container: Container): Promise<void> {
{ target: 'customerCode', source: { from: 'initiation', path: 'customerCode' } }, { target: 'customerCode', source: { from: 'initiation', path: 'customerCode' } },
// records 来自 A1 的输出口(受理归一的产物),不是起单入参 —— 起单时还没有这些行 // records 来自 A1 的输出口(受理归一的产物),不是起单入参 —— 起单时还没有这些行
{ target: 'records', source: { from: 'output-port', port: 'intake', path: 'records' } }, { 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, reopen: false,
}, },
@@ -302,6 +312,14 @@ export async function seedQuote(container: Container): Promise<void> {
{ target: 'customerCode', source: { from: 'initiation', path: 'customerCode' } }, { target: 'customerCode', source: { from: 'initiation', path: 'customerCode' } },
// records 来自 A1 的输出口(受理归一的产物),不是起单入参 —— 起单时还没有这些行 // records 来自 A1 的输出口(受理归一的产物),不是起单入参 —— 起单时还没有这些行
{ target: 'records', source: { from: 'output-port', port: 'intake', path: 'records' } }, { 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, reopen: true,
}, },
+23 -5
View File
@@ -13,19 +13,36 @@ import type { ToolHandler } from '@modules/toolbox';
const historyPriceRead: ToolHandler = async ({ input }) => { const historyPriceRead: ToolHandler = async ({ input }) => {
const pn = String(input['pn'] ?? ''); const pn = String(input['pn'] ?? '');
// 录制样本:演示用的历史价情报(真 CRM 接入时换 endpoint,契约不变) // 录制样本:演示用的历史价情报(真 CRM 接入时换 endpoint,契约不变)
const table: Record<string, { historyDays: number; marketStable: boolean; lifecycle: string }> = { // 历史价情报含**上次成交单价** —— 没有价的「历史价工具」是取不出价格基准的
LM358DR: { historyDays: 30, marketStable: true, lifecycle: '在产' }, const table: Record<string, { historyDays: number; marketStable: boolean; lifecycle: string; lastPrice: number }> = {
NE555P: { historyDays: 240, marketStable: false, lifecycle: '在产' }, LM358DR: { historyDays: 30, marketStable: true, lifecycle: '在产', lastPrice: 8.4 },
TL072CDR: { historyDays: 15, marketStable: true, lifecycle: 'EOL' }, NE555P: { historyDays: 240, marketStable: false, lifecycle: '在产', lastPrice: 3.25 },
TL072CDR: { historyDays: 15, marketStable: true, lifecycle: 'EOL', lastPrice: 21.6 },
}; };
const hit = table[pn]; const hit = table[pn];
if (hit === undefined) { if (hit === undefined) {
// 无历史 → 走 B(重估后报);这不是错误 // 无历史 → 走 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 } }; 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 的决策表 —— 与活动包里的资产同一份规则 */ /** 三分类规则 P-03 的决策表 —— 与活动包里的资产同一份规则 */
const P03_RULES = [ const P03_RULES = [
{ id: 'p03-c', when: ['-', '-', '"EOL","缺货","要求替代"'], then: { type: 'C', play: '转案处理' } }, { 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<readonly [string, ToolHandler]> = [ export const FIRST_PARTY_TOOLS: ReadonlyArray<readonly [string, ToolHandler]> = [
['crm.history-price-read', historyPriceRead], ['crm.history-price-read', historyPriceRead],
['quote-intake.classify', classify], ['quote-intake.classify', classify],
['quote-pricing.basis', basis],
['quote-pricing.margin', margin], ['quote-pricing.margin', margin],
['quote-pricing.floor-gate', floorGate], ['quote-pricing.floor-gate', floorGate],
['quote-archive.profile', profile], ['quote-archive.profile', profile],