CDP — browser-harness-js 技能
自定義程式碼生成的 CDP SDK(來自 browser_protocol.json + js_protocol.json 的每個方法都有型別包裝器),加上一個持有單一持久 CDP Session 的小型 HTTP 伺服器。browser-harness-js CLI 會在首次使用時自動啟動伺服器,並將 JS 程式碼片段轉發給它。
SDK 位於技能的 sdk/ 目錄中。在本文檔的其餘部分,<skill-dir> 指的是 npx skills add 安裝技能的位置(Claude Code:~/.claude/skills/cdp;Cursor:~/.cursor/skills/cdp;其他代理可能不同)。CLI 應在 PATH 上可用,名為 browser-harness-js。
設定(一次,首次使用)
npx skills add 會將技能放入代理的技能目錄,但不會將 CLI 加入 PATH。在第一次呼叫之前,請確認它可以被找到,如果沒有,則將其符號連結到 PATH 中的任何目錄:
# macOS(Apple Silicon + Homebrew)
command -v browser-harness-js >/dev/null || ln -sf <skill-dir>/sdk/browser-harness-js /opt/homebrew/bin/browser-harness-js
# macOS(Intel)/ 大多數 Linux — 可能需要 sudo
command -v browser-harness-js >/dev/null || ln -sf <skill-dir>/sdk/browser-harness-js /usr/local/bin/browser-harness-js
# Linux 沒有 sudo(確保 ~/.local/bin 在 PATH 中)
command -v browser-harness-js >/dev/null || { mkdir -p ~/.local/bin && ln -sf <skill-dir>/sdk/browser-harness-js ~/.local/bin/browser-harness-js; }
CLI 在首次運行時會自動安裝 bun(如果缺少)。設定 BROWSER_HARNESS_SKIP_BUN_INSTALL=1 來選擇不安裝。
使用方法
直接執行 browser-harness-js '<JS>'。第一次呼叫會在背景啟動伺服器;後續呼叫會命中同一個進程,因此會重用相同的 session、相同的連接到 Chrome 的 WebSocket,以及你設定的任何全域變數。
browser-harness-js 'await session.connect()'
browser-harness-js 'await session.Page.navigate({url:"https://example.com"})'
browser-harness-js '(await session.Runtime.evaluate({expression:"document.title",returnByValue:true})).result.value'
輸出是原始結果內容 — 沒有 {ok,result} 封裝。
| 結果類型 | stdout |
|---|---|
| 字串 | 純文字,無 JSON 引號(例如 Example Domain) |
| 數字 / 布林值 | 42, true |
| 物件 / 陣列(非空) | 緊湊 JSON(例如 {"frameId":"..."}, [1,2,3]) |
undefined / null / "" / {} / [] | 空(無輸出) |
錯誤會輸出到 stderr,退出碼為 1。CDP 錯誤訊息和 JS 堆疊會照原樣列印,例如:
Error: CDP -32602: invalid params
at _call (.../session.ts:117:33)
...
使用 if browser-harness-js '...'; then ...; else handle_error; fi 或檢查 $? 來檢測失敗。
透過 stdin(heredoc)的多行程式碼片段。 重要:多陳述式片段不會自動返回最後一個表達式 — 請明確撰寫 return X。作為第一個參數傳遞的單表達式片段會自動返回。
browser-harness-js <<'EOF'
const tabs = await listPageTargets();
globalThis.tid = tabs[0].targetId;
await session.use(globalThis.tid);
return globalThis.tid;
EOF
CLI 命令
| 命令 | 行為 |
|---|---|
browser-harness-js '<js>' | 如有需要自動啟動伺服器,評估 JS,列印結果。 |
browser-harness-js <<EOF…EOF | 同上,程式碼來自 stdin。 |
browser-harness-js --status | 列印健康 JSON(執行時間、連線狀態、sessionId),若伺服器關閉則退出碼 1。 |
browser-harness-js --start | 明確啟動(如果已在運行則無操作)。 |
browser-harness-js --stop | 優雅關閉。丟棄 session 狀態。 |
browser-harness-js --restart | 停止 + 重新啟動。 |
browser-harness-js --logs | tail -f 伺服器日誌(/tmp/browser-harness-js.log)。 |
環境變數:CDP_REPL_PORT(預設 9876),CDP_REPL_LOG(預設 /tmp/browser-harness-js.log)。
片段內部的 API 介面
這些全域變數已預先載入 — 無需匯入:
session— 持久的Session。掛載了所有 CDP 領域:session.Page、session.DOM、session.Runtime、session.Network、… 總共 56 個領域,652 個方法。listPageTargets()— 透過 CDP 的Target.getTargets列出真實頁面目標(在 Chrome 144+ 上也適用),並已過濾掉chrome://和devtools://URL。無需參數 — 使用已連線的 session。detectBrowsers()— 掃描作業系統特定的設定檔目錄,尋找正在運行且啟用了遠端除錯的基於 Chromium 的瀏覽器。返回[{name, profileDir, port, wsPath, wsUrl, mtimeMs}],按最近啟動排序。resolveWsUrl(opts)— 從{wsUrl}|{port, host?}|{profileDir}解析 WS URL。對於無參數的自動檢測流程,請直接呼叫session.connect()。CDP— 生成的命名空間(CDP.Page、CDP.Runtime、…),用於型別名稱參考。
呼叫 CDP 方法
每個方法都接受一個與 CDP 線路參數匹配的單一物件參數;它解析為型別化的返回值(沒有 result 封裝,沒有 id 關聯 — 已為你處理)。
// 無參數
await session.DOM.enable()
// 必需參數
await session.Page.navigate({ url: 'https://example.com' })
// 全部為可選參數(物件也是可選的)
await session.Page.captureScreenshot()
await session.Page.captureScreenshot({ format: 'png', quality: 80 })
// 返回值已被剝離為型別化的形狀
const { root } = await session.DOM.getDocument()
const { nodeId } = await session.DOM.querySelector({ nodeId: root.nodeId, selector: 'h1' })
連線
預設:只需使用無參數呼叫 session.connect()。 它會透過掃描作業系統特定的設定檔目錄中的 DevToolsActivePort 檔案,自動檢測正在運行的基於 Chromium 的瀏覽器(Chrome、Chromium、Edge、Brave、Arc、Vivaldi、Opera、Comet、Canary),按最近啟動排序,並選取第一個其 WebSocket 接受連線的瀏覽器。作業系統無關 — 適用於 macOS、Linux、Windows。
await session.connect() // 自動檢測
如果你想知道有哪些瀏覽器可用(或讓使用者選擇),請先使用 detectBrowsers():
const found = await detectBrowsers()
// [{ name: 'Google Chrome', profileDir, port, wsPath, wsUrl, mtimeMs }, ...]
明確形式 — 僅在自動檢測選錯瀏覽器時,或你已經知道要連線到哪裡時使用:
| 形式 | 使用時機 |
|---|---|
{ profileDir } | 當多個瀏覽器正在運行時,鎖定特定瀏覽器。直接讀取 <profileDir>/DevToolsActivePort。 |
{ wsUrl } | 你已經有 ws://…/devtools/browser/<uuid>(例如從其他地方傳遞而來)。 |
await session.connect({ profileDir: '/Users/<you>/Library/Application Support/Google/Chrome' })
await session.connect({ wsUrl: 'ws://127.0.0.1:9222/devtools/browser/<uuid>' })
各作業系統的設定檔路徑 — 與 { profileDir } 搭配使用:
- macOS:
~/Library/Application Support/<Browser>(例如Google/Chrome、Comet、BraveSoftware/Brave-Browser、Arc/User Data) - Linux:
~/.config/<browser>(例如google-chrome、chromium、BraveSoftware/Brave-Browser) - Windows:
%LOCALAPPDATA%\<Browser>\User Data(例如Google\Chrome、Microsoft\Edge、BraveSoftware\Brave-Browser)
對每個候選者的 WS 開啟超時時間預設為 5 秒 — 正在運行的瀏覽器通常會在 100 毫秒內回應開啟/關閉,因此 5 秒已經很寬裕。唯一 5 秒太短的情況是當 Chrome 顯示允許彈出視窗並等待使用者點擊時。如果你預期這種情況,請傳入 timeoutMs: 30000:
await session.connect({ profileDir: '/Users/<you>/Library/Application Support/Google/Chrome', timeoutMs: 30_000 })
如果你看到 No detected browser accepted a connection — 瀏覽器有 DevToolsActivePort 檔案,但目前沒有瀏覽器在提供 WS 服務。最常見的原因是:遠端除錯已啟用,但使用者尚未在提示上點擊允許。請告知他們點擊允許,然後重試(或增加 timeoutMs)。
選取目標(標籤頁)
在 connect() 之後,呼叫 session.use(targetId) 一次;後續的頁面級別呼叫(Page/DOM/Runtime/Network 等)會自動路由到該目標的 sessionId。Browser.* 和 Target.* 呼叫始終命中瀏覽器端點。
const tabs = await listPageTargets() // 無需參數;使用已連線的 session
const sid = await session.use(tabs[0].targetId)
await session.Page.enable()
await session.Page.navigate({ url: 'https://example.com' })
listPageTargets() 使用 CDP 的 Target.getTargets(而不是 /json),因此它也可以在 Chrome 144+ 上運作。它已經過濾掉了 chrome:// 和 devtools:// URL。等效的原始呼叫:
const { targetInfos } = await session.Target.getTargets({})
const tabs = targetInfos.filter(t => t.type === 'page' && !t.url.startsWith('chrome://') && !t.url.startsWith('devtools://'))
切換標籤頁:session.use(otherTargetId)。解除連接:session.setActiveSession(undefined)。
事件
// 訂閱(返回一個取消訂閱函式)
const off = session.onEvent((method, params, sessionId) => { ... })
// 或者等待一個匹配的單一事件,帶有可選的斷言和超時
await session.Network.enable()
const ev = await session.waitFor(
'Page.frameNavigated',
(p) => p.frame.url.includes('example.com'),
10_000
)
跨呼叫持久化狀態
每個程式碼片段在自己的非同步包裝器中執行,因此其 let/const 宣告在返回時會消失。要將資料傳遞到下一次呼叫,請附加到 globalThis:
browser-harness-js '(await listPageTargets()).forEach((t,i)=>globalThis["tab"+i]=t.targetId)'
browser-harness-js 'await session.use(globalThis.tab0)'
browser-harness-js 'await session.Page.navigate({url:"https://example.com"})'
session 本身、活動的 sessionId 和事件訂閱者已由伺服器保留 — 全域變數僅用於隨附資料。
連線到正在執行的 Chrome(chrome://inspect 流程)
當連接到用戶已正在運行的瀏覽器時:
- 首先嘗試
await session.connect()(無參數)— 自動檢測透過DevToolsActivePort處理所有基於 Chromium 的瀏覽器。如果成功返回,你就完成了。 - 如果自動檢測失敗,出現
No running browser with remote debugging detected,使用者需要手動開啟。開啟檢查頁面:只有 macOS 的 AppleScript 路徑可以避免設定檔選擇器;Linux/Windows 可能會提示使用者先選取設定檔。# macOS — 偏好使用 AppleScript 而非 `open -a`(重用當前設定檔,避免設定檔選擇器) osascript -e 'open location "chrome://inspect/#remote-debugging"' # Linux google-chrome 'chrome://inspect/#remote-debugging' # 或:chromium, google-chrome-stable # Windows(PowerShell) Start-Process chrome 'chrome://inspect/#remote-debugging' - 在
chrome://inspect中勾選「Discover network targets」,然後在 Chrome 提示時點擊允許。 - 如果自動檢測選錯了瀏覽器(多個瀏覽器正在運行,你想要特定的那個):使用
await detectBrowsers()列出它們,然後await session.connect({ profileDir: <你想要的設定檔> })。 - 如果
session.connect()返回No detected browser accepted a connection,使用者已啟用遠端除錯但尚未點擊允許。告知他們點擊它並重試,或傳入timeoutMs: 30000等待點擊。
處理目標(標籤頁)
- 過濾 Chrome 內部頁面。
listPageTargets()已經丟棄了chrome://和devtools://URL。如果你直接呼叫Target.getTargets(),請手動過濾。 - CDP 目標順序 ≠ 可見的標籤頁順序。 當使用者說「我看到的第一個標籤頁」時,請使用螢幕截圖或頁面標題來識別它 —
Target.activateTarget僅切換到已知的 targetId。
查詢方法
完整的型別化 API 在 <skill-dir>/sdk/generated.ts 中(約 655 KB,僅在你讀取它時才會載入)。每個方法都有其 CDP 描述作為 JSDoc 註解,以及每個領域命名空間中的型別化 *Params / *Return 介面。
grep -n "navigate" <skill-dir>/sdk/generated.ts | head
重新生成 SDK
當上游協定 JSON 變更時,替換 sdk/browser_protocol.json 和/或 sdk/js_protocol.json 並重新執行:
cd <skill-dir>/sdk && bun gen.ts
browser-harness-js --restart # 載入新的繫結
檔案
所有路徑都相對於 <skill-dir>(安裝路徑 — 請參閱本文檔頂部)。
/usr/local/bin/browser-harness-js→<skill-dir>/sdk/browser-harness-js(CLI)sdk/repl.ts— HTTP 伺服器(Bun.serve在127.0.0.1:9876)sdk/session.ts—Session類別(傳輸、連線、目標路由、事件)sdk/generated.ts— 程式碼生成輸出:每個 CDP 方法作為型別化包裝器sdk/gen.ts— 程式碼生成腳本sdk/{browser,js}_protocol.json— 上游協定(已帶入 Vendor)