/** * 活动包清单 —— 统一报价 A4 · 成本定价(案例活动)。 * * 业务素材:`docs/MVP示例工作流.html` §1.2(S1 取价基准 → S2 逐条定价 → S3 底价自检): * 「BD 逐条定价:价格 = 底价 × 利润模型。提交审批前先自查底价门禁 —— * 超限的自己先调,不把问题留给 PM。」 * * 形状与 A3 同一条(2026-08-03 重写 @3.0.0):归一算价 → 一张执行单交人 → 超限才叫人 * 拍板 → 放行。**能算的不问人**(P-01 毛利档、P-02 底价门禁两把尺写死), * **该人负责的不自算**(拟报单价是必填,库内没有基准的那几行系统一个价都不编)。 * * 与 @2.0.0 的差别(三处都是踩过的账): * ① `input.required` 从 `['rfqRef','customerCode','records']` 清空 —— 宽进严出, * 上游(A3)发的是 `{rfqRef, rows}`,门口拦的恰是主路; * ② 台账补 `required` —— 从前一个必填都没有,执行卡的闸没牙,人不填也能交卷; * ③ 判据与哨兵全部换成**基座读数**(`_rows` / `_missing` / `_<列>_<值>`)—— * 从前 L1 读 `unpriced_total`、哨兵读 `draft_total`,两个都是活动自报的数。 */ import type { ActivityManifest, DmnModel } from '@contracts'; import { activityIdSchema } from '@contracts'; import { PRICING_SKILLS } from './skills.ts'; import { CMMN_XML, NARRATIVE, PRICING_PHASES, PRICING_STEPS } from './case-model.ts'; import { PRICING_ROSTER, PRICING_WORKSHEET } from './views/report.ts'; const D = 'quote_pricing'; const ACTIVITY_ID = activityIdSchema.parse('quote-pricing'); /** * 两把尺 —— **规则本身不是数据**,随包冻结。 * * · 利润模型 P-01:客户等级 × 类别 → 目标毛利区间(FIRST,行序即业务优先级); * · 底价门禁 P-02:相对底价线的偏离度 → 处置码。 * * 出参就是台账列的取值(`disposition` 的三个大写码),模型算完照抄即可 —— * 「出参键名与台账列同名」是为了它拿回去直接 write(规范 §5.6)。 * 两把尺的档位在三处出现:这张表、剧本 guide、工作单上的规则块 —— * 包内自测把三处钉在一起(改一处不改另两处,测就红)。 */ export const P01_DMN: DmnModel = { drd: [ { decision: 'p01-margin', requires: [], inputs: ['customer_grade', 'type_tag'] }, { decision: 'p02-floor', requires: ['p01-margin'], inputs: ['deviation'] }, ], tables: [ { id: 'p01-margin', label: '利润模型 P-01', hitPolicy: 'FIRST', inputs: [ { name: 'customer_grade', expression: 'customer_grade' }, { name: 'type_tag', expression: 'type_tag' }, ], outputs: [{ name: 'margin_low' }, { name: 'margin_high' }], rules: [ { id: 'p01-1', when: ['"战略客户"', '"A"'], then: [0.08, 0.1], annotation: '战略客户 A 类标准件(大批量长单)8%~10%' }, { id: 'p01-2', when: ['"普通客户"', '"A"'], then: [0.12, 0.15], annotation: '普通客户 A 类标准件 12%~15%' }, { id: 'p01-3', when: ['-', '"B"'], then: [0.15, 0.2], annotation: 'B 类波动件 15%~20%' }, { id: 'p01-4', when: ['-', '"C"'], then: [0.2, 0.5], annotation: 'C 类替代/停产件 20%~50%(一事一议)' }, { id: 'p01-default', when: ['-', '-'], then: [0.12, 0.15], annotation: '拿不准按普通客户标准件档' }, ], }, { /** 底价门禁 P-02 —— 偏离底价线 ±20% 即止;出参就是台账 `disposition` 的取值 */ id: 'p02-floor', label: '底价门禁 P-02', hitPolicy: 'FIRST', inputs: [{ name: 'deviation', expression: 'deviation' }], outputs: [{ name: 'disposition' }, { name: 'passable' }], rules: [ { id: 'p02-1', when: ['>= 0'], then: ['PASS', true], annotation: '在底价线之上 → 直接放行' }, { id: 'p02-2', when: ['[-0.20..0)'], then: ['NOTE', true], annotation: '低于底价线两成以内 → 附理由放行' }, { id: 'p02-3', when: ['< -0.20'], then: ['BLOCK', false], annotation: '低于两成以上 → 拦截,人拍板' }, ], }, ], }; export const manifest: ActivityManifest = { identity: { activityId: ACTIVITY_ID, name: 'A4 · 成本定价', packageVersion: '3.5.0', category: '统一报价', sdkVersion: '0.1.0-m5', checksum: 'dev-checksum-quote-pricing', }, /* ── 契约七件 ────────────────────────────────────────────────── */ contract: { activityId: ACTIVITY_ID, version: '1.0.0', input: { /* * **宽进到底:一个必填都不设**(与 A1/A2/A3 同口径 —— 宽进严出, * 输入契约在活动内部归一 + 补全)。 * * 来路三种形状:寻源转来(rows,行上带 base_price)、受理/选型直达(records/rows, * 没有底价)、单飞(一段文字)。上一版必填 `rfqRef + customerCode + records`, * 而 A3 的 `quotation` 口发的是 `{rfqRef, rows}` —— 门口拦的恰是主路。 */ data: { type: 'object', required: [], properties: { rfqRef: { type: 'string', title: '询价单号(单标识,贯穿全流程)' }, customerCode: { type: 'string', title: '客户码' }, rows: { type: 'array', title: '目标报价清单(寻源/选型转来,行上带底价)' }, records: { type: 'array', title: '上游受理记录(受理直达)' }, subject: { type: 'string', title: '需求主题' }, body: { type: 'string', title: '需求正文' }, attachments: { type: 'array', format: 'file', title: '附件', items: { type: 'object', properties: { name: { type: 'string', title: '文件名' }, contentType: { type: 'string', title: '类型' }, size: { type: 'integer', title: '字节数' }, ref: { type: 'string', title: '存档号' }, text: { type: 'string', title: '读出来的文本' }, }, }, }, }, }, messageAllowed: true, messageHint: '客户等级、急单、竞对价、这一单的让价授权写这里', }, output: { /* * 只有一个出口:报价草案交定价审批(流程包 E8 是无条件边)。没有选择组 —— * 没有去向要选,定盘就是把账写齐。 * * 载荷键名照下游(A6 议价)的入参写:`rfqRef / customerCode / items` —— * 边读什么、下游要什么,上游就得发什么。标量都有台账列承载(自测钉着)。 */ mandatory: [ { name: 'draft', payload: { type: 'object', required: ['rfqRef', 'items'], properties: { rfqRef: { type: 'string', title: '询价单号(单标识 —— 下游台账的主键)' }, customerCode: { type: 'string', title: '客户码' }, items: { type: 'array', title: '报价草案(每条带拟报单价、偏离度与门禁处置)' }, }, }, description: '报价草案(全条目带拟报单价与门禁处置)', }, ], selective: [], }, /* * 必交付物只有一件:**报价草案**。定价结论(verdict)是它的结论栏, * `ledgers` 说清这份成果由哪几张表承载(一份成果,不是两份)。 */ requiredDeliverables: [ { key: 'quote-draft', label: '报价草案(每条带拟报单价与门禁处置)', ledger: `${D}.draft`, /* 验收页上让 BD 就地改的两列 —— 恰是他的职权面(定价与让价理由) */ editableAtRelease: ['quoted_price', 'price_note'], ledgers: [`${D}.draft`, `${D}.verdict`], }, ], /* * 验收判据 —— **机检(L1/L2)只读基座数得出来的数**: * `_rows` 基座数行数、`_missing` 基座按 required 现数(与执行卡提交闸同一个数 * 同一套算法)、`_disposition_*` 基座按值现数。 * 上一版两条机检读活动自报的 `unpriced_total` / `unresolved_over_total` —— * 那不是门,是「改哪个数能过」的提示(违反史已记四次,这是第五类)。 */ acceptanceCriteria: [ { key: 'roster-complete', label: '草案齐:上游条目逐条承继,说了几条草案里就躺着几行', level: 'L1', predicate: `line_total > 0 and ${D}_draft_rows = line_total`, }, { key: 'all-priced', label: '每条都有拟报单价:库内无基准价的那几行也由人定过价,缺一行放不了', level: 'L1', predicate: `${D}_draft_rows > 0 and ${D}_draft_missing = 0`, }, { key: 'floor-gate', label: '底价门禁已过:处置逐条落定,没有还压在拦截档上的条目 —— 超限的要么回调、要么经人拍板附理由', level: 'L2', modelRef: 'p02-floor', predicate: `${D}_draft_disposition_block = 0 and ${D}_draft_disposition_pass + ${D}_draft_disposition_note + ${D}_draft_disposition_block = ${D}_draft_rows`, }, /* * 报价来路可核是**裁量**(L3):基准是不是那个基准、让价理由站不站得住, * 机器数不出来。这句 label 与剧本里 judge 的 criterion **必须逐字一致**。 */ { key: 'price-defensible', label: '报价的来路说得清:基准价来源、毛利档与让价理由逐条可溯,BD 对报价负责', level: 'L3', }, /* L5 是责任关:那张卡本身就是这道关,卡面上不摆它(基座统一滤) */ { key: 'bd-release', label: 'BD 放行:签下去这份报价草案即定盘并交定价审批', level: 'L5' }, ], revisableAtRelease: [], prohibitions: [ { key: 'no-silent-override', statement: '不得静默绕开底价门禁的结论;要破线只能走「人 · 决策」并留下理由' }, { key: 'no-made-up-basis', statement: '没有成本基准的料不许编一个偏离率出来:基准空着就空着,价由人定并注明依据' }, { key: 'no-stale-floor', statement: '底价超一周未更新的,先刷底价再判 —— 拿过期的成本算出来的毛利是假的' }, ], authorization: { discretion: [{ key: 'margin-band', statement: '在 P-01 给出的毛利区间内自决;出区间要么回调,要么调人拍板' }], preauthorizedActions: [], /* 一张执行单 + 可能一张决策卡 + 放行 = 三次;留一倍余量 */ humanCallBudget: 5, }, resources: { tools: [{ name: 'crm.history-price-read' }], skills: PRICING_SKILLS.map((s) => s.id), models: ['deepseek-v4-flash'], roles: ['BD'], /* SLA <2h(MVP示例 §1.2);预算按 A3 实测(6-8 拍 / 40-50k)留一倍余量 */ budget: { steps: 60, tokens: 400_000, activeMinutes: 120, humanCalls: 5 }, }, }, /* ── 声明 ────────────────────────────────────────────────────── */ declarations: { models: ['deepseek-v4-flash'], tools: [{ name: 'crm.history-price-read' }], ledger: [ { domain: D, object: 'draft', kind: 'record', caseKey: 'rfq_ref', label: '报价草案', fields: [ { name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' }, { name: 'pn', type: 'text', primaryKey: true, masked: false, label: 'PN' }, { name: 'customer_pn', type: 'text', primaryKey: false, masked: false, label: '客户料号' }, { name: 'category', type: 'text', primaryKey: false, masked: false, label: '品类' }, { name: 'brand', type: 'text', primaryKey: false, masked: false, label: '制造商' }, { name: 'eau', type: 'integer', primaryKey: false, masked: false, label: '年用量 EAU' }, { name: 'lead_time', type: 'text', primaryKey: false, masked: false, label: '客户要求交期' }, /* 客户备注承继自上游 —— 认证要求、预授权、急料标注,让价时的第一手依据 */ { name: 'remark', type: 'text', primaryKey: false, masked: false, label: '客户备注(约束/急料)' }, { name: 'type_tag', type: 'text', primaryKey: false, masked: false, label: '类别(承继)', tally: ['A', 'B', 'C'] }, /* * **客户目标价标 `expect`** —— 客户该给而没给的那一问不能省(件上出橘色 * 「建议填」胶囊)。它不拦单:`_gaps` 只喂 L4,而本活动没有 L4 判据。 * 注意边界:我方自己写的注(price_note)**不标 expect** —— 标了 `_gaps` * 恒大于 0,那条判据永远绿不了(A1 的账)。 */ { name: 'target_price', type: 'number', primaryKey: false, masked: false, expect: true, label: '客户目标价' }, /* 成本基准:上游底价或库内历史价。**取不到就空着** —— 空着不是异常,是「无基准」那一档 */ { name: 'base_price', type: 'number', primaryKey: false, masked: false, label: '成本基准' }, { name: 'price_basis', type: 'text', primaryKey: false, masked: false, label: '基准来源', tally: ['HIST', 'SOURCED'] }, { name: 'margin_band', type: 'text', primaryKey: false, masked: false, label: '毛利档(P-01)' }, { name: 'floor_price', type: 'number', primaryKey: false, masked: false, label: '底价线' }, /* * **系统建议价是 AI 的活,拟报单价是人的活** —— 两列分开是**结构治**,不是措辞治。 * * 实测账(2026-08-03,单飞那条案例):从前只有 `quoted_price` 一列,剧本写着 * 「无基准的行留空,由 BD 定」—— 模型照样给两颗库里查不到的料编了价 * (一颗抄了客户目标价,另一颗凭空写了 0.085),交代里还说着「价空着等你定」。 * 一条要模型分辨「这行算得出/算不出」的规矩,它十次里会走岔; * 而「这一列你一格都不写」没有分支可走岔。 * * 于是:AI 只写 `suggested_price`(算得出就给,算不出就空着), * **`quoted_price` 永远是人在件上定的那个数** —— 它标 `required`, * 件上的缺价红带、执行卡的提交闸、放行门 L1 三处同源(都数基座的 `_missing`)。 * 不用 `requiredWhen`:每一行都要有价,没有「这一行还没轮到」的情形。 */ { name: 'suggested_price', type: 'number', primaryKey: false, masked: false, label: '系统建议价' }, { name: 'quoted_price', type: 'number', primaryKey: false, masked: false, required: true, label: '拟报单价(BD 定)' }, { name: 'margin', type: 'number', primaryKey: false, masked: false, label: '毛利率' }, { name: 'deviation', type: 'number', primaryKey: false, masked: false, label: '偏离底价线' }, /* * **门禁处置:取值域给出来,条数由基座现数** —— PASS / NOTE / BLOCK。 * 守恒判据、分布条、门禁徽读的全是基座读数;值是机器码, * 人读名(直接放行 / 附理由 / 拦截待拍板)由模版样式表翻译。 */ { name: 'disposition', type: 'text', primaryKey: false, masked: false, label: '门禁处置', tally: ['PASS', 'NOTE', 'BLOCK'] }, { name: 'price_note', type: 'text', primaryKey: false, masked: false, label: '备注 / 让价理由' }, ], }, { /* * 定价结论 —— 一单一行的**汇总表**,哨兵与报告读它。 * 汇总列与逐行列**故意不同名**:任务视图把草稿行摊平到顶层是「后写的赢」, * 拿逐行列求值只会验到最后写的那一行,那不叫机检、叫抽样(A1 的教训)。 */ domain: D, object: 'verdict', kind: 'record', caseKey: 'rfq_ref', label: '定价结论', fields: [ { name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' }, { name: 'customer_code', type: 'text', primaryKey: false, masked: false, label: '客户码' }, { name: 'customer_name', type: 'text', primaryKey: false, masked: false, label: '客户名称' }, /* 客户等级是 P-01 的入参之一(战略客户 / 普通客户),认不出按普通客户 */ { name: 'customer_grade', type: 'text', primaryKey: false, masked: false, label: '客户等级' }, { name: 'line_total', type: 'integer', primaryKey: false, masked: false, label: '草案条目数' }, { name: 'basis_total', type: 'integer', primaryKey: false, masked: false, label: '有成本基准的条数' }, /* 算价走完的凭据(0 也要写)—— 哨兵按它放交人核价那一步入场 */ { name: 'priced_total', type: 'integer', primaryKey: false, masked: false, label: '已算出建议价条数' }, /* 「BD 核过价了」的凭据 —— 只在人环走完才产生 */ { name: 'confirmed_total', type: 'integer', primaryKey: false, masked: false, label: 'BD 已核价条数' }, { name: 'route_decided_total', type: 'integer', primaryKey: false, masked: false, label: '已定盘' }, { name: 'summary', type: 'text', primaryKey: false, masked: false, label: '定价结论' }, ], }, ], listeners: [ { /** 时限过半:SLA <2h,给 BD 与负责人提个醒 —— 静态监听,时限必配 */ origin: 'static', eventType: 'time', keyFilter: { kind: 'half-deadline' }, semantic: 'observe', deadline: 'PT1H', escalation: ['nudge'], }, ], harness: { memoryWindow: 22, // SLA <2h(MVP示例 §1.2 留痕 · SLA) stallAfter: 'PT2H', nudge: [{ after: 'PT1H', action: 'nudge' }], retry: { times: 2, backoff: 'PT30S' }, }, }, /* ── 资产初始版 ──────────────────────────────────────────────── */ assets: { businessModel: { cmmnXml: CMMN_XML, narrative: NARRATIVE, dmn: P01_DMN, steps: PRICING_STEPS, phases: PRICING_PHASES, }, skills: PRICING_SKILLS, /* * 两个局面各一张(与 A2/A3 同款口径): * · `pricing-worksheet` 定价工作单 —— 算价与 BD 核价期间摆它(逐颗定价档案、 * 两把尺、填回工作表); * · `pricing-roster` 报价草案清单 —— 拍板、放行、归档摆它。 */ viewTemplates: [ { name: 'pricing-worksheet', html: PRICING_WORKSHEET }, { name: 'pricing-roster', html: PRICING_ROSTER }, ], cardSchemas: {}, distillHooks: [ { on: 'terminal', prompt: '归纳本单定价:哪些组合的毛利档被人回调过、底价门禁拦下的形态、破线的理由与拍板结果。', }, ], }, /* ── 样例集 —— 三条形状真不同(常备案例即验收案例)───────────── */ samples: [ { /* * ① 寻源转来 · 常路:A3 `quotation` 口的真实形状 —— rows 里清单行(带底价与价源) * 混着结论行。客户目标价都在底价线之上,全 PASS,不必叫人拍板。 */ name: '寻源转来 · 两条料底价齐 · 全在底价线之上(常路)', input: { rfqRef: 'RFQ MD14.1', customerCode: 'CU-0142', customer_name: 'Itelma', rows: [ { rfq_ref: 'RFQ MD14.1', pn: 'BBPY00201209601Y00', customer_pn: '4-14-01502', category: '滤波器', brand: 'YAGEO', eau: 30000, target_price: 0.085, lead_time: '8周', type_tag: 'B', base_price: 0.058, price_basis: 'SOURCED', supplier: '深圳华强渠道', }, { rfq_ref: 'RFQ MD14.1', pn: 'CJTB0101R6N', customer_pn: '4-00-04102', category: '集成电路', brand: 'Jiangsu Changjing', eau: 60000, target_price: 0.42, lead_time: '6周', type_tag: 'A', base_price: 0.35, price_basis: 'HIST', }, { rfq_ref: 'RFQ MD14.1', quote_line_total: 2, route_decided_total: 1, customer_name: 'Itelma', summary: '两条底价齐,交定价' }, ], }, expect: { ports: ['draft'] }, }, { /* * ② 有超限条目:客户把目标价压到成本之下(0.06 对成本 0.082)—— * 偏离底价线超过两成,门禁判 BLOCK,必须叫人拍板(决策卡)。 */ name: '客户压价 · 一条低于底价线两成以上 → 决策卡叫人拍板', input: { rfqRef: 'RFQ-2026-0777', customerCode: 'CU-0311', customer_name: '青岛海洋电子', rows: [ { rfq_ref: 'RFQ-2026-0777', pn: 'PESD2ETH-XR', category: '二极管', brand: 'Nexperia', eau: 30000, target_price: 0.06, lead_time: '8周', type_tag: 'B', base_price: 0.082, price_basis: 'SOURCED', remark: '客户明说这个价是竞对给的,要跟', }, { rfq_ref: 'RFQ-2026-0777', pn: 'NE555P', category: '定时器', brand: 'TI', eau: 5000, target_price: 1.05, lead_time: '4周', type_tag: 'A', base_price: 0.85, price_basis: 'HIST', }, { rfq_ref: 'RFQ-2026-0777', quote_line_total: 2, route_decided_total: 1, customer_name: '青岛海洋电子' }, ], }, expect: { ports: ['draft'] }, }, { /* * ③ 单飞宽进:没有单号没有清单,正文列料 —— 归一造案号;库内查不到历史价, * 系统一个价都不编,两行拟报单价空着交 BD 定 → **提交闸真拦**空手交卷。 */ name: '单飞需求 · 正文列两颗料 · 库内无基准 → 价由人定(闸真拦)', input: { subject: '两颗料报个价', body: '客户要两颗料的报价:XCJ-7788T 电源模块 8K,目标价 12.5;LMR-3302QQ 稳压器 15K,没给目标价。库里应该没做过这两颗,你先出个草案。', }, expect: { ports: ['draft'] }, }, ], entry: { file: 'index.js', checksum: 'dev-entry-quote-pricing' }, };