>, tag: string): number {
- return rows.filter((r) => r['type_tag'] === tag).length;
-}
+export default defineActivity({ name: 'quote-intake' });
diff --git a/activities/quote-intake/src/manifest.ts b/activities/quote-intake/src/manifest.ts
index 55b99c7..e77b9f6 100644
--- a/activities/quote-intake/src/manifest.ts
+++ b/activities/quote-intake/src/manifest.ts
@@ -1,182 +1,730 @@
/**
- * 活动包清单 —— 统一报价 · 客户RFQ受理(受理归一)。
+ * 活动包清单 —— 统一报价 A1 · 询价受理。
*
- * 业务素材出处:docs/MVP示例工作流.html §1.1;按 README《归一化对照表》映射为:
- * SOP 节点 → **标准活动(flow)**;作业指导书 S1…S6 → **步序声明**(四种步型);
- * 步内判断 D1 完整度 → **条件谓词**;三分类规则 P-03 → **DMN 决策表**;
- * SLA(受理<15min、打标<45min)→ Harness 时限与预算。
+ * 业务素材出处:`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 { INTAKE_SKILLS } from './skills.ts';
-import { CMMN_XML, NARRATIVE } from './case-model.ts';
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 INTAKE_DOMAIN = 'quote_intake';
+const D = 'quote_intake';
const ACTIVITY_ID = activityIdSchema.parse('quote-intake');
/**
- * 三分类规则 P-03 —— 把「装得下的判断」从 LLM 手里拿走。
- * 由纯工具步 `quote-intake.classify` 逐条求值,命中规则行留痕。
+ * 两张判定表 —— **能算的不问 LLM**:同输入必同结论、零 token、命中行留痕。
+ *
+ * F-01 受理字段完整度(《询价受理规范》):三要素齐才放行,缺关键字段退回补全。
+ * P-03 三分类:A 历史价≤90天且行情稳 / B 无历史·过期·波动 / C EOL·缺货·要替代。
+ *
+ * 两张都是 `FIRST`:规则从上往下,第一条命中即出结论 —— 顺序本身就是业务优先级,
+ * 所以 C 的那一行必须在最前面(EOL 的料哪怕历史价很新,也不该判成 A 直接快答)。
*/
-export const P03_DMN: DmnModel = {
- drd: [{ decision: 'p03-type', requires: [], inputs: ['historyDays', 'marketStable', 'lifecycle'] }],
- tables: [{
+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' as const,
+ hitPolicy: 'FIRST',
inputs: [
- { name: 'historyDays', expression: 'historyDays' },
- { name: 'marketStable', expression: 'marketStable' },
+ { name: 'history_days', expression: 'history_days' },
+ { name: 'market_stable', expression: 'market_stable' },
{ name: 'lifecycle', expression: 'lifecycle' },
],
- outputs: [{ name: 'type' }, { name: 'play' }],
+ 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: '客户RFQ受理',
- packageVersion: '0.1.9',
+ name: 'A1 · 询价受理',
+ packageVersion: '40.0.0',
category: '统一报价',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-quote-intake',
},
- /* 契约七件 */
+ /* ── 契约七件 ────────────────────────────────────────────────── */
contract: {
activityId: ACTIVITY_ID,
- version: '0.1.0',
+ version: '1.0.0',
input: {
+ /*
+ * 入参就是**那封信** —— 客户邮箱 / 主题 / 邮件内容 / 邮件附件,前三样必填。
+ *
+ * 从前这里收的是「客户编码 + 询价单号」,那是**受理之后**才有的东西:
+ * 一封真的询价来函不会带着我们内部的客户码,单号也常常压根没有。
+ * 把它们摆成必填的后果实测过:流程包起单时通道抽不出这两个键,基座当场发补全卡,
+ * 整单 0 拍停在那儿等人 —— 而信本身是完整的。认客户、认单号是 S1 的活,不是门口的闸。
+ *
+ * 起单仍是宽进:门口不拒缺项(缺 required 由基座发补全卡,不是活动的事)。
+ */
data: {
type: 'object',
- required: ['customerCode', 'rfqRef'],
+ required: ['customerEmail', 'subject', 'body'],
properties: {
- customerCode: { type: 'string' },
- rfqRef: { type: 'string' },
- subject: { type: 'string' },
- /** 询价原文 —— 归一步拆解的对象 */
- body: { type: 'string' },
+ 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: '客户口头急单等特殊性写这里',
+ 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'],
- properties: { records: { type: 'array' } },
+ required: ['records', 'needAlternative', 'needSourcing'],
+ properties: {
+ records: { type: 'array', title: '询价受理记录(已打标)' },
+ needAlternative: { type: 'boolean', title: '需要选型' },
+ needSourcing: { type: 'boolean', title: '需要寻源' },
+ },
},
- description: '统一受理记录(已打标)',
+ 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',
+ },
+ ],
},
],
- selective: [],
},
+ /*
+ * 必交付物**带可机检依据**:`ledger` 是台账地址,预检自己数行数。
+ * 不给地址,「缺一不得提交放行」就只能靠活动自报 —— 自报等于没有门。
+ */
requiredDeliverables: [
- { key: 'intake-record', label: '统一受理记录(打标)' },
- { key: 'type-table', label: '料号分类表' },
+ {
+ 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: 'fields-complete', label: '八字段齐(PN/EAU/交期为必要三要素)', level: 'L1', predicate: 'missingKeyFields = 0' },
- { key: 'typed', label: '每条都已打标 A/B/C', level: 'L2', predicate: 'untyped = 0' },
- { key: 'downgrade-safe', label: '吃不准降 B,不许臆断 A', level: 'L3' },
- { key: 'owner-release', label: 'BD 放行', level: 'L5' },
+ /* ① 定位客户 —— 认得出老客户或人建了新客户,这一单才落得到某个客户名下 */
+ { 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/数量/交期必须退回补全' }],
+ discretion: [
+ { key: 'ask-back', statement: '缺次要字段可受理并追问;缺 PN/EAU/交期必须退回补全' },
+ { key: 'lifecycle-check', statement: '疑似 EOL 的料可随手拉一次生命周期核查' },
+ ],
+ /* 空着不是省事:这个字段全仓没有一个消费者(债 ⑨),写了也不会被执行 */
preauthorizedActions: [],
humanCallBudget: 4,
},
resources: {
- tools: [{ name: 'crm.history-price-read' }],
+ 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'],
- budget: { steps: 40, tokens: 120_000, activeMinutes: 60, humanCalls: 4 },
+ /*
+ * 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' }],
+ tools: [{ name: 'crm.history-price-read' }, { name: 'quote-intake.classify' }, { name: 'mail.send' }],
ledger: [
+ /*
+ * 客户主数据 —— **A1 只声明它要用的那几列**,全量结构由平台播种
+ * (`seed-mvp-data.ts` 的四域)。声明合并是「最大化合并、只增不破坏」,
+ * 两边并存不冲突;`kind: 'master'` 必须与平台一致,否则治理台会把它归错类。
+ *
+ * 为什么受理活动要写主数据:第一件事就是认人 —— 认得出取它的编码,
+ * 认不出请人建档。建出来的那一行是**给下一单用的**:下次同一个邮箱来信,
+ * 第一步就认得出,不必再问一遍。
+ */
{
- domain: INTAKE_DOMAIN,
- object: 'record',
- label: '统一受理记录',
+ domain: 'customer',
+ object: 'profile',
+ kind: 'master',
+ label: '客户档案',
fields: [
- { name: 'customer_code', type: 'text', primaryKey: true, masked: false, label: '客户码' },
- { name: 'pn', type: 'text', primaryKey: true, masked: false, label: 'PN' },
- { name: 'brand', type: 'text', primaryKey: false, masked: false, label: '厂牌' },
- { name: 'eau', type: 'integer', primaryKey: false, masked: false, label: 'EAU' },
- { name: 'type_tag', type: 'text', primaryKey: false, masked: false, label: 'Type 标' },
- { name: 'target_price', type: 'number', primaryKey: false, masked: false, label: '目标价' },
- { name: 'lead_time', type: 'text', primaryKey: false, masked: false, label: '交期' },
- { name: 'status', type: 'text', primaryKey: false, masked: false, label: '状态' },
- { name: 'missing_key_fields', type: 'integer', primaryKey: false, masked: false, label: '缺关键字段数' },
+ { name: 'customer_code', type: 'text', primaryKey: true, masked: false, label: '客户编码' },
+ { name: 'name', type: 'text', primaryKey: false, masked: false, label: '客户名称' },
],
},
{
- domain: INTAKE_DOMAIN,
+ 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: 'pn', type: 'text', primaryKey: true, masked: false },
- { name: 'type_tag', type: 'text', primaryKey: false, masked: false },
- { name: 'basis', type: 'text', primaryKey: false, masked: false, label: '分级依据' },
- { name: 'history_hit', type: 'boolean', primaryKey: false, masked: false, label: '历史价命中' },
- // 打标的三个判据必须落表:S5 的 DMN 从这里读,不落就无米下锅
+ { 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: 'human',
- keyFilter: { kind: 'elicitation' },
- semantic: 'complete-step',
- // 时限必配 —— 悬空监听安装时拒绝
+ eventType: 'data',
+ keyFilter: { kind: 'rfq-amend' },
+ /*
+ * 语义只有两种:`observe`(记一笔观察,交 Agent 裁量)与 `complete-step`(驱动步完成)。
+ * 客户改需求要的是「整单回炉」,那是 Agent 看到这条观察后自己决定重入哪一段 ——
+ * 监听没有「重入」这一档,也不该有:重入是业务判断,不是事件语义。
+ */
+ semantic: 'observe',
deadline: 'P1D',
escalation: ['nudge', 'escalate'],
},
],
harness: {
memoryWindow: 20,
- // SLA:受理<15min、打标<45min —— 停滞检测按此设
+ // SLA:判定<45min —— 停滞检测按此设;受理<15min 那一档做催办
stallAfter: 'PT45M',
nudge: [{ after: 'PT15M', action: 'nudge' }],
retry: { times: 2, backoff: 'PT30S' },
},
},
- /* 资产初始版:flow = 步序声明 */
+ /* ── 资产初始版 ──────────────────────────────────────────────── */
assets: {
- /* 资产初始版:案例模型(2026-07-29 起活动只有这一种业务模型) */
businessModel: {
cmmnXml: CMMN_XML,
narrative: NARRATIVE,
- dmn: P03_DMN,
+ 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 },
@@ -186,16 +734,111 @@ export const manifest: ActivityManifest = {
distillHooks: [
{
on: 'terminal',
- prompt: '归纳本单受理归一的经验:字段缺口形态、打标依据、被驳回的理由。',
+ prompt: '归纳本单受理归一的经验:字段缺口形态、打标依据、去向判断被驳回的理由。',
},
],
},
- /* 样例集 —— 首装审以静态审 + 样例集替代回放审 */
+ /* ── 样例集 —— 首装审以静态审 + 样例集替代回放审 ───────────────── */
+ /*
+ * 样例就是**真客户会发的那种信**(2026-07-31 用户定)。
+ *
+ * 从前两个样例都写着「我司客户编码 C001」—— 那是我们内部的编码,
+ * 客户凭什么知道?真实来函里人只会说**自己是哪个公司的谁**。
+ * 拿带编码的样例去试,等于把「定位客户」这一步的难处提前替它做掉了:
+ * 认不出客户、要请人建档的那条路根本走不到,而那恰恰是这一状态的主路。
+ */
samples: [
{
- name: '车规 Analog 询价',
- input: { customerCode: 'C001', rfqRef: 'mail:m-1001', subject: '询价 RFQ - 车规 Analog' },
+ /*
+ * **新客户** —— 这个邮箱**不在** `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'] },
},
],
diff --git a/activities/quote-intake/src/skills.ts b/activities/quote-intake/src/skills.ts
index 5116652..2a67943 100644
--- a/activities/quote-intake/src/skills.ts
+++ b/activities/quote-intake/src/skills.ts
@@ -6,6 +6,8 @@
* + 结晶片段脚本(可选:反复出现的确定性子序列)。
*
* **载方法,不载权力**:这里提到的工具都在本活动白名单内;技能不扩权、不改约。
+ * 渐进披露:常驻上下文只有 `id label:applicability`,正文 Agent 说要才加载 ——
+ * 一次把所有正文塞进去,上下文会被方法论淹掉,真正的局面反而看不见。
*/
import { skillIdSchema } from '@contracts';
import type { Skill } from '@contracts';
@@ -20,17 +22,21 @@ export const INTAKE_SKILLS: Skill[] = [
'目标价常带货币符号或「目标价/target」,交期常带「周/weeks/wks/交期/lead」。',
'',
'三条纪律:',
- '① **原文没写的不要补**。宁可标 missingKeyFields,也不要按常识填一个「合理值」——',
+ '① **原文没写的不要补**。宁可把 missing_key_fields 记上,也不要按常识填一个「合理值」——',
' 受理阶段编出来的数,会一路流到定价和报价单上,越往后越难发现。',
'② **单位归一但不换算口径**:12K → 12000 可以;「月用量 1000」不要自作主张乘 12,',
' 那是业务判断,该走补全卡问客户。',
'③ **一条需求一行**。同一 PN 不同数量档是两行,不是一行的两个值。',
'',
- '拆不动就交人:缺 PN / 数量 / 交期任一,把 missingKeyFields 加一,让 D1 判断把补全卡发出去。',
+ '写台账时一拍写完:`ledger.write` 的 rows 收数组,一份 BOM 几十条也是一次写完 ——',
+ '一行一拍等于把同一件事拆成几十次模型调用,每次都要重读一遍上下文。',
+ '',
+ '写完顺手把 `quote_intake.verdict` 的 line_total 与 missing_key_total 填上:',
+ '那两个数是完整度判定表 F-01 的入参,也是验收的机检判据 —— 不写,下一步无从判起。',
].join('\n'),
crystallized: [],
- knowledgeRefs: ['《询价受理规范》'],
- version: '1.0.0',
+ knowledgeRefs: ['《询价受理规范》F-01'],
+ version: '2.0.0',
},
{
id: skillIdSchema.parse('intake.classify-basis'),
@@ -39,15 +45,48 @@ export const INTAKE_SKILLS: Skill[] = [
method: [
'P-03 只认三个入参:距上次报价天数、行情是否稳定、生命周期状态。三者齐了,结论是**算出来**的,不是判出来的。',
'',
- '所以打标前唯一该做的事是**把判据取齐**:调历史价情报工具,拿回这三项。',
- '取不到(新料号无历史)不是错误——工具会回「无历史」,那本身就是判据:',
- '距上次报价记为极大值 → 命中 B 类「重估后报」,这是对的,新料号本就该重估。',
+ '所以打标前唯一该做的事是**把判据取齐**:调 `crm.history-price-read`,',
+ '**参数 pns 收数组** —— 这一单所有 PN 一次传进去。一条一条调,每调一次就多烧一拍模型,',
+ '而拿回来的东西一模一样。',
'',
- '不要绕过决策表自己判。规则表是可回放、可追责的;临场判断不是。',
+ '取不到不是错误:新料号无历史,工具回 historyDays=9999、hit=false。',
+ '**那本身就是判据** —— 距上次报价极大 → 命中 B 类「重估后报」,这是对的:新料号本就该重估。',
+ '',
+ '判据逐条写进 `quote_intake.type_table`(history_days / market_stable / lifecycle),',
+ '再调 `quote-intake.classify` 一次把这几行传进去拿回 A/B/C。',
+ '**不要绕过决策表自己判**:规则表可回放、可追责;临场判断不是。',
].join('\n'),
- // 结晶片段:反复出现的确定性子序列 —— 取历史价这一步每单都一样
- crystallized: [{ tool: 'crm.history-price-read', input: { pn: 'pn' } }],
+ // 结晶片段:取历史价这一步每单都一样 —— 确定性子序列,不必每次让模型重想
+ crystallized: [{ tool: 'crm.history-price-read', input: { pns: 'pns' } }],
knowledgeRefs: ['三分类规则 P-03'],
- version: '1.0.0',
+ version: '2.0.0',
+ },
+ {
+ id: skillIdSchema.parse('intake.route-and-dispatch'),
+ label: '定去向:状况标不等于去向',
+ applicability: '打标完成、要决定这一单往哪走(要不要选型、要不要寻源)时。',
+ method: [
+ '**状况标是判断的输入,不是去向本身**。同一个 B 标,可能只需寻源,也可能连带选型 ——',
+ '把标直接当去向用,是这一步最常见的错。',
+ '',
+ '两问分开答,可以同时成立:',
+ '· **要不要选型**:原厂料能正常供货且客户没点名替代 → 不需要;',
+ ' EOL/停产、长交期缺货,或客户点名要替代 → 需要,交 A2 替代选型。',
+ '· **要不要寻源**:无近期历史价或行情波动(B 类)→ 需要,交 A3 寻源询价。',
+ '· 两者可同时成立 —— 选型定案后仍要为替代料寻价。',
+ '',
+ '**不要在这一步开什么请求单**:随令牌流去下一个节点的只有**验收过的那份受理记录**,',
+ 'A2 / A3 拿到之后按自己的口径归一需要的数据。替它们先摆一份,',
+ '等于同一份事实在两个活动里各存一份 —— 改一处就分叉,而分叉之后没人看得出以哪份为准。',
+ '',
+ '答完把 `quote_intake.verdict` 的 need_alternative / need_sourcing 两枚旗写死。',
+ '**这两枚旗是下游两道网关唯一读的东西**:不写,整条流出了受理口就没路可走',
+ '(踩过:两枚旗没有台账列承载,G1 全流死路)。',
+ '',
+ '吃不准就走判断,不许臆断直通:直通省下的十分钟,错了要用三天返工换。',
+ ].join('\n'),
+ crystallized: [],
+ knowledgeRefs: ['三分类规则 P-03', 'docs/MVP示例工作流.html §1.1 S6/G1'],
+ version: '3.0.0',
},
];
diff --git a/activities/quote-intake/src/views/prd-style.ts b/activities/quote-intake/src/views/prd-style.ts
new file mode 100644
index 0000000..9d2c0b5
--- /dev/null
+++ b/activities/quote-intake/src/views/prd-style.ts
@@ -0,0 +1,409 @@
+/**
+ * A1 两份执行页的样式表 —— **从 PRD 目标件原样搬过来**,不是我照着复述的。
+ *
+ * 出处:`~/coding/JCAI_PRD/docs/活动执行页/A1-询价受理需求.html` 的整段 `
+${PRD_SPRITE}
+
+
+
+ ${input.body}
+
+
+
`;
+
+/** 章节头 —— 原型的 `jc-rp-part`:小标签 + 细线 + 右端小签 */
+const part = (label: string, aside = '', tone = ''): string =>
+ `${label}${aside === '' ? '' : `${aside}`}
`;
+
+/** 落款 —— 署名取任务负责人(基座填);右端那枚签写「这份件在流里的位置」,不写状态 */
+const sign = (note: string, tag = ''): string =>
+ `
+
${ico('user', 15)}
+
${note}
+
${tag === '' ? '' : `${ico('git-branch', 13)}${tag}`}`;
+
+/* ==================== 共用件 ==================== */
+
+/**
+ * 来源信件 —— 原型的 `jc-letter`。正文 `pre-wrap` 原样照录:摘要过的原文就不是原文了。
+ * 收件签旁那枚归属签(通道入站 → 归了谁)与右端入站时刻都由基座填 ——
+ * 让活动自报「归属解析 → 冯兵」,等于让被指派的人自己说自己是谁。
+ */
+const LETTER = `
+
+ ${ico('mail-open', 12)}收件
+ · 归属解析 →
+
+
+
+
+
+
+ ${ico('sheet', 13)}
+
+`;
+
+/**
+ * 受理要素四卡 —— 原型的 `jc-four`。
+ * `kind` 决定第四张卡下面那条刻度条按什么上色:过程屏按缺口(红格 = 必填未补),
+ * 归档屏按状况标(A 绿 / B 蓝 / C 橙)。**一格一条料**,每格都对得上清单里的一行。
+ */
+const four = (kind: 'gap' | 'type'): string => `
+
+
${ico('building')}客户公司
+
+
${ico('building', 17)}
+
+
+
已命中客户资料库
+
+
+
+
${ico('user')}联系人
+
+
采购口 · 本单对外发件收件人
+
+
+
+ ${ico('history')}客户属性
+ ${ico('shield-check', 13)}
+
+ 成交台账尚未接入 —— 历史价由下游定价环节现查;
+ 新客户走首单口径,不得取锚点
+
+
+
+
+
${ico('boxes')}有效 PN
+
/ 行 BOM
+
+
+
+
+ ${
+ kind === 'type'
+ ? '一格一条料 · 按状况标着色 · 条已交下游'
+ : '一格一条料 · 红格是必填未补 条'
+ }
+
+
+
`;
+
+/**
+ * 有效 PN 清单 —— 原型那六列,一列不多一列不少。
+ *
+ * 「客户料号」原型明写不收(『Itelma Code 不是本记录的字段』),这里也不摆:
+ * 报告是给报价这条线看的,客户自己的编码在 A1 出口的载荷里,不占版面。
+ * `req`/`opt` 只决定空格子出哪种胶囊;归档屏一律 `dimempty`(那时空就是「客户没给」,
+ * 再摆「你还能填」的红胶囊就是在催一件已经办完的事)。
+ */
+const listTable = (mode: 'draft' | 'final'): string => {
+ /*
+ * `data-col` 一挂,这一格就多渲一个输入孪生件(基座 `twinInputs`),
+ * 待处理那一屏可就地改;写的是**台账列名**,不是表头文字 ——
+ * 表头改个说法不该让写回错位。归档件不挂:那时它是件,不是表单。
+ */
+ const cell = (key: string, head: string, kind: 'req' | 'opt', style = ''): string => {
+ const cls = mode === 'final' ? 'dimempty' : kind;
+ return ` | `;
+ };
+ const req = mode === 'final' ? '' : 'req';
+ /*
+ * 序号那一格在编辑态换成「撤掉这一行」的 × —— 一列两用,不为它单开一列。
+ * 只读时它是行号(CSS 计数器画的),编辑时人要的是「这一行不该在」的那个动作。
+ */
+ const del = ` | `;
+ return `
+
+ |
+ ${mode === 'final' ? '类 | ' : ''}
+ PN |
+ 品类 |
+ 年用量 |
+ 制造商 |
+ 目标价 |
+ 备注 |
+
+
+ ${del}
+ ${mode === 'final' ? ' | ' : ''}
+ ${cell('pn', 'PN', 'req', 'mono')}
+ ${cell('category', '品类', 'opt')}
+ ${cell('eau', '年用量', 'req', 'mono')}
+ ${cell('brand', '制造商', 'opt')}
+ ${cell('target_price', '目标价', 'opt', 'num')}
+ ${cell('remark', '备注', 'opt')}
+
+
`;
+};
+
+/** 编辑态那条提示带 —— 原型的 `jc-editbar`,只在待处理那一屏现身 */
+/**
+ * 一张可改的表的**闸** —— 默认锁着,按「编辑」才开(2026-08-01 用户定)。
+ *
+ * 从前进这一屏整份件就是编辑态:54 个输入框一起亮着,人还没决定要改什么,
+ * 屏上已经是一张待填的表 —— **看**与**改**是两件事,件首先是给人看的。
+ *
+ * 三个按钮由外层挂事件(容器 `allow-same-origin`,读得到也改得动 iframe 的 DOM);
+ * **iframe 里仍然一行脚本都没有**。`data-edit-*` 是外层认它们的钩子:
+ * · `open` → 给根节点加 `editing`,输入孪生件现身;
+ * · `cancel` → 把每个输入还原成进编辑态那一刻的值,再关掉;
+ * · `save` → 收值交给外层缓冲(提交那一下才真落库),再关掉。
+ * 一屏可能有两张表(需求表 / 分类表),所以闸带 `data-edit-for="域.对象"` —— 各管各的。
+ */
+const editGate = (address: string, what: string): string => `
+ ${ico('lock', 12)}${what}已锁定 —— 看的时候不误碰,要改按右边
+
+
+
+
+${ico('pen', 14)}
+ 改哪一格点哪一格:整份清单每一格都能覆盖 —— AI 抽错的直接改,缺的补上;
+ 多出来的行按右边的 × 撤掉,漏了的用底下的「加一行」补。
+ 红框是必填未补。改动只落任务草稿层,保存不等于放行。
+
`;
+
+/**
+ * 表尾那条「加一行」—— **只在编辑态现身**(样式表按根节点的 `editing` 决定)。
+ *
+ * 为什么人得能加行:手改本来就是**兜 AI 的错**的那一手,而错不止「这一格填错了」——
+ * 「BOM 里有而它没抽出来」是同一类错,只给改格子就没有出口。
+ */
+const addRowBar = (address: string): string => `
+
+ 照末行的样子添一行空的;主键(型号 PN)填了才存得下,不填这一行不落库。
+
`;
+
+/*
+ * ── 这里**去掉了「本轮不报」那条横幅**(2026-08-02 用户当场指出:「也删了」)────
+ *
+ * 原文:「本轮不报 N 行:… —— 客户补齐规格后可另起一单。
+ * 有效条目数 = 来件条目 − 本轮不报,两个数对不上就是拆漏了」。
+ *
+ * 绝大多数单子它是「0 行:—」——**一条永远摆在那儿、九成时候说「没有」的横幅**,
+ * 读的人每次都得扫过它才知道没事。而后半句是讲给拆料那一步听的对账口诀,
+ * 不是给验收的人看的:拆漏没拆漏由收步判据 `record_rows = line_total` 当场拦着,
+ * 拦不住的时候摆一句口诀也没用。
+ *
+ * 题头那一行仍如实写着「N 条 · 已剔除 M」——「剔除了几条」这个事实没有丢。
+ */
+
+/**
+ * 料号分类:分布条 + 图例 —— 原型的 `jc-dist`。
+ *
+ * 三段按条数分宽:段宽在原型里是 JS 写的 `style="flex:N"`,模版不许算,
+ * 于是把**条数本身**投影成 class(`data-class-of` → `v-5`),让样式表按梯子取宽。
+ * 段里印的数字与图例读的是同三个汇总列 —— 一处数据两处呈现,对不上就是台账自己错了。
+ */
+const DIST = `
+
+
+
+
+
+
+ A 类 · 行情完备
+ B 类 · 需寻源询价
+ C 类 · 需替代选型
+
+
`;
+
+/* ==================== 局面一 · 受理中(阶段2 待处理页嵌的那份)==================== */
+
+export const INTAKE_OVERVIEW = doc({
+ badge: 'file-text',
+ kicker: '统一报价 · A1 受理归一 · 过程成果',
+ title: '询价受理需求',
+ titleRef: 'rfq_ref',
+ state:
+ '缺必填 · 缺选填 ',
body:
- kvCard('询价来源', [
- ['客户编码', 'customer_code', 'mono'],
- ['询价单号', 'rfqRef', 'mono'],
- ['主题', 'subject'],
- ]) +
- kvCard('归一结果', [
- ['型号 PN', 'pn', 'mono'],
- ['厂牌', 'brand'],
- ['年用量 EAU', 'eau'],
- ['目标价', 'target_price'],
- ['交期', 'lead_time'],
- ['缺关键字段', 'missing_key_fields'],
- ]) +
- noteCard('这一屏在说什么', '归一由模型从询价原文抽取,逐条落任务级草稿层:本任务可见、正式版不可见、不产生数据事件。放行前的一切都还改得动。'),
+ part('来源 · 客户 RFQ 邮件', ' 入站', 'jc-mono') +
+ LETTER +
+ `${ico('info', 14)}
+ 下方受理要素与 PN 清单,均由 AI 从这封来函与附件 BOM 抽取归一而来 —— 正文里的口径要求
+ (币种 / 贸易条款 / 交期单位 / 认证 / 预授权替代 / 本轮跳哪条)已逐条落到对应字段与备注;
+ 附件那一行写的是基座读取的结果,读得出文本才作数。
+
` +
+ part('受理要素', 'F-01 完整度自检') +
+ four('gap') +
+ part('有效 PN 清单', ' 条 · 已剔除 ') +
+ editGate('quote_intake.record', '有效 PN 清单') +
+ listTable('draft') +
+ addRowBar('quote_intake.record') +
+ `${ico('info', 13)}
+ 带 * 的两列是必填(PN / 年用量):缺任一按 F-01 退回补全,不许放行;
+ 其余列缺了标灰「选填」—— 先受理、后追问,不为它们拦单,但那一问不能省。
+ 原文没写的一律不补:受理阶段编出来的数会一路流到定价和报价单上,越往后越难发现。
+
`,
+ foot: sign('AI 拟稿,你审定 —— 要改哪一格,在「待处理」那张单的需求表上就地改', '统一报价 · A1 受理归一'),
});
-export const INTAKE_CLASSIFIED = viewShell({
- title: '料号分类与判据',
- kicker: 'A1 · 三分类 P-03',
- tone: 'ok',
+/* ==================== 局面三 · 已分类 ==================== */
+
+export const INTAKE_CLASSIFIED = doc({
+ badge: 'shield-check',
+ kicker: '统一报价 · A1 受理归一 · 三分类 P-03',
+ title: '料号分类表',
+ titleRef: 'rfq_ref',
+ state: '已分级 · 未打标 ',
body:
- kvCard('分类结论', [
- ['型号 PN', 'pn', 'mono'],
- ['Type 标', 'type_tag', 'tag'],
- ['分级依据', 'basis'],
- ]) +
- kvCard('打标判据(DMN 入参)', [
- ['距上次报价', 'history_days'],
- ['行情稳定', 'market_stable'],
- ['生命周期', 'lifecycle'],
- ['历史价命中', 'history_hit'],
- ]) +
- noteCard(
- '为什么是确定性的',
- '三分类规则 P-03 是决策表不是模型判断:同样的判据必得同样的结论,零方差零 token,命中的规则行随出参留痕。' +
- 'A = 直接快答;B = 重估后报(转寻源);C = 转案处理(转替代选型)。',
- ),
+ part('料号分类', '三分类规则 P-03 · 逐条判定') +
+ DIST +
+ part('逐条分级(判据 → 结论)', '按序求值 · 首个命中即出') +
+ editGate('quote_intake.type_table', '料号分类表') +
+ `
+
+ |
+ 型号 PN |
+ 距上次报价(天) |
+ 行情稳定 |
+ 生命周期 |
+ 类 |
+ 分级依据 |
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
` +
+ `${ico('info', 13)}
+ 三分类规则 P-03 是决策表,不是临场判断:同样的输入必得同样的结论,凭哪一条命中都留了痕。
+ A = 历史价 ≤ 90 天且行情稳;B = 无历史 / 过期 / 波动;C = EOL / 缺货 / 客户要求替代。
+ 规则从上往下第一条命中即出 —— C 那一行排在最前,EOL 的料哪怕历史价很新也不判 A。
+ 拿不准的一律降 B:错标一个 A,就是让一条本该重新核价的料带着旧价走到报价单上。
+
`,
+ foot: sign('状况标由判定表 P-03 求值 —— 判据与结论逐条可回核', '统一报价 · A1 受理归一'),
});
-export const INTAKE_RELEASE = viewShell({
- title: '统一受理记录 · 待放行',
- kicker: 'A1 · 成果预览',
- tone: 'case',
+/* ==================== 局面四 · 待放行(阶段3 工作成果里的那份)==================== */
+
+/**
+ * G2 分支网关 —— 三条互斥条件按序求值,只命中一条、只落一个活动。
+ *
+ * **命中哪一条不是这屏自己算的**:契约给每个出口写了判据,基座在放行那一刻按序求值,
+ * 把结果注入 `case:route*`(与发口用的是同一次判定)。这里只把它摆出来:
+ * 壳上带 `v-r1/r2/r3`,对应那一行自己上色。
+ * 摆成三张并列的去向卡就丢了「按序求值、只落一个」那层意思,所以照原型做成判定表。
+ */
+const dmnRow = (n: 1 | 2 | 3, cond: string, node: string, to: string, hit: string): string =>
+ `
+ ${n}
+ ${cond}
+ ${node}${to}
+ ${ico('check', 13)}命中 · ${hit}未命中 · 排他网关只落一口
+
`;
+
+const G2 = `
+
序条件(按序求值)去向判定
+ ${dmnRow(1, '存在
C 类(需替代选型)', 'A2', '替代选型', 'C 类
条')}
+ ${dmnRow(2, '无 C 类、存在
B 类(需寻源询价)', 'A3', '渠道寻源', 'B 类
条')}
+ ${dmnRow(3, '全部为
A 类(行情完备)', 'A4', '成本定价', 'A 类
条')}
+
+
+
+
本次判定 · 按序求值,首个命中即出
+
+
去向是分类表算出来的:三条规则按序求值、首个命中即出,
+ 在你签字那一刻现算。所以它不是谁点出来的一次手感 ——
+ 要改去向,就在上面的清单里改料号的类,这一格会跟着变。
+
+
${ico('arrow-right', 26)}
+
+
转入活动
+
+
${ico('user', 11)}下游负责人按运营分派落定
+
放行即落地:草稿层合并进正式版、intake 口的数据事件此刻才发射,
+ 这一口的载荷随之交给下游。门前的都还改得动,门后的都算数了。
+
+
`;
+
+export const INTAKE_RELEASE = doc({
+ badge: 'file-check',
+ kicker: '统一报价 · A1 受理归一 · 交付成果',
+ title: '询价受理记录',
+ titleRef: 'rfq_ref',
+ /*
+ * 题头这枚灯**在办与归档同一句话才算数**:这份件放行前嵌在放行卡上、放行后原样进
+ * 「工作成果」。写「待放行」进了档案就成了谎,写「受理完成」在放行前就是替人签了字 ——
+ * 所以只写这份件本身是什么:归一了几条、三分类已定。
+ */
+ state: '受理记录 · 条 · 三分类已定',
body:
- tableCard(
- '本单交付',
- 'mine:quote_intake.record',
- [
- ['型号', 'pn', 'mono'],
- ['厂牌', 'brand'],
- ['年用量', 'eau'],
- ['目标价', 'target_price'],
- ['交期', 'lead_time'],
- ],
- '只列本单写下的行 —— 别单放行过的记录不混进来。',
- ) +
- kvCard('分流去向', [
- ['Type 标', 'type_tag', 'tag'],
- ['依据', 'basis'],
- ]) +
- noteCard(
- '放行意味着什么',
- '放行通过即刻落地:草稿层合并入正式版、数据事件此刻才发射,G2 网关按 Type 分流。' +
- '放行是草稿态与事实态之间的那道门 —— 门前的都还改得动,门后的都算数了。',
- ),
+ /*
+ * **来件原文留在归档件里**(2026-08-02 用户指出:「成果里为啥没有邮箱原文了」)。
+ *
+ * 待处理与工作成果本来就是**同一份件**,差别只在编辑态开不开。
+ * 归档件少一块来件,就等于把「凭什么这么受理」的出处从档案里抽走了 ——
+ * 三个月后回看这一单,九条料是怎么来的、客户那句「本轮跳过」在哪儿说的,
+ * 全都无从对起。件要自己说得清自己。
+ */
+ part('来源 · 客户 RFQ 邮件', ' 入站', 'jc-mono') +
+ LETTER +
+ part('受理要素', 'F-01 完整度自检 · 通过', 'ok') +
+ four('type') +
+ part('料号分类', '三分类规则 P-03 · 逐条判定') +
+ DIST +
+ part('有效 PN 清单', ' 条 · 已剔除 ') +
+ editGate('quote_intake.record', '有效 PN 清单') +
+ listTable('final') +
+ addRowBar('quote_intake.record') +
+ part('结论与去向', '分支判定 · 起单 ×1', 'ok') +
+ G2 +
+ /*
+ * ── 这里**去掉了两张请求单表与那条对账说明**(2026-08-02 用户当场指出)──────
+ *
+ * 原来摆的是「开出的替代评估请求(交 A2)」「开出的询价请求(交 A3)」两张明细表,
+ * 外加一句「这两条等式在你签字前由系统当场对账,对不上直接拒收」。
+ *
+ * 两个理由:
+ * · 那句话**已经不成立** —— `route-alt` / `route-rfq` 两条对账判据同日删掉了
+ * (它们是内部一致性,该由引擎自己看住,不占放行卡)。件上还写着「系统当场对账」,
+ * 就是在说一件不再发生的事;
+ * · 这份件是**受理记录**:它答的是「这一单受理成了什么样」。派出去的两类单是
+ * **下游的输入**,不是这一单的成果 —— 要看它们,去 A2 / A3 那两条流水线上看。
+ * 摆在这儿只是把同一份数据在两处各画一遍。
+ *
+ * 「这一签下去开出了几张单」仍看得见:放行之后验收卡那一屏(落地结果)按表点名,
+ * 询价请求 / 替代评估请求各几行都在上面 —— 那才是它该出现的时刻与位置。
+ */
+ part('放行后即执行的落地动作', '门前 → 门后') +
+ `
+
${ico('check', 11)}
+
草稿层合并进正式版 本任务写下的受理记录、分类表、结论与两类请求单一并落账;
+ 人工补全与就地修订的那几处随之落定。
+
+
${ico('check', 11)}
+
受理结果此刻才对外发出 连同
+ 条有效料号与它们的分类一并交出 —— 放行之前,世界上什么都没有发生。
+
+
${ico('check', 11)}
+
按分类算出的去向落下游一单 本次转入
+ ,已进对方的任务清单;另两条去向本次不起单。
+
+
`,
+ /*
+ * ── 这里**去掉了「受理结论」那张表**(2026-08-02 用户当场指出:「这个也多余」)──
+ *
+ * 摆的是 `bom_total / skipped_total / skipped_note / summary` 四格,而
+ * 前三格与同日删掉的那条「本轮不报」横幅说的是同一件事,
+ * 第四格是一句 AI 复述的总结 —— 底下的清单、分布条、去向横幅把该说的都说了,
+ * 再加一句转述只是让人多读一行。
+ *
+ * 一份件里同一个数出现两次,人得停下来对一遍两处一不一样 —— 那是白花的注意力。
+ */
+ foot: sign('AI 拟稿 · 待审定放行 —— 要改哪一格,在这张放行单的成果表上就地改', '统一报价 · A1 受理归一 → 交下游'),
});
diff --git a/activities/quote-negotiate/src/case-model.ts b/activities/quote-negotiate/src/case-model.ts
index d374d81..a3d52f3 100644
--- a/activities/quote-negotiate/src/case-model.ts
+++ b/activities/quote-negotiate/src/case-model.ts
@@ -1,77 +1,115 @@
/**
- * A6 报价议价的案例模型 —— **双表示同版本**:CMMN XML(机器面)与案例说明书(叙事面)。
+ * A6 报价议价的案例模型 —— **发出去是触达世界的一下,等回音是立监听**
+ * (2026-07-31 照 A1 的状态机模式重做)。
*
- * 2026-07-29 由步序声明(flow)改写而来。发出报价是**触达外界**的一下,
- * 恒需人工激活(AM 4.1 原理一);客户回音是外部事件,靠 userEventListener 等,
- * 不靠轮询。三个去向(成交 / 有异议 / 拒绝)由验收放行时的选择组给出。
+ * 这个活动的要害不是步数,是**两处不可省的边界**:
+ * · 发正式报价单 = 触达外界 → 必须人工激活(铁律③ / AM 4.1);
+ * · 客户什么时候回是**外界的事** → 立监听等它,不在这一单里轮询、也不假装等到了。
+ *
+ * 三项:
+ * T_compose —— 把草案落成正式报价单(条款齐全),写进台账;
+ * T_send —— 发出并等回音:人工激活 → 登记发信意向 + 立监听;
+ * T_release —— 放行(人工激活),三个去向由输出选择组给出。
*/
export const CMMN_XML = `
-
-
-
+
+
-
- create
-
-
- update
- missingQuoteFields = 0
-
-
- update
- sent = true
+
+ update
+ quote_total > 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
- P3D
-
-
- P7D
-
-
+
+
+
+
+
+
+
+
+
+
`;
-export const NARRATIVE = `§0 开案:拿到报价草案即开案。成品是**一份发出去的正式报价单**,外加一个明确的结果。
+export const NARRATIVE = `§0 开案:拿到报价草案即开案。成品是**一份发出去的正式报价单**,外加一个明确的结果
+(成交 / 有异议 / 拒绝)—— 那三个去向由放行时的选择组定。
-S1 成单发出:把草案写成正式报价单(中英双语 PDF)—— 单价、交期、SPQ、MOQ、贸易条款、
-有效期六样缺一不可,有效期按 P-04 注明。**发出这一下必须人工按**:它触达客户,
-系统不替人做这个决定。写类动作先登记意向,验收放行的那一刻才真发。
+这一单有两处不能含糊:
+· **发出去是触达世界的一下** —— 那一项标了人工激活,人不按你发不出去;
+· **客户什么时候回是外界的事** —— 立监听等它,别在这一单里轮询,更别假装等到了。
-S2 跟进议价:没有跟进的报价等于没报 —— D+3、D+7 两记提醒自动到点,人去问。
-客户提异议就议价调整(裁量项:改价、改交期、换料都可能),改完是新一版报价单,重新走发出。
+【T_compose 落正式报价单】把草案落成能发出去的件:单价 / 交期 / SPQ / MOQ / 贸易条款 /
+有效期,逐条写进 \`quote_negotiate.formal_quote\`,并把 quote_total(条数)写上。
+条款缺一样都不算一份能发的报价单 —— 缺了就问,别自己填一个"看起来合理"的。写完 finish。
-§Z 结案:验收时**恰选一口**给出去向 —— 成交 / 有异议 / 拒绝或超时。
-选哪一口就是这一单的结论,下游网关读的就是它;不选口不许放行,含糊的结果比没结果更坏。`;
+【T_send 发出并等回音】**先 start T_send**(人工激活:发出去触达客户)。
+然后用 \`mail.send\` 登记发信意向(写类工具:过程中只登记,放行那一刻才真发),
+接着 listen 一个 data 事件等客户回音,**时限必配**(比如 P7D)——
+不配时限就是个幽灵等待,这一单会永远挂着。
+回音到了再看:成交 / 有异议 / 拒绝,分别对应放行时的三个出口。
+
+【提交放行】**先 start T_release**(人工激活),提交前用 judge 裁掉裁量判据
+(criterion 照判据名原样写),然后 submit。终局唯放行:你不能自己宣布完成。
+
+【一路上要说话】每一拍带一句 say,说**业务的话**:报了什么价、什么时候发的、客户回了什么。
+一两句、口语。别报「写草稿 N 行」这种系统词。`;
+
+/**
+ * 步骤剧本 —— 与 A1 同一套。
+ * `T_send` 与 `T_release` 都不给 `doneWhen`:一个触达客户、一个触达世界,都得人按一下。
+ */
+export const NEGOTIATE_STEPS = [
+ {
+ item: 'T_compose',
+ intent: '把草案落成条款齐全的正式报价单,并结算条数',
+ writes: ['quote_negotiate.formal_quote', 'quote_negotiate.verdict'],
+ reads: [],
+ tools: [],
+ doneWhen: 'quote_total > 0',
+ guide: `逐条写进 \`quote_negotiate.formal_quote\`:单价 / 交期 / SPQ / MOQ / 贸易条款 / 有效期,
+并把 quote_total 写上。条款缺一样都不算一份能发的报价单 ——
+缺了就问,**别自己填一个「看起来合理」的值**。
+**同一拍再写一行 \`quote_negotiate.verdict\`**(验收机检读的就是它):
+· quote_total = 报价行条数;missing_field_total = 六项条款还缺的条数(要 = 0);
+· validity_days = 报价有效期天数(要 > 0);
+· followup_total = 已建跟进条数(**要 ≥2** —— D+3 与 D+7 两条,没有跟进的报价等于没报);
+· summary 一句人话结论。写不对 submit 会被门挡回来。写完再 finish。
+**每一行都要带 \`rfq_ref\`** —— 它是这一单的单标识(起单入参里就有,原样抄进去)。
+台账主键第一列就是它:不写,这一行会和**别的单**同料号的那一行撞主键,把人家覆盖掉
+(实测栽过:A4 的报价草案主键只有 pn,跑四轮只剩最后一轮的 3 行)。`,
+ },
+ {
+ item: 'T_send',
+ intent: '发出报价并立监听等客户回音',
+ writes: [],
+ reads: [],
+ tools: ['mail.send'],
+ // 触达客户 —— 人工激活,不给 doneWhen
+ guide: `**先 start T_send**(人工激活:发出去触达客户)。
+用 \`mail.send\` 登记发信意向(写类工具:只登记,放行那一刻才真发),
+再 listen 一个 data 事件等回音,**时限必配**(如 P7D)—— 不配就是幽灵等待,这一单会永远挂着。`,
+ },
+ {
+ item: 'T_release',
+ intent: '议价有结果,提交放行交 BD 验收',
+ writes: [],
+ reads: [],
+ tools: [],
+ guide: `**先 start T_release**(人工激活),提交前用 judge 裁掉裁量判据,然后 submit。
+三个去向(成交 / 有异议 / 拒绝)由放行时的选择组定。
+**台账里要写 outcome**(won / revise / lost)—— 下游三条边读的就是它,不写整条流走不下去。`,
+ },
+];
diff --git a/activities/quote-negotiate/src/index.ts b/activities/quote-negotiate/src/index.ts
index 068520a..906b78f 100644
--- a/activities/quote-negotiate/src/index.ts
+++ b/activities/quote-negotiate/src/index.ts
@@ -11,7 +11,6 @@ const QUOTE = 'quote_negotiate.formal_quote';
export default defineActivity({
name: 'quote-negotiate',
- paradigm: 'flow',
async run(base: ActivityBase): Promise {
const lines = await base.ledger.read(QUOTE);
diff --git a/activities/quote-negotiate/src/manifest.ts b/activities/quote-negotiate/src/manifest.ts
index 5bcd321..cce20b6 100644
--- a/activities/quote-negotiate/src/manifest.ts
+++ b/activities/quote-negotiate/src/manifest.ts
@@ -7,7 +7,7 @@
*/
import type { ActivityManifest } from '@contracts';
import { NEGOTIATE_SKILLS } from './skills.ts';
-import { CMMN_XML, NARRATIVE } from './case-model.ts';
+import { CMMN_XML, NARRATIVE, NEGOTIATE_STEPS } from './case-model.ts';
import { activityIdSchema } from '@contracts';
import { QUOTE_COMPOSED, QUOTE_RELEASE } from './views/report.ts';
@@ -18,7 +18,7 @@ export const manifest: ActivityManifest = {
identity: {
activityId: ACTIVITY_ID,
name: 'A6 · 报价议价',
- packageVersion: '0.1.5',
+ packageVersion: '2.1.0',
category: '统一报价',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-quote-negotiate',
@@ -30,8 +30,8 @@ export const manifest: ActivityManifest = {
input: {
data: {
type: 'object',
- required: ['customerCode', 'items'],
- properties: { customerCode: { type: 'string' }, items: { type: 'array' } },
+ required: ['rfqRef', 'customerCode', 'items'],
+ properties: { rfqRef: { type: 'string', title: '询价单号(单标识,贯穿全流程)' }, customerCode: { type: 'string' }, items: { type: 'array' } },
},
messageAllowed: true,
},
@@ -42,23 +42,67 @@ export const manifest: ActivityManifest = {
{
group: 'negotiation',
ports: [
- { name: 'won', payload: { type: 'object' }, description: '成交 → 转订单履约(ORM)' },
- { name: 'revise', payload: { type: 'object' }, description: '有异议 → 返回调整 A4' },
- { name: 'lost', payload: { type: 'object' }, description: '拒绝/超时 → 转 A7 情报归档' },
+ /*
+ * 载荷里**必须带 `outcome`** —— 流程包 E10/E11/E12 三条出边读的就是它
+ * (`outcome = "won"/"revise"/"lost"`)。从前三个口的载荷都是空对象 `{}`,
+ * 于是三条边一条也不成立:整条流走到 A6 就没路可走,而口是"发射"过的。
+ * 这与 A1 当年 G1 全流死路是同一种错 —— **边读什么,口就得发什么**。
+ */
+ {
+ name: 'won',
+ payload: {
+ type: 'object',
+ required: ['outcome'],
+ properties: { outcome: { type: 'string', title: '议价结果(固定 won)' } },
+ },
+ description: '成交 → 转订单履约(ORM)',
+ },
+ {
+ name: 'revise',
+ payload: {
+ type: 'object',
+ required: ['outcome'],
+ properties: { outcome: { type: 'string', title: '议价结果(固定 revise)' } },
+ },
+ description: '有异议 → 返回调整 A4',
+ },
+ {
+ name: 'lost',
+ payload: {
+ type: 'object',
+ required: ['outcome'],
+ properties: { outcome: { type: 'string', title: '议价结果(固定 lost)' } },
+ },
+ description: '拒绝/超时 → 转 A7 情报归档',
+ },
],
},
],
},
+ /*
+ * 必交付物**必须给 `ledger` 地址**(2026-07-31 修)。
+ * 不给的话预检退回 `scope[key]` 去找 —— key 是 `benchmark-table` 这样的短横线别名,
+ * 任务视图里永远没有这个键,于是「必交付物缺一不得提交放行」永远拒,
+ * 活办完了也放不了行,连拒五拍熔断(实测 A2 死于此)。
+ */
requiredDeliverables: [
- { key: 'formal-quote', label: '正式报价单(PDF · 中英双语)' },
- { key: 'followup', label: 'D+3/D+7 跟进提醒' },
+ { key: 'formal-quote', label: '正式报价单(PDF · 中英双语)', ledger: `${DOMAIN}.formal_quote` },
+ // 跟进不单独立表 —— 建了几条记在议价结论的 followup_total 上
+ { key: 'followup', label: 'D+3/D+7 跟进提醒', ledger: `${DOMAIN}.verdict` },
],
+ /*
+ * 机检谓词读的是 `verdict` 那张**汇总表**的列(照 A1 的口径)。
+ * 上一版的名字(missingQuoteFields / validityDays / followupSet)**台账里根本没有** —— S-FEEL 拿未绑定的名字
+ * 比大小求值为假,判据永远达不了标,submit 门永远拒,连拒五拍熔断。
+ * 判据的名字必须落在台账上,否则「机检」是一句空话(2026-07-31 销账)。
+ */
acceptanceCriteria: [
- { key: 'quote-complete', label: '单价/交期/SPQ/MOQ/贸易条款/有效期齐备', level: 'L1', predicate: 'missingQuoteFields = 0' },
- { key: 'validity', label: '按报价有效期 P-04 注明有效期', level: 'L2', predicate: 'validityDays > 0' },
- { key: 'followup-set', label: '已建 D+3/D+7 跟进 —— 没有跟进的报价等于没报', level: 'L2', predicate: 'followupSet = true' },
+ { key: 'quote-complete', label: '单价/交期/SPQ/MOQ/贸易条款/有效期齐备', level: 'L1', predicate: 'missing_field_total = 0' },
+ { key: 'validity', label: '按报价有效期 P-04 注明有效期', level: 'L2', predicate: 'validity_days > 0' },
+ { key: 'followup-set', label: '已建 D+3/D+7 跟进 —— 没有跟进的报价等于没报', level: 'L2', predicate: 'followup_total >= 2' },
{ key: 'bd-release', label: 'BD 放行', level: 'L5' },
],
+ revisableAtRelease: [],
prohibitions: [
{ key: 'no-break-line', statement: '客户议价超出授权范围的,回 A5 重批,不私自破线' },
{ key: 'no-send-before-release', statement: '报价单在放行前不得发出 —— 写类动作集中在放行时落地' },
@@ -85,9 +129,12 @@ export const manifest: ActivityManifest = {
{
domain: DOMAIN,
object: 'formal_quote',
+ kind: 'record',
+ caseKey: 'rfq_ref',
label: '正式报价单',
fields: [
- { name: 'quote_no', type: 'text', primaryKey: true, masked: false, label: '报价单号' },
+ { name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' },
+ { name: 'quote_no', type: 'text', primaryKey: false, masked: false, label: '报价单号' },
{ name: 'pn', type: 'text', primaryKey: true, masked: false, label: 'PN' },
{ name: 'unit_price', type: 'number', primaryKey: false, masked: false, label: '单价CNY' },
{ name: 'lead_time', type: 'text', primaryKey: false, masked: false, label: '交期' },
@@ -98,8 +145,9 @@ export const manifest: ActivityManifest = {
],
},
{
- domain: 'shared',
- object: 'history_price',
+ domain: 'market',
+ object: 'price_history',
+ kind: 'master',
label: '历史价格情报(共享域)',
fields: [
{ name: 'pn', type: 'text', primaryKey: true, masked: false },
@@ -108,6 +156,28 @@ export const manifest: ActivityManifest = {
{ name: 'lead_time', type: 'text', primaryKey: false, masked: false },
],
},
+ {
+ /*
+ * 议价结论 —— 一单一行的**汇总表**,验收机检谓词的入参就在这儿。
+ * 汇总列与逐行列**故意不同名**:任务视图把草稿行摊平到顶层是「后写的赢」,
+ * 拿逐行列求值只会验到最后写的那一行,那不叫机检、叫抽样(A1 的教训)。
+ */
+ domain: DOMAIN,
+ object: 'verdict',
+ kind: 'record',
+ caseKey: 'rfq_ref',
+ label: '议价结论',
+ fields: [
+ { name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' },
+ { name: 'quote_no', type: 'text', primaryKey: false, masked: false, label: '报价单号' },
+ { name: 'quote_total', type: 'integer', primaryKey: false, masked: false, label: '报价行条数' },
+ { name: 'missing_field_total', type: 'integer', primaryKey: false, masked: false, label: '条款缺失条数' },
+ { name: 'validity_days', type: 'integer', primaryKey: false, masked: false, label: '报价有效期天数' },
+ { name: 'followup_total', type: 'integer', primaryKey: false, masked: false, label: '已建跟进条数' },
+ { name: 'outcome', type: 'text', primaryKey: false, masked: false, label: '议价结果(won/revise/lost)' },
+ { name: 'summary', type: 'text', primaryKey: false, masked: false, label: '议价结论' },
+ ],
+ },
],
listeners: [
{
@@ -134,6 +204,8 @@ export const manifest: ActivityManifest = {
cmmnXml: CMMN_XML,
narrative: NARRATIVE,
dmn: { drd: [], tables: [] },
+ /* 步骤剧本 —— 与 A1 同一套:doneWhen 真了引擎自己收步,不问模型 */
+ steps: NEGOTIATE_STEPS,
},
skills: NEGOTIATE_SKILLS,
viewTemplates: [
diff --git a/activities/quote-negotiate/src/views/report.ts b/activities/quote-negotiate/src/views/report.ts
index 88f2846..a2ffc30 100644
--- a/activities/quote-negotiate/src/views/report.ts
+++ b/activities/quote-negotiate/src/views/report.ts
@@ -1,50 +1,189 @@
/**
- * A6 视图模版 —— 议价两屏:正式报价单 / 待放行即发出(AM 07)。
- * 这是**唯一会触达外界**的活动:放行一刻要发信、要回写台账,所以那一屏必须说清后果。
+ * A6 报价议价的 HTML 报告模版 —— **活动自己持有的资产**(§10.3),随包版本冻结。
+ *
+ * 议价是**多轮往来**:成单 → 人工按下发出 → D+3 催 → D+7 催 → 客户还价 → 改一版再发。
+ * 所以这两屏都要让人看得出**先后**:这一版是第几轮、上一轮报的是多少、下一记提醒在哪天。
+ * 只摆一个「当前单价」的屏,人签字时没有参照系,也就无从判断这一让让得值不值 ——
+ * 于是「历次报价」那一节摆的是共享域整表(含既往单),它不是本单成果,是本单的尺子。
+ *
+ * 两个局面:
+ * ① 正式报价单(草稿)—— 六样条款齐不齐;这一步还没碰外界,一封信都没发;
+ * ② 放行即发出 —— 按下的那一刻信就到客户手上,签字前是最后一次能改的机会。
+ *
+ * 容器是 `sandbox=""` iframe:无脚本、无外链、样式自带;数据由基座推送时填
+ * (`data-slot` 取值、`data-rows` 逐行展开)。
+ *
+ * 两条贯穿全包的呈现纪律:
+ * · **状态章一律用「在办」灯,不用「已办」印** —— 模版是随包冻结的静态字符串,
+ * 它盖不了一个还没发生的章;这两张都是放行前的局面;
+ * · **`verdict` 的橙档专指「还没发生,等你按」** —— 发出的后果是预告不是既成事实。
+ * 整屏的结构色一律品牌蓝/案卷紫,红橙绿只落在「达标没达标」上,不拿来渲染气氛。
*/
-import { kvCard, noteCard, tableCard, viewShell } from '@sdk';
+import { noteOnly, section, stagesOnly, statsBand, tableOnly, verdict, viewShell } from '@sdk';
+/**
+ * 静态阶梯 —— **规则与节拍不是数据**:六样条款的要求、D+3/D+7 的节拍、三个出口的去向
+ * 都随包冻结,不该走 `data-rows` 等基座来填(那会让「规矩」看起来像「这一单的结果」)。
+ * 沿用外壳已有的 `.steps` 版式,与引擎推的阶段列表同一副面孔。
+ */
+function ladder(rows: ReadonlyArray): string {
+ return `${rows
+ .map(
+ ([no, term, gloss]) =>
+ `- ${no}${term} ${gloss}
`,
+ )
+ .join('')}
`;
+}
+
+/**
+ * 键值格 —— 报价单的条款是**一份单据的正面**,六样并排比一张六列横向滚动的表好读。
+ * `@sdk` 的 `kvCard` 自带一个「·」号章节,与本页「一/二/三」的编号排不到一起,
+ * 所以这里只借外壳已有的 `.kv` 版式,章节仍交给 `section` 统一编号。
+ */
+function kvGrid(items: ReadonlyArray): string {
+ return `${items
+ .map(([label, path, style]) => {
+ const cls = style === undefined ? '' : ` class="${style === 'mono' ? 'mono' : 'tag'}"`;
+ return `
${label}
`;
+ })
+ .join('')}
`;
+}
+
+/** 局面一:S1 成单 —— 人此刻要核的是「六样齐没齐、这一版能不能发」 */
export const QUOTE_COMPOSED = viewShell({
- title: '正式报价单',
- kicker: 'A6 · 报价议价',
+ title: '正式报价单(草稿)',
+ kicker: 'A6 · 报价议价 · S1 成单',
tone: 'brand',
+ icon: 'handshake',
+ stamp: { kind: 'live', text: '成单中' },
+ sign: 'A6 报价议价 · 正式报价单(本单草稿)',
body:
- kvCard('报价抬头', [
- ['报价单号', 'quoteNo', 'mono'],
- ['客户编码', 'customerCode', 'mono'],
- ['型号 PN', 'pn', 'mono'],
- ]) +
- kvCard('报价条款', [
- ['单价', 'unit_price'],
+ /* `items` 是起单入参的数组,基座渲染数组时给的是条数 —— 正好是「这一单要报几条」 */
+ statsBand([
+ ['报价条目', 'items'],
+ ['单价 CNY', 'unit_price'],
['交期', 'lead_time'],
- ['最小包装量 SPQ', 'spq'],
- ['最小起订量 MOQ', 'moq'],
- ['贸易条款', 'trade_terms'],
['有效期', 'valid_until'],
]) +
- noteCard('这一步还没碰外界', '报价单此刻只是草稿。发出动作是写类的,过程中只登记为「动作意向」——入参冻结、等放行,放行前一封信都不会发出去。'),
-});
-
-export const QUOTE_RELEASE = viewShell({
- title: '正式报价单 · 放行即发出',
- kicker: 'A6 · 成果预览',
- tone: 'danger',
- body:
- tableCard(
- '本单报价',
- 'mine:quote_negotiate.formal_quote',
- [
+ section('报价条款六样 —— 缺一不可',
+ kvGrid([
+ ['单价 CNY', 'unit_price'],
+ ['交期', 'lead_time'],
+ ['最小包装量 SPQ', 'spq'],
+ ['最小起订量 MOQ', 'moq'],
+ ['贸易条款', 'trade_terms'],
+ ['有效期', 'valid_until'],
+ ]) +
+ noteOnly(
+ '单价与交期照审批通过的草案抄,不做任何「顺手优化」—— 改价要重新过审批点。' +
+ '有效期按行情波动定,不要一律写「长期有效」:那等于把汇率与料价风险自己扛了。' +
+ '拿不准的项宁可空着走补全,不要填一个看起来合理的值。',
+ ),
+ { text: '缺一样多一轮' },
+ ) +
+ section('本单已成形的报价行',
+ tableOnly('mine:quote_negotiate.formal_quote', [
['报价单号', 'quote_no', 'mono'],
- ['型号', 'pn', 'mono'],
+ ['型号 PN', 'pn', 'mono'],
['单价', 'unit_price'],
['交期', 'lead_time'],
- ],
+ ['有效期', 'valid_until'],
+ ]),
+ { text: '只列本单写下的行' },
) +
- noteCard(
- '按下放行会发生什么',
- '① 落地预检先跑:交付物齐备、动作意向可执行、授权在契约边界内 —— 预检不过不发卡。' +
- '② 放行通过即刻落地:台账回写 + 出站发送(报价单发给客户),动作附幂等键。' +
- '③ 落地收敛后输出口才发射 —— 下游只见落地完成的任务;已成动作留存不回滚。' +
- '④ 出口三选一(成交 / 有异议 / 拒绝)恰发一口,随放行一并确认,G4 网关按它分流。',
+ section('议价的节拍 —— 一轮一版',
+ ladder([
+ ['1', '写成正式报价单', '中英双语,六样齐备'],
+ ['2', '人工按下发出', '触达客户的一下,系统不替人做这个决定'],
+ ['3', 'D+3 提醒到点', '没有跟进的报价等于没报'],
+ ['4', 'D+7 提醒到点', '第二记催办,仍无回音即等超时'],
+ ['5', '客户回音 → 议价调整', '改价、改交期、换料都是新一版,重新走发出'],
+ ]),
+ { text: '往来看得出先后' },
+ ) +
+ section('案例当下进程', stagesOnly('case:stages'), { text: 'Enabled ≠ Active' }) +
+ section('这一步还没碰外界',
+ verdict('报价单此刻只是草稿 —— 一封信都还没发出去', 'wait', '发出这一下必须人工按') +
+ noteOnly(
+ '发出是写类动作:过程中只登记为「动作意向」—— 入参冻结、等放行,放行前不触达世界。' +
+ '预授权是逐项列举的:催办模板可即时发,报价单不行。',
+ ),
+ ),
+});
+
+/** 局面二:待放行 —— 按下的那一刻信就发出去,这是最后一次能改的机会 */
+export const QUOTE_RELEASE = viewShell({
+ title: '正式报价单 · 放行即发出',
+ kicker: 'A6 · 报价议价 · 成果待签',
+ tone: 'case',
+ icon: 'send',
+ stamp: { kind: 'live', text: '待签字放行' },
+ sign: 'A6 报价议价 · 正式报价单(待放行即发出)',
+ body:
+ statsBand([
+ ['单价 CNY', 'unit_price'],
+ ['交期', 'lead_time'],
+ ['最小起订量 MOQ', 'moq'],
+ ['有效期', 'valid_until'],
+ ]) +
+ section('本单交付:正式报价单',
+ tableOnly('mine:quote_negotiate.formal_quote', [
+ ['报价单号', 'quote_no', 'mono'],
+ ['型号 PN', 'pn', 'mono'],
+ ['单价', 'unit_price'],
+ ['交期', 'lead_time'],
+ ['SPQ', 'spq'],
+ ['MOQ', 'moq'],
+ ['贸易条款', 'trade_terms'],
+ ['有效期', 'valid_until'],
+ ]),
+ { text: '发出去的就是这几行' },
+ ) +
+ section('历次报价对照 —— 这一轮让了多少',
+ /*
+ * 这一张**故意不用 `mine:`**:它取共享域整表,含既往单的行。
+ * 本单成果只看 `mine:`(见上一节),而「让了多少」这件事天然要跟别的单比 ——
+ * 没有上一轮的价摆在旁边,人签字时判断不了这一让的分寸。
+ */
+ tableOnly('shared.history_price', [
+ ['型号 PN', 'pn', 'mono'],
+ ['报价日期', 'quoted_at', 'mono'],
+ ['单价', 'unit_price'],
+ ['交期', 'lead_time'],
+ ]) +
+ noteOnly(
+ '这一节不是本单成果,是共享域里同一料号的历次报价(含既往单)—— 摆在这里只做一件事:' +
+ '给这一轮的价一个参照系。报价日期在列上,先后按日期对;它由 A7 情报归档一单一单沉下来。',
+ ),
+ { text: '含既往单' },
+ ) +
+ section('放行前要过的四道判据',
+ ladder([
+ ['1', '单价 / 交期 / SPQ / MOQ / 贸易条款 / 有效期齐备', 'L1 机检 · missingQuoteFields = 0'],
+ ['2', '按报价有效期 P-04 注明有效期', 'L2 机检 · validityDays 大于 0'],
+ ['3', '已建 D+3 / D+7 跟进', 'L2 机检 · 没有跟进的报价等于没报'],
+ ['4', 'BD 放行', 'L5 人签 · 签了才算数'],
+ ]),
+ { text: '三机检 · 一人签' },
+ ) +
+ section('出口三选一 —— 恰发一口',
+ ladder([
+ ['1', '成交', '转订单履约(ORM),这条流走到终点'],
+ ['2', '有异议', '回 A4 重开新实例重算 —— 旧的已终,不再入算'],
+ ['3', '拒绝 / 超时', '转 A7 情报归档,把拒绝原因与竞对价位登记下来'],
+ ]) +
+ noteOnly(
+ '三个出口对应三种后续,随放行恰选一口,下游网关读的就是它;不选口不许放行 ——' +
+ '含糊的结果比没结果更坏。「拒绝」不是失败终态:原因回写历史情报后,下一单的三分类与利润模型会用上它。',
+ ),
+ { text: '下游只读这一口' },
+ ) +
+ section('按下放行会发生什么',
+ verdict('放行即落地:台账回写 + 报价单出站发送', 'wait', '已成动作留存,不回滚') +
+ noteOnly(
+ '① 落地预检先跑:交付物齐备、动作意向可执行、授权在契约边界内 —— 预检不过不发卡;' +
+ '② 放行通过即刻落地,出站动作附幂等键;' +
+ '③ 落地收敛之后输出口才发射 —— 下游只见落地完成的任务。',
+ ),
),
});
diff --git a/activities/quote-pricing/src/case-model.ts b/activities/quote-pricing/src/case-model.ts
index 978b82f..5c524d4 100644
--- a/activities/quote-pricing/src/case-model.ts
+++ b/activities/quote-pricing/src/case-model.ts
@@ -1,81 +1,137 @@
/**
- * A4 成本定价的案例模型 —— **双表示同版本**:CMMN XML(机器面)与案例说明书(叙事面)。
+ * A4 成本定价的案例模型 —— **算价归判定表,超限那一下归人**(2026-07-31 照 A1 的状态机模式重做)。
*
- * 2026-07-29 由步序声明(flow)改写而来。原六步各归其位:取价基准与逐条定价属「算价」,
- * 偏离率与底价门禁属「自检」(两支 DMN:P-01 利润模型、P-02 底价门禁),
- * 超限条目调人决策是**触达客户利益的一下**,必须人工激活;末尾一张验收单。
+ * 与 A2/A3 的分工:那两个是「一次专家判断」,派一张执行单收回结论就够了;
+ * 定价不同 —— 它**有确定性规则可依**(P-01 利润模型、P-02 底价门禁两支 DMN),
+ * 算价这件事该算不该问。所以它照 A1 的样子分步建模,而不是甩一张单给人。
+ *
+ * 三项:
+ * T_price —— 逐条算价并落草案(拟报单价 / 毛利 / 偏离度 / 门禁处置);
+ * T_approve —— **只在门禁判超限时入场**:触达客户利益的一下,人工激活,派决策卡;
+ * T_release —— 放行(人工激活)。
+ *
+ * 「超限才叫人」是这个活动的要害:每条都叫人,人就成了橡皮图章;一条都不叫,
+ * 底价门禁就是摆设。所以那一项挂前提(`s_over`),由判定表的结论决定它入不入场。
*/
export const CMMN_XML = `
+
-
-
-
-
+
+
+
-
- create
-
-
- create
-
- update
- unpriced = 0
+ update
+ draft_total > 0
-
- update
- breaches > 0
+
+
+ update
+ over_total > 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
- PT8H
-
+
+
+
+
+
+
+
+
+
+
+
`;
export const NARRATIVE = `§0 开案:拿到一批待定价条目即开案。成品是**一份全条目带偏离度的报价草案** ——
-每条有拟报单价与毛利率,且都过了底价门禁。
+人签的不是一个数字,是**这个数字的来路**:基准从哪来、毛利怎么算、偏离多少、门禁怎么判。
-S1 算价:先取价基准(历史成交价 / 供应商回价 / 替代料对标价,按可得性择优),
-再按利润模型 P-01 逐条定价 —— 客户等级、数量档、料号类型、行情共同决定加价率。
-基准取不到的条目不许拍脑袋定价,回头找寻源或标注「无基准」。
+**能算的不问人**:P-01(利润模型)与 P-02(底价门禁)两支判定表说了算,你不临场拍价。
+你的活是把判据取齐、把结论落进台账;**只有超限的那几条才叫人**。
-S2 自检:算出偏离度(基准价 ↔ 拟报价),按底价门禁 P-02 逐条判。
-**偏离 ±20% 以内自动放过;超限的一律调人** —— 要么回调价格,要么写明为什么这样报。
-底价超一周没更新的先刷底价再判(裁量项),拿旧底价判新行情等于没判。
+【T_price 逐条算价】基准价取自历史价情报(A3 的比价结论 / CRM 历史价)。新料号无历史时
+基准记 0,由底价门禁按「无基准」处置 —— **不凭空编一个偏离率出来**。
+逐条写进 \`quote_pricing.draft\`:拟报单价 / 目标毛利 / 相对基准偏离 / 门禁处置。
+同一拍接着把 draft_total(条数)与 over_total(门禁判「超限」的条数)写上 ——
+这两个数一个是完成判据,一个决定要不要叫人。写完 finish。
-§Z 结案:每条都有拟报单价与毛利率、超限条目已处置、BD 签字放行,自动结案。
-草案交出去就是下游议价的起点 —— 它不是最终报价,但每一条都要经得起问「为什么是这个价」。`;
+【T_approve 超限决策】**只在 over_total > 0 时走这一步**。它触达客户利益,
+所以标了人工激活:**先 start,再发决策卡**(decide,itemId 填 T_approve)。
+选项按门禁的处置给(照原价报 / 按底价报 / 退回重算),consequence 写清各自的后果;
+basis 摆上是哪几条、偏离多少、底价是多少(裸卡基座拒发)。
+人点完,轨迹上会出现「decision 卡回传 —— 人选了:xxx」—— **看到它就照那条路把 draft 改掉**,
+别再发第二张卡。改完 finish。
+
+【提交放行】草案有行了 —— **先 start T_release**(人工激活),然后 submit,BD 验收。
+提交前把两条裁量判据裁掉(judge,criterion 照判据名原样写);终局唯放行,你不能自己宣布完成。
+
+【一路上要说话】每一拍带一句 say,说**业务的话**:这批料算下来什么价、哪几条压线、
+要不要找人拍板。一两句、口语。别报「写草稿 N 行」这种系统词。`;
+
+/**
+ * 步骤剧本 —— 与 A1 同一套:`doneWhen` 为真,引擎自己收步、**不问模型**。
+ *
+ * 两处不给 `doneWhen`:`T_approve`(人工激活,要人拍板)与 `T_release`(触达世界)。
+ * 给了就等于给引擎一把替人签字的钥匙。
+ */
+export const PRICING_STEPS = [
+ {
+ item: 'T_price',
+ intent: '逐条算价并落草案:拟报单价 / 毛利 / 偏离度 / 门禁处置,并结算条数与超限数',
+ writes: ['quote_pricing.draft', 'quote_pricing.verdict'],
+ reads: [],
+ tools: [],
+ // 草案有行了才算算完。不写 `!= null` —— 未绑定的名字那样求值为 true(A1 踩过)
+ doneWhen: 'draft_total > 0',
+ guide: `基准价取自历史价情报(A3 的比价结论 / CRM 历史价)。新料号无历史时基准记 0,
+由底价门禁按「无基准」处置 —— **不凭空编一个偏离率**。
+逐条写进 \`quote_pricing.draft\`:拟报单价 / 目标毛利 / 相对基准偏离 / 门禁处置。
+**同一拍接着**把 draft_total 与 over_total(门禁判超限的条数)写上 ——
+前者是完成判据,后者决定要不要叫人。
+**同一拍再写一行 \`quote_pricing.verdict\`**(验收机检读的就是它):
+· draft_total / unpriced_total = 草案条数 / 还没定出价的条数(后者要 = 0);
+· over_total = 门禁判超限的条数;
+· unresolved_over_total = 超限里**还没处置**的条数(要 = 0 —— 回调了或注明了理由都算已处置);
+· summary 一句人话结论。写不对 submit 会被门挡回来。三笔写完再 finish。
+**每一行都要带 \`rfq_ref\`** —— 它是这一单的单标识(起单入参里就有,原样抄进去)。
+台账主键第一列就是它:不写,这一行会和**别的单**同料号的那一行撞主键,把人家覆盖掉
+(实测栽过:A4 的报价草案主键只有 pn,跑四轮只剩最后一轮的 3 行)。`,
+ },
+ {
+ item: 'T_approve',
+ intent: '超限条目要人拍板 —— 发决策卡,照人选的那条路改草案',
+ writes: ['quote_pricing.draft'],
+ reads: [],
+ tools: [],
+ // 人工激活 + 要人拍板 —— 不给 doneWhen,引擎不许替人收
+ guide: `**只在 over_total > 0 时走这一步**。它触达客户利益,标了人工激活:
+**先 start T_approve**(与发卡排同一拍即可),再发决策卡(decide,itemId 填 T_approve)。
+选项按门禁处置给(照原价报 / 按底价报 / 退回重算),consequence 写清后果;
+basis 摆上哪几条、偏离多少、底价多少(**裸卡基座拒发**)。
+人点完轨迹上会出现「decision 卡回传 —— 人选了:xxx」,**照那条路改 draft,别再发第二张卡**。`,
+ },
+ {
+ item: 'T_release',
+ intent: '草案已齐,提交放行交 BD 验收',
+ writes: [],
+ reads: [],
+ tools: [],
+ guide: `草案有行了 —— **先 start T_release**(人工激活),提交前用 judge 裁掉裁量判据
+(criterion 照判据名原样写),然后 submit。终局唯放行:你不能自己宣布完成。`,
+ },
+];
diff --git a/activities/quote-pricing/src/index.ts b/activities/quote-pricing/src/index.ts
index 04bbb8a..76cb9d6 100644
--- a/activities/quote-pricing/src/index.ts
+++ b/activities/quote-pricing/src/index.ts
@@ -11,7 +11,6 @@ const DRAFT = 'quote_pricing.draft';
export default defineActivity({
name: 'quote-pricing',
- paradigm: 'flow',
async run(base: ActivityBase): Promise {
const items = await base.ledger.read(DRAFT);
diff --git a/activities/quote-pricing/src/manifest.ts b/activities/quote-pricing/src/manifest.ts
index a409d67..e1bf687 100644
--- a/activities/quote-pricing/src/manifest.ts
+++ b/activities/quote-pricing/src/manifest.ts
@@ -7,7 +7,7 @@
*/
import type { ActivityManifest, DmnModel } from '@contracts';
import { PRICING_SKILLS } from './skills.ts';
-import { CMMN_XML, NARRATIVE } from './case-model.ts';
+import { CMMN_XML, NARRATIVE, PRICING_STEPS } from './case-model.ts';
import { activityIdSchema } from '@contracts';
import { PRICING_BASIS, PRICING_PRICED, PRICING_RELEASE } from './views/report.ts';
@@ -62,7 +62,7 @@ export const manifest: ActivityManifest = {
identity: {
activityId: ACTIVITY_ID,
name: 'A4 · 成本定价',
- packageVersion: '0.1.6',
+ packageVersion: '2.0.0',
category: '统一报价',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-quote-pricing',
@@ -74,8 +74,9 @@ export const manifest: ActivityManifest = {
input: {
data: {
type: 'object',
- required: ['customerCode', 'records'],
+ required: ['rfqRef', 'customerCode', 'records'],
properties: {
+ rfqRef: { type: 'string', title: '询价单号(单标识,贯穿全流程)' },
customerCode: { type: 'string' },
records: { type: 'array' },
// B/C 类由 A3/A2 交回的底价与替代方案;A 类直通时为空
@@ -90,21 +91,36 @@ export const manifest: ActivityManifest = {
name: 'draft',
payload: {
type: 'object',
- required: ['items'],
- properties: { items: { type: 'array' } },
+ required: ['rfqRef', 'items'],
+ properties: { rfqRef: { type: 'string' }, items: { type: 'array' } },
},
description: '报价草案(逐条带偏离度)',
},
],
selective: [],
},
- requiredDeliverables: [{ key: 'quote-draft', label: '报价草案(全条目带偏离度)' }],
+ /*
+ * 必交付物**必须给 `ledger` 地址**(2026-07-31 修)。
+ * 不给的话预检退回 `scope[key]` 去找 —— key 是 `benchmark-table` 这样的短横线别名,
+ * 任务视图里永远没有这个键,于是「必交付物缺一不得提交放行」永远拒,
+ * 活办完了也放不了行,连拒五拍熔断(实测 A2 死于此)。
+ */
+ requiredDeliverables: [
+ { key: 'quote-draft', label: '报价草案(全条目带偏离度)', ledger: `${DOMAIN}.draft` },
+ ],
+ /*
+ * 机检谓词读的是 `verdict` 那张**汇总表**的列(照 A1 的口径)。
+ * 上一版的名字(unpriced;floor-gate 更是连谓词都没有)**台账里根本没有** —— S-FEEL 拿未绑定的名字
+ * 比大小求值为假,判据永远达不了标,submit 门永远拒,连拒五拍熔断。
+ * 判据的名字必须落在台账上,否则「机检」是一句空话(2026-07-31 销账)。
+ */
acceptanceCriteria: [
- { key: 'all-priced', label: '每条都有拟报单价与毛利率', level: 'L1', predicate: 'unpriced = 0' },
+ { key: 'all-priced', label: '每条都有拟报单价与毛利率', level: 'L1', predicate: 'unpriced_total = 0' },
// 底价门禁沉为 L2 机检 —— 「约束力收缩」的终点
- { key: 'floor-gate', label: '偏离底价 ≤±20%,超限者已回调或注明理由', level: 'L2', modelRef: 'p02-floor' },
+ { key: 'floor-gate', label: '偏离底价 ≤±20%,超限者已回调或注明理由', level: 'L2', modelRef: 'p02-floor', predicate: 'unresolved_over_total = 0' },
{ key: 'pm-release', label: 'BD 放行', level: 'L5' },
],
+ revisableAtRelease: [],
prohibitions: [
{ key: 'no-silent-override', statement: '不得静默绕开 P-02 的结论;要破线只能走「人 · 决策」' },
{ key: 'no-stale-floor', statement: '底价超一周未更新的,先刷底价再判' },
@@ -115,7 +131,13 @@ export const manifest: ActivityManifest = {
humanCallBudget: 3,
},
resources: {
- tools: [{ name: 'quote-pricing.basis' }, { name: 'crm.history-price-read' }],
+ /*
+ * `quote-pricing.basis` 已从声明里去掉(2026-07-31):工具箱里从没注册过它,
+ * 装包时静态审一直报「运行时转人代办」—— 而重做后的案例模型压根不调它
+ * (算价归判定表 P-01/P-02,取基准读历史价)。声明一件不存在也不用的工具,
+ * 只会让每一单跑到 A4 就弹一张莫名其妙的转人卡。
+ */
+ tools: [{ name: 'crm.history-price-read' }],
skills: PRICING_SKILLS.map((s) => s.id),
models: ['deepseek-v4-flash'],
roles: ['BD'],
@@ -125,13 +147,16 @@ export const manifest: ActivityManifest = {
declarations: {
models: ['deepseek-v4-flash'],
- tools: [{ name: 'crm.history-price-read' }, { name: 'quote-pricing.basis' }],
+ tools: [{ name: 'crm.history-price-read' }],
ledger: [
{
domain: DOMAIN,
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: 'quoted_price', type: 'number', primaryKey: false, masked: false, label: '拟报单价' },
{ name: 'margin', type: 'number', primaryKey: false, masked: false, label: '毛利率' },
@@ -140,6 +165,27 @@ export const manifest: ActivityManifest = {
{ name: 'disposition', type: 'text', primaryKey: false, masked: false, label: '门禁处置' },
],
},
+ {
+ /*
+ * 定价结论 —— 一单一行的**汇总表**,验收机检谓词的入参就在这儿。
+ * 汇总列与逐行列**故意不同名**:任务视图把草稿行摊平到顶层是「后写的赢」,
+ * 拿逐行列求值只会验到最后写的那一行,那不叫机检、叫抽样(A1 的教训)。
+ */
+ domain: DOMAIN,
+ object: 'verdict',
+ kind: 'record',
+ caseKey: 'rfq_ref',
+ label: '定价结论',
+ fields: [
+ { name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' },
+ { name: 'pn', type: 'text', primaryKey: false, masked: false, label: '型号' },
+ { name: 'draft_total', type: 'integer', primaryKey: false, masked: false, label: '草案条数' },
+ { name: 'unpriced_total', type: 'integer', primaryKey: false, masked: false, label: '未定价条数' },
+ { name: 'over_total', type: 'integer', primaryKey: false, masked: false, label: '超限条数' },
+ { name: 'unresolved_over_total', type: 'integer', primaryKey: false, masked: false, label: '超限未处置条数' },
+ { name: 'summary', type: 'text', primaryKey: false, masked: false, label: '定价结论' },
+ ],
+ },
],
listeners: [
{
@@ -166,6 +212,8 @@ export const manifest: ActivityManifest = {
cmmnXml: CMMN_XML,
narrative: NARRATIVE,
dmn: P01_DMN,
+ /* 步骤剧本 —— 与 A1 同一套:doneWhen 真了引擎自己收步,不问模型 */
+ steps: PRICING_STEPS,
},
skills: PRICING_SKILLS,
viewTemplates: [
diff --git a/activities/quote-pricing/src/views/report.ts b/activities/quote-pricing/src/views/report.ts
index 0cd8a3d..e42c3e1 100644
--- a/activities/quote-pricing/src/views/report.ts
+++ b/activities/quote-pricing/src/views/report.ts
@@ -1,67 +1,192 @@
/**
- * A4 视图模版 —— 定价的三屏:取价基准 / 定价与门禁 / 报价草案待放行(AM 07)。
- * 定价这件事最需要「把判据摊开」:人签的不是一个数字,是这个数字的来路。
+ * A4 成本定价的 HTML 报告模版 —— **活动自己持有的资产**(§10.3),随包版本冻结。
+ *
+ * 定价这件事最需要「把判据摊开」:**人签的不是一个数字,是这个数字的来路**。
+ * 所以三张屏不按「概览 / 明细 / 结论」排,按**一个价是怎么长出来的**排:
+ * ① 取价基准 —— 跟谁比。基准取错,后面每一条偏离率都是假的,而假偏离率长得跟真的一样;
+ * ② 定价与底价门禁 —— 凭什么是这个价。两张决策表的规则原样摆出来,人能逐条倒着核;
+ * ③ 报价草案待放行 —— 签字前最后一次能改的机会,门后的都算数了。
+ *
+ * 一个局面一张,局面变了让 Agent 用 `view` 换,不在一张里堆条件分支。
+ * 容器是 `sandbox=""` iframe:无脚本、无外链、样式自带;数据由基座推送时填
+ * (`data-slot` 取值、`data-rows` 逐行展开)。
+ *
+ * 两条贯穿全包的呈现纪律:
+ * · **状态章一律用「在办」灯,不用「已办」印** —— 模版是随包冻结的静态字符串,
+ * 它盖不了一个还没发生的章。三张屏都是放行前的局面,盖「已通过」就是撒谎;
+ * · **`verdict` 的橙档专指「还没发生,等你按」** —— 绿只留给已达标的机检结论。
+ * 放行后果是预告不是既成事实,一律走橙档。
*/
-import { kvCard, noteCard, tableCard, viewShell } from '@sdk';
+import { noteOnly, section, stagesOnly, statsBand, tableOnly, verdict, viewShell } from '@sdk';
+/**
+ * 静态阶梯 —— **规则本身不是数据**:P-01 的毛利档、P-02 的三档处置随包冻结,
+ * 它们不该走 `data-rows` 等基座来填(那会让「规则」看起来像「这一单的结果」)。
+ * 沿用外壳已有的 `.steps` 版式,与引擎推的阶段列表同一副面孔 —— 页面上只该有一套样式。
+ */
+function ladder(rows: ReadonlyArray): string {
+ return `${rows
+ .map(
+ ([no, term, gloss]) =>
+ `- ${no}${term} ${gloss}
`,
+ )
+ .join('')}
`;
+}
+
+/** 局面一:S1 算价刚开场 —— 人此刻要核的是「这一单跟谁比、落在哪一档」 */
export const PRICING_BASIS = viewShell({
title: '取价基准',
- kicker: 'A4 · 成本定价',
+ kicker: 'A4 · 成本定价 · S1 算价',
tone: 'brand',
+ icon: 'coins',
+ stamp: { kind: 'live', text: '算价中' },
+ sign: 'A4 成本定价 · 取价基准(本单草稿)',
body:
- kvCard('本单标的', [
- ['客户编码', 'customerCode', 'mono'],
- ['型号 PN', 'pn', 'mono'],
- ['物料类型', 'materialType', 'tag'],
- ]) +
- kvCard('价格基准', [
- ['上次成交价', 'last_price'],
+ /*
+ * 指标带摆的是**利润模型 P-01 的三个入参 + 这一单的工作量**,不是所有字段。
+ * 三个档位键取的是清单里 P-01 声明的决策入参名(customerGrade / materialType /
+ * volumeTier)—— 决策表照这三个名字取值,模版就照这三个名字摆,不另起炉灶。
+ * `records` 是起单入参的数组,基座渲染数组时给的是条数,正好是「这一单要定几条价」。
+ */
+ statsBand([
+ ['待定价条目', 'records'],
['客户等级', 'customerGrade'],
+ ['料号类型', 'materialType'],
['数量档', 'volumeTier'],
]) +
- noteCard('基准从哪来', '基准价取自历史价情报(CRM)。新料号无历史时基准记 0,由底价门禁按无基准处置 —— 不凭空编一个偏离率出来。'),
+ section('基准从哪来 —— 四级优先级',
+ ladder([
+ ['1', '本客户近期成交价', '最贴近这一单的真实议价空间'],
+ ['2', '同型号他客户成交价', '价位可比,但议价空间要留余量'],
+ ['3', '供应商底价表', '成本侧的锚,不含客户侧信息'],
+ ['4', '无基准', '新料号、久未报价的老料号都会落到这里'],
+ ]) +
+ noteOnly(
+ '无基准不是异常,它本身就是一种基准。此时偏离率记 0 —— 没有可偏离的对象,' +
+ '由底价门禁按「无基准」处置。不要拿一个不相干的价去凑一个偏离率出来:' +
+ '那比没有基准更危险,因为它看起来像有依据。',
+ ),
+ { text: '越靠前越贴近实情' },
+ ) +
+ section('本单待定价条目',
+ tableOnly('mine:quote_pricing.draft', [
+ ['型号 PN', 'pn', 'mono'],
+ ['基准价', 'last_price'],
+ ['拟报单价', 'quoted_price'],
+ ['毛利率', 'margin'],
+ ]),
+ { text: '只列本单写下的行' },
+ ) +
+ section('案例当下进程', stagesOnly('case:stages'), { text: 'Enabled ≠ Active' }),
});
+/** 局面二:S2 自检 —— 人此刻要核的是「凭什么是这个价、门禁怎么判的」 */
export const PRICING_PRICED = viewShell({
title: '定价与底价门禁',
- kicker: 'A4 · DMN 两表',
- tone: 'ok',
+ kicker: 'A4 · 成本定价 · S2 自检',
+ tone: 'brand',
+ icon: 'gauge',
+ stamp: { kind: 'live', text: '自检中' },
+ sign: 'A4 成本定价 · 定价自检(判定表 P-01 / P-02)',
body:
- kvCard('定价结论', [
+ /* 标签一律照台账声明的列名(基准价 / 拟报单价 / 毛利率 / 偏离度 / 门禁处置):
+ 同一个数在指标带上叫一个名、在下面的表里叫另一个名,签字的人就得先花一秒确认它俩是不是一回事 */
+ statsBand([
+ ['基准价', 'last_price'],
['拟报单价', 'quoted_price'],
- ['目标毛利', 'margin'],
- ['相对基准偏离', 'deviation'],
- ['门禁处置', 'disposition', 'tag'],
+ ['毛利率', 'margin'],
+ ['偏离度', 'deviation'],
]) +
- noteCard(
- '两张决策表各管一段',
- '利润模型 P-01:客户等级 × 物料类型 × 数量档 → 目标毛利区间。' +
- '底价门禁 P-02:偏离底价 ±20% 即止 —— 超限的不是「拦下」,而是转人裁断(回调还是注明理由)。' +
- '两表都是确定性求值,同输入必同结论;命中的规则行随出参留痕,事后可逐条复盘。',
+ section('逐条定价结论',
+ tableOnly('mine:quote_pricing.draft', [
+ ['型号 PN', 'pn', 'mono'],
+ ['基准价', 'last_price'],
+ ['拟报单价', 'quoted_price'],
+ ['毛利率', 'margin'],
+ ['偏离度', 'deviation'],
+ ['门禁处置', 'disposition'],
+ ]),
+ { text: '判据在左 · 结论在右' },
+ ) +
+ section('目标毛利怎么定的 —— 利润模型 P-01',
+ ladder([
+ ['1', '战略客户 · A 类标准件 · 大批量', '目标毛利 8%–10%'],
+ ['2', '普通客户 · A 类标准件', '12%–15%'],
+ ['3', 'B 类波动件', '15%–20%'],
+ ['4', 'C 类替代 / EOL', '≥20%,或一事一议'],
+ ['5', '以上都不命中', '兜底按普通客户标准件 12%–15%'],
+ ]) +
+ noteOnly(
+ '自上而下取第一条命中的规则,同输入必得同结论、零方差零 token,命中的规则行随出参留痕。' +
+ '不要为了让门禁「过」而回头改毛利参数 —— 那是把判据改成结论,决策表也就失去了它唯一的价值。',
+ ),
+ { text: '首条命中' },
+ ) +
+ section('偏离怎么判的 —— 底价门禁 P-02',
+ ladder([
+ ['1', '拟报价 ≥ 底价', '直接放行'],
+ ['2', '低于底价 ≤ 20%', 'PM 附理由放行'],
+ ['3', '低于底价 超 20%', '拦截 · BOSS 拍板'],
+ ]) +
+ noteOnly(
+ '门禁不是「拦下」而是分流:超限的一律调人 —— 要么回调价格,要么写明为什么这样报,' +
+ '两条路都合法,但都要留下理由。这条留痕将来会成为客户等级与利润模型的迭代素材。',
+ ),
+ { text: '±20% 即止' },
+ ) +
+ section('本单门禁结论',
+ /*
+ * 结论章里嵌 `data-slot`:处置与偏离都是**这一单算出来的事实**,
+ * 写死一句「已通过」就是替判定表下结论。橙档在此的意思是「还没定,等人处置」。
+ */
+ verdict('底价门禁 P-02 处置:', 'wait', '偏离度 ') +
+ noteOnly(
+ '禁区两条随本单同行:不得静默绕开 P-02 的结论 —— 要破线只能走「人 · 决策」;' +
+ '底价超一周未更新的,先刷底价再判 —— 拿旧底价判新行情等于没判。',
+ ),
),
});
+/** 局面三:待放行 —— 验收单上人要签的那份成果报告 */
export const PRICING_RELEASE = viewShell({
title: '报价草案 · 待放行',
- kicker: 'A4 · 成果预览',
+ kicker: 'A4 · 成本定价 · 成果待签',
tone: 'case',
+ icon: 'shield-check',
+ stamp: { kind: 'live', text: '待签字放行' },
+ sign: 'A4 成本定价 · 报价草案(待放行)',
body:
- tableCard(
- '本单报价草案',
- 'mine:quote_pricing.draft',
- [
- ['型号', 'pn', 'mono'],
+ statsBand([
+ ['拟报单价', 'quoted_price'],
+ ['毛利率', 'margin'],
+ ['偏离度', 'deviation'],
+ ['门禁处置', 'disposition'],
+ ]) +
+ section('本单交付:报价草案',
+ tableOnly('mine:quote_pricing.draft', [
+ ['型号 PN', 'pn', 'mono'],
['基准价', 'last_price'],
- ['拟报价', 'quoted_price'],
- ['毛利', 'margin'],
- ['偏离', 'deviation'],
- ['门禁', 'disposition'],
- ],
- '逐条列出,便于放行前逐条核对。',
+ ['拟报单价', 'quoted_price'],
+ ['毛利率', 'margin'],
+ ['偏离度', 'deviation'],
+ ['门禁处置', 'disposition'],
+ ]) +
+ noteOnly('只列本单写下的行 —— 别单放行过的记录不混进来。逐条列出,便于放行前逐条核对。'),
+ { text: '本单草稿' },
) +
- noteCard(
- '放行之后',
- '放行通过 → 草稿合并入正式版 → 进 AP 定价审批(卡到 PM 工位)。' +
- '审批驳回按声明走向回 A4 重开新实例 —— 新任务 ID、留痕关联,不复活旧任务。',
+ section('放行前要过的三道判据',
+ ladder([
+ ['1', '每条都有拟报单价与毛利率', 'L1 机检 · unpriced = 0'],
+ ['2', '偏离底价 ≤ ±20%,超限者已回调或注明理由', 'L2 机检 · 底价门禁 P-02'],
+ ['3', 'BD 放行', 'L5 人签 · 签了才算数'],
+ ]),
+ { text: '两机检 · 一人签' },
+ ) +
+ section('放行之后会发生什么',
+ verdict('放行即落地:草稿合并入正式版,草案提交 A5 定价审批', 'wait', '审批点卡在 PM 工位') +
+ noteOnly(
+ '审批驳回按声明走向,不由临场决定:回 A4 重开新实例 —— 新任务 ID、留痕关联,' +
+ '不复活旧任务。所以这一屏上还改得动的每一格,过了这道门就要花一整个新实例才改得回来。',
+ ),
),
});
diff --git a/activities/quote-sourcing/src/case-model.ts b/activities/quote-sourcing/src/case-model.ts
index df3af27..631e008 100644
--- a/activities/quote-sourcing/src/case-model.ts
+++ b/activities/quote-sourcing/src/case-model.ts
@@ -1,120 +1,132 @@
/**
- * A3 渠道寻源的案例模型 —— **双表示同版本**:CMMN XML(机器面)与案例说明书(叙事面)。
- * 「说明书是 XML 的叙事投影,迭代时一起放行」(AM 3.1)。
+ * A3 渠道寻源的案例模型 —— **一张执行单办完**(2026-07-31 用户定,与 A2 同一口径)。
+ *
+ * 上一版按四种询价场景(BOM 早期 / BOM 后期 / 现货 / 样品)把流程铺开成一串计划项。
+ * 场景之分在业务上是真的,可它**不改变系统要管的事**:发函、收回价、比价、给结论。
+ * 而这四步在一次寻源里是连着做的 —— 采购不会办完「发函」就交回来等系统叫他。
+ * 把它切成四项,换来的只是 Agent 每一步都要跟 harness 对一次账。
+ *
+ * 现在只有两项:
+ * T_source —— 派一张执行单给采购(卡面两张可编辑表:供应商回价 / 寻源比价),他填完提交;
+ * T_release —— 放行(触达世界,人工激活)。
+ *
+ * 场景之分不进模型,进**说明书**:采购自己知道 BOM 早期该广撒网、现货该问库存。
+ * 那是他的判断,不是系统的步。
*/
export const CMMN_XML = `
+
-
-
-
+
+
-
- create
-
-
- update
- needNewQuote = true
-
-
- update
- channelsPicked >= 2
-
-
- create
-
-
- create
+
+ update
+ compare_total > 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
- P4D
-
-
- P7D
-
+
+
+
+
+
`;
-export const NARRATIVE = `§0 开案:本案例覆盖四种询价场景 —— BOM早期、BOM后期、现货询价、样品询价。
-场景不同,供应商选择尺度与价格验证规则各异,但都遵循同一主线:
-PM 提交询价请求 → 查历史报价判定可否直采 →(须询价时)按场景圈渠道 → 发询验证 → 填入寻源比价表反馈。
+export const NARRATIVE = `§0 开案:A1 判出一条料要寻源(无近期历史价、或行情波动),这一单就到你手上。
+成品只有一样:**一份能拿去定价的比价结论** —— 几家报了什么价、交期多久、推荐哪一家、为什么。
-S1 接需核史:系统带出在供供应商与历史报价。先核历史,按场景规则逐 PN 判定是否可直接采用 ——
-可直采则跳过询价直接进入反馈交付;须新询价则进入选渠阶段。
- · 查历史报价:未达 3 次询价但有效期已过的,标记须重询。
- · 判定采用策略:BOM早期有效期内直采、过期浮动≤10%直采、10-20%取均价、>20%重询;
- BOM后期历史最低(≥3次)直采,否则降 20% 谈;现货/样品一律重询。
+**这一单只有一步**:把寻源这件事派给采购(执行工单),他填完你收进台账,然后提交放行。
+四种询价场景(BOM 早期 / BOM 后期 / 现货 / 样品)的打法差别是**采购的判断**,
+不是系统要管的步 —— 你在 ask 里说清是哪种场景就够了,别替他把流程铺开。
-S2 选渠定策:BOM早期/样品渠道可放宽;BOM后期按主→备选→贸易商顺序圈 2~3 家,贸易商尽量不用;
-现货仅选有库存且可提供仓库实拍的供应商。渠道资质核实是「看情况」的裁量项。
+【T_source 派执行单】发 human 行动(workOrder="execution"),卡面 blocks 放**两张可编辑表**:
+· 供应商回价:列 = 型号 / 供应商 / 回价 / MOQ / 交期 / 有效期;
+· 寻源比价:列 = 型号 / 供应商 / 回价 / 结论。
+两张都 editable:true;没有的值给空字符串 —— 空格子界面会自己标红「待填写」。
+ask 说清要他做什么(哪颗料、什么场景、要几家、什么时候要);
+basis 摆上型号、数量、目标价、客户交期要求(裸卡基座拒发)。
-S3 发询比价:同一料所有渠道一次发齐;PN/数量/目标价/回价时限写死在询价里。
-回价逐单登记,一一对应不可缺项。价格验证因场景而异。回价异常或未应答则追价补询(裁量项)。
+他提交后,回传值出现在你的轨迹里:表格那一份的键是 **table:0 / table:1**(块序号),
+值是整张表的 JSON —— **第一行是列名**,其余每行一条,列序与你派出去时一致。
+照它 write 进 \`quote_sourcing.supplier_quote\` 与 \`quote_sourcing.compare_table\`,
+并把比价表条数记进 compare_total。**写完就 finish T_source,别再发第二张单** —— 他已经答过了。
-S4 反馈交付:确认报价/LT/SPQ/有效期逐项填入寻源比价表,供 PM 直接查阅。
-现货/样品场景多一步下单与跟踪(裁量项)。
+【提交放行】比价表有行了 —— **先 start T_release**(人工激活:放行触达世界,必须按一下),
+然后 submit,BD 验收。终局唯放行:你不能自己宣布完成。
-全程随时可做:知识入库 —— 回价与成交沉淀历史价格情报,渠道表现回写供应商渠道目录。
+【一路上要说话】负责人正看着这一单。每一拍带一句 say,说**业务的话**:
+哪颗料在寻源、找了几家、回价怎么样、这一单往哪走。
+一两句、口语。别报「写草稿 2 行」这种系统词。`;
-§Z 结案:必做的事都办完、没有进行中的事,自动算结案(CMMN AutoComplete)。
-寻源比价表交付 PM 即结案 —— 每询一轮,行情底账厚一层。`;
+/**
+ * 步骤剧本 —— 与 A1/A2 同一套机制:`doneWhen` 为真,引擎自己收步、**不问模型**。
+ * 放行那一项不给 `doneWhen` —— 它触达世界,必须人按一下(铁律③)。
+ */
+export const SOURCING_STEPS = [
+ {
+ item: 'T_source',
+ intent: '派一张执行单给采购,收回供应商回价与比价结论',
+ writes: ['quote_sourcing.supplier_quote', 'quote_sourcing.compare_table', 'quote_sourcing.verdict'],
+ reads: [],
+ tools: [],
+ /*
+ * 比价表有行了才算办完。**不能写 `compare_total != null`** ——
+ * S-FEEL 里未绑定的名字 `!= null` 求值为 true,那一步开局即被收掉(A1 踩过)。
+ */
+ doneWhen: 'compare_total > 0',
+ guide: `**照这个骨架发,一个字段都别少**(prose 描述不管用,直接抄):
+{"kind":"human","itemId":"T_source","workOrder":"execution","card":{
+ "title":"…","ask":"这颗料要寻源,请找几家报价并给比价结论",
+ "basis":[{"k":"…","v":"…"}],
+ "blocks":[
+ {"t":"table","title":"供应商回价","cols":["型号","供应商","回价","MOQ"],"editable":true,
+ "rows":[["NE555P","","",""],["NE555P","","",""]]},
+ {"t":"table","title":"寻源比价","cols":["型号","供应商","回价","结论"],"editable":true,
+ "rows":[["NE555P","","",""],["NE555P","","",""]]}
+ ]}}
+**一条料至少摆 2 行** —— 判据「BOM后期每料 2~3 家渠道」机检的就是渠道数,
+只问一家过不了门(型号那列你先填,供应商与价格留给采购)。
+**rows 必须有行**:一条料一行,不知道的值给空字符串(界面自己标红「待填写」)。
+只给 cols 不给 rows,人看到的是一排表头加一片空白,一个格子都填不了(实测栽过)。
+**blocks 少了整张卡就废了** —— 基座会用当下局面兜底成一句「需要你判断下一步」,
+人拿到一张没有表的空卡,这一步等于没派(实测栽过)。
+
+**rows 一条料一行**,型号那一列先填上;ask 说清什么场景、要几家、什么时候要;
+basis 摆上型号、数量、目标价、交期要求(**裸卡基座拒发**)。
+
+他提交后回传值进你的轨迹:表格的键是 **table:0 / table:1**,值是整张表的 JSON,
+**第一行是列名**。照它 write 进两张台账,并把比价表条数记进 compare_total。
+**同一拍再写一行 \`quote_sourcing.verdict\`**(验收机检读的就是它):
+· supplier_total = 询了几家渠道(判据要 ≥2,**只问一家过不了门**);
+· quoted_total / unquoted_total = 已回价 / 未回价条数(未回价要 = 0);
+· compare_total = 比价表条数;recommend_supplier 推荐哪家;summary 一句人话结论。
+这几个数写不对,submit 会被门挡回来 —— **它们不是台账装饰,是判据本身**。
+**写完就 finish,别再发第二张单** —— 他已经答过了。
+**每一行都要带 \`rfq_ref\`** —— 它是这一单的单标识(起单入参里就有,原样抄进去)。
+台账主键第一列就是它:不写,这一行会和**别的单**同料号的那一行撞主键,把人家覆盖掉
+(实测栽过:A4 的报价草案主键只有 pn,跑四轮只剩最后一轮的 3 行)。`,
+ },
+ {
+ item: 'T_release',
+ intent: '比价结论已齐,提交放行交 BD 验收',
+ writes: [],
+ reads: [],
+ tools: [],
+ guide: `比价表有行了 —— **先 start T_release**(它标了人工激活),然后 submit,BD 验收。
+终局唯放行:你不能自己宣布完成。`,
+ },
+];
diff --git a/activities/quote-sourcing/src/index.ts b/activities/quote-sourcing/src/index.ts
index 4f4584d..9250438 100644
--- a/activities/quote-sourcing/src/index.ts
+++ b/activities/quote-sourcing/src/index.ts
@@ -12,7 +12,6 @@ const COMPARE = 'quote_sourcing.compare_table';
export default defineActivity({
name: 'quote-sourcing',
- paradigm: 'loop',
async run(base: ActivityBase): Promise {
const quotes = await base.ledger.read(QUOTES);
diff --git a/activities/quote-sourcing/src/manifest.ts b/activities/quote-sourcing/src/manifest.ts
index c1fe17c..581d0e5 100644
--- a/activities/quote-sourcing/src/manifest.ts
+++ b/activities/quote-sourcing/src/manifest.ts
@@ -11,7 +11,7 @@ import type { ActivityManifest, DmnModel } from '@contracts';
import { SOURCING_SKILLS } from './skills.ts';
import { SOURCING_CASE } from './views/report.ts';
import { activityIdSchema } from '@contracts';
-import { CMMN_XML, NARRATIVE } from './case-model.ts';
+import { CMMN_XML, NARRATIVE, SOURCING_STEPS } from './case-model.ts';
const DOMAIN = 'quote_sourcing';
const ACTIVITY_ID = activityIdSchema.parse('quote-sourcing');
@@ -45,7 +45,7 @@ export const manifest: ActivityManifest = {
identity: {
activityId: ACTIVITY_ID,
name: 'A3 · 渠道寻源',
- packageVersion: '0.1.3',
+ packageVersion: '2.0.0',
category: '统一报价',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-quote-sourcing',
@@ -57,8 +57,9 @@ export const manifest: ActivityManifest = {
input: {
data: {
type: 'object',
- required: ['pn', 'scenario'],
+ required: ['rfqRef', 'pn', 'scenario'],
properties: {
+ rfqRef: { type: 'string', title: '询价单号(单标识,贯穿全流程)' },
pn: { type: 'string' },
scenario: { type: 'string' },
quantity: { type: 'integer' },
@@ -78,13 +79,28 @@ export const manifest: ActivityManifest = {
],
selective: [],
},
- requiredDeliverables: [{ key: 'compare-table', label: '寻源比价表' }],
+ /*
+ * 必交付物**必须给 `ledger` 地址**(2026-07-31 修)。
+ * 不给的话预检退回 `scope[key]` 去找 —— key 是 `benchmark-table` 这样的短横线别名,
+ * 任务视图里永远没有这个键,于是「必交付物缺一不得提交放行」永远拒,
+ * 活办完了也放不了行,连拒五拍熔断(实测 A2 死于此)。
+ */
+ requiredDeliverables: [
+ { key: 'compare-table', label: '寻源比价表', ledger: `${DOMAIN}.compare_table` },
+ ],
+ /*
+ * 机检谓词读的是 `verdict` 那张**汇总表**的列(照 A1 的口径)。
+ * 上一版的名字(unquoted / channelsPicked)**台账里根本没有** —— S-FEEL 拿未绑定的名字
+ * 比大小求值为假,判据永远达不了标,submit 门永远拒,连拒五拍熔断。
+ * 判据的名字必须落在台账上,否则「机检」是一句空话(2026-07-31 销账)。
+ */
acceptanceCriteria: [
- { key: 'each-has-quote', label: '每料有有效报价或明确无货', level: 'L2', predicate: 'unquoted = 0' },
- { key: 'channels-enough', label: 'BOM后期每料 2~3 家渠道', level: 'L2', predicate: 'channelsPicked >= 2' },
+ { key: 'each-has-quote', label: '每料有有效报价或明确无货', level: 'L2', predicate: 'unquoted_total = 0' },
+ { key: 'channels-enough', label: 'BOM后期每料 2~3 家渠道', level: 'L2', predicate: 'supplier_total >= 2' },
{ key: 'cross-check', label: '价格验证按场景规则做过', level: 'L3' },
{ key: 'prm-release', label: '采购放行', level: 'L5' },
],
+ revisableAtRelease: [],
prohibitions: [
{ key: 'no-stockless-trader', statement: '现货场景不得选无库存转单商' },
{ key: 'no-unverified', statement: '不实回价或远低行情的,必须换渠道补询,不得直接采用' },
@@ -110,8 +126,11 @@ export const manifest: ActivityManifest = {
{
domain: DOMAIN,
object: 'supplier_quote',
+ 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 },
{ name: 'supplier', type: 'text', primaryKey: true, masked: false, label: '供应商' },
{ name: 'price', type: 'number', primaryKey: false, masked: false, label: '回价' },
@@ -124,14 +143,39 @@ export const manifest: ActivityManifest = {
{
domain: DOMAIN,
object: 'compare_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 },
{ name: 'supplier', type: 'text', primaryKey: false, masked: false },
{ name: 'price', type: 'number', primaryKey: false, masked: false },
{ name: 'conclusion', type: 'text', primaryKey: false, masked: false, label: '结论' },
],
},
+ {
+ /*
+ * 寻源结论 —— 一单一行的**汇总表**,验收机检谓词的入参就在这儿。
+ * 汇总列与逐行列**故意不同名**:任务视图把草稿行摊平到顶层是「后写的赢」,
+ * 拿逐行列求值只会验到最后写的那一行,那不叫机检、叫抽样(A1 的教训)。
+ */
+ domain: DOMAIN,
+ object: 'verdict',
+ kind: 'record',
+ caseKey: 'rfq_ref',
+ label: '寻源结论',
+ fields: [
+ { name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' },
+ { name: 'pn', type: 'text', primaryKey: false, masked: false, label: '型号' },
+ { name: 'supplier_total', type: 'integer', primaryKey: false, masked: false, label: '已询渠道数' },
+ { name: 'quoted_total', type: 'integer', primaryKey: false, masked: false, label: '已回价条数' },
+ { name: 'unquoted_total', type: 'integer', primaryKey: false, masked: false, label: '未回价条数' },
+ { name: 'compare_total', type: 'integer', primaryKey: false, masked: false, label: '比价条数' },
+ { name: 'recommend_supplier', type: 'text', primaryKey: false, masked: false, label: '推荐供应商' },
+ { name: 'summary', type: 'text', primaryKey: false, masked: false, label: '寻源结论' },
+ ],
+ },
],
listeners: [
{
@@ -167,6 +211,8 @@ export const manifest: ActivityManifest = {
// 双表示同版本:XML 是机器面,说明书是叙事面 —— 不允许分叉
narrative: NARRATIVE,
dmn: STRATEGY_DMN,
+ /* 步骤剧本 —— 与 A1 同一套:doneWhen 真了引擎自己收步,不问模型 */
+ steps: SOURCING_STEPS,
},
skills: SOURCING_SKILLS,
viewTemplates: [{ name: 'case-overview', html: SOURCING_CASE }],
diff --git a/activities/quote-sourcing/src/views/report.ts b/activities/quote-sourcing/src/views/report.ts
index 71b3b49..6f4a810 100644
--- a/activities/quote-sourcing/src/views/report.ts
+++ b/activities/quote-sourcing/src/views/report.ts
@@ -1,25 +1,101 @@
/**
- * A3 视图模版 —— 案例全景(AM 07:loop 按案例模型的**阶段与里程碑**组织汇报)。
+ * A3 渠道寻源的 HTML 汇报模版 —— **活动自己持有的资产**(§9.3),随包版本冻结。
*
- * 画面素材全部来自语义引擎:阶段的生命周期态、当下可为(Enabled 集)、目标缺口。
- * 这些是算出来的确定事实,零 LLM 即可成屏 —— 模型只决定「下一步做什么」,不负责画面。
+ * ── 这一屏给谁看、他此刻要判什么 ──────────────────────────────────
+ * 看的人是采购(PRM)与等着底价的 PM。他此刻只判一件事:**这价能不能拿去往下走**。
+ * 于是版面按「先成果、后局面」排:
+ * 一 供应商回价(收回来多少、都是什么渠道)· 二 寻源比价表(交回 PM 的成果本体)
+ * → 三 案例阶段 · 四 当下可为 · 五 目标缺口(AI 的办案局面)
+ * 指标带扫一眼就该有结论(回价够不够、比价表填没填),往下五节是「凭什么这么说」。
+ *
+ * ── 画面上的数从哪来(一个都不是编的)────────────────────────────
+ * · `mine:quote_sourcing.*` 是**本单自己写下的草稿行** —— 别单放行过的回价不混进来;
+ * 列名逐字取自 `declarations.ledger`,编一个不存在的列名 = 屏上永远空白。
+ * · `case:*` / `enabledCount` / `gapCount` / `activeStage` 由语义引擎每拍算出,
+ * 是确定事实、零 LLM 即可成屏 —— 模型只决定「下一步做什么」,不负责画面。
+ * · 指标带上「已收回价 / 比价表行数」两格指的是**数组本身**:填数时数组取长度
+ * (view-render 的 `displayValue` 约定),不必为「几单」另立一个计数字段。
+ *
+ * ── 两条不撒谎的纪律 ──────────────────────────────────────────
+ * ① 右上角挂的是「在办」灯不是印章:这一屏是活的局面,它没盖过章,就别画成盖了章的样子;
+ * 紫(tone `case`)是**案例活动的标记色不是状态色** —— 红绿橙只留给「达标没达标」。
+ * ② 表空着就是「本单还没写下这一行」,不是渲染坏了 —— 每张表的说明里都写明空着意味着什么。
+ * 寻源尤其怕这个:空白被读成「这家没报价」,而它可能只是还没登记。
+ *
+ * 容器是 `sandbox=""` iframe:无脚本、无外链,样式与图标由 `@sdk` 自带。
*/
-import { kvCard, noteCard, stagesCard, viewShell } from '@sdk';
+import { noteOnly, section, stagesOnly, statsBand, tableOnly, viewShell } from '@sdk';
export const SOURCING_CASE = viewShell({
title: '渠道寻源 · 案例全景',
- kicker: 'A3 · 案例活动(loop)',
+ kicker: 'A3 · 渠道寻源 · 案例活动',
tone: 'case',
+ icon: 'mail',
+ stamp: { kind: 'live', text: '案例在办 · 非定稿' },
+ sign: 'A3 渠道寻源 · 本页每拍按当下局面重画',
body:
- kvCard('本单标的', [
- ['型号 PN', 'pn', 'mono'],
- ['场景', 'scenario'],
- ['当前阶段', 'activeStage', 'tag'],
+ statsBand([
+ ['已收回价', 'mine:quote_sourcing.supplier_quote'],
+ ['比价表行数', 'mine:quote_sourcing.compare_table'],
['当下可为', 'enabledCount'],
['目标缺口', 'gapCount'],
]) +
- stagesCard('阶段(CMMN)', 'case:stages', '阶段的生命周期态由语义引擎推进到 Enabled 为止;凡触达世界或产出关键交付的计划项必须人工启动 —— Enabled ≠ Active。') +
- stagesCard('当下可为', 'case:enabled', 'AI 只能在这个集合里选下一步:可以越谱,不可越权。') +
- stagesCard('目标缺口', 'case:gaps', '缺口只有人知道 → 调人;缺口在世界侧 → 立监听(必配时限)。当下可为 ∩ 目标所需 = 空,就是依赖断点。') +
- noteCard('这个活动为什么是 loop', '寻源是一事一议:几家供应商、谁先回、要不要追加一轮,事前排不出谱。调度权交给 AI,系统只保证边框 —— 预算、停滞熔断、放行归人。'),
+ section('供应商回价(本单登记)',
+ tableOnly('mine:quote_sourcing.supplier_quote', [
+ ['型号 PN', 'pn', 'mono'],
+ ['供应商', 'supplier'],
+ ['渠道等级', 'grade'],
+ ['回价', 'price', 'mono'],
+ ['交期', 'lead_time'],
+ ['有效期', 'valid_until', 'mono'],
+ ]) +
+ noteOnly(
+ '回价逐单登记、一一对应不可缺项:发出去几家就该收回几条,没回音的也要留一条写明' +
+ '「无货 / 未应答」—— 少一条,下游读到的就是「没这回事」。渠道等级是场景尺度落到纸面的地方:' +
+ 'BOM后期按主 → 备选 → 贸易商圈 2~3 家、贸易商尽量不用,现货只选有库存的。' +
+ '「有效期」是回价到期监听盯的那一列,过期即触发重询。表空着,是本单还没登记回价。',
+ ),
+ { text: '一料一渠一行' },
+ ) +
+ section('寻源比价表(本单交付件)',
+ tableOnly('mine:quote_sourcing.compare_table', [
+ ['型号 PN', 'pn', 'mono'],
+ ['供应商', 'supplier'],
+ ['回价', 'price', 'mono'],
+ ['结论', 'conclusion'],
+ ]) +
+ noteOnly(
+ '这是交回 PM 的成果本体:一料一结论,多渠道横比定底。不实回价或远低行情的必须换渠道补询,' +
+ '不得直接采用;现货场景不得选无库存转单商。采用策略由判定表出(直采 / 取均价 / 重询),' +
+ '不是模型自己说了算。表空着,是本单还没走到反馈交付。',
+ ),
+ { text: '交回 PM 的成果' },
+ ) +
+ section('案例阶段(CMMN 生命周期)',
+ stagesOnly('case:stages') +
+ noteOnly(
+ '阶段的生命周期态由语义引擎推进到 Enabled 为止;凡触达世界或产出关键交付的计划项' +
+ '必须人工启动 —— Enabled ≠ Active。寻源是一事一议:几家供应商、谁先回、' +
+ '要不要追加一轮,事前排不出谱,所以调度权交给 AI,系统只保证边框:预算、停滞熔断、放行归人。',
+ ),
+ { text: '推进到 Enabled 为止' },
+ ) +
+ section('当下可为(AI 的合法选项)',
+ stagesOnly('case:enabled') +
+ noteOnly(
+ 'AI 只能在这个集合里选下一步:可以越谱,不可越权。发询价信是触达世界的动作 ——' +
+ '标「待启动」的项要人按一下再动,信发出去就收不回来。这一栏空着不是它偷懒,' +
+ '是当下确实无可为:要么在等人回话,要么在等供应商的回音。',
+ ),
+ { text: '本拍的合法选项' },
+ ) +
+ section('目标缺口(离交付还差什么)',
+ stagesOnly('case:gaps') +
+ noteOnly(
+ '缺口只有人知道 → 调人;缺口在世界侧(等供应商回价)→ 立监听,时限必配 ——' +
+ '没有时限的等待就是无限期挂起,而回价的时限本就写死在询价信里。' +
+ '当下可为 ∩ 目标所需 = 空,就是依赖断点。这一栏空着,是必做的事都办完了。',
+ ),
+ { text: '空 = 必做的都办完了' },
+ ),
});
diff --git a/api-report/apps/api/src/modules/activity-registry/index.d.ts b/api-report/apps/api/src/modules/activity-registry/index.d.ts
index 2da6f47..7509263 100644
--- a/api-report/apps/api/src/modules/activity-registry/index.d.ts
+++ b/api-report/apps/api/src/modules/activity-registry/index.d.ts
@@ -6,7 +6,8 @@ export { ActivityNotInstalledError, InstallRejectedError, ToolNotDeclaredError }
export type { Clock, IdGenerator, LedgerDeclarationApplier, ModelBindingProbe, RegistryStore, TemplateGuard, ToolContractProbe, } from './port.ts';
export type { RegistryServiceDeps } from './internal/usecase/registry-service.ts';
export { createRegistryService } from './internal/usecase/registry-service.ts';
-export { acceptanceLevelFindings, auditPassed, danglingListeners, loopFindings, scanArtifact, staticAudit, } from './internal/domain/static-audit.ts';
+export type { AcceptanceAuditInput, DeliverableAuditInput } from './internal/domain/static-audit.ts';
+export { acceptanceLevelFindings, deliverableAddressFindings, ledgerKindFindings, auditPassed, danglingListeners, loopFindings, scanArtifact, staticAudit, } from './internal/domain/static-audit.ts';
export { ASSET_KINDS, assertTransition, buildLoadEnvelope, canTransition, IllegalIncrementTransitionError, INCREMENT_STATES, judgeReplay, mayTakeEffect, shouldSeedAsset, } from './internal/domain/versioning.ts';
export { createRegistryStore } from './internal/adapter/registry-store.ts';
export { activityAssetTable, activityPackageTable, assetIncrementTable } from './internal/adapter/registry-schema.ts';
diff --git a/api-report/apps/api/src/modules/human-tool/contract.d.ts b/api-report/apps/api/src/modules/human-tool/contract.d.ts
index 8b2b693..521a32c 100644
--- a/api-report/apps/api/src/modules/human-tool/contract.d.ts
+++ b/api-report/apps/api/src/modules/human-tool/contract.d.ts
@@ -54,6 +54,8 @@ export type IssuedChatCard = {
};
export type ReplyOutcome = {
readonly order: WorkOrder;
+ /** 这张卡挂在哪一单 —— 装配层要拿它把「人就地改的那几行」写回草稿层 */
+ readonly taskId: TaskId;
/** 完成只出自放行通过 —— 这里为真,任务才可收敛终态 */
readonly producesCompletion: boolean;
/** 驳回:环内回转,任务继续 */
@@ -78,9 +80,39 @@ export type HumanToolService = {
}): Promise;
issueChatCard(context: IssueContext, card: ChatCard): Promise;
replyWorkOrder(reply: WorkOrderReply, by: StaffId): Promise;
- replyChatCard(reply: ChatCardReply, by: StaffId): Promise;
+ /**
+ * 回传带上 `taskId` —— **数据录入卡的确认要当场写草稿**,而写库要知道写给哪一单。
+ * 由本模块自己写会让 human-tool 依赖 ledger(越层),所以把事实交出去,由组装层落笔。
+ */
+ replyChatCard(reply: ChatCardReply, by: StaffId): Promise<{
+ card: ChatCard;
+ taskId: TaskId;
+ }>;
/** 驳回重答:新卡实例,原卡闭合 —— 只增不改 */
rejectAndReask(cardId: string, reason: string): Promise;
+ /**
+ * 给已发出的验收工单补上选择组(AM 1.2 恰选一口)。
+ *
+ * 为什么分两步:卡由 `release.submitForRelease` 发 —— 它守的是预检那道门,
+ * 不该也不必知道「这个活动有哪些出口」。出口是契约的事实,由 app 从 manifest 现读后贴上。
+ */
+ attachPortChoice(workOrderId: string, choice: {
+ group: string;
+ ports: ReadonlyArray<{
+ name: string;
+ label?: string | undefined;
+ }>;
+ }): Promise;
+ /**
+ * **算出来的去向**上卡 —— 不给人选,只告诉他走哪一口、凭哪一条。
+ * 去向是台账那几个数的函数;摆成选择题,等于把可回放的判定换成一次手感。
+ */
+ attachRoutedPort(workOrderId: string, routed: {
+ group: string;
+ name: string;
+ label?: string | undefined;
+ because: string;
+ }): Promise;
openWorkOrders(taskId: TaskId): Promise;
/** 全部工单(含已办结)—— 工作成果要回看定稿的报告,只给 open 就永远是空的 */
allWorkOrders(taskId: TaskId): Promise;
diff --git a/api-report/apps/api/src/modules/human-tool/port.d.ts b/api-report/apps/api/src/modules/human-tool/port.d.ts
index 0cc6664..d6b4593 100644
--- a/api-report/apps/api/src/modules/human-tool/port.d.ts
+++ b/api-report/apps/api/src/modules/human-tool/port.d.ts
@@ -17,6 +17,8 @@ export type CardStore = {
} | undefined>;
listWorkOrders(taskId: TaskId, openOnly: boolean): Promise;
markWorkOrderDone(id: string, done: NonNullable): Promise;
+ /** 局部改卡面 —— 只增不改是对**已回传**的卡;未答的卡还可补齐契约事实(如选择组) */
+ patchWorkOrder(id: string, patch: Partial): Promise;
saveChatCard(taskId: TaskId, card: ChatCard, recipientId: StaffId): Promise;
getChatCard(id: string): Promise<{
taskId: TaskId;
diff --git a/api-report/apps/api/src/modules/interaction/contract.d.ts b/api-report/apps/api/src/modules/interaction/contract.d.ts
index d7e9bb0..e52f6ab 100644
--- a/api-report/apps/api/src/modules/interaction/contract.d.ts
+++ b/api-report/apps/api/src/modules/interaction/contract.d.ts
@@ -50,17 +50,36 @@ export type InteractionService = {
/** 视图流控:活动按局面推结构化块 */
pushView(taskId: TaskId, blocks: readonly ViewBlock[]): Promise;
/** 视图流控:活动推 HTML 模版 —— 过净化门,不过即抛 */
- pushViewHtml(taskId: TaskId, html: string): Promise;
+ /**
+ * 推一帧 HTML。`template` **要一起存下来** —— 件是投影,而投影得能按当下数据**重渲**。
+ *
+ * 只存渲染好的 HTML,那一帧就成了快照:人在件上改完几格,由数据算出来的东西
+ * (类徽 A/B/C、分布条、结论横幅那条去向)全停在渲染那一刻 ——
+ * 屏上一半新一半旧,那不是没更新,是界面在撒谎。记住模版名,才重得出来。
+ */
+ pushViewHtml(taskId: TaskId, html: string, template?: string): Promise;
/** 视图区当前呈现什么 —— 只读投影,刷新即重建(前端零过程状态) */
viewOf(taskId: TaskId): Promise<{
blocks?: readonly ViewBlock[];
html?: string;
+ template?: string;
} | undefined>;
/** 控制权转交 —— 随引擎的激活 / 完成事件自动发生 */
setPhase(taskId: TaskId, phase: InteractionPhase): Promise;
phaseOf(taskId: TaskId): Promise;
/** SSE:单连接、按信封目标失效缓存 */
subscribe(staffId: StaffId, onPush: (envelope: PushEnvelope) => void): Subscription;
+ /**
+ * **系统级转接**:装配层把所有推送接出去(转给前端的 SSE 广播),不按人筛。
+ *
+ * 为什么不复用 `subscribe`:那个要一个收件人,而转接是系统的事 ——
+ * 为它编一个假员工 id 是撒谎。不按人筛也不漏:推送**不带业务载荷**
+ * (只说「哪个任务动了」),谁该看见由前端自己那几把查询决定(§9.2 前端零过程状态)。
+ *
+ * 没有这个口子的后果实测过:AI 说的每一句、推的每一帧画面都广播给了空气 ——
+ * `subscribe` 一个订阅者都没有,前端只好靠人点刷新。
+ */
+ onPush(listener: (envelope: PushEnvelope) => void): Subscription;
/** 供上层广播失效信号 */
invalidate(target: 'task' | 'workflow' | 'desk', id: string): void;
};
diff --git a/api-report/apps/api/src/modules/interaction/port.d.ts b/api-report/apps/api/src/modules/interaction/port.d.ts
index e09fc13..d4c64fb 100644
--- a/api-report/apps/api/src/modules/interaction/port.d.ts
+++ b/api-report/apps/api/src/modules/interaction/port.d.ts
@@ -16,10 +16,12 @@ export type InteractionStore = {
putView(taskId: TaskId, view: {
blocks?: readonly ViewBlock[];
html?: string;
+ template?: string;
}): Promise;
getView(taskId: TaskId): Promise<{
blocks?: readonly ViewBlock[];
html?: string;
+ template?: string;
} | undefined>;
setPhase(taskId: TaskId, phase: InteractionPhase): Promise;
phaseOf(taskId: TaskId): Promise;
diff --git a/api-report/apps/api/src/modules/ledger/contract.d.ts b/api-report/apps/api/src/modules/ledger/contract.d.ts
index 6dc50b7..e4ec979 100644
--- a/api-report/apps/api/src/modules/ledger/contract.d.ts
+++ b/api-report/apps/api/src/modules/ledger/contract.d.ts
@@ -51,6 +51,12 @@ export type DraftWrite = {
export type ReleaseWriteResult = {
readonly mergedRows: number;
readonly emittedEvents: number;
+ /**
+ * **试跑单被挡下的主数据行数** —— 合到出参里去,是因为界面不撒谎:
+ * 放行痕上写「合并 N 行」而其中几行根本没进表,那句话就是假的。
+ * 非试跑单恒为 0。
+ */
+ readonly simulatedRows: number;
/**
* 合并进正式版的那些行(键 + 值)。
* 「落地收敛,输出口才发射」——发射的载荷就该是**这些已落地的事实**,
@@ -78,6 +84,22 @@ export type LedgerService = {
}>>;
systemWrite(write: SystemWrite): Promise;
draftWrite(write: DraftWrite): Promise;
+ /**
+ * **撤掉本单草稿层的一行** —— 写错了得有退路(2026-08-01 加)。
+ *
+ * 不是第四种写:它只作用在**本单草稿层**,正式版一个字都动不了,也不抛事件 ——
+ * 撤的是「我这一单还没过门的那一笔」。放行门的位置没变。
+ *
+ * 没有它的后果实测过:客户说本轮跳过的那颗料被写进了有效清单,条目数报 9 而表里躺着 10,
+ * 判据 `record_rows = line_total` 正确地拦住,而模型**改不动** ——
+ * 它唯一能改的是那个数(改了 skipped_total 又不对),13 次拒回、23 拍、234k token 才熔断。
+ * 门拦住一件办错的事却不给改正的手段,那扇门就只是个死胡同。
+ */
+ draftDrop(drop: {
+ taskId: TaskId;
+ address: ObjectAddress;
+ key: Record;
+ }): Promise;
releaseWrite(taskId: TaskId): Promise;
/** 正式版查询 —— 跨任务只见过门的事实 */
query(query: LedgerQuery): Promise;
diff --git a/api-report/apps/api/src/modules/ledger/port.d.ts b/api-report/apps/api/src/modules/ledger/port.d.ts
index 6bbe988..c6db05d 100644
--- a/api-report/apps/api/src/modules/ledger/port.d.ts
+++ b/api-report/apps/api/src/modules/ledger/port.d.ts
@@ -1,7 +1,7 @@
/**
* module: ledger —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
*/
-import type { EnvelopeDraft, LedgerKey, LedgerQuery, LedgerRow } from '@contracts';
+import type { EnvelopeDraft, LedgerKey, LedgerQuery, LedgerRow, LedgerObjectKind } from '@contracts';
import type { MaterializedObject, SchemaChange } from './internal/domain/schema-merge.ts';
import type { DraftEntry } from './internal/domain/task-view.ts';
export type Clock = {
@@ -14,6 +14,20 @@ export type IdGenerator = {
export type EventPublisher = {
publish(envelope: EnvelopeDraft): Promise;
};
+/**
+ * 这一单是不是**试跑** —— app 注入(设计台的两条试跑路径都算,见 `container.ts`)。
+ *
+ * 台账在 L1,认不出「谁起的单」;而放行写要不要落主数据,取决的恰恰是这件事。
+ * 所以由 app 把这一个布尔判断喂进来 —— 与 `EventPublisher` 同一个套路:
+ * 模块只声明「我需要知道什么」,不声明「去哪儿问」。
+ *
+ * 为什么只挡主数据、不挡单据:单据按单标识分行,一次试跑写一行自己的,
+ * 清运行数据就没了;主数据是**按业务实体**分行的档案,试跑往里建一位客户,
+ * 「新客户」那条样例就此再也试不出来 —— 它成了老客户。
+ */
+export type SimulationProbe = {
+ isSimulated(taskId: string): Promise;
+};
export type ObjectRef = {
readonly domain: string;
readonly object: string;
@@ -26,9 +40,18 @@ export type LedgerStore = {
/** 已物化对象的列与主键;未物化返回 undefined */
describe(ref: ObjectRef): Promise;
/** 出参带人读名(表注释);没登记就没有 label,呈现退回对象键 */
+ /** 记下对象性质(单据/主数据/派生)与单标识 —— 随声明走,不随建表走 */
+ setObjectMeta(ref: ObjectRef, kind: LedgerObjectKind, caseKey: string | undefined): Promise;
listObjects(): Promise>;
+ /**
+ * 哪些对象是**主数据** —— 放行写要据此把试跑单的主数据挡在正式表外。
+ *
+ * 单独一个方法而不是让调用方 `listObjects` 自己筛:性质记在 `__object_meta` 里,
+ * 一次 where 就够;`describe` 读的是 information_schema,那里没有性质这一维。
+ */
+ masterRefs(): Promise>;
/** 执行「只增不破坏」的变更计划 */
applyChanges(ref: ObjectRef, changes: readonly SchemaChange[]): Promise;
select(query: LedgerQuery): Promise<{
@@ -46,5 +69,13 @@ export type DraftStore = {
list(taskId: string): Promise>;
listForObject(taskId: string, ref: ObjectRef): Promise;
put(taskId: string, ref: ObjectRef, entry: DraftEntry): Promise;
+ /**
+ * 撤掉本单草稿层的**一行** —— 写错了得有退路(2026-08-01 加)。
+ *
+ * 只动 `taskId` 那一份,正式版碰不到。没有它的后果实测过:客户说本轮跳过的那颗料
+ * 被写进了有效清单,条目数报 9 而表里躺着 10 —— 判据正确地拦住,而模型改不动
+ * (唯一能改的是那个数,改了别的判据又不对),13 次拒回、23 拍、234k token 才熔断。
+ */
+ drop(taskId: string, ref: ObjectRef, key: LedgerKey): Promise;
clear(taskId: string): Promise;
};
diff --git a/api-report/apps/api/src/modules/llm/contract.d.ts b/api-report/apps/api/src/modules/llm/contract.d.ts
index b8d82c5..4c4f12a 100644
--- a/api-report/apps/api/src/modules/llm/contract.d.ts
+++ b/api-report/apps/api/src/modules/llm/contract.d.ts
@@ -31,6 +31,12 @@ export type ModelCall = {
readonly prompt: string;
readonly temperature?: number;
readonly maxOutputTokens?: number;
+ /**
+ * 压推理:`off` 不许想、`low` 少想;缺省由着模型。
+ * 只对画像里声明了 `reasoningControl` 的厂家生效 —— 没姿势的厂家如实忽略。
+ * 规划型调用(loop 单拍)该关:隐藏思考按秒计,而它的判断本就交给了判定表与工具。
+ */
+ readonly reasoning?: 'off' | 'low';
/** 出参 schema(JSON Schema);给定即要求结构化出参并校验 */
readonly outputSchema?: Record;
/** 录制/回放的定位:任务 × 步序号(§7.3) */
diff --git a/api-report/apps/api/src/modules/loop-engine/contract.d.ts b/api-report/apps/api/src/modules/loop-engine/contract.d.ts
index b0452f7..cb67d01 100644
--- a/api-report/apps/api/src/modules/loop-engine/contract.d.ts
+++ b/api-report/apps/api/src/modules/loop-engine/contract.d.ts
@@ -8,7 +8,8 @@ import type { LoopModel, TaskId } from '@contracts';
import type { CaseModel, PlanItem, PlanItemType } from './internal/domain/cmmn-parse.ts';
import type { CaseRuntime, CaseState, EnabledEntry, EnabledSet, ItemRuntime, ItemState, Navigation, TerminalAuthority } from './internal/domain/lifecycle.ts';
import type { DecisionResult, DecisionTrace, Gap } from './internal/domain/dmn.ts';
-export type { CaseModel, CaseRuntime, CaseState, DecisionResult, DecisionTrace, EnabledEntry, EnabledSet, Gap, ItemRuntime, ItemState, LoopModel, Navigation, PlanItem, PlanItemType, TerminalAuthority, };
+import type { PhaseProgress } from './internal/domain/phase.ts';
+export type { CaseModel, CaseRuntime, CaseState, DecisionResult, DecisionTrace, EnabledEntry, EnabledSet, Gap, ItemRuntime, ItemState, LoopModel, Navigation, PhaseProgress, PlanItem, PlanItemType, TerminalAuthority, };
export { CmmnParseError } from './internal/domain/cmmn-parse.ts';
export { IllegalTransitionError } from './internal/domain/lifecycle.ts';
export declare class ForbiddenTerminalError extends Error {
@@ -28,6 +29,48 @@ export type AgentAction =
readonly tool: string;
readonly input: Record;
}
+/**
+ * 写台账草稿 —— **活动过程写的唯一形态**(铁律②:登记那一刻世界上什么都没变)。
+ *
+ * 一拍可写多行:逐条把一份 BOM 解析成几十行,是**一件事**,不该拆成几十拍
+ * 各烧一次模型。地址必须在活动清单声明过的台账对象之内(越域写与越权调工具同罪)。
+ */
+ | {
+ readonly kind: 'write';
+ readonly itemId: string;
+ /**
+ * 台账地址「域.对象」—— **一个字符串,不是两个字段**。
+ *
+ * 系统上下都用这一种写法(可写台账字典、`requiredDeliverables[].ledger` 都是它),
+ * 拆成 domain + object 只会让模型把整个地址塞进其中一格 —— 实测第一拍就这么错。
+ */
+ readonly address: string;
+ readonly rows: ReadonlyArray<{
+ readonly key: Record;
+ readonly patch: Record;
+ }>;
+}
+/**
+ * **撤掉自己写错的那一行** —— 写错了得有退路(2026-08-01 加)。
+ *
+ * 只作用在**本单草稿层**:正式版一个字都动不了,也不抛事件 ——
+ * 撤的是「我这一单还没过门的那一笔」,放行门的位置没变。
+ *
+ * 起因是实测的一次死胡同:客户明说本轮跳过的那颗料被写进了有效清单,
+ * 条目数报 9(对)而表里躺着 10,判据 `record_rows = line_total` 正确地拦住 ——
+ * 而模型**改不动**:它唯一能改的是那个数,改了 `skipped_total` 又不对。
+ * 13 次拒回、23 拍、234k token 才熔断。**门拦住一件办错的事却不给改正的手段,
+ * 那扇门就只是个死胡同。**
+ */
+ | {
+ readonly kind: 'drop';
+ readonly itemId: string;
+ /** 台账地址「域.对象」—— 与 write 同一种写法 */
+ readonly address: string;
+ /** 要撤的那几行的主键 —— 认不出的行如实回「草稿层里没有这一行」 */
+ readonly keys: ReadonlyArray>;
+ readonly reason: string;
+}
/** 调人:四卡之一 —— 调人 = 立监听听人 */
| {
readonly kind: 'human';
@@ -35,6 +78,119 @@ export type AgentAction =
readonly card: Record;
readonly workOrder: 'execution' | 'acceptance';
}
+/**
+ * **发补全卡** —— 对话卡族之一,与执行工单是两种东西。
+ *
+ * 「按缺口清单追问补全」(A1-S2)要的就是它:缺口清单摆在对话区、带一张表单,
+ * 人就地填齐。而执行工单是「派一件活给你办」—— 派一张工单去问几个字段,
+ * 收卡的人得进待处理、办完再回来,重得不成比例。
+ *
+ * 从前 Agent **发不出对话卡**:行动型里只有 `human` 且只能选执行 / 验收两种工单,
+ * 于是 SOP 上画着「补全卡」的那一步只能拿执行工单顶 —— 形似而神非。
+ */
+ | {
+ readonly kind: 'ask';
+ readonly itemId: string;
+ /** 问什么 —— 一句话说清缺口 */
+ readonly ask: string;
+ /**
+ * 要对方填的格子。`hint` 是格子里的灰字提示;
+ * **`value` 是预填值** —— AI 从来件里读得出的先替人填上(公司名、联系人姓名),
+ * 人只需核对一眼、补上填不出的那个,而不是对着一排空格从头敲。
+ * 读不出来的就别给:填错了人还得先擦掉再写,比空着更费事。
+ */
+ readonly fields: ReadonlyArray<{
+ readonly key: string;
+ readonly label: string;
+ readonly hint?: string;
+ readonly value?: string;
+ }>;
+ /** 依据快照:哪几条缺哪几个字段 —— 调用必附上下文,裸调用拒发 */
+ readonly basis?: ReadonlyArray<{
+ readonly k: string;
+ readonly v: string;
+ }>;
+}
+/**
+ * 发**数据录入卡** —— 人类工具的第五类:**人按下确认那一刻就是写入**。
+ *
+ * 与补全卡(`ask`)的分工:那张收的是「答案」,你拿到之后还要自己 `write` 一遍;
+ * 这张收的是「这一行数据」—— 卡上写明落哪张表、哪个主键,人确认,基座当场落草稿层,
+ * **你不经手**。建档这类事该用它:主数据建错一次,后面每一单都跟着错,
+ * 那一笔该由人亲手落下,不该经你转述一遍。
+ *
+ * 你的活是**把能填的先填好**(`value`),填不出的留空 —— 界面会标「待填写」。
+ */
+ | {
+ readonly kind: 'entry';
+ readonly itemId: string;
+ readonly ask: string;
+ /** 落哪张表:`域.对象` */
+ readonly ledger: string;
+ /** 主键值 —— 确认时按它定位那一行 */
+ readonly key: Readonly>;
+ readonly fields: ReadonlyArray<{
+ readonly key: string;
+ readonly label: string;
+ readonly value?: string;
+ readonly required?: boolean;
+ }>;
+ /**
+ * **同一次确认还要落的另几张表**(2026-08-02)。
+ *
+ * 建一位新客户落的是**两条主数据**:公司档 + 联系人档。只把公司那一半摆上卡、
+ * 联系人由你自己写,等于让人确认了一半 —— 而联系人档的 `contact_code`
+ * 恰恰是你编出来的那一个。分成两张卡也能办,但那是把一件事拆成两次打断。
+ */
+ readonly also?: ReadonlyArray<{
+ /** 这一段是什么 —— 卡上那行小标题(如「联系人档案」) */
+ readonly title: string;
+ readonly ledger: string;
+ readonly key: Readonly>;
+ readonly fields: ReadonlyArray<{
+ readonly key: string;
+ readonly label: string;
+ readonly value?: string;
+ readonly required?: boolean;
+ }>;
+ }>;
+ readonly basis?: ReadonlyArray<{
+ readonly k: string;
+ readonly v: string;
+ }>;
+}
+/**
+ * 发**决策卡** —— 对话卡族的第二种:把一个岔路口摆给人,人点一个选项。
+ *
+ * 与补全卡的分工:补全卡问的是**事实**(缺哪几个字段,你填);
+ * 决策卡问的是**处置**(这一步该往哪走,你选)。A1 的
+ * 「字段没齐 —— 要我系统外去问客户,还是替你草拟一封邮件」正是后者:
+ * 两条路都合法,选哪条是业务判断,不该由模型替人决定。
+ *
+ * 从前决策卡**只有 UI 没有 Agent 侧**:卡面在对话区画得出来,而行动型里发不出它 ——
+ * 于是所有岔路口都被写成补全卡,人拿到的是一张要自己敲字的表单,而不是两个可点的选项。
+ */
+ | {
+ readonly kind: 'decide';
+ readonly itemId: string;
+ /** 问什么 —— 一句话说清这是个什么岔路 */
+ readonly ask: string;
+ /** 选项:一条路一项。`consequence` 写清选了它会发生什么 */
+ readonly options: ReadonlyArray<{
+ readonly value: string;
+ readonly label: string;
+ readonly detail?: string;
+ readonly consequence?: string;
+ readonly recommended?: boolean;
+ }>;
+ /** 依据快照:凭什么问这一问 —— 调用必附上下文,裸调用拒发 */
+ readonly basis?: ReadonlyArray<{
+ readonly k: string;
+ readonly v: string;
+ }>;
+ /** 允许人给出选项之外的处置(附理由) */
+ readonly allowCustom?: boolean;
+}
/** 立监听:AI 不能调用世界,对世界只有两种合法姿势 —— 触达它(动作)与等它(监听) */
| {
readonly kind: 'listen';
@@ -66,10 +222,38 @@ export type AgentAction =
readonly satisfied: boolean;
readonly rationale: string;
}
-/** 判定成果已齐,提交放行 */
+/**
+ * 这一项干完了 —— **完成由 Agent 自己宣告,不是某个动作的副作用**。
+ *
+ * 从前 `tool` / `write` 一做完就把计划项收了。可一个计划项常常要好几个动作才交得出成果
+ * (先逐条写记录、再结算总数;先调工具取判据、再把判据写进台账)——
+ * 一收就再也动不了它,于是那一项该产出的东西永远缺着,而环在原地打转。
+ * 实测:20 拍烧掉 120k token,卡在「当下可为 ∩ 目标所需 = 空」。
+ *
+ * 宣告完成不等于成果被认可:终局仍唯放行,人验收才算数。
+ */
+ | {
+ readonly kind: 'finish';
+ readonly itemId: string;
+ readonly rationale: string;
+}
+/**
+ * 判定成果已齐,提交放行。
+ *
+ * `fields` = 验收报告上**让人当场改**的那几个值(PRD:「AI 已填好、可编辑」)——
+ * `ai` 是活动拟的值,`ledger` 是改完落回哪个台账地址(`域.对象`)。
+ * 不给 `fields`,验收报告就只能看不能改;给了 `ai` 不给 `ledger`,人改了也只留在
+ * 工单留痕里,落地用的还是活动的原值 —— 两样都得有,修订才落得下去。
+ */
| {
readonly kind: 'submit';
readonly rationale: string;
+ readonly fields?: ReadonlyArray<{
+ readonly key: string;
+ readonly label: string;
+ readonly ai?: string;
+ readonly ledger?: string;
+ }>;
}
/**
* 本回合**已在工具循环里调过人**:卡已发、监听已立(工具总线执行的)。
@@ -81,6 +265,41 @@ export type AgentAction =
readonly cardId: string;
readonly rationale: string;
};
+/**
+ * **对人说的一句话** —— 交代进展,并可顺手换一张局面画面。
+ *
+ * 它**不是一型行动,是每一拍的附带** —— 这一点是改过的:一开始把它做成独立行动,
+ * 结果它占一整拍又不推进局面,预算下的 Agent 理所当然跳过它,对话区照旧空白。
+ * 让说话与干活抢拍,是把「必须说」设计成了「有空才说」。
+ *
+ * 现在:一拍 = 一个行动 + 一句交代。说话不花额外的拍,也就没有不说的理由。
+ * `view` 是可选的局面模版名 —— 局面变了,人既要听到一句为什么,也要看到新画面。
+ */
+export type Narration = {
+ readonly text: string;
+ /** 语气签:指导 / 判据 / 结论 —— 前端各有其样式 */
+ readonly tag?: 'guidance' | 'criterion' | 'conclusion';
+ readonly view?: string;
+};
+/**
+ * 一拍的产出:**一串行动**(按序执行)+ 对人交代一句。
+ *
+ * 为什么是一串不是一件:一拍只许一个行动时,每个 write、每个 finish、每个 start
+ * 都单烧一次完整模型调用(说明书 + 轨迹整段重读)。实测 A1 六步的活恰好花 21 拍 ——
+ * 其中 19 拍是这个约束下的**最短路径**,浪费不在模型跑偏,在架构按拍计费。
+ * 收成一串后,「写记录、写结算、finish、start 下一项」一口气给,一单 5-8 拍收得了口。
+ *
+ * 执行纪律(引擎强制,不靠模型自觉):
+ * · 按序执行,谁被拒就停在谁那儿 —— 已执行的前缀落笔留痕,理由喂回下一拍;
+ * · `tool` 之后截断 —— 出参没看见,后面的行动是闭着眼计划的;
+ * · `ask` / `decide` / `human` / `listen` / `submit` 让本拍挂起 —— 只能是最后一个;
+ * · 一串封顶 `TURN_ACTION_CAP` 件,超出的直接丢弃(留痕)。
+ */
+export declare const TURN_ACTION_CAP = 6;
+export type AgentTurn = {
+ readonly actions: readonly AgentAction[];
+ readonly say?: Narration;
+};
export type TurnOutcome = {
readonly runtime: CaseRuntime;
readonly enabled: EnabledSet;
@@ -96,6 +315,14 @@ export type LoopEngineService = {
/** 装载:XML 进语义引擎,说明书进 Agent 上下文;版本全部入信封 */
load(taskId: TaskId, model: LoopModel): Promise;
runtime(taskId: TaskId): Promise;
+ /**
+ * 进度刻度:这一单在第几阶段(见 `internal/domain/phase.ts`)。
+ *
+ * 由 loop 出而不是让调用方自己算:阶段声明与计划项状态**同在一行运行时里**,
+ * 调用方要自己算就得先把模型和运行时都捞出来 —— 那是把内部结构摊给外面。
+ * 活动没声明阶段就回 `undefined`:界面照此不画进度条,不编一个出来。
+ */
+ phaseOf(taskId: TaskId, terminal: boolean): Promise;
/** 发布 Enabled 集与红绿灯 —— 引擎推进的终点 */
publish(taskId: TaskId): Promise;
/** 跑一个回合:定位 → 择向 → 决断 → 行动 → 写数据 */
diff --git a/api-report/apps/api/src/modules/loop-engine/index.d.ts b/api-report/apps/api/src/modules/loop-engine/index.d.ts
index e5fa97c..98b217b 100644
--- a/api-report/apps/api/src/modules/loop-engine/index.d.ts
+++ b/api-report/apps/api/src/modules/loop-engine/index.d.ts
@@ -1,13 +1,13 @@
/**
* module: loop-engine —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
*/
-export type { AgentAction, CaseModel, CaseRuntime, CaseState, DecisionResult, DecisionTrace, EnabledEntry, EnabledSet, Gap, ItemRuntime, ItemState, LoopEngineService, LoopModel, Navigation, PlanItem, PlanItemType, TerminalAuthority, TurnOutcome, } from './contract.ts';
-export { AutomaticOnWorldTouchingError, CmmnParseError, ForbiddenTerminalError, IllegalTransitionError, } from './contract.ts';
+export type { AgentAction, AgentTurn, Narration, CaseModel, CaseRuntime, CaseState, DecisionResult, DecisionTrace, EnabledEntry, EnabledSet, Gap, ItemRuntime, ItemState, LoopEngineService, LoopModel, Navigation, PlanItem, PlanItemType, TerminalAuthority, TurnOutcome, } from './contract.ts';
+export { AutomaticOnWorldTouchingError, CmmnParseError, ForbiddenTerminalError, IllegalTransitionError, TURN_ACTION_CAP, } from './contract.ts';
export type { AgentPort, CardGateway, Clock, ListenerRegistrar, LoopStateStore, MemoryRecorder, Sfeel, TaskContext, TaskDataGateway, ToolGateway, } from './port.ts';
export type { TurnRunnerDeps } from './internal/usecase/turn-runner.ts';
export { createTurnRunner } from './internal/usecase/turn-runner.ts';
export { parseCmmn, parseXml, PLAN_ITEM_TYPES } from './internal/domain/cmmn-parse.ts';
-export { assertItemTransition, autoCompleteReady, canTransitionCase, canTransitionItem, CASE_STATES, evaluateCriterion, initialRuntime, ITEM_STATES, mayCompleteCase, mayTerminateCase, navigate, occur, publishEnabled, reenter, transitionItem, } from './internal/domain/lifecycle.ts';
+export { assertItemTransition, achieveMilestones, autoCompleteReady, canTransitionCase, canTransitionItem, criterionKey, readingsOf, CASE_STATES, evaluateCriterion, initialRuntime, ITEM_STATES, mayCompleteCase, mayTerminateCase, navigate, occur, publishEnabled, reenter, transitionItem, } from './internal/domain/lifecycle.ts';
export { deduceGaps, evaluateTable, evaluationOrder, isBindingConclusion } from './internal/domain/dmn.ts';
export { createLoopStateStore } from './internal/adapter/loop-state-store.ts';
export { loopStateTable } from './internal/adapter/loop-state-schema.ts';
diff --git a/api-report/apps/api/src/modules/loop-engine/port.d.ts b/api-report/apps/api/src/modules/loop-engine/port.d.ts
index 3ae272a..3683729 100644
--- a/api-report/apps/api/src/modules/loop-engine/port.d.ts
+++ b/api-report/apps/api/src/modules/loop-engine/port.d.ts
@@ -4,7 +4,7 @@
import type { LoopModel, StaffId, TaskId } from '@contracts';
import type { CaseModel } from './internal/domain/cmmn-parse.ts';
import type { CaseRuntime, EnabledSet } from './internal/domain/lifecycle.ts';
-import type { AgentAction } from './contract.ts';
+import type { AgentTurn } from './contract.ts';
export type Clock = {
now(): Date;
};
@@ -27,13 +27,53 @@ export type Sfeel = {
/** 任务数据 —— app 注入 `ledger`(L1):引擎对**任务视图**求值,不依赖事件 */
export type TaskDataGateway = {
view(taskId: TaskId): Promise>;
+ /**
+ * 本活动**声明过**的台账对象(域.对象 → 列名与中文标)。
+ *
+ * 两用:喂给 Agent(不知道能写哪儿就只会瞎猜列名),以及校验 `write` 行动 ——
+ * 没声明过的地址一律拒写。越域写与越权调工具同罪,都得在结构上拦住。
+ */
+ writable(taskId: TaskId): Promise;
+ }>>;
+ /**
+ * 写草稿,并回报**这一笔到底有没有改变什么**。
+ *
+ * 值和上次一模一样的写是空转。不回报,Agent 就看不出自己在原地打转 ——
+ * 实测:同一行 verdict 被反复重写十几拍,每拍烧一次模型,预算就这么淌完。
+ */
draftWrite(input: {
taskId: TaskId;
domain: string;
object: string;
key: Record;
patch: Record;
- }): Promise;
+ }): Promise<{
+ changed: boolean;
+ }>;
+ /**
+ * **撤掉本单草稿层的一行** —— 写错了得有退路。
+ *
+ * 回 `false` = 草稿层里根本没有这一行(多半是主键写法对不上,
+ * `DAG…` 与 `DAG…-R0` 是两行)。如实说比静默成功有用:静默成功会让模型
+ * 以为撤掉了,下一拍判据照旧不过,它又去撤一次。
+ */
+ draftDrop(input: {
+ taskId: TaskId;
+ domain: string;
+ object: string;
+ key: Record;
+ }): Promise<{
+ dropped: boolean;
+ }>;
};
/**
* Agent 回路 —— app 注入(Mastra Agent 在 adapter,§6.1)。
@@ -46,7 +86,32 @@ export type AgentPort = {
narrative: string;
enabled: EnabledSet;
scope: Record;
- }): Promise;
+ /**
+ * 这一单能调什么 —— **声明 × 目录**编译出来的工具清单(`compileToolBus`)。
+ *
+ * 不给它这份清单,它就只知道说明书里提过的工具名,连入参长什么样都得猜 ——
+ * 实测的后果是:该调工具的那一步改成了发一张卡问人(「请为以下三个PN查询历史价格」),
+ * 明明系统里就有这件工具。工具是声明式的,清单得摆在它眼前。
+ */
+ tools: Array<{
+ id: string;
+ description: string;
+ input: Record;
+ effect: 'read' | 'write';
+ }>;
+ /** 本活动可写的台账地址与列名 —— 不给它字典,它只会照想象编列名 */
+ writable: Array<{
+ domain: string;
+ object: string;
+ label: string;
+ keys: string[];
+ fields: Array<{
+ name: string;
+ label: string;
+ type?: string;
+ }>;
+ }>;
+ }): Promise;
};
/** 工具 / 人类工具 / 监听 —— 三型行动的落点 */
export type ToolGateway = {
@@ -61,6 +126,16 @@ export type ToolGateway = {
error?: {
message: string;
};
+ /**
+ * 已转人代办,等人回填(§6.2)——「人是工具的一种实现」。
+ *
+ * 系统里没有这件工具时,不是失败:基座已开了一张执行卡并立了监听。
+ * 这一拍该**挂起等人**,不该把计划项判 Failed —— 判 Failed 等于告诉 Agent
+ * 「这条路走不通,换一条」,可这条路明明正在走,只是走的人换成了人。
+ */
+ awaitingHuman?: {
+ readonly workOrderId: string;
+ };
}>;
isWriteEffect(tool: string): Promise;
registerIntent(input: {
@@ -69,6 +144,13 @@ export type ToolGateway = {
input: Record;
label: string;
}): Promise;
+ /** 这一单能调什么(声明 × 目录)—— 单拍路径与开环路径吃同一份清单 */
+ busOf(activityId: string): Promise;
+ effect: 'read' | 'write';
+ }>>;
};
/**
* 在途验收(提交放行的持久痕):卡发出即记,裁决到达再记 verdict ——
@@ -80,6 +162,39 @@ export type PendingAcceptance = {
readonly selectedPorts?: Record;
};
export type CardGateway = {
+ /** 数据录入卡 —— 人确认那一下由基座直接写草稿层,不回到 Agent 手里 */
+ entry(input: {
+ taskId: TaskId;
+ ownerId: StaffId;
+ humanCallCap: number;
+ ask: string;
+ ledger: string;
+ key: Readonly>;
+ fields: ReadonlyArray<{
+ key: string;
+ label: string;
+ value?: string;
+ required?: boolean;
+ }>;
+ /** 同一次确认还要落的另几张表 —— 建一位新客户 = 公司档 + 联系人档 */
+ also?: ReadonlyArray<{
+ title: string;
+ ledger: string;
+ key: Readonly>;
+ fields: ReadonlyArray<{
+ key: string;
+ label: string;
+ value?: string;
+ required?: boolean;
+ }>;
+ }>;
+ basis?: ReadonlyArray<{
+ k: string;
+ v: string;
+ }>;
+ }): Promise<{
+ id: string;
+ }>;
issue(input: {
taskId: TaskId;
ownerId: StaffId;
@@ -89,6 +204,56 @@ export type CardGateway = {
}): Promise<{
id: string;
}>;
+ /**
+ * 发**补全卡**(对话卡族)—— 缺口清单摆在对话区、带一张表单,人就地填齐。
+ *
+ * 与执行工单分开是因为它们是两件事:工单是「派一件活给你办」,
+ * 派一张工单去问几个字段,收卡的人得进待处理、办完再回来,重得不成比例。
+ * SOP 上画着「补全卡」的那一步(A1-S2),要的就是这一件。
+ */
+ ask(input: {
+ taskId: TaskId;
+ ownerId: StaffId;
+ humanCallCap: number;
+ ask: string;
+ fields: ReadonlyArray<{
+ key: string;
+ label: string;
+ hint?: string;
+ }>;
+ basis?: ReadonlyArray<{
+ k: string;
+ v: string;
+ }>;
+ }): Promise<{
+ id: string;
+ }>;
+ /**
+ * 发**决策卡**(对话卡族)—— 岔路口摆给人**点一个**,不是让人敲字。
+ *
+ * 与补全卡的分工:补全卡问事实(缺哪几个字段,你填);决策卡问处置(往哪走,你选)。
+ * 两条路都合法、选哪条是业务判断的时候,该发的是这一件。
+ */
+ decide(input: {
+ taskId: TaskId;
+ ownerId: StaffId;
+ humanCallCap: number;
+ ask: string;
+ options: ReadonlyArray<{
+ value: string;
+ label: string;
+ detail?: string;
+ consequence?: string;
+ recommended?: boolean;
+ }>;
+ basis?: ReadonlyArray<{
+ k: string;
+ v: string;
+ }>;
+ allowCustom?: boolean;
+ }): Promise<{
+ id: string;
+ }>;
};
export type ListenerRegistrar = {
register(input: {
@@ -107,12 +272,21 @@ export type ListenerRegistrar = {
* 零 LLM 即可成屏;模型只决定「下一步做什么」,不负责画面。
*/
export type ViewGateway = {
+ /**
+ * 推一帧画面。**回报推没推上** —— 从前是 `Promise` 且模版查不到就静默 return:
+ * 引擎每拍推一个硬编码的 `case-overview`,而 A1 声明的四张模版一个都不叫这个名字,
+ * 于是**每一帧都被悄悄丢掉**,工作区从头到尾空白,日志里也没有一个字。
+ * 静默失败比报错坏得多:它让「没画面」看起来像「活动还没产出」。
+ */
push(input: {
taskId: TaskId;
activityId: string;
template: string;
scope: Record;
- }): Promise;
+ }): Promise<{
+ pushed: boolean;
+ templates: string[];
+ }>;
};
/**
* 技能 —— app 注入 `skill-engine`(L4)。
@@ -134,7 +308,7 @@ export type SkillGateway = {
export type MemoryRecorder = {
append(input: {
taskId: TaskId;
- kind: 'engine-transition' | 'evaluation' | 'thought' | 'action' | 'result' | 'observation';
+ kind: 'engine-transition' | 'evaluation' | 'thought' | 'action' | 'result' | 'observation' | 'narration';
title: string;
detail?: Record;
}): Promise;
@@ -143,10 +317,19 @@ export type MemoryRecorder = {
layer: string;
statement: string;
}>>;
- /** 本任务自己的最近轨迹(窗口大小由活动声明的 memoryWindow 定)—— 喂回叙事面,Agent 每拍不失忆 */
- window(taskId: TaskId): Promise | undefined;
}>>;
};
export type TaskContext = {
@@ -155,4 +338,14 @@ export type TaskContext = {
ownerId: StaffId;
humanCallCap: number;
}>;
+ /**
+ * 这一单的**机检判据名**(归一化后的 key 与 label 都在里头)。
+ *
+ * 给 `judge` 行动当门用:L1/L2 是拿 S-FEEL 对台账求值的,Agent 裁不动它 ——
+ * 判据投影压根不读 Agent 对机检项的裁定。可从前引擎照收不误,还往轨迹里
+ * 写一句「语义性前提评估:XXX → 满足」:模型于是以为这条已经过了,
+ * 转头就 submit,被门以同一条判据拒回;再裁、再拒,连拒五拍熔断(实测 A2 死于此)。
+ * 轨迹里那句「满足」是**假话** —— 界面不撒谎,对 Agent 也一样。
+ */
+ machineCriteria(taskId: TaskId): Promise>;
};
diff --git a/api-report/apps/api/src/modules/release/contract.d.ts b/api-report/apps/api/src/modules/release/contract.d.ts
index 393e63f..774daf6 100644
--- a/api-report/apps/api/src/modules/release/contract.d.ts
+++ b/api-report/apps/api/src/modules/release/contract.d.ts
@@ -28,6 +28,11 @@ export type SubmitForReleaseInput = {
readonly criteria: readonly CriterionCheck[];
/** 报告可修订:AI 拟值已填的表单字段 */
readonly fields?: ReadonlyArray>;
+ /**
+ * 验收页的正文块 —— 与执行工单同一套件,可含**绑台账的可编辑表**。
+ * 验收是最重的一道门:人得能在成果本身上改,不只是改几个整单级字段。
+ */
+ readonly blocks?: ReadonlyArray>;
};
export type SubmitOutcome = {
readonly report: PrecheckReport;
diff --git a/api-report/apps/api/src/modules/release/port.d.ts b/api-report/apps/api/src/modules/release/port.d.ts
index 6c50205..98555ec 100644
--- a/api-report/apps/api/src/modules/release/port.d.ts
+++ b/api-report/apps/api/src/modules/release/port.d.ts
@@ -23,6 +23,8 @@ export type LedgerGateway = {
releaseWrite(taskId: TaskId): Promise<{
mergedRows: number;
emittedEvents: number;
+ /** 试跑单被挡在正式表外的主数据行数(非试跑恒为 0)—— 放行痕要如实说 */
+ simulatedRows: number;
/** 合并进正式版的行 —— 输出口发射的载荷取自这里 */
merged: ReadonlyArray<{
domain: string;
@@ -79,12 +81,27 @@ export type CardGateway = {
label: string;
level: 'L1' | 'L2' | 'L3' | 'L4' | 'L5';
pass: boolean;
+ settled?: boolean | undefined;
+ rationale?: string | undefined;
}>;
actions: ReadonlyArray<{
label: string;
effect: 'read' | 'write';
detail: string;
}>;
+ /**
+ * 可修订字段 —— 验收报告「AI 已填好、可编辑」改的就是它们(带 `ai` 拟值与 `ledger` 落回地址)。
+ * **端口不声明它,值就在装配层被丢掉**:上游一路传下来,而 TypeScript 对条件展开
+ * 不做多余属性检查,于是编译不报、运行时静默丢 —— 卡面 `fields` 恒空(踩过)。
+ */
+ fields?: ReadonlyArray>;
+ /**
+ * 验收页的正文块 —— 绑台账的可编辑成果表走这里。
+ * **端口不声明它,值就在装配层被丢掉**(与 `fields` 同一个坑,又踩了一次:
+ * 契约声明了、submit 传了、release 转了,到装配层那个显式对象里被抹平,
+ * 编译不报、运行时静默丢 —— 卡面 blocks 恒空)。
+ */
+ blocks?: ReadonlyArray>;
}): Promise<{
id: string;
}>;
diff --git a/api-report/apps/api/src/modules/task/contract.d.ts b/api-report/apps/api/src/modules/task/contract.d.ts
index 85f198f..29c3e64 100644
--- a/api-report/apps/api/src/modules/task/contract.d.ts
+++ b/api-report/apps/api/src/modules/task/contract.d.ts
@@ -32,6 +32,8 @@ export type Task = {
/** 起单来源:工作流节点;登记触发的直起单为 undefined */
readonly workflowId?: string;
readonly nodeId?: string;
+ /** `testbed` = 设计台试跑 —— 认得出才拔得掉(仿真不该在别人工作台上过夜) */
+ readonly origin?: string;
readonly ownerId: StaffId;
readonly title: string;
readonly runtime: TaskRuntimeState;
@@ -52,6 +54,7 @@ export type StartTaskInput = {
readonly title: string;
readonly workflowId?: string;
readonly nodeId?: string;
+ readonly origin?: string;
/** 输入:data(按 schema 校验,缺项不得启动)+ message(委托消息,可为空) */
readonly data: Record;
readonly message?: string;
diff --git a/api-report/apps/api/src/modules/task/port.d.ts b/api-report/apps/api/src/modules/task/port.d.ts
index 8fbcab4..23d50cc 100644
--- a/api-report/apps/api/src/modules/task/port.d.ts
+++ b/api-report/apps/api/src/modules/task/port.d.ts
@@ -26,10 +26,34 @@ export type ActivityLoader = {
envelope: Record;
/** 输入 data 的 JSON Schema —— 缺项不得启动 */
inputSchema: Record;
+ /**
+ * 这一单是被一封信叫起来的 —— 活动清单声明的**来件字段映射**。
+ * 声明了,起单那一刻就把来件归进沟通记录(工作台那一屏)。
+ */
+ inboundComm?: {
+ channel: string;
+ partyField: string;
+ subjectField?: string | undefined;
+ bodyField?: string | undefined;
+ attachmentsField?: string | undefined;
+ };
businessModel: unknown;
stallAfter: string;
}>;
};
+/** 沟通记录 —— 一单往来的原件存档(工作台「沟通记录」那一屏读它) */
+export type CommRecorder = {
+ record(input: {
+ taskId: TaskId;
+ direction: 'in' | 'out';
+ channel: string;
+ party: string;
+ subject: string;
+ body: string;
+ attachments: readonly unknown[];
+ note: string;
+ }): Promise;
+};
export type SchemaValidator = {
validate(schema: Record, value: unknown): {
ok: boolean;
@@ -76,8 +100,11 @@ export type LandingGateway = {
export type MemoryRecorder = {
append(input: {
taskId: TaskId;
- /** control = 治理干预(叫停 / 恢复 / 改派):人对过程的裁断,归人裁决侧 */
- kind: 'engine-transition' | 'terminal' | 'observation' | 'control';
+ /**
+ * control = 治理干预(叫停 / 恢复 / 改派):人对过程的裁断,归人裁决侧。
+ * trigger = 触发事件:这一单被什么叫起来的 —— 不是机器产出,也不是谁的动作。
+ */
+ kind: 'engine-transition' | 'terminal' | 'observation' | 'control' | 'trigger';
title: string;
detail?: Record;
}): Promise;
diff --git a/api-report/apps/api/src/modules/toolbox/index.d.ts b/api-report/apps/api/src/modules/toolbox/index.d.ts
index 49ceb17..7f24da1 100644
--- a/api-report/apps/api/src/modules/toolbox/index.d.ts
+++ b/api-report/apps/api/src/modules/toolbox/index.d.ts
@@ -3,15 +3,17 @@
*/
export type { ActionIntent, AuditFinding, CallDisposition, EffectLevel, EvolutionViolation, InvocationTrace, InvokerMode, Preauthorization, RegisterToolInput, RegistrationVerdict, ToolCall, ToolContract, ToolInvoker, ToolResult, ToolboxService, } from './contract.ts';
export { INVOKER_MODES, RegistrationRejectedError, ToolDisabledError, ToolNotAllowedError, ToolNotFoundError, ToolReplayMissError, } from './contract.ts';
-export type { Clock, IdGenerator, SchemaValidator, SecretBox, ToolRegistry, ToolReplayStore, ToolRuntime, } from './port.ts';
+export type { Clock, IdGenerator, SchemaValidator, SecretBox, ToolHandoff, ToolHandoffStore, ToolRegistry, ToolReplayStore, ToolRuntime, } from './port.ts';
export type { ToolInvokerDeps } from './internal/usecase/tool-invoker.ts';
export { createToolInvoker, toolReplayKey } from './internal/usecase/tool-invoker.ts';
export type { ToolboxServiceDeps } from './internal/usecase/toolbox-service.ts';
export { createToolboxService } from './internal/usecase/toolbox-service.ts';
export { checkEvolution, dispositionOf, EFFECT_LEVEL_LABEL, EFFECT_LEVELS } from './internal/domain/tool-contract.ts';
export type { BusTool, CatalogEntry, ToolBusInput, ToolBusReport } from './internal/domain/tool-bus.ts';
-export { compileToolBus, HUMAN_TOOL, LLM_TOOLS } from './internal/domain/tool-bus.ts';
+export { compileToolBus, HUMAN_TOOL, LEDGER_TOOL, LLM_TOOLS } from './internal/domain/tool-bus.ts';
export { auditPassed, staticAudit } from './internal/domain/registration-audit.ts';
+export type { HandoffField } from './internal/domain/handoff.ts';
+export { callKeyOf, coerceBySchema, handoffFields } from './internal/domain/handoff.ts';
export type { ToolHandler } from './internal/adapter/toolbox-store.ts';
-export { createSchemaValidator, createToolRegistry, createToolReplayStore, createToolRuntime, isAllowed, } from './internal/adapter/toolbox-store.ts';
+export { createSchemaValidator, createToolRegistry, createToolReplayStore, createToolHandoffStore, createToolRuntime, isAllowed, } from './internal/adapter/toolbox-store.ts';
export { replayToolTable, toolAllowlistTable, toolTable } from './internal/adapter/toolbox-schema.ts';
diff --git a/api-report/apps/api/src/modules/toolbox/port.d.ts b/api-report/apps/api/src/modules/toolbox/port.d.ts
index 37de32c..6222120 100644
--- a/api-report/apps/api/src/modules/toolbox/port.d.ts
+++ b/api-report/apps/api/src/modules/toolbox/port.d.ts
@@ -43,6 +43,26 @@ export type ToolReplayStore = {
* 工具执行 —— 实现对系统:server 侧执行代码,运行时受限,
* **只可访问注册申报的连接目标,无其他环境能力**(OS 7.3)。
*/
+/**
+ * 工具转人台(§6.2)—— 人是工具的一种实现:发出去的卡、答回来的出参都记在这。
+ * 内容地址 `callKey` 保证同一次调用只问人一次。
+ */
+export type ToolHandoffStore = {
+ find(taskId: string, callKey: string): Promise;
+ open(handoff: Omit): Promise;
+ /** 人答完:值即出参 */
+ answer(workOrderId: string, output: Record): Promise;
+};
+export type ToolHandoff = {
+ readonly id: string;
+ readonly taskId: string;
+ readonly tool: string;
+ readonly callKey: string;
+ readonly input: Record;
+ readonly workOrderId: string;
+ readonly state: 'pending' | 'answered';
+ readonly output?: Record | undefined;
+};
export type ToolRuntime = {
execute(input: {
readonly contract: ToolContract;
diff --git a/api-report/packages/activity-sdk/src/index.d.ts b/api-report/packages/activity-sdk/src/index.d.ts
index a39b727..c507c0d 100644
--- a/api-report/packages/activity-sdk/src/index.d.ts
+++ b/api-report/packages/activity-sdk/src/index.d.ts
@@ -10,6 +10,6 @@ export { defineActivity } from './define.ts';
export type { ActivityMemoryLayer, ChatCardDraft, FormFieldDraft, LedgerKey, LedgerRow, ViewBlockLike, WorkOrderDraft, } from './types.ts';
export type { HostOptions, HostRecording, MockHost } from './host.ts';
export { createMockHost } from './host.ts';
-export type { Tone } from './view.ts';
-export { kvCard, noteCard, stagesCard, tableCard, viewShell } from './view.ts';
+export type { ColStyle, FactTone, Stamp, Tone } from './view.ts';
+export { actionList, bigCount, factCards, letter, meter, noteOnly, ruleTable, section, stagesOnly, statsBand, tableOnly, verdict, viewShell, } from './view.ts';
export declare const SDK_VERSION = "0.1.0-m5";
diff --git a/api-report/packages/contracts/src/api/governance.d.ts b/api-report/packages/contracts/src/api/governance.d.ts
index 30c2b6c..f473b24 100644
--- a/api-report/packages/contracts/src/api/governance.d.ts
+++ b/api-report/packages/contracts/src/api/governance.d.ts
@@ -116,9 +116,9 @@ export declare const listGrantsRoute: {
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"governance">;
function: z.ZodEnum<{
+ trigger: "trigger";
data: "data";
task: "task";
- trigger: "trigger";
system: "system";
tool: "tool";
activity: "activity";
@@ -155,9 +155,9 @@ export declare const setGrantsRoute: {
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"governance">;
function: z.ZodEnum<{
+ trigger: "trigger";
data: "data";
task: "task";
- trigger: "trigger";
system: "system";
tool: "tool";
activity: "activity";
@@ -187,9 +187,9 @@ export declare const setGrantsRoute: {
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"governance">;
function: z.ZodEnum<{
+ trigger: "trigger";
data: "data";
task: "task";
- trigger: "trigger";
system: "system";
tool: "tool";
activity: "activity";
@@ -508,6 +508,7 @@ export declare const govTaskLogRoute: {
id: z.ZodString;
at: z.ZodString;
sideLabel: z.ZodString;
+ action: z.ZodString;
title: z.ZodString;
}, z.core.$strip>>;
}, z.core.$strip>;
@@ -1835,12 +1836,20 @@ export declare const installActivityRoute: {
data: z.ZodRecord;
messageAllowed: z.ZodDefault;
messageHint: z.ZodOptional;
+ inboundComm: z.ZodOptional;
+ partyField: z.ZodString;
+ subjectField: z.ZodOptional;
+ bodyField: z.ZodOptional;
+ attachmentsField: z.ZodOptional;
+ }, z.core.$strip>>;
}, z.core.$strip>;
output: z.ZodObject<{
mandatory: z.ZodDefault;
description: z.ZodOptional;
+ when: z.ZodOptional;
}, z.core.$strip>>>;
selective: z.ZodDefault;
description: z.ZodOptional;
+ when: z.ZodOptional;
}, z.core.$strip>>;
}, z.core.$strip>>>;
}, z.core.$strip>;
@@ -1855,6 +1865,9 @@ export declare const installActivityRoute: {
key: z.ZodString;
label: z.ZodString;
schema: z.ZodOptional>;
+ ledger: z.ZodOptional;
+ ledgers: z.ZodOptional>;
+ editableAtRelease: z.ZodOptional>;
}, z.core.$strip>>>;
acceptanceCriteria: z.ZodDefault;
modelRef: z.ZodOptional;
}, z.core.$strip>>>;
+ revisableAtRelease: z.ZodDefault>>;
prohibitions: z.ZodDefault;
+ kind: z.ZodDefault>;
+ caseKey: z.ZodOptional;
fields: z.ZodArray;
primaryKey: z.ZodDefault;
masked: z.ZodDefault;
+ required: z.ZodOptional;
+ tally: z.ZodOptional>;
+ expect: z.ZodOptional;
+ auto: z.ZodOptional;
}, z.core.$strip>>;
}, z.core.$strip>>>;
listeners: z.ZodDefault>;
}, z.core.$strip>>;
}, z.core.$strip>;
+ steps: z.ZodOptional>;
+ reads: z.ZodDefault>;
+ view: z.ZodOptional;
+ tools: z.ZodDefault>;
+ doneWhen: z.ZodOptional;
+ }, z.core.$strip>>>;
+ phases: z.ZodOptional;
+ }, z.core.$strip>>>;
}, z.core.$strip>;
skills: z.ZodDefault;
@@ -2139,7 +2181,13 @@ export declare const listActivitiesRoute: {
name: z.ZodString;
type: z.ZodString;
required: z.ZodBoolean;
+ label: z.ZodOptional;
+ format: z.ZodOptional;
}, z.core.$strip>>;
+ samples: z.ZodDefault;
+ }, z.core.$strip>>>;
outputPorts: z.ZodArray;
@@ -2184,6 +2232,12 @@ export declare const listLedgerObjectsRoute: {
domain: z.ZodString;
object: z.ZodString;
label: z.ZodOptional;
+ kind: z.ZodOptional>;
+ caseKey: z.ZodOptional;
}, z.core.$strip>>;
}, z.core.$strip>;
};
@@ -2355,6 +2409,30 @@ export declare const testRunRoute: {
}>;
}, z.core.$strip>;
};
+/**
+ * 仿真测试台 · 试跑**一个活动** —— 与试跑流程包并列的第二种试法。
+ *
+ * 为什么要单有一条:开发一个活动时,关心的是「这个活动自己跑不跑得动」——
+ * 案例模型推不推得开、判据取不取得齐、台账写不写得对、验收卡长什么样。
+ * 走流程包试,前面几个节点的噪音全得先趟一遍,而且每改一次活动就要重跑整条流。
+ *
+ * 起出来的是**真任务**(不挂工作流),照旧落到被指派人的工作台、照旧过 Harness 与放行门 ——
+ * 试跑不等于假跑:假跑试不出任何东西。区别只在它带 `origin='testbed'`,可以连根拔掉。
+ */
+export declare const testRunActivityRoute: {
+ readonly method: "POST";
+ readonly path: "/api/gov/testbed/run-activity";
+ readonly body: z.ZodObject<{
+ activityId: z.ZodString;
+ data: z.ZodDefault>;
+ message: z.ZodDefault;
+ ownerId: z.ZodDefault;
+ }, z.core.$strip>;
+ readonly output: z.ZodObject<{
+ taskId: z.ZodString;
+ title: z.ZodString;
+ }, z.core.$strip>;
+};
/**
* 停止仿真 —— **把这一单连根拔掉**:工作流、它的活动任务、工单、实例、trace 全部销毁。
* 仿真不是生产:试跑留下的东西不该在库里、在别人工作台上过夜。
@@ -2362,8 +2440,10 @@ export declare const testRunRoute: {
export declare const testStopRoute: {
readonly method: "POST";
readonly path: "/api/gov/testbed/stop";
+ /** 两种试法各有其拔法:流程包给 workflowId,单活动给 taskId;都不给 = 拔掉所有试跑残留 */
readonly body: z.ZodObject<{
- workflowId: z.ZodString;
+ workflowId: z.ZodDefault;
+ taskId: z.ZodDefault;
}, z.core.$strip>;
readonly output: z.ZodObject<{
destroyed: z.ZodBoolean;
@@ -2371,12 +2451,271 @@ export declare const testStopRoute: {
}, z.core.$strip>;
};
/** 当前在跑的仿真(测试台一次只跑一条)—— 刷新页面也认得回来 */
+/**
+ * 当前这一台仪器上的试跑单(测试台一次只跑一条)—— 刷新页面也认得回来。
+ *
+ * **返回还在跑的那一单,以及刚跑完的那一单** —— 两者要分得开:
+ * `live` 为真才锁旋钮(在跑的不许换台);为假表示它已收尾,观测面照旧摆着结果给人看,
+ * 但可以再起一单了。从前不分,已终结的单照样被当成「在跑」——
+ * 跑完之后旋钮永久锁住,人只能刷新整页才起得了下一单(实测)。
+ */
export declare const testCurrentRoute: {
readonly method: "GET";
readonly path: "/api/gov/testbed/current";
readonly output: z.ZodObject<{
workflowId: z.ZodOptional;
subject: z.ZodOptional;
+ taskId: z.ZodOptional;
+ activityId: z.ZodOptional;
+ live: z.ZodDefault;
+ terminal: z.ZodOptional;
+ }, z.core.$strip>;
+};
+/**
+ * 单活动试跑的**观测面** —— 一屏看穿这一单:
+ * 轨迹(每一拍干了什么)· 台账草稿(成果落到哪儿了)· 卡(问了谁什么)· 预算读数。
+ *
+ * 流程包那侧看的是 trace 树(节点 → 步骤);活动这侧要看的是**活动内部**,
+ * 拿 trace 树看不出「第三拍为什么被拒」。
+ */
+export declare const testActivityStateRoute: {
+ readonly method: "GET";
+ readonly path: "/api/gov/testbed/activity/:taskId";
+ readonly params: z.ZodObject<{
+ taskId: z.ZodString;
+ }, z.core.$strip>;
+ readonly output: z.ZodObject<{
+ taskId: z.ZodString;
+ activityId: z.ZodString;
+ title: z.ZodString;
+ runtime: z.ZodEnum<{
+ running: "running";
+ "awaiting-human": "awaiting-human";
+ "awaiting-event": "awaiting-event";
+ suspended: "suspended";
+ "landing-blocked": "landing-blocked";
+ terminal: "terminal";
+ }>;
+ terminal: z.ZodOptional>;
+ reason: z.ZodOptional;
+ used: z.ZodObject<{
+ steps: z.ZodNumber;
+ tokens: z.ZodNumber;
+ humanCalls: z.ZodNumber;
+ }, z.core.$strip>;
+ caps: z.ZodObject<{
+ steps: z.ZodNumber;
+ tokens: z.ZodNumber;
+ humanCalls: z.ZodNumber;
+ }, z.core.$strip>;
+ trail: z.ZodArray>;
+ }, z.core.$strip>>;
+ drafts: z.ZodArray;
+ patch: z.ZodRecord;
+ }, z.core.$strip>>;
+ cards: z.ZodArray;
+ title: z.ZodString;
+ done: z.ZodBoolean;
+ result: z.ZodOptional;
+ }, z.core.$strip>>;
+ }, z.core.$strip>;
+};
+/**
+ * 上传一个文件 —— 走 base64 而不是 multipart。
+ *
+ * 这一层所有路由都由同一套 zod 契约与 typed-fetch 收着,multipart 得在它旁边另开一条
+ * 不受契约管的路;而这里要传的是 BOM、规格书这类几十 KB 的件,base64 那点膨胀
+ * 换来「照旧一条契约管到底」是划算的。真大的件不该走界面上传,该走通道。
+ */
+export declare const uploadFileRoute: {
+ readonly method: "POST";
+ readonly path: "/api/gov/upload";
+ readonly body: z.ZodObject<{
+ name: z.ZodString;
+ contentType: z.ZodDefault;
+ dataBase64: z.ZodString;
+ }, z.core.$strip>;
+ readonly output: z.ZodObject<{
+ ref: z.ZodString;
+ name: z.ZodString;
+ contentType: z.ZodString;
+ size: z.ZodNumber;
+ text: z.ZodOptional;
+ note: z.ZodString;
+ }, z.core.$strip>;
+};
+export type UploadedFile = z.infer;
+/** 取回原件 —— 存的是真字节,人点得开才算真留住了 */
+export declare const downloadFileRoute: {
+ readonly method: "GET";
+ readonly path: "/api/gov/upload/:id";
+ readonly params: z.ZodObject<{
+ id: z.ZodString;
+ }, z.core.$strip>;
+ readonly output: z.ZodObject<{
+ name: z.ZodString;
+ contentType: z.ZodString;
+ dataBase64: z.ZodString;
+ }, z.core.$strip>;
+};
+/**
+ * 一次试跑的存档 —— **销毁那一刻拍的快照**。
+ *
+ * 「停止仿真 = 连根拔掉」这条不变(试跑起的是真任务,不该在别人工作台上过夜);
+ * 拔掉之前把这一单的观测面存下来。开发活动是反复试的活,「上一版跑了几拍、
+ * 卡在哪一步、台账写成什么样」正是要拿来比对的东西 —— 拔干净等于每改一次
+ * 就把上一次的账一并烧了。
+ */
+export declare const testLogRowSchema: z.ZodObject<{
+ id: z.ZodString;
+ kind: z.ZodEnum<{
+ activity: "activity";
+ package: "package";
+ }>;
+ subject: z.ZodString;
+ target: z.ZodString;
+ taskId: z.ZodOptional;
+ workflowId: z.ZodOptional;
+ terminal: z.ZodOptional;
+ reason: z.ZodOptional;
+ used: z.ZodObject<{
+ steps: z.ZodOptional;
+ tokens: z.ZodOptional;
+ humanCalls: z.ZodOptional;
+ }, z.core.$strip>;
+ caps: z.ZodObject<{
+ steps: z.ZodOptional;
+ tokens: z.ZodOptional;
+ humanCalls: z.ZodOptional;
+ }, z.core.$strip>;
+ trailCount: z.ZodNumber;
+ draftCount: z.ZodNumber;
+ cardCount: z.ZodNumber;
+ startedAt: z.ZodOptional;
+ archivedAt: z.ZodString;
+}, z.core.$strip>;
+export type TestLogRow = z.infer;
+export declare const listTestLogsRoute: {
+ readonly method: "GET";
+ readonly path: "/api/gov/testbed/logs";
+ readonly output: z.ZodObject<{
+ runs: z.ZodArray;
+ subject: z.ZodString;
+ target: z.ZodString;
+ taskId: z.ZodOptional;
+ workflowId: z.ZodOptional;
+ terminal: z.ZodOptional;
+ reason: z.ZodOptional;
+ used: z.ZodObject<{
+ steps: z.ZodOptional;
+ tokens: z.ZodOptional;
+ humanCalls: z.ZodOptional;
+ }, z.core.$strip>;
+ caps: z.ZodObject<{
+ steps: z.ZodOptional;
+ tokens: z.ZodOptional;
+ humanCalls: z.ZodOptional;
+ }, z.core.$strip>;
+ trailCount: z.ZodNumber;
+ draftCount: z.ZodNumber;
+ cardCount: z.ZodNumber;
+ startedAt: z.ZodOptional;
+ archivedAt: z.ZodString;
+ }, z.core.$strip>>;
+ }, z.core.$strip>;
+};
+/** 一次试跑的正文 —— 逐拍轨迹 / 台账草稿 / 发过的卡 */
+export declare const testLogDetailRoute: {
+ readonly method: "GET";
+ readonly path: "/api/gov/testbed/logs/:id";
+ readonly params: z.ZodObject<{
+ id: z.ZodString;
+ }, z.core.$strip>;
+ readonly output: z.ZodObject<{
+ id: z.ZodString;
+ kind: z.ZodEnum<{
+ activity: "activity";
+ package: "package";
+ }>;
+ subject: z.ZodString;
+ target: z.ZodString;
+ taskId: z.ZodOptional;
+ workflowId: z.ZodOptional;
+ terminal: z.ZodOptional;
+ reason: z.ZodOptional;
+ used: z.ZodObject<{
+ steps: z.ZodOptional;
+ tokens: z.ZodOptional;
+ humanCalls: z.ZodOptional;
+ }, z.core.$strip>;
+ caps: z.ZodObject<{
+ steps: z.ZodOptional;
+ tokens: z.ZodOptional;
+ humanCalls: z.ZodOptional;
+ }, z.core.$strip>;
+ trailCount: z.ZodNumber;
+ draftCount: z.ZodNumber;
+ cardCount: z.ZodNumber;
+ startedAt: z.ZodOptional;
+ archivedAt: z.ZodString;
+ trail: z.ZodArray>;
+ }, z.core.$strip>>;
+ drafts: z.ZodArray;
+ patch: z.ZodRecord;
+ }, z.core.$strip>>;
+ cards: z.ZodArray;
+ }, z.core.$strip>>;
+ }, z.core.$strip>;
+};
+/** 删一条 / 清空 —— 日志是给人比对用的,留多久由人说了算 */
+export declare const deleteTestLogRoute: {
+ readonly method: "DELETE";
+ readonly path: "/api/gov/testbed/logs/:id";
+ readonly params: z.ZodObject<{
+ id: z.ZodString;
+ }, z.core.$strip>;
+ readonly output: z.ZodObject<{
+ deleted: z.ZodNumber;
+ }, z.core.$strip>;
+};
+export declare const clearTestLogsRoute: {
+ readonly method: "DELETE";
+ readonly path: "/api/gov/testbed/logs";
+ readonly output: z.ZodObject<{
+ deleted: z.ZodNumber;
}, z.core.$strip>;
};
/** 全链 Trace:一条工作流的 span 树(节点 → 步骤 / 工具 / 模型 / 人卡) */
diff --git a/api-report/packages/contracts/src/api/workbench.d.ts b/api-report/packages/contracts/src/api/workbench.d.ts
index bc89b7b..6ae3d1f 100644
--- a/api-report/packages/contracts/src/api/workbench.d.ts
+++ b/api-report/packages/contracts/src/api/workbench.d.ts
@@ -34,9 +34,9 @@ export declare const sessionRoute: {
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"governance">;
function: z.ZodEnum<{
+ trigger: "trigger";
data: "data";
task: "task";
- trigger: "trigger";
system: "system";
tool: "tool";
activity: "activity";
@@ -94,9 +94,9 @@ export declare const loginRoute: {
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"governance">;
function: z.ZodEnum<{
+ trigger: "trigger";
data: "data";
task: "task";
- trigger: "trigger";
system: "system";
tool: "tool";
activity: "activity";
@@ -158,6 +158,11 @@ export declare const taskCardSchema: z.ZodObject<{
}>>;
position: z.ZodNumber;
total: z.ZodNumber;
+ phase: z.ZodOptional>;
workflowSubject: z.ZodString;
workflowState: z.ZodEnum<{
terminated: "terminated";
@@ -194,6 +199,11 @@ export declare const listTasksRoute: {
}>>;
position: z.ZodNumber;
total: z.ZodNumber;
+ phase: z.ZodOptional>;
workflowSubject: z.ZodString;
workflowState: z.ZodEnum<{
terminated: "terminated";
@@ -249,6 +259,11 @@ export declare const taskDetailSchema: z.ZodObject<{
}>>;
position: z.ZodNumber;
total: z.ZodNumber;
+ phase: z.ZodOptional>;
workflowSubject: z.ZodString;
workflowState: z.ZodEnum<{
terminated: "terminated";
@@ -331,6 +346,8 @@ export declare const taskDetailSchema: z.ZodObject<{
rows: z.ZodArray>;
editable: z.ZodOptional;
editableCols: z.ZodOptional>;
+ ledger: z.ZodOptional;
+ fields: z.ZodOptional>;
linkDonut: z.ZodOptional;
@@ -549,6 +566,27 @@ export declare const taskDetailSchema: z.ZodObject<{
src: z.ZodString;
}, z.core.$strip>], "kind">;
}, z.core.$strip>>;
+ comms: z.ZodDefault;
+ channel: z.ZodString;
+ party: z.ZodString;
+ subject: z.ZodString;
+ body: z.ZodString;
+ attachments: z.ZodArray;
+ ref: z.ZodOptional;
+ note: z.ZodOptional;
+ readable: z.ZodOptional;
+ }, z.core.$strip>>;
+ note: z.ZodString;
+ unread: z.ZodDefault;
+ }, z.core.$strip>>>;
workOrders: z.ZodArray;
preset: z.ZodOptional>;
editable: z.ZodOptional;
editableCols: z.ZodOptional>;
+ ledger: z.ZodOptional;
+ fields: z.ZodOptional>;
linkDonut: z.ZodOptional;
@@ -789,11 +829,17 @@ export declare const taskDetailSchema: z.ZodObject<{
group: z.ZodOptional;
hint: z.ZodOptional;
ai: z.ZodOptional;
+ ledger: z.ZodOptional;
unit: z.ZodOptional;
required: z.ZodOptional;
cols: z.ZodOptional, z.ZodLiteral<2>]>>;
}, z.core.$strip>>;
submitLabel: z.ZodOptional;
+ gate: z.ZodOptional>;
+ }, z.core.$strip>>;
id: z.core.$ZodBranded