核心用法 / Core Usage

等待与断言

Agent 翻车,更多是败在乱等,而不是选错元素。 点完、填完之后,页面还在动——这时候该等什么、怎么确认动作真的生效, 决定了你的脚本是稳还是脆。这一页讲清楚 wait 怎么等、 expect 怎么断言、--observe 怎么看变化, 以及 --if-present 怎么让可选步骤保持幂等。

观察质量由 observed.status 的 complete、partial、unavailable 表示,动作 success 值不因观察失败而改写。证据不足时 changed 为 null,不会伪造空页面或 URL。基线失败但后置树可用时返回后置树。不完整观察会附错误和 retryAction:false,应检查当前状态,不要为了补观察重放动作。

只有动作后的捕获失败时,observed.refs 说明会话里的 ref 怎么处理。kept-unverified:保留上一次快照的 ref,但之后的命令使用其中某个 ref 前,chrome-use 会实时确认标签页、frame、文档和元素都没变;确认不了就在动作前拒绝,绝不按角色或名称重新定位。DOM 遍历快照(snapshot --dom)得到的 ref 在这种状态下一律拒绝。dropped:页面可能已换成别的文档。两种情况都用 snapshot -i 取当前 ref,不要重放动作。

选对等待条件 — wait

每次会改变页面的动作之后,都要挑一个具体的条件去等, 而不是拍脑袋 sleep 一个固定毫秒数。wait 支持这些形式:

bash
$ chrome-use wait @e1                     # 直到某个元素出现
$ chrome-use wait 2000                    # 傻等,毫秒(最后手段)
$ chrome-use wait --text "Success"        # 直到页面上出现这段文字
$ chrome-use wait --url "**/dashboard"    # 直到 URL 匹配模式(glob)
$ chrome-use wait --load networkidle       # 直到网络空闲(导航后收尾)
$ chrome-use wait --load domcontentloaded  # 直到 DOMContentLoaded
$ chrome-use wait --fn "window.myApp.ready === true"  # 直到 JS 条件成立

任何会改变页面的动作之后,从下面三种里挑一个:

⚠️ 少用裸 wait 2000
除了调试,别用固定毫秒的 wait 2000——它让脚本又慢又脆。 超时默认 25 秒,所以等一个真实条件不会白等太久,但会在条件满足的那一刻就继续。

读之前它自己会等 — settle

一次观察值多少,取决于它拍下的那一刻页面在做什么。所以 snapshot 和 --observe 会先等页面停下来再采集, 你不需要在前面自己塞一个 sleep:

以最慢的那个为准,上限 1 秒。静态页面大约只花 100ms; 而一次触发 XHR 的点击会等到响应回来,而不是把「响应前的树」当成结果返回。

两者有一处不同:单独的 snapshot 没有动作要反应,页面静止就是答案,安静了立刻返回; 而动作之后的 --observe 会先花上限的一半(默认 500ms)盯着「有没有第一个反应」, 才肯报 changed:false —— 否则一个 300ms 后才渲染的控件会被读成「什么都没发生」。 这份代价只由真的什么都没做的动作承担;只要页面有反应,窗口立刻结束,包括动作自己的处理函数在派发过程中同步做的重绘(同步重绘列表的点击大约在 100ms 安静窗口内就结束,而不是 500ms)。

observed.status: complete 只表示这次捕获完整,不表示任务完成:页面先显示 Loading、再用定时器渲染结果时,那一刻它是安静的。接着用 wait --text "Report ready"(或页面自己的最终信号)等结果。

bash
$ chrome-use snapshot -i --settle-ms 3000   # 慢页面:把上限调高
$ chrome-use snapshot -i --no-settle        # 就要中间态:不等,立刻采
⚠️ 上限到点了会说出来
页面到上限还在动时,回复里会带一行 Page had not settled after 1000ms (request in flight still active) — this capture may be mid-transition. Re-read to confirm, or raise the ceiling with AGENT_BROWSER_SETTLE_MS.——意思是这张树可能是中间态,请重读一次再信它。 卡在网络上时,会点名最多三个请求(只写主机和路径,不带查询参数)和各自已经等了多久, JSON 里是 settle.pendingRequests,取的是等待结束那一刻的状态;等的请求到截止时已不再计入(已完成,或超过了过期阈值),就写 state at the deadline unknown;page did not answer the settle check 表示页面忙或正在跳转,不代表 DOM 还在变。 中间态被当成最终态返回,比慢更糟,所以这条永远不会被吞掉。

既然已经等过一次,像素就可以搭这趟车:--with-screenshot <path> 让 snapshot 或带 --observe 的动作在同一个稳定时刻 同时留下结构和截图(各等各的会得到两个时刻的东西,那比不合并还糟)。结构照旧走 stdout, 图片落盘并在输出里报路径 —— 截图在这里是输出,不是 agent 读页面的方式。

