# 云浏览器详细参考 · 给执行 Agent

可选说明修订与当前能力摘要通过同源 `GET /version?view=agent-guide` 或 MCP 资源 `capybrowser://service/agent-guide` 主动查询；与构建/API 合同版本独立，不插入普通业务结果。[安静发现与缓存规则](./agent-updates.md)仅用于新任务创建前，不干扰既有任务恢复、控制、结束或结算。

首次任务请先看 [最短接入说明](./installation.md)；本页供需要 Profile、人工处理、下载、错误恢复或完整接口细节时按需查阅。

> 只有当前 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_list_egress_lines`、`browser_create_session`、`browser_get_session`、`browser_list_sessions`、`browser_stop_session`、`browser_control`、`browser_recover_control`、`browser_create_view_link`、`browser_revoke_view_link`、`browser_create_profile`、`browser_get_profile`、`browser_list_profiles`、`browser_update_profile_egress`、`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` |
| 换发丢失且尚未使用的控制链接 | `POST /browser/sessions/:id/control/recover` | `browser_recover_control` |
| 查询价格（只读、不计费） | `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/view-links`；`DELETE /browser/sessions/:id/view-links/:grant_id` | `browser_create_view_link`；`browser_revoke_view_link` |
| 人工处理 | `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：默认首次路线

### 网络出口（可选）

用户可以直接在原 Agent 对话中说“这次用指定线路”或“以后我的浏览器都用指定线路”，不必进入管理页配置。Agent 收到本次选择时，先调用 `GET /openapi/v1/browser/egress-lines` 或 MCP `browser_list_egress_lines` 查询可用线路，再在创建请求中加入 `"egress_line_id":"线路ID"`。收到长期偏好时，在 `PATCH /openapi/v1/browser/profiles/:id` 发送 `{"egress_line_id":"线路ID"}`，或用 MCP `browser_update_profile_egress`；设为 `default` 则清除指定线路偏好。未指定网络出口时，临时运行继续使用默认云出口；有 Profile 时先沿用其已保存偏好。显式指定 `"default"` 可覆盖 Profile 偏好，本次覆盖不会改写偏好。

以当前身份可见目录及其可用状态为准；如果只返回 `default`，当前就没有其他线路可选。接口支持选择不代表该环境已配置指定线路，也不代表提供固定或独立公网 IP。用户仅要求本次线路时不要保存长期偏好；旧 Profile 没有偏好时仍走默认出口，不自动迁移或补写。

指定线路 · 体验期免线路附加费。浏览器启动及运行按现行标准计费。体验期结束后的线路价格将另行公布，收费前会明确告知。线路附加费与整个浏览器运行的 `billing.status` 分开：`egress.additional_fee_centipoints=0` 不等于浏览器整单免单。旧运行的 `egress.status=unknown` 是缺少历史线路记录，不代表曾验证代理或公网 IP；指定线路也不保证同一次运行的公网 IP 不变化。线路不可用或容量满时创建会明确失败，不会悄悄走默认出口。

