miniprogram-automator 在 2026 年还能用吗?——官方 SDK 三处硬伤的排查与重建

本文记录了 2026 年 Windows 环境下,官方 miniprogram-automator 在实际使用中遇到的兼容性问题排查与重建过程。起因是想做一个 Claude Code Skill,让 AI 读懂微信小程序的 WXML/JS、自动生成自动化测试脚本,结果动手第一步就撞上官方 SDK 的三处硬伤:launch() 在我当前的 Node v24.12.0 + Windows 环境下稳定触发 spawn EINVAL,且把错因误报成 cliPath 不对;Page.* 命令族整族不响应;排查中又被残留会话骗了两轮------DevTools 在 cli 子进程被 kill 后不关自动化端口,导致 launch() 秒连上上一轮的残留会话,几秒后被顶掉。最终把元素层能力整个重建在 evaluate 之上,做成适配包 miniprogram-automator-next,在真实项目上跑通 27 项自检。本文区分了已实测与仍属推断的证据边界,供同样在微信生态里做自动化测试的同学避坑。

上回刚写了《微信云托管迁移后内容含违规信息全线排查》,把后端链路的坑趟平了。这次换了个方向------想给小程序前端本身做自动化测试。结果没想到,官方 SDK 先给我上了一课。

一、起因:想做测试 Skill,先修的是 SDK

为什么选择 miniprogram-automator

起因是看到 Qwen-UI-Agent(阿里通义的 GUI Agent 方向,让模型看屏幕操作手机)。我想的是更轻的路线 :不部署视觉模型实时看截图,而是让 AI 读小程序的代码静态结构,生成确定性的测试脚本。脚本为主、探索为辅。

调研到官方有 miniprogram-automator,月下载约 4.8 万------地基现成,不用自己造自动化层。

原本的技术路线

计划很简单:写个 Claude Code Skill,教 AI 读 WXML/JS → 生成 automator 脚本 → 在开发者工具里跑。

然后就没有然后了。接下来两天全花在让官方 SDK 先能跑起来上。

环境信息

操作系统 Windows 11
Node.js v24.12.0
微信开发者工具 2.01.2510290
基础库 3.17.0
官方 SDK miniprogram-automator(2023-11-07 后未更新)
被测项目 真实小程序项目(WXML + TS)

二、launch() 必炸:spawn EINVAL,而错误信息在骗你

1. 表面错误:cliPath 明明正确

照官方 README 写第一行代码:

javascript 复制代码
const automator = require('miniprogram-automator')
await automator.launch({ cliPath, projectPath })

得到:

vbnet 复制代码
Failed to launch wechat web devTools, please make sure cliPath is correctly specified

于是我开始查 cliPath,查了很久。结果路径完全没问题。

网上教程都写 C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat,但安装位置是用户可改的 ,我本机装在 D:\微信web开发者工具\。正确的探测方式(工具开着时最准):

powershell 复制代码
Get-Process wechatdevtools | Select-Object Path

换成绝对正确的路径,还是同一句报错。这时候才意识到:错误信息本身是误导------照它去查 cliPath 是死路。

2. 从错误堆栈找到 spawn EINVAL

打开错误堆栈才看见真相:spawn EINVAL,errno -4071

这个行为与 Node.js 在 Windows 上针对 .bat / .cmd 的 spawn 安全修复高度吻合,涉及 CVE-2024-27980(BatBadBut) 。在我的 Node v24.12.0 环境中,官方 Launchercli.bat 的直接 spawn 会稳定触发 EINVAL。SDK 最后更新是 2023-11-07,比这个 CVE 早。

更坑的是它把这次失败 catch 住,然后统一报成cliPath 不对这是我损失时间最多的地方。

3. 为什么是 cli.bat

cli.bat 的全部内容其实就一行:

bat 复制代码
"%~dp0.\node.exe" "%~dp0.\cli.js" %*