全局可用 AGENT_BROWSER_SETTLE_MS(设 0 关闭等待)和 AGENT_BROWSER_SETTLE_QUIET_MS 调。等某个具体条件 仍然是 wait 的活:settle 只知道页面停了,不知道你要的东西出现了没有。


确认动作生效 — expect

动作做完之后,用断言去确认结果,而不是拉一张快照用眼睛看。 expect 是一个带退出码的 pass/fail 动词 (0 通过 / 1 为假 / 2 无法判断),所以它能和 &&、 chrome-use batch 组合,而且只花 ~1 行、不用你去读一整张快照。

bash — expect 断言
$ chrome-use click @e8 && chrome-use expect "#toast" visible      # toast 弹出来了吗?
$ chrome-use expect count ".result" ">=" 1                        # 结果加载出来了吗?
$ chrome-use expect text @e3 contains "Saved"                    # 成功提示?
$ chrome-use expect url contains /dashboard                       # 导航落地了吗?
$ chrome-use expect "#spinner" gone                               # 加载完成了?
$ chrome-use requests --clear && chrome-use click @save \
    && chrome-use expect request /api/save --status 2xx           # POST 发出且 2xx?
$ chrome-use expect no-errors                                     # 没有 console 报错?

expect 自带等待

expect 会在超时时间内轮询、等条件成立, 所以你常常不需要再单独写一个 wait。支持的条件有:

--not 反转判断,--no-wait 只检查一次不轮询。

ℹ️ 请求与控制台的前置条件
expect request 只能看到开始跟踪之后捕获的请求—— 先跑一次 requests --clear;no-errors 需要控制台捕获—— 先跑一次 console。详见 排查问题。

重复操作仍未观察到变化

同一目标和页面连续三次相同的 click、dblclick 或 press,相邻间隔不超过 60 秒时,observed.noProgress 才可能出现。每次观察都需 complete、quiet,已开启等待(waitedMs > 0),changed:false、sawChange:false,无 pending 工作,也未发现请求、资源、新 frame 或人机校验活动。有活动或证据不完整会中断连续计数。

提示带 retryAction:false,建议检查当前状态或等待任务所需的条件;它不改变动作 success,也不执行其他动作。未观察到可访问性树或请求变化不证明服务器写入失败,不要盲目重放发送、提交、购买或删除。

普通 CLI/MCP 准备连接时,只有成功复用同一浏览器连接、目标和会话,且未加载 storage state,才会保留连续计数。新浏览器、重新绑定、准备失败或加载 storage state 仍会清空计数。

脚本在 advisories(CLI script --json 顶层;daemon envelope 中为 data.advisories) 中保留提示,最多 20 条,嵌套脚本和后续失败的运行也会保留。JSON op-list 的对应 steps 条目另带 noProgress;文字输出只打印汇总提示一次。JS 脚本失败时保留 ok:false、return:null、error、logs 和 advisories。daemon envelope 中嵌套脚本的 data.ok:false 会使父脚本失败,即使传输层 success 为 true;调度成功不能证明程序成功。

看清动作改了什么 — --observe

请求最多显示 20 条摘要,每条最多 256 个 UTF-8 字节;data URL 只显示媒体头和编码后载荷大小。JSON 同时返回 requestsTotal、requestsOmitted、requestsShortened。完整记录用 network requests --json 查看。changed:false 只表示树与 URL 没变,有请求时仍显示摘要。

给一个会改变页面的动作(click / fill / type / select / check / press / eval / hover,悬停常会展开菜单或说明文字) 加上 --observe,就不用你手动跑「动作 → wait → snapshot → diff」那一套了: 返回结果里会带一个 observed 增量——新增/移除的可交互行 (新的 toast、校验提示也会通过 alert 面通道带上)、URL 变化、发出的请求; 什么都没动就返回 {changed:false}。

