让 Agent 操作网页,第一反应通常是”让它像人一样点”。截图、识别按钮、移动鼠标、输入文字,看起来很自然,也很符合我们对”电脑自动化”的直觉。

但实际做过几次就会发现:能点,不等于好用。一次临时演示可能没问题,真正要把它变成稳定、可重复、以后一句话就能执行的能力,难点完全不同。

这篇文章记录我目前认为更适合个人 Agent 的浏览器自动化方案:复用用户已经登录的 Chrome,通过 Apple Events 向当前页面注入 JavaScript,直接读取和操作 DOM,并把最终流程沉淀成 Skill。

文中不会讨论某个网站的具体按钮、接口或业务细节,只讲通用方法。


候选方案

选择浏览器自动化方案时,先看 Agent 要控制的是哪一层:浏览器页面、整个桌面、独立自动化浏览器、现有 Chrome、还是网站接口。下面六种方案各自控制不同的对象,不应混为一谈。

方案一:使用 Hermes Agent 内置的浏览器工具

Hermes Agent 内置了 browser_navigatebrowser_snapshotbrowser_clickbrowser_typebrowser_pressbrowser_scrollbrowser_backbrowser_get_imagesbrowser_visionbrowser_console 等工具。它们通常通过 CDP 控制一个由 Agent 管理的 Chromium 浏览器。

CDP 是 Chrome DevTools Protocol(Chrome 开发者工具协议)的缩写。它允许程序直接与 Chrome 或 Chromium 通信,可以理解为”代码版的 Chrome 开发者工具”。其中,browser_navigate 用来打开网页,browser_snapshot 读取页面文本、控件和元素编号,browser_clickbrowser_typebrowser_pressbrowser_scroll 负责交互,browser_vision 用于截图识别,browser_console 则可以查看控制台错误或执行页面 JavaScript。

这套工具可以完成登录后的网页操作、填写表单、下载文件、测试 Web 应用和提取动态页面内容。它控制的是 Agent 自己的浏览器会话,不是用户桌面上正在使用的 Chrome。如果任务依赖用户日常 Chrome 中已有的登录状态,通常仍要在这个独立会话中重新登录。

方案二:使用 Hermes Agent 的 Computer Use Skill

Hermes Agent 还提供了基于 cua-driver 的 Computer Use Skill。CUA 通常指 Computer Use Agent,cua-driver 是 Hermes 用来驱动整个电脑桌面的底层程序。它通过 macOS Accessibility、屏幕捕获和输入事件等系统能力,控制 Chrome、Finder、系统设置及其他原生应用。

这套方案支持截图、点击、输入、拖动、快捷键和切换应用,也可以读取辅助功能树后按元素操作。设计上会尽量在后台发送输入,不抢用户真实的鼠标指针和键盘焦点。它的控制对象是整个桌面,而不是网页 DOM。

它适合跨应用流程,以及 Finder、系统设置等无法使用浏览器工具的场景。用于复杂网页时,辅助功能信息可能不完整,坐标也会受窗口位置和页面布局影响。因此它是通用的桌面方案,也是 DOM 自动化失效后的重要兜底,但不是批量网页操作的首选。实际使用前还要确认当前 Hermes 会话是否注册了 computer_use 工具;只有 Skill 文档而没有对应工具时,Agent 无法真正执行桌面控制。

方案三:编写 Playwright 或 Puppeteer 脚本并启动独立浏览器

这一方案由开发者自行编写自动化程序,并启动独立的 Chromium 或专用 Chrome Profile。脚本可以精确读取 DOM、等待元素、监听页面请求、保存截图,还能实现重试、日志和测试。

它适合 CI、自动化测试、专用账号或长期运行的无人值守任务。边界也很清楚:脚本管理自己的浏览器进程和会话,不依赖用户当前打开的 Chrome。

个人 Agent 场景里的主要问题是登录态。新浏览器通常没有用户日常 Chrome 的 Cookie,也没有已经完成的企业认证。复制 Profile 还可能遇到 macOS 钥匙串加密、浏览器锁文件和安全策略。自动化能力很完整,但需要额外维护一套浏览器身份。

方案四:通过 CDP 接管开启了远程调试的现有 Chrome

这一方案不再启动独立浏览器,而是让 Playwright、Puppeteer 或其他程序通过 CDP 连接现有 Chrome。这样既能读取 DOM,也能复用浏览器中的登录态。

它与方案一的区别是:方案一使用 Agent 管理的浏览器工具和会话;方案四连接用户指定的 Chrome 实例。与方案三的区别是:方案三由脚本创建浏览器,方案四只接管已经启动的浏览器。

Chrome 通常需要带远程调试参数启动,并开放本地调试端口。已经运行的日常 Chrome 往往需要重启才能启用,长期开放调试端口也需要注意本机安全。它适合愿意维护专用 Chrome 启动方式的用户。

方案五:绕过页面,直接调用网站 API

如果网站提供公开 API,Agent 可以直接发送 HTTP 请求,不必操作浏览器。这是最干净、最快也最容易验证的方案,应该优先考虑。

有些网站没有公开接口,也可以从开发者工具里观察前端请求,再尝试复用内部 API。但这已经是另一种风险更高的做法:接口路径和请求体可能随前端版本改变,调用还可能依赖 Cookie、CSRF Token、签名或设备信息。它也绕过了页面原本的交互和状态检查。

所以这一方案的控制对象是服务端接口,不是浏览器。公开 API 值得优先使用;未公开接口则要评估维护成本和误操作风险。

最终采用的方案六:通过 Apple Events 操作日常 Chrome 的 DOM

这里先解释一下 DOM。DOM 是 Document Object Model(文档对象模型)的缩写。浏览器加载网页后,会把 HTML 解析成一棵由标题、按钮、输入框、表格等节点组成的树。JavaScript 可以沿着这棵树读取页面内容、定位元素、触发点击,也能检查操作后的状态。对 Agent 来说,读取 DOM 相当于直接理解网页结构,而不是只看一张截图猜测哪里能点。

在 macOS 上,Chrome 支持通过 Apple Events 在当前标签页执行 JavaScript。它不需要启动第二个浏览器,也不要求 Chrome 预先开启远程调试端口。Agent 通过 AppleScript 或 osascript 找到用户当前的 Chrome 标签页,再把 JavaScript 送进页面执行。

1
2
3
4
5
6
7
8
9
Agent

AppleScript / osascript

用户当前打开且已登录的 Chrome

JavaScript

页面 DOM、按钮、表格、分页和弹窗

AppleScript 只负责导航标签页和传递 JavaScript。页面识别、元素定位、点击以及结果验证都由 JavaScript 完成。

例如,通用执行器可以很短:

1
2
3
4
5
6
7
8
9
on run argv
set jsText to read POSIX file (item 1 of argv) as «class utf8»
tell application "Google Chrome"
if (count of windows) is 0 then error "Chrome 没有窗口"
tell active tab of front window
return execute javascript jsText
end tell
end tell
end run

这套方案的控制对象是用户日常 Chrome 中的真实页面。它既复用了已有的登录、企业认证、Cookie 和权限,又保留了 DOM 自动化的精确性。对于 macOS 上需要用户身份、没有公开 API、而且会重复执行的网页任务,这是我最终采用的方案。


为什么这个方案更适合个人 Agent

直接复用真实登录态

不复制 Cookie,不启动第二个 Profile,也不要求用户重新登录。Agent 操作的就是用户正在使用的 Chrome。

这对企业后台尤其重要。很多系统除了账号密码,还包含扫码、设备验证、企业切换或单点登录。复用现有会话省掉了最麻烦的一段。

定位依据是 DOM,不是像素

脚本可以按应用名、按钮文字、ARIA role、稳定 class 和元素关系定位目标。例如:

1
2
const row = [...document.querySelectorAll("tr")]
.find(element => element.innerText.includes(targetName));

如果一行里有多个按钮,还可以限定在该行内部继续查找。页面滚动、窗口尺寸和屏幕分辨率不再影响定位。

能拿到业务标识

批量任务不能只看显示名称。DOM 中常常包含列表行的 data-row-key、详情链接或资源 ID。脚本可以在点击前记录这些标识,并在操作后进行核对。

这比”点击第六行的配置按钮”可靠得多。

可以验证结果,而不是只验证点击