没有跨次状态需求时不要先建 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`      | 属于本人处理期间的预期状态；等待交还或处理链接过期后再申请控制。                          |
| `control_busy` | 本次申请未发出新链接，手中仍有效的旧地址可继续首次建连。若地址丢失，先 GET 原 Session，按下文的 `control_grant` 显式恢复；不要循环申请或新建运行。 |
| `control_grant_changed` | 引用已变化；先 GET 原 Session，再根据当前阶段决定，不能原样循环换发。 |
| `control_recovery_unavailable` | 原链接已过期或第一次连接已开始；重新查状态，不抢占建立中或已连接的控制。 |
| `control_recovery_forbidden` | 使用签发该链接时的同一身份和凭据；切换 Key 不能恢复它。 |
| `session_not_ready`         | 先查询状态；starting 才等待，终态则停止申请控制。                                         |
| `capacity_exceeded`         | 停止创建；确认容量释放后最多再试一次，仍可能失败，不能承诺重试成功。                      |
| `egress_capacity_exceeded`  | 所选线路容量已满；稍后重试原请求，或为新请求显式选 `default`。                           |
| `egress_line_unavailable`   | 所选线路不可用；刷新线路列表，不自动切换或重放网页写操作。                              |
| `egress_preference_changed` | Profile 偏好在创建期间变化；用原幂等键重试，以获取原运行或按新偏好创建。                 |
| `profile_deletion_pending`  | 删除仍在处理中，使用同一删除请求稍后重试一次。                                            |
| `insufficient_balance`      | 积分不足，补充积分后再创建运行。                                                          |
| `billing_pending`           | 积分预留仍在处理中；复用原幂等键与原正文，不重复创建。                                    |
| `runtime_unavailable`       | 查询原运行状态；原因不明确时保留运行 ID 供排障，不盲目重试网页写操作。                    |
| `download_not_ready`        | 下载仍在生成；仅在当前运行仍有效时做有界重试。                                            |
| `download_gone`             | 当前运行已结束，文件不再可取；不要新建运行去猜测或恢复旧文件。                            |
| `download_limit_exceeded`   | 文件或本次运行的下载数量、总量超过限制；缩小文件或减少下载。                              |

### `control_busy` 的等待与重试

HTTP 控制错误在响应顶层返回这些字段，包括控制申请返回的 HTTP `409 control_busy`；MCP `tools/call` 的错误文本 JSON 使用相同的 `code`、`message`、`retryable` 及适用的等待字段。服务端只在确认原 Session 有一个尚未首次建连、尚未被领取的一次性控制 grant 时，才返回 `control_phase: "awaiting_first_connection"` 和正整数 `retry_after_seconds`；秒数向上取整至该 grant 的首次建连截止时间。

`409 control_busy` 表示这次控制申请没有发出新 grant；失败响应本身不会让你手中尚未消费、未过期且未被撤销或替换的原 `cdp_url` 失效。若仍持有该地址，先用它首次建连；不要因为 409 覆盖或丢弃它。只有 `POST /control` 成功返回新的 `cdp_url` 后，才用新地址替换旧地址。

### 丢失未使用控制链接时恢复

只有链接确实丢失、尚未用于第一次连接时，才使用换发；仍持有有效地址就直接建连。先 `GET /openapi/v1/browser/sessions/:id`（MCP 用 `browser_get_session`），读取可选的：

```json
{"control_grant":{"id":"cgr_0123456789abcdef0123456789abcdef","phase":"awaiting_first_connection","connect_before":"2026-10-21T10:05:00Z"}}
```

`id` 是状态比较引用，不是连接凭据；GET 不返回 `cdp_url`。`connect_before` 只是首次连接截止时间，不是当前控制会自动释放的时间。`phase` 为：

- `awaiting_first_connection`：尚未开始第一次连接；链接丢失时可显式换发。
- `claimed`：第一次连接已经开始，包括建立中及已连接；不表示链路健康，也不能换发。
- `expired`：首次连接期限已过；原 Session 仍 ready、未到期且无人工操作时，走普通 `POST /control`。

确认原 Session 仍 `ready`、未到期且没有人工操作，使用原认证主体与当前引用调用：应用内 Key 必须是签发原链接的同一把有效 Key；网关认证须保持原用户／团队，不能在两种认证来源之间切换。

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

{"expected_grant_id":"cgr_0123456789abcdef0123456789abcdef"}
```

MCP 对应 `browser_recover_control({session_id, expected_grant_id})`。成功的 `data`（MCP 为工具 JSON）包含新的 `cdp_url`、`expires_at` 和 `grant_id`。旧链接立即失效；只用新链接首次建连。换发不会创建新运行或重复收取启动费，原 Session 的运行计时继续。

若换发响应丢失，不要盲目重复原请求；同一个旧引用不会再次撤销后来的链接。先 GET 原 Session：没有 `control_grant` 时按普通控制流程处理；`claimed` 或人工阶段停止换发并查明当前操作；若当前仍为 `awaiting_first_connection`，且新链接也确实未收到，才可用**当前新引用**再显式换发一次。`control_grant_changed` 要先重新观察，不是无条件重试许可。不要自动循环换发、结束重建、抢占别人的连接或重放结果不明的网页写操作。

旧服务若没有 `control_grant` 字段而返回 `control_phase: "awaiting_first_connection"` 与 `retry_after_seconds`，这只是未使用链接的等待信息。只有等待后仍留有实际操作时间才对同一 Session 有界重试；`retryable` 不保证 Session 到期前能恢复。不能恢复时说明实际状态；自己创建的运行不再需要就正常结束，不让它闲置计费。HTTP/OpenAPI 的 `ErrorEnvelope` 与 MCP 的 `PublicError` 都将等待字段标为可选。

