Browser Evaluate CDP 重構計畫
背景
act:evaluate 在頁面中執行使用者提供的 JavaScript。目前透過 Playwright(page.evaluate 或 locator.evaluate)執行。Playwright 會為每個頁面序列化 CDP 指令,因此卡住或長時間執行的 evaluate 會阻塞頁面指令佇列,導致後續對該分頁的所有操作看起來「卡住」。
PR #13498 加入了務實的安全網(有界 evaluate、abort 傳播和盡力復原)。本文件描述更大規模的重構,讓 act:evaluate 從根本上與 Playwright 隔離,使卡住的 evaluate 不會卡死正常的 Playwright 操作。
目標
act:evaluate不可永久阻塞同一分頁的後續瀏覽器操作。- 逾時有單一事實來源,端對端可靠地依賴預算。
- abort 和逾時在 HTTP 和行程內分派中以相同方式處理。
- 支援 evaluate 的元素定位而不需將所有功能從 Playwright 遷出。
- 維持現有呼叫者和 payload 的向後相容性。
非目標
- 以 CDP 實作取代所有瀏覽器操作(click、type、wait 等)。
- 移除 PR #13498 引入的既有安全網(保留為有用的回退)。
- 引入超出現有
browser.evaluateEnabled開關的新不安全功能。 - 為 evaluate 加入行程隔離(worker 行程/執行緒)。若重構後仍出現難以復原的卡住狀態,這會是後續方向。
現有架構(為何會卡住)
概略來說:
- 呼叫者將
act:evaluate傳送至瀏覽器控制服務。 - 路由處理器呼叫 Playwright 執行 JavaScript。
- Playwright 序列化頁面指令,因此永不結束的 evaluate 會阻塞佇列。
- 被阻塞的佇列意味著後續對該分頁的 click/type/wait 操作會看似掛起。
提議的架構
1. 截止時間傳播
引入單一預算概念,從中衍生所有值:
- 呼叫者設定
timeoutMs(或未來的截止時間)。 - 外層請求逾時、路由處理器邏輯和頁面內的執行預算全部使用相同預算,必要時預留少量的序列化開銷。
- Abort 以
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 session。
實作方向:
- 新模組,例如
src/browser/cdp-evaluate.ts,功能包含:- 連接至設定的 CDP 端點(瀏覽器層級 socket)。
- 使用
Target.attachToTarget({ targetId, flatten: true })取得sessionId。 - 執行:
- 頁面級 evaluate 使用
Runtime.evaluate,或 - 元素級 evaluate 使用
DOM.resolveNode加上Runtime.callFunctionOn。
- 頁面級 evaluate 使用
- 逾時或 abort 時:
- 盡力傳送
Runtime.terminateExecution。 - 關閉 WebSocket 並回傳清楚的錯誤。
- 盡力傳送
注意:
- 這仍然在頁面中執行 JavaScript,因此終止可能有副作用。優勢在於不會卡死 Playwright 佇列,且可在傳輸層透過關閉 CDP session 來取消。
3. Ref 方案(不全面改寫的元素定位)
難點在於元素定位。CDP 需要 DOM handle 或 backendDOMNodeId,而目前大部分瀏覽器操作使用基於快照 ref 的 Playwright locator。
建議方式:保留既有的 ref,但附加選用的 CDP 可解析 id。
3.1 擴展儲存的 Ref 資訊
擴展儲存的 role ref 中繼資料,選用地包含 CDP id:
- 現行:
{ role, name, nth } - 提議:
{ role, name, nth, backendDOMNodeId?: number }
這讓所有既有的 Playwright 操作繼續運作,並允許 CDP evaluate 在 backendDOMNodeId 可用時接受相同的 ref 值。
3.2 在快照時填入 backendDOMNodeId
產生 role 快照時:
- 如現行方式產生既有的 role ref 對應(role、name、nth)。
- 透過 CDP(
Accessibility.getFullAXTree)取得 AX 樹,使用相同的重複處理規則計算(role, name, nth) -> backendDOMNodeId的並行對應。 - 將 id 合併回當前分頁的儲存 ref 資訊。
若某個 ref 的對應失敗,留 backendDOMNodeId 為 undefined。這讓此功能為盡力而為且可安全上線。
3.3 帶 Ref 的 Evaluate 行為
在 act:evaluate 中:
- 若有
ref且含backendDOMNodeId,透過 CDP 執行元素 evaluate。 - 若有
ref但無backendDOMNodeId,回退至 Playwright 路徑(搭配安全網)。
選用的逃生艙口:
- 擴展請求格式以直接接受
backendDOMNodeId供進階呼叫者使用(以及除錯),同時保留ref作為主要介面。
4. 保留最後手段的復原路徑
即使有 CDP evaluate,仍有其他方式可卡死分頁或連線。保留既有的復原機制(terminate execution + 斷開 Playwright)作為最後手段,用於:
- 舊版呼叫者
- CDP attach 被阻擋的環境
- 非預期的 Playwright 邊緣案例
實作計畫(單一迭代)
可交付成果
- 基於 CDP 的 evaluate 引擎,在 Playwright 逐頁面指令佇列之外執行。
- 呼叫者和處理器一致使用的單一端對端逾時/abort 預算。
- 可選用地攜帶
backendDOMNodeId的 ref 中繼資料,用於元素 evaluate。 act:evaluate盡可能使用 CDP 引擎,不行時回退至 Playwright。- 證明卡住的 evaluate 不會卡死後續操作的測試。
- 讓失敗和回退可見的日誌/指標。
實作檢查清單
- 新增共用的「預算」輔助函式,將
timeoutMs+ 上游AbortSignal串聯為:- 單一
AbortSignal - 絕對截止時間
- 供下游操作的
remainingMs()輔助函式
- 單一
- 更新所有呼叫者路徑使用該輔助函式,使
timeoutMs在各處含義一致:src/browser/client-fetch.ts(HTTP 和行程內分派)src/node-host/runner.ts(node 代理路徑)- 呼叫
/act的 CLI 包裝器(在browser evaluate加入--timeout-ms)
- 實作
src/browser/cdp-evaluate.ts:- 連接至瀏覽器層級的 CDP socket
Target.attachToTarget取得sessionId- 頁面 evaluate 使用
Runtime.evaluate - 元素 evaluate 使用
DOM.resolveNode+Runtime.callFunctionOn - 逾時/abort 時:盡力
Runtime.terminateExecution然後關閉 socket
- 擴展儲存的 role ref 中繼資料,選用地包含
backendDOMNodeId:- 保留既有的
{ role, name, nth }行為供 Playwright 操作 - 為 CDP 元素定位新增
backendDOMNodeId?: number
- 保留既有的
- 在快照建立時填入
backendDOMNodeId(盡力):- 透過 CDP(
Accessibility.getFullAXTree)取得 AX 樹 - 計算
(role, name, nth) -> backendDOMNodeId並合併至儲存的 ref 對應 - 若對應模糊或遺失,留 id 為 undefined
- 透過 CDP(
- 更新
act:evaluate路由:- 無
ref:一律使用 CDP evaluate ref解析為backendDOMNodeId:使用 CDP 元素 evaluate- 否則:回退至 Playwright evaluate(仍有界且可中止)
- 無
- 保留既有的「最後手段」復原路徑作為回退,非預設路徑。
- 新增測試:
- 卡住的 evaluate 在預算內逾時,下一個 click/type 成功
- abort 取消 evaluate(用戶端斷線或逾時),後續操作不被阻塞
- 對應失敗時乾淨地回退至 Playwright
- 新增可觀測性:
- evaluate 持續時間和逾時計數器
- terminateExecution 使用情況
- 回退率(CDP -> Playwright)與原因
驗收標準
- 刻意掛起的
act:evaluate在呼叫者預算內回傳,且不卡死該分頁的後續操作。 timeoutMs在 CLI、代理工具、node 代理和行程內呼叫中行為一致。- 若
ref可對應至backendDOMNodeId,元素 evaluate 使用 CDP;否則回退路徑仍有界且可復原。
測試計畫
- 單元測試:
(role, name, nth)在 role ref 和 AX 樹節點間的比對邏輯。- 預算輔助函式行為(預留量、剩餘時間計算)。
- 整合測試:
- CDP evaluate 逾時在預算內回傳且不阻塞下一個操作。
- Abort 取消 evaluate 並盡力觸發終止。
- 合約測試:
- 確保
BrowserActRequest和BrowserActResponse保持相容。
- 確保
風險與緩解
- 對應不完美:
- 緩解:盡力對應、回退至 Playwright evaluate,並加入除錯工具。
Runtime.terminateExecution有副作用:- 緩解:僅在逾時/abort 時使用,並在錯誤中記載行為。
- 額外開銷:
- 緩解:僅在請求快照時取得 AX 樹、按目標快取,並保持 CDP session 短期存活。
- 擴充套件中繼限制:
- 緩解:當逐頁面 socket 不可用時使用瀏覽器層級 attach API,並保留現有 Playwright 路徑作為回退。
待決問題
- 新引擎是否應可設定為
playwright、cdp或auto? - 是否為進階使用者公開新的「nodeRef」格式,還是僅保留
ref? - frame 快照和 selector 範圍快照如何參與 AX 對應?