bash
$ chrome-use click @e8 --observe   # 读取动作后的变化和有界请求摘要
ℹ️ 报 no change 时,先读下面的 why: 行
动作之后什么都没变,daemon 会探一下目标并报出第一个决定性的原因:控件被禁用、没有渲染、 在视口外,或被另一个元素盖住(会点名是谁)。修掉那一件事,再用同一个语义动作重试。 如果 why 说这些都不成立,那就是动作到了一个本来就不产生可见变化的控件—— 不要把空的 delta 读成失败,也不要重复这个动作。
ℹ️ 一次点击把整页换掉时,给的是新树
点了链接、提交后跳转这类动作,--observe 不再给「旧树全删 + 新树全增」的 diff, 而是在 observed snapshot: 下直接给新页面的树,前面一行说明旧页面有多少行已不在。 判定条件是两棵树都有页面规模(≥20 行)且各自 ≥80% 的行换掉;树里的 ref 是活的,直接用。
ℹ️ 小页面直接给整棵当前树
动作后的树不超过 4 KB 且不超过 60 行时,observed.snapshot 就是这次采集到的整棵当前树(ref 已登记,直接用), observed.changes 只列出 +/- 行,不带上下文。动作之后紧跟一次 snapshot 不会多出信息。 更大的页面照旧给统一格式的 delta。采集不完整时 observed.status 照实写 partial 或 unavailable。
ℹ️ 静态文字的变化在 observed.text 里
可交互树不收普通静态文字,iframe 里的回执、操作日志、「Page 2 of 3」这类计数原先都看不到,点击会显示成没有变化。 observed.text 按帧列出新出现(+)和消失(-)的可见文字行,子帧的行以 [frame 名称] 开头; 只有文字变了也算 changed: true。最多 20 行、1 KB、每行 200 字节,多出的计入 textOmitted 并标 textTruncated: true。 文字直接从渲染后的 DOM 读取,不用 innerText:<input>、<textarea>、<select> 和所有可编辑元素(可编辑宿主及其全部内容、designMode 下的整个文档)连同子树一起跳过,open shadow root 里也一样。 动作前后只要任一帧(含 open shadow root)里有密码框带值,就一行文字都不返回,textStatus 为 redacted:页面可能把密码复制到任何帧的文字里,所以没有哪一行算安全。密码值本身不会离开页面。 每个帧在自己的隔离环境中读取并与帧树核对,读完再取一次帧树作为最终清单。一次采集在所有会话里最多读 32 个帧、最多 2.5 秒;没读到的帧(超出预算,或读的过程中才出现)记为 partial,读失败或读的过程中文档变了的帧记为 unavailable。 只要有这样的帧,所有文字行都不返回(它可能藏着密码),它会带原因列在 textFrames 里,整个观察也不再完整—— textStatus 为 unavailable,status 最多是 partial,树没变也不会再报 changed: false,不会给出 noProgress。 最终清单确认已经消失的帧(gone)和文档换了的帧(frameDocumentChanged,比如同一 URL 重新加载)都算变化。 小页面的 changes 最多 60 行、4 KB、每行 240 字节(changesOmitted、changesShortened)。
✅ expect 还是 --observe?
想要一个硬性 pass/fail 门禁(配合退出码、&&、批处理)用 expect;想看清到底发生了什么、拿到结构化的变化用 --observe。

一次填完整张表单 — form fill --map

与其写 N 步 fill / select / check, 不如传一个 {标签或选择器: 值} 的映射:它按 <label>、 aria-label、placeholder、name、CSS 逐个解析字段,自动派发到对应的控件类型 (字符串 → 文本/下拉/单选,true/false → 复选框), 还能顺手提交(--submit "<文本|选择器>"),返回 {filled, submitted, errors}——errors 就是行内校验消息, 所以一次提交被拒会在同一次调用里告诉你为什么。

bash — form fill --map
$ chrome-use form fill --map \
    '{"Email":"a@b.com","Country":"US","Subscribe":true}' \
    --submit "Sign up"
ℹ️ 富文本编辑器例外
DraftJS / Monaco / CodeMirror 这类富文本编辑器,用 fill 单独填——它能处理; form fill 覆盖的是标准控件。更多输入细节见 交互操作。

可选步骤 — --if-present

给任意基于选择器的动作加上 --if-present(别名 --optional), 当目标不存在时它就变成一个空操作但成功(↷ skipped,退出码 0), 而不是报错——不用事先读一遍页面判断,流程也能反复重跑。

bash
$ chrome-use click ".cookie-accept" --if-present   # 关掉可能不存在的 cookie 横幅
$ chrome-use check "#opt-in" --if-present
⚠️ 只跳过「元素不存在」
--if-present 只对「目标缺失」放行;真正的失败(比如元素在但点不动) 仍然会报错,不会被悄悄吞掉。

observe 里的资源摘要和人机校验拦截

--observe 的结果里有 resources:动作期间页面新加载的资源(取自 resource timing,Network 域关闭时也有),脚本和请求排在前面,最多 10 条,另有 resourcesTotal。

如果动作之后页面没有任何变化,但期间加载了已知的人机校验脚本(OpenAI Sentinel、hCaptcha、Cloudflare Turnstile、reCAPTCHA、Arkose、DataDome、HUMAN、GeeTest),observed.humanCheck 会写明是哪家,结果带 blocked_by_human_check 警告。这只能说明脚本已加载且页面未变化,不能据此判断必须由真人操作。不要重复原来的提交;读取当前验证码,按 core/captcha 在已授权的任务中尝试正常控件并核验结果。尝试失败或需要本人操作时再用 session handoff。


下一步