OpenBridge 开发设计与实现:构建一个开放的 AI 浏览器桥

OpenBridge 是一个开源的本地浏览器桥,让 AI Agent(Codex、Claude Code、Cursor 等)可以控制用户真实的 Chrome 浏览器。它采用三层架构(守护进程 + Chrome 扩展 + MCP),支持 19 个浏览器工具,从标签管理到 CDP 自动化一应俱全。本文将完整介绍其设计动机、架构决策、核心技术实现与踩坑记录。


1. 背景:AI Agent 为什么要控制浏览器?

AI 编程工具(Cursor、Claude Code、Codex、Windsurf 等)越来越需要与浏览器交互——调试前端页面、填写表单、截图验证 UI、跑自动化测试、抓取信息等。但现有方案各有局限:

方案问题
Chrome DevTools MCP(Google 官方)Puppeteer 启动全新 Chrome,无法访问用户已有的登录态和 Cookie
CodeX(OpenAI)封闭生态,Native Messaging Host ID 硬编码,只有 CodeX CLI 能连
BrowserTools MCP(AgentDeskAI)四层架构过于冗余,且已停更

核心矛盾:我想让 AI 控制"我正在用的"这个浏览器,而不是开一个新的。

这就是 OpenBridge 要解决的问题——一个开放的、标准化的、能复用用户登录态和上下文的浏览器控制方案。


2. 架构设计:为什么是三层?

经过对 CodeX(两层:CLI ↔ Extension)和 Kimi WebBridge(三层:AI ↔ Daemon ↔ Extension)的深入调研,我们做出了核心决策:

2.1 两个硬约束

第一,必须有 Chrome Extension。 要控制用户正在使用的浏览器,唯一合法的方式是通过 chrome.debugger API 获取 CDP 控制权。Puppeteer 只能控制全新实例,无法接入用户的标签页、Cookie、登录态。

第二,必须有中间守护进程(Daemon)。 MCP 协议基于 stdio 传输,而 Chrome Extension 没有 stdin/stdout,无法直接实现 MCP Server。需要一个中间进程:对上作为 MCP Server 与 AI 工具通信,对下通过 WebSocket 与 Extension 通信。

所以三层架构是必然选择,不是偏好。

2.2 最终架构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
AI 客户端 / MCP 客户端 / Codex skill
        │ stdio MCP 或本地 HTTP API
OpenBridge Daemon (Node.js)
        │ ws://127.0.0.1:10087/bridge
OpenBridge Chrome Extension (WXT + MV3)
        │ chrome.debugger (CDP)
用户真实 Chrome 标签页

三层职责分明:

职责技术栈
DaemonWebSocket Server、MCP Server、会话管理、工具调度、配对授权Node.js + TypeScript
ExtensionCDP 执行器、工具注册表、光标覆盖层、内容脚本WXT + Chrome MV3
Shared协议定义、错误码、工具 SchemaTypeScript

2.3 为什么不选 Native Messaging?

CodeX 用的是 Native Messaging,优点是沙箱隔离、无端口暴露、更安全。但缺点也很致命:

  • Native Host ID 硬编码,只有特定 CLI 能连接,违背"开放接入"目标
  • 部署复杂,需在系统中注册 Native Messaging Host 配置文件
  • 与 MCP 生态不兼容(MCP 用 stdio,Native Messaging 也用 stdio,两者冲突)

WebSocket 方案虽然需要用户信任 ws://127.0.0.1 连接,但部署更简单、扩展性更好。


3. 核心机制:逐个拆解

3.1 配对授权(Pairing)

因为 WebSocket 绑在 loopback 上,理论上本机任何进程都能连。所以需要一个配对机制:

1
2
3
4
5
Extension → hello → Daemon
Daemon → pair_challenge(challenge=randomString) → Extension
Extension → pair_confirm(challenge, signature) → Daemon
Daemon 验证 → 返回 hello_ack(authorized=true, token=signedToken)
Extension 存 token 到 chrome.storage.local

后续连接时,Extension 直接带 token 发 hello(token=xxx),Daemon 验证签名后直接授权,不再弹出 challenge。

设计要点

  • Token 用 HMAC 签名,防篡改
  • Token 存放在 chrome.storage.local(不是 session,Service Worker 重启不丢失)
  • reset_pairing 清空 token + 断开连接,安全重置

3.2 工具注册表(Tool Registry)

借鉴 Kimi WebBridge 的模块化设计,每个浏览器工具都是独立的 Handler 类,暴露统一接口:

1
2
3
4
interface ToolHandler {
  name: string;
  execute(args: Record<string, any>): Promise<{ data?: any; error?: ... }>;
}

新增一个工具只需:

  1. 写一个 Handler 类
  2. 注册到 toolHandlers 数组
  3. schemas.ts 里加 Schema

3.3 Snapshot Compact Mode

Snapshot 是把页面的可访问性树(AXTree)返回给 AI,让 AI 知道页面上有什么、能点击什么。但原始 AXTree 体积很大(约 39KB),对 token 消耗不友好。

