# 云浏览器接入参考 · 给执行 Agent

> 只有当前 Agent 能访问此文档所在服务地址，并且健康与鉴权响应正常时，才可按下面合同调用；不要把路由可达写成浏览器任务已成功。

你的目标是使用用户已有的 Agent 完成网页任务并返回真实结果。执行顺序见 [Agent 使用流程](./skills.md)。不要把整篇文档转述给用户。

## 服务地址与鉴权

- 同源服务根地址：`https://capybrowser.app.baizhi.cloud`。本地开发通常由前端代理到本机后端；远程 Agent 无法访问用户机器的 `127.0.0.1`。
- 无鉴权、无计费的版本查询：`GET https://capybrowser.app.baizhi.cloud/version`。`data.version` 是当前部署的服务构建版本，不要将它与 `data.api_contract_version` 这个独立的 HTTP 接口合同版本混用。正式 tag 的 `v` 前缀不属于服务构建版本；MCP `initialize` 的 `serverInfo.version` 与服务构建版本相同。变更摘要见 [changelog](./changelog.md)。
- 外部 Agent 管理 API：`/openapi/v1`。
- MCP 接入地址：`https://capybrowser.app.baizhi.cloud/mcp`（Streamable HTTP）。MCP 与浏览器 OpenAPI 共用身份、归属权限、运行状态和计费。将 `Authorization: Bearer <API_KEY>` 配置在 MCP 客户端的 HTTP Header 中。
- 鉴权：平台确认网关已为本应用的 OpenAPI/MCP 启用个人中心 Key 鉴权时，可使用平台 API Key；应用内创建的云浏览器 API Key 是兼容备用。两者都使用 `Authorization: Bearer <API_KEY>`，只放在 Agent 的安全凭据配置中，不写入 Prompt、代码仓库、日志或结果。
- 网关恢复出已认证的用户（以及可选团队）时，以该身份为准；没有网关用户身份时，服务才校验应用内 Key。只有团队头、没有用户身份时不能鉴权。用户和 Agent 都不要手动设置 `X-Baizhiyun-*` 身份头。
- 无副作用验证：`GET /openapi/v1/me`。成功返回只证明 Key 可用，不代表浏览器任务已成功。
- 控制台 `/api/v1` 使用百智云网关身份，不接受 API Key 代替网页登录身份；不要伪造网关 Header 作为正式接入方式。

示例变量只表示配置位置，不包含真实凭据：

```sh
export CAPYBROWSER_BASE_URL="https://capybrowser.app.baizhi.cloud"
export CAPYBROWSER_API_KEY="从控制台创建后安全保存的密钥"
```

Agent 可先调用 `browser_auth_check` 验证 Key，再调用只读的 `browser_pricing` 获取价格。随后可用 MCP 工具代替下文 `/openapi/v1/browser/*` 的 HTTP 请求：`browser_create_session`、`browser_get_session`、`browser_list_sessions`、`browser_stop_session`、`browser_control`、`browser_create_profile`、`browser_get_profile`、`browser_list_profiles`、`browser_delete_profile`、`browser_begin_handoff`、`browser_reissue_handoff`、`browser_cancel_handoff`、`browser_revoke_handoff`、`browser_list_downloads`、`browser_download_content`、`browser_usage`、`browser_usage_summary`。两种入口调用相同的业务服务，可按 Agent 的 MCP/HTTP 能力选择，**不要为同一任务各创建一次运行**。

常用管理能力对照如下。HTTP 的列表参数统一使用整数 `page`、`page_size`；Session 列表可传 `status`、`query`，Profile 列表也可传 `status`、`query`。MCP 的分页参数同样必须传 JSON 整数。HTTP 与 MCP 都按当前凭据恢复出的用户和团队范围查询。

| 能力 | HTTP | MCP |
| --- | --- | --- |
| 版本（不鉴权、不计费） | `GET /version` | `initialize.serverInfo.version` |
| 验证身份 | `GET /openapi/v1/me` | `browser_auth_check` |
| 查询价格（只读、不计费） | `GET /openapi/v1/pricing` | `browser_pricing` |
| 创建 / 查询 / 列出 / 结束运行 | `POST /browser/sessions`；`GET /browser/sessions/:id`；`GET /browser/sessions`；`DELETE /browser/sessions/:id` | `browser_create_session`；`browser_get_session`；`browser_list_sessions`；`browser_stop_session` |
| 创建 / 查询 / 列出 / 删除 Profile | `POST /browser/profiles`；`GET /browser/profiles/:id`；`GET /browser/profiles`；`DELETE /browser/profiles/:id` | `browser_create_profile`；`browser_get_profile`；`browser_list_profiles`；`browser_delete_profile` |
| CDP 控制 | `POST /browser/sessions/:id/control` | `browser_control` |
| 人工处理 | `POST /browser/sessions/:id/handoff`；`POST /browser/sessions/:id/handoff/reissue`；`DELETE /browser/sessions/:id/handoff`；`POST /browser/sessions/:id/handoff/revoke` | `browser_begin_handoff`；`browser_reissue_handoff`；`browser_cancel_handoff`；`browser_revoke_handoff` |
| 下载 | `GET /browser/sessions/:id/downloads`；`GET /browser/sessions/:id/downloads/:download_id/content` | `browser_list_downloads`；`browser_download_content` |
| 用量 | `GET /browser/usage?page=1&page_size=20`；`GET /browser/usage/summary?since=<RFC3339>&until=<RFC3339>` | `browser_usage`；`browser_usage_summary` |

表中 `/browser/*` 均相对 `/openapi/v1`。当前没有 Profile clone/export 合同。

Session 创建参数的 [OpenAPI schema](./browser-session-openapi.yaml) 与下面的 HTTP 示例使用相同的公开时长上限。

支持远程 Streamable HTTP 的 MCP 客户端可按其配置格式填写：服务 URL 为 `https://capybrowser.app.baizhi.cloud/mcp`，HTTP Header 为 `Authorization: Bearer <CAPYBROWSER_API_KEY>`。把 Key 放在客户端安全凭据中，切勿把完整配置连同真实 Key 贴进对话。连通性先用 `browser_auth_check` 验证；该检查不创建运行，也不收费。

`browser_create_session` 的 `idempotency_key` 对应 HTTP `Idempotency-Key`；`browser_control` 返回同样的一次性 `cdp_url`。MCP 负责运行管理，网页导航、点击、输入和截图仍通过标准 CDP 客户端完成，并非仅安装 MCP 后就自动有页面操作工具。费用、人工处理和结束状态沿用下文合同。

HTTP 与 MCP 的成功形状不同：普通 HTTP 成功响应为 `{ "code": "ok", "message": "ok", "data": ... }`，下载内容接口除外，它直接返回二进制。MCP 的 `tools/call` 即使传输层是 HTTP 200，也必须继续检查 JSON-RPC `result.isError`；普通工具把业务 JSON 编码在 `result.content[]` 的文本块中，客户端应解析 `content[].text`。工具失败时 `isError: true`，文本 JSON 使用 `code`、`message`、`retryable`。`browser_download_content` 则返回说明文本和一个 MCP resource；资源的 `blob` 是 base64，`mimeType` 为 `application/octet-stream`，保存后仍须核对说明中的字节数和 SHA-256。不能把 HTTP 200、MCP 建连成功或存在 `content` 块单独当作工具成功。

## Temporary Session：默认首次路线

没有跨次状态需求时不要先建 Profile。`max_seconds` 可随任务设置；省略时为 900 秒，新运行允许范围为 1–7000 秒。公开 `expires_at` 到达时停止网页操作并开始结束运行；服务随后尝试保存档案并释放资源，结果以原运行的最终状态为准，不增加到期后的运行费用。为每个逻辑创建请求生成稳定、唯一的 `Idempotency-Key`；同一请求结果不确定时复用它，不能换键重复创建。

已有的 7001–7200 秒运行若需重试创建结果，沿用原 `Idempotency-Key` 和完全相同的请求正文直接调用 HTTP；服务端只识别已有运行，不允许新建该时长。新版 MCP/OpenAPI schema 对新运行声明上限 7000 秒，按 schema 预校验的客户端可能拦下这类历史重试。

```http
POST /openapi/v1/browser/sessions
Authorization: Bearer <CAPYBROWSER_API_KEY>
Idempotency-Key: <8-128 characters>
Content-Type: application/json

{"max_seconds":900}
```

服务返回 `202` 和 Session，标识字段为 `session_id`。`GET /openapi/v1/browser/sessions?page=1&page_size=20` 可列出自己的运行。轮询 `GET /openapi/v1/browser/sessions/:id`，直到：

