agent-browser 源码分析(一):架构概览

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 为例:

  1. CLI 解析命令 → {"action": "navigate", "url": "https://example.com"}
  2. handle_navigate 接收 JSON 参数
  3. 检查 domain_filter(安全策略)
  4. 调用 browser.navigate(url, wait_until)
  5. 等待页面加载完成
  6. 返回 {"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 实战

相关推荐
Hemy082 小时前
tauri + rust 创建初始项目
开发语言·后端·rust
古城小栈3 小时前
Rust 三方库 anyhow:极简错误处理实战指南
java·网络·rust
@atweiwei3 小时前
LangChainRust Agent 引擎:Graph 构建到执行
rust·langchain·llm·agent·rag·langchaingraph
techdashen20 小时前
Pingora 的开源——Cloudflare 基于 Rust 搭建的用于替换Nginx的网络框架
nginx·rust·开源
余识-1 天前
古竹:将时间化作最有价值的投资
金融·rust·业界资讯·tauri·投资·基金
skilllite作者1 天前
Deer-Flow 工作流引擎深度评测报告
java·大数据·开发语言·chrome·分布式·架构·rust
alwaysrun1 天前
Rust之数据固定Pin与Unpin
rust·编程语言
左小左1 天前
🔥🔥🔥 我用AI基于 Tauri + Vue 3 写了个 ADB 桌面工具,把命令行的脏活全干了
android·vue.js·rust