不要仅凭 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；确认仍为 `ready` 且没有正在进行的人工操作后，才为原 Session 再次调用 `POST /control` 并重连。不要新建 Session，也不会重复收取启动费。若断开时某个网页写操作结果不明确，重连后先观察实际页面状态，再决定下一步，不能自动重放该写操作。主动接管导致的断开按下文的人工状态处理，不直接判定任务失败或结束原 Session。

已有 claimed 连接时，`POST /control`（MCP `browser_control`）仍允许签发并返回 `200`：**签发成功即替换原 grant 并关闭旧控制连接**，不是等新地址连接后才替换，也不是无副作用的预签发。不要在健康连接中为备用地址反复申请。新 grant 尚未首次连接且仍有效时，再次签发会返回 `409 control_busy`；因此不能把 `control_busy` 理解为所有活动连接都会返回的结果。旧 `expected_grant_id` 对应的恢复会返回 `control_grant_changed`，须重新 GET 当前 Session；只有当前未使用的 grant 才可按恢复规则处理，成功恢复又会使原未使用地址失效。

页面操作可使用当前环境已有且兼容 CDP 的客户端：Playwright 用 `chromium.connectOverCDP(cdp_url)`；Puppeteer 用 [`puppeteer.connect({ browserWSEndpoint: cdp_url })`](https://pptr.dev/api/puppeteer.connectoptions)；Patchright 用下节的锁定版本示例。不要把这些客户端写成同一个连接函数。先用 `Browser.getVersion` 或等价方式确认连接，再按真实页面状态操作；网站会按自身规则决定是否允许访问，客户端连接成功不等于目标网站允许访问，也不等于业务任务完成。

### Patchright 可选客户端

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

百智云云浏览器负责启动和管理浏览器，因此不要运行 `patchright install`，也不要调用 `chromium.launch()`。客户端断开不会停止 Session 或停止计费；若连接中断，先查询原 Session 的人工状态，确认仍为 `ready` 且没有正在进行的人工操作后，才为同一 Session 调用新的 `POST /control`，再从已有页面中找回同一个 Target。不要复用旧 `cdp_url`、创建第二个 Session 或自动重放结果不明的写操作。

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

首次试用可选择无需登录、无副作用的公开页面，核对真实标题和正文；网络或页面不可达时如实返回阻塞，不伪造结果。官方 [first-run.mjs](./first-run.mjs) 检查成功 HTTP 状态、非空标题与正文，输出最多 500 字符的 `body_excerpt`；H1 可为 null，不决定成功。可用 `CAPYBROWSER_EXPECT_TITLE`、`CAPYBROWSER_EXPECT_TEXT` 提供当前目标应包含的文字（区分大小写、空白归一化），不匹配会报错并进入同一清理流程。不提供预期文字时只能证明读到了文档，Agent 仍须核对内容，不能把挑战页、登录页或任意 HTTP 200 当业务成功。

## 实时观看与主动接管

用户要求查看 Agent 正在使用的浏览器时，对**已有且 ready 的 Session**签发观看链接：

```http
POST /openapi/v1/browser/sessions/:id/view-links
Authorization: Bearer <CAPYBROWSER_API_KEY>
Idempotency-Key: <稳定且唯一的签发请求键>
Content-Type: application/json

{"scope":"view"}
```

MCP 用 `browser_create_view_link(session_id, scope, idempotency_key)`。`scope=view` 只允许看；仅当用户明确希望必要时自己操作，才选 `view_takeover`。把返回的 `page_url` 交给用户直接打开，无需控制台登录。签发、打开、观看均不打断 Agent。只有用户在页面点“我要操作”并确认后才接管；完成后点击“完成并交还 Agent”，页面提供“我已完成操作，请接回当前浏览器并继续”，由用户发回原对话。Agent 此时应重新取得同一 Session 的控制并先检查页面状态。观看或人工操作期间浏览器仍在运行和计费；关闭观看页不会结束 Session。

观看的三类期限应分别解释，按实际响应判断，不承诺固定分钟数：

| 时间 | 用途 |
| --- | --- |
| `open_before` | 最晚首次领取时间；领取成功后，它不是已打开页面的自动断开时间。 |
| `access_expires_at` | 已领取浏览器的观看授权访问截止，且不晚于 Session 到期；撤销或 Session 提前结束也会失效。 |
| WebSocket ticket 的 `expires_at` | 页面申请的一次性连接票据最晚使用时间，可能早于前两者；票据到期不等于已建立的观看流必须在此时断开。 |

短 Session 的 `open_before` 与 `access_expires_at` 可以相同，甚至都等于 Session 到期；不要据此把观看页面和短时 WebSocket ticket 混为同一个期限。

在远端网页点击“复制”不会把内容自动同步到本机剪贴板。如需这段文本，正在人工操作时请先完成并交还给 Agent，再请 Agent 从页面提取后发回对话。

主动接管会撤销原 Agent 的 CDP 控制连接。连接断开时先按原 Session ID 查询 `human_action_status` 与 Session 状态，不把断开直接判为任务失败，也不因此对原 Session 执行 `DELETE`。确认 `human_action_status=in_progress` 后，停止网页写操作、自动重连抢占及自动清理，保留原 Session 身份和结果不明的操作。确认交还或人工期限结束后，再申请原 Session 控制并重新观察；未知状态则有界查询、记录并报告，不推断交还或继续执行，不重放结果不明的网页写操作。这是 Agent 调用方应遵守的流程，不表示服务能自动暂停所有第三方 Agent。

已登录的 Console 用户也可在运行详情生成同一类型的只读或可介入链接，并撤销当前生成的链接。Console 入口是补充方式；Agent 返回的链接可直接打开。

链接只能由首次领取的浏览器使用，可按 `grant_id` 显式撤销：`DELETE /openapi/v1/browser/sessions/:id/view-links/:grant_id` 或 `browser_revoke_view_link(session_id, grant_id)`。撤销也会结束该链接派生的人工控制，但不会停止 Session。签发响应若丢失，重复相同幂等键不会再造链接；由于服务端只存链接秘密的哈希，可能返回 `view_link_result_unknown`，此时不要以新键盲目签发或新建 Session。平台 Gateway 来源仍按可信主体鉴权；当前没有平台逐 Key 撤销查询，具体平台 Key 的单独撤销不能承诺即时联动。

在另一浏览器首次打开已领取链接，可能在 `redeem` 阶段得到 `401 unauthorized`。这是链接领取能力校验失败，和携带 Bearer 的管理 API 身份校验是不同阶段；仅凭 401 不能断定“API Key 错误”，也不能断定唯一原因是“已被领取”。先回到原领取浏览器，核对实际期限、撤销及 Session 状态；原因不明就报告未知，不指导反复换 Key。失效链接上的旧画面或重连提示不证明仍有权限，也不能延长授权；明确失效后不循环重连。

### 交还后再次接管

有效 `view_takeover` 链接完成一轮交还后，可在同一窗口继续只读观看并复制交还回复；需要再次操作时点“再次接管”，核对当前选中页面，再点“确认接管”。每轮确认都会暂停 Agent 控制并断开其当前 CDP；完成后点“完成并交还 Agent”，Agent 再接回原 Session、检查页面后继续。取消确认不会开始新一轮。若所用部署的页面没有该入口，不自行拼接操作请求或宣称它已支持此循环。

新一轮由用户明确确认才创建；本轮结果未知时查询原操作，不另开一轮。循环沿用原 grant 和访问期限，不新建 Session、不换观看链接、不延长授权；只读链接、明确失效或 Session 已结束时不能再次接管。页面仍有旧画面不代表原授权有效。

## 下载文件：使用 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`，表示资源已经不存在。

## 必要的人工处理

原有 `POST /handoff` 与 MCP `browser_begin_handoff` 继续直接可用，不依赖观看链接，也没有新增必填参数。两种入口在进入人工操作后使用同一操作页面。原直接入口的多页选择规则未改变；需要明确选择页面时，可使用可选的观看接管入口。操作提示“未执行”时没有发送该操作；提示结果未知时先核对页面，不自动重放。

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 的 `human_action_status`；等待进入或人工操作到期时只撤销人工控制并把控制权交回 Agent，不结束 Session，仍需人工处理时重新生成链接。只有运行自身达到 `expires_at` 才进入停止流程。Agent 在人工处理期间申请控制返回 `409 human_control_active` 属预期。确认交还或到期后重新申请原 Session 控制、重新观察页面，再继续任务。不要自行拼接人工链接，也不要让用户去运行列表里猜是哪一次运行。

链接尚未过期却打不开时，先查询原 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，无论成功或失败，都应在收尾时先取回需要的下载，再请求结束并有界查询到明确状态。若查询确认人工正在操作，暂缓自动清理，保留原 Session 身份与清理责任；交还或运行到期确认后再收尾。若只是连接用户提供、由其他流程拥有的 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 操作；不授权自动重放结果不明的网页写操作。
