数据截至 (上游 commit 548b159b30ee)
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 入口:普通 CLI、MCP 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.rs、cli/src/commands.rs |
| 客户端连接层 | socket 路径计算、daemon 自我 spawn、发送/接收 JSON | cli/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.rs、element.rs (RefMap) |
| 交互层 | 把 ref/选择器解析成坐标,派发真实鼠标键盘事件 | cli/src/native/element.rs、interaction.rs |
| Agent 入口 | MCP server、自然语言 chat 循环 | cli/src/mcp.rs、cli/src/chat.rs、stream/chat.rs |
| 安全护栏 | 域名白名单、动作策略、输出上限 | cli/src/native/network.rs、policy.rs |
主线走一遍(高层,不进代码)
以 agent-browser click @e2 为例,端到端:
- CLI 解析(
main.rs→commands.rs):把click @e2解析成{"action":"click","selector":"@e2"}之类的 JSON。 - 确保 daemon(
connection.rs的ensure_daemon):看 socket 是否可连;不可连就把自己再 exec 一遍、带上AGENT_BROWSER_DAEMON=1环境变量,让那个副本进入 daemon 模式。 - 发命令:CLI 把 JSON 写进 unix socket(每条命令一行,换行分隔)。
- daemon 收命令(
daemon.rs的handle_connection):逐行读、反序列化,交给execute_command。 - 动作分发(
actions.rs):"click"匹配到handle_click。 - 解析 ref → 坐标(
element.rs的resolve_element_center):@e2查RefMap拿到backendNodeId,经 CDPDOM.getBoxModel拿到盒模型中心点(x, y);顺手做**「点击点被谁挡住了」**的命中测试。 - 派发点击:CDP
Input.dispatchMouseEvent在(x, y)真点一下。 - 回响应: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.rs | main、AGENT_BROWSER_DAEMON 分支 |
| 命令解析 | cli/src/commands.rs | parse_command |
| 确保 daemon / 自我 spawn | cli/src/connection.rs | ensure_daemon、apply_daemon_env |
| socket 路径策略 | cli/src/connection.rs | get_socket_dir |
| daemon 主循环 | cli/src/native/daemon.rs | run_daemon、handle_connection |
| 动作总分发 | cli/src/native/actions.rs | execute_command |
| CDP 客户端 | cli/src/native/cdp/client.rs | CdpClient、connect_with_headers |
| 快照渲染 | cli/src/native/snapshot.rs | take_snapshot、INTERACTIVE_ROLES |
| ref 映射表 | cli/src/native/element.rs | RefMap、RefEntry、parse_ref |
| ref→坐标 + 遮挡校验 | cli/src/native/element.rs | resolve_element_center、check_node_interception |
| MCP server | cli/src/mcp.rs | run_mcp、call_tool、list_tools |
| chat 系统提示 / 工具 | cli/src/native/stream/chat.rs | get_system_prompt、CHAT_TOOLS |
| 域名白名单 | cli/src/native/network.rs | DomainFilter、is_allowed |
| 动作策略 | cli/src/native/policy.rs | ActionPolicy、PolicyResult |