ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OpenClaw浏览器自动化:感知决策执行链路与密钥管理实战

OpenClaw浏览器自动化:感知决策执行链路与密钥管理实战

1. 从"能点按钮"到"知道为什么能点":浏览器控制这条链路到底怎么走通的

很多人第一次接触 OpenClaw 的浏览器操作能力时,注意力都放在"它居然能自己点按钮、填表单"这件事上,觉得神奇,然后就去研究怎么写更复杂的指令。但真正把项目跑进生产环境之后你会发现,卡住你的从来不是"能不能点",而是"为什么这次点得中、下次点不中",以及"它到底是怎么看见这个页面的"。这一篇我想把这条链路从头到尾拆开讲清楚,顺带把明文密钥管理这个几乎所有人都会踩的坑一起说了。

先把结论摆在前面:Agent 操作浏览器,本质上不是"AI 直接操控鼠标键盘",而是感知—决策—执行三段式循环。感知层负责把网页变成模型能理解的结构化信息,决策层负责在候选动作里挑一个,执行层负责把这个动作落到真实的浏览器实例上。三段里任何一段出问题,表现出来都是"它不动了"或者"它点错了",但根因完全不同。你要会区分,否则排查就是瞎猜。

这篇适合两类人看:一类是已经把 OpenClaw 跑起来、但一遇到动态页面就翻车的开发者;另一类是正准备把浏览器自动化接进自己系统、还没想清楚密钥该怎么放的人。前者我会重点讲感知层和执行层的细节,后者我会用一整节讲明文密钥的风险和几种可行的收敛方案。中间穿插的都是我在实际项目里踩出来的经验,不是文档里抄的。

需要提前说明的是,下面涉及的具体实现细节,有一部分是基于这类浏览器自动化框架的通用实践做的合理补充,因为原始资料里没有展开到代码级别。我会明确标注哪些是通用原理、哪些是我个人的做法,你按自己项目的实际情况取舍。

2. 感知层:Agent 眼里的网页和你眼里的网页根本不是一回事

2.1 为什么截图方案和 DOM 方案会得出完全不同的结果

先问一个问题:你觉得 Agent 是怎么"看见"网页的?大部分人的第一反应是截图,然后让多模态模型看图。这个方案直观,但实际用下来问题很大。截图丢失了语义信息——一个按钮在图上就是一个色块,模型知道那里有个东西,但不知道它叫什么、能不能点、点了会跳去哪。而且截图对分辨率、缩放、滚动位置极其敏感,页面稍微变一下布局,模型就懵了。

所以主流做法是走 DOM 路线:把页面的 DOM 树抽取出来,做一次"可交互元素筛选",只保留真正能操作的东西——按钮、链接、输入框、下拉框、单选框这些,然后把每个元素的标签名、文本内容、可访问性属性、位置信息打包成结构化数据喂给模型。这样模型看到的不是像素,而是一份"这个页面上有哪些可操作项、它们分别是什么"的清单。

但这里有个关键细节:不是所有 DOM 元素都值得喂给模型。一个复杂页面动辄几千个节点,全塞进去既超上下文又干扰判断。所以筛选逻辑很重要,通常要过滤掉不可见的(display:none、visibility:hidden、尺寸为 0)、被遮挡的、纯装饰性的元素。我自己的经验是,筛选后保留的元素数量控制在几十到一两百个之间比较合适,太多模型会挑花眼,太少又会漏掉关键操作项。

2.2 可访问性树:被大多数人忽略的那把钥匙

如果你只抽 DOM,会遇到一个很烦的问题:很多现代前端框架(React、Vue 那一类)生成的 DOM 结构又深又乱,一层套一层 div,真正的语义信息藏在 aria 属性或者 role 里。这时候更靠谱的做法是走可访问性树(Accessibility Tree)。

可访问性树是浏览器专门为辅助技术(比如屏幕阅读器)维护的一棵树,它把 DOM 里那些乱七八糟的容器节点过滤掉,只保留有语义的节点,并且把 role、name、state 这些信息规整好。对 Agent 来说,这简直是量身定做的输入——每个节点天然就是"一个可操作项",name 就是它的可读名称,role 就是它的类型。你不需要自己去猜哪个 div 是按钮,可访问性树已经告诉你了。

我实测下来,基于可访问性树做感知,元素定位的准确率比纯 DOM 方案高出一大截,尤其是在那些用了组件库的页面上。代价是可访问性树对某些自定义组件支持不好——如果开发没写 aria 属性,节点可能就是空的。所以稳妥的做法是可访问性树为主、DOM 属性为辅,两者结合着用。

2.3 动态渲染页面:等还是不等,这是个问题

现代网页大量使用异步渲染,你拿到 DOM 的那一刻,页面可能还没渲染完。这时候如果直接抽取元素,抽到的是一份残缺的清单,Agent 基于残缺清单做决策,自然就点空了。

常见的处理方式是等待,但"等"这件事很有讲究。无脑 sleep 三秒是最蠢的做法——快的时候浪费三秒,慢的时候三秒还不够。更好的方式是等待特定条件:等某个关键元素出现、等网络请求静默、等 DOM 不再变化。这类框架一般会提供 waitForSelector、waitForLoadState 之类的原语,你要做的是根据页面特点选对等待条件。

我踩过的一个坑是:页面主体渲染完了,但某个下拉框的选项是点击后才异步加载的。Agent 看到下拉框就点了,点完立刻去选项列表里找目标项,结果列表还没加载出来,找不到,就报错了。后来改成"点击后等待选项容器出现再继续",问题解决。这个经验告诉我,等待点要设在真正需要数据的地方,而不是笼统地等页面加载。

3. 决策层:模型是怎么从一堆元素里挑出那个"对的"的

3.1 动作空间的设计决定了 Agent 的能力上限

感知层给出一份元素清单后,决策层要做的就是"选一个动作执行"。这里有个容易被忽视的设计问题:动作空间怎么定义。最朴素的做法是让模型输出"点击第 N 个元素",但这样模型很容易数错。更稳的做法是给每个元素分配一个稳定的标识(比如基于可访问性树的路径或者一个临时 ID),让模型输出标识而不是序号。

动作类型也要设计好。基础的有点击、输入、选择、滚动、等待,复杂一点的有拖拽、悬停、键盘快捷键。动作空间越大,Agent 能力越强,但模型选错的概率也越高。我的建议是按需扩展:先把点击和输入做扎实,遇到确实需要的能力再加,不要一上来就堆一大堆动作类型。

还有一个细节是动作的参数。比如输入动作,除了"往哪个元素输入",还要有"输入什么内容"。这个内容从哪来?通常是从任务描述里提取,或者从上一步的结果里推导。这里就涉及到上下文管理——你得把任务目标、历史动作、当前页面状态都组织好喂给模型,它才能做出合理决策。

3.2 单步决策 vs 多步规划:什么时候该让模型想远一点

最简单的决策模式是单步:看当前页面,选一个动作,执行,再看新页面,再选。这种模式实现简单,但有个致命问题——短视。有些操作单独看没意义,组合起来才能达成目标。比如"先点筛选、再选条件、再点确定",如果模型只看当前一步,可能点完筛选就不知道下一步干嘛了。

所以更成熟的做法是引入多步规划:让模型先根据任务目标生成一个粗略的行动计划,然后逐步执行,每执行一步根据实际页面状态调整计划。这有点像人做事——先想个大概,边做边修正。规划的好处是 Agent 有全局观,不会做着做着跑偏;代价是规划本身可能出错,而且增加了 token 消耗。

我的经验是,任务步骤少于五步时用单步决策就够了,超过五步或者有明显阶段划分的任务再上规划。别为了炫技硬上规划,很多时候单步反而更稳,因为每一步都基于真实页面状态,不会因为计划本身错了而一路错下去。

3.3 失败重试:不是所有错误都值得重试

Agent 执行动作会失败,这太正常了。元素没找到、点击被拦截、输入框只读,各种情况都有。关键问题是:失败了要不要重试,怎么重试。

无脑重试是最糟的——同一个动作重试三次,三次都失败,白白浪费时间。正确的做法是先判断失败类型:如果是"元素暂时没出现",那等一下重试有意义;如果是"元素根本不存在",重试一百次也没用,应该重新感知页面、重新决策。如果是"点击被遮挡",那可能需要先滚动或者关闭弹窗,这是要插入新动作,不是简单重试。

我一般会给每个动作设一个重试上限(比如两次),超过就触发重新感知。同时记录失败原因,如果同一个动作连续失败,说明决策本身有问题,要回退到上一步重新规划。这套机制听起来复杂,但实现起来就是几个 if-else,收益却很大——它能把很多"卡死"的情况自动化解掉。

4. 执行层:动作是怎么真正落到浏览器上的

4.1 浏览器实例的生命周期管理

执行层的第一件事是管好浏览器实例。这里有个常见误区:很多人以为开一个浏览器就一直用,用完关掉。实际项目里,浏览器实例的管理要精细得多。

首先是隔离。不同任务之间最好用独立的上下文(context),这样 cookie、localStorage 互不干扰。如果多个任务共用一个上下文,A 任务登录了某个站点,B 任务访问同一站点时可能带着 A 的登录态,行为就乱了。其次是复用。频繁开关浏览器开销很大,启动一次 Chromium 动辄一两秒,所以通常会维护一个实例池,任务来了从池里取,用完还回去。

还有一个容易被忽略的点是清理。任务跑完不清理上下文,时间长了内存会涨,而且残留的 cookie 可能影响后续任务。我的做法是每个任务结束后显式关闭它的上下文,定期重启整个浏览器实例,避免长时间运行积累的隐性状态。

4.2 点击为什么点不中:坐标、遮挡与事件冒泡

"元素明明在那里,就是点不中"——这是浏览器自动化里最经典的疑难杂症。原因通常有三类。

第一类是坐标问题。有些框架用元素中心点坐标去点击,但如果元素被滚动到视口外,或者页面有缩放,坐标就偏了。解决办法是点击前先把元素滚动到可视区域,并且用元素自身的 bounding box 计算坐标,而不是用页面绝对坐标。

第二类是遮挡问题。元素在 DOM 里存在,但被一个浮层、弹窗、或者透明的 loading 遮罩盖住了。这时候点击事件会被上层元素截获。解决办法是点击前检查元素是否真的可点击(很多框架提供 isVisible、isEnabled 之类的判断),如果被遮挡,先处理遮挡物。

第三类是事件冒泡问题。有些前端组件监听的是 mousedown 而不是 click,或者需要完整的 mousedown-mouseup-click 序列。这时候用简单的 click 方法可能不触发。解决办法是模拟完整的鼠标事件序列,或者直接用框架提供的更底层的点击原语。

我遇到过一个特别刁钻的案例:一个按钮的点击处理逻辑绑在父元素上,通过事件委托实现。直接点按钮本身没反应,因为事件冒泡被某个中间层 stopPropagation 了。最后是改成点击父元素才解决。这个案例说明,点击不中不一定是坐标问题,也可能是事件机制问题,排查时要往深了看。

4.3 输入框的那些坑:清空、粘贴与输入法

输入看起来简单,其实坑不少。第一个坑是清空。很多输入框有默认值,你直接输入会追加而不是替换。正确做法是先清空再输入,但清空也有讲究——有些框架的输入框是受控组件,直接设 value 不触发 React 的 onChange,得模拟键盘操作。

第二个坑是粘贴。有些场景下逐字符输入太慢,或者会触发输入框的实时校验导致中间状态报错。这时候用剪贴板粘贴更快更稳。但剪贴板操作涉及权限,而且不同平台行为不一致,要谨慎使用。

