开源项目 Webhands 实现浏览器 Agent 读取 UI 数据的安全模型:默认安全读取,Mutation 操作需用户显式确认,防止爬虫/自动化框架意外修改生产状态。
每隔几周我就会撞上同一堵墙。我需要从中拉取数据的工具没有可用的 API。卖家中心、供应商门户、3PL 仪表盘。数据就在屏幕上,但获取它的唯一方式是像人类一样登录并点击 UI。于是我构建了 Webhands:一个计算机用途 Agent,在真实的无头浏览器中操作这些仪表盘,返回干净的結構化数据,并拒绝任何写入操作——除非我明确确认。
一旦你把真实的浏览器会话交给 Agent,你就赋予了它登录用户可以做的一切能力。这包括危险操作。发出退款。确认发货。取消订单。读取页面是安全的。点击"发出退款"则不安全,二者的区别只在于一个按钮。
大多数自动化框架对所有操作一视同仁。点击就是点击。这恰恰是我不想要的设计选择,因为一次悄悄改变生产状态的爬取运行不是便利,而是一起等待发生的事故。我希望读取默认安全、写入刻意为之,没有任何方式可以意外触发变更。
你通过 POST 一个配方来驱动 Webhands。配方包含一个入口 URL、一个可选的登录和导航步骤列表,以及一个提取规格。以下是源码中一个步骤的形状:
export type Step =
| { action: "goto"; url: string }
| { action: "type"; selector: string; text: string; secret?: boolean }
| { action: "click"; selector: string; write?: boolean }
| { action: "waitFor"; selector: string; timeoutMs?: number };
注意 click 上的 write?: boolean。这个标志位就是整个安全模型的全部。输入、等待、导航和读取本质都是安全的。唯一能改变状态的操作是点击,所以唯一能携带 write: true 的操作也是点击。如果一个步骤被标记为写入,除非该请求同时携带 confirm: true,否则会被拒绝。
这道门本身很小,且在任何浏览器启动之前就已完成检查:
if (hasWriteStep(recipe) && !confirm) {
return {
ok: false,
mode: env.BROWSER ? "live" : "dry",
steps: [],
error: "recipe contains a write step; resend with confirm:true to execute",
};
}
hasWriteStep 在任何步骤是标记了 write: true 的点击时返回 true。当这种情况发生且缺少 confirm 时,函数返回一个错误且从不打开浏览器。要实际执行写入,你需要用 "confirm": true 重新发送完全相同的配方。读取永远不需要确认,因为它们永远不会改变任何东西。
我很喜欢这个检查在浏览器配置之前就运行了。不存在写入步骤已部分执行然后才被捕获的窗口。拒绝是提前的、确定性的,仅基于配方的形状。
一次成功的运行会返回三样东西。首先是 data:结构化的 JSON。你有两种提取方式。给配方一个包含 CSS 选择器的字段列表,它会直接爬取,不涉及任何模型。或者给它一个自然语言的 extract.prompt,它会把页面文本交给 Claude 并请求匹配你请求的 JSON。其次是 screenshotBase64:浏览器所见的 base64 PNG,这样你就有了证据。第三是 steps:实际执行操作的日志,比如 goto ...、type into #email (secret)、click #signin。
提取路径对降级是坦诚的。如果没有设置 Anthropic key,基于 prompt 的提取器会返回原始文本片段而不是失败,这样开发环境中流水线仍能运行。当 key 存在时,它用 Claude Haiku 模型调用 Messages API,只请求 JSON,剥除任何代码围栏,然后解析。如果解析失败,它返回未解析的文本而不是丢弃结果。
Webhands 运行在 Cloudflare Workers 上,使用 Browser Rendering。有两种模式。在 live 模式下,BROWSER binding 存在,它通过 Cloudflare 的 Puppeteer 驱动一个真实的无头浏览器。在 dry 模式下 binding 不存在,它返回它本应执行的计划,标记为 dry 结果,而不是执行任何操作。这意味着你可以在没有付费 binding 的情况下编写和测试配方,然后在准备好时切换到 live。即使 binding 存在但 Browser Rendering 超出配额或未配置,启动也会被包装起来降级为干净的错误而不是崩溃。
写门控保护你免于执行未确认的变更。它不理解某个点击意味着什么。write: true 标志由配方的编写者设置。如果你把一个真正有破坏性的按钮标记为读取,或者完全忘记标记它,Webhands 会乐意点击它而不询问,因为在它看来一个没有 write: true 的点击只是导航。安全模型的好坏取决于标签的质量。它是刻意写入的强制函数,而不是能自行检测危险的分类器。我特意选择了这个折衷方案,因为从按钮标签猜测意图远不如显式标志位可靠,但它确实意味着编写配方的人仍然需要对判断负责。
另一个坦诚的注意事项是:它在真实的 UI 上操作。选择器会在仪表盘变化时失效,登录会受到挑战,渲染缓慢的页面可能会超时。截图证据和步骤日志的存在正是因为针对真实门户的运行比针对 API 契约的运行更混乱。
「没有 API」变成了「有一个 Agent」。Webhands 是我让这种切换足够安全以实际用于生产仪表盘的尝试,方式是让读取免费、写入需要你请求两次。配方格式有意做得很小,门控只有几行代码,两种模式都让你在任何东西接触真实账户之前能低成本地迭代。