Commit 2b179c7c by luoqi

fix(embed): postMessage 载荷字段名简化成 desc / treatments / stage;早期矫治 → 早矫

字段名按对接方意见简化:
  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>
parent 9392a676
Pipeline #3485 failed in 0 seconds
......@@ -54,9 +54,9 @@ PAC 通过 `window.parent.postMessage` 把患者上下文推给你们。
| 字段 | 类型 | 说明 |
|---|---|---|
| `patientId` | `string` | 宿主侧患者 id —— 就是你们摄入时给 PAC 的那个 id,可直接用来定位患者 |
| `pendingTreatmentDesc` | `string` | **待治疗描述**。取客服此刻在页面顶部读到的那句 AI 召回简报(「这通电话的由头」);简报还没生成好时退回结构化召回原因文本。两者都没有 → 空串(**字段一定在,不会是 undefined**) |
| `potentialTreatments` | `string[]` | **关联治疗项目**,项目名数组(不带「治疗」后缀,如 `"种植"`)。无潜在治疗 → 空数组 |
| `caseStage` | `string` | **病例阶段**,PAC 恒发 `"已咨询"` |
| `desc` | `string` | **待治疗描述**。取客服此刻在页面顶部读到的那句 AI 召回简报(「这通电话的由头」);简报还没生成好时退回结构化召回原因文本。两者都没有 → 空串(**字段一定在,不会是 undefined**) |
| `treatments` | `string[]` | **关联治疗项目**,项目名数组(不带「治疗」后缀,如 `"种植"`)。无潜在治疗 → 空数组 |
| `stage` | `string` | **病例阶段**,PAC 恒发 `"已咨询"` |
### 完整示例
......@@ -67,14 +67,14 @@ PAC 通过 `window.parent.postMessage` 把患者上下文推给你们。
"action": "OPEN_POTENTIAL_TREATMENT",
"payload": {
"patientId": "1499498",
"pendingTreatmentDesc": "多颗缺牙拖 3 个月易致邻牙移位;高价值老客,医生医嘱交代过「半年定期检查」,可约复查顺带评估种植修复。",
"potentialTreatments": ["种植", "修复"],
"caseStage": "已咨询"
"desc": "多颗缺牙拖 3 个月易致邻牙移位;高价值老客,医生医嘱交代过「半年定期检查」,可约复查顺带评估种植修复。",
"treatments": ["种植", "修复"],
"stage": "已咨询"
}
}
```
### `potentialTreatments` 取值范围(共 8 类)
### `treatments` 取值范围(共 8 类)
**只有下面 8 个值**,不带「治疗」后缀。中文是 PAC 的措辞口径、可能随业务调整;
要**稳定键**请按下表自己映射一次。
......@@ -83,7 +83,7 @@ PAC 通过 `window.parent.postMessage` 把患者上下文推给你们。
|---|---|---|
| 种植 | `implant` | 缺牙待种 |
| 正畸 | `ortho` | 成人正畸 |
| 早期矫治 | `early_ortho` | 儿童早矫(替牙期)。**注意这一项没有后缀可砍**,就叫「早期矫治」 |
| 早矫 | `early_ortho` | 儿童早矫(替牙期)。界面上这一项叫「早期矫治」,项目名简称「早矫」 |
| 根管 | `endo` | 牙髓 |
| 牙周 | `perio` | 牙周 |
| 充填 | `filling` | 龋齿 |
......@@ -96,7 +96,7 @@ PAC 界面上客服看到的是「种植**治疗**」「根管**治疗**」,发
且由同一张表推导,不会各自漂。
</Callout>
### `caseStage` 为什么恒为「已咨询」
### `stage` 为什么恒为「已咨询」
PAC 侧的"潜在治疗"是从**诊断 / 医生建议**推出来的客观缺口,还没进你们的病例流程;
客服点这个按钮的语义就是"我已经跟患者聊过、请在宿主侧建单",所以阶段恒为已咨询。
......@@ -115,12 +115,12 @@ window.addEventListener('message', (e) => {
if (!msg || msg.source !== 'pac' || msg.type !== 'action') return;
if (msg.action === 'OPEN_POTENTIAL_TREATMENT') {
const { patientId, pendingTreatmentDesc, potentialTreatments, caseStage } = msg.payload;
const { patientId, desc, treatments, stage } = msg.payload;
openPotentialTreatmentDialog({
patientId,
desc: pendingTreatmentDesc,
items: potentialTreatments, // ["种植", "修复", ...]
stage: caseStage, // "已咨询"
desc, // 待治疗描述
items: treatments, // ["种植", "修复", ...]
stage, // "已咨询"
});
}
});
......@@ -135,7 +135,7 @@ PAC 侧已经用 `targetOrigin` 定向发送(不广播),但那只防"发错人",
## 5. 注意事项
- **URL 模式不带这三个字段**。`pendingTreatmentDesc` 是长文本、`potentialTreatments` 是数组,
- **URL 模式不带这三个字段**。`desc` 是长文本、`treatments` 是数组,
塞 query string 会撞长度上限,还会把病情描述写进浏览器历史和你们的 access log。
URL 模式只带 `{patientId}` `{brandId}` `{clinicId}` `{medicalRecordNumber}` 这类占位。
要 URL 模式也拿到全量字段,得改成宿主提供 POST 接口 —— 需要就提。
......
......@@ -48,16 +48,23 @@ describe('postMessage 动作契约 ↔ 交付文档', () => {
expect(missing).toEqual([]);
});
test('⭐ 项目名 = 卡片标签砍掉「治疗」后缀,且不许砍出空串', () => {
for (const [code, label] of Object.entries(POTENTIAL_TREATMENT_CARD_LABEL)) {
test('⭐ 项目名不带「治疗」后缀、不出空串', () => {
for (const code of Object.keys(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(item.endsWith('治疗')).toBe(false);
}
// 「早期矫治」本来就没有该后缀 —— 原样保留,别被改成「早矫」之类
expect(potentialTreatmentItemName('early_ortho')).toBe('早期矫治');
expect(potentialTreatmentItemName('implant')).toBe('种植');
// 早矫是例外(卡片叫「早期矫治」,砍后缀砍不出来)—— 锁住,别哪天被"统一成推导"改回去
expect(potentialTreatmentItemName('early_ortho')).toBe('早矫');
});
test('⭐ 载荷字段名就是 desc / treatments / stage —— 改名等于毁约,宿主的解构会拿到 undefined', () => {
for (const f of ['`desc`', '`treatments`', '`stage`']) expect(DOC).toContain(f);
// 旧名字不许还留在交付文档里(留着对方会照旧名写)
for (const old of ['pendingTreatmentDesc', 'potentialTreatments', 'caseStage']) {
expect(DOC).not.toContain(old);
}
});
test('文档给出了 origin 校验示例 —— 少这一句宿主就等于开后门', () => {
......
......@@ -387,11 +387,11 @@ export function PlanDetailApp({
const codes = ((persona.features.find((f) => f.key === PersonaFeatureKey.POTENTIAL_TREATMENT)
?.data ?? null) as { types?: unknown } | null)?.types;
return {
pendingTreatmentDesc: recallBrief ?? visibleReasons[0]?.reason ?? '',
potentialTreatments: Array.isArray(codes)
desc: recallBrief ?? visibleReasons[0]?.reason ?? '',
treatments: Array.isArray(codes)
? codes.filter((c): c is string => typeof c === 'string').map(potentialTreatmentItemName)
: [],
caseStage: HOST_CASE_STAGE_CONSULTED,
stage: HOST_CASE_STAGE_CONSULTED,
};
};
const openPotential = hostActionMode('OPEN_POTENTIAL_TREATMENT')
......
......@@ -38,10 +38,10 @@ export interface HostPotentialTreatmentPayload {
* 待治疗描述 —— 取页面顶部那句 AI 召回简报(「这通电话的由头」),
* 简报还没生成好时退回结构化召回原因文本。两者都拿不到 → 空串(字段仍在,别让宿主判 undefined)。
*/
pendingTreatmentDesc: string;
desc: string;
/**
* 关联治疗项目 —— **项目名**数组,共 8 类:
* 种植 / 正畸 / 早期矫治 / 根管 / 牙周 / 充填 / 修复 / 拔牙(见 potentialTreatmentItemName)。
* 种植 / 正畸 / 早 / 根管 / 牙周 / 充填 / 修复 / 拔牙(见 potentialTreatmentItemName)。
* 无潜在治疗 → 空数组。
*
* ⚠️ 不带「治疗」后缀:宿主拿它当项目名落单,「种植治疗」在那边读起来像句子、不像项目。
......@@ -50,9 +50,9 @@ export interface HostPotentialTreatmentPayload {
*
* 为什么发中文不发 code:宿主拿去直接显示/落单最省事;要稳定键按文档里的对照表映一次。
*/
potentialTreatments: string[];
treatments: string[];
/** 病例阶段 —— 恒为 {@link HOST_CASE_STAGE_CONSULTED} */
caseStage: typeof HOST_CASE_STAGE_CONSULTED;
stage: typeof HOST_CASE_STAGE_CONSULTED;
}
/** 所有动作共有的载荷字段 */
......
......@@ -107,14 +107,25 @@ export function potentialTreatmentCardLabel(code: string): string {
}
/**
* code → **项目名**(去掉「治疗」后缀):种植 / 正畸 / 早期矫治 / 根管 / 牙周 / 充填 / 修复 / 拔牙。
* 项目名的例外 —— 砍后缀砍不出来的那几个,在这里显式给。
* early_ortho 的卡片措辞是「早期矫治」(没有「治疗」后缀可砍),业务要的项目名是「早矫」。
* ⚠️ 只放**真的推不出来**的;能靠砍后缀得到的别往这里堆,否则又变成两张手维护的表。
*/
const POTENTIAL_TREATMENT_ITEM_OVERRIDE: Record<string, string> = {
early_ortho: '早矫',
};
/**
* code → **项目名**:种植 / 正畸 / 早矫 / 根管 / 牙周 / 充填 / 修复 / 拔牙。
*
* 用途只有一个:发给宿主的 postMessage 载荷(`potentialTreatments`)。宿主拿它当**项目名**落单,
* 用途只有一个:发给宿主的 postMessage 载荷(`treatments`)。宿主拿它当**项目名**落单,
* 「种植治疗」在那边读起来像句子、不像项目;界面上客服看的仍是「种植治疗」(业务 2026-07-29 定的措辞)。
*
* ⭐ 从卡片措辞**推导**而非另立一张表 —— 两张手维护的表必然漂,而这里的规则只有一条:
* 砍掉结尾的「治疗」。early_ortho 的「早期矫治」本来就没有该后缀,原样保留(它不叫「早矫治疗」)。
* ⭐ 默认从卡片措辞**推导**(砍掉结尾的「治疗」)而非另立一张全表 —— 两张手维护的表必然漂;
* 推不出来的走上面那张例外表(目前只有早矫一个)。
*/
export function potentialTreatmentItemName(code: string): string {
return potentialTreatmentCardLabel(code).replace(/治疗$/, '');
return (
POTENTIAL_TREATMENT_ITEM_OVERRIDE[code] ?? 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