它只是 node.exe + cli.js 的壳。也就是说,绕开这个 .bat 壳,直接 spawn 同目录的 node.exe + cli.js 就行------不需要 shell: true(不重新打开 CVE 修掉的那个注入面):

javascript 复制代码
function resolveCliRunner(cliPath) {
  const dir = path.dirname(cliPath)
  const nodeExe = path.join(dir, process.platform === 'win32' ? 'node.exe' : 'node')
  const cliJs = path.join(dir, 'cli.js')
  // 同目录有 node.exe + cli.js 就直接用它们,绕开 .bat
  if (fs.existsSync(nodeExe) && fs.existsSync(cliJs)) {
    return { mode: 'node+cli.js', cmd: nodeExe, prefixArgs: [cliJs] }
  }
  return { mode: 'raw', cmd: cliPath, prefixArgs: [] }
}

4. 为什么不提 PR

想提 PR 修掉它?提不了。 官方 SDK 的 repository 字段指向腾讯内网域名 git.code.oa.com,没有公开仓库。既不能 fork 也不能 PR,物理上没有对象可提。这就是后来做成独立适配包的原因。

三、Page.* 整族死亡:协议死了,不是会话断了

1. 实际表现

launch 通了,能拿到 miniProgram。接着照文档写:

javascript 复制代码
const page = await miniProgram.currentPage()
const btn = await page.$('.guest-login-btn')
await btn.tap()

全部超时。

2. 协议族对照

一个个试下来,边界非常干净:

协议族 状态
App.*evaluate / pageStack / reLaunch / mockWxMethod / screenshot ...) 全活
Tool.*
Page.*data / setData / callMethod / $ / $$ / xpath / waitFor 整族超时
Element.* 无法正常使用(element handle 只能从 Page.getElement 拿,而它也超时)

也就是说,官方文档首页那句 page.$('.btn').tap(),在我这个版本的开发者工具上跑不通。

3. 如何排除会话断开

这一步很关键,因为会话失效也会表现成全超时,只跑一遍就下结论极容易误判。

做法:Page.* 调用和 evaluate 调用交错着跑,并且互换先后顺序各跑一次。

时间 调用 结果
0s Page.data() 超时
46.9s evaluate() 13ms 返回
后续 Page.* 持续超时
后续 App.* 正常

逻辑很简单:如果整个 WS 会话已经断开,Page.*evaluate() 应该一起失败。但实际是 Page.data() 在第 0 秒就超时,evaluate() 在第 46.9 秒仍然 13ms 返回------会话要是断了,后面的 evaluate() 不可能还活着。所以是协议死了,不是连接断了

4. 利用 evaluate() 重建 Page 能力

evaluate 能在小程序运行时里执行任意代码,那元素层能力就都能重做:

javascript 复制代码
// page.data() 的替代:直接从页面栈顶读
await mp.evaluate(() => {
  const pages = getCurrentPages()
  return pages[pages.length - 1].data
})

setDataquery(走 wx.createSelectorQuery().selectAll(sel).fields({...}))、waitForSelector 都是同样的路子。在当前环境下,这些基于 evaluate() 的替代实现本身开销很低:data() 约 9ms、setData() 约 16ms、query() 约 34~55ms、tap() 约 5ms。至少从当前测试数据看,重建层没有带来明显的性能负担。

5. evaluate() 的两个硬约束

① 函数是序列化过去执行的,闭包不生效。 外部值必须当参数传:

javascript 复制代码
const n = 42
await mp.evaluate(() => n + 1)        //  n is not defined
await mp.evaluate((x) => x + 1, n)    //  43

② 小程序逻辑层禁用 eval / new Function 所以在运行时侧还原一个函数这条路是堵死的。凡是需要函数的,得在 Node 侧构造好、把结果当数据传进去。

这条约束直接决定了 tap 的实现形状:event 对象在 Node 侧拼好,整个当参数传进 evaluate。反过来也带来一个意外好处------waitForData 的 predicate 在 Node 侧跑,所以它的闭包是正常可用的

