agent-browser 架构概览:从 CLI 到 CDP 的分层设计
本文是「agent-browser 代码原理」系列第 1 篇,基于 vercel-labs/agent-browser v0.26.0 源码分析。
一、agent-browser 是什么
agent-browser 是 Vercel Labs 开源的浏览器自动化 CLI 工具,专为 AI Agent 设计。它的核心定位是**"AI 友好的浏览器自动化"**------输出 accessibility tree 而非原始 DOM,让 LLM 能直接理解页面结构。
与传统工具对比:
| 工具 | 核心语言 | 输出格式 | 定位 |
|---|---|---|---|
| Playwright | Node.js | DOM / Selector | 测试框架 |
| Puppeteer | Node.js | DOM / Selector | 浏览器控制 |
| agent-browser | Rust | Accessibility Tree | AI Agent 专用 |
二、整体架构分层
agent-browser 的核心代码约 36,000 行 Rust,分布在 cli/src/native/ 目录下。整体架构可分为五个层次:
┌─────────────────────────────────────────┐
│ Layer 5: CLI 接口层 │
│ - 命令解析(open/snapshot/click/fill) │
│ - JSON-RPC over stdio / HTTP daemon │
├─────────────────────────────────────────┤
│ Layer 4: Action 处理层 │
│ - handle_navigate / handle_evaluate │
│ - handle_snapshot / handle_click │
│ - 文件: actions.rs (9000+ 行) │
├─────────────────────────────────────────┤
│ Layer 3: Browser 管理层 │
│ - Chrome 进程启动/发现/清理 │
│ - 页面导航(含 waitUntil 策略) │
│ - 多 Target/Session 管理 │
│ - 文件: browser.rs (2200 行) │
├─────────────────────────────────────────┤
│ Layer 2: CDP 客户端层 │
│ - WebSocket 连接管理 │
│ - 命令/响应 id 匹配 │
│ - 事件广播(broadcast channel) │
│ - 文件: cdp/client.rs (360 行) │
├─────────────────────────────────────────┤
│ Layer 1: 传输层 │
│ - WebSocket(tokio-tungstenite) │
│ - TCP Keepalive + Ping 保活 │
│ ↕ │
│ Chrome DevTools Protocol │
└─────────────────────────────────────────┘
三、Layer 5: CLI 接口层
agent-browser 提供两种使用模式:
3.1 单次命令模式
bash
agent-browser open example.com
agent-browser snapshot
agent-browser click @ref_7
每次命令独立执行,完成后退出。这是最常见用法。
3.2 Daemon 模式
bash
agent-browser daemon
启动一个常驻进程,通过 JSON-RPC over stdio 或 HTTP 接收命令。适合需要保持浏览器会话的场景。
Daemon 的核心状态机 (daemon.rs):
rust
pub struct DaemonState {
pub browser: Option<BrowserManager>,
pub webdriver_backend: Option<WebDriverBackend>,
pub ref_map: RefMap,
pub iframe_sessions: HashMap<String, String>,
pub active_frame_id: Option<String>,
// ... 网络拦截、Cookie、路由等状态
}
四、Layer 4: Action 处理层
actions.rs 是整个项目最大的文件(9000+ 行),包含所有用户命令的处理逻辑。
4.1 命令分发
每个 CLI 命令对应一个 handle_* 函数:
rust
async fn handle_navigate(cmd: &Value, state: &mut DaemonState) -> Result<Value, String>;
async fn handle_evaluate(cmd: &Value, state: &DaemonState) -> Result<Value, String>;
async fn handle_snapshot(cmd: &Value, state: &mut DaemonState) -> Result<Value, String>;
async fn handle_click(cmd: &Value, state: &mut DaemonState) -> Result<Value, String>;
async fn handle_screenshot(cmd: &Value, state: &mut DaemonState) -> Result<Value, String>;
async fn handle_route(cmd: &Value, state: &mut DaemonState) -> Result<Value, String>;
// ... 共 30+ 个 handle 函数
4.2 命令执行流程
以 agent-browser open https://example.com 为例:
- CLI 解析命令 →
{"action": "navigate", "url": "https://example.com"} handle_navigate接收 JSON 参数- 检查
domain_filter(安全策略) - 调用
browser.navigate(url, wait_until) - 等待页面加载完成
- 返回
{"url": "...", "title": "..."}
rust
// actions.rs:handle_navigate 核心逻辑
async fn handle_navigate(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
let url = cmd.get("url").and_then(|v| v.as_str())
.ok_or("Missing 'url' parameter")?;
// 安全检查
let df = state.domain_filter.read().await;
if let Some(ref filter) = *df {
filter.check_url(url)?;
}
// WebDriver backend 路径
if let Some(ref wb) = state.webdriver_backend {
if state.browser.is_none() {
wb.navigate(url).await?;
let new_url = wb.get_url().await.unwrap_or_else(|_| url.to_string());
let title = wb.get_title().await.unwrap_or_default();
return Ok(json!({ "url": new_url, "title": title }));
}
}
// CDP backend 路径
let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
let wait_until = cmd.get("waitUntil")
.and_then(|v| v.as_str())
.map(WaitUntil::from_str)
.unwrap_or(WaitUntil::Load);
state.ref_map.clear();
state.iframe_sessions.clear();
state.active_frame_id = None;
mgr.navigate(url, wait_until).await
}
五、Layer 3: Browser 管理层
browser.rs 负责 Chrome 进程的生命周期管理和页面操作。
5.1 Chrome 启动流程
rust
// browser.rs 伪代码
pub async fn launch(options: LaunchOptions) -> Result<BrowserManager, String> {
// 1. 查找 Chrome 可执行文件
let executable = find_chrome_executable(&options.executable_path)?;
// 2. 构建启动参数
let args = build_chrome_args(&options)?;
// 3. 启动子进程
let child = Command::new(&executable).args(&args).spawn()?;
// 4. 等待 DevToolsActivePort 文件
let (port, ws_path) = wait_for_devtools_port(&user_data_dir).await?;
// 5. 验证 WebSocket 端点
let ws_url = resolve_cdp_from_active_port(port, &ws_path).await?;
// 6. 建立 WebSocket 连接
let client = CdpClient::connect(&ws_url).await?;
Ok(BrowserManager { client, process: child, ... })
}
5.2 页面等待策略
agent-browser 支持三种页面加载完成判定:
rust
pub enum WaitUntil {
Load, // `Page.loadEventFired` 事件
DOMContentLoaded, // `Page.domContentEventFired` 事件
NetworkIdle, // 500ms 内没有新的网络请求
}
实现原理:监听 CDP 事件流,当满足条件时返回。
rust
pub async fn navigate(&mut self, url: &str, wait: WaitUntil) -> Result<Value, String> {
// 发送导航命令
let params = json!({ "url": url });
self.client.send_command("Page.navigate", Some(params), Some(session_id)).await?;
// 等待加载完成
match wait {
WaitUntil::Load => self.wait_for_event("Page.loadEventFired").await,
WaitUntil::DOMContentLoaded => self.wait_for_event("Page.domContentEventFired").await,
WaitUntil::NetworkIdle => self.wait_for_network_idle().await,
}
}
六、Layer 2: CDP 客户端层
cdp/client.rs 是 agent-browser 最精妙的模块之一,仅 360 行代码实现了一个生产级 WebSocket CDP 客户端。
6.1 核心数据结构
rust
pub struct CdpClient {
ws_tx: Arc<Mutex<SplitSink<WebSocketStream, Message>>>,
next_id: AtomicU64,
pending: Arc<Mutex<HashMap<u64, oneshot::Sender<CdpMessage>>>>,
event_tx: broadcast::Sender<CdpEvent>,
raw_tx: broadcast::Sender<RawCdpMessage>,
}
| 字段 | 作用 |
|---|---|
ws_tx |
WebSocket 发送端(Mutex 保护,支持多任务并发发送) |
next_id |
自增命令 ID(AtomicU64,线程安全) |
pending |
等待响应的命令映射(id → oneshot channel) |
event_tx |
事件广播发送器(支持多订阅者) |
raw_tx |
原始消息广播(inspect proxy 用) |
6.2 命令/响应匹配机制
CDP 是异步协议------发送命令后,响应可能夹杂在事件流中到达。agent-browser 通过 id 字段实现精确匹配:
rust
pub async fn send_command(&self, method: &str, params: Option<Value>, session_id: Option<&str>)
-> Result<Value, String>
{
// 1. 生成唯一 id
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
// 2. 构建命令 JSON
let cmd = CdpCommand { id, method: method.to_string(), params, session_id: ... };
let json = serde_json::to_string(&cmd)?;
// 3. 创建 oneshot channel 等待响应
let (tx, rx) = oneshot::channel();
{
let mut pending = self.pending.lock().await;
pending.insert(id, tx);
}
// 4. 发送 WebSocket 消息
{
let mut ws_tx = self.ws_tx.lock().await;
ws_tx.send(Message::Text(json)).await?;
}
// 5. 等待响应(30 秒超时)
let response = tokio::time::timeout(Duration::from_secs(30), rx).await?;
// 6. 处理错误
if let Some(error) = response.error {
return Err(format!("CDP error ({}): {}", method, error));
}
Ok(response.result.unwrap_or(Value::Null))
}
6.3 事件广播机制
Reader 任务持续从 WebSocket 读取消息,区分响应和事件:
rust
// Reader 任务(spawn 在独立 tokio 任务中)
while let Some(msg) = ws_rx.next().await {
let text = match msg {
Ok(Message::Text(t)) => t,
Ok(Message::Binary(d)) => String::from_utf8(d)?,
Ok(Message::Close(_)) => break,
_ => continue,
};
let parsed: CdpMessage = serde_json::from_str(&text)?;
if let Some(id) = parsed.id {
// 这是响应 ------ 找到 pending 中的 sender
let mut pending = pending_clone.lock().await;
if let Some(tx) = pending.remove(&id) {
let _ = tx.send(parsed);
}
} else if let Some(ref method) = parsed.method {
// 这是事件 ------ 广播给所有订阅者
let event = CdpEvent {
method: method.clone(),
params: parsed.params.clone().unwrap_or(Value::Null),
session_id: parsed.session_id.clone(),
};
let _ = event_tx_clone.send(event);
}
}
七、Layer 1: 传输层
传输层使用 tokio-tungstenite 实现 WebSocket 通信,并做了两个关键优化:
7.1 TCP Keepalive
rust
fn enable_tcp_keepalive(stream: &MaybeTlsStream<TcpStream>) {
let tcp_stream = match stream {
MaybeTlsStream::Plain(s) => s,
MaybeTlsStream::Rustls(s) => s.get_ref().0,
_ => return,
};
let sock = socket2::SockRef::from(tcp_stream);
let keepalive = TcpKeepalive::new()
.with_time(Duration::from_secs(30));
#[cfg(not(any(target_os = "openbsd", target_os = "haiku")))]
let keepalive = keepalive.with_interval(Duration::from_secs(10));
let _ = sock.set_tcp_keepalive(&keepalive);
}
7.2 WebSocket Ping 保活
rust
const WS_KEEPALIVE_INTERVAL_SECS: u64 = 30;
// 每 30 秒发送 Ping 帧
loop {
tokio::select! {
_ = tokio::time::sleep(interval) => {}
_ = cancel_rx.changed() => break,
}
let mut tx = keepalive_tx.lock().await;
if tx.send(Message::Ping(Vec::new())).await.is_err() {
break;
}
}
为什么需要两层保活? - TCP Keepalive:检测底层连接是否断开 - WebSocket Ping:防止中间代理(nginx、Envoy、云 LB)关闭空闲连接
八、多 Backend 架构
agent-browser 不仅支持 CDP,还抽象了 WebDriver 和 Appium 后端:
rust
pub enum BrowserBackend {
Cdp(CdpBackend),
WebDriver(WebDriverBackend),
Appium(AppiumManager),
}
Backend 选择逻辑:
用户命令 → Action 处理层
├── CDP backend(默认,功能最全)
│ ├── snapshot ✓
│ ├── network 拦截 ✓
│ └── 录屏 ✓
├── WebDriver backend(远程浏览器)
│ ├── snapshot ✗
│ ├── network 拦截 ✗
│ └── 基本操作 ✓
└── Appium backend(移动端)
├── iOS Safari ✓
└── Android Chrome ✓
所有 backend 实现相同的 BrowserBackend trait,上层代码无需关心底层协议差异。
九、总结
agent-browser 的架构设计体现了分层解耦 和多后端抽象的思想:
| 层级 | 核心职责 | 关键文件 |
|---|---|---|
| CLI | 命令解析、JSON-RPC | main.rs, daemon.rs |
| Action | 业务逻辑、状态管理 | actions.rs |
| Browser | 进程管理、页面导航 | browser.rs |
| CDP Client | WebSocket 通信、协议解析 | cdp/client.rs |
| Transport | TCP/WebSocket 连接 | tokio-tungstenite |
下一篇将深入剖析 CDP WebSocket 客户端的实现细节。
系列文章 : 1. agent-browser 架构概览:从 CLI 到 CDP 的分层设计 ← 本文 2. CDP WebSocket 客户端实现:命令/响应匹配与事件广播 3. Accessibility Tree 快照原理:如何让 AI 看懂网页 4. Chrome 进程管理与多 Backend 架构 5. Network 拦截与路由:Fetch Domain 实战