可靠的浏览器自动化之旅

Claude Code 的浏览器插件在交互式会话中效果很好,但在隔夜 cron 任务中却失效:错误的浏览器实例、会话丢失、没有配置文件感知。本文将介绍如何使用 CDP、AppleScript 窗口指纹识别和 Vercel 的 agent-browser 构建可靠的浏览器自动化,以解决计划任务的多配置文件浏览器控制问题。这是我让浏览器自动化 cron 任务在凌晨 4 点、5 点等时间运行的旅程。

你的隔夜 cron 任务在凌晨 3 点触发。浏览器扩展已失效。错误的配置文件处于最前端。而且 Claude Code 不知道它正在查看哪个客户的仪表板。

我为不同的公司使用不同的 Brave 浏览器配置文件。作为顾问,这是不可协商的。每个配置文件都是不同工作流的窗口:一个用于我的个人账户(Medium、LinkedIn、GitHub),一个用于 NovaTech(Teams、Outlook、Jira),一个用于 GreenField Labs(Harvest 工时卡,他们的内部工具),以及一个用于我自己的公司 SpillWave(X、产品仓库)。我绝不想让一个客户的 Intranet 标签页出现在另一个客户的浏览器会话中。配置文件完全隔离了凭证、Cookie 和浏览历史。

Brave 和 Chrome 都是基于 Chromium 的浏览器。我更喜欢 Brave,但相同的技术也兼容 Chrome,就像适用于 Chrome 的 Claude 插件也适用于 Brave 浏览器一样。

这个设置在我尝试将 Claude Code 的 /loop/schedule 功能用于夜间自动化(从 Claude Code 桌面应用)之前一直完美运行。

1、问题:凌晨 3 点需要可靠的浏览器

Claude Code 中的 /loop 命令确实是变革性的。你可以告诉 Claude 每隔五分钟检查一次部署、照看一个 PR,或按重复计划扫描错误日志。我在一篇之前的文章中写过这些功能。可能性令人兴奋。

最有用的计划任务需要浏览器访问。"每小时检查我们的 staging 环境并截图仪表板。""监控 CI 管道,如果有问题就联系我。""每天早上从分析仪表板拉取最新指标。"这些正是你希望在睡觉时运行的任务类型。

为此,Claude Code 需要控制一个浏览器。Claude 浏览器插件(Chrome 中的 Claude)是一个明显的起点,在交互式会话中效果很好。当我开始用于夜间自动化时,三个问题很快浮现:

  • 错误的浏览器实例。 插件有时会连接到错误配置文件的浏览器窗口,当我想要 GreenField 的数据时却从 NovaTech 的仪表板拉取数据。(NovaTech 和 GreenField 是我的 Brave Browser 的配置文件名。如果你使用 Chrome 或 Chromium,所有这些都适用于你的浏览器)。
  • 会话丢失。 扩展会在夜间长时间空闲期间静默丢失连接。一个在凌晨 3 点触发的 cron 任务会发现一个已失效的扩展。
  • 没有配置文件感知。 插件不知道它连接的是哪个 Brave 配置文件。它只看到碰巧处于最前端的浏览器窗口。

我需要更可靠的东西:一个可以针对特定浏览器配置文件、保持夜间稳定且无需照料的能力。于是我开始深入研究。(到目前为止,我已经写了本文的第二部分。)

2、洞察:Brave 会说 Chrome DevTools 协议

关键 realization 很简单。Brave 是基于 Chromium 的,所以它原生支持 CDP(Chrome DevTools Protocol)。任何能说 CDP 的工具都可以连接到正在运行的 Brave 实例。你只需要用远程调试端口启用来启动该 Brave 配置文件。

--remote-debugging-port 标志在 localhost 上打开一个调试端口。--profile-directory 标志告诉 Brave 要加载哪个配置文件,包括其所有 Cookie、登录会话和扩展。将两者结合,你就拥有了一个可编程访问的浏览器会话,已经登录了你需要的一切。

对于 macOS 上的 Brave,启动命令如下:

/Applications/Brave\ Browser.app/Contents/MacOS/Brave\ Browser \
  --remote-debugging-port=9222 \
  --remote-allow-origins="*" \
  --profile-directory="Default"

--remote-allow-origins="*" 标志是新版 Chromium 允许 WebSocket 连接到调试端口所必需的。没有它,CDP 客户端会被静默拒绝,不会显示任何错误信息来帮助你诊断问题。

3、理解 CDP 及其重要性

在进一步之前,理解什么是 CDP 以及为什么它能解决夜间可靠性问题会有所帮助。

它做什么:CDP 是内置于每个基于 Chromium 的浏览器中的调试协议。它暴露了一个 WebSocket 接口,允许外部程序在底层观察和控制浏览器:导航页面、点击元素、读取 DOM、截取屏幕截图、拦截网络请求等等。

为什么这种方法更好:与浏览器扩展不同,CDP 连接存活在浏览器进程之外。它不依赖于扩展保持安装状态、保持连接,甚至根本不安装。浏览器监听一个端口,你的工具连接到该端口。如果连接断开,你就重新连接。没有扩展状态会损坏,也没有扩展会在夜间变陈旧。

何时使用:任何你需要能经受进程重启、空闲期或夜间运行的自动化时。当你需要针对特定已登录会话进行自动化而不需要重新认证时也很有用。

第一步:发现我的配置文件

Brave 将配置文件元数据存储在 Local State JSON 文件中。一个简短的 Python 脚本提取目录名和人类可读的配置文件名之间的映射:

import json

local_state_path = (
    "~/Library/Application Support/"
    "BraveSoftware/Brave-Browser/Local State"
)
with open(local_state_path) as f:
    profiles = json.load(f)['profile']['info_cache']
    for key, val in profiles.items():
        print(f"{key}: {val.get('name')}")

我的输出映射到端口分配(Brave 浏览器配置文件名):

  • Default → Rick → 端口 9222
  • Profile 1 → NovaTech → 端口 9223
  • Profile 3 → GreenField Labs → 端口 9224
  • Profile 7 → Coastline → 端口 9225

我选择从 9222 开始的顺序端口,因为它们高于特权范围,不太可能与其他本地服务冲突。只要它们保持一致,具体数字并不重要。聪明的 web 开发者们已经发现了一个主要问题。嘘..别破坏惊喜。

第二步:Shell 别名

我在 ~/.zshrc 中添加了别名,这样启动任何配置文件也会启用其调试端口:

alias brave-rick='/Applications/Brave\ Browser.app/Contents/MacOS/Brave\ Browser \
  --remote-debugging-port=9222 --remote-allow-origins="*" \
  --profile-directory="Default" &'

alias brave-novatech='/Applications/Brave\ Browser.app/Contents/MacOS/Brave\ Browser \
  --remote-debugging-port=9223 --remote-allow-origins="*" \
  --profile-directory="Profile 1" &'
alias brave-greenfield='/Applications/Brave\ Browser.app/Contents/MacOS/Brave\ Browser \
  --remote-debugging-port=9224 --remote-allow-origins="*" \
  --profile-directory="Profile 3" &'
alias brave-coastline='/Applications/Brave\ Browser.app/Contents/MacOS/Brave\ Browser \
  --remote-debugging-port=9225 --remote-allow-origins="*" \
  --profile-directory="Profile 7" &'

尾部的 & 将进程置于后台,这样终端不会挂起。来源 ~/.zshrc 后,输入 brave-novatech 会在端口 9223 上启用调试启动 NovaTech 配置文件。干净、可预测、易于记忆,或者我是这么认为的。我应该添加 nohup,但总有改进空间。

第三步:安装 CDP MCP 服务器

我使用的 CDP MCP 服务器是来自 Google Chrome DevTools 团队的 chrome-devtools-mcp。它通过调试端口连接到正在运行的浏览器,并通过 MCP 协议让 Claude Code 完全访问浏览会话。

一个前提条件:它需要 Node >= 22.12.0。我当时是通过 nvm 使用 v20.18.3,所以我升级了:

nvm install 22
nvm alias default 22

然后我将 MCP 服务器条目添加到 ~/.claude.json,使用完整的 npx 路径以避免 nvm 路径解析问题:

{
  "mcpServers": {
    "brave-rick": {
      "command": "/Users/rick/.nvm/versions/node/v22.22.1/bin/npx",
      "args": ["chrome-devtools-mcp@latest",
               "--browserUrl=http://127.0.0.1:9222"]
    },
    "brave-novatech": {
      "command": "/Users/rick/.nvm/versions/node/v22.22.1/bin/npx",
      "args": ["chrome-devtools-mcp@latest",
               "--browserUrl=http://127.0.0.1:9223"]
    },
    "brave-greenfield": {
      "command": "/Users/rick/.nvm/versions/node/v22.22.1/bin/npx",
      "args": ["chrome-devtools-mcp@latest",
               "--browserUrl=http://127.0.0.1:9224"]
    },
    "brave-coastline": {
      "command": "/Users/rick/.nvm/versions/node/v22.22.1/bin/npx",
      "args": ["chrome-devtools-mcp@latest",
               "--browserUrl=http://127.0.0.1:9225"]
    }
  }
}

4、为什么需要完整的 npx 路径?

使用 nvm 管理的 npx 二进制文件的完整绝对路径的重要性超出你的想象。当 Claude Code 生成子进程时,它不会继承你的 shell 的 PATH 或 nvm 环境。如果你写 "command": "npx",子进程会找到系统 PATH 中的任何 npx,这可能是一个不符合 >= 22.12.0 要求的旧 Node 版本,会静默失败。完整路径完全绕过了这个问题。

第一次测试:它工作了

在完全退出 Brave(Cmd+Q,而不仅仅是关闭窗口)并用 brave-rick 重新启动后,我验证了调试端口是活的:

curl -s http://127.0.0.1:9222/json/version
{
  "Browser": "Chrome/145.0.7632.109",
  "Protocol-Version": "1.3",
  "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/browser/..."
}

CDP 正在响应。Claude Code 现在可以通过 MCP 服务器连接到我的浏览器会话,完全访问我已登录的账户。不需要扩展。没有会话丢失。没有关于它在谈论哪个浏览器实例的歧义。

或者我是这么想的。细节是魔鬼。

剧情转折:Chromium 是单实例

这就是计划碰壁的地方。

我用各自的别名启动了所有四个配置文件。然后测试了每个端口:

端口 9222 -> 响应
端口 9223 -> 连接被拒绝
端口 9224 -> 连接被拒绝
端口 9225 -> 连接被拒绝

发生了什么?

基于 Chromium 的浏览器是单实例应用程序。你启动的第一个进程成为父进程。每个后续启动,即使带有不同的 --profile-directory--remote-debugging-port,也只是向已经运行的父进程发送一条 IPC 消息说"打开这个配置文件"。新进程立即退出。父进程打开配置文件窗口,但完全忽略新的端口标志

因此,当所有四个配置文件运行时,只有端口 9222 是活动的。所有四个配置文件的标签页都可以通过那个单一端口访问,但它们显示为一个平面列表,没有配置文件标识:

curl -s http://127.0.0.1:9222/json/list
# 返回所有配置文件的 100+ 个标签页混合在一起
# 没有 profileName,没有 profileId,无法区分它们

CDP 目标对象有一个 browserContextId 字段,每个配置文件都不同,但它是一个不透明十六进制字符串,没有到配置文件名的映射。CDP 规范中没有任何内容告诉你"这个标签页属于 NovaTech 配置文件"。至少我没有找到任何方法,但如果有这样的方法,请告诉我。

我的每个端口一个配置文件的干净架构出师未捷身先死。哇哇哇!

为什么单实例存在

这个行为不是 bug。Chromium 故意使用单实例模型来共享资源、确保一致的配置文件锁定,并防止两个进程同时写入相同的配置文件数据。这是正常浏览器使用的正确设计。对于多配置文件的自动化,这是一个重大障碍。

5、变通方案:AppleScript 窗口指纹识别

我需要一个旁道:一个可以识别哪个浏览器窗口属于哪个配置文件的东西,完全在 CDP 之外。

幸运的是,macOS 提供了一个:AppleScript。Brave 通过 AppleScript 脚本接口暴露其窗口。每个配置文件运行在自己的窗口中,AppleScript 可以枚举窗口并读取每个标签页的 URL。关键洞察是每个配置文件可靠地保持某些"签名"站点打开,这些 URL 可以作为指纹。