6. 为什么 tap() 必须结合 WXML 静态分析

Element.tap 协议不可达,所以点击实际是构造 event 对象直接调页面 handler。拆成三个要素看:

要素 运行时拿得到吗
元素存在性 / 位置 / 尺寸 selectorQuery.fields({rect, size})
元素的 dataset / id fields({dataset: true, id: true})------包会自动读出来填进 event,所以读 e.currentTarget.dataset.xxx 的 handler 能正常工作
bindtap="xxx" 这个绑定关系 拿不到fields() 不给事件绑定,它只存在于 WXML 源码里

所以要点一个按钮,必须有人去静态读 WXML 把 handler 名找出来。

这一条把我原本的定位给坐实了:AI 读 WXML 在这个方案里不是包装卖点,是技术必要条件。 我本来担心 AI 读代码生成脚本听着像给一个普通 SDK 套壳,结果协议层的缺失反过来证明了这个环节不可省。

四、残留会话:0.4 秒的 launch 比超时更危险

1. 一个看似正常的 launch()

包写完,demo 跑起来------每次都死在 open('/pages/home/index')

sql 复制代码
Connection closed, check if wechat web devTools is still running

而工具明明开着。

2. 用时间线定位残留会话

先怀疑是首页 onLoad 里的逻辑把运行时搞崩了,换一个已知好的页面(explain)跑对照------也死。又怀疑工具真的挂了,写脚本在死掉后立刻重连------连上了,evaluate 正常。问题不在业务调用,在启动阶段。

于是写了个诊断脚本:verbose: true 打开 cli 全部输出、每秒探活一次、且完全不做任何导航(用来区分是空闲时自己掉线,还是被我的调用搞掉的)。

时间线一出来,真相就明显了:

scss 复制代码
[+0.4s]  launch 返回成功            ← 太快了
[+4.0s]  cli 打印出 ✔ auto
[+4.0s]  探活失败:Connection closed  ← 就在这一刻

launch 只花了 0.4 秒。 而干净启动实测要 6.5--12.3 秒。

3. 为什么端口还在但连接却会死

去查端口:

powershell 复制代码
Get-NetTCPConnection -LocalPort 9420    # 由 wechatdevtools 进程 listen,而此时并没有 cli 子进程

根因:DevTools 在 cli 子进程被 kill 之后,不会立刻关掉自动化端口。 官方 launch() 一起来就轮询 connect,于是瞬间连上上一轮的残留会话 并宣布启动成功;几秒后新会话把它顶掉,你就在随后随便哪个调用上收到 Connection closed

所以报错落在哪个 API 上纯属随机------它伪装成了 open() 这个方法有毒。在我的测试环境中,亚秒级 launch 成功是非常强的残留会话信号。正常冷启动实测为 6.5~12.3 秒,因此 0.x 秒级的成功反而值得警惕。

4. 最终启动策略

  1. 等 cli 输出里那行单独的 auto 再开始 connect。 别硬匹配 ✔ auto 那个对勾(不同终端编码下字节不稳),剥掉行首非字母数字字符再比 === 'auto'
  2. ✔ auto 只是必要条件不是充分条件------实测它打出来之后端口还可能没起来(遇到过打完标记然后 120s 连接全超时),所以标记只用来拦住抢跑,端口就绪仍靠轮询判断。
  3. 连上后 evaluate 探活 → 等 1.5s → 再探一次。残留会话就是在第二次探活被筛掉的。

5. 一个反面案例:不要用裸 TCP 探 WebSocket 服务端

我中间写过一个 portInUse() 用裸 TCP 探端口占用------net.connect 捅一下再立刻 destroy()。在我的一次实测中,它确实导致 DevTools 后续无法正常建立自动化连接:✔ auto 打了,但端口始终不 listen,硬等 120s 超时。

对一个 WebSocket 服务端做裸 TCP 连接再立刻断开,本来就不是个礼貌的动作。而且探不探端口,处理办法完全一样(都是等待信号 + 复探),所以我把这个函数删掉了,无条件走同一条路。少一个主动干扰服务端的动作,也少一个风险点。

五、最终方案:miniprogram-automator-next

整体架构

把前面三处修完,适配包的整体形态是这样的:

分工是:skill 负责读代码 → 写脚本,修复包负责让脚本真的能跑起来。两者可以单独用------修复包是普通 npm 包,不装 skill 也能手写脚本调用。

核心代码(最终测试的样子)

javascript 复制代码
const assert = require('assert')
const { launch } = require('miniprogram-automator-next')

;(async () => {
  let mp
  try {
    // 冷启动 6~13s 是正常的。秒启动反而是坏事------说明连上了残留会话
    mp = await launch({ projectPath: 'D:\\workspace\\coach-miniapp', port: 9420 })

    const page = await mp.open('/pages/home/index', { settle: 2000 })
    assert.strictEqual((await page.route()).route, 'pages/home/index')

    // 断言 1:empty 态下,登录按钮被 wx:if 藏着,不该渲染
    await page.setData({ phase: 'empty' })
    await page.wait(300)
    assert.strictEqual(await page.count('.guest-login-btn'), 0)

    // 断言 2:直接把页面摆到 guest 态------不必为了测一个按钮走完整前置流程
    await page.setData({ phase: 'guest' })
    await page.waitForSelector('.guest-login-btn', { timeout: 3000 })
    const btn = await page.query('.guest-login-btn')
    assert.ok(btn.width > 0 && btn.height > 0)

    // 断言 3:'loginAndLoad' 来自 WXML 的 bindtap,运行时读不到,只能静态抄
    const r = await page.tap('.guest-login-btn', 'loginAndLoad')
    assert.strictEqual(r.handler, 'loginAndLoad')

    console.log('✓ 全部通过')
  } finally {
    if (mp) await mp.teardown()   // 必须收,否则 cli 子进程残留,下一轮又撞残留端口
  }
})()

实测结果(27 项自检)

指标 实测值
自检通过 27 / 27 全通过
冷启动 launch 耗时 12.3s(正常范围 6.5--12.3s)
page.data() 替代(evaluate) 约 9ms
page.setData() 替代 约 16ms
page.query() 替代 约 34~55ms
page.tap() 替代 约 5ms
截图存盘 73864 bytes
覆盖能力 open / 自动读 dataset 的 tap / waitForData 闭包 / 截图

六、哪些结论已经确定,哪些还只是推断

写文档时我一度把适用范围写成 DevTools 2.01.2510290 / 基础库 3.17.0 / Node v24.12.0 / Windows 11,暗示这四个变量都是嫌疑人。用户看完提了一句:这个基础库好像基本没啥问题。 一句话点醒------回头看自己的数据:evaluate() 可以在当前小程序运行环境中稳定执行 JS,多次调用均能在毫秒级返回。这至少说明当前运行时以及 evaluate() 所依赖的通信链路是正常工作的 ,因此没有证据表明基础库运行时本身是主要故障点。死掉的是按协议域切分的 Page.*,而协议域更接近开发者工具那一侧的实现边界。所以这里必须把实测和推断分开写,不然读者会去换一个没用的变量重测。

已实测

  • 当前环境下(Node v24.12.0 + Windows),官方 Launcher 直接 spawn cli.bat 触发 spawn EINVAL(errno -4071);
  • 当前环境下 App.* 正常、Tool.* 正常;
  • 当前环境下 Page.* 持续超时;依赖 Page.getElement 获取 handle 的 Element.* 因而无法正常使用;
  • evaluate() 可以正常执行;
  • Page.*evaluate() 交错执行后,可以排除整个会话已经断开;
  • 残留 DevTools 会话可能造成亚秒级 launch 假成功(实测 0.4s);
  • 正常冷启动耗时约 6.5~12.3 秒;
  • 当前适配方案可以通过 evaluate() 重建部分 Page / Element 能力,27 项自检全通过。

