- 30 Jul, 2026 16 commits
-
-
## 测试服全量实证(不是本地样本) jvs-dw 43,279 条亲戚边,与年龄矛盾 428 条 → 1.0%,方向基本可信 friday 2,427 条;只看父母/子女**对**更刺眼: mother↔ child 326 对里只有 8 对方向对(2.5%) father↔ child 231 对里只有 9 对(3.9%) → 97% 反了 ## 根因 data/friday/assemblers/patient_relation.yaml 的 enum_mapping 假设源码语义是 「本人是对方的 X」,于是映射时取了**逆关系**(码2 爸爸 → child)。数据说这个假设是错的: FRIDAY 存的码本来就是「对方是本人的 X」,跟 PAC 契约同向 —— 多反转了一次。 互反边可以证:秦佳(47)--mother-->韩秦瑜(21) 与 韩秦瑜--child-->秦佳 两条边互相自洽、 但都与年龄矛盾;去掉那次反转两条就都对了。⚠ ️ 摄入侧的修法(改 yaml + 重摄该资源)另开一件事 —— patient_relation 是 upsert 资源、 不进 transaction,没有原文可 reparse,得从源重拉。 ## 这次做的是展示侧:年龄定方向,不降级 按你的要求不降级成「亲属」,而是**实时算出真实关系**: · 标成长辈但对方更年轻 → 子女 / 孙辈 · 标成晚辈但对方更年长 → 按对方性别拆父/母(性别缺 → 中性「父母」,不硬猜) · 配偶 / 兄弟姐妹是对称关系,没有方向可纠,原样返回(同岁配偶很正常,不许被误标) · 任一方缺生日 → 原样返回,不猜(会显示的边 98.8% 两边都有生日) 纠正过的在关系后打一个 `*`,hover 显示「源数据记的是「mother」,与双方年龄不符,已按年龄纠正」—— 客服跟宿主对账时能看出差异在哪,而不是以为 PAC 显示错了。载荷同时留 relationshipRaw。 口径收在 @pac/types/kin-relationship.ts(纯函数,附实证数字),11 条用例锁住, 其中"同岁也算矛盾""配偶不许被纠""缺生日不猜"三条是最容易被后来人改坏的。 本地实测(王红兵 58 岁):源里的「妈妈 王迪 31岁」现在显示「子女* · 31岁」,配偶 59 岁不动。 717 tests / 47 suites + 两端 typecheck 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
落表:plan_executions 加一列 inaccurate_treatments TEXT[] NOT NULL DEFAULT '{}'。 · **加列不建表**:它是 abandon_reasons 里 'inaccurate' 那项的限定词,不是独立事实 —— 同一次提交、同一条 execution、基数 ≤8。单独建表只多一次 join。 · **存 code 不存中文**:这列唯一价值是可统计(哪类召回最容易被判不准 → 回去改规则)。 中文措辞两天内改过三轮(潜在种植→种植治疗→种植),存中文等于把当时措辞烤进历史数据。 (同类教训:原 abandon_other 自由文本列就是因为"只能取到字面量、统计不了"被删的。) · **不塞 notes**:自由文本统计不了,正好废掉这个字段的唯一价值。⚠ ️ 按业务口径明确两条,都写进 schema 注释免得后来人误解: ① **不参与抑制**。抑制仍是信号级、按 plan 全部 reason 一起压 —— 客服选"只有种植不准" 不会只放过根管那条。看到这列别以为闸接上了。 ② **跟「其他原因」不构成关联**。只在勾了 inaccurate 时收集/落库;后端落库时再判一次, 免得前端残留勾选污染统计口径。 必填校验前后端各一道。服务端那道不是冗余:这条反馈事后补不回来(没人会为已结案的单再来一遍), 收进来一批空的就等于白填。 候选项 = 本患者画像 potential_treatment.types(同召回池卡片那排标签的来源),所以每人不同; 中文在前端查 POTENTIAL_TREATMENT_CARD_LABEL(改措辞即时生效)。 患者没有潜在治疗标签时不卡必填 —— 理论上不该发生,真遇到宁可放行,不让客服卡在填不了的必填项上。 708 tests / 46 suites + 两端 typecheck 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
## 测试服全量实证(不是本地样本) jvs-dw 43,279 条亲戚边,与年龄矛盾 428 条 → 1.0%,方向基本可信 friday 2,427 条;只看父母/子女**对**更刺眼: mother↔ child 326 对里只有 8 对方向对(2.5%) father↔ child 231 对里只有 9 对(3.9%) → 97% 反了 ## 根因 data/friday/assemblers/patient_relation.yaml 的 enum_mapping 假设源码语义是 「本人是对方的 X」,于是映射时取了**逆关系**(码2 爸爸 → child)。数据说这个假设是错的: FRIDAY 存的码本来就是「对方是本人的 X」,跟 PAC 契约同向 —— 多反转了一次。 互反边可以证:秦佳(47)--mother-->韩秦瑜(21) 与 韩秦瑜--child-->秦佳 两条边互相自洽、 但都与年龄矛盾;去掉那次反转两条就都对了。⚠ ️ 摄入侧的修法(改 yaml + 重摄该资源)另开一件事 —— patient_relation 是 upsert 资源、 不进 transaction,没有原文可 reparse,得从源重拉。 ## 这次做的是展示侧:年龄定方向,不降级 按你的要求不降级成「亲属」,而是**实时算出真实关系**: · 标成长辈但对方更年轻 → 子女 / 孙辈 · 标成晚辈但对方更年长 → 按对方性别拆父/母(性别缺 → 中性「父母」,不硬猜) · 配偶 / 兄弟姐妹是对称关系,没有方向可纠,原样返回(同岁配偶很正常,不许被误标) · 任一方缺生日 → 原样返回,不猜(会显示的边 98.8% 两边都有生日) 纠正过的在关系后打一个 `*`,hover 显示「源数据记的是「mother」,与双方年龄不符,已按年龄纠正」—— 客服跟宿主对账时能看出差异在哪,而不是以为 PAC 显示错了。载荷同时留 relationshipRaw。 口径收在 @pac/types/kin-relationship.ts(纯函数,附实证数字),11 条用例锁住, 其中"同岁也算矛盾""配偶不许被纠""缺生日不猜"三条是最容易被后来人改坏的。 本地实测(王红兵 58 岁):源里的「妈妈 王迪 31岁」现在显示「子女* · 31岁」,配偶 59 岁不动。 717 tests / 47 suites + 两端 typecheck 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
- 话术头部新增「关联客户」:亲戚姓名/关系/年龄 + 档案链接(配了 VIEW_PATIENT 跳宿主, 没配回落 PAC 工单页,都新标签页开) - 画像标签按业务字典 A/B/C/D 类区上色 + 定序,首屏与画像详情抽屉收成一套(删掉原三分组) - 选患者列 / 详情左栏宽度 300 → 320
luoqi committed -
原来只跳 PAC 自己的工单页。宿主档案才是客服真正要看的那一页(有真手机号、有全量病历), PAC 工单页只是兜底 —— 所以跟本人的「原始档案」同一套逻辑: 配了 actionUrls.VIEW_PATIENT → 跳宿主档案,占位换成**这位关系人**的 {patientId} / {medicalRecordNumber} 没配 → 回落 /plans/<planId>(本 scope 内的活跃工单) 都没有 → 只显示信息,标「无档案入口」(原来叫「无工单」,现在两条路都可能缺,措辞跟着改) 为此后端 include 补了关系人的 externalId / medicalRecordNumber —— 注意 VIEW_PATIENT 模板里的 {patientId} 是**宿主侧 id**,不是 PAC 的 uuid,拿 relatedPatientId 去填会得到一个查不到人的链接。 关系人没建档时 externalId 退回边上的 relatedExternalId(那个始终有)。 本地两条路都实测: 未配 → /plans/2669df95… 配了 → …/patient?pid=115199&mrn=JN0A016246 —— pid/mrn 是**关系人的**(本人是 110959/JN0A025573), 没有串成本人的 id 708 tests / 46 suites + 两端 typecheck 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
一家人常在同一家诊所看牙。客服打电话前看到"这人的配偶也是我们的客户",既能顺口关心、 也能顺手跟进那一位,所以放在话术头部(AI 简报下方)而不是收进抽屉。 patient_relations 表本来就有(摄自 fact_customer_referee_out,family_structure 特征在用), 详情接口原来也已经返回 contacts,这次补三件:
⭐ 只列**亲戚**。新增 KIN_RELATIONSHIPS 白名单(配偶/子女/孙辈/父母/祖辈/兄弟姐妹), friend 和 other **刻意不进** —— 那条边表摄自推荐关系,other 占了一半以上(本地 4043/6222), 混的是推荐人、代付人这类"认识但不是亲属"。字典把 other 译成「亲属」,但照它列出来 客服会把推荐人当家属去问病情,比不显示更糟。 (sibling 这里算亲戚,而 family_structure 把它算"非直系" —— 两处判的不是同一件事,口径不同是有意的)⭐ 年龄:从关系人 birthDate 现算(include 补 birthDate)。⭐ 链接目标 = PAC 自己的工单页,**且必须过 scope**。关系人常和本人不同品牌/诊所,不过滤就会 给出一个点进去 404 的链接 —— 那比"没有链接"更糟,客服会以为系统坏了。 批量一条 SQL 查(loadRelatedPlanIds),查不到 → planId=null → 只显示信息、位置上标「无工单」。 新标签页走 openHostUrl(带 sandbox 三级兜底),不顶掉当前工单。 未建档(linked=false / 无姓名)的关系不出行:一行只有关系没有人,客服拿不到任何可用信息。 本地实测(王红兵):渲染出「王希亮 配偶·59岁 → 关联客户档案」「王迪 妈妈·31岁 → …」, href=/plans/<id> target=_blank rel=noopener noreferrer。 708 tests / 46 suites + 两端 typecheck 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
## ① 类区 = 唯一一套分类 A/B/C/D 本来就在 PERSONA_FEATURE_SPECS 每个标签上方的注释里(《客户画像标签字典 v3.0》的 编号 A.1.1 / B.1.1 / C.1.1 / D.2.4…),16 个全有,只是没结构化。现在提成 display.category: C 临床需求(琥珀) 治疗史 · 潜在治疗 · 急迫等级 B 价值与阶段(靛蓝) 价值分群 · 生命周期 · 权益身份 · 转介绍达人 D 行为与偏好(玫红) 折扣锚点 · 禁忌标签 · 特别关注 · 治疗敏感 · 时间偏好 A 基础属性(灰) 获客渠道 · 年龄段 · 性别 · 家庭构成 类区序 C→B→D→A(业务定,不是字母序 —— 字母序会把"基础属性"排首屏开头,客服最不需要的 东西占最显眼的位置)。类区内序沿用 7-29 业务给的相对顺序。
⭐ **删掉了原来那套三分组**(跟进要点/价值与阶段/基础属性)。两套分类并存的代价是加标签时 要想"归哪组"两遍、而且两边各自漂;更直接的问题是同一批标签在首屏和抽屉里是**两个顺序**, 客服在抽屉里得重新找一遍。现在首屏 chip 和抽屉共用 personaFeatureSortKey,颜色共用 PERSONA_FEATURE_CATEGORY_META —— 真正同出一源。 首屏 chip 从统一素色改成**按类区 4 色**(不是按标签 16 色 —— 一标签一色等于没有层次); 抽屉分组标题补同色小圆点,两处认的是同一套色。 ## ② 两栏宽度 300 → 320(选患者列 + 详情左栏)⚠ ️ 副作用:xl 断点(1280)恰好那档,中栏被压到 196px(原 236px)。宽屏无影响, 1280 附近本来就挤,这次更挤了 —— 要治得动断点,不在本次范围,已在回话里说明。 ## ③ Tailwind 陷阱记在注释里 chip 的颜色类必须整串来自 TONE 表(字面量),别写 `'hover:' + C.bg` —— Tailwind 静态扫源码, 拼出来的类名不会被生成,表现为"颜色没生效"且难查。 新增 5 条防漂移测试:每个标签必有合法类区、类区序覆盖四类且等于 C→B→D→A、 每类有中文名+颜色、同类区内 order 不重复、首屏白名单排完类区下标单调不减。 708 tests / 46 suites + web typecheck 通过。本地实测首屏三色与抽屉分组色一致。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
- 新标签页改一步 open —— 修宿主加 allow-popups 后的 SecurityError(两步走断 opener 导致) - 召回池卡片去掉优先级那排五个色点,只留分数 - postMessage 字段名简化 desc / treatments / stage;早期矫治 → 早矫 - 交付文档补 allow-popups-to-escape-sandbox 的说明与完整 sandbox 推荐串
luoqi committed -
宿主加上 allow-popups 后炸了两条,都是上一版那个两步走导致的: Unsafe attempt to initiate navigation for frame with URL 'about:blank' … The frame attempting navigation is sandboxed and is trying to navigate a popup, but is not the popup's opener and is not set to propagate sandboxing to popups. Uncaught SecurityError: Failed to execute 'replace' on 'Location': The current window does not have permission to navigate the target frame to '<host url>'. 因果:**opener 一断,我们就不再是那个弹窗的 opener**,而沙箱化的 frame 只有作为 opener 才有权 导航它 → 第二步 location.replace 被拒,新标签页停在空白页。顺序反过来也不行(导航到跨源之后 再设 opener 会抛)。上一版之所以两步走,是想"手工断 opener 以等效 noopener、同时保留可检测性" —— 这个组合在沙箱下不成立。 改成一步 `window.open(url, '_blank')`: · 可检测性仍在 —— 被拦时返回 null(features 里**不写** noopener;写了成功也返回 null, 那才是分不清被拦和成功的写法) · opener 只做尽力而为的切断(跨源会抛,catch 掉)。可接受:目标是宿主管理页配好的自家地址, 不是用户输入的任意站点,tabnabbing 面本来就很窄。 三级兜底(新页 → 顶层 → 本 frame)保持不变。 交付文档补上:只给 allow-popups 时新标签页会**继承沙箱**,宿主自己的页面在里面可能功能不全, 建议连 allow-popups-to-escape-sandbox 一起加,并给出完整推荐的 sandbox 串。⚠ ️ 内嵌浏览器面板自身拦弹窗,"正常开出新标签页"这一支我这边仍证不了; 但两步导航已从结构上去掉,SecurityError 那类报错不会再有。web typecheck 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
业务 2026-07-30。分数本身已经带档位信息(而且还按档着色),五个点是同一件事的 第二种编码 —— 一行里两处讲优先级,反而要人对照着看。 档位色保留在数字上(极低/低 绿 → 中/高 琥珀 → 极高 玫红),扫一眼仍分得出轻重; 算分明细仍走外层 PriorityHover,没动。
⚠ ️ 只改左栏列表卡片。详情页顶栏那个「优先级 ●●○○○ 低」是 shared.tsx 的 PriorityBar (带文字档位标签),不是同一个组件,业务没要求动。 本地实测:卡片只剩 3.12 / 2.74,色点已无;web typecheck 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
- 嵌宿主 sandbox iframe 时新标签页被拦 → 三级兜底(新页 → 顶层 → 本 frame) - 去掉「已在新标签页打开宿主预约页」toast;顶栏「回访」按钮改「跟进」 - 打开潜在治疗的 postMessage 补 desc / treatments / stage 三个字段 - 新增交付文档 docs/integration/postmessage-actions.mdx(可直接给对接方)
luoqi committed -
字段名按对接方意见简化: pendingTreatmentDesc → desc potentialTreatments → treatments caseStage → stage 信封(source/type/action)不动。这三个字段还没交付给任何宿主,现在改是零成本; 之后再改就是毁约(宿主的解构会拿到 undefined),所以测试里把三个名字锁死、 并断言旧名字不许还留在交付文档里(留着对方会照旧名写)。 早矫的项目名从「早期矫治」改「早矫」。它砍后缀砍不出来 —— 加一张**只有一条**的例外表, 注释写明"能靠砍后缀得到的别往这里堆",免得又退化成两张手维护的中文表。 界面上仍叫「早期矫治」(卡片措辞不动)。
⚠ ️ 与列表 API 的 PlanPatientBrief.potentialTreatments 无关 —— 那是 PAC 自己的 REST 字段, 不是宿主契约,不跟着改。 703 tests / 46 suites + web typecheck 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
发给宿主的是 ["种植","修复"] 而不是 ["种植治疗","修复治疗"] —— 宿主拿它当**项目名**落单, 带后缀读起来像句子、不像项目。界面上客服看到的仍是「种植治疗」(业务 7-29 定的展示措辞), 两处措辞不同是有意的。
⭐ 从卡片措辞**推导**而非另立一张表(potentialTreatmentItemName = 砍掉结尾的「治疗」): 两张手维护的表必然漂。early_ortho 的「早期矫治」本来就没这个后缀,原样保留 —— 它不叫「早矫治疗」,也不该被砍成「早矫」。 交付文档同步:字段说明 / 示例 JSON / 8 类取值表 / 监听示例注释全部换成项目名, 并加一段 Callout 说清"界面带后缀、载荷不带"是有意的。 测试跟着改成查**项目名**而不是卡片标签(发出去的是前者,文档要跟载荷一致、不是跟界面一致), 再加一条:砍后缀不许砍出空串、不许换词、early_ortho 必须原样。 702 tests / 46 suites + web typecheck 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
载荷从 `{patientId}` 扩到: pendingTreatmentDesc 待治疗描述 —— 取页面顶部那句 AI 召回简报;还没生成好则退回 结构化召回原因文本;都没有 → 空串(字段一定在,别让宿主判 undefined) potentialTreatments 关联治疗项目 —— 中文标签数组,与召回池卡片同一套措辞(8 类) caseStage 病例阶段 —— 恒「已咨询」⭐ 三个字段一律取**客服此刻在页上看到的东西**,不另算一套:宿主建出来的单要和客服刚读的 那句话对得上,否则对账时说不清是谁改的。所以简报由 RecallBriefLine 拿到后回报给父层 (它本来就负责 get-or-generate),不再单独查一次 —— 否则会出现"页面显示 A、发过去 B"。⭐ 契约收在 @pac/types/host-action-message.ts 而不是组件里:它是**对外接口**,宿主照它写监听。 放在 types 意味着改字段过类型检查 + 有文档同源,而不是某个组件里悄悄多塞一个 key。 兼容纪律写进注释:只增字段、不改已有语义、不删字段。⚠ ️ 这三个字段**刻意不进 URL 模式**:长文本 + 数组塞 query string 会撞长度上限, 还会把病情描述写进浏览器历史和宿主 access log。要 URL 模式也带得宿主改成 POST。 交付文档 docs/integration/postmessage-actions.mdx(可直接给对接方): 信封 / 字段表 / 完整示例 JSON / 8 类治疗项目的中文↔ code 对照 / 宿主监听示例 (含必须校验 e.origin)/ 注意事项(URL 模式不带、单向无回调、sandbox 别漏 allow-popups)。 加防漂移测试:信封字面、caseStage 恒值、8 类标签与 code 必须在交付文档里列全 —— 这份文档是发给外部照着写的,漂了要等联调才暴露。 701 tests / 46 suites + web typecheck 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
## ① 内嵌宿主时控制台报错、点了没反应 线上(嵌在宿主 iframe 里)实测: Blocked opening '<url>' in a new window because the request was made in a sandboxed frame whose 'allow-popups' permission is not set. 宿主的 <iframe sandbox> 没给 allow-popups → window.open 直接被浏览器拦掉。 上一版(7-29 改新标签页)只写了一行 window.open,**连"被拦了"都不知道**,表现就是点了没反应。 openHostUrl 改成三级兜底:新标签页 → 顶层跳转(老 _top 行为)→ 本 frame 内跳转。 能开新页就开,开不了也得让人到得了那个页面。
⚠ ️ 检测"被拦"不能靠 window.open 的返回值配 noopener —— features 里带 noopener 时 **成功也返回 null**,永远分不清。改成先开同源 about:blank、拿到句柄手工断 opener 再导航, 效果同 noopener 但可检测。⚠ ️ 两个 a 标签(原始档案 / 原始病历)也改成 onClick 走 openHostUrl:声明式的 target="_blank" 在 sandbox 下同样被拦,而那条路我们既拦不到也补不了。href 保留让 右键"复制链接"仍可用。 根治仍在宿主侧 —— 注释和 console.warn 都写明了:请对接方给 iframe 加 allow-popups (想让新页不继承沙箱再加 allow-popups-to-escape-sandbox)。 ## ② 去掉「已在新标签页打开宿主预约页」toast 新标签页开出来用户自己看得见,再报一句是噪音。失败路径的提示都还在。 兜底路径也刻意不弹 toast:下一行就导航走了,提示根本来不及被看见。 ## ③ 顶栏「回访」按钮文案改「跟进」 这个按钮跳的是宿主侧动作页,落到宿主那边不一定叫回访;而 PAC 里「回访」已被 "诊所回访记录 / 历史联系"占着,同一个词指两件事。⚠ ️ 只改按钮字面,槽位 key(OPEN_RETURN_VISIT)不动 —— 那是宿主配置里的键名。 本地实测:打桩让 window.open 返 null(模拟 sandbox)→ 确实走兜底跳到了宿主页; 按钮 DOM 是 title="跟进" / 文案"跟进";预约不再弹成功 toast。⚠ ️ 未实测的:真 Chrome 里"正常开出新标签页"这一支 —— 内嵌浏览器面板本身拦弹窗, 两种 window.open 形式都返 null,证不了。697 tests / 45 suites + web typecheck 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed
- 29 Jul, 2026 24 commits
-
-
- 品牌:主色换 PANTONE 286 C(#0032A0),teal-* 全站改名 brand-* - 话术:自报家门改「我是{诊断医生}医生的助理X」(三档 promptVersion 全 bump) - 详情页:关键画像标签上首屏;宿主槽位跳转改新标签页;隐藏「牙位」「历史联系·详情」 - 召回池:到诊派生字段落 patient_profiles + 筛选索引;卡片信息重排;医生筛选可搜索 - 修复:排序键精度、筛选 chip 显示原始 code、筛选面板 React 重复 key、 医生名单缓存 6h→10min、客服可自助返池(带归属闸)luoqi committed -
luoqi committed
-
冲突解法:两处 ensurePatientStub 之后的 stats.patientStubsCreated++ 保 main 侧 —— 那是 host-id-format-defense 加的空壳计数(回执要透出空壳患者数,否则宿主看不出 自己推早了);本分支只是没有这行,不是要删它。
luoqi committed -
- 医生名单缓存 6h → 10min,重算收尾主动清 key(测试服「暂无医生名单」的根因) - staff 可自助返池,配套归属闸:只能退自己认领的单(leader 仍可退任意人的)
luoqi committed -
## ① 医生名单搜不到(测试服实证) 「上次医生 / 偏好医生」的候选名单缓存 6 小时,**且没有失效钩子**。三个派生列刚上线 全是 NULL,第一次打开筛选面板就把 `[]` 缓存了 6 小时;之后重算把值填上了,客服那边 照旧「暂无医生名单」,输入框搜谁都搜不到 —— 表现成"这个医生搜不到",没人会想到是 Redis。 缓存久的真实代价不是"名单旧几小时",是**排查成本**。两处改: · TTL 6h → 10min(仍挡得住"每开一次面板跑一次 DISTINCT",44 万行有索引,不贵) · recompute-persona 收尾主动删该 host 各 tenant 的 key → 刚跑完就能搜到,不用等 10 分钟 key 拼法导出成 doctorOptionsCacheKey,CLI 不再手写字符串(写歪了是静默删空)。 ## ② staff 自助返池 认错人 / 打不通 / 不该由我跟,客服自己就该能退回池,否则只能挂着占位,或者硬走 「关闭机会」—— 那会污染放弃原因统计。
⚠ ️ 光给权限是有洞的:客服 A 能把客服 B 手里的单退回池再自己认领,认领闸挡的是 "没认领就作业",挡不住"先把别人的单退了"。所以配套加了归属闸 assertCanRecycle: · 有 PLAN_VIEW_ALL(leader/admin)→ 可退任意人的单,这是"组长回收"的原义 · 没有(staff)→ 只能退自己认领的,否则 PLAN_CLAIMED_BY_OTHER(带占用人,前端能提示是谁) 判据取权限不取角色字符串 —— 权限模型的真理源是 ROLE_PERMISSIONS,service 不该再认一次角色。 顺带订正 recycle-scheduler 里"staff 无返池权限所以自动回收是唯一释放路径"的注释(已不成立)。 663 tests / 42 suites 通过(新增 6 条:归属闸四态 + 权限矩阵两条)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
本轮迭代(2026-07-29): - 品牌:主色换 PANTONE 286 C(#0032A0),teal-* 全站改名 brand-* - 话术:自报家门改「我是{诊断医生}医生的助理X」,非客服身份(三档 promptVersion 全 bump) - 详情页:关键画像标签上首屏(姓名下,素色无 label,点开定位到画像详情对应那条) - 详情页:宿主槽位跳转一律新标签页(原 _top 会顶掉宿主页);隐藏「牙位」「历史联系·详情」 - 召回池:到诊派生字段落 patient_profiles(上次时间/上次医生/偏好医生)+ 筛选索引 - 修复:排序键精度、筛选 chip 显示原始 code、筛选面板 React 重复 keyluoqi committed -
## 换色 globals.css 里新增品牌色阶 brand-50…900(同色相 H=221.25 推出), 主色 #0032A0 坐 600 档 —— 全站 `bg-brand-600` 是主按钮、`--primary` 也指这档, 主色必须落这里才算真换了。--primary / --ring 同步改成 hsl(221.25 100% 31.4%)。 实测 `bg-brand-600` 计算值 rgb(0,50,160),与色卡 RGB 0/50/160 一致。 ## 为什么顺手改名 没有走"偷偷把 Tailwind 的 teal 调色板改成蓝色"这条捷径 —— 那样类名写着 teal、 渲染出来是蓝的,下一个人读代码会以为自己看错了。品牌色就该叫 brand: - 263 处 `teal-N` → `brand-N`(28 个文件,纯类名替换) - tone 色板键 'teal' → 'brand'(labels.ts 的 PERSONA_FEATURE_META 一并改; tone 值全在代码里,不来自 DB,改名没有兼容问题) - 宠物 SVG 里手挑的 3 个十六进制(11 处)按色阶位置 1:1 映射过去,不然吉祥物 还是青色、跟全站撞色 以后换色只改 globals.css 那 10 行。 web typecheck + 657 tests / 42 suites 通过;本地实测列表/详情/表单全站已是蓝。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
业务口径(2026-07-29):回访挂医生的名义,患者更愿意接、也更贴事实(话术全程就是 以诊断医生的关怀为主线);"客服"听起来像销售,开场就掉信任。 改动收在两处口径源,三档共用: - agent-identity.ts:岗位称呼 staff/leader/admin 一律「助理」(患者不关心内部层级, 原来 leader 报"客服主管"),无姓名兜底也从"客服"改"助理"。 - prompt 里的自报家门句:`我是{clinicName}的【回访客服】` → `我是{诊断医生}医生的【回访客服】` (稳健档 prompt / 标准+深度档 fact-block / 稳健档 LLM 失败兜底文案,三处全改)。 医生姓取诊断医生,取不到时 deidentifyDoctor 给"您的主治" → "我是您的主治医生的助理X"。 占位符字面量 `【回访客服】` **故意不动** —— 线上 plan_scripts 已有正文都含这串, 改字面量等于老缓存回填不上、患者会看到光秃秃的占位符。它现在只是个 token, 回填出来已经是"助理X";老话术因此平滑变成「我是XX诊所的助理李倩」,不会出洋相。 诊所名不再进自报家门,但仍作为一行事实给 LLM(标注"只在需要提到诊所时用,别自己编 XX口腔")—— 原来它就是防编造用的,直接删会让 LLM 自己造诊所名。 三档 promptVersion 全 bump(inputHash 含 promptVersion → AI 缓存自然失效,新生成走新话术; 已生成的老话术不会自动重跑,要新措辞得点「重新生成」)。 657 tests / 42 suites 通过(agent-identity + script-facts 两个 spec 同步改口径)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
业务要求(2026-07-29)。两处都做成**不传回调即不渲染**,抽屉本体 (kind='teeth' / 'return-visits')一行没动 —— 想恢复就是把 onOpenTeeth / onOpenDetail 传回去,注释里写好了原样。 顺带改了历史联系的兜底文案:原来是「N 条历史联系 · 点「详情」查看」, 详情入口一隐藏就成了指路指向不存在的东西,现在只留「N 条历史联系」。 web typecheck 通过;本地实测关键事实卡只剩「详情 →」。 (历史联系卡本地这个患者没有回访记录、不渲染,那处未实测。) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
三处按业务意见调整: ① 顺序由业务定,写死在 PERSONA_KEY_FEATURE_KEYS 的数组序: 治疗史 → 潜在治疗 → 价值分群 → 生命周期 → 权益身份 → 折扣锚点 → 获客渠道 → 禁忌标签 → 特别关注 (原来跟抽屉共用 personaFeatureSortKey,那是按"跟进要点/价值与阶段/基础属性" 分组排的;首屏没有分组标题,照那个序排客服看不出章法) ② 急迫等级下首屏 —— 顶栏「优先级」条已经表达同一件事(急迫是它权重最大的一项), 同屏两处讲一件事会被当成两个指标。 ③ 去 hover,改点击:chip 变真 button,点了开画像详情抽屉并**滚到那张卡、加重描边**。 扫首屏时鼠标划过就弹浮层是干扰;真想知道"这标签怎么算的"的人,该看的是完整那张卡。 抽屉里十几张卡,要找的常在折叠线以下 —— 不定位的话点标签和点「详情 →」没区别。 走「详情 →」进仍是看全量,不定位。 滚动等一帧再执行:抽屉是本次渲染才挂上的,同步滚时容器还没布局,scrollIntoView 是空操作。 label 视觉上去掉了,button 补 aria-label(读屏仍念得出标签名)。 657 tests / 42 suites 通过;web typecheck 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
事故(2026-07-28 19:19,测试服务器):FRIDAY 推 med_emr_info 时 patient_id 变成了 浮点字符串 "676600.0"(典型 pandas 症状:列含空值 → int64 提升 float64 → 导出带 .0)。 PAC 原样收下,患者索引查 "676600.0" 命中不了真档 "676600" → ensurePatientStub 建出无名幽灵患者。3 个幽灵档吸走 4224 条事务,其中一个带着 2246 条诊断进了召回池、 算了画像、生成「应治未治」计划排队等客服打电话 —— 而推送回执是干净的 failed=0, 宿主和我们都看不出来,直到有人在池子里看见一个没名字的患者。 两层防御: 1) 归一层去尾巴(field-mapper.normalizeCanonical) `Id` 后缀的 canonical 字段,值形如 "676600.0" / "676600.00" → 截成 "676600"。 用后缀约定而非逐资源枚举,新增 canonical 字段自动纳入。只处理**整数值**的浮点 写法,不动 "676600.5" —— 那是另一种问题,应该显形而不是被悄悄改写。 放在 normalizeCanonical 意味着 cold-import / push / pull / reparse 行为一致。 2) 回执透出 patientStubsCreated(通用信号) 空壳 >0 本身不是错(「推送顺序无要求」正是靠它兜底),但**持续 >0 说明患者号 对不上** —— id 格式漂移只是其中一种表现形式,主档漏推同样会触发。宿主据此自检, 不必等人肉在召回池里发现幽灵。同时 logger.warn 提示常见成因。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
同一情况三条路给了三个答案: processSubject → ensurePatientStub(),照常落库 processPatientRelations → `if (!patientId) continue`,静默丢 processPatientReturnVisits → `if (!patientId) continue`,静默丢 那句 continue 的注释写着「非 active client,无处挂靠」,但 cold-import 的 cohort 路径 经 injectPatientFilter 对每张表都按患者 id 过滤、patients 又先跑,它几乎永不触发; 真正踩中的是 push —— 宿主先推 customer_referee_circle(07-27)、后推 customer_basic_info(07-28~29),10169 条关系边只落 257 条(2.5%), 且不计 failed、不进回执,宿主看到的是 accepted=N / failed=0,完全无从察觉。 契约文档承诺「推送顺序无要求」,兑现它靠的就是空壳兜底(pull 侧的等价物是 ClickHouseSourceService 的「反向拉主档」)。空壳只建**本人**这一侧;关系的对方 (relatedPatientId 可空)不建,靠 upsert 的 `update: { relatedPatientId }` 在对方入库后重推时回填 —— 那段回填代码本来就在。 加源码闸 ingest-patient-stub-consistency.spec.ts 锁住「三条路对齐」, 将来加第四个 process* 方法不至于再漏。已验证该闸对修复前的代码报 processPatientRelations / processPatientReturnVisits 各 1 处静默丢弃、 且两者都缺 ensurePatientStub。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
ColdImportService 是 Nest 单例,却把 source_unit 解析器存成实例字段,每次摄入开头 覆写、在长 async 循环里读。原注释「cold-import 按 host 串行,实例字段安全」在 push (webhook,按请求并发)接入后失效: friday identity_namespace_field = tenant_id jvs-dw identity_namespace_field = brand 两者并发摄入互相覆写 resolver → 对方的行解析不出命名空间列 → source_unit='' → 患者索引 (source_unit, external_id) 未命中 → 建出空命名空间的重复患者主档。 测试服务器实测(2026-07-29 查证): - friday 首推 07-23 16:24:26,jvs-dw 首个空品牌患者 16:24:31(5 秒内); 此前 jvs-dw 自 06-28 起 39 万患者零个空品牌。 - jvs-dw 空品牌患者 51063 条,其中 51037(99.95%)与真主档同 external_id; friday 空品牌 498 条,496 条落在 jvs-dw pull 窗口内,354 条是重复档。 改法照抄 tenantResolver 一贯的纪律:局部 const 构建 + 逐层传参。涉及 4 个入口 (reparse / ingestRawTables / importDirectory / importPatient)与 5 个读取方法 (processPatients / processPatientRelations / processPatientReturnVisits / processSubject / hydratePushLookupTables)。纯管道改造,无行为变更。 并加源码闸测试 ingest-resolver-no-instance-state.spec.ts:这类 bug 单跑任何一条 路径都正确,只有并发交错才炸,单测抓不到,只能在源码层禁止 `this.*Resolver =`。 (已验证该闸对修复前的代码报 4 处赋值 / 9 处读取。) 注:线上已污染的 5.1 万条空命名空间主档需另行归并,不在本次范围。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
按业务意见调整: ① 治疗史 / 折扣锚点 / 获客渠道 / 权益身份 进首屏(现 10 个,16 个里留 6 个不进: 时间偏好、治疗敏感 —— 通话中才用;年龄段、性别 —— 姓名行已有; 家庭构成、转介绍达人 —— 不改变开场)。 ② chip 去掉标签名,只留取值:「急迫等级 紧急」→「紧急」。 取值本身自解释(紧急 / 种植禁忌 / 储值会员),label 是冗余;十来颗标签 每颗都带名字会把身份卡撑满。要看标签名或口径 → hover / 进抽屉。 ③ 去掉按 tone 的多彩着色,统一素色,与左栏列表卡片的通用标签同一套 (rounded + slate 描边)。首屏十几颗各一种颜色 = 满屏彩条,反而看不出轻重。 新增 plain 模式挂在 PersonaTagCloud 上,画像标签卡兜底/抽屉仍是原来的带 label 彩色版 —— 那两处标签少、要的是辨识度,口径不同不强行统一。 657 tests / 42 suites 通过。本地实测张红渲染 潜在种植+1 / 紧急 / 低活跃 / 流失客 一行素色 chip。
⚠ ️ 本地库这个宿主没有治疗史/折扣锚点/权益/获客渠道的数据,这四颗的观感未实测。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
FRIDAY 推 med_emr_info 时 patient_id 被 float 化成 "676600.0"(pandas 空值提升 int64→float64), PAC 匹配不到真档 "676600" → 建无名幽灵患者(3 档吸走 4224 事务,含 2246 诊断进召回池)。 两层防御: 1. normalizeCanonical 去浮点尾巴,四条摄入通道一致;只改整数值浮点写法。 2. 回执透出 patientStubsCreated —— 持续 >0 = 患者号对不上的通用信号(id 漂移/主档漏推皆可)。 部署后 FRIDAY 再推自动纠正;存量 3 个幽灵档另行清理。
luoqi committed -
业务(2026-07-29):关键标签在完整客户名字下直接显示,可快速查看,要细看再进详情。 标签云组件(PersonaTagCloud)一直在,只是自从画像卡改成 LLM 一句话摘要后 被降级成"摘要生成失败"的兜底 —— 客服想看标签得点开抽屉。现在把**关键那几个** 提到身份卡姓名下方,复用同一套 chip(hover 仍能看「怎么算的」),不另起渲染。 关键 = PERSONA_KEY_FEATURE_KEYS(types 里定,附选取理由): 禁忌标签 / 特别关注 / 急迫等级 / 潜在治疗 / 价值分群 / 生命周期 按「能不能治 → 能不能打 → 多急 → 聊什么 → 给多大力度」排。 时间偏好、折扣锚点、治疗敏感是通话**中**才用的,基础属性姓名行已有,都不进首屏。 16 个标签一个不少,仍在画像详情抽屉全量可查 —— 取舍的是首屏不是信息。 排序沿用 personaFeatureSortKey(与抽屉同一套序),白名单只管"哪些进"。 加防漂移测试:白名单里的 key 必须还活着 —— 改 key 名不会报错, 只会静悄悄少一个 chip,而少的若是禁忌/免打扰就是事故。 657 tests / 42 suites 通过;本地实测张红渲染出 潜在治疗·急迫等级·价值分群·生命周期 四颗。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
事故(2026-07-28 19:19,测试服务器):FRIDAY 推 med_emr_info 时 patient_id 变成了 浮点字符串 "676600.0"(典型 pandas 症状:列含空值 → int64 提升 float64 → 导出带 .0)。 PAC 原样收下,患者索引查 "676600.0" 命中不了真档 "676600" → ensurePatientStub 建出无名幽灵患者。3 个幽灵档吸走 4224 条事务,其中一个带着 2246 条诊断进了召回池、 算了画像、生成「应治未治」计划排队等客服打电话 —— 而推送回执是干净的 failed=0, 宿主和我们都看不出来,直到有人在池子里看见一个没名字的患者。 两层防御: 1) 归一层去尾巴(field-mapper.normalizeCanonical) `Id` 后缀的 canonical 字段,值形如 "676600.0" / "676600.00" → 截成 "676600"。 用后缀约定而非逐资源枚举,新增 canonical 字段自动纳入。只处理**整数值**的浮点 写法,不动 "676600.5" —— 那是另一种问题,应该显形而不是被悄悄改写。 放在 normalizeCanonical 意味着 cold-import / push / pull / reparse 行为一致。 2) 回执透出 patientStubsCreated(通用信号) 空壳 >0 本身不是错(「推送顺序无要求」正是靠它兜底),但**持续 >0 说明患者号 对不上** —— id 格式漂移只是其中一种表现形式,主档漏推同样会触发。宿主据此自检, 不必等人肉在召回池里发现幽灵。同时 logger.warn 提示常见成因。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
业务约定:客户档案 / 病历 / 预约等宿主页面「打开新页,不在当前页面做跳转」。 原来四处槽位跳转全用 `_top` —— PAC 嵌在宿主 iframe 里时会把整个宿主页顶掉, 客服看完档案回不到刚才的工单(召回上下文、已填的通话结果全丢)。 四处统一收口到 action-url.ts 的两个出口,免得以后再各写各的 target: a 标签 → HOST_LINK_PROPS (target=_blank + rel=noopener noreferrer) JS 派发 → openHostUrl() 覆盖:VIEW_PATIENT(原始档案)、VIEW_MEDICAL_RECORD(原始病历)、 CREATE_APPOINTMENT(新建预约)、OPEN_POTENTIAL_TREATMENT / OPEN_RETURN_VISIT 的 URL 模式(postMessage 模式不涉及跳转,不动)。 本地实测:两个 a 标签渲染成 target="_blank" rel="noopener noreferrer"; 新建预约走 openHostUrl 打开宿主页。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
luoqi committed -
现象:上次时间已选后显示「18m_plus ×」「12_18m ×」而不是「18 个月以上 ×」「12-18 个月 ×」。 根因:消费方按 `d.key` 查维度,而 source='patient' 的三个维度(上次时间 / 上次医生 / 偏好医生) key 是空串、筛选串用的是 `id` —— 查不到维度 → 退回显示 value 本身。 加 `id` 时改了组装侧(plan.service)和面板渲染,漏了这两处消费点: - patient-picker-rail 的已选 chip:改用 personaTagDimId 查维度; 顺带把 `kv.split(':')` 改成按**首个冒号**拆(取值理论上可含冒号,如医生姓名)。 - mcp-server.factory 的 personaTags 说明:同样按 key 生成,会给 LLM 一个**空维度名**, 它照着发就永远筛不出人。改用 personaTagDimId;开集维度(医生)options 为空, 提示改成"自由文本 + 指向 GET /pac/v1/plans/doctors",免得 LLM 瞎猜姓名。 测试:persona-spec-drift 补一条 —— 闭集维度的每个取值都必须能按 **id** 查回中文, 锁住"用 id 一定查得回来"这个不变量。656 tests / 42 suites 全过。 本地实测:已选 chip 显示「0-3 个月 / 12-18 个月 / 18 个月以上」, hover 提示「上次时间:0-3 个月(点击移除)」;请求带 personaTags=last_visit_bucket:0_3m,… 返回 200。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
返工上一版:visit_recency 曾做成 persona 特征,当天被否 ——「上次到诊 2022-06-19」是 **原始事实**,而画像该是「重要价值 / 流失客 / 禁忌」这类判断。做成特征会混进患者详情的 画像标签抽屉、并喂给画像摘要 LLM。该特征一行都没写进生产就撤了。 【落在哪】patient_profiles(1:1 副表),不是 patients: profile 本来就装派生 / 摄入属性(获客渠道、转介绍统计),patients 是主数据。 派生的归派生,主表不被越搞越宽,也不用新建一张表。 【为什么必须物化】跨全池筛选没有不物化的:否则「上次到诊在 3-6 个月」要对 2,500 万行 patient_facts 做 GROUP BY … HAVING max(occurred_at),交互式扛不住。 画像筛选也一样物化了 —— 它的柱子就叫 persona_features,只是立得早、又用 JSONB 做成了 免 DDL 的扩展点,所以加维度时感觉不到成本。这三个字段不属于画像,没有现成柱子可搭。 【
⭐ 存日期不存分档 —— 零定时任务】 存 `0_3m` 这种分档,时间流逝自己就会错(今天 2 个月、下月变 3 个月)→ 得配到期重扫 (上一版为此实现了 nextBoundaryAt);存日期则**只有患者又来了才会变**,而"又来了"必然 先写到诊 fact → 推进事实水位 → 触发该患者画像重算 → 三列顺路刷新。触发时机与既有机制 天然重合,自洽。0-3 / 3-6 / … 的区间在**查询时**按 now 现算(visitRecencyRange)。 【⭐ 到诊口径 = encounter + 实际治疗 + 挂号 + 病历】 本地实测:17,637 个有事实的患者里 **8,586 个(48.7%)只有病历、没有 encounter/治疗/挂号** (宿主 appointment.in_time 缺失等)。只用 visitFactsOf 会让近一半患者「上次到诊」为空 —— 本轮实测正是如此(487 → 加病历后 2,408,近 5 倍)。医生写了病历,患者必然到过场; 详情页 lastVisit 早就是这个并集口径,这里与它对齐。⚠ ️ 顺带发现:rfm / lifecycle_stage / urgency_level 仍用窄口径 visitFactsOf, 同一批患者会因此少算就诊次数 —— 值得另开一件事核实,本次不动。 【卡片与筛选同源】两者都读这三列,不做"取不到就现算"的兜底 —— 那会让展示值与筛选口径再次分叉,正是本次返工要消灭的问题。 首次重算前为 NULL:卡片留空、筛选筛不到、医生名单为空,都是可接受的降级。 其余:PersonaTagFilterDim 加 source/patientField 分流查询;医生名单接口改查 patient_profiles; 写入挂在画像重算遍历上(顺路,失败只告警不中断 —— 它不是画像的一部分); persona-spec-drift 的 key 校验跳过 source='patient' 维度。 655 tests / 42 suites 全过;types / service / web typecheck 通过; 本地重算实测卡片显示「2022-06-19 · 张 敏」,与筛选同源。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
`<div key={dim.key}>` 在 visit_recency 出三个可筛字段(上次时间/上次医生/偏好医生)后 产生重复 key,React 报 "Encountered two children with the same key"。 改用 personaTagDimId(dim)(维度唯一标识,正是为这种情况加的),并就地注释说明。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
业务要「上次医生 / 偏好医生」筛选。医生是开集(单宿主几百个),chip 平铺放不下。
⭐ 核心取舍:**模糊只发生在前端**。 输入框在已加载的医生名单里做子串匹配,选中后发**精确姓名** → 后端仍是等值查询, 吃得到偏索引。若改成后端 ILIKE '%x%',这三条索引立刻失效、退回全分区扫 —— 就是 2026-07-29 那个「圈人 20 秒」。要真·后端模糊得先上 pg_trgm + GIN, 并确认托管 PG 允许装扩展(见迁移注释)。 附带好处:客服不用记全名、不会因打错字得到空结果还以为"真没人"。 改动: - PersonaTagFilterDim 加两个字段: · `id?` —— 筛选串 `id:value` 的维度标识。**同一特征出多个可筛字段时必填**: visit_recency 一个特征出 bucket / lastDoctor / preferredDoctor 三个,只靠 key 分不开。 不填回落 key,存量维度零影响。新增 personaTagDimId() 统一取值。 · `dynamic?` —— 取值是开集,跳过闭集校验(维度 id 仍校验,取值走参数化 equals,无注入面)。 - plan.service 的筛选组装改为**按维度 id 分组**(原按特征 key 分组,三个 visit_recency 维度会被串成一条 OR,筛出错的人)。 - 新增 GET /pac/v1/plans/doctors:本 scope 医生名单(visit_recency 的 lastDoctor ∪ preferredDoctor 去重),Redis 缓存 6h —— 医生变动以月计,不该每次开面板都跑 DISTINCT 聚合。⚠ ️ 路由声明在 @Get(':id') **之前**,否则 'doctors' 会被当成 planId。 - 前端 DoctorPicker:已选可点掉;输入为空**不铺全量名单**(几百个会把面板撑爆); 名单拉取失败则输入框仍可用,只是没候选。 - 迁移 20260729080000 补两条医生索引(与 bucket 同形态:btree(表达式, persona_id), 第二列让内层 EXISTS 走 index-only scan)。 测试:persona-spec-drift 补两条 —— · 开集维度 options 必须**空**(留样例值会让前端误以为是闭集,把没列出的医生筛没了); ·⭐ 维度 id 必须唯一(重复 id 会指向错的 dataPath,筛出错的人)。 655 tests / 42 suites 全过;types / service / web typecheck 通过; 本地 GET /plans/doctors 实测 200。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
按 2026-07-29 四点反馈做: 【1】下线的 5 个筛选项改成**隐藏**而非删除。 PersonaTagFilterDim 加 `hidden?: boolean`,面板 `.filter(d => !d.hidden)`。 这样 API / MCP 仍接受这些维度 —— 已保存的筛选链接、机器人调用不会突然失效; persona_features 照常写、偏索引照常在,复活只需删一行。 (上一版直接从字典摘掉,连 API 一起禁了,比"前端隐藏"狠。) 【2】【4】新增画像特征 visit_recency —— 一个特征喂三处: · 列表卡片的「2022-06-19 · 张敏」 · 圈人筛选的「上次时间」(0-3/3-6/6-12/12-18/18+ 月),已接面板 · 「上次医生 / 偏好医生」数据已就位,面板未接(医生是高基数,静态 options 的 chip 面板放不下,需可搜索下拉 —— 已在 persona-tag-filters 注释说明)⭐ 偏好医生用**主治医生**代替(病历里出现频次最高的医生)—— DW fact_client_out.favor_doctor_id 才是真值,PAC 尚未摄入;换成真值时消费方不用改。⭐ 到诊口径用 visitFactsOf(encounter + actual treatment + 挂号),与 rfm / lifecycle / urgency **同一口径**。我上一版卡片是现算 encounter ∪ emr —— 两套口径会算出两个"末诊", 卡片与筛选打架。本次把卡片改成读同一份画像行,fetchLastVisits 删除, 两条查询合并成 fetchCardFeatures 一条(顺带少一次往返)。⭐ lastDoctor 只认**末诊当天**那份病历的医生,跨天不认 —— 否则"日期是A次、医生是B次", 客服照着说就露馅,宁可留空。本地实测填充率:主治医生 1133/1133,上次医生 392/1133(34.6%) —— 因为末诊常落在没有当天病历的到诊上(挂号/治疗)。这是刻意的准确性取舍。⭐ 实现 nextBoundaryAt:纯时间流逝就会跨桶,不报的话水位闸永远不放行、分档停在算的那一刻。 【3】筛选字段加索引:迁移 20260729080000,visit_recency.bucket 的偏索引 (btree(表达式, persona_id),第二列让内层 EXISTS 走 index-only scan)。 这是 persona-tag-filters 顶部那条纪律的兑现 —— 加维度必须同时加索引, 漏了就是 2026-07-29 那个「圈人 20 秒」。新特征建索引时表里还没数据,故无需 CONCURRENTLY。 登记:PersonaFeatureKey / FeatureRegistry / PERSONA_FEATURE_SPECS / PERSONA_FEATURE_META 四处齐全(测试 persona-spec-drift 会卡漏登记,本次就被它卡住两次)。 654 tests / 42 suites 全过;types / service / web typecheck 通过; 本地重算实测特征写入正常。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed -
两个 P0(2026-07-29 查验 FRIDAY 测试环境推送时定位): 1. source_unit 解析器挂在 Nest 单例的实例字段上,push(并发)与 pull 互相覆写 → 建出空命名空间的重复患者主档(jvs-dw 51063 条、friday 498 条)。 2. 关系边/回访在本人未入库时静默丢弃,不计 failed 不进回执 → 宿主先推 customer_referee_circle 时 10169 条边只落 257 条(2.5%)。 部署后需让 FRIDAY 重推 customer_referee_circle 补齐关系边; 线上已有的空命名空间重复主档另行归并,不在本次范围。luoqi committed
-