1. 11 Jun, 2026 12 commits
    • feat(web): 助手窗口跟随钮位弹出 — 上方优先/左右半屏对齐/视窗内夹紧 · de08d506
      点开瞬间按钮当前位置算窗口落点:水平按钮在左/右半屏决定对齐边;垂直优先钮上方,
      放不下弹下方;整体 clamp 视窗内。拖钮避让区域的意义由此成立。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(web): 助手钮/小窗 z-40→z-[60] — 高于左栏容器(z-50),拖到左侧不再被挡 · b46a8211
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(web): 详情 TopBar 加退出登录 + 助手悬浮钮可拖移 · 4a58cd0c
      - 退出:TopBar 最右(头像后)LogOut 钮,与原列表页同口径(clear token → AuthGate 弹回登录)。
      - 助手钮:pointer 拖动,4px 位移阈值区分点击/拖动(拖完不误开窗);
        位置 localStorage 记忆(pac-assistant-fab-pos),clamp 视口内;默认右下角。
      
      web tsc 0 + Next 生产构建过。仅本地,未部署。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(web): /plans 改工作台入口解析器 — 直落详情(列表页弃用,组件保留未挂载) · 0ebda9bc
      根路径/旧链接进来直接到详情工作台,选人规则(纯规则):
        ① 我的「进行中」优先级 TOP1(续上手头工作)
        ② 无 → 召回池 TOP1(开新工作)
        ③ 都无 → 空工作台:左栏照常可筛(自行找人)+ 右侧整体空态(说明 + 刷新)
      解析期间居中过场;PLAN_VIEW_OWN 权限门同列表页。旧 PlansListApp 保留在仓库不再挂载。
      
      web tsc 0 + Next 生产构建过。仅本地,未部署。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(web): artifact 放大窗 portal 到 body — 小窗 transform 祖先把 fixed 圈住致其困在助手窗内 · dc5de128
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(web): artifact 放大改本屏居中大窗 — 流式生成时大窗同步实时"长" · 9dc2093f
      - iframe 渲染抽成 ArtifactFrame(持久壳 + postMessage 注入)可多实例:
        内联卡 + 居中大窗各挂一个,同吃一份流式 html → 放大后内容继续实时增长;
        生成中大窗头部带 ⟳ 提示。
      - 放大键不再开新 tab → 居中大窗(min(980px,94vw) × 86vh,遮罩/✕ 关闭,z-[70]);
        大窗内保留「新窗口打开」小按钮;内联卡 autoHeight 原行为,大窗锁高内部滚动。
      
      web tsc 0 + Next 生产构建过。仅本地,未部署。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(web): 助手小窗开场建议场景化(规则生成,与 /assistant 页不共用) · 74735286
      - AssistantChat 加 examples 覆盖参数(不传 = 页面版默认三条 → 两版不共用)。
      - 详情页 ready 时把 {planId, 患者名} 发布到 plan-sync-store.current;
        小窗按规则出建议(非 AI):有当前患者 → ①为什么被召回/捋关键事实 ②画像+潜在治疗
        ③这通电话怎么开口;无上下文 → 召回池TOP/今日工作 兜底。切患者建议自动跟随。
      
      web tsc 0 + Next 生产构建过。仅本地,未部署。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(web): 助手抽公用 — 详情页右下角吸附小窗(与 /assistant 同能力) · ce6bec2d
      - AssistantChat 加 variant('page'|'widget'):page 整页原样(/assistant 保留零回归);
        widget 紧凑头部(助手 + 模型切换 + ⌄收起)+ h-full 适配容器。
      - 新 AssistantWidget:右下角悬浮圆钮 ⇄ 400×620(max 78vh)吸附窗;
        收起为 CSS 隐藏不卸载 → 对话/工具步骤/artifact/听写状态保留;
        挂在 plans/layout(路由段之上)→ 切患者对话不丢;权限 AGENT_INVOKE 同 /assistant。
      - 功能与 /assistant 完全一致(MCP 工具透明步骤 / markdown / HTML 卡片 / 实时听写)。
      
      web tsc 0 + Next 生产构建过。仅本地,未部署。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(web): 工作台抽屉临界点 1280→1024(lg) + 把手 z-50 防被详情面板覆盖 · 4c0af7a8
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(web): 工作台跨栏状态同步 + 小屏左栏抽屉化(左缘把手点击点) · ee05257d
      1. 右侧操作 → 左栏实时同步:新增 plan-sync-store(zustand 事件总线);
         详情提交回写成功(submitExecution → planStatus)→ notify → 左栏 patchItem
         原地更新该行状态 pill(不重拉、不丢滚动/筛选)。
      2. <xl 左栏收成固定抽屉:单实例 CSS 平移(不重挂 → 筛选/分页/滚动保留);
         左缘 teal 竖把手「选患者」作明确点击点;遮罩点击/抽屉沿 ✕/选中患者 三种方式收起;
         xl+ 常驻不变。
      
      web tsc 0 + Next 生产构建过。仅本地,未部署。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(web): 画像标签全量圈人(14维)+ 左栏认领按钮 + 切患者不再白闪 · 01663a75
      1. 画像标签筛选扩到全部枚举型标签(4→14 维):新增 年龄段9/性别2/家庭构成4/权益身份5/
         治疗史4/治疗敏感4/特殊关注4/时间偏好5/转介绍达人2/禁忌1;code中文逐一对齐各 extractor
         的 data 写入值。未收录并注明原因:获客渠道(取值随宿主非稳定枚举)、折扣锚点(数值型)。
         后端零改动(字典驱动,plan.service 通吃)。
      2. 左栏行 hover 出「认领」(仅 待认领+未分配)→ assign 给自己 → patchItem 原地改"进行中"
         (不重拉、不丢滚动/筛选)+ toast;usePatientPicker 加 patchItem。
      3. 切患者白闪修复 — 保留 URL 路由(可刷新/分享/回退),loader 加 stale-while-loading:
         模块级缓存上一份聚合,加载期旧详情垫底(60% 透明)+ "切换中…"小罩,新数据到无缝替换。
      
      web tsc 0 + Next 生产构建过。仅本地,未部署。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(web): 工作台布局改上下(header 全宽到最左)+ 左栏行/筛选五处打磨 · 4d04c361
      按反馈:
      1. 上下大布局:详情 TopBar 经 createPortal 渲到 plans/layout 顶部全宽槽
         (HeaderSlotPortal;无槽独立打开时原位渲染兜底)→ header 延伸到最左,左栏在其下;
         PlanDetailApp 根 h-screen→h-full,详情页包裹补高度链。
      2. 左栏行加性别(formatGender,与列表页同源)+ 诊所名(token 字典翻译)。
      3. 加诊所筛选(popover 多选 → 服务端 targetClinicIds,与列表页同款交互)。
      4. 优先级展示对齐列表页:五格色条 + 10 分制数值 + PriorityHover 算分明细。
      5. 行内去掉场景标签;状态改彩色 pill(待认领/进行中/已完成/已放弃,STATUS_META 同款)。
      
      web tsc 0 + Next 生产构建过。仅本地,未部署。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
  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 14 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
    • feat(web): 助手页按 Claude Design 稿重做 + LLM 输出 markdown 解析 · 7b79d63d
      按设计交接包(claude.ai/design)的 PAC 助手.html / assistant-app.jsx 重做样式:
      - teal(#0D9488)主题;header:teal 机器人 logo + "PAC 助手" + "MCP 患者工具" 药丸 + 模型下拉
        (自定义 dropdown,DeepSeek/Gemini/通义千问);
      - 用户气泡 slate-800 rounded-2xl rounded-tr-sm;助手 teal 头像;
      - 工具调用折叠卡(slate-50/70 + 扳手 + code 名 + 结果摘要 + ✓;展开看 入参/返回 CodeBlock)= Claude 式透明步骤;
      - composer rounded-xl + 自增高 textarea + teal 发送键 + 底部说明;空态 teal + 示例 chips。
      - LLM 文本输出改用 react-markdown + remark-gfm 解析(表格/标题/列表/code),不复用设计的结构化卡。
      
      SSE 流式 hook 不变;tsc 0。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(web): P3 — /assistant 独立聊天页(Claude 式透明步骤,接入 PAC MCP) · 02f05176
      GPT/Claude 式聊天页:模型经 MCP 自主调用 PAC 患者工具,过程透明可见。
      - use-assistant-chat:fetch+ReadableStream 消费 /assistant/chat 的 SSE(镜像 use-script-stream,
        Bearer header;EventSource 不支持自定义 header),把 text/tool_call/tool_result 事件组织成
        消息内的有序 blocks(文本 + 工具步骤)。
      - assistant-chat:消息气泡 + **工具步骤卡(可展开看 入参 + 返回数据)**= Claude 式"看得到怎么做到"
        + 模型切换(deepseek/gemini/qwen)+ 流式渲染 + 示例 prompt + 停止。
      - /assistant 路由(AuthGate + AGENT_INVOKE 权限)。
      
      前端 tsc 0 错误;后端 P1(MCP)+P2(agent loop)已端到端验证。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(assistant): P2 — "外部 agent" 模拟器(模型自主调 MCP 工具 + SSE 流式) · f86e39a5
      独立 assistant 模块(不复用 AiCall 单发框架),实现 model-driven tool-calling agent loop:
      - McpClientService:极简 HTTP MCP 客户端(raw JSON-RPC),助手作为外部 agent 真连 PAC 自己的
        /mcp(dogfood),转发用户 Bearer → 工具在该用户 tenant scope 执行。
      - AssistantService:动态拉 MCP 工具 → 包成 AI SDK tool(execute=回调 MCP)→ streamText
        多步循环(模型自主决定调哪些工具,stopWhen=stepCountIs(8))→ provider 可切换
        (deepseek/gemini/qwen,复用 AiProviderService)。
      - AssistantController:POST /assistant/chat(JWT)→ fullStream 部件转 SSE
        (text / tool_call / tool_result / error / done),供前端流式渲染 + 工具调用可视化。
      
      本地端到端验证(:3101):问"孙柯画像+召回计划"→ 模型自主 find_patient→get_patient_overview,
      流式产出基于真实数据的答案(价值分群/画像/召回 3 条),手机号掩码。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(mcp): P1 — PAC MCP 只读服务(开放患者工具给 agent) · cb405aef
      POST /pac/v1/mcp(Streamable HTTP,无状态)暴露 6 个只读工具,薄封装现有 service:
      - find_patient(检索,掩码号)/ get_patient_overview(360 一把梭)/ get_persona(全量画像)
        / get_facts(纯事实时间轴)/ get_recall_plan(召回原因+优先级)/ list_recall_queue(工作台)
      - 鉴权:Bearer 平台 JWT(McpAuthService 复用 JwtService)→ 派生 TenantScope,工具继承租户隔离。
      - 越权防护:assertPatientInScope 堵住 persona.getCurrent 等不带 scope 的读(defense-in-depth)。
      - 集成:SDK 是 NodeNext-typed,经典 moduleResolution=Node 跟不动 → 手写最小 ambient 声明
        (src/types/mcp-sdk.d.ts),运行时走 clean subpath(exports map 放行),类型与运行时解耦。
      
      本地端到端验证(:3101):401 无 token / initialize 返回 pac-patient-tools / tools/list 6 工具 /
      find_patient(孙柯,138****7369)/ get_patient_overview(persona v4+召回+20事实)/
      跨租户 patientId → isError 不在租户范围内。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(mcp): P1 基座 — PatientService.search(find_patient)+ 装 MCP SDK + 集成路径确认 · 1d8e6bab
      开放给 agent 的 MCP 只读服务,第一步基座:
      - PatientService.search(scope, query, limit):按 姓名/手机/externalId 模糊检索,强制 tenant scope,
        返回极简候选卡片(手机号掩码,真号走 revealPhone)。= find_patient 工具背后。
      - 装 @modelcontextprotocol/sdk@1.29.0。
      - 集成侦察(避免踩坑):经典 moduleResolution=Node 下静态 import 走
        @modelcontextprotocol/sdk/dist/cjs/server/{mcp,streamableHttp}.js(CJS + 带 .d.ts);
        registerTool(name,{description,inputSchema:ZodRawShape},cb)+ StreamableHTTPServerTransport
        无状态(sessionIdGenerator:undefined, enableJsonResponse:true)+ handleRequest(req,res,body)。
      - Bearer 鉴权复用 JwtService(jwt.secret),payload 含 hostId/tenantId/permissions → 派生 scope。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed