实验
浏览器 Evaluate CDP 重构
背景
act:evaluate 在页面中执行用户提供的 JavaScript。目前它通过 Playwright(page.evaluate 或 locator.evaluate)运行。Playwright 按页面序列化 CDP 命令,因此一个卡住或长时间运行的 evaluate 会阻塞页面命令队列,并使该标签页上的后续每个操作看起来都“卡住”。PR #13498 增加了一个实用的安全网(有界 evaluate、中止传播和尽力恢复)。本文档描述了一个更大的重构,使 act:evaluate 本质上与 Playwright 隔离,这样卡住的 evaluate 就无法阻塞正常的 Playwright 操作。
目标
act:evaluate不能永久阻塞同一标签页上的后续浏览器操作。- 超时是端到端的单一事实来源,以便调用者可以依赖预算。
- 中止和超时在 HTTP 和进程内分派中以相同方式处理。
- 支持 evaluate 的元素定位,而无需将所有内容从 Playwright 切换出去。
- 保持对现有调用者和负载的向后兼容性。
非目标
- 用 CDP 实现替换所有浏览器操作(点击、输入、等待等)。
- 移除 PR #13498 中引入的现有安全网(它仍然是一个有用的后备方案)。
- 在现有的
browser.evaluateEnabled门控之外引入新的不安全功能。 - 为 evaluate 添加进程隔离(工作进程/线程)。如果在此次重构后我们仍然看到难以恢复的卡死状态,那将是一个后续想法。
当前架构(为何会卡住)
从高层次看:
- 调用者向浏览器控制服务发送
act:evaluate。 - 路由处理程序调用 Playwright 来执行 JavaScript。
- Playwright 序列化页面命令,因此一个永不结束的 evaluate 会阻塞队列。
- 卡住的队列意味着该标签页上后续的点击/输入/等待操作可能看起来挂起。
提议的架构
1. 截止时间传播
引入单一的预算概念,并从中推导出所有内容:
- 调用者设置
timeoutMs(或未来的一个截止时间)。 - 外部请求超时、路由处理程序逻辑以及页面内的执行预算都使用相同的预算,并在需要时 为序列化开销留出少量余量。
- 中止作为
AbortSignal到处传播,以便取消操作保持一致。
实施方向:
- 添加一个小型辅助工具(例如
createBudget({ timeoutMs, signal })),返回:signal:链接的 AbortSignaldeadlineAtMs:绝对截止时间remainingMs():用于子操作的剩余预算
- 在以下位置使用此辅助工具:
src/browser/client-fetch.ts(HTTP 和进程内分派)src/node-host/runner.ts(代理路径)- 浏览器操作实现(Playwright 和 CDP)
2. 独立的 Evaluate 引擎(CDP 路径)
添加一个基于 CDP 的 evaluate 实现,该实现不共享 Playwright 的每页面命令队列。关键特性是 evaluate 传输使用单独的 WebSocket 连接和附加到目标的单独 CDP 会话。实施方向:
- 新模块,例如
src/browser/cdp-evaluate.ts,该模块:- 连接到配置的 CDP 端点(浏览器级别套接字)。
- 使用
Target.attachToTarget({ targetId, flatten: true })获取sessionId。 - 运行以下之一:
Runtime.evaluate用于页面级 evaluate,或DOM.resolveNode加上Runtime.callFunctionOn用于元素 evaluate。
- 在超时或中止时:
- 尽力为该会话发送
Runtime.terminateExecution。 - 关闭 WebSocket 并返回明确的错误。
- 尽力为该会话发送
注意:
- 这仍然在页面中执行 JavaScript,因此终止可能会有副作用。好处在于它不会阻塞 Playwright 队列,并且可以通过在传输层终止 CDP 会话来取消。
3. Ref 策略(无需完全重写的元素定位)
难点在于元素定位。CDP 需要一个 DOM 句柄或 backendDOMNodeId,而目前大多数浏览器操作使用基于快照中 refs 的 Playwright 定位器。推荐方法:保留现有的 refs,但附加一个可选的 CDP 可解析 id。
3.1 扩展存储的 Ref 信息
扩展存储的角色 ref 元数据以可选地包含 CDP id:
- 当前:
{ role, name, nth } - 提议:
{ role, name, nth, backendDOMNodeId?: number }
这使所有现有的基于 Playwright 的操作保持工作,并允许 CDP evaluate 在 backendDOMNodeId 可用时接受相同的 ref 值。