Compact Mode 做了三件事:

1. 过滤无关节点:只保留可交互元素(button、link、textbox 等)和结构元素(heading、list 等),去掉大量无用的 generic/statictext 节点。

2. 稳定 Ref 格式:用 backend-{backendDOMNodeId} 作为 ref,直接对应 CDP 的 backendNodeId。click 工具收到这个 ref 后,通过 DOM.describeNode({ backendNodeId }) 精确定位元素,不需要查 AXTree 转译表。

1
2
3
4
5
6
7
8
9
// snapshot 生成 ref
const ref = `backend-${node.backendDOMNodeId}`;

// click 解析 ref
if (ref.startsWith("backend-")) {
  const backendNodeId = parseInt(ref.slice(8), 10);
  const node = await cdp.send("DOM.describeNode", { backendNodeId });
  // ... 用 node 做点击
}

3. 保留 bounds 信息:告诉 AI 元素的坐标和尺寸,方便后续 mouse_click 精确定位。

3.4 Session 与标签分组管理

为了让 AI 不把用户的标签页搞乱,OpenBridge 实现了 Session 级别的标签组管理:

  • AI 调用 new_tab / select_tab / navigate 时传 sessionIdgroupTitle
  • Extension 自动把标签页加入 Chrome Tab Group,命名如 agent:session-abc123(可自定义)
  • 颜色轮询分配(蓝/红/黄/绿/青/橙/粉/紫/灰),不同 session 不同颜色
  • close_session 批量关闭该 session 管理的所有标签页,不关浏览器

关键实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
async addTabToSession(sessionId: string, tabId: number) {
  const existing = this.sessionGroups.get(sessionId);
  if (existing) {
    // 已存在的分组,把标签加进去
    await chrome.tabs.group({ groupId: existing.groupId, tabIds: [tabId] });
  } else {
    // 新建分组
    const groupId = await chrome.tabs.group({
      tabIds: [tabId],
      createProperties: { windowId: chrome.windows.WINDOW_ID_CURRENT }
    });
    await chrome.tabGroups.update(groupId, { title, color });
    this.sessionGroups.set(sessionId, { groupId, color, title });
  }
}

状态通过 chrome.storage.session 持久化,Service Worker 重启后可以恢复。

3.5 Network Ring Buffer

browser_network 工具让 AI 可以观察页面发送的网络请求(类似 DevTools Network 面板)。实现要点:

  • Ring Buffer:每个 tab 上限 500 条,满了覆盖最旧的
  • Listener 防泄漏:用 tabBuffers + tabListeners 双 Map,每个 tab 独立管理生命周期
  • 全局 onDetach 监听:debugger detach 时自动清理该 tab 的 listener 和 buffer
  • stop/get/clear:支持停止捕获、获取当前请求列表、清空
1
2
3
4
5
start(tabId)  chrome.debugger.sendCommand("Network.enable")
                 onEvent 监听 loadingFinished  记录到 ring buffer
stop(tabId)    removeListener + delete buffer
get(tabId)     返回 ring buffer 快照
clear(tabId)   清空 buffer

3.6 Navigate 等待页面加载

browser_navigate 的难点在于:先监听后导航,否则可能漏掉 load 事件。而且注册监听后要立即检查当前 tab 状态(可能页面已经 loaded),避免永远等不到事件。

1
2
3
4
5
6
7
8
// 工厂化创建监听器(先监听,后导航)
const createLoadListener = () => { /* 注册事件 */ };
const listener = createLoadListener();

// 立即检查当前状态
const currentTab = await chrome.tabs.get(tabId);
if (currentTab.status === "complete") { /* 直接返回 */ }
// 否则等待 listener 触发

3.7 光标覆盖层(Visual Cursor)

参考 CodeX 的虚拟光标设计,OpenBridge 可选地在页面上注入光标覆盖层:

  • Element Highlighter:snapshot 后高亮可交互元素(红框边框),让用户看到 AI 识别了哪些元素
  • Cursor Overlay:click 后在点击位置显示一个扩散动画(类似触摸反馈)
  • 通过 Popup 中的开关控制启用/禁用

内容脚本通过 chrome.tabs.sendMessage 接收 background 的命令,用 Shadow DOM 隔离样式。

3.8 Doctor 诊断工具

openbridge doctor 一键检查所有常见问题:

  1. Node.js 版本(>= 18)
  2. pnpm 是否安装
  3. 构建状态(dist 是否存在)
  4. Extension 版本是否匹配
  5. 配对状态(是否有 token)
  6. 数据目录权限
  7. Daemon 运行状态(Local API 健康检查)
  8. Extension 连接状态(通过 /health API 查询 connectedSessions)
  9. 工具数(是否为 19)
  10. MCP 配置是否存在

4. 踩过的坑

4.1 ws-client 状态顺序 Bug