- `ready`：浏览器已就绪，`billing.charge_start_at` 是当前本地计量起点；
- `released`：浏览器已确认关闭，且不再继续计费；
- `failed`：浏览器未能启动，读取 `error` 中的安全原因后停止盲目重试；
- `starting`、`stopping`、`ending_review_required`：仍是中间态，不能当成成功或已释放。

列表的 `status` 筛选只使用这些公开状态词；不要传内部状态名。

服务会请求桌面浏览器窗口，但这不是对网页 `innerWidth × innerHeight` 的精确承诺。历史旧 Profile 环境曾实测页面尺寸为 1440×844；这只是该次观测，不代表当前部署的固定尺寸。需要依赖布局尺寸时，应在页面就绪后测量：`screen` 是系统显示区域，`outer`/CDP window bounds 是浏览器窗口，`inner`/`visualViewport` 才是页面布局视口，不要把它们当成同一个尺寸。

Agent 可按任务使用 `Browser.setWindowBounds` 改真实窗口，或用 `Emulation.setDeviceMetricsOverride` 临时模拟页面尺寸。`Emulation.clearDeviceMetricsOverride` 恢复的是**当前真实窗口**对应的页面视口，不会抹掉用户主动进行的窗口 resize，也不保证回到某个固定数字。后续新 Session 会重新走启动窗口规则。`configured_environment` 只表示完整的版本化环境模板；旧 Profile 的该字段为 null/缺省，不等于没有应用启动窗口，也不表示语言、时区、字体或登录内容已被迁移或清空。

`409 idempotency_conflict` 表示同一个幂等键被用于不同请求；`409 profile_busy` 表示档案正被另一运行占用；`429 capacity_exceeded` 表示当前并发已满。

## 常见返回与下一步

| 返回                        | 下一步                                                                                    |
| --------------------------- | ----------------------------------------------------------------------------------------- |
| `idempotency_conflict`      | 同一 Idempotency-Key 只能配同一请求正文；结果不确定时用原键和原正文重试，以取回同一运行。 |
| `profile_busy`              | 等待当前运行结束，不并发使用或删除该档案。                                                |
| `profile_recovery_required` | 该档案当前不可恢复；先核对相关 Session，不自动删除旧 Profile。需要继续时由用户决定保留旧档案另建 Profile，或确认后删除重建。 |
| `human_control_active`      | 属于本人处理期间的预期状态；等待交还或处理链接过期后再申请控制。                          |
| `session_not_ready`         | 先查询状态；starting 才等待，终态则停止申请控制。                                         |
| `capacity_exceeded`         | 停止创建；确认容量释放后最多再试一次，仍可能失败，不能承诺重试成功。                      |
| `profile_deletion_pending`  | 删除仍在处理中，使用同一删除请求稍后重试一次。                                            |
| `insufficient_balance`      | 积分不足，补充积分后再创建运行。                                                          |
| `billing_pending`           | 积分预留仍在处理中；复用原幂等键与原正文，不重复创建。                                    |
| `runtime_unavailable`       | 查询原运行状态；原因不明确时保留运行 ID 供排障，不盲目重试网页写操作。                    |
| `download_not_ready`        | 下载仍在生成；仅在当前运行仍有效时做有界重试。                                            |
| `download_gone`             | 当前运行已结束，文件不再可取；不要新建运行去猜测或恢复旧文件。                            |
| `download_limit_exceeded`   | 文件或本次运行的下载数量、总量超过限制；缩小文件或减少下载。                              |

不要仅凭 HTTP 状态或 Go/nginx 的默认文本猜合同：

- 未带有效 Bearer 身份访问 `/openapi/v1` 或 `/mcp` 时是 `401`；这只能说明本次 API 身份校验未通过，不能据此判断某个 Profile 是否存在。
- 已认证后，Session/Profile/下载不存在与“存在但当前用户/团队不可见”统一为 JSON `404`，用于避免资源枚举。不要把它解释成其他人的资源一定不存在。
- 未知路径（未注册路由）可能由边缘、nginx 或应用返回 `404`，响应类型不一定是产品 JSON；先核对 HTTP 方法、完整路径、服务版本与 OpenAPI，不解析默认错误正文。
- 一次性 `cdp_url` 已过首次使用期限、已被使用或已被新租约替换时，连接层可能返回 `401`/`404` 或直接握手失败；查询原 Session，若仍为 `ready` 且未到期，为同一 Session 重新申请控制。不要因此创建新 Session。

