Skip to content

OpenClaw 源码解读(九)浏览器控制系统

本文基于 OpenClaw 2026.3.2 源码,深入解读浏览器控制系统(Browser Control System)的架构设计与实现细节。


一、模块概览

浏览器控制系统赋予 AI 助手操控真实浏览器的能力:打开网页、截图、读取页面内容、点击按钮、填写表单……它是 OpenClaw 最复杂的子系统之一,包含 120+ 个源文件,横跨 Chrome 进程管理、CDP 协议、Playwright 集成、HTTP API、Chrome 扩展中继等多个层面。

1.1 核心源码分布

目录/文件行数(约)职责
src/browser/server.ts~200Express 控制服务器(启动、挂载路由、绑定端口)
src/browser/server-context.ts~242路由上下文(Profile 选择、Tab 管理、浏览器生命周期代理)
src/browser/server-context.*.ts多个上下文功能拆分(availability、tab-ops、selection、reset)
src/browser/server-lifecycle.ts~48生命周期管理(Extension Relay 初始化、Profile 停止)
src/browser/routes/多个HTTP 路由层(basic、tabs、agent/act/snapshot/debug)
src/browser/chrome.ts~427Chrome 进程管理(启动、停止、健康检查、Profile 装饰)
src/browser/chrome.executables.ts~150+浏览器可执行文件发现(Chrome/Brave/Edge/Chromium 跨平台)
src/browser/cdp.ts~260+CDP 协议核心(WebSocket 通信、截图、JS 执行、AX 树)
src/browser/cdp.helpers.ts~200+CDP 辅助函数(fetch、socket、超时)
src/browser/pw-session.ts~800+Playwright 会话管理(CDP 连接复用、Page 状态跟踪、操作)
src/browser/extension-relay.ts~500+Chrome 扩展 CDP 中继服务器(WebSocket 桥接)
src/browser/navigation-guard.ts~104导航安全守卫(SSRF 防护、协议白名单)
src/browser/config.ts~300+配置解析与 Profile 配置
src/browser/profiles.ts~113CDP 端口分配与 Profile 颜色管理
src/browser/screenshot.ts~58截图归一化(尺寸/字节限制)
src/browser/client.ts~200+浏览器 HTTP 客户端(发起请求到控制服务器)
src/browser/client-actions.ts~300+客户端操作封装(navigate、act、screenshot 等)
src/browser/client-actions-core.ts~100+核心操作类型定义(FormField、ActKind)
src/browser/proxy-files.ts~40远程节点代理文件持久化
src/agents/tools/browser-tool.ts~639Agent 工具入口(16 种 action、节点路由)
src/agents/tools/browser-tool.schema.ts~138工具参数 Schema(TypeBox)
src/agents/tools/browser-tool.actions.ts~348工具操作执行器(snapshot、console、act)

1.2 系统全景架构

