Archived
活动侧(E04 2.1.0 → 2.1.9,I01 1.2.2): - 覆盖闭合成为机检:基座新增 distinct 读数,「计划盖住的客户数 = 候选行数」钉进 S3 收步 / S4 哨兵 / 放行 L2;人加的候选客户不再被整单无视 - 「关键人待识别」占位句式全链退役:查无联系人先发数据录入卡收人;契约新增 refersTo, 计划/回执的 contact_code 只许引用真实存在的联系人(安装期核引用目标) - 台账更新确认摆全落库面:客户档案/联系人档案两张落库预览(touched: 数据源, 正式现值 + 草稿叠加),可补改、可加行(自增主键由基座续号);姓名升必填 - 更新明细:update_key 位置键 + 「同一处只留一条」L2;依据写人话不带机器键; 标题旁不摆单号;判据与文案不夹字段名;行标识去掉单标识那一段 - 回执骨架 = 计划行数钉进 S3 收步与 S4 哨兵;重做分支不许删计划凑数 - 阶段刻度对齐文档 7 步;起单样例预填满 基座侧: - 两个后端连同一个库:task-claim 跨进程租约(迁移 0030)+ Postgres LISTEN/NOTIFY 广播桥;对话区按任务内记忆序号排,不按墙上时钟;并卡「刚说过话」按记忆序列判紧邻 - 多行 write 整批先验后写(dryRun),被拒 = 一行都没落 - 决策卡意见与本单草稿字面比对,归属以观察进轨迹(note-anchors) - 括号栈补「补闭括号」两形(少一个 } / 行对象没关直接跟下一行) - 件上加行:空着的自增主键直进续号,不从别的行借号;未落下的行点名回给前端 - 人答复落库那一刻就把 runtime 翻 running(task.wake),两块同帧变脸 - 试跑单可落真库(land_master,迁移 0031,默认开);数据域按活动分组; JSON 列不再是 [object Object];按钮键感 + 在办进度条 + 防抖 - 报价链 A1–A7 从系统卸载(只卸不删源码),种子只装 E04/I01 契约变更(api-report 已刷新):FieldDeclaration.distinct / refersTo, objectDeclaration.file,landMaster,unkeyed,assetFiles,ledgerByActivityRoute。 1700 测 / typecheck / lint / depcruise / api:check 绿。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEJX8Z7CHejvwmDQyPQuBX
640 lines
37 KiB
TypeScript
640 lines
37 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.1.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`,
|
|
},
|
|
{
|
|
/*
|
|
* **覆盖闭合**(2026-08-24 柴浩实测补):人核对后的候选清单是后续每一步的输入契约 ——
|
|
* 他加的那家客户,计划里必须有它。两边都是基座数:计划里有几家不同客户
|
|
* (`distinct`,声明在 plan.customer_code 上)= 候选清单几行(增删后现数)。
|
|
* S3 的收步判据与 S4 的入场哨兵同一句 —— 这里是第二道墙,防的是绕过收步的路。
|
|
*/
|
|
key: 'plan-covers-scope',
|
|
label: '每家确认纳入的候选客户都进了经营计划(含人核对时新增的,一家不落)',
|
|
level: 'L2',
|
|
predicate: `${D}_plan_distinct_customer_code = ${D}_candidate_rows`,
|
|
},
|
|
{
|
|
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`,
|
|
},
|
|
{
|
|
/* 2026-08-24:重做后过期的旧明细不许留 —— 同一处字段只能有一条更新 */
|
|
key: 'no-duplicate-update',
|
|
label: '同一处字段只留一条更新(重做后过期的旧记录已撤掉)',
|
|
level: 'L2',
|
|
predicate: `${D}_ledger_update_distinct_update_key = ${D}_ledger_update_rows`,
|
|
},
|
|
{
|
|
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: '放行前不得触达外部系统 —— 触达世界的动作集中在放行那一刻' },
|
|
/* 2026-08-24:模型给缺口节点自造过顺延引用码 + 占位人名 —— 计划里「有人」必须真的有人 */
|
|
{ key: 'no-phantom-contact', statement: '计划与回执只许引用 customer.contact 已有的联系人编码与真人姓名;不得自造编码或以占位称呼顶格 —— 查无联系人先以数据录入卡收人' },
|
|
],
|
|
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 必填(2026-08-24 柴浩实测:新客户档案落库连名字都是空的 —— 候选行里
|
|
* 明明写着,模型没抄)。`customer_profile_missing` 本就在 S6 收步与放行哨兵里,
|
|
* 标上它,「无名档案」从此进不了正式表;代价是碰过的客户行要把 name 照现值带上
|
|
* (剧本写死)。industry/region 标建议填:AI 从来件与背景说明推得出就填,
|
|
* 推不出留橘胶囊给人 —— 台账更新确认屏上现在改得了。
|
|
*/
|
|
{ name: 'name', type: 'text', primaryKey: false, masked: false, required: true, label: '客户名称' },
|
|
{ name: 'industry', type: 'text', primaryKey: false, masked: false, expect: true, label: '所属行业' },
|
|
{ name: 'region', type: 'text', primaryKey: false, masked: false, expect: true, 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 必填(2026-08-24 柴浩实测:早前一轮模型替「计划要经营的岗位」凭空建了
|
|
* 两条无名联系人档,人名职务全空、却标着关键人/亲密度3 —— 那不是联系人,是把
|
|
* 计划里的节点冒充成了人)。`customer_contact_missing` 本就在收步与放行哨兵里,
|
|
* 标上它,无名建档从此进不了正式表;代价是碰过的联系人行把 name 照现值带上。
|
|
*/
|
|
{ name: 'name', type: 'text', primaryKey: false, masked: false, required: true, 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: '覆盖轮次编号' },
|
|
/*
|
|
* `refersTo` —— 计划只许指向真实存在的联系人(2026-08-24,措辞治不了的结构治):
|
|
* 模型三次给缺口节点自造顺延引用码 + 占位人名,反例描述/说明书纪律/禁令
|
|
* 全拦不住;现在基座在写入那一刻查 customer.contact(正式 + 本单草稿),
|
|
* 查无此码当场拒并指路(先发数据录入卡收人)。receipt 同款。
|
|
*/
|
|
{ name: 'contact_code', type: 'text', primaryKey: true, masked: false, refersTo: 'customer.contact.contact_code', label: '联系人编码' },
|
|
/*
|
|
* `distinct: true` —— 基座数「计划里有几家不同客户」(`crm_coverage_plan_distinct_customer_code`)。
|
|
* 它是覆盖闭合判据的落点(2026-08-24 柴浩实测:人在覆盖范围确认卡上加了一家客户,
|
|
* 行落进了台账,而计划/回执/收口把它从头无视到尾 —— 收步与放行只数「计划有没有行」,
|
|
* 「盖没盖住每一家候选」没有任何一个基座数说得出来)。计划行数 ≠ 客户数
|
|
* (一家客户几位关键人就几行),所以必须另数这一个。
|
|
*/
|
|
{ name: 'customer_code', type: 'text', primaryKey: false, masked: false, required: true, distinct: 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, refersTo: 'customer.contact.contact_code', 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: '依据(用人话引用回执事实)' },
|
|
/*
|
|
* 更新位置键(2026-08-24 用户:「重做后老的变更记录应该删除」):主键是序号,
|
|
* 同一处字段(同一张表·同一客户·同一联系人·同一字段)重做时若新开一行,旧行就留在
|
|
* 明细里骗人 —— SQ-0031 的覆盖度一度同时挂着 50→50 与 50→100 两条。
|
|
* 这一列由模型按「台账地址|客户编码|联系人编码|字段」拼,`distinct` 让基座数
|
|
* 「有几处不同位置」;判据钉 distinct = rows —— 同一处只留一条。
|
|
*/
|
|
{ name: 'update_key', type: 'text', primaryKey: false, masked: false, required: true, distinct: 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' },
|
|
};
|