Browser Evaluate CDP 重構計畫

背景

act:evaluate 在頁面中執行使用者提供的 JavaScript。目前透過 Playwright(page.evaluatelocator.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:串聯的 AbortSignal
    • deadlineAtMs:絕對截止時間
    • 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
    • 逾時或 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 快照時:

  1. 如現行方式產生既有的 role ref 對應(role、name、nth)。
  2. 透過 CDP(Accessibility.getFullAXTree)取得 AX 樹,使用相同的重複處理規則計算 (role, name, nth) -> backendDOMNodeId 的並行對應。
  3. 將 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 不會卡死後續操作的測試。
  • 讓失敗和回退可見的日誌/指標。

實作檢查清單

  1. 新增共用的「預算」輔助函式,將 timeoutMs + 上游 AbortSignal 串聯為:
    • 單一 AbortSignal
    • 絕對截止時間
    • 供下游操作的 remainingMs() 輔助函式
  2. 更新所有呼叫者路徑使用該輔助函式,使 timeoutMs 在各處含義一致:
    • src/browser/client-fetch.ts(HTTP 和行程內分派)
    • src/node-host/runner.ts(node 代理路徑)
    • 呼叫 /act 的 CLI 包裝器(在 browser evaluate 加入 --timeout-ms
  3. 實作 src/browser/cdp-evaluate.ts
    • 連接至瀏覽器層級的 CDP socket
    • Target.attachToTarget 取得 sessionId
    • 頁面 evaluate 使用 Runtime.evaluate
    • 元素 evaluate 使用 DOM.resolveNode + Runtime.callFunctionOn
    • 逾時/abort 時:盡力 Runtime.terminateExecution 然後關閉 socket
  4. 擴展儲存的 role ref 中繼資料,選用地包含 backendDOMNodeId
    • 保留既有的 { role, name, nth } 行為供 Playwright 操作
    • 為 CDP 元素定位新增 backendDOMNodeId?: number
  5. 在快照建立時填入 backendDOMNodeId(盡力):
    • 透過 CDP(Accessibility.getFullAXTree)取得 AX 樹
    • 計算 (role, name, nth) -> backendDOMNodeId 並合併至儲存的 ref 對應
    • 若對應模糊或遺失,留 id 為 undefined
  6. 更新 act:evaluate 路由:
    • ref:一律使用 CDP evaluate
    • ref 解析為 backendDOMNodeId:使用 CDP 元素 evaluate
    • 否則:回退至 Playwright evaluate(仍有界且可中止)
  7. 保留既有的「最後手段」復原路徑作為回退,非預設路徑。
  8. 新增測試:
    • 卡住的 evaluate 在預算內逾時,下一個 click/type 成功
    • abort 取消 evaluate(用戶端斷線或逾時),後續操作不被阻塞
    • 對應失敗時乾淨地回退至 Playwright
  9. 新增可觀測性:
    • evaluate 持續時間和逾時計數器
    • terminateExecution 使用情況
    • 回退率(CDP -> Playwright)與原因

驗收標準

  • 刻意掛起的 act:evaluate 在呼叫者預算內回傳,且不卡死該分頁的後續操作。
  • timeoutMs 在 CLI、代理工具、node 代理和行程內呼叫中行為一致。
  • ref 可對應至 backendDOMNodeId,元素 evaluate 使用 CDP;否則回退路徑仍有界且可復原。

測試計畫

  • 單元測試:
    • (role, name, nth) 在 role ref 和 AX 樹節點間的比對邏輯。
    • 預算輔助函式行為(預留量、剩餘時間計算)。
  • 整合測試:
    • CDP evaluate 逾時在預算內回傳且不阻塞下一個操作。
    • Abort 取消 evaluate 並盡力觸發終止。
  • 合約測試:
    • 確保 BrowserActRequestBrowserActResponse 保持相容。

風險與緩解

  • 對應不完美:
    • 緩解:盡力對應、回退至 Playwright evaluate,並加入除錯工具。
  • Runtime.terminateExecution 有副作用:
    • 緩解:僅在逾時/abort 時使用,並在錯誤中記載行為。
  • 額外開銷:
    • 緩解:僅在請求快照時取得 AX 樹、按目標快取,並保持 CDP session 短期存活。
  • 擴充套件中繼限制:
    • 緩解:當逐頁面 socket 不可用時使用瀏覽器層級 attach API,並保留現有 Playwright 路徑作為回退。

待決問題

  • 新引擎是否應可設定為 playwrightcdpauto
  • 是否為進階使用者公開新的「nodeRef」格式,還是僅保留 ref
  • frame 快照和 selector 範圍快照如何參與 AX 對應?