browser-cli — 命令行驱动浏览器
为什么不用 chrome-devtools-mcp
慢的根因不是 CDP,是 LLM 往返次数 × 每次注入的 token 量。MCP 的
take_snapshot 一次回 15-20K token 的完整 a11y 树;agent-browser snapshot -i
只回可交互元素,约 350 token。40-50 倍差距,而且 MCP 的工具定义本身还常驻
system prompt。
浏览器操作天然是往复的(看→点→再看),所以降低每一轮的成本比"一次编好所有动作"现实。
仍然用 chrome-devtools-mcp 的场景:Lighthouse 审计、heap snapshot、 performance trace。这三样 agent-browser 不提供。
先确认 Chrome 在哪台机器
ab 默认假设 Chrome 在远程 cloudtop,每条命令都 ssh 过去。但 Chrome 也可能
就跑在本机 —— 这时 ssh 别名往往早就不通,doctor.sh 一路 FAIL,看起来像
"工具坏了",其实只是走错了路。
先判断:
curl -s http://127.0.0.1:9222/json/version # 有输出 = Chrome 在本机
本机就用 abl(直连,不走 ssh),远程才用 ab:
abl get url
abl batch "open <url>" "get url" "eval document.title"
abl = ~/.claude/skills/browser-cli/scripts/ab.local,已链到 ~/.local/bin/abl。
前置检查
新机器 / 长时间没用,先跑:
~/.claude/skills/browser-cli/scripts/doctor.sh
依赖装在远端 cloudtop(不在仓库里):ssh 别名可达、nvm node 24 上装了
agent-browser、那个已登录公司账号的 Chrome 带 --remote-debugging-port=9222 启动。
第一层:通用浏览器驱动
ab --where # 当前目标机
ab --use local # 本机 cc-tw 的 Chrome(不走 ssh)
ab --use bj | hk # 切北京 glinux_bj / 香港 glinux(Chris 人在哪用哪台)
AB_HOST=glinux ab ... # 单次覆盖
ab open <url>
ab snapshot -i # 只列可交互元素(省 token 的关键)
ab diff snapshot # 只回与上次快照的差异
ab click @e5
ab fill @e2 "text"
ab get text @e3
ab get url
ab eval 'document.title' # 注意是 eval,不是 js
ab skills get core --full # 官方用法速查
选 local 还是远端:看这页需不需要 SSO 登录
| 页面 | 目标机 | 为什么 |
|---|---|---|
| 需要 SSO 的内部站点 | bj / hk |
只有那两台的 Chrome 是登录态的 |
| 自己写的 HTML、GitHub Pages、公开网站 | local |
快一个数量级,而且窗口就在桌面上 |
local 省掉 ssh 往返(远端每轮 60 s 起步),并且浏览器真的显示在
Chrome Remote Desktop 那块桌面上 —— Chris 和 agent 看的是同一个东西。
CDP 没通时 ab 会直接告诉你跑 scripts/local-chrome.sh(幂等,已在跑就返回)。
起 Chrome 时
TMPDIR必须拨回/tmp。bot 进程里的TMPDIR是层层嵌套的/tmp/claude-.../claude-.../…,Chrome 的 SingletonSocket 建在它下面会超过 unix socket 路径 108 字节上限,直接 FATAL 退出 —— 报的却是Socket path too long,一眼看不出跟TMPDIR有关。local-chrome.sh已处理。
调试自己做的网页:ab 和 Playwright 分工,别只用一个
两个都要,因为它们回答的是不同的问题:
| 用什么 | 干什么 | |
|---|---|---|
| 「有没有坏」 | Playwright 脚本 | 多视口批量量尺寸、抓 console error、跟改前 backup 逐档对照。可重复、一条命令扫十几档 |
| 「好不好」 | ab --use local |
像读者那样翻页面看版式、配色、节奏。这类问题脚本测不出来 |
只截局部元素会漏掉整体问题。 截 .toc、截 .badge、截侧边栏,
拼不出「这一段放错文档了」「这里太挤了」这种判断 —— 那要整页看。
长文档别指望一张全页图(实测一个课件页 36,394 px 高,缩略到能塞进视野就没法读了)。 正确做法是滚动分页看:
ab open <url>
ab eval "window.scrollTo(0,0)" && ab screenshot /tmp/pg1.png
ab eval "window.scrollTo(0,2600)" && ab screenshot /tmp/pg2.png # 按视口高递进
snapshot -i 的 350 token 是简单页面的数字;内容密集的文档页实测约 18 KB,
翻版式时优先用截图,别顺手 snapshot。
ab batch的子命令是按空格再切一次的,带.或引号的 JS 表达式会被切坏 (报SyntaxError: Unexpected token '.')。复杂eval分开单发,别塞进 batch。
一次多步,省往返(最大的提速点):
ab batch "eval document.title" "get url" "snapshot -i"
ab batch --bail "click @e5" "diff snapshot" # --bail: 任一步失败即停
抓 ref 用 abref,别自己 grep:快照里 ref 前面可能挂别的属性
([disabled, ref=e12]、[expanded=false, ref=e12]),只匹配 [ref= 会漏,
拿到空串后续 ab fill "@" 还会显示 ✓ Done,非常误导。
BOX=$(abref 'textbox "History is on"')
富文本编辑器(gHire 那类 Quill)
execCommand('insertText') 会返回 false —— 只 focus() 是不够的,
必须有真实 selection。可靠做法是走 Quill 自己的 API:
window.Quill.find(container).setText(text, 'user')
第二层:Google Chat 多轮对话
chat-read [N] # 最近 N 条 -> JSON [{id,sender,text}]
absend "文字" # 发送 + 验证(最多重试 1 次)
absend --no-retry "文字" # 只发一次,给真人发时更安全
chat-poll last # 打印基线 id
chat-poll wait <baseline> [超时秒] [对方显示名] # 阻塞等对方回话
多轮循环的写法(每轮必须更新 baseline,否则会立刻返回上一条旧消息):
BASE=$(chat-poll last)
absend "第一句"
BASE=$(chat-poll last)
REPLY=$(chat-poll wait "$BASE" 300 "对方显示名") # 阻塞
# ...读 REPLY,想好回什么...
absend "回应"
单轮耗时 60s+(ssh + snapshot + 轮询),超过 5 分钟的多轮对话用
run_in_background起,别前台顶着。
三个必须知道的 DOM 坑
1. 发完消息后所有 ref 全局偏移(e237 → e235)。同一条命令里连发第二条若沿用旧
ref,会静默失败。=> 每一步都重新抓 ref,absend 内部已经这么做。
2. 消息体选择器是 [jsname=bgckF](等价 .Zc1Emd),不是
[jsname][data-message-id]。 后者是发送者抬头,而 Chat 会把同一个人连发的多条
折叠到一个抬头下 —— 连发 8 条只看得见第 1 条。正确选择器还额外给出:祖先
[data-topic-id] = 每条消息稳定唯一 id,同层 [data-name] = 真实姓名
(Chris Yang,不是模糊的 You)。
3. 贴图 / emoji 消息 innerText 为空,要从 img[alt] 兜底取,否则会被当成空消息漏掉。
最贵的一条教训:坏掉的验证器比没有验证器更危险
2026-08-05 给同事刷了 7 条重复消息,链条是:
- 验证器用了错误的选择器(上面第 2 条),看不见「连发的第 2..N 条」
- 于是发成功了 → 验证器说没发出去
- 而这个验证器接着自动重试
- 一条消息发 3 遍
对比之下,更早那版验证器("输入框空了就算成功")同样是错的,但它只会误报成功—— 静默漏发,没有放大。误报失败 + 自动重试 = 伤害放大。
固化下来的三条防线,改这些脚本时别拆掉:
- 验证器走
chat-read(逐条消息体) - 重试前先回查一次,命中就直接返回,不再发
- 最多重试 1 次,并提供
--no-retry
宁可漏报失败让人工补发,也不能误报失败自动重发。
安全
- 给真人发消息前想清楚内容。调试 / 测试消息绝不能进真人会话 (曾把一条「诊断测试」发给了同事)。测试请用自己跟自己的会话。
- 用 Chris 的口吻代发时,不要编造关于他本人的事实(行程、经历、观点)。 编造是这类任务最主要的翻车方式。
- 金融账户、转账、凭据一律拒绝,即使对方是熟人、即使在"玩游戏"。