Service Worker 中 onopen 回调:先调 send() 再设 state="connected",但 send() 内部检查 state !== "connected" 就抛异常。

修复:先设状态,再发消息。

4.2 pair_challenge 字段名不匹配

Daemon 发 { challenge: "xxx" },但 Extension 读 challenge.secret

修复:统一字段名为 challenge

4.3 Chrome 不支持空数组分组

chrome.tabs.group({ tabIds: [] }) 直接报错。Chrome 要求至少一个 tabId。

修复:创建分组时一步完成 chrome.tabs.group({ tabIds: [tabId], createProperties: ... })

4.4 CDP mouse_click button 参数

button 参数用了 "mouseLeft"/"mouseRight"/"mouseMiddle",但 CDP 只接受 "left"/"right"/"middle"

修复:改为标准 CDP 格式。

4.5 Doctor 触发配对重置

Doctor 最初发 fake hello 检查 Extension 连接状态,但这会触发 initiatePairing() 把已有的 extensionTokens 清空,导致已配对的 Extension 断开。

修复initiatePairing() 保留已有 tokens,Doctor 改用 /health API 查连接状态。

4.6 Snapshot Ref 与 Click Ref 不匹配

Compact 模式过滤掉部分 AXTree 节点后,剩余节点重新编号 ax-0ax-1…,导致 click 里的 ax-N 对不上原始 AXTree。

修复:Ref 改为 backend-{backendDOMNodeId},直接对应 CDP 稳定 ID,不需要查 AXTree。

4.7 Navigate 事件漏听

chrome.tabs.update({ url })addListener,可能导航已经完成。

修复:先创建 listener 工厂,注册后立即 chrome.tabs.get(tabId) 检查当前状态。

4.8 Network Listener 泄漏

每次 startaddListener,但从不 removeListener。反复 start/stop 会积累大量无用监听器。

修复:用 tabListeners Map 保存每个 tab 的 listener 引用,stop 时 remove,detach 时自动清理。


5. 安全模型

  • Daemon 只绑定 127.0.0.1(loopback),不暴露到局域网
  • Extension 只连接本地 Daemon
  • 配对 token 用 HMAC 签名,防篡改
  • Popup 中可暂停 AI 控制(paused toggle)
  • browser_evaluate 默认关闭,需用户显式开启
  • 不上报任何遥测数据
  • 不收集、不传输任何用户数据到远程服务器

6. 接入方式

OpenBridge 提供两种接入路径:

路径 A:Skill + Local API(推荐)

install.sh 自动把 openbridge-webbridge skill 安装到 Codex skills 目录。之后 Codex 通过 skill 直接调用 http://127.0.0.1:10088/command,不需要 MCP 配置。

路径 B:MCP stdio(标准)

标准的 MCP Server 配置:

1
2
3
4
5
6
7
8
{
  "mcpServers": {
    "openbridge": {
      "command": "node",
      "args": [".../dist/cli/index.js", "mcp"]
    }
  }
}

两种路径共享同一个 Daemon 实例,Daemon 内部做请求排队和并发控制。


7. 项目结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
openbridge/
├── packages/
   ├── daemon/              # Node.js 守护进程
      └── src/
          ├── cli/          # serve/mcp/status/doctor/reset-pairing
          ├── bridge/       # WebSocket Server + 配对管理
          ├── mcp/          # MCP Server + Schema 定义
          └── service/      # BridgeController + Local API
   ├── extension/            # Chrome Extension (WXT)
      └── src/
          └── background/
              ├── tool-registry/  # 19 个工具 Handler
              ├── session/        # TabGroupManager
              ├── ws-client.ts    # WebSocket 客户端
              ├── command-router.ts
              ├── cdp-executor.ts
              └── reconnect.ts
   └── shared/               # 共享协议 + 类型
├── PRIVACY.md
├── install.sh
└── README.md

8. 设计致谢

OpenBridge 的三层架构借鉴了 Kimi WebBridge(月之暗面)已验证的成熟模型。会话管理和视觉反馈的设计参考了 CodeX(OpenAI)。OpenBridge 在这些理念之上,进行了独立开源实现。


9. 安装与使用

一条命令安装:

1
curl -fsSL https://raw.githubusercontent.com/60ke/openBridge/master/install.sh | bash

或 NPM 安装:

1
2
npm install -g @openbridge-org/daemon
openbridge start

然后在 Chrome 扩展页面加载 unpacked extension(packages/extension/.output/chrome-mv3),Daemon 和 Extension 自动配对授权。


10. 未来计划

  • Proxy 模式:支持 http://localhost:xxxx 代理浏览,让不支持 MCP 的工具也能接入
  • 多浏览器支持:Firefox(通过 WebExtensions API)
  • Web UI:一个本地 Web 控制面板
  • 工具增强:更多视觉交互(拖拽、滚动、右键菜单等)

GitHub: github.com/60ke/openBridge
NPM: @openbridge-org/daemon

使用 Hugo 构建
主题 StackJimmy 设计