## 取得 CDP 并操作网页

Session 为 `ready` 后调用：

```http
POST /openapi/v1/browser/sessions/:id/control
Authorization: Bearer <CAPYBROWSER_API_KEY>
Content-Type: application/json

{}
```

响应中的 `cdp_url` 是短期、单 Session 控制能力，不是普通详情链接；不要记录、转发或长期保存。响应的 `expires_at` 是这个链接最晚可以**首次建立连接**的时间，固定为签发后 5 分钟且不会晚于 Session 自身的 `expires_at`。它不是已建立连接的自动断开时间：在期限内成功连接后，可以持续使用到客户端断开、Session 公开到期或停止、人工接管或新控制租约替换、创建该 Session 的 Key 或控制 Key 被撤销。

同一个控制链接只能建立一条连接。链接未使用即过期，或已连接后异常断开时，为原 Session 再次调用 `POST /control` 并重连；不要询问用户，不要新建 Session，也不会重复收取启动费。若断开时某个网页写操作结果不明确，重连后先观察实际页面状态，再决定下一步，不能自动重放该写操作。

页面操作可使用当前环境已有且兼容 CDP 的 Playwright、Puppeteer，或选择 Patchright 客户端，通过 Capy 返回的 CDP 地址连接。先用 `Browser.getVersion` 或等价方式确认连接，再按真实页面状态操作；网站会按自身规则决定是否允许访问，客户端连接成功不等于目标网站允许访问，也不等于业务任务完成。

### Patchright 可选客户端

[Patchright 连接示例](./patchright-client.md) 使用已验证的 `patchright@1.63.0` 作为一个明确版本示例，不代表任意 Patchright 版本都兼容。连接必须使用 `chromium.connectOverCDP(cdp_url)`，取得默认 browser context 并复用已有页面；需要额外页面时可在该 context 中创建，不要新建 context 来改变 Session 边界。

Capy 负责启动和管理浏览器，因此不要运行 `patchright install`，也不要调用 `chromium.launch()`。客户端断开不会停止 Session 或停止计费；若连接中断而 Session 仍为 `ready`，为同一 Session 调用新的 `POST /control`，再从已有页面中找回同一个 Target。不要复用旧 `cdp_url`、创建第二个 Session 或自动重放结果不明的写操作。

Patchright 1.63.0 与 Playwright 的执行上下文和 console 事件语义并不完全相同：该版本的 `evaluate` 默认使用隔离世界，需要读取页面主世界变量时要显式选择主世界；依赖 `page.on("console")` 的逻辑也应单独验证。不要把这两类差异当作网页业务结果。更完整的版本锁定和兼容说明见上面的公开示例。

首次试用可选择无需登录、无副作用的公开页面，读取真实标题或条目；网络或页面不可达时如实返回阻塞，不伪造结果。

## 下载文件：使用 Session 制品接口

远端浏览器和 Agent 不共享文件系统。不要对远端 CDP 的 Playwright `Download` 使用 `path()`、`saveAs()` 或 `createReadStream()`；浏览器的 `download` 事件和 `failure() == null` 也不能证明 Agent 已取得字节。点击下载后轮询当前 Session 的制品列表：

```http
GET /openapi/v1/browser/sessions/:session_id/downloads
Authorization: Bearer <CAPYBROWSER_API_KEY>
```

最小响应形状如下；下载条目只使用 `id`、`filename`、`state`、`size_bytes`、`sha256`、`error_code`：

```json
{
  "code": "ok",
  "message": "ok",
  "data": {
    "items": [
      {
        "id": "dl_0123456789abcdef",
        "filename": "report.csv",
        "state": "ready",
        "size_bytes": 1234,
        "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      },
      {
        "id": "dl_fedcba9876543210",
        "filename": "unfinished.pdf",
        "state": "failed",
        "error_code": "download_unavailable"
      }
    ],
    "pagination": {"page": 1, "page_size": 2, "total": 2, "has_more": false}
  }
}
```

条目为 `receiving` 时继续有界等待；`failed` 时依据 `error_code` 报告失败：`download_limit_exceeded` 表示超限，`download_canceled` 表示下载被取消，`download_unavailable` 表示文件未能交付。只有 `item.state === "ready"` 才能读取内容。用该条目的 `item.id` 填充内容 URL 中的 `:download_id`；不要读取不存在的 `status` 或 `download_id` 条目字段：

```http
GET /openapi/v1/browser/sessions/:session_id/downloads/:download_id/content
Authorization: Bearer <CAPYBROWSER_API_KEY>
```

HTTP Header 名称大小写不敏感。通过所用客户端库的大小写不敏感 Header 接口读取 `Content-Length` 与 `X-Content-SHA256`，不要对原始 Header 字符串做精确大小写匹配。保存完毕后核对实际字节数及 SHA-256；预期非空却得到 0 字节、长度或摘要不符、响应中途断开，均不能报告成功。响应使用 `Content-Disposition: attachment`、`Cache-Control: no-store` 和 `X-Content-Type-Options: nosniff`。

制品只在原 Session 处于 `starting` 或 `ready` 时可取；停止或超时后返回 `410 download_gone` 并清理。不存在及其他 owner 的 Session/制品统一返回 404，未完成内容返回 `409 download_not_ready`。默认限制为单文件 16 MiB、单 Session 64 MiB、最多 8 次下载、单次传输 60 秒。不要猜测内部路径、把文件编码进日志，或把 Profile 当作下载历史。

## Profile：仅在需要复用状态时

```http
POST /openapi/v1/browser/profiles
Authorization: Bearer <CAPYBROWSER_API_KEY>
Content-Type: application/json

{"name":"办公账号"}
```

创建 Profile 只写档案元数据，不启动浏览器。需要使用时，在创建 Session 的正文中加入 `profile_id`。同一 Profile 同时只允许一个活跃 Session。用户主动结束或运行到达公开 `expires_at` 后，服务会尝试受控关闭浏览器并保存本次状态；只有确认退出且快照提交成功，才会产生新版本。结束请求已受理或公开期限已到时，不再继续网页操作；继续查询原 Session，直到保存结果和资源释放明确。保存失败时，有可信旧快照则保留旧版本；没有旧快照时 Profile 为 `recovery_required`，需重新准备登录状态。

HTTP 可分页列出自己有权访问的档案，并按 ID 查询单项；这两项都只读取元数据，不启动或计费：

```http
GET /openapi/v1/browser/profiles?page=1&page_size=20&status=ready&query=办公
Authorization: Bearer <CAPYBROWSER_API_KEY>

GET /openapi/v1/browser/profiles/:id
Authorization: Bearer <CAPYBROWSER_API_KEY>
```

`status` 可选 `ready`、`in_use`、`recovery_required`、`deleting`；`query` 匹配名称或 ID。对应 MCP 工具为 `browser_list_profiles` 和 `browser_get_profile`。

### 如何确认一次保存

1. 查询**原 Session**，以它的最终 `save_status` 为第一事实：`saved` 才表示这次保存已提交；`failed` 表示保存失败；仍是中间状态则继续有界查询，不能先报成功。
2. `saved` 后查询 Profile，核对 `current_version` 与 `last_saved_session_id`。后续 Session 也可成功保存并推进 latest，所以晚些时候 `last_saved_session_id` 指向新 Session，不会推翻较早 Session 自己已经是 `saved` 的事实。
3. `snapshot_sha256` 只标识当前提交的快照内容，可用于完整性关联；它不要求每次保存都变化，也不能证明目标网站仍保持登录。恢复后应打开目标业务页实际确认是否需要重新认证。

要分别报告三类结果：保存失败（Profile 快照没有提交）、恢复失败（新 Session 没能加载可信快照）、网站要求重新认证（快照可能已正常保存和恢复，但站点会话失效）。公开 `profile_save_failed` 不定位到底层某个阶段，不应把视口覆盖、网站后台缓存、IndexedDB 或登录本身猜成根因。

视口覆盖本身不代表状态无法保存，也不要求保存前清除覆盖。保存失败时，核对原 Session 的最终 `save_status` 和安全 `error`，保留 `session_id` 供排障；不能把请求已受理当作保存成功，也不能仅凭覆盖操作推断失败原因。

### 当前公开限制

| 项目 | 当前合同或实现边界 |
| --- | --- |
| 新运行时长 | 1–7000 秒，默认 900 秒；`expires_at` 是操作截止。 |
| CDP 控制链接 | 最晚首次使用时间为 5 分钟，并受 Session 到期时间限制；一次使用、一个连接。已连接后的存活期不是这 5 分钟。 |
| 人工处理 | 待首次进入链接默认 5 分钟；当前部署未配置更短的独立活动人工期限，进入后最多到 Session 自身到期。若以后配置活动期限，也不会越过 Session 到期时间。 |
| 下载 | 单文件 16 MiB、单 Session 64 MiB、最多 8 个、单次传输 60 秒；只在原 Session 为 `starting`/`ready` 时可取。 |
| Profile 压缩档案 | 当前实现拒绝大于 256 MiB 的 tar.gz 输入/保存内容；空档案也拒绝。 |
| Profile 解包内容 | 读取 tar.gz 时，归档内常规条目展开总量上限 256 MiB，单条目也不能超过 256 MiB；只接受受控目录、常规文件。 |
| 合成启动包 | 浏览器连接程序、字体等基础运行文件与 Profile 合成后的 tar.gz 也必须不超过 256 MiB；基础文件会占用该合成预算，因此 256 MiB 不是用户 Profile 的可用空间承诺。导出的完整运行包在提取 Profile 前同样受压缩输入与展开总量守卫。 |
| 加密存储输入 | 保存前的 Profile tar.gz 输入上限 256 MiB；该阈值不能换算成单 Profile、单用户或长期可用空间，也不表示容量无上限。 |
| 收尾预算 | 后端资源清理预算默认 60 秒、可配置上限 120 秒；它约束浏览器退出、导出等资源阶段，不是对本地落盘、数据库提交或所有外部系统的统一绝对时限。到期后收尾不增加运行费用，结果仍以 Session/Profile 终态为准。 |

这些值是当前服务边界，不是存储权益。接近边界时应缩小 Profile/下载内容并留出基础运行文件余量；不要用单个 `256 MiB` 阈值推导完整可用容量。

不再需要保存状态时可永久删除：

```http
DELETE /openapi/v1/browser/profiles/:id
Authorization: Bearer <CAPYBROWSER_API_KEY>
```

删除会清除该 Profile 的全部加密登录状态和历史版本，无法恢复，但不会删除历史运行与费用记录。`200` 表示状态对象和元数据均已确认删除；活跃 Session 占用时返回 `409 profile_busy`。不存在或不属于当前用户/团队均返回 `404 profile not found`，避免泄露其他租户资源。状态对象清理失败返回 `503 profile_deletion_pending`，Profile 会保持“删除处理中”并禁止新 Session 使用，可用同一请求重试；若首次成功响应丢失，重试可能返回 `404`，表示资源已经不存在。

## 必要的人工处理

Agent 发现必须本人登录或授权时，针对同一 Session 调用：

```http
POST /openapi/v1/browser/sessions/:id/handoff
Authorization: Bearer <CAPYBROWSER_API_KEY>
Content-Type: application/json

{}
```

把服务返回的完整 `human_action.url` 作为“人工处理”链接交给用户。`POST /handoff` 返回的 `human_action.expires_at` 表示链接最晚首次进入时间；首次进入成功后，Session 查询中的同名字段表示人工操作截止时间。原首次进入期限不会中断已开始的操作；人工操作截止时间由服务配置决定，且不晚于 Session 自身的 `expires_at`。链接只在 `POST /handoff` 时签发，Agent 应在当前任务中安全保留；查询 Session 只返回到期时间与 `url:null`，不重发能力令牌。没有有效人工处理需求时，`human_action` 为 null。人工控制会撤销当前 Agent 控制；关闭页面不等于交还，用户必须点击“我已完成，交还 Agent”。Agent 控制连接断开后为原 Session 重新申请控制；等待进入或人工操作到期时只撤销人工控制并把控制权交回 Agent，不结束 Session，仍需人工处理时重新生成链接。只有运行自身达到 `expires_at` 才进入停止流程。Agent 在人工处理期间申请控制返回 `409 human_control_active` 属预期。随后重新申请控制、重新观察页面，再继续任务。不要自行拼接人工链接，也不要让用户去运行列表里猜是哪一次运行。

链接尚未过期却打不开时，先查询原 Session。若为 `human_action_pending`，获授权的 Agent 可对原 Session 调用 `POST /openapi/v1/browser/sessions/:id/handoff/reissue` 换发链接，或调用 `DELETE /openapi/v1/browser/sessions/:id/handoff` 取消等待；换发响应包含新的 `human_action.url` 和期限，旧票立即失效。与用户打开旧链接的竞争由服务端原子判定；收到 `handoff_state_changed` 就重新查询，不盲目重试。人工已开始时 Agent 不能换发或取消。

仅当用户在原 Agent 对话中明确要求撤销时，Agent 才可调用 `POST /openapi/v1/browser/sessions/:id/handoff/revoke`。若使用应用内 API Key，必须仍使用发起当前人工处理时绑定且未停用、未重置的那一把 Key；同一用户的其他应用 Key 不可代替。若请求由平台可信网关认证，则按当前已认证的平台身份处理，不要求或允许客户端自行填写网关身份头。只读的平台 Key/身份验证成功只说明该请求已认证，并不单独证明资源归属或人工处理生命周期操作一定获准。用户也可在登录控制台点击“撤销人工操作”。撤销后旧窗口立即失效，Agent 应在同一 Session 重新申请控制、重新观察页面，不重放结果不明的写操作。换发、取消、撤销和正常交还都不创建新 Session，也不重复收启动费。若新票仍打不开，应定位页面或网关故障，而不是无限换发。

`GET /openapi/v1/browser/sessions/:id` 的 `human_action_status` 独立于 `control_state`：`waiting` 表示等待进入，`in_progress` 表示正在人工操作；结束后保留最近一次结果 `completed`（正常交还）、`cancelled`（取消等待）、`expired`（过期）或 `revoked`（明确撤销）；`none` 表示没有可判定的结果，不应猜作已交还。新的 `POST /handoff` 会重置上次结果。此字段不提供处理链接，也不表示浏览器 Session 已结束。

## 结束、释放与用量

先确定谁负责该 Session 的生命周期：Agent 自己创建的 Session，无论成功或失败，都应在 `finally` 中先取回需要的下载，再请求结束并有界查询到明确状态。若只是连接用户提供、由其他流程拥有的 Session，不得在断开时自动终止；只返回控制并报告当前状态，除非用户明确把结束责任交给当前 Agent。下载接口在 Session 结束后不可用，因此不要先停止再下载。

正常和失败路径都要请求结束：

```http
DELETE /openapi/v1/browser/sessions/:id
Authorization: Bearer <CAPYBROWSER_API_KEY>
```

`202` 表示已受理结束，首次受理会冻结 `billing.charge_stop_at`，重复 DELETE 不移动该时间。证据仍不完整时，DELETE 继续返回 `202`、`ending_review_required` 和可重试的安全 `error`，不会宣称浏览器已经关闭。继续查询同一 Session：`released` 保证浏览器已经关闭且不再继续计费，`ended_at` 是结束时间；`ending_review_required` 表示关闭结果仍待核对。费用最终结算独立看 `billing.status`。断开 CDP 不等于结束或释放。

`GET /openapi/v1/browser/usage?page=1&page_size=20` 返回已结束的运行记录，包含 `session_id`、`duration_ms`、`ended_at`、`ended_reason` 和 `billing`。只按 `billing.status` 报告费用：`settled` 时用 `billing.actual_charge_centipoints` 换算实际费用，不能用估算代替；`waived` 为未扣费，`reserved` 为积分已预留，`preparing` 为预留准备中，`settling` 为结算中，`review_required` 为待核对。仅运行中且尚未结算时可按 `billing.estimated_charge_centipoints` 报告预计费用。金额为精确整数，100 centipoints = 1 积分；未知金额为 null，零为确定的零。

## 首发体验价与结算边界

- 先调用 `GET /openapi/v1/pricing`（或 MCP `browser_pricing`）读取结构化价格、计时边界和舍入规则；该请求不创建 Session、不启动浏览器、不收费。
- 首发体验价：成功启动 1 积分／次，运行 1 积分／分钟，按秒计费。
- 浏览器就绪后开始计费，启动失败不收费。运行时长按毫秒计量，最终金额四舍五入到 1 centipoint（0.01 积分）。等待 Agent、人工处理或断开连接期间仍会计费；结束请求首次被受理或运行到达 `expires_at` 时停止计费，之后的资源清理不计费。平台运行故障、平台关停或平台恢复事故导致的结束整次免收。
- 只按 `billing.status` 报告费用：`settled` 时用 `billing.actual_charge_centipoints` 换算实际费用，不能用估算代替；`waived` 为未扣费，`reserved` 为积分已预留，`preparing` 为预留准备中，`settling` 为结算中，`review_required` 为待核对。仅运行中且尚未结算时可按 `billing.estimated_charge_centipoints` 报告预计费用。金额为精确整数，100 centipoints = 1 积分；未知金额为 null，零为确定的零。响应不确定时复用原 Session 的幂等键查询或重试，不换键创建第二次运行。

## 真正成功的判断

至少分别核对：Key 验证、Session 就绪、真实网页操作与结果、Session 释放。存在 Profile 时再核对保存状态。复制说明、创建 Key、浏览器启动或结束，都不能单独代替业务任务成功。

成功和失败都可展示完整 Session ID。成功时说清任务结果、浏览器已关闭且不再计费以及实际费用（结算中则明确说明）；失败时用自然语言说明原因和下一步。请求 ID 仅在排障时展示，不原样复述后端字段名。实际返回可打开的结果链接时才提供；控制地址、能力令牌、人工处理链接和内部日志不得放入最终结果。

## 短 FAQ 与排障

**断线后是否新建运行？** 不。查询原 Session；仍 `ready` 时为它重新申请控制，先重新观察页面和目标结果。不得更换幂等键重复创建，也不得自动重放结果未知的提交、支付、发送等写操作。

**人工页是白色画面，是否等于断线？** 不一定。先区分页面本来是 `about:blank`/白底、目标页仍在加载或被站点拦截、screencast 尚未得到首帧、WebSocket/控制租约已失效。核对连接状态、当前 URL/Target、页面就绪状态和首帧后再处理；不要自动导航覆盖用户原页面。

**等待或断线是否收费？** 浏览器 `ready` 后，到结束请求被受理或到达公开 `expires_at` 前，等待 Agent、等待人工处理以及连接断开仍按现合同计费。断开 CDP 不是停止运行。

**怎样量尺寸？** 页面就绪后，在同一 Target 读取 `screen`、`outerWidth/outerHeight`、`innerWidth/innerHeight`、`visualViewport`，并用 `Browser.getWindowBounds` 读取真实窗口；不要只看启动参数，也不要在导航前后混用不同 Target 的结果。

**费用和结束如何汇报？** 保存、资源释放、费用结算分别核对。`status=released` 才确认浏览器已关闭且不再计费；只有 `billing.status=settled` 时，才把 `billing.actual_charge_centipoints` 换算成实际费用。估算值、预留值或正在结算不能写成已扣实际费用。

需要排障时，按需记录：`GET /version` 返回的服务/合同版本，Session ID 与不含凭据的 request ID，UTC 时间，最终 Session 状态和 `save_status`，Profile 的版本关联，`ended_at`/结束原因，以及 billing 状态。不要记录或输出 API Key、Cookie、`cdp_url`、人工处理链接、签名下载地址或内部原始错误。只复查受影响路径：版本查询无副作用；Profile 列表归属/分页；原 Session 保存→Profile 关联→下一次恢复；断线重连仍是同一 Session；释放与结算分别明确。

## 产品响应字段

Session 使用 `session_id`、`status`、`created_at`、`ready_at`、`expires_at`、`ended_at` 和 `ended_reason`。结束原因包括 `user_requested`（主动结束）、`duration_limit_reached`（到达时限）、`authorization_revoked`（访问资格已撤销）、`startup_failed`（未能启动）、`runtime_interrupted`（运行中断）。没有结束原因时省略该字段。

`billing` 包含状态、实际与预计费用，以及本次价格快照和计费起止时间。实际费用仅在 `settled` / `waived` 时确定，其他状态为 null。`released` 与 `settled` 独立：关闭不再计费之后，结算仍可能处理中。

用量汇总 `GET /openapi/v1/browser/usage/summary?since=<RFC3339>&until=<RFC3339>` 返回运行次数与时长，`billing.pending_runs` 表示待确认费用的运行数，`billing.actual_charge_centipoints` 是已确认实际费用合计，不代表待确认部分为零。

错误只使用稳定 `code`、安全 `message`、`retryable`。Session 内为 `error` 对象，无异常时 null；HTTP 失败在响应顶层返回这三个字段。`request_id` 可供排障，普通结果无需展示。`retryable` 仅指导该 API 操作；不授权自动重放结果不明的网页写操作。
