Skip to content

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

本章介绍 deepseek-browser-use 的统一接口。完成第一个任务后,再了解请求、响应和错误处理。所有浏览器控制命令使用 POST /playwright/command;执行过程见公共执行链。

项目源码:deepseek-browser-use。智能体使用说明位于 .agents/skills/deepseek-browser-use/SKILL.md 及其 references/ 分册;该文件也是智能体的操作指南。Skill 提供使用规则,后端服务仍需单独启动。

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

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

当前工程使用 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 与登录态。
  • 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 转换 的「不输出 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 缺失。这些字段不能代替业务判断,涉及提交的动作应先确认结果。元素类错误需先改变页面条件或修正定位,不要无脑重试。

可选精简响应 ​

在请求顶层添加 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 读取示例见客户端。

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

阅读页面优先使用 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:

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 自身外,命令清单里的 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 取消(取消在下一步之前生效)。客户端超时不再等于任务失败 —— 超时之后批次其实还在服务端继续执行。详见站点配方、技能与异步作业。

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。

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