This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/activities/crm-relationship-coverage/src/manifest.ts
T
xcsandClaude Opus 5 0aa2fc11b0 资料文件真产出来了;E04/I01 补到验收线,顺手填了几个「墙拦住却不给路」的坑
**资料文件全链**(用户:审核资料这一步要真产出 word/excel/md/html,前端能渲、点资料名能开)
接手时这一步一个文件都产不出来 —— S3 剧本白纸黑字写着「file_ref 本 MVP 阶段可留空」。
文件是台账那一行的投影,不另存一份:
· 声明 `file:{titleColumn,bodyColumn,formatColumn}`,它同时是授权面(没声明就点不开)
· 现造真字节 —— docx/xlsx 是自己打的 OOXML 包(zip-write:store+CRC32,零依赖)
· 读回来渲 —— 解的就是造出来那份字节,渲得出来才证明文件是真的
· 件上 `data-file-of` → 工作区二级界面(Word 纸面/Excel 网格/渲好的 md/沙箱 html)+ 下载原件
放行落地前把每一份冻进 `sys.task_asset_file`(迁移 0029):归档件点开的必须是签字那一刻
那一份,而资料落在主数据上、试跑单不落正式表 —— 不存这一份,收工后一个都点不开。
工作成果里「产出的资料」自成一摞。Markdown 富文本渲染顺带销账(仍零依赖)。

**Harness**
· 画面按「这一拍办完之后」在办的那一步挑 —— 从前拿拍首的局面决定拍尾的事:
  收掉 S4、发出 S5 的卡,而屏上还摆着 S4 的只读件,人无处可填(墙要配路第四次现形)
· 验收「重做」真能退回重做,且打回后台账没动就不许再提交(软硬两段,不锁死单子)
· 立监听的授权面是清单声明;发卡的授权面是调人预算,不是计划项
· 读工具同参重调有兜底;空转写身后还有行动就不废串;finish 的 doneWhen 按写完后的草稿算

**界面**
· 翻件与翻页各归各位(从前二选一,多卡多页时页翻不动);没得翻写「—」不写 0/0
· 工作成果详情能翻份;表宽超出能横滑(E04/I01 抄版式时把 overflow-x 抄丢了)
· 查看面灰底滚过一屏变白 —— 两条独立的路都治了(沙箱内不同色 / 弹性子项不长个)
· 测试台「取不到」不再当成「还活着」

**活动**
E04 关系覆盖、I01 品牌内容各自补到验收线:判据全读基座数、判据指向的东西摆上卡面、
样例满载且名字 ≤14 字;提交前要裁的判据搬进说明书(模型会压拍,压过之后只有说明书还在)。

验收:e2e i01 4/4、e04 3/3 全部 released,资料在办中渲得出、放行后存了档点得开;
门禁 typecheck/lint/depcruise/1628 测全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEJX8Z7CHejvwmDQyPQuBX
2026-08-24 10:26:45 +08:00

585 lines
33 KiB
TypeScript

