开始 / Getting Started

它怎么工作

一条 chrome-use 命令从终端出发,要经过四个进程才碰到页面:CLI 本身、每个会话一个的守护进程、 Chrome 为扩展拉起的中继进程、浏览器里的 ab-connect 扩展。这一页把整条链路拆开:每一站在哪个进程里、说什么协议、 状态放在谁手上。读完你就能解释 relay-cdp-url 是什么文件、@ref 为什么会失效、 以及为什么装一个扩展而不是开调试端口。

一条命令走过的四站

chrome-use open https://example.com 为例,完整链路是:

chrome-use open <url> CLI · 执行完即退出 Unix 域套接字 ~/.chrome-use/<session>.sock · 一行 JSON 往返 守护进程 daemon 每个 --session 一个 · 常驻后台 持有 CDP 连接 · @ref 映射 · 脚本引擎 标准 CDP over WebSocket · ws://127.0.0.1:<port>/<guid> 中继 chrome-use __nm-host Chrome 为扩展拉起的进程 本地应答 Target.* 发现命令 · 其余转发 Chrome 原生消息 · stdio(4 字节长度前缀 + JSON 帧) ab-connect 扩展 MV3,装一次 · chrome.debugger 按标签页附加 你已登录的真实 Chrome 标签页 真实 cookie · 真实指纹 · 站点读成真人

每一段都是标准协议,没有私有魔法:CLI 到守护进程是本机套接字,守护进程到中继是普通 CDP, 中继到扩展是 Chrome 原生消息,扩展到页面是 chrome.debugger(和 DevTools 同一套)。

CLI 很薄,状态都在守护进程里

chrome-use 这个二进制在 CLI 模式下不持有任何浏览器状态。它把命令序列化成一行 JSON, 写进 Unix 域套接字,读回一行 JSON,打印,退出。套接字路径是 ~/.chrome-use/<session>.sock (有 XDG_RUNTIME_DIR 时用 $XDG_RUNTIME_DIR/chrome-use/,见 cli/src/native/daemon.rsget_daemon_socket_dir)。Windows 没有 Unix 套接字,改用 127.0.0.1 上按会话名哈希出的 TCP 端口。

第一次对某个会话发命令时,CLI 发现套接字不存在,就先 fork 出守护进程再重试。守护进程旁边还有几个 sidecar 文件:<session>.pid<session>.version<session>.stream(实时流服务的端口)。

