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 对比数据
| 维度 | OpenClaw | SmartClaw |
|---|---|---|
| 上手难度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 执行成功率 | 60% | 92% |
| 可复用性 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 调试成本 | 高(3-5 次) | 低(录制即可) |
| 适用场景 | 个人演示/实验 | 企业生产环境 |
二、Chrome Extension 三文件架构
如果把这三部分职责混在一起,会出现两个典型问题:
content.js太重,页面兼容性差,容易影响目标页面行为background.js只做转发,不做缓冲和批量,会导致事件上报过于频繁
所以 SmartClaw 的做法是:
content.js负责贴近页面采集,background.js负责与浏览器扩展环境打交道,服务端负责真正的"理解动作"。
这背后其实是一个很经典的工程取舍:
离页面越近,越适合采集;离业务越近,越适合理解。
如果把"采集"和"理解"都塞进 content.js,最终得到的通常不是强大的录制器,而是一个又重又脆的页面脚本。
text
recorder-extension/
├── manifest.json # 权限声明
├── content.js # 注入目标页面,监听 DOM 事件
├── background.js # Service Worker,接收消息并上报 Server
└── popup.js # 弹窗 UI,控制录制开始/停止
2.1 content.js ------ 注入目标页面
javascript
// 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 候选集
function buildSelectors(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=${role}${ariaLabel ? `[name='${ariaLabel}']` : ''}` : '';
const css = el.id ? `#${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:
java
// 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();
}
// 兜底 CSS
return 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 步骤:
yaml
steps:
- action: fill
params:
selector: "#search"
value: "B"
- action: fill
params:
selector: "#search"
value: "Bei"
- action: fill
params:
selector: "#search"
value: "Beij"
- action: fill
params:
selector: "#search"
value: "Beijing"
这不仅浪费执行时间,还可能导致页面响应异常(每次输入都触发搜索)。
SmartClaw 的做法: 500ms 内只保留最后一次输入值。
javascript
// 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 步骤:
yaml
steps:
- action: fill
params:
selector: "#search"
value: "${keyword}" # 自动识别为变量
4.2 变量自动抽取
如果用户输入的内容在不同执行场景中会变化,SmartClaw 会自动将其识别为变量:
java
// EventToDslService.java 变量检测逻辑
private boolean isVariableCandidate(String value, List<RecorderEvent> similarEvents) {
// 规则 1:相同位置的输入,值不同 → 变量
if (similarEvents.stream()
.map(RecorderEvent::getTargetValue)
.distinct()
.count() > 1) {
return true;
}
// 规则 2:值符合常见变量模式(姓名、身份证、手机号等)
if (value.matches("[\\u4e00-\\u9fa5]{2,4}") // 中文姓名
|| value.matches("\\d{17}[\\dX]") // 身份证
|| value.matches("1\\d{10}")) { // 手机号
return true;
}
return false;
}
生成的 DSL 自动插入变量占位符:
yaml
vars:
required: [name, idcard, phone]
steps:
- action: fill
params:
selector: "input[name='name']"
value: "${name}"
- action: fill
params:
selector: "input[name='idcard']"
value: "${idcard}"
五、DSL 生成与置信度评分
5.1 从事件流到 DSL
录制完成后,服务端异步触发 DSL 生成:
java
// RecorderService.java
@PostMapping("/api/recorder/events")
public void receiveEvents(@RequestBody List<RecorderEvent> events) {
// 1. 入库
recorderEventRepository.saveAll(events);
// 2. 如果是 REC_STOP 事件,触发 DSL 生成
if (events.stream().anyMatch(e -> "REC_STOP".equals(e.getType()))) {
eventToDslService.convertAsync(events.get(0).getSessionId());
}
}
转换流程:
原始事件流 → 去重/去抖 → 合并连续输入 → 补充等待步骤 → 生成 DSL YAML
5.2 置信度评分
生成的 DSL 会有一个 confidence 分数(0-100),表示自动化执行的可靠性:
java
// EventToDslService.java
public DslConversionResult convert(String sessionId) {
List<RecorderEvent> events = fetchEvents(sessionId);
int totalSteps = 0;
int highConfidenceSteps = 0;
for (RecorderEvent event : events) {
totalSteps++;
String selector = selectBestSelector(event);
// 评分规则
if (selector.contains("data-testid")) {
highConfidenceSteps++; // +100 分
} else if (selector.startsWith("role=")) {
highConfidenceSteps++; // +80 分
} else if (selector.startsWith("#")) {
highConfidenceSteps += 0.6; // +60 分
} else {
// className 或 xpath,低分
}
}
int confidence = (highConfidenceSteps * 100) / totalSteps;
return new DslConversionResult(dslYaml, confidence);
}
评分标准:
- confidence ≥ 80:可直接发布
- 60 ≤ confidence < 80:建议人工审核
- confidence < 60:必须人工修订
5.3 自动补充等待步骤
SmartClaw 会在关键操作后自动插入 waitText 或 waitVisible,提高稳定性:
java
// 点击"提交"按钮后,自动等待"成功"文本出现
if (event.getText().contains("提交") || event.getText().contains("保存")) {
steps.add(WaitStep.builder()
.action("waitText")
.params(Map.of("text", "成功", "timeoutMs", 8000))
.build());
}
生成的 DSL:
yaml
steps:
- stepId: s7
action: clickRole
params:
role: "button"
name: "提交"
- stepId: s8
action: waitText
params:
text: "成功"
timeoutMs: 8000
这一步看似简单,但实际上解决了 70% 的"点击后页面还没加载完成就执行下一步"的问题。
六、真实案例:某 ERP 系统录入流程
6.1 背景
某制造企业 ERP 系统,每天需要录入 200+ 条采购订单,每条订单包含:
- 供应商名称
- 物料编码(10 项)
- 数量、单价
- 交货日期
人工录入平均耗时 5 分钟/条,每天耗时 16 小时。
6.2 录制过程
- 操作员点击 SmartClaw 插件"开始录制"
- 正常录入一条订单(5 分钟)
- 点击"停止录制"
- 服务端自动生成 DSL,confidence = 87
6.3 生成的 DSL(简化版)
yaml
dslVersion: 1
templateId: erp-purchase-order-create
name: ERP 采购订单录入
vars:
required: [supplier, materials, deliveryDate]
settings:
timeoutMs: 15000
steps:
- stepId: s1
action: navigate
params:
url: "${baseUrl}/#/purchase/order"
- stepId: s2
action: fill
params:
selector: "[data-testid='supplier-input']"
value: "${supplier}"
- stepId: s3
action: clickRole
params:
role: "button"
name: "添加物料"
- stepId: s4
action: fill
params:
selector: "input[placeholder='物料编码']"
value: "${materials[0].code}"
- stepId: s5
action: fill
params:
selector: "input[placeholder='数量']"
value: "${materials[0].quantity}"
# ... 循环填充其他物料
- stepId: s10
action: clickRole
params:
role: "button"
name: "提交"
- stepId: s11
action: waitText
params:
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 录制一次后,可以通过变量替换适配不同数据:
yaml
# 第一次执行
variables:
supplier: "华为技术"
materials: [...]
# 第二次执行
variables:
supplier: "小米科技"
materials: [...]
7.3 可审计性
OpenClaw 的执行过程是黑盒,无法追溯哪一步错了。
SmartClaw 每一步都有详细日志和截图:
json
{
"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 应用 》 敬请期待
如果本文对你有帮助,欢迎点赞、收藏、转发。你的团队在浏览器自动化落地中遇到的最大坑是什么?是异步渲染、弹窗拦截,还是跨系统数据对不齐?欢迎在评论区交流 👇