我的配置文件签名:

  • NovaTech: teams.microsoft.com, outlook.office
  • GreenField Labs: harvestapp.com
  • Rick: medium.com, linkedin
  • SpillWave: x.com

这是一个 hack 吗?是的。是的,它是。但目前它可以工作。所有的自动化都面向"Rick"配置文件,我甚至不想触碰其他配置文件。

选择你始终在每个配置文件中保持打开且不太可能出现在任何其他配置文件中的标记 URL。同样,如果你知道更好的方法,请分享。

两阶段模式

在多次尝试在错误的配置文件窗口中打开标签页后,我发现了一个关键规则:永远不要在同一个循环中识别和操作。打开一个新标签页会改变哪个窗口是最前端的,这会改变窗口索引。你必须先完成识别扫描,然后使用确认的索引进行操作。

第一阶段:识别

# AppleScript - 第一阶段:通过签名 URL 识别窗口
tell application "Brave Browser"
    repeat with i from 1 to (count of windows)
        set w to window i
        set tabURLs to URL of tabs of w
        set profileName to "Unknown"

        repeat with u in tabURLs
            set urlText to u as text
            if urlText contains "teams.microsoft.com" or
               urlText contains "outlook.office" then
                set profileName to "NovaTech"
                exit repeat
            end if
        end repeat
        if profileName is "Unknown" then
            repeat with u in tabURLs
                if (u as text) contains "harvestapp.com" then
                    set profileName to "GreenField"
                    exit repeat
                end if
            end repeat
        end if
        -- ... 对 Rick 和 Coastline 进行类似的检查
    end repeat
end tell

第二阶段:操作(使用确认的窗口索引)

# AppleScript - 第二阶段:对确认的窗口索引进行操作
tell application "Brave Browser"
    tell window 1 to make new tab with properties
        {URL:"https://gemini.google.com/"}
    tell window 2 to make new tab with properties
        {URL:"https://www.google.com/"}
    tell window 3 to make new tab with properties
        {URL:"https://claude.ai/"}
    tell window 4 to make new tab with properties
        {URL:"https://chatgpt.com/"}
end tell

艰难学到的教训

每个配置文件使用单独的扫描传递。 单一的 if/else 链按顺序检查每个 URL 与每个配置文件。如果一个窗口的标签页 URL 部分匹配错误配置文件的标记,它会错误地声明窗口。具有明确 contains 检查的单独传递消除了这种歧义。

按相反顺序关闭标签页。 关闭 10 个中的第 3 个会将 4 到 10 向下移动一个,打破你的索引引用。关闭多个标签页时始终从高到低迭代。这与按索引删除数组元素的原则相同:向后走。

窗口顺序是不稳定的。 当标签页被打开、窗口被聚焦或配置文件被切换时,它会改变。永远不要依赖窗口索引作为跨单独脚本调用的稳定标识符。在每个动作序列之前始终通过扫描标签页 URL 重新识别。

这个核心逻辑编码在我编写的 cron 任务基础设施调用的技能中。我使用 Claude Code 桌面(和 CoWork)的 /schedule 命令安排这个新的 cron 任务。

6、添加 agent-browser:词元高效层

在 CDP 和 AppleScript 基础到位后,我添加了另一个工具:Vercel 的 agent-browser。它是一个会说 CDP 的 Rust/Node CLI,带有一个 Claude Code 技能,教 Claude 交互工作流。(这将成为第二篇文章的开场。)

关键优势是词元效率。传统的浏览器自动化工具,包括 chrome-devtools-mcp,将完整 DOM 发送到 Claude 的上下文窗口。一个典型页面仅识别一个按钮就需要 3,000 到 5,000 个令牌。当你运行每五分钟与浏览器交互的 /loop 任务时,这些令牌快速累积。你会耗尽上下文预算,Claude 开始丢弃更早的对话历史。

vercel agent-browser 采用了根本不同的方法。它生成一个紧凑的无障碍快照,带有元素引用(@e1@e2@e3),你使用这些引用进行交互。同一页面花费 200 到 400 个令牌。这是每次交互令牌使用量减少 90%,这在你运行夜间循环触发数十次时非常重要。如果你想了解更多关于 Vercel 的 agent-browser 技术,请阅读这篇文章 Agent-Browser:节省 93% 上下文窗口的 AI 优先浏览器自动化(由我妈妈最喜欢的 AI 技术作者撰写)。

