OpenClaw源码解读之浏览器自动化

OpenClaw 内置了一套完整的浏览器自动化系统,让 AI Agent 能够像人一样操作网页——导航、点击、输入、截图、读取页面内容。整套系统建立在两层协议之上:底层的 Chrome DevTools Protocol(CDP)和上层的 Playwright,通过一个本地 HTTP 服务器暴露为 RESTful API,最终封装为 Agent 的 `browser` 工具。本文将从协议层、会话管理、交互操作、到 Agent 工具接口,完整剖析这套约 15000 行代码的浏览器控制引擎。

一、CDP 底层:直接与 Chrome 对话
CDP 是 Chrome 浏览器的调试协议,通过 WebSocket 通信。`cdp.helpers.ts` 中的 `withCdpSocket` 封装了最底层的 CDP 会话管理:
// src/browser/cdp.helpers.tsfunction createCdpSender(ws: WebSocket) {let nextId = 1;const pending = new Map<number, Pending>();const send: CdpSendFn = (method, params?, sessionId?) => {const id = nextId++;ws.send(JSON.stringify({ id, method, params, sessionId }));return new Promise<unknown>((resolve, reject) => {pending.set(id, { resolve, reject });});};ws.on("message", (data) => {const parsed = JSON.parse(rawDataToString(data));const p = pending.get(parsed.id);if (!p) return;pending.delete(parsed.id);if (parsed.error?.message) {p.reject(new Error(parsed.error.message));} else {p.resolve(parsed.result);}});return { send, closeWithError };}
这是一个经典的请求-响应多路复用器——每个 CDP 命令分配递增 ID,发送后挂起到 `pending` Map,收到匹配 ID 的响应后 resolve。与 Gateway 协议的模式如出一辙。
`withCdpSocket` 在此基础上提供了自动连接和清理的生命周期管理,调用方只需关心”发送什么命令”:
// src/browser/cdp.tsexport async function captureScreenshot(opts: {wsUrl: string; fullPage?: boolean; format?: "png" | "jpeg"; quality?: number;}): Promise<Buffer> {return await withCdpSocket(opts.wsUrl, async (send) => {await send("Page.enable");let clip;if (opts.fullPage) {const metrics = await send("Page.getLayoutMetrics");const size = metrics?.cssContentSize ?? metrics?.contentSize;clip = { x: 0, y: 0, width: size.width, height: size.height, scale: 1 };}const result = await send("Page.captureScreenshot", {format, fromSurface: true, captureBeyondViewport: true, ...(clip ? { clip } : {}),});return Buffer.from(result.data, "base64");});}
截图是最直观的 CDP 用例——先启用 Page domain,如果需要全页截图则获取布局尺寸计算 clip 区域,然后调用 `Page.captureScreenshot`。返回的 base64 数据解码为 Buffer。
`cdp.ts` 还提供了其他底层能力:`evaluateJavaScript` 执行 JavaScript、`snapshotAria` 获取无障碍树、`snapshotDom` 获取 DOM 树、`querySelector` 执行 CSS 选择器查询。这些是不依赖 Playwright 的轻量级操作。
二、Chrome 启动与配置管理
`chrome.ts` 负责启动和管理 Chrome 进程。`launchOpenClawChrome` 组装启动参数并 spawn 子进程:
// src/browser/chrome.tsexport async function launchOpenClawChrome(resolved: ResolvedBrowserConfig,profile: ResolvedBrowserProfile,): Promise<RunningChrome> {await ensurePortAvailable(profile.cdpPort);const exe = resolveBrowserExecutable(resolved);const userDataDir = resolveOpenClawUserDataDir(profile.name);const spawnOnce = () => {const args = [`--remote-debugging-port=${profile.cdpPort}`,`--user-data-dir=${userDataDir}`,"--no-first-run","--disable-sync","--disable-background-networking","--disable-blink-features=AutomationControlled", // 反自动化检测"about:blank",];if (resolved.headless) { args.push("--headless=new"); }if (resolved.noSandbox) { args.push("--no-sandbox"); }return spawn(exe.path, args, { stdio: "pipe" });};// ... bootstrap + decorate + launch}
几个值得注意的设计:
-
反检测:`–disable-blink-features=AutomationControlled` 隐藏 `navigator.webdriver` 标志,防止网站检测到自动化
-
独立数据目录:每个 profile 有独立的 `userDataDir`,与用户正常浏览完全隔离
-
首次引导:新 profile 需要先启动一次 Chrome 让它创建默认配置文件,然后停止、装饰(设置主题色等)、再正式启动
-
多浏览器支持:`resolveBrowserExecutable` 按优先级查找 Chrome、Brave、Edge、Chromium
系统支持两种 profile 模式:`”openclaw”` 启动独立的受控浏览器;`”chrome”` 通过 Chrome 扩展中继接管用户已有的 Chrome 标签页。
三、Playwright 会话:高级抽象层
Playwright 建立在 CDP 之上,提供了更高级的 API。`pw-session.ts` 管理 Playwright 与 Chrome 的连接和页面状态。
核心函数 `getPageForTargetId` 通过 CDP URL 连接浏览器,然后根据 `targetId` 定位具体的标签页:
// src/browser/pw-session.tsexport async function getPageForTargetId(opts: {cdpUrl: string; targetId?: string;}): Promise<Page> {const { browser } = await connectBrowser(opts.cdpUrl);const pages = await getAllPages(browser);if (!opts.targetId) {return pages[0]; // 默认返回第一个页面}const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);if (!found && pages.length === 1) {return pages[0]; // 单页面时的 fallback}return found;}
Playwright 连接被全局缓存——多个操作共享同一个 `Browser` 实例,避免重复连接开销。
引用系统(Refs)
浏览器自动化最核心的问题是:Agent 怎么指定要操作的元素?OpenClaw 设计了一套引用系统。Agent 先通过 `snapshot` 获取页面的结构化描述,每个可交互元素被分配一个短引用(如 `e1`、`e2`、`e12`),后续操作直接用引用定位。
`refLocator` 函数将引用解析为 Playwright Locator:
// src/browser/pw-session.tsexport function refLocator(page: Page, ref: string) {const normalized = ref.startsWith("@") ? ref.slice(1): ref.startsWith("ref=") ? ref.slice(4) : ref;if (/^e\d+$/.test(normalized)) {const state = pageStates.get(page);if (state?.roleRefsMode === "aria") {// ARIA 模式:直接用 aria-ref 属性定位return page.locator(`aria-ref=${normalized}`);}// Role 模式:用角色+名称定位const info = state?.roleRefs?.[normalized];if (!info) {throw new Error(`Unknown ref "${normalized}". Run a new snapshot.`);}const locator = info.name? page.getByRole(info.role, { name: info.name, exact: true }): page.getByRole(info.role);return info.nth !== undefined ? locator.nth(info.nth) : locator;}return page.locator(`aria-ref=${normalized}`);}
引用系统支持两种模式:
-
role 模式(默认):引用映射到 `(role, name, nth)` 元组,如 `e3` → `button “Submit”`。这种模式跨快照可能不稳定(页面变化后引用可能指向不同元素)
-
aria 模式:引用直接对应 Playwright 的 `aria-ref` 属性,更稳定但需要 Playwright 支持
快照生成时,`storeRoleRefsForTarget` 将引用映射存储到页面状态中;操作时,`restoreRoleRefsForTarget` 恢复映射。这保证了”先 snapshot 后 act”的操作序列中引用的一致性。
四、交互操作:click、type、fill
`pw-tools-core.interactions.ts` 实现了所有的页面交互操作。每个操作遵循统一的模式:
// src/browser/pw-tools-core.interactions.tsexport async function clickViaPlaywright(opts: {cdpUrl: string; targetId?: string; ref: string;doubleClick?: boolean; button?: "left" | "right" | "middle";modifiers?: Array<"Alt" | "Control" | "Meta" | "Shift">;timeoutMs?: number;}): Promise<void> {const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });ensurePageState(page);restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });const ref = requireRef(opts.ref);const locator = refLocator(page, ref);const timeout = Math.max(500, Math.min(60_000, Math.floor(opts.timeoutMs ?? 8000)));try {if (opts.doubleClick) {await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });} else {await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });}} catch (err) {throw toAIFriendlyError(err, ref);}}
四步标准流程:
1. `getPageForTargetId` — 获取目标页面
2. `ensurePageState` + `restoreRoleRefsForTarget` — 恢复引用映射
3. `refLocator` — 将 `e12` 这样的引用转为 Playwright Locator
4. 执行操作(click、type、hover…),catch 错误并通过 `toAIFriendlyError` 转化为 Agent 易理解的错误信息
除了基础的 click、hover、type,还支持:
-
`dragViaPlaywright`:拖拽操作(从 startRef 到 endRef)
-
`fillFormViaPlaywright`:批量表单填充,接受字段数组一次性填写多个输入框
-
`selectOptionViaPlaywright`:下拉框选择
-
`pressKeyViaPlaywright`:键盘按键(包括组合键)
-
`waitForViaPlaywright`:等待条件(文本出现/消失、URL 变化、元素出现、JavaScript 表达式为真)
-
`evaluateViaPlaywright`:在页面上下文中执行 JavaScript
五、快照:页面的结构化描述
快照是 Agent 理解页面内容的关键。`pw-tools-core.snapshot.ts` 提供了多种快照格式。
AI 快照(`snapshotAiViaPlaywright`)调用 Playwright 的内部 `_snapshotForAI` API,获取专门为 AI 优化的页面描述:
// src/browser/pw-tools-core.snapshot.tsexport async function snapshotAiViaPlaywright(opts) {const page = await getPageForTargetId(opts);const maybe = page as unknown as WithSnapshotForAI;const result = await maybe._snapshotForAI({ timeout: 5000, track: "response" });let snapshot = String(result?.full ?? "");// 截断过长的快照if (limit && snapshot.length > limit) {snapshot = `${snapshot.slice(0, limit)}\n\n[...TRUNCATED - page too large]`;}// 从快照中提取引用映射const built = buildRoleSnapshotFromAiSnapshot(snapshot);storeRoleRefsForTarget({ page, cdpUrl, targetId, refs: built.refs, mode: "aria" });return { snapshot, refs: built.refs };}
ARIA 快照(`snapshotAriaViaPlaywright`)通过 CDP 的 `Accessibility.getFullAXTree` 获取无障碍树,是一种更结构化但更底层的页面表示。
Role 快照(`snapshotRoleViaPlaywright`)生成基于角色的树形描述,每个可交互元素标注 `[eN]` 引用。
快照结果会附带一个 `refs` 映射表(如 `{“e1”: {“role”: “link”, “name”: “Home”}, “e2”: {“role”: “button”, “name”: “Submit”}}`),Agent 在后续操作中直接使用这些引用。
六、HTTP 服务器:REST API 层
所有浏览器操作通过一个 Express 服务器暴露为 HTTP API。`startBrowserControlServerFromConfig` 负责启动:
// src/browser/server.tsexport async function startBrowserControlServerFromConfig(): Promise<BrowserServerState | null> {const cfg = loadConfig();const resolved = resolveBrowserConfig(cfg.browser, cfg);if (!resolved.enabled) return null;const app = express();app.use(express.json({ limit: "1mb" }));// 认证中间件if (browserAuth.token || browserAuth.password) {app.use((req, res, next) => {if (isAuthorizedBrowserRequest(req, browserAuth)) return next();res.status(401).send("Unauthorized");});}// 注册路由registerBrowserRoutes(app, ctx);const server = app.listen(port, "127.0.0.1", () => resolve(s));// ... 初始化 Chrome 扩展中继}
服务器只绑定 `127.0.0.1`,不对外暴露。路由分布在多个文件中:
-
`routes/agent.act.ts`:操作路由——`POST /act` 支持 click、type、press、hover、scroll、drag、select、fill、resize、wait、evaluate、close
-
`routes/agent.snapshot.ts`:快照路由——`POST /navigate`、`POST /screenshot`、`GET /snapshot`、`POST /pdf`
-
`routes/tabs.ts`:标签页管理——列表、创建、关闭、切换
-
`routes/basic.ts`:基础路由——状态、启动、停止
每个路由接收 JSON 请求,委托给对应的 Playwright/CDP 函数,返回 JSON 响应。`POST /act` 是最通用的操作端点,它根据请求中的 `command` 字段分发到不同的处理函数。
七、Agent 工具:browser
Agent 通过名为 `browser` 的工具与浏览器交互。`createBrowserTool` 创建工具定义:
// src/agents/tools/browser-tool.tsexport function createBrowserTool(opts?) {return {name: "browser",description: "Control the browser via OpenClaw's browser control server...",parameters: BrowserToolSchema,execute: async (_toolCallId, args) => {const action = readStringParam(params, "action", { required: true });const target = readStringParam(params, "target"); // sandbox | host | node// 解析目标:沙箱浏览器、宿主浏览器、或远程节点浏览器const baseUrl = resolveBrowserBaseUrl({ target, sandboxBridgeUrl, allowHostControl });switch (action) {case "status": return jsonResult(await browserStatus(baseUrl, { profile }));case "start": /* 启动浏览器 */case "stop": /* 停止浏览器 */case "tabs": /* 列出标签页 */case "open": /* 打开新标签页 */case "snapshot": /* 获取页面快照 */case "screenshot": /* 截图 */case "navigate": /* 导航到 URL */case "act": /* 执行交互操作 */// ... 更多 action}},};}
工具支持三种目标环境(`target`):
-
sandbox:Docker 容器内的隔离浏览器(`ensureSandboxBrowser` 负责容器编排)
-
host:宿主机上的本地浏览器
-
node:远程节点代理的浏览器(通过 Gateway 转发)
Agent 的典型操作流程是:`start` → `open`(URL)→ `snapshot`(获取页面结构)→ `act`(click/type)→ `screenshot`(验证结果)。每次 `snapshot` 返回的引用(`e1`、`e2`…)可以直接传给 `act` 使用。
工具描述中特意提到了 Chrome 扩展中继模式——当用户提到”Chrome extension”或”attach tab”时,Agent 应使用 `profile=”chrome”` 而非独立浏览器,这样可以操作用户当前正在浏览的标签页。
八、沙箱浏览器与安全边界
在 Docker 沙箱环境中,Agent 使用容器内的浏览器。`ensureSandboxBrowser` 检查或创建沙箱浏览器:
// src/agents/sandbox/browser.tsexport async function ensureSandboxBrowser(params) {// 检查现有的 bridge 是否可用const existing = BROWSER_BRIDGES.get(params.scopeKey);if (existing) {const reachable = await isChromeReachable(cdpUrl);if (reachable) return { bridgeUrl: ... };}// 否则启动新的 bridge serverconst bridge = await startBrowserBridgeServer({ resolved, ... });BROWSER_BRIDGES.set(params.scopeKey, { bridge, containerName });return { bridgeUrl: ... };}
Bridge Server 是一个轻量级代理,将 Agent 的浏览器操作请求转发到容器内的 Chrome。通过 `BROWSER_BRIDGES` Map 做复用,避免每次操作都创建新容器。宿主浏览器控制可以通过 `allowHostControl: false` 策略完全禁用,确保 Agent 只能操作沙箱内的浏览器。
小结
OpenClaw 的浏览器自动化系统采用了四层架构:CDP 原始协议 → Playwright 高级抽象 → Express HTTP API → Agent 工具接口。引用系统(`e1`、`e2`…)解决了 AI 与 DOM 元素之间的指代问题,快照提供了结构化的页面理解,三种目标环境(sandbox/host/node)覆盖了安全隔离和远程控制的需求。整套系统让 AI Agent 具备了完整的浏览器操作能力,从简单的页面截图到复杂的表单填写和多标签页管理。
下面是讲解项目的基本信息:
-
项目地址:https://github.com/openclaw/openclaw
-
使用的项目分支是:main
-
commit版本是:f5160ca6becaeeb6a4dfd892fffd2130a696f766
讲解模块和顺序如下:
1. CLI 框架与进程模型
2. 配置系统
3. Gateway 核心
4. 通道与路由
5. Agent 引擎
6. 自动回复管线
7. 插件系统
8. 记忆系统
9. Web 控制台
10. 原生客户端
11. 浏览器自动化(今日讲解)
12. 运维与测试
夜雨聆风