第三个坑是输入法。如果你的输入内容包含中文,而浏览器实例的输入法状态不对,可能输入进去的是乱码或者拼音。稳妥的做法是确保输入内容通过剪贴板或者直接设值的方式注入,避免依赖输入法。

5. 明文密钥管理:那个所有人都知道有问题但都在凑合的事

5.1 明文密钥到底危险在哪

聊完技术链路,必须说说密钥管理。我见过太多项目,配置文件里明晃晃写着各种密钥——API key、数据库密码、第三方服务的 token,全是明文。问起来就是"先跑通再说",然后这个"先"就变成了永远。

明文密钥的危险不是理论上的,是实打实的。第一,版本控制泄露。配置文件一旦提交到代码仓库,密钥就进了历史记录,就算后来删了,历史里还在,谁 clone 都能翻出来。第二,日志泄露。程序出错时经常把配置打出来,密钥就进了日志文件,日志再被收集到某个平台,扩散面就大了。第三,权限过大。很多密钥是"一把钥匙开所有门",泄露一个等于泄露全部。

更麻烦的是,密钥泄露往往发现不了。不像程序崩溃有报错,密钥被人拿去用了,你这边可能一点感觉都没有,等到账单爆炸或者数据异常才发现,那时候已经晚了。

5.2 环境变量:最基础但也最容易做错的一层

最基础的收敛手段是环境变量。把密钥从代码里挪到环境变量,代码里只读不写。这一步能挡住大部分"顺手提交"的泄露。

但环境变量也有讲究。第一,不要用 .env 文件提交到仓库。.env 文件本身是明文的,如果提交了等于没做。正确做法是把 .env 加进 .gitignore,只提交一个 .env.example 作为模板。第二,环境变量也会泄露。程序崩溃时的 core dump、某些调试接口、子进程继承,都可能把环境变量带出去。所以环境变量只是第一层,不是终点。

第三,不同环境用不同密钥。开发、测试、生产必须用不同的密钥,这样开发环境泄露了不会影响生产。这一点很多人图省事用一个密钥走天下,风险极大。

5.3 密钥管理服务:什么时候值得上

当项目规模上来之后,环境变量就不够用了。你需要的是密钥管理服务——把密钥集中存起来,程序运行时动态获取,而且有审计日志、有权限控制、有轮换机制。

这类服务通常提供几个核心能力:加密存储(密钥在服务端加密,不是明文)、访问控制(哪个服务能读哪个密钥)、审计日志(谁在什么时候读了什么密钥)、自动轮换(定期换密钥,降低泄露影响)。用上之后,你的代码里不再有任何密钥,只有一个"去哪里取密钥"的地址。

代价是引入了一个依赖,而且服务本身要保证高可用——它挂了你的程序也起不来。所以要不要上,取决于你的项目规模和密钥敏感度。我的判断标准是:如果密钥泄露会造成实际损失(钱、数据、声誉),那就值得上。如果只是内部工具、密钥权限很小,环境变量加严格的 .gitignore 也够用。

5.4 我实际项目里的分层做法

说说我自己怎么做的,供你参考。我把密钥分成三层。

第一层是完全公开的配置,比如 API 的 base URL、超时时间这些,直接写在代码里或者配置文件里,无所谓。

第二层是敏感但低风险的密钥,比如只读的第三方 API key。这类放环境变量,配合 .gitignore 和 CI 的 secret 管理,够用。

第三层是高风险密钥,比如能写数据、能扣费的。这类一律走密钥管理服务,代码里只存一个引用,运行时动态取。而且这类密钥我会设置最小权限——只给需要的操作权限,不给全量权限。比如一个只需要读某个表的密钥,就绝不给它写权限。

还有一条经验:密钥要能轮换。设计系统时就要考虑"如果这个密钥泄露了,我怎么在不停机的情况下换掉它"。如果换密钥需要改代码、重新部署,那说明设计有问题。好的设计是密钥存在外部,程序启动时读取,换密钥只需要更新外部存储,重启或者热加载即可。