为什么是无障碍快照而不是完整 DOM?

现代网页的完整 DOM 包含大量噪音:不可见的 div、分析脚本、广告容器、CSS 类字符串、深度嵌套的布局元素。几乎所有这些都与"点击提交按钮"无关。无障碍快照只遍历无障碍树,这是现代框架专门为描述交互元素而维护的。它更紧凑、生成更快,而且发送给 LLM 更便宜。

安装

# 安装 CLI
npm install -g agent-browser

# 安装 Claude Code 技能
cd ~
npx skills add https://github.com/vercel-labs/agent-browser \
  --skill agent-browser -y

快照-引用工作流

# 连接到端口 9222 的 Brave
agent-browser connect 9222

# 获取快照以获取元素引用
agent-browser snapshot -i

# 输出:
# @e1 [input type="email"] placeholder="Email"
# @e2 [input type="password"] placeholder="Password"
# @e3 [button] "Sign In"
# 使用引用进行交互
agent-browser fill @e1 "user@example.com"
agent-browser fill @e2 "password123"
agent-browser click @e3

# 导航后,始终重新快照(引用失效)
agent-browser wait --load networkidle
agent-browser snapshot -i

导航后的重新快照步骤很重要。元素引用与当前页面状态绑定。导航或重大 DOM 更新后,旧引用失效,你必须在再次交互之前获取新快照。你不需要直接与 CLI 交互,vercel 有一个技能,允许你的 cron 代理处理 CLI,这样你就可以使用自然语言。

agent-browser 增加了什么

除了令牌效率,agent-browser 还提供了 Chrome 插件和 chrome-devtools-mcp 都不具备的功能:

  • 认证库。 静态加密保存凭证。LLM 永远看不到明文密码。
  • 命名会话。 同时运行隔离的浏览器会话(--session site1--session site2)。
  • 状态持久化。 跨重启保存和恢复 Cookie 和 localStorage。
  • 视频录制。 将会话录制为 WebM 文件以进行调试或审计。
  • 视觉对比。 比较操作前后的页面状态以检测意外变化。
  • 设备模拟。 在协议级别模拟移动设备。
  • 网络拦截。 模拟 API 响应或阻止特定请求。

对于自动化工作流,值得强调认证库。当 cron 任务在凌晨 3 点运行并遇到登录页面时,你希望凭证可用,而不必将它们以明文存储在脚本文件中。

7、完整技术栈

这是所有部分如何组合在一起的:

┌─────────────────────────────────────────────┐
│  Claude Code                                │
│  ┌──────────────────────────────────────┐   │
│  │  agent-browser skill                 │   │
│  │  (teaches Claude the workflow)       │   │
│  └──────────┬───────────────────────────┘   │
│             │ bash 命令                      │
│  ┌──────────▼───────────────────────────┐   │
│  │  agent-browser CLI                   │   │
│  │  snapshot → @refs → interact         │   │
│  └──────────┬───────────────────────────┘   │
│             │                               │
│  ┌──────────▼───────────────────────────┐   │
│  │  chrome-devtools-mcp                 │   │
│  │  (MCP 服务器,更深入的 DevTools)      │   │
│  └──────────┬───────────────────────────┘   │
│             │ CDP                            │
└─────────────┼───────────────────────────────┘
              │
   ┌──────────▼──────────────────────────┐
   │  Brave Browser (端口 9222)         │
   │  所有配置文件的标签页都可访问        │
   │                                     │
   │  AppleScript 识别哪个               │
   │  窗口 = 哪个配置文件                 │
   └─────────────────────────────────────┘

每一层都有 distinct 责任:

  • AppleScript 处理配置文件定位。它使用标签 URL 指纹识别回答"哪个窗口属于与哪个浏览器配置文件关联的哪个客户"。(在这种情况下,指纹识别是一个花哨的术语,用于临时修复和凑合。)
  • CDP 处理页面内的所有操作:点击、填写表单、读取内容、截取屏幕截图。
  • agent-browser 使用无障碍快照和元素引用提供令牌高效的交互层。
  • chrome-devtools-mcp 在需要时提供更深入的 DevTools 访问:性能跟踪、Lighthouse 审计和内存快照。
