主题
OpenClaw 源码解读(九)浏览器控制系统
本文基于 OpenClaw 2026.3.2 源码,深入解读浏览器控制系统(Browser Control System)的架构设计与实现细节。
一、模块概览
浏览器控制系统赋予 AI 助手操控真实浏览器的能力:打开网页、截图、读取页面内容、点击按钮、填写表单……它是 OpenClaw 最复杂的子系统之一,包含 120+ 个源文件,横跨 Chrome 进程管理、CDP 协议、Playwright 集成、HTTP API、Chrome 扩展中继等多个层面。
1.1 核心源码分布
| 目录/文件 | 行数(约) | 职责 |
|---|---|---|
src/browser/server.ts | ~200 | Express 控制服务器(启动、挂载路由、绑定端口) |
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 | ~427 | Chrome 进程管理(启动、停止、健康检查、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 | ~113 | CDP 端口分配与 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 | ~639 | Agent 工具入口(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 客户端) │ │ │
│ │ └────────────────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘浏览器控制系统/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 扩展
浏览器控制系统/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()浏览器控制系统/03-infographic-port-profile-1775150651050.png)
五、Chrome 进程管理
5.1 浏览器可执行文件发现
chrome.executables.ts 实现了跨平台的浏览器发现:
| 平台 | 搜索路径 | 优先级 |
|---|---|---|
| macOS | /Applications/Google Chrome.app → Brave Browser.app → Microsoft Edge.app → Chromium.app | Chrome > Brave > Edge > Chromium |
| Linux | google-chrome-stable → google-chrome → brave-browser → microsoft-edge-stable → chromium-browser → chromium (PATH 查找) | Chrome > Brave > Edge > Chromium |
| Windows | %ProgramFiles%\Google\Chrome\Application\chrome.exe → Brave → Edge → Chromium | Chrome > 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 };
}浏览器控制系统/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 通过两种方式使用它:
方式一:原始 WebSocket(cdp.helpers.ts → withCdpSocket)
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 的高级 API6.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=2AI 通过 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。
浏览器控制系统/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 });浏览器控制系统/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");
}浏览器控制系统/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://)以避免误报
}浏览器控制系统/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 | 执行 JavaScript | fn |
close | 关闭标签页 | — |
其中 evaluate 和 wait --fn 可以通过配置 browser.evaluateEnabled=false 禁用(安全考虑)。
浏览器控制系统/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 会知道这段内容来自外部浏览器,不应该将其作为系统指令执行。
浏览器控制系统/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浏览器控制系统/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.debuggerAPI 获取 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 指令的内容。
浏览器控制系统/12-infographic-design-decisions-1775150658875.png)
十九、总结
OpenClaw 的浏览器控制系统是一个精密的多层架构,有以下亮点:
- 双驱动架构:openclaw 驱动(自管理 Chrome)和 extension 驱动(连接已有浏览器),覆盖不同使用场景
- 多 Profile 支持:每个 Profile 有独立的 CDP 端口、user-data、颜色标识,支持并行运行
- 三种运行目标:sandbox(沙箱隔离)、host(本地)、node(远程代理),灵活部署
- 双协议层:CDP 原始协议(轻量高效)+ Playwright(功能强大),按需选择
- AI 优化的页面表示:ARIA 快照 + AI 快照 + 标签截图,多通道帮助 AI "理解"网页
- 深度安全防护:SSRF 导航守卫、反自动化检测、外部内容安全标记、Profile 隔离
- 优雅的故障处理:Renderer Swap 自动跟踪、Stale Target 自动恢复、Extension 重连宽限
- 可观测的截图管线:渐进式压缩、多格式支持、媒体系统集成
整个设计遵循"让 AI 安全高效地操控浏览器"的理念,在功能、安全、性能之间取得了出色的平衡。