浏览器实时预览功能设计
背景
当前前端对话页的“电脑/浏览器画面”主要依赖浏览器工具执行后的关键截图:
- Executor 在浏览器工具调用结束后,通过
BrowserScreenshotHook抓取当前 Chrome 页面截图。 - 截图上传到 Executor Manager,再写入对象存储。
- 前端根据
tool_use_id拉取预签名 URL,用于执行轨迹和历史回放。
这个方案适合回放和审计,但不适合“实时同步浏览器画面”。如果继续通过 hook 高频上传截图,会带来存储、轮询、延迟和 Backend/Executor Manager 压力问题。
本方案目标是在不影响 AI 操作浏览器的前提下,为前端提供只读实时浏览器预览能力。
现状依据
Executor 已具备 CDP 浏览器入口
启用浏览器能力时,Executor 会注入内置 Playwright MCP:
__poco_playwright -> poco-playwright-mcp -> Chrome CDP endpoint默认 CDP 地址为:
http://127.0.0.1:9222相关代码:
executor/app/core/engine.py_inject_playwright_mcpPOCO_BROWSER_CDP_ENDPOINT
当前截图 hook 已经使用 CDP
BrowserScreenshotHook 已经实现了:
- 查询
/json/list获取 page target - 连接
webSocketDebuggerUrl - 执行 CDP 命令
- 调用
Page.captureScreenshot
相关代码:
executor/app/hooks/computer.py_resolve_page_ws_url_cdp_send_and_wait_cdp_capture_screenshot
这说明实时预览不需要重新发明浏览器控制链路,可以复用现有 CDP 基础。
Executor Manager 已维护 session 到容器的映射
Executor Manager 的 ContainerPool 已维护:
self.session_to_container: dict[str, str]
self.containers: dict[str, Container]容器启动时只随机映射宿主机端口,但 EM 会读取 Docker 的 8000/tcp 映射并生成 executor_url:
http://{EXECUTOR_PUBLISHED_HOST}:{random_host_port}因此随机端口不是问题,EM 是当前系统里的服务发现中心。
相关代码:
executor_manager/app/services/container_pool.pyget_or_create_container_resolve_executor_urlsession_to_container
设计结论
最佳方案:
控制面:
Frontend -> Backend -> Executor Manager
数据面:
Frontend -> Executor WebSocket -> Chrome CDP Page.startScreencast也就是说:
- Backend 负责用户鉴权。
- Executor Manager 负责根据
session_id找到对应 Executor。 - Executor 负责浏览器实时画面流。
- Frontend 直接连接对应 Executor 的 WebSocket。
- 历史回放继续使用现有关键截图机制。
不建议让实时帧长期经过 Backend 或 Executor Manager 转发。实时画面是高频二进制数据流,单 Backend/单 EM 容易成为瓶颈。
目标架构
实时预览链路
1. Frontend 申请实时预览地址
建议新增 Backend 用户态接口:
POST /api/v1/sessions/{session_id}/computer/browser-stream职责:
- 校验当前登录用户是否拥有该 session。
- 校验 session 是否启用了浏览器能力。
- 调用 Executor Manager 确认对应 Executor 容器可用。
- 返回 Backend 自己的 WebSocket 代理 URL,前端不直连 worker 或 Executor。
返回示例:
{
"stream_url": "wss://api.example.com/api/v1/sessions/browser-stream/ws?token=xxx",
"expires_at": "2026-04-29T12:05:00Z"
}未配置 BACKEND_URL 时会回退到 Backend 代理相对路径。生产部署应配置 BACKEND_URL,让浏览器直接连接 Backend WebSocket:
{
"stream_url": "ws://localhost:8000/api/v1/sessions/browser-stream/ws?token=xxx",
"expires_at": "2026-04-29T12:05:00Z"
}2. Backend 调用 EM 做服务发现
建议新增 EM 内部接口:
GET /api/v1/computer/browser-stream-url?session_id=...职责:
- 根据
session_id查询容器。 - 确认容器仍在运行。
- 确认容器是 browser-enabled 容器。
- 获取当前容器的
executor_url。 - 生成短期 stream token。
- 返回可连接地址。
3. Frontend 直连 Executor WebSocket
建议 Executor 新增 WebSocket endpoint:
/v1/browser/stream职责:
- 在 WebSocket 握手时校验 token。
- 确认 token 绑定的
session_id与容器环境变量SESSION_ID一致。 - 连接本地 Chrome CDP。
- 找到当前 page target。
- 调用
Page.startScreencast。 - 将 CDP 产生的 JPEG 帧发送给前端。
- 客户端断开时调用
Page.stopScreencast。
Token 设计
Token 只用于建立 WebSocket 连接,不用于连接期间每帧鉴权。
推荐规则:
- token 短期有效,建议 1 到 5 分钟。
- WebSocket 握手成功后,连接可持续到任务结束、用户关闭面板或网络断开。
- 如果连接断开或页面刷新,前端必须重新向 Backend 申请新 token。
Token 需要绑定:
session_id
user_id
container_id
expires_at
nonce / jti
scope = browser_streamExecutor 校验要求:
- token 未过期。
- scope 正确。
- token 中的
session_id等于当前容器环境变量SESSION_ID。 - token 中的
container_id匹配当前容器,避免旧 token 连接到复用后的容器。
与 Playwright MCP 的关系
Playwright MCP 继续负责 AI 浏览器操作。
实时预览只允许只读订阅浏览器画面,不能抢占控制权。
实时预览允许的 CDP 命令:
Page.enable
Page.startScreencast
Page.screencastFrameAck
Page.stopScreencast实时预览禁止执行会改变页面状态或焦点的命令:
Page.bringToFront
Target.activateTarget
Runtime.evaluate
Input.dispatchMouseEvent
Input.dispatchKeyEvent
Emulation.setDeviceMetricsOverride
Page.navigate
Browser.close当前静态截图 hook 为了抓取关键截图会执行 Page.bringToFront。这适合工具执行后的低频关键帧,但实时预览不应频繁调用它,否则可能影响多标签页或弹窗场景下的 AI 操作。
多标签页策略
CDP Screencast 本质上绑定某个 page target,不天然等价于“整个浏览器窗口”。
第一版建议沿用当前截图 hook 的 page 选择逻辑:
- 优先选择非空 URL 页面。
- 避免选择
about:blank、chrome://newtab/。 - 只做当前主要页面的实时预览。
后续增强可以维护显式 active target:
session_id -> active_target_id触发来源:
- Playwright MCP 打开新 tab 或 popup。
- 浏览器工具调用完成后更新当前 page。
- 前端未来提供 tab 切换能力。
第一版不建议支持用户远程切换 tab 或远程输入,避免影响 AI 操作。
性能策略
实时流的成本主要来自:
- Chrome 截图编码 CPU。
- Executor WebSocket 推送带宽。
- 前端图片解码和 Canvas 绘制。
- 慢客户端造成的背压。
建议默认参数:
maxWidth: 1280
maxHeight: 720
format: jpeg
quality: 50-60
目标帧率: 5-10 FPSExecutor 端必须实现:
- 只在前端打开实时预览面板时启动 screencast。
- 面板关闭或 WebSocket 断开时停止 screencast。
- 每个 session 默认只启动一个 CDP screencast。
- 多个 viewer 观看同一 session 时,广播同一份帧。
- 慢客户端不堆积帧,只保留最新帧。
- 对每个 session 和每个 Executor 设置最大观看连接数。
回放策略
实时预览和历史回放分开设计。
实时预览:
CDP Screencast -> WebSocket -> Canvas
不落盘,只展示当前画面
历史回放:
BrowserScreenshotHook -> 关键帧截图 -> 对象存储 -> tool_use_id 回放
继续沿用现有机制不建议第一版将实时流所有帧落盘。否则会引入:
- 大量对象存储写入。
- 帧索引。
- 生命周期清理。
- 视频转码。
- 时间轴同步。
- 任务异常结束后的收尾处理。
如果未来需要更精细回放,可以增加低频抽样帧,例如每 1 到 2 秒保存一帧,但不应阻塞第一版实时预览。
前端展示策略
当前 computer-panel 已经支持按 tool_use_id 展示历史截图。
建议新增一个“实时预览”区域:
- session 活跃且浏览器启用时,显示实时预览入口。
- 用户打开预览后才申请 stream URL。
- WebSocket 收到 JPEG binary frame 后绘制到 Canvas。
- WebSocket 断开时,如果面板仍打开,则重新申请 token 并重连。
- 实时预览失败时,降级到现有关键截图展示。
前端不应直接猜测 Executor 地址,必须通过 Backend/EM 获取。
部署形态
MVP / 本地开发
EM 可以直接返回 Executor 的随机端口直连地址:
ws://localhost:{random_port}/v1/browser/stream?token=xxx适合本地验证。
生产推荐
生产环境建议通过稳定网关暴露实时流:
Frontend
-> wss://executor-gateway.example.com/executors/{session_id}/browser-stream?token=xxx
-> 对应 Executor网关可以使用 Caddy、Nginx、Traefik 或 Envoy。
Backend 和 EM 不负责转发高频帧,只负责:
- 鉴权
- 服务发现
- token 签发
- 返回稳定 stream URL
当前任务容器实际发布的是 Executor FastAPI 的 8000/tcp,不是 sandbox Caddy 的 8080/tcp。因此第一版建议把 WebSocket endpoint 放在 Executor FastAPI 上,而不是优先改 noVNC/Caddy。
改造步骤
第一步:Executor 实现 CDP Screencast WebSocket
新增:
executor/app/api/v1/browser_stream.py核心逻辑:
- 校验 stream token。
- 复用 CDP page target 发现逻辑。
- 调用
Page.startScreencast。 - 收到
Page.screencastFrame后: - 发送 JPEG bytes 给前端。
- 调用
Page.screencastFrameAck。 - 断开时停止 screencast。
第二步:EM 增加 stream URL 发现接口
新增或扩展:
executor_manager/app/api/v1/computer.py核心逻辑:
- 根据
session_id找容器。 - 获取
executor_url。 - 生成短期 token。
- 返回容器信息;Backend 使用固定 WebSocket 代理转发到该 worker。
第三步:Backend 增加用户态入口
新增:
POST /api/v1/sessions/{session_id}/computer/browser-stream核心逻辑:
- 校验 session 所属用户。
- 调用 EM 内部接口。
- 返回前端可连接地址。
第四步:Frontend 增加实时预览组件
在 computer-panel 中新增 Canvas 实时预览:
- 打开面板时申请 stream URL。
- 建立 WebSocket。
- 绘制 JPEG 帧。
- 关闭面板时断开。
- 断线时重新申请 token。
- 失败时回退到现有截图展示。
第五步:保留并优化现有关键截图回放
现有 BrowserScreenshotHook 保留,用于历史回放。
后续可考虑:
- 降低关键截图失败率。
- 标记截图与工具步骤的关系。
- 对浏览器步骤首帧/末帧做更清晰的 UI 呈现。
风险与应对
风险:实时流影响 AI 浏览器操作
应对:
- 实时预览只读。
- 不发送输入事件。
- 不调用
Page.bringToFront。 - 不修改 viewport。
- 不主动切 tab。
风险:Executor CPU 或带宽升高
应对:
- 限制分辨率和质量。
- 限制帧率。
- 慢客户端丢帧。
- 多 viewer 共享一个 screencast。
- 仅面板打开时推流。
风险:Backend/EM 被高频流量打满
应对:
- Backend/EM 不转发实时帧。
- 数据面由 Frontend 直连 Executor 或稳定网关。
风险:token 过期影响长任务预览
应对:
- token 只用于握手。
- 已建立 WebSocket 不因 token 过期主动断开。
- 重连时重新申请 token。
风险:容器复用导致旧 token 误连
应对:
- token 绑定
container_id。 - Executor 校验当前容器身份。
- token 有短过期时间。
推荐验收标准
- 浏览器启用的运行中 session 可以打开实时预览。
- 实时预览不经过 Backend/EM 转发帧。
- 前端关闭预览后 Executor 停止 screencast。
- Playwright MCP 正常点击、输入、导航,不被预览影响。
- 多用户观看同一 session 时,Executor 只启动一个 screencast。
- 历史回放仍使用原有
tool_use_id关键截图。 - 网络断开后,前端可以重新申请 token 并恢复预览。
- session 结束后,实时预览连接自动关闭或进入停止态。
结论
本方案将实时预览从现有“工具结束后截图回放”中拆出来,形成独立的数据面:
实时预览:Frontend 直连 Executor,Executor 用 CDP Screencast 推流
历史回放:继续使用 BrowserScreenshotHook 关键截图
权限路由:Backend/EM 只负责鉴权、服务发现和 token 签发这与当前代码结构最匹配,改造边界清晰,同时避免单 Backend/单 EM 承担高频浏览器画面流。