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 最终架构
| |
三层职责分明:
| 层 | 职责 | 技术栈 |
|---|---|---|
| Daemon | WebSocket Server、MCP Server、会话管理、工具调度、配对授权 | Node.js + TypeScript |
| Extension | CDP 执行器、工具注册表、光标覆盖层、内容脚本 | WXT + Chrome MV3 |
| Shared | 协议定义、错误码、工具 Schema | TypeScript |
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 上,理论上本机任何进程都能连。所以需要一个配对机制:
| |
后续连接时,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 类,暴露统一接口:
| |
新增一个工具只需:
- 写一个 Handler 类
- 注册到
toolHandlers数组 - 在
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 转译表。
| |
3. 保留 bounds 信息:告诉 AI 元素的坐标和尺寸,方便后续 mouse_click 精确定位。
3.4 Session 与标签分组管理
为了让 AI 不把用户的标签页搞乱,OpenBridge 实现了 Session 级别的标签组管理:
- AI 调用
new_tab/select_tab/navigate时传sessionId和groupTitle - Extension 自动把标签页加入 Chrome Tab Group,命名如
agent:session-abc123(可自定义) - 颜色轮询分配(蓝/红/黄/绿/青/橙/粉/紫/灰),不同 session 不同颜色
close_session批量关闭该 session 管理的所有标签页,不关浏览器
关键实现:
| |
状态通过 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:支持停止捕获、获取当前请求列表、清空
| |
3.6 Navigate 等待页面加载
browser_navigate 的难点在于:先监听后导航,否则可能漏掉 load 事件。而且注册监听后要立即检查当前 tab 状态(可能页面已经 loaded),避免永远等不到事件。
| |
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 一键检查所有常见问题:
- Node.js 版本(>= 18)
- pnpm 是否安装
- 构建状态(dist 是否存在)
- Extension 版本是否匹配
- 配对状态(是否有 token)
- 数据目录权限
- Daemon 运行状态(Local API 健康检查)
- Extension 连接状态(通过 /health API 查询 connectedSessions)
- 工具数(是否为 19)
- 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-0、ax-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 泄漏
每次 start 都 addListener,但从不 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 配置:
| |
两种路径共享同一个 Daemon 实例,Daemon 内部做请求排队和并发控制。
7. 项目结构
| |
8. 设计致谢
OpenBridge 的三层架构借鉴了 Kimi WebBridge(月之暗面)已验证的成熟模型。会话管理和视觉反馈的设计参考了 CodeX(OpenAI)。OpenBridge 在这些理念之上,进行了独立开源实现。
9. 安装与使用
一条命令安装:
| |
或 NPM 安装:
| |
然后在 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