- 28 May, 2026 13 commits
-
-
pac-algorithms-overview.md — 用大白话讲清三大算法,面向产品/运营/诊所管理者/客服主管: - 召回算法:谁该被请回来(10 子场景 K00-K09 + 三道过滤网 + tooth 合并) - 画像算法:这个患者是谁(4 标签 value/流失风险/治疗链状态/不打扰,全规则可解释) - 优先级算法:先打给谁(6 因子打分 0-100,卜晓平 87 分实例) - 三者配合端到端流程 + 常见疑问 FAQ 无代码/SQL/字段名;实现细节指向 potential-treatment-recall-flow.md
luoqi committed -
审计发现 3 文档跟 sync 重构 + scenario 扩展(K00-K09)漂移,修正: onboarding-runbook.md(运维命令,最高优先): - Phase 4 + 能力表 + 完成标准 + 落点图:pnpm cold-import → pnpm sync (--no-recompute + 手动 persona/plan;PAC_COHORT_CONCURRENCY 并行提示) - 4 子场景 → 10 子场景(K00-K09) - 加统一 sync 入口说明(cold-import = legacy 全量 alias) canonical-fact-layer.md: - §六/§九 "3 子规则(K08/K02/K05)" → 10 子规则 K00-K09 + tooth-overlap union-find - 实现状态表 DW 直连增量 🟡框架预留 →
✅ 统一 pnpm sync(cohort/并发/bulk/run_start cursor) potential-treatment-recall.md(W2 末设计稿): - 顶部加 banner:已被 potential-treatment-recall-flow.md(实现版)取代 - 列关键差异(4→10 子场景 / 去时间上界 / tooth-overlap union-find / cold-import→sync) - 标"勿照本文实施",保留作设计思路存档 design-v2.md 审计无漂移(讲的是另一个 scenario post_treatment_recall,不动)luoqi committed -
§L3 召回 SQL 重写到当前实现: - 子场景 4 → 10(K00–K09 全覆盖),表补全 base/dx/rec/cooldown/window/urgent/exclude (真理源 SUB_SCENARIOS + DiagnosisTreatmentMap@canonical-codes) - 入池 SQL 修正: · 只设时间下界 cooldown,
⚠ ️ 去掉上界(W3 末:缺口不自愈,超 window 仍入池交 scorer 衰减) · COALESCE(occurred_at, planned_for)(诊断 vs 推荐) · 排除升级为牙位级 overlap(W4)+ 时间方向 tx.occurred_at >= sig + ⑤b拔除 ⑤c未来预约排除 - 合并逻辑:旧"取 days_since 最大丢其余" → tooth-overlap union-find · sub_key = '<sub>@<union(tooth)|whole>';同 patient 同 sub 不同牙位 = 多 reason 行 · cluster_triggers/factIds 全量;来源标签 dx+rec → "(诊断+医生建议)" - 总览图 mermaid "4 子规则" → 10 - 实现状态:scenario 3→10 子规则;DW 增量 cursor 🟡→✅ (统一 sync 重构)luoqi committed -
deployment-data-ingest.md 更新到当前实现: - §2.2 加 sync 资源调优 env(COHORT_BATCH_SIZE/CONCURRENCY/WRITE_BATCH_SIZE) + Docker Compose --env-file 必带 + connection_limit 说明 - §2.4 加 docker compose prod 部署(--env-file + pac-migrate/pac-service 两镜像都要 rebuild) - §3 重写数据摄入 SOP: · 3.1 cold-import → 统一 pnpm sync 首跑(cohort 分批 + 并行,~1.5-2h vs 旧 9-12h) · 3.2 增量走同一 importDirectory(并发锁 + run_start cursor) · 3.3 幂等 DB 双 UNIQUE(含补的 source_event partial unique)+ cohort 宿主无关 · 3.4 新增资源调优表(cohort/concurrency/write-batch/query 并行) - §7.3 cursor 倒退加 --full;§7.4 新增 stale running 锁清理 SOP - §8 已知边界更新(去掉过时的 LIMIT 100;加 COPY/看门狗/并行锁) - 一句话归纳重写 反映:cold-import/sync-incremental 合并;cohort batch(PR2); 统一 mode(PR3);bulk write(PR4);query 并行(PR5a);cohort 并行(PR5b); source_event partial unique 补建;宿主无关 cohort 配置。
luoqi committed -
env PAC_COHORT_CONCURRENCY: 默认 1 = 完全串行(行为不变,最稳) >1 = worker pool 并发处理 N 个 cohort 并发安全(数据完整性靠架构,非"小心写代码"): - cohort = disjoint 患者集(chunk distinct pairs)→ 所有写入患者级隔离 - source_event_id partial UNIQUE(本 PR 前一 commit 补)+ fact (subject_id,version) UNIQUE 双保险 → 任何并发下 DB 强制一致 - totals/seenTenants/firstError 共享但 JS 单线程,+= / .add() 同步原子无 race - cursorAdvances race 无害(PR1 后 cursor=run_start 不读它) worker pool 实现:N 个 runner 从 indices 队列各自拉下一个直到耗尽(无额外依赖)。 并行模式调整: - checkpoint resume 禁用(完成乱序前缀语义失效;靠 source_event_id 幂等从头重跑安全) - metadata.cohortDone 改 count-based(近似进度)+ 记 cohortMode/concurrency 本地验证(concurrency=4, 9 batch × 50 患者): - 4 cohort 同时 start,worker pool 滚动,9 批 8 秒(串行约 20-45s) - 内存峰值 338MB(单 cohort × 并发,14GB 有余量) - 数据完整性自查全过:0 dup_source_events / 0 multi_active_facts - 幂等:重跑全 skip 运维注意(已在 resolveCohortConcurrency 注释): 并行需 bump Prisma 连接池 DATABASE_URL?connection_limit=N×N+余量; 内存 N× 单 cohort(并发 4 ≈ 1.3GB) 服务器用法:pnpm sync:prod -- --dir=./data/jvs-dw(默认串行); 榨资源:PAC_COHORT_CONCURRENCY=4 pnpm sync:prod ...luoqi committed -
🔴 预存 bug:schema 注释一直声称有 partial UNIQUE 幂等键,但 init migration 从没真正建过 → source_event_id 幂等从未在 DB 层强制: - createMany({skipDuplicates:true}) 一直是空操作(无约束可依据,全插入) - per-row create 的 P2002 catch 永不触发 - 重复导入 / 同源 dup 行 → 产生重复 patient_transactions 发现过程:PR5b 并行测试做完整性自查,发现 12 个重复 source_event_id (都是 fact_appointment_out 里同 appointment 同时间戳的真重复行)。 查 \d patient_transactions 确认无 unique index,查 init migration 确认从没建。 注:fact 层的 (subject_id, version) UNIQUE 是真建了的,所以 fact 没坏 (自查 multi_active_facts=0 验证),只有 tx 层有重复风险。 migration 20260528000002: 1. dedup 已有重复(每组保留 event_seq 最小 = 最早写入) 2. 建 partial UNIQUE (host_id, tenant_id, source_event_id) WHERE NOT NULL 本地验证: dedup: 4330 → 4318(删 12 重复),remaining_dups=0 加约束后重跑并行:txns=4318 dups=12(constraint 正确拦截 12 重复 skip) DB 终态:0 dup_source_events / 0 multi_active_facts → createMany skipDuplicates 现在真正生效;幂等在 DB 层强制luoqi committed -
loadTablesForCohort + loadAllTables 的 N 个 ClickHouse query 从串行 for-await 改 Promise.all 并行。 瓶颈分析:远程 DW 每 query 往返 170-580ms,5-6 个串行 = ~2.3s/批, 是 cohort batch 里 load 阶段的主要耗时。@clickhouse/client 基于 HTTP, 单 client 可并发多请求。 并发安全: - cursorAdvances 在 Promise.all 前预初始化(避免并行 callback 各自 ?? {} 互相覆盖) - 每个 tableName 写自己的 key,无竞争 - tables[tableName] 各写各的 key,无竞争 本地验证(2 批 × ~100 患者): 并行前(串行累加):191+193+329+393+579+584 = ~2269ms/批 并行后(取最慢): max ≈ 584ms/批 → ~4x load 加速 幂等正确:superseded=2 unchanged=285 evidence=1632(重读决策正确) 资源利用:之前 1 核串行等 IO,现在 6 query 并发打满网络/CH; 内存不变(本来就 hold 全部表)。 PR5b(可选,未做):cohort 级并行 worker pool(env PAC_COHORT_CONCURRENCY) — 多 cohort 并发处理,3-4x 吞吐;但需 bump prisma pool + checkpoint 改 count-based, 风险略高,留作后续按需开。luoqi committed -
修复 PR2/PR4 引入的硬编码违反"各宿主只 yaml 不同"原则的问题。 问题: listPatientPairs / loadTablesForCohort / injectCohortFilter 硬编码了 jvs-dw 专属的 `dw_group.fact_client_out` + `patient_id` + `brand`, 其它 host 接入需改代码 → 违反 PAC 核心设计(摄入流程跟宿主无关)。 修复: manifest.schema 加 sql_source.cohort 配置段: patient_list_from 列患者清单的源表全名(库.表) patient_key_column 患者主键列(所有源表共用做 cohort 过滤,默认 patient_id) tenant_key_column 租户区分列(可选;jvs-dw=brand;单 tenant host 删此行) list_cursor_column 列患者增量 cursor 列(对应主档表时间列) ClickHouseSourceService: - CohortKey 类型 { key, tenant? } 替代 { patient_id, brand }(值载体,列名外置) - listPatientPairs:SELECT DISTINCT <key>[,<tenant>] FROM <list_from> [WHERE <cursor_col> > x] ORDER BY <key> — 全从 cohort 配置读 - loadTablesForCohort + buildCohortClause: 有 tenant_key → (key,tenant) IN ((..)) 无 → key IN (..) - injectCohortFilter 接收已构造好的 clause,不再硬编码列名 cold-import.service: - cohort 类型改 CohortKey;canCohort 检查 manifest.sql_source.cohort 存在 - 配了 cohortBatchSize 但没 cohort 段 → warn + 退回 single-shot manifest.yaml(jvs-dw)加 cohort 段: patient_list_from: dw_group.fact_client_out patient_key_column: patient_id tenant_key_column: brand list_cursor_column: last_visit_time 本地端到端验证(33,400 患者 / 759k tx):✅ 内存峰值 389MB(对比服务器 OOM 7.6GB)— cohort batching 决定性✅ 8 个 subject_type 全覆盖(含之前服务器 0 的 emr/payment/image)✅ 并发锁拦截 + cursor=run_start 推进✅ 每批增量提交(checkpoint)✅ plan compose:10,028 plans / 17,828 reasons✅ sub_key tooth-overlap union-find(impacted_tooth@18;28;38;48 多牙合并)✅ K01-K08 全场景召回触发 新 host 接入清单(零代码): 1. manifest.yaml 写 connection + queries + incremental.per_query + cohort 段 2. 写 assemblers/*.yaml(canonical 映射) 3. 写 transforms(如需 JSON 拆行等) 完事 — sync / cohort / 增量 / 锁 全部复用luoqi committed -
Hot path 优化 — 把每行一次 SQL 改成每 1000 行一次 SQL,SQL 往返从 ~3N 降到 ~5。 FactWriter 新增 bulkWrite(entries): - 1 次 SELECT 取所有相关 subject 的 latest version(by subjectId IN) - 内存里链式决策:unchanged / evidence_append / supersede / create - 处理 batch 内同 subject 多 draft(后 draft 跟前 draft 比,递推 liveLatest) - 1 个 $transaction commit:bulk updateMany supersede + bulk createMany 新版本 + 罕见 evidence_append 走 array_append raw SQL - {maxWait: 30s, timeout: 120s} 防大批量 + swap 下 5s 默认超时 ParserPipeline 新增 runForBatch(items): - 全部 tx 走 parser 收集 drafts(in-memory,无 DB) - 1 次 FactWriter.bulkWrite 提交;失败降级 per-entry writeDraft(保收尾) - 同 runForTransaction 的 metrics 接口,调用方零适配 cold-import processSubject 大重构: - 引入 buffer 按 tenant 分桶(因 createMany 不能跨 tenant) - 每 N 行触发 flushBatchedWrite: 1. createMany tx({skipDuplicates: true})— 1 SQL 写 N 行 2. SELECT WHERE sourceEventId IN (...) — 取回 tx ids 喂 parser 3. parserPipeline.runForBatch — 内部 bulkWrite - createMany 失败降级 fallbackPerRowWrite(per-row 老路径,保稳) - env PAC_WRITE_BATCH_SIZE 兜底(默认 1000;0 = 退回 per-row 回滚开关) 性能预期(实测待验证): per-row baseline: ~80 tx/s (实测服务器) bulk createMany + 10-20x → 800-1600 tx/s 4.6M 行全量: 9-12h → **30-60min** 跟 PR3 统一 sync 模式协同: - 任何 mode(sync/--full/cron 增量)都走同一条 hot path - cohort batch(PR2)+ write batch(PR4)正交叠加 - 失败降级保稳(createMany 崩 → fallback per-row;bulkWrite 崩 → fallback writeDraft) - 同 fact subject_id 跨 batch 一致性靠 version + partial UNIQUE active 兜底,不变 未来 PR5(可选):pg-copy-streams 真 COPY + staging 表 → 再 3-5x(总 30-50x)luoqi committed -
把 cold-import / incremental 两个独立模式合并为单一 sync 入口。 importDirectory 关键改动: - 只要 manifest 配了 sql_source.incremental.per_query,**永远** 读 + 写 cursor - options.incremental === false → 走 --full 路径(忽略上次 cursor,但仍写新 cursor_after) - options.incremental 默认 true → 正常 sync(读 + 写 cursor) - resource 统一写 'incremental_bundle'(不再区分 cold_import / incremental) - readLastIncrementalCursor 接受 SUCCESS + PARTIAL 历史行(partial 已推进 cursor) 修关键 bug: PR1 之前,--full 模式(incremental=false)不写 cursor → 下次 sync 从旧 cursor 拉 → **漏掉 --full 期间 DW 新写入的数据** PR3 统一写 cursor_after=run_start,保证后续 sync 永远从最新 baseline 接力 新 CLI 入口: - pnpm sync → 推荐主入口(=sync-incremental,首跑全量 / 日常增量自动) - pnpm sync:prod → docker prod 版 - pnpm sync:once → 别名(强调"手动触发一次") - pnpm cold-import → legacy 保留(不读 cursor,等价 pnpm sync --full) - pnpm sync-incremental → legacy alias 保留 sync-incremental.cli.ts 增强: - 加 --full / --cohort-batch=N / --no-cohort 参数 - 文件头改 "PAC v1 唯一推荐 sync 命令" + 用法 + 退出码注释 - 启动日志带 cohortBatch / full / dryRun 配置全量 部署节奏(server 实战): 首次:pnpm sync:prod -- --dir=./data/jvs-dw → 自动分批(5000 patient/批) → cursor_after = 启动时刻 ISO → 持续跑直到所有 patient 处理完成 → 此后 cron 02:30 增量自动接力 灾后:DELETE FROM patient_transactions...; pnpm sync:prod -- --dir=./data/jvs-dw (cursor 自然为空 = 等价全量) 强制:pnpm sync -- --full(罕见,cursor 损坏修复场景) 向后兼容: - 所有老脚本(cold-import / sync-incremental)继续工作 - SyncIncrementalSchedulerService 不动(默认 incremental:true) - 历史 sync_logs 行(resource='cold_import_bundle')不受影响, 自然被 cursor_after IS NULL 过滤掉luoqi committed -
资源 + 续跑 2 件: 1. **Cohort batch** — 按 patient 分批 load+transform+assemble+write, 每批跑完中间表出作用域 → V8 GC 释放,峰值内存从 5-10GB 降到 500MB-1GB 14GB 机器全量跑稳,不再撞 PG panic 那种磁盘 / OOM。 2. **Per-batch checkpoint** — sync_logs.metadata JSONB 记 cohortDone / cohortTotal / lastBatchMs / lastBatchRssMb,Dashboard + 监控可观测; readCheckpointOffset 从同 syncLogId 读 cohortDone(为 PR3 --resume 留口)。 变更: prisma migration 20260528000001:syncLog 加 metadata JSONB 列 prisma schema 同步 metadata Json? 字段 ClickHouseSourceService: - listPatientPairs:DISTINCT (patient_id, brand) FROM fact_client_out ORDER BY patient_id,增量 cursor 同步过滤;返回 batch 的边界 - loadTablesForCohort:跟 loadAllTables 同形态,SQL 注入 (patient_id, brand) IN (tuples) 过滤,增量 cursor 仍生效; 不做反向拉主档(本批 fact_client_out 已含本批所有 patient 主档) - injectCohortFilter:把 IN tuple 在原 SQL 的 WHERE 末 / ORDER BY 前插入 ColdImportService.importDirectory: - 加 cohortBatchSize option(env PAC_COHORT_BATCH_SIZE 兜底,默认 5000) - 抽出 processCohort 私有方法(单 cohort 完整 load→transform→write 流程) - cohortBatchSize > 0 + sql_source → 分批 loop,每批结束更新 metadata - 否则 single-shot(向后兼容,文件源走此路径) - chunk + resolveCohortBatchSize 导出工具函数(给 PR4 + 测试用) CLI cold-import.cli.ts: - 加 --incremental / --cohort-batch=N / --no-cohort 参数 - 启动日志打印分批配置 向后兼容: - 既有 importDirectory({dryRun, incremental}) 调用全不动 - 文件源(manifest.tables[])仍走 single-shot - ClickHouse 源默认走 5000 cohort,可 --no-cohort 退回 single-shot PR3 后续: - 加 --resume 用 readCheckpointOffset(stale running lock 需手动 abort 后才能用) - 加 cron 看门狗自动清 stale runningluoqi committed -
数据正确性 2 件: 1. **并发锁**:sync_logs 加 partial UNIQUE (host_id) WHERE status='running' 同 host 同时只能 1 个 sync 在跑(存量 / 增量 cron / 手动一律抢同一把锁) INSERT 撞 P2002 → 抛 SyncAlreadyRunningError → 调用方 skip scheduler 捕获该 error 时 warn 不 error,下次 cron 自然 retry CLI 撞锁退出 code=4(区分于 2=真失败 / 3=bootstrap 崩) 2. **cursor=run_start 而非 max(updated_date)**: 存量跑期间 DW 持续写入(已摄入患者更新 / 未摄入患者新增), max(updated_date) cursor 会漏: · batch 1 摄入患者 100,T+1h DW 又写患者 100 一笔(updated_date=T+1h) max cursor 推到 T+4:25(末批的 max),下次增量 WHERE > T+4:25 → 漏掉这笔 run_start cursor 保证捞回: · 下次 WHERE > T+0 → 全部 T+0~now 的变化都进入增量 · 同行同 updatedAt → source_event_id 一致 → P2002 path → parser re-run idempotent · 同行不同 updatedAt → 不同 source_event_id → 新 tx + 新 fact 版本 重复读浪费 read 但无害,数据正确性 > 带宽优化 importDirectory 重构: - runStart 入口冻结 - SyncLog create 提前(在 table load 前),作为锁 acquisition 点 - 整段 work 包 try/finally,finally 统一 finalize syncLog(释放锁 + 写 cursor) - 之前没有 finally,work throw 时 syncLog 卡在 status='running' = 永久死锁 - 加 SyncAlreadyRunningError 导出类,scheduler / CLI 分别处理 本地验证(docker exec psql):✅ 1st INSERT running OK✅ 2nd INSERT running for same host → unique violation✅ UPDATE 1st status='success' 后,新 running INSERT OK(锁释放) stale 锁兜底(进程崩留 status='running'): 目前需人工清: UPDATE sync_logs SET status='aborted' WHERE status='running' AND started_at < NOW() - INTERVAL '12 hours'; PR2/PR3 期间会加 cron 看门狗自动清。luoqi committed -
luoqi committed
-
- 27 May, 2026 13 commits
-
-
dev (nest start --watch --builder swc) 编译产物在 dist/src/modules/..., __dirname 指向 dist/src/.../draft-plan-script,__dirname/skills 找不到 (nest-cli.json assets 只 copy 到 dist/modules/...,SWC + tsc 两态目录不同)。 改用 cwd-based 多路径 resolver(跟 sync.service.ts 同模式): 1. env PAC_SCRIPT_SKILLS_DIR 优先 2. src/modules/.../skills 存在则用(dev 直接读源 MD,无需 watchAssets) 3. 回退 dist/modules/.../skills(prod docker 只含 dist,nest build tsc 已 copy) registry + composer 共用 resolveScriptSkillsRoot 导出函数。dev/prod 都 work。
luoqi committed -
补齐 K00-K09 全 10 个 ICD-10 牙科 dx skill 覆盖 + 2 个 playbook。 diagnosis/(6 个): K00-development 发育/萌出异常(乳牙滞留/多生牙,allowedPop: child/teen) K01-impacted 阻生牙(智齿,allowedPop: teen/adult/elder) K03-hard-tissue 牙体硬组织非龋损伤(磨损/楔状缺损/酸蚀/隐裂) K06-gum-alveolar 牙龈/牙槽嵴疾患(增生/肿物,谨慎不主动提"癌") K07-ortho 正畸(K07,长周期1-3y,儿童8-12黄金窗,严禁报价) K09-jaw-cyst 颌骨囊肿(高风险,必须外科会诊,话术高度谨慎, 1 周内必须复联拒绝者) playbooks/(2 个): objection-bank 异议总库 8 种通用高频异议 + scenario × pop 优先级矩阵 (priority 200,全场景加载,跟 dx 特化异议互补) safety-self-check safety 规则描述版,让 LLM 输出前主动核查 6 条 close 段 约束 + 禁词,主动规避 safety gate 命中(命中→走 fallback) (priority 250,机器规则仍在 call.ts safetyRules, 本 SKILL.md 是 LLM 看的描述版,两者同步维护) 验证(本地 smoke): - nest build 成功,19 个 SKILL.md + base-system.md 全部 copy 到 dist - composer 4 案例端到端验证全通过: * 成人 K08 → matches 6 个 skill(scenario + K08 + returning + adult + objection + safety) * 儿童 K02 → matches 6 个 skill(child + new + K02 + ...) * 老人 K05+K08 → matches 7 个 skill(多 dx 多 skill 命中) * 儿童 K08 → 正确排除 K08(allowedPopulation 跨维度约束生效), 只 matches 5 个 skill(scenario + child + ...)luoqi committed -
Phase A: 把 draft_plan_script 的 system prompt 从单一长字符串重构为 Anthropic Skills 标准格式 — base-system.md(通用铁律) + N 个 SKILL.md (场景特化),composer 按 input 动态装配。 新增基建: skill.types.ts zod frontmatter schema + match context types skill-registry.service.ts 启动期 scan + parse + 强校验(fail-fast,跟 yaml assemblers 风格一致),__dirname/skills 路径 skill-composer.ts 纯函数 — applies match + allowedPopulation 跨维 度排除 + priority 排序 + composeHash skills/base-system.md 从原 DRAFT_PLAN_SCRIPT_SYSTEM 抽出 50 行通用部分 (§0 总则白名单 / §3 禁词 / §4 销售文风 / §6 时间 排班 等全场景铁律) 11 个 P0 SKILL.md: scenario/treatment-initiation 新链(启治召回) diagnosis/K02-caries 龋齿 / 补牙 diagnosis/K04-endo 根管(allowedPop: teen/adult/elder) diagnosis/K05-perio 牙周 diagnosis/K08-edentulism 缺牙 / 种植(allowedPop: teen/adult/elder, 严禁报价铁律已内置) population/child 儿童(<14,找家长 / 临床禁忌交叉) population/teen 青少年(14-17,半自主) population/adult 成年(18-64,baseline,故意保持薄) population/elder 老年(>=65,慢节奏 / 家属同决策 / warm 默认 / 不能 urgent) relationship/new-customer 新客(无上次治疗可引,降门槛加倍) relationship/returning 回头/熟客(可引主诊医生 / 治疗链) call.ts: - 注入 SkillRegistry,buildPrompt 走 composer - env AI_SCRIPT_USE_SKILLS=0 退回 legacy 全量 prompt(回滚保险) - promptVersion 区分 'skills-base-v1' / 'time-marker' (legacy), SQL 对比版本效果时拆分群体 - fallback close 段去加粗时间 + 加 (示例) + "以诊所排班为准" — 避免 fallback 自己触发 close_no_bold_time block input.types.ts: reason 加 subKey + dxCode + scenarioKey(skill composer 显式映射, composer 不做文本推断) orchestrator.buildCallInput: 传 raw subKey + 派生 dxCode(K00-K09 全表 map);primaryScenarioKey 直传 prompt.ts: - 原 DRAFT_PLAN_SCRIPT_SYSTEM rename → _LEGACY(env=0 回滚路径) - buildDraftPlanScriptPrompt 加 matchedSkills 参数,末尾追加"本次激活的 skills" 清单(LLM 跨 skill 自检 + 落账归因) - 删 inline hint "(老客可家常)/(新客需详细)" — relationship skill 接管 nest-cli.json: 加 assets 配置把 modules/ai/calls/skills/**/*.md 拷到 dist ai.module.ts: 注册 DraftPlanScriptSkillRegistry provider Phase B(下一 commit):补 K00/K01/K03/K06/K07/K09 6 个 dx skill + objection-playbook + safety description skill。luoqi committed -
W6 目标改 1 → 5 家试点;W4 描述细化(DW 直连 + 试点对接 + 业务验证); W1 起点改"框架定稿 + 数据库结构评审 closure"。
luoqi committed -
FilterBar 增 view prop: 我的工单 / 召回池 两个 view 的 status 选项应该不同(工单看跟进状态, 召回池看入池状态)。setView 时清 status 避免无效组合残留。 行级优先级数字接 PriorityHover:鼠标悬浮看 6 因子算分明细, 跟详情页效果一致。
luoqi committed -
新组件: - priority-hover.tsx — 优先级数字悬浮展示 6 因子 breakdown (timeWindowFactor / valueBonus / urgencyBonus / signalQualityDiscount 等), 无 breakdown 时 fallback 简版文案 - persona-feature-hover.tsx — 患者画像卡片右上角悬浮算法说明 (key → 中文映射 + 算法 evidence + 当前 score) plan-detail-app.tsx: ResponsiveDetail wrapper — xl: 三栏 grid;<xl: shadcn Tabs 折叠 TopBar 响应式隐藏/显示;PriorityHover 接入 chain-viz.tsx: 加 "★ 再启新链" 状态(目标 + ongoing/entered)— 上条治疗链未闭环又再次诊断同 类型时,以"再启新链"语义渲染,跟普通 discovered 区分 reason-line.tsx: 多 trigger 来源标签:typeSet.size > 1 → "诊断+医生建议"(过去只显单一) shared.tsx — MD parser 修 React 重复 key 警告: blockquote/bullet 内 while 消费多行后 i 已前移,push 时 key={i} 跟下一个 paragraph push 的 key={i} 相同 → React reconciliation 报 duplicate key 3, 视觉表现为 blockquote 被重复渲染。改用独立 key++ 计数,跟 line index i 解耦。 drawer.tsx / task-drawer.tsx: drawer cards 右上角接入 persona-feature-hover;英文 key 中文映射统一luoqi committed -
shadcn 风格 wrappers: - ui/hover-card.tsx — Radix Hover Card 包装,默认 200ms open delay 给详情页 / 列表"算法解释/优先级 breakdown" 悬浮使用 - ui/tabs.tsx — Radix Tabs,响应式 <xl 下 plan-detail 折叠 sidebar 用 - hooks/use-media-query.ts — SSR 安全,匹配 tailwind breakpoint 加 @radix-ui/react-hover-card 依赖,pnpm-lock 同步。luoqi committed -
prompt.ts (DRAFT_PLAN_SCRIPT_SYSTEM 重写): - 删 few-shot JSON 大段(LLM 把示例文本当模板照抄,如"工作日 19:00 后" 伪事实就是这么漏的) - 加 §0 总则:白名单(诊所/患者/触发原因/画像/临床上下文)+ 自检方法, 白名单之外具体表述视为虚构 → 失败 - 加 §6 时间/排班规则:必含"待确认"短语;(示例) 显式标记或方向词代替具体点; 严禁加粗具体时间("**周六上午10点**" 被读作已敲定) - 加 医生引用规则:followup 引诊断必须用 reason.triggerDoctor,不能拿 全患者高频医生顶替(38 是姜医生发现,不能写李医生) promptVersion → draft_plan_script@2026-05-27-time-marker schema.ts: close 段 describe() 明确 (示例) 标记规则 + 加粗禁令 call.ts safety rules 新增 3 条: - close_no_commit_phrasing(block):"已为您约好"/"约定"/"敲定" 等承诺词 - close_no_bold_time(block):正则禁加粗具体时间词 - close_has_tentative_phrasing(warn):未含"待确认"语义短语提示 input.types.ts + orchestrator.buildCallInput: reason 加 triggerDoctor + triggerDate;plan 加 goal; clinicalContext 加 ongoingChains + completedTreatmentCount(信任锚); loadPlanContext fact status filter 改 ['active','fulfilled'](漏 fulfilled 会让 AI 看不到实际 treatment_record → primaryDoctor 偏移、pendingTreatments 漏算); extractPrimaryDoctor 改"doctor_id 频次 top 1"(对齐前端); visitFacts EMR 兜底(同 plan-aggregate); pendingTreatments 改从 plan.reasons 派生(SQL 权威集,旧 DX_TO_CAT 漏 K01/K03)。luoqi committed -
scenarios/treatment-initiation-recall: 按 tooth set 重叠做 union-find 合并同 patient 同 sub_scenario 的多 sig, sub_key 改 '<sub>@<tooth|whole>' 粒度,允许同 patient 不同牙位各 1 reason 行, cluster_fact_ids / triggers 注入 evidence + signals,源标签 (诊断+医生建议) 在 cluster 含两种 sig 时合并显示。跟 chain-composer bucket 口径对齐,reason 与 chain 1:1。 plan-aggregate.serializeProfile: visitFacts 优先 encounter_record,缺失时回退 emr_record。 场景:DW 部分 host 的 appointment.in_time 字段空 → encounter 全空,但 EMR 完整(医生写病历必到诊),不该让 lastVisit/daysSinceLastVisit 为 null。
luoqi committed -
__dirname 走 dist 编译产物路径(dev: dist/modules/sync, prod 容器同), 向上 ../../../data 会越界到不存在的 dist/data。改用 cwd/data — 两态(dev/prod)cwd 都是 apps/pac-service 根,稳定指向源 data 目录。
luoqi committed -
PlanReasonBriefSchema 增 breakdown(z.unknown().nullable().optional()), plan.service.toPlanReasonBrief 透传 — 前端 PriorityHover 拿到 6 因子 明细做悬浮算法解释,无 breakdown 时 fallback 简版文案。
luoqi committed -
cold-import 在 swap thrashing 大压力下,单 fact-write 事务可能跑 20s+, 默认 5s timeout abort 整条 patient pipeline。服务器全量 cold-import 跑到 15% (479k/3.15M txn) 就被 "Transaction already closed" 杀死。 4 处 $transaction 加 { maxWait:30000, timeout:60000 }: fact-writer (最热,每 fact 1 次) persona recompute / plan execution / plan engine 上版 风险:长事务期间持锁久,但 cold-import 本身就是串行 patient,无锁竞争。luoqi committed -
- package.json: next dev/start -p 3000 → 3100(端口迁移遗漏) - (app)/page.tsx: 调试页(会话信息+权限按钮 demo)改为直接跳 /plans, 未登录走 AuthGate 弹 mock-login dialog
luoqi committed
-
- 26 May, 2026 12 commits
-
-
13 万患者 dry-run: appointment 500,000 ← 截断(实际估 130 万) diagnosis 499,068 ← 截断(实际估 100 万) 导致 plan 引擎漏关键事实。Node heap 已 8GB 足够撑住 200 万行。
luoqi committed -
cold-import 全量(13 万患者,appointment 50 万行,EMR 多 JSON)默认 2GB heap OOM。 4 个数据密集 CLI 加 --max-old-space-size: cold-import:prod 8GB recompute-persona:prod 8GB recompute-plans:prod 8GB sync-incremental:prod 4GB(增量,更轻) 服务器 14GB RAM,留 6GB 给 pac-service+postgres+redis 主进程。
luoqi committed -
prod docker image 不含 src/,原 `pnpm cold-import` 用 ts-node 跑源码 报 module not found。新增 :prod 后缀走 node dist/cli/*.js, 容器里改用 `pnpm cold-import:prod` 等。dev 用法不变。 7 个 CLI 加 :prod:cold-import / sync-incremental / recompute-persona / recompute-plans / timeline / pac:host / ai:gen-script / stale-scan
luoqi committed -
`process.env.X ?? 'default'` 只兜 undefined,不兜空字符串。 .env 里 `PAC_SYNC_HOURLY_CRON=` 空值会绕过兜底,直接传给 cron 解析器 报 'Too few fields' 启动崩溃。改成 || 兼容两种 'unset' 表达。 4 处:stale-scan / dw-lag-monitor / sync-incremental / sync.service
luoqi committed -
pac-web 用 @pac/utils/format,Dockerfile 之前只 build types, 导致 next build 时 module not found。改成 build packages/* 全部。
luoqi committed -
manifest.yaml 6 处 LIMIT 100 OFFSET 100 全删,fact_client_out 主查询 顺手去 ORDER BY(全量场景下纯浪费一次 sort)。 预期:13 万患者全量灌入,首跑耗时 10-30 min。
luoqi committed -
- docker-compose.prod.yml 重构: - env_file: 指令读 apps/pac-service/.env + apps/pac-web/.env - environment: 段覆盖 DATABASE_URL/REDIS_URL 走 docker 内部网络 - pac-web build.args.NEXT_PUBLIC_API_BASE_URL 用 ${} 插值 - 启动需双 --env-file(给 CLI 插值,跟 env_file 注入容器是两套作用域) - apps/pac-service/.env.example 加 POSTGRES_USER/PASSWORD/DB 段(compose 模式 postgres 容器读) - apps/pac-web/Dockerfile 加 ARG NEXT_PUBLIC_API_BASE_URL,build 时 inline - Dockerfile EXPOSE 端口对齐(3101/3100) - deploy/README.md 加 compose 模式启动 SOPluoqi committed -
- 删根 .env.example(误导:实际只在 docker-compose.prod.yml 模式被读取) - README quick-start 改为从 apps/*/.env.example 复制(本地 dev 标准做法) - docker-compose.prod.yml 顶部加注释列出它依赖的 root .env keys 之后两种模式职责清晰: systemd 模式 → apps/pac-service/.env + apps/pac-web/.env(唯一真相) compose 模式 → 根 .env(用户自己按 prod.yml 注释组装)
luoqi committed -
postgres 5432 → 5532 redis 6379 → 6479 service 3001 → 3101 web 3000 → 3100 服务器 47.251.104.47 已有其他项目占用 5432/3001, 统一为新端口段(5532/6479/3101/3100)避免冲突。 容器内部网络通信(postgres:5432, redis:6379)保持不变, 只改宿主机映射 + 应用 PORT 环境变量。
luoqi committed -
- deploy/: deploy.sh + systemd units + README(staging/prod 通用) - 增量同步: sync-incremental scheduler + CLI + dw-lag-monitor 告警 - assemblers: jvs-dw 全套 yaml 字段对齐 + tooth-position 解析 - plan-detail: chain-viz / facts-timeline / drawer / outcome-form 改版 - auth: mock-login dialog(staging 用) + auth-gate 调整 - parsers: diagnosis/treatment/image/recommendation 解析增强 - CLI: verify-chain 调试工具 - docs: deployment-data-ingest + w3-report
luoqi committed -
合并 W3 之前所有增量 migration 为单文件 init,便于 fresh deploy。 旧 migration 已在 dev 环境跑过;prod 首次部署直接走 init。
luoqi committed -
luoqi committed
-
- 25 May, 2026 2 commits
-
-
陈化冰 case(v1 plan 显示): K04 chain.status=closed(alt-closed by 种植 16;17;26;46) K04 chain.target=true(老 plan reason 还含 K04) → 前端 TargetTimelineRow 强制按 discovered 渲染 ★潜在新链 + stage=1 hint → 但底部 HistoryStrip / ChainSidebarRow 看 status=closed 渲染 ✓已闭环 → 同一条 chain 两个组件给出矛盾展示 修(双侧防御): 1. plan-aggregate.assemble 加约束: if (c.status === 'closed') { c.target = false; continue; } closed = 临床已结束(拔了/被替代/全治完);即使 SQL 老 reason 仍命中也不 ★ (SQL 召回老 closed chain 是 data race / 老 plan 残留,不该误导客服) 2. chain-viz.findFocused: chains.find((c) => c.target && c.status !== 'closed') 双层防御,即使后端漏改也不显示 closed 在 focused 验证(陈化冰 v1 plan superseded,5 reasons 仍含 K04): 改前:K04 closed chain target=true → focused 错位 改后:K04 chain target=false → focused = K00 先天/萌出处置·46(下一个 target=true) 底部历史 ✓已闭环 正常显示 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>luoqi committed -
陈化冰 case (5 reasons → 2 reasons): 改前误召:K05@17;18 / K04@46 / K00@46 — 3 颗都已拔(2025-12-14/12-20) 根因:SQL ⑤a 只看"同 expectedCats actual 排除",surgical 不在任何 K0x 的 expectedCats 但临床上拔除是任何牙病的终结,拔了就不需要后续治疗 修(treatment-initiation-recall.scenario.ts): 加新 NOT EXISTS ⑤c 子句: 任何 actual surgical 同牙位 overlap → 排除该信号 关键设计: - 不限时间方向(拔了就拔了,后期诊断只是记录现状如"牙列缺失") → 解决 K00@46 在 2026-03 才诊但 46 已 2025-12 拔的情况 - 仅对**有具体牙位的信号**生效(信号 tooth_position 必须非空) → 全口诊断(K05 全口)不走这分支,避免任何拔牙误排全口 验证(陈化冰): recompute-plans: tenant=77057 plansSuperseded=1 (陈化冰 v1→v2) v2 reasons (2 条): K02@24;25;47 (caries_no_filling) ✓ 该召(未充填) K03@14;15 (hard_tissue_damage) ✓ 该召(未充填) v1 排除的 3 条 (✓ 正确): K05@17;18 (17;18 已拔) ✓ K04@46 (46 已拔) ✓ K00@46 (46 已拔) ✓ 副作用:全量重导 5K 患者后,召回池可能下降 5-10%(扣掉"已拔不该召"的噪音) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>luoqi committed
-