- 26 Jul, 2026 7 commits
-
-
上一版为了"精简"把自明标签的 rules 清成空数组,结果卡片退化成三段平铺文字 —— 没有层次,也没有取值之间的对照,取值多的标签(权益身份 5 类、治疗史 4 类)尤其难读。 精简该精简的是 body 字数,不是砍结构。16 个标签统一回「加粗取值 + 一句短说明」: 客服先扫左边那列找到自己要的取值,再读右边一句。措辞仍是上一版改过的客服版, 口径订正(权益身份 5 类、转介绍达人门槛、急迫档位)都保留。 `rules[].label` 由可选改必填;CI 断言相应收紧:至少一条、每条都得有 label。 454 tests green · pac-web build 通过 · 本地逐个 hover 核对过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
## ① 依据行的日期折行了 定宽 68px 装不下 `2026-03-27`(11px 字号),每条依据的日期都断成两行。 改为按内容撑开 + nowrap;tabular-nums 保证各行等宽,不定宽也自然对齐。 ## ② hover 说明太啰嗦,而且是写给程序员的 16 个标签的说明全部重写。原则:客服扫三秒能用,取值本身说得清就不再复述一遍。 典型对比(潜在治疗): 改前 8 行 —— 副标题 + 3 条把 chip 上已有的词再列一遍 + 一句 「口径与召回完全一致(就是召回在挖的机会),只是不受冷静期等时间限制」 改后 3 行 —— 副标题「诊断或建议过、但还没做的项目」+ 取值 + 「已经在别处做掉的,这里看不到」 rules 现在允许为空(性别 / 治疗史 / 权益身份 / 治疗敏感 / 潜在治疗等 6 个都清空了), note 只留会改变客服**做法**的那句。新增一条 CI 断言拦实现术语(fact / 字段 / K0 / data. …), 免得下次又写成实现说明。 ## ③ 圈人面板的标签用户看不懂 「重要保持」vs「重要挽留」差在哪?「待激活」vs「沉睡客」几个月分界?这些名字是算法口径的 产物,客服只能凭感觉勾。给 PERSONA_TAG_FILTER_DIMS 的选项补 hint(64 项),内容是**判定条件** 的大白话,如「重要挽留 · 消费高,但很久没来了」「待激活 · 半年到一年半没来」。 展示方式**没用浮层**:这些选项是拿来横向比较的,浮层恰好盖住正要比的那几个(实测会遮住 2 个 chip)。改成面板底部一行定高说明,悬停即换,不遮挡、无 z-index 问题、扫读时一直在。 454 tests green · pac-web build 通过 · 三处均在本地真实数据上人工验过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
## 问题 水位闸只在**事实变了**时放行重算。这是对的 —— 没有它 40 万患者天天全量重算。 但有一类变化不伴随任何事实变动,时间自己走了就会改档: 急迫等级跨 30/90 天线 · 生命周期跨 180/540/730 · RFM 的 R 跨 540/730/1095/1460 年龄跨段 · 种植年龄禁忌满 19 岁解除 · 时间偏好/特别关注的滑动窗口把最老记录挤出去 这类患者永远等不到重算。年龄段和年龄禁忌最极端 —— 它们**完全不依赖 fact**, 一个 54 岁患者会一直显示「中老年」,直到他碰巧有新事实进来。而年龄还喂着按龄排治疗。 ## 解法:算出确切到期时刻,而不是每天盲扫 FeatureExtractor 新增可选 `nextBoundaryAt(ctx)`,报出"纯时间流逝会让我改档的最近时刻"; PersonaService 取全体最小值存 personas.next_boundary_at;夜扫加一条 UNION 分支 只捞 `next_boundary_at <= now()`,走 (superseded_at, next_boundary_at) 索引。 isFresh 同步加这一维 —— 否则夜扫捞出来入队也会被水位当场 noop 掉,白捞。 八个特征声明了边界(rfm / lifecycle_stage / urgency_level / age_bracket / contraindication / potential_treatment / time_preference / special_attention), 日期算术统一走 features/time-boundary.ts 的四个原语,不手搓。 顺带把 rfm / lifecycle_stage / urgency_level 各抄一遍的「到诊」口径收进 visit-facts.ts —— 三个标签的 description 都印着"就诊 N 次 / 末诊 N 天",口径对不上客服当场就能发现。 ## 本地实测(friday host,13,268 患者) --force 回填 success=0 · refreshed=885 · unchanged=12,383 → 全量跑几乎不产新版本 边界分布 10,808 有到期时刻 / 2,463 已过全部档界(NULL) 每日到期量 5~30 条(外推生产 40 万约 150~900/天,而不是 40 万) 端到端 人为把某条置为已过期 → 夜扫 SQL 捞到 → 不带 --force 重算 noop=0(闸放行) → 内容真没变故判 unchanged 不写 → 边界推进到 2029-06-06,不会重复捞 ## 一并清理(逻辑清晰向) · persona-display.ts 删掉 `[enum] 文本` 前缀解析与 PERSONA_STATUS_ZH —— 服务的四个特征 (treatment_chain_status / recall_risk / value / do_not_contact_status)W7 已摘除, 现存 16 个 extractor 没有一个产这种前缀,抽屉里那个 tag 恒为 null。 · PERSONA_FEATURE_META 从 33 个键收到 16 个(= 在跑的全集)。原来混着已摘除的和只存在于 路线图上的,"看着 30 个标签实际只出 16 个"。枚举保留路线图,展示元数据只登记会上屏的。 · tone 重排:红色只留给警示(急迫/特别关注/禁忌/治疗敏感)。原来「转介绍达人」「折扣锚点」 这类正面或中性信息也是红的,和「禁忌」同色,客服扫一眼分不出轻重。两条 CI 断言守住。 · mock 画像换成在跑的特征 —— 原来摆着六个系统根本不产出的标签,拿它当参照会看错。 · 折扣锚点超过 3 年在详情页给过时提醒(生产上见过 2018 年的锚点被原样展示)。 453 tests green · pac-web build 通过。 ## 部署 两个迁移的回填是同一次运行,且**必须带 --force**: next_boundary_at 的 NULL 语义是"不再改档",不会被判 stale,不强制跑就永远算不出来。 pnpm recompute-persona:prod -- --host=jvs-dw --concurrency=8 --force Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
页面上「对画像的解释」的四个毛病,一次修掉。 ## ① 说明写错了 —— 前端自己硬编了一份算法字典 persona-feature-hover.tsx 里的 ALGORITHMS 与后端口径各写各的,已经漂: · 权益身份只列「商保客户 / 医保结算」2 类,extractor 实际产 5 类;还承诺 「显示保司 + 最近日期」—— description/data 里压根没有这两样 · 转介绍达人写「社交型 = 推外人 ≥3 人」,真实口径是「推荐 ≥3 人**且**带来成交, 直系亲属关系 <3 才归社交型」 · 急迫等级缺档位说明;年龄段 labelValues 写 0-3 / 46-55,与自家 algorithm 的 0-2 / 46-54 自相矛盾 客服是照着这段话跟患者讲的,写错比不写更糟。 改为收进 PERSONA_FEATURE_SPECS[key].display(与 algorithm 口径贴脸放,前端只渲染)。 不新开接口 —— packages/types 本来就是前后端共享的。 新增 tests/persona-spec-drift.spec.ts:registry↔ 注册表↔ 圈人字典三方必须对齐, 每个标签必须有 display —— 光收拢不设闸,过两个月照样漂。 ## ② 只给结论不给出处 后端一直存着 evidence.factIds(生产上 rfm + lifecycle 两个特征就占 1 GB), 但 adapt-data 把它写死成 `evidence: []`。现在 /full 透出 evidenceFactIds, 详情页反查同一响应的 facts 渲染「依据 · N 条」(默认 4 行,可展开)。 本地实测证据 id 100% 可解析。factLabel 从 tooth-timeline 抽成共享模块并补齐 结算/预约/就诊/病历等类型 —— 否则证据行会把 `payment_record` 这种表名甩给客服。 ## ③ 顺序是随机的 抽屉排序取 `orderBy createdAt`,而同一版 feature 行是一条 createMany 写进去的、 createdAt 完全相同 → 排序实际未定义,生产上呈现为英文 key 字母序: 「急迫等级 = 紧急」排最后一个,「性别」「获客渠道」排最前。 现按用途分三组(跟进要点 / 价值与阶段 / 基础属性),组与组内序都定义在标签卡里。 标签云用同一套序。 ## ④ `?` 够不着 原来是 16×16 的 <span>,无 tabindex 无 role、纯 hover —— 键盘、读屏、平板触屏都点不到 (可访问性树里一个都读不出来)。改成 24×24 button,focus 即可唤出说明卡, aria-label 带标签名。 顺带:「更新于」改用 refreshedAt(就地刷新不升版本,computedAt 会停在旧值, 拿它显示会让客服以为数据比实际更旧);extractor 清单从 module/registry 两处收成一处。 ## 验证 432 tests green · pac-web build 通过 · 本地真实数据人工验: 分组与依据渲染正确、hover 内容来自 spec、`?` 可聚焦且 focus 能唤出对应说明卡。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
## 事故 画像算的是 patient_facts,stale 判定却只看 patient_transactions 的 event_seq。 reparse 重写事实但不产新事务,于是 2026-07-21「amount_cents 应收→实付」reparse 之后紧跟的全量重算被水位闸整批 noop 掉 —— 生产日志: manual:cli noop=325,979 success=79,010 后果(生产 3000 抽样):reparse 前算的画像 96% 累计消费与实时事实不符,中位高估 91%。 页面上同屏两个数打架 —— 关键事实 ¥154,350(实时)vs 画像 ¥195,750(旧口径)。 而且不只是显示错:monetaryCents 要跟实时算出的 M 分位阈值比大小 → mScore 系统性 偏高 → RFM 分群整体偏「重要」→ 直接抬高召回优先级排序。 ## 修法 水位对齐:新增 personas.fact_watermark = max(patient_facts.updated_at), 与 event_watermark 取「任一落后即重算」。存量行为 NULL → 判 stale,自然回填。 stale-scan 同步加这一维。 闸放宽必然带来「算了但没变」,所以补一道**写入侧的闸**(persona-diff.ts): unchanged 一字未变 → 不写 feature 行,只推水位 refreshed 只有时间派生值变 → 就地刷新,不升版本(记 refreshedAt) semantic 真变了 → 升版本 判据比 data(剔易变键)+ evidence.factIds,不解析 description —— 同 reason-refresh 思路, canonical 序列化抽成 common/canonical-json.ts 两边共用(jsonb 不保留键序)。 顺带:闸里查过的水位不再在正算路径重查(原来 transaction / persona 各查两遍)。 ## 本地验证(friday host,13,268 患者) 首轮回填 noop=0(旧代码这里会大批 noop)· success=48 · refreshed=11,155 · unchanged=2,065 → 写侧节流把 13,268 次升版本压到 48 次 再次运行 noop=13,268 全零其余,142s → 29s,幂等 事故复现 只改 fact 不动 transaction → 触发重算,¥24,994 订正为 ¥15,996,v1 留痕 ## 部署注意 patient_facts 370 万行,新索引请先手动 CREATE INDEX CONCURRENTLY 预建(迁移里是 IF NOT EXISTS,预建后即 no-op);上线后跑一次全量 recompute-persona 补 fact_watermark。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
- 探针只在 status=success 的空轮上跑(失败轮误报修复) - 探针反查带业务过滤,与真实增量拉取同一套 WHERE(退费单过度计数修复) 生产同样受此误报影响;本次仅合入,暂不部署生产。
luoqi committed -
测试服务器收到 CRITICAL「增量空转:PAC 拉到 0 行,但 DW 有新数据」。排查 sync_logs: 07-25 16:15 UTC failed fetched=0 err="fatal: unexpected end of file" 07-26 00:15 UTC success fetched=239959 tx=103652 ← 下一轮全额补回 即:一次瞬时 ClickHouse 连接断开导致该轮失败,游标未推进,下一轮把积压全额补回 (tx=10万+,远高于平时 ~1万),**无数据丢失**。告警是误报,两处 bug 叠加: ## ① 探针在失败轮上也跑(主因) 空转告警条件只看 `tx+dup=0`,没看 status。失败轮 tx=dup=0 → 走进探针 → 把瞬时网络失败误诊成「游标格式失效/空转」。失败轮本就经日报失败计数 + error_message 暴露,不该再被探针误标。 修:加 `status===SUCCESS` 守卫,只在**成功空轮**上探。 ## ② 探针反查不带业务过滤(放大误报) 探针反查只拼 `cursor > val`,而真实增量拉取还叠加业务过滤(结算正表 `is_refund=0 AND settlement_status=1`、退费表 `is_refund=1 OR settlement_status=4`、 结算方式 `settlement_status='1'`)。于是"游标之后、但会被业务条件过滤掉"的退费行 被探针数进去 → 真实拉取正确地不拉,探针却告警。 告警数据自证:settlement 正/退表各 5788 完全相等,正是这批全为退费单的铁证。 修:探针复用 extractBusinessFilters,与真实拉取同一套 WHERE。 ## 验证 - 388 单测通过(新增 6 例:各表业务过滤保留/剔除、括号 OR 不被 AND 拆开) - 失败原因「unexpected end of file」是瞬时网络错误,已自愈,无需数据补摄 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed
-
- 24 Jul, 2026 15 commits
-
-
## 问题 引擎变化判定只比 `(scenario, subKey)` 集合;集合没变就走 unchanged 分支, **plan_reasons 一个字都不改**。于是"只改 signals 内容"的算法升级对存量 plan 永远不生效 —— 生产 3,170 条高龄缺牙即使上线了按年龄排治疗(signals 新增 focusCategory / patientAge),仍显示「目标·种植」。 原先只能删库重算(丢认领、丢版本号、丢触达计数)或写一次性 SQL 回填(逻辑重复、 易与引擎口径漂移)。 ## 做法 unchanged 分支新增就地刷新:**不升版本、不动认领/状态/抑制窗/触达计数**。 同时补 plan.goal 的比较(goal 是静态文案,算法改措辞要能落到存量)。 刻意不动的字段:closedReason / closedAt(关闭状态)、source / sourceActorId / campaignId(来源溯源)、lifecycle;已关闭的 reason 行直接跳过。 ##
⭐ 关键:只在语义变化时才写 reason 文案与 signals 都内嵌天数(`${days} 天前` / `signals.daysSince`),天天变。 无条件刷新 = 每日重算把全部 plan_reasons 重写一遍(生产 23 万+ plan),纯废写 + WAL 膨胀。 判据(reason-refresh.ts):比 **signals(去掉 daysSince)+ evidence.factIds(排序后)**, **不解析文案** —— reason 文案完全由 signals 派生(诊断名←triggers、牙位←toothPosition、 治疗类目←expectedCategories+patientAge),所以比 signals 既充分又不依赖文案格式。 踩到并修掉的坑:**Postgres jsonb 不保留键序**(按键长+字节序重排),直接 JSON.stringify 比较会让库里读出的和新算出的永远不等 → 每次都判"变了"。本地实测连跑三次每次都刷新。 改用递归排序键的规范化序列化后,第二三次不再写。 ## 验证 - 382 单测通过(新增 16 例 reason-refresh 单元 + 2 例批量路径集成) - 本地端到端(90 岁吕学文,人为退回存量旧状态 + 认领 + 触达 2 次): 第 1 次重算 → 「reason 就地刷新 3 条」,signals 补上 focusCategory/patientAge、 文案变「未启动活动义齿 / 种植」、goal 换高龄版; version 仍为 1、status=assigned、assignee=u-tester、contactAttempts=2 全部未动; 第 2、3 次重算 → 0 条刷新(幂等,不产生废写) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
- 关闭原因冷静期:inaccurate / treated 由 14 天改永久 - 品牌患者数更新:瑞尔 351,024 / 瑞泰 64,883(瑞泰必须排除) - 新增「三之二、认领怎么捞」:快照与账本的分工、plan_event_logs 字段字典、 三条常用查询(处理过哪些患者 / 接手后放弃 / 认领时长分布) - 表关系图补 plan_event_logs - 坑表补两条:assignee_user_id 是快照、按 plan_id 聚合会因升版本重复计数 SQL 均已在生产库执行验证。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
排查分支状态时发现文档与脚本长期不一致:main 上文档早已写「部署脚本不写死分支」, 而 main 的脚本仍是 `git pull --ff-only origin main`(该改动只在 test 上)。 本次已 cherry-pick 脚本改动到 main,文档主体因此自然对齐,只补两处: 1.
⚠ ️ 回滚命令必须带 --no-pull —— 原文档写的 `git checkout <commit> && bash deploy/deploy-prod.sh` 在旧脚本下会紧接着 pull 回分支最新,**把刚回滚的 commit 又拉走且静默无提示**; 新脚本在 detached HEAD 下 fetch 会失败退出,不再假成功,但正确用法仍是 --no-pull。 并提醒回滚后切回 main,否则下次部署仍在 detached HEAD。 2. 补 DEPLOY_BRANCH 用法 —— 脚本一直支持,文档从没提过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
deploy-prod.sh 原写死 git pull origin main。改为 BRANCH=${DEPLOY_BRANCH:-当前分支}: 生产机停 main、测试机停 test 即各自部署对应分支;可用 DEPLOY_BRANCH 覆盖。 拉取改 fetch+checkout+ff-only(有分歧仍失败退出,不静默 reset)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
- 缺牙建议治疗按年龄排序(>70 活动义齿在前、种植并行保留;只改顺序不增删) - 「已完成治疗」「机会识别不准确」抑制期 14d → 永久(原档押注排除闸接手,生产证伪) - 认领闸:未认领只能看不能操作(服务端 20504/20505 硬拦),认领入口收口到列表页 - 历史联系摘要不再把未来排程当漏做(程序给今日锚点 + isFuture 标记 + taskStatus) - 关闭超时自动回收(默认关,开关 PAC_PLAN_AUTO_RECYCLE=on) - 新增 plan_event_logs 生命周期事件账本(认领/指派/返池/自动回收/召回反馈) 含 DB 迁移:20260724091507_add_plan_event_log 已在测试服务器(47.251.104.47)部署验证通过。
luoqi committed -
- 摄入契约收敛为 9 张自洽表(宿主 inline 自己的引用,全路径一致) - 结算 WHERE 迁移单一真理源 + 冷导入网页通道 + 数据对账 - push 预检(dryRun)+ 推送记录查询(HMAC 免登录) - 修 traceRawSourceTable union 回溯(refund_full 在 push/reparse 静默跳过) 阶段性完成,业务确认可合。
luoqi committed -
## 1. 自动回收改为默认关闭(PAC_PLAN_AUTO_RECYCLE=on 才跑) 它一直在生产跑着(ScheduleModule 已启用、服务已注册),日志可查:两天触发 9 次、 累计回收 32 单。前端那句"暂无自动回收机制"说的是倒计时控件被隐藏,不是后端。 关闭理由:回收会把 assignee_user_id / assigned_at **就地置 null**,而「认领」现在要 当作「该患者已被客服处理」用于统计 —— 每 10 分钟抹一次,统计地基是漏的。 代码保留不删:超时兜底的原始问题(领了不做 → 患者被锁死)依然成立,后续想改成 更长超时或"只提醒不回收",打开开关即可。启动时打印开关状态(它会静默改数据)。 注:生产当前登录角色全是 leader,自认领 / 自返池都在权限内,无"staff 无法释放工单" 的沉淀风险。将来放开 staff 角色需重新评估(staff 无 PLAN_RECYCLE)。 ## 2. PlanEventLog — plan 生命周期事件账本(append-only) 为什么 PlanGenerationLog 顶不上:它是**每次引擎跑批一行**的管道账本,**连 plan_id 都没有**,只记"引擎跑了没有、产出几个"(生产 750 万行 / 24 万患者,约 80 万行/天)。 回答不了"这个 plan 经历了什么"。 已收事件:claim / assign / release / auto_release(归属)+ feedback(召回
👍 👎 )。 assign 为门诊经理分配预留 —— actor != assignee 时自动区分,无需事后反推。⭐ 收录边界(注释里划死,防止变垃圾桶):✅ 人工动作 + 会被就地覆盖且事后无法还原的状态变更❌ 引擎重算 / supersede —— 80 万行/天,与人工动作(约 30~100/天)差 4 个数量级, 混入会淹没人工动作;引擎侧已有 PlanGenerationLog + followup_plans 版本流❌ 前端行为埋点(浏览/点击)—— 噪声大、可刷,属产品分析那一摊 ### 写入方式:应用层 + 同事务,**不用数据库触发器** 1. 触发器拿不到操作人 —— actor 在 JWT 里,DB 只看得见行变了;靠 session 变量传递 还是得应用配合,只是变隐式更易漏。本项目 AsyncLocalStorage 目前也不含 user。 2. 拿不到 actor 就分不出 claim / assign —— 这恰是本表最核心的价值。 3. 触发器会被运维脚本误触发:任何一句 `UPDATE followup_plans SET assignee_user_id=NULL` (数据修复 / 清理测试数据)都会凭空造出假的 release 事件。 4. Prisma 不管触发器,得走 raw SQL 迁移,schema 里看不见、易随漂移丢失。 弱点(已认):靠自觉。缓解 = 写入收口到 recordPlanEvent() 单一入口 + 只接受事务客户端 (状态变更与账本同生共死)+ 事件类型走枚举编译期拦拼写错误。 ### 扩展性 新增 PlanEventType + PLAN_EVENT_META(@pac/types,照 EXECUTION_OUTCOME_META 的样子): 事件类型的单一真理源,带 labelZh / group / byHuman / holdsPatient。 新增事件 = META 加一行,不是在调用处随手写字符串。 HUMAN_TOUCH_EVENTS 收口「算不算客服处理过」的口径(auto_release 是系统行为,不算)。 两个关键字段的存在理由: · heldSeconds 必须在清空 assigned_at **之前**算好(computeHeldSeconds),事后算不出来 · feedback 立 reason 列(up/down):followup_plans.recall_feedback 是就地覆盖的, 先👍 后👎 会把前一次冲掉,准确度统计只看最终值会低估分子、也看不出"改判" 配套:assign / recycle 接口补传操作人(controller 原先根本没取 user.sub, 自认领与指派他人在数据上完全无法区分)。 ## 验证 - 340 单测通过(新增 15 例:四种归属事件、幂等不重复记账、事件类型登记完整性、 auto_release 不计入人工、computeHeldSeconds 边界、开关默认关且只认显式 'on') - 本地端到端:认领 →👎 (带 note)→ 返池,账本 3 行齐全、held_seconds 准确; 同期 followup_plans.recall_feedback 只剩最终值 —— 正是就地覆盖会丢的那部分 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
上一提交(33dcecb6)基于错误的范围假设删了 fact_returnvisit_out 的 org 白名单, 现予撤回。摘要侧的修复保留不动。 ## 为什么撤回 生产的数据范围是**瑞尔 + 起始 2025-01-01**(存量 cold-import 圈定),瑞泰不在范围内。 按品牌重新切分后: 被该白名单丢掉的【瑞尔】记录 = 7,212 条,organization_id **全部为 NULL**, 时间跨度 2016-08 ~ 2021-01 —— 全早于 2025-01-01 起始线。 即:**在生产真实范围内,这个过滤造成的有效数据损失为零。** 之前那个"丢 98.8%" 是瑞泰的数字,而瑞泰患者本不该在库里(见下)。删掉白名单反而有两个副作用: 把不该进的瑞泰回访放进来 + 增量每轮拉取量 336 万 → 1,340 万(该表无游标)。 ## 顺带确认的两个事实(排查副产品) 1. **一个患者的回访确实跨多诊所**:瑞尔患者里 14.6% 跨 2 家、2.9% 跨 3 家, 最多 16 家(张学军跨 4 家)。所以过滤条件按 org 收窄确实有风险 —— 只是 实际被收窄掉的都是 NULL org 的老数据,没伤到有效数据。 2. **跨品牌同号不是同一个人**:196 万个跨品牌重号 id 里,姓名+生日都相同的 仅 10 个。manifest 顶部原有的"同号不同品牌=不同人"注释是对的。 ## 真正的问题在别处(本提交不处理,单独跟进) 瑞泰患者是**增量漏进来的**:07-15 存量跑当天瑞泰 0 人;07-16 起增量每天带入, 07-21 单日进 32,586 人(比当天瑞尔的 12,718 还多),累计 62,992 人。 根因是增量路径没有继承 cold-import 的 `--clinics` / `--since` 范围限定。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
生产排查(张学军 BJ0D036664 等)带出的两个独立问题。 ## 1. 摄入:删除 fact_returnvisit_out 的 org 白名单 原条件 `organization_id IN (SELECT ... FROM fact_emr_treatment_out)` 本意"与核心 数据同范围",但那张 EMR 表 **瑞尔 63 家 / 瑞泰仅 2 家**,而回访表瑞泰有 98 家: 瑞尔 保留 3,238,588 / 丢 7,212 → 丢 0.2% 瑞泰 保留 119,884 / 丢 10,039,288 → 丢 98.8% 后果:PAC 的 6.3 万瑞泰患者只有 5.4% 有回访记录(瑞尔 78.9%),历史联系整体空白。 回访是纯展示数据、不进召回信号,没理由绑定 EMR 诊所集合 → 删掉,范围交给 cohort。
⚠ ️ 排查中的一个反转,记在 manifest 注释里防后人再踩:DW 的 patient_id **跨品牌复用** (374 万 id 中 196 万重号,如 708971 在瑞尔=张学军、瑞泰=余盼盼)。所以 cohort 必须用 `(patient_id, brand)` 复合键;摄入侧 sourceUnit 取自本行 brand、按 `brand#patient_id` 索引患者,跨品牌行天然各归各家,不会串号。按品牌正确切分后,瑞尔患者其实一条没漏。 ## 2. 摘要:未来排程被写成"未执行,需优先补做" patient_return_visits.task_date **含未来排程**,而 prompt 既无今日锚点、又按 taskDate desc 排序 → 未到期的排程恒排第一被当成"最新一次",status=未回访 就被判成漏做。 生产实测:934 条已生成摘要中 196 条最新回访是未来排程,其中 158 条(81%)带 "未执行/补做/漏/尚未"措辞;对照组(最新在过去)仅 38%。 修复按"能程序算的事实全算好、LLM 只润色"这条既有纪律: · orchestrator 算 today 与每条的 isFuture(确定性比较,不交给模型) · prompt 渲染成【未来排程·尚未到期】显式标记 + 条数警告 + 硬约束文案 · 补 taskStatus(「已预约」≠「没做」,原先根本没喂给模型) · promptVersion → draft_recall_summary@2026-07-24-e ## 本地验证 - 325 单测通过(新增 11 例锁住"未来排程不得说成漏做" + promptVersion 必须 bump) - 摄入:导入瑞泰患者 殷海霖(2332662,12 条回访旧过滤下一条留不住)→ 12 条全部落库 - 摘要:吕学文(未来排程 2026-10-09 + 真·过期未执行 2026-03-28)连跑 3 次稳定输出 「3月28日种植咨询回访未执行…需优先跟进;…10月9日已排复查洁牙邀约」 —— 未来的说"已排"、真漏做的仍报出,两个方向都对 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
三处业务反馈跟进。 ## 1. 「机会识别不准确」14d → 永久 原 14d 的理由是"算法修好后本就该重新召回",但那是**我们**的排期,不该由 患者兜着 —— 两周后同一条不准的召回原样弹回来,客服只会再关一次再骂一次, 而算法多半还没改。真要在修复后放回来,应由「改完规则 → 定向重算/解除抑制」 这条显式动作驱动,而不是赌"两周内一定修好"的定时器。 至此 WEAK_SIGNAL_SUPPRESS_DAYS(14d)的两个使用者(treated / inaccurate) 全部转永久 → **删除该常量**,免得后来者把新原因挂到一个已被推翻的假设上。 ## 2. 认领入口收口到列表页 详情页顶栏的「认领」按钮去掉,改为**只读**徽标(未认领 · 仅查看 / 仅查看)。 同一动作两个入口会让状态同步和归属口径都变复杂;列表页本就有行内认领 + 批量认领,那里才是自然入口。拦截文案同步改为指回列表。
⚠ ️ 随之出现的缺口已一并补上:列表认领只 patchItem 本地行,详情页不会知道 → 刚认领完回详情点操作仍被拦。为此给 plan-sync-store 加**反向通道** notifyClaim(列表 → 详情),认领/返池后详情页的闸即时解锁/上锁。 assigneeUserId 用 undefined 表示"本次事件与认领无关"(不能用 null, 那会被当成已返池而误上锁)。 ## 3. 「潜在」「预约」也过认领闸 顶栏跳宿主的三个动作(潜在/预约/回访)现在口径统一:全部过闸。 「潜在」虽是查看,但看完顺手就在宿主侧动手了,而 PAC 这边没认领 = 没归属人, 回头对不上账。统一口径比"哪个算查看"的细分更好维护。 ## 验证 - 314 单测通过(抑制期用例改用 unreachable/wrong_number 的 30d 档 继续锁住"短档不被 outcome 60d 盖掉"这条红线) - 本地真实患者(吕学文 90 / 李石明 78,已从本地 CH 摄入)实测: 未认领 → 顶栏「未认领 · 仅查看」徽标、点「潜在」被拦并提示去列表认领; 列表点认领 → 详情页徽标即时消失、「关闭机会」弹窗正常打开(未重拉页面) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
两项业务反馈驱动的改动。 ## 1. 「已完成治疗」抑制期 14d → 永久 原 14d(WEAK_SIGNAL_SUPPRESS_DAYS)押的注是「等一个 DW 同步周期 + 排除闸 接手」,生产实测证伪:缺牙召回两例(吕学文 DW 无义齿完成记录、李石明义齿 在上颌而召回在下颌)排除闸都接不住,到期只会原样弹回来再挨一次投诉。 档位本身也不自洽 —— 「已在外院治疗」早就是永久,在别家做完永久闭嘴、 在自己家做完两周后再问,说不通。 误判不会把患者封死:抑制是**信号级**的(key=scenario|subKey,subKey 含 牙位)且带时间锚,结案后新发的同类信号照常放行。 WEAK_SIGNAL_SUPPRESS_DAYS 现仅剩「机会识别不准确」使用(算法问题,修好 规则后本就该重新召回)。 ## 2. 认领闸 —— 未认领只能看,不能操作 submitExecution 原先只校验 status、**完全没看 assignee**,生产上因此 出现两条 plan 在 assignee IS NULL 时被直接关闭。危害不止撞单: execution.service 特意保留 assigneeUserId 作业绩归属(清了会让 「我的已完成」/ 转化率永远为 0),未认领就提交 = 这条执行没有归属人, 统计里直接蒸发 —— 前端提示替代不了服务端约束。 新增 plan/claim-guard.ts,两种拒绝态分码(20504 未认领 / 20505 他人认领, 文案必须分开,否则组员以为自己没点对)。 过闸:提交通话结果 / 关闭机会、重生成话术(含 SSE)、话术反馈、召回反馈 不过闸: · assign(认领本身是入口,过闸会死锁) · recycle(PLAN_RECYCLE 是 leader 独占「回收 staff 已认领的单」, 按"必须自己认领"拦会把组长回收功能整个废掉) · recompute(ops 工具) · 只读端点 + 「潜在」(查看类)+ 刷新(拉数据,不改业务态) 前端同步:详情页按钮**不禁用**,点了才讲原因(禁用会让客服不知道为什么 点不动);顶栏未认领显「认领」按钮(闸的唯一出口)、他人认领显「仅查看」。 ## 验证 - 314 个单测通过(新增 claim-guard 5 例 + treated 永久 2 例) - 本地端到端:未认领 3 个入口全返 20504;他人认领返 20505 带占用人; 自己认领后放行;treated 关闭落库 snoozed_until = 36500 天(99.9 年) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
现象(业务反馈):90 岁(吕学文)、78 岁(李石明)的缺牙召回,界面一律显示 「目标 · 种植」,话术也推种植。业务反弹"为什么给 90 岁老人推种植"。 根因:UI 取 signals.expectedCategories[0],而 K08 的 categories 首项恒为 implant。 整条链路(目标标签 / 理由文案 / goal / 话术)都没有年龄维度 —— 召回场景 SQL 与 priority-scorer 里一个年龄条件都没有。 ## 修法:改在诊断→治疗的源头,不在流程末端打特例 canonical-codes.ts 新增 recommendedCategoriesForAge(categories, age) 作建议侧单一源: 候选同时含 implant + prosthodontic 时,age > DENTURE_FIRST_AGE(70)→ prosthodontic 提前。 scenario / 前端 reason-line / 话术 prompt 全部调这一个 resolver,不各写年龄分支。 ## 业务方口径(对齐后修正) 初版做成了"高龄禁推种植",过重。业务反馈:**70-80 不是种植禁忌**,能否种植取决于 骨量骨质 / 全身状况 / 患者意愿,由医生评估。故改为: - 建议**只调顺序**:活动义齿在前,**种植并行保留**(不删) - 话术从"严禁推荐种植"改为"先讲活动义齿,种植可并行提及;不主推手术、 不承诺能不能种 —— 落到来院让医生按身体条件评估" - PAC 不替医生下禁忌结论 ## 三条设计红线(各有用例锁住) 1. 只排序不增删 —— 任何年龄下返回集合恒等于入参(种植不会被抹掉) 2. 不碰排除闸 —— DiagnosisTreatmentMap[].categories 仍是完整候选集 (否则高龄患者做过种植反而排除不掉 → 重复召回) 3. 不碰"要不要召回" —— 缺牙影响咀嚼营养,高龄同样该管 ## 改动 - canonical-codes:DENTURE_FIRST_AGE=70 + recommendedCategoriesForAge + treatmentCategoryNameZhFor - reason-signals:signals 加 focusCategory / patientAge(均 optional,旧 plan 回落原行为) - scenario:SQL 取年龄 → 走 resolver;高龄 goal 文案(义齿为主、种植并行) - 前端:目标标签用 focusCategory;理由行调同一 resolver 显示「活动义齿 / 种植」 - 话术:fact-block + stable prompt 加高龄沟通约束;三档 promptVersion bump 实测(本地 75 岁真实患者重算):focusCategory=prosthodontic · patientAge=75 · 理由「…未启动活动义齿 / 种植」。308 tests passed,service + web typecheck 干净。⚠ ️ 存量:plan 变化判定只比 (scenario, subKey),signals 变化不升版本 → 已有 plan 不会自动拿到新字段(前端回落原行为)。生产 775 个 80+ 缺牙患者 需回填或强制重算才生效,另行处理。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
对接反馈:① 文档站 API 参考没更新 ② 查推送记录还要登录,联调不便。 1) 新增 GET /pac/v1/push/logs(HMAC 验签,免登录) 复用宿主推送时已在用的 X-PAC-Host-Id / Timestamp / Signature 三件套,联调不必另开账号; GET 无 body,签名对空串算 hex(HMAC-SHA256(`<ts>.`, secret))。 **不是公开接口** —— 签名错误返回 10106,拿不到任何数据,不泄露宿主推送情况。 与登录态 /admin/host/self/push-logs 同一实现(sync.module 引入 AdminModule 复用 HostsService.getPushLogs,两模块无环),返回同一份数据。 2) API 文档补全:@Query 缺 @ApiQuery 导致 Swagger 采不到 —— push/rows 的 dryRun、 push/logs 的 limit 现已进 spec;重新 dump 静态 spec(apps/pac-docs/openapi/pac.json, 59 paths),文档站 API 参考随之更新。 3) 契约文档 §7.2 改写为「两种查法」:免登录(HMAC,联调推荐)+ 登录态,并说明签名对空串计算。 验证:正确签名返回推送记录、错误签名被拒(10106 签名不匹配);typecheck 干净,299 测试绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
FRIDAY 开发对接 push 时需要"我推了什么 / PAC 收到什么 / 成了几条 / 错在哪",补两个自助能力: 1) 预检 POST /pac/v1/push/rows?dryRun=1 跑完整管线(transforms + 装配 + 归一 + 租户解析)但**不落任何库、不触发重算**, 响应带 dryRun 标志与各资源**样本 canonical** —— 宿主可直接核对 PAC 把原生行翻译成了什么。 复用 cold-import 既有 dryRun 通路(processSubject/processPatients 已支持), ingestRawTables 此前硬编码 false,现按 opts.dryRun 透传;预检批次 syncLog 以 triggeredBy `push-dryrun:` 前缀标识,不与正式推送混淆。 2) 推送记录 GET /pac/v1/admin/host/self/push-logs?limit=50(宿主自助权限) 按时间倒序列最近 N 批(上限 200):source / status / dryRun / 收行数 / 落库数 / 去重 / 失败 / 报错原因。sync_logs 本已记全,此前未对宿主暴露 —— 当时的同步响应 没留存也能事后查"哪批错了、错在哪"。 文档补 §7「对接自测」:预检用法与通过标准(failed=0 且 mappingMisses/suspectFields=0)、 推送记录、可用面(API 文档 /api/docs、宿主自助页、队列面板)、常见错误码速查。 验证:本地实推 —— 预检 6 行→"若推会落 5"(1 噪音被过滤)、返回 payment/refund 样本、 **患者数仍为 0 确认零落库**;push-logs 正确区分预检(dryRun=true)与正式批次。 typecheck 干净,299 测试绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
两条纪律收紧(宿主零改名、零行过滤): 1) inline 列不再用 PAC 自造名,一律沿用宿主源列名: - customer_basic_info:phone/phone_relationship → contacts_tel/relationship(customer_contacts 原名) - med_emr_info.diag[]:stdCode → std_code(std_diag 原名) - class_name / organization_id / plan_name 本就是宿主原名,不变 宿主无需为 PAC 做任何字段改名。 2) 文档删除各表「推送范围」:med_emr_info 那条 status∈{3,4} 是残留的宿主侧过滤, 而 manifest E.0 已在 PAC 侧排草稿 —— 重复且违背单一真理源。改为 §1 统一声明 「整表照推,宿主不做任何行过滤;status/金额切分全部由 PAC 完成」。 验证:元和王永 dry-run 与改前逐项一致(patients=16472 txns=117718 failed=0; phone/K04/modality/退费 190+308 全对);typecheck 干净,299 测试绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed
-
- 23 Jul, 2026 8 commits
-
-
push 自洽收尾(判据:PAC 已有主体的不 inline,值不可信的才 inline): - refund_item:spec.patient_id 部分品牌是诊所本地垃圾 id、不可信 → 宿主导出时 JOIN 结算头 把真 patient_id 反填进 spec(inline);删 S.3.1 跨表 lookup,refund_item 单表 push 自洽。 (patient_relation 不动:referee 本身是 PAC 患者实体,referee_sex 可从自有实体解析,无需 inline) - drift 误报修复:refund_full/refund_item 共用 subjectType='refund' 但源表列集不同, detectRawColumnDrift 按「与本批列签名 Jaccard≥0.5」自选同源样本,排除跨源混列(消除 suspectFields 虚警) 验证:元和王永 dry-run refund_item=308(样本 patientExternalId=659325 真 6 位 id,无 lookup)、 refund_full=190、failed=0;299 测试绿、typecheck 干净。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
manifest 共用于所有摄入路径(cold-import/file/pull/push/reparse),改它一处全生效。 删 5 处跨表 lookup + 5 张源表,宿主导出时 within-host join 后 inline: - 私有字典(std_diag/std_check_class)→ diag[].stdCode / med_check.class_name(删 lookup + 源表) - 联系方式 1:N(customer_contacts)→ customer_basic_info.phone/phone_relationship(挑默认号) - 计划头(customer_treat_plan)→ 计划行 organization_id/plan_name(删 lookup + 头源表 + group) - 支付通道(settlement_modes)→ 不摄入(金额在结算头 net_receipts_this;payment 去掉 method) - incremental 清废弃表名(patient_settlement_refund/spec_refund)+ 补 patient_settlement_spec export.sh 同步改成自洽形态(within-host join;含 diag stdCode 跨库预取 map); image.yaml primary image_rows→med_check;payment.yaml 去 method。 验证:元和王永 16472 患者 dry-run 逐资源计数与 inline 前**完全一致** (patient/diagnosis 4458/treatment 5781/image 6170/payment 20995/refund 190+308); 中间表 41→33;typecheck 干净;全量 299 测试绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
确立职责边界:宿主推送前解析好自己的引用、inline 进相关行;PAC 只做跨宿主临床归一。 14 → 9 个 source,删掉 5 张 lookup-only 源表: - std_diag / std_check_class(纯翻译字典)→ 宿主解析 stdCode/class_name inline 进 diag[]/med_check; 省掉大表全量重推 + PAC 持久化字典存储(仅留 _shared 跨宿主临床字典) - customer_contacts(PAC 只要默认号+归属)→ inline phone/phone_relationship 进 customer_basic_info - settlement_modes(总值在结算头 net_receipts_this,24% 多通道拆付暂不需要)→ 不摄入 - customer_treat_plan 头(PAC 只取诊所/方案名)→ inline organization_id/plan_name 进计划行 结果:每张 source 自洽,增量推变更行天然成立,无跨表依赖/无新表/无 hydration。 注:manifest + export.sh 的对应改动(删 lookup、宿主侧 join inline)随后跟进。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
- std_check_class 此前仅在 med_check 末尾一行带过 → 补成与 std_diag 对齐的正式字段小节; §4 病历链 3→4 个 source(14 张源表全部有独立小节) - 两字典作用域澄清:按品牌维护、但 code(diag_code/class_code)全局唯一 → lookup 不需租户限定 - 推送方式从「变更时全量重推」改为「按 code upsert、增量推变更行」(与其他表一致); 规模更正:std_diag 生产多品牌合计 1.5万~3万+行(非测试库 4 品牌的 2739),全推确实浪费。 依赖 PAC 侧持久化字典存储(落地中) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
union-primary 资源(refund_full_rows ← union[status4, status3负额])此前回溯不到源表 (traceRawSourceTable 只认单 input/output),导致 push source=patient_settlement 时 refund_full assembler 不入选、reparse 也跳过 → **整单退费在这两条路径静默丢财务数据**。 修:union 各输入独立回溯,全部收敛到同一根表(两路都 → patient_settlement)才返回该根; 发散到多根则停在 union 输出(维持旧行为不误判)。export 供单测。 验证:108467 重推 patient_settlement → refund_full 落库(txn=1 新增,2 payment 幂等 dup); 单测 6 绿(union 收敛/发散/单链/route/lookup);全量 299 绿无回归。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
- friday-push-payload:结算段从「宿主分流 4 source」改为「推 3 张原表、WHERE 全在 PAC」; 删 patient_settlement_refund/spec_refund 两个已废弃 source;新增 spec.patient_id 不可信说明; source 计数 15→14 - 新增 data-reconciliation:源
↔ PAC 计数对账口径 - ingestion / meta:配套更新 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
- 结算/病历 WHERE 从导出侧迁入 manifest transforms(单一真理源):宿主推原表全 status, PAC 侧 filter 切消费/退费/退费明细;transforms 新增数值算子 lt/lte/gt/gte - 退费明细身份纠正(S.3.1):spec.patient_id 部分品牌是诊所本地 id 不可信, 改从结算头 settlement_id→patient_id 继承(元和王永实测 26421 行全不符) - 冷导入网页上传通道:上传 zip → yauzl 白名单解压 → 迷你 data 根(含 _shared)→ 队列 cold-import;宽容缺独立表 + 成组完整性校验 + dry-run 预检 - 数据对账:日报复用摄入查询做「源
↔ PAC 去重患者数」比对(clickhouse countDistinct) - 测试 18 绿:friday-settlement-where / filter-numeric / cold-import-extract / cold-import-groups Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
现象:测试服 2026-07-23 8:00 报「DW 数据滞后」,增量游标 33.7h 未推进。 根因:sync_logs 用 partial UNIQUE(host_id) WHERE status='running' 作并发锁。 部署 force-recreate 重启 service 容器时,正在跑的同步进程被杀,但那行 running sync_log 来不及标终态 → 永久占住锁 → 之后每次 cron 增量都撞锁被 skip → 游标不动。代码注释早写了"进程崩需依赖 stale 清理",但该清理从未实现。 修复:SyncIncrementalSchedulerService.onModuleInit 注册 cron 前,先把 startedAt 早于本进程启动时刻(PROCESS_STARTED_AT)的 running 行标 failed。 判据安全性:sync 只在本 service 进程的 cron 回调、或一次性 CLI(跑完即退)里跑, 两者都不可能比本进程启动得更早还活着。反过来,本进程启动后新建的 running (可能是并存的手动 CLI 真锁)用 lt 严格排除,绝不误清。 幂等:标 failed 只释放锁,不动数据 —— 游标没推进,下次增量靠 48h 回看窗补齐。 数据零丢失:回看窗默认 48h > 本次空档 33.7h,靠 source_event_id 幂等去重完整补回。 测试:sync-lock-reap.spec 覆盖 回收/lt判据锁/空启动/失败不抛;295 全绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed
-
- 22 Jul, 2026 3 commits
-
-
三件事,因为改动在 plan.service / execution.service / types 里交织,合成一次提交。 ## 1. fix:话术自报家门串名(线上 bug) 现象:登录人是李倩,话术却写"我是本诊所的客服主管晋芳辰"。 根因:登录客服姓名被烤进 LLM 生成的正文,而 plan_scripts 的 UNIQUE 是 plan_id (一个 plan 一份话术、不分人),召回池又是共享的 —— 谁先打开触发生成, 姓名就固化给后面所有人看。线上 44 条话术 100% 中招,是必然不是偶发。 解法:姓名不进 LLM,改「生成期占位 → 渲染期回填」 - 新增 shared/agent-identity.ts 作单一源(占位符 + resolveScriptAgent + renderAgentIdentity) - 稳健/标准/深度三档 prompt 与 format.md 统一输出 【回访客服】(沿用 v8 立的 `{}`=填空 / `【】`=原样保留 的约定,归第二类) - 四个出口渲染回填:详情聚合、strict Zod 详情、同步重生成、SSE 流式 - DraftPlanScriptInput 去掉 agent 字段 副作用(正向):input_hash 不再含人名 → 同一 plan 不同客服共享 AI 缓存; 工单转派后名字自动跟着换,不用重生成。 三档实跑验证模型都保留了占位符;浏览器验证同一份缓存对两个登录人显示各自姓名。⚠ ️ 存量 44 条仍是旧正文,需部署后逐条重生成(dist/cli/ai-gen-script.cli.js)。 ## 2. refactor:删 plan_executions 两个死字段 - invalid_reason 从没被写过(pac-web 零引用),读回路径也没带它 - abandon_other 写得进没人读,且信息恒冗余 —— 放弃原因是固定复选框无自由文本, 该列只可能取到字面量"其他",而 abandon_reasons 已含 'other' 生产 0 行受影响(执行总数 2,两列全空)。 ## 3. feat:宿主回访模式的「关闭机会」闭环 瑞尔侧回访在宿主系统做,PAC 只提供关闭入口做闭环。**刻意不引入新概念**: 不加 closed outcome、不加 close_reasons 列、不加 plan 状态 —— 关闭就是 outcome='abandoned' + abandon_reasons,跟 PAC 自带表单同一条路、同一列。 分叉只在展示层(ABANDON_REASON_META.shownIn,两个面板各渲染各的子集)。 - AbandonReason 扩成两种模式的并集;近义项不合并(两模式按 host 配置二选一、 运行时不共存,同一 host 只出现自己那套 key,统计不会碎) - 关闭弹窗改多选无上限;「其他」说明写 notes,不另立列 - 原因级抑制窗:各原因 suppressDays 取 max(每个原因独立成立,取最严的); 未配的沿用 outcome 默认 60d,行为跟改造前一致 - 「识别不准确」不再双写 recall_feedback(该表留给拇指控件那条路) - outcome-form 的中文硬编码数组 + 中译映射表删除,key/label 改为同源 (那张表里「已转介他人→treated_elsewhere」是语义错位) ## 文档 新增「取数说明」(pac-docs)—— 给 DW 团队自助查生产 PG:召回池筛选条件、 关闭操作落表、数据范围过滤(品牌/诊所/时间)、字段字典、常用 SQL。 291 tests passed;service + web 双端 typecheck 干净。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
清缓存与 reparse/persona/plan 不是一类动作(不重算、秒级、按需重生成), 放进同一张表会误导"改 prompt 也要跑重算"。改为表下引用块说明。 顺带修补编辑残留:表格首行多一列、③ 代码块缺收尾 ```。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
luoqi committed -
面向"自己动手部署"的 runbook,不重复 deployment.mdx 的架构介绍,直接给命令: - 三环境速查表(位置/目录/分支/DB/部署方式)+ 分支规约 - 本地开发(docker 基础设施 + 原生跑应用)+ 4 个常见问题 - 测试/生产部署命令(生产强调 COMPOSE_MANAGED=1 与 source .env) - 部署后要不要重算的对照表 + 固定顺序(reparse→persona→plan→清缓存)+ 耗时参考 - 定向补数据(PAC_COHORT_ONLY_PATIENT @file)参数与分批阈值 - 失败处理/回滚、7 个踩过的坑(含 pkill 自杀、ClickHouse 256KB 上限)、日常巡检 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
luoqi committed
-
- 21 Jul, 2026 7 commits
-
-
# Conflicts: # apps/pac-service/tests/recall-suppression.spec.ts
luoqi committed -
luoqi committed
-
背景:DW 病历表 patient_id 错位污染(高逸铭案例),需按名单重摄 3 万患者。 沿用现有 PAC_COHORT_ONLY_PATIENT(cohort 收窄)时发现两个阻塞问题: 1)
⭐ 静默丢数据:只要 manifest 配了 incremental.per_query(jvs-dw 配了), cold-import 任何模式都写 cursor_after=run_start。定向轮只覆盖名单内患者, 却推进全局水位 → 名单外患者在本轮期间的 DW 新写入被永久埋在游标下方。 (与本次排查发现的"12,595 个患者符合 cohort 却从未摄入"是同类机制) 修:定向模式下 cursor 保持上次水位不推进,daily 增量照常从原点接力。 2) 大名单 E2BIG:3 万个 id 的逗号串约 275KB > 环境变量单值 128KB(MAX_ARG_STRLEN)。 修:支持 PAC_COHORT_ONLY_PATIENT=@/path/to/file(每行一个 id);逗号列表照旧兼容。 顺带:定向生效时打印命中患者数,便于核对名单规模。 测试 7 例:空/空白不误判定向、逗号列表、@file(含 3 万行)、文件缺失须抛错不静默退化。 生产验证(高逸铭组 7 人小批):删除→重摄后 高逸铭 62 事实/6 诊所 → 21/1(上海时代),别人的数据清光 刘晓旭 13 → 25、徐嘉悦 126 → 130,被吞的数据补回,诊所归属全部正确 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>luoqi committed -
跟进诊所重归属(7b4f0d93)的补丁:诊所隔离是硬边界(list/detail 都强制 targetClinicId ∈ scope.clinicIds),已认领单漂出原认领人 scope 后会变成 谁都看不见的孤儿单(assigned 不进池、新店客服非 assignee、原客服出 scope)。 - unchanged 就地改归属:若 status=assigned 且诊所变了 → 返池(assignee/assignedAt/recycleAt 清空) - 升版本路径:clinicMoved 时 assigned 不继承,新版本直接回池(与上同规则) - 两路径都记 log(哪单从哪店改到哪店),上线后可巡检 - 测试 +3:两路径返池 + 归属未变时继承不误伤(12/12);顺带修 mock seed 缺 targetClinicId Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
luoqi committed -
业务口径(2026-07):运营按客户最后一次到诊的诊所做跟进分类,谁家最后来过就归谁家客服 ——原口径 target_clinic_id = 触发诊断 fact 所在诊所,会把主诊所患者顺路在别处拍片/被诊断 的人错分到那家只去过一次的诊所(反馈案例 BJ0D046171:150+次天使大厦、1次印象城影像AI诊断 → 却进了印象城列表)。适用全部召回场景,不止影像 AI。 - target_clinic_id = 患者最近一条 encounter/emr(到诊)所在诊所;无到诊记录兜底回退诊断诊所(不置空) - unchanged 分支(reason 未变)也就地改归属,不升版本 → 普通重算即可修复存量错分 - 批量 prefetch 修 uuid bug:patient_id IN (text 参数) 对 uuid 列在 PG 报类型错, 批量整体静默退回诊断诊所(修不动);改 = ANY(::uuid[])。本地 2470 患者全量重算后残留错分 0 - 单刷路径 resolveLastVisitClinic 直查 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
方案 A 落地(jvs-dw): - incremental_cron: '15 0,8-22/2 * * *'(08:15~22:15 每 2h + 过天 00:15; cron 小时域无 24,'0' 即用户说的"24点"次日凌晨轮) - 空轮短路:transactionsWritten=0 且 patientsUpserted=0 → 跳过 persona/org-tree/plan, 空轮成本收敛到每表一条 cursor 探查(秒级)。条件保守:患者主档变化不产事务但进画像,不跳。 背景:DW 当前每日 08:00 一批(实测 13:56 时三表 updated_date 仍停昨日 23 点), 日内轮询换来的是 ① DW 迟到/失败当天自愈(原等 24h)② 手动补摄自动收口 ③ 瑞尔 DW 计划提频 2h,PAC 先就绪。防重前提已具备:并发锁 + cursor 幂等 + 同行去重。 计划重算链路不动(空轮短路后不会被空跑)。 验证:cron 库实测 next 14 次触发序列正确;manifest 解析 auto_sync/cron 正常;tsc 干净。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
luoqi committed -
业务定调(张雪反馈):一线关注真金白银,应收对免费套餐/团购客严重虚高 (胡新丽:应收¥1,270 实付¥0;张弦:应收¥77,795 实付¥57,887)。 - jvs-dw payment.yaml:amount settlement_money(实付);friday:net_receipts_this - 新增 receivableAmount(应收)辅助字段:canonical schema 显式声明 + moneyFields 登记同口径归一 + payment_record.content.receivable_cents - discount-anchor 折扣率分母换 receivable_cents(旧事实 fallback amount_cents) - 顺带修掉两个隐藏口径 bug: ① refund 一直是实收冲减 → 原 LTV 实为"应收收入−实收退款"混口径,现自洽 ② 储值客户双计:充值(实付)计一次 + 扣卡消费应收又计一次,现扣卡单实付0不再重复 下游自动切换:profile.ltv(UI 累计消费)/ rfm.M 值 / lifecycle 消费档 → 实付口径。 金额守恒已对账(张弦 mode 表 17 行:实付57,887/应收77,795,无多通道重复计)。 上线步骤:部署后跑 reparse(rawPayload 完整保留源列,无需重拉 DW)→ persona 重算。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
luoqi committed
-