Commit 9392a676 by luoqi

fix(embed): potentialTreatments 发项目名,去掉「治疗」后缀

发给宿主的是 ["种植","修复"] 而不是 ["种植治疗","修复治疗"] —— 宿主拿它当**项目名**落单,
带后缀读起来像句子、不像项目。界面上客服看到的仍是「种植治疗」(业务 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>
parent 9ba57bc9
...@@ -55,7 +55,7 @@ PAC 通过 `window.parent.postMessage` 把患者上下文推给你们。 ...@@ -55,7 +55,7 @@ PAC 通过 `window.parent.postMessage` 把患者上下文推给你们。
|---|---|---| |---|---|---|
| `patientId` | `string` | 宿主侧患者 id —— 就是你们摄入时给 PAC 的那个 id,可直接用来定位患者 | | `patientId` | `string` | 宿主侧患者 id —— 就是你们摄入时给 PAC 的那个 id,可直接用来定位患者 |
| `pendingTreatmentDesc` | `string` | **待治疗描述**。取客服此刻在页面顶部读到的那句 AI 召回简报(「这通电话的由头」);简报还没生成好时退回结构化召回原因文本。两者都没有 → 空串(**字段一定在,不会是 undefined**) | | `pendingTreatmentDesc` | `string` | **待治疗描述**。取客服此刻在页面顶部读到的那句 AI 召回简报(「这通电话的由头」);简报还没生成好时退回结构化召回原因文本。两者都没有 → 空串(**字段一定在,不会是 undefined**) |
| `potentialTreatments` | `string[]` | **关联治疗项目**,中文标签数组,与召回池卡片上那排标签同一套措辞。无潜在治疗 → 空数组 | | `potentialTreatments` | `string[]` | **关联治疗项目**,项目名数组(不带「治疗」后缀,如 `"种植"`)。无潜在治疗 → 空数组 |
| `caseStage` | `string` | **病例阶段**,PAC 恒发 `"已咨询"` | | `caseStage` | `string` | **病例阶段**,PAC 恒发 `"已咨询"` |
### 完整示例 ### 完整示例
...@@ -68,7 +68,7 @@ PAC 通过 `window.parent.postMessage` 把患者上下文推给你们。 ...@@ -68,7 +68,7 @@ PAC 通过 `window.parent.postMessage` 把患者上下文推给你们。
"payload": { "payload": {
"patientId": "1499498", "patientId": "1499498",
"pendingTreatmentDesc": "多颗缺牙拖 3 个月易致邻牙移位;高价值老客,医生医嘱交代过「半年定期检查」,可约复查顺带评估种植修复。", "pendingTreatmentDesc": "多颗缺牙拖 3 个月易致邻牙移位;高价值老客,医生医嘱交代过「半年定期检查」,可约复查顺带评估种植修复。",
"potentialTreatments": ["种植治疗", "修复治疗"], "potentialTreatments": ["种植", "修复"],
"caseStage": "已咨询" "caseStage": "已咨询"
} }
} }
...@@ -76,18 +76,25 @@ PAC 通过 `window.parent.postMessage` 把患者上下文推给你们。 ...@@ -76,18 +76,25 @@ PAC 通过 `window.parent.postMessage` 把患者上下文推给你们。
### `potentialTreatments` 取值范围(共 8 类) ### `potentialTreatments` 取值范围(共 8 类)
中文标签是 PAC 的展示口径,可能随业务措辞调整;要**稳定键**请按下表自己映射一次。 **只有下面 8 个值**,不带「治疗」后缀。中文是 PAC 的措辞口径、可能随业务调整;
要**稳定键**请按下表自己映射一次。
| 中文标签 | 稳定 code | 含义 | | 取值 | 稳定 code | 含义 |
|---|---|---| |---|---|---|
| 种植治疗 | `implant` | 缺牙待种 | | 种植 | `implant` | 缺牙待种 |
| 正畸治疗 | `ortho` | 成人正畸 | | 正畸 | `ortho` | 成人正畸 |
| 早期矫治 | `early_ortho` | 儿童早矫(替牙期) | | 早期矫治 | `early_ortho` | 儿童早矫(替牙期)。**注意这一项没有后缀可砍**,就叫「早期矫治」 |
| 根管治疗 | `endo` | 牙髓 | | 根管 | `endo` | 牙髓 |
| 牙周治疗 | `perio` | 牙周 | | 牙周 | `perio` | 牙周 |
| 充填治疗 | `filling` | 龋齿 | | 充填 | `filling` | 龋齿 |
| 修复治疗 | `restoration` | 冠桥 / 贴面 / 嵌体 | | 修复 | `restoration` | 冠桥 / 贴面 / 嵌体 |
| 拔牙治疗 | `extraction` | 残根残冠等需拔 | | 拔牙 | `extraction` | 残根残冠等需拔 |
<Callout type="info">
PAC 界面上客服看到的是「种植**治疗**」「根管**治疗**」,发给你们的是去掉后缀的项目名 ——
**两处措辞不同是有意的**(那边是给人读的标签,这边是给系统落单的项目名),
且由同一张表推导,不会各自漂。
</Callout>
### `caseStage` 为什么恒为「已咨询」 ### `caseStage` 为什么恒为「已咨询」
...@@ -112,7 +119,7 @@ window.addEventListener('message', (e) => { ...@@ -112,7 +119,7 @@ window.addEventListener('message', (e) => {
openPotentialTreatmentDialog({ openPotentialTreatmentDialog({
patientId, patientId,
desc: pendingTreatmentDesc, desc: pendingTreatmentDesc,
items: potentialTreatments, // ["种植治疗", ...] items: potentialTreatments, // ["种植", "修复", ...]
stage: caseStage, // "已咨询" stage: caseStage, // "已咨询"
}); });
} }
......
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
HOST_ACTION_MESSAGE_TYPE, HOST_ACTION_MESSAGE_TYPE,
HOST_CASE_STAGE_CONSULTED, HOST_CASE_STAGE_CONSULTED,
POTENTIAL_TREATMENT_CARD_LABEL, POTENTIAL_TREATMENT_CARD_LABEL,
potentialTreatmentItemName,
} from '@pac/types'; } from '@pac/types';
/** /**
...@@ -14,8 +15,10 @@ import { ...@@ -14,8 +15,10 @@ import {
* 收不到 / 值对不上,而且要等到联调才暴露。所以拿测试盯住三件事: * 收不到 / 值对不上,而且要等到联调才暴露。所以拿测试盯住三件事:
* ① 信封固定字段(source/type)别改字面 —— 宿主是按它过滤的 * ① 信封固定字段(source/type)别改字面 —— 宿主是按它过滤的
* ② 病例阶段恒「已咨询」—— 文档承诺了"恒为",代码里改成别的值就是骗人 * ② 病例阶段恒「已咨询」—— 文档承诺了"恒为",代码里改成别的值就是骗人
* ③ 8 类治疗项目的**中文标签和稳定 code 都要在文档里列全** —— * ③ 8 类治疗项目的**项目名和稳定 code 都要在文档里列全** ——
* 改措辞(labels.ts)或加一类,交付文档必须同步,否则宿主的映射表会缺项 * 改措辞(labels.ts)或加一类,交付文档必须同步,否则宿主的映射表会缺项。
* 注意查的是**项目名**(去「治疗」后缀)而不是卡片标签:发出去的是前者,
* 文档要跟载荷一致,不是跟界面一致
*/ */
const DOC = readFileSync( const DOC = readFileSync(
join(__dirname, '../../pac-docs/content/docs/integration/postmessage-actions.mdx'), join(__dirname, '../../pac-docs/content/docs/integration/postmessage-actions.mdx'),
...@@ -35,15 +38,28 @@ describe('postMessage 动作契约 ↔ 交付文档', () => { ...@@ -35,15 +38,28 @@ describe('postMessage 动作契约 ↔ 交付文档', () => {
expect(DOC).toContain('已咨询'); expect(DOC).toContain('已咨询');
}); });
test('⭐ 8 类治疗项目的中文标签 + 稳定 code 都要在交付文档里列全', () => { test('⭐ 8 类治疗项目的项目名 + 稳定 code 都要在交付文档里列全', () => {
const missing: string[] = []; const missing: string[] = [];
for (const [code, label] of Object.entries(POTENTIAL_TREATMENT_CARD_LABEL)) { for (const code of Object.keys(POTENTIAL_TREATMENT_CARD_LABEL)) {
if (!DOC.includes(label)) missing.push(`标签「${label}」`); const item = potentialTreatmentItemName(code);
if (!DOC.includes(`| ${item} |`)) missing.push(`项目名「${item}」`);
if (!DOC.includes(`\`${code}\``)) missing.push(`code \`${code}\``); if (!DOC.includes(`\`${code}\``)) missing.push(`code \`${code}\``);
} }
expect(missing).toEqual([]); expect(missing).toEqual([]);
}); });
test('⭐ 项目名 = 卡片标签砍掉「治疗」后缀,且不许砍出空串', () => {
for (const [code, label] of Object.entries(POTENTIAL_TREATMENT_CARD_LABEL)) {
const item = potentialTreatmentItemName(code);
expect(item.length).toBeGreaterThan(0);
expect(item.endsWith('治疗')).toBe(false); // 后缀必须真砍掉了
expect(label.startsWith(item)).toBe(true); // 只砍后缀,不换词
}
// 「早期矫治」本来就没有该后缀 —— 原样保留,别被改成「早矫」之类
expect(potentialTreatmentItemName('early_ortho')).toBe('早期矫治');
expect(potentialTreatmentItemName('implant')).toBe('种植');
});
test('文档给出了 origin 校验示例 —— 少这一句宿主就等于开后门', () => { test('文档给出了 origin 校验示例 —— 少这一句宿主就等于开后门', () => {
expect(DOC).toContain('e.origin'); expect(DOC).toContain('e.origin');
}); });
......
...@@ -46,7 +46,7 @@ import { ...@@ -46,7 +46,7 @@ import {
ABANDON_REASON_META, ABANDON_REASON_META,
personaFeatureSortKey, personaFeatureSortKey,
isKeyPersonaFeature, isKeyPersonaFeature,
potentialTreatmentCardLabel, potentialTreatmentItemName,
HOST_CASE_STAGE_CONSULTED, HOST_CASE_STAGE_CONSULTED,
type HostPotentialTreatmentPayload, type HostPotentialTreatmentPayload,
personaKeyFeatureOrder, personaKeyFeatureOrder,
...@@ -380,7 +380,8 @@ export function PlanDetailApp({ ...@@ -380,7 +380,8 @@ export function PlanDetailApp({
// 三个字段一律取**客服此刻在页上看到的东西**,不另算一套 —— 宿主建出来的单要和客服 // 三个字段一律取**客服此刻在页上看到的东西**,不另算一套 —— 宿主建出来的单要和客服
// 刚读的那句话对得上,否则对账时说不清是谁改的。 // 刚读的那句话对得上,否则对账时说不清是谁改的。
// · 描述:顶部那句 AI 召回简报;还没生成好 → 退回结构化召回原因文本(跟 UI 的回退口径一致) // · 描述:顶部那句 AI 召回简报;还没生成好 → 退回结构化召回原因文本(跟 UI 的回退口径一致)
// · 治疗项目:画像 potential_treatment 的 code → 卡片同一套中文(改措辞即时生效,见 labels.ts) // · 治疗项目:画像 potential_treatment 的 code → **项目名**(卡片措辞去掉「治疗」后缀,
// 宿主拿它当项目名落单;界面上客服看的仍是「种植治疗」。见 labels.ts)
// · 阶段:恒「已咨询」,PAC 不猜宿主流程内的状态 // · 阶段:恒「已咨询」,PAC 不猜宿主流程内的状态
const potentialTreatmentPayload = (): HostPotentialTreatmentPayload => { const potentialTreatmentPayload = (): HostPotentialTreatmentPayload => {
const codes = ((persona.features.find((f) => f.key === PersonaFeatureKey.POTENTIAL_TREATMENT) const codes = ((persona.features.find((f) => f.key === PersonaFeatureKey.POTENTIAL_TREATMENT)
...@@ -388,7 +389,7 @@ export function PlanDetailApp({ ...@@ -388,7 +389,7 @@ export function PlanDetailApp({
return { return {
pendingTreatmentDesc: recallBrief ?? visibleReasons[0]?.reason ?? '', pendingTreatmentDesc: recallBrief ?? visibleReasons[0]?.reason ?? '',
potentialTreatments: Array.isArray(codes) potentialTreatments: Array.isArray(codes)
? codes.filter((c): c is string => typeof c === 'string').map(potentialTreatmentCardLabel) ? codes.filter((c): c is string => typeof c === 'string').map(potentialTreatmentItemName)
: [], : [],
caseStage: HOST_CASE_STAGE_CONSULTED, caseStage: HOST_CASE_STAGE_CONSULTED,
}; };
......
...@@ -40,13 +40,15 @@ export interface HostPotentialTreatmentPayload { ...@@ -40,13 +40,15 @@ export interface HostPotentialTreatmentPayload {
*/ */
pendingTreatmentDesc: string; pendingTreatmentDesc: string;
/** /**
* 关联治疗项目 —— 中文标签数组,与召回池卡片上那排标签**同一套措辞** * 关联治疗项目 —— **项目名**数组,共 8 类:
* (查 POTENTIAL_TREATMENT_CARD_LABEL,共 8 类:种植治疗 / 正畸治疗 / 早期矫治 / * 种植 / 正畸 / 早期矫治 / 根管 / 牙周 / 充填 / 修复 / 拔牙(见 potentialTreatmentItemName)。
* 根管治疗 / 牙周治疗 / 充填治疗 / 修复治疗 / 拔牙治疗)。
* 无潜在治疗 → 空数组。 * 无潜在治疗 → 空数组。
* *
* 为什么发中文不发 code:宿主拿去直接显示/落单最省事;code 若要也可以加(见文档), * ⚠️ 不带「治疗」后缀:宿主拿它当项目名落单,「种植治疗」在那边读起来像句子、不像项目。
* 但目前对接方要的是"跟客服看到的一致"。 * 界面上客服看到的仍是「种植治疗」(业务 2026-07-29 定的展示措辞)—— 两处措辞不同是有意的,
* 且由同一张表推导(砍后缀),不会各自漂。
*
* 为什么发中文不发 code:宿主拿去直接显示/落单最省事;要稳定键按文档里的对照表映一次。
*/ */
potentialTreatments: string[]; potentialTreatments: string[];
/** 病例阶段 —— 恒为 {@link HOST_CASE_STAGE_CONSULTED} */ /** 病例阶段 —— 恒为 {@link HOST_CASE_STAGE_CONSULTED} */
......
...@@ -105,3 +105,16 @@ export const POTENTIAL_TREATMENT_CARD_LABEL: Record<string, string> = { ...@@ -105,3 +105,16 @@ export const POTENTIAL_TREATMENT_CARD_LABEL: Record<string, string> = {
export function potentialTreatmentCardLabel(code: string): string { export function potentialTreatmentCardLabel(code: string): string {
return POTENTIAL_TREATMENT_CARD_LABEL[code] ?? code; return POTENTIAL_TREATMENT_CARD_LABEL[code] ?? code;
} }
/**
* code → **项目名**(去掉「治疗」后缀):种植 / 正畸 / 早期矫治 / 根管 / 牙周 / 充填 / 修复 / 拔牙。
*
* 用途只有一个:发给宿主的 postMessage 载荷(`potentialTreatments`)。宿主拿它当**项目名**落单,
* 「种植治疗」在那边读起来像句子、不像项目;界面上客服看的仍是「种植治疗」(业务 2026-07-29 定的措辞)。
*
* ⭐ 从卡片措辞**推导**而非另立一张表 —— 两张手维护的表必然漂,而这里的规则只有一条:
* 砍掉结尾的「治疗」。early_ortho 的「早期矫治」本来就没有该后缀,原样保留(它不叫「早矫治疗」)。
*/
export function potentialTreatmentItemName(code: string): string {
return potentialTreatmentCardLabel(code).replace(/治疗$/, '');
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment