/** * 活动包清单 —— 统一报价 A1 · 询价受理。 * * 业务素材出处:`docs/MVP示例工作流.html` §1.1。归一化映射: * SOP 节点 → **案例活动**(CMMN 子集 + 说明书 + DMN); * 作业指导书 S1…S6 → 案例说明书的六段(见 `case-model.ts` 的建模口径注); * D1 判完整度 → **DMN 判定表 F-01**(《询价受理规范》); * 三分类规则 P-03 → **DMN 判定表**; * SLA(受理<15min、判定<45min)→ Harness 时限与预算。 * * 「清单机器可读,系统不执行代码即知包为何物」—— 这份清单就是这个活动的全部。 */ import type { ActivityManifest, DmnModel } from '@contracts'; import { activityIdSchema } from '@contracts'; import { INTAKE_SKILLS } from './skills.ts'; import { CMMN_XML, INTAKE_PHASES, INTAKE_STEPS, NARRATIVE } from './case-model.ts'; import { INTAKE_CLASSIFIED, INTAKE_OVERVIEW, INTAKE_RELEASE } from './views/report.ts'; const D = 'quote_intake'; const ACTIVITY_ID = activityIdSchema.parse('quote-intake'); /** * 两张判定表 —— **能算的不问 LLM**:同输入必同结论、零 token、命中行留痕。 * * F-01 受理字段完整度(《询价受理规范》):三要素齐才放行,缺关键字段退回补全。 * P-03 三分类:A 历史价≤90天且行情稳 / B 无历史·过期·波动 / C EOL·缺货·要替代。 * * 两张都是 `FIRST`:规则从上往下,第一条命中即出结论 —— 顺序本身就是业务优先级, * 所以 C 的那一行必须在最前面(EOL 的料哪怕历史价很新,也不该判成 A 直接快答)。 */ export const F01_DMN: DmnModel['tables'][number] = { id: 'f01-completeness', label: '受理字段完整度 F-01', hitPolicy: 'FIRST', inputs: [ { name: 'missing_key_total', expression: 'missing_key_total' }, { name: 'missing_minor_total', expression: 'missing_minor_total' }, ], outputs: [{ name: 'completeness' }, { name: 'disposition' }], rules: [ { id: 'f01-2', when: ['> 0', '-'], then: ['缺PN/数量/交期', '退回补全'], annotation: '三要素缺任一 → 必须追问补全' }, { id: 'f01-3', when: ['0', '> 0'], then: ['缺次要字段', '受理并追问'], annotation: '只缺厂牌/包装/备注 → 先受理' }, { id: 'f01-1', when: ['0', '-'], then: ['八字段齐', '受理转寻源'], annotation: '齐 → 直接往下走' }, ], }; export const P03_DMN: DmnModel['tables'][number] = { id: 'p03-type', label: '三分类规则 P-03', hitPolicy: 'FIRST', inputs: [ { name: 'history_days', expression: 'history_days' }, { name: 'market_stable', expression: 'market_stable' }, { name: 'lifecycle', expression: 'lifecycle' }, ], outputs: [{ name: 'type_tag' }, { name: 'play' }], rules: [ { id: 'p03-c', when: ['-', '-', '"EOL","缺货","要求替代"'], then: ['C', '转案处理'], annotation: 'EOL/缺货/要替代 → C' }, { id: 'p03-a', when: ['<= 90', 'true', '-'], then: ['A', '直接快答'], annotation: '历史价≤90天且行情稳 → A' }, { id: 'p03-b', when: ['-', '-', '-'], then: ['B', '重估后报'], annotation: '无历史/过期/波动 → B' }, ], }; export const INTAKE_DMN: DmnModel = { drd: [ { decision: 'f01-completeness', requires: [], inputs: ['missing_key_total', 'missing_minor_total'] }, { decision: 'p03-type', requires: [], inputs: ['history_days', 'market_stable', 'lifecycle'] }, ], tables: [F01_DMN, P03_DMN], }; export const manifest: ActivityManifest = { identity: { activityId: ACTIVITY_ID, name: 'A1 · 询价受理', packageVersion: '40.0.0', category: '统一报价', sdkVersion: '0.1.0-m5', checksum: 'dev-checksum-quote-intake', }, /* ── 契约七件 ────────────────────────────────────────────────── */ contract: { activityId: ACTIVITY_ID, version: '1.0.0', input: { /* * 入参就是**那封信** —— 客户邮箱 / 主题 / 邮件内容 / 邮件附件,前三样必填。 * * 从前这里收的是「客户编码 + 询价单号」,那是**受理之后**才有的东西: * 一封真的询价来函不会带着我们内部的客户码,单号也常常压根没有。 * 把它们摆成必填的后果实测过:流程包起单时通道抽不出这两个键,基座当场发补全卡, * 整单 0 拍停在那儿等人 —— 而信本身是完整的。认客户、认单号是 S1 的活,不是门口的闸。 * * 起单仍是宽进:门口不拒缺项(缺 required 由基座发补全卡,不是活动的事)。 */ data: { type: 'object', required: ['customerEmail', 'subject', 'body'], properties: { customerEmail: { type: 'string', title: '客户邮箱' }, subject: { type: 'string', title: '主题' }, /** 询价原文 + BOM 转述 —— 归一这一步拆解的对象 */ body: { type: 'string', title: '邮件内容' }, /* * **真文件**(`format: 'file'`):界面给文件选择器,基座在入口把它读成文本。 * 每一件是 `{ name, contentType, size, ref, text?, note }` —— * `text` 有值就是读得出内容(xlsx 读成制表符分列的行); * 读不出的类型 `text` 缺席、`note` 说明为什么。**说明书里写死了怎么用**。 */ 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: '读出来的文本' }, note: { type: 'string', title: '读取说明' }, }, }, }, }, }, messageAllowed: true, messageHint: '客户口头急单、老客户先受理后补单等特殊性写这里', /* * **这一单是被一封信叫起来的** —— 声明入参里哪几个字段合起来是那封来件, * 基座在起单那一刻就把它归进任务的沟通记录(工作台那一屏),原件与附件一并存下。 * * 为什么是声明而不是跑起来再认一遍:「入参的 subject 就是来件主题」是写清单时 * 就知道的事实,没有临场判断的成分 —— 交给模型每单认一遍,是拿一整拍的 token 换一个常量。 * 而且人一点开这单,沟通记录里就已经躺着那封信,不必等模型跑几拍。 */ inboundComm: { channel: 'mail', partyField: 'customerEmail', subjectField: 'subject', bodyField: 'body', attachmentsField: 'attachments', }, }, output: { /* * 出口载荷**从落地事实推导**(§7.5):标量字段必须有台账列承载。 * 两枚去向旗就是下游两道网关唯一读的东西 —— 没有 `need_alternative` / * `need_sourcing` 这两列,整条流出了受理口就没路可走(踩过:G1 全流死路)。 */ mandatory: [ { name: 'intake', payload: { type: 'object', required: ['records', 'needAlternative', 'needSourcing'], properties: { records: { type: 'array', title: '询价受理记录(已打标)' }, needAlternative: { type: 'boolean', title: '需要选型' }, needSourcing: { type: 'boolean', title: '需要寻源' }, }, }, description: '询价受理记录(打标)+ 两枚去向旗', }, ], /* * **三条出边 = 一个 XOR 选择组**(2026-07-31 用户定,对齐流程包 E2/E3/E4)。 * * 受理这件事办完,这一单只会往三个地方去其中一个: * · 只有 A 类(有历史价、行情稳)→ 直通定价; * · 有 C 类要替代 → 去选型(选型定案后仍可能回头寻价); * · 无选型但有 B 类 → 去寻源。 * * 从前这里是空的:活动契约只说得出「我发一个 intake 口」,三条边的知识 * 全长在流程包的边条件上 —— 光看活动清单不知道它有三个出口, * 换一条流来编排就得重新猜。现在**契约自己说得出**。 * * 两枚旗照旧留在 `intake` 载荷里:下游两道网关读的是它们, * 撤掉等于把 G1 全流死路那个坑再踩一遍(踩过)。选口与旗必须自洽 —— * 说明书里写死了对应关系,放行确认时选口随之落定。 */ selective: [ { group: 'intake-route', /* * **三条按序求值,首个命中即出** —— 与报告上那张 G2 规则表逐条对应。 * 每一口都写了 `when`,所以这一单往哪走是**算出来的**: * 放行时基座自己判,卡上只如实告诉人走哪一口、凭哪一条,不摆成选择题。 * 去向是分类表那几个数的函数;让人凭印象点一下, * 等于把一个可回放的判定换成一次不可回放的手感。 */ ports: [ { name: 'to-alternative', payload: { type: 'object', required: ['rfqRef', 'records', 'altRequests'], properties: { rfqRef: { type: 'string', title: '询价单号(单标识 —— 下游台账的主键)' }, records: { type: 'array', title: '询价受理记录(已打标)' }, altRequests: { type: 'array', title: '替代评估请求' }, }, }, description: '有 C 类要替代 —— 去选型', /* ① 有 C 类就先去选型(替代定案后仍可能回头寻价) */ when: 'quote_intake_record_type_tag_c > 0', }, { name: 'to-sourcing', payload: { type: 'object', required: ['rfqRef', 'records', 'rfqRequests'], properties: { rfqRef: { type: 'string', title: '询价单号(单标识 —— 下游台账的主键)' }, records: { type: 'array', title: '询价受理记录(已打标)' }, rfqRequests: { type: 'array', title: '询价请求' }, }, }, description: '无选型但有 B 类 —— 去寻源', /* ② 无 C 类、有 B 类 → 寻源 */ when: 'quote_intake_record_type_tag_c = 0 and quote_intake_record_type_tag_b > 0', }, { name: 'to-pricing', payload: { type: 'object', required: ['rfqRef', 'records'], properties: { rfqRef: { type: 'string', title: '询价单号(单标识 —— 下游台账的主键)' }, records: { type: 'array', title: '询价受理记录(全 A 类)' }, }, }, description: '只有 A 类:有近期历史价且行情稳 —— 直通定价', /* ③ 前两条都不命中,才是「全 A 直通定价」 */ when: 'quote_intake_record_type_tag_c = 0 and quote_intake_record_type_tag_b = 0', }, ], }, ], }, /* * 必交付物**带可机检依据**:`ledger` 是台账地址,预检自己数行数。 * 不给地址,「缺一不得提交放行」就只能靠活动自报 —— 自报等于没有门。 */ requiredDeliverables: [ { key: 'intake-record', label: '询价受理记录', ledger: `${D}.record`, /* * 验收页上让 BD 就地改的那几列 —— **数据是他该管的**。 * * **`type_tag` 在其列**(2026-08-01 用户实测逼出来的):三分类是 P-03 算的, * 但算得对不对该由人说了算 —— 他改一颗料的类,就是在推翻一次判定,那是他的职权。 * 从前它不在这份名单里,于是人在件上把三条全改成 A、按了保存, * **改动被整个丢掉**(写回按这份名单摆格子,不在名单里的列压根不进载荷)—— * 屏上类徽照旧、去向照旧写着「去选型」。他以为自己改了,其实什么也没发生。 * * **去向仍不在其列**:那是 `type_tag` 的函数(见 output.selective 的 `when`)—— * 改类就够了,去向随之重算;再让人单独选一次去向,等于给同一件事开两个入口。 */ editableAtRelease: ['pn', 'customer_pn', 'category', 'eau', 'brand', 'target_price', 'remark', 'type_tag'], /** 这份成果由哪几张表承载 —— 分类表是判据来源、结论是结论栏,都是它的内部结构 */ ledgers: [`${D}.record`, `${D}.type_table`, `${D}.verdict`], }, /* * ── **料号分类表与受理结论不是两份成果**(2026-08-02 用户当场指出: * 「不都是包含在询价受理记录的结构里的吗,为啥单独陈列?」)──────────────── * * 从前它们各占一条 `requiredDeliverables`,于是人在成果面上看到三样东西 —— * 而在业务上受理这件事**只交出一份东西**:一份能往下走的询价受理记录。 * 分类表是它的判据来源、受理结论是它的结论栏,两者都是**同一份成果的内部结构**。 * * 现在合成一条,`ledgers` 说清它由哪几张表承载:落地那一屏按交付物点名, * 三张表的行数并进「询价受理记录」一行,不再各报各的。 */ ], /* * 验收判据逐条标真实等级。L1/L2 必带 predicate —— 当场对任务视图 S-FEEL 求值, * 不带就等于把「机检」写成了摆设(安装期 acceptance-level-misreported 挡这个)。 * * 谓词读的是**汇总列**(line_total / missing_key_total / untyped_total), * 不是逐行列:任务视图把草稿行摊平到顶层是「后写的赢」,拿逐行的 * missing_key_fields 求值只会验到最后写的那一行 —— 那不叫机检,叫抽样。 * 所以汇总列的名字与逐行列**故意不同名**,免得互相盖掉。 */ acceptanceCriteria: [ /* ① 定位客户 —— 认得出老客户或人建了新客户,这一单才落得到某个客户名下 */ { key: 'customer-known', label: '客户已确认:发件邮箱认出为老客户,或已为新客户建好档', level: 'L1', predicate: 'customer_total > 0' }, /* * 条目数**与真实行数对得上**。判据不读活动自报的那一个数就完了 —— * `quote_intake_record_rows` 是基座数出来的本单行数(草稿层),两边同时成立才算。 * 实测拦的是这个:PN 抄漏了后缀,补写时又成了新的一行,表里 10 行而 line_total 记着 9; * 从前两边都由活动自己报,自然对得上,人签完字,下游拿到一份多一行的交付物。 */ { key: 'lines-present', label: '来件已逐条归一:说了几条,受理记录里就躺着几行', level: 'L1', predicate: 'line_total > 0 and quote_intake_record_rows = line_total', }, /* * ② 必填与选填**分量不同**(2026-07-31 用户定): * · 必填(PN / 年用量)**是验收标准** —— 不齐不许放行; * · 选填(品牌 / 备注)**只提醒** —— 缺了照样放行,但那一问不能省。 * 所以选填这条判据不看「补齐没有」,看**问过没有**: * 客户不肯给品牌是客户的事,没去问过是我们的事。 */ /* * **读基座数出来的数,不读活动自报的**(2026-08-01 改)。 * * 从前是 `missing_key_total = 0` —— 那个数由模型自己重算。人在卡上补了年用量、 * 基座已经写回草稿层,而模型漏算一次这条判据就永远绿不了:连跑十轮里三轮死在这, * 每次都是同一条连拒五拍熔断。`quote_intake_record_missing` 由基座按 * 台账声明的 `required` 现数(缺在哪几行也一并摆进拒回),模型改不动也不必改 —— * 它只要把那一格补上。**一条只接受「是」而模型又左右不了的判据,是死胡同。** */ { key: 'required-complete', label: '必填齐:每一条都有型号 PN 与年用量 EAU', level: 'L1', predicate: 'quote_intake_record_missing = 0', }, /* * ── 这一条为什么**不是机检**(2026-08-01,实测逼下来的)────────────── * * 它原本是 L2:`missing_minor_total = 0 or asked_total > 0`。两个数都由活动自己写, * 于是这条判据对模型来说不是「去把那一问办了」,而是**「把这个数改小就能过」**。 * 实测就是这么走的:NE555P 只缺目标价,模型如实记了 missing_minor_total = 1, * 提交被这条判据连拒四次,第五拍它把那个数**改回了 0** —— 判据当场变绿、单子放行, * 而台账上从此写着「缺选填 0 条」,那一格空白再没人提起。 * * **机检判据只许读基座数得出来的数**(如 `域_对象_rows`);读活动自报的数, * 它就不是门,是一句「改哪个数能过」的提示。而「问过客户没有」是过程事实, * 基座此刻数不出来(卡上不带计划项号,数不清哪张卡是为这件事发的)—— * 那就**别假装机器判得了**:降到 L4 交人,并把「缺选填几条」摆到放行页的指标带上, * 签字的人一眼看得见。要它重回机检,得先让基座能按用途数卡(见 debt A1-3)。 */ /* * **「问过」不是结论,是过程**(2026-08-02 用户实测:「我这次没给目标价, * 你 L4 咋又达标了?」)。 * * 原文是「要么**问过客户**,要么放行时确认可以先受理」—— 而 A1 每一单必发一张 * 受理需求审定卡,「问过」于是恒真:这条判据永远能裁成绿,等于没有这道门。 * (上一版剧本还明写「发过卡问过就 satisfied=true」,是亲手把它焊死在绿上。) * * 结论只有两种:**缺口补齐了**,或者**还缺着、交给签字的人定**。 * 后者正是这条判据的价值 —— 把「还缺这几格」连同理由摆到他面前, * 而不是替他宣布「问过了就算数」。裁成不满足照样能提交,门不因此拦人。 */ /* * **前半句由基座数,后半句才交人**(2026-08-02 用户实测逼出来的)。 * * 「已补齐」是 `quote_intake_record_gaps = 0` —— 基座照台账声明的 `expect` * 现数(草稿层实时),模型算不算得对都不影响它。从前整条只读裁断, * 于是它凭记忆写理由:用户在件上把目标价填好了,它照样裁「未满足」, * 理由写「NE555P 缺目标价,客户未提供」。用户原话:「这个价格是我填写的, * 但是它判错了」。复现的那一单更离谱 —— 两行年用量都填着,它写 * 「共 4 处选填缺口」,而 EAU 压根是必填不是选填。 * * 等级仍是 L4:缺口真还在的时候,「先受理还是退回」确实只有人能定。 * 谓词为真时引擎当它已达标、把那句理由一并丢掉(见 `criterionCheck`)。 */ { key: 'optional-asked', label: '选填缺口(客户料号/品类/制造商/目标价)已补齐,或由你放行时确认可以先受理', level: 'L4', predicate: 'quote_intake_record_gaps = 0', }, /* * **这条判据读基座数出来的数**(2026-08-02 实测逼出来的,「机检只许读基座数得出来的数」 * 第三次被违反)。从前读活动自写的 `untyped_total` —— 那个数它改一下就骗过去了。 * * 现在比的是:**打了标的条数(基座按 `type_tag` 的取值域现数)= 受理记录行数**。 * 两边都由基座出,模型碰不着。 */ { key: 'typed', label: '每条都按三分类规则 P-03 打了标:A 直接报 / B 重估后报 / C 找替代', level: 'L2', predicate: 'quote_intake_record_rows > 0 and quote_intake_record_type_tag_a + quote_intake_record_type_tag_b + quote_intake_record_type_tag_c = quote_intake_record_rows', }, /* * ── 这里**去掉了四条**(2026-08-02 用户当场指出:「这些判据都删了吧」)────── * * `downgrade-safe`(L3 吃不准降 B)· `route-alt`(C 类都开了替代评估请求)· * `route-rfq`(B 类都开了询价请求)· `typed-on-record`(每条记录都带状况标)。 * * 四条都是**内部一致性**:打标依据对不对、几类料开了几张单、两张表行数齐不齐。 * 它们该由引擎自己看住(数对不上就是活没干完,拒回让模型改), * 而**放行卡是给签字的人看的**——他要判的是「这一单受理得对不对」, * 不是替引擎复核一遍它自己的账。一屏九条,真正要他过目的那几条反而淹了。 * * 落地那一侧不因此变松:派单条数与打标覆盖仍在剧本的收步判据里 * (S5/S6 的 `doneWhen` 读同一批基座读数),错了照旧走不到放行门。 * 「几张单开出去了」改在**验收之后**如实呈现(落地结果), * 那才是人真正关心的:这一签下去,世界上发生了什么。 */ /* * ── 这里**去掉了 `route-judgement`**(L4,2026-08-01 用户当场指出)────── * * 原文是「去向判断合乎业务常理(客户特殊要求、急单等)」。 * **去向已经不是判断了**:三个出口各写了 `when`,放行那一刻基座按序求值、 * 首个命中即出(`type_c_total > 0` …),人签的字确认的正是算出来的那一口。 * 再挂一条 L4 让模型裁一次「这个去向合不合理」,是让它给一个自己没做的决定盖章: * 裁「满足」是废话,裁「不满足」也改不动任何东西 —— 判据读的那几个数才改得动。 * 要改去向,改的是三类条数;那已经由 route-alt / route-rfq 两条机检看着了。 * * 一条判据得能**指向一个可做的动作**,否则它只是一道要人按一下的手续。 */ /* * L5 是**责任关**,不是质量关:它声明「这份交付物必须由人放行」, * 由 `T_release` 的人工激活 + 终局唯放行落实。 * 卡面上不摆它 —— **那张卡本身就是这道关**(基座统一滤,见 `engines.ts`)。 */ { key: 'owner-release', label: 'BD 放行:签下去这一单即正式受理并转入下游', level: 'L5' }, ], /* * 放行时人**可以改什么** —— 2026-08-01 改口径:**去向不在其列**。 * * 从前这里摆的是两枚去向旗,让 BD 在放行卡上当场改。那是把「这一单往哪走」 * 交给了一次手感 —— 而它本是**分类表算得出来的事实**(有没有 C 类、有没有 B 类), * 现在由契约的出口谓词按序求值定下(见 output.selective 的 `when`), * 卡上只如实告诉人走哪一口、凭哪一条。可回放的判定不该退化成一次不可回放的点击。 * * 那人在验收页上改什么?**改成果本身** —— 那份受理记录清单就绑在卡上, * 九行逐格可改,改完由基座写回草稿层、随放行合并(AI 不经手)。 * 数据是他该管的;去向不是。 */ revisableAtRelease: [], prohibitions: [ { key: 'no-guess-field', statement: '原文没写的字段不得凭常识补,缺就记缺' }, { key: 'no-guess-a', statement: '吃不准的一律降 B,不得臆断为 A' }, { key: 'no-unit-convert', statement: '不得自作主张换算用量口径(月用量×12 是业务判断,须问客户)' }, { key: 'no-external-write', statement: '不得在放行前写任何外部系统' }, ], authorization: { discretion: [ { key: 'ask-back', statement: '缺次要字段可受理并追问;缺 PN/EAU/交期必须退回补全' }, { key: 'lifecycle-check', statement: '疑似 EOL 的料可随手拉一次生命周期核查' }, ], /* 空着不是省事:这个字段全仓没有一个消费者(债 ⑨),写了也不会被执行 */ preauthorizedActions: [], humanCallBudget: 4, }, resources: { tools: [{ name: 'crm.history-price-read' }, { name: 'quote-intake.classify' }, { name: 'mail.send' }], skills: INTAKE_SKILLS.map((s) => s.id), models: ['deepseek-v4-flash'], roles: ['BD'], /* * SLA:受理<15min、判定<45min → activeMinutes 60 封顶;打扰上限 4。 * * token 预算按**实测**给:一份三条的 BOM 走完全程约 20 拍、18 万 token * (每拍的提示词都带着案例说明书 + 轨迹,越往后越长)。原来给 20 万, * 正好卡在收口前一两拍被熔断 —— 预算给得太紧,等于让 Harness 去杀正在干的活。 * 条目多的单会更贵,这里留一倍余量。 */ budget: { steps: 60, tokens: 400_000, activeMinutes: 60, humanCalls: 4 }, }, }, /* ── 声明 ────────────────────────────────────────────────────── */ declarations: { models: ['deepseek-v4-flash'], tools: [{ name: 'crm.history-price-read' }, { name: 'quote-intake.classify' }, { name: 'mail.send' }], ledger: [ /* * 客户主数据 —— **A1 只声明它要用的那几列**,全量结构由平台播种 * (`seed-mvp-data.ts` 的四域)。声明合并是「最大化合并、只增不破坏」, * 两边并存不冲突;`kind: 'master'` 必须与平台一致,否则治理台会把它归错类。 * * 为什么受理活动要写主数据:第一件事就是认人 —— 认得出取它的编码, * 认不出请人建档。建出来的那一行是**给下一单用的**:下次同一个邮箱来信, * 第一步就认得出,不必再问一遍。 */ { domain: 'customer', object: 'profile', kind: 'master', label: '客户档案', fields: [ { name: 'customer_code', type: 'text', primaryKey: true, masked: false, label: '客户编码' }, { name: 'name', type: 'text', primaryKey: false, masked: false, label: '客户名称' }, ], }, { domain: 'customer', object: 'contact', kind: 'master', label: '客户联系人', fields: [ { name: 'customer_code', type: 'text', primaryKey: true, masked: false, label: '客户编码' }, /* * **自增,不问人**(2026-08-02 用户:「联系人ID自增啊,不用填」)—— * 它不承载业务含义,只是「这家客户下的第几位」。人哪知道已经有几位; * 让模型编更糟,编重了就是覆盖别人那一行。基座在写入那一刻按 * `客户编码-NN` 生成(见 `FieldDeclaration.auto`)。 */ { name: 'contact_code', type: 'text', primaryKey: true, masked: false, auto: true, label: '联系人编码' }, { name: 'name', type: 'text', primaryKey: false, masked: false, label: '姓名' }, /* 邮箱是**认人的钥匙**,也是个人信息 —— 与平台声明一致标脱敏 */ { name: 'email', type: 'text', primaryKey: false, masked: true, label: '邮箱' }, ], }, { domain: D, object: 'record', 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_code', type: 'text', primaryKey: false, masked: false, label: '客户码' }, /* 客户自己的物料编码 —— 回价单必须按它对齐,否则客户那边对不上账 */ { name: 'customer_pn', type: 'text', primaryKey: false, masked: false, expect: true, label: '客户料号' }, { name: 'category', type: 'text', primaryKey: false, masked: false, expect: true, label: '品类' }, { name: 'brand', type: 'text', primaryKey: false, masked: false, expect: true, label: '制造商' }, /* * **年用量是必填** —— 标了 `required`,基座就数得出「缺必填的有几行」 * (读数 `quote_intake_record_missing` / `_missing_keys`)。 * * 为什么非要基座来数(2026-08-01,连跑十轮里三轮死在这): * 「必填齐」是 L1 机检,从前读的是活动自己写的 `missing_key_total` —— * 人在卡上补了年用量、基座已经写回草稿层,而那个汇总列还要模型自己重算; * 它算漏一次,这条判据就永远绿不了,连拒五拍熔断。 * PN 不必标:它是主键,没有 PN 那一行压根不存在。 */ { name: 'eau', type: 'integer', primaryKey: false, masked: false, required: true, label: '年用量 EAU' }, { name: 'target_price', type: 'number', primaryKey: false, masked: false, expect: true, label: '目标价' }, { name: 'lead_time', type: 'text', primaryKey: false, masked: false, label: '交期' }, { name: 'packaging', type: 'text', primaryKey: false, masked: false, label: '包装' }, { name: 'quality_req', type: 'text', primaryKey: false, masked: false, label: '品质要求' }, /* * 备注**不算选填缺口**(2026-08-02):`expect` 的意思是「客户该给而没给的」—— * 备注是我方写上去的注,一行没有备注不是缺了什么。 * 标着它,`_gaps` 几乎恒大于 0,那条 L4 判据于是永远绿不了 —— 门就废了。 */ { name: 'remark', type: 'text', primaryKey: false, masked: false, label: '备注' }, /* * **分类列:取值域给出来,条数由基座现数** —— `quote_intake_record_type_tag_a/b/c`。 * * 三分类条数从前是活动自己写的 `type_a_total` 之流,而分布条、放行时的去向谓词、 * 验收判据读的全是它们。于是人在放行页把九条料全改成 A:类徽变了, * 分布条还是 5/3/1、去向还是「去选型」—— 逐行变了,汇总数没人重算。 * **手工修改的全部意义就是纠正 AI 的判断**:改了类标而流向不跟着变,那个兜底就是假的。 */ { name: 'type_tag', type: 'text', primaryKey: false, masked: false, label: '状况标', tally: ['A', 'B', 'C'] }, { name: 'status', type: 'text', primaryKey: false, masked: false, label: '状态' }, { name: 'missing_key_fields', type: 'integer', primaryKey: false, masked: false, label: '本条缺关键字段数' }, ], }, { domain: D, object: 'type_table', 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' }, // 打标的三项判据必须落表:P-03 从这里读,不落就无米下锅 { name: 'history_days', type: 'integer', primaryKey: false, masked: false, label: '距上次报价天数' }, { name: 'market_stable', type: 'boolean', primaryKey: false, masked: false, label: '行情稳定' }, { name: 'lifecycle', type: 'text', primaryKey: false, masked: false, label: '生命周期' }, { name: 'history_hit', type: 'boolean', primaryKey: false, masked: false, label: '历史价命中' }, { name: 'last_price', type: 'number', primaryKey: false, masked: false, label: '上次成交单价' }, { name: 'type_tag', type: 'text', primaryKey: false, masked: false, label: '状况标' }, { name: 'basis', type: 'text', primaryKey: false, masked: false, label: '分级依据' }, ], }, { /* * 受理结论 —— **一单一行**。它承担三件别处放不下的事: * ① 两枚去向旗(下游两道网关唯一读的东西); * ② 三个汇总数(验收 L1/L2 机检谓词的入参); * ③ 一句人话结论(验收单上给人看的)。 */ 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: '客户码' }, /* * 客户名与新老 —— **报告上那两格,和 A4 的一条硬约束**。 * * 名字:认出来的老客户不必重写 `customer.profile`(那是主数据), * 于是报告要显示「这一单是哪家公司」就无处可取 —— 落一列在结论上, * 一份件自己说得出自己是谁,不必去连主数据表。 * 新老:新客户走首单口径,**A4 不得取历史价锚点**。这是下游的判据,不是装饰。 */ { name: 'customer_name', type: 'text', primaryKey: false, masked: false, label: '客户名称' }, /* * 联系人姓名 —— 报告「联系人」那张卡上摆的那个人,**本单对外发件的收件人**。 * * 它本来在 `customer.contact.name`(主数据)里,但报告取不到:老客户不重写主数据, * 于是本单的草稿里根本没有那一行;而顶层同名字段又与 `customer.profile.name` * 撞名(「后写的赢」),摆上去随时可能变成公司名 —— 一张写着公司名的联系人卡 * 比空着更坏。跟 customer_name 同一笔写下,一份件自己说得出这单是跟谁在谈。 */ { name: 'contact_name', type: 'text', primaryKey: false, masked: false, label: '联系人姓名' }, { name: 'customer_kind', type: 'text', primaryKey: false, masked: false, label: '客户属性(老客户/新客户)' }, /* ① 定位客户那一步的收步判据 —— 认出老客户或人建了新客户,都记 1 */ { name: 'customer_total', type: 'integer', primaryKey: false, masked: false, label: '客户已确认' }, /* * 「是不是新客户」与「问过人没有」—— 这两个数是**建档必须经过人**的机检形态。 * 实测:剧本写了「别自己编客户编码」,模型照样从邮箱域名编出 CUST-SZ-MACH * 直接建了档。说明书管不住,得让收步条件管:新客户没问过人就收不了步。 */ { name: 'customer_new_total', type: 'integer', primaryKey: false, masked: false, label: '新客户(需建档)' }, { name: 'customer_asked_total', type: 'integer', primaryKey: false, masked: false, label: '已就建档问过人' }, { name: 'line_total', type: 'integer', primaryKey: false, masked: false, label: '有效条目数' }, /* * 来件一共几条、本轮不报几条 —— 「有效 PN 9 / 10 行 BOM」那句话的出处。 * 没有这两个数,「9 条」是个孤零零的数字:人看不出漏没漏, * 而客户点名跳过的那一条到底有没有被看见,也就无从对账。 */ { name: 'bom_total', type: 'integer', primaryKey: false, masked: false, label: '来件条目数' }, { name: 'skipped_total', type: 'integer', primaryKey: false, masked: false, label: '本轮不报条数' }, { name: 'skipped_note', type: 'text', primaryKey: false, masked: false, label: '不报的是哪几条·为什么' }, /* * ② 「问过没有」—— 选填缺口靠**问过**来解,不靠补齐。 * 用户定的口径:必填项是验收标准,选填项只提醒,但两种缺失都必须经过人。 */ { name: 'asked_total', type: 'integer', primaryKey: false, masked: false, label: '已就缺口问过人的次数' }, /* * **问过 ≠ 答过**。收步判据从前只看 `asked_total`:卡一发出去它就为真, * 于是人填完提交、引擎在下一拍开头就把这一步收了 —— 模型根本没轮到把答案写回台账, * 那一格白填(实测:人补的制造商落了空,而台账里那一列仍旧是空的)。 * 收步要等的是**答案落了表**,所以另记一个数。 */ { name: 'answered_total', type: 'integer', primaryKey: false, masked: false, label: '人答完并已落表的次数' }, /* * **这一单经过人的手了没有** —— 阶段2 那张受理需求执行页签回来才记 1。 * * 它存在的理由是一次实测:从前「经过人」是靠 `missing_minor_total = 0 or * answered_total > 0` 表达的 —— **只在缺东西的时候才叫人**。于是字段齐全的 * 一封询价进来,这个条件在拆完那一刻就为真,引擎自己把归一那步收了, * 人一眼没看过受理需求,下一次被叫到已经是终局验收 * (用户 2026-08-01:「我都没确认需求 它就让我验收了」)。 * 确认不是缺口的补丁,是一道**恒在**的门,所以它得有自己的一个数。 */ { name: 'confirmed_total', type: 'integer', primaryKey: false, masked: false, label: '受理需求已经人审定' }, { name: 'missing_key_total', type: 'integer', primaryKey: false, masked: false, label: '缺必填字段条数(PN/年用量)' }, { name: 'missing_minor_total', type: 'integer', primaryKey: false, masked: false, label: '缺选填字段条数(品牌/备注)' }, { name: 'untyped_total', type: 'integer', primaryKey: false, masked: false, label: '未打标条数' }, /* * 分类与派单的条数 —— 「去向旗与状况标对得上」这件事**必须机检**。 * 它本是 L4 交人判的:实测里人(我)盲签了通过,而 C 类那条的替代评估请求 * 根本没开 —— 旗立着、单没开,下游 A2 收到一个空活。 * 算术不该靠人的注意力:把条数落成列,判据就能自己对账。 */ { name: 'type_a_total', type: 'integer', primaryKey: false, masked: false, label: 'A 类条数' }, { name: 'type_b_total', type: 'integer', primaryKey: false, masked: false, label: 'B 类条数' }, { name: 'type_c_total', type: 'integer', primaryKey: false, masked: false, label: 'C 类条数' }, /* * **去向定过了没有**(2026-08-02):从前这儿是 `alt_request_total` / * `rfq_request_total` 两个派单计数 —— 而 A1 已经不开单了(见下面那段注释), * 那两个数没了对象。收步仍要一个「这一步办过了」的凭据: * 旗是布尔(合法值含 false),分不清「没写」与「写了 false」,所以另记一个计数。 */ { name: 'route_decided_total', type: 'integer', primaryKey: false, masked: false, label: '已定去向' }, { name: 'need_alternative', type: 'boolean', primaryKey: false, masked: false, label: '需要选型' }, { name: 'need_sourcing', type: 'boolean', primaryKey: false, masked: false, label: '需要寻源' }, { name: 'summary', type: 'text', primaryKey: false, masked: false, label: '受理结论' }, ], }, /* * ── 这里**去掉了 `alt_request` / `rfq_request` 两张表**(2026-08-02 用户当场定)── * * 原话:「替代评估请求 / 询价请求这两项数据不在这里产出;随令牌流向下一个节点的 * 只有工作成果验收内的内容;进入选型替代后,活动会自己归一需要的数据。」 * * 从前 A1 逐条开这两张单交给 A2 / A3 —— 那是**替下游把它的输入先摆好**, * 而下游本来就要照自己的口径归一一遍:同一份事实在两个活动里各存一份, * 改一处就分叉,而分叉之后没人看得出以哪一份为准。 * A1 交出去的是**受理记录**(带状况标与两枚去向旗),下游读它即可。 * * 连带退休的:两条派单对账判据(同日删)、`alt_request_total` / `rfq_request_total` * 两个计数列、报告里那两张明细表、S6 的开单动作。 */ ], listeners: [ { /** 客户改需求:整单要回炉 —— 静态监听,时限必配(悬空监听安装时拒绝) */ origin: 'static', eventType: 'data', keyFilter: { kind: 'rfq-amend' }, /* * 语义只有两种:`observe`(记一笔观察,交 Agent 裁量)与 `complete-step`(驱动步完成)。 * 客户改需求要的是「整单回炉」,那是 Agent 看到这条观察后自己决定重入哪一段 —— * 监听没有「重入」这一档,也不该有:重入是业务判断,不是事件语义。 */ semantic: 'observe', deadline: 'P1D', escalation: ['nudge', 'escalate'], }, ], harness: { memoryWindow: 20, // SLA:判定<45min —— 停滞检测按此设;受理<15min 那一档做催办 stallAfter: 'PT45M', nudge: [{ after: 'PT15M', action: 'nudge' }], retry: { times: 2, backoff: 'PT30S' }, }, }, /* ── 资产初始版 ──────────────────────────────────────────────── */ assets: { businessModel: { cmmnXml: CMMN_XML, narrative: NARRATIVE, dmn: INTAKE_DMN, /* 步骤剧本 —— 引擎按它组织每一拍的上下文,并自己收掉成果已在的步骤 */ steps: INTAKE_STEPS, /* 阶段 —— 只给人看的进度刻度:三段激活态 + 放行收口 = 卡上的 x/4 */ phases: INTAKE_PHASES, }, skills: INTAKE_SKILLS, // 四张模版对应四个局面;在办与归档同一张(§9.3 第 2 条) viewTemplates: [ { name: 'intake-overview', html: INTAKE_OVERVIEW }, { name: 'intake-classified', html: INTAKE_CLASSIFIED }, { name: 'intake-release', html: INTAKE_RELEASE }, ], cardSchemas: {}, distillHooks: [ { on: 'terminal', prompt: '归纳本单受理归一的经验:字段缺口形态、打标依据、去向判断被驳回的理由。', }, ], }, /* ── 样例集 —— 首装审以静态审 + 样例集替代回放审 ───────────────── */ /* * 样例就是**真客户会发的那种信**(2026-07-31 用户定)。 * * 从前两个样例都写着「我司客户编码 C001」—— 那是我们内部的编码, * 客户凭什么知道?真实来函里人只会说**自己是哪个公司的谁**。 * 拿带编码的样例去试,等于把「定位客户」这一步的难处提前替它做掉了: * 认不出客户、要请人建档的那条路根本走不到,而那恰恰是这一状态的主路。 */ samples: [ { /* * **新客户** —— 这个邮箱**不在** `customer.contact` 里。 * * 它跟样例 2 必须是两家公司、两个邮箱。曾经两条样例用的是同一个邮箱 * (`purchasing@sz-machinery.com`,正是随主数据播下的那位老客户), * 于是「认不出 → 发数据录入卡请人建档」这条路**一次也没被走到过** —— * 两条样例在第一个状态上是同一条路,而卡面上写着它们是两条。 * 认客户的钥匙只有邮箱:换名字不算换客户,换邮箱才算。 */ name: '新客户询价(三条:在产/波动/EOL)—— 认不出客户,要请人建档', input: { customerEmail: 'sourcing@qd-marine.com', subject: '【询价】RFQ-2026-0001 车规 Analog 三项', body: '你好,\n我是青岛海洋电子有限公司的李娜,采购部。\n' + '我们是第一次和贵司询价,本次三项,请按标准交期报价:\n' + 'LM358DR 12K/年 目标价 8.0 交期8周;NE555P 5K/年 交期6周;TL072CDR 3K/年 目标价 22 交期10周\n' + '付款方式请一并给个建议,谢谢。\n李娜 | 青岛海洋电子有限公司', attachments: [], }, expect: { ports: ['intake'] }, }, { /* * ── **老客户案例 · Itelma MD14.1** ──────────────────────────────── * * 这一条**不是编的**:正文、附件 BOM、缺口位置、三分类结果,逐项照 * `~/coding/JCAI_PRD/docs/活动执行页/A1-询价受理需求.html`(阶段2 执行目标) * 与 `A1-询价受理记录.html`(阶段3 验收目标)两份目标件抄下来。 * 那两份是「这个活动办完该长什么样」的答案,这一条是「照它跑一遍」的题面 —— * 题面与答案不同源,测出来的东西就不作数。 * * 它一条压住 A1 全部要害: * · 邮箱在 `customer.contact` 里(CU-0142)→ 第一状态认得出,**不发建档卡** * (与样例 1 的新客户正好两条路); * · 料在**附件**里不在正文里 —— 正文只给口径要求(CNY含税/EXW/交期按周/ * AEC-Q200/预授权 Analog/跳过 XTSD),逼着走「读附件」那条路; * · 附件 10 行,其中 XTSD 那行客户明说本轮不报 → **有效 PN 应是 9 条**; * · 两处缺口:UMW AO6604 缺制造商(俄文 BOM 那一列为空)、DAG… 缺年用量 * → 必填缺(年用量)要派执行卡,选填缺(制造商)只提醒; * · 补齐后三分类判出 **5A / 3B / 1C**,有 C 类 → 去向是选型(to-alternative)。 * * 附件给的是**基座读 xlsx 之后的形态**(制表符分列的文本),不是二进制: * 契约里 `attachments[].text` 就是「读出来的文本」,活动看见的正是这个。 */ name: '老客户询价 · Itelma MD14.1(认得出客户 · 料在附件 · 10 行报 9 条)', input: { customerEmail: 'e.volkova@itelma.ru', subject: 'RFQ MD14.1 — 10 positions, BOM attached', body: 'Dear Colleagues,\n\n' + 'We are preparing the sourcing package for our new project MD14.1 and would like to request ' + 'your quotation for the 10 positions listed in the attached BOM.\n\n' + 'Please note the following requirements:\n' + '1. Prices in CNY including VAT, terms EXW.\n' + '2. Lead time in weeks. Please state SPQ and MOQ separately.\n' + '3. For position 4-02-01812 (TVM6C160K203R, varistor) AEC-Q200 certification is mandatory ' + '— please attach the certificate together with your offer.\n' + '4. Where the column "Analog" is filled in, the listed replacement is already pre-approved ' + 'by our engineering department. Any other analog requires our written confirmation.\n' + '5. Please indicate the validity period of your prices.\n\n' + 'Position 4-00-04187 (XTSD series, SD controller) is not finalised on our side yet ' + '— please skip it in this round.\n\n' + 'We would appreciate receiving your offer by 17 July.\n\n' + 'Best regards,\nEkaterina Volkova\nProcurement Department, Itelma', attachments: [ { name: 'BOM_Itelma_MD14.1.xlsx', contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', size: 18_432, ref: 'sample-bom-md141', /* 基座读 xlsx 的产物:首行列名,其后一行一条,制表符分列;空格 = 客户没填 */ text: [ 'Itelma Code\tPN\tКатегория\tПроизводитель\tAnalog\tСертификация\tEAU (k)\tTarget price', '4-02-01812\tTVM6C160K203R\tВаристор\tThinking Electronic\t\tAEC-Q200\t30\t', '4-02-01733\tPESD2ETH-XR\tДиодная сборка\tNexperia\t\t\t30\t', '4-14-01389\tBSCH00160808R12J00\tДроссель\tYAGEO\tCLH1608T-R12J-S\t\t60\t', '4-14-01455\tBWVF005050201R0T00\tДроссель\tYAGEO\tLVF505020-1R0T-N\t\t90\t', '4-00-04102\tCJTB0101R6N\tМикросхема\tJiangsu Changjing\t\t\t60\t', '4-00-03987\tSGM2202-ADJYN6G/TR\tМикросхема\tSGMicro\t\t\t30\t', '4-02-01698\tUMW AO6604\tТранзисторная сборка\t\t\t\t30\t', '4-14-01502\tBBPY00201209601Y00\tФильтр\tYAGEO\tPBY201209T-601Y-N\t\t30\t', '9-00-00251\tDAG2400L321504XZB01-R0\tАнтенна\t浙江嘉康\t\t\t\t', '4-00-04187\tXTSD04GLGEAQG-I\tКонтроллер SD\t\t\t\t\t', ].join('\n'), note: 'xlsx 已读出 11 行(含表头),10 条料', }, ], }, expect: { ports: ['intake'] }, }, ], entry: { file: 'index.js', checksum: 'dev-entry-quote-intake' }, };