统一命令接口与人机协作
本章介绍 deepseek-browser-use 的统一接口。完成第一个任务后,再了解请求、响应和错误处理。所有浏览器控制命令使用 POST /playwright/command;执行过程见公共执行链。
项目源码:deepseek-browser-use。智能体使用说明位于 .agents/skills/deepseek-browser-use/SKILL.md 及其 references/ 分册;该文件也是智能体的操作指南。Skill 提供使用规则,后端服务仍需单独启动。
后续章节补全当前工程的运行与配置细节,可按需查阅:
- 05. 浏览器、profile 与登录态:
browser参数的五种取值、托管与用户 profile、CDP 模式与沙箱。 - 06. 窗口尺寸与页面视口:窗口按可用工作区计算、
browser.viewport的三档取值、截图像素与devicePixelRatio。 - 12. 配置项与运维自省:全部配置项清单与四个只读查询端点。
- 09. 调用追踪、页面留档与文件上传:
logs/trace/、data/<id>/与POST /playwright/upload。 - 13. 命令清单:116 个方法按功能分组,便于快速定位。
- 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只关掉该任务自己的页签,最后一个任务关闭时浏览器才退出。 - 托管 profile 默认位于
~/.config/browseruse/profiles/shared-<服务端口>(按端口派生,同一台机器上多个服务实例各用一份,不会互相抢锁)。登录态留在这份 profile 里,后续任务直接复用,但网站仍可能使其失效。详见浏览器、profile 与登录态。 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 绑定。
{"method":"start","params":{"headless":false}}
取出返回的 data.id 后,后续请求沿用该 ID:
{"id":"1001","method":"go_to_url","params":{"url":"https://example.com"}}
{"id":"1001","method":"get_browser_state","params":{}}
这里的 1001 仅为占位示例,实际调用应替换为启动响应中的 ID。
统一响应外层保持兼容,包括成功时的空字段:
{"data":{},"code":1,"ok":true,"error":null,"msg":null}
失败时 code=0、ok=false,通过 msg 读取原因。缺少参数、未知方法等受控错误通过响应体表达,不能再按旧示例假定缺参数一定返回 HTTP 500。动作错误在可分类时提供 data.errorCode:
| 错误码 | 处理方式 | 可自动重试 |
|---|---|---|
ELEMENT_READ_ONLY | 使用日期选择器等页面控件,不要反复填只读输入框 | 否 |
ELEMENT_DISABLED | 检查前置条件,等待控件启用 | 否 |
ELEMENT_OBSCURED | 检查遮挡层或弹窗 | 否 |
ELEMENT_NOT_FOUND | 选择器匹配数为 0,检查选择器和当前页面 | 否 |
SPURIOUS_DISPATCH | 对象释放异常;动作可能已生效,先读取状态,不直接补点 | 动作不可直接重试;部分只读命令由服务有限重试 |
PAGE_NAVIGATING | 页面正在导航或重建;动作先确认是否已生效 | 可以,但动作不能盲目重发 |
ACTION_UNCERTAIN | 执行器异常,无法确定动作是否已生效,先读取状态 | 否 |
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 秒)。这些字段不能代替业务判断,涉及提交的动作应先确认结果。元素类错误需先改变页面条件或修正定位,不要无脑重试。
可选精简响应
在请求顶层添加 responseMode:"compact",可以减少协议中的重复字段:
{"id":"1001","method":"get_browser_state","params":{},"responseMode":"compact"}
默认仍返回完整响应。精简模式保留 ok/code/error/msg,包括空值;去掉本地截图路径、重复的页面描述,以及默认不需要的点击诊断字段。顶层 diagnostics:true 可保留点击诊断字段,但不会恢复其他被精简的字段。转换只处理协议拥有的字段,不递归删改网站响应体或脚本执行结果。
三、页面状态和动作回执
阅读页面优先使用 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 点击遇到遮挡时停止降级,以及对象释放异常后的防重复提交流程,见表单与多层弹窗排障。
批量仍使用同一个端点,method=commands:
{
"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 自身外,命令清单里的 116 个方法都可以放进 commands。
用 expect 断言判断「动作成功但页面没变」
每个步骤可以再带一个 expect,用来抓住「接口说成功、页面其实没变」这种情况:
{
"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 取消(取消在下一步之前生效)。客户端超时不再等于任务失败 —— 超时之后批次其实还在服务端继续执行。详见站点配方、技能与异步作业。
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 精确匹配,避免将旧请求的响应当作本次结果。请求体和响应体都可能截断,调用方必须检查标记。
五、人类协助必须进入任务流程
遇到无法自行处理的登录、验证码、短信码、扫码、滑块验证、点击验证、设备确认等环节,智能体必须主动请求人类帮助。不得反复猜测、提交验证答案,也不能把等待人工处理视为任务完成。
- 明确告知当前网站、受阻原因和需要用户做什么。例如:“请在已打开的浏览器中完成登录和滑块验证,完成后告诉我,我会继续查询。”
- 使用有头浏览器,通过
bring_to_front或request_human_input展示当前页签。保留 ID、页面和浏览器,不在等待期间刷新、关闭或继续操作验证控件。 - 宿主必须实际展示求助信息。
request_human_input只是创建请求记录,接口成功不等于用户已收到通知。 - 用户可直接在浏览器操作,或通过
submit_human_input提交答案。登录优先由用户在浏览器中完成,不要求用户在对话里提供密码。 - 用户处理后重新调用
get_browser_state,确认已登录或验证通过、取得新索引,再继续原任务。请求过期、用户答复或一般页面变化本身都不能替代业务状态确认。
{
"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。
查询任务还要区分“没有记录”和“金额为零”。先确认主体、时间口径、筛选条件与查询是否成功;不能仅凭空表格推断税额或其他业务金额为零。