/**
* 活动包清单 —— E04 关系覆盖与关键人经营(案例活动)。
*
* 业务出处:`docs/SS01-客户与市场/e04-html/E04-活动流程与工单说明.md`(7 步业务链条,
* AI 承担信息汇总/记录/初稿生成,人负责判断与决策,收成 2 张执行工单 + 2 张验收/决策工单)、
* `docs/SS01-客户与市场/Demo-E04-I01-Activity台账关联与设计.html`(L-02 客户主台账 /
* L-08 客户联系人台账的读写关系)。
*
* 这是本仓库第一个不属于 `quote-*` A1-A7 报价管线的活动:触发方式是月度例行 / 事件驱动
* 而非来件邮件,核心读写对象是共享主数据 `customer.profile`/`customer.contact`。
*
* 两处与 A 系列的刻意不同,写在这里免得下一个读者当成疏漏:
* ① `customer.profile`/`customer.contact` 已由 A1(quote-intake)声明 `kind:'master'`
* 并持有 `customer_code`/`name` 等基本列 —— 这里只**追加**本活动要用的新列
* (`schema-merge.ts` 的声明合并是纯 add-column,不冲突,不需要重复声明已有列)。
* ② 「验收工单1」(经营计划审批)是**非终局**的通过/驳回+AI重做循环 ——
* `issueAcceptance` 硬绑定终局放行、一个任务只能发一次,不能拿来做中间验收点,
* 所以这里用 `decide` 决策卡实现(S4_plan_approval),真正的终局验收放行是
* 「验收工单2」(T_release,台账更新确认)。
*/
import type { ActivityManifest, DmnModel } from '@contracts';
import { activityIdSchema } from '@contracts';
import { COVERAGE_SKILLS } from './skills.ts';
import { CMMN_XML, NARRATIVE, COVERAGE_PHASES, COVERAGE_STEPS } from './case-model.ts';
import { COVERAGE_SCOPE, COVERAGE_PLAN, COVERAGE_RECEIPT, COVERAGE_LEDGER_UPDATE } from './views/report.ts';
const D = 'crm_coverage';
const ACTIVITY_ID = activityIdSchema.parse('crm-relationship-coverage');
/**
* 没有判定表 —— 覆盖度与排序是**连续加权算术**(BT 层级权重 × 覆盖率、
* 影响力权重 × 亲密度缺口),不是离散分支,不适合 DMN 命中表,交给两个纯工具算
* (见 `apps/api/src/app/tools/first-party.ts` 的
* `crm-relationship-coverage.recompute-score` / `.rank-key-persons`)。
* `dmn` 在清单里是必填字段,空表即可 —— 静态审不对 DMN 表数量做任何要求。
*/
const EMPTY_DMN: DmnModel = { drd: [], tables: [] };
export const manifest: ActivityManifest = {
identity: {
activityId: ACTIVITY_ID,
name: 'E04 · 关系覆盖与关键人经营',
packageVersion: '2.0.9',
category: '客户关系经营',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-crm-relationship-coverage',
},
/* ── 契约七件 ────────────────────────────────────────────────── */
contract: {
activityId: ACTIVITY_ID,
version: '1.0.0',
input: {
/*
* 不是来件解析(没有唯一「这封信」),是触发参数。§六 E04 业务说明与
* `Demo-E04-I01-Activity台账关联与设计.html`(L-02/L-08 完整字段表)两份文档核对
* 后按用户明确要求做成**两级下拉 + 一个数组**:
* ① `trigger` 必填 —— 月度例行 / 事件驱动,用户明确要求这一级必须是显式的
* 下拉框(不接受"从数组是否点名反推"这种省略,那样选项与数组状态一旦不一致
* 就是一处会撒谎的地方);
* ② `eventType` 选填,只在 `trigger:'event'` 时在测试台表单里现身(靠
* `showWhen` 这个测试台通用能力),值就是业务说明里那四种事件;
* ③ `customers` 数组,元素两种形状二选一:字符串 = 已有客户的 `customer_code`
* (业务说明里这就是 L-02 的 `customer_id`,同一个概念,本仓库沿用既有列名
* 不改叫法);对象 = **仅 `eventType:'new_customer'` 时使用**,这一刻这家
* 客户还没有编码(新客户建档要等人在录入卡上现场给),数组元素就相当于
* "新建一条 L-02 客户主台账记录所包含的字段"。录入卡会把这些字段落进
* `customer.profile`/`customer.contact` —— 那几列(name/industry/region/
* title/email)本版起**都在下方 declarations.ledger 里显式声明**
* (授权面是台账声明;从前只在说明书里出现、声明里没有,是一处会撒谎的注释)。
* `items` 不声明单一形状 —— 字符串与对象二选一,声明成 object 会让测试台的
* 类型提示(object[])与首选用法(字符串数组)相悖。
*/
data: {
type: 'object',
required: ['trigger'],
properties: {
trigger: {
type: 'string',
enum: ['monthly', 'event'],
enumNames: ['月度例行(常规覆盖轮次)', '事件驱动'],
title: '触发方式',
},
eventType: {
type: 'string',
enum: ['new_customer', 'key_person_change', 'major_deal', 't04_project'],
enumNames: ['新客户建立合作', '关键人离职/调岗', '重大商机启动', '攻坚项目(T04)立项'],
title: '事件类型(仅事件驱动时选)',
showWhen: { field: 'trigger', equals: 'event' },
},
customers: {
type: 'array',
title:
'本轮涉及的客户 —— 已有客户填客户编码字符串;若事件类型是"新客户建立合作",' +
'改填新客户档案对象(公司名等 L-02 字段,不含客户编码——那一格由人在录入卡上现场给)。留空 = 月度例行,AI按台账规则从整个客户库筛选候选',
examples: [
['CU-0142'],
[{ name: '青岛新星电子有限公司', custType: '模组厂', industry: '车规电子', region: '山东', contact: { name: '王芳', title: '采购经理', email: 'wang.fang@example.com' } }],
],
},
note: {
type: 'string',
title: '本轮说明(选填,背景信息 —— 帮AI判断覆盖深度与经营侧重)',
examples: ['客户 CU-0142 启动新项目,商机金额较大,AM Lead 要求本轮优先覆盖其决策链'],
},
},
},
messageAllowed: true,
messageHint: 'AM Lead 可在此说明本轮覆盖的特殊要求、优先客户、或需要重点关注的关键人变动',
},
output: {
/*
* 单一必发口,无分支:E04 全程线性(①→②③④→⑤⑥→⑦),没有像 A1 那样
* 「有没有 C 类」这种要按序求值的岔路。E05/E06 在本仓库尚未实现,暂无消费者 ——
* 按「契约先立、实现后补」的纪律先声明,不因为没有下游就不发。
*/
mandatory: [
{
name: 'coverage-map',
description:
'本轮更新后的关系覆盖地图 —— 供未来 E05互动与拜访 / E06账户计划读取;当前代码库尚未实现下游活动,此口暂无消费者,仅按"载荷=已落地事实"的契约纪律预先声明',
payload: {
type: 'object',
required: ['roundRef', 'coveredCustomerTotal', 'keyPersonCoveredTotal'],
properties: {
roundRef: { type: 'string', title: '覆盖轮次编号' },
coveredCustomerTotal: { type: 'integer', title: '本轮覆盖客户数' },
keyPersonCoveredTotal: { type: 'integer', title: '本轮覆盖关键人数' },
},
},
},
],
selective: [],
},
/*
* 四份必交付物,逐一对应四张工单产出的成果。全部 `editableAtRelease: []`——
* 业务说明明文"验收工单只读展示,不可修改"(与 A1 放行时允许改 type_tag 是刻意对比,
* 不是遗漏:E04 的验收工单从设计上就没有"就地修订"这一环)。
*/
requiredDeliverables: [
{
key: 'coverage-scope',
label: '覆盖范围客户清单',
ledger: `${D}.candidate`,
ledgers: [`${D}.candidate`, `${D}.verdict`],
editableAtRelease: [],
},
{ key: 'coverage-plan', label: '关键人经营计划', ledger: `${D}.plan`, editableAtRelease: [] },
{ key: 'coverage-receipt', label: '执行回执', ledger: `${D}.receipt`, editableAtRelease: [] },
{
key: 'ledger-update',
label: '台账更新明细',
ledger: `${D}.ledger_update`,
ledgers: [`${D}.ledger_update`, `${D}.verdict`],
editableAtRelease: [],
},
],
/*
* 验收判据 —— **机检(L1/L2)只读基座数得出来的数**:`_rows`/`_missing` 基座现数,
* 与执行卡的提交闸同一个数同一套算法。L4 三条是纯裁量(有没有依据、数值合不合理、
* 有没有张冠李戴)——机器数不出「这条更新站不站得住」,那是人对结果负责的地方。
*/
acceptanceCriteria: [
/*
* 两条 L1 的「人过了没有」一半仍靠记账旗(scope_confirmed_total / plan_approved_total,
* 只有人回卡之后模型才记 —— 与 A1 的 confirmed_total 同款,规范 §5.3-4 背书的人环凭据);
* 但**每条都必须再钉一半基座数**:旗是模型写的,单独成谓词就是「机检只读基座数」
* 第 5 类违反的伪装形 —— 抢跑写旗那单(2026-08-22 活单熔断)正是踩着这半边过的门。
*/
{
key: 'scope-confirmed',
label: '覆盖范围已经 AM Lead 核对确认(执行工单1已交回,清单有行且必填齐)',
level: 'L1',
predicate: `scope_confirmed_total > 0 and ${D}_candidate_rows > 0 and ${D}_candidate_missing = 0`,
},
{
key: 'plan-approved',
label: '关系经营计划已经 AM Lead 审批通过(验收工单1已过,不处在驳回待改状态)',
level: 'L1',
predicate: `plan_approved_total > 0 and ${D}_plan_rows > 0 and ${D}_plan_missing = 0`,
},
{
key: 'plan-complete',
label: '经营计划逐条完整:关键人、建议动作、优先级、理由、时间点都有',
level: 'L2',
predicate: `${D}_plan_rows > 0 and ${D}_plan_missing = 0`,
},
{
key: 'receipt-matches-plan',
label: '每条计划都收到执行回执,且完成状态、结果摘要、关键人变动均已填写',
level: 'L2',
predicate: `${D}_receipt_rows > 0 and ${D}_receipt_rows = ${D}_plan_rows and ${D}_receipt_missing = 0`,
},
{
key: 'ledger-update-complete',
label: '台账更新明细逐条完整:每条都写明台账、字段、更新前、更新后、依据',
level: 'L2',
predicate: `${D}_ledger_update_rows > 0 and ${D}_ledger_update_missing = 0`,
},
{
key: 'no-missed-update',
label: "每条「已完成」的执行回执至少有一条对应的台账更新(没有漏更)",
level: 'L2',
predicate: `${D}_ledger_update_rows >= ${D}_receipt_completion_status_done`,
},
{
key: 'update-has-evidence',
label: '每条台账更新都能在执行回执里找到对应事实依据,不是凭空推算',
level: 'L4',
},
{ key: 'values-reasonable', label: '更新后的数值(覆盖度、亲密度等)变化合理,没有异常跳变', level: 'L4' },
{ key: 'no-wrong-update', label: '更新内容准确对应回执事实,没有张冠李戴或误更别的客户/联系人', level: 'L4' },
/* L5 是责任关:那张卡本身就是这道关,卡面上不摆它(基座统一滤) */
{ key: 'owner-release', label: '台账更新入库放行:签下去即正式生效并回写主数据', level: 'L5' },
],
revisableAtRelease: [],
prohibitions: [
{ key: 'no-guess-contact', statement: '决策链节点对应哪位联系人不确定时不得臆断 —— 匹配不上就留空,那就是缺口' },
{ key: 'no-skip-evidence', statement: '台账更新明细每条都要有回执依据,不得凭空推算写入主数据' },
{ key: 'no-external-write', statement: '放行前不得触达外部系统 —— 触达世界的动作集中在放行那一刻' },
],
authorization: {
discretion: [],
preauthorizedActions: [],
/*
* 基线四次(S2/S4通过/S5/T_release)+ 决策卡循环预留两轮(每轮驳回+重提交各算一次
* 人回复,两轮就是 +4)——**基座按 `used >= cap` 判耗尽**(实测栽过:6 次刚好等于
* 6 的上限,「裁决:pass」那次回复本身就是第 6 次,harnessCheck 在 land() 之前
* 就判耗尽,放行卡显示 9/9 达标、验收也点了通过,任务却终态 exhausted、草稿从没
* 合并进正式版)。留够余量,cap 必须严格大于「最后一次拍板」时的实际次数。
*/
humanCallBudget: 12,
},
resources: {
tools: [{ name: 'crm-relationship-coverage.recompute-score' }, { name: 'crm-relationship-coverage.rank-key-persons' }],
skills: COVERAGE_SKILLS.map((s) => s.id),
models: ['deepseek-v4-flash'],
roles: ['AM Lead'],
/* 月度关系维护不是 live-inbox 那种时间敏感的单;人环预算同上调宽到 12(见上注) */
budget: { steps: 80, tokens: 500_000, activeMinutes: 90, humanCalls: 12 },
},
},
/* ── 声明 ────────────────────────────────────────────────────── */
declarations: {
models: ['deepseek-v4-flash'],
tools: [{ name: 'crm-relationship-coverage.recompute-score' }, { name: 'crm-relationship-coverage.rank-key-persons' }],
ledger: [
/*
* ── 主数据追加列(kind:'master' 显式声明,纯 add-column,与 A1 现有声明并存)──
* 只声明本活动要用的新列;`customer_code` 只是回显/关联用途的重复声明,
* 平台已由 `customer.profile`/`customer.contact` 的全量结构持有主键。
*/
{
domain: 'customer',
object: 'profile',
kind: 'master',
label: '客户档案',
fields: [
{ name: 'customer_code', type: 'text', primaryKey: true, masked: false, label: '客户编码' },
/*
* 建档卡会写的那几列(name/industry/region)也要声明 —— **授权面是台账声明**:
* 说明书让录入卡落什么,声明里就得有什么(物理列平台基线本就有,声明是权责面)。
* 与 A1 的 name 声明并存(add-column 合并,同名同型不冲突)。
*/
{ name: 'name', type: 'text', primaryKey: false, masked: false, label: '客户名称' },
{ name: 'industry', type: 'text', primaryKey: false, masked: false, label: '所属行业' },
{ name: 'region', type: 'text', primaryKey: false, masked: false, label: '区域' },
/*
* L-02 文档明列的字段,权威维护者是尚未建成的 T02(客户建档评估) —— 平时
* E04 不写它;唯一的例外是"新客户建立合作"这一刻,E04 代替不存在的 T02
* 履行建档职责(与 A1 quote-intake 的 S1_customer 完全同构),此时由数据
* 录入卡收人现场给的值。不带 tally——存文档给的中文枚举字面值,不再造一套
* ASCII 编码来回译(这一列从不参与 data-class-of 徽标染色,没有 ASCII 约束)。
*/
{ name: 'cust_type', type: 'text', primaryKey: false, masked: false, expect: true, label: '客户类型' },
{ name: 'coverage_score', type: 'integer', primaryKey: false, masked: false, required: true, label: '关系覆盖度' },
/* 结构:{nodes:[{node, btLevel, required, contactCode, covered}]} —— 见 case-model.ts 的 guide 注 */
{ name: 'decision_chain', type: 'json', primaryKey: false, masked: false, required: true, label: '决策链地图' },
/* 结构:{entries:[{date, type, contactCode, note}]},append-only(每次整个数组重写) */
{ name: 'interaction_timeline', type: 'json', primaryKey: false, masked: false, expect: true, label: '互动时间线' },
],
},
{
domain: 'customer',
object: 'contact',
kind: 'master',
label: '客户联系人',
fields: [
{ name: 'customer_code', type: 'text', primaryKey: true, masked: false, label: '客户编码' },
/*
* `auto:true` **必须跟 A1 一致**(2026-08-20 修死):`autoKeys()`
* (`apps/api/src/app/routes/desk.ts`)按**当前任务自己所属活动**的声明判断
* 哪几列自增,不是按全仓合并后的物理表结构 —— E04 若只抄字段名不抄这个标记,
* E04 名下的任务发数据录入卡建新联系人时,`contact_code` 就悄悄不再自动生成
* (静默退化成留空,不报错)。
*/
{ name: 'contact_code', type: 'text', primaryKey: true, masked: false, auto: true, label: '联系人编码' },
/* 建档卡的联系人段会写的三列 —— 同上,声明即授权面;email 照平台口径标脱敏 */
{ name: 'name', type: 'text', primaryKey: false, masked: false, label: '姓名' },
{ name: 'title', type: 'text', primaryKey: false, masked: false, label: '职务' },
{ name: 'email', type: 'text', primaryKey: false, masked: true, label: '邮箱' },
{ name: 'role', type: 'text', primaryKey: false, masked: false, expect: true, label: '角色' },
{ name: 'decision_node', type: 'text', primaryKey: false, masked: false, expect: true, label: '决策链节点' },
{
name: 'influence',
type: 'text',
primaryKey: false,
masked: false,
required: true,
tally: ['high', 'medium', 'low'],
label: '影响力评级',
},
{ name: 'relationship', type: 'integer', primaryKey: false, masked: false, required: true, label: '关系亲密度(1-5)' },
{ name: 'comm_pref', type: 'text', primaryKey: false, masked: false, expect: true, label: '沟通偏好' },
{ name: 'key_person', type: 'boolean', primaryKey: false, masked: false, required: true, label: '关键人标记' },
/* 我方追加的批注(入职/离职/调岗留痕),不标 expect —— 标了会让 _gaps 永远大于零 */
{ name: 'change_log', type: 'text', primaryKey: false, masked: false, label: '变动记录' },
],
},
/* ── E04 自有工作台账(kind:'record',caseKey:'round_ref') ── */
{
domain: D,
object: 'candidate',
kind: 'record',
caseKey: 'round_ref',
label: '候选覆盖客户清单',
fields: [
{ name: 'round_ref', type: 'text', primaryKey: true, masked: false, label: '覆盖轮次编号' },
{ name: 'customer_code', type: 'text', primaryKey: true, masked: false, required: true, label: '客户编码' },
{ name: 'customer_name', type: 'text', primaryKey: false, masked: false, required: true, label: '客户名称' },
/* 本轮工作快照 —— 有 customer.profile.cust_type 就抄它,没有才按台账信号猜(见 guide) */
{ name: 'cust_type', type: 'text', primaryKey: false, masked: false, expect: true, label: '客户类型' },
{ name: 'bt_level', type: 'text', primaryKey: false, masked: false, expect: true, label: 'BT 层级' },
{ name: 'status', type: 'text', primaryKey: false, masked: false, tally: ['pending', 'covered', 'excluded'], label: '状态' },
{ name: 'basis', type: 'text', primaryKey: false, masked: false, expect: true, label: '纳入依据' },
{ name: 'depth', type: 'text', primaryKey: false, masked: false, expect: true, label: '覆盖深度' },
{ name: 'origin', type: 'text', primaryKey: false, masked: false, label: '来源(ai/human-added)' },
],
},
{
domain: D,
object: 'plan',
kind: 'record',
caseKey: 'round_ref',
label: '关键人经营计划',
fields: [
{ name: 'round_ref', type: 'text', primaryKey: true, masked: false, label: '覆盖轮次编号' },
{ name: 'contact_code', type: 'text', primaryKey: true, masked: false, label: '联系人编码' },
{ name: 'customer_code', type: 'text', primaryKey: false, masked: false, required: true, label: '客户编码' },
{ name: 'customer_name', type: 'text', primaryKey: false, masked: false, expect: true, label: '客户名称' },
{ name: 'contact_name', type: 'text', primaryKey: false, masked: false, expect: true, label: '联系人姓名' },
{ name: 'action', type: 'text', primaryKey: false, masked: false, required: true, label: '建议动作' },
{
name: 'priority',
type: 'text',
primaryKey: false,
masked: false,
required: true,
tally: ['high', 'medium', 'low'],
label: '优先级',
},
{ name: 'rationale', type: 'text', primaryKey: false, masked: false, required: true, label: '理由' },
{ name: 'influence', type: 'text', primaryKey: false, masked: false, required: true, label: '影响力(写计划那一刻的快照)' },
{ name: 'relationship_gap', type: 'integer', primaryKey: false, masked: false, required: true, label: '亲密度缺口' },
{ name: 'rank_score', type: 'number', primaryKey: false, masked: false, required: true, label: '排序分(模版不排序,须已按此列降序写入)' },
{ name: 'target_date', type: 'date', primaryKey: false, masked: false, required: true, label: '建议动作时间点' },
/*
* 没有 status 列(2.0.0 撤):tally 里的 approved 从没人写过 —— 「计划过没过审」
* 由 plan_approved_total 一个数说了算,同一件事实两处各存一份就会分叉。
*/
],
},
{
/*
* 与 `plan` 分表:回执列若挤进 `plan`,`required` 是对象级全局声明 ——
* S3/S4 阶段的 `_missing` 会把还没执行的回执列也算成「缺」,doneWhen/L2 判据
* 永远打不开。S3 那一步顺手把 action/target_date 回填进来,供 S5 工单只读展示
* (单张表内混排只读+可编辑列,复用工作区渲染器 mine:域.对象 单地址绑定的既有能力)。
*/
domain: D,
object: 'receipt',
kind: 'record',
caseKey: 'round_ref',
label: '经营计划执行回执',
fields: [
{ name: 'round_ref', type: 'text', primaryKey: true, masked: false, label: '覆盖轮次编号' },
{ name: 'contact_code', type: 'text', primaryKey: true, masked: false, label: '联系人编码(与计划一一对应)' },
{ name: 'customer_code', type: 'text', primaryKey: false, masked: false, expect: true, label: '客户编码' },
/* 目标件的「对象」列长「AvtoKom · 产品 VP」—— 客户名与人名都要在,S3 从计划抄 */
{ name: 'customer_name', type: 'text', primaryKey: false, masked: false, label: '客户名称(承继自计划,只读展示)' },
{ name: 'contact_name', type: 'text', primaryKey: false, masked: false, expect: true, label: '联系人姓名' },
{ name: 'action', type: 'text', primaryKey: false, masked: false, label: '计划动作(承继自计划,只读展示)' },
{ name: 'target_date', type: 'date', primaryKey: false, masked: false, label: '建议时间点(承继自计划,只读展示)' },
{
name: 'completion_status',
type: 'text',
primaryKey: false,
masked: false,
required: true,
tally: ['done', 'partial', 'skipped'],
label: '完成状态',
},
{ name: 'result_summary', type: 'text', primaryKey: false, masked: false, required: true, label: '结果摘要' },
{ name: 'customer_feedback', type: 'text', primaryKey: false, masked: false, expect: true, label: '客户反馈' },
{
name: 'key_person_change',
type: 'text',
primaryKey: false,
masked: false,
required: true,
tally: ['changed', 'unchanged'],
label: '关键人变动',
},
],
},
{
domain: D,
object: 'ledger_update',
kind: 'record',
caseKey: 'round_ref',
label: '台账更新明细',
fields: [
{ name: 'round_ref', type: 'text', primaryKey: true, masked: false, label: '覆盖轮次编号' },
{ name: 'update_seq', type: 'integer', primaryKey: true, masked: false, label: '本轮更新序号' },
{ name: 'ledger_label', type: 'text', primaryKey: false, masked: false, required: true, label: '台账(人读名)' },
{ name: 'ledger_address', type: 'text', primaryKey: false, masked: false, required: true, label: '台账地址(机器地址)' },
{ name: 'customer_code', type: 'text', primaryKey: false, masked: false, required: true, label: '客户编码' },
{ name: 'contact_code', type: 'text', primaryKey: false, masked: false, expect: true, label: '联系人编码(空=客户级字段)' },
{ name: 'field_label', type: 'text', primaryKey: false, masked: false, required: true, label: '字段(人读列名)' },
{ name: 'before_value', type: 'text', primaryKey: false, masked: false, required: true, label: '更新前(无值写"(无)")' },
{ name: 'after_value', type: 'text', primaryKey: false, masked: false, required: true, label: '更新后' },
{ name: 'evidence_ref', type: 'text', primaryKey: false, masked: false, required: true, label: '依据(引用具体回执事实)' },
],
},
{
domain: D,
object: 'verdict',
kind: 'record',
caseKey: 'round_ref',
label: '本轮覆盖结论',
fields: [
{ name: 'round_ref', type: 'text', primaryKey: true, masked: false, label: '覆盖轮次编号' },
/*
* 列名 `trigger_label` 不叫 `trigger`(2.0.0 改):任务视图把草稿列摊平到顶层、
* 与起单入参**同名同位** —— 从前这一列就叫 trigger,S1 写下中文标签之前,
* 视图上 `data-slot="trigger"` 摆的是入参英文码(monthly/event),
* 「报告上不摆系统内英文名」在首帧上破了功。改名后绑定读 trigger_label,
* 写下之前那一格真空着(data-blank),空着是诚实的。
*/
{ name: 'trigger_label', type: 'text', primaryKey: false, masked: false, label: '触发方式(人读标签,非入参英文码)' },
{ name: 'event_type', type: 'text', primaryKey: false, masked: false, label: '事件类型(入参原样抄的人读标签,月度例行留空)' },
{ name: 'note', type: 'text', primaryKey: false, masked: false, label: '本轮说明' },
{ name: 'scope_confirmed_total', type: 'integer', primaryKey: false, masked: false, label: '覆盖范围已确认' },
{ name: 'plan_approved_total', type: 'integer', primaryKey: false, masked: false, label: '经营计划已审批通过' },
/*
* 没有 receipt_confirmed_total(2.0.2 退役,两轮活单熔断实测):模型自写的
* 「人环走完」旗让它抢跑 —— S5 的门改读基座数(receipt _rows/_missing,
* 回执四列全是人在件上填出来的),被叫的那一方无账可记。
*/
{ name: 'ledger_update_confirmed_total', type: 'integer', primaryKey: false, masked: false, label: '台账更新已收口' },
{ name: 'covered_customer_total', type: 'integer', primaryKey: false, masked: false, label: '本轮覆盖客户数' },
{ name: 'key_person_covered_total', type: 'integer', primaryKey: false, masked: false, label: '本轮覆盖关键人数' },
{ name: 'summary', type: 'text', primaryKey: false, masked: false, label: '本轮覆盖结论' },
],
},
],
listeners: [],
harness: {
memoryWindow: 24,
/* 月度关系维护不是 live-inbox 那种紧迫的单,给够时间让人执行拜访这类线下动作 */
stallAfter: 'PT4H',
nudge: [{ after: 'PT2H', action: 'nudge' }],
retry: { times: 2, backoff: 'PT30S' },
},
},
/* ── 资产初始版 ──────────────────────────────────────────────── */
assets: {
businessModel: {
cmmnXml: CMMN_XML,
narrative: NARRATIVE,
dmn: EMPTY_DMN,
steps: COVERAGE_STEPS,
phases: COVERAGE_PHASES,
},
skills: COVERAGE_SKILLS,
/*
* 四个局面各一张:
* · `coverage-scope` —— S1/S2 摆,候选客户清单,可编辑;
* · `coverage-plan` —— S3/S4 摆,经营计划,只读(业务说明明文"人只做审批,不能修改结论");
* · `coverage-receipt` —— S5 摆,执行回执,混排只读(计划动作/时间点)+ 可编辑(回执四列);
* · `coverage-ledger-update` —— S6/T_release 摆,台账更新明细,只读,收口后即归档件。
*/
viewTemplates: [
{ name: 'coverage-scope', html: COVERAGE_SCOPE },
{ name: 'coverage-plan', html: COVERAGE_PLAN },
{ name: 'coverage-receipt', html: COVERAGE_RECEIPT },
{ name: 'coverage-ledger-update', html: COVERAGE_LEDGER_UPDATE },
],
cardSchemas: {},
distillHooks: [
{
on: 'terminal',
prompt: '归纳本轮关系覆盖:候选客户识别依据、计划审批是否被驳回过(为什么)、执行回执里客户反馈最集中的诉求、覆盖度变化幅度。',
},
],
},
/*
* ── 样例集 —— 五条形状真不同(常备案例即验收案例)─────────────
*
* **第一条必须是满载的那条**(2026-08-23 用户:「询价受理起单时左边都有预填好的入参,
* E04 也要」):测试台落到一个活动上就自动铺 `samples[0]`(activity-run.tsx 的
* useEffect),所以第一条就是人打开这一屏看见的那份入参。从前第一条是月度例行、
* 只有 `{trigger:'monthly'}` 一个键 —— 表单上除了那个下拉全是空的,看着像没预填。
* 现在第一条是**点名客户的事件驱动**:四个字段全有值,起单钮按下去就能跑。
*
* **样例名一律 ≤14 字**:测试台的样例钮超过 14 字就退成「样例 N」(activity-run.tsx),
* 那等于没名字 —— 人得逐个点开才知道哪条是哪条。
*/
samples: [
{
name: '重大商机 · 点名客户',
input: {
trigger: 'event',
eventType: 'major_deal',
customers: ['CU-0142'],
note: '客户 CU-0142(Itelma)启动新项目 MD14.2,商机金额较大,AM Lead 要求本轮优先覆盖其决策链',
},
expect: { ports: ['coverage-map'] },
},
{
/* 月度例行的本分就是不点名(客户格留空 = 由 AI 按台账规则筛)—— 但背景说明要给,不然表单半空 */
name: '月度例行 · 全库筛',
input: {
trigger: 'monthly',
note: '八月例行覆盖轮次:优先看覆盖度陈旧、决策链有空白节点的重点客户',
},
expect: { ports: ['coverage-map'] },
},
{
name: '关键人离职调岗',
input: {
trigger: 'event',
eventType: 'key_person_change',
customers: ['CU-0087'],
note: '客户 CU-0087 采购负责人 Chen Gang 已离职,需要重新识别关键人并更新决策链',
},
expect: { ports: ['coverage-map'] },
},
{
name: '新客户建立合作',
input: {
trigger: 'event',
eventType: 'new_customer',
customers: [
{
name: '青岛新星电子有限公司',
custType: '模组厂',
industry: '车规电子',
region: '山东',
profileTags: '价格敏感 · 首次合作',
contact: { name: '王芳', title: '采购经理', email: 'wang.fang@example.com' },
},
],
note: 'AM Lead 上周展会新结识的客户,想尽快建档并启动关系覆盖',
},
expect: { ports: ['coverage-map'] },
},
{
name: '攻坚项目 T04 立项',
input: {
trigger: 'event',
eventType: 't04_project',
customers: ['CU-0142'],
note: '客户 CU-0142(Itelma)的 MD14.2 项目已升级为 T04 攻坚项目,需要加强决策链覆盖、确保关键节点无缺口',
},
expect: { ports: ['coverage-map'] },
},
],
entry: { file: 'index.js', checksum: 'dev-entry-crm-relationship-coverage' },
};