成熟的自动化流程不应该把 button.click() 当成成功。正确的完成条件应该来自页面最终状态:

  • 开关文字是否发生变化
  • 弹窗是否消失
  • URL 是否返回列表页
  • 目标行是否从列表中消失
  • 再次遍历分页时是否仍能找到目标

点击只是动作,状态变化才是结果。

开发速度快

第一次探索页面时,可以先运行只读 JavaScript,输出按钮、表格行和局部 outerHTML。确认结构后,再写操作逻辑。

调试过程不需要搭建完整的浏览器工程,也不需要处理新的登录会话。一个 Python 脚本、一个 AppleScript 执行器,加上若干 DOM 查询就能工作。

容易沉淀为 Skill

页面选择器、状态判断、风险边界和验证方式都可以写进 Skill。以后 Agent 再遇到同类任务,不需要重新从截图和坐标开始摸索。


实现步骤

第零步:先由人完整演示一遍

在写自动化脚本之前,最好先让熟悉业务的人从头到尾实际操作一次,并同步讲清楚自己正在做什么。描述越具体越好:从哪个入口进入、使用了哪些筛选条件、为什么选择这个对象、点击后出现了什么提示、如何判断当前步骤成功,以及完成后怎样回到列表继续处理。容易被忽略的翻页、等待、二次确认和异常分支也应记录下来。

这次演示的重点不是教 Agent 模仿鼠标轨迹,而是把人脑中的隐性流程变成可分析的操作说明。Agent 可以据此梳理页面入口、步骤依赖、目标匹配规则和完成条件,再去寻找对应的 DOM 元素。

如果边操作边打字太麻烦,可以直接录音讲解。飞书妙记很适合这个场景:人一边操作一边口述,结束后把妙记生成的完整文字稿交给 Agent。尽量提供逐字稿,而不是只给 AI 总结,因为确认弹窗、返回路径和状态变化等细节很容易在摘要中被省略。Agent 拿到文字稿后,应先整理成结构化步骤,并对照实际页面补齐可验证的状态,再开始设计自动化。

第一步:确认任务边界

自动化之前先明确四件事:

  • 目标资源如何精确匹配
  • 哪些页面是权威入口
  • 操作顺序是否有前置依赖
  • 什么状态才算真正完成

批量删除尤其不能用模糊包含匹配。显示名称相同的对象,还要记录各自的唯一 ID。

第二步:验证 JavaScript 注入

先执行只读探针:

1
2
3
4
5
6
JSON.stringify({
title: document.title,
url: location.href,
ready: document.readyState,
body: document.body?.innerText.slice(0, 1000)
})

这一步用于确认:

  • 当前标签页确实是目标页面
  • 登录态有效
  • DOM 已经可读
  • Chrome 允许 Apple Events 执行 JavaScript

Chrome 需要开启”视图 → 开发者 → 允许 Apple 事件中的 JavaScript”。这是一次性设置。

第三步:只读分析 DOM

不要一上来就点击。先找出页面中稳定的语义锚点:

  • 表格行
  • 名称字段
  • 状态字段
  • 操作按钮
  • 分页控件
  • 确认弹窗
  • 唯一资源 ID

可以输出局部结构:

1
2
3
4
5
6
const rows = [...document.querySelectorAll("table tbody tr")];
JSON.stringify(rows.map(row => ({
text: row.innerText,
id: row.getAttribute("data-row-key"),
html: row.outerHTML.slice(0, 1500)
})));

稳定 class 可以使用,但不要只依赖一串自动生成的 class。更稳妥的做法是将 class、文字、父子关系和业务 ID 组合起来。

第四步:把流程写成状态机

批量网页操作最好按状态推进,而不是写成长串固定点击:

1
2
3
4
5
6
7
8
9
10
11
扫描列表

找到一个符合条件的目标

进入详情并再次核对

执行操作

等待最终状态

返回列表重新扫描

每处理一个对象就重新扫描,虽然比一次性缓存所有行稍慢,却能避免分页、排序和列表刷新导致的旧元素引用失效。

第五步:处理异步加载

网页组件经常先渲染外壳,再加载业务数据。固定 sleep(3) 可以用于实验,但最终脚本应尽量等待条件:

1
2
3
4
5
6
7
def wait_for(expression, timeout=15):
deadline = time.time() + timeout
while time.time() < deadline:
if execute_js(f"String(Boolean({expression}))") == "true":
return
time.sleep(0.4)
raise RuntimeError("页面等待超时")

