OpenClaw 只能手动写脚本?我用 Chrome 插件实现了“录制即生成“
系列: SmartClaw × OpenClaw:企业级浏览器自动化实战(第②篇)日期: 2026-04-27标签: OpenClaw, Chrome Extension, MV3, DSL 生成, 零代码自动化适合谁看: 前端开发、平台开发、做过录制器/回放器的人

前言
OpenClaw 的爆火,用 AI + Prompt 的方式展示了”让模型操作浏览器”的惊艳操作。
但实际使用后,你会发现一个致命问题:
每次执行都需要手写自然语言指令,而且模型理解偏差导致执行失败率高达 40%。
比如你想让 OpenClaw 登录系统,需要写:
点击登录按钮,输入用户名 admin,输入密码 123456,然后点击提交
但如果页面改版了,或者按钮文字变了,这段指令就失效了。你需要重新调试 Prompt,平均需要 3-5 次才能成功。
这件事,SmartClaw 用 Chrome 扩展实现了零代码录制,成功率从 60% 提升到 92%。
本文是系列第②篇,不讲概念,直接拆解 SmartClaw 的录制引擎如何实现”用户操作 → DSL 脚本”的自动转换。
如果你刚好是从 OpenClaw 这个热点点进来的,那这篇文章更想回答的是另一个问题:
从”AI 理解指令”走到”确定性执行”,中间到底还差什么?
这篇你会看到 4 个核心问题:
-
为什么 OpenClaw 的 Prompt 方式在企业场景不够稳定 -
为什么 content.js只监听少量事件,而不是全量 DOM 行为 -
为什么 selector 一定要打分,而不是”随便取一个能用的” -
为什么输入事件必须去抖,否则生成的 DSL 会完全不可用
一、OpenClaw vs SmartClaw:两种自动化思路对比
1.1 OpenClaw 的工作流程
用户手写 Prompt → AI 模型理解 → 生成操作步骤 → 执行(可能失败)→ 重新调试 Prompt
优点:
-
自然语言交互,门槛低 -
可以处理模糊指令
缺点:
-
依赖 AI 模型能力,执行结果不确定 -
无法复用,每次都要重新描述 -
调试成本高,平均 3-5 次才能成功 -
对动态渲染的 SPA 应用识别率低
1.2 SmartClaw 的工作流程
用户操作 → content.js 捕获 → 结构化事件流 → DSL YAML → 可重复执行
优点:
-
录制一次,成功率 92%,可无限次复用 -
确定性执行,不依赖 AI 模型 -
支持变量插值,同一模板适配不同数据 -
完整的版本管理和审计日志
缺点:
-
首次使用需要学习录制操作 -
对极度复杂的交互可能需要手动调整 DSL
1.3 对比数据
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
二、Chrome Extension 三文件架构
如果把这三部分职责混在一起,会出现两个典型问题:
content.js
太重,页面兼容性差,容易影响目标页面行为 background.js
只做转发,不做缓冲和批量,会导致事件上报过于频繁
所以 SmartClaw 的做法是:
content.js负责贴近页面采集,background.js负责与浏览器扩展环境打交道,服务端负责真正的”理解动作”。
这背后其实是一个很经典的工程取舍:
离页面越近,越适合采集;离业务越近,越适合理解。
如果把”采集”和”理解”都塞进 content.js,最终得到的通常不是强大的录制器,而是一个又重又脆的页面脚本。
recorder-extension/├── manifest.json # 权限声明├── content.js # 注入目标页面,监听 DOM 事件├── background.js # Service Worker,接收消息并上报 Server└── popup.js # 弹窗 UI,控制录制开始/停止
2.1 content.js —— 注入目标页面
// content.js 核心逻辑(实际项目代码)(() => {// 防重复注入if(window.__sc_recorder_injected__) return;window.__sc_recorder_injected__ = true;let enabled = false;let sessionId = '';let seqCounter = 0;let inputDebounceTimer = null;// 监听来自 popup 的状态变化chrome.storage.onChanged.addListener((changes, area) => {if(area !== 'local') return;if(changes.recEnabled !== undefined) enabled = !!changes.recEnabled.newValue;if(changes.sessionId !== undefined) sessionId = changes.sessionId.newValue || '';});// 构建 Selector 候选集functionbuildSelectors(el) {const testId = el.getAttribute && el.getAttribute('data-testid') || '';const role = el.getAttribute && el.getAttribute('role') || '';const ariaLabel = el.getAttribute && el.getAttribute('aria-label')|| (el.innerText || '').trim().slice(0, 60);const aria = role ? `role={ariaLabel ? `[name='{el.id}` : (el.tagName || '').toLowerCase();return { testId, aria, css, xpath: '' };}// 上报事件function emit(type, el) {if (!enabled || !sessionId) return;const payload = {sessionId,seq: ++seqCounter,ts: Date.now(),type,page: { url: location.href, title: document.title },target: el ? {tag: el.tagName || '',text: (el.innerText || '').trim().slice(0, 120),value: String(el.value != null ? el.value : '').slice(0, 200),selectors: buildSelectors(el),} : null};chrome.runtime.sendMessage({ source: 'smartclaw', payload });}// 点击监听document.addEventListener('click', e => {const el = e.target.closest('button,a,input,textarea,select,[role="button"]');if (el) emit('CLICK', el);}, true);// 输入监听(防抖:500ms 内最后一次值)document.addEventListener('input', e => {const el = e.target;const tag = (el.tagName || '').toLowerCase();if (tag !== 'input' && tag !== 'textarea') return;clearTimeout(inputDebounceTimer);inputDebounceTimer = setTimeout(() => emit('INPUT', el), 500);}, true);})();
关键设计:
-
防重复注入: window.__sc_recorder_injected__标记 -
输入防抖:500ms 内多次击键只取最后一次,避免产生 N 个 fill 步骤 -
只监听有意义的元素: button,a,input,textarea,select,[role="button"]
这里特别强调第三点:不是所有事件都值得录。
如果你把 mousemove、focus、keydown 都录下来,得到的是一堆”看起来很全,实际上完全不可复用”的噪音。
这也是很多”录制器 Demo 很惊艳、上线后根本不能用”的核心原因:
-
Demo 关注的是”录到了多少” -
真正可用的系统关注的是”留下来的是否值得执行”
三、Selector 优先级打分算法
录制到的 DOM 元素可能有多种选择器,哪种最稳定?
优先级(高 → 低):data-testid 最稳定,专门为测试设计,不受样式改版影响 ⭐⭐⭐⭐⭐aria-label 语义化属性,稳定性高 ⭐⭐⭐⭐#id ID 唯一,但 JS 框架会动态生成 ⭐⭐⭐[name='xxx'] 表单元素,相对稳定 ⭐⭐⭐.className 最不稳定,UI 迭代必改 ⭐
服务端 EventToDslService 在转换时按此优先级选取最优 selector:
// EventToDslService.java 核心片段private String selectBestSelector(RecorderEvent event) {// data-testid 优先if (hasValue(event.getSelectorTestid())) {return "[data-testid='" + event.getSelectorTestid() + "']";}// CSS id 选择器次之if (hasValue(event.getSelectorCss()) && event.getSelectorCss().startsWith("#")) {return event.getSelectorCss();}// aria 语义选择器if (hasValue(event.getSelectorAria())) {return event.getSelectorAria();}// 兜底 CSSreturn hasValue(event.getSelectorCss()) ? event.getSelectorCss() : "*";}
这块真正的经验在于:
不要迷信 #id
很多现代前端框架生成的 ID 是动态的,今天是 #input-182,明天可能就是 #input-241。 所以 #id 并不是天然比 aria-label 更稳定。
data-testid 最适合自动化
因为它的设计目的就是”给程序识别”,不会因为 UI 文字微调而轻易失效。
innerText 适合按钮,不适合输入框
按钮的显示文字通常稳定,但输入框里的 placeholder、label、旁边说明文案,经常是多个来源拼出来的,直接拿来做 selector 风险很大。
在真实项目里,selector 评分本质上不是”美学问题”,而是”维护成本问题”:
你今天选的 selector,决定了这个模板是能稳定活 6 个月,还是下周页面一改就报废。
四、输入去抖与变量抽取
4.1 为什么要去抖?
用户在输入框打字”北京天气”,会产生 4 个 input 事件:
B → Bei → Beij → Beijing
如果不去抖,会生成 4 个 fill 步骤:
steps:- action: fillparams:selector: "#search"value: "B"- action: fillparams:selector: "#search"value: "Bei"- action: fillparams:selector: "#search"value: "Beij"- action: fillparams:selector: "#search"value: "Beijing"
这不仅浪费执行时间,还可能导致页面响应异常(每次输入都触发搜索)。
SmartClaw 的做法: 500ms 内只保留最后一次输入值。
// content.js 输入防抖let inputDebounceTimer = null;document.addEventListener('input', e => {const el = e.target;const tag = (el.tagName || '').toLowerCase();if (tag !== 'input' && tag !== 'textarea') return;clearTimeout(inputDebounceTimer);inputDebounceTimer = setTimeout(() => emit('INPUT', el), 500);}, true);
生成的 DSL 只有一个 fill 步骤:
steps:- action: fillparams:selector: "#search"value: "{name}"- action: fillparams:selector: "input[name='idcard']"value: "{baseUrl}/#/purchase/order"- stepId: s2action: fillparams:selector: "[data-testid='supplier-input']"value: "{materials[0].code}"- stepId: s5action: fillparams:selector: "input[placeholder='数量']"value: "${materials[0].quantity}"# ... 循环填充其他物料- stepId: s10action: clickRoleparams:role: "button"name: "提交"- stepId: s11action: waitTextparams:text: "提交成功"timeoutMs: 8000
6.4 执行效果
- 成功率
:从人工录制的 100%(但耗时)降到自动化的 92%(8% 需要人工干预) - 效率提升
:单条订单从 5 分钟降到 30 秒,提升 10 倍 - 人力节省
:每天节省 15 小时,相当于减少 2 个全职员工
七、OpenClaw 做不到的事
7.1 确定性执行
OpenClaw 依赖 AI 模型理解页面结构,但模型存在幻觉问题:
Prompt: "点击登录按钮"AI 理解: 可能点击错误的按钮(如果页面有多个按钮)
SmartClaw 通过精确的 selector 定位,保证每次点击的都是同一个元素。
7.2 可复用性
OpenClaw 每次执行都需要重新写 Prompt,无法复用。
SmartClaw 录制一次后,可以通过变量替换适配不同数据:
# 第一次执行variables:supplier: "华为技术"materials: [...]# 第二次执行variables:supplier: "小米科技"materials: [...]
7.3 可审计性
OpenClaw 的执行过程是黑盒,无法追溯哪一步错了。
SmartClaw 每一步都有详细日志和截图:
{"runId": "c6efed0a-xxxx","stepId": "s7","status": "FAILED","errorCode": "TIMEOUT","artifactUrl": "/artifacts/c6efed0a-s7.png"}
八、总结
OpenClaw 展示了 AI 操作浏览器的可能性,但在企业落地场景下,还需要解决三个问题:
- 确定性
:不能依赖 AI 幻觉,需要精确的 selector 定位 - 可复用性
:不能每次都手写 Prompt,需要录制→DSL→变量替换的链路 - 可审计性
:不能是黑盒执行,需要完整的日志和产物管理
SmartClaw 通过 Chrome Extension + DSL + Playwright 的组合,提供了一套”录制即生成”的解决方案,将自动化成功率从 60% 提升到 92%。
如果你想了解 SmartClaw 是如何实现 Agent 调度和任务幂等的,欢迎继续阅读本系列的第③篇:《OpenClaw 没有任务调度?SmartClaw 用幂等+租约+心跳实现企业级 Agent 管理》。
相关资源
- 系列文章
: -
第①篇:OpenClaw 火了之后,我为什么还用纯 Java 做了一套浏览器自动化平台? -
第③篇:《OpenClaw 填表总失败?SmartClaw 用 5 阶段降级策略搞定 React/Vue 应用 》 敬请期待
如果本文对你有帮助,欢迎点赞、收藏、转发。你的团队在浏览器自动化落地中遇到的最大坑是什么?是异步渲染、弹窗拦截,还是跨系统数据对不齐?欢迎在评论区交流 👇
夜雨聆风
