乐于分享
好东西不私藏

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

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(wsWebSocket) {  let nextId = 1;  const pending = new Map<numberPending>();  const sendCdpSendFn = (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.wsUrlasync (send) => {    await send("Page.enable");    let clip;    if (opts.fullPage) {      const metrics = await send("Page.getLayoutMetrics");      const size = metrics?.cssContentSize ?? metrics?.contentSize;      clip = { x0y0width: size.widthheight: size.heightscale1 };    }    const result = await send("Page.captureScreenshot", {      format, fromSurfacetruecaptureBeyondViewporttrue, ...(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.nameexacttrue })      : 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.cdpUrltargetId: opts.targetId });  ensurePageState(page);  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrltargetId: opts.targetId, page });  const ref = requireRef(opts.ref);  const locator = refLocator(page, ref);  const timeout = Math.max(500Math.min(60_000Math.floor(opts.timeoutMs ?? 8000)));  try {    if (opts.doubleClick) {      await locator.dblclick({ timeout, button: opts.buttonmodifiers: opts.modifiers });    } else {      await locator.click({ timeout, button: opts.buttonmodifiers: 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({ timeout5000track"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.refsmode"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.enabledreturn 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", { requiredtrue });      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 server  const 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. 运维与测试

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » OpenClaw源码解读之浏览器自动化

评论 抢沙发

5 + 4 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