1. 28 Aug, 2026 11 commits
  2. 27 Aug, 2026 13 commits
  3. 26 Aug, 2026 4 commits
    • docs(召回): 补第 2 例 王丽芳 TS0M013040 —— 残根被推荐种植,已改为拔牙 · 377d4547
      新增「处理」列区分已修复与待定。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • docs(召回): 新建误召个案表 —— 首条 宋志宏 TS0M001982 · da7fc018
      一患者一行,后续个案追加。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • fix(生产): 主进程堆上限 4G→8G —— 每 4 小时崩一次,三天没跑成过一次全量 plans · 470a44d9
      【症状】容器 exitCode=139 自动重启,08-23~08-26 共 **11 次**,间隔约 4 小时。
        页面无感(自动重启 + health 200),所以一直没人发现。
      
      【根因】每轮增量同步结束会在**主进程内**跑一次 runAllForHost 全量召回。补摄把患者
        灌到 111 万、召回池 54 万之后,selectHits 一次出 **96 万条命中**,连同 prefetch 的
        latest/snoozed/persona 全在堆里 —— 而主进程启动命令
        `node --enable-source-maps dist/main.js` **没带 max-old-space-size**,
        取 Node 默认 **4144 MB**,装不下:
          Mark-Compact 3985.6 → 3953.5 MB (mu = 0.008)   ← GC 已经回收不动
          FATAL ERROR: Reached heap limit — JavaScript heap out of memory
      
      【代价】那三天 `Result(ruier-grp)` 在日志里出现 **0 次** —— 一次完整的全量 plans
        都没跑成过。每轮崩在 selectHits 之后、写库之前:
           增量导入成功落库    persona 重算成功落库    plans 整轮作废
        数据不丢,丢的是"召回池的定时刷新"。另外每次崩溃留一个僵尸 sync 锁(重启后被回收),
        且会带走同容器里正在跑的 docker exec —— bj04 第一次尝试就是这么死的(白跑 1h27m,
        靠 batch.sh 的 3 次重试救回)。
      
      【为什么现在才发现】补摄期间增量被 sync 并发锁挡下(00:15/08:15/10:15 全是 skip),
        不跑全量 plans 就不崩 —— 实测连续 12 小时零崩溃。补摄一停,下一轮就会再崩。
      
      ️ 排查时别往"容器内存不够"上找:容器**没有内存上限**(HostConfig.Memory=0),
        宿主 15.36G 一直有 7-11G 空闲。撑爆的是 V8 自己的堆,与宿主余量无关。
      ️ 同一容器里 CLI 那条路(cold-import / recompute-plans)一直是 8192,
        **两条路配置不一致**,崩的一直是没配的这条。这次补齐。
      ️ 宿主内存账写进注释了:8G(服务) + 8G(CLI) = 16G > 15.36G。实测两者不会同时顶到
        上限(max-old-space 是天花板不是预留;实测峰值 服务 4G / CLI 4.9G),但
         别手工并发跑全量 `recompute-plans`(它不走 sync 锁)。
      
      ️ **这是止血不是根治**:池子还在涨,8G 迟早也会到顶。根治是「增量之后别跑全量」——
        recompute-plans 已经有 --clinics 收窄能力,同样的思路搬到
        SyncIncrementalSchedulerService 那一处即可。另开分支做。
      
      改动只有一行环境变量(docker-compose.prod.yml 的 pac-service),不动代码不动数据。
      YAML 已校验;managed overlay 只合并 DATABASE_URL/REDIS_URL(非 !override),不影响本项。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
  4. 23 Aug, 2026 4 commits
    • fix(宿主跳转): 按子串换,别按域名标签精确匹配 —— arrail-test 漏了 · 8827ac0b
      2026-08-23 测试环境实测:瑞泰患者点跳转,还是去了 arrail。
      
      根因:上一版把 hostname 拆成标签、找**等于** `arrail` 的那一段。而测试环境的域名是
      `arrail-test.5i5ya.com`,标签是 `arrail-test` —— 精确匹配匹配不上,一个字都没换。
      各环境的域名前缀会带后缀(test / uat / …),枚举不完。
      
      ⇒ 认这个**词**本身,不认它周围是什么:hostname 里出现的别的品牌片段整词替换
         (arrail-test → rytime-test)。
      
      ️ 仍然只换 hostname, 不碰路径和查询串(`?from=arrail` 是宿主自己的业务参数)。
      ️ 解不出 URL(相对路径 / 哨兵值 'postMessage')仍原样返回。
      
      判据本身没问题 —— 同一时刻查过那个患者:sourceUnit='瑞泰',取值是对的,
      只是替换规则太严。补回归用例(带环境后缀的域名),前端 15 条全绿。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • fix(宿主跳转): 瑞泰患者跳瑞尔门户 —— 打开时把 arrail 换成 rytime · eb5d8b9f
      jvs-dw 底下两个品牌各有一套门户:瑞尔 arrail.5i5ya.com / 瑞泰 rytime.5i5ya.com,
      两边**只差域名这一段**,路径参数完全一样。而 host.actionUrls 是 host 级配置、只有一套
      (现在配的是瑞尔那套)。于是瑞泰患者点「查看档案 / 病历 / 新建预约 / 回访」,
      会被带到瑞尔门户 —— 那边查不到这个人。
      
      ⇒ 打开的那一刻按**患者所属品牌**把域名换掉(lib/host-brand-url.ts)。
      
      【为什么按患者判、不按登录人判】
      登录人的品牌前端**拿不到**:/auth/session 下发的 sourceUnits 对诊所级用户是哨兵
      `__pac_deny_no_brand__`(那条路按 clinicIds 过滤,不走品牌维)。2026-08-23 拿生产
      三家诊所的 token 实测,全是这个值 —— 判不出品牌。而患者的 sourceUnit 是实打实的品牌名。
      语义上也是患者对:这些链接要打开的就是**那个患者**在宿主里的页面。
      
      【改了什么】
      - 后端 plan-aggregate:患者 + **关系人**都下发 sourceUnit
        (关系人用他自己的, 不拿本人的顶替 —— 亲属跨品牌时会跳错门户)
      - 新增 lib/host-brand-url.ts:applyBrandHost(url, sourceUnit)
      - resolveActionUrl / openHostAction 加可选的品牌参数;plan-detail-app 六个调用点全传上
      
      【两个容易漏的点】
      - 🔴 **HOST_ORIGIN 也必须换** —— 它是 postMessage 的 targetOrigin。PAC 嵌在 rytime 里
        而 targetOrigin 写着 arrail,浏览器**静默丢弃**这条消息:按钮点了没反应,控制台连个错都没有。
      - ️ **只换域名,不碰路径和查询串**:`?from=arrail` 是宿主自己的业务参数,替换它属于越界。
        URL 解不出来(相对路径 / 哨兵值 'postMessage')原样返回 —— 这条补丁不许把能用的链接变没。
      
      【纪律】 别把它做成通用的"多品牌路由"。它成立的前提是"两个门户只差一个域名片段"。
      真要支持多品牌,正解是 actionUrls 按品牌分组配置(host 管理页加一层),那时删掉这个文件。
      
      【验证】
      - 新增 host-brand-url.test.ts 14 条:替换 / 幂等 / 只换域名不碰路径查询串 / 哨兵值与
        相对路径原样 / 未知品牌与空值不动 / **每个调用点都带上了品牌参数**(源码断言,
        防的是"新加调用点时忘了传" —— 参数可选,漏了 tsc 不报)
      - 本地实测 /plans/:id/full:瑞泰患者 sourceUnit='瑞泰'、瑞尔患者 ='瑞尔'
      - 两端 tsc + 后端 1443 用例 + 前端 46 用例全绿
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
  5. 22 Aug, 2026 5 commits
    • docs(口径): 钉住预约成功率的分母 —— 不筛结果项,未接通也算他打过 · e46bd0dd
      产品确认(2026-08-22):分母判据只有一条——**他有没有真打**。
      所以「没进展」那一桶(未接通 / 待跟进 / 需要找医生 / 已发短信 / 改约)
      照样进分母:打了没人接,那也是打了。
      
      ️ 未接通往往占分母的大头,所以这个数读的是「触达 × 转化」合起来的效率,
       不是"聊上了的人里谈成几成"。 别为了好看把 keep 组从分母里摘掉——
      那会变成另一个指标,而名字没变。
      
      唯一不在分母里的是「没有结果」:那些单一条执行记录都没有,天然不在
      plan_executions 里,不需要额外排除。
      
      代码一行没动,只是把这条决定写进 schema 注释 + 加一条回归(分母 SQL 里
      出现 outcome 就红)。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • refine(主管工作台): 约上→预约上、完成→近N天已处置、成功率只出一个值 · 6d74e284
      主管走查第二轮:
      
      ① 「还没收尾」说明补「(含已经超期的)」
         到期不再自动回池(回收器 2026-08-19 删了),过了时限的单就压在客服手上,
         批次靠 inHand>0 留在这一组。不写明的话主管会以为过了时限的都沉到历史里了,
         而那恰恰是最该动手的一批。判据一个字没动。
      
      ② 「约上」→「预约上」
         「约上」仍有歧义:约好了既可以是约了预约、也可以是约了下次再联系,
         而这两列分的正是这件事。
      
      ③ 「完成」→「近 N 天已处置」
         主管问「完成和已处置是同一个意思吗」—— 是。同一个判据(真的落了通话结果),
         差的只是范围(本批 vs 近 N 天),而同一屏上叫了两个词。
         而且「完成」听起来像"做成了",是这套口径一直在躲的读法。
         ⇒ 统一叫「已处置」,并带上窗口前缀(预约成功率同)——
         不带的话切了 7天/30天,数字变了却看不出为什么。
      
      ④ 预约成功率只出一个百分比
         下面那行 rateNote(退回 N · 没动 M)删掉。
          released / idle 没有消失,仍在响应里 —— 只是不该挤在这一列下面。
      
      顺带拆掉一颗雷:AgentWorkloadRow.handled(= done + released)删了。
      它和界面上的「已处置」(只算写了结果的)是两个意思,同一个词两个含义 ——
      主管问的正是这个。服务端留局部量 touched 算「没动」,语义不变。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • feat(主管工作台): 「约上」拆成约上/约下次,退回率换成预约成功率,「还在跑的」改名 · 0d7c6a53
      四处口径/措辞,主管实测提的:
      
      ① 「还在跑的」→「还没收尾」
         被读成"分配还没完成",像有个后台任务在跑。它说的是**批次**的生命周期。
         ️ 没叫「时限内」:判据有两支,另一支(还有单压在客服手上)在时限过了
         之后仍然成立,叫「时限内」会让过了期、单还压着的批次莫名待在这一组。
         判据一个字没动。
      
      ② 「约上」收窄成**只数转化新预约**,约定下次回访单列「约下次」
         两者原来都算 close 组、合成一个数,靠小字解释「含约定下次回访」。
         而两件事差着一整个台阶 —— 前者患者进了预约表、有个日子会来;
         后者只是客服答应"那天再联系您",患者什么都没答应,单子还挂在他手上
         (drivesStatus 一个 completed 一个 keep,状态机上就是两回事)。
         主管看到「约上 6」会当成 6 个人要来了。
      
      ③ 抽屉「通话成效」四个桶 → 五个
         第一格「成功」拆成「约上 / 约下次」。要靠小字解释的数,本身就该拆开摆。
         五个桶仍然穷尽(= 条数),回归里锁着。
      
      ④ 「退回率」→「预约成功率」
         分子 = 真的约上的( 不含约定下次回访 —— 算进来的话,一个只会往后拖的人
         能拖出一条漂亮的成功率);
         分母 = 「完成」他真打过的( 不是"已处置" —— 退回的单他压根没打,
         摆进分母等于因为他退单而扣他的成功率)。
          退回和没动没有消失,降级成后面那行小字:两者仍是主管调分配的输入(T7)。
      
      - schema: brief/detail 加 bookedNext,outcomes 加 appointed/scheduledNext
        (success 保留为两者合计, 但界面不再单独摆它),workload row 加 booked
      - SQL 由「close 组 IN (...)」改成按 outcome 逐项 FILTER,DISTINCT ON 不动
      - 助手解读规范同步(guides.ts),旧那句「outcomes.success 含约定下次回访」删掉
      - 新增 booked-vs-scheduled-next.spec.ts(21 条)
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
    • fix(助手): 「这批都给某某」改不动确认单 —— 三层各错一处,且都不报错 · 245f0b01
      本批 5 人,主管说「这5人都分配给韩维」。界面回「已把整批铺给 1 位客服()」,
      括号是空的:一条都没动,而助手接着说「这版已经改成 5 人都归韩维」。
      
      三层:
      
      ① schema 判断本身就是错的
         `batch + assign` 被 refine 判非法。而 batch 是这句话唯一自然的表达,
         韩维就印在卡片下面那行「本批未分到」里。
         改判:batch + balance 合法;只有 batch + owner 不行 ——
         专属关系只有「待分配」那份数据带着(pending[].ownerUserId),
         已排好的条目卡片上查不到它归谁专属。
      
      ② 那条 refine 一次都没跑过(根因,不是 ① )
         两个改单工具的入参用的是**手写的 JSON Schema**(给模型看的那份),
         zod 那份从没进过运行时。两份 schema 各写各的,
         写在没跑的那份里的约束只是注释。
         ⇒ 加 `checkEditOps`:推给界面之前按 zod 校一遍,校不过整组退回给模型,
         并说清哪一条错在哪(只说「参数错误」它会原样再发一遍)。
         确认单、调整单同一条路。
      
      ③ 界面把 batch 解成空名单
         `return { planIds: [], label: '整批' }`,注释说"只有 set_expiry /
         set_benefit 走得到这里" —— 而那两件在上面就短路了(走批次级控件),
         真正走到这一行的恰恰是 assign / remove。
         改:整批 = 生效条目 + 还留在待分配里的,与顶栏「共 N 人」同一口径。
         另加一道总闸:选中 0 人就到此为止 —— 下面每个分支都会 push 一句
         "已如何如何",一条没动而主管读到的是成功。
      
      顺带(同一类):owner 分不下去的两种原因分开报。
      「卡片没带这条的专属关系」被报成「查不到在岗的专属客服」,
      那是一句关于数据的断言,而卡片没有资格下这个断言。
      
      - 整批移出仍然拦掉,但界面要吭声, 不许静默什么都不做
      - 工具描述补 batch 那一格该怎么填;ASSISTANT_PROMPT_VERSION → 2026-08-22-a
      - 新增 sheet-edit-batch-assign.spec.ts(14 条),改判 mcp-clinic-scope 两条旧断言
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      luoqi committed
  6. 21 Aug, 2026 3 commits