/**
* E04 关系覆盖与关键人经营的案例模型 —— **双表示同版本**:CMMN XML(机器面)与
* 案例说明书(叙事面)。
*
* 业务出处:`docs/SS01-客户与市场/e04-html/E04-活动流程与工单说明.md`(7 步业务链条,
* 收成 2 张执行工单 + 2 张验收工单)与 `docs/SS01-客户与市场/Demo-E04-I01-Activity台账
* 关联与设计.html`(L-02/L-08 台账读写关系)。
*
* 7 个业务步收成 8 个案例模型项,非逐一对应:
* ① 启动与覆盖范围确认 → S1_identify_candidates(AI 起草)+ S2_scope_confirm(人核对);
* ②③④ 读现状→缺口分析→制定计划 → S3_plan(一项,AI)+ S4_plan_approval(决策卡循环);
* ⑤⑥ 执行经营动作→更新关系台账 → S5_execute_receipt(人执行填回执)+
* S6_writeback(AI 写回主数据)+ S6_ledger_update(AI 留更新明细与结论 ——
* 与写回拆成两项是 2.0.4 的结构治:一串挤四张表,串尾最费字数的明细表
* 被截成空数组挨拒,实测两轮都栽在同一处;拆开后每项的剧本只摊自己那几张表);
* ⑦ 复盘与完结 → T_release(终局验收放行)。
*
* 这是本仓库第一个用 `decide` 实现「非终局通过/驳回+AI重做」循环的活动
* (S4_plan_approval)——`issueAcceptance` 硬绑定终局放行,一个任务只能发一次,
* 不能拿来做②③④这个中间验收点;`decide` 才是唯一能循环的机制,细则见 S4 的 guide。
*/
export const CMMN_XML = `
update
crm_coverage_candidate_rows > 0
update
scope_confirmed_total > 0
update
crm_coverage_plan_rows > 0 and crm_coverage_plan_missing = 0
update
plan_approved_total > 0
update
crm_coverage_receipt_rows > 0 and crm_coverage_receipt_missing = 0
update
customer_profile_rows > 0 and customer_profile_missing = 0 and customer_contact_rows > 0 and customer_contact_missing = 0
update
ledger_update_confirmed_total > 0 and crm_coverage_ledger_update_rows > 0 and crm_coverage_ledger_update_missing = 0 and customer_profile_missing = 0 and customer_contact_missing = 0
`;
/**
* 案例说明书 —— **只写整单一以贯之的事**(逐步做法归剧本,两处不重复写同一件事)。
*/
export const NARRATIVE = `§0 开案:**这一轮要经营哪些客户的关系,经营完把结果写回台账**。
入参四个顶层字段:
· \`trigger\`——**必有**,\`"monthly"\`(月度例行)或 \`"event"\`(事件驱动),翻成人读标签
写进 \`crm_coverage.verdict.trigger_label\`(报告上不摆系统内英文码),不用你自己判该归哪一类。
· \`eventType\`——仅 \`trigger:"event"\` 时可能有,四种之一:\`"new_customer"\`(新客户建立
合作)/ \`"key_person_change"\`(关键人离职/调岗)/ \`"major_deal"\`(重大商机启动)/
\`"t04_project"\`(攻坚项目 T04 立项)。同样翻成人读标签写进
\`crm_coverage.verdict.event_type\`(对照表见下方 S1 的 guide),\`trigger:"monthly"\`
时这一列留空。
· \`customers\`——本轮涉及的客户,**元素形状分两种**:①字符串,已有客户的 \`customer_code\`
(就是 L-02 客户主台账的 \`customer_id\`,同一个东西);②对象,**仅
\`eventType:"new_customer"\` 时出现**——这一刻这家客户还没有编码,对象里装的是
"新建一条 L-02 客户档案要用的字段"(公司名等),编码要等人在数据录入卡上现场给
(细节见 S1 的 guide,这是本轮唯一会用到的第二种卡)。**留空 = 月度例行,你自己
按台账规则从整个客户库识别本轮候选**。
· \`note\`——本轮背景说明(选填),帮你判断覆盖深度与经营侧重。
**成品是三样东西,缺一不可**:①一份**核对过的候选覆盖清单**;②一份**经审批通过的
关系经营计划**(逐个关键人的建议动作与优先级);③**写回 L-02/L-08 主数据的台账更新**
(覆盖度重算、决策链更新、联系人档案更新),每条更新都能在执行回执里找到依据。
**AI 承担信息汇总、记录与初稿生成,人负责判断与决策**:识别候选、缺口分析、排序计划、
收口写台账,都是你的活;核对候选清单、审批计划、到场执行、签字放行,四道都要人过。
**能算的不问人**:关键人是否覆盖(决策链节点比对)、覆盖度、排序,都调工具算,
你不临场拍;调 \`crm-relationship-coverage.recompute-score\` 和
\`crm-relationship-coverage.rank-key-persons\`,一批一次,别一条条调。
**六步走**:识别候选 → 人核对范围 → 制定计划 → 人审批(可能来回改一轮)→
人执行填回执 → 收口写台账 → 人签字放行。
【本单可能用到的另一种卡 —— 新客户建档,长这样,只在 S1_identify_candidates 这一项里
发生,\`customers\` 里出现对象形状的元素才发】新客户这一刻没有编码,**不能自己编一个**
(客户编码是主数据的主键,编错一次后面每一单都跟着错——这条纪律与 A1 quote-intake
建新客户档案那一步完全一样,照抄它已经验证过的机制):
{"kind":"entry","itemId":"S1_identify_candidates","ask":"这是位新客户,请核对建档信息并给客户编码",
"ledger":"customer.profile","key":{"customer_code":""},
"fields":[
{"key":"customer_code","label":"客户编码","required":true},
{"key":"name","label":"公司名","value":"青岛新星电子有限公司","required":true},
{"key":"cust_type","label":"客户类型","value":"模组厂"},
{"key":"industry","label":"行业","value":"车规电子"},
{"key":"region","label":"区域","value":"山东"}
],
"also":[{"title":"联系人档案","ledger":"customer.contact","key":{"customer_code":""},
"autoNote":"联系人编码由系统按顺序生成,不用你填",
"fields":[
{"key":"name","label":"联系人姓名","value":"王芳","required":true},
{"key":"title","label":"职位","value":"采购经理"},
{"key":"email","label":"邮箱","value":"wang.fang@example.com"}
]}]}
**只有 \`customers\` 里那个对象已经给出的字段才带 \`value\`**,没给的字段整格别写(界面自己
留白待填,别拿假值凑数);\`customer_code\` 从不给 \`value\`,标 \`required:true\`,人不填就
确认不了;对象里没给 \`contact\` 就不带 \`also\` 段(不是每次都已经知道具体联系人)。
**联系人编码不用你写**——自增列,基座在确认那一刻生成,写进 \`fields\` 也会被摘掉。
**人按下确认那一刻基座已经把这两张表直接落进草稿层**——你不经手,同一拍或下一拍都不许
再 \`write\` 一遍 \`customer.profile\`/\`customer.contact\`(写了会被"写了等于没写"拒回)。
一次只发一张,这张的回复到了、这家客户的编码定下来了,才轮到 \`customers\` 里下一个
对象形状的元素(如果还有的话)。
【这一单唯一的岔路口 —— 经营计划审批,长这样,无论哪一拍都照这个发,别自己另编一张】
计划审批不是一次性的:人可能驳回、要求你按他的意见改。**这一步只在 S4_plan_approval
这一项里发生**,发决策卡之前先在轨迹里找有没有已经答过、还没处理的 \`[card-reply]\`:
· **\`value\` 是 \`"approve"\`**——写 \`plan_approved_total=1\`,finish 这一项,别再发卡;
· **\`value\` 是 \`"reject"\` 或以 \`"custom:"\` 开头**——都算驳回:\`"reject"\` 是点了固定
按钮、没写具体意见,**不许再发卡追问理由**(那两个按钮点出来的回复永远没有 note,
再问一遍是把人指向一扇打不开的门);以 \`"custom:"\` 开头的,\`note\` 就是他写的具体
意见,按这条意见改。两种都要对 \`crm_coverage.plan\` 做出至少一处实质性修订(同 PK
覆盖写,不开新计划项、不重新进入 sentry),然后**换一种问法**再发一张 decide 卡
(措辞不换会被系统当成同一张卡静默吞掉);
· **没有**——才发第一张 decide 卡。
{"kind":"decide","itemId":"S4_plan_approval","allowCustom":true,
"ask":"经营计划已算出,共 条,覆盖 家客户的关键人,请审批。",
"options":[{"value":"approve","label":"通过","recommended":true},
{"value":"reject","label":"驳回(不写具体意见)"}]}
三个 \`value\` 只有这三种形态:\`"approve"\`、\`"reject"\`、\`"custom:"\` 开头(人自己写的话)——
**别自己造第四种**。
【一路上要说话】负责人正看着这一单。**每一拍带一句 say** —— 说业务的话:
识别到几家候选、计划排了几个关键人、执行回执收了几条、覆盖度改了多少;不说系统词。
到了要找人的那一拍,先说清这一轮办完了什么、为什么现在需要他。
【怎么把一项干完 · 一拍连做】write 和 tool 都不会自动收步 —— 做完用 finish 宣告。
动作排进同一拍的 actions 串一口气给;只有发卡 / 提交(挂起)必须停拍。
**这一单常路 8-11 拍就该收口**(比 A1 多两道人环:候选核对 + 计划审批 + 执行回执 + 放行,
共四次;审批若被驳回会多出一到两拍)。
§Z 结案:AM Lead 在验收单上签字,系统把草稿合并进 L-02/L-08 正式版并发射
\`coverage-map\` 口 —— 这一轮的覆盖地图供下游读取(E05/E06 尚未实现,暂无消费者)。`;
/**
* 步骤剧本 —— 一个计划项一条;`doneWhen` 是「成果已存在」的判据,不是「该不该做」。
*
* 四处不给 `doneWhen`:`S2_scope_confirm`(交人核对触达世界)、`S4_plan_approval`
* (要人拍板,且这一项内部可能循环)、`S5_execute_receipt`(拜访/交流等 AI 做不到的线下动作)、
* `T_release`(签下去触达世界)—— 给了等于给引擎一把替人签字的钥匙。
*/
export const COVERAGE_STEPS = [
{
item: 'S1_identify_candidates',
intent: '按台账规则识别本轮候选覆盖客户,铸造本轮案号',
writes: ['crm_coverage.candidate', 'crm_coverage.verdict', 'customer.profile', 'customer.contact'],
/* 入参顶层字段不声明就会被整表段截断刀砍掉 —— 事件驱动那几条路全靠这四个字段;
case:openedAt 是铸 round_ref 的料(引擎注入的开案时刻,2026-08-22 起真的在作用域里) */
reads: ['trigger', 'eventType', 'customers', 'note', 'case:openedAt', 'customer.profile', 'customer.contact'],
view: 'coverage-scope',
tools: [],
/*
* **`_rows > 0` 独木难支,`_missing = 0` 也只顾得上半条命**(2026-08-22 两轮实测
* 逼出来的:事件驱动、customers 数组两个客户点名的入参)。
* 第一轮:模型写成的那一行 customer_name 是空的——`_missing = 0` 补上就治好了。
* 第二轮:模型干脆**没写第二颗客户那一行**(不是写了个空值,是那一行压根不存在)——
* `_missing` 只查「已经存在的行缺不缺字段」,查不出「该有的行还没生出来」;
* `_rows > 0` 更是从一开始就跟"两颗"这个数没关系,一行也算数。
* 两轮都在 S1/S2/S3/S4 四道全部放行,一路悄悄漏到 S5 提交闸才第一次被拦下。
* 治法只能是**把「该有几行」也摆进判据**:customers 数组点了几个名,
* 候选行就该有几条。
*
* **`count(customers)` 在入参没给这个键时 = 1,不是 0**(sfeel 的 count 对未绑定名
* flatten 出 `[undefined]`,长度 1 —— 包内 steps 测试用真 @sfeel 钉死了这三种形状):
* 月度例行(键不存在)走的是 `rows >= 1` 这一半,与前面 `rows > 0` 恰好同义,
* 所以行为对;但别把 `count(...) = 0` 那半句当成「留空时不约束」的开关 ——
* 它只在显式传了 `customers: []` 时才为真。
*/
doneWhen:
'crm_coverage_candidate_rows > 0 and crm_coverage_candidate_missing = 0 ' +
'and (count(customers) = 0 or crm_coverage_candidate_rows >= count(customers))',
guide: `第一件事:把这一轮**候选覆盖客户**列出来。**分两段**——先把每一位客户的编码
定下来,全部定下来了才写候选清单(候选清单不许分批写,写一半会被引擎当成这一步已经办完)。
**customers 里点了几个名,候选清单就该有几行**——事件驱动且点名的那几条路,
遗漏一颗客户等于让它从这一单里凭空消失,后面所有步骤都不会再补回来;
写完候选清单,自己数一遍行数是不是等于 customers 里点名的客户数,对不上就是漏写了。
【先铸造本轮案号 round_ref】格式固定:\`'COV-' + case:openedAt 按 YYYYMMDDHHmmss 格式化\`
(\`case:openedAt\` 就在你的作用域里,形如 "2026-08-22 08:47:58" —— 去掉分隔符即得;
你不必也不许自己编)。**不许从 \`crm_coverage.candidate\` 等正式表里已有的别单案号里挑**
—— 那是别的轮次的号,借了就把两轮的数据搅进同一个主键(A2 借号那一课的同款)。
**整单只有一个 round_ref**,写
\`crm_coverage.candidate\` / \`.plan\` / \`.receipt\` / \`.ledger_update\` / \`.verdict\`
时都用这同一个串 —— 造出两个号就是把一单裂成两单。
【第一段:把 customers 里每一项都定成一个确切的客户编码】
· \`customers\` 留空 —— **月度例行**,不必定编码,直接跳到"没点名"那条候选识别路径。
· 元素是字符串 —— 已经是编码,直接用,不必确认。
· 元素是对象(只在 \`eventType:"new_customer"\` 时出现)——**这是新客户,还没有编码,
发一张数据录入卡请人现场建档**,骨架照 NARRATIVE 那段"新客户建档"原样抄
(\`"kind":"entry"\`,\`ledger:"customer.profile"\`,\`key:{"customer_code":""}\`)。
一次只发一张,发完这一拍就结束(挂起);**人按下确认那一刻基座已经把公司档
(和给了的话,联系人档)落进草稿层**——不许你再 \`write\` 一遍这两张表。回复到了,
从轨迹里取人刚给的 \`customer_code\`,继续处理 \`customers\` 里下一项(还有的话)。
【第二段:全部编码定下来了,才做候选识别与写盘,一拍连做】
· 有确切编码的(月度例行没有这一条)—— **这一轮是事件驱动**,以这些编码为骨架,
逐个从 \`customer.profile\` / \`customer.contact\` 里取画像与联系人现状;
**候选清单只收这几个编码,一个不多、一个不少**——事件驱动点了名的,是这一轮
唯一该覆盖的客户;别顺手把台账里其他覆盖度低、决策链有空白的客户也一并纳入
(那是月度例行的活,这一轮不归它管)。已实测炸过:入参只点了 1 家,模型却在
交代里说"识别到 2 家候选客户",多出来的那一家台账里从没写过——人在核对卡上
看见的候选数量,得和候选清单里躺着的行数完全对得上,不能一个在嘴上一个在表里。
· \`customers\` 从一开始就留空的 —— **这一轮是月度例行**,按台账规则识别:优先选
覆盖度低(\`coverage_score\` 低或缺)、决策链有空白节点、或近期有商机/关键人变动
信号的客户。识别不出候选也要如实写 \`crm_coverage.verdict.summary\`,别硬凑几行充数。
【\`decision_chain\` 是什么形状,没有时怎么办】它是一个 JSON 结构:
\`{nodes:[{node, btLevel:"bt-01"|"bt-02"|"bt-03", required:boolean, contactCode, covered}]}\`。
这家客户若从没跑过 E04(\`customer.profile.decision_chain\` 空的),这一步不必初始化它 ——
留给 S3 那一步在做缺口分析时按标准三节点(bt-01 采购/供应链、bt-02 技术、bt-03 高层)
起草,这一步只管把候选客户与联系人现状读出来、把清单列好。
【写 \`crm_coverage.candidate\`,一颗客户一行】(rows 收数组)
round_ref、customer_code(**必填**,上一段定下来的那个)、customer_name(**必填**)、
cust_type(选填,\`customer.profile.cust_type\` 有值就直接抄它——新客户刚在录入卡上
给过;没有才按 \`grade\`/\`profile_tags\` 猜,如"Tier1 OEM"/"模组厂"/"方案商"/"贸易商"/
"终端",猜不出留空)、bt_level(选填,ASCII \`bt-01\`/\`bt-02\`/\`bt-03\` 之一,按这家
客户目前哪个层级的联系人最缺覆盖来定)、status(选填,ASCII \`pending\`,本步一律先写
这个值)、basis(选填,一句纳入依据,如"覆盖度已 40 天未更新"或"客户点名,商机联动"或
"新客户建档,首轮覆盖")、depth(选填,ASCII \`light\`/\`standard\`/\`deep\` 之一,按 basis
的紧迫程度定)、origin(固定写 \`ai\`)。
【同一拍写 \`crm_coverage.verdict\`(一行)】round_ref、trigger_label(**翻成人读标签写,
不写入参那个英文码**——\`"monthly"\`→"月度例行"、\`"event"\`→"事件驱动";报告上不摆系统内
英文名)、event_type(\`trigger:"event"\` 时把入参 \`eventType\` 也翻成人读标签写进来——
\`new_customer\`→"新客户建立合作"、\`key_person_change\`→"关键人离职/调岗"、
\`major_deal\`→"重大商机启动"、\`t04_project\`→"攻坚项目(T04)立项";\`trigger:"monthly"\`
时这一列留空)、note(入参 \`note\` 原样抄,没有留空)、summary 一句人话(本轮识别到几家
候选、依据是什么)。写完 finish 这一项。`,
},
{
item: 'S2_scope_confirm',
intent: '交人核对候选清单:增删改,确认本轮真正要覆盖谁',
writes: ['crm_coverage.verdict'],
reads: ['crm_coverage.candidate'],
view: 'coverage-scope',
tools: [],
/* 不给 doneWhen —— 交人核对触达世界(manualActivation),引擎不替人收 */
guide: `候选清单起草好了,**该 AM Lead 核对了** —— 这是本活动第一张执行单,零输入。
工作区摆的是候选清单的报告(编辑闸开着就能增删改),他在那份件上核对:AI 没识别到但
他了解有价值的客户,他会加一行;AI 纳入了但实际不该覆盖的,他会删掉;识别错的信息,
他会就地改。
这一步标了人工激活:发卡就算启动,不必单发 start;人交回后你自己 finish。
【照这个骨架发,别少字段】(卡面是纯文本,steps/standard 里别写星号记号,别绑 \`blocks\`
或 \`fields\` —— 整份清单在工作区那份件上,执行卡零输入)
{"kind":"human","itemId":"S2_scope_confirm","workOrder":"execution","card":{
"title":"覆盖范围确认:本轮共 家候选客户",
"ask":"AI 已按台账规则识别出 家候选客户,请核对确认。",
"steps":["工作区的件上核对候选清单:客户编码与客户名称是必填,缺一条这一单走不到经营计划那一步",
"加一行 = 额外纳入(AI 没识别但你了解有价值的客户)",
"删一行 = 排除(AI 纳入了但实际应排除的客户)",
"改单元格 = 修正 AI 识别的内容 —— 客户类型、BT 层级、纳入依据都能改",
"改完保存,回这张卡提交"],
"standard":"候选清单里每一行客户编码、客户名称都齐;该纳入的都纳入、该排除的都排除了。",
"basis":[{"k":"触发口径","v":"<照 crm_coverage.verdict 刚写的 trigger_label/event_type 说,如
「月度例行」或「事件驱动:重大商机启动」>"},
{"k":"AI 识别","v":" 家候选客户"}]}}
发完这一拍就结束了(发卡挂起)。
【人交回之后】这一步你只做两件事,然后 finish:
① 写 \`crm_coverage.verdict.scope_confirmed_total = 1\`;他在件上撤掉了行或加了行的话,
下一步的缺口分析、计划制定按当下 \`crm_coverage.candidate\` 的实际行数走,不必你重写。
② finish 这一项。**别在这一拍做缺口分析** —— 那是下一步的活,这里做了也白做。
【这张卡一单只发一次】人交回来之后不许再发同一张卡 —— 缺必填有提交闸兜着,
卡回来时客户编码/客户名称一定是齐的(或那几行已被他撤掉)。`,
},
{
item: 'S3_plan',
intent: '读覆盖现状、缺口分析、制定关系经营计划',
writes: ['crm_coverage.plan', 'crm_coverage.receipt', 'crm_coverage.candidate'],
reads: ['crm_coverage.candidate', 'customer.profile', 'customer.contact'],
view: 'coverage-plan',
tools: ['crm-relationship-coverage.recompute-score', 'crm-relationship-coverage.rank-key-persons'],
doneWhen: 'crm_coverage_plan_rows > 0 and crm_coverage_plan_missing = 0',
guide: `候选范围定了,**现在读现状、找缺口、排计划** —— 固定三拍,一拍一件事,别在同一拍里
把「调工具」和「写台账」串在一起(**工具调用后同拍其余动作作废,结果要下一拍才看得见**;
串了也是白串,只会多烧一拍)。这一步只需要调工具**各一次**,拿到结果后不要再调第二次。
**这一步只管 \`crm_coverage.candidate\` 里躺着的那几家,一家都不能多**——\`reads\` 里
还带着 \`customer.profile\`/\`customer.contact\` 整表,那是给你查这几家候选客户各自的
决策链与联系人现状用的**参考资料**,不是"顺手也把看见的其他客户一起排上计划"的许可。
已实测炸过:候选清单里明明只有 1 家,写出来的计划却多了一家台账里压根不存在于候选清单
的客户(那家客户是**从 customer.profile/customer.contact 整表里"看见"的**,和这一轮
候选范围毫无关系)——动手前先把候选清单的 \`customer_code\` 抄一遍,写计划时逐条核对
是不是都在这张抄出来的清单里,不在的一律不许写。
【第一拍:缺口分析,只调工具,别的什么都不写】把本轮全部候选客户(读
\`crm_coverage.candidate\`)一次性打包,调一次
\`crm-relationship-coverage.recompute-score\`,入参:
\`\`\`
{"customers":[
{"customerCode":"CU-0142",
"decisionNodes":[
{"node":"bt-01","btLevel":"bt-01","required":true},
{"node":"bt-02","btLevel":"bt-02","required":true},
{"node":"bt-03","btLevel":"bt-03","required":true}],
"contacts":[
{"contactCode":"CU-0142-01","decisionNode":"bt-01","keyPerson":false,"influence":"","relationship":0}]}
]}
\`\`\`
· \`decisionNodes\`:这家客户若已有 \`customer.profile.decision_chain\`,原样照抄它的节点;
没有(新客户/首次跑 E04)就照抄上面这三行(**node 与 btLevel 写同一个值**,
\`bt-01\`/\`bt-02\`/\`bt-03\`,一个字符不许错、不许留空 —— node 留空会让出参里那个节点
变成 \`null\`,缺口分析就废了一条)。
· \`contacts\`:这家客户名下**所有**已知联系人(读 \`customer.contact\`),逐人填
\`contactCode\`/\`influence\`/\`relationship\`/\`keyPerson\`;\`decisionNode\` 按人现有的
\`customer.contact.decision_node\` 填,没有就按职位/角色估一个最贴近的层级(如「采购部」
估 bt-01),都判不出就填 \`"bt-01"\` 兜底 —— **这一格不许留空**,留空这个人就永远配不上
任何节点、白算了这一趟。空缺字段(如 \`influence\` 还没定过)照现有值填,是空字符串
或 0 都行,工具认得。
多家候选客户就都塞进同一个 \`customers\` 数组里,**一次调完,不要一家调一次**。
调完这一拍就结束(工具出参要下一拍才看得见)。
【第二拍:排序,只调工具,别的什么都不写】把上一拍 \`results\` 里出现过的**所有联系人**
(不止缺口节点上的那几位)一次性打包,调一次 \`crm-relationship-coverage.rank-key-persons\`,
入参 \`{"contacts":[{"contactCode":"...","influence":"...","relationship":0}, ...]}\`——
**批量调,不要一人调一次**。调完这一拍就结束。
【第三拍:写计划,一拍连做】拿着前两拍的工具出参,**一次都不要再调工具**,直接写:
① 逐家候选客户看第一拍的 \`gapNodes\`:**gapNodes 里的每一个缺口节点,都要在计划里
至少配一个关键人经营动作**——就算这个节点上目前一个联系人都没有覆盖到(\`coveredNodes\`
是空的、\`coverageScore\` 是 0),那正是这一步该管的事:挑这家客户名下**已知的、
离这个节点最近的联系人**(按 \`decisionNode\` 匹配,匹配不上就挑影响力最高的那位)
写一条「去覆盖这个节点」的动作。**\`gapNodes\` 全员缺口是正常结果,不是异常,
不需要回去重调工具确认。**
② **写 \`crm_coverage.plan\`,按第二拍的 rank_score 降序、一个关键人一行**(rows 收数组;
模版不排序,你必须已经按序写)。round_ref、contact_code(**必填**)、customer_code
(**必填**)、customer_name(选填)、contact_name(选填)、action(**必填**,具体到能
执行的建议动作,如"下月中拜访技术总监,带 XX 认证报告"而不是笼统的"拜访")、priority
(**必填**,ASCII \`high\`/\`medium\`/\`low\`,照第二拍算出的档位抄)、rationale(**必填**,
说清为什么是这个人这个动作,引影响力与亲密度差值,或"该节点目前空白,此人是最接近的
候选")、influence(**必填**,写计划这一刻的快照,照抄联系人当前的 influence,没有就写
\`"low"\`)、relationship_gap(**必填**,第二拍算出的数)、rank_score(**必填**,第二拍
算出的数)、target_date(**必填**,具体日期,没有时间点的建议不算数)。
③ **同一拍,给每条计划配一行 \`crm_coverage.receipt\` 骨架**(这一步只写骨架,完成状态那几列
留空,S5 那一步才由人填):round_ref、contact_code(与 plan 同一个,**必填**)、
customer_code(选填,照抄)、customer_name(选填,照抄 —— 执行回执工单的「对象」列长
"客户名 · 关键人",两个名字都从这里来)、contact_name(选填,照抄)、action(选填,
照抄 plan 的 action,供执行回执工单只读展示)、target_date(选填,照抄)。
completion_status / result_summary / customer_feedback / key_person_change
**这一步一格都不写**。
④ 若某候选客户的 \`cust_type\`/\`bt_level\` 在 S1 那步没判出来,顺手补写
\`crm_coverage.candidate\` 的这两列(同 PK 覆盖写)。
写完这一拍就结束(不必你 finish)——\`crm_coverage_plan_rows > 0 and
crm_coverage_plan_missing = 0\` 一旦为真,引擎自己收这一步。`,
},
{
item: 'S4_plan_approval',
intent: '经营计划审批:通过就往下走,驳回则按意见修订后换一种问法再提交',
writes: ['crm_coverage.plan', 'crm_coverage.receipt', 'crm_coverage.verdict'],
reads: ['crm_coverage.plan'],
view: 'coverage-plan',
tools: [],
/* 决策卡循环 + 要人拍板 —— 不给 doneWhen,引擎不许替人收 */
guide: `**动手之前先在轨迹里找一行 \`[card-reply]\` 带这一项的答复:**
· **没找到** —— 这是第一次,跳到下面【发一张决策卡】那一段;
· **\`value\` 是 \`"approve"\`** —— 写 \`crm_coverage.verdict.plan_approved_total = 1\`,
finish 这一项,**不许再发卡**;
· **\`value\` 是 \`"reject"\` 或以 \`"custom:"\` 开头** —— 都算驳回,跳到下面【驳回之后】
那一段,修订计划再重新发卡。
【这一步办完了没有,看一个数】\`plan_approved_total\`:它写成 1 就是这一步办完了,
引擎不会再把这一项交给你(它归零 doneWhen 帮不上忙,只有你自己 finish)。
【发一张决策卡,照这个骨架】(选项少于两个基座拒发;\`allowCustom\` 别漏 —— 少了它,人只有
两个固定按钮可点,连意见都没地方写;\`ask\` 每一轮都要按下面的规则换措辞)
{"kind":"decide","itemId":"S4_plan_approval",
"allowCustom":true,
"ask":"经营计划已算出,共 条,覆盖 家客户的关键人,请审批。",
"options":[
{"value":"approve","label":"通过","recommended":true,"consequence":"计划进入执行阶段,交人去落实"},
{"value":"reject","label":"驳回(不写具体意见)","consequence":"AI按自己的判断复核一遍计划,重新提交审批"}],
"basis":[{"k":"覆盖客户","v":" / <本轮候选总数> 家已各有至少一条计划"},
{"k":"排序依据","v":"影响力 × 亲密度缺口"},
{"k":"想说明理由","v":"不点上面两个按钮,直接在下方输入框写你的意见再提交"}]}
发完这一拍就结束了(发卡挂起)。
【驳回之后 —— 不管有没有具体意见,都只处理这一轮,不许再追问一遍】
① \`value\` 以 \`"custom:"\` 开头 —— \`note\` 就是他写的具体意见,**按这条意见**修订
\`crm_coverage.plan\`(同 PK 覆盖写对应的行,不开新计划项、不重新进入 sentry)。
② \`value\` 是 \`"reject"\`(点了固定按钮,没写具体意见)—— **不许再发卡追问理由**
(那两个按钮点出来的回复永远没有 note,再问一遍是把人指向一扇打不开的门,
会被同一句话拒回,连问出真话的机会都没有)。改成你自己复核:排序有没有明显不合理、
有没有遗漏更紧要的缺口节点、理由那一句站不站得住,尽量改得更有说服力;复核后**至少要有
一处实质性变化**(改排序、换动作、补理由,不能原样重发)。
③ **改了 \`action\` 这一列的话,\`crm_coverage.receipt\` 同一行的 \`action\` 也要跟着改**
(同 PK——\`round_ref\`+\`contact_code\`——覆盖写)。receipt.action 是 S3 那一拍从
plan.action **抄**过来的展示副本(执行回执工单直接读它、只读展示),这里只改了
plan 不改 receipt,S5 那一步人看到的还是驳回前的旧动作——已经实测过:审批来回改了
三稿,receipt 里摆的还是第一稿,推进到执行回执时对不上人刚通过的那一版。
④ **重新发卡的 \`ask\` 必须换一种说法**,带上修订轮次与改了什么,例如
"已复核并修订(第2稿:调整了 CU-0142 的优先级,补充了理由)请再次审批"——
措辞不换,系统会把它当成同一张卡:前两次静默吞掉(人看不到新卡),第三次直接拒。
**每一轮的 \`ask\` 都要与上一轮明显不同**,不止改个数字。
⑤ 再走一遍【发一张决策卡】那一段(用新措辞,\`allowCustom\` 仍然要给),然后这一拍结束
(发卡挂起)。
**这一步只在这一项里循环,不许把驳回意见带去改别的项**(S1/S2/S3 写下的东西不动)。`,
},
{
item: 'S5_execute_receipt',
intent: '交人执行经营动作,回填执行回执',
writes: [],
reads: ['crm_coverage.receipt'],
view: 'coverage-receipt',
tools: [],
/*
* 拜访 / 技术交流等线下动作 AI 做不到 —— 不给 doneWhen。
* **这一步没有任何记账旗**(2.0.2 退役 receipt_confirmed_total,两轮活单熔断实测):
* 旗是模型写的,S4 一过它顺手写旗就坚信 S5 已办完,连续五拍
* [写旗 + finish 被 worldTouched 拒] 熔断。回执四列全是人在件上填的,
* 收口那道门读的是基座数(`_rows`/`_missing`)—— 模型无账可记,也就无处抢跑。
*/
guide: `计划审批通过了,**该 AM 出场了** —— 这是本活动第二张执行单,零输入。
触达客户的那一下由人做:拜访、技术交流、价值传递这些线下动作 AI 做不到,
工作区摆的是执行回执工单(逐个关键人一行,计划动作只读展示,回执四列可填)。
这一步标了人工激活:发卡就算启动,不必单发 start。**这一步不写任何台账** ——
回执四列由人在件上填、由基座直接落草稿层,你只有两个动作:发卡,和人交回后 finish。
**这一步「人被问过没有」不用你判断 —— 【当前这一步】标题下面那行引擎事实写得明明白白**
(「这一单里已经真的给它发过卡…人已经被问过了」或「这一单里从没给它发过卡」),照它分两条路:
· 那行说**「从没给它发过卡」** —— 你唯一合法的动作就是照下面骨架发卡,发完这一拍结束(挂起)。
**别去轨迹里数 \`[card-reply]\` 自己判断**:S2 的执行单、S4 的决策卡都会在轨迹里留
\`[card-reply]\`,它们不是这一步的 —— 实测两轮连拒都是把 S4 审批的答复当成了 S5 的回执,
凭空宣布「执行回执已交回」。引擎那行事实比你对轨迹的读法准,以它为准。
· 那行说**「人已经被问过了」** —— 人交回后直接 finish 这一项,一个字都不用写
(回执数据人已经在件上填好、基座已落草稿层;**别在这一拍收口写台账** ——
重算覆盖度、写回主数据是下一步的活)。
【照这个骨架发,别少字段】(执行卡零输入,别绑 \`blocks\` 或 \`fields\`)
{"kind":"human","itemId":"S5_execute_receipt","workOrder":"execution","card":{
"title":"经营计划执行回执:共 项动作",
"ask":"经营计划已审批通过,请执行并回填回执。",
"steps":["每项动作执行后,在工作区的件上逐项回填:完成状态、结果摘要为必填,客户反馈选填",
"关键人变动如实回答「是/否」;若填「是」,请在结果摘要里说清具体变动是什么",
"确实没有变动的,「关键人变动」这一格要写「无变动」,不能留空",
"改完保存,回这张卡提交"],
"standard":"每一项动作的完成状态与结果摘要都填了;关键人变动逐项如实标注,不遗漏。",
"basis":[{"k":"本轮动作数","v":" 项"}]}}
发完这一拍就结束了(发卡挂起)。
【这张卡一单只发一次】人交回来之后不许再发同一张卡 —— 缺必填有提交闸兜着。`,
},
{
item: 'S6_writeback',
intent: '写回主数据:按回执重算覆盖度,更新联系人/客户档案与候选状态',
writes: ['customer.profile', 'customer.contact', 'crm_coverage.candidate'],
reads: ['crm_coverage.receipt', 'crm_coverage.plan', 'customer.profile', 'customer.contact'],
view: 'coverage-ledger-update',
tools: ['crm-relationship-coverage.recompute-score'],
/*
* 四个数全是基座现数:重算分与档案都写齐了才算办完。
* 收口从一项拆成两项(2.0.4 结构治):从前「三拍纪律」写得再明白,模型照样把
* 四张表挤进一串,串尾最费字数的 ledger_update 被截成空数组挨拒(两轮实测同一处)。
* 拆开之后每项的剧本只摊自己那几张表 —— 串想长也长不起来。
*/
doneWhen:
'customer_profile_rows > 0 and customer_profile_missing = 0 and customer_contact_rows > 0 and customer_contact_missing = 0',
guide: `回执都收齐了,**现在写回主数据** —— 固定两拍,别把「调工具」和「写台账」串进
同一拍(工具调用后同拍其余动作作废,结果要下一拍才看得见)。
**这一步不碰 \`crm_coverage.ledger_update\`** —— 留更新明细是下一步(S6_ledger_update)
的活,这一步只管把主数据写对。
【第一拍:重算,只调工具,别的什么都不写】逐个联系人按回执推导新的 relationship/influence
(草稿,先想清楚,还不落库):\`completion_status\` 为 \`done\` 且关系明显推进的,
\`relationship\` 上调(不超过 5);\`partial\`/\`skipped\` 的原样不动或视回执具体内容微调。
**调不出结论就别改** —— 没有回执支持的调整不许写。把本轮涉及的候选客户**一次性打包**,
调一次 \`crm-relationship-coverage.recompute-score\`:
\`decisionNodes\` 照抄 \`customer.profile.decision_chain\` 现有的节点(\`node\`/\`btLevel\`/
\`required\` 三样都要,一个字符不许错);\`contacts\` 是这家客户名下**所有**已知联系人,
**逐人五个字段都要给全**——\`contactCode\`/\`decisionNode\`(按 \`customer.contact
.decision_node\` 填)/\`keyPerson\`(这轮判断的关键人标记)/\`relationship\`(推导后的新值)/
\`influence\`(推导后的新值)。
\`\`\`
{"customers":[{"customerCode":"CU-0142",
"decisionNodes":[{"node":"bt-01","btLevel":"bt-01","required":true}, ...],
"contacts":[{"contactCode":"CU-0142-01","decisionNode":"bt-01","keyPerson":true,
"relationship":4,"influence":"high"}]}]}
\`\`\`
**\`keyPerson\` 这一格漏了,工具会把这个人当成"不存在",这个节点当场判空**——已经实测炸过:
同一批入参只差这一个字段,coverageScore 从 17/33 变成 0,跟台账里明明白白写着的
"bt-01 已覆盖"对不上;模型因此怀疑结果、来回改了又调、调了又改,一单纯为这一步烧了 26 次
工具调用、烧穿了 500k token 预算,\`crm_coverage.ledger_update\` 一行没写成就 exhausted 了。
拿到 \`results\`(逐家 coverageScore/coveredNodes)——**一次调完,不要一家调一次,更不要见
结果与预期不符就反复重调**:它是纯工具,同样的输入永远同样的结论,算出来的覆盖度比预想低,
先检查自己传的 \`contacts\` 是不是漏了字段,而不是重调一遍赌下次会不一样(不会)。
调完这一拍就结束。
【第二拍:只写这两张,别的表这一拍不碰】
· **写 \`customer.contact\`**(对应联系人行,同 PK 覆盖写):**碰过的每一行,
\`influence\` / \`relationship\` / \`key_person\` 三列一列都不许缺**(基座数缺必填
只看本单写下的行 —— 你写了这一行却漏了其中一列,\`customer_contact_missing\`
就大于零,放行门永远打不开):\`influence\` 写推导后的评级(没变化就照现值抄一遍)、
\`relationship\` 写推导后的新值(没变化照现值抄)、\`key_person\` 照这轮判断写;
**本单新建档的联系人**(录入卡只落了姓名/职务/邮箱)这三列由你首评:
\`influence\` 按职务估(总监/VP 级 high、经理级 medium、其余 low)、
\`relationship\` 按实际接触定(还没接触过写 1)、\`key_person\` 如实判断;
\`role\`/\`decision_node\`/\`comm_pref\` 若这轮有新信息一并补;
\`change_log\` 追加一句本轮变动(离职/调岗从回执的 \`key_person_change\` 抓,
没有变动不必写这一列)。
· **写 \`customer.profile\`**(对应客户行,同 PK 覆盖写):\`coverage_score\` = 工具
算出的新分数(**必填**);\`decision_chain\` = 更新后的节点覆盖状态(**必填**,
整个数组重写,不是增量合并);\`interaction_timeline\` 追加本轮的互动条目(选填,
没有新增互动的客户不必写这一列)。
· **把办完的候选行状态置 \`covered\`**(写 \`crm_coverage.candidate\`,同 PK 覆盖写,
只带 status 一列):回执里有 \`done\` 动作的客户置 ASCII \`covered\`;
全部 \`skipped\` 的留 \`pending\` 如实 —— 清单上那枚状态徽是给人看的,
一轮跑完还全写着「待覆盖」就是界面在撒谎。
写完这三张,这一步就办完了 —— 不必你 finish,\`doneWhen\` 会在主数据写齐那一刻自动收步,
留更新明细归下一步(S6_ledger_update),这一拍到此为止。`,
},
{
item: 'S6_ledger_update',
intent: '留痕收口:逐条台账更新明细 + 本轮覆盖结论',
writes: ['crm_coverage.ledger_update', 'crm_coverage.verdict'],
reads: ['crm_coverage.receipt', 'customer.profile', 'customer.contact'],
view: 'coverage-ledger-update',
tools: [],
doneWhen:
'crm_coverage_ledger_update_rows > 0 and crm_coverage_ledger_update_missing = 0 and customer_profile_missing = 0 and customer_contact_missing = 0',
guide: `主数据已在上一步写回,**这一步只写两张表、一拍连做**:先写更新明细
(它最费篇幅,放在串首),再写结论。**上一步那三张表(contact/profile/candidate)
这一拍不许再写一遍** —— 再写一遍原值只会被当空转拒掉。
· **写 \`crm_coverage.ledger_update\`,每条主数据更新对应一行**(rows 收数组,
**数组不能是空的** —— 第二拍写的每一处真实变化,这里都要有一行对应):
round_ref、update_seq(**必填**,本轮内从 1 递增)、ledger_label(**必填**,人读名,
如"客户主台账·关系覆盖度")、ledger_address(**必填**,机器地址,如
"customer.profile.coverage_score")、customer_code(**必填**)、contact_code
(选填,客户级字段留空)、field_label(**必填**,人读列名)、before_value(**必填**,
无值写字面"(无)")、after_value(**必填**)、evidence_ref(**必填**,引用具体回执
事实,如"回执 contact_code=CU-0142-01:已完成拜访,亲密度由3提升至4")。
**没有真实变化的字段不必开一行** —— 明细表只记「真改了什么」。
每一处真实变化的依据都在回执与上一步写下的新值里,对照着写,别凭记忆。
· **写 \`crm_coverage.verdict\`**(同一拍接着写):ledger_update_confirmed_total = 1、
covered_customer_total = 这一轮覆盖度提升或已达标的客户数、
key_person_covered_total = 这一轮标记为 key_person 的联系人数、summary
补一句人话(这一轮覆盖了几家客户、覆盖度平均涨了多少)。
写完 finish 这一项 —— 或者不必你 finish,\`doneWhen\` 会在必填列都齐的那一刻自动收步。`,
},
{
item: 'T_release',
intent: '台账已收口,提交放行交 AM Lead 验收;被打回就按意见改完再交',
/* 被打回重做时要改的就是这三张 —— 不声明就写不进去(写的授权面是台账声明) */
writes: ['crm_coverage.plan', 'crm_coverage.ledger_update', 'crm_coverage.verdict'],
reads: ['crm_coverage.plan', 'crm_coverage.ledger_update', 'crm_coverage.receipt'],
view: 'coverage-ledger-update',
tools: [],
/* 放行触达世界,必须人按一下 —— 引擎不自动收它 */
guide: `台账更新明细齐了、主数据缺项也补齐了,**可以走放行了**
(T_release 人工激活:动它就算启动,不必单发 start)。
【**先看一眼轨迹里有没有「验收被打回重做」**】有的话,这一拍**不是**再提交一遍 ——
人已经看过这份成果并把它退回来了,原样递上去他看见的还是刚被他打回的那一份
(基座那道闸也会当场拒:「打回之后台账一个字没改」)。照他的意见改**台账本身**,
改完这一拍再 judge + submit:
· 他说**计划的理由空/不实** → 改 \`crm_coverage.plan\` 的 \`rationale\`(同 PK 覆盖写),
把影响力评级、亲密度差值、决策链缺的是哪个节点写进去,一条一条落实;
动作不具体的连 \`action\`/\`target_date\` 一起改。
· 他说**台账更新没依据 / 数不对** → 改 \`crm_coverage.ledger_update\` 的
\`evidence_ref\`(点名是哪条回执支持它)或 \`before_value\`/\`after_value\`。
· 他说**结论没说清** → 改 \`crm_coverage.verdict.summary\`。
· 他**没写具体意见**(只点了重做)→ 自己把成果复核一遍,挑最站不住的那处改实
(理由最空的那条计划、依据最含糊的那条更新),**至少改对一处**;
实在判断不出他要什么,发一张零输入执行工单问清楚,别原样重提。
**改的是值,不是数**:同一行覆盖写,别新开行、别把行数改了 —— 那会把收步判据弄假。
【没被打回(或已按意见改过了)】先把裁量判据裁掉,然后 submit,同一拍连做:
· judge criterion="每条台账更新都能在执行回执里找到对应事实依据,不是凭空推算" ——
逐条看 \`crm_coverage.ledger_update\` 的 \`evidence_ref\`:说得出具体哪条回执支持这条
更新就 satisfied=true;有一条说不清来路的,satisfied=**false**,rationale 逐条写清,
交签字的人定。
· judge criterion="更新后的数值(覆盖度、亲密度等)变化合理,没有异常跳变" ——
看每条数值型更新的 before/after 幅度是否与回执的完成程度相称。
· judge criterion="更新内容准确对应回执事实,没有张冠李戴或误更别的客户/联系人" ——
核对 customer_code/contact_code 与回执主体是否一致。
**裁成不满足照样能提交** —— 理由连着卡一起交给签字的人。criterion 照判据名整句原样抄,
写别的名字对不上。
这一单没有分支去向 —— 全程线性,只发一口 \`coverage-map\`,不必你选出口。
终局唯放行:你不能自己宣布完成。`,
},
];
/**
* 阶段 —— 进度条的刻度,只给人看。除放行外每个计划项恰属一个阶段;
* 放行那一格由基座统一加,人看到的是 x/4。
*/
export const COVERAGE_PHASES = [
{ name: '确认覆盖范围', items: ['S1_identify_candidates', 'S2_scope_confirm'] },
{ name: '制定与审批计划', items: ['S3_plan', 'S4_plan_approval'] },
{ name: '执行与更新台账', items: ['S5_execute_receipt', 'S6_writeback', 'S6_ledger_update'] },
];