# 豆包智能服务服务通知接入指南

本文指导 Agent 使用新版业务模板链路配置并发送服务通知。配置事实以当前命令按 [业务模板调试边界](business-template-debug.md) 解析到的 `business-templates.yaml` 为准，运行时字段以 [发送服务通知](server/openapi/notice/push-send.md) 为准。旧 MiniAppPush 的数字模板 ID、自由渠道和旧模板协议只作为存量背景，不用于新增能力。

涉及 Sandbox Session、模拟器调试或真机调试时，先读并遵守 [业务模板调试边界](business-template-debug.md)。

## 目录

- [先确定工作范围](#先确定工作范围)
- [不可违反的约束](#不可违反的约束)
- [配置通知模板](#配置通知模板)
- [校验和同步模板](#校验和同步模板)
- [实现发送链路](#实现发送链路)
- [Sandbox 验证](#sandbox-验证)
- [正式发布边界](#正式发布边界)
- [常见错误](#常见错误)
- [交付检查](#交付检查)

## 先确定工作范围

先判断用户需要哪一层能力，只实现命中的范围：

| 用户目标 | 必须完成 |
| --- | --- |
| 只配置模板 | 新建或修改 `business-templates.yaml`，校验模板结构，不实现发送代码 |
| 模拟器验证通知 | 配置模板，由 CLI/平台将 Manifest 与可选 `business-templates.yaml` 加入 Sandbox Session 创建请求，再使用模拟器注入的身份发送测试通知 |
| 真机调试通知 | 将 Manifest、`business-templates.yaml`、前端资源及其它应用产物一起上传为真机调试包，再验证真机链路 |
| 业务事件发送通知 | 配置模板、获取应用级 Token、保存用户 OpenID、实现幂等发送和结果落库 |
| 正式上线 | 完成本地验证、同步模板、提交审核并确认模板可用后，再用正式凭据发送 |

不要因为用户提到“通知”就默认新增 MCP Tool、Widget 或 Page。服务通知由业务服务端在业务事件发生后调用平台 OpenAPI；只有用户还需要对话入口、通知落地页或通知设置页时，才同步规划 MCP、Widget/Page 和 Manifest。

开始编码前输出简短计划，至少说明：通知触发事件、接收用户身份来源、模板和变量、幂等键、落地页、验证环境，以及当前缺失的正式凭据。

## 不可违反的约束

- 标准布局把 `business-templates.yaml` 放在 dbx 项目根目录、与默认 Manifest 同目录；自定义 Manifest 或上传路径时按 [业务模板调试边界](business-template-debug.md) 显式对齐。不要把 `notice_templates` 写进 `manifest.yaml`。
- 使用开发者声明的字符串 `template_id`；不要查找、保存或传递下游数字模板 ID、SupplierID、TypeID。
- 服务通知模板由开发者在 `business-templates.yaml` 中自定义，只声明当前业务需要的标题、关键词和变量。
- `keyword_key` 是模板内稳定的字段标识，`keyword_name` 是通知卡片左侧展示名称；字段含义确定后不要随意改名。
- `name` 不允许变量；`title` 和 `field_value` 可以使用 `{variable}`。
- 所有占位符必须在 `variable_list` 声明；所有声明变量都必须实际使用。变量名以字母开头，只包含字母、数字和下划线。
- 通知模板和任务模板的 `template_id` 在当前 validator 中全局唯一，不能跨 `notice_templates` / `task_templates` 重名。
- AppSecret、AccessToken、完整 OpenID、完整通知正文和业务敏感字段只存在于服务端安全边界，不写入前端、Manifest、运行态 Skill、代码仓库或日志。
- 为一次业务通知生成稳定 `out_msg_id`；网络重试必须复用，不能每次重试生成新值。
- HTTP 200 不代表业务成功。响应 `code == 0` 且存在 `data.notice_id` 只表示平台已接受本次通知发送请求；它不自动证明实际送达或用户侧可见。保留脱敏后的 `log_id` 排障。
- Sandbox 结果只能说明测试环境链路可用，不能描述为正式通知已上线。
- Sandbox 身份只能来自模拟器链路实际注入的请求上下文。缺少可信 OpenID 或目标接口要求的身份时停止发送；不得使用固定用户、localhost 默认用户、伪造 Header 或其它 fallback。

## 配置通知模板

标准布局在项目根目录创建或更新 `business-templates.yaml`；自定义 Manifest 路径时遵循共享路径规则。文件可以同时包含服务通知和任务模板：

```yaml
schema_version: 1

notice_templates:
  - template_id: milk_tea_ready
    name: "奶茶制作完成通知"
    title: "你的{name}已经做好啦"
    brief: "{name}已制作完成，取餐码 {pickup_code}"

    variable_list:
      - name
      - pickup_code
      - store_name

    keywords:
      - keyword_key: pickup_code
        keyword_name: "取餐码"
        field_value: "{pickup_code}"

      - keyword_key: store_name
        keyword_name: "取餐门店"
        field_value: "{store_name}"
```

字段职责：

| 字段 | 规则 |
| --- | --- |
| `schema_version` | 当前固定为 `1` |
| `template_id` | 当前应用内稳定业务标识；以字母或数字开头，最长 128，只含字母、数字、`_`、`-` |
| `name` | 模板管理名称，不允许占位符 |
| `title` | 通知标题，可以引用已声明变量 |
| `brief` | 必填；通知摘要，可以引用已声明变量；不能为空或只包含空格 |
| `variable_list` | 运行时必须提供的变量集合，值统一按 string 处理 |
| `keywords` | 开发者自定义的展示字段，至少一项 |
| `keyword_key` | 模板内稳定字段 key，模板内不可重复 |
| `keyword_name` | 通知卡片左侧可见名称，不允许变量 |
| `field_value` | 通知卡片右侧值；省略时默认使用 `{keyword_key}` |

`brief` 用于通知列表或系统提醒中的摘要，应独立表达本次通知的关键信息，不要依赖用户必须展开卡片后才能理解。`brief` 中的所有占位符都必须在 `variable_list` 声明；只在 `brief` 中使用的变量也属于有效使用。

只声明用户理解通知所需的关键词。建议使用清晰稳定的英文 `keyword_key`，例如 `order_no`、`order_status`、`updated_at`，并用对应的中文 `keyword_name` 展示。不要在 `variable_list` 或运行时变量中声明 `_SupplierName`、`_SupplierIcon` 等平台变量。

如果同一事件有明显不同的用户授权语义或字段集合，使用不同 `template_id`；不要依靠大量可选变量让一个模板承载无关通知。

## 校验和同步模板

先在 dbx 项目根目录运行兼容校验：

```bash
dbx app artifacts validate ./business-templates.yaml \
  --type business-templates \
  --json
```

修复所有 error 后再继续。重点检查返回问题中的 `code` 和 `path`，不要只看错误文案。

只同步业务模板、不提交新的前端/Manifest/Skill 版本时运行：

```bash
dbx app artifacts upload \
  --project <project_root> \
  --business-templates <business_templates_path> \
  --business-templates-only \
  --json
```

`manifest.yaml` 仍是 AppID 的主要来源；如果项目没有 Manifest，显式传 `--app-id`。Manifest 存在时，`--app-id` 必须与 `manifest.app_key` 一致。

完整产物上传时，项目根目录存在 `business-templates.yaml` 就会自动校验和上传；非默认路径才传 `--business-templates`。上传阶段使用严格校验，并返回 `business_template_sync_result`。检查逐项结果，不要把“文件上传成功”当成“全部模板同步成功”。

配置中未出现某个线上模板不等于删除。需要删除或下线时使用平台明确提供的管理操作，不通过删 YAML 行推断线上删除。

## 实现发送链路

组件职责：

| 组件 | 负责 | 不负责 |
| --- | --- | --- |
| Page / Widget | 收集用户操作、展示通知设置或落地页 | 保存 AppSecret、获取应用级 Token、直接发送通知 |
| MCP Tool | 触发或查询业务动作，返回业务结果 | 暴露 AccessToken、替代业务事件幂等 |
| 业务服务端 | 保存 OpenID、判断触发条件、获取 Token、调用通知接口、幂等和审计 | 信任前端传入任意模板或接收人 |
| 开放平台 | 解析开发者模板、校验变量、发送服务通知 | 替代业务方判断是否应该通知用户 |

标准流程：

1. 从安全配置读取当前环境 AppID/AppSecret。
2. 按 [获取调用凭证 Token](server/openapi/token/get-client-token.md) 获取并按 `expires_in` 缓存应用级 Token。
3. 从可信的账号绑定记录取得目标用户 OpenID；不要接受客户端任意指定其他用户。
4. 根据业务事件选择固定的 `template_id`，构造精确变量集合。
5. 生成并持久化稳定 `out_msg_id`，同时保存事件、用户、模板和当前发送状态。
6. 调用 [发送服务通知](server/openapi/notice/push-send.md)。当前接口中的 `channel_info_list` 和 `biz_data` 都是 JSON 字符串。
7. `code == 0` 时保存 `notice_id`、成功渠道和 `log_id`；失败时保存错误分类并按策略重试。
8. 对明确的参数、权限或模板状态错误不要盲目自动重试；对短暂网络和限流错误使用有上限的退避。

请求示例：

```json
{
  "open_id": "<user_open_id>",
  "template_id": "milk_tea_ready",
  "channel_info_list": "[\"service_notice\"]",
  "biz_data": "{\"name\":\"珍珠奶茶\",\"pickup_code\":\"A018\",\"store_name\":\"中关村店\"}",
  "out_msg_id": "milk-tea-order-20260722-001-ready",
  "jump_data": "{\"path\":\"/pages/order/detail\",\"query\":\"order_id=20260722-001\"}"
}
```

`jump_data` 只传智能服务内部页面信息，其中 `path` 必须以 `/` 开头；不接受用户提供的任意外部 URL。若目标环境尚未确认跳转字段协议，先省略并验证基础发送，再按平台当前接口文档补充；不要自行发明 schema。

## Sandbox 验证

### 模拟器调试

创建模拟器使用的 Sandbox Session 时，CLI/平台将当前 Manifest 与解析到的可选 `business-templates.yaml` 加入 Session 创建请求：

```bash
cd <project_root>
dbx dev --mcp-endpoint <mcp_endpoint>
```

这是 **Session 创建/重建**，不是模板同步，也不是每次刷新模拟器时自动执行。模板变化后先手动运行兼容校验，再重新创建 Sandbox Session。Sandbox 身份和 Session 由模拟器链路管理，不要在业务代码中或通过私有接口创建 Session，也不要持久化或回显 Sandbox AppSecret。

至少验证：

- 确认 Manifest 与 `business-templates.yaml` 已加入 Session 创建请求，并单独记录 Session 创建结果；没有逐模板结果时，将“平台模板解析/持久化”标记为 `NOT VERIFIED`。
- 正确模板和完整变量请求返回 `code == 0` 和 `notice_id`，并记录为“平台接受发送请求”。
- 缺少变量、额外变量、错误模板 ID 被拒绝。
- 同一 `out_msg_id` 重试不会产生重复业务通知。
- 两个 Sandbox Session 的通知资源互不泄漏。
- 落地页存在时，通知点击只能进入当前智能服务允许的 Page。
- 服务重启后仍能根据持久化记录恢复发送状态。

模拟器是否提供服务通知收件箱或其它可见入口，必须以当前版本实际能力和观察结果为准。没有观察到入口时，将“模拟器用户侧展示”标记为 `UNSUPPORTED` 或 `NOT VERIFIED`；不得由 `notice_id` 推断通知已经在模拟器中展示。

### 真机调试

真机调试是另一条链路。生成真机调试包时，必须将 `business-templates.yaml` 与本次 Manifest、前端资源及其它应用产物一起上传打包。模拟器创建的 Sandbox Session、模板单独同步结果或模拟器中的发送结果，都不能代替真机调试包上传和真机观察结果。

Sandbox 可以使用草稿模板预览；正式发送必须使用已通过审核并处于可用状态的模板。不要用 Sandbox 身份验证真实用户通知。

### 结果口径

按以下层级分别记录，不得合并成“通知闭环成功”：

1. 模板结构校验。
2. Manifest/业务模板加入 Session 创建请求及 Session 创建结果，或真机调试包上传结果。
3. 业务事件或 MCP 调用。
4. 平台接受发送请求：`code == 0` 且存在 `notice_id`。
5. 用户侧展示：只有实际观察到时才通过。

## 正式发布边界

正式发送前确认：

1. `business-templates.yaml` 已严格校验并同步成功。
2. 目标模板已通过审核并上线；不要把 DRAFT 当作可正式发送。
3. 正式 AppID/AppSecret、OpenID 和模板均属于同一应用与环境。
4. 业务已明确通知触发条件、用户授权/关闭策略、频控和重试策略。
5. 日志只记录模板 ID、脱敏用户标识、`out_msg_id`、结果码、耗时和 `log_id`。

执行面向真实用户的发送前，确认目标环境、接收范围、模板和预期事件。不要为了验证而向真实用户批量发送。

## 常见错误

- 把 `notice_templates` 写到 `manifest.yaml`：移动到当前命令按共享路径规则解析的 `business-templates.yaml`。
- 缺少 `brief` 或只填写空格：为每个通知模板提供可独立理解的非空摘要，并声明其中使用的变量。
- `template_id` 与任务模板重名：当前校验器要求跨类型全局唯一。
- `keyword_key` 含义不清或复用后改了含义：为新的业务含义使用新的稳定 key。
- `channel_info_list` / `biz_data` 直接传数组或对象：当前 OpenAPI 要求 JSON 字符串。
- 变量多传或少传：发送变量必须与模板声明精确匹配。
- 每次重试生成新 `out_msg_id`：改为按同一业务事件持久化并复用。
- 只看到 HTTP 200 就标记平台接受：检查响应业务码和 `notice_id`；仍不得据此宣称已送达或用户侧可见。
- 缺少用户身份时使用默认 OpenID 或本地 fallback：停止发送，使用模拟器实际注入或可信账号绑定的身份。
- 用模拟器 Session 创建/重建代替真机调试包：真机调试必须按当前工具链要求重新处理 Manifest、前端资源和业务模板。
- 把 Sandbox 发送描述成正式上线：分别记录环境和验证结论。

## 交付检查

- 当前命令按共享路径规则解析到的 `business-templates.yaml` 已校验。
- `template_id`、关键词和变量均来自当前业务的真实定义。
- 服务端按环境安全读取 AppID/AppSecret，并缓存应用级 Token。
- 目标 OpenID 来自可信账号绑定，发送操作具备稳定幂等键。
- 成功、参数失败、限流/短暂失败和重复请求均有测试。
- 模拟器、真机调试与正式环境验收结论分开记录。
- 平台接受发送、实际送达和用户侧展示使用不同状态，不互相推断。
- 完整上传时检查 `business_template_sync_result`，正式发送前确认模板已上线。
