关于 cdp
cdp 是供编码 Agent 使用的浏览器控制技能,提供覆盖 CDP 协议的类型化 SDK 和 browser-harness-js 命令行工具。它通过常驻会话复用与 Chrome 的连接,并允许跨调用保留状态。
主要功能
- 01提供类型化 CDP 方法封装
- 02命令行执行 JavaScript 片段
- 03后台服务维持持久 CDP 会话
- 04连接并切换浏览器标签页
- 05支持事件订阅与等待
适用场景
- 01通过脚本导航和检查网页
- 02在多个调用间复用浏览器会话
- 03监听页面或网络事件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ browser-use/ browser-harness-js --skill cdp安装后在 Claude Code 中输入/cdp确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdLICENSE.gitignoreinteraction-skills/connection.mddialogs.mdcookies.mdcross-origin-iframes.mddrag-and-drop.mddownloads.mddropdowns.mdiframes.mdprint-as-pdf.mdscreenshots.mdnetwork-requests.mdscrolling.mdshadow-dom.mdtabs.mduploads.mdviewport.mdsdk/bun.lockbrowser-harness-jsgen.tsrepl.tspackage.jsonsession.tssession.test.tsjs_protocol.jsontsconfig.json详细介绍
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;其他 Agent 的位置各不相同)。CLI 应已位于 PATH 中,名称为 browser-harness-js。
设置(一次性,首次使用时)
npx skills add 会将技能放入 Agent 的技能目录,但不会将 CLI 添加到 PATH。首次调用前,请确认 CLI 可用;如果不可用,请将其符号链接到 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) / most Linux — may need sudo
command -v browser-harness-js >/dev/null || ln -sf <skill-dir>/sdk/browser-harness-js /usr/local/bin/browser-harness-js
# Linux without sudo (ensure ~/.local/bin is on 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} 封装。
| 结果类型 | 标准输出 |
|---|---|
| 字符串 | 原样文本,不带 JSON 引号(例如 Example Domain) |
| 数字 / 布尔值 | 42、true |
| 对象 / 数组(非空) | 紧凑 JSON(例如 {"frameId":"..."}、[1,2,3]) |
undefined / null / "" / {} / [] | 空(无输出) |
错误会输出到标准错误,退出代码为 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 关联,这些由 SDK 自动处理)。
// no params
await session.DOM.enable()
// required params
await session.Page.navigate({ url: 'https://example.com' })
// all-optional params (object also optional)
await session.Page.captureScreenshot()
await session.Page.captureScreenshot({ format: 'png', quality: 80 })
// returns are stripped to the typed shape
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() // auto-detect
如果想在连接前查看可用浏览器(或让用户选择),请先使用 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 打开超时默认为 5s——运行中的浏览器会在约 100ms 内响应打开/关闭,因此 5s 已经很充裕。只有当 Chrome 显示 Allow 弹窗并等待用户点击时,5s 才会不够。如果预期会出现这种情况,请传入 timeoutMs: 30000:
await session.connect({ profileDir: '/Users/<you>/Library/Application Support/Google/Chrome', timeoutMs: 30_000 })
如果看到 No detected browser accepted a connection——浏览器存在 DevToolsActivePort 文件,但当前没有浏览器在提供 WS 服务。最常见的原因是:已启用远程调试,但用户还没有点击提示中的 Allow。请让用户点击 Allow,然后重试(或增大 timeoutMs)。
选择目标(标签页)
调用 connect() 后,调用一次 session.use(targetId);之后的页面级调用(Page/DOM/Runtime/Network 等)会自动路由到该目标的 sessionId。Browser.* 和 Target.* 调用始终发往浏览器端点。
const tabs = await listPageTargets() // no args; uses the connected 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)。
事件
// Subscribe (returns an unsubscribe fn)
const off = session.onEvent((method, params, sessionId) => { ... })
// Or wait for a single matching event with optional predicate + timeout
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 — prefer AppleScript over `open -a` (reuses current profile, avoids the profile picker) osascript -e 'open location "chrome://inspect/#remote-debugging"' # Linux google-chrome 'chrome://inspect/#remote-debugging' # or: chromium, google-chrome-stable # Windows (PowerShell) Start-Process chrome 'chrome://inspect/#remote-debugging' - 勾选 chrome://inspect 中的“Discover network targets”,然后在 Chrome 提示时点击 Allow。
- 如果自动检测选错浏览器(有多个浏览器正在运行,而你想指定某一个):调用
await detectBrowsers()列出浏览器,然后调用await session.connect({ profileDir: <the one you want> })。 - 如果
session.connect()返回No detected browser accepted a connection,说明用户已启用远程调试,但还没有点击 Allow。请让用户点击后重试,或传入timeoutMs: 30000等待用户点击。
使用目标(标签页)
- 过滤 Chrome 内部页面。
listPageTargets()已经会过滤掉chrome://和devtools://URL。如果直接调用Target.getTargets(),请手动过滤。 - CDP 目标顺序 ≠ 可见的标签栏顺序。 用户说“我能看到的第一个标签页”时,请使用截图或页面标题来识别——
Target.activateTarget只能切换到已知的 targetId。
查找方法
完整的类型化接口位于 <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 # pick up the new bindings
文件
所有路径均相对于 <skill-dir>(安装路径,参见本文档顶部)。
/usr/local/bin/browser-harness-js→<skill-dir>/sdk/browser-harness-js(CLI)sdk/repl.ts— HTTP 服务(在127.0.0.1:9876上运行的Bun.serve)sdk/session.ts—Session类(传输、连接、目标路由、事件)sdk/generated.ts— 代码生成输出:每个 CDP 方法均为类型化封装sdk/gen.ts— 代码生成脚本sdk/{browser,js}_protocol.json— 上游协议(随附)