1. 17 Aug, 2026 10 commits
    • perf(圈人): 圈人侧也改读预存标签 —— 603ms → 57ms,顺带堵上「两侧会漂」的口子 · 7bbe9a1b
      上一轮只改了**看**侧(初选矩阵),圈人侧的 `labelExistsSql` /
      `labelTemperatureExistsSql` 仍在实时回查 `patient_facts` 算标签。
      
      🔴 这不只是慢,是**我上一轮亲手造出的口径不一致**:
        · 看侧读预存的 potential_labels(回填那一刻的年龄)
        · 圈人侧实时算(当前年龄)
        标签规则里有三条带年龄(K08>18 / K07 3~12 / K07 13~40)⇒ 过生日跨档的人两边不一样。
        而 `reason-temperature.sql.ts` 里那段注释早就写过这个坑:
          「曾经它们各自带一个 mode 参数,一旦调用方漏传就是主管看到 87 人、
            确认单捞出另一批,而且全程不报错(T14)」
        我差点原样重演一遍,只是这次漂的原因从"参数"换成了"数据源"。
      
      改动:两处 EXISTS 都换成 `pr.potential_labels @> ARRAY[label]`。
      
      ■ 实测(本地,15,884 条 plan 的诊所,人数逐字未变)
          只按治疗       603 ms → 57 ms   2305 人 → 2305 人
          治疗 + 温度     971 ms → 82 ms    353 人 →  353 人
        这条路一次弹窗要走两遍(整格人数 + 当页明细),还被 propose 复用。
      
      ■ ️ 三条既有断言失败 —— 但它们守的不变量**没有消失,是搬家了**
        · 「只认未治疗的证据(f.status='active')」
        · 「证据必须有日期(COALESCE(occurred_at, planned_for) IS NOT NULL)」
        两者现在由回填 SQL 保证。 没有删断言,而是搬到 plan-label.spec.ts 钉住 ——
        删了的话,谁把"治完的诊断"或"没有日期的证据"算进标签都不会有人发现,
        而那会直接改变召回谁。️ 已做变异校验:拿掉 status 守卫立刻变红。
        · 第三条改成断言"锚点仍是末诊",并注明非空守卫搬去了哪。
      
      ■ 新增《看侧与圈人侧同源》一组:三处都读 potential_labels、
        三处都不再出现 patient_facts / jsonb_array_elements_text / 按年龄现算。
      
      验证:tsc 通过;jest 86 套 1351 例全过;eslint 无新增错误。
      luoqi committed
    • fix(矩阵): 补回 JOIN patients —— 线上 500 修复(missing FROM-clause "p") · a0a86b94
      3aa69952 上线后矩阵整页 500:`missing FROM-clause entry for table "p"`。
      
      原因:标签预计算后我判断"年龄已烘进标签、用不到 patients 了",把
      `JOIN patients p` 删了。但 `poolBaseSql` 在 **scope.sourceUnits 非空**时会拼
      `AND p.source_unit IN (...)`(多品牌隔离) —— 别名没了,SQL 直接失效。
      
      🔴 为什么本地全绿还是漏到线上 —— 三层防线**同时**失守,每一层都是我自己造的:
        1. **基准脚本硬编 `sourceUnits: []`** ⇒ 永远走不到那一支,测了个假场景。
           本地那台 host 恰好也没配品牌,连真实调用都碰不到。
        2. **单测把 bug 固化成了断言**:写着 `expect(sql).not.toMatch(/JOIN patients/)` ——
           它不但没拦住,反而会**保护**这个错误。️ 断言写的是"我以为的",不是"必须成立的"。
        3. tsc / jest / eslint 全绿 —— 这类 SQL 别名闭合问题它们天生看不见。
      
      修复与加固:
        · 补回 `JOIN patients p`(注释写明它是被 poolBaseSql 引用的, 别再删)
        · 基准脚本改为**两支都跑**:sourceUnits=[] 与真实品牌列表各跑一遍。
          ️ 已做变异校验:删掉 JOIN 后基准立刻报 missing FROM-clause。
        · 新增单测《poolBaseSql 用到的别名,查询里必须都在 FROM 里》——
          含一条**通用的别名闭合检查**(扫出 SQL 里所有 `x.` 前缀,逐个确认在 FROM/JOIN 里声明过),
          不连库、jest 里就红。️ 同样做了变异校验:删 JOIN 立刻 3 条红。
        · 删掉那条错的断言,换成断言**真正变了的事**:查询里不再出现按年龄推标签的 CASE。
      
      验证:tsc 通过;jest 86 套 1345 例全过;eslint 干净;
        基准两支都跑通(15,884 plan 的诊所 137ms,磁盘读 0)。
      luoqi committed
    • merge: 初选矩阵提速 8.7 倍 —— 潜在治疗标签预计算落库 · 3aa69952
      · plan_reasons 加 potential_labels,矩阵不再回查 patient_facts
        本地实测 958ms → 110ms,磁盘读 119,861 → 0,24 格逐字未变
      · 补上就地刷新 reason 那条路的标签失效(证据变了就标记重算)
      · 加 backfill-plan-labels CLI —— 上线后必须跑一次(迁移后全表 NULL,
        实测置空后矩阵返回 0 行)
      · 撤回「重算 plan 会释放客服」那个错判断
      luoqi committed
    • docs(cli): 撤回「重算 plan 会释放客服」—— 那是错的,因果说反了 · 5b01bfec
      上一条(2b0c5406)在 CLI 头注里写「 别为了填派生列去重算 plan,会 auto_release
      释放客服手上的单」。查代码后确认**这个判断是错的**:
      
      `auto_release` 只有两个触发条件,都在 plan-engine 里写得很清楚:
        · 本轮该患者 **0 命中**(信号真没了,比如治疗做完了)
        · 患者**最后到诊诊所变了**,原客服将出 scope
      两者都是**真实的业务状态变化**,不是"重算"这个动作造成的。
      而且 `plansSkippedAssigned = 0`(assigned 不再 skip),这套逻辑本来就跟着
      增量同步每天在跑 —— 该释放的早已释放,再跑一次不会多释放任何人。
      
      ⇒ 日常维护**本来就靠原机制**:plan 生成收尾会调 `backfillMissing()`, 并没有绕开它。
        这个 CLI 只解决一件事:**上线那一刻的空窗** —— 迁移后全表 NULL,矩阵遇到 NULL
        什么都不出 ⇒ 矩阵全是 0,要等下一次增量同步(最长两小时)或夜间刷新(03:30)
        才自愈;CLI 把窗口压到约一分钟。
      
      ️ 留着那句错话的代价不是"说错一次",是下一个人会据此**避开一个本来安全的操作**。
      luoqi committed
    • chore(cli): 加 backfill-plan-labels —— 别为了填派生列去重算 plan · 2b0c5406
      上线后必须立刻跑一次:迁移只是加了列,全表都是 NULL,而矩阵
      `unnest(potential_labels)` 遇到 NULL 什么都不出 ⇒ **部署完到回填完之间矩阵全是 0**。
       别指望夜间任务兜 —— 那要等到次日 03:30。
      
      ️ 「重算 plan 也能顺带补齐」是真的(收尾会调 backfillMissing),但**不该拿它当手段**:
        重算会升版本、关闭无信号的 plan、auto_release 释放客服手上的单 ——
        为一个派生列去动召回池,代价完全不对等。
        (测试服上还有一批"特殊处理过另有他用"的在手单,更不该被卷进去。)
      
      用法:
        pnpm backfill-plan-labels           # 只补没算过的(上线后跑这个)
        pnpm backfill-plan-labels -- --all  # 连算过的一起重算(改了标签规则后)
        pnpm backfill-plan-labels -- --check # 只报还差多少,不写库
      
      ️ 回填后若仍有剩余 → 报 error 且 exit 1:那说明有写入路径没接上补齐,
         别等矩阵少人了才发现。
      
      验证:tsc 通过;jest 86 套 1342 例全过;eslint 干净;
        本地实跑:--check 报 0 → 人为置空 119 条 → 回填写入 119 条 / 0.1s / 剩余 0。
      luoqi committed
    • fix(矩阵): 就地刷新 reason 时把标签标记重算 —— 补上一版漏掉的那条路 · 99032119
      上一版(1293108a)只在**新建** reason 时补标签,漏了 `plan-engine` 的**就地刷新**路径
      (plan-engine.service.ts:569):signals/evidence 语义变了但不升版本时,
      它直接 `planReason.update({ evidence: { factIds } })` 改写证据。
      
      🔴 那条路上 `potential_labels` 既不是 NULL(收尾的 backfillMissing 会跳过它)、
        又已经对不上新证据 ⇒ **初选矩阵按旧标签把人算进错的格子**,
        要到夜间全量重算才自愈,中间一整天不报错。
      
      ⇒ 同事务里把这些 reason 的 `potential_labels` 置回 NULL(=「没算过」),
        本轮收尾的 `backfillMissing()` 用同一份 SQL 填上。
         不在这里用 TS 顺手算一个:规则表是产品配置,第二份实现迟早和矩阵那份漂开。
      
      ️ 走 raw:Prisma 的**标量数组字段不能用类型化 API 置 null**(tsc 会拦);
        而改用 `[]` 会让「没算过」和「算过是空」分不开 —— 那正是这一列设成可空的理由。
      ️ 第一版 raw 写成 `IN (a,b)::uuid[]` —— **tsc 全绿但 SQL 是错的**
        (cast 套在了布尔结果上,PG 报 `cannot cast type boolean to uuid[]`)。
        这类错类型检查天生看不见,只能真连库跑一次。已改 `= ANY(ARRAY[...]::uuid[])` 并实测。
      
      顺带:单患者重算路径(recomputeForPatient)也补上 backfillMissing ——
         不补的话这位患者在矩阵里**当场消失**,直到夜间重算才回来。
        平时代价接近零:`potential_labels IS NULL` 上有部分索引。
      
      测试:
        · 替身补 `tx.$executeRaw`,并给 engine() 传标签刷新器替身
        · 新断言「证据被就地改写 ⇒ 必须同时标记重算」——
          ️ 做过变异校验(把期望改成 99 立刻变红),确认这条断言真的在跑,不是假绿
      
      验证:tsc 通过;jest 86 套 1342 例全过;矩阵基准 24 格仍与基线逐字相同;
        eslint 无新增错误(plan.service 里 3 条 no-empty-object-type 是既有的)。
      luoqi committed
    • perf(矩阵): 潜在治疗标签预计算落库 —— 958ms → 110ms,24 格逐字未变 · 1293108a
      初选矩阵此前每次开页面都从最原始的事实现推:
        plan_reasons → 展开 evidence.factIds → 回查 patient_facts(1567 万行 / 18 GB)
      最忙的诊所(15,884 条 plan)一次摊 34,459 次随机查、372 MB I/O ——
      **而这一切的唯一产出就是一个标签字符串**。温度那根轴根本不碰 fact
      (它来自 patient_profiles.last_visit_at)。
      
      ⇒ 加一列 `plan_reasons.potential_labels text[]`,矩阵改成
        followup_plans ⋈ plan_reasons ⋈ patient_profiles + unnest。
      
      ■ 实测(本地,15,884 条 plan 的诊所,与测试服 15,770 同规模)
          耗时      958 ms → 110 ms   (8.7×)
          磁盘读   119,861 → 13,500   (-89%)
          JIT       触发   → 不触发
          小诊所   11 ms  → 0.4 ms    (地板消失)
        🔴 **五个诊所的 24 格逐字相同**(scripts/bench-matrix.ts 的结果快照 diff 零差异)——
          性能改动最容易的失败是悄悄少算一批人,只比耗时看不出来。
      
      ■ 为什么是数组而不是单列
        一条依据可挂多个 fact:93.7% 只有一个,但**3.7%(本地 1,391 条)会推出不止一个 code**。
         存单值会把这批人算少。
      
      ■ ️ 年龄被冻结,所以必须刷
        规则里三条带年龄(K08>18 / K07 3~12 / K07 13~40)⇒ 标签不是纯函数。
        实测受影响 5,551 人中「明天跨档 0 人、30 天内 16 人」(≈0.5 人/天,占矩阵 1.4 万人的 0.003%)。
        ⇒ 每晚 03:30 全量重算(PlanLabelRefreshService);
           不刷就逐日漂,且不会有任何报错。
      
      ■ 三条写入路径共用同一份 SQL
        🔴  绝不在写入侧用 TS 再算一遍:规则表是产品配置,第二份实现迟早和矩阵那份漂开,
          而漂了之后矩阵的数会**静默**不一样。
        · 生成完 plan 立刻 backfillMissing() ——  不补的话新 plan 的列是 NULL,
          `unnest` 直接跳过 ⇒ **今天生成的人在矩阵里看不见**,且不报错。失败不阻断生成。
        · 每晚全量重算(年龄)
        · 部分索引 `WHERE potential_labels IS NULL` —— 平时几乎是空的,补齐即 O(1),
          否则每次生成完都要为找 NULL 扫一遍 31 万行。
      
      ■ 两个自己踩了又修的坑(都写进了注释与测试)
        · 全量重算最初只写 `ORDER BY id LIMIT n` **没有游标** —— 每次取同一批,原地打转,
          而且不报错只是每晚白跑。改成按 maxId 推进。
        · 停止条件最初看"写了几行" —— 全量重算时绝大多数行算出来跟原值一样、不会被写,
          written=0  不等于做完了。改成看 seen / maxId,并把两者分开报。
      
      列**刻意可空**,三态有别:NULL=没算过 / '{}'=算过但推不出标签 / 非空=标签集。
       别加 NOT NULL DEFAULT '{}' —— 那样刷新任务再也找不到漏网的行。
      
      验证:tsc 通过;jest 86 套 1342 例全过(新增 12);eslint 干净;
        真 AppModule 起容器确认两个 service 都注入成功;
        回填 37,300 条/8.4s;全量重算 37,300 条/2.7s 未卡死;人为挖 25 个洞→补齐→剩余 0。
      luoqi committed
    • chore(基准): 加初选矩阵基准脚本 —— 为「标签落库」改造留一把可复用的尺 · 628d253a
      优化之前先把现状量下来,否则改完只能凭感觉说"快了"。
      
      脚本同时产出两样, 缺一不可:
        · **耗时**:EXPLAIN ANALYZE 的 Execution Time,每档取 N 轮**最快值**
           不取平均 —— 本机跑着别的东西,实测同一条件 1388~2130ms 都出现过,
            平均值被噪声主导,最快值才稳定可比。
        · **结果快照**:24 格逐格的数,拼成一行可逐字比对
          🔴 只比耗时是不够的 —— 把 join 改掉、把标签预计算,最容易的失败是
            **悄悄少算一批人**,而那正是产品最不能接受的
            (矩阵那段注释里写着「主管一对数就觉得系统在骗他」)。
      
      本地基线(15,884 plan 的诊所,与测试服 15,770 几乎同规模):
        15884 →  958 ms  · 磁盘读 119,861 buffers
         2458 →  127 ms
           14 →   11 ms   ← 地板
      
       顺带印证了一件事:本地 `patient_facts` 只有 111 万行(测试服 1567 万,1/14),
        但同规模诊所耗时同一量级(958ms vs 1399ms)。⇒ **表大小不是主因,查询次数才是**
        —— 15,884 个 plan 摊出的三万多次随机查才是,这正是「标签落 plan_reasons」要消掉的东西。
      luoqi committed
  2. 16 Aug, 2026 30 commits
    • merge: 助手会话 id + 多步文本修复 + 术语「时效」→「时限」全线统一 · 3f2c02fd
      · 会话 id 落 workflow_run_id —— 同一次对话各轮共用, 不再靠 turnNo 反推
      · outputText 拼全每一步 —— OnFinishEvent 只带末步,中间说的话原本会丢
      · 时效 → 时限:代码/界面 213 处 + 文档 71 处; 跳过语义不同的(话术语气、临床有效期)与 _archive
      luoqi committed
    • docs: 文档口径跟上「时限」—— 71 处; _archive 保持原样 · 9cd3cd5e
      代码/界面已统一到「时限」(6a1bee6f),文档站与设计文档还写着「时效」——
      不跟上就等于刚消掉的漂移又从另一头长回来。
      
      改 6 个文件 71 处:站点两篇(assignment-agent / batch-assignment)+
      设计文档四篇(doctrine / agent-flow / 两份 dev-plan)。
      
       `docs/_archive/` 11 处不动 —— 那是历史记录,改了等于篡改当时的原文。
      
      验证:pac-docs build 通过(OG 图字体拉取失败是内网够不到 Google Fonts 的老问题,非致命)。
      luoqi committed
    • refactor(术语): 时效 → 时限,全线统一 213 处 —— 但三类同名不同义的不动 · 6a1bee6f
      产品定的口径是「时限」(截止期限),而界面此前 63 处全写「时效」。
      提示词第②层的铁律是「使用者的词汇表 = 他在界面上见过的那些」⇒ 界面、提示词、
      工具描述、文档必须同时改,只改一处等于制造新的漂移。
      
      改了 35 个文件 / 213 处:界面文案 · 提示词正文 · 工具描述 · 枚举 labelZh · 注释。
       字段名一律不动(expiresInDays / set_expiry / assignment_expires_at)——
        那是接口与数据库契约,跟着中文改会连累落库与老数据。
      
      ️ 跳过 7 个文件 —— 那里的「时效」是**别的意思**,连坐会改出假语义:
        · tone.ts:「urgent = 有时效紧迫」是话术**语气**,不是期限
        · clinical-signals / persona-feature-specs / signal-extractor / contraindication:
          临床禁忌的「时效」= 有效期(妊娠 280 天、哺乳 180 天、放疗终身)
        (enums 里那条 `deadline_too_tight` 的注释早就警告过「同名不同义是统计事故的
          标准配方」—— 这次正是同一类风险,只是方向反过来。)
      
      提示词正文变了 ⇒ 按纪律 bump `ASSISTANT_PROMPT_VERSION` → assistant@2026-08-16-b。
      packages/types 改了 src ⇒ 重建 dist(否则 tsc 绿、一跑就炸)。
      
      验证:service tsc + web tsc 通过;jest 85 套 1330 例全过。
      luoqi committed
    • fix(助手留痕): 补上会话 id,并把多步文本拼全 —— 两处都是「看起来对、其实会丢」 · df0d263d
      ■ 会话 id(`workflowRunId`)
        上一版每轮各生成一个 run id,判断"哪几轮是一批"只能靠 `turnNo` 归 1 反推 ——
        那是启发式,️ 两位主管并发聊天时行是交错的,前端一旦裁剪历史 turnNo 还会错位。
        ⇒ 前端在**messages 为空**(= 一段新对话的第一句)时现生成 uuid,此后每轮原样带上;
          服务端落进 `workflow_run_id`,同一次对话各轮共用一个值。
        ️ 前端可改的入参:只用于归组, 不参与鉴权取数;非 uuid 一律丢弃(服务端另生成),
           别让它成为写库的注入面(该列是 uuid 类型)。
         这也正好落在产品设计上:没有"新建会话",会话跟着他此刻在做的那件事走 ——
          离开工作台、组件重挂,messages 归空,下一句就是新的一段。
      
      ■ outputText 只落了最后一步
        🔴 AI SDK 的 `OnFinishEvent` 继承的是**最后一步**的 `StepResult`,`ev.text` 只有末步那段。
        模型在工具之间穿插说的话(「我先查一下」)全在前面的步里,只取 ev.text 就永远不进记录 ——
        而排查"它当时到底说了什么"全靠这一列。
        ️ 线上实测那轮 5 次工具调用、6924 输出 token,落库只有 60 字 —— 那次凑巧没丢
          (它把话都留到了最后一步), 别指望每次都这样。
        ⇒ `joinStepTexts` 拼所有步,去重末步(它通常已在 steps 末项里)。
      
      验证:
        · tsc(service + web)通过;jest 85 套 1330 例全过(新增 5);eslint 干净
        · 真模型跑三轮:会话A 两轮共用 run=1cd20f63、会话B 独立 run=985bf1d3,
          按 run 分组正好 2 个会话 
      
      ️ 未做,留给产品定:界面 63 处写「时效」、0 处「时限」,而文档已改口径为「时限」。
        提示词的铁律是「使用者的词汇表 = 他在界面上见过的那些」⇒ 现在**该改的是文档或界面**,
         不能只把提示词单方面改成「时限」。
      luoqi committed
    • merge: 分配助手文档重排 + 助手调用留痕(agent_invocations) · fe79f0f3
      文档 25 个提交:《分配助手》按产品口径全面重排(决策树前置、意图分析、
      去内部黑话、编号撞车修正、代价三档)。
      功能 1 个提交:助手每轮往 agent_invocations 落一行(只存当轮, 不存上下文),
      外加保留期清理任务与成本计算共享化。
      luoqi committed
    • feat(助手): 每轮往 agent_invocations 落一行 —— 落「该调的工具调没调」,不落聊天记录 · ad264616
      助手是这条生产线上唯一**一次都没留过痕**的一层:验收判据写着「该调的工具
      调没调」,而线上没有任何地方记录模型实际调了什么,只能靠临时跑批。
      
      表和写入器都是现成的(`agent_invocations` + `InvocationRecorderService`),
      后台 `admin/ai-invocations` 也不写死 kind —— 落进去直接就能看。缺的只是接上。
      
      ■ 只存当轮, 不存上下文
        助手的 messages 里带着 propose_assignment 返回的整张确认单(几百人 ×
        十几个字段)。第 N 轮把前 N-1 轮全带上 ⇒ **落库量按平方涨**。
        实测口径:全量约 100 KB/行,只存当轮约 2.5 KB/行;按 1800 次/天算是
        65 GB/年 vs 1.6 GB/年 —— ️ 而测试机盘常年 90%+、PG 卷已占 75 G。
        ⇒ `slimInputSnapshot` 只取本轮用户原话 + 轮次 + 现场 id,
           不含历史、 不含任何工具返回值。同 agent-architecture「摘要 + 指针」:
          在源头就只给摘要, 不事后压缩(事后压缩要改历史,而改历史作废 KV 缓存)。
      
      ■ systemPrompt 一律不落
        主管那份 6.9 KB **每次一模一样**,逐行存等于把同一份东西抄一万遍。
        改用 `ASSISTANT_PROMPT_VERSION` 做锚 —— 🔴 改任意一层正文必须 bump,
        ️ 不 bump 这条审计链就断,而断了不会有任何报错。
      
      ■ 工具留痕装在一处
        在 tools 建好之后统一给**每个** execute 套计时包装(MCP 拉来的 + 本地那 7 个)。
         不在各自 execute 里写:漏一个,那个工具就永远不出现在验收数据里。
        落 output.toolCalls = [{name, ok, ms, args截200}], 不落工具返回值。
      
      ■ 落库绝不影响对话
        start/end 全程 try/catch 吞掉,只打日志;onFinish/onError 挂在 streamText 上。
        主管正等着一版方案, 不该因为审计写不进去而看不到结果。
        scope 缺失(内部入口)整条跳过 —— hostId/tenantId 是必填列。
      
      ■ 顺带补两个既有缺口
        1. 保留期清理任务(`InvocationRetentionService`,每天 04:00 沪)——
           schema 注释里写了一年「N 天后清 inputSnapshot 仅保留元数据」,任务一直不存在。
           清 inputSnapshot/prompt/systemPrompt/outputText 四样肥字段(占一行 95%+ 字节),
           留 token/成本/延迟/status/judge/userFeedback;失败行留 3 倍时长;
            不删行(成本与通过率曲线要长期可比,元数据行才 ~1 KB)。
           ️ 判据用 startedAt 不用 updatedAt —— 清理本身会刷新 updatedAt,
             用它当闸门的话清过的行永远追不上,每天全表重清一遍。
        2. 成本计算抽成共享纯函数 `ai/core/cost.ts` —— 它是**计价**逻辑,
           抄第二份必然漂(2026-08-13 栽过:换 qwen 旗舰后成本被低报约四倍)。
      
      本地验证(不只是单测):
        · tsc 通过;jest 85 套 1325 例全过(新增 27 例);eslint 干净
        · 真 AppModule 起容器 → recorder 注入成功、retention 服务已注册
        · 保留期在真库上跑通:60 天前成功行已清且元数据完好、2 天前未动、
          失败行未动、第二次跑幂等、185 行真实数据零误伤
        · **真模型跑通一轮**(deepseek-v4-flash):
          tokens 1720/158/1878 · cached 896 · ¥0.001169 · latency 23.9s ·
          finishReason "stop" · inputSnapshot 82 B( 无历史)· systemPrompt 未落
          ️ 先用 MockLanguageModelV3 试过,usage 拿不到 —— 是 mock 喂不进去,
            不是代码问题;真模型一次跑通。
      
      ️ 遗留:代码里 ASSIGNMENT_SCENE 仍写「时效」,产品口径已改「时限」,未统一。
      luoqi committed
    • docs(站点): 只删与图重复的,提示词和工具原样保留 —— 470 → 415 行 · edbbe4f5
      上一版(已回退)把 §2 提示词、§3 工具整节删掉了,那是砍错地方:
      产品要的是「能在图里说明的就不另开篇幅」,不是删掉这两节。
      
      删的全是与决策树图重复的表:
      - 《选人 = 主管初选 + 助手精选》整表 —— 图上 A1/A2 就是这个结构,
        只留下面那段「助手不等指令、先把方案做出来」
      - 《引导什么时候才冒出来》的「满足什么才出」列 —— 触发条件写在 G1/G2 节点上,
        表只留「为什么卡那个条件」(这一列图里没有)
      - 分人三趟表 —— 图上 B 节点已逐趟写清,只留水位公式和三条「为什么」
      - 「人手在两个地方摆给他看」两行表 → 一句话
      - 《③ 确认 · ④ 确认之后》表 → 两句话
      - §5「另有几位」的两局面表 → 一句话(该做的事正好相反)
      
      合并:
      - 《代价不对称》整节上提到图下 —— 它是读图的钥匙,本来就该紧跟着图,
        放章末等于让人看完全章再回头理解开头那两个环
      
      §2 §3 内容一句没删,只压掉工程细节的铺陈:
      - 四条硬规则的分条罗列 → 一行(判据那句引言保留,它才是要点)
      - 《只有主职责常驻》两段并一段
      
      八节结构不变。验证:pnpm --filter pac-docs build 通过。
      luoqi committed
    • docs(站点): 分配助手按「拿去讲」的标准全面校订 —— 修编号撞车、去内部黑话、拆代价三档 · af291464
      拿这份去做产品介绍前的一次完整审校,改动分三类:
      
      一、会当场被问住的(3 处)
      - 编号撞车:①②③④ 同时指四步、四个引导节点、五层提示词。
        原第 71 行「从②『这批多大』改」,而 ② 在同一页刚被定义成「分人」。
        ⇒ 圈码只留给四步;引导节点本来就有名字,一律改用「名字」引用。
      - 「格」仍在用且从未定义:产品早已定过不用这个说法。
        ⇒ 全部改为「候选人群 / 这批候选」,并在四问表之后补一句它是什么
          (治疗 × 多久没来 交叉出来的那批人)—— 后面所有平均、门槛都以它为准。
      - 代价表自相矛盾:表称「分人环 = 就地改」,下一句就承认「换无专属的补上」
        走重排。实际是三档(就地改 / 重排 / 重出),只写了两档。
        ⇒ 新表按三档写,并点明中间那档是唯一「长在分人环、代价却偏向选人环」的。
      
      二、结构与重复
      - §1 原「三处例外」摆在《召回策略》之前,却引用尚未定义的引导节点 ⇒ 下沉。
      - 原「两条闭合的环」与开篇「两个环的代价不一样」讲同一件事 ⇒ 合并,
        与「红色菱形」一起收成《代价不对称:什么时候重来,什么时候就地改》。
      - 原「后三步」里 ② 分人是上一节的重复 ⇒ 删节,唯一新信息(主管可微调)
        并入分人那节;该节改名《③ 确认 · ④ 确认之后》。
      - §2 原名「提示词怎么搭的」盖住了它更重要的第一节(登录时装配、无对象实例)
        ⇒ 改名《这个助手是怎么装出来的》,下分「装配」「提示词分层」。
      
      三、文风与用词(要拿出去讲)
      - 术语统一:「自由患者」⇒「无专属患者」(全文其余处一直用后者)。
      - 去口语/网络语:层层套娃、甩锅、白付上下文、旋钮、拖一下、桶名、一刀切光。
      - 去代码术语:「按客服 id 打破平局」⇒「按固定顺序打破平局」。
      - 代词歧义:开篇「助手…出确认单。他只要看一眼」的「他」紧跟「助手」;
        §5 标题「要他定的事」无先行词;§8 结句「他在做的那件事就是它存在的理由」。
      - 引号统一为「」;mermaid 标签内的半角标点改全角(已在浏览器验证可渲染)。
      -  由 40+ 降到 26 —— 只留红线表与真禁令,句中当「不」用的去掉。
      
      事实层复核(未改动,均与代码一致):
        DAILY_CALLS_PER_AGENT=15、NARROW.MIN_COHORT=50 / MIN_COUNT=10、
        工具 9 通用(MCP 8 + render_artifact)/ 12 主管(MCP 6 + 助手侧 6)= 21。
      
      验证:pnpm --filter pac-docs build 通过;本地 3102 实测两张 mermaid
      均渲染(680×1582、680×158),零 syntax error。
      luoqi committed
    • docs(站点): 新增 §8「跟一般的助手不一样在哪」—— 八条产品选择,不是技术选择 · 64f7f351
      产品提到「没有新建会话功能」这条设计考量,并让我补齐能想到的。核过代码确认:
      `messages` 就是组件状态、助手挂在 plans/layout 上、全仓找不到任何
      新建会话/清空/历史列表的代码 —— **它确实不是一个聊天产品**。
      
      八条(每条都配"如果按常规做会怎样"):
        · 会话:没有新建、没有历史 —— 让他管理"会话"等于给他一件本来不存在的活
        · 搞不清时:**不追问,直接出一版** —— 追问一轮=他等一轮,
          而**一版具体的方案本身就是最好的问题**
        · 让他选择:摆成按钮,且按钮和说话走同一套动作 —— 打字要过"理解→翻成动作"
        · 输出:**模型自己排版**,卡片落在正文哪一句之后由它调工具的位置决定
        · 与界面的关系:界面显示过的不再说 —— 读两遍他就开始跳读
        · 数字:一个都不许自己产生
        · 兜底:**默认永远安全** —— 一条不点直接确认,任何情况下都不出事
        · 验收:看多轮通过率, 不是"跑通一次"
      
      🔴 收在一句上:**助手是工作台的一部分,不是工作台旁边的一个聊天机器人。**
        它没有自己的"产品面"—— 没有会话管理、没有历史、没有设置。
        他在做的那件事就是它存在的全部理由。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 装配那节按 agent-architecture §六 的深度重写 —— 补上判据,不只描述现象 · bfd92b98
      产品指出我写的不如 `docs/design/agent-architecture.md` §六。对比之后确实:
      我只写了「按权限装出角色/现场/工具」这个**现象**,而那份写的是**判据** ——
      为什么必须这么装、不这么装会怎样。照它的骨架重写。
      
      补进来的四块:
        · **哪几维必须按身份分**:会话、工具清单(看不见比看见被拒更安全)、数据范围
          (下推到查询条件, 不是查全量再过滤)必须分;系统提示词分**但这是最不重要的一条**;
          模型和代码实现不用分
        · **为什么同一会话不能切换身份**:上下文是**单向**的,高权限会话里已载入的数据
          不会因为一句「你现在是低权限角色」而消失 ——
          **提示词不是删除操作,也从来不是安全边界**
        · **「两个 agent」要拆开**:同一套实现按身份参数化; 这不是「多 agent 编排」——
          编排指 agent 互相调用,而不同身份之间不需要通信,真要协作走业务对象
        · **四条硬规则**(身份随调用传递 / 授权在工具内部 / 数据范围下推 / 工具清单按身份下发),
          连同那条判据:**如果模型不传某个参数,越权就不可能发生 —— 那这个参数就不该是参数**
      
      ️ 保留我原来那个推论(它没有"记性" ⇒ 动过手之后必须重新看那张单),
        那条在原文里没有,而它正好解释了「看当前确认单」为什么存在。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 补上「助手是登录之后现装出来的」—— 它是理解整个 §2 的前提 · b2de3308
      产品指出文档没讲 agent 是怎么形成的。代码里 `buildSystemPrompt` 就是一个纯函数:
        权限位 → ④角色 / ⑤现场 / 工具清单,①②③ 与谁登录无关
        return [IDENTITY, EVIDENCE, voice, role, scene].join('\n\n')
      **没有 agent 对象、没有实例、没有状态** —— 每次请求现拼一份字符串,工具也按权限现注册。
      
      ⇒ 补一节讲清三个后果:
        · 换个账号登录**它就是另一个助手**, 不是"同一个助手换了套权限"
        · **不登录就是一块白板**(现在没这种场景,但结构上就是如此)
        · 加一条新业务线 = 多一个 ⑤,前四层一个字不动
      
      ️ 并点出一个平时容易忘的推论:**它没有"记性"** —— 不是存着状态的对象,
        所以主管在确认单上动过手之后,助手**必须重新去看那张单**,
         不能拿出方案那一版的数接着算(这正是「看当前确认单」那个工具存在的理由)。
      
      顺带删掉原来那条「一个开关驱动三样东西」的 Callout —— 新那节已经把它讲全了。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 把方法论和清单分开 —— 三问是「描述怎么写」,清单只做摘要 · d8b15e3c
      上一版我把方法论塞进了清单里(每个工具都写满三问),产品指出搞反了。
      
      **方法论**(描述该怎么写)—— 一条工具描述要答三件事:
        · **干什么**:一句话说清它回答哪个问题
        · **什么时候用、 什么时候别用**
        · **怎么用**:参数怎么填、跟哪个工具配套
        🔴 点明「什么时候别用」是最值钱的那条:**一个工具能干什么,看名字就猜得到;
          什么时候不该用,只能踩出来** —— 几乎每条背后都有一次实测事故。
        ️ 两条硬边界(都踩过):
          · 返回值只给事实、 不给成品句子 —— 某个返回值混了给模型的指令,
            模型照抄,内部指令原样贴进主管的对话框。护栏写成规范, 不写成台词。
          · 描述里 不留**正面举例** —— 写了「他会说『只要商保直付的』」,
            模型把这个例子逐字念给主管,而那版数据里根本没这一类。反例可留,正例不留。
      
      **清单**:21 个工具各一句话, 不再重复三问。
      
      MDX 编译通过。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 工具清单改成「干什么 / 什么时候用 / 什么时候别用」,不再只是目录 · 766a6075
      产品指出上一版是**目录不是解释**。⇒ 21 个工具逐条重写,每条三件事,
      内容全部从**真实工具描述**里提炼(不是我另编一套)。
      
      几条值得单独看的「别用」——它们背后都有实测事故:
        · 我是谁 ——「我能不能做某件事」 不用问它:做得了的事工具就在手上,
          做不了的那个工具压根不会出现
        · 看人手 ——  出方案**不需要**先调它(方案自己会取名册);
          曾抓到模型写字前白调一次、返回的数一个没用
        · 看当前确认单 —— 他在卡片上动过手之后**必须先调它再报数**:
          卡片改动助手看不见, 拿旧版的数去算就是错的
        · 摆确认单 ——  算出来了也别急着调:先讲怎么排的,明细是给他核对那几句用的
        · 摆引导 ——  别攒着一起调,前一类的按钮会落到后一类的说明后面
        · 撤销 —— ️ 唯一会改数据的工具,**试探性调一次没有"预览",那一次就是真撤**
        · 召回池名单/概览 —— 没有看全池权限的人拿到的只是自己名下那些, 别说成"整个池子"
      
      ️ 末尾点明:这些「别用」几乎每条背后都有一次实测事故 —— 它们是这套工具
        最值钱的部分, 不是凑数的注意事项。
      
      MDX 编译通过。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 工具那节前移到提示词之后,补设计思路 + 21 个工具逐条列出 · 8bf1fe0f
      按产品要求:位置移到 §3(提示词之后),补上**工具设计的思路**,并把工具**全部列出来**。
      
      **设计思路(从代码里的判定提炼,不是新造):**
        🔴 **能靠结构解决的, 不靠叮嘱。** 模型会说出 `cold_3y` `implant` 这类内部码,
          主管看到不认识的词第一反应是系统坏了。最初在提示词里写两页「 不许说码」——
          **拦不住**,因为它调工具时必须拿码当参数,回话自然带出来。
          真解法是**返回值里就给中文** ⇒ 它手里有话可说,就不会去说码。
          「能让它说不出来的,就别写成不许说」——提示词里的禁令只拦得住预想到的那些。
      
      **工具描述怎么写(四问,例子全取自真实描述):**
        它答什么问题 / ️ 哪里容易误读 /  什么时候不该用它 / 配套的那个工具是谁
        两条硬边界:返回值**只给事实不给成品句子**(否则模型照抄,内部指令原样贴进主管对话框 ——
        踩过);中文标签只有一处真源, 不在工具层另立一套。
      
      **全部工具**:两种人都有 9 个、只有主管 12 个,逐条一句话说清干什么。
      ️ 数字是从代码数出来的,不是抄旧文:MCP 通用 8 + render_artifact = 客服 9;
        MCP 全部 14 + 本地 7 = 主管 21。与文档原有的 21/9 一致。
      
      后续章节顺延(三者分工→四 / 引导→五)。MDX 编译通过。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 加载机制那段砍到一句话 —— 产品层面只有「主职责常驻,其余留一行索引」 · 44d4cfc6
      产品判断:三种加载方式属于实现细节,文档里说清"要不要在常驻里留索引"就够了。
      ⇒ 表格和「为什么必须分开」整段删掉,留三句:
        · 常驻的成本是**所有业务线常驻之后的总和**,而规则越多每条被遵守的概率越低
        · 所以只有主职责常驻;次要的活写成单独一篇,常驻里只留一行索引
        · 实测:问"前面那两批怎么样了",模型第一步就自己去取了做法
      
      ️ 自查:上一版我把 `_guide` / `open_playbook` 的机制差异写了满满一节 ——
        那是**实现**,不是产品设计。读者要知道的是"次要业务线不常驻",
         不是"它用哪种方式送到模型手上"。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 去掉 push/pull 这两个词,改用「什么时候用得上」讲清楚 · 528c4c72
      产品读不懂 push / pull —— 那说明这段没写好。那两个词是我从实现里搬来的
      (`_guide` 挂返回值 / `open_playbook` 让模型自己取),**不是产品语言**。
      
      差别其实只有一条:**这段知识是「看到数据之后」才用得上,还是「决定动手之前」就得有。**
        · 看到数据之后才用得上 → **跟着那份数据一起送**(查完批次详情,返回值里就带着读法:
          处理率≠成功率、退回率要给两个分母、样本不足 50 不给百分比)
        · 决定动手之前就得有 → **让它自己去取**(「分配追踪这活怎么干」——
          它得先知道查什么、按什么顺序查,才谈得上动手)
      
      为什么必须分开也写了:跟着数据走那种最便宜(不用主动要、必然看到、用不到零成本),
      但**只能在调完之后到达** ⇒ 凡是「要不要调、按什么顺序调」的知识跟着数据走就永远晚一步。
      这个边界在 `guides.ts` 里本来就写着,只是文档没讲出来。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 提示词那节按 artifact 重写并前移到决策树之后;按当前代码对齐 · ab00afda
      素材来自 artifact《主管侧助手系统提示词 · 分层校对》(2026-08-15,那份正文是跑
      `buildSystemPrompt()` 拼出来的真件)。产品要求:放决策树后面、去掉多余描述,
      **只留设计思路 + 分层**。
      
      留下的:
        · 分层判据 =「什么会让它变」,装配顺序 = 通用 → 特殊(就是模型的阅读顺序)
        · 五层表(装置 / 诚实 / 人设语气 / 角色 / 现场)+ 各自的变更原因与篇幅占比
        · **一个开关驱动三样东西**(④角色、⑤现场、工具清单)—— 换账号三样一起换,
           不会出现「给了主管的话术却没给主管的工具」
        · 三种加载方式(常驻 / push / pull)+ 为什么只有主职责常驻:
          常驻的成本是**每条业务线都常驻之后的总和**,而规则越多每条被遵守的概率越低
      去掉的:提示词全文、字数明细、待办清单 —— 那些属于校对稿,不属于产品设计文档。
      
      ️ **按当前代码重新对齐**(artifact 是昨天的快照,这两天改过):
        · 篇幅占比用今天的常量重算(9/20/32/7/32), 没照抄昨天那份
        · 「当前登录人」那行昨天已删,artifact 与代码一致,不再提
        · artifact 里的「无主」现在是「无专属」(见前一提交),文档统一用后者
      
      后续章节顺延重编号(三者分工→三 / 引导→四 / 工具→五)。MDX 编译通过。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): ④ 同步「另有 N 位」,并写下「只给一个带数的选项」的理由 · 67947be0
      f8fd1ee5 的代码改动对齐:
        · 图里 ④ 补「报最忙那位 + 另有几位也超」
        · 表里补上那半句实际文案
        · 说明为什么要报这个数:只说最忙一个,主管分不出「只有他超」和「全队都超」——
          而这两种局面该做的事相反(前者给他少分点/改派,后者减量/延时效)
      
      ️ 顺带把**只给一个选项、且必须带数**这条纪律写进文档(产品这次再次确认):
        「整批时效改成 3 天」那个 3 是按最忙那位的量算出来的。
        曾经还有「改每人每天打几通」「减少本批人数」两个,删掉了 —— 它们一个数都不带,
        点下去等于替主管说了句「减少一些」。**一个不带数的按钮,严格弱于他自己开口说一句。**
      
      MDX 编译通过;mermaid 无悬空。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • feat(引导): 「最忙的那位」补上「另有 N 位也超时效」;修一处注释与 SQL 打架 · f8fd1ee5
      **① 只报最忙一位,分不出两种相反的局面**(产品定):
        · 只有他一个人超 → 该给他少分点 / 改派几个
        · 全队都超       → 该减少这批 / 延长时效
        同一句话、相反的处置 —— 而本节点唯一的选项是「整批时效改成 N 天」,
        碰上第一种局面它本身就是错的(为一个人的负载去延长整批时效)。
      ⇒ why 里补「另有 N 位也超过 X 天;每位分完之后要打几天,确认单上逐位都写着」。
      ️ 只加一个数 + 一句引路, 不在引导里铺开每个人:确认单每行已经有「约 N 天」,
        分布本来就在眼皮底下(引导节点的职责是点出要他定的事,不是展示数据)。
      ️ 只有一个人超时那半句**不出现** ——  不制造无谓噪音。
      📌 加在**工具产出的数据**里(`daily_overload` 的 why), 没动提示词 ——
        模型从返回值里直接读,不需要被提醒(工具返回值 > 提示词)。
      
      **② 团队面板那段注释与 SQL 打架**:注释把「超期」和「在手」并列写成"与窗口无关",
        而 SQL 里是 `assignment_expires_at >= since` —— **SQL 是对的**:
        一年前过期的单报上来对主管没意义,他此刻能处置的只有近期这批。
        ⇒ 改注释、 别照注释去掉 SQL 的窗口,并把理由写在旁边。
      
      新增 2 条测试(多人超时 → 报数并引到确认单;只有一人超 → 那半句不出现),1298 全绿。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 补上「专属排满了」漏掉的第四个选项,并接回第三趟 · dac5e781
      产品指出图里那条引导缺一项。核了代码,`pending` 节点其实有**四个**选项,
      我只列了三个 —— 漏的正是代价不同的那一个:
      
        · 换无专属客服的患者补上   ← 漏了。️ `canRefill` 为真(池子里还有无专属的人)时才给,
                                      而且它换的是人 → 走 refill 重排, 不是就地改
        · 铺平给在岗 / 各自归专属客服 / 移出本批
      
      顺带把这条引导和落人第三趟接起来:**待分配那一组就是第三趟排不进去的人** ——
      原文两处各说各的,读者看不出是同一批人。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 分人写清楚 —— 目标水位 + 三趟,不是"谁空给谁" · 0f91e390
      产品指出图上那三行没说清分法。去 `placeAgents` 核了算法,原文漏掉的正是**最关键的水位**:
      
        目标水位 = ⌈(团队现有在手总量 + 本批人数) ÷ 在岗人数⌉,至少 1
      
       它**不是新旋钮**,完全由「这批多大」推出来;存在的唯一理由是**给第一趟封顶**。
      
      三趟(顺序本身就是设计):
        一趟 有专属的回自己人 —— 但到水位就不再给
        二趟 无专属的补给当前手上最少的(这一趟才是"最少优先")
        三趟 专属这轮排满的单列成一组, 不自动改派
      
      三条理由都写进去了,都是代码注释里记着的实测/判定:
        · **为什么必须三趟**:二、三趟都是水位法、总量一样,但**拆散的专属关系数不一样** ——
          先用无专属的补空手的人能少动一个有主患者;合成一趟就会随机改派。
        · **为什么第一趟封顶**:本地实测池子 1,081 人里 755 人(70%)挂同一个客服,
          不封顶他一批拿 248 条,而 17 位在岗里 10 位名下一个患者都没有。
        · **为什么第三趟不自动改派**:把患者从专属客服手里挪走是**关系层面的决定,
          助手没资格替主管做**;这些人不是被丢掉,是原地不动交他定。
        ️ 另补:同水位按客服 id 打破平局 —— 同样输入两次必须算出同样的分法,
          这是主管敢按确认键的前提。
      
      MDX 编译通过;mermaid 无悬空引用。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • fix(站点): 多段 Callout 的开闭标签要各自独占一行,否则 MDX 编译失败 · 943336ff
      Build Error: `Expected a closing tag for <Callout> before the end of paragraph`。
      
      原因:写成 `<Callout type="info">正文…` 再跟空行时,MDX 把它当成**段落内的行内 JSX**,
      要求在同一段里闭合;而中间的空行已经结束了那个 paragraph ⇒ 报"缺闭合标签"。
      ️ 同文件另外两个 Callout **没**报错 —— 它们内部是软换行、没有空行,属于同一段,合法。
         所以这个坑只在"想写多段落 Callout"时才踩到。
      
      ⇒ 改成开闭标签各自独占一行、与正文之间留空行,并在文件里就地留一段注释说明。
      
      验证:用工作区的 @mdx-js/mdx 直接编译,本文件通过;顺手把 content/docs 下
      全部 36 个 mdx 都编了一遍,无一失败。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • fix(引导)+docs: 撤掉「候选够不着 N 就不出这批多大」那道闸;四问补上深层理由 · 939a2fb8
      **① 代码:那道闸的理由只成立一半**(产品指出)。
        原判据 `candidateTotal > batchSize`,注释理由是「候选 312 / N 495 时往上调 target 还是 312,
        点了没反应,而 why 说人数会变 ⇒ 说谎」。️ **那只考虑了往上调** —— 主管同样可以往下调:
        把每天几通 15→5,N=165 < 312 ⇒ target 真的变成 165。他想少发一些,而这道闸把入口藏了。
        ⇒ 闸撤掉,改成**据实说明**:候选够不着 N 时 why 直接讲清方向
          「符合条件的只有 N 人,已经全在本批里了 —— 往上调不会更多,往下调可以少发一些」。
         别再拿"点了没反应"当不出的理由 —— 该修的是那句话,不是把节点藏起来。
        两条测试跟着反转:原来锁"不出",现在锁"两种局面都出且措辞据实";1296 全绿。
      
      **② 文档:四问补上深层理由**(产品指出"分析过的没写上,缺说服力")。
        1 治疗 —— 诊所这季度的经营重点,医生排期/设备耗材/话术都围着它转,**只有他知道**,
          系统里没有任何数据能推出来
        2 多久没来 —— **他能估出回来多少**,而这个估算往下游一步就是诊所排班:
          要不要给医生和科室提前留位。**捞回来了却没人接诊,比没捞更伤客户** ⇒ 必须他拍
        3 高价值 —— **他答不了**:不看数据不知道这批里有多少高价值,也不知道切完还剩几个
          (可能一刀下去只剩三个人)
        4 团队负载 —— 捞太多打不完、到期回池等于白发一轮;而"还吃得下多少"的数在系统里,
          不在他脑子里
        ⇒ 「1、2 问他就有;3、4 问他他也说不出」—— 选人要拆两半的理由,到这里才立得住。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 决策树的两个环只画引导节点,触发条件写进图里 · d253f173
      产品指出:边上那串「加条件 · 改这批多大 · 换治疗项 · 换多久没来」混了两类东西 ——
      **换治疗项 / 换多久没来 / 直接说改成 200 人是自由输入**,不走按钮、靠助手听懂 + 调工具,
       不该画在这张图上。图上只留**引导节点**,并把每条的触发条件写死。
      
      两个环现在各挂两个节点(全部去代码核过阈值):
        选人环(重出,几十秒)
          ① 能加个条件 —— 这一格 ≥50 人 · 他还没加过 · 切完还剩 ≥10 人
             消费高于本格平均 / 转介绍达人 / 权益身份 / 获客渠道
          ② 这批多大 —— 人数是系统估的 · 且候选 > 本批人数
             可改 **每人每天几通 · 时效几天**(此前图里完全没提这两个可调量)
        分人环(就地改,同步)
          ③ 专属排满了 —— 这一版真有人排不进去
          ④ 最忙的那位 —— 分完后的量 > 每天通数 × 时效
      
      🔴 顺带纠正两处我先前写错/漏写的:
        · **「改时效」有两条路,代价不一样**:从②改会连人数一起重估(重出);
          从④改只延长这一批的期限、人不变(就地改)。前端注释里 `basis.set` 走
          「让模型重出一版」而 `expiry.set` 是「局部改单、同步」,两者本就不同路。
        · **「换无专属客服的患者补上」挂在③下面,但它换的是人** —— 走 refill 重排(几秒),
          是四个节点里唯一"长在分人环、代价却在选人环"的选项。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 「加条件」如实写清四条 + 补上引导的出场规则 · 59ebef9c
      产品指出原边标签「换条件 · 改人数 · 换时间档」不如实。去代码核了一遍,三处要改:
      
      **① 是「加」不是「换」**,而且加完是**重新选一版**(assignment-signals 的 why 原话:
        「点哪一条都是重新选一版……不是在已经排好的那些人里再挑」)。按钮上那个数字是
        **换完之后这一格还剩多少**, 不是"从本批里筛掉几个"。
      
      **② 四条条件全列出来**(此前只写"换条件"三个字):
        · 只选消费高于**这一格自己的平均**的 ——  不是全库分位(全库 ¥4,442,某格实测 ¥0 起,
          用全库的数会一刀切光)
        · 只选转介绍达人 —— 推荐 ≥3 人且带来成交,家庭型/社交型合并成一条
        · 只选某个权益身份的 —— 取这一格里人最多的那一项
        · 只选某个获客渠道的 —— 同上
        + 兜底「按别的条件选」:助手把其余十几个维度各多少人报一遍,他再挑
      
      **③ 补上引导的出场规则**(整篇此前一个字都没有):
        · 专属排满了  —— 真有人排不进去才出(唯一"不处理就真漏人"的一条)
        · 能加个条件  —— 这一格 ≥50 人 · 他还没加过条件 · 切完至少还剩 10 人
        · 这批多大    —— 人数是系统估的 **且候选 > 这批人数**
        · 最忙的那位  —— 最忙的人分完后的量 > 每天通数 × 时效
      🔴 「这批多大」那条的门槛值得单独写:候选够不着 N 时,改时效/改通数点了**不会有任何反应**,
        而它的说明写着「人数会跟着变」—— 那就成了说谎。所以候选 > N 才出。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 助手精选把默认值摆进图里;两个判断点改口语,去掉内部行话 · e57d5a2a
      **① 默认值要写出来(产品指出)**:原图只说助手"出一版",没讲**它是拿什么默认值出的**。
        主管其实只说了 1、2 两问,剩下的全是默认:
          · 谁排前面 —— 没联系过的排前面,其余按优先级从高到低
          · 每人每天 15 通 · 时效 1 天
          · 这批多大 = 在岗人数 × 15 × 时效
        ⇒ 图里补一段,正文补一张「默认的是什么 / 取值 / 他想改说一句就行」的表。
      🔴 配一条纪律:**默认值一个都不许藏**。一个他看不见的默认值,等于系统替他做了一个
        他不知道的决定 —— 而这批人是真发下去了。摆出来他才有得改; 不摆,他连
        "原来还能改这个"都不知道。(出方案时那个式子摊开写,就是这条的落地。)
      
      **② 去掉内部行话(产品指出)**:两个判断点原来写「待定的都摆成了按钮」——
        「待定」「摆成按钮」是我们内部说法。改成主管视角的话:
          「这批人对吗?要改哪一项,点一下就行」
          「这么分行吗?要调谁,点一下就行」
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点)+fix(措辞): 选人/分人画成两个各自闭合的环;「无主」统一改成「无专属」 · da078f8c
      **① 决策树的缺陷(产品指出)**:原图把两个阶段的调整并成了**一个环** ——
      「选人 → 分人 → 统一问一次要不要改」。这抹掉了两件事:
        · 「选人定下来」这个关口不见了 —— 图上看不出"人先定,才轮到怎么分";
        · **两种调整的代价被画成一样的** —— 实际上改选人是**整版重出、连已经排好的
          分法一并作废**,改分人只在这一版上动、人不变。
      ⇒ 改成两个各自闭合的环,中间用粗箭头标出「人定了 —— 这一关过了才谈怎么分」,
        并配一张表把代价差写死。顺序不能颠倒:先排分法再让他改人群,那趟排班就白做了。
      
      **② 用词统一(产品指出)**:「无主」→「无专属」。
      ️ 界面上的按钮文案**本来就是**「换无专属客服的患者补上」,而这三处模型会照着念给
        主管听的文本还写着「无主」—— 两处不一致,他会以为是两拨人:
          · assignment-facts 的「落人规则」(modelFacts,模型常原样引用)
          · assistant-prompts / lab.controller 的落人规则那一句
          · assignment.controller 里「换无主患者补上」的工具描述
         纯措辞替换,不动语义。代码注释里的「无主」没动(内部行话,不进主管视野)。
      
      1296 个测试全绿;mermaid 节点引用与 style 目标均无悬空。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 「选人」拆成召回策略 → 主管初选 → 助手精选(产品定的叙述框架) · 0b0acba4
      产品指出:选人不是一次动作,要**从主管的运营意图出发**拆开讲 —— 而且叙述该
      「先列出要回答的问题,再说谁答、凭什么答」,这才是产品思维。按这个重写 §1。
      
      **召回策略(在选人之前的一环)= 一批召回要回答的四个问题**
        1 做哪一类还没启动的治疗   → 主管答,凭这季度的经营重点
        2 找多久没来的人           → 主管答,凭经验
        3 要不要只挑高价值的       → 主管定,但**助手先摆数据**
        4 团队还吃得下多少         → 主管定,但**助手先摆数据**
      
      ⇒ 1、2 是运营意图,他张口就来;**3、4 他答不了** —— 不把全景摆在面前,
        「要不要只挑高价值的」根本没法回答:他不知道这批里有多少是高价值的,
        也不知道切完还剩几个。所以:
          **选人 = 主管初选(1、2)+ 助手精选(3、4,并顺手给出一版已经分好的方案)**
        助手在这一步 不是"等下一个指令",而是先把该看的摆出来、把方案做出来。
      
      🔴 **补上「多久没来」的真正分量**(产品点出,此前整篇都没写):
        它不只是筛选条件 —— **有经验的主管能从它估出这批大概能回来多少**
        (一两年没来的和三年以上的,回头率不是一个量级),而这个估算往下游一步
        就是**诊所的排班**:预计回来多少人、多少要做种植,医生和科室要不要提前留位。
        ⇒ 这一项必须由主管拍, 系统不代劳也不推荐。
      
      决策树相应改成 召回策略 → 主管初选 → 助手精选 → 分人 → 确认 → 确认之后;
      重跑那条环回到「主管初选」。mermaid 节点引用与 style 目标已校验无悬空。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): ① 选人补上「团队还吃得下多少」;顺带修掉一处文档与产品的漂移 · 0ffc8d40
      产品指出:主管在 ① 里还有一项很实际的顾虑 —— **团队当前负载**。人捞太多客服打不完,
      到期回池等于白发一轮。原文只写「这批发多大 · 给几天」,既是术语,也漏了他真正的顾虑。
      
      ⇒ 改成「团队现在还吃得下多少 · 几天内打完」,并新增一节讲人手在**两个地方**主动摆给他看:
         · 出方案时:这批多大按「在岗人数 × 每人每天几通 × 时效」估,式子和每个数都摊开
         · 确认单上:每位客服一行「分完之后约几天打完」,算的是**分完后手上的总量**
           (在手 + 本批), 不只是本批那几条
      
      🔴 **顺带修掉一处文档与产品的漂移**:文档把那个引导节点叫「打不完」,而产品里它的
        真实措辞是「这批发下去,最忙的是王强:手上共 45 条,约 3 天的量」—— **只报数,不下判断**。
        文档那个名字自己就违反了它下一段在讲的纪律。四个节点全部按代码里的真实措辞重写:
        专属排满了 / 还能再收窄 / 这批多大 / 最忙的那位。
        ️ 并补一句点明: 不说「打不完」「人太多了」「建议减到 200」—— 那是替主管做决定。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(站点): 决策树改用运营语言 —— 「① 选人」讲的是主管的召回策略,不是界面操作 · 885152ea
      产品走查:原文写「主管在矩阵上点一格」「这一格还能再切」「就按整格来」——
      **这是界面操作术语,不是产品设计语言**。看这篇的人要知道的是主管在做什么决定,
      而不是他的鼠标落在哪。
      
      ⇒ ① 选人那一步改写成他真正在决定的四件事:
         · 做哪一类还没启动的治疗(种植 / 正畸 / 拔牙…)
         · 找多久没来的人(三个月内 … 三年以上)
         · 要不要再收窄到高价值的(消费额 / 商保 / 意向)
         · 这批发多大、给几天时效
         ② 分人相应改成「谁去打这些电话」。
      
      ️ 补一条 Callout 点破两步的性质:**①是运营策略,②是排班**。主管脑子里是
        「这个月主推种植,把一两年没来、消费额够的那批捞出来」, 不是在操作界面 ——
        界面只是把这句话变成可点的形状。
      
      ️ 主管的**引语保持口语**(「只要商保直付的」「换成一两年没来的」)—— 那是他真会说的话,
        正是助手要听懂的输入, 不该改成书面语。改的是叙述语言,不是他的语言。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed