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,最终得到的通常不是强大的录制器,而是一个又重又脆的页面脚本。

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"]

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

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

这也是很多"录制器 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 会在关键操作后自动插入 waitTextwaitVisible,提高稳定性:

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 录制过程

  1. 操作员点击 SmartClaw 插件"开始录制"
  2. 正常录入一条订单(5 分钟)
  3. 点击"停止录制"
  4. 服务端自动生成 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 操作浏览器的可能性,但在企业落地场景下,还需要解决三个问题:

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

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

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


相关资源

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

相关推荐
爱学习的张大1 小时前
具身智能论文问答(三):Open VLA
人工智能·算法
霍格沃兹测试学院-小舟畅学1 小时前
多模态AI(图像+文本)该怎么测试?不是把图片丢给模型这么简单
人工智能
DFT计算杂谈1 小时前
VASP官方教程 TRIQS DFT+DMFT计算教程
运维·css·自动化·html·css3
大山同学1 小时前
claudecode精炼版-CoreCoder
数据库·人工智能·claude code·corecoder
hughnz1 小时前
建井数字化
人工智能
极智视界1 小时前
分类数据集 - 蘑菇分类数据集下载
人工智能·yolo·数据集·图像分类·算法训练·蘑菇分类
爱学习的张大1 小时前
具身智能论文精读(四):Diffusion Policy
人工智能
yingyima1 小时前
正则表达式实战:如何高效清洗脏数据
前端
兔子零10241 小时前
Ofox AI值得用吗?
前端·javascript·后端