H繁中版
文件開發者指南browser supervisor
<!-- Source: https://hermesbible.com/docs/developer-guide/browser-supervisor -->

CDP Supervisor 填補了 Hermes 瀏覽器工具中長期存在的兩個缺口:

  1. 原生 JS 對話框alert/confirm/prompt/beforeunload)會阻塞頁面的 JS 執行緒。如果沒有監控機制,代理程式無法得知對話框已經開啟 — 後續的工具呼叫會卡住或拋出不明確的錯誤。
  2. 跨來源 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.enablejavascriptDialogOpeningframeAttachedframeNavigatedframeDetached
  • Runtime.enableexecutionContextCreatedconsoleAPICalledexceptionThrown
  • Target.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.pyCDPSupervisorSupervisorRegistryPendingDialogFrameInfo
  • tools/browser_dialog_tool.pybrowser_dialog 工具處理器
  • tools/browser_tool.pybrowser_navigate 啟動鉤子、browser_snapshot 合併、/browser connect 重新連結、_cleanup_browser_session 拆除
  • toolsets.py — 在 browserhermes-acphermes-api-server 和核心工具組中註冊 browser_dialog(基於 CDP 可達性進行門控)
  • hermes_cli/config.pybrowser.dialog_policybrowser.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 族瀏覽器並執行上述的對話框/框架測試案例。



Context Engine Plugins