1. 05 Jun, 2026 1 commit
    • docs: 删 W1 评审过程稿 + 新增 AI 话术生成设计 + 修死链 · d4865450
      - 删除 db-review / db-suggest-after-review / db-review-confirm(~3.5k 行 W1 评审
        ping-pong,决议已并入 db-design-v2 §13 + schema.prisma,无独立价值)
      - 修 db-design-v2 / treatment_aftercare_recall 对已删稿的死链
      - 新增 docs/algorithm/ai-script-generation.md:话术生成分档设计
        (投入档=输入/输出/步数联动;脊柱=AiCallRunner、策略=AiCall;
         知识 vs 模板分离;ScriptContext 聚焦切片不含治疗链;单一源收口清单)
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
  2. 04 Jun, 2026 4 commits
    • fix(ingest): image_finding SQL 适配 prod CH 23.8(分层嵌套避免 多JOIN+ARRAYJOIN) · 4eee5874
      prod CH 23.8 拒绝「Multiple JOIN + ARRAY JOIN 同层」且不支持相关子查询。改成分层嵌套:
      s1(image⋈client⋈org,无 array join)→ s2(array join 读 s1)→ 外层。
      cohort 注入(裸 IN)落到 po 子查询 GROUP BY 前 → po 按 batch scope + 外层 notEmpty(po.org)
      过滤到 batch 患者(prod CH 实测等价正确)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(ingest): 本次禁用 image_finding(prod CH 不兼容 多JOIN+ARRAYJOIN) · 4400e2a7
      prod ClickHouse 拒绝「Multiple JOIN + ARRAY JOIN 混用」+ org 子查询 GROUP BY 带偏 cohort 注入。
      本地 CH 24.3 容忍故未暴露。暂注掉 image_finding_rows query + assembler 注册,
      放行召回质量/联系人/回访 3 块;影像 AI 信号源作 follow-up(经 prod CH 重做 SQL)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(recall+ingest): 召回质量收口 + 联系人/影像AI/回访 三个新信号源 · 4a7750d0
      召回质量(摄入层 yaml + 召回 scenario,host 无关引擎增强):
      - C.10 治疗痕迹覆盖扩(治疗后/术中/X后/粘结中 + 修复/备牙/取模/根尖/植骨/错颌 keyword)
      - treat_plan/dispose 定向合并(real_no_tooth 闸 path2:治疗无牙位→dispose 找回牙位)
      - 牙位归一:全角字母数字折叠(RCT→RCT)、裸象限数字过滤(笔误 16→1 幽灵召回清零)
      - keyword none 排除 + 切段含全角标点 + 若条件从句 strip
      - 治疗家族 resolver / 同牙取代 / oracle 对账(差分 0)
      
      联系人(patientpatient 自关联,替代 primaryContactType):
      - 新 PatientRelation 边表(referee → 关系人即 patient,姓名/电话现取零冗余)
      - 删 primaryContactType 全链路;详情页"联系人"行
      
      影像 AI 信号源(image_analysis → diagnosis_record,code_source=image_ai):
      - 源 SQL pivot+炸牙位(病种→K码),独立 subject,去重靠召回 (subKey,tooth) 聚类
      
      回访展示(独立表,5 试点):
      - 新 PatientReturnVisit 表(org∈5试点 + customer_id→patient),详情页"历史联系"滚动卡
      
      裁决:referee/image/returnvisit 已接(天然试点 scope);complain/consult 全集团覆盖不全,不接。
      本地 191/200/1000/2000 四样本对抗验证:幽灵召回 0、resolver 漏判 0。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(recall): 治疗家族 resolver + 拔除 afterDx + 乳牙归一 + oracle 对账工具 · 6d201fdf
      召回核心(scenario + canonical-codes):
      - 治疗家族 resolver(resolverCategoriesFor):11 张码白名单 → 结构家族一张表 +
        牙周/正畸沿用。结构码(K00-04/08/09)= 任何局部结构治疗算已治(充填/根管/冠桥/
        种植/外科/美学/儿牙);刻意排除 牙周/正畸/预防/复查流程 → 不跨病误销。
        修李梦维 1B 贴面误召;差分实测 18 例补判全合理、0 误销。
      - ⑤c 拔除改 afterDx:拔除只终结诊断前/同时的病;诊断在拔除后=新信号(不压)。
      - toothArrSql 乳牙归一:对齐 toothSet 只剥"空格+牙面字母",保留 Palmer 字母。
        修旧实现把 1D/1E 塌成象限 1 的错标 + 整象限过度相减漏召(差分 26 患者)。
      - expectedCats(窄,展示"未启动 X")与 resolverCats(宽,判已解决)拆开。
      
      前端对账工具(差分验证):
      - recall-oracle.ts:召回算法独立第二实现(按单牙时序状态机),共享家族配置、
        判定逻辑独立 → 与生产 SQL 差分比对,分歧即 bug 捕获点。
      - tooth-timeline:顶部对账面板(✓一致/仅生产/仅oracle)+ 每泳道召回徽标。
      - 去掉治疗链判断(plan-detail-app reasonAltClosed 过滤)——治疗链弃用第一步,
        召回显示统一收口到「召回算法 + 牙位事实」。
      
      本地差分全量验证:605 一致 / 0 分歧。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
  3. 03 Jun, 2026 9 commits
    • feat(recall+ingest+ui): 深窝沟去信号 / 处置→治疗 / 按牙相减召回 / 牙位事实抽屉 · 59038480
      后端 · 召回 & 摄入口径
      - diagnosis.yaml:深窝沟/脱矿/牙本质敏感/牙菌斑堆积/食物嵌塞 摘出疾病码(预防/风险/症状,
        落 code=null 不召、病历照原文显示;菌斑性龈炎等真病灶保留)
      - 处置(EMR.dispose)→ actual 治疗:treat_plan 空时拆 dispose,message union 进 treatment_actual_rows,
        复用现有 keyword_mapping 分类 + treatment.parser(不加 assembler/parser/置信度)。
        filter `empty` 谓词升级:空壳 JSON 数组([{treatName:""}] 占位行)也算空 → 正确去 dispose 找治疗。
        treatment_actual keyword 顺序修:窝沟封闭提到充填前、牙周去裸"洁牙"(避开"清洁牙面"误命中)。
      - 召回按牙相减(修多牙诊断被部分治疗整体误抑制):
        scenario LATERAL 算 sig_teeth / resolved_teeth(⑤a同类∪⑤c拔除∪⑤e替代,按牙)/ remaining;
        有牙位信号 → 剩余未治牙位非空才召、reason 牙位=剩余;全口信号沿用 category 级。
        例 龚靖舜 浅龋@16;26;46;36 补了16 → 召 caries_no_filling@26;36;46(不再整簇误压)。
      
      前端
      - 关键事实卡加「牙位 →」抽屉:每颗牙 + 全口(牙周/正畸/其他)泳道,时间倒序、零推断展示。
      - 事实时间轴 + 牙位抽屉:同一时刻按类型排序,随整体倒序(治疗→计划→建议→影像→诊断)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(chain): 替代闭环类别补 cosmetic(贴面)+ altSources 去重 · a4973449
      markAlternativeClosed 的 ALT_CATS 漏了 cosmetic — 同牙做了贴面(定性修复)
      不算"已被替代",导致 K00 乳牙滞留链卡在 discovered:
        李梦维 K00 1B;4C — 4C 粘结桥(prosthodontic 已覆盖)+ 1B 贴面(cosmetic 漏判)
        → 全牙位覆盖闸不通过 → 链一直"已发现",实际两颗都已修复。
      加 cosmetic 后两颗全覆盖 → 链闭环(已被替代);画像缺口 2→1(只剩种植46,与召回一致)。
      顺带 altSources 去重(同牙多条同类 actual 如"贴面修复+戴贴面"会重复)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(plan-detail): 治疗历史的"治疗计划"仅在晚于最近一次实际治疗时显示 · 22d8ccbc
      旧版按 plannedFor 取最新 planned treatment 置顶,不管它是否已被后续治疗超越。
      韩滨:拆线47(计划 2026-02-06)早于最新实际治疗(2026-03-16)→ 实为过期/已执行
      的旧计划,却仍标"治疗计划"。改为:仅当计划日期 > 最近一次实际治疗日才显示。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(plan-detail): 召回反馈剥离到 plan + 标题栏拇指弹窗;画像缺口口径统一;病历诊断原文;事实轴牙位筛选 · d7a251a3
      1) 召回反馈剥离(执行 → plan 自身)
         - schema: FollowupPlan 加 recall_feedback(up/down)+ recall_feedback_note(2 列);
           PlanExecution 删 recall_feedback。migration 20260603090000。
         - 端点 POST /plans/:id/recall-feedback { feedback, note? } → 写 plan;/full 回显。
         - 详情页标题栏(优先级右边)拇指:👍 直接记;👎 弹 shadcn Dialog,
           预选项=快速输入到文本框(沿用 RECALL_FEEDBACK_OPTIONS)+ 自由补充。
         - 执行表单移除原召回反馈多选块;执行链路 schema/service/types 去掉 recallFeedback。
      
      2) 画像治疗链缺口数口径统一(韩滨 1→2)
         - treatment_chain_status 旧版类别级判定漏算被无关同类治疗掩盖的缺口;
           改为复用 ChainComposerService,数 discovered 且非替代闭环的链 = 缺口,
           与治疗链面板 / 召回理由逐条一致。PersonaModule 注入 ChainComposerService。
      
      3) 病历快读 评估段 加医生诊断原文,且与"本次治疗"同结构:[标签] 原文(深色) · 牙位
         (数据已在 diagnosis_record.content.name_zh,纯展示)。
      
      4) 患者事实时间轴 加单牙位筛选(下拉,牙位归一去重),与类型筛选叠加。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(ingest): treat_plan 的复查/拆线/暂观归为 actual(本次治疗),不再误落治疗计划 · 99b39c08
      EMR.treat_plan = 本次治疗(actual,本次确实做了);EMR.plan = 治疗计划(planned)。
      treatment_review_rows 100% 来自 treat_plan(plan 字段的复查项当前 drop),
      但 treatment_review.yaml 原写死 action=treatment_planned → parser 推 kind=planned,
      导致"拆线/复查/暂观"等本次做的事被错误落到「治疗计划」。
      
      修复:treatment_review.yaml action → treatment_completed(kind=actual / status=fulfilled)。
      - 韩滨 2024-04-07 拆线 18;48 现正确落「本次治疗」,与源数据 treat_plan 一致
      - 验证无副作用:chain S4 复查信号按 category='review' 匹配(不看 kind);
        review 已排除在 actual 治疗链 S3 外、不触发 scenario ⑤a → 召回逻辑不受影响
      - 留注释:未来若 union EMR.plan 的复查项进来(那才是 planned),需按来源分流 kind
      
      至此 treatment_record 的 kind 完全由来源字段决定:treat_plan→actual / plan→planned。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(plan-detail): 召回理由替代闭环抑制按 (诊断码+牙位) 对齐,修复同牙不同病程误杀 · 354eaecd
      WhyCard 的替代闭环抑制原先只按牙位收集 altCoveredTeeth,把某条
      alternativeClosedBy 链覆盖的牙位上【所有】召回理由都 drop。同一颗牙若
      存在不同病程会互相误杀:
      
        韩滨 11 号牙:2024 龋齿(K02)→修复 已闭环(altClosedBy),
        误删了 2025 缺牙(K08)→种植 的召回理由(分 85,本应是主理由),
        WhyCard 退而显示 perio(分 47)→ 与治疗链「种植修复·11 潜在新链」口径对不上。
      
      修复:按 (诊断码 + 牙位 overlap) 把 reason 对齐到【它自己】那条
      altClosedBy 链才抑制(reasonAltClosed)。K02 闭环不再误杀 K08 召回;
      而 K04 根管被同牙后续替代关闭(同一条 K04 链的 altClosedBy)仍正确抑制。
      
      - mock-data / plan-detail-types: Chain 加 code 字段;adapt-data 透传 code
      - visibleReasons 上提到父组件,WhyCard 召回理由卡 + 诊断/目标治疗标签 +
        TopBar 共用同一份,口径统一(避免卡片显示 A、标签显示 B)
      - 真实数据验证:韩滨主理由 => missing_tooth@11(缺失牙·11),与治疗链一致
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(emr): plan 的"建议"接入 recommendation + 病历显示 + 牙位全展示 · e199bcdf
      医生建议(plan 字段长尾"建议X")原先 MVP 直接 drop,现忠实保留并在病历快读显示:
      - transforms 新增 union 算子(表级合并)— 补上"transforms 无 union"的 TODO
      - manifest:plan 的"建议"→ _plan_recommendation_raw → union 并入 treat_plan 的 _recommendation_raw
      - recommendation.yaml:加 name(医生原文,无 enum)+ sourceEncounterExternalId(按接诊匹配)
      - fact-content-schemas:recommendation.code 改可空 + 加 name / source_encounter_external_id
      - recommendation.parser:未命中白名单不再丢(code=null 但保留原文 name);空 code 不触发召回
      - emr-soap-view:P 段加「医生建议」(忠实原文,如"建议更换 · 11;12;…")
      
      病历牙位:SOAP 全部牙位完整展示(formatToothPosition maxShow=999),不再"等 N 颗"。
      
      注:存量患者需重摄入才有历史"建议";新增量自动带。无 DB migration(content 为 JSON)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs: W4 进展报告 · fb388a17
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(recall): ⑤f 就诊冷静 — 近 N 天到过诊的患者本轮不召(防打扰) · ae2f1fe5
      患者级抑制门(与具体诊断信号无关):最近一次到诊(encounter/emr)在
      POST_VISIT_COOLDOWN_DAYS(默认 14)天内 → 本轮不入召回池,别催刚来过的人。
      
      - 锚"最近到诊",跟诊断 cooldown(锚诊断日、按 K 码定长)不同,二者并存各补各的洞:
        cooldown 给新诊断缓冲;本门拦"旧诊断 + 近期又来过"。
      - 配合已有的"0 命中关闭遗留 plan":患者一到诊,下次重算自动从召回列表撤下。
      - 不含 appointment(预约不一定到诊;未来预约已由 ⑤b 排除)。
      - 现内联在 initiation scenario;将来上复购 scenario 时抽成共享 fragment 复用。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
  4. 02 Jun, 2026 17 commits
    • feat(plan-detail ui): 参考话术 + 关键事实 + 治疗历史 + 病历 + 登录人名 · 7dc4ed0f
      参考话术:
      - 去默认 demo 话术,未生成显示空态;原文模式默认全展开;诊断·目标治疗标签
      - 4 段 id/标题对齐(opening/informMissed/reviewAdvice/closing),去时长显示
      
      侧栏:
      - 患者画像移到「为什么召回」下;关键事实默认展开,改 4 行(主治医生/专属客服/累计消费/保险客户-显示保险名)
      - 为什么召回:只显示一条 + "+N" hover card 看其余(对齐列表页)
      - 新增「治疗历史」卡(治疗计划置顶 + actual 倒序,标在右,4 行高滚动)+「历史联系」占位;隐藏召回建议
      
      头部 + 病历:
      - 去"普通患者"占位;病历号显示在名字后(括号)
      - 头部登录用户显示人名(dictionary)+ 中文角色(userDisplayName/roleNameZh)
      - 模拟登录按钮显示岗位·人名
      - 病历快读 SOAP:P 段加「本次治疗」(actual 治疗,同次接诊)
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(ai-script + aggregate): 话术按业务规格对齐 + 去提示词污染 + 详情页数据透出 · 7115d68c
      话术(draft-plan-script):
      - 删 AI_SCRIPT_USE_SKILLS 开关与 legacy system prompt(只剩 skills 路径)
      - 术语 漏诊→应治未治 全量对齐;base/成人/儿童 skill 逐字还原业务原模板
      - 清除 system + user prompt 污染(去 PAC 内部说明/解释/溯源/内部元数据/金额/FDI/方案词)
      - 占位符统一:{xxx}=填值替换、【xxx】=原样保留(时间段/具体预约时间/缺失牙位)
      - 停用 14 个旧 skill(诊断 K00-K09/场景/关系/异议库/safety-rules)— 不在业务规格内且与新结构冲突
      - composer:age 未知默认成人;skill 拼接去内部 name/version
      - 自报家门用登录客服 岗位+姓名(input.agent;controller resolveAgent;mockLogin 配演示人名)
      
      聚合(plan-aggregate):
      - parseScriptMarkdownToSections 标题/ID 对齐新 4 段(修"刷新后话术空白")
      - 透出 病历号 medicalRecordNumber、专属客服 dedicatedCs、persona feature.data(保险等)
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(ai-script ui): 前端 4 段对齐 + 去时长显示 + 告知漏诊项目→告知应治未治 · a18083f6
      页面加载中仍显示旧 4 段标题:根因 use-script-stream 硬编码旧 id/label
      (opening/followup/objection/close)做加载骨架。修:
      - use-script-stream:ServerSection.id + LABEL_FALLBACK + makeEmptySections 改新 4 段
        (开场白/告知应治未治/复查建议/结束回访语)
      - 去掉建议时长显示(script-viewer 3 处 durationHint span;无意义)
      - 告知漏诊项目 → 告知应治未治(orchestrator label + markdown header + mock + 前端 fallback)
      
      web TS 0 + service TS 0;本地 pac-service 已重启载新标题。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • chore(ai-script): bump promptVersion → 2026-06-02-4module-v3(4模块重构生效 + 清缓存) · a60137dc
      改了 schema/prompt/skills 但没 bump version → PromptCacheService 按 promptVersion+input
      缓存,同患者"重新生成"返回旧结果(老 5 段)。bump 后缓存失效,新 4 模块生效。
      (本地 dev 服务 .md 启动只读一次,改 skill 需重启进程才重载——已重启)
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • refactor(ai-script): 年龄群先不细分 — 回归原提示词 2 分支,撤掉额外年龄微调 · 0ac2613f
      按业务"年龄群先不细分、保持原汁原味":
      - 撤 adult SKILL.md 里我加的"年龄微调(青少年/老年)"段(原提示词没有)
      - 撤 prompt 里"年龄适应性(青年/中年/老年)"注入 + resolveAgeGroup 用法
      - 仅保留 儿童≤12 / 成人≥13 两分支(原提示词口径)
      - script-facts 的 ageFit 数据 + resolveAgeGroup 函数保留休眠(原配置自带,未来要细分再启用,现不注入)
      
      typecheck 0 + build + 25 测试通过。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • refactor(ai-script): 删 teen/elder skill(归 2 分支)+ 页面 demo 同步 4 段 · c479d07e
      - 删 population/teen(14-17)/elder(65+):你的提示词是 2 分支(儿童≤12/成人≥13),
        二者都落在"成人",且正文还是旧 5 段 → 删之归并;老年/青少年沟通微调并入 adult skill,
        年龄适应性细节已由 script-facts ageFit(青年/中年/老年)注入,不丢东西
      - 页面:script-viewer 本就是通用渲染(sections.map by label/markdown)→ 自动适配新 4 段;
        mock-data demo 段同步成 4 模块(开场白/告知漏诊项目/复查建议/结束回访语)短句版
      - (诊断 skill 里几处 population-elder/teen 是注释性文字提及,无害,低优先后清)
      
      web TS 0 + service TS 0 + build 通过。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(ai-script): 单一聚焦的上下文也项目相关 — 日期/主诉用"那次诊断"非泛泛最近 · d2a02f0f
      聚焦一个漏诊项时,喂 LLM 的上下文应针对该项目,而非泛泛"最近一次就诊":
      - orchestrator resolveReasonTrigger 扩展:除触发医生/日期,再取该诊断**那次接诊的主诉**
        (evidence.factIds[0]→诊断 fact→source_encounter→该 encounter 的 emr.illness_desc)
      - reason 加 triggerChiefComplaint;prompt 注入:
        · 智能日期 = top.triggerDate(该漏诊项诊断日期)优先,非最近就诊
        · "那次就诊主诉" = top.triggerChiefComplaint(那次为什么来),非最近一次主诉
        · 医生 = top.triggerDoctor 优先
      - 临床上下文"最近一次接触"降级为仅参考;fallback 同步用项目相关日期/医生
      
      避免"缺牙2025-12诊断却说自从2026-03洁牙后"的错位。typecheck 0 + build + 25 测试通过。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(ai-script): 单一聚焦 — user prompt 只喂主漏诊项,其他不展示 · d09f4339
      对齐业务提示词"只处理最高优先级漏诊项,其他完全忽略":
      - 之前 user prompt 仍把全部 reason + 全部待做治疗塞进去 → LLM 看得到就可能提其他项
      - 改:只喂 priorityScore 最高那条 reason 的触发详情;其他 reason **不展示**(渐进式披露,
        LLM 看不到自然不会提)+ 显式标注"另有 N 项,本次一律不提,下次召回单独处理"
      - 删 pendingTreatments 多项列表(主漏诊项已覆盖);ongoingChains 保留(语义不同:已在管别再约)
      
      typecheck 0 + build 通过。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(ai-script): 上次主诉接入 + 成人/儿童 SKILL.md 按业务模板重写 + 年龄对齐 · d3d093c4
      - 上次主诉:emr_record.illness_desc 原文接入(自由文本直喂 LLM,无需结构化)
        → input.clinicalContext.lastChiefComplaint + orchestrator 取最近 emr + prompt 注入
      - population/adult SKILL.md:按业务"成人漏诊话术模板"原样重写(4 模块 + 各小节句式/示例)
      - population/child SKILL.md:按业务"儿童话术模板"原样重写(对象=家长,间隙保持器路径,≤18禁拍片)
      - 年龄分档对齐业务口径:child ageMax 13→12,adult ageMin 18→13(≥13/未知=成人模板),
        跟 script-facts.resolveAgeBranch(≤12童/≥13成人)一致
      
      typecheck 0 + build 通过 + registry 正常加载。
      待续:teen/elder SKILL.md 旧结构增量微调(低优先,base-system 已主导)、页面 4 段、live 验证。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(ai-script): 注入程序算好的事实 + 补字段(病历号)+ base-system 4模块铁律 · f9a457d8
      补字段(PAC 有的补上,没有的优雅降级):
      - prompt.ts:user prompt 加「程序已算好的事实」块 — 称呼/智能日期/主漏诊项(转换层)/
        接诊医生/复查时长/风险要点/治疗优势/年龄适应性,全 TS 算好直接给,LLM 不再判断
      - 病历号(patient.medicalRecordNumber,PAC patients 表有)补进 input + orchestrator
      - PAC 没有的:客服岗位角色/姓名 → 开场用"我是{诊所}的客服"兜底(不编头衔);
        上次主诉(无结构化)→ 省略;联系人姓名(无)→ 儿童称呼用 患者姓+家长
      - base-system.md 重写:旧 5 段 → 4 模块铁律(医疗关怀非销售 / 只讲单漏诊项 /
        用"程序算好的事实" / 时间用【占位】/ ≤18禁拍片 / 禁费用·方案·推销 / 短句互动 / 主动约)
      - safety/任务footer 同步 4 段
      
      typecheck 0 + build 通过;script-facts 单测 25 通过。
      待续:population 成人/儿童 SKILL.md 正文重写(+年龄分档对齐 ≤12/≥13)、页面4段、live 验证。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(ai-script): 漏诊项转换层 — PAC 应治未治 reason → 漏诊项口径 · a1cd620d
      确认"漏诊项" = PAC treatment_initiation_recall 的 reason(K00-K09 应治未治):
      - SUBKEY_TO_MISSED:10 个子场景 subKey(missing_tooth/perio_no_srp/ortho_no_consult…)
        → 漏诊项配置 key + 患者口径 label(missedFromReason,带 @tooth 后缀处理 + 文本兜底)
      - 补齐 PAC 子场景对应的 key-points(牙周炎/错颌畸形/牙体损伤/牙龈问题/恒牙萌出空间不足/
        儿牙早矫)+ 复查时长,确保每个子场景都查得到
      - 主漏诊项 = priorityScore 最高的 reason(用 PAC 6 因子排序,不另用业务硬优先级)
      - fallback 改用 missedFromReason(plan.reasons 最高分那条)
      
      script-facts 单测 25 通过(+5 转换层),typecheck 0。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • refactor(ai-script): 参考话术骨架 — 4模块 + 确定性逻辑提取(渐进式组合) · 1e0c5151
      吸收业务"参考话术"提示词,把本该程序判断的确定性逻辑从 LLM 提取出来:
      - script-facts.ts(新):年龄分支(≤12童/≥13成人/未知→成人)、智能日期(今年X月X号/
        去年X月/更早XXXX年X月,替代提示词里的 python)、智能称呼(姓+先生/女士·家长·您好)、
        漏诊项优先级挑选 + 归一、MISSED_DIAGNOSIS_KEY_POINTS / TREATMENT_DURATION 字典查表
        → LLM 不再做年龄分支/日期格式/优先级/查表;渐进式只塞命中那一条,不发全表
      - schema/output:5段(opening/followup/objection/close)→ 4模块
        (opening 开场白 / informMissed 告知漏诊项目 / reviewAdvice 复查建议 / closing 结束回访语)
      - orchestrator:section id/title + fullMarkdown 同步 4 模块
      - safetyRules:精简为 no_forbidden_phrases / no_commit_phrasing / no_bold_concrete_time
        (≤18禁拍片改 prompt 约束,SafetyContext 不带 age);fallback 重写为 4 模块占位版
      - 缓存/渐进式:静态铁律→system(前缀缓存),确定性事实→TS 算好塞 user(下一阶段注入)
      
      骨架验证:script-facts 单测 20 通过,service typecheck 0 错 + build 通过。
      下一阶段(待续):base-system/成人+儿童 SKILL.md 正文重写、prompt.ts 注入计算事实、
      orchestrator 补字段(漏诊项/岗位/上次处置/主诉/就诊日期)、页面 4 段、live 验证。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(execution): 通话结果重构为 成功/不成功/保持 三组 · f05fe163
      按业务重新归类通话结果(EXECUTION_OUTCOME_META 单一真理源,前后端同步):
      - 成功(close→completed):「转化新预约」(原"成功转化为新预约");去掉"近期不考虑"
      - 不成功(give_up):明确拒绝、已在外院治疗(终态 abandoned)+ 再考虑(冷静期7天)
        · 再考虑 = 原 declined_recent 改 drivesStatus=keep + suppressDays=7(非终态,7天后自动浮现)
      - 保持(keep):打不通(原"未接通")、秒挂(新 quick_hangup,算保持)、约定下次回访
      - 待跟进/无效 → hiddenInForm(不在表单展示,仅翻译历史)
      - 新建预约按钮常显(不再仅"成功"时出现)
      - group label 改 成功/不成功/保持(仅 outcome-form 消费,不影响列表 tab)
      
      resolveSnoozedUntil(suppressDays!=null → now+N)天然支持 再考虑=keep+7天;测试更新通过。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • refactor(persona): 结构化特征值 evidence.data → 独立 persona_features.data 列 · 9765bb32
      evidence 语义是"为什么(证据溯源)",塞业务 payload 越界。改用独立 data Json? 列:
      - schema: persona_features 加 data Json?(+ migration 20260602054612)
      - PersonaFeatureDraft.data 顶层字段(从 evidence.data 移出);evidence 回归只放 factIds
      - persona.service 写入 + 序列化透出 data 列
      - entitlement extractor 结构化明细写 data 列
      
      本地验证(6857):data 列含 {commercialInsurers/lastCommercialInsuranceAt/...},evidence 只剩 factIds。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(persona): 权益身份特征(商保/医保)+ 专属客服摄入 · d8285684
      吸收 CDP 画像字典 v3.0「权益身份」+「专属客服」,落 PAC 三层:
      
      权益身份(entitlement_status persona feature,事实投影型):
      - 商业保险强时效(雇主团险换工作即失效,DW 无保单有效期)→ 不断言"当前在保",
        产「史 + 最近日期」:everCommercial + insurers[] + lastInsuranceAt + monthsSinceLast,
        时效判断留给读取方(UI 按日期变措辞 / scorer 按 monthsSinceLast 套窗口)
      - 判定 channel='insurance' OR content.insurance_name 非空(拆单支付里保险非主导也能捕获)
      - 保司名 57 脏名在 feature 层 canonicalInsurer 归一(别名折叠 + 排除测试数据)
      - 零 DB 迁移:description 人读串 / score=monthsSinceLast / evidence.data 放结构化明细
      
      fact 层:payment_record 加 insurance_name(payment.yaml 映射 + parser + zod schema)
      专属客服(current_task_director):路由属性非画像,并入 patients.preferences.dedicatedCs
        (mergePatientPreferences 共享 helper,cold-import + dispatcher 两处 upsert,零迁移)
      
      本地端到端验证(患者 6857):payment.insurance_name 写入 / entitlement="商保客户·平安
      健康险·最近2026-03(2个月前)"(PINGAN+PingHealth 归一为一家)/ dedicatedCs={832,康慧捧}
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(recall): 引擎两缺口 — 0命中关闭遗留plan + wholeMouth全口病牙位忽略 · d005735f
      修"正畸/牙周误召"(40393fbe)后暴露的两个独立引擎缺口。
      
      缺口1(主):0 命中时不关闭遗留 active plan
        recomputeForPatient 在 hits.length===0 时直接 return,runAllForHost
        只遍历 hitsByPatient(有命中的患者)→ 患者治完疗变 0 命中后,旧召回
        plan 永远残留召回池(status 一直 active,该结案却不结)。
        修复:
          - recomputeForPatient 0 命中 → closeStaleActivePlan supersede 关闭
            该患者 active(非 assigned)plan。
          - runAllForHost upsert 循环后,扫该 (host,tenant) 全部 status='active'
            plan,凡不在本轮 hitsByPatient 的 supersede 关闭。
          - 纪律:按 tenant 限定;assigned 不动(客服跟进中);终态不动;
            不建后继版本(无信号是终结,非版本演进);循环后扫避免误关本轮新 active。
          - 新增 plansClosed 计数(EngineRunResult + recomputeForPatient 返回 + CLI 日志)。
      
      缺口2(次):scenario 排除/聚类不消费 wholeMouth flag
        K05 标了 wholeMouth(牙周全口病)但 ⑤a 排除 SQL / tooth-overlap 聚类
        都没消费 → K05 诊断带具体牙位(如@38)、牙周治疗在别的牙(@36)时被当
        单牙不重叠 → 误召 perio_no_srp@38。
        修复:wholeMouth 码把"信号牙位"统一替换成 NULL(sigToothExpr)→
          - ⑤a 第一分支"信号无牙位"恒真 → category 级排除
          - ⑤c/⑤e"信号有牙位"守卫恒假 → 单颗拔除/冠桥不误终结全口病
          - 主 SELECT tooth=null → 聚类归 'whole' cluster(sub_key=...@whole)
        K07 经临床确认正畸=全牙弓矫治,同加 wholeMouth(K07 +
        ORTHO_CONSULT_RECOMMENDED),消除残留 3 例 ortho 误召。
      
      测试:plan-engine-stale-close.spec.ts 覆盖
        - 有命中→0命中→plan 被关闭(recomputeForPatient 4 例 + runAllForHost 3 例)
        - wholeMouth flag 标注防漂移(K05/K07/SRP/ORTHO 标,K02/K04/K08 不标)
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
  5. 01 Jun, 2026 5 commits
    • fix(recall): 正畸/牙周误召 — 治疗分类 + 时间方向两处修复 · 40393fbe
      根因(陈施羽 998421 复现):
      1. 分类 bug:keyword "二期" 太宽,把"二期隐形矫正"(正畸)误判成 implant 种植
         → treatment_actual.yaml: 二期 → 种植二期;ortho 加"隐形矫"兜底
      2. 时间方向:正畸/牙周全口慢性病,复诊反复重记诊断,"治疗须晚于诊断"失效 → 误召"未启动"
         → canonical-codes K05/K07 加 excludeIfEverTreated;scenario ⑤a 对其忽略时间方向(曾做过即排除)
         按牙位的码(K02/K04/K08 等)规则不变
      
      定向重处理工具(线上只重摄受影响的 ~1493 人,不全量):
      - cold-import: PAC_COHORT_ONLY_PATIENT 支持逗号列表(IN 过滤)
      - recompute-persona / recompute-plans: 加 --pids=<逗号externalId> 定向重算
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat: 列表页加诊所多选筛选 · b756531c
      - ListPlansQuerySchema 加 targetClinicIds(逗号串 preprocess 拆数组)
      - plan.service list 按 targetClinicId IN [...] 过滤(仍受 staff clinic 隔离 OR 取交集)
      - plans-api 数组序列化为逗号串上线
      - 前端 ClinicFilter(Popover + 复选框),选项来自 token dictionary.clinics(用户可见诊所),>1 诊所才显示
      - 选中存进 query.targetClinicIds,切换回第一页 + 清选择
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • revert: 撤掉 Twilio 网页拨号,只留占位拨打按钮 · ed0dba69
      Twilio 不支持拨打中国大陆(CN 不在其 218 个自助可拨国家列表,需工单审批且高资费/美国主叫),
      不适合国内患者召回外呼。撤掉集成,保留"拨打"按钮做占位,点击提示"需企业资质认证 + 接入国内合规外呼线路"。
      生产改接国内线路(阿里云语音/腾讯云/容联等)时,本架构(token+voice回调+拨打组件)可平移。
      
      - 删 telephony 后端模块 + use-twilio-call hook
      - call-widget 简化为占位按钮 + toast 提示
      - 移除 twilio / @twilio/voice-sdk 依赖、configuration twilio 块、wrap-response twilio 跳过、.env TWILIO 变量
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat: Twilio 网页点击拨号(Voice JS SDK) · ad06a8bf
      - 后端 telephony 模块:POST /twilio/token(签 Voice AccessToken,JWT)+ POST /twilio/voice(@Public,Twilio 回调返回 <Dial> TwiML)
      - 患者真号不下发浏览器(前端传 planId,后端查库解析);API Key Secret 只服务端
      - TWILIO_FORCE_TO 联调开关(trial 账号只能打已验证号 → 强制拨它)
      - 前端 use-twilio-call hook(@twilio/voice-sdk 动态 import)+ CallWidget(拨打/呼叫中/计时/静音/挂断/重拨),挂在详情页"通话结果"头部
      - wrap-response 拦截器跳过 /twilio/voice(返回裸 TwiML XML)
      - 配置:twilio 块(accountSid/apiKeySid/apiKeySecret/twimlAppSid/callerId/forceTo)
      
      注:trial 阶段只能拨已验证号 + 美国主叫;生产需 Upgrade + China geo permissions,主叫建议换国内合规线路
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • 召回闭环 + 通话结果重构 + 召回反馈 + 工作台修复 · 524efac7
      召回闭环(snoozedUntil 列):
      - execution 按 outcome 写 snoozedUntil(成功60/拒绝90/外院·无效永久/熔断30/约定回访到回访日)
      - upsertPlan 信号级抑制:终态+冷静期内同 (scenario,subKey) 不复活;患者新长的别的诊断照常召回
      - pool 视图过滤未到期 snooze;去掉 considering 的 likelihood 加权
      - 回收 cron(每10min):超时 assigned 自动回池,跳过约定回访中的(snooze 未到不收)
      
      详情页:
      - 患者级"召回历史"卡(跨版本最近8条)+ "暂缓跟进"角标
      - 终态(已完成/已放弃)提交按钮置灰 + 提示
      
      工作台修复:
      - 结案保留 assigneeUserId(我的已完成/转化率不再恒为0)
      - "全部我的"含已完成;assigned 状态文案改"进行中"
      
      通话结果重构:13→3 大类(结案/保持/放弃),group状态机列表tab 一一对应
      
      召回反馈:plan_executions.recall_feedback,5 tag 选填收集一线对召回准确性的反馈(喂算法迭代)
      
      日期组件:shadcn Date Picker(Calendar+Popover,captionLayout dropdown)+ 原生时间输入
      
      迁移:20260601032022_add_plan_snoozed_until / 20260601072323_add_execution_recall_feedback
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
  6. 31 May, 2026 4 commits
    • fix(web): 病历快读分页点窗口化,病历多时不再横向溢出 · b8fbd7ad
      每个病历一个点 → 病历多(13+)时溢出。改窗口化:最多 9 个点跟随当前居中,
      首尾小点暗示更多,容器 overflow-hidden 兜底;精确位置由"第 N/M 次"文字给出。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • refactor(ai): user prompt 去掉冗余的 skills 名字索引 · 4ae1aa8a
      skills 正文已直接注入 system,user prompt 末尾再列名字纯冗余(还易困惑);删之。
      promptVersion bump 到 skills-base-v2 便于 eval 区分。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(ai): agent_invocations 落库完整 system prompt(审计/复盘) · 2a43e5a7
      prompt_template 只存 user prompt;新增 system_prompt 列存完整 system(base-system +
      注入的 skills 正文),一键审计完整 prompt,免去用 input+promptVersion 重跑 composeSystem。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(realtime-coach): 实时坐席辅助教练(Qwen-Omni-Realtime + Gemini Live 双 provider) · ff6ea96b
      详情页正中底部麦克风钮 → 旁听患者语音 → 字幕式实时输出给客服的单句提示。
      - 后端独立模块 realtime-coach:WS 网关(socket.io,JWT 握手)+ DashScope/Gemini Live
        两套代理(同 RealtimeProvider 接口,按 provider 选);DASHSCOPE/GEMINI key 只服务端
      - 复用话术的患者上下文装配(buildScriptInputForPlan),教练专属指令(抓主诉/循序渐进,
        不复用销售脚本);turn-end(患者停顿)触发,前端 VAD 静音门控
      - 前端独立组件:麦克风浮钮 + 字幕浮层(逐级虚化)+ 真实电平波形 + 背景虚化 + 模型切换
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed