跳到主要內容

瀏覽器 CDP 監控器 — 設計

狀態: 已發佈 (PR 14540) 最後更新: 2026-04-23 作者: @teknium1

問題

原生 JS 對話框(alert/confirm/prompt/beforeunload)和 iframe 是我們瀏覽器工具鏈中最大的兩個空白:

  1. 對話框阻塞 JS 線程。 頁面上的任何操作都會停滯,直到對話框被處理。在此工作之前,Agent 無法得知對話框已打開——後續的工具調用會掛起或拋出不透明的錯誤。
  2. Iframe 不可見。 Agent 可以在 DOM 快照中看到 iframe 節點,但無法在其中點擊、輸入或執行 eval——尤其是生活在獨立 Chromium 進程中的跨源 (OOPIF) iframe。

PR #12550 提議了一個無狀態的 browser_dialog 包裝器。這並沒有解決檢測問題——它只是一個更乾淨的 CDP 調用,適用於 Agent 已經通過症狀知道對話框已打開的情況。因被取代而關閉。

後端能力矩陣(於 2026-04-23 實時驗證)

使用一次性探測腳本針對一個數據 URL 頁面進行測試,該頁面在主框架和同源 srcdoc iframe 中觸發 alert,以及一個跨源 https://example.com iframe:

後端對話框檢測對話框響應幀樹通過 browser_cdp(frame_id=...) 進行 OOPIF Runtime.evaluate
本地 Chrome (--remote-debugging-port) / /browser connect✓ 完整工作流
Browserbase✓ (通過橋接)✓ 完整工作流 (通過橋接)✓ (document.title = "Example Domain" 在真實跨源 iframe 上已驗證)
Camofox✗ 無 CDP (僅 REST)部分通過 DOM 快照

Browserbase 響應的工作原理。 Browserbase 的 CDP 代理內部使用 Playwright,並在約 10ms 內自動關閉原生對話框,因此 Page.handleJavaScriptDialog 無法跟上。為了解決這個問題,監控器通過 Page.addScriptToEvaluateOnNewDocument 注入一個橋接腳本,該腳本用同步 XHR 覆蓋 window.alert/confirm/prompt,指向一個魔術主機 (hermes-dialog-bridge.invalid)。Fetch.enable 在這些 XHR 接觸網絡之前攔截它們——對話框變為監控器捕獲的 Fetch.requestPaused 事件,而 respond_to_dialog 通過 Fetch.fulfillRequest 完成請求,載荷為注入腳本解碼的 JSON 主體。

最終結果:從頁面的角度來看,prompt() 仍然返回 Agent 提供的字符串。從 Agent 的角度來看,無論如何都是相同的 browser_dialog(action=...) API。針對真實的 Browserbase 會話進行的端到端測試——4/4(alert/prompt/confirm-accept/confirm-dismiss)全部通過,包括值往返傳回頁面 JS。

Camofox 在此 PR 中仍不受支持;計劃在 jo-inc/camofox-browser 上游提出問題,請求添加對話框輪詢端點。

架構

CDPSupervisor

每個 Hermes task_id 在後臺守護線程中運行一個 asyncio.Task。持有到後端 CDP 端點的持久 WebSocket 連接。維護:

  • 對話框隊列List[PendingDialog],包含 {id, type, message, default_prompt, session_id, opened_at}
  • 幀樹Dict[frame_id, FrameInfo],包含父子關係、URL、源、是否為跨源子會話
  • 會話映射Dict[session_id, SessionInfo],以便交互工具可以將 OOPIF 操作路由到正確的附加會話
  • 最近的控制檯錯誤 — 最後 50 條的環形緩衝區(用於 PR 2 診斷)

附加時訂閱:

  • Page.enablejavascriptDialogOpening, frameAttached, frameNavigated, frameDetached
  • Runtime.enableexecutionContextCreated, consoleAPICalled, exceptionThrown
  • Target.setAutoAttach {autoAttach: true, flatten: true} — 暴露子 OOPIF 目標;監控器在每個目標上啟用 Page+Runtime

通過快照鎖實現線程安全的狀態訪問;工具處理程序(同步)讀取凍結的快照而無需等待。

生命週期

  • 啟動: SupervisorRegistry.get_or_start(task_id, cdp_url) — 由 browser_navigate、Browserbase 會話創建、/browser connect 調用。冪等。
  • 停止: 會話 teardown 或 /browser disconnect。取消 asyncio 任務,關閉 WebSocket,丟棄狀態。
  • 重新綁定: 如果 CDP URL 更改(用戶重新連接到新的 Chrome),停止舊監控器並重新啟動——切勿在不同端點間重用狀態。

對話框策略

可通過 config.yaml 中的 browser.dialog_policy 進行配置:

  • must_respond(默認)— 捕獲,在 browser_snapshot 中顯示,等待顯式的 browser_dialog(action=...) 調用。如果在 300 秒安全超時後沒有響應,則自動關閉並記錄日誌。防止有缺陷的 Agent 永遠停滯。
  • auto_dismiss — 記錄並立即關閉;Agent 隨後通過 browser_snapshot 內的 browser_state 看到它。
  • auto_accept — 記錄並接受(對於 beforeunload 很有用,用戶希望乾淨地導航離開)。

策略是按任務設置的;v1 中沒有每個對話框的覆蓋設置。

Agent 表面 (PR 1)

一個新工具

browser_dialog(action, prompt_text=None, dialog_id=None)
  • action="accept" / "dismiss" → 響應指定的或唯一的待處理對話框(必需)
  • prompt_text=... → 提供給 prompt() 對話框的文本
  • dialog_id=... → 當有多個對話框排隊時用於消除歧義(罕見情況)

該工具僅用於響應。Agent 在調用之前從 browser_snapshot 輸出中讀取待處理對話框。

browser_snapshot 擴展

當附加了 supervisor 時,向現有的快照輸出添加三個可選字段:

{
"pending_dialogs": [
{"id": "d-1", "type": "alert", "message": "Hello", "opened_at": 1650000000.0}
],
"recent_dialogs": [
{"id": "d-1", "type": "alert", "message": "...", "opened_at": 1650000000.0,
"closed_at": 1650000000.1, "closed_by": "remote"}
],
"frame_tree": {
"top": {"frame_id": "FRAME_A", "url": "https://example.com/", "origin": "https://example.com"},
"children": [
{"frame_id": "FRAME_B", "url": "about:srcdoc", "is_oopif": false},
{"frame_id": "FRAME_C", "url": "https://ads.example.net/", "is_oopif": true, "session_id": "SID_C"}
],
"truncated": false
}
}
  • pending_dialogs:當前阻塞頁面 JS 線程的對話框。Agent 必須調用 browser_dialog(action=...) 進行響應。在 Browserbase 上為空,因為他們的 CDP 代理會在約 10 毫秒內自動關閉對話框。

  • recent_dialogs:最多包含 20 個最近關閉對話框的環形緩衝區,帶有 closed_by 標籤 — "agent"(我們已響應)、"auto_policy"(本地 auto_dismiss/auto_accept)、"watchdog"(觸及 must_respond 超時)或 "remote"(瀏覽器/後端為我們關閉了它,例如 Browserbase)。這是 Browserbase 上的 Agent 仍能瞭解發生情況的方式。

  • frame_tree:包括跨源 (OOPIF) 子項的幀結構。上限為 30 個條目 + OOPIF 深度 2,以限制廣告密集頁面上的快照大小。當觸及限制時,truncated: true 會顯現;需要完整樹的 Agent 可以使用帶有 Page.getFrameTreebrowser_cdp

這些都沒有新的工具 schema 表面 — Agent 讀取其已經請求的快照。

可用性門控

兩個表面都基於 _browser_cdp_check 進行門控(supervisor 僅在 CDP 端點可達時才能運行)。在 Camofox / 無後端會話中,對話框工具被隱藏,且快照省略新字段 — 不會導致 schema 膨脹。

跨源 iframe 交互

擴展對話框檢測工作,browser_cdp(frame_id=...) 通過 supervisor 已連接的 WebSocket 路由 CDP 調用(特別是 Runtime.evaluate),使用 OOPIF 的子 sessionId。Agent 從 browser_snapshot.frame_tree.children[] 中挑選 is_oopif=true 的 frame_ids,並將其傳遞給 browser_cdp。對於同源 iframe(沒有專用的 CDP 會話),Agent 改用來自頂層 Runtime.evaluatecontentWindow/contentDocument — 當 frame_id 屬於非 OOPIF 時,supervisor 會顯示指向該回退方案的錯誤。

在 Browserbase 上,這是 iframe 交互的唯一可靠路徑 — 無狀態 CDP 連接(每次 browser_cdp 調用時打開)會遇到簽名 URL 過期問題,而 supervisor 的長壽命連接保持有效的會話。

Camofox(後續)

計劃在 jo-inc/camofox-browser 上解決的問題,添加:

  • 每個會話的 Playwright page.on('dialog', handler)
  • GET /tabs/:tabId/dialogs 輪詢端點
  • POST /tabs/:tabId/dialogs/:id 用於接受/關閉
  • 幀樹內省端點

涉及的文件(PR 1)

新增

  • tools/browser_supervisor.pyCDPSupervisor, SupervisorRegistry, PendingDialog, FrameInfo
  • tools/browser_dialog_tool.pybrowser_dialog 工具處理程序
  • tests/tools/test_browser_supervisor.py — 模擬 CDP WebSocket 服務器 + 生命週期/狀態測試
  • website/docs/developer-guide/browser-supervisor.md — 本文件

修改

  • toolsets.py — 在 browser, hermes-acp, hermes-api-server, core toolsets 中註冊 browser_dialog(受 CDP 可達性門控)
  • tools/browser_tool.py
    • browser_navigate 啟動鉤子:如果 CDP URL 可解析,則執行 SupervisorRegistry.get_or_start(task_id, cdp_url)
    • browser_snapshot(約第 1536 行):將 supervisor 狀態合併到返回 payload 中
    • /browser connect 處理程序:使用新端點重啟 supervisor
    • _cleanup_browser_session 中的會話清理鉤子
  • hermes_cli/config.py — 向 DEFAULT_CONFIG 添加 browser.dialog_policybrowser.dialog_timeout_s
  • 文檔:website/docs/user-guide/features/browser.md, website/docs/reference/tools-reference.md, website/docs/reference/toolsets-reference.md

非目標

  • Camofox 的檢測/交互(上游缺口;單獨跟蹤)
  • 將對話框/幀事件實時流式傳輸給用戶(需要網關鉤子)
  • 跨會話持久化對話框歷史(僅限內存)
  • 每個 iframe 的對話框策略(Agent 可以通過 dialog_id 表達這一點)
  • 替換 browser_cdp — 它仍然作為長尾情況(cookie、視口、網絡節流)的應急出口

測試

單元測試使用一個 asyncio 模擬 CDP 服務器,該服務器足以執行協議來演練所有狀態轉換:附加、啟用、導航、觸發對話框、關閉對話框、幀附加/分離、子目標附加、會話清理。真實後端 E2E(Browserbase + 本地 Chrome)是手動的;來自 2026-04-23 調查的探測腳本保留在倉庫中的 scripts/browser_supervisor_e2e.py 下,以便任何人都可以在新後端版本上重新驗證。