- 15 Aug, 2026 1 commit
-
-
提示词按「什么会让它变」分五层(装置/诚实/语气/角色/现场),现场段按 选人 → 分人 → 他确认 → 确认之后 四步分节 —— 每条规矩站在它生效的那一步里, 位置本身就是适用范围。次要业务线(分配追踪)走 pull:系统提示词只留索引, 模型用 open_playbook 按需取正文。 实测抓到并修掉的: - 「在岗 N 人 × 每天几通 × 时效」印的是 byAgent.length(本批分到人的客服数), 实测印出「在岗 9 人」而主管同屏写着 33 位在岗,9×15×1 连它自己那行的 「本批人数 214」都算不出来 ⇒ 新增 rosterCount,由算 batchSize 的同一行赋值。 - 「换无专属客服的患者补上」不回传 narrowedBy,主管收窄过之后点它会悄悄 退回整格取人(实测 2178 vs 296,差 1882 人)⇒ 链路补齐 + 按钮自己报范围。 - 确认单的草稿态服务端查不到(改派/移出/时效/福利只在浏览器里),模型只能把 对话里几行回声自己累加,而手动拖动/点×/改时效这几条路连回声都没有 ⇒ SheetSnapshot 随请求捎来 + get_current_sheet(拉) + 回声带当前值(推)。 - 「没分到的客服」只给了个数字,主管问「那 1 位为什么没轮到」答不上来 ⇒ 补他们的在手量(实测 193 对 49,对比自己说清)。 - 模型讲区间写 8~9,GFM 单波浪号当删除线吃掉 → 屏幕上是「89 条」 ⇒ 渲染层 singleTilde: false(与 range() 拆两个键是同一条纪律的两半)。 - daily_overload 里两颗不带数的按钮(改每天几通/减少本批人数)连同两个 intent 删掉:点下去替主管说的话也不带数,那个数只能由模型自己拍。 - 福利改判:确认之前问一句(客服一确认就开始打这批单),确认单里不再摆引导节点。 模型对齐:qwen 统一 3.8-max;计划摘要跟另外三个摘要走同一个模型(裸键 'qwen'), schema 的硬长度约束改 describe 软引导 —— 否则 qwen 写得短必然 too_small (判据同 draft_plan_script v27)。 界面:助手会话按 Claude Code 的排布重做(无边框头部、白底、底部工具栏、 思维链/工具步骤可折叠),确认单卡片与引导节点跟着四步走。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed
-
- 12 Aug, 2026 14 commits
-
-
⭐ 只留「倍数 ≥2」那一条的话,「一般发展 69% vs 48%」这类大基数偏移全部打不着 —— 这就是加百分点判据的实证,记下来免得以后有人把它当冗余删掉。 结果有明显规律,可作为判据可信的旁证: - 2-3 年那一列几乎全是「低活跃」(4.6x / 6.2x / 5.0x) - 3 年以上几乎全是「重要价值」+「口碑客户」(种植 60%、牙周 83%) → 沉睡的恰恰是高价值的那批 - 早矫/正畸的 3 年以上是「儿牙会员」(81%),买了年卡然后没再来 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
🔴 本地实测发现「倍数 ≥2」**结构上打不着大类**: 朝阳公园的获客渠道是 口碑 33% / 集团营销 23% / 走入 22% —— 口碑要够 2 倍得占到 66%(现实里不可能);而能翻倍的小类(电商 2.3%) 在一格 71 人里连 10 个人都凑不出 ⇒ **整个维度永远不会命中**。 ⇒ 命中改成两条取其一:倍数 ≥2(抓小基数聚集) 或 高出 ≥12 个百分点(抓大基数偏移)。 措辞跟着口径走:大基数说"高出 N 个百分点",小基数说"约 N 倍"; 排序按可比的偏离强度,⛔ 不能只按倍数(那会让百分点命中项永远垫底)。🔴 修一处内部码泄露:点「只圈这些人」时替主管说的那句话是 「这批只圈符合『referral_champion:yes』的人」—— 内部码原样进对话框, 正是 P0 花一整节要消灭的东西。改成中文名 + 人数(「只圈『口碑客户』那 62 人」),⚠ ️ 拿不到中文名就**宁可不发**,⛔ 不许退化成把码念出来。 人数带上是因为「圈完还剩多少」正是主管点这一下想知道的事。 获客渠道补成 hidden 可筛维度(闭集 9 项,data.channel); 折扣锚点**不纳入** —— 它是连续量没有闭集,要用得先定分桶(产品判断)。 补一行画像亮点的 trace:没有它,「算了但没命中」和「压根没算」在日志里 长得一模一样(实测就吃了这个亏)。 1247 + 29 passed;web 20 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
① 时效默认 1 天(产品定): N = 在岗 × 每天 15 通 × 时效,D=1 正好是**一天的量** —— 当天没打完的自动落回池子、明天重排。朝阳公园实测:765 → 255。
⚠ ️ 连带后果(已知并接受):daily_overload 阈值变成 15×1,只要谁手上还有旧单 就会亮。那不是 bug,它是工作量预估,每批本来就该讲清楚。 ② 引导节点在卡片上**只渲染标题 + 按钮**:🔴 2026-08-12 浏览器实测:模型那段就在卡片正上方,两边都写全 —— 「72 人的专属客服这轮已排满 / 他们有专属… / 不处理 = 这批不发给他们」 主管要把同一件事读两遍。 ⇒ 分工:**卡片管动作,对话管解释**。why / defaultLabel 仍随 signal 下发 (模型要用),只是卡片不再渲染;折叠区的纯信息类旁边没有对话解释,仍给一句。⛔ 按钮**不能**搬进对话:打字 → 模型理解 → 翻译成 intent,每一环都可能错 (实测栽过 owner/balance 选反,18 人被散给 17 位别人);点击没有理解环节。 ③ 两份设计文档同步这条分工。 测试:1247 passed;web 20 passed;tsc / build 干净。⚠ ️ 改了一条断言:原来锁死"整体时效=3",改成断"整体没被那个人的精调带偏" —— 锁死默认值会让每次调默认都红一片。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
现象:点了「各自归专属客服」,72 人的待分配已经清零、汇总行也更新成 「拟分 103 人」,而节点区还挂着「72 人的专属客服这轮已排满」+ 四个按钮 —— 再点一次是空操作,主管只会觉得"我明明处理过了"。 根因:signals 是**服务端随提案下发的快照**,本地 edit 只改了前端的生效条目, 没有任何东西去更新它。违了引导节点设计规则④「每次重算,不做增量」。 修法:只撤掉**主语已经消失**的那些,判据全部来自卡片本地看得见的事实: · pending —— 待分配被处置光了,这条没有主语了 · expiry_default —— 主管改过时效,"用的是默认值"就不成立了
⛔ **不在前端重算判定** —— 判定逻辑复制到两边必然漂(T6a 同源)。 其余(候选不足 / 工作量 / 画像亮点)本地改单不影响、或需重跑才知道 → 原样留着。⚠ ️ 这个 bug 单测抓不到:逻辑各自都对,错在"服务端快照 + 本地可变状态"的接缝上。 只有真的点一下才看得见。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
25 提示词减法 —— 重点不是删多少行,而是修掉**三处已经变成错的** (留着它们模型会照着告诉主管错的东西): - 「两个基数都自动沿用主管上一次的值」+ 首次估法 `在岗 × 20` → P1 已改成 `在岗 × 15 × D` 且不再沿用 - 「
⛔ 不许拿上一轮的数字回答」→ 与 P3 之后的第 1 条**直接矛盾** (现在是"条件对得上就能用",而参数就写在 〔调用 …〕 摘要行里) - 「转述那三句依据(原话)」→ P2 之后那三句成品句子已经不喂给模型了 外加清掉五处「照抄 xxxNote」残留;重写文件顶部方法论注释 —— 原文写的是「凡是靠模型算对的约束一律降级成照抄」,P2 已经把这条推翻了。 26 golden set: - tests/golden/assignment-golden.ts —— 9 条用例,每条都来自**真实踩过的坑** 且写明由来。🔴 判定标准是**工具调用**不是文字:最贵的那类失败恰恰是 「话说得挺好、工具一次没调」(实测栽过两次,两次文字读起来都完全正常)。⚠ ️ 用「必须调到 / 必须没调」两个集合,⛔ 不锁完整顺序 —— 多查一次不算错, 锁死顺序会让每次合理优化都变成红。 - golden-set-integrity.spec.ts(**进 CI**)—— 防腐:用例引用的工具还在不在、 每条有没有写由来、id 有没有重复、mustNotSay 有没有被拿去锁措辞偏好。 -⬜ **跑批 runner 未做**,dev-plan 里如实标了。它要真调模型(花钱/慢/有波动)、 刻意不进 CI,且开工前要先定:拿哪套凭据、用哪个诊所的数据、多少失败率算回归。⛔ 不做一个跑不起来的 runner 然后宣称 P6 完成。 1247 passed;web 20 passed;type-check(含 tests)干净。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
⛔ 在此之前模型没有这个工具,被问到只能**推理** —— 而推理出来的理由听起来完全合理, 主管照着去调策略就是白跑一趟。这是 F5(每加一个写工具,先回答"它怎么知道自己做对了") 在读侧的对应物。 - 依据全部来自 followup_plans 上那组**分配当时的决策快照**列 (assign_strategy / dedicated_cs_at_assign / dedicated_cs_last_visit_at / priority_score_at_assign / selection_mode)—— schema 注释写的正是 「为了半年后回答'当初为什么选了这个人'」。
⛔ 不拿"现在"的值解释"当时"的决定:专属客服会被摄入覆盖、优先级分会被引擎 就地改分,两者都不留痕。 -🔴 查不到就**如实说查不到**,⛔ 不返回空壳 —— 空壳会让模型顺着编。有测试锁。 - 「专属排满溢出」与「从来没有专属」必须分得开:主管据此做的事完全不同 (前者是要不要挪回专属,后者是这人根本没人管)。 - dedicated_cs_last_visit_at 回**时间戳不回结论**:「在岗」是滚动窗口算的, 今天在岗的人半年后回查会变成离岗,给证据让主管按当时口径自己判断。 - 只回**已落库**的分配;草稿阶段的「为什么」由卡片解释(明细在它手里,不在模型手里)。 dev-plan 标了 P5 各项真实状态:21 validate 暂缓(草稿不落库,服务端拿不到它); 23 count 模式已有一半;22/24 未做。 1242 passed(新增 5 条);type-check(含 tests)干净。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
在此之前 `toApiMessage` **只回传文本块**,工具块整块丢弃。代价: - 模型每轮从零开始,会重复调同一个工具; - 它上一轮唯一能看到的只有**自己说过的话** —— 而那正是"背出成品句子"的温床。 现在按 blocks 原始顺序拼(
⛔ 不把工具记录堆到末尾:先查什么、再说什么本身是脉络): - 工具调用 → `〔调用 名字 {关键参数} → 结果摘要〕`,args 截 120 / result 截 220 - 失败 → **只留第一行且切掉堆栈**。⚠ ️ 光靠长度截不住(实测:堆栈就在前 120 字里, 回归当场红了)—— 要按结构剥,` at ` 之后的是给开发者看的,进历史纯属污染 - 卡片 / 确认单 → 只留一个存在性锚(内容是渲染产物,复述没意义且很长) 配套改提示词:原来那条「⛔ 不许从上文抄数字」是为"历史里没有数字"写的, 现在自相矛盾了。改成**条件对得上才能用**:摘要行里写着当时传的参数, 主管一换条件旧数就作废 —— 这正是"同一批人报出 12、17、13、70"的由来。 测试:web 20 passed(新增 7 条分层回传回归,含肥载荷截断与堆栈剥离); service 1237 passed;tsc / build 干净。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
⛔ 🔴 核心:propose_assignment 的返回值从「三段成品句子 + 十几行『原样输出、 不要改写、不要合并』」换成**纯结构化事实**(assignment-facts.modelFacts)。 为什么: ① 成品句子有固定形状,模型学会形状就能**凭空背出来** —— 实测背出过 「已撤销批次:收回 9 条。」,而撤销工具一次都没被调用。 结构化事实编不出来:JSON 里的具体值瞎写一眼就对得出来。 ② 「照抄」浪费了模型唯一不可替代的能力:把事实组织成人话。 让它当复读机等于花钱买了个模板引擎。 - assignment-facts.ts:三段投影(怎么选的 / 怎么派的 / 要主管定的),中文键。⛔ 一个字的指令都没有;⛔ 不含 planId / 患者姓名(那些走侧信道)。 候选不够时**本批人数与基数分别给** —— 否则主管以为系统偷偷改小了他的设置。 - 提示词第 0.5 条重写成**唯一一处写作规范**;删掉靠模型自觉的 `###` 骨架 (2026-08-11 实测:给了规则它一个 `###` 都不出)—— 结构现在由返回值的键名决定, ③ 级校验换成 ① 级结构。 - 清掉三处「照抄 rosterNote / basisNote」「转述那三句依据」的残留指令。 -⚠ ️ basisNote/selectionNote/pendingNote 字段**暂时保留**(卡片与 criteria 快照还在用),只是不再喂给模型。 测试: - 两条**对源码 grep 的断言**改成行为断言 —— 它们锁的不变式还在,但台词稿一删 整条就失效了。⇒ 断行为不断源码。 - 新增 7 条事实投影回归,其中「⛔ 里面一个字的指令都不许有」直接扫关键词。 1237 passed;web 13 passed;type-check(含 tests)干净。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
- confirm-sheet-signals.tsx:纯呈现。action 层**全部展开**、info 层折叠成一行,
🔴 防噪音靠分层⛔ 不靠丢弃 —— 藏起一条主管就不知道有东西卡着。 每条都显示「不处理会怎样」,让「一条不点直接确认」成为**看得见**的安全选项。 - 位置紧贴汇总行,⛔ 不放卡片底部:主管视线从汇总往下走,"还有什么要我定的" 必须在这一跳被看到 —— 埋到底部等于没有(那正是待分配当初被漏掉的原因)。 - intent 路由(chat-blocks,纯函数 + 回归): · 待分配三动作 / 改时效 → 翻成 SheetEditOp[],压进和 `edit_assignment_sheet` **同一条 edits 队列**。⛔ 不给按钮另开执行路径 —— 两条路各做各的必然漂。 · 换人群 / 换基数 → 必须重跑算法,替主管说那句话交给助手。⛔ 不能用局部改单去凑:凑出来人群没变,他以为条件生效了其实没有(静默错)。 · 重排一版 / 改时效由卡片就地做(它本来就有实现)。 -🔴 owner / balance 由按钮直出,没有理解环节 —— 实测栽过:主管说「各自分给 各自的专属客服」,助手用了铺平,18 个人被散给 17 位别人。 测试:web 13 passed(新增 5 条 intent 路由,含"两张表不能都认领同一个 intent"); service 1230 passed;tsc + build 干净。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
画像亮点: - computeHighlights —— 纯函数,比两份分布:**这一格** vs **同治疗项整个池子**。
⭐ 判据是「相对基线的偏离」而不是绝对占比:「这批 80% 自费」没用(全池也是), 「转介绍是平时的 2.6 倍」才是主管会据此改打法的东西。⚠ ️ 基线取同治疗项,⛔ 不取全池 —— 种植的重要价值天然高于补牙,用全池会永远命中。 - 三道闸,缺一道就变噪音:倍数 ≥2 · 本批命中 ≥10 人 · 基线 ≥50 人。 基线为 0 时**不比** ——⛔ 不许当成「这一格独有」凭空下结论。 - CohortAttributesService.dimShares:精简版取数(只出分布),与 describe 共用 同一份 cohortWhereSql / personaBreakdown,⛔ 不另写一套(T6a 同源)。 - 维度只留 rfm / referral_champion / entitlement_status 三个。⚠ ️ 产品还点了「获客渠道」「折扣锚点」,它们目前只是 PersonaFeatureKey、 不在 PERSONA_TAG_FILTER_DIMS 里,取不到分布 —— 要加得先补成 hidden 维度。 - 亮点失败不让确认单挂掉(锦上添花),但要 warn 出来。🔴 修一处假绿: 给 AssignmentProposalService 加第三个依赖后,测试仍按两参构造却全绿 —— 因为那两处用的是 `const { X } = require(...)`,返回 any,**arity 查不出来**; 运行时 this.cohorts 是 undefined,又正好被业务侧 try/catch 吞成"没有亮点" ⇒ 测试绿着,而新加的整条路径根本没跑。 - require → 有类型的 import;补 cohortsStub 显式传入 - 由此暴露并修掉 5 处此前被掩盖的类型错误(TemperatureValue / find() 可能 undefined) - package.json 的 type-check 接上仓库里**已有**的 tsconfig.typecheck.json (2026-08-05 因 ReleaseReason.OVER_CAPACITY 同类假绿建的,之前没进脚本) 1230 passed;type-check 含 tests 干净。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
产品定(2026-08-12):N 只算新增不扣在手,所以这个节点每批基本都会亮 —— 那不是 bug,每一批本来就该把「这活多大、要打几天」讲清楚。 -
⛔ 措辞去掉「打不完 / 超了 / 过载」:常亮 + 报错口吻 = 主管开始怀疑系统, 而不是做决定。改成「这批发下去,最忙的是张悦:手上共 60 条,约 4 天的量」。 -⚠ ️ 数字**拆开说**:「本批 45 条、原本在手 15 条」。不拆开主管会读成 "这批一下压了 60 条给他"(增量与总量分不开,同类问题栽过)。 - 默认路径说清后果是可接受的(到期落回池子、下批还能再分),⛔ 不制造紧迫感。 - 补第三个动作 DAILY_RATE_SET「改每人每天打几通」—— 它同时决定 N, 原来只给了延时效/减人数两条。 - 文档补一节说明为什么不改成 `Σ max(0, 15D − 在手)`:那会踩回 2026-08-03 删掉的坑(第二批恒为 0)。 测试:新增「措辞不许像报错 + 必须拆开说」一条,fixture 支持分别给 本批条数与在手条数。1224 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
引导节点(Signal): - 新增 assignment-signals.ts —— **纯函数**,输入一份已组装的提案、输出整套节点。
⚠ ️ 不查库不看时钟:节点用的每个数都来自这一份提案,⛔ 不许再查一次 —— 否则卡片和节点两个数字都"对"但对不上,那是最难查的一类 bug(T6a 同源)。 - 五个节点:pending / daily_overload / short_supply / expiry_default / anchor_nondefault。前三个 tier=action(全部展开),后两个 tier=info(可折叠)。🔴 **不截断** —— 防噪音靠分层不靠丢弃:藏起一条主管就不知道有东西卡着。 - 每个节点的默认都是 no-op,「一条不点直接确认」永远安全(有测试锁)。 - ASSIGNMENT_INTENTS:**按钮与模型工具共用的同一组动作 id**。这是「按钮能做的、 说话也能做」的结构保证,⛔ 不是提示词里的一句叮嘱。 基数改口径(产品定 2026-08-12): - N = 在岗人数 × DAILY_CALLS_PER_AGENT(15) × 时效 D,⛔ **不再沿用上一次**。 原设计"调一次省一次"会让 N 悄悄漂:某次为小格子调成 44,此后永远是 44, 而主管看不出为什么变小了。 - N 与时效挂钩:D 从 3 改到 5,能承接的量本来就该跟着变。 -⚠ ️ 15 仍然**不是容量上限**、⛔ 不参与落人 —— 落人只有水位法。 它只回答"一批推多大",以及当 daily_overload 的判定尺。 - 沿用机制本身留着(resolveBaseline 还在读),要开回来只需把 baseline.batchSize 接回去。 - basisNote 改成把算式写出来 —— 主管看到 405 的第一反应是"怎么这么多"。 测试:改 11 条旧口径断言,**不变式留住**(如「第二批不为 0」改成断言 >0 而不是 断言具体数字);新增 11 条节点判定。共 1223 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
产品定(2026-08-12):作废发生在**新草稿生成时**,不是在确认时。 否则主管面前会同时摆着两张都能点确认的卡,点错就是分错一批人。 - state: 'pending' → 'active' | 'superseded' | 'confirmed' | 'cancelled'
⚠ ️ 顺带解掉一个命名撞车:原 state:'pending' 与「待分配」的 group:'pending' 同名不同义,读代码时极易混。 - 新草稿到达时**跨消息**扫描,把此前所有 active 作废;confirmed/cancelled 是终态,⛔ 不许被回退。 - edit 指令改为落到**唯一那张 active**,⛔ 不再是"最后一张" —— 重出一版后 最后一张可能正是刚作废的,改上去主管一点反应都看不到。 - 作废态:顶部横幅 + 底部说明 + 确认按钮禁用 + 降到 60% 不透明度。⛔ 不隐藏、⛔ 不折叠 —— 他要能看见自己出过几版、哪一版才是当前的。⚠ ️ 顶部横幅不能省:确认单可以很长,视线停在上半部分时看不到底部那句。 代码组织(按 dev-plan §三): - 数据模型 + 纯函数操作抽到 chat-blocks.ts(use-assistant-chat 534 → 479 行)。 草稿状态机的失败形态全是静默的,端到端点一遍代价高且不稳 —— 抽成纯函数锁住。 - pac-web 补 vitest(此前无任何测试基建),8 条回归覆盖跨消息作废、终态不回退、 同 id 重推清空 edits、findActiveSheet 不退化成"最后一张"。 tsc(service/web) 干净;pac-web build 通过;vitest 8 passed;jest 1212 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
文档(新增四份,成套): - agent-doctrine.md 通用教条 49 条,按「后补代价」分三档 - agent-architecture.md 四控制面 + 八原则 + 术语甄别 + 三方分工 - assignment-agent-flow.md 分配的 Agent 行为规格(状态机 + 引导节点,定稿) - assignment-agent-dev-plan.md P0-P6 改造规划 + 代码组织规范 P0 落地: - model-facing.ts —— 取值码/桶名 → 中文。给模型现成的中文(①级), 替代提示词里两页「
⛔ 不许说出取值码」(③级)。只翻译不判断,底层 service 形状不动(REST/前端是另一个消费者)。 - guides.ts —— 解读规范随返回值下发(`_guide`)。get_assignment_detail 的 35 行描述是常驻成本,而只有约三成会话用得到;挂返回值上零往返、 必然到达、用不到时零成本。 - stopWhen 到顶不再静默 —— finishReason='tool-calls' 时发 step_limit 事件,前端如实告知。原来模型可能停在「我先查一下」之后,主管以为查完了。 - SYSTEM_PROMPT 不再写死「为牙科诊所的客服人员」—— 主管也走同一条链路, 与后面拼上的 DISPATCHER_EXTRA 当场矛盾。 - 提示词减 35 行:删掉已被结构保证的部分(取值码、五个进度桶、noTag 桶名),⛔ 保留仍需靠模型的(工具名、内部黑话)。 - 骨架顺序改成「怎么排的 → 已排好 → 要您定的」,与 propose_assignment 的实际输出顺序对齐(此前提示词与代码打架)。 tsc(service/web) 干净;jest 1212 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed
-
- 11 Aug, 2026 12 commits
-
-
① 窗口宽度 400 → 480(产品:「调宽一点」)。
⚠ ️ `openAt` 里的 480 与 className 的 `w-[480px]` 是一对,Tailwind 任意值必须是字面量、插不进常量,改一处要改两处; 顺带把注释里当年按 400 写的依据一并同步(下一个人不会再按 400 做决定)。 ② 确认单汇总行**没做响应式**:`flex items-center` 不换行,窄窗里 「共 N 人 · 拟分 X · 待分配 Y」+ 角标 + 时效下拉挤在一行 —— 实测「已移除 1」被压成**竖排单字**、时效下拉顶出卡片边界。 ⇒ 改 `flex-wrap` + `gap-y-1`;两个角标补 `flex-none whitespace-nowrap` (不锁就会被压到零宽、中文按单字折行,那正是竖排的成因); 时效那组补 whitespace-nowrap。`ml-auto` 换行后仍按行推到右端,不受影响。 同样的毛病在"已分配 + 撤销三个数"那一行,一并加了 wrap。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
标题 `mb` 6px 与紧随其后那段的 `mt` 14px **折叠成较大的那个**, 于是"标题
↔ 正文"的缝跟"段↔ 段"一样宽,标题看着浮在半空。 ⇒ 容器加 `[&_hN+*]:mt-1.5`,只压**紧跟标题**的那个块;段与段仍是 14px。⛔ 别改 p 的 my 去凑 —— 那会把所有段落间距一起缩掉,段落又不成块了。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
产品指出两处: ① **标题比正文还小、还不加粗**。第一版 h3 做成了 12px/slate-500 的"小节标签", 结果句中那个 font-medium 的加粗反而比标题更抢眼 —— 层级是**反的**。 ⇒ h3 = 14px semibold slate-900(比正文 13.5 大半号),h1/h2 顺次放大。
⛔ 别再改回小灰字:那是**卡片小节**的写法,不是**对话正文标题**的写法 —— 这段文字里标题是扫视的落脚点,它得是这一屏最重的东西。 ② **一行断在半路、右边空一大片**:上一版加的 `max-w-[34em]`(限中文行长 25–35 字) 在助手窗口放大后看起来像渲染坏了。⇒ 撤掉。 窄窗本来就只有 ~28 字/行,行长问题只在放大时出现,而放大正是主管要"看得多"的时候;⛔ 真要限行长得连用户气泡一起收进居中的对话栏(整块布局的事),别只勒住助手这一半。 实测(强制模型输出 ###):h3 = 14px/600/slate-900,正文 = 13.5px/400/slate-700, 段距 14px,容器 maxWidth: none。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
产品:「输出抓不住重点」。查下来不是字体的事,四条: ① **段落根本没成块**:`p` 是 my-1.5(6px),而行距 22px —— 段距只有行距的 1/4, 段与段比行与行还紧。→ 14px(通则:段距 ≥ 1.5 倍行距)。 ② **加粗身兼三职**(段落标题 / 句中强调 / 后果警示),还同时跳字重和颜色 (semibold + slate-900),一屏 7 处黑块 = 没有重点。 → strong 降成 font-medium;**标题交给 h3**。 ③ **标题层此前是死的**:h1-h3 定义得好好的,模型一个 `#` 都没输出过 —— 它只有粗体一件武器,所有层级都拿它凑。 → h3 改成小节标签(12px / slate-500 / 加字距 / mt-4),与正文靠**字号+颜色**分层,⛔ 不靠字重(靠字重就又跟句中加粗抢)。 ④ 行长 50+ 字(中文舒适区 25–35)→ max-w-[34em];数字加 tabular-nums。🔴 **标点:模型是在照抄提示词**。实测提示词里中文之间用半角逗号/分号 91 处、 全角 0 处 —— 光加一条"要用全角"的规则等于跟例子对着干。 ⇒ 提示词自身的标点也一并改了(逗号 176 / 分号 18 / 冒号 92 / 括号 78 处), 再加一条第 0.5 条「怎么写」。改完实测:**半角残留 0**。⚠ ️ 但**只给规则,结构没跟上**:第一轮实测标点全对、`###` 一个没出、仍旧三大段 + 6 处加粗。 ⇒ 按本仓既有方法论(「靠模型算对的一律降级成照抄」)补了一个**成品骨架** (要您定的 / 已排好 / 怎么排的),并把那次失败写进提示词当反例。 第二轮实测:h3 三块齐、加粗降到 2 处、要主管动手的那块排到了最前面。⚠ ️ 两条断言里的半角标点改成 `[,,]` 兼容 —— 把测试钉死在某种标点上, 失败与被测行为无关,只是噪音。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
原来是「搜索框 + 医生下拉 + 客服下拉」三件。并成一个:输入任意词, 姓名 / 病历号 / 主治医生 / 上次就诊医生 / 专属客服名 任一命中即可(全部服务端 ILIKE)。
⛔ 别再拆回下拉:医生和客服的候选各要拉一个接口(/plans/doctors + 名册), 而主管想的只是"跟薛玫有关的",他不关心那个名字落在哪一列。⚠ ️ 专属客服名直接读 `patients.preferences.dedicatedCs.name`(库里就有),⛔ 不用先查名册再翻 id —— 那要在查询前多一次往返。⚠ ️ `%` `_` `\` 转义后再进 pattern:病历号里真有下划线,不转义会变通配符, 搜「BJ0U_1」把 BJ0UX1 也捞出来 —— 多捞不报错,更难发现。 顺带删掉 NO_OWNER(「无专属客服」那一档随下拉一起没了,⛔ 别留没人用的常量)。 本地实测:薛玫→首页 50 条、金芮冰→24 条、BJ0U0175→19 条; 按钮仍然写「确定,交给助手(420 人)」——搜索不改变交出去的人群。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
翻页按钮换成滚到底继续加载(与批次列表同一套:scroll 事件 + inFlight ref 守卫 + 按 planId 去重,
⛔ 不用 IntersectionObserver —— 零高度哨兵不回调、内置浏览器面板里 整个不工作,那条路没法在我们的验证环境里证伪)。 筛选四个:姓名/病历号(防抖 300ms)、医生(主治**或**上次接诊任一命中,候选走 /plans/doctors)、专属客服(候选走本诊所名册,另有「无专属」一档)。🔴 全部在**服务端**过滤:滚动加载只加载了前几页,放前端过滤就成了"在已加载的那几页里搜", 第 7 页的张三搜不到还不报错。🔴 **筛选只影响"看",不影响"分"**:确定按钮上永远写整格人数(「确定,交给助手(420 人)」), 筛选条旁边再明说一遍。这是"筛到 12 人却分出去 420"这个误解唯一的拦截点。 要让筛选参与圈人是另一件事(医生/客服本就是画像维度,得一并传给助手),⛔ 别偷偷用 filtered。🔴 途中真炸过一次,注释钉住了:`input.agentUserId === NO_OWNER` 这种写法,在服务进程 抱着旧 @pac/types dist 时 `NO_OWNER` 是 undefined,而不传筛选时 agentUserId 也是 undefined → 恒等成立 → **每次查询都被悄悄加上"只看无主的"**,420 人的格子只回 20 行, 而 count 仍是 420(它不走这段)。改成先判"传没传"再判哨兵。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
主管点的是一个数字(「充填 · 半年到1年 420」),而他要为这批人负责。 现在多一步:摊开这一格圈定的**全部**患者(姓名/病历号/性别/年龄/主治医生/ 上次就诊医生/专属客服),看过再点「确定,交给助手」。
🔴 与出确认单**同源**:GET /plans/assignments/cohort 用的是 propose 那段 **同一个** cohortWhereSql + 同一个 SELECTION_ORDER。⛔ 别为它另写查询 —— 排序一旦不同,第 1 页看到的人 ≠ 助手真会挑走的前 N 个,而且两边都不报错(T14)。 实测:中间表标题 420,助手随后说「这批 50 人从符合条件的 420 人里挑」,对得上。 分页取,不一次拉全:最大的格子四千多人(测试服 充填·3年以上 4,159), 一次拉回来 DOM 撑不住,主管也不会逐行看完。⚠ ️ 这里**用 offset 不用游标**(与列表页那条纪律相反,刻意):候选集是静态快照、 排序键确定性、而且主管要「第 3 / 84 页」这种可回跳的翻页。⭐ 助手**默认不最大化**了(handoff 原来传 maximize: true): 前面已经有中间表让他看过人,再铺满整屏等于把他刚看完的工作台盖掉, 而他多半还要回去点下一格。要看大图,窗口自己有最大化按钮。⚠ ️ 粒子起点从"格子"改成中间表上的「确定」按钮 —— 从一个已被弹窗盖住的格子 起飞,看起来像凭空出现。 越权闸两层都有:controller 一次(让"收 clinicId 必过闸"在源码上可数)+ service 一次(换任何入口进来都拦得住);测试两条都锁。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
「机会有多老」不可观测(onset_date 宿主 0 填充),手上只有两个观测时间: · diagnosis(默认) 医生最后一次写下该诊断 → 患者最后一次被提醒多久了(话术) · last_visit 患者末诊 → 这人多久没露面(接通率) 改口径的由头是实测出来的偏差:复诊时医生常不重复写同一诊断 —— 41.3% 的机会在诊断后患者又来过(平均 6 次)却一次都没再写,诊断日与末诊日 中位差 526 天(矩阵上差 1-2 档);各医生重复记录率 20.7%~43.2%,差一倍。 ⇒ diagnosis 档位里含着"这个医生爱不爱重复写"的成分,不该被读成临床新鲜度。
🔴 **这是口径开关不是样式开关**,它决定分到的是哪批人。所以三处同时到位: ① 看(/plans/matrix?anchor=)与圈人(cohort-filter ← criteria.anchorMode)同 mode; ② 随确认单进 plan_assignments.criteria 快照; ③ 批次名标「(按末诊)」—— 只标非默认那版,老批次不用改名。 少任何一处就是「主管看到 87 人、确认单给另一批」,两个数都对且不报错(T14)。 助手侧判据只有一条:那句话里带「按末诊」三个字才传 last_visit (ANCHOR_MODE_TOOL_DESC);矩阵的移交措辞与它必须同进同退。 两版**只换锚点,不换档位切分**(90/180 天 + 自然年),测试锁着这条。 本地实测(朝阳):按末诊后「3 年以上」整列清空、人数整体左移 (充填 半年到1年 326→420),因为诊断三年前没做的人其实一直在来。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
六档改成绝对时间轴之后,第三档的实际区间是 180 天–1 年,而列头还写「1 年内」—— 挨着「三个月到半年」看过去,很容易读成"从 0 到 1 年"(那就跟前两档重叠了)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
① 就诊冷静(⑤f)14 天 → **90 天**。这不是文案改动:近三个月到过诊的人整批退出 召回池,而引擎跑全量时会把他们**已经在跑的单一并关掉**(0 命中 → supersede, 连 assigned 的也关,补 auto_release 账)。本地库实测:池子 2,711 条里 276 条(10.2%) 会被新闸挡掉,其中 3 条已认领。 ② 矩阵前两档从「该治疗自己的临床周期」改成**固定 90/180 天**,列头改叫 「三个月内」「三个月到半年」——六档从此共用一个锚点、一把尺子。 · 常量单一真理源 HOT_BUCKET_DAYS / WARM_BUCKET_DAYS(@pac/types) · 判档 SQL 里那张按码的 (code,urg,wnd) VALUES 表连同 JOIN 一起删了 —— 它当年顺带起的过滤由 labelCase 全额承担,行集不变(测试锁着) ·⚠ ️ DiagnosisTreatmentMap 的 urgency/window **仍然**管入池 cooldown 与打分衰减, 只是不再管矩阵档位,两者已脱钩 本地实测重分布:3,678 个 (单×码) 里 409 个(11.1%)换档,新的第一档 +20%、第二档 +25%。 ③ 矩阵去掉「潜在治疗」标题行,底部说明缩成一句(前两档那半句解释没有了)。🔴 `make_interval(days => $n)` 必须带 `::int`:Prisma 把 JS number 绑成 double, 而这个重载不存在 → /plans/matrix 直接 500(改的过程中实炸过一次)。 不需要重算画像、也不需要手动重跑引擎:档位是读时从召回单证据算的, 新闸随每日增量 cron 的全量 runAllForHost 自然生效。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
globals.css 里那条 `* { border-color: hsl(var(--border)) }` **不在任何 @layer 里**。 无层级样式整体胜过 `@layer utilities`(与优先级无关),于是它压过了 Tailwind **全部** border 颜色工具类:写 `border-l-brand-400` / `border-rose-300` 不报错、 也不生效,算出来永远是那个灰 #D6DDE6。实测登录框 91 个有边框的元素,91 个都是灰的。 被吃掉的不只是"某处颜色不对": - `border-t-transparent` —— 两处转圈 loading 的缺口被填成灰,整圈同色,转了看不出来 - `border-current` —— 本该跟随文字色调,恒为灰 - ~32 处 hover:/focus: 边框反馈全哑,配着 `transition-colors` 动画一个空 - chain-viz / facts-timeline 整套语义色板的 border 分支形同虚设 改法:把这条规则包进 `@layer base`。⚠ ️ Tailwind v4 没有 `--default-border-color` 主题变量(v3 的 borderColor.DEFAULT 已移除,preflight 改成 `border: 0 solid`),分层的 `*` 规则就是 v4 官方写法。 同时删掉 147 处中性 `border-slate-*` / `divide-slate-*` 类(31 个文件): 它们此前零效果,而分层后会**开始**生效 —— `border-slate-100`(114 处)会从 #D6DDE6 变 #F1F5F9(对白底对比度 1.30→1.06,发丝线几乎消失),`slate-50` 直接看不见。 这些类当初就是对着灰写的(该 `*` 规则从首次提交就在),删掉后落回 base 层默认灰, **构造上保证零像素变化**。三个页面实测:中性边框数量与颜色与改前逐一对齐, 彩色边框恢复(brand-600 = rgb(0,50,160)、brand-400 = rgb(59,118,247))。 batch-tracking 那条竖轨收回工具类(`border-l-2 border-l-brand-400`), 连同解释这条地雷的注释一起删;`past` 保留 2px 透明边以维持两组首列对齐。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
「分一批新的」不再是按钮 + 浮层: · 删掉「样式2 里世界」整版(传送门 / 锤子光标 / 敲击动效)与那个样式开关, 连同 globals.css 的 .pac-portal-* 一起清掉 —— 只留矩阵那版。 · 矩阵直接摊在「我分的批次」上方:左栏是上下结构(挑格子 → 看这些格子 分出去之后跑成什么样),整栏再与「团队现在什么状态」左右并排。 · 矩阵在助手确认/撤销后跟着重拉 —— 刚分走的人就是从这些格子里出去的。 「我分的批次」区分在跑与历史: · 在跑的 = 主色竖轨 + 白底 + 满饱和语义色;历史 = 灰底 + 同色相低饱和。 · 语义色**保留**(还看得出哪列是退回哪列是约上),
⛔ 不做成纯灰。🔴 顺带记下一个全站地雷:globals.css 的 `* { border-color: … }` 不在任何 @layer 里,压过 Tailwind 所有 border 颜色工具类 —— `border-l-brand-400` 写了不报错也不生效(竖轨最初就是这么哑掉的)。彩色边框只能走 inline style。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed
-
- 10 Aug, 2026 13 commits
-
-
产品:确认分配后主管工作台的两个表格能不能做到刷新。 确认单在**助手浮层**里、两张表在页面上,两者没有父子关系 —— 谁也拿不到谁的 setState。 所以走一个极简全局信号(与 plan-sync-store「详情页动作同步左栏」同一套路): 确认分配 / 撤销这批 → notifyChanged() 两张表的 fetch effect → 把 seq 放进依赖,重新拉
⭐ **只发 seq,不带任何数据**(⛔ 不学 plan-sync 那样带 planId/status): 那边改的是"某一行的角标",能就地打补丁;这里落库的是**一整批** —— 批次列表多一行、每个客服的在手/分到全变、退回率的分母也变。 想靠事件把这些补丁算准,等于把服务端的聚合口径在前端再实现一遍 (必漂,而且不报错)。⇒ 一律重拉,让服务端说了算(T14:口径只有一份)。🔴 批次表重拉必须**回到第一页**(setItems([]) + setCursor(null) 一起做): 只调 load() 会把新的一页**追加**在旧列表后面,而新批次排最前 —— 它会出现在列表中段,这是游标分页的天然后果。⚠ ️ 发信号用 getState() 不订阅:确认单只发不收,订阅了会因 seq 自增把卡片自己也重渲染。 实测(全程没刷页面): 确认前 全部 27 · 在手 35 确认「早矫 2 条」 全部 28 · 在手 37 点「撤销这批」 全部 28 · 在手 35 ← 批次仍在(标已撤销,列表保留做回顾),在手正确退回 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
产品:主管进客服执行页时,召回池和分配隐藏。 T16 原版把池子和分配挂在这一页,是因为那时主管和客服共用它。 08-04 评审推翻了后半句、分配搬进独立的主管工作台之后, 这一页就该回到它唯一的职责:**把手上的任务打完**。留着就是同一个动作两个入口。
⚠ ️ 藏 tab 不等于做到了 —— view 还有两条会自己滑到 pool 的路,一起堵掉: ① 左栏「我的为空 → 自动回落召回池」的 effect ② /plans 落地页「② 召回池第一个(仅主管)」的兜底 —— 它是最后一条还能把人滑进池子的路:主管的「我的」一空就被送进 池子里某个患者的详情页,而左栏根本没有那个 tab,他会以为这条单是分给自己的。 顺带清掉随之而来的死代码:矩阵浮层(openMatrix / pickCell / matrix 三个 state)、 canDispatch 判据、以及 PoolMatrix / Button / useAssistantStore 等 6 个 import。⚠ ️ view='pool'/'all' 的**取数逻辑保留**,将来要放回 tab 时不用重接。 空态文案跟着改:原来主管那句写着「我的任务与召回池都是空的,可以在左侧调整筛选(视图/…)」 —— 那两样东西现在都没有了,照说就是让人去找一个不存在的地方。 改成「这一页只显示分给你自己的任务。要给团队分新的一批,去主管工作台」+ 一个入口。 实测:主管 / 客服进执行页,左栏都只剩「我的」,无分配按钮; 主管空态给出「主管工作台↗ 」,且不再被自动滑进召回池。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
↗ 上一个提交刚把工作台的「客服执行页
↗ 」在嵌入态保留下来, 这边却给「主管工作台↗ 」加了 `isEmbedded()`,两条改动自相矛盾: 主管在宿主里进了执行页就回不去,而"必须双向"正是这条链接存在的全部理由。 当时写的理由是「宿主自有导航」——那个理由是错的: 宿主提供的是**它自己的**导航,不是 PAC 这两个页面之间的路。 ⇒ 按嵌入态藏的只有**宿主已经给了的那些**:品牌、面包屑标题、身份三件套。 功能区(诊所切换、两个页面互跳)不藏。 实测(本地宿主外壳 iframe): 嵌入 · 主管 · 执行页 → 有入口,点了落工作台,那边有「客服执行页↗ 」,双向通 嵌入 · 客服 · 任务详情 → 入口不出现(权限闸未动) 非嵌入 → 两边都不受影响 注释里钉了「⛔ 别加 isEmbedded(),2026-08-10 加过一次当天就被打回」。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
⛔ 产品:「标题栏不要全部隐藏,学一下客服工作台,保持一致」。 执行页的规矩是整栏保留、嵌入时只藏 PAC 徽标 + 面包屑(宿主已提供品牌与导航), 功能区照常。工作台这边写成了 `if (isEmbedded()) return null` —— 整栏没了。
⚠ ️ 代价不只是不好看:嵌入态下**诊所切换**和**「客服执行页↗ 」**一起被藏掉了, 主管在宿主里再也回不到执行页 —— 那正是上一个提交刚补上的那条路。 ⇒ 与 plan-detail 的 `embedded` 同一条规矩:藏品牌与标题,留功能区。⚠ ️ 分隔线跟着身份三件套一起藏 —— IdentityCluster 嵌入态返 null, 不一起藏就剩一条悬空的竖线。 实测(本地起了个宿主外壳 iframe): 嵌入 · 工作台 → 只剩「北京朝阳公园诊所」+「客服执行页↗ 」,与执行页同构 嵌入 · 执行页 → 场景 chip / 召回反馈 / 刷新 / 预约 照常 非嵌入 → PAC | 主管工作台 | 诊所 | 身份,未受影响 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
工作台头部那条「客服执行页
↗ 」旁边的注释早就写了 「两个页面是双向的,⛔ 别做成有去无回」,但实际只做了去的那一半: 主管点进执行页之后,除了改地址栏没有回去的路(产品发现)。⚠ ️ 判据用 PLAN_DISPATCH,与 /supervisor 那条路由**同一把闸** ——⛔ 不用 role 判断:权限按 role 现算,而 token 里的 role 是签发时固化的, 两处判据不同源就会出现"看得见入口、点进去被弹回来"。⚠ ️ 嵌入宿主时隐藏,与 IdentityCluster 同一条规矩(宿主自有导航)。 实测:主管进执行页有入口、点了落 /supervisor;客服进任务详情不出现。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
实测:姜茜「这 7 天分到 53 条,一条都还没动」,拆开是 20(8/4 17:31 那批,**已撤销**)+ 20 + 13。 头一个 20 是主管自己在撤销窗口里收回去的,她根本没机会碰。 摆在「分到…一条都没动」里,等于拿主管自己的操作去指责客服。 ⇒ assign 统计排掉落了 `auto_release/revoked` 的那些。
⚠ ️ 判据是**逐条 + 同批次**,⛔ 不是"整批 status=revoked 就全扣": · 撤销刻意不收已被客服打开过(view 事件)的单 —— 那些仍在他手上、仍该算他的 · 同一条单可能先在 A 批被撤、后在 B 批正常分给他,B 批那次不能跟着被扣⚠ ️ 到期回池(assignment_expired)**照样算** —— 单子在他手上放到过期, 正是「没动」要表达的东西,⛔ 别一起排掉。 测试机实测(同一条 SQL 直查):李银兰 62→42、张悦 61→41、康慧捧 58→40、 刘艳阳 55→35,各减掉自己在撤销批里的那一份;不在该批的(钱俏虹 397)不动。 回归锁 SQL 文本 —— 口径写在原生查询里,$queryRaw 一 mock 就是"喂什么返什么", 行为测试证明不了任何事。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
复现条件:localStorage **已经有票**(第二次进来)+ URL 带 ?code=。 干净浏览器不复现,所以第一次测没发现。 时序: ① store 反水合 → isAuthenticated 立刻 true → AuthGate 放行 children ② /plans 落地解析器跑起来 → 主管 → router.replace('/supervisor') ③ `await exchangeCode()` 才回来,然后 replaceState 把地址写回 /plans ③ 把 ② 那次跳转冲掉了 —— Next 的路由树被拉回 plans(实测 history.state 里 `__PRIVATE_NEXTJS_INTERNALS_TREE` 停在 plans),而解析器已经 return、不会再试, 于是永远停在占位文案上。 ⇒ 剥 code 与 exchange 成不成功无关,提到 await **之前**做。本 effect 跑在首次提交上 (那时 mounted 还是 false、children 尚未挂载),任何页面的落地跳转都必然在它之后, 不会再被回写覆盖。⚠ ️ replaceState 第二个参数改传 window.history.state 而不是 `{}` —— App Router 的路由树就存在 history state 里,传空对象等于把它抹掉。 实测三种都过: 已登录 + /plans?code= → 落 /supervisor 已登录 + /plans?exec=1&code= → 走执行页(exec=1 没被剥 code 时一起吃掉) 清空 storage + /plans?code= → 落 /supervisor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
产品带 ?code= 走授权登录,控制台已经 code-ok、票也拿到了, 浏览器里却还有一发 `mock-users` —— 那是 dev「快速登录」的探测请求。 真因:AuthGate 里那个探测是**空依赖的 useEffect,每次挂载无条件发**, 结果只在"未登录"那条分流里才用得上。
⚠ ️ 它不只是多一个请求:`/auth/mock-users` 是 @Public() 的, 返回**全部客服的真实姓名 + 所属诊所**(测试机 116 人,公网无凭证可拉)。 挂在每次加载上 = 每开一次页面就把花名册取一遍,嵌在宿主 iframe 里也照发。 ⇒ 收紧到"下面那条分流真的会用到它":已登录 / 还在 bootstrap / 嵌入态, 三种都一次不发。⚠ ️ hooks 不能条件调用,所以在 effect 里判、条件进依赖。 实测:已登录重新加载 → mock-users 0 次,只剩 session / client-diag / assignments / workload; 未登录(dev)→ 快速登录框照常弹,没被误伤。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
上一个提交补完 /agents、/workload、/plans/matrix 三个 query 参数的读接口, **紧接着就漏了这条** —— `POST /plans/assignments/propose/refill` 的 clinicId 在请求体里,按 `@Query('clinicId')` 去数根本数不到。 实测比前三个都重:朝阳公园的主管带杭州大厦的 id POST 过来, 拿回了**那家诊所患者的真实姓名**(前三个只到员工名册与人数分布)。 ⇒ 闸钉进 `assignment-proposal.service.propose()` 而不是 controller: 提案是唯一会吐患者名单的读路径,谁调都得拦得住(REST / MCP / 以后的新入口),⛔ 不指望每个 controller 记得加 —— 这一轮漏的就是"记得"。⚠ ️ 幂等:MCP 那边已经 resolve 过一次,合法 id 原样返回,再过一次无副作用。 回归同步加一条:锁 service 层那句 resolveClinicId,并禁掉 `const { clinicId } = input` 这种直接解构(那正是漏的写法)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
产品在测试机上发现工作台发了两个 workload 请求、clinicId 不一样。 查下去不只是"多发一个",是**那一发真的取回了别家的数据**。 ── 服务端(真问题)────────────────────────────────────────── `/plans/assignments/agents`、`/plans/assignments/workload`、`/plans/matrix` 三个读接口拿着查询串里的 clinicId 直接查,不校验是否在 scope 内。 实测:北京朝阳公园的主管(clinicIds=[66701845…])带杭州大厦的 id 请求, 拿到 200 + 那家 26 位客服的**姓名与在手负载**;矩阵同样能拿到完整患者量分布。
⚠ ️ 写路径一直是拦的(create 里那句 includes 判断),所以分不走别家的人 —— 但名册是员工姓名、矩阵是患者量分布,读一样不能敞。 闸抽成 common/decorators/resolve-clinic-id:不传取第一个诊所,传了必须在范围内, 范围外抛 Forbidden(10107)并列出真实 id ——⛔ 不能"当成这个诊所没人"返回 0, 0 是合法答案,静默返回会让助手拿着 0 去解释"为什么这批人是空的"(违 T14)。 集团级(clinicIds 为空)原样放行,⛔ 别把空数组当成没权限。 MCP 里原来抄了一份一模一样的实现 —— 抄一份的直接后果就是补的时候只补了一边。 现在两边共用一份。 ── 前端(触发源)──────────────────────────────────────────── 第一帧 user.clinicIds 还是 undefined(JWT 里没有这一项,只能等 /auth/session), visibleClinics 回落到"字典里的全部诊所" → 锁了第一个「杭州大厦」→ 发出那一发。 session 回来后自己纠正成朝阳公园,所以肉眼只看见"发了两个请求"。 新增 clinicScopeReady():undefined=还没加载 / []=集团级,两者必须分开 ——⛔ 别改 visibleClinics 的回落语义去顺手修,那会让集团级用户的筛选器空掉。 实测改后:进工作台只发 1 个 workload,clinicId 正确。⚠ ️ 前端等待与服务端闸是**两层**,缺一不可:查询串是用户可改的。 测试:回归从"grep MCP 源码"改成锁**闸只有一份实现** + **每个收 clinicId 的 REST 读接口都过闸**(按 @Query('clinicId') 出现次数比对),这正是漏掉的那一类。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
🔴 独立路由推翻了 T16 的后半句(2026-08-04 评审定,doctrine 已改写并保留病史): 需求方三次讲同一件事「场景和思路不应该去混」。T16 没被推翻的那半句仍作数 (主管也要打电话),所以头部常驻「客服执行页↗ 」,两个页面双向。 后端四处聚合: · workload —— 名册走 T10 同源;「当前在手」是此刻,超期/完成/退回/没动都在窗口内 · 窗口内超期走**账本**:回收器每 10 分钟扫,"当前超期"结构上几乎永远是 0 (实测账本 378 条 vs 当前 0)。归属回捞到到期前最后一次 assign, 并上"仍在手且窗口内到期"那一小撮,按 plan_id 去重 · agentStats[].done 改执行口径(有 plan_executions 才算):池子口径的 resolved 是引擎判定需求没了,客服一根手指没动也会被算进他的「已处置」 · 列表补 booked/handled + 游标分页(⛔ 不是 offset) 界面: · 落地规则双向 —— 有派单权的 /plans→/supervisor(?exec=1 是明确导航意图的出口); 没派单权的 /supervisor→/plans。原来只有单向,客服落到工作台是**纯白页** (Can 无 fallback 渲染 null),不报错也没出口 · 批次跟踪:滚动位置翻页(⛔ IntersectionObserver 在零高度 sentinel 上不触发)、 inFlight ref 防重入、IDLE_HEAVY=0.2 一个常量两处共用 · 团队状态:超期上琥珀(好事有色坏事没色,眼睛只会被绿色勾住) · 分一批新的两版并存;传送门改 transform:scale 的小圆 (clip-path 的 at 按 reference box 算,祖先一有 transform/filter 就整体偏掉、 而且不报错;且 clip-path 不上合成层,那半秒每帧重画整屏) 通话记录: · plan_executions.notes 此前**全线读不到** —— 接口一直在,前端没调、MCP 没开工具 · 逐条明细 + 纪要 + 客服勾的子选项(放弃原因 / 判断不对的治疗 / 约的回访日 / 渠道)⛔ 只回 outcome 等于把客服填的一半烂在库里 · 删掉 outcomes.note:它把给人看的数和给模型的指令焊在一根 string 里, 而工具说明还写着"照抄最稳" → 模型把「不要算成功率、不要画图」原样贴进了对话框 测试:$queryRaw 的桩改成**认 SQL 不认调用次序**(加第三条原生查询时次序错位, 17 条一起红,报的还是 toISOString 这种跟真因无关的错) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
气泡:bg-slate-800 → bg-brand-600。助手那侧是白底,主管这侧是这一屏 唯一的实色块,中性深灰把这个位置浪费了。 待分配:实测 30 条撑到 997px,底下的「确认分配」被顶到三屏开外, 而主管八成是扫两眼就去点确认的。max-h-60(约 7 条)+ overscroll-contain (滚到底不把消息区一起带着滚,手上正拖着人的时候尤其乱)。
⛔ 没用固定高度:人少时留白会像"还有内容没加载出来"。 拖拽自动滚动不受影响 —— scrollerRef 从卡片 parentElement 往上找,找不到这个 ul。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
ZodSerializationException 继承的是 InternalServerErrorException,
⛔ 不是 ZodValidationException —— 名字像,血缘不同。于是它掉进 filter 的通用 HttpException 分支:code=90000、HTTP 保持 200、**一条日志都不写** (那条分支不打日志,"5xx 兜底日志"也因为 status 是 200 而不触发)。 实测代价:workload 接口挂了,接口只回「Internal Server Error」, 服务日志从头到尾干干净净,查了很久才定位到是响应少了一个字段。 单独拦下并排在通用分支之前,打印 zod issues(哪个字段/期望什么/收到什么), 非生产环境连 details 一起回;单独打 Sentry tag(这是 PAC 自己的 bug)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed
-