1. 11 Jun, 2026 17 commits
  2. 10 Jun, 2026 14 commits
    • feat(web): 详情页一页式工作台 — 固定左栏选患者列(4列)+ 画像标签筛选 · 21645e8e
      把"我的任务"hover 抽屉升级为详情页固定左栏(列表页将弃用,核心要素并入;KPI 面板按需求不带):
      
      - plans/layout.tsx:布局挂在动态段之上 — 选中 planId 时渲染「左栏 300px + 详情(原三列)」=4列;
        切患者只重渲染右侧,左栏不重挂(筛选/已加载分页/滚动位置全保留);/plans 列表页不受影响。
      - PatientPickerRail:召回池/我的/全部(权限)tab + 搜索(300ms 防抖)+ 排序 + 真实号码开关 +
        画像标签筛选(popover 四维分组多选:价值分群8/生命周期7/紧迫度3/潜在治疗8,选中 chips 可单删);
        紧凑患者卡(名/性别年龄/真角标/优先级分色/场景/掩码号/状态),触底+按钮双加载;选中 teal 高亮。
      - usePatientPicker:use-my-tasks 泛化版 — 全套筛选上服务端,筛选变化自动回第一页。
      - 后端:ListPlansQuery.personaTags("key:value" 逗号串)→ plan.service 匹配患者当前版画像
        (personas.supersededAt IS NULL)的 features:同维多选 OR、跨维 AND;数组型(潜在治疗)
        用 JSON array_contains;非法 key/value 静默丢弃。
      - @pac/types persona-tag-filters.ts:可筛维度字典(code中文,单一真理源,与 extractor 枚举对齐)。
      - 详情页移除 TaskDrawer(被左栏取代)。
      
      验证:API 对 DB 直查口径一致(重要价值 57==57;OR 173 / AND 10 / 数组 contains 40 / 非法=全量);
      web tsc 0 + Next 生产构建过。仅本地,未部署。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • perf(assistant): 听写提速三处 + 底部提示简化(固定文案) · c52200fe
      服务器实测体感"启动慢"对症:
      1. ASR 容器启动即预热(0.5s 静音跑一遍)— onnxruntime 首推建图/分配比稳态慢很多,
         之前重启后用户第一句要付这笔冷启动。
      2. 网关首句快路径:攒够 ~0.6s 音频立刻出第一个 partial(不等节奏),开口更快上屏。
      3. partial 节奏 700→450ms、tick 250→150ms(inFlight 防重叠不变,服务器解码 ~0.3s 扛得住)。
      
      底部提示按用户简化为固定"结果仅供参考"(去掉听写状态行;修正引号原样渲染)。两端 tsc 0。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs: push 契约措辞修订(回执码表精简/示例数值统一)+ W5 周报 · 1a8496a4
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(deploy): pull 后 re-exec 新版脚本 — 本轮才会用上 pull 进来的脚本变更(SERVICES 等) · ab3d047a
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(assistant): 语音输入升级实时听写 — 点击开启,边说边出字,再点纯退出 · 91f79dc6
      交互(用户要求):点 🎤 开始 → 说话时文字实时滚动进输入框 → 再点 = 纯退出(已上屏文字保留)。
      
      实现(复用 realtime-coach 成熟模式,ASR 容器零改动):
      - 前端:PCM16 16k 采音 + RMS 静音门控(无声不发帧)→ socket.io 推帧;
        dictation:partial(当前句滚动覆盖)/ dictation:final(句定稿累加)→ setInput 实时渲染;
        base 保留输入框已有文字,听写追加其后。
      - 后端 DictationGateway(socket.io,JWT 握手鉴权同 coach):按"帧到达间隙"断句 ——
        说话中每 700ms 把当前句 PCM 包 44 字节 WAV 头调 TranscribeService 出 partial;
        停顿 ≥800ms / 超 30s 整句 final 并清缓冲。inFlight 防解码重叠;先清缓冲再 final
        解码(下一句帧不混入)。SenseVoice 离线模型 RTF~0.1 → 句级重解码远快于实时。
      
      实测(模拟浏览器推帧):开口 1.3s 首个 partial,~0.7s/次滚动更新,停顿 1.3s 出整句 final,
      文本与一次性识别完全一致。两端 tsc 0。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(assistant): 语音输入 — 自部署 SenseVoice-small 听写(PII 不出内网) · 5644d1af
      三层:
      - apps/asr-sensevoice:sherpa-onnx + SenseVoice-small int8(纯 CPU,无 torch,镜像 <400MB);
        FastAPI /transcribe:任意浏览器音频(webm/mp4)→ ffmpeg 16k 单声道 → 离线识别(ITN 标点)。
        模型不进镜像/git —— 卷挂载 ${PAC_MODELS_DIR:-../pac-models}/sensevoice(model.int8.onnx + tokens.txt,
        来源 sherpa-onnx releases sense-voice-zh-en-ja-ko-yue-2024-07-17)。内网专用不发布端口(prod)。
      - 后端:POST /assistant/transcribe(全局 JWT 鉴权,multipart ≤15MB)→ TranscribeService 转发
        PAC_ASR_URL(provider 单一出口,以后切云 ASR 改这一处)。
      - 前端:composer 加 🎤(点击录音/再点结束,红色脉冲态)→ MediaRecorder(webm/opus,Safari mp4)
        → 上传转写 → 文字落输入框可编辑再发;错误/录音中提示在底部状态行。
      
      compose:dev + prod 都加 pac-asr 服务(prod 限 cpus:2/mem:2g,阿里云镜像源构建参数);
      pac-service 注入 PAC_ASR_URL=http://pac-asr:8000;deploy 脚本 SERVICES 加 pac-asr。
      
      本地验证:容器直测 3.9s 音频 0.46s 出字"帮我查一下患者孙科的画像和召回计划。";
      经后端鉴权链路同样通过;无 token 10106 拒。两端 tsc 0。暂不部署服务器(模型待 scp)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(alerting): 企微 webhook 告警 + 增量空转探针 + DW 滞后告警接通 · 4c93c669
      背景:增量游标 ISO 格式 bug 空转三天才被人工发现 — "success+fetched=0" 无人知晓。
      
      - AlertService:识别企微 webhook(qyapi.weixin.qq.com)→ markdown 格式(级别 emoji/颜色,
        context 引用块,body 截断);企微 HTTP 永远 200 → 检查 body.errcode 才算送达。
      - 增量空转探针(ColdImportService 收尾):带游标的增量跑出 0 写入时,反查 DW
        「比游标新的行数」— DW 也 0 = 真没数据(静默);DW>0 = 拉不到但有 → 当天 critical 告警。
        探针(ClickHouseSourceService.probeNewRowCounts)任何异常吞掉,绝不影响同步主流程。
      - DwLagMonitorService:🟡/🔴 滞后从"只打日志"接到 AlertService(企微同收)。
      
      验证:本地用编译产物真发企微群,payload 被接受(无 errcode 报错)。tsc 0。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(sync): 增量游标 ISO 格式与 DW String 列字典序比较不兼容 — 增量三天空转 · 7375f010
      诊断(2026-06-10 服务器):增量 cron 每天 success 但 fetched=0 × 3 天;DW 实际有新数据
      (fact_appointment_out max updated_date=06-09 22:50)。根因:DW 的 cursor 列是
      Nullable(String)('YYYY-MM-DD HH:mm:ss'),WHERE 比较走字典序;游标存的是 run_start ISO
      ('2026-06-09T03:30:00.012Z'),第 11 字符 'T'(0x54) > ' '(0x20) → 所有真实行字典序都
      小于游标 → 永远 0 行。实测同一时刻 ISO 谓词 0 行 / 普通格式 1178 行。
      
      修复:读取侧规范化(toDwCursorLiteral)— 构造 IncrementalConfig 时把 ISO 游标按
      manifest.timezone 转成 'YYYY-MM-DD HH:mm:ss'(sv-SE locale 恰好此格式);
      旧游标无需迁移;已是普通格式/垃圾值原样返回;秒精度(同秒 .SSS 行值字典序更大仍被选中,
      轻微重叠由 source_event_id 幂等吃掉)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(deploy): 逻辑包进 main() — 脚本 git pull 自更新时 bash 边读边执行会错位 · bd442595
      bash 按字节偏移惰性读脚本;本脚本运行中会 pull 更新自己 → 后半段按新文件偏移执行旧/错位代码
      (实测:验证逻辑跑的是旧版)。包进 main() 并在文件末尾调用,bash 先整体解析完再执行,免疫自更新。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(deploy): migrate status 校验改先落变量再 grep — pipefail+grep -q SIGPIPE 误报 · 55444ced
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(deploy): migrate status 校验不再 tail 截断 — prisma 版本提示框位置不固定致误报 · c8c6943a
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(deploy): 确定性生产部署脚本 — 根治 compose "不切新镜像" 缺陷 · 87741f59
      docker compose `up -d --build` 有跨版本反复复发的已知缺陷(docker/compose#9308 #9259):
      镜像构建成功但运行中容器不重建(仍跑旧镜像)。本项目 2026-06-10 部署实测中招:
      service 容器还在旧镜像 → migrate 报 "no pending" → phone_verified 列没建。
      
      根治思路:生产部署不依赖 compose 的 diff 启发式判定。deploy/deploy-prod.sh:
        git pull --ff-only → 显式 build → up -d --force-recreate → 三道硬验证
        (① 容器镜像 ID == 新构建镜像 ID;② migrate exit 0 + migrate status 无 pending;
         ③ service health 200 + web 200),任一不过即非零退出。
      README 把日常更新指到脚本,并警告勿直接 up -d --build。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(web): 详情页手机号加 真/假 角标(假号也明确标出) · cfaca4c4
      - plan-aggregate 聚合 patient 透出 phoneVerified;
      - 详情页手机号旁角标:真(teal,外部对照表已核实)/ 假(灰,宿主同步造数号),
        tooltip 注明含义;类型/adapter/mock 同步。
      
      验证:真号患者(刘倍磊)phoneVerified=true、假号患者(吴小燕)false,两端 tsc 0。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(patient): 真实号码导入(病历号对照)+ phoneVerified 标记 + 列表"真实号码"筛选 · 5671371c
      测试库 phone 全为造数假号;业务提供 1500 名患者的真实手机号(按病历号 file_num 对照)。
      
      - schema: Patient 加 phoneVerified(默认 false)+ migration;true = 外部对照表核实替换的真号。
      - cli/import-real-phones: 读 CSV(file_num,client_phone)→ 按 medical_record_number 匹配 →
        phone 改真号 + phoneVerified=true;支持 --dry-run;号码做基本卫生(去非数字)。
      - 列表: ListPlansQuerySchema 加 phoneVerified(query 串 'true' preprocess 还原布尔);
        plan.service 把 keyword 与 phoneVerified 合并进 patient 子查询;PlanPatientBrief 透出 phoneVerified。
      - web: 筛选条加"真实号码"开关(teal 高亮);行内手机号旁加"真"角标(tooltip 注明已核实)。
      
      本地验证:1500 行对照表匹配 12 名(本地数据不全属预期)→ 更新 12;
      列表 all=274 → phoneVerified=true 筛出 12,行内标记正确。两端 tsc 0。
      
      注:重新全量摄入会被宿主假号覆盖 phone(phoneVerified 不回退)→ 重摄后需重跑导入脚本。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
  3. 09 Jun, 2026 9 commits
    • chore(web): 助手空态示例文案改为 患者信息/应治未治机会/今日工作准备 · be08cd62
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(assistant): artifact 流式空白 — 改持久壳 iframe + postMessage 注入(不重载) · d4a8fbfd
      上版每次流式增量都换 srcDoc → iframe 整段重载 → 反复重新下载 Tailwind/Chart.js CDN,
      还没加载完下一帧又来 → 整个流式期间一直"重载中"=空白(用户实测空白)。
      
      改为持久壳方案:
      - iframe 只加载一次(srcDoc=固定壳:CSP+Tailwind+Chart.js+#root+消息监听),CDN 只下一次;
      - 内容通过 postMessage 注入 #root.innerHTML(Tailwind 的 MutationObserver 自动给新内容上样式)→
        流式内容实时"长出",不重载、不白闪;
      - Artifact 加 streaming 标志:流式中只 set innerHTML(图表脚本不执行,避免半截脚本报错);
        最终 tool_call(streaming=false)时重建 <script> 节点 → Chart.js 执行、图表渲染。
      - 高度由 iframe 内 ResizeObserver 实时回传(initial 120,随内容增长),不再是空大白框。
      - openFull 用自包含整页(buildStandaloneDoc)。
      
      web tsc 0。纯前端改动。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • perf(assistant): artifact 流式渲染 — 卡片边生成边"长出",不再干等整段 · 5737df69
      诊断:render_artifact 慢点全在"模型逐字吐 ~5.5KB HTML"(~32s),而 tool-call 要等整段
      生成完才触发 → 用户对着"生成中"干等 30s+。MCP/工具往返仅 0.2s,非瓶颈;默认模型已是
      deepseek-v4-flash(已是快的)。
      
      优化(感知为主):
      - 后端:消费 AI SDK 的 tool-input-delta,按 toolCallId 累积入参 JSON,增量提取 html 字段
        (extractHtmlField 容忍半截转义),250ms 节流推 artifact_html 事件。
      - 前端:artifact_html 按 callId upsert 到 artifact 块(实时增长),最终 tool_call 覆盖完整 html+title;
        ArtifactView 对 srcDoc 重建做 600ms 节流(避免每增量都重载 iframe 白闪 / Tailwind 反复重扫)。
      - 提示词加"HTML 力求紧凑"以略减 token。
      
      实测:卡片 html 从 ~8s 起流式到达(35→5537 字符,65 次增量),界面从第 ~8s 起"长出"卡片,
      而非 ~35s 整张蹦出。两端 tsc 0。
      
      注:总时长仍受模型出字速度限制(~40s);若要"秒开"常见列表,需走机制 A(预制组件直渲工具结果)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(assistant): 工具报错终止 loading + callId 精确匹配 + 生成中指示 · 6ce494ae
      三个交互问题:
      1. 工具 execute 抛错(如模型传错 patientId)时,AI SDK 发的是 tool-error 分片,
         而 controller 只处理了 tool-result → 前端该步骤永远停在 running(右侧转圈不停)。
         现转发 tool_error → 前端标记 status='error'(显示"出错"),loading 终止。
      2. 事件带 toolCallId,前端 findToolIdx 优先按 callId 匹配(退化到"最后一个 running"),
         并行/多工具调用不再串位。
      3. 加"生成中…"指示(GeneratingHint):流式中且最后一块不是增长的文字时显示——
         覆盖"工具已返回、artifact 整段 HTML 还在生成"的空档,避免界面看起来卡住。
      
      验证:强制假 patientId → tool_call 与 tool_error 同 callId,步骤正确收尾。两端 tsc 0。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(assistant): HTML artifact PoC — 模型按需产卡片/报表,沙箱 iframe 渲染 · 46b60bec
      机制 B(Claude Artifacts 式,即用即焚):
      - 后端加本地工具 render_artifact({title, html}),html 是 <body> 内部片段;
        SYSTEM_PROMPT 加"展示方式"skill:简短问答用文字;召回列表/画像卡/分析报表用 render_artifact,
        Tailwind + PAC teal 配色,图表用 Chart.js,禁外部网络、数据内联、手机号掩码。
      - 前端 use-assistant-chat 加 artifact block(拦截 render_artifact 的 tool_call → html);
        assistant-chat 加 ArtifactView:沙箱 iframe 渲染。
      - 安全:sandbox="allow-scripts"(无 allow-same-origin)→ 脚本在 null origin 碰不到父页
        cookie/凭证;注入 CSP default-src 'none' + 只放行 Tailwind/Chart.js CDN + connect-src 'none'
        堵死外传(患者数据不会被偷渡);postMessage 自适应高度;支持新窗口打开。
      
      运行环境(iframe shell)由前端注入 Tailwind Play CDN + Chart.js,模型不写 <html>/<head>。
      注:沙箱隔离,无法用父页的 Next.js/shadcn 组件;Tailwind+原生JS+Chart.js 已覆盖卡片+图表需求。
      
      验证(deepseek):天气→纯文字不产卡;"卡片展示召回池TOP5"→ list_recall_queue→render_artifact,
      HTML 含 canvas 图表、teal 配色、无 fetch。两端 tsc 0。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(assistant): 放开话题限制 — 通用助手人设,无关问题照常答 · 19c9ddfd
      外部 agent 是通用的(可同时挂多个 MCP),真正服务对象是牙科诊所客服。把 SYSTEM_PROMPT
      从"你只是 PAC 患者召回助手"改为"通用智能助手 + PAC 只是附带的患者工具",并显式要求
      "不要因为问题与患者业务无关就拒绝"。
      
      保留的是数据正确性约束(用工具答患者问题时只依据真实数据、不编造、手机号掩码)—— 那不是
      话题限制。
      
      验证(qwen):冷笑话/算术等无关问题正常答;患者问题仍自主走 list_recall_queue。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • refactor(mcp): 评审小瑕疵清理 + 修 list_recall_queue clinicId 静默失效 · c9743ed0
      评审发现的 4 处小瑕疵:
      1. 工具清单进程级缓存(McpClientService.toolsCache)— 工具静态、与租户无关,省每轮 tools/list 往返。
      2. list_recall_queue 去掉 `as unknown as ListPlansQueryDto` 强转,改类型化字面量;
         过程中发现隐藏 bug:schema 字段是 targetClinicId 而非 clinicId,旧 cast 把 clinicId 静默吞掉
         → 工具的诊所过滤一直没生效。改用 targetClinicId,验证 271→96 条且全为该诊所。
      3. McpClientService.url 注释澄清:同进程 loopback,PORT 与 main.ts 监听端口一致(默认 3001)。
      4. get_facts 加注释说明经 getTimeline 自守(findFirst{host,tenant}+NotFound),与其它工具显式 assert 等价安全;
         顶部注释据实修正两条隔离路径。
      
      tsc 0;端到端验证:6 工具正常 + clinicId 过滤生效。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(web): 助手流式输出"贴底跟随,上滚即停"— 不再和用户滚动竞争 · 8bc9927d
      之前每次 messages 变化都无条件 scrollInto, 流式时把上滚阅读的用户一直拽回底部。
      改为 stick-to-bottom:onScroll 据是否接近底部(<80px)维护 stickRef,仅贴底时才
      scrollTop=scrollHeight 跟随;用户上滚 → 停在阅读位;滚回底部 → 恢复跟随;发起新提问 → 重新贴底。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(ai): qwen fetch 中间件仅"非工具调用"才注入 response_format=json_object · 0e168281
      之前无条件给所有 qwen 请求注入 json_object(给结构化输出框架用),把助手的
      tool-calling(streamText+tools)也污染了 → DashScope 报 400(json mode 与工具调用互斥,
      且硬性要求 messages 含 "json")。改为:仅当请求无 tools 且未自带 response_format 时才注入。
      结构化框架(generateObject,json mode 无 tools)不受影响;助手 agent loop 带 tools 跳过。
      
      验证:助手 model=qwen 问召回池 → 自主 list_recall_queue → 正常列出 TOP 患者,无报错。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed