CDP Supervisor 填補了 Hermes 瀏覽器工具中長期存在的兩個缺口:
- 原生 JS 對話框(
alert/confirm/prompt/beforeunload)會阻塞頁面的 JS 執行緒。如果沒有監控機制,代理程式無法得知對話框已經開啟 — 後續的工具呼叫會卡住或拋出不明確的錯誤。 - 跨來源 iframe(OOPIF) 對頂層的
Runtime.evaluate是不可見的。代理程式可以在 DOM 快照中看到 iframe 節點,但無法在其中進行點擊、輸入或執行 JS,除非有一個 CDP session 連結到子目標。
Supervisor 透過為每個瀏覽器任務維持一個到後端 CDP 端點的持久 WebSocket 連線來解決這兩個問題,將待處理的對話框和框架結構呈現到 browser_snapshot 中,並提供 browser_dialog 工具用於明確回應。
後端支援
| 後端 | 對話框偵測 | 對話框回應 | 框架樹 | 透過 browser_cdp(frame_id=...) 進行 OOPIF Runtime.evaluate |
|---|---|---|---|---|
本地 Chrome(--remote-debugging-port)/ /browser connect | ✓ | ✓ 完整流程 | ✓ | ✓ |
| Browserbase | ✓(透過橋接器) | ✓ 完整流程(透過橋接器) | ✓ | ✓ |
| Camofox | ✗ 無 CDP(僅 REST) | ✗ | 透過 DOM 快照部分支援 | ✗ |
Browserbase 特殊情況。 Browserbase 的 CDP 代理在內部使用 Playwright,並在約 10ms 內自動關閉原生對話框,因此 Page.handleJavaScriptDialog 來不及處理。Supervisor 透過 Page.addScriptToEvaluateOnNewDocument 注入一個橋接腳本,將 window.alert/confirm/prompt 替換為指向一個魔法主機(hermes-dialog-bridge.invalid)的同步 XHR。Fetch.enable 會在這些 XHR 接觸網路之前攔截它們 — 對話框變成一個 Fetch.requestPaused 事件,由 Supervisor 擷取,然後 respond_to_dialog 透過 Fetch.fulfillRequest 以注入腳本解碼的 JSON 體回應體來完成。
從頁面的角度來看,prompt() 仍然返回代理程式提供的字串。從代理程式的角度來看,無論哪種方式都是相同的 browser_dialog(action=...) API。
Camofox 不受支援 — 沒有 CDP 介面,僅支援 REST。
架構
CDPSupervisor
每個 Hermes task_id 對應一個在背景守護執行緒中運行的 asyncio.Task。維持一個到後端 CDP 端點的持久 WebSocket 連線。維護:
- 對話框佇列 —
List[PendingDialog],包含{id, type, message, default_prompt, session_id, opened_at} - 框架樹 —
Dict[frame_id, FrameInfo],包含父級關係、URL、來源、是否為跨來源子 session - Session 對應表 —
Dict[session_id, SessionInfo],使互動工具能正確路由到連結的 session 以進行 OOPIF 操作 - 最近的控制台錯誤 — 用於診斷的最近 50 筆環形緩衝區
訂閱事件:
Page.enable—javascriptDialogOpening、frameAttached、frameNavigated、frameDetachedRuntime.enable—executionContextCreated、consoleAPICalled、exceptionThrownTarget.setAutoAttach {autoAttach: true, flatten: true}— 顯示子 OOPIF 目標;Supervisor 在每個目標上啟用Page+Runtime
透過快照鎖實現執行緒安全的狀態存取;工具處理器(同步)在不等待的情況下讀取凍結的快照。
生命週期
- 啟動:
SupervisorRegistry.get_or_start(task_id, cdp_url)— 由browser_navigate、Browserbase session 建立、/browser connect呼叫。具冪等性。 - 停止: session 拆除或
/browser disconnect。取消 asyncio 任務,關閉 WebSocket,捨棄狀態。 - 重新綁定: 如果 CDP URL 變更(使用者重新連接到新的 Chrome),會停止舊的 Supervisor 並啟動一個新的 — 狀態永遠不會在端點之間複用。
對話框策略
可透過 config.yaml 中的 browser.dialog_policy 進行設定:
must_respond(預設)— 擷取、在browser_snapshot中顯示、等待明確的browser_dialog(action=...)呼叫。在 300 秒安全逾時且無回應後,自動關閉並記錄日誌。防止有問題的代理程式永遠停住。auto_dismiss— 記錄後立即關閉;代理程式事後透過browser_snapshot中的browser_state查看。auto_accept— 記錄後接受(對於beforeunload很有用,當工作流程需要順暢地導航離開時)。
策略是每個任務獨立的;沒有針對每個對話框的覆寫。
代理程式介面
browser_dialog 工具
browser_dialog(action, prompt_text=None, dialog_id=None)
action="accept"/"dismiss"→ 回應指定的或唯一的待處理對話框(必填)prompt_text=...→ 提供給prompt()對話框的文字dialog_id=...→ 當多個對話框在佇列中時用於消歧(少見)
工具是僅回應的。代理程式在呼叫前從 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 執行緒的對話框。代理程式必須呼叫browser_dialog(action=...)來回應。在 Browserbase 上為空,因為它們的 CDP 代理在約 10ms 內自動關閉。 -
recent_dialogs— 最近關閉的最多 20 個對話框的環形緩衝區,帶有closed_by標籤:"agent"(我們回應了)、"auto_policy"(本地 auto_dismiss/auto_accept)、"watchdog"(must_respond 逾時觸發)、"remote"(瀏覽器/後端關閉了它,例如 Browserbase)。這是 Browserbase 上的代理程式仍然能看到發生了什麼的方式。 -
frame_tree— 框架結構,包含跨來源(OOPIF)子框架。限制為 30 個條目 + OOPIF 深度 2,以控制廣告密集頁面的快照大小。當達到限制時會顯示truncated: true;需要完整樹狀結構的代理程式可以使用browser_cdp搭配Page.getFrameTree。
這些都不需要新的工具 schema 介面 — 代理程式讀取它已經請求的快照。
可用性控制
兩個介面都透過 _browser_cdp_check 進行門控(Supervisor 只能在 CDP 端點可達時運行)。在 Camofox / 無後端 session 上,對話框工具會被隱藏,快照會省略新欄位 — 不會增加 schema 膨脹。
跨來源 iframe 互動
browser_cdp(frame_id=...) 使用 OOPIF 子 sessionId,透過 Supervisor 已連線的 WebSocket 路由 CDP 呼叫(特別是 Runtime.evaluate)。代理程式從 browser_snapshot.frame_tree.children[] 中挑選 is_oopif=true 的 frame_id 並傳遞給 browser_cdp。對於同來源 iframe(沒有專用的 CDP session),代理程式改為從頂層 Runtime.evaluate 使用 contentWindow/contentDocument — 當 frame_id 屬於非 OOPIF 時,Supervisor 會顯示一個錯誤指向該替代方案。
在 Browserbase 上,這是 iframe 互動唯一可靠的路徑 — 無狀態 CDP 連線(每次 browser_cdp 呼叫時開啟)會遇到簽名 URL 過期,而 Supervisor 的長期連線保持有效 session。
檔案結構
tools/browser_supervisor.py—CDPSupervisor、SupervisorRegistry、PendingDialog、FrameInfotools/browser_dialog_tool.py—browser_dialog工具處理器tools/browser_tool.py—browser_navigate啟動鉤子、browser_snapshot合併、/browser connect重新連結、_cleanup_browser_session拆除toolsets.py— 在browser、hermes-acp、hermes-api-server和核心工具組中註冊browser_dialog(基於 CDP 可達性進行門控)hermes_cli/config.py—browser.dialog_policy和browser.dialog_timeout_s預設值
非目標
- Camofox 的偵測/互動(上游缺口;另行追蹤)
- 即時將對話框/框架事件串流給使用者(需要閘道器鉤子)
- 跨 session 持久化對話框歷史(僅限記憶體)
- 每個 iframe 的對話框策略(代理程式可以透過
dialog_id表達) - 取代
browser_cdp— 它仍然作為長尾功能的逃生艙口(cookie、viewport、網路限流)
測試
單元測試(tests/tools/test_browser_supervisor.py)使用一個 asyncio 模擬 CDP 伺服器,該伺服器實作了足夠的協定來測試所有狀態轉換:連結、啟用、導航、對話框觸發、對話框關閉、框架連結/斷開、子目標連結、session 拆除。真實後端的端對端測試(Browserbase + 本地 Chromium 族瀏覽器)是手動的 — 透過 /browser connect 連結到即時的 Chromium 族瀏覽器並執行上述的對話框/框架測試案例。