跳到主要内容

agent-browser — 架构与原理

30 秒导读: agent-browser 是一个原生 Rust 写的浏览器自动化 CLI,专门给 AI agent 用。它的核心招数是:不让 agent 去猜 CSS 选择器,而是把网页的**无障碍树(accessibility tree)**渲染成一棵带编号的文本,每个可交互元素配一个稳定的引用号(@e2@e3)。agent 先 snapshot 看树,再 click @e2 点击——稳、准、token 省。

1. 这是什么(零基础也能懂)

一句话定义: 一个命令行工具,让程序(尤其是 AI)能像人一样开浏览器、点按钮、填表单、截图、读网页——而且是为「机器读得懂、点得准」专门设计的。

解决什么问题 / 给谁用。 假设你写了个 AI agent,要它「上某网站登录、搜索、把结果抓回来」。传统做法是让模型生成 CSS 选择器(#login-btn)或 XPath,但模型经常猜错:类名变了、按钮在 iframe 里、被 cookie 横幅挡住。agent-browser 把这件事反过来——先把页面真实结构喂给 agent,agent 只需引用其中的编号。

它能做什么(功能):

  • 启动 / 连接 Chrome(也支持 Lightpanda 引擎),无需 Playwright 或 Node 运行时
  • snapshot 把页面无障碍树输出成带 @e 引用的文本(AI 首选)
  • 点击 / 双击 / 填写 / 选择 / 拖拽 / 上传 / 滚动 / 截图 / 存 PDF
  • 语义定位(按 ARIA role、文本、label、testid 找元素)
  • 读 cookie / localStorage、保存恢复登录态、网络拦截、React 树检视
  • 三种 agent 入口:普通 CLIMCP server(给支持 MCP 的客户端)、chat 自然语言模式
  • 安全护栏:域名白名单、动作策略(allow/deny/confirm)、输出上限

用起来什么样(README「Quick Start」):

agent-browser open example.com
agent-browser snapshot # 拿到带 @e 引用的无障碍树
agent-browser click @e2 # 用 snapshot 里的引用号点击
agent-browser fill @e3 "test@example.com" # 用引用号填表
agent-browser get text @e1 # 用引用号取文本
agent-browser close

snapshot 的输出长这样(示意,基于无障碍树渲染逻辑):

- button "Sign in" [ref=e1]
- textbox "Email" [ref=e2]
- textbox "Password" [ref=e3]
- link "Forgot password?" [ref=e4]

agent 看到这棵树,就知道 @e2 是邮箱框,直接 fill @e2 ...,不用碰 CSS

一句话直觉/类比。 把它想成「给页面元素发号牌」:你进银行先抽号(snapshot 给每个可交互元素发 @e1/@e2/...),办业务时报号牌(click @e2),柜员(daemon)凭号牌找到你。号牌比「那个穿蓝衣服站在第三个窗口的人」(CSS 选择器)稳定得多。

2. 顶层全景(它大概怎么转)

三层结构

这个项目最该先记住的一张图,是**「瘦客户端 + 常驻 daemon + Chrome」三层**:

你/agent 敲的命令 常驻后台进程 真正的浏览器
┌─────────────────┐ JSON over ┌──────────────────┐ CDP over ┌──────────┐
│ agent-browser │ unix socket │ daemon 进程 │ WebSocket │ Chrome │
│ (CLI 一次性进程) │ ─────────────► │ (持有 Chrome 连接) │ ───────────► │ (CDT 调试)│
│ main.rs │ ◄───────────── │ daemon.rs │ ◄─────────── │ │
└─────────────────┘ JSON 响应 └──────────────────┘ CDP 事件 └──────────┘
解析命令 每行一个 JSON 把动作翻成 CDP 命令
确保 daemon 活着 串行执行动作 维护 ref→节点 映射

为什么要 daemon? 因为 CLI 每次执行完就退出。如果每条命令都重启 Chrome,既慢又丢状态(登录、cookie、打开的标签页全没了)。daemon 常驻、持有 Chrome 的 CDP 连接,多条 CLI 命令复用同一个浏览器会话

部件一句话职责

部件干什么在哪个文件
CLI 主程序解析命令行、确保 daemon 活着、把命令发过 socket、打印响应cli/src/main.rscli/src/commands.rs
客户端连接层socket 路径计算、daemon 自我 spawn、发送/接收 JSONcli/src/connection.rs
daemon 主循环监听 socket、逐行读 JSON 命令、串行交给动作层cli/src/native/daemon.rs
动作分发action 字段映射到 handle_* 函数(150+ 个动作)cli/src/native/actions.rs (execute_command)
CDP 客户端经 WebSocket 跟 Chrome 说 CDP 协议,管请求/响应/事件cli/src/native/cdp/client.rs (CdpClient)
快照与 ref取无障碍树、给可交互元素发 @e 引用、渲染成文本cli/src/native/snapshot.rselement.rs (RefMap)
交互层把 ref/选择器解析成坐标,派发真实鼠标键盘事件cli/src/native/element.rsinteraction.rs
Agent 入口MCP server、自然语言 chat 循环cli/src/mcp.rscli/src/chat.rsstream/chat.rs
安全护栏域名白名单、动作策略、输出上限cli/src/native/network.rspolicy.rs

主线走一遍(高层,不进代码)

agent-browser click @e2 为例,端到端:

  1. CLI 解析(main.rscommands.rs):把 click @e2 解析成 {"action":"click","selector":"@e2"} 之类的 JSON。
  2. 确保 daemon(connection.rsensure_daemon):看 socket 是否可连;不可连就把自己再 exec 一遍、带上 AGENT_BROWSER_DAEMON=1 环境变量,让那个副本进入 daemon 模式。
  3. 发命令:CLI 把 JSON 写进 unix socket(每条命令一行,换行分隔)。
  4. daemon 收命令(daemon.rshandle_connection):逐行读、反序列化,交给 execute_command
  5. 动作分发(actions.rs):"click" 匹配到 handle_click
  6. 解析 ref → 坐标(element.rsresolve_element_center):@e2RefMap 拿到 backendNodeId,经 CDP DOM.getBoxModel 拿到盒模型中心点 (x, y);顺手做**「点击点被谁挡住了」**的命中测试。
  7. 派发点击:CDP Input.dispatchMouseEvent(x, y) 真点一下。
  8. 回响应:daemon 把成功/失败 JSON 写回 socket;CLI 打印,退出。

3. 核心原理(逐个机制)

本项目的精华集中在四条主线,每条单独成章。建议按顺序读——前一章是后一章的地基:

  • 01 — 快照与 @e 引用: 全项目最核心的「为什么」。无障碍树怎么取、哪些元素配引用号、RefMap 怎么把 @e2 和真实 DOM 节点绑在一起。读懂这章就读懂了 agent-browser 的存在理由。
  • 02 — CLI / daemon / CDP 三层: 进程架构。瘦 CLI 怎么自我 spawn 出 daemon、socket 协议、daemon 主循环、CDP 客户端的请求/事件多路复用。
  • 03 — 动作落地: ref 怎么变成屏幕坐标、为什么点击会「提前失败」(被横幅挡住)、stale ref 的容错重查。
  • 04 — agent 入口与安全: 同一套动作如何同时暴露成 CLI、MCP 工具、和自然语言 chat;以及域名白名单 / 动作策略这两道护栏。

4. 巧妙之处(精华预览)

几个值得带走的设计,详见各章:

  • 用无障碍树而非 DOM 当 agent 的「视野」。 AX 树天然去掉了不可见 / 装饰性节点,role+name 比 class 稳定,token 也更省(第 01 章)。
  • CLI 自我 re-exec 成 daemon。 不打包两个二进制,同一个 exe 靠 AGENT_BROWSER_DAEMON 环境变量切换身份(第 02 章,connection.rs:ensure_daemon)。
  • 点击前先命中测试。 在派发点击前,用 JS 在目标坐标做 elementFromPoint,若命中的是别的元素(cookie 横幅、模态框)就提前报错并指名遮挡者,而不是盲点(第 03 章,element.rs:check_node_interception)。
  • CLI / MCP 严格对齐。 MCP 工具尽量复用 CLI 解析器,parity_tests.rs 专门证明两个面行为一致(第 04 章)。
  • 关闭走优雅信号而非 process::exit 避免跳过析构函数留下孤儿 Chrome 进程(daemon.rs,issue #1113)。

5. 边界与局限(诚实)

  • 本文档未覆盖的子系统: Lightpanda 引擎适配(cdp/lightpanda.rs)、WebDriver/iOS/Appium 后端(native/webdriver/)、dashboard 前端(packages/dashboard)、install.rs 的 Chrome 下载、stream/dashboard.rs 的可视化协议。它们存在,但不在主线讲解范围内。
  • 平台差异: Unix 用 unix domain socket,Windows 没有 .sock,改用 TCP + .port 文件(daemon.rs)。
  • chat 依赖外部网关: 自然语言 chat 需要 AI_GATEWAY_API_KEY,否则直接退出(chat.rs)。

6. 代码地图(导航索引)

主题文件路径关键符号
CLI 入口 / daemon 模式切换cli/src/main.rsmainAGENT_BROWSER_DAEMON 分支
命令解析cli/src/commands.rsparse_command
确保 daemon / 自我 spawncli/src/connection.rsensure_daemonapply_daemon_env
socket 路径策略cli/src/connection.rsget_socket_dir
daemon 主循环cli/src/native/daemon.rsrun_daemonhandle_connection
动作总分发cli/src/native/actions.rsexecute_command
CDP 客户端cli/src/native/cdp/client.rsCdpClientconnect_with_headers
快照渲染cli/src/native/snapshot.rstake_snapshotINTERACTIVE_ROLES
ref 映射表cli/src/native/element.rsRefMapRefEntryparse_ref
ref→坐标 + 遮挡校验cli/src/native/element.rsresolve_element_centercheck_node_interception
MCP servercli/src/mcp.rsrun_mcpcall_toollist_tools
chat 系统提示 / 工具cli/src/native/stream/chat.rsget_system_promptCHAT_TOOLS
域名白名单cli/src/native/network.rsDomainFilteris_allowed
动作策略cli/src/native/policy.rsActionPolicyPolicyResult