┌─────────────────────────────────────────────────────────────────────────────┐
│                         浏览器控制系统全景架构                                │
│                                                                              │
│  ┌─── AI Agent ──────────────────────────────────────────────────────────┐  │
│  │                                                                        │  │
│  │  browser-tool.ts  →  browser-tool.actions.ts                          │  │
│  │  (16 种 action)       (snapshot/console/act 执行器)                    │  │
│  │       │                                                                │  │
│  │       ├── target = "host" ─→ 本地浏览器控制服务器                       │  │
│  │       ├── target = "sandbox" ─→ 沙箱浏览器(Docker bridge)            │  │
│  │       └── target = "node" ─→ 远程节点代理(Node Proxy)               │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│              │                                                               │
│              ▼                                                               │
│  ┌─── 浏览器 HTTP 客户端 ────────────────────────────────────────────────┐  │
│  │                                                                        │  │
│  │  client.ts ←─── 发起 HTTP 请求 ───→ :18791/browser/*                  │  │
│  │  client-actions.ts                                                     │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│              │                                                               │
│              ▼                                                               │
│  ┌─── 浏览器控制服务器(Express :18791)─────────────────────────────────┐  │
│  │                                                                        │  │
│  │  server.ts ──→ routes/                                                 │  │
│  │                ├── basic.ts      GET  / | /profiles | POST /start|stop │  │
│  │                ├── tabs.ts       GET  /tabs | POST /tabs/open|close    │  │
│  │                └── agent.ts      POST /navigate|/act|/screenshot|...   │  │
│  │                    ├── agent.snapshot.ts  (快照 + 截图 + PDF)          │  │
│  │                    ├── agent.act.ts       (11 种操作)                  │  │
│  │                    └── agent.act.hooks.ts (upload + dialog)            │  │
│  │                                                                        │  │
│  │  server-context.ts ──→ ProfileContext                                  │  │
│  │  (Profile 选择 → 浏览器可用性 → Tab 管理)                             │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│              │                                                               │
│              ▼                                                               │
│  ┌─── 浏览器引擎层 ─────────────────────────────────────────────────────┐  │
│  │                                                                        │  │
│  │  ┌── Driver: "openclaw" ────────────────────────────────────────────┐ │  │
│  │  │                                                                    │ │  │
│  │  │  chrome.ts ──→ spawn() Chrome/Brave/Edge/Chromium               │ │  │
│  │  │  (进程启动/停止/Profile 装饰/健康检查)                            │ │  │
│  │  │       │                                                          │ │  │
│  │  │       ▼                                                          │ │  │
│  │  │  CDP 协议 (ws://127.0.0.1:{cdpPort})                            │ │  │
│  │  │  ├── cdp.ts ──→ 原始 CDP WebSocket 通信                         │ │  │
│  │  │  └── pw-session.ts ──→ Playwright CDP 连接(高层操作)           │ │  │
│  │  └────────────────────────────────────────────────────────────────────┘ │  │
│  │                                                                        │  │
│  │  ┌── Driver: "extension" ───────────────────────────────────────────┐ │  │
│  │  │                                                                    │ │  │
│  │  │  extension-relay.ts ──→ WebSocket 中继服务器                     │ │  │
│  │  │  (Chrome 扩展 ←WS→ Relay Server ←WS→ CDP 客户端)              │ │  │
│  │  └────────────────────────────────────────────────────────────────────┘ │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────┘

![浏览器控制系统全景架构](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(九)浏览器控制系统/01-infographic-system-overview-1775150649304.png)


二、双驱动架构

浏览器控制系统支持两种驱动模式(Driver),这是理解整个系统的关键:

2.1 OpenClaw 驱动(默认)

AI Agent ──→ HTTP API ──→ chrome.ts ──→ spawn Chrome ──→ CDP 直连

OpenClaw 自己启动和管理 Chrome 进程:

  • 自动检测系统上的 Chrome/Brave/Edge/Chromium
  • 使用独立的 user-data-dir(隔离于用户的正常 Chrome)
  • 通过 --remote-debugging-port 暴露 CDP 端口
  • 支持 headless 和 no-sandbox 模式

2.2 Extension 驱动(连接已有浏览器)

AI Agent ──→ HTTP API ──→ Extension Relay ──→ Chrome 扩展 ──→ CDP 转发

通过 Chrome 扩展连接用户已打开的浏览器

  • 不启动新进程
  • 通过 WebSocket 中继服务器桥接
  • 可以操作用户当前浏览的页面
  • 需要安装 OpenClaw Chrome 扩展

![双驱动架构对比](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(九)浏览器控制系统/02-infographic-dual-driver-1775150650079.png)


三、端口分配体系

系统有严格的端口分配方案,避免服务冲突:

18789 — Gateway WebSocket(核心控制平面)
18790 — Bridge(预留)
18791 — Browser 控制服务器
18792 — 预留
18793 — Canvas
18794-18799 — 预留
18800-18899 — CDP 端口池(最多 100 个 Profile)
typescript
// profiles.ts
export const CDP_PORT_RANGE_START = 18800;
export const CDP_PORT_RANGE_END = 18899;

export function allocateCdpPort(usedPorts: Set<number>): number | null {
    for (let port = CDP_PORT_RANGE_START; port <= CDP_PORT_RANGE_END; port++) {
        if (!usedPorts.has(port)) {
            return port;
        }
    }
    return null;
}

四、Profile 多实例系统

OpenClaw 支持同时运行多个浏览器实例,每个实例称为一个 Profile

4.1 Profile 配置

typescript
// config.ts
type ResolvedBrowserProfile = {
    name: string;           // "openclaw" | "chrome" | 自定义名
    cdpPort: number;        // CDP 调试端口
    cdpUrl: string;         // "http://127.0.0.1:18800"
    cdpIsLoopback: boolean; // 是否是本地回环地址
    color: string;          // Profile 标识颜色 (#FF4500)
    driver: "openclaw" | "extension";
    attachOnly: boolean;    // 仅连接不启动
};

4.2 Profile 颜色系统

每个 Profile 有独立的颜色标识,用于视觉区分:

typescript
// profiles.ts
export const PROFILE_COLORS = [
    "#FF4500", // Orange-red (默认)
    "#0066CC", // Blue
    "#00AA00", // Green
    "#9933FF", // Purple
    "#FF6699", // Pink
    "#00CCCC", // Cyan
    "#FF9900", // Orange
    "#6666FF", // Indigo
    "#CC3366", // Magenta
    "#339966", // Teal
];

颜色不仅用于 UI 展示,还会写入 Chrome 的 Profile 配置(decorateOpenClawProfile),修改浏览器的主题色,让用户一眼区分哪个 Chrome 窗口是 AI 控制的。

4.3 ProfileContext — 核心操作上下文

每个 Profile 对应一个 ProfileContext,封装了该 Profile 的所有操作:

typescript
// server-context.ts
type ProfileContext = {
    profile: ResolvedBrowserProfile;
    ensureBrowserAvailable: () => Promise<void>;    // 确保浏览器已启动
    ensureTabAvailable: (targetId?) => Promise<Tab>; // 确保有可用标签页
    isHttpReachable: (timeoutMs?) => Promise<boolean>;
    isReachable: (timeoutMs?) => Promise<boolean>;
    listTabs: () => Promise<BrowserTab[]>;
    openTab: (url: string) => Promise<BrowserTab>;
    focusTab: (targetId: string) => Promise<void>;
    closeTab: (targetId: string) => Promise<void>;
    stopRunningBrowser: () => Promise<{ stopped: boolean }>;
    resetProfile: () => Promise<{ ... }>;
};

server-context.ts 使用组合模式,将功能拆分到多个子模块:

createProfileContext()
    ├── createProfileTabOps()         → listTabs(), openTab()
    ├── createProfileAvailability()   → ensureBrowserAvailable(), stopRunningBrowser()
    ├── createProfileSelectionOps()   → ensureTabAvailable(), focusTab(), closeTab()
    └── createProfileResetOps()       → resetProfile()

![端口分配与 Profile 多实例体系](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(九)浏览器控制系统/03-infographic-port-profile-1775150651050.png)


五、Chrome 进程管理

5.1 浏览器可执行文件发现

chrome.executables.ts 实现了跨平台的浏览器发现:

平台搜索路径优先级
macOS/Applications/Google Chrome.appBrave Browser.appMicrosoft Edge.appChromium.appChrome > Brave > Edge > Chromium
Linuxgoogle-chrome-stablegoogle-chromebrave-browsermicrosoft-edge-stablechromium-browserchromium (PATH 查找)Chrome > Brave > Edge > Chromium
Windows%ProgramFiles%\Google\Chrome\Application\chrome.exe → Brave → Edge → ChromiumChrome > Brave > Edge > Chromium

5.2 Chrome 启动流程

typescript
// chrome.ts — launchOpenClawChrome()
async function launchOpenClawChrome(resolved, profile): Promise<RunningChrome> {
    // 1. 确保 CDP 端口可用
    await ensurePortAvailable(profile.cdpPort);

    // 2. 发现浏览器可执行文件
    const exe = resolveBrowserExecutable(resolved);

    // 3. 创建隔离的 user-data 目录
    const userDataDir = resolveOpenClawUserDataDir(profile.name);
    //    → ~/.openclaw/browser/{profileName}/user-data

    // 4. 首次启动引导(Bootstrap)
    //    如果 Preferences 文件不存在,先启动一次让 Chrome 创建默认配置
    if (needsBootstrap) {
        const bootstrap = spawnOnce();
        // 等待 Local State 和 Preferences 文件出现
        // 然后 SIGTERM 关闭
    }

    // 5. Profile 装饰(写入自定义颜色/名称到 Chrome 配置)
    if (needsDecorate) {
        decorateOpenClawProfile(userDataDir, { name, color });
    }

    // 6. 确保干净退出(清除 crash bubble)
    ensureProfileCleanExit(userDataDir);

    // 7. 正式启动
    const proc = spawn(exe.path, [
        `--remote-debugging-port=${profile.cdpPort}`,
        `--user-data-dir=${userDataDir}`,
        "--no-first-run",
        "--no-default-browser-check",
        "--disable-sync",
        "--disable-background-networking",
        "--disable-component-update",
        "--disable-features=Translate,MediaRouter",
        "--disable-session-crashed-bubble",
        "--hide-crash-restore-bubble",
        "--password-store=basic",
        "--disable-blink-features=AutomationControlled",  // 反检测
        ...(resolved.headless ? ["--headless=new", "--disable-gpu"] : []),
        ...(resolved.noSandbox ? ["--no-sandbox", "--disable-setuid-sandbox"] : []),
        ...resolved.extraArgs,
        "about:blank",
    ]);

    // 8. 等待 CDP 就绪(轮询 /json/version)
    while (Date.now() < readyDeadline) {
        if (await isChromeReachable(profile.cdpUrl)) break;
        await sleep(CHROME_LAUNCH_READY_POLL_MS);
    }

    // 9. 清理 stderr 监听(避免长期运行内存泄漏)
    proc.stderr?.off("data", onStderr);

    return { pid, exe, userDataDir, cdpPort, startedAt, proc };
}

![Chrome 启动流程 9 步](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(九)浏览器控制系统/04-infographic-chrome-launch-1775150652049.png)

5.3 Chrome 停止流程

SIGTERM ──→ 等待 CDP 不可达(轮询) ──→ 超时 ──→ SIGKILL

渐进式终止:先礼貌请求(SIGTERM),再强制杀死(SIGKILL)。

5.4 反自动化检测

javascript
"--disable-blink-features=AutomationControlled"

这个标志隐藏了 navigator.webdriver 属性,防止网站通过 WebDriver 检测判断浏览器被自动化控制。


六、CDP 协议层

6.1 原始 CDP 通信(cdp.ts)

CDP(Chrome DevTools Protocol)是 Chrome 暴露的调试协议,OpenClaw 通过两种方式使用它:

方式一:原始 WebSocketcdp.helpers.tswithCdpSocket

typescript
// 直接建立 WebSocket 连接,发送 CDP 命令
async function captureScreenshot(opts: { wsUrl: string }) {
    return await withCdpSocket(opts.wsUrl, async (send) => {
        await send("Page.enable");
        const result = await send("Page.captureScreenshot", {
            format: "png",
            fromSurface: true,
            captureBeyondViewport: true,
        });
        return Buffer.from(result.data, "base64");
    });
}

方式二:Playwright CDP 连接pw-session.ts

typescript
// 通过 Playwright 连接到 Chrome 的 CDP 端口
const browser = await chromium.connectOverCDP(cdpUrl);
const page = browser.contexts()[0].pages()[0];
// 然后使用 Playwright 的高级 API

6.2 两种方式的选择策略

操作使用方式原因
截图(无 ref/element)CDP 原始简单高效,不需要 Playwright
截图(有 ref/element)Playwright需要元素定位
ARIA 快照CDP 原始或 Playwright取决于 driver 类型
AI 快照Playwright需要 _snapshotForAI 私有 API
点击/输入/拖拽等操作Playwright需要元素定位和交互模拟
JS 执行CDP 原始简单直接
导航Playwright需要等待加载完成

6.3 ARIA 可访问性快照

这是浏览器控制系统最核心的功能之一。AI 无法"看"网页,但可以通过 ARIA 可访问性树(Accessibility Tree)"读"网页:

typescript
// cdp.ts
export async function snapshotAria(opts: { wsUrl: string; limit: number }) {
    return await withCdpSocket(opts.wsUrl, async (send) => {
        // 获取完整的可访问性树
        const result = await send("Accessibility.getFullAXTree");
        // 格式化为结构化的节点列表
        return formatAriaSnapshot(result.nodes, limit);
    });
}

输出格式:

ax1  role=WebArea  name="Google"  depth=0
ax2  role=search   name="Search"  depth=1
ax3  role=textbox  name="Search"  value=""  depth=2
ax4  role=button   name="Google Search"  depth=2

AI 通过 ref(如 ax3)来引用页面元素,实现点击、输入等操作。

6.4 AI 快照(Playwright 增强)

除了 ARIA 快照,系统还支持AI 快照(通过 Playwright 的 _snapshotForAI 私有 API 或 snapshotRoleViaPlaywright):

  • ARIA 格式:基于 CDP Accessibility API,轻量但信息有限
  • AI 格式:基于 Playwright 的 Role Snapshot,支持更多功能:
    • interactive 模式:只显示可交互元素
    • compact 模式:压缩输出
    • maxDepth 限制深度
    • labels 模式:生成带标签的截图(视觉 + 文本双通道)
    • selector / frameSelector:限定范围

6.5 标签截图(Labels)

当 AI 需要视觉参考时,可以请求带标签的截图:

snapshot(labels=true) →
    1. 获取 Role Snapshot(得到 refs 映射)
    2. screenshotWithLabelsViaPlaywright() → 在元素上覆盖数字标签
    3. normalizeBrowserScreenshot() → 压缩到合适大小
    4. 返回:快照文本 + 标注图片

AI 看到的效果类似于:图片上每个可交互元素旁边有一个数字标签(如 [1] [2] [3]),对应 snapshot 中的 ref。

![CDP 协议选择策略与快照体系](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(九)浏览器控制系统/05-infographic-cdp-snapshot-1775150652891.png)


七、Playwright 会话管理

7.1 连接复用

typescript
// pw-session.ts
let cached: ConnectedBrowser | null = null;
let connecting: Promise<ConnectedBrowser> | null = null;

Playwright 的 CDP 连接是单例复用的:

  • 首次连接时 chromium.connectOverCDP(cdpUrl)
  • 后续请求复用同一个连接
  • 如果 cdpUrl 变化(切换 Profile),自动断开重连
  • 断开事件触发后清空缓存

7.2 Page 状态跟踪

typescript
// pw-session.ts
const pageStates = new WeakMap<Page, PageState>();
const observedPages = new WeakSet<Page>();

type PageState = {
    console: BrowserConsoleMessage[];     // 控制台消息(最多 500 条)
    errors: BrowserPageError[];           // 页面错误(最多 200 条)
    requests: BrowserNetworkRequest[];    // 网络请求(最多 500 条)
    roleRefs?: Record<string, { role: string; name?: string }>;  // 快照元素引用
    roleRefsMode?: "role" | "aria";
};

每个 Page 对象上挂载了事件收集器

typescript
page.on("console", (msg) => { state.console.push(...); });
page.on("pageerror", (err) => { state.errors.push(...); });
page.on("request", (req) => { state.requests.push(...); });
page.on("response", (res) => { /* 更新对应请求的状态 */ });
page.on("requestfailed", (req) => { /* 记录失败 */ });