比较:每种工具给你什么

如你所见,Vercel 的 agent-browser 是相当高效的性能忍者,正如我之前所说,它成为我的 Claude Code 浏览器自动化之旅第二篇文章中的明星/超级英雄。

Chrome 插件在白天的快速交互会话中仍然是正确的选择。零设置、零摩擦。CDP 工具的复杂性只有在需要无人值守的自动化时才值得。但对于在深夜运行的 Cron 任务,agent-browser 掌控全局。

8、我现在实际使用什么

对于白天的交互式工作,Claude 浏览器插件仍然很好用。它是零摩擦的,不需要特殊设置。我不后悔学习 CDP 方法,但我也不会替换正在工作的工具。

对于需要浏览器访问的夜间 cron 任务和 /loop 任务,我使用通过 CDP 连接到端口 9222 的 Brave 的 agent-browser。值得一提的是:我的 cron 任务主要使用 /schedule 和桌面 Claude Code 环境,因为它们是持久的。/loop 在会话持续方面存在一些限制,而 /schedule 没有,但它是 CoWork/Claude Desktop 化身的 Claude Code 的一部分。关键是连接是稳定的,能在空闲期间存活,并且不需要扩展来保持活力,而扩展似乎在最不合时宜的时候注销自己。令牌效率意味着我的夜间监控脚本可以运行数小时而不会耗尽上下文窗口。

当我需要针对特定配置文件时,我使用 AppleScript 两阶段模式:通过签名标签页识别窗口,然后对确认的索引进行操作。它不优雅。它是基于 URL 指纹识别和不稳定窗口索引的变通方案。好吧。好吧。这是一个可怕的 hack。但只要遵循规则,它就能可靠地工作:先识别,后操作,按相反顺序关闭标签页。

梦想是让 Chromium 支持多个调试端口用于多个同时存在的配置文件。在那之前,AppleScript 桥接是一个功能性的变通方案,可以完成工作。或者能够通过 CDP 识别标签页来自哪个配置文件。我有一个梦想。这是一个简单的梦想。

9、快速参考

# 使用 CDP 启动 Brave
brave-rick    # 端口 9222,Default 配置文件

# 验证 CDP 是活的
curl -s http://127.0.0.1:9222/json/version

# 连接 agent-browser
agent-browser connect 9222

# 快照和交互
agent-browser snapshot -i
agent-browser click @e3
agent-browser fill @e1 "text"

# 保存/恢复浏览器状态
agent-browser state save session.json
agent-browser state load session.json

# 识别配置文件窗口(AppleScript)
osascript -e 'tell application "Brave Browser"
    repeat with i from 1 to (count of windows)
        set urls to URL of tabs of window i
        log "Window " & i & ": " & (urls as text)
    end repeat
end tell'

AI 编程工具的浏览器自动化领域发展迅速。当你读到本文时,其中一些变通方案可能有更干净的解决方案。但 CDP、配置文件隔离和 Chromium 浏览器单实例限制的底层架构,对于需要与已认证浏览器会话交互的自动化工作流的任何开发者来说,仍然是相关的。

这就是我让浏览器自动化 cron 任务在凌晨 4 点、5 点等时间运行的旅程。我不太确定这个世界在这些时间是否存在。我听说它存在,但我不能确定,因为我从未见过。但说笑归说笑,为浏览器自动化选择正确工具只是战斗的一半。另一半是克服权限疲劳。在交互式 Claude Code 终端中的权限疲劳很烦人,但在应该无人值守运行的 cron 任务中的权限疲劳则是致命的。我可以为此单独写一篇文章,如果你们感兴趣的话请告诉我。剧透提示:我经常使用 /debug,经常编辑 settings local json,并经常创建具有正确权限的子代理。还有很多轻微咒骂的迭代。第二篇文章已经完成了,所以如果你想读它,请点赞、评论和订阅。


原文链接: When Claude Code's Browser Plugin Wasn't Enough: A Journey to Reliable Browser Automation

汇智网翻译整理,转载请注明出处