等待条件可以是:

  • 某个元素出现
  • 某段状态文字变化
  • URL 进入详情页
  • 弹窗出现或消失
  • 列表重新加载完成

第六步:写入操作要有双重验证

删除类操作至少做两次核对:

  1. 列表页通过名称和唯一 ID 选中目标
  2. 详情页再次确认名称、状态和 ID

操作后也要验证两次:

  1. 当前页面显示成功后的状态
  2. 重新进入列表,完整遍历分页,确认目标不存在

这比增加更多”你确定吗”的人工弹窗更适合无人值守任务。安全性来自程序的精确边界和验证,不是让用户重复确认同一件事。

第七步:保留降级路径

DOM 自动化并不能覆盖所有情况。合理的降级顺序是:

  1. DOM JavaScript
  2. 浏览器可访问性元素
  3. 像素点击
  4. 用户处理登录、验证码或权限弹窗

如果页面变成 Canvas、跨域 iframe 或封闭的 Shadow DOM,DOM 路径可能受限。此时再使用 Computer Use,而不是从一开始就依赖坐标。


如何沉淀为 Skill

一次脚本能跑通还不够。Skill 要记录的不是某次成功,而是下次如何稳定地再次成功。

一个实用的目录可以这样组织:

1
2
3
4
5
6
browser-task-skill/
├── SKILL.md
├── scripts/
│ └── automation.py
└── references/
└── dom-and-troubleshooting.md

SKILL.md 写什么

主文件只保留每次都需要的内容:

  • 触发条件
  • 前置环境
  • 权威页面入口
  • 命令参数
  • 操作顺序
  • 安全边界
  • 完成标准
  • 常见失败分支

不要把所有 DOM 片段和调试记录都塞进主文件。页面结构细节更适合放到 references。

脚本如何设计

脚本最好把”只读观察”和”实际执行”分开。首次运行或页面改版后,先做只读扫描,确认目标数量、页面结构和权限状态;正式执行时再进入完整流程,并在结束后重新扫描。

目标名称、租户 URL 等会变化的信息应该作为参数传入,避免把个人环境硬编码进逻辑。具体采用命令行参数、配置文件还是环境变量,可以根据任务复杂度决定。博客只需要说明这个设计原则,不必把脚本接口写成使用手册。

references 写什么

参考文件适合保存:

  • 当前验证过的选择器
  • 页面改版后的排查方法
  • 常见错误和修复方式
  • 哪些元素容易异步加载
  • 降级到 Computer Use 的条件

Skill 的完成标准

Skill 创建后至少做三类验证:

  1. 静态检查:脚本能编译,Front Matter 合法
  2. 只读测试:scan 能在真实页面返回结果
  3. 实战验证:写操作执行后,最终状态满足验收条件

实战验证很重要。没有真实运行过的自动化说明,只是一份猜测。


这个方案的限制

它依赖 macOS 和 Google Chrome,也依赖用户允许 Apple Events 执行 JavaScript。脚本默认操作当前活动标签页,因此运行期间不适合让用户同时在同一个 Chrome 窗口里频繁切换标签。

页面改版后,DOM 选择器仍可能需要更新。好在更新过程是可诊断的:先运行只读扫描,查看新的结构,再修正选择器。相比坐标漂移,这种故障更容易定位,也更容易写进 Skill。

如果网站提供稳定、权限清晰的公开 API,应该优先使用 API。DOM 自动化适合填补”没有公开接口,但用户在网页里可以完成操作”的空白。


结语

个人 Agent 的浏览器自动化,重点不是模拟人类鼠标,而是复用人类已经建立好的浏览器会话,然后以机器更擅长的方式理解页面。

在 macOS 上,现有 Chrome、Apple Events 和 DOM JavaScript 的组合很好地平衡了登录态、开发成本、执行速度与可验证性。视觉点击仍然有价值,但更适合作为最后的兜底。

真正值得沉淀的也不是某段选择器,而是一套纪律:先只读观察,精确匹配目标,按状态推进,每一步都验证,最后重新扫描,并把这些经验写进 Skill。下一次再遇到类似网页,Agent 就不必重新学着”点鼠标”了。