CDP — browser-harness-js 技能
自定义代码生成的 CDP SDK(browser_protocol.json + js_protocol.json 中的每个方法都有类型化包装)加上一个微型的 HTTP 服务器,该服务器持有一个持久的 CDP Session。browser-harness-js CLI 在首次使用时自动启动服务器,并将 JS 片段转发给该服务器。
SDK 位于技能的 sdk/ 目录中。本文档的剩余部分中,<skill-dir> 指的是 npx skills add 安装技能的位置(Claude Code:~/.claude/skills/cdp;Cursor:~/.cursor/skills/cdp;其他 Agent 的位置可能不同)。CLI 应位于 PATH 上,名为 browser-harness-js。
设置(仅在首次使用时)
npx skills add 会将技能放入 Agent 的技能目录,但不会将 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; }
如果缺少 bun,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 |
|---|---|
| string | 纯文本,无 JSON 引号(例如 Example Domain) |
| number / boolean | 42、true |
| object / array(非空) | 紧凑型 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 | 优雅关闭。丢弃会话状态。 |
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。无参数 — 使用已连接的会话。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)
每个候选的 WebSocket 打开超时默认值为 5s — 正在运行的浏览器大约在 100ms 内响应打开/关闭,所以 5s 已经很宽裕了。5s 太短的唯一情况是当 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() // 无参数;使用已连接的会话
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 中勾选“发现网络目标”,然后在 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— 上游协议(已提供)