Communitygithub.com

browser-use/browser-harness-js

通过 DevTools 协议,使用 JavaScript 驱动 Chrome。借助 `browser-harness-js` CLI 运行 JS 片段,它自动生成一个持久的 Bun HTTP 服务,持有完整类型化的 CDP `Session`。

browser-harness-js 是什么?

`browser-harness-js` 是一个 AI Agent 技能,它通过 CDP (Chrome DevTools 协议) 从 JavaScript 脚本或命令行直接驱动和控制 Chrome 浏览器。该技能利用一个持久的 Bun HTTP 服务,该服务持有完全类型化的 CDP `Session`,使得浏览器会话、活动标签页和全局变量在多次调用之间保持存活。它支持单标签页和多标签页操作,可以附加到正在运行的 Chrome 实例上,具备事件监听、目标切换、持久化状态等高级功能。核心价值在于为 AI Agent(如 Claude Code、Cursor)提供原生的浏览器自动化能力,无需 Selenium 或 Puppeteer 等重型第三方依赖。支持通过 CLI、stdin 或直接 API 调用执行 CDP 方法(包括 Page.navigate、DOM.getDocument 等),并带有智能浏览器自动检测、多标签页切换、事件订阅和持久化状态传递等丰富特性。

兼容平台✓Claude Code~Codex CLI✓Cursor
npx skills add browser-use/browser-harness-js

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

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 / boolean42、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 --logstail -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 流程)

当附加到用户已经正在运行的浏览器时:

  1. 首先尝试 await session.connect()(无参数)— 自动检测通过 DevToolsActivePort 处理所有基于 Chromium 的浏览器。如果它返回,您就完成了。
  2. 如果自动检测失败,并显示 No running browser with remote debugging detected,则用户需要开启远程调试。打开检查页面:
    # 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'
    
    只有 macOS 的 AppleScript 路径可以避免配置文件选择器;Linux/Windows 可能会先提示用户选择一个配置文件。
  3. 在 chrome://inspect 中勾选“发现网络目标”,然后在 Chrome 提示时点击允许。
  4. 如果自动检测选择了错误的浏览器(有多个在运行,您想要特定的一个):使用 await detectBrowsers() 列出它们,然后使用 await session.connect({ profileDir: <您想要的那个> })。
  5. 如果 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 — 上游协议(已提供)

相关技能