1. 09 Jun, 2026 21 commits
    • 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
    • feat(sync): 宿主改表检测(suspectFields)— 列缺席/删/加 漂移可见化 · bab32e83
      回答"宿主改了表结构/字段/枚举没通知,PAC 能不能察觉":在已有 mappingMiss(枚举漂移)基础上加
      "字段/列漂移"检测,两路:
      - assembler 层(column_absent):映射字段对应 host 列在装配行里整列缺席 → 进 stats.suspectFields。
        对直连原生表的资源有效;transform 产出表会补 key 掩盖,故补第二路 ↓
      - raw 层(form A,column_removed/added,最可靠):ingestRawTables 在 transform 前快照推送列集,
        对比"历史同源 rawPayload 列集"(基线抽样 50 条,排除本 run 防自污染,基线<5 跳过)→ 删列/加列。
      两路都并入 SyncLog.metadata.suspectFields,/admin/mapping-miss 端点一并透出(报告含 misses+suspectFields)。
      
      本地直连验证:删 doctor_name + 加 new_weird_col 推送 →
      suspectFields=[{doctor_name,column_removed},{new_weird_col,column_added}]。
      
      不建表(复用 SyncLog.metadata)。只检测+可见化,不告警/不阻断(按需后续接 webhook)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(admin): GET /admin/mapping-miss — 映射覆盖漏审计端点 · 43fa2672
      扫最近 N 次 sync run 的 SyncLog.metadata.mappingMisses,按出现量倒序聚合,给运维看
      "摄入时哪些原值没映射上、落了 _default"(诊断名/获客渠道长尾)→ 扩 yaml → reparse 闭环。
      权限 PLATFORM_MANAGE,host self scope(同其它 admin 端点)。
      
      本地端到端验证(:3101):
      - POST /push/rows + HMAC → {code:0, transactionsWritten:4, personaEnqueued:1}(验签+落库+画像入队)
      - GET /admin/mapping-miss + JWT(admin) → 精确返回推送行里的未映射诊断 "恒牙列"
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(assembler): 映射覆盖漏监控(mappingMiss)— code=null 等悄悄丢的常设可见化 · 7aa3e1a8
      保障机制:enum_mapping 精确+keyword 都没命中、落到 _default 的原值,过去静默(code=null
      悄悄烂),现在按 field+rawValue 聚合记账 → 落 SyncLog.metadata,供"看漏了啥 → 扩字典 →
      reparse"闭环。pull(cold-import)/push/reparse 三入口同享(都过 assembler)。
      
      - assembler-engine:applyEnum 落 _default 时 recordMappingMiss(单值+数组);
        AssemblerResult.stats.mappingMisses[];导出 MappingMiss + mergeMappingMisses。
      - cold-import:PerResourceStats.mappingMisses;processSubject 携带;
        ingestRawTables(push)写 SyncLog.metadata + warn;reparse 聚合 + top20 日志(批量审计面)。
      - 不建表、不加列(复用既有 SyncLog.metadata Json)。
      
      本地验证:push 一条含"乳牙列"(故意不映射)的 EMR 行 → metadata.mappingMisses 精确捕获
      [{field:code, rawValue:乳牙列, fellBackTo:"", count:1}]。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(push): 形态 A 推原生表行 — POST /push/rows 复用 pull/cold-import 同一摄入管线 · 54691f62
      宿主把原生表行(导出给数仓那种)照抄推来,PAC 复用 transforms+assembler+processSubject
      同核落库(拆分/映射/外键/幂等全 PAC 做),与 pull/reparse 完全一致,只是数据入口是 webhook。
      
      - ColdImportService.ingestRawTables({hostName, source, rows}):tables={[source]:rows}
        → transformEngine.run → 对"终端原生表=source"的每个 assembler cfg 跑 processSubject;
        tenant 走 manifest brand resolver(同 pull,非 stub);返回 touched 患者(带 tenant)。
      - push.schema:PushRowsRequest/Response。
      - PushReceiverService.receiveRows:ingest → 对 touched 患者 enqueuePersonaRecompute
        (复用现有 BullMQ 触发 + watermark 幂等 + 凌晨 cron 兜底);plan 不在此触发(定时任务保)。
      - PushController:POST /push/rows(HMAC 验签复用 /push/events 同逻辑)。
      
      本地核验:原样推=同口径去重(dup=6/txn=0);bump updated_date 推=新 txn=6/touched=1
      (tenant 正确解析);版本流无重复(active facts 9→9)。main.ts 已 rawBody:true。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs(push): 加 §6 可靠投递/异常/漏推 + checklist(回执看 envelope code、outbox、reconcile 补漏) · cb303524
      - §6.1 回执:统一 envelope {code,msg,data},HTTP 恒 200(崩溃才 5xx)→ 判成败看 body.code 非 HTTP;
        按 code 族判重试(1xxxx/30802 不重试告警;10003/9xxxx/5xx/超时 退避重试)。修正之前误用 HTTP 401/400 的表。
      - §6.2 防丢:本地 outbox + 重试到确认 + 异步不阻塞业务 + 不确定也重试(幂等去重)。
      - §6.3 漏推补齐:数仓兜底(reconcile,首选)/ 周期幂等补推(纯 push)/ PAC 侧检测告警。
      - checklist 补 outbox / 按 code 判重试 / 周期补推 / amountUnit+timezone 配置。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs(push): 金额/时间改为「宿主原样发、PAC 按接入配置(amountUnit+timezone)归一」 · fb52cf5e
      不再要求宿主自己转整数分/ISO8601带时区:接入时配置 amountUnit(fen/yuan)+timezone(IANA),
      宿主按自身系统原样发,PAC 的 normalize 归一成 分+UTC(同数仓口径)。带 offset 的时间直接用。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs(push): 重整 Push 契约 — A(推原生行)为主/C(结构化事件)备选;subjectId 选填;emrId 移入 payload · 48610164
      - 两形态二选一,A 默认(字段最少、口径同数仓、可 reparse 自愈),C 备选(强宿主)。
      - 公共 envelope 收敛为真·通用字段(subjectType/action/tenantId/patientId/occurredAt/updatedAt);
        subjectId 选填(有则更准、无则 PAC 用自然键合成),clinicId 条件,emrId 归 payload(资源专属)。
      - 幂等:PAC 合成 source_event_id(宿主不发),partial UNIQUE + 内容指纹双层保证,A 即数仓同款已实证。
      - 软删走 *_cancelled + updatedAt。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs(push): 新增 host-integration-push 宿主 Push 对接契约(独立、面向宿主) · c613d0e1
      我们讨论定稿的 Push 方案:一个端点 + HMAC + envelope/payload 分层;宿主只发原始字段
      (原始诊断名等),K码映射/幂等键(subjectId+updatedAt)/ID合成/时区 全 PAC 内政;
      诊断等一父多子用 payload.items[](身份 PAC 用 emrId+name+tooth 合成,宿主不发 source_event_id);
      软删除走 *_cancelled 事件 + updatedAt;TLS + 批量 + 幂等重试。面向宿主开发、可直接实现。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs(deploy): reparse SOP 写入 deployment-data-ingest + 标 task#46 完成 + 诊断 code 长尾待办 · 0ef13ebb
      - 新增 §六bis Reparse SOP:原理(raw_payload 离线重跑、非破坏、不连 DW)/ 用法(dry-run/全量/
        --patient/--no-recompute/docker)/ 实测基线(全量 541k ~28min,内存 ~630MB;单批 274 ~46s)/
        典型场景(诊断漏 code → 漏召,正畸 5554→1)。
      - §五运维表 + §六 yaml 变更表:truncate 重导 → 改指向 reparse。
      - §八已知边界:reparse 模式缺  已落地;新增"诊断 code 长尾待评估"(松动牙/种植体周围炎,
        需业务决策,不是补 code 能解决)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(parser): K07 关键词补"合"简写(反合/开合/对刃合)—— host 高频正畸漏召 · 807ded08
      服务器 reparse 后残留 279 条正畸 null,几乎全是用"合"简写(反合 120+、开合系 ~70、对刃合)
      而非"颌/牙合"的 crossbite/openbite 诊断。补这 3 个特定术语(不误伤:咬合/深覆合 等不含子串),
      预计再 reparse 把残留压到 ~30(剩习惯/TMD 等非正畸,合理留 null)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
  2. 08 Jun, 2026 19 commits
    • perf(sync): reparse 按患者分批,避免全量时把全 host rawPayload 拉进内存 OOM · e2960fa2
      全量 reparse 时一次性 findMany 所有 rawPayload 可达 GB 级(服务器 diagnosis 975MB/541k txn)。
      改为按患者 cohort 分批(PAC_REPARSE_BATCH,默认 3000):每批只重建该批的 distinct 源行 →
      transform → processSubject,内存恒定(本地实测 rss ~460MB);跨批累加 stats,supersededAt 圈出
      变更患者定向重算。dry-run 改报 scope(count,不分批装配)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(web): 牙位事实全口判定保持按 category(方案A)+ 注明依赖诊断已编码 · 84820f32
      正畸/牙周治疗一律归全口泳道,与"诊断按 code 判全口"对齐(K07/K05 诊断 + 同科治疗同泳道,不分叉)。
      前提:正畸/牙周诊断已正确编码(靠 diagnosis.yaml keyword 兜底 + reparse 把存量补成 K07),
      否则空 code 诊断仍按牙、与其全口治疗分叉(孙柯 15/25 案例)。仅补充注释,逻辑同原始。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(parser): 诊断 K07 正畸族 keyword 兜底(牙列间隙/错牙合/开颌/反颌/深覆/拥挤/安氏 → K07) · c0512c74
      精确白名单只列了 错颌畸形/牙列不齐/安氏…错颌畸形,漏掉大量变体写法(98% 诊断无 stdCode,
      全靠中文名匹配)→ 这些正畸诊断 code=null → 进不了 K07 召回(FN)。
      加 keyword_mapping 含词兜底(放最后,K08 缺牙间隙/K09 颌骨囊肿先吃 + none 防误伤)。
      本地 reparse 验证:正畸 null 诊断 36→2,余 2 为 regex-only 边缘变体。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(sync): 加 reparse 离线重解析 —— 改 yaml 字典不再需要全量重摄(task #46) · 9caef1b0
      ColdImportService.reparseFromTransactions + `pnpm reparse` CLI:用已存的
      transaction.rawPayload 重跑 transformEngine + assembler + processSubject(**不连 DW**),
      fact 走版本流(内容变 supersede 升版本,不变幂等 no-op),只对真正变更的患者定向重算 persona/plan。
      
      - 非破坏:不 truncate、不碰 transaction 账本 → plan_executions(客服回写)/ 分配全保住;
        比全量重摄快一个量级(省掉 DW 拉取)。
      - 覆盖 field/enum/keyword_mapping + transforms 算子 + parser 改动(长尾字典修补 99%);
        非 transform 产出的资源(如 image_finding_rows = CH SQL 视图)自动跳过(其 rawPayload 非装配输入,需走 DW)。
      - traceRawSourceTable:沿 transforms output→input 链回溯到原始源表(rawPayload 就是它的行)。
      - 用法:pnpm reparse -- --host=jvs-dw --subject-type=diagnosis [--patient=..] [--dry-run] [--no-recompute]
      - 本地实测:diagnosis 全量 superseded=36 / unchanged=3820 / created=0(零 churn),单患者 end-to-end
        fact 升版本 + 定向重算 + 正畸召回正确出现。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(web): 多值画像标签 chip 显示「首值 +N」而非静默只显示第一个 · 5696e951
      治疗史/权益/禁忌/潜在治疗/时间偏好/治疗敏感/特别关注 这 7 个特征是 `/` 并列多值,
      原 chip 用 shortPersonaValueLabel 在 `/` 处截断 → 只剩第一个值且不提示还有更多。
      改读结构化 data.labels(后端这 7 个都带):多值显示首值 + 灰色 +N,完整列表走 hover 卡片;
      单值特征不变。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(plan): assigned 重算升版本并继承分配 + 缺口解决即关闭 + 刷新回传 outcome/currentPlanId · ee00c6c5
      - assigned plan 不再整体 skip:理由集合变 → supersede + 新版本,新版本继承
        assignee/assignedAt/recycleAt/contactAttempts(客服不丢单/不被抢/熔断计数延续);理由没变只就地刷分。
      - 缺口全解决(0 命中)→ 即使 assigned 也关闭退池(closeStaleActivePlan 放开 assigned)。
      - recomputeForPatient / 单刷 API 回传 planOutcome + currentPlanId。
      - /full:plan superseded 且无活跃后继 = 已关闭 → 报 PLAN_NOT_FOUND(前端提示+退池),不再静默渲染冻结快照。
        (前端零改:既有 router.replace(currentPlanId) 切新版本 + PLAN_NOT_FOUND 提示逻辑自动生效)
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(sync): 单患者刷新兼容嵌套 manifest SQL + 加 dev 抽样开关 · 7a91d425
      - injectPatientFilter 原用 regex 解析 SELECT...FROM table,遇嵌套子查询(image_finding s1/s2 + array join)
        会拖坏括号 → CH 语法错 → 单刷 500。改为包裹原查询为子查询 + 外层 patient_id/brand 过滤(兼容任意复杂 SQL)。
      - 加 PAC_COHORT_LIMIT / PAC_COHORT_SAMPLE(recent/oldest/random)dev 抽样开关,默认全量、行为不变。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(tz): 本地 CH 设 Asia/Shanghai 对齐远程 DW + 移除 arrived_at +8h band-aid · 55f8de0c
      - 本地 pac-clickhouse 原跑 TZ=UTC,裸 DateTime(in_time)读出比北京裸字段早 8h。
        加 clickhouse/config.d/timezone.xml(<timezone>Asia/Shanghai</timezone>)+ compose TZ env,
        重建后本地行为与远程 DW(本就 Shanghai)一致;源 instant 一直正确,无需改数据。
      - special-attention 的 ARRIVED_TZ_FIX_MS(+8h)是为补偿上述本地 UTC 偏差加的;源 CH 统一 Shanghai 后
        会反向多加 8h → 移除(服务器连远程 DW,该 band-aid 本就一直让"迟到"晚 8h,一并修)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • refactor(web): 精简成功 toast(仅留无其它反馈的)+ 模型下拉加风格标签 · d8ea5d52
      - 删冗余成功 toast:单条认领/回收、登录/登出、归档、点赞点踩、召回反馈(界面已就地反映);
        保留:表单保存、批量计数、异步生成完成、"为何无动作"类、占位跳转。错误 toast 全保留。
      - 重生成模型下拉加风格标签:慢·精细 / 快·均衡 / 快·流畅 / 极快·简洁。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(ai-script): qwen3.7-max 结构化输出修复 + gemini 撤回关思考 + 超时兜底 + TTFT/latency 落账 + 价格表修正 · 70b70ce0
      - qwen3.7-max:DashScope 不强制 json_schema → 改 supportsStructuredOutputs:false + fetch 中间件强制
        response_format:json_object + enable_thinking:false;runner 从 outputSchema 自动注入英文 key 骨架
        (withQwenStructuredHint),否则模型自编 key 必 fail。
      - 三档 schema 去掉 .min/.max/.length/.int 硬约束(对中文偏严,qwen 简洁输出被打回 too_small)→ 改 describe 软引导。
      - gemini-3.5-flash 撤回 thinkingBudget:0(关思考会致结构化输出偶发 parse 失败);慢/卡由超时+重试+兜底兜住。
      - 给 generateObject/streamObject 接 180s 超时(AI_REQUEST_TIMEOUT_SEC,原 60→180)防永久挂起。
      - agent_invocations 加 ttft_ms / latency_ms(+migration),流式记首字、收尾记总耗时(对齐 Dify usage)。
      - 价格表按官方页修正(¥/M):deepseek-v4-pro 0.03/3.13/6.26、flash 0.02/1.01/2.02、
        gemini-3.5-flash 1.08/10.8/64.8、qwen3.7-max 1.2/12/36(旗舰价,之前低估 5x)。
      - 三档 promptVersion 随 schema 变更 bump。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(ai-script): qwen3.7-max 结构化输出修复(DashScope 三件套) · 746c5b0a
      接入后报错'No object generated: response did not match schema'。逐项查实 DashScope compatible-mode 三个前提:
      1. supportsStructuredOutputs:true — SDK 才发 response_format=json_schema(DashScope 实测严格按 schema 返;
         松散 json_object 不带 schema → qwen 乱返 → Zod 不符)。
      2. prompt 含 'json' 字样 — DashScope 硬性校验(否则 400 InvalidParameter);runner 对 qwen 注入(withQwenJsonHint)。
      3. enable_thinking:false — qwen3.7-max 是推理模型,流式下 thinking 污染 content 流 → JSON 解析碎;
         SDK 无此参数,用 createOpenAICompatible 的 fetch 中间件往 body 注入。关思考还顺带提速、降本(¥0.031→0.008)。
      验证:standard 流式 source=agent、succeeded、¥0.0078。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(ai-script): 接入通义千问 qwen3.7-max(DashScope 兼容端点)做话术生成 provider · 6dc6a790
      - 装 @ai-sdk/openai-compatible,AiProviderService 加 qwen provider(DashScope compatible-mode 端点)。
      - resolve 支持 qwen 前缀(qwen3.7-max / 裸键 qwen → 默认);config 加 qwenApiKey/qwenBaseUrl/qwenDefaultModel
        (QWEN_API_KEY 缺省回落 DASHSCOPE_API_KEY;默认模型 qwen3.7-max);priceTable 加 qwen3.7-max 估算价。
      - 前端模型下拉加'通义千问 Max'(qwen3.7-max)。
      - 注:该账号 key 仅 qwen3.7-max 可用(qwen-max/qwen3-max 等均 Model.AccessDenied,实测确认)。
      验证(本地 standard 流):start→逐字partial→done,source=agent,¥0.031/4823tok,落库 succeeded。
      - ️ 密钥走 .env(gitignore),服务器部署需单独在 .env 加 QWEN_API_KEY。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(ai-script): 深度档 repair 严格按自检逐条改(喂上一稿 + 强约束) · a0afbd7e
      问题:verify 后的修订阶段,模型没严格按自检 issue 改 —— 根因是 repair 只给了大纲+问题提示、
      没给上一稿,模型是'凭大纲重写'而非'在原稿上逐条改'。
      
      - DeepWriteInput 加 prevDraft;strategy 的 repair(run + runStream)把上一稿传进去。
      - buildWritePrompt repair 分支重写:① 附【上一稿(待修订)】②【必须修正的问题,逐条缺一不可,每条带'必须改成'】
        ③【修订铁律:只动被点名处/其余原样/不得引入新违规/缺一不可】。
      - writeCall promptVersion → v11。
      验证(本地深度生成触发 repair):repair invocation prompt 含上一稿+逐条问题+铁律(4项校验全 t)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(web): 深度过程步骤名精简(接地+安全自检→安全自检,按问题修订→问题修订) · 01508da3
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(web): 伴飞分段条响应式不溢出 + 深度过程步骤箭头更友好 · c482de02
      - 伴飞(copilot)分段进度条:按钮加 min-w-0(flex 子项默认 min-width:auto 不收缩 → truncate 失效 →
        6步时溢出截断),序号圈加 flex-none。现在多段自适应收缩 + 省略,不再被右侧切掉。
      - 深度过程面板:展开标识改成'右指箭头→展开旋90°朝下'的经典 disclosure(更清晰友好)+ 整行 hover 高亮 +
        aria-expanded;detail 缩进对齐按钮 padding。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(ai-script): 深度过程面板精简 — 去标题行/进行中默认展开可折叠/单色背景 · 1f2e3288
      按反馈调整 ScriptDeepProcess:
      - 去掉'深度生成过程'标题行,更简洁。
      - 步骤可折叠:进行中的默认展开、其余收缩;用户手动点过的步骤改由用户控制(in manual map → 不再受程序自动展开影响)。
      - 背景改单色(slate-50 + slate 边框/图标),去掉 indigo 多色。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(ai-script): 深度档流式可见过程(GPT/Claude 式:规划→撰写→自检→修订) · 60ce7324
      深度档原来是 start →(黑盒30-60s)→ done,看不到中间过程。改成逐步可见:
      后端:
      - deep.strategy 加 runStream():逐步 yield step 事件(plan/write/verify/repair 的 running/done +
        各步摘要:大纲/质量分/问题数);撰写/修订步走 runner.stream 逐字 partial(打字机)。run() 保留不动。
      - orchestrator 深度档 SSE 路径改为消费 runStream → 转发 step + partial 事件;新增 PlanScriptStreamEvent.step。
      前端:
      - use-script-stream 解析 step 事件,维护 steps 时间线(done 后保留可回看)。
      - 新增 ScriptDeepProcess 组件:步骤时间线(spinner/✓ + 大纲展示 + 接地安全/质量分 + 修订问题数)。
      - plan-detail-app 在话术区顶部渲染过程面板;正文 sections 在下方逐字流。
      端到端实测(本地 deepseek-v4-flash):start→step plan(running/done)→step write+逐字partial→
        step verify→step repair→done,全程事件正常流出。abort/停止复用现有。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(web): 工作台头部加退出登录(配合模拟登录) · 219f7440
      头像旁加 LogOut 按钮 → useAuthStore.clear() 清 token → AuthGate 自动弹回登录对话框。
      口径与登录一致(mock/SSO 都走 AuthGate);clear() 已存在,纯前端。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs(dw-issues): #15 in_time/billing_date 类型违约(DateTime/UTC vs 契约北京naive)→ 交宿主修 · a3b60813
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed