乐于分享
好东西不私藏

OpenClaw 只能手动写脚本?我用 Chrome 插件实现了“录制即生成“

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 个核心问题:

  1. 为什么 OpenClaw 的 Prompt 方式在企业场景不够稳定
  2. 为什么 content.js 只监听少量事件,而不是全量 DOM 行为
  3. 为什么 selector 一定要打分,而不是”随便取一个能用的”
  4. 为什么输入事件必须去抖,否则生成的 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,最终得到的通常不是强大的录制器,而是一个又重又脆的页面脚本。

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(060);    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"]

这里特别强调第三点:不是所有事件都值得录。

如果你把 mousemovefocuskeydown 都录下来,得到的是一堆”看起来很全,实际上完全不可复用”的噪音。

这也是很多”录制器 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();    }    // 兜底 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 步骤:

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 内只保留最后一次输入值。

// 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: fill    params:      selector: "#search"      value"{name}"  - action: fill    params:      selector: "input[name='idcard']"      value"{baseUrl}/#/purchase/order"  - stepId: s2    action: fill    params:      selector: "[data-testid='supplier-input']"      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 录制一次后,可以通过变量替换适配不同数据:

# 第一次执行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 操作浏览器的可能性,但在企业落地场景下,还需要解决三个问题:

  1. 确定性
    :不能依赖 AI 幻觉,需要精确的 selector 定位
  2. 可复用性
    :不能每次都手写 Prompt,需要录制→DSL→变量替换的链路
  3. 可审计性
    :不能是黑盒执行,需要完整的日志和产物管理

SmartClaw 通过 Chrome Extension + DSL + Playwright 的组合,提供了一套”录制即生成”的解决方案,将自动化成功率从 60% 提升到 92%。

如果你想了解 SmartClaw 是如何实现 Agent 调度和任务幂等的,欢迎继续阅读本系列的第③篇:《OpenClaw 没有任务调度?SmartClaw 用幂等+租约+心跳实现企业级 Agent 管理》。


相关资源

如果本文对你有帮助,欢迎点赞、收藏、转发。你的团队在浏览器自动化落地中遇到的最大坑是什么?是异步渲染、弹窗拦截,还是跨系统数据对不齐?欢迎在评论区交流 👇