当前推断

  • 基础库 3.17.0 不是主要故障点 (高可信推断):evaluate 在基础库运行时里跑得好好的,死的是按协议域切分的 Page.*,那是工具侧的边界。但这个结论是间接证据推出来的,不是对照实验;
  • 更值得优先怀疑的是 SDK 与开发者工具之间的兼容性(SDK 2023-11 后没更新,工具一直在走,形状更像版本错配),而不是基础库运行时本身;
  • 但由于目前没有进行不同 DevTools 版本的 A/B 对照,因此这仍然属于推断。

尚未验证

  • Page.* 是 DevTools 版本兼容问题------尚无版本 A/B 测试,待验证
  • 更换 DevTools 版本可以恢复 Page.*------未测试,未知
  • macOS / 其他 Node 版本上的表现------未测试。

一张表收拢(这也是全文最重要的证据边界):

结论 当前证据 状态
cli.bat 在当前环境触发 spawn EINVAL 实际复现 已确认
Page.* 在当前 DevTools 环境不可用 多 API 交错测试 已确认
App.* / evaluate() 正常 多次测试 已确认
残留会话导致秒启动 启动时间线 + 二次探活 已确认
基础库 3.17.0 不是主要故障点 间接证据 高可信推断
Page.* 是 DevTools 版本兼容问题 尚无版本 A/B 待验证
更换 DevTools 版本可以恢复 Page.* 未测试 未知

如果后续要验证 Page.* 是否会恢复,优先应该进行 DevTools 版本 A/B 测试,而不是单纯更换基础库版本。当前证据更支持前者,但还不能把它当作已验证结论。

七、当前方案的边界

  • 自定义组件是硬边界。 页面级 selectorQuery 不跨组件边界,tap 也只调页面方法。组件内部的元素查不到,定义在组件里的 handler 会提示页面对象上不存在。这是当前方案的已知边界,没解决。
  • 只能本机跑。 强依赖本地开发者工具 CLI,不支持 Linux / CI / 云端。这是官方 SDK 的限制,不是适配包能绕开的。
  • selector 不是完整 CSS。 createSelectorQuery 只支持 id / class / 标签 / ::before ::after 及其并集与后代组合。没有 nth-child,没有属性选择器。
  • 结论绑定版本。 上面所有实测数据都绑在 DevTools 2.01.2510290 / 基础库 3.17.0 / Node v24.12.0 / Windows 11 这一套环境上,换环境请重测,别直接外推。
  • 修复包刚发首版(0.1.0)。 只在一个真实项目上验证过(27 项自检),欢迎报 issue。

八、和其他方案的区别

这个方向不是空白市场,我也没打算装作是。客观说下几条路线:

项目 思路 和本项目的关系
miniprogram-automator 微信官方 SDK 本项目的地基,不是竞品;修复包以它为 peerDependency
@weapp-vite/miniprogram-automator 走 headless 模拟器 绕过EINVAL 而不是修它,路线不同
miniprogram-automator-mcp / @creatoria/miniapp-mcp / @purea/wechat-devtools-mcp 包成 MCP server 给 AI 用 形态不同(MCP tool vs Claude Code skill),本项目的差异在于针对当前环境下 Page.* 不可用的问题做了一层基于 evaluate() 的适配,而不是继续包装已失效的 API

如果只是想让 AI 能操作小程序,那几个 MCP 可能更顺手。本项目更适合:想要确定性、可提交进仓库、可重复跑的测试脚本,并且恰好撞上了这两个坑的人。

九、一个意外的收获:直接 setData 驱动状态测试

排障之外,这套基于 evaluate() 的方案还带来一个意外的能力:直接 setData 把页面摆到想测的状态

javascript 复制代码
// 测登录按钮的 guest 态:一句话把页面摆到位,而不是先走一遍登录流程
await page.setData({ phase: 'guest' })
await page.waitForSelector('.guest-login-btn', { timeout: 3000 })

