<!-- Source: https://deepseek-browser-use.io.nexus/04 -->

# 统一命令接口与人机协作

本章介绍 deepseek-browser-use 的统一接口。完成[第一个任务](<https://deepseek-browser-use.io.nexus/03>)后，再了解请求、响应和错误处理。所有浏览器控制命令使用 `POST /playwright/command`；执行过程见[公共执行链](<https://deepseek-browser-use.io.nexus/14>)。

项目源码：[deepseek-browser-use](https://gitee.com/ppnt/deepseek-browser-use)。智能体使用说明位于 `.agents/skills/deepseek-browser-use/SKILL.md` 及其 `references/` 分册；该文件也是智能体的操作指南。Skill 提供使用规则，后端服务仍需单独启动。

后续章节补全当前工程的运行与配置细节，可按需查阅：

- [05. 浏览器、profile 与登录态](<https://deepseek-browser-use.io.nexus/05>)：`browser` 参数的五种取值、托管与用户 profile、CDP 模式与沙箱。
- [06. 窗口尺寸与页面视口](<https://deepseek-browser-use.io.nexus/06>)：窗口按可用工作区计算、`browser.viewport` 的三档取值、截图像素与 `devicePixelRatio`。
- [12. 配置项与运维自省](<https://deepseek-browser-use.io.nexus/12>)：全部配置项清单与四个只读查询端点。
- [09. 调用追踪、页面留档与文件上传](<https://deepseek-browser-use.io.nexus/09>)：`logs/trace/`、`data/<id>/` 与 `POST /playwright/upload`。
- [13. 命令清单](<https://deepseek-browser-use.io.nexus/13>)：122 个方法按功能分组，便于快速定位。
- [10. 站点配方、技能与异步作业](<https://deepseek-browser-use.io.nexus/10>)：`run_recipe`、`skills/` 与后台批次。

## 一、启动与实例生命周期

当前工程使用 Java 21 或以上环境。在 `playwright-server` 目录运行 `mvn spring-boot:run`，或使用平台对应的发行包运行 `java -jar <发行包文件名>.jar`。通过 `GET http://localhost:10049/playwright/health` 检查服务状态。

开发时重点区分以下两层生命周期：

- **浏览器与 profile 都是全进程共享的**：所有任务用同一个浏览器进程、同一份 profile，任务之间靠**页签**隔离 —— 不是每个任务一个浏览器。原因是用户数据目录天生是单例，同一个目录同时只允许一个浏览器进程。
- Playwright driver 同样由服务共享；普通 `close` 结束任务，**最后一个任务保留浏览器与页面**。`closeBrowser:true` 显式关闭浏览器，有其他任务时拒绝。
- 托管 profile 默认位于 `~/.config/browseruse/profiles/shared-<服务端口>`（按端口派生，同一台机器上多个服务实例各用一份，不会互相抢锁）。登录态留在这份 profile 里，后续任务直接复用，但网站仍可能使其失效。详见[浏览器、profile 与登录态](<https://deepseek-browser-use.io.nexus/05>)。
- `start` 不传 ID 时由 `nexus.io.tio.utils.snowflake.SnowflakeIdUtils.id()` 生成；同一个 ID 不能重复 `start`，需要先 `close` 或换一个 ID。
- **浏览器类型、有头/无头、profile 目录、可执行文件都是浏览器级属性**：与正在运行的那个不一致时，空闲状态下一次 `start` 会按新配置重建，还有任务在跑时直接报错 —— 不会把别人的页签弄没。
- 同一实例的操作按顺序执行，不能并发使用同一个 ID；要并行请用多个 ID。
- 需要人工登录或验证时使用 `headless=false`。等待人工协助时保留浏览器，任务完成后再关闭。

启动参数由 `PlaywrightService.chromiumArgs()` 和 `chromiumSandbox()` 管理。Windows/macOS 开启 Chromium 沙箱；Linux 当前为兼容常见容器运行环境关闭沙箱，并添加 `--disable-dev-shm-usage`。

不要添加 `--disable-blink-features=AutomationControlled`：该参数会触发浏览器顶部的“不受支持的命令行标志”提示。也不要通过 `--disable-infobars` 隐藏问题。修改参数后需要重启更新后的后端并重新创建浏览器实例，旧窗口不会自动应用新参数。

读取环境配置使用 `EnvUtils.get`。生成任务或请求的雪花 ID 使用 `SnowflakeIdUtils.id()`，对外以字符串传递，避免 JavaScript 数字精度损失。

## 二、统一请求与响应

所有浏览器操作使用 `POST http://localhost:10049/playwright/command`，请求头为 `Content-Type: application/json`。方法参数放在 `params`，不再使用 query/form 绑定。

```json
{"method":"start","params":{"headless":false}}
```

取出返回的 `data.id` 后，后续请求沿用该 ID：

```json
{"id":"1001","method":"go_to_url","params":{"url":"https://example.com"}}
```

```json
{"id":"1001","method":"get_browser_state","params":{}}
```

这里的 `1001` 仅为占位示例，实际调用应替换为启动响应中的 ID。

统一响应外层保持兼容：成功时只有 `data`、`code`、`ok` 三个字段，**空字段不再输出**（详见下面的说明）：

```json
{"data":{},"code":1,"ok":true}
```

失败时 `code=0`、`ok=false`，通过 `msg` 读取原因。缺少参数、未知方法等受控错误通过响应体表达，不能再按旧示例假定缺参数一定返回 HTTP 500。动作错误在可分类时提供 `data.errorCode`：

### 响应里不输出 null 值字段

每条回执都带 `"msg":null`、`"error":null` 是纯粹的噪声，尤其批量回执里每一步都带一份，所以这个服务打开了框架的全局开关 `tio.json.skipNull`：**所有走默认工厂的序列化点都不再写 null 值字段**（框架实现与影响范围见 [Json 转换](https://tio-boot.com/zh/14_tio-utils/05.html) 的「不输出 null 值字段」）。

对读回执的一方有两点要知道：

- **字段"不在"与"为 null"是同一件事**：判断失败看 `ok` / `code`，不要用 `"msg" in response` 之类的存在性判断；
- 想恢复带 null 的输出：项目自己的键 `browser.json.skipNull=false`（默认 `true`，优先级高于框架键），或把框架键 `tio.json.skipNull` 设为 `false`。四条来源的优先级与其它配置一致 —— 命令行 / `app.properties` 里写，或用 JVM 参数、环境变量覆盖；启动日志末尾的 `响应跳过null=` 会说明这次到底生效没有。

| 错误码 | 处理方式 | 可自动重试 |
| --- | --- | --- |
| `ELEMENT_READ_ONLY` | 使用日期选择器等页面控件，不要反复填只读输入框 | 否 |
| `ELEMENT_DISABLED` | 检查前置条件，等待控件启用 | 否 |
| `ELEMENT_OBSCURED` | 检查遮挡层或弹窗 | 否 |
| `ELEMENT_NOT_FOUND` | 选择器匹配数为 0，检查选择器和当前页面 | 否 |
| `SPURIOUS_DISPATCH` | 对象释放异常；动作可能已生效，先读取状态，不直接补点 | 动作不可直接重试；部分只读命令由服务有限重试 |
| `PAGE_NAVIGATING` | 页面正在导航/整页重建（主 frame 为 null，含 SSO 重定向循环、新版控制台 lite 页）；**只读命令**服务端按可重发名单自动重试，回执带 `pageNavigatingRetry`；**动作类命令**不自动重发，先读页面状态 | 只读可以；动作不能盲目重发 |
| `ACTION_UNCERTAIN` | 执行器异常，无法确定动作是否已生效，先读取状态（导航类异常已不再落到这里，改归 `PAGE_NAVIGATING`） | 否 |
| `ELEMENT_HIDDEN` | 重新定位当前可见控件 | 否 |
| `ELEMENT_NOT_EDITABLE` | 检查元素类型和编辑状态 | 否 |
| `STALE_ELEMENT` | 重新获取页面状态和元素索引 | 可以，但必须先重取快照 |
| `RATE_LIMITED` | 站点侧节流（例如“不允许重复提交，请稍候再试”）：等一会儿再试，不要连续重发 | 可以 |
| `ACTION_TIMEOUT` | 结合完整错误原因和页面状态判断，不统一归因于页面已变化 | 可以 |
| `ACTION_FAILED` | 兜底分类：读完整错误原因与页面状态 | 否 |

可重试的错误会在 `data.retryable` 给 `true`，并在 `data.retryAfterMs` 给出建议退避毫秒数（`RATE_LIMITED` 约 35 秒、`ACTION_TIMEOUT` 约 1.5 秒、`STALE_ELEMENT` 约 0.3 秒、`PAGE_NAVIGATING` 约 1 秒）。

导航类失败最常见的形态是**主 frame 为 null**：这时 `get_browser_state` / `list_frames` 会拿到 `Cannot invoke "...Frame.childFrames()" because "frame" is null`，构建快照时还可能抛 `Index 0 out of bounds for length 0`。这类异常归入 `PAGE_NAVIGATING`（**不是** `ACTION_UNCERTAIN`）：只读命令可重试，回执另给 `data.pageNavigatingRetry` 说明服务端已重发过几次。处理方式与实战案例见[页面正在导航与主 Frame 缺失](<https://deepseek-browser-use.io.nexus/36>)。这些字段不能代替业务判断，涉及提交的动作应先确认结果。元素类错误需先改变页面条件或修正定位，不要无脑重试。

### 可选精简响应

在请求顶层添加 `responseMode:"compact"`，可以减少协议中的重复字段：

```json
{"id":"1001","method":"get_browser_state","params":{},"responseMode":"compact"}
```

HTTP 不传模式时仍返回完整响应；dsb CLI 默认发送 compact，可用 `--response-mode full` 恢复完整响应。精简模式保留 `ok/code/error/msg` 的语义，空值是否输出仍由序列化配置决定。

正常成功回执省略已完成的 `execution`、零重试的 `retryBudget`、正常截图的 `capture` 与 `capture_degraded:false`，以及重复页面描述和点击诊断字段。截图未内联时省略 `inline:false`、`base64Omitted` 及其提示，保留路径、URL 和文件大小：

```json
{"data":{"path":"data/1001/shot-1.png","url":"/data/1001/shot-1.png","size":85297},"code":1,"ok":true}
```

失败响应不裁剪；动作未知、观测不完整、截图失败或降级时保留诊断。发生重试时保留对应重试指标。成功的同步批量逐条投影，失败批次整体保留。顶层 `diagnostics:true`（CLI `--diagnostics`）跳过全部裁剪。转换只处理协议字段，不递归删改网站 body 或脚本 result。

`--out response.json` 保存当前模式收到的 JSON，客户端留档和服务端 trace 也不会自动恢复已省略的字段。不要重放业务动作补诊断；最近命令可通过只读 `diagnose` 查看。截图与 PowerShell 读取示例见[客户端](<https://deepseek-browser-use.io.nexus/03_client>)。

## 三、页面状态和动作回执

阅读页面优先使用 `get_browser_state` 的 `data.text` 与 `data.tabs`。索引来自最近一次快照，导航或动态更新后应重新获取，不能沿用旧索引。

表单状态必须读取 DOM property，而不能只使用初始 HTML attribute：

- 输出实时 `value`、`checked`、`selected`、`selected-text` 和 `readonly/disabled/editable`。
- 密码输入框的值输出为 `[redacted]`。
- 可见的只读或禁用表单控件即使没有交互索引，也应展示状态；没有索引不代表元素不存在。
- 不再把所有属性统一截断为 15 个字符。标识和表单值完整保留，展示类长属性限制长度并转义。
- 压缩无语义容器的缩进，最多保留 6 层语义缩进；缩进不再对应原始 DOM 的每一层。

点击回执的 `changed` 根据 URL、标签页、正文、表单状态及 DOM 结构指纹观察变化，可识别等长文本替换等情况。`changeStatus` 为 `observed` 或 `not_observed`；`observationComplete` 表示探测是否完整，`observationWindowMs` 为 500。浏览器调用自身耗时不包含在这个观察窗口内。

`ok=true` 只表示动作执行未报错，`changed=true` 也不证明业务已成功。`changed=false` 仅表示短观察窗口内没有发现变化，不能据此重复提交；应读取结果文本、等待目标状态或检查相关响应。

截图在执行层统一附加：单条和批量动作共用相同路径；成功的动作类命令截图一次，失败动作不附加成功截图。`get_browser_state` 自己保存截图和结构化文本，避免重复截图。默认读取文本，只有文本无法表达必要信息时才查看图片。

## 四、批量执行和网络关联

多层表单的定位、auto 点击遇到遮挡时停止降级，以及对象释放异常后的防重复提交流程，见[表单与多层弹窗排障](<https://deepseek-browser-use.io.nexus/08>)。

批量仍使用同一个端点，`method=commands`：

```json
{
  "id":"1001",
  "method":"commands",
  "params":{
    "stopOnError":true,
    "commands":[
      {"go_to_url":{"url":"https://example.com"}},
      {"get_browser_state":{}}
    ]
  }
}
```

每项只有一个命令键，最多 200 项，不允许嵌套 `commands`。`stopOnError` 默认 `true`；只有后续步骤不依赖前一步成功时才考虑设为 `false`。结果保留在 `data.results`，同时返回成功、失败和停止计数信息。批量不会因为页面变化自动中断，依赖新索引的动作应放到下一批，在重新读状态后决定。

单条命令也可以和批量一样传 `responseMode:"compact"`，两条路径的响应装饰是同一套实现。除 `commands` 自身外，[命令清单](<https://deepseek-browser-use.io.nexus/13>)里的 122 个方法都可以放进 `commands`。

### 用 expect 断言判断「动作成功但页面没变」

每个步骤可以再带一个 `expect`，用来抓住「接口说成功、页面其实没变」这种情况：

```json
{
  "id":"1001",
  "method":"commands",
  "params":{
    "stopOnError":false,
    "stopOnExpectFailure":true,
    "commands":[
      {"click_element_by_selector":{"selector":".ant-modal-confirm .ant-btn-primary","mode":"mouse"},
       "expect":{"js":"document.querySelectorAll('.ant-modal-confirm').length","equals":0}}
    ]
  }
}
```

断言没过时批次整体失败，但 `data.failed` 仍是 `0`、`data.expectFailed` 是 `1`，`msg` 指出是哪一步的断言没过 —— **命令本身执行成功了，是页面状态没变成期望的样子**。

### 长批次用异步作业

批次可能跑几十秒以上。加 `"async":true` 后接口立刻返回 `data.jobId`，批次在后台继续跑：用 `get_job` 取结果、`list_jobs` 看全部、`cancel_job` 取消（取消在下一步之前生效）。客户端超时不再等于任务失败 —— 超时之后批次其实还在服务端继续执行。详见[站点配方、技能与异步作业](<https://deepseek-browser-use.io.nexus/10>)。

`get_requests` 返回请求元数据，包括字符串 `requestId`、`requestedAt`、URL、方法及状态；有请求体时附加 `postData`、`postDataLength`、`postDataTruncated`。响应到达后补充 `respondedAt`，失败请求记录 `failure`、`finishedAt`。

读取响应体使用 `get_response_body` 或 `wait_for_response`，响应包含请求关联信息，以及 `bodyAvailable`、`bodyLength`、`truncated` 等字段。同一个 URL 被重复请求时，用 `get_response_body` 的 `params.requestId` 精确匹配，避免将旧请求的响应当作本次结果。请求体和响应体都可能截断，调用方必须检查标记。

## 五、人类协助必须进入任务流程

遇到无法自行处理的登录、验证码、短信码、扫码、滑块验证、点击验证、设备确认等环节，智能体必须主动请求人类帮助。不得反复猜测、提交验证答案，也不能把等待人工处理视为任务完成。

1. 明确告知当前网站、受阻原因和需要用户做什么。例如：“请在已打开的浏览器中完成登录和滑块验证，完成后告诉我，我会继续查询。”
2. 使用有头浏览器，通过 `bring_to_front` 或 `request_human_input` 展示当前页签。保留 ID、页面和浏览器，不在等待期间刷新、关闭或继续操作验证控件。
3. 宿主必须实际展示求助信息。`request_human_input` 只是创建请求记录，接口成功不等于用户已收到通知。
4. 用户可直接在浏览器操作，或通过 `submit_human_input` 提交答案。登录优先由用户在浏览器中完成，不要求用户在对话里提供密码。
5. 用户处理后重新调用 `get_browser_state`，确认已登录或验证通过、取得新索引，再继续原任务。请求过期、用户答复或一般页面变化本身都不能替代业务状态确认。

```json
{
  "id":"1001",
  "method":"request_human_input",
  "params":{"prompt":"请在浏览器中完成登录和滑块验证，完成后告诉我。","timeoutSeconds":300}
}
```

返回的 `data.requestId` 用于 `get_human_input` 或 `submit_human_input`。需要展示验证图片时可传元素 `index` 或 `selector`，取得 `imageBase64`。直接在浏览器操作不会自动更新请求记录，状态可能仍是 `pending`；确认页面已通过验证后即可继续，不必死等该字段变为 `answered`。

无头实例需要切换到有头模式时，先说明并记录当前 URL，再按同一 ID 关闭、重新启动并打开页面；这可能丢失未提交表单和当前验证进度。需要人工协助的流程应尽早选择有头模式。

## 六、开发验证要点

回归验证应覆盖：实时表单值与密码脱敏、只读错误分类、等长内容变化、异步弹出标签页、无变化点击、单条和批量截图一致性、重复 URL 的请求关联，以及精简模式不删改业务数据。相关测试位于 `BrowserResponseIntegrationTest`、`ResponseFormatterTest`、`ActionErrorTest` 和 `PlaywrightServiceTest`。

查询任务还要区分“没有记录”和“金额为零”。先确认主体、时间口径、筛选条件与查询是否成功；不能仅凭空表格推断税额或其他业务金额为零。
