Poco 使用手册
平台说明

浏览器实时预览功能设计

背景

当前前端对话页的“电脑/浏览器画面”主要依赖浏览器工具执行后的关键截图:

  • 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_mcp
  • POCO_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.py
  • get_or_create_container
  • _resolve_executor_url
  • session_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_stream

Executor 校验要求:

  • 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:blankchrome://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 FPS

Executor 端必须实现:

  • 只在前端打开实时预览面板时启动 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 承担高频浏览器画面流。

On this page