进阶 / Advanced
单次成型脚本
逐条命令驱动浏览器时,模型卡在循环里:每走一步都要读结果、想下一步、再发一条命令。
chrome-use script 把整个观察、判断、动作、验证流程一次交给守护进程执行,
决策逻辑贴着浏览器跑,一次 tool call 拿回结构化结果。两种写法:JSON op-list(可机器生成、可 dry-run)
和真正的同步 JS(cu.* 助手函数)。实测 HN 翻 3 页采集高分故事,从 13+ 次调用降到 1 次。
逐条命令的循环有多贵
chrome-use 的基本用法是一条命令一个动作:snapshot 看页面,click @e8 点按钮,
eval 读数据。每条命令都是一次完整往返:进程启动、Unix socket 到守护进程、扩展 relay 到浏览器、
结果原路返回,然后 agent 的模型读输出、决定下一步。单步任务这样没问题,代价在多步流程上:翻页采集、
按条件重试、拿上一步结果决定下一步,模型被迫留在循环里,每一步花一次推理,每一步一次 tool call。
固定的动作序列已经有 batch 可以一次往返跑完(press --hold 和 wait
在守护进程内阻塞,时序精确),但 batch 的步骤之间不能传值,也没有循环和条件。script 补的就是这一层。
batch(固定序列,一次往返)→ script(步骤间传值 +
循环 + 条件 + 断言,一次往返)→ stream WebSocket(持续实时驱动)。往下每一层能表达的逻辑更多,
往返和 tool call 更少;选到刚好够用的那层就行。
JSON op-list 形式
第一种写法是 JSON 语句数组,适合让模型直接生成、让程序拼装。后面的 op 通过 {{name.path}}
值总线读前面 op 的结果:
chrome-use script - <<'JSON'
[
{"do":"navigate","url":"https://example.com"},
{"do":"evaluate","script":"document.title","bind":"t"},
{"assert":{"contains":["{{t.result}}","Example"]},"msg":"wrong page"},
{"return":"{{t.result}}"}
]
JSON
语句一共八种(cli/src/native/script.rs):
{ "do": "<action>", ...params, "bind"?: "name", "optional"?: bool } // 跑一个命令,结果存进变量
{ "waitUntil": <cond>, "timeoutMs"?: n, "pollMs"?: n } // 轮询到条件成立
{ "forEach": "{{list}}", "as"?: "item", "max": n, "body": [ ... ] } // 循环(max 必填,硬上限)
{ "assert": <cond>, "msg"?: "..." } // 断言,失败即止损(退出码 1)
{ "set": "name", "value": <v|"{{expr}}"> } // 赋值
{ "push": "name", "value": <v|"{{expr}}"> } // 往数组追加
{ "return": <v|"{{expr}}"> } // 返回并结束
{ "log": <v|"...{{expr}}..."> } // 记一行进度
条件(<cond>)有 {"eq":[a,b]}、{"ne"}、{"contains"}、
{"matches":[a,rx]}、{"exists":"<selector>"}、{"gone"}。
{{expr}} 值总线:整串就是 {{x}} 时取到原类型的值(数字还是数字),嵌在文本里
...{{x}}... 时做字符串插值;路径带下标,如 rows.0.ref、rows[0].url。
evaluate 的返回值裹在 .result 里,所以引用页面求值结果写 {{t.result}} 而不是 {{t}}。
用 --dry-run 只校验程序结构、不碰浏览器;用 --arg k=v 预置变量。退出码:0 成功、1 运行失败或断言不过、2 程序非法。
JS 程序形式
第二种写法是一段真正的 JavaScript,用 cu.* 助手函数直接驱动你已登录的真 Chrome。
这是 ego-lite 的 code base 写法,控制流就是 JS 本身的 for / if / break。
不用 await:每个 cu.* 调用会阻塞到浏览器返回,脚本是同步的。
chrome-use script --timeout 120000 <<'JS'
cu.open('https://news.ycombinator.com');
const out = [];
for (let page = 0; page < 3; page++) {
const rows = cu.eval("[...document.querySelectorAll('.athing')].map(r=>({id:r.id,title:r.querySelector('.titleline a')?.innerText}))");
for (const r of rows) {
const pts = cu.eval(`+(document.querySelector('#score_${r.id}')?.innerText.match(/\\d+/)?.[0] ?? 0)`);
if (pts >= 100) out.push({ title: r.title, points: pts });
}
if (!cu.visible('a.morelink')) break;
cu.click('a.morelink'); // 整页硬跳转,脚本状态不丢
cu.waitFor('.athing', 8000);
}
return { count: out.length, top: out.slice(0, 5) };
JS
助手函数一览:cu.snapshot / cu.eval / cu.open / cu.click /
cu.fill / cu.type / cu.press / cu.hover / cu.select /
cu.check / cu.scroll / cu.find / cu.visible / cu.waitFor /
cu.wait / cu.extract / cu.text / cu.log。cu.eval
直接返回页面里的值;其余失败即抛异常,脚本快速失败。程序的返回值就是命令的返回值,cu.log(...) 收集进度行。
引擎跑在守护进程里
ego 也能写一段 JS 跑完整个任务,chrome-use script 反超它的点在引擎的位置。
扛得住硬跳转。注入到页面世界的运行时,页面一跳转整个执行环境就没了,多页流程正好死在翻页那一下。
chrome-use 的 JS 引擎跑在守护进程里、页面外面:上面例子里 cu.click('a.morelink') 触发整页导航,
脚本状态(out 数组、循环计数)原地不动,等 cu.waitFor 确认新页加载完继续跑。
纯 Rust 引擎,全平台。JS 引擎是 boa,纯 Rust 实现,编译进 chrome-use 二进制本体, 不依赖 Node 也不依赖系统 V8。macOS / Linux / Windows 都能跑;ego 只有 macOS。
每个 op 走同一条正门。script 的每一步(两种形式都是)重新进入守护进程的
execute_command,和你手敲单条命令走完全相同的路径:策略检查、隐身传输、--humanize、
@ref 自愈、跨进程会话恢复。没有任何东西被重新实现,所以脚本里的行为和命令行里一模一样。
整条链路怎么走见 它怎么工作。
实测:HN 翻 3 页
基准任务:Hacker News 翻 3 页,收集所有 ≥100 分的故事,输出 JSON。上一节的 JS 程序就是完整题解, 已在真 Chrome 上跑通(3 页、34 条、约 4.8 秒)。
| 逐条 verb | chrome-use script | |
|---|---|---|
| tool call 次数 | 13+ | 1 |
| 模型推理次数 | 每步一次,翻页、逐条判分都要回模型 | 2 次:写程序 + 读结果 |
| 中间输出 | 每步的 snapshot / eval 结果都进上下文 | 只有 return 的最终 JSON |
| 翻页(硬跳转) | 天然没问题(每步独立) | 没问题,引擎在守护进程里 |
「≥100 分」这类判断在脚本里用一行 if 解决,旧写法里每条故事都要把分数带回模型判一次。
步数越多、判断越密,差距越大。全景对比和 ego-lite 逐项实测见 对比其他方案。
该用哪一层
script 不取代单条命令,它是多步流程的上层。选择标准:
- 一两步的任务:单条 verb 就好,加
expect或--observe收尾验证。 - 固定序列、步骤间不传值:
batch,时序精确、开销最小。 - 要用上一步的结果、要循环或条件、要断言中途止损:
script。模型来生成程序就用 JSON 形式(可--dry-run预验);人写或逻辑复杂就用 JS 形式。 - 持续实时驱动(游戏、逐帧交互):
streamWebSocket,见 Canvas / WebGL。