守护进程持有三样跨命令的东西:

  1. 到浏览器的 CDP 连接(CdpClient
  2. @ref 到 DOM 节点的映射表(RefMap,见下文 snapshot 一节)
  3. chrome-use script 的 JS 引擎(boa,纯 Rust)。引擎跑在守护进程里而不是页面里,所以页面硬跳转时脚本状态不丢,详见 单次成型脚本
ℹ️ 为什么守护进程常驻
CDP 连接和 @ref 映射建立一次就复用,省掉每条命令重新连、重新快照的开销。这也是 batchscript 能「一次往返跑完多步」的底座。守护进程闲置约 10 分钟自动退出,清掉它开的临时标签。

中继进程是 Chrome 自己拉起来的

第三站最容易让人迷糊:中继不是你启动的,是 Chrome 启动的。

装扩展时,chrome-use extension install 往 Chrome 的原生消息宿主目录写一份 manifest (宿主名 com.leeguoo.chrome_use,兼容旧名 com.agent_browser.connect),里面登记了 chrome-use 二进制的路径和允许对话的扩展 id。之后 ab-connect 扩展一调用 connectNative, Chrome 就以隐藏的 __nm-host 模式拉起 chrome-use(cli/src/connect.rsrun_nm_host), 两边用 stdio 说原生消息协议:4 字节小端长度前缀加一个 JSON 帧。

这个 nm-host 进程做两件事:

  1. 在 127.0.0.1 上随机绑一个端口,开一个 WebSocket 端点 ws://127.0.0.1:<port>/<guid>,把 URL 写进 ~/.chrome-use/relay-cdp-url,文件权限 0600。guid 不可猜、文件只有当前用户可读,所以本机其他进程拿不到入口。守护进程读这个文件,把它当成一个普通 Chrome 的 CDP 端点去连。
  2. 在扩展协议和标准 CDP 之间做翻译,并本地应答 Target.getTargets 这类发现命令,避免每次都往扩展绕一圈(cli/src/native/relay.rs)。
⚠️ 排障锚点
~/.chrome-use/relay-cdp-url 不存在,多半是中继没起来(扩展没连上); nm-host.logstdin EOF 一般是 MV3 后台 worker 被 Chrome 回收了。 细节见 故障排查

扩展端只做一件事

ab-connect 是个 MV3 扩展,核心就是拿 chrome.debugger 按标签页执行 CDP 命令,把结果通过原生消息交回中继。 权限足迹是 7 项,没有 <all_urls>。两条对指纹重要的性质:

  1. 只附加 agent 自己的标签。你正在看的页面不会被 attach,也就不会出现「chrome-use 已开始调试此浏览器」的横幅(扩展 0.5.5 起,只 attach agent 拥有的标签)。
  2. 这条路径不注入任何 JS 补丁。页面看到的就是你真实的 Chrome、真实的 cookie、真实的指纹,所以 CreepJS 测出来是 0% bot。反检测靠的不是伪装,是根本没有东西需要伪装(见 反检测与隐身)。

换句话说,扩展是整条链路里唯一碰得到页面的进程,而它碰页面的方式和 DevTools 是同一套协议。

为什么装扩展,而不是开 --remote-debugging-port

驱动真实 Chrome 的传统做法是加 --remote-debugging-port=9222 然后直连 CDP。这条路从 Chrome 136 起废了一半: 每次有客户端连上来,Chrome 都弹一个阻塞式的「Allow remote debugging?」确认框。agent 每连一次你点一次,自动化无从谈起。 而且端口要在启动 Chrome 前就开好,等于要求用户换一种方式启动日常浏览器。

原生消息没有这个问题,因为信任关系是反过来的:不是外部进程去敲 Chrome 的端口,而是 Chrome 根据宿主 manifest 里 登记的扩展 id,主动拉起并认证这个宿主。装扩展点一次「添加至 Chrome」,之后每次使用零确认。本机安全性由两层兜住: 只有 manifest 里登记的扩展能启动宿主;中继的 WebSocket URL 带不可猜的 guid,写在 0600 权限的文件里。

两种模式

上面讲的四站链路是默认模式:连你真实的、已登录的 Chrome。还有第二条路,--launch

bash
# 默认:走扩展中继,用你的真实 Chrome
$ chrome-use open https://example.com
# 另开一个隔离的隐身 Chromium
$ chrome-use --launch open https://example.com
扩展中继(默认)--launch
浏览器你正在用的那个 Chrome守护进程自己拉起的独立 Chromium
登录态全部现成空的临时 profile(--profile 可指定持久 profile)
链路daemon → 中继 → 扩展 → chrome.debuggerdaemon 直连 CDP,没有中继和扩展
反检测不需要:本来就是真浏览器,零注入原生覆盖(改 UA、时区、webdriver 等,见 stealth.rs),不打 JS 补丁
适用需要登录态、过反爬、用户随时接管干净环境测试、并行跑一次性任务

两种模式共用同一个守护进程架构和同一套命令,区别只在守护进程连的是谁。

每个 --session 一个彩色标签组

多个 agent 共用一个真实 Chrome 时,隔离靠标签组:

bash
$ chrome-use --session research open https://a.com   # research 组
$ chrome-use --session shopping open https://b.com   # shopping 组,互不可见

守护进程启动时把会话名记进 DAEMON_SESSION,它开的每个标签都落进这个会话专属的彩色 Chrome 标签组。 隔离在中继层强制执行(relay.rsclient_groups):守护进程连上中继后先声明自己的组, 此后它的 Target.getTargets 只返回本组的标签。它看不见你的标签,也看不见其他 agent 的标签,自然也就点不到。

细节都有归属规则:新标签打上创建者的组标签;页面弹出的 pop-up 继承 opener 的组,跨进程跳转(比如 OAuth 弹窗)不会跑丢; chrome-use adopt 是唯一的跨组通道,收养后那一个标签被重新打进收养者的组,其余照旧不可见。会话与持久化细节见 会话与持久化

snapshot 和 @ref 从哪里来

chrome-use snapshot -i 输出的那棵文本树不是 HTML,是无障碍树(accessibility tree)。守护进程通过 CDP 的 Accessibility.getFullAXTree 拿到整棵 AX 树(cli/src/native/snapshot.rs),过滤出 button、link、 textbox 等可交互角色,渲染成带缩进的文本:

button "提交" [ref=e12]
textbox "邮箱" [ref=e13]

每个 [ref=eN] 是守护进程在生成快照时分配的编号,背后是 RefMapcli/src/native/element.rs): ref 映射到 CDP 的 backend_node_id,也就是那个 DOM 节点在浏览器进程里的稳定句柄。chrome-use click @e12 时,守护进程查表拿到节点 id,直接对节点发操作,不需要 CSS 选择器,也不需要坐标。由此推出两条使用规则:

  1. ref 是守护进程的内存状态。守护进程重启(比如闲置退出)后旧 ref 全部作废,重新 snapshot 即可。
  2. 页面大改后 ref 可能指向已不存在的节点。RefMap 存了每个 ref 的角色、名称等指纹,--heal 能据此把 ref 重新定位到最接近的节点。定位细节见 定位元素

出身

chrome-use 最初基于 vercel-labs/agent-browser(Apache-2.0)。CLI 骨架和部分命令语义来自上游, 但隐身与扩展中继架构、反检测、humanize、多 agent 隔离、以及本页讲的整条 nm-host 链路都是本项目自己长出来的, 现在是独立项目。完整的分叉说明见 血统与上游

🔧 想动手验证
AGENT_BROWSER_DEBUG=1 让守护进程把日志写到 ~/.chrome-use/<session>.logcat ~/.chrome-use/relay-cdp-url 看中继的实时端点;chrome-use doctor 把每一站的健康状况报一遍。 排障见 故障排查,扩展安装见 驱动真实 Chrome,反检测数据见 反检测与隐身