传统测试要测一个按钮,得先走完整前置流程:启动页面 → 等登录状态 → 请求接口 → 等数据 → 进入 guest → 点按钮。当前方案直接定位到某个 UI 状态:

javascript 复制代码
// 断言 1:empty 态下,登录按钮被 wx:if 藏着,不该渲染
await page.setData({ phase: 'empty' })
await page.wait(300)
assert.strictEqual(await page.count('.guest-login-btn'), 0)

// 断言 2:直接把页面摆到 guest 态------不必为了测一个按钮走完整前置流程
await page.setData({ phase: 'guest' })
await page.waitForSelector('.guest-login-btn', { timeout: 3000 })
const btn = await page.query('.guest-login-btn')
assert.ok(btn.width > 0 && btn.height > 0)

这不是单纯为了绕过前置流程,而是让测试可以直接定位到某个 UI 状态进行验证 。empty 态验证按钮被 wx:if 藏住不渲染,guest 态验证按钮渲染且可点击------两个分支都没走真实登录流程。这是通过 evaluate 换来的额外好处,原生协议反而做不到这么随意。

十、总结

回答开头的三个问题:

1. miniprogram-automator 在 2026 年还能不能用?

能用,但带条件。在我当前的 Node v24.12.0 + Windows 环境下:launch() 会稳定触发 spawn EINVAL(且误报成 cliPath 不对),官方 Page.* / Element.* 元素层无法正常工作;活着的是 App.* / Tool.*evaluate()。结论绑定环境,换环境请重测。

2. 为什么我要重建 Page / Element 层?

因为官方元素层协议在当前环境不可用,而测试恰恰最需要元素级能力(查元素、点按钮、断言状态)。evaluate() 是当前环境可用的通用通道,重建在它之上才能拿到这些能力------从当前测试数据看,重建层开销很低(data() 约 9ms、tap() 约 5ms)。

3. 为什么 WXML 静态分析最终变成了这个项目不可缺少的一部分?

因为事件绑定关系(bindtap="xxx")运行时读不到,只存在于 WXML 源码里;而 Element.tap 协议不可达,点击只能构造 event 直接调页面 handler,handler 名必须从 WXML 静态找出来。这不是包装卖点,是技术必要条件。

这次排查最后没有停留在官方 SDK 不能用了这个结论上。因为真正需要的是:

lua 复制代码
AI 读取 WXML → 识别页面元素和事件 → 生成确定性测试步骤 → 通过 automator-next 执行 → 得到测试结果

因此我把这套适配层和 Claude Code Skill 一起开源成了 miniprogram-auto-test

如果你使用的是其他版本的微信开发者工具,尤其是不同版本的 DevTools,欢迎测试 Page.* 是否仍然可用------最好按照本文第三节的交错验证法测,只跑一遍很容易把残留会话误判成协议死亡。

十一、项目地址

十二、参考资料 / 延伸阅读

相关推荐
拖孩5 小时前
一个人 + AI 做的小程序,上线 15 天赚了 10 块 5
前端·后端·微信小程序
show4337 小时前
2026微信小程序创作工具生态技术趋势:从单一功能到AI整合型平台演进
人工智能·微信小程序·小程序
PHP实战开发录1 天前
微信小程序多环境配置混用排查记录
微信小程序·php·开发·接口隔离原则
zhangminghuan2 天前
微信小程序开发核心知识点全景解析(前端必备硬核基础)
前端·微信小程序·小程序
sltin2 天前
需求到落地:我如何在微信小程序里实现证件照换底、隐私打码与水印相机
数码相机·微信小程序·小程序
咸虾米_3 天前
小程序流量主哪种广告最赚钱?各类广告收益与实操建议
微信小程序·小程序·小程序开发·流量主广告
show4335 天前
2026微信小程序视频转文字多语种识别引擎技术选型:22种方言25种外语挑战
微信小程序·小程序·音视频
MrFlySand_飞沙5 天前
uniapp开发微信小程序实现接入企业微信客服
微信小程序·小程序·uni-app·企业微信