使用环形缓冲策略:超过上限时移除最早的记录。

7.3 Role Refs 缓存

Playwright 每次获取快照会返回新的 Page 对象,但 AI 的 ref 引用需要跨请求保持稳定:

typescript
// 全局缓存:targetId → refs 映射
const roleRefsByTarget = new Map<string, RoleRefsCacheEntry>();
const MAX_ROLE_REFS_CACHE = 50;  // LRU 淘汰

// 获取快照时存储
storeRoleRefsForTarget({ page, cdpUrl, targetId, refs, mode });

// 执行操作时恢复
restoreRoleRefsForTarget({ cdpUrl, targetId, page });

![Playwright 会话管理机制](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(九)浏览器控制系统/06-infographic-playwright-session-1775150653672.png)


八、Chrome 扩展中继(Extension Relay)

8.1 为什么需要中继?

普通方式下,OpenClaw 自己启动 Chrome,直接通过 CDP 连接。但如果用户想让 AI 操控自己正在使用的浏览器,Chrome 的 CDP 端口默认是关闭的。

Chrome 扩展中继解决了这个问题:安装扩展后,扩展主动连接到 OpenClaw 的中继服务器,建立一个 WebSocket 桥接通道。

8.2 中继架构

┌──── Chrome 浏览器 ────┐        ┌──── Relay Server ────┐        ┌──── CDP 客户端 ────┐
│                        │        │     (Node.js HTTP     │        │  (Playwright/cdp)  │
│  Chrome Extension ─WS──┼───→    │      + WebSocket)     │  ←─WS──┤                    │
│  (background.js)       │        │                       │        │                    │
│                        │        │  /extension (扩展WS)   │        │  /json/list        │
│                        │        │  /devtools/* (CDP WS) │        │  /devtools/page/*  │
└────────────────────────┘        │  /json/list (HTTP)    │        └────────────────────┘
                                  └───────────────────────┘

8.3 消息转发机制

CDP 客户端发送命令:
    { id: 1, method: "Page.captureScreenshot", params: {...}, sessionId: "sess1" }


    Relay Server 转换为扩展格式:
    { id: 1, method: "forwardCDPCommand", params: { method: "Page.captureScreenshot", ... } }


    Chrome Extension 执行并返回:
    { id: 1, result: { data: "base64..." } }


    Relay Server 转发回 CDP 客户端:
    { id: 1, result: { data: "base64..." }, sessionId: "sess1" }

8.4 Target 管理

Extension Relay 维护连接的 Tab 列表:

typescript
type ConnectedTarget = {
    sessionId: string;     // CDP Session ID
    targetId: string;      // Tab Target ID
    targetInfo: TargetInfo; // Tab 信息(URL、title、type)
};

当 CDP 客户端请求 /json/list 时,Relay 返回所有已连接的 Tab 信息,模拟 Chrome 原生的 CDP HTTP API。

8.5 重连优雅处理

Chrome 扩展可能因为 Chrome 更新、休眠等原因断开连接:

typescript
const DEFAULT_EXTENSION_RECONNECT_GRACE_MS = 20_000;      // 20 秒重连宽限
const DEFAULT_EXTENSION_COMMAND_RECONNECT_WAIT_MS = 3_000; // 命令等待重连
  • 扩展断开后,Relay 保持 20 秒宽限期
  • 宽限期内收到的 CDP 命令会等待最多 3 秒
  • 如果扩展在宽限期内重连,命令继续执行
  • 超过宽限期,返回错误

8.6 认证机制

Relay 通过 x-openclaw-relay-token 头或 ?token= 查询参数进行认证:

typescript
// 只允许 loopback 地址
if (!isLoopbackHost(info.host)) {
    throw new Error("extension relay requires loopback cdpUrl host");
}

// Token 认证
const token = getRelayAuthTokenFromRequest(req, url);
if (!acceptedTokens.includes(token)) {
    rejectUpgrade(socket, 403, "Forbidden");
}

![Extension Relay 中继架构](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(九)浏览器控制系统/07-infographic-extension-relay-1775150654498.png)


九、导航安全守卫

9.1 SSRF 防护

所有浏览器导航都经过安全检查,防止 SSRF(Server-Side Request Forgery)攻击:

typescript
// navigation-guard.ts
export async function assertBrowserNavigationAllowed(opts) {
    // 1. URL 不能为空
    if (!rawUrl) throw new InvalidBrowserNavigationUrlError("url is required");

    // 2. 协议白名单(只允许 http: 和 https:)
    if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
        // 特例:about:blank 允许通过
        if (!isAllowedNonNetworkNavigationUrl(parsed)) {
            throw new Error(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
        }
    }

    // 3. 代理环境下的严格检查
    //    如果配置了 HTTP 代理,浏览器可能绕过 DNS 解析
    //    在 strict 模式下,直接拒绝
    if (hasProxyEnvConfigured() && !isPrivateNetworkAllowedByPolicy(ssrfPolicy)) {
        throw new Error("Navigation blocked: strict SSRF policy with proxy");
    }

    // 4. DNS 解析检查(防止解析到内网 IP)
    await resolvePinnedHostnameWithPolicy(parsed.hostname, { policy: ssrfPolicy });
}

9.2 导航后检查

除了导航前检查,还有导航后检查assertBrowserNavigationResultAllowed),防止页面通过 JavaScript 跳转到危险 URL:

typescript
export async function assertBrowserNavigationResultAllowed(opts) {
    // 只检查 http/https 和 about:blank
    // 忽略浏览器内部 URL(如 chrome-error://)以避免误报
}

![多层安全防护体系](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(九)浏览器控制系统/08-infographic-security-layers-1775150655387.png)


十、HTTP 路由层

10.1 路由分组

浏览器控制服务器将路由分为三组:

registerBrowserRoutes(app, ctx)
    ├── registerBrowserBasicRoutes()     // 基础操作
    ├── registerBrowserTabRoutes()       // Tab 管理
    └── registerBrowserAgentRoutes()     // Agent 高级操作

10.2 基础路由

方法路径功能
GET/获取浏览器状态(running、cdpReady、pid 等)
GET/profiles列出所有 Profile 及状态
POST/start启动浏览器(Profile 维度)
POST/stop停止浏览器
POST/reset-profile重置 Profile(清除数据)
POST/profiles/create创建新 Profile
DELETE/profiles/:name删除 Profile

10.3 Tab 路由

方法路径功能
GET/tabs列出所有标签页
POST/tabs/open打开新标签页
POST/tabs/close关闭标签页
POST/tabs/focus聚焦标签页

10.4 Agent 路由

方法路径功能
POST/navigate导航到 URL
GET/snapshot获取页面快照(ARIA/AI 格式)
POST/screenshot截取页面截图
POST/pdf保存页面为 PDF
POST/act执行浏览器操作(11 种)
POST/response/body获取网络响应体
POST/highlight高亮页面元素

10.5 Act 操作详解

/act 端点支持 11 种操作(ActKind):

Kind功能必需参数
click点击元素ref
type在元素中输入文本ref, text
press按键key
hover悬停在元素上ref
scrollIntoView滚动元素到可视区域ref
drag拖拽startRef, endRef
select选择下拉选项ref, values
fill批量填写表单fields
resize调整视口大小width, height
wait等待条件timeMs/text/textGone/selector/url/loadState/fn
evaluate执行 JavaScriptfn
close关闭标签页

其中 evaluatewait --fn 可以通过配置 browser.evaluateEnabled=false 禁用(安全考虑)。

![HTTP 路由层与 Act 操作矩阵](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(九)浏览器控制系统/09-infographic-routes-actions-1775150656113.png)


十一、Agent 工具层

11.1 工具入口(browser-tool.ts)

createBrowserTool() 创建一个名为 "browser" 的 Agent 工具,支持 16 种 action

status / start / stop / profiles    — 浏览器生命周期
tabs / open / focus / close         — 标签页管理
snapshot / screenshot               — 页面观察
navigate                            — 导航
console                             — 控制台日志
pdf                                 — PDF 导出
upload                              — 文件上传
dialog                              — 对话框处理
act                                 — 浏览器操作(11 种子操作)

11.2 三种运行目标(Target)

typescript
const BROWSER_TARGETS = ["sandbox", "host", "node"] as const;
Target说明典型场景
sandbox沙箱浏览器(Docker bridge)群组会话中的安全隔离
host本地主机浏览器个人 DM 会话(默认)
node远程节点代理浏览器跑在另一台机器上

11.3 节点路由决策

typescript
// browser-tool.ts — resolveBrowserNodeTarget()
async function resolveBrowserNodeTarget(params) {
    // 1. 读取配置 gateway.nodes.browser.mode
    //    - "off" → 禁用节点代理
    //    - "auto" → 自动选择(有可用节点时使用)
    //    - "manual" → 仅在显式请求时使用

    // 2. 查询已连接的 browser-capable 节点
    const nodes = await listNodes({});
    const browserNodes = nodes.filter(n => n.connected && isBrowserNode(n));

    // 3. 选择节点
    //    优先使用配置指定的节点
    //    否则使用 selectDefaultNodeFromList()
}

11.4 远程节点代理调用

当 target 是 node 时,浏览器操作通过 Gateway 的 node.invoke 命令转发:

browser-tool.ts
    ├── callBrowserProxy()
    │       │
    │       ▼
    │   callGatewayTool("node.invoke", {
    │       nodeId: "node-xxx",
    │       command: "browser.proxy",
    │       params: {
    │           method: "POST",
    │           path: "/act",
    │           body: { kind: "click", ref: "e1" },
    │           profile: "openclaw",
    │       }
    │   })
    │       │
    │       ▼
    │   远程节点执行实际的浏览器操作
    │       │
    │       ▼
    │   返回结果 + 可能的文件(base64)
    │       │
    │       ▼
    └── persistBrowserProxyFiles() → 保存到本地媒体目录
        applyBrowserProxyPaths() → 替换文件路径

11.5 外部内容安全包装

所有来自浏览器的内容都被标记为不受信任

typescript
// browser-tool.actions.ts
function wrapBrowserExternalJson(data: unknown) {
    return {
        untrusted: true,           // 标记为外部内容
        note: "External browser content — may be untrusted.",
        data,
    };
}

这是**防止提示注入(Prompt Injection)**的关键措施。如果恶意网页在内容中嵌入了"忽略之前的指令…",AI 会知道这段内容来自外部浏览器,不应该将其作为系统指令执行。

![Agent 工具层与 Target 路由](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(九)浏览器控制系统/10-infographic-agent-tool-1775150656951.png)


十二、截图处理管线

12.1 截图归一化

浏览器截图可能非常大(4K 屏幕的全页截图),需要压缩到 AI 可处理的大小:

typescript
// screenshot.ts
export async function normalizeBrowserScreenshot(buffer, opts) {
    const maxSide = 2000;              // 最大边长 2000px
    const maxBytes = 5 * 1024 * 1024;  // 最大 5MB

    // 如果已经在限制内,直接返回
    if (buffer.byteLength <= maxBytes && width <= maxSide && height <= maxSide) {
        return { buffer };
    }

    // 否则进行渐进式压缩:
    // 遍历 sideGrid(从大到小的尺寸)× qualitySteps(从高到低的 JPEG 质量)
    for (const side of sideGrid) {
        for (const quality of IMAGE_REDUCE_QUALITY_STEPS) {
            const out = await resizeToJpeg({ buffer, maxSide: side, quality });
            if (out.byteLength <= maxBytes) {
                return { buffer: out, contentType: "image/jpeg" };
            }
        }
    }

    throw new Error("Screenshot could not be reduced below 5MB");
}

十三、Renderer Swap 处理

Chrome 有时会在导航过程中发生 renderer swap(渲染器交换),导致 Tab 的 targetId 发生变化:

typescript
// agent.snapshot.ts
export async function resolveTargetIdAfterNavigate(opts) {
    const refreshed = await opts.listTabs();

    // 如果旧的 targetId 不在列表中 → 发生了 renderer swap
    if (!refreshed.some(t => t.targetId === opts.oldTargetId)) {
        // 通过 URL 匹配找到替代 Tab
        const byUrl = refreshed.filter(t => t.url === opts.navigatedUrl);
        const replaced = byUrl.find(t => t.targetId !== opts.oldTargetId) ?? byUrl[0];

        if (replaced) {
            return replaced.targetId;
        }

        // 重试一次(800ms 后)
        await sleep(800);
        const retried = await opts.listTabs();
        // ...
    }

    return currentTargetId;
}

这个处理确保 AI 在导航后仍然能正确引用当前页面。


十四、Stale Target 自动恢复

Extension Relay 模式下,Chrome 扩展可能丢失对 Tab 的跟踪(stale target):

typescript
// browser-tool.actions.ts — executeActAction()
export async function executeActAction(params) {
    try {
        return await browserAct(baseUrl, request, options);
    } catch (err) {
        // 如果是 stale target 错误且使用 Extension Relay
        if (isStaleTargetError(err) && isExtensionProfile) {
            // 刷新 Tab 列表
            const freshTabs = await browserTabs(baseUrl);
            // 找到 URL 匹配的新 Tab
            const match = freshTabs.find(t => t.url === currentUrl);
            if (match && match.targetId !== request.targetId) {
                // 用新的 targetId 重试
                return await browserAct(baseUrl, {
                    ...request,
                    targetId: match.targetId,
                }, options);
            }
        }
        throw err;
    }
}

十五、配置参考

浏览器系统的配置位于 browser 顶层键:

json5
{
  "browser": {
    "enabled": true,
    "headless": false,           // 无头模式
    "noSandbox": false,          // 禁用沙箱(Docker/root 环境需要)
    "executablePath": "",        // 自定义浏览器路径
    "extraArgs": [],             // 额外 Chrome 启动参数
    "evaluateEnabled": true,     // 允许 JS 执行
    "defaultProfile": "openclaw",
    "profiles": {
      "openclaw": {
        "cdpPort": 18800,
        "color": "#FF4500",
        "driver": "openclaw"     // "openclaw" | "extension"
      }
    },
    "ssrfPolicy": {
      // SSRF 防护策略
    }
  },
  "gateway": {
    "nodes": {
      "browser": {
        "mode": "auto",          // "auto" | "manual" | "off"
        "node": ""               // 指定节点 ID
      }
    }
  }
}

十六、与其他模块的交互

┌─── Agent 系统 ───┐
│                   │
│  Pi Agent Core    │──→ browser-tool.ts ──→ 16 种 action
│                   │
│  工具注册         │──→ createBrowserTool() ──→ ToolDefinition
└───────────────────┘


┌─── Gateway ───────┐
│                    │
│  server.ts         │──→ 挂载浏览器控制服务器 (/browser/*)
│                    │
│  node.invoke       │──→ 远程节点浏览器代理
│                    │
│  status            │──→ 浏览器状态查询
└────────────────────┘


┌─── 媒体系统 ──────┐
│                    │
│  media/store.ts    │──→ 截图/PDF 持久化存储
│                    │
│  media/image-ops   │──→ 截图压缩(sharp)
└────────────────────┘


┌─── 安全系统 ──────┐
│                    │
│  infra/net/ssrf.ts │──→ 导航 SSRF 防护
│                    │
│  gateway/net.ts    │──→ loopback 地址检查
└────────────────────┘

十七、数据流全景图

AI 说:"打开 github.com,截个图给我看看"


browser-tool.ts: action="navigate", targetUrl="https://github.com"

        ├── resolveBrowserNodeTarget() → 决定用本地还是远程节点
        ├── resolveBrowserBaseUrl() → 决定用 sandbox 还是 host


client-actions.ts: browserNavigate(baseUrl, { url })


HTTP POST http://127.0.0.1:18791/browser/navigate
  body: { url: "https://github.com", targetId: "..." }


routes/agent.snapshot.ts: /navigate handler

        ├── assertBrowserNavigationAllowed() → SSRF 检查
        ├── withPlaywrightRouteContext() → 获取 Playwright 连接
        ├── pw.navigateViaPlaywright() → Playwright 执行导航
        ├── resolveTargetIdAfterNavigate() → 处理 renderer swap


返回 { ok: true, targetId: "...", url: "https://github.com" }


browser-tool.ts: action="screenshot"


routes/agent.snapshot.ts: /screenshot handler

        ├── captureScreenshot() via CDP 或 Playwright
        ├── normalizeBrowserScreenshot() → 压缩到 ≤5MB、≤2000px
        ├── saveMediaBuffer() → 保存到 ~/.openclaw/media/


返回 { ok: true, path: "/path/to/screenshot.png" }


browser-tool.ts: imageResultFromFile(path) → 返回给 AI

![端到端数据流全景](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(九)浏览器控制系统/11-infographic-data-flow-1775150658084.png)


十八、关键设计决策与权衡

18.1 为什么同时支持 CDP 原始协议和 Playwright?

CDP 原始协议的优势:

  • 轻量级、低延迟
  • 不需要加载 Playwright 库(~150MB)
  • 适合简单操作(截图、JS 执行)

Playwright 的优势:

  • 高层抽象(click by ref、wait for text、form fill)
  • 更好的错误处理和重试
  • AI 快照(_snapshotForAI

所以系统按需使用:简单操作走 CDP,复杂交互走 Playwright。

18.2 为什么需要 Extension Relay?

直接 CDP 连接要求在启动 Chrome 时添加 --remote-debugging-port,但用户日常使用的 Chrome 通常不带这个参数。

Extension Relay 解决了这个"鸡生蛋"问题:

  • Chrome 扩展可以在运行中的 Chrome 里安装
  • 扩展通过 chrome.debugger API 获取 CDP 能力
  • 然后通过 WebSocket 反向连接到 OpenClaw

18.3 Profile 隔离 vs 共享

OpenClaw 为每个 Profile 创建完全隔离的 user-data-dir,不与用户的 Chrome 共享:

  • Cookie、书签、历史记录全部独立
  • 防止 AI 操作影响用户的 Chrome
  • 也防止用户的 Chrome 状态影响 AI 操作

18.4 为什么截图要这么大力度压缩?

AI 模型处理图片有 token 成本,过大的图片既浪费 token 又可能超出上下文限制。5MB / 2000px 的限制是在保留足够细节控制成本之间的平衡。

18.5 外部内容安全标记

所有浏览器返回的内容(ARIA 快照、Tab 列表、控制台日志)都被包裹在 { untrusted: true, data: ... } 中。这是防止提示注入攻击的关键防线:恶意网页可能在可见文本中嵌入看起来像 AI 指令的内容。

![5 大关键设计决策与权衡](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(九)浏览器控制系统/12-infographic-design-decisions-1775150658875.png)


十九、总结

OpenClaw 的浏览器控制系统是一个精密的多层架构,有以下亮点:

  1. 双驱动架构:openclaw 驱动(自管理 Chrome)和 extension 驱动(连接已有浏览器),覆盖不同使用场景
  2. 多 Profile 支持:每个 Profile 有独立的 CDP 端口、user-data、颜色标识,支持并行运行
  3. 三种运行目标:sandbox(沙箱隔离)、host(本地)、node(远程代理),灵活部署
  4. 双协议层:CDP 原始协议(轻量高效)+ Playwright(功能强大),按需选择
  5. AI 优化的页面表示:ARIA 快照 + AI 快照 + 标签截图,多通道帮助 AI "理解"网页
  6. 深度安全防护:SSRF 导航守卫、反自动化检测、外部内容安全标记、Profile 隔离
  7. 优雅的故障处理:Renderer Swap 自动跟踪、Stale Target 自动恢复、Extension 重连宽限
  8. 可观测的截图管线:渐进式压缩、多格式支持、媒体系统集成

整个设计遵循"让 AI 安全高效地操控浏览器"的理念,在功能、安全、性能之间取得了出色的平衡。

读文档、看源码、写代码,理解 AI Agent 本质 🤖