浏览器控制 · 总览与架构¶
让 Ethan 在任意对话渠道(Web / 飞书 / CLI)中,操作 ethan server 所在机器上的真实 Chrome —— 创建隔离的浏览器会话、管理标签页、读取页面可访问性树、点击/输入、截图、执行页面脚本。
本组文档面向需要理解、维护或扩展该子系统的工程师,按"总览 → 传输与协议 → 扩展内核 → 会话/并发/安全"四层组织。设计决策的来龙去脉见 设计决策记录。
1. 设计目标与边界¶
在做什么¶
Ethan 自身是一个常驻的异步 HTTP 服务(单进程 uvicorn)。浏览器控制子系统在此之上增加一条能力:Agent 调用工具 → 服务端把指令经 WebSocket 下发给浏览器扩展 → 扩展通过 Chrome DevTools Protocol(CDP)操作真实页面 → 结果原路返回。
整套能力对齐 browser_use / agent-browser 的交互范式:以可访问性树(Accessibility Tree)+ ref 句柄为主的结构化页面操作,辅以坐标级鼠标事件作为兜底。
不在本阶段范围¶
- 跨公网远程控制:当前假设 ethan 与浏览器在同一台机器(本机裸跑或本机 Docker),传输层只面向
localhost。 - 多浏览器连接池:同一时刻只维护一条扩展连接(后文 last-wins 策略)。
- 完整跨域 iframe 操作:仅做基础处理。
- 完整的 turn/lease/overlay 运行时:RPC id 仅用于请求追踪。
2. 调用链总览¶

调用链共三段(相比同类桌面方案省去了 Native Messaging Host 与独立 RPC server 两层):
flowchart LR
subgraph EthanServer["Ethan Server (单进程 uvicorn)"]
A["Agent 工具<br/>browser_session / browser_tab / browser_page"]
H["BrowserHub<br/>(进程内单例)"]
WS["FastAPI WebSocket 路由<br/>/ws/browser"]
A -->|"call(method, params)"| H
H <-->|"JSON-RPC over WS"| WS
end
subgraph Extension["Chrome 扩展 (MV3 Service Worker)"]
WC["ws-client<br/>(保活 + 重连)"]
RPC["rpc dispatch<br/>(method 路由)"]
SS["session-store<br/>(状态唯一真源)"]
PC["page-controller"]
CDP["cdp-client<br/>(按 tab 缓存 attach)"]
WC --> RPC --> SS
RPC --> PC --> CDP
end
WS <-->|"WebSocket<br/>ws://localhost:<port>/ws/browser"| WC
CDP -->|"Chrome DevTools Protocol"| Page["Chrome 标签页 / TabGroups / Page"]
三段链路的职责切分¶
| 层 | 位置 | 职责 | 是否持有状态 |
|---|---|---|---|
| Agent 工具 | ethan/tools/builtin/browser.py |
把模型的 action 参数映射为 JSON-RPC method;做会话归属门禁、snapshot 截断、截图落盘 | 否 |
| BrowserHub | ethan/browser/hub.py |
持有唯一 WS 连接;请求/响应按 id 配对;30s 超时;per-session 串行锁;断连即 fail | 仅连接运行态,不镜像浏览器状态 |
| WS 路由 | ethan/browser/ws_route.py |
/ws/browser 端点;首帧 token 鉴权;ping/pong;把连接交给 Hub |
否 |
| 扩展 ws-client | browser-extension/.../ws-client.ts |
WS 客户端;心跳 ping;chrome.alarms 保活;指数退避重连 |
连接态 |
| 扩展 rpc | browser-extension/.../rpc.ts |
method 白名单 + 参数校验 + 分发到 session-store / page-controller | 否 |
| session-store | browser-extension/.../session-store.ts |
session ↔ Chrome TabGroup 映射,标签归属,浏览器状态的唯一真源 | 是(权威) |
| page-controller | browser-extension/.../page-controller.ts |
经 CDP 执行 snapshot / 交互 / 截图 / eval | 否(每次实时取 active tab) |
| cdp-client | browser-extension/.../cdp-client.ts |
按 tabId 缓存 chrome.debugger attach,避免反复 attach/detach |
是(attach 缓存) |
状态归属原则:浏览器的"真相"只在扩展侧。ethan 服务端不保存 session/tab/page 的状态镜像,只保留一张"ethan 会话 ↔ browser session"的轻量映射用于隔离与生命周期管理(见会话/并发/安全)。
3. 一次 page.snapshot 的端到端时序¶
sequenceDiagram
autonumber
participant Agent as Agent 工具<br/>(browser_page)
participant Hub as BrowserHub
participant WS as /ws/browser
participant Ext as 扩展 rpc
participant PC as page-controller
participant CDP as cdp-client
participant Page as 页面
Agent->>Agent: 会话归属校验<br/>session ∈ list_for(ethan_session)
Agent->>Hub: call("pages.snapshot", {sessionId, ...})
Note over Hub: 取 per-session Lock<br/>(pages.* 串行)
Hub->>Hub: 分配 req_id, 建 Future, 30s 超时
Hub->>WS: send {jsonrpc, id, method, params}
WS->>Ext: WebSocket 文本帧
Ext->>Ext: 白名单校验 + 参数归一化
Ext->>PC: snapshot(params)
PC->>CDP: withCdpClient(activeTabId)
CDP->>Page: Accessibility.getFullAXTree / DOM.*
Page-->>CDP: AX 节点 + DOM box
CDP-->>PC: 原始树
PC->>PC: 构建 ref map + 渲染文本/JSON
PC-->>Ext: {snapshot, refs}
Ext-->>WS: {jsonrpc, id, result}
WS-->>Hub: on_message → resolve Future(id)
Hub-->>Agent: result
Agent->>Agent: 输出 > 30k 字符则截断并提示缩小范围
Agent-->>Agent: 刷新 session 活跃时间 (touch)
关键不变量:
- req_id 配对:Hub 用单调递增的整型 id 关联请求与响应;扩展原样回填 id。响应到达时按 id 找到挂起的
Future并 resolve。 - pages.* 串行:同一
browser_session_id的页面操作在 Hub 侧用asyncio.Lock排队,避免两个并发对话对同一 active tab 发出交错的 CDP 命令。不同 session 之间并行。 - ref 时效:
refs仅对"最近一次该 tab 的 snapshot"可靠。页面发生导航或刷新后,旧 ref 失效,必须重新 snapshot(见扩展内核)。
4. 代码地图¶
ethan/browser/
├── __init__.py 子系统说明
├── protocol.py JSON-RPC method 表 / error code / 超时常量
├── hub.py BrowserHub 单例:连接管理 / req-id 配对 / 锁 / 超时
├── ws_route.py FastAPI WS 端点 /ws/browser:鉴权 / ping-pong
├── session_map.py ethan 会话 ↔ browser session 映射 + idle release 扫描
├── screenshot.py 截图 base64 落盘 + 定时按龄清理
├── auth.py 会话级一次性授权状态
└── http_route.py /api/browser/shot/{name} 截图文件路由
ethan/tools/builtin/browser.py browser_session / browser_tab / browser_page 三工具
browser-extension/
├── src/manifest.json MV3 manifest(无 nativeMessaging,改 host_permissions)
├── src/background/
│ ├── index.ts SW 入口:装配 dispatch + ws-client + 事件监听
│ ├── ws-client.ts WS 连接 / 保活 / 重连(本方案新增)
│ ├── rpc.ts method 白名单 + 分发(复用,改传输)
│ ├── session-store.ts session/TabGroup 状态真源(复用)
│ ├── page-controller.ts CDP 页面操作(复用)
│ ├── cdp-client.ts 按 tab 缓存 CDP attach(复用)
│ ├── ax-snapshot.ts AX 树 → ref map(复用)
│ ├── ref-store.ts ref 生命周期(复用)
│ └── page-runtime.ts 页面运行时辅助(复用)
├── src/shared/ 本地 vendored 协议常量与类型
└── src/popup/ 弹窗设置页(地址 / token / 连接状态)
扩展侧的 CDP / AX / session 内核逻辑整体复用自一套成熟实现,仅把传输层从 Native Messaging 改为 WebSocket。复用与重写的精确边界见设计决策记录。
5. 各专题文档导航¶
- 传输层与协议 —— WebSocket 选型、JSON-RPC 信封、method/error 表、req-id 配对、超时与断连语义、last-wins 连接策略。
- 扩展内核(CDP/AX) —— Service Worker 保活、CDP attach 缓存、可访问性树快照算法、ref 句柄生命周期、page-controller 各动作的 CDP 实现。
- 会话/并发/安全 —— 会话绑定模型、per-session 锁、idle release、token 鉴权、会话级授权门禁、会话归属隔离、截图隐私与清理、
eval权限边界。