- 04 Aug, 2026 11 commits
-
-
主管侧批次分配从地基到闭环:三趟落人(专属封顶 → 无主补空 → 有主改派)、 确认单可局部修正、批次归因走 plan_event_logs 账本、通话成效四桶、撤销/退回 分离。企微话术(深度档单块可复制)+ 复制埋点。含路由遮蔽修复 —— script-feedback / script-copy 此前被 script:regenerate 整个吞掉。 自动合并无冲突;删除的 plans-list-app / task-drawer 等来自 3cbb3899 的重构。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
「我分过的批次,哪批出问题了?」换成两条能落到工具上的: · 总览我在跑的所有分配批次,出个表格 → list_assignment_batches · 最近一批分配出份报表:… → get_assignment_detail 指标措辞用服务端原词:「未动过」不写「曝光」、「已处理」不写「完成」。 后者服务端每次返回都在喊「这是处理率不是成功率」,例句写「完成」会把 模型往"谈成了"上带 —— 同文件里那条「别写转化率」的注释防的是同一件事。 点名的七个指标每个都有真实返回值(planned/untouched/progress.done/ outcomes.success/expired/releaseReasons/byOutcome),不是许愿。 顺带:/assistant 整页的兜底建议此前写死一份客服版,主管在那儿看不到任何 分配入口 —— 而整页恰恰是他做批次活最可能待的地方(表格宽、能铺开)。 按同一个 PLAN_DISPATCH 闸拆成 EXAMPLES_LEADER / EXAMPLES_STAFF。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
═ 埋点 ═ plan_event_logs 新增事件 script_copy,reason 列存渠道(wecom / phone,都登记进 PlanEventReason —— 那一列的注释写死了"不要在调用处随手写字符串")。 为什么值得记:企微稿的正常用法就是复制出去发给患者,复制那一下之后客服就离开 PAC 了 —— 这是我们能观测到的**最接近"真的用了"**的信号。生成完没人复制 = 生成了但没人用,那是产品问题不是模型问题,而这件事此前完全看不见。 口径(回归里锁着,这两条改错不会有任何编译错误): · byHuman=false —— 复制 ≠ 联系了患者(复制完可能没发)。算进 HUMAN_TOUCH_EVENTS 的话「处理过的患者」会静默膨胀成"复制一下也算",与 view 同一类错误。 · holdsPatient=false —— 可能发生在未认领的单上,不参与归属区间。 端点不加认领闸(同 :id/view):主管浏览时也能复制,加闸统计直接偏。 ═ 顺带修掉两个真 bug ═ 1)
🔴 路由遮蔽 —— **已有的话术👍 👎 反馈按钮从来没生效过**。 Express 把 ':id/script:regenerate' 里的 :regenerate 当路径参数,模式实际是 「字面量 script + 参数」,于是 /script-feedback 和 /script-copy 全部命中它 (参数 = '-feedback' / '-copy')。点一次反馈 = 真的重新生成一次话术: 花 AI 钱 + 覆盖已有稿,而前端拿到 200、界面一切正常,**不报任何错**。 治标:字面量路由挪到冒号路由之前(Express 按声明顺序匹配)。 治本是把 :verb 改成 /verb,会动前端与文档契约,没在这一刀做。 新增 route-shadowing.spec 用文本扫描锁住顺序 —— 这个 bug 恰恰不会在类型 或运行时暴露,只能这么防。已验证:把 script-copy 挪回去,测试立刻红。 2) 复制按钮在 http / 宿主 iframe 下**点了毫无反应**。 navigator.clipboard.writeText 在非 https 或 iframe 里直接 NotAllowedError, 而原写法把 setCopied 放在它后面 → 不报错、不变文案,客服会以为按钮坏了。 加 execCommand 回落路径。⭐ 埋点挪到最前面,与剪贴板成败**解耦** —— 记的是"点了复制"这个意图。 剪贴板被拒时客服往往改成手动选中复制(真的用了),埋点不该跟着一起丢。 实测:点复制 → script_copy | wecom | 操作人 832 | 带批次号 t 落库。 1024 tests,两个 tsc + next build 干净。luoqi committed -
基于 docs/design/plan-assignment-doctrine.md 重写成产品视角,放进 docs app 新建的「设计 › 产品设计」分组。 与原教条文档的关系:那份是工程内部的决策留痕(含表名、字段、弯路记录、 踩坑复盘),给开发看;这份只讲**为什么这么设计**和**流程长什么样**, 去掉全部实现细节与代码路径 —— 讲给产品和业务听。 结构按设计思路重排(不按 T 编号顺序): 问题 → 全流程 → 这是什么 → 谁做什么 → 怎么选人 → 怎么落到人头上 → 确认单 → 闭环 5 张图代替长段落: ① 五步生产线(带反哺回环) ② 三方职责与唯一写动作 ③ 初选两轴矩阵 ④ 落人三趟(专属封顶 → 无主补空 → 有主改派) ⑤ 一条单子的状态机(含退回/到期/撤销三条回池路径) 保留了对业务最有说服力的两处真实数据:70% 挂在同一客服名下、 封顶前后 248/34/31/14/14 → 每人 20。 实测:docs 已渲染,5 张图全部成 SVG(4 flowchart + 1 stateDiagram), 无残留代码块;导航挂在「设计 › 产品设计 › 召回分配」。
⚠ ️ 新增 content 目录会让 Fumadocs 索引发僵(整树打不开), 清 .next/.source 重建才恢复 —— 老坑,已按记录处理。luoqi committed -
1) 不再另造生成按钮 企微视图里的「生成/重新生成」删掉,统一走顶栏那一个入口(与电话稿同一个)。 两个"生成"按钮=两条路径两套状态,必然不一致。 切到企微时隐藏档位下拉(只有深度档,摆一个单选下拉是误导);模型下拉保留。 2) 交互与电话深度档一致(过程可见) 加 GET :id/wecom-script:stream(SSE),事件形状与 script:stream 对齐 → 前端 ScriptDeepProcess 时间线 / 停止 / AIStamp 整套复用,不另画一套。 企微一轮跑 60s 上下,没有过程可见就是一个转圈白屏。
⚠ ️ 只推步骤不推正文增量:企微是一整块,逐字推只是闪。 3)⛔ 禁止输出任何时间占位符 电话稿留【时间段1】是对的(客服边打边填);企微这条消息是整段复制直发的 —— 占位会原样发到患者微信里,或者客服得先手动编辑一遍,"可直接发送"当场不成立。 改成不含具体时间的邀约(「您方便的时候回我一下,我帮您安排」)。 四处一起改才拦得住:format.md / verify system / verify user prompt / repair 铁律, 外加策略侧硬扫兜底(只靠 prompt 拦不住,实测模型照着电话档习惯写出来了)。 forbiddenWordsBlock 加 timePlaceholders 参数 —— 不然它那句「占位照旧保留」 会和企微 format.md 的「一个都不许出现」拼进同一份 system 打架。 踩的坑: · 企微 done 事件不带 costYuan → 外层 toast 直接 .toFixed() 整页崩(实测)。 两条流的 done 形状不一样,别假设一致。 · 步骤时间线渲染了两遍(外层已有一份,我在视图里又画了一份)。 实测:切企微 → 顶栏「重新生成」→ 步骤逐个亮 → 正文出,收尾是 「您看最近工作日哪天方便,回我一下帮您安排复查时间」,无任何【时间段】占位。 1020 tests(1 条并发计时测试在全量负载下抖动,单独跑通过,与本次改动无关), 两个 tsc + next build 干净。luoqi committed -
═ 沿用了什么、没沿用什么 ═ 共用(直接 import,
⛔ 不复制):ScriptContext / buildRichFactBlock 患者事实块 / 安全护栏 forbiddenWordsBlock / 福利硬约束 / 自报家门占位 / 医生姓脱敏 / 人群 skills / composeSystem 装配 —— 与"用电话还是企微说"无关。 复制的代价是护栏两处,改了一处另一处悄悄留旧版,而漏了哪条要等客服已经发给患者才发现。 自己写(draft-wecom-script/): · schema:电话 sections[](伴飞逐段高亮要它)→ 企微单块 markdown · format.md:口语/分段/口头二选一 → 书面/断行/可复制即发 · verify 多一条⑤「可直接发送」(无小标题、无占位残留、无给客服看的话、 无"您现在方便吗"这类需对方当场回话的电话句式) · plan 步语义改成"排要点顺序"而非"拆几段" ═ 几个关键取舍 ═ · composeSystem 加 formatPath 覆盖参数,⛔ 不往 ScriptTier 里塞 'wecom' —— tier 是质量档、渠道是另一维度,混进一个枚举后"深度档企微"就表达不出来了。 formatPath 一并进 composeHash(否则两个渠道 promptVersion 撞车,eval 数据混在一起)。 · plan_scripts 加 channel + 改 @@unique([planId,channel]),⛔ 不另立表: 状态机/source/agentInvocationId/聚合查询全一样,两张表必然漂。 · 企微**无模板兜底**,失败就是 failed。电话失败可以给模板(客服拿着电话必须有东西念, 平淡但不出事);企微是原样发给患者的,套话复制发出去比没有更糟,而且发出去收不回。 ═ 前端 ═ · 话术面板 3 视图切换(伴飞/卡片/原文)隐藏,那个位置改成渠道 tab 电话/企微; 电话固定「原文」渲染(视图代码整套保留,开关可恢复) · 企微视图单块 + 复制按钮;whitespace-pre-wrap 原样呈现,⛔ 不过 markdown 渲染器 —— 客服复制的必须是他看到的那些字 · 执行结果的「触达方式」选择器隐藏(⚠ ️ 期间 channel 全落 'phone',触达方式分布不可用) ═ 踩的三个坑 ═ 1. __dirname 拿不到 format.md(ENOENT):SWC dev 产物在 dist/src/,tsc prod 在 dist/, 同一个 __dirname 指向不同层级。照抄 resolveScriptRoot 的 cwd 策略。 2. serializeScript 会把正文拆成 sections 并丢原文 → 企微稿 content 长度 0。 企微单独序列化,不复用那个。 3. ai.module 的 exports 没加(正则打在了 providers 上,那里有同样的三行序列)→ Nest 启动时 UnknownDependenciesException。 实测:真库生成一份企微稿(单块、空行断段、无小标题、收尾是"您回我一下方便的时间就行" 而不是电话的口头二选一),界面 tab 切换 + 复制按钮 + 【回访客服】按登录人回填全部正常。 1020 tests,两个 tsc + next build 干净。luoqi committed -
1) 左栏「真」号码筛选 + 「筛选标签」隐藏 开关 RAIL_FILTERS_VISIBLE=false,置 true 即恢复;PersonaTagFilter 与服务端 phoneVerified 谓词都留着,不是删除。
⚠ ️ 藏的只是入口:realPhoneOnly / tags 初值都是"不筛",所以请求里不会残留条件。⛔ 别改它们的初值,否则会出现"看不见的筛选"——列表少人而主管找不到原因。 2) 详情页号码角标:只在已核实时打「真」,未核实**不打标** 原来给未核实号打「假」,而那是绝大多数 —— 整页每个患者都挂着「假」,既是噪音, 又在客服正要拨号的那一秒告诉他"这号是假的",平白制造犹豫。 「未核实」≠「假」:只是外部对照表里没有,号码本身可能完全正常。 3) 列表电话明文(PlanPatientBrief 加 phone) 脱敏号 138****2417 客服拨不出去,等于每次都要点进详情再拨。⚠ ️ 不是新开暴露面:详情页早就明文下发同一个号(plan-aggregate.phone), 两处同权限同 tenant scope。⚠ ️ phoneMasked 保留:配了原始档案(VIEW_PATIENT)的宿主整行隐藏手机号, 两个字段都不显示 —— 别因为加了明文就把脱敏删了。 1020 tests,两个 tsc + next build 干净。按你说的没进浏览器调试。luoqi committed -
前一刀(跳转前必答)让 plan_executions 有数了,这一刀把它接进批次报表。 1) plan_executions 加 assignment_id 理由与 plan_event_logs 那次一字不差:followup_plans.assignment_id 会被下一次 分配覆盖,时间窗是启发式不是键。
⚠ ️ 落这一列的判据是 assignment_expires_at != null(「非空 ⟺ 有一次在办的分配」),⛔ 不是 plan.assignment_id 非空 —— 后者在退回后仍留着(那是退回率的分母), 此时客服自己捞回来打的电话会被算进一个早就结束的批次。 2) detail 增加 outcomes:success / failed / keep / noOutcome / byOutcome / note · 每个患者只取最近一次执行(DISTINCT ON) —— 打三次才约上是一个成功不是三个 · noOutcome 单独成桶:那不是效果差,是根本没做/没记。并进 failed 会把执行问题 读成召回问题,而两者要改的东西相反 · 四桶穷尽 success+failed+keep+noOutcome === planned 3)⛔ outcomes 与 releaseReasons 不能互相顶替(工具描述里也写死了) releaseReasons = 没打就还回去(分配问题);outcomes = 打了结果这样(召回效果问题) 写这一版时踩了一次:noOutcome 里多减了 reassigned → 被压成 0。 「现在归谁」与「做没做」是正交维度,不能放进同一个减法。已加回归锁住。 实测(真库):3 人批次打 refused + no_answer → 执行带批次号落库 → 详情读出「成功 0 · 不成功 1 · 打了没进展 1 · 一次结果都没有 1」,四桶合计 3 === planned 3, 明细「明确拒绝 1」,note 自带样本不足与「成功含约定下次回访」两条护栏。 1020 tests,两个 tsc 干净。luoqi committed -
═ 为什么 ═ 这两个按钮跳走之后客服基本不回来 —— 这正是 plan_executions 回写率只有 11% (生产 65 个认领单只有 7 条结果)的根源。回写率低到这个程度,"成功率""退回率" 全是猜的。⇒ 把一次必答的极简输入挪到跳转之前:人还在 PAC 里、手还在键盘上, 那一刻才录得到。 ═ 做了什么 ═ 1) 吸附在按钮上的小浮层(Popover),各只收**一个必填**: · 预约 → 备注 → 落 success_appointed(结案 + 60 天抑制) · 跟进 → 下次时间 → 落 scheduled_next(不结案,snooze 到那天) 多一个字段都不行 —— 这是拦在人和他真正想做的事之间的门,门越重越会被绕开。 2) scheduled_next 的 group: keep → close(「保持」→「成功」)。
⚠ ️ drivesStatus 保持 'keep' —— group 与状态机在这一项上**故意不一致**: 算成功是统计口径,工单必须留着 snooze 到回访日。改成 completed 的话, 客服刚答应患者"那天再联系您",这单当场出池,约好的回访再也不会发生。 面板分组是读 meta 现算的 → 「保持」组自动只剩未接通/秒挂,历史数据一并重归类。 ═ 几条纪律 ═ · **先落库、后跳转**。反过来的话写失败人已跳走,他会以为记过了。 · URL 未配置时**先拦住**,⛔ 不先把单结了再告诉他跳不过去。 · channel 记 'other',⛔ 不许默认 'phone' —— 我们不知道他怎么触达的,编一个会把 触达方式分布做脏。 · 浮层里写明后果(「记为转化新预约并结案」)—— 不写就是让他在不知情下关掉工单。 实测(真库):跟进 → scheduled_next/other/2026-08-12 落库,plan 仍 assigned + snoozed_until=回访日;预约 → success_appointed + 备注落库,plan → completed。 未配 URL 时确认不落库、浮层保持打开。新增 taxonomy 回归锁住 group/drivesStatus 分离。 1013 tests,两个 tsc + next build 干净。luoqi committed -
开关 HEADER_PRIORITY_VISIBLE=false,置 true 即恢复; 代码整段保留(含 hover 的算分明细),不是删除。
⚠ ️ 只关头部这一处。左栏每行的优先级分数不动 —— 那是主管挑人的排序依据,隐藏了他就没法判断先跟谁。 实测:头部已无「优先级」;左栏分数 7.38/6.78/6.70 完好。tsc + build 干净。luoqi committed -
召回池每行都写「待分配」、我的每行都写「进行中」—— 整列同一个词, 读者第二行就不再看它,却一直占着行尾最值钱的位置(退回按钮就在那儿)。
⛔ 但不能把药丸整个删掉:「我的」不带 status 过滤(where.assigneeUserId = 我), 已完成 / 已放弃的单也在这一栏里,而那两个才是真要一眼看见的。 删掉等于让客服分不出哪条已经结案。 ⇒ 按「这一行是不是本视图自身的定义」判断: pool → 隐藏 active,mine → 隐藏 assigned,其余照常显示。 两栏处理还不一样: · 召回池没有退回按钮 → 默认态**整个不渲染**(留 42px 空白纯属白占, 而右侧正是手机号/诊所名要用的地方) · 我的有退回按钮,药丸与它叠在同一 grid 格、格宽取较大值 → 默认态用 invisible **占位**,否则 hover 前是 0 宽,按钮一冒出来整行位移, 而那个叠层机制存在的全部理由就是防这个 实测:召回池 25 行 0 个药丸;我的 22 个药丸全 invisible、占位 42px, hover 姓名位移 0(退回按钮 37px 落在占位内)。tsc + build 干净。luoqi committed
-
- 03 Aug, 2026 29 commits
-
-
上一刀把 released 计数改成账本口径,但**分布和名册还读 followup_plans**, 于是自己跟自己对不上: followup_plans.release_reason 是当前值,而 assign 的 SQL 里有 release_reason = NULL → 一条退过的单被后面的批次挑走后: released 1 (账本) releaseReasons [] (列已被清空) 主管看到"退了 1 条"却没有任何原因,会以为是客服没填。实测复现。 更糟的是 agentStats 的名册按当前 plans 建 —— 被挑走的人从所有人名下蒸发, 「各人之和 === planned」这条本仓最早锁的不变量当场破(而 planned 已是账本口径)。 修法:名册与退回都从本批账本事件建(按 assignment_id 取,不是按当前 planIds)。 · release 事件自身 assigneeUserId 是 null → 按 planId 回查 assign 的原始承接人 · 同一 plan 多条 assign(退回→自认领→再退回)去重后计数 · 老批次(账本没批次号)整条回落到原路径,行为不变 实测(真库):把退过的那条重新分配一次 → 列被清空、账本仍在 → 分布之和 1 === released 1;agentStats 各人之和 9 === planned 9。 1008 tests,tsc 干净。luoqi committed -
主管四问引出的一组修复。 1) 归因失血(不报错的真 bug) followup_plans.assignment_id 会被下一次分配覆盖 —— 一条单退回/到期回池后 被后面的批次挑走,旧批次就少一个人。批次跑得越久缩得越厉害。 实测 c02e1b80 分过 9 条,重分后 planned 显示 0。与 supersededAt 那坑同构。 ⇒ plan_event_logs 加 assignment_id(不建明细表:账本本来就是明细, 缺的只是"属于哪一批"这个维度;该表自己写着"要按它筛就该立柱")。 四个写入点全覆盖;claim 刻意不写(自认领捞的是池子,plan 上那个 id 是陈迹)。 ⇒ planned/agents/released/expired 走账本,inHand/done 走当前状态。 ⇒ 补 progress.reassigned 让穷尽性重新成立(不补则主管一对数就少人)。 ⇒ 老批次回落到 followup_plans 现算;到期数无源可落,只能给 0。 2) 到期终于能统计 到期事件一直在账本里(auto_release + assignment_expired),但任何接口都没报, 全被 backToPool 一桶吞掉。而到期与退回主管的下一步动作相反: 退回多=分配策略不对,到期多=派多了/时效太紧。现在分开报,note 也分开说。 3) overdue 加 snoozed 守卫 回收器刻意跳过"约了下次回访"的单,于是它们永远超期;而 progress 已把它算作 suppressed(已处理)。不加守卫,同一条单「已处理」和「超期」同时成立 —— 主管看到「薛玫 超期 3」以为她压单,实际她打了电话约好了下次。 4) 批次人话名 label(服务端唯一生成) 「8/3 23:35 · 牙周治疗 · 窗口内 · 9 人 · 2 位客服」。没有列表页时这是主管 指认一批的唯一抓手。做这个才发现 temperature 一直没进 criteria 快照 —— 它参与圈人却没随确认单下发,导致批次说不清"当时按哪个温度圈的"。已补。 顺带修一个潜伏编译错误:assignmentExpiresAt 加进 FollowupPlanSchema 时漏了 serializePlan(19bd658f)。当时没报错是因为 packages/types/dist 是旧的。 实测(真库):新批次 9 人 → 撤销 → 同一批人重分 → 旧批次 planned 仍是 9 (旧实现下是 0)、五桶+重分 0+0+0+9=9;退回 1 条、到期 2 条(等回收器真扫) 分别落账,详情读出「客服主动退回 1 条、到期没人动 2 条」。 1005 tests,两个 tsc + next build 干净。luoqi committed -
走查两条: 1) loading 文案闪 - 加最短停留 700ms(useStickyLabel)。get_current_user / get_agents 这类 100ms 就返回,文案在屏幕上活不过两帧,主管只看到一片闪烁,反而以为卡了; 慢工具的文案看得见,于是他以为"只有那一句"。 用「最新覆盖待显示」而非排队 —— 排队会在活干完后继续播,5 个快工具能 拖出 3 秒假忙碌。上限就是一个停留周期。 - 修并行调用:原来只看最后一块,3 个工具并行时最后那块可能已 done 而前两个 还在跑 → 退回「生成中」,前两句文案一次都不出现。改成找还在跑的那个。 - 补齐 4 个漏掉的工具中文名(get_cohort_attributes / edit_assignment_sheet / revoke_assignment / render_artifact),此前兜底把英文工具名摆给主管看。 实测(多工具一轮):5 个文案,最短 955ms,无一低于 700ms。 2) 输入框占位符 + 开场建议是客服口径 主管打开助手是来分配和看批次的,占位符从不提这两件事 = 把一半能力藏起来。 按 PLAN_DISPATCH 分两版(与召回池/确认单同一个闸)。 顺带删掉「(Enter 发送,Shift+Enter 换行)」—— 输入框只有 300px,而原句 450px, 那半句从来就没显示出来过,占着长度却看不见。 996 tests,web tsc + next build 干净。luoqi committed -
主管实测:助手撤销必挂,报「批次 c02e1b80 不存在」。 根因不在撤销,在确认后注入消息流的那句话: `已确认分配:批次 #${id.slice(0,8)}` —— 界面与模型共用这一份, 模型手里只有 8 位短号,拿短号查主键当然查不到。 卡片上的撤销按钮握着完整 id 一直是好的,所以只测卡片那条就漏了。 修法两层: - 文本块加 modelText:界面显示一份、喂模型一份(后者带完整 uuid); 界面顺带不再显示批次号(uuid 对主管没有意义) - 服务端 resolveAssignmentId 兜底认短号,并能从「批次 #c02e1b80」 这种整句里抠出 id(实测模型就这么传);撞到多个报错不猜 追查中发现更严重的一个:助手会把「已撤销批次:收回 9 条。」**背出来** —— 没调 revoke_assignment,库里那批仍是 confirmed、9 条工单一条没动。 工具返回被设计成"成品句子、原话转述",模型学会形状后就能凭空生成。 报错主管会重试,假成功会让他停止补救。 ⇒ 前端每轮结束对账:主管在要求撤销 + 助手自称撤成了 + 本轮没有 revoke_assignment 的 tool_result ⇒ 把事实贴出来。 措辞只陈述观察(不断言撒谎):主管问"刚才那批撤了吗"时如实回顾也不调工具。 实测:短号经模型这条路已跑通(a7e1b6de → revoked,9 条回池无认领人)。 996 tests,两个 tsc + next build 干净。luoqi committed -
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
主管问"有撤销吗、前端实现了吗" —— 后端(限时 30 分钟 + view 判据)和助手工具 revoke_assignment 都在,但**前端零实现**:assignmentsApi 连 revoke 方法都没有, 批次列表页也不存在。先做 A(卡片上的入口),批次列表页单独排。 · 确认后的卡片终态加「撤销这批」,30 分钟窗口内可见(按**确认那一刻**算,
⛔ 不是提案生成时间 —— 主管可能盯着单子改了二十分钟才点确认,那不该算进补救窗口), 到点按钮自己消失,⛔ 不让他点了才被服务端拒。 · 弹窗**先说「客服已经打开过的收不回来」**:撤销几乎总是部分成功, 主管以为"点了就全回来了"就会停止补救,而那几条还挂在别人手上。 结果如实报三个数(revoked/skippedTouched/alreadyReleased),⛔ 不只弹「已撤销」。 note 原话进对话(既给主管看,也补模型上下文)。 ·⛔ 去掉「· 批次 #9a9fc222」:uuid 前 8 位对主管没意义,只占位置 + 制造记忆负担。 模型照旧走 list_assignment_batches 拿完整 id,与界面无关。 ·⭐ **助手撤销时卡片跟着切态**:revoke_assignment 是 MCP 工具推不了侧信道, 但前端本来就收得到它的 tool_result —— 拿它当同步信号,按 assignmentId 精确匹配。 不同步的话卡片说「已分配」、助手说「已撤回」,两句互相打架。🔴 顺带修一个**我自己上一版引入的回归**:`key={i}` + 「把确认单钉到消息末尾」—— 助手每追加一句文本,sheet 下标就变 → React 卸载重建 → **卡片本地状态全丢** (主管删的条、拖的改派、逐条时效、确认时刻)。症状是"撤销按钮压根不出现", 但丢的远不止那一个按钮。改成按 requestId 做 key。 本地实测:分 17 条 → 卡片出现「撤销这批」→ 弹窗 → 确认 → 「已分配 已撤销 17 条」;库里 plan_assignments.status=revoked、 plan_event_logs 落 17 条 auto_release/revoked。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
主管问"现在有时效了,是不是可以显示以前隐藏的自动回收时间" —— 查下来 **不是取消隐藏,是要接一条新的**:那个被隐藏的倒计时基于 `recycleAt`(认领的 24h 兜底, PAC_PLAN_AUTO_RECYCLE 默认 off,**从未启用**,值几乎恒为 null), 而现在真正生效的是 `assignmentExpiresAt`(主管在确认单上定的 1-7 天, AssignmentExpiryScheduler 每 10 分钟扫,到点回池记 assignment_expired)。 两个字段一直被混为一谈,详情接口**根本没返回后者**。 · 服务端 /plans/:id/full 补 assignmentExpiresAt;契约里给两个字段各写清适用场景。 · 前端 RecycleCountdown 换成 ExpiryBadge 并挂回头部,只在**单子还挂在人手上**时显示。 ·
⚠ ️ 粒度跟着换:原来是 HH:MM:SS(为 24h 设计),而时效是**天**级 —— 3 天的单显示 71:59:58 没人读得出来。改成「还剩 2 天」/「还剩 5 小时」, 最后 1 小时才精确到分钟;刷新从 1s 降到 30s(天级不需要每秒重渲染整页)。 ·⚠ ️ 配色:最后一天转琥珀、最后一小时转红。⛔ 不更早变红 —— 时效本来就是几天, 提前两天飘红会让客服对颜色脱敏,真到期时反而看不见。 本地实测(客服「位其蕾」):头部显示「还剩 3 天退回」,hover 出具体到期时刻。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
走查:分配完登录客服账号点「退回」,没有任何弹窗 —— 一点就退掉了。 查下来这条教条**此前是零实现**:枚举齐了(8 个 ReleaseReason + needNote)、 API 收得了、releaseReasons 分布报表也写了,但前端唯一的退回入口是 `plansApi.recycle(planId)` —— **一个参数都没传**。于是那张分布表永远拿到 null: 主管只知道"退了 12 条",不知道该改什么,而"派多了 / 时效太紧 / 压根不该派给他" 这三种的下一步动作完全不同(T7 说这张表是主管调整分配策略的输入)。 · 新增 ReleaseReasonDialog:8 个原因,每个**带一句 desc** ——「不是我的客户」和 「该由其他角色跟」光看标题分不出,分不出就会随手点第一个,**假数据比没数据更难发现**。 other 展开必填说明框,不填不给提交。 · 服务端:**分配来的单**(assignment_id 非空)强制要 releaseReason。
⚠ ️ 只卡分配来的 —— 自助认领的单没有派单方,原因对谁都没用; **系统自动到期回收不走这条路**(scheduler 直接改库,记 assignment_expired),不会被卡死。⚠ ️ 必填必须服务端拦:只做前端的话,换个客户端(助手/脚本/企微)就绕过去了 (与 other 必填说明、execution 的 inaccurate 必填同一条纪律)。 本地实测(客服「位其蕾」12 条在手):点退回 → 弹窗 → 选「其他原因」出必填框且确认禁用 → 改选「时效太紧」→ 确认 → toast「已退回」、我的 12→10、 followup_plans.release_reason=deadline_too_tight、 plan_event_logs 落 release/deadline_too_tight 且带 held_seconds=2839。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
① **「没被分过的排在前面」改成条件化**。查了口径:池子基线已经排除"还在客服手上"的单, 分过但已退回/到期回池的**仍在池子里**(T7:退回是正常路径,不能分过一次就永不再召), 只是排到后面。本地实测池子 1094 人里回池的 **0 人** —— 这时说那句是废话, 还会让主管以为系统在防一个不存在的情况。 现在:有回池的人才说「之前分过又退回的 N 人排在后面」,否则只说「优先级高的排在前面」。 countCandidates 顺带 FILTER 出这个数,不多一次查询。 ② **确认单等这段话说完才出现**。上一版只做了"钉到消息末尾",但流式期间卡片已经在下面, 主管看到的是"卡片先蹦出来、文字再在它上面一行行长" —— 比卡片在前更怪。 现在流式期间不渲染 sheet,说完再出现。 ③ **引导语补两条**(主管不知道能这么用,不说他就只会点确认): · 再挑一挑人:「只要商保直付的」「排掉最近来过的」→ 助手先看分布再重新圈; · 带个福利:「这批带上『老客户复查免挂号费』」→ 新增 set_benefit,写进 attributes.benefit.text。 福利链路本来就通(话术 prompt 已接),缺的只是主管怎么填进去。
⚠ ️ 福利在卡片上**只读**(有才显示一个绿标):在 400px 卡里放自由文本框, 主管十有八九写成一段带承诺的话,而那道护栏在话术生成侧,别污染源头。⚠ ️ 引导语要求用主管的话说,⛔ 不许报工具名。 本地实测:说「这批带上「老客户复查免挂号费」」→ 卡片顶部出现「福利 · 老客户复查免挂号费」 绿标,界面回报那句也进了对话。989 tests green。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
走查:那段输出像给程序员看的 ——「按『未进过批次优先 → 优先级高优先 → 患者号』排序」 「从手上最少的开始填」「首次按在岗 17 位 × 20 估」,而且卡片在前、文字在后。 ① **顺序:文字在前,卡片在后**。
⚠ ️ 不是"让模型先说话"能解决的:工具调用**必然在文字之前**(模型要先拿到结果才知道说什么), 而卡片是工具执行时经侧信道推来的,到达顺序天然是"卡片 → 文字"。 所以在**渲染层**把 assignment_sheet 块钉到消息末尾(stable 排序,⛔ 不整体 sort —— 文字块之间的先后是流式拼出来的,打乱就成乱码)。 ② **措辞改成说给主管听**: · 怎么选:「符合条件的一共 1084 人,这批挑了 340 人:没被分过的排在前面,其次是优先级高的。 剩下 744 人在排队,随时可以再分一批。」 · 怎么分:「340 人分给 17 位客服:有专属客服的先回到自己人手上,剩下的谁手上活少先给谁。 分完后每人手里 20 条,3 天没动会自动退回池子。人数和时效是第一次的估算值……」⛔ 「排序键/收敛/铺平/水位/基数/探索配额/患者号」一律不进主管界面 —— 加了断言守着。⚠ ️ T14 要的三件事一件没少,只是换了说法:分完每人多少、这两个数哪来的、精调过的人点名。 ③ **内容收敛到「怎么选 + 怎么分」两句**,末尾加一句调整引导 (「把某某移出这批」「某某的单给 5 天」「这批 200 人」—— 我直接改)。 名册那句(近 12 月有回访记录的近似判定)不再单独占一条 bullet,rosterNote 字段保留。 本地实测通过,助手实际输出已核对。989 tests green。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
走查实录:主管说「把杨丽华移出这个确认单」,助手回"我这边没法单独剔除,您先点确认、 之后在批次详情里单条退回" —— 把界面能做的事推回给主管手工做,而卡片就在他眼前。 新增本地工具 edit_assignment_sheet(不走 MCP,同 propose_assignment): 能做的与卡片完全一致 —— 删患者 / 移除客服 / 改派 / 改时效(整批、按客服、按患者)。
⭐ **指令用姓名,不用 planId**:那些 id 从来没进过模型上下文(确认单走侧信道,故意的)。 模型负责"听懂",**界面拿自己手里那份确认单去匹配**。为了让模型能指名道姓而把明细 喂给它,等于把当初走侧信道的理由(几百个 uuid、幻觉、成本)全部退回去。⚠ ️ **结果由界面回一句进对话**(复用 appendAssistantNote),⛔ 模型不许自己宣布成功 —— 它看不到卡片,匹配不到/同名/客服不在本批都是界面才知道的事。那一句同时进模型上下文, 下一轮它才不会替系统撒谎(T14)。工具返回值里也写死了"等界面那句再说"。🔴 修两个实测抓到的坑: · 指令挂载**必须跨消息找**最后一张确认单 —— 卡片几乎总在上一轮消息里, 只在当前消息里找永远找不到,而且不报错、界面毫无反应; · **同名歧义不许猜**:实测「张悦」既是本批客服又是本批患者,模型选了 remove_agent, 于是"把张悦移出确认单"变成把客服连同 20 条一起移除。现在这种情况直接拒执行并说明, 提示词也定死默认按**患者**理解(猜错的代价不对称:一下子动 20 条 vs 1 条)。 · 卡片底部那块只读的「人数 本批 340 人 / 要改说…」撤掉:只读信息不该占跟可操作区 一样重的位置,它想说的两件事都有更好的落点(顶部数字 + 现在可以直接让助手改)。 本地实测:对话说「把张悦移出这个分配确认单」→ 助手调工具 → 界面执行并回报 → 确认按钮 340 条变 320 条。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
产品走查四条: ① **输入框加回边框**(上一版"无边框"太散,找不到落点):外层仍与会话同底、无分隔线, 输入块自己有边框。同时补一个**「回到底部」**圆钮 —— 只在没贴底时出现,浮在输入框 正上方居中(对齐 Claude Code)。
⚠ ️ 判据复用 onScroll 里那个 80px 阈值,⛔ 不另写一个: 两个阈值不一致会出现"按钮还在、其实已经到底"这种解释不清的状态。 为此把 stickRef 加了个可渲染副本 atBottom(ref 改了不触发重渲染)。 ② 患者条目补**性别·年龄**,与患者详情卡片同口径(formatGender + calcAge)。⚠ ️ 年龄**读时算**(存的是 birthDate),两处各写一遍必然在生日当天差一岁。 ③🔴 **拖拽自动滚动**:HTML5 DnD 不会自己滚容器,助手窗一屏只放得下三四个客服, 拖到边缘就卡住 —— 想丢给下面的人基本不可能。现在拖动中指针进入上/下 56px 感应带 就按 rAF 持续滚。⚠ ️ 滚的是**离卡片最近的可滚动祖先**(运行时找),不是 window: 写死 window 在最大化/非最大化两种形态下会滚错东西。 ④ 卡片底部那三句(selectionNote/basisNote/rosterNote)撤掉展示 —— "规则拼成的,用户不需要知道"。⚠ ️ 三句话没删:助手在消息正文里已经原话转述过一遍, 卡片再贴一次是同一段出现两遍。⛔ 服务端那三个字段别删,助手要靠它们照抄。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
· 助手每条消息左边那个 28px 圆头像撤掉:助手窗只有 400px,头像 + 间距白白吃掉 一成宽度,而"这段是助手说的"靠气泡形态(用户右对齐深底 / 助手左对齐无底)已经分得清。 头像只留在 header 和空态引导页。 · 底部输入区改成与会话区**同底色、无边框、无分隔线**:去掉 border-t、外层白底、 内层的圆角边框与 focus ring。
⚠ ️ 底色用 `bg-inherit` 而不是写死 bg-slate-50 —— 写死的话根容器换底色时这一块会漂。 · 去掉「结果仅供参考 请核对后使用」那行免责声明。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
🔴 **点客服删除会卡死** —— 根因与上一条下拉看不见同源,但更严重: 助手窗 fixed z-[60],而 shadcn 给**所有**浮层的默认层级是 z-50。 · Select/Popover/DropdownMenu/HoverCard:点了有反应(DOM 里 listbox 确实打开), 但一个选项都看不见; · Dialog/AlertDialog:Radix 会给 body 设 pointer-events:none —— **弹窗看不见、 整页却已经被锁住**,用户看到的就是卡死。实测确认:alertdialog=true、 overlayZ=50、bodyPE=none。 两次都**不报任何错**。这次在 components/ui/* **统一修**而不是在调用处打补丁: 浮层 z ≥ 70(select/popover/dropdown-menu/hover-card),模态 z ≥ 80(dialog/alert-dialog), 六个文件顶部都写了层级约定,免得下次 re-generate 或新组件又掉回 50。 · 批次时效移到卡片**右上角**,与左边「拟分 N 人」同一层级(它是这一整批的设置); 文案改成「3 天**后自动退回**」—— 光写"时效"主管不知道到期会发生什么。 · 底部微调区只留人数 + 出处说明(沿用哪次/首次默认/本次指定 + 几条单独设了时效)。 本地实测:点康慧捧的 X → 弹窗正常显示 → 移出本批 → 340/17 位 变 320/16 位 + 「已移除 20」。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
产品走查:时效应该落到每一条召回计划;条目要能删、能拖给别的客服;客服要能移除; 整体用 shadcn 组件承载。 · 布局全部换成 shadcn:Card / Badge / Button / Select / AlertDialog。 · **时效逐条**:每条召回计划一个 1-7 天下拉,批次级那个只是"没单独设过的默认"。 落库时只在与批次不同时带 expiresInDays(一样也带 = 把批次时效冻进每条, 将来批次层面改时效就改不动这些单子了)。 · **单条删除** + **移除客服**(AlertDialog 二次确认,明说"他名下的条目一并移出本批")。 · **拖拽改派**:原生 HTML5 DnD(
⛔ 不引 DnD 库),整个客服块都是投放区 —— 展开后列表很长,只认标题行会让人反复试。拖过的落库时 assignStrategy 改成 **manual**, 不改的话事后分析会把它算成算法的选择,而 T20 要反推的正是"算法选得准不准"。 · 所有展示改从**生效条目**派生(sheet.items − 删掉的 + 改派 + 逐条时效),⛔ 不再直接用 sheet.byAgent —— 那是提案时的分组,拖过一条就对不上了。🔴 修一个"改了不报错、但功能等于没有"的坑:助手窗 fixed z-[60],而 shadcn SelectContent 自带 z-50,Radix 会把 content 的 z 复制到 body 上那层 popper wrapper → 下拉渲染在助手窗 **底下**:listbox 确实打开了(DOM 里查得到)、但一个选项都看不见。实测才发现,已抬到 z-[70] 并写进教条(凡在助手窗里用 Radix 浮层都要抬 z)。 本地实测:删一条 → 薛玫 20→19 且在手 0→19;改一条时效 3→5 天只影响那一条; 下拉 1-7 天全部可见可选。989 tests green。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
产品走查三条: ① 患者明细只有 `#912e5dae` —— 主管展开客服看的就是"这 20 个人是谁", 给一串十六进制等于让他猜(T14)。ProposedItem 加 patientName / medicalRecordNumber, 服务端把 dedicatedCsOf 合并成 patientsOf 一次取齐(同一批 id 同一张表,分两次纯属多一个来回)。
⚠ ️ 身份在**落人之后**贴上:placeAgents 是纯算法,让它认识"患者叫什么"只会更难测。 ② 时效 3/5/7 三档 → **1-7 天下拉**(批次级 + 按客服两处同一个控件)。 档位是我们拍的,主管对自己团队的节奏有判断(「明天就要」= 1 天)。 顺带确认:时效**早已落到每条召回计划**(assignment_expires_at 逐行写), 不是只挂在批次头上 —— 卡片上补了一句说明。 ③ 汇总行的「铺平 N」标撤掉(看不懂)。明细里只标「专属」,没标的自然就是不是他的老客户 —— 同一个信息换成主管的语言。⚠ ️ assignStrategy 仍逐条落库,T20 反推不受影响 (T15 要的是"可见",不是"必须以这三个字出现在汇总行")。 本地实测:展开康慧捧 20 条全带姓名+病历号+专属标;薛玫 20 条全是铺来的,无标。 989 tests green。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
主管走查:一批 340 人分布是 248/34/31/2×14,完全不齐。查了数据,根因是 专属关系极度集中 —— 池子 1,081 人里 755 人(70%)挂在同一个客服名下, 而 17 位在岗中有 10 位名下一个患者都没有,自由患者只有 119。 上一版的硬约束(客服不能接别人的专属患者,给不下就 unplaced)让"满"和"平" 不可能同时成立:严格齐平上限是每人 9 条、一批只有 153 人。 按产品意见改成**优先级**而不是禁止: ① 专属:分给他,但**封顶在目标水位**((团队在手+N)/在岗人数,由 N 推出,非新旋钮) ② 无主补空:无主/专属已离岗的患者给最空的人 —— 拿他们填坑零代价 ③ 有主改派:无主的用完还没填平,才把超出水位的专属患者改派出去,标 spread_overflow
⚠ ️ ②③ 的先后不能颠倒:两趟都是水位法、总量一样,但**拆散的专属关系数不一样**。 合成一趟会随机改派某个有主患者,而同时某个无主患者落给了别人。 实测同一批:248/34/31/2×14 → **每人 20 条,17 位完全齐平,340 条一条不丢**。 spread_overflow 枚举复活(硬约束那版里它"不再产生"),别当死枚举清掉。 989 tests green;教条把两条弯路都记了进去。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
① 基数 N 的取值改成产品定的算法: 首次 min(在岗人数 × 20, 候选总数),之后 min(上一次的 N, 候选总数),时效首次 3 天后沿用。
⚠ ️ 与候选取小压低的是 target,⛔ **不回写基数** —— 候选不够是这一批的偶然事实, 存进去的话主管点一次 44 人的小格子,以后所有批次永远 44 人,而他看不出为什么。 所以确认单同时给 batchSize(基数)与 target(本批实际),卡片存前者。⚠ ️ fetchLimit 改按基数算,让 count 与取明细还能并发,不多一个来回。 ②🔴 硬约束:**客服不能接别人的专属患者**。有专属(且在名册内)的患者只有一个去处, 给不下就落不下(unplaced),不允许溢出给第三个人。 · 推翻原「专属已满 → 溢出转铺平」,SPREAD_OVERFLOW 不再产生(枚举保留,老批次在用); · 两趟分工改为「专属独占 → 无主患者水位法」,后者负责抹平前者造成的不齐; · maxThisBatch 精调压过专属:说了只给 5 条,第 6 个她的专属患者就落不下,⛔ 不许改派; · 上一版给专属加的"目标水位封顶"与本约束冲突(会把专属患者改派给第三人),撤掉。⚠ ️ 直接后果,已在教条里写明:「最后齐平」变成尽力而为。实测 N=340 时康慧捧 一个人是其中 248 人的专属,他就拿 248 条 —— 那些患者只有他能接。 调节手段是给他设 maxThisBatch 或换更小的 N,⛔ 不是让系统偷偷分给别人。 本地实测:1,081 候选 → 首批 340 人(17×20),池子里还有 741 人排队。988 tests green。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
主管走查抓到死路:批次规模 = Σ(容量−在手) 时,**第二批必然是 0** —— 第一批把所有人填到水位,再算就是 Σ(20−20)。想连着圈两批人分,只能把容量 往上棚,而容量会被记住 → 下周的"习惯"是个虚高的数。 根子是一个数当了两个用:「这轮推多少」和「一个人最多压多少」。拆开后发现 上限那一半根本不需要 —— 负载不需要阈值,inHand 本身就是负载。 · 基数改为 **本批人数 N(首次 100)+ 时效(3 天)**,都沿用主管上一次的值; 容量、AGENT_CAPACITY_RANGE、capacityRange 全删。 · 落人第二趟由「轮转均分」改 **水位法**:每条都给当前在手最少的人。 均分是"每人加一样多",起点不齐终点还是不齐;水位法把差距抹平。 ·
🔴 **专属那一趟也受目标水位约束**(=(团队在手+N)/在岗人数,由 N 推出,非新旋钮): 容量删掉后没东西约束专属了,实测 100 条里 76 条是同一人的专属,他吃到 76, 水位法只剩零头可铺。加约束后同一批变成每人 5~6 条。 超出份额的专属转 spread_overflow(关系还在,这轮没轮到),语义同原「专属已满转铺平」。 · 按客服精调 capacity → maxThisBatch(本批名额,0 = 这轮不给),不再是"上限"。 · 沿用同时认新键 batchSize 与老键 target —— 否则改版后所有人的沿用静默退回默认。 · 卡片「已满未分配」改「本批未分到(在手 N)」:没有上限了,写「已满(0)」自相矛盾。 本地实测:1,081 候选 → 本批 100 人铺给 17 位客服,分完后每人在手 5~6 条, 池子里还有 981 人排队(话术明说"随时可以再分一批")。986 tests green。 教条 T22 整节重写,把这条弯路记进去。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
① 容量无上下限:原来卡在 20-50。那是主管对自己团队的判断,系统没有任何数据 能证明 5 太少或 200 太多(全生产 plan_executions 仅 7 条),拿一个同样没依据的 区间去卡他,只会让他撞上一个解释不了的墙。只校验正整数。 连带撤掉名册接口的 capacityRange —— 返回区间会被读成「合法范围」,而它从来不是。 ② 两个基数从"整体一个值"扩成"整体默认 + 按客服精调"(agentOverrides): · 按客服**容量**:改的是人数(Σ 容量−在手)→ 会换人群 → 走对话让助手重出单; · 按客服**时效**:不改人群 → 卡片上展开那一行直接改,落到他名下每条任务的 assignment_expires_at(写路径早已支持逐条覆盖)。 精调与基数一样沿用,所以卡片标「精调」+ capacityNote 点名 —— 一条上个月的临时精调静默沿用三个月,没人会发现。⚠ ️ 精调必须穿到落人层:placeAgents 从收单个 capacity 改成收 capacityOf(userId)。 只改展示不改 room,李莉照样被分满 20 条而卡片上写着 5 —— 不报错,要等她抱怨。 已加回归并验证:改回单值该用例即红。 本地实测:薛玫单独设 7 天 → 行上出现「精调」标,整批仍 3 天;总人数不变。 教条 T22 补精调与无上下限两节。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
批次规模不再是一个独立的默认常量(100),改成由容量推出来: 本批人数 = Σ max(0, 容量 − 该客服在手) 这实际上是**恢复**裁决表里一直写着的「拟分 N 人 = 默认分满容量」—— 代码里那个 DEFAULT_BATCH_SIZE=100 与它矛盾了两周,且注释还引 T5 给自己背书 (T5 的「100 人」是举例,约束的是容量该拍多大,不是批次规模)。常量删掉。 · 容量首次默认取**下界 20**(原来取上界 50):17 位客服 × 50 = 850 条一批, 正是 T5 反对的做法。起点低、不够再往上调;反过来那一批已经分出去了。 · 两个基数都从 plan_assignments.criteria 快照里读上一次的值(零新列), 查找顺序:该主管在该诊所的上一次 → 该诊所任何人的上一次 → 首次默认。 · 确认单显示**推导链 + 出处**:「在岗 17 位 × 每人容量 20 − 已在手 0 = 可分 340 人; 容量/时效沿用 7-28 那次分配」—— 规模会随在手量波动,看不到推导只能怀疑系统抽风。 · 时效初值改为跟着提案走(原来写死 3),确认时把卡片当前值存回快照。 · 容量**不做成卡片控件**:改容量会改变人群,而卡片微调项只允许不改人群的那两项(T13)。 主管说「每人 30 条」→ 助手带 capacity 重出单。 · targetCount 保留为**一次性**覆盖,
⛔ 不回写基数 —— 拿「这批只要 60 人」反推出 「以后每人 6.7 条」等于把临时决定固化成长期参数。 本地实测:充填·窗口外 1,080 → 本批 340 人 = 17 × 20 − 0,每人 20 条。 教条补 T22,T5 补一条「100 是举例不是参数」的反读说明。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
主管在矩阵点「充填 · 窗口外 1,080」,确认单却说「从 170 位候选里取前 100 人」 —— 170 正是 ceil(100*1.5)+20 这个取明细的 LIMIT。而 selectionNote 是要求助手 原话转述的句子(T14),等于让系统当着主管的面报一个他刚看过的、对不上的数。 · 新增 countCandidates():走同一份 cohortWhereSql,count(DISTINCT patient_id) —— 与矩阵格子同一种数法,不是 count(*)(同一患者两条召回会被数两次)。 · candidateTotal / selectionNote 三处判断全改用真候选数;取明细仍只取 1.5 倍窗口。 · zod describe 里那句"已按取数上限截断"一并改掉(它把 bug 写成了规格)。 测试侧同时修一个假绿:原来的假 prisma 不管 LIMIT、一律吐 candidateCount 行, 所以"截断"在测试里根本不可能发生。改成照 LIMIT 截断 + count/明细分流, 并加一条 1080 的回归(已验证:改回旧实现该用例即红)。 本地实测:矩阵 1,080 → 确认单「从 1080 位候选里取前 100 人」。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
点矩阵格子只往助手走,左栏一动不动(原筛选/滚动位置全保留)。 两个理由: ① 人群的证据在助手的确认单里,那才是要审的东西。左栏再给一份, 两处口径一旦不一致(列表按 plan 分页、矩阵按患者去重,本来就不是一个数) 主管不知道信哪个; ② 左栏是他挑患者的地方,被上一次初选筛住会让他以为池子空了 —— 而那条告示要他先看懂、再点掉,才回得到整池。 cell 只留作矩阵内部回显(再打开时看得出刚交过哪一格)。 服务端 potentialTreatment / temperature 两个列表参数保留未动,只是前端不再自动带上。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
原来是「点分配 → 立刻弹浮层 → 浮层里转圈 → 数据到了重排」。 改成 shadcn 的常规做法:**加载态落在触发钮上**(钮里转圈 + 禁用), 取到数才把浮层打开,于是浮层一出现就是完整的矩阵,不会先以骨架尺寸出现再重排 —— 浮层的抖动比列表刺眼得多。 实现上 Popover 改**受控**:Radix 请求"打开"时先不开,去取数,取回来再置 open。
⚠ ️ 关闭要照常放行(点外面 / 再点一次钮),否则浮层关不掉。 PoolMatrix 随之变成**纯展示组件**:数据由调用方传入,不再自取, loading / error 两段 UI 一并删掉 —— 取数失败只弹 toast **不开浮层**, 开一个空浮层比不开更让人困惑。⚠ ️ 每次点都重取,不缓存:池子随时在变(客服在处理、引擎在重算), 缓存一份旧的会让主管照着过期的数字挑格子,而他完全看不出这数是旧的。 一次 ~90ms,不值得为它引入失效逻辑。 触发钮换成 ui/button 的 <Button>(尺寸压到 h-7 贴合 tab 行): 拿到 shadcn 的 gap-2 / disabled:opacity-50 / focus ring,不再手写一套。 next build 通过,971 tests passing。(按要求未在浏览器复验) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
点中矩阵某一格后:一串粒子沿弧线飞向助手 → 助手进「吸收」姿态 → 落地后自动 开会话窗(最大化)并说出那句话,接上原有流程。 沿用本仓既有取舍(不引动画库):CSS Motion Path(offset-path)+ 原生 keyframes, 曲线一次算好塞进 --pac-stream-path,粒子只改 offset-distance —— 位置由合成器算, 不占主线程,与宠物那 31 个 keyframes 同一套路。
⭐ **两个端点都在播放那一刻现取**,这是"位置要准"的关键: · 起点 = 事件带来的格子矩形 —— 浮层紧接着就关,不当场取就没了; · 终点 = 助手 FAB 的**实时** rect(挂 data-assistant-fab 现查)—— 那颗钮可被主管拖到任意角落并记进 localStorage,写死右下角的话, 拖过钮的人看到的就是粒子飞向空气。 弧线的控制点取中点沿法线抬起,抬的方向跟着走向翻转,否则从左上飞和从右下飞 会一个上凸一个下凹,看着像两套动画。 分层没有破:业务只发语义事件(cohort_handoff + from),**不描述怎么飞**; 飞法/粒子数/时长全在舞台层 cohort-stream。换成传送带只改那一个文件。⚠ ️ 时长的真理源在舞台层,store 引它来排"飞完再开窗" —— 业务侧自己 setTimeout 的话,每个调用点都要知道动画多长,改一次要满仓找。 反过来也不让舞台层去开助手:那会让"怎么演"决定"接下来做什么"。 顺带把 maximized 从 AssistantWidget 的组件内 useState 提到 store —— 与 open 当初同一个毛病:业务侧要"移交后直接开大窗"就够不着。 无障碍:prefers-reduced-motion: reduce 时粒子整段不出现,移交本身照常发生。 浏览器实测:一次点击产生 10 颗粒子,offset-path 起点 = 格心(312,180)、 终点 = FAB 中心(1236,676) 精确吻合,0.62s/颗、45ms 间隔; 落地后助手自动最大化并发出「帮我给「种植·黄金期」这批患者出一份分配方案」, 确认单渲染出「确认分配 43 条」与该格数字一致。 next build 通过,971 tests passing,控制台零报错。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
黑框会被读成边界线、像数据的一部分;这一圈框表达的是「你正指着谁 / 选中了谁」, 属于交互态,而交互态在全站都该是品牌主色。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
这一节此前两次改动都**没落到文档上**(python 替换静默 no-match,只改了代码没改教条), 于是教条里还写着早就撤掉的「蓝→琥珀→橙 温度渐变 +
🔥 热/🌡 温/❄ ️冷」。 现补齐到实际状态:列头临床说法(黄金期/窗口内/窗口外)、代码名与显示名为何刻意不一致、 1b 连续色带、以及「数量不参与配色」「hover 用内描边不换底色」两条纪律。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
按产品在 claude design 出的稿子(分配矩阵.dc.html 的 1b 方案)重做: · 三列共用**一条** amber → emerald → sky 的横向渐变,格子透明浮在这张面上 —— 列与列之间没有缝,整片读起来是一条"由烫到冷"的轴,只用色相区分窗口。 · 用 shadcn Card(CardHeader/CardContent)承载,浮层只负责定位与动效,宽度交给 Card 自己定 —— 两处都写宽度必然对不齐,而且改一处会静默留下另一处的白边。 · 表头补列汇总(黄金期 271 / 窗口内 280 / 窗口外 3,254),现算不新增字段。 · hover 文案**保持原样**:只给天数区间,不给解释。
⚠ ️ 配色是**恢复**但不是回到老样子:老那套是"温度比喻"的冷暖渐变(已撤), 这套是与列头一一对应的三个色相 —— 色相与文字是同一信息的两种编码, 刻意的冗余(色盲靠文字、扫视靠颜色)。⛔ 「数量不参与配色」这条自始至终没变。⭐ 顺带修掉两处**写了不生效、且完全不报错**的样式: ① popover.tsx 一直挂着 shadcn 官方的 `animate-in fade-in-0 …`,而那套来自 tailwindcss-animate 插件、**本仓根本没装** → 浮层一帧动画都没有。 改用原生 keyframes,并**经 @theme 注册成 --animate-***: Tailwind 的 variant 只作用在它自己生成的 utility 上,手写同名 class 拿不到 `data-[state=open]:` —— 实测 animation-name: none(class 在、样式在,就是没被选中)。 缩放锚点接 --radix-popover-content-transform-origin,浮层从触发按钮那个角长出来。 ② 格子的 hover 描边先写成 `shadow-[inset_0_0_0_2px_var(--color-slate-900)]`, Tailwind v4 把带 var() 的任意值解析成"只有颜色的阴影",偏移和扩散全丢, 实测是 `oklab(0 0 0 / 0) 0 0 0 0 inset` —— 完全透明。改用原生 outline;⚠ ️ 还必须带 `outline-solid`,否则基态的 `outline-none` 把 outline-style 钉成 none, 只加 outline-2 就是"宽度 2px、样式 none",一条线都不画。 浏览器逐项实测:渐变面(oklab 插值)、hover 内描边(solid 2px, offset -2px)、 出现/消失动画(overlayIn 0.14s / overlayOut 0.1s,Radix 会等动画结束再卸载)、 tooltip 区间不变;next build 通过,971 tests passing,控制台零报错。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
列头「热 / 温 / 冷」→「**黄金期 / 窗口内 / 窗口外**」,并把温度渐变整套撤掉。
⭐ 两件事是一件事:「热」是**比喻**,而这三档本来就有精确的临床语义。 名字说准之后,冷暖色就成了**第二套更弱的编码** —— 只是把列头已经说清的事再说一遍, 还要额外维护对比度、色标位置、深色模式。所以列头改名与撤配色一起做。⛔ 在文件头和教条里都写死了「别再把冷暖色加回来」,尤其别按数量深浅上色: 「这格橙是因为在黄金期、还是因为人多」分不清 —— 数量已用数字表达、档位已用列头表达。 唯一允许的颜色是交互态(hover / 选中),那表达的是"你正指着谁",不是数据。⚠ ️ **代码名不动**:枚举值、API 参数 temperature=、persona_features.data.temperature 的 JSON 路径仍是 hot/warm/cold。改它们要同时动 API 契约、SQL 谓词、已落库的 JSON 路径 和前端 query,收益只是"看着顺眼"。已在 TEMPERATURE_META 上写明这条,免得下一个人顺手重构。 顺带收口一处漂移:TEMPERATURE_META 此前**定义了却没人用**, rail 里另有两张内联中文表(初选 chip、宠物气泡)。现在三处都走 META, 改措辞只改一处 —— 否则这次改名就会漏掉那两处,而且不会报错。 浏览器实测:列头三档正确、矩阵内温度色节点归零(0)、hover 区间不变; next build 通过,971 tests passing。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed
-