瀏覽器 CDP 監控器 — 設計
狀態: 已發佈 (PR 14540) 最後更新: 2026-04-23 作者: @teknium1
問題
原生 JS 對話框(alert/confirm/prompt/beforeunload)和 iframe 是我們瀏覽器工具鏈中最大的兩個空白:
- 對話框阻塞 JS 線程。 頁面上的任何操作都會停滯,直到對話框被處理。在此工作之前,Agent 無法得知對話框已打開——後續的工具調用會掛起或拋出不透明的錯誤。
- 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.enable—javascriptDialogOpening,frameAttached,frameNavigated,frameDetachedRuntime.enable—executionContextCreated,consoleAPICalled,exceptionThrownTarget.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.getFrameTree的browser_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.evaluate 的 contentWindow/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.py—CDPSupervisor,SupervisorRegistry,PendingDialog,FrameInfotools/browser_dialog_tool.py—browser_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.pybrowser_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_policy和browser.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 下,以便任何人都可以在新後端版本上重新驗證。