# 豆包智能服务多任务管理接入指南

本文指导 Agent 使用新版业务模板链路配置、创建和更新远程任务。新能力使用 `business-templates.yaml` 中的开发者 `template_id`、`scene_key` 和运行时变量；旧 `remote_normal_v1` 只作为存量兼容路径，不用于新增任务能力。

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

## 目录

- [先确定工作范围](#先确定工作范围)
- [不可违反的约束](#不可违反的约束)
- [配置任务模板](#配置任务模板)
- [理解配置与运行时的映射](#理解配置与运行时的映射)
- [校验和同步模板](#校验和同步模板)
- [实现远程任务主链路](#实现远程任务主链路)
- [持久化和并发](#持久化和并发)
- [Sandbox 验证](#sandbox-验证)
- [正式发布边界](#正式发布边界)
- [常见错误](#常见错误)
- [交付检查](#交付检查)

## 先确定工作范围

| 用户目标 | 必须完成 |
| --- | --- |
| 只配置任务模板 | 新建或修改 `business-templates.yaml`，校验 fallback、scene key 和变量 |
| 展示长耗时任务进度 | 配置 `remote` 模板，实现创建、更新、完成和任务落地页 |
| 模拟器验证任务 | 配置模板，由 CLI/平台将 Manifest 与可选 `business-templates.yaml` 加入 Sandbox Session 创建请求，使用模拟器注入的 SessionToken 跑通创建/更新 |
| 真机调试任务 | 将 Manifest、`business-templates.yaml`、前端资源及其它应用产物一起上传为真机调试包，再验证真机任务链路 |
| 正式上线 | 同步模板、审核上线、使用正式 Token 和会话身份验证完整状态流转 |

当前 `create_remote_task` / `update_remote_task` 是远程任务链路。虽然配置 schema 接受 `task_type: local`，但不要声称 local 模板可通过这两个服务端 OpenAPI 创建；只有目标项目已有明确的端侧 local task API 和类型声明时才实现 local 分支。

开始编码前输出简短计划，至少说明：任务的业务身份、模板、状态和进度来源、创建触发点、更新频率、任务落地页、持久化方式、Sandbox 验证范围，以及当前缺失的 SessionToken 或正式权限。

## 不可违反的约束

- 标准布局把 `business-templates.yaml` 放在 dbx 项目根目录、与默认 Manifest 同目录；自定义 Manifest 或上传路径时按 [业务模板调试边界](business-template-debug.md) 显式对齐。不要把 `task_templates` 写入 `manifest.yaml`。
- 新任务使用开发者模板 `template_id`，不要为新增能力使用 `remote_normal_v1`。
- `template_id` 在通知模板和任务模板之间也必须唯一，符合当前 validator 的全局唯一规则。
- `fallback.title/sub_title` 是无变量的静态兜底；动态文案写入 `text_templates`。
- 一个 `scene_key` 在当前任务模板内只能出现一次。scene key 本身不绑定 title/sub_title/progress；运行时把它放在哪个字段，就渲染到哪个字段。
- 不允许运行时直接传 title、sub_title 或 progress_text 原文绕过模板；只传 scene key 和变量。
- 运行时传入的 scene key 必须属于该模板，变量必须满足所选文案需要，不能依赖下游模板 miss 后的空字符串兜底。
- `out_task_id` 是开发者侧任务实例的稳定幂等键，同一业务任务重试必须复用。
- 首次更新使用创建响应的 `push_token.token`；运行中更新后保存并优先使用最新 `next_push_token.token`。当前 token 是带过期时间的签名凭证，刷新有效期时旧 token 可能仍在原到期前有效，不要依赖“返回新 token 就立即撤销旧 token”。
- AccessToken、SessionToken 和 push token 只保存在需要调用平台的服务端安全边界，不进入前端、Manifest、运行态 Skill或日志。
- 任务实例、最新 token、状态、版本和更新时间必须持久化；不要只放在进程内存。
- Sandbox SessionToken 只能来自模拟器链路实际注入的请求 Header。缺失时停止创建任务；不得伪造 Header、使用固定 token、localhost 默认用户或其它身份 fallback。
- 开发态 MCP 调用传入的 SessionToken Header 是 `token=sess.<payload>.<signature>;expire_in=<seconds>` 形式的复合值。调用任务 OpenAPI 前必须拆出 `token` 字段；下游 `X-DB-SessionToken` 只传纯 token，不得原样转发复合值，也不得把 `token=`、`;expire_in=...` 或其它元数据带入下游 Header。

## 配置任务模板

标准布局在项目根目录创建或更新；自定义 Manifest 路径时遵循共享路径规则：

```yaml
schema_version: 1

task_templates:
  - template_id: milk_tea_preparing
    task_type: remote

    fallback:
      title: "奶茶制作中"
      sub_title: "请稍候"

    variable_list:
      - name
      - pickup_code
      - minutes

    text_templates:
      - scene_key: preparing_title
        text: "{name}制作中"

      - scene_key: preparing_subtitle
        text: "取餐码 {pickup_code}"

      - scene_key: preparing_progress
        text: "预计 {minutes} 分钟完成"

```

字段职责：

| 字段 | 规则 |
| --- | --- |
| `template_id` | 应用内稳定业务标识；最长 128，只含字母、数字、`_`、`-` |
| `task_type` | 当前服务端任务链路使用 `remote` |
| `fallback.title` | 必填，不允许占位符 |
| `fallback.sub_title` | 可选，不允许占位符 |
| `variable_list` | 模板允许使用的变量，值统一为 string |
| `text_templates[].scene_key` | 模板内唯一的文案选择 key |
| `text_templates[].text` | 非空，可以使用已声明变量 |

为不同展示阶段定义不同 scene key，例如 `preparing_title`、`delivering_title`、`completed_title`。不要把状态枚举编码成不透明数字，也不要给 title/subtitle/progress 分别创建可重复的同名 scene key。

只声明实际使用的变量。动态进度数值和动态进度文案是两件事：`progress_data.progress` 使用整数 `0–100`，文案中的 `{minutes}`、`{progress}` 等变量仍使用 string。

## 理解配置与运行时的映射

创建或更新时的 `task_detail` 使用以下结构：

```json
{
  "expire_in_sec": 1800,
  "display_data": {
    "title_scene_key": "preparing_title",
    "sub_title_scene_key": "preparing_subtitle"
  },
  "progress_data": {
    "progress": 30,
    "text_scene_key": "preparing_progress",
    "remaining_seconds": 180
  },
  "variables": {
    "name": "珍珠奶茶",
    "pickup_code": "A018",
    "minutes": "3"
  },
  "jump_page_data": {
    "path": "/pages/order/detail",
    "query": "order_id=20260722-001"
  }
}
```

映射规则：

- `title_scene_key` 选择标题模板；不传时使用 `fallback.title`。
- `sub_title_scene_key` 选择副标题模板；不传时使用 `fallback.sub_title`。
- `text_scene_key` 选择进度文案模板。
- `variables` 同时供本次选择的标题、副标题和进度文案渲染。
- `jump_page_data` 只能指向当前智能服务内部 Page，`path` 必须以 `/` 开头。`query` 是查询字符串，前端读取时按平台编码约定解码。
- 创建时 `expire_in_sec` 必须大于 0；更新时只有需要刷新有效期才传。

当前对外 V1 HTTP 结构仍把 `task_detail` 表示为 JSON 字符串，因此业务服务端必须先把上述对象序列化一次，再放进外层请求。不要手工拼接转义字符串。

## 校验和同步模板

先运行：

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

只同步模板时运行：

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

完整上传会自动包含项目根目录的 `business-templates.yaml`；自定义位置必须显式传 `--business-templates`。上传使用严格校验，检查 `business_template_sync_result.results` 中每个模板的同步状态；不能只检查命令退出或文件上传结果。

`manifest.yaml` 仍是 AppID 的主要来源。模板未出现在下一次 YAML 中不表示删除；下线或删除必须走平台明确的管理动作。

## 实现远程任务主链路

### 1. 获取调用凭证和用户会话

业务服务端使用 AppID/AppSecret 获取并缓存应用级 Token。创建任务还需要 `X-DB-SessionToken`；它来自平台调用开发者 MCP Tool 时携带的请求 Header。只在当前调用链和必要的安全存储中使用，不返回前端，不写日志。

开发态 MCP 调用传入的 Header 固定为复合值：

```text
token=sess.<payload>.<signature>;expire_in=<seconds>
```

其中 `token` 是任务创建接口需要的用户会话凭证，`expire_in` 是 MCP 调用侧附带的有效期元数据。调用 `create_remote_task` 前必须完成切分：

1. 去掉整个 Header 值首尾空白；
2. 按分号拆分字段，再按每个字段的第一个 `=` 拆分 key 和 value；
3. 要求存在且仅存在一个非空 `token` 字段和一个合法非负整数 `expire_in` 字段；
4. 取 `token=` 后的完整值作为纯 SessionToken；不要用无上限的 `split("=")`，避免破坏未来可能包含 `=` 的 opaque token；
5. Header 为空、缺字段、字段重复或格式非法时停止创建并返回可诊断错误；
6. 下游 `X-DB-SessionToken` 只能传拆出的纯 token。`expire_in` 不能拼入 Header；尤其不要根据开发态的 `expire_in=0` 自行推断 token 永久有效或立即失效。

TypeScript 示例：

```ts
export function extractDbSessionToken(rawValue: string | undefined): string {
  const raw = rawValue?.trim();
  if (!raw) throw new Error("missing X-DB-SessionToken");

  const fields = raw.split(";").map((field) => field.trim()).filter(Boolean);
  const params = new Map<string, string>();
  for (const field of fields) {
    const separator = field.indexOf("=");
    if (separator <= 0) throw new Error("invalid X-DB-SessionToken field");

    const key = field.slice(0, separator).trim().toLowerCase();
    const value = field.slice(separator + 1).trim();
    if (!value || params.has(key)) {
      throw new Error(`invalid X-DB-SessionToken field: ${key}`);
    }
    params.set(key, value);
  }

  const token = params.get("token");
  const expireIn = params.get("expire_in");
  if (!token || expireIn === undefined || !/^\d+$/.test(expireIn)) {
    throw new Error("invalid X-DB-SessionToken wrapper");
  }
  return token;
}
```

框架可能把重复 Header 表示成字符串数组；先按框架约定拒绝歧义值或选取唯一值，再调用切分函数。日志只记录“缺失、切分成功、格式非法”等状态，不记录输入值、拆出的 token 或可逆摘要。

如果业务动作发生在原 MCP 请求结束之后，应在平台允许的有效期和使用方式内保存任务所需身份；不要假设 SessionToken 永久有效。缺少有效用户会话时停止创建并返回可诊断错误，不伪造用户身份。

### 2. 创建任务

调用 [创建云端任务](server/openapi/task/create-remote-task.md)：

```json
{
  "out_task_id": "milk-tea-order-20260722-001",
  "task_template": "milk_tea_preparing",
  "task_detail": "{\"expire_in_sec\":1800,\"display_data\":{\"title_scene_key\":\"preparing_title\",\"sub_title_scene_key\":\"preparing_subtitle\"},\"progress_data\":{\"progress\":30,\"text_scene_key\":\"preparing_progress\",\"remaining_seconds\":180},\"variables\":{\"name\":\"珍珠奶茶\",\"pickup_code\":\"A018\",\"minutes\":\"3\"}}"
}
```

创建成功后，在同一事务边界或可恢复流程中保存：业务任务 ID、`out_task_id`、模板 ID、平台 `task_id`、当前状态、`push_token.token`、token 过期时间和版本号。

同一个用户、同一个 `out_task_id` 重试时平台会定位已有任务并更新；不得把同一 `out_task_id` 改绑到另一个模板。

### 3. 更新进度或展示

调用 [更新云端任务](server/openapi/task/update-remote-task.md)。首次传创建返回的 push token：

```json
{
  "push_token": "<latest_push_token>",
  "task_detail": "{\"expire_in_sec\":1800,\"display_data\":{\"title_scene_key\":\"preparing_title\",\"sub_title_scene_key\":\"preparing_subtitle\"},\"progress_data\":{\"progress\":70,\"text_scene_key\":\"preparing_progress\",\"remaining_seconds\":60},\"variables\":{\"name\":\"珍珠奶茶\",\"pickup_code\":\"A018\",\"minutes\":\"1\"}}"
}
```

如果变量变化会影响已渲染文案，同一次请求重新携带对应 scene key；只更新 variables 而不携带 scene key，不保证已有文案自动重渲染。

每次运行中更新成功后保存 `next_push_token`。如果本次更新修改了 `expire_in_sec`，新 token 会携带新的到期时间；旧 token 仍可能在原到期前通过签名校验。对同一任务串行更新或使用版本号，始终从持久化读取当前首选 token。

### 4. 完成任务

业务事实确认完成后，使用当前首选 push token 更新终态：

```json
{
  "push_token": "<latest_push_token>",
  "task_status": "completed"
}
```

当前实现中 completed 会把任务更新为 SUCCESS/HIDDEN，忽略同一请求中的 `task_detail`，并且响应不再返回 `next_push_token`。如果需要在隐藏前展示 100% 或最终文案，先发送一次 running 更新并保存其响应，再用返回的 token 单独发送 completed。只有真实业务任务完成后才能标记 completed；不要用前端页面关闭、MCP 调用结束或进度到 100 自动推断业务完成。

## 持久化和并发

至少持久化：

- 业务任务主键、用户和 `out_task_id`。
- 开发者模板 ID、平台任务 ID。
- 本地状态、平台原始状态和进度。
- 当前首选 push token、过期时间和更新版本。
- 最近一次请求幂等信息、结果码、脱敏 `log_id` 和更新时间。

同一任务的更新按版本串行化或使用数据库乐观锁。收到 token 失效时先检查是否有另一更新已提交不同到期时间的 token；无法确认最新状态时查询本地持久化和业务事实，不自动新建另一个 `out_task_id`。

Token 不进入普通业务日志。结构化日志记录事件名、脱敏任务标识、模板 ID、阶段、进度、耗时、结果码、错误类型和平台 `log_id`。

## Sandbox 验证

### 模拟器调试

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

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

这是 **Session 创建/重建**，不是模板同步，也不是每次刷新模拟器时自动执行。模板变化后先手动运行兼容校验，再重新创建 Sandbox Session。

Sandbox 身份由模拟器链路管理；MCP Tool Call 会携带当前用户的 Sandbox 会话凭证，服务端使用该上下文调用相同的创建和更新 OpenAPI。不要在业务代码中或通过私有接口创建 Session，也不要持久化或回显完整 secret 和 token。缺少有效 SessionToken 时停止验证，不得降级为伪造身份或直接调用底层接口后宣称端到端成功。

至少验证：

- 确认 Manifest 与 `business-templates.yaml` 已加入 Session 创建请求，并单独记录 Session 创建结果；没有逐模板结果时，将“平台模板解析/持久化”标记为 `NOT VERIFIED`。
- 创建后标题、副标题和进度文案按 scene key 与变量正确渲染。
- 同一 `out_task_id` 重试不重复创建；换模板被拒绝。
- 未声明 scene key、缺变量、额外变量、非法进度被拒绝。
- 运行中更新返回 `next_push_token`，服务端保存并用于下一次更新。
- 修改有效期后，新旧 token 的到期时间行为符合当前实现，业务始终使用持久化的首选 token。
- completed 使用最新 token 成功进入终态。
- 两个 Sandbox Session 的任务和增量游标互不泄漏。
- 服务重启后仍可从持久化数据继续更新任务。

模拟器是否提供任务展示入口，必须以当前版本实际能力和观察结果为准。没有观察到入口时，将“模拟器用户侧展示”标记为 `UNSUPPORTED` 或 `NOT VERIFIED`，不能由任务 API 成功推断为模拟器已展示。

### 真机调试

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

### 结果口径

按以下层级分别记录：

1. 模板结构校验。
2. Manifest/业务模板加入 Session 创建请求及 Session 创建结果，或真机调试包上传结果。
3. MCP 请求携带有效 SessionToken。
4. 创建返回 `task_id` 和 `push_token`。
5. 运行中更新返回 `next_push_token`，下一次更新使用最新 token。
6. completed 进入预期终态。
7. 用户侧展示：只有实际观察到时才通过。

只有已验证的层级可以写为 `PASS`。例如接口链路通过但模拟器无展示入口时，应写“远程任务 API 创建/更新/完成通过；模拟器用户侧展示 UNSUPPORTED”，不要合并成“任务闭环成功”，也不要写成正式多任务能力已上线。

## 正式发布边界

正式创建任务前确认：

1. 业务模板已严格校验、同步、审核并处于可用状态。
2. 正式 AppID/AppSecret、SessionToken、模板和目标用户属于同一环境。
3. 任务落地页已经在 `app.config.ts` 注册，跳转参数只包含必要数据。
4. 服务端具备持久化、并发保护、token 轮换和失败恢复。
5. 更新频率、过期时间和数据保留满足产品与平台限制。

不要用真实用户和生产任务做破坏性试验。正式验证使用受控测试账号和可识别的测试业务任务，并在完成后按正常业务语义收敛到终态。

## 常见错误

- 新能力仍使用 `remote_normal_v1`：改为 `business-templates.yaml` 中真实 `template_id`。
- 把 `task_detail` 外层直接传对象：当前 V1 HTTP 字段是 JSON 字符串，需要正确序列化。
- 直接传 title/sub_title/progress_text：改为 scene key + variables。
- scene key 在 title/progress 分组内分别复用：当前模板内必须全局唯一。
- 模板变量少传、多传或类型不是 string：按所选文案严格匹配。
- 进度超出 `0–100`：在业务服务端先校验。
- 忽略更新响应中的 `next_push_token`：保存为后续请求的首选 token，尤其是修改有效期时。
- 假设新 token 返回即撤销旧 token：当前凭证按签名和到期时间校验；业务并发仍应使用版本号或串行更新。
- 把配置里的 `local` 模板交给远程任务接口：当前服务端 OpenAPI 只处理 remote。
- 只看到 HTTP 200 就标记成功：检查业务 `code`、响应数据和 `log_id`。
- 将开发态 `token=sess...;expire_in=...` 原样传给任务接口：先从固定复合格式中拆出 `token` 字段，只把纯 token 放进下游 Header。
- 使用 `raw.split("=")[1]` 切 token：改为先定位 `token=` 字段，再只按第一个 `=` 取余下内容，避免截断 opaque token。
- 缺少 SessionToken 时伪造用户或直接调用底层接口：停止端到端验证并报告身份缺失；直接调用只能记录为接口诊断。
- 用模拟器 Session 创建/重建代替真机调试包：真机调试必须按当前工具链要求重新处理 Manifest、前端资源和业务模板。

## 交付检查

- `business-templates.yaml` 中只声明已实现的 remote 任务模板。
- fallback、scene key、变量和跳转 Page 均已校验。
- MCP 请求 Header 中固定格式的 SessionToken 已正确切分；创建任务的 `X-DB-SessionToken` 只包含拆出的纯 token。
- 创建响应和运行中更新响应中的 token 都持久化并有并发保护；完成响应允许没有 next token。
- 幂等创建、进度更新、完成、错误 scene/变量、旧 token 和重启恢复均有测试。
- 模拟器、真机调试与正式验证结论分开记录。
- 创建、token 轮换、完成和用户侧展示按层级记录，不互相推断。
- 正式创建前确认模板审核上线，完整上传时检查 `business_template_sync_result`。
