- 10 Aug, 2026 9 commits
-
-
工作台头部那条「客服执行页
↗ 」旁边的注释早就写了 「两个页面是双向的,⛔ 别做成有去无回」,但实际只做了去的那一半: 主管点进执行页之后,除了改地址栏没有回去的路(产品发现)。⚠ ️ 判据用 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
-
- 07 Aug, 2026 3 commits
-
-
卡片上那句是 LLM 压出来的摘要,而摘要天然会丢东西 —— 客服看到「未留结果」 「未接通」这种结论时,第一反应就是"到底原文写了什么"。没有入口他只能信摘要, 或者去问别人。抽屉里是逐条原文(日期/类型/内容/结果),正是那个反问的答案。
⚠ ️ 抽屉 kind='return-visits' 一直都在(drawer.tsx),2026-07-29 那次只是 把回调摘掉了 —— 改动就一行,⛔ 别当"功能没做"重写一遍。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
产品 2026-08-04:「第二句是他跟我们以前的关系亲密度是什么样的,就是联系他 多不多,然后有没有过不好的体验」。原来第②句只写"最近一次联系是什么事", "多不多"和"不好的体验"整个缺失。 「不好的体验」只有一种形态(全量查证): - `type` 只有 常规回访/咨询回访/术后回访 —— 没有投诉类 - `result` 自由文本 3 万行 6765 种取值,长尾里 投诉/不满/纠纷/态度/退费 **一条都没有** - 真负面只有**「挂断」**:688 次 / 595 位患者(占有回访记录患者的 11.5%)
⚠ ️ 第一遍扫的时候「无不适」被"不适"接住,假阳性 5816 条 —— 负面词表必须排除否定式 三个刻意的设计: ① 总次数**单独聚合**:回访历史只给最近 3 条,让模型拿那 3 条数总量必然低估 (实测 1131 位患者被联系过 10 次以上,全会被说成"联系过 3 次"); ② 挂断为 0 时**整段不出现**。⛔ 不是输出「被挂断 0 次」——一旦出现那个 0, 模型必然翻译成"沟通一直顺畅"。**没有记录 ≠ 关系好**,那是拿没证据当反面用 (与 noTag 铁律同一件事); ③ schema 与 prompt **必须同时改** —— 只改一边 = 定义了但没喂,静默失效。 单独一条测试锁两边。 promptVersion → 2026-08-07-closeness(不升的话老患者读到的还是旧简报, 而本地怎么测都是新的 —— 那种"改完看不到效果"最难查)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
产品 2026-08-04 评审原话:「我不可能挨个去看他分的对不对,你在这儿应该给的是 **结论性的东西**…如果你不能告诉他哪不对、哪里需要主管判断,那就默认他必须 一条一条看过去才能确认。」 卡片顶部新增 opsNote,只答三个问题: - 这活多大:本批每人拿到 1~4 条;分完手上共 57~62 条 —— 按每人每天 15 通算,最满的那位要 5 天打完 - 谁的批次全是自己的老客户(dedicated === count)→ 点名,主管可以直接过 顺带修掉一个真问题:卡片原来只写「分完每人 57~62 条」——那是**在手总量**, 而那一批实际每人只给 1~4 条。拟分 16 条的单子上写着 57~62,很容易读成 "这批好大一坨"。现在增量与总量分开说,并把这句从 basisNote 里挪走 (它答的是"凭什么这么分",不是"这活多大";两处都说就是复述)。
⚠ ️ DAILY_CALLS_PER_AGENT=15 是评审口头经验值不是实测,所以文案写成 「按每天 15 通算」而不是「需要 5 天」,让主管看得见这个前提;⛔ 常量注释钉死不许接进落人算法 —— 那等于把删掉的"容量"从后门放回来。⚠ ️ 结论钉在卡片上不进助手那段话:助手的话会被后续对话冲走,而重排换卡片时 它也不会重说 —— 与 refillNote 同一条理由。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed
-
- 06 Aug, 2026 7 commits
-
-
luoqi committed
-
-
🔴 姓名搜索/phoneVerified/画像标签全部静默失效:温度重构时把 `where.patient = {...}` 的挂载整段删掉了,tsc 绿、单测绿、界面无报错, 只是筛选条件从此不生效。改回合并式挂载(顺带修掉 sourceUnit 被覆盖的隐患), 补了会在缺这行时变红的回归测试 - 画像圈人性别男女都返回 0 - jest 吃满 CPU:transform 里的 isolatedModules 被挪进 tsconfig 后**更慢** (18.1s/165s vs 6.9s/43s),因为 pac-service 的 tsconfig 把它设成了 false。 按弃用警告改反而变慢 —— 以实测为准。配 maxWorkers 50% - 新增 tsconfig.typecheck.json(单测不覆盖类型,提交前得单独跑) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
秒挂原来算在「保持」里,而它明明是没接通的一种。改 group 之后 tone 和提示文案没跟着改 —— 边框还是 slate、提示还写着「等下次跟进」, 而实际已经压 30 天。教训:一个字段辐射到多处渲染时,每一处都要跟着查。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
- 回访摘要改读 result/followContent,
⛔ 不再念「任务已完成」这类状态字段 —— 主管要的是事实不是流程 - 「—」这类空占位不再成行:列不出来的就不展示 - daysText:Math.round(74/30)=2 而 Math.round(75/30)=3,相差一天的两次回访 被渲染成 2 个月 vs 3 个月。改成按天分档 + 30.44,再修 floor(365/30.44)=11 让整年读成 11 个月 -🔴 标签泄漏三次同一形状(cold_3y → 本批 → 备注):**输入里出现的任何标签 都可能被原样抄进输出**。逐一堵掉 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
- 「待分配」单列一组(amber + 顶部粗边),可拖可删可收拢、默认展开, 带医生列与已分配一致;
⛔ 不处理就是不分,文案必须说清 - 顶栏「共 N 人 · 拟分 X(Y 位客服) · 待分配 Z 人」—— 先给总数再拆, 光写「拟分 21 · 待分配 39」读不出 21 含不含 39 - 患者/客服姓名点击复制(draggable=false + stopPropagation,否则吃掉整行拖拽) - 重排后卡片自己交代换了什么 —— 重排走 HTTP 只换卡片,助手不会重新说一遍, 不交代就是"上面写着 34 人要您定、卡片显示 0 待分配"两份打架的口径 -🔴 同批指令后一条读不到前一条:「李汝明给李闻,剩下的都给高瑞珍」—— 第二条读的还是旧 state,李汝明被连同其余 31 人一起铺给了高瑞珍, 而两条都回报"已执行"。改成 moveDraft/dropDraft 草稿,最后一次性提交 - findPatients 要搜到「待分配」、findAgent 要搜到「本批未分到」—— 卡片上看得见的人,指令就必须能指到 - 拖给「未分到」的客服后组标题不再退化成 #678 · 在手 0 - 铺平算水位时排除本次要改派的这批(否则算了两遍) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
同一个形状一天栽了三次:**工具契约少一格,模型必然填错且静默**, 返回值都是合法的(一个数字、一个 0、一次成功的操作),只有肉眼比对才发现。 追提示词修不好 —— 得补格子。 - 温度枚举只有 hot/warm/cold(缺 cold_2y/cold_3y)→ 主管点「2–3 年」67 人, 助手报 324。改成从 TEMPERATURE_ORDER 派生 + 中文对照表 +
⛔ 不许把码说出口 - clinicId 必填且无兜底 → 模型编了 CL001,SQL 正确、返回 0, 助手转头去解释"这批人为什么是空的"。改成可选 + resolveClinicId 兜底, 范围外**抛错**并列出真实 id,⛔ 绝不当成"这个诊所没人"返回 0 - 确认单指令 8 条并列字面量 → **三个正交的轴**: select(patients/agent/pending/batch) × action × to(owner/balance) 由来:主管说「把待分配的患者各自分给各自的专属客服」,而 「待分配 × 改派 × 各自的专属」这一格是空的 —— 只有铺平可用, 18 个人被散给了 17 位别人。而那恰恰是助手唯一不许自作主张干的事。 正交化不消灭"枚举漏值",但缺的组合变成**表格里的空格**(看得见), 测试里是一张 test.each 的「说话 → 拼法」对照表,加一行比加一条指令便宜 另:助手第 0 条「说人话」—— 禁说取值码/字段名/工具名/「温度」。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
三趟落人改成两趟 + 待分配:专属客服排满的人不再被自动改派给别人, 单列一组交主管决策。把患者从他的专属客服手里挪走是关系层面的决定。 「重新排一版」当天推翻两版才定稿,病史都写进注释了: - v1 预先下发「顶替名单」→ 名单只能从取数窗口挑,池子 19 位无主、 窗口里只落进 6 位,界面只敢说"顶 3 位" - v2 把整个窗口丢给落人 + stopAt 跳过 → 水位按 chosen.length 算被窗口撑大 (⌈(1001+50)/17⌉=62 → ⌈(1001+165)/17⌉=69),34 个"专属排满"的人原地进了 同一位客服手里(她 59→68,别人 58)。底线没破,但负载塌了、口径全错 - v3(定稿)只换"挑谁":池子里无主的全换进来,其余按优先级用有专属的补满 N, 然后走完全一样的三趟。有专属的那部分**按客服轮着取** —— 直取前 N 名会把 名额全给专属大户(111/165 属同一人),另两位客服的余量白白空着。 实测 拟分 16·待分配 34 → 拟分 31·待分配 19 顺带:确认后可补挂/改/撤福利(此前只改 state 亮角标,DB 一个字没变)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed
-
- 05 Aug, 2026 2 commits
-
-
luoqi committed
-
bull-board.module.ts 原注释称「本路由由 NestJS 路由系统接管;需要走全局 JwtAuthGuard」—— 不成立。@bull-board/nestjs 用 ExpressAdapter 自己挂独立 Express handler,不经过 Nest 的路由与守卫。 2026-08-05 从公网实测旧生产(网关把 /admin/queues 转到了 3101): GET /admin/queues → 200,面板 HTML GET /admin/queues/api/queues → 200,队列数据 返回体带 "readOnlyMode":false / "allowRetries":true —— 未鉴权即可翻 job payload(含 patientId / hostId / tenantId)并重试、清理任务。 加守卫要下沉到 Express 中间件层;而这个面板平时没人用(前端无任何入口链接, 只在 docs/monitoring 里作为排障手段提过),故取成本最低的解法:默认不挂载, 排障时 PAC_BULL_BOARD=1 临时开。 关掉的只是**面板**:队列本身、定时任务、job 消费全部照常,它只是查看器。 同步订正三处文档里「需登录 admin」「豁免前缀」的旧描述。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed
-
- 04 Aug, 2026 19 commits
-
-
luoqi committed
-
luoqi committed
-
docker-compose.prod.yml 里写死 DOCS_API_URL: https://pac.jarvismedical.asia, 而这份 compose 是**测试服与生产共用**的 —— 于是生产 docs 页上点 Scalar 的"发送请求", 实际打到测试服去。当前生产就是这个状态(不影响文档阅读,但"试一试"跑错环境)。 改为复用 NEXT_PUBLIC_API_BASE_URL:它就是同一个东西(API 的对外地址), 各环境 apps/pac-web/.env 里本来就有 —— 零额外配置,且不会再随环境漂移: DOCS_API_URL: ${DOCS_API_URL:-${NEXT_PUBLIC_API_BASE_URL:-http://localhost:3101}} 要单独指定时才设 DOCS_API_URL 覆盖;都没有则退回本地。
⚠ ️ 它是 **build-time** 参数(server 在 next build 期烘进 SSG 页), 改了必须重新构建 pac-docs 镜像,不是改 env 重启就行。 测试锁住:值必须是变量插值、且不得出现任何具体域名。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
luoqi committed
-
luoqi committed
-
README 原文「期间 service 秒级重启」会让人以为一两秒,实测三次都在 15.97~16.86s。 force-recreate 不是滚动更新,就是停旧起新,中间必然有真空期。 补三点容易误解的: · build 阶段不影响服务(老容器一直跑)→ 部署总耗时长短与停机无关 · 影响面:API 报错、刷新即恢复;登录态不掉(JWT 在浏览器);无数据风险 · 要真零停机得网关层蓝绿,不在本脚本范围 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
rootless podman 下 `up -d --force-recreate` 要删**运行中**的容器,而 rootless 杀不掉 它的网络进程,直接失败: rootless netns: kill network process: permission denied 失败还留下 <hash>_<proj>-<svc>-1 半成品容器,不清则重建报名字冲突。 改为:主路径不变(直接 force-recreate),**仅在它失败时**回退 —— 清残留 → stop → 重建。docker 侧一行路径都没变,podman 侧多走一次回退。 【停机实测,顺带纠正我自己两次错判】2026-08-04 三组对照(0.5s 间隔探 /health): docker + 直接 force-recreate ~16s docker + 先 stop 再 recreate 15.97s podman + 先 stop 再 recreate 5.44s 即 **部署本来就有约 16 秒停机,一直如此** —— force-recreate 不是滚动更新, 它就是停旧起新。⚠ ️ 第一轮曾测出 docker「0 停机(535 样本全 200)」,那是**测量错误**: 探测有 300s 上限,而那次是全量构建、耗时更长,容器重建发生在探测结束之后 (实测 probe 比 deploy 早结束 29s),535 个样本全采自"还在 build、老容器好好跑着"的阶段。 我据此先判「本改动引入退化」、后判「修正后恢复零停机」,两次都错。 教训:探测窗口必须覆盖被测阶段,否则"全绿"只是没看见。 本改动保留的理由因此不是"避免退化",而是**改动面最小**:docker 走原路径, 只有 rootless 才多一步。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
2026-08-04 新生产机(Ubuntu 26.04,运维刻意配的 rootless podman:非 root 账号 + netavark/pasta,攻击面更小)实测: rootless netns: kill network process: permission denied Error: ... up -d --force-recreate pac-service: exit status 1 失败还留下 <hash>_pac-pac-service-1 半成品容器,要手工清。 根因:force-recreate 要删**运行中**的容器,rootless 杀不掉其网络进程。 手动 stop → rm 实测正常,说明不是普遍权限问题,只是删运行中容器这条路走不通。 改为先 stop 再 up --force-recreate: · rootless podman 下通过(网络进程正常退出后再删) · docker 上语义等价甚至更彻底(stop 后必然重建,不依赖 compose 判定) · 仍保留 --force-recreate,不退回裸 up -d —— 那正是本脚本要绕开的 compose diff 缺陷 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
【podman 差异】docker 默认就是 json-file(写不写行为一样),但 **podman rootless 默认 journald**。2026-08-04 新服务器(Ubuntu 26.04 + podman 5.7 兼容层)实测: · `docker logs <c>` → 只回一行兼容层提示 · `podman logs <c>` → 0 行 · 日志进用户 journal,普通用户不在 systemd-journal 组 → 读不到 等于线上出问题**完全看不到日志**。今天排查那四个增量 bug 全靠 docker logs,这在新环境行不通。 显式声明后两种运行时行为一致。 【顺带治一个既有隐患】此前没有任何 rotate —— 当前生产 /var/lib/docker/containers 已 1.4 GB, 只增不减。50m × 5 = 单容器上限 250MB:够排查近期问题,又不会把盘吃满 (2026-08-01 测试服正是被写满后 Postgres 写不了 pg_wal 崩溃重启)。 用 YAML anchor 统一 7 个服务,避免逐个漂移。对 docker 侧唯一的行为变化是**加了 rotate**, 驱动本身就是原默认值。 顺带纠正一个误判:pac-asr 早已是 `profiles: ["asr"]` 默认不启(当前生产也没跑它), 「迁移时 ASR 不部署」不需要改 deploy 脚本 —— 之前判断错了,已用测试钉住该事实。 测试 828 项(+4)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
luoqi committed
-
服务器迁移窗口里新旧实例并存,新实例要能起来接流量、验证配置,但绝不能同时对 DW 做 增量摄入 + persona/plan 重算: - 各连各库:两边各拉各的,待迁移的数据快照持续偏移,dump 出来就是旧的 - 同连一库:互抢 sync_logs 的 partial UNIQUE(host_id) WHERE status='running' 锁, 还会把对方**正在跑**的锁当"僵尸"回收(判据只看 startedAt 早于本进程启动) 【为什么必须是代码开关,env 关不掉】 - jvs-dw 的 manifest 写死 auto_sync: true,scheduler 启动即自动发现并注册 - cron 也写在 manifest(incremental_cron),优先级高于全局 PAC_INCREMENTAL_CRON - PAC_INCREMENTAL_HOSTS= 留空只会 fallback 到自动发现 - 改 manifest 能关,但 deploy-prod.sh 会 git pull 覆盖掉 覆盖范围**只关写、不关读**:✅ sync-incremental(摄入+persona+plan)—— 闸判定放在 reapStaleRunningLocks **之前**, 禁用态完全不碰 sync_logs✅ stale-scan(enqueue persona 重算)❌ dw-lag-monitor / daily-health-report —— 只发告警和报表,备用实例照常跑反而多一双眼睛 这条边界用测试钉住,防后来人"顺手统一"掉 → 迁移期静默失去监控。 只认严格的 '1','true'/'yes' 都不算 —— 半开状态比全开更难查。⚠ ️ 迁移完成、旧实例下线后务必移除该 env 并重启,否则新生产静默不摄入(只表现为数据越来越旧)。 测试 824 项(+6)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
原来两个入口共用一个 RAIL_FILTERS_VISIBLE,只要电话这个就得拆开: PHONE_FILTER_VISIBLE = true ← 放回 TAG_FILTER_VISIBLE = false ← 仍隐藏 外层条件跟着两个开关一起判 —— 只判其中一个的话,以后放开另一个时 整排还是不显示,而且不会报错。 服务端谓词一直都在(plan.service.ts 的 patientWhere.phoneVerified), 这次只是把入口露出来;realPhoneOnly 初值仍是"不筛",不会出现看不见的筛选。 `!hasPatientArchive` 守卫保留:宿主自带患者档案时号码整个不展示, 筛「真」没有意义(同 PatientRow 的 hidePhone 判据)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
CI 的 openapi-drift 闸只在 MR 和默认分支跑,合 test 时不会拦; 但合 main 时会 fail。在这里补掉,别把债留到下一刀。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
主管侧批次分配从地基到闭环:三趟落人(专属封顶 → 无主补空 → 有主改派)、 确认单可局部修正、批次归因走 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
-