6. 把感知、决策、执行串起来:一个完整的排查思路

6.1 出问题时先定位在哪一层

前面把三层拆开讲了,实际排查时最重要的是快速定位问题出在哪一层。我的做法是看现象。

如果 Agent 完全不动,或者报"找不到元素",那大概率是感知层问题——页面没加载完、元素筛选逻辑漏了、可访问性树没建好。这时候要去看感知层输出的元素清单,确认目标元素在不在里面。

如果 Agent 动了但点错了,那是决策层问题——模型选错了元素,或者动作参数不对。这时候要去看模型的决策日志,看它为什么选了这个。常见原因是元素清单里有多个相似项,模型分不清。

如果 Agent 点了但没反应,那是执行层问题——坐标偏了、被遮挡了、事件没触发。这时候要去看执行层的详细日志,确认点击事件到底有没有发出去、发到哪了。

这个分层定位法能帮你省下大量瞎试的时间。很多人一遇到问题就改 prompt,其实问题根本不在决策层,改 prompt 当然没用。

6.2 日志要记到什么粒度

排查依赖日志,但日志记太粗没用,记太细又刷屏。我的经验是关键节点必记,中间过程按需记。

关键节点包括:每次感知后记录元素清单的摘要(多少个元素、有哪些关键项)、每次决策后记录选了哪个动作、每次执行后记录成功还是失败以及失败原因。这些是必记的。

中间过程比如完整的 DOM 快照、完整的模型输入输出,这些默认不记,出问题时再开。因为完整快照体积很大,全记下来存储扛不住,而且大部分时候用不上。

还有一个技巧是给每次任务分配一个 trace ID,所有日志都带上这个 ID。这样排查时一搜就能把一次任务的完整链路拉出来,不用在茫茫日志里捞。

6.3 那些"看起来是 bug 其实是特性"的情况

最后说几个我踩过的、一开始以为是 bug 后来发现是正常行为的情况,帮你少走弯路。

第一个是页面加载慢导致的超时。有时候不是 Agent 的问题,是目标站点本身慢。这时候调大超时时间就好,不用改逻辑。

第二个是反自动化检测。有些站点会检测自动化工具,检测到了就返回一个假页面或者直接拒绝。这时候 Agent 看到的是假页面,自然操作不了。这种情况要具体分析,通常需要调整浏览器指纹或者降低操作频率。

第三个是动态内容导致的元素失效。页面重新渲染后,之前拿到的元素引用就失效了,再操作会报错。解决办法是每次操作前重新定位元素,不要缓存元素引用。

这些情况共同的特点是:问题不在你的代码逻辑,而在外部环境。识别出这一点,你就不会在错误的方向上浪费时间了。

7. 关于密钥轮换和权限收敛的一点个人体会

写到这里,浏览器控制这条链路基本讲完了。最后想单独说说密钥这件事,因为它太容易被忽视,又太容易出事。

我现在的习惯是,任何新项目一开始就把密钥管理设计进去,而不是等出了问题再补。具体做法是:项目初始化时就确定密钥存哪、怎么取、怎么轮换,然后写进 README,让所有参与的人都清楚规则。这比事后补救省事得多。

另一个体会是权限要收得比你以为的还紧。我见过太多"为了方便先给全权限"最后变成安全隐患的案例。正确的做法是默认最小权限,需要什么加什么,而不是反过来。加权限麻烦一点,但安全。

还有一点是定期审计。每隔一段时间检查一下:哪些密钥还在用、哪些已经废弃但没删、哪些权限过大。这个习惯能帮你及时发现隐患。密钥管理不是一次性的工作,是持续的。

浏览器自动化和密钥管理,一个是让 Agent 能干活,一个是让 Agent 安全地干活。两件事都做好,项目才算真正能上生产。希望这篇拆解能帮你把这两块都想清楚。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进