---
title: postMessage 动作契约(宿主侧接收)
description: PAC 嵌在宿主 iframe 里时,「打开潜在治疗」等动作如何以 postMessage 通知宿主 —— 信封、字段、示例监听代码、安全要点。
icon: MessageSquareShare
---

本页是给**宿主开发**看的交付文档:PAC 作为 iframe 嵌在你们系统里,某些动作按钮点下去需要**你们弹自己的组件**,
PAC 通过 `window.parent.postMessage` 把患者上下文推给你们。

<Callout type="info">
**优先用 URL,不是 postMessage**。动作只要有独立页面(或能用深链参数触发弹窗),就配 URL 模板 —— 见
[接入总览 §4](/docs/integration/overview)。只有「动作是弹窗/组件、拿不到 URL」时才用本页这条通道。
</Callout>

---

## 1. 怎么开启

在宿主管理页(或让 PAC 侧配)把动作键的值配成**哨兵字符串** `postMessage`,并且**必须同时配 `HOST_ORIGIN`**:

```json
{
  "OPEN_POTENTIAL_TREATMENT": "postMessage",
  "HOST_ORIGIN": "https://host.example.com"
}
```

- `HOST_ORIGIN` = 承载 PAC iframe 的**父页 origin**,用作 `postMessage` 的 `targetOrigin`。
- **不接受 `*`** —— 载荷含患者信息,不能广播给任意父页。没配 `HOST_ORIGIN` 时 PAC 判定该动作不可用、按钮直接不渲染。
- 哨兵值大小写不敏感(`postMessage` / `POSTMESSAGE` 都认)。

---

## 2. 消息信封

```ts
{
  source: 'pac',        // 固定
  type: 'action',       // 固定
  action: 'OPEN_POTENTIAL_TREATMENT',   // 动作键,同 actionUrls 的 key
  payload: { /* 见下 */ }
}
```

**请用 `source === 'pac' && type === 'action'` 过滤** —— 同一个父页可能还嵌着别的 iframe,别按 `action` 裸判。

信封**没有** `version` 字段。兼容纪律由 PAC 保证:**只增字段、不改已有字段语义、不删字段**。
你们按需要读字段即可,多出来的忽略。

---

## 3. `OPEN_POTENTIAL_TREATMENT` 载荷

| 字段 | 类型 | 说明 |
|---|---|---|
| `patientId` | `string` | 宿主侧患者 id —— 就是你们摄入时给 PAC 的那个 id,可直接用来定位患者 |
| `pendingTreatmentDesc` | `string` | **待治疗描述**。取客服此刻在页面顶部读到的那句 AI 召回简报(「这通电话的由头」);简报还没生成好时退回结构化召回原因文本。两者都没有 → 空串(**字段一定在,不会是 undefined**) |
| `potentialTreatments` | `string[]` | **关联治疗项目**,项目名数组(不带「治疗」后缀,如 `"种植"`)。无潜在治疗 → 空数组 |
| `caseStage` | `string` | **病例阶段**,PAC 恒发 `"已咨询"` |

### 完整示例

```json
{
  "source": "pac",
  "type": "action",
  "action": "OPEN_POTENTIAL_TREATMENT",
  "payload": {
    "patientId": "1499498",
    "pendingTreatmentDesc": "多颗缺牙拖 3 个月易致邻牙移位;高价值老客,医生医嘱交代过「半年定期检查」,可约复查顺带评估种植修复。",
    "potentialTreatments": ["种植", "修复"],
    "caseStage": "已咨询"
  }
}
```

### `potentialTreatments` 取值范围(共 8 类)

**只有下面 8 个值**,不带「治疗」后缀。中文是 PAC 的措辞口径、可能随业务调整;
要**稳定键**请按下表自己映射一次。

| 取值 | 稳定 code | 含义 |
|---|---|---|
| 种植 | `implant` | 缺牙待种 |
| 正畸 | `ortho` | 成人正畸 |
| 早期矫治 | `early_ortho` | 儿童早矫(替牙期)。**注意这一项没有后缀可砍**,就叫「早期矫治」 |
| 根管 | `endo` | 牙髓 |
| 牙周 | `perio` | 牙周 |
| 充填 | `filling` | 龋齿 |
| 修复 | `restoration` | 冠桥 / 贴面 / 嵌体 |
| 拔牙 | `extraction` | 残根残冠等需拔 |

<Callout type="info">
PAC 界面上客服看到的是「种植**治疗**」「根管**治疗**」,发给你们的是去掉后缀的项目名 ——
**两处措辞不同是有意的**(那边是给人读的标签,这边是给系统落单的项目名),
且由同一张表推导,不会各自漂。
</Callout>

### `caseStage` 为什么恒为「已咨询」

PAC 侧的"潜在治疗"是从**诊断 / 医生建议**推出来的客观缺口,还没进你们的病例流程;
客服点这个按钮的语义就是"我已经跟患者聊过、请在宿主侧建单",所以阶段恒为已咨询。
PAC **不推断**「已确诊 / 已排期」这类流程内状态 —— 那是宿主的真理源,猜错比不给更糟。

---

## 4. 宿主侧监听示例

```js
window.addEventListener('message', (e) => {
  // ① 校验来源 origin —— 只信你自己嵌的那个 PAC 域名,别用 e.origin 之外的东西判
  if (e.origin !== 'https://pac.example.com') return;

  const msg = e.data;
  if (!msg || msg.source !== 'pac' || msg.type !== 'action') return;

  if (msg.action === 'OPEN_POTENTIAL_TREATMENT') {
    const { patientId, pendingTreatmentDesc, potentialTreatments, caseStage } = msg.payload;
    openPotentialTreatmentDialog({
      patientId,
      desc: pendingTreatmentDesc,
      items: potentialTreatments,   // ["种植", "修复", ...]
      stage: caseStage,             // "已咨询"
    });
  }
});
```

<Callout type="warn">
**必须校验 `e.origin`**。`message` 事件任何页面都能发,不校验 origin 等于给自己开了个后门。
PAC 侧已经用 `targetOrigin` 定向发送(不广播),但那只防"发错人",防不了"别人冒充 PAC 发给你"。
</Callout>

---

## 5. 注意事项

- **URL 模式不带这三个字段**。`pendingTreatmentDesc` 是长文本、`potentialTreatments` 是数组,
  塞 query string 会撞长度上限,还会把病情描述写进浏览器历史和你们的 access log。
  URL 模式只带 `{patientId}` `{brandId}` `{clinicId}` `{medicalRecordNumber}` 这类占位。
  要 URL 模式也拿到全量字段,得改成宿主提供 POST 接口 —— 需要就提。
- **单向、无回调**。PAC 不等你们的响应、也不接收回执。动作结果通过数据摄入回流(如新建的治疗单 → 治疗事实)
  + 客服在 PAC 记通话结果来体现。
- **iframe sandbox 别漏权限**。若你们给 iframe 加了 `sandbox`,至少要有
  `allow-scripts allow-same-origin`;**动作要开新标签页则必须加 `allow-popups`**
  (否则浏览器直接拦掉,控制台报 `Blocked opening ... 'allow-popups' permission is not set`,
  PAC 会退化成整页跳转)。想让新页不继承沙箱再加 `allow-popups-to-escape-sandbox`。
- **认领前点不到**。这些按钮在 PAC 侧过「认领闸」——工单没被客服认领时点了会被拦,
  是有意为之(没有归属人后面对不上账),不是 bug。
