微信小程序「上线体检官」(miniapp-launch-doctor)
> 中文名沿用「上线体检官」:覆盖 UI 审阅、上线前多角色环测、过审预检三件事,角色感强、口语好记。
概述
小程序的界面缺陷,静态读代码看不出来。本流水线用 miniprogram-automator 驱动真实微信
开发者工具逐页渲染截图,把"看起来没问题"变成"有图为证"。
同时提供一条平行轨:当模型不支持读图、或截图尚未产出时,用静态量化审计把"肉眼判断"
翻译成可计算的量(WCAG 对比度、44pt 触摸区、安全区、键盘避让、组件覆盖),
所有结论可复算、可进 CI、不会因模型能力波动而失真。
> 实战教训:一次完整审阅中,模型读不了图,静态轨却查出 4 个 P0——其中
> 「主色按钮白字对比度不足」「返回/收藏热区低于 44pt」这两类,是逐页肉眼扫几十张图时
> 极易漏掉的系统性问题:它们不产生任何视觉异常,只在计算时暴露。
双轨产出一致的结论才算审阅完成;两轨冲突时以截图为准,并回头修门禁。
⛔ 铁律一:automator API 形态必须读源码确认,不能凭直觉
这是本流水线最大的坑:传错参数形态时脚本不报明确错,只是静默失败或报一个
完全指不到根因的错误。以下契约全部经 node_modules/miniprogram-automator/out/*.js 核对。
| API | 传参 | 说明 |
|---|---|---|
switchTab(t) / navigateTo(t) / redirectTo(t) / reLaunch(t) |
只收字符串 | 库内部 callWxMethod(t, {url: e}),传 {url} 报 parameter.url should be String instead of Object |
screenshot(t = {}) |
只收对象 | 唯一收对象的 API:{ path, fullPage? } |
navigateBack() / currentPage() |
无参 | |
page.$(sel) / page.callMethod(name) / element.tap() |
— | element.tap() 走协议,不依赖元素在视口内可见 |
miniProgram.callWxMethod('pageScrollTo', {scrollTop, duration}) |
对象 | page.windowProperty() 是只读的,不能用来滚动 |
其他两条要点:
Connection.send()没有超时机制,只在连接关闭时 reject。冷启动必须脚本侧
自行withTimeout(promise, ms, label)兜底,否则 connect 阶段会永久挂住。changeRoute内置sleep(3000)。遍历多页时用redirectTo替换而非
navigateTo+navigateBack:省 3s/页,且页面栈恒为 1~2 层,不撞小程序 10 层上限。
⛔ 铁律二:截图成功 ≠ 页面切过去了
反直觉但高频:redirectTo 没报错、screenshot 也返回了 base64,但页面根本没切换
(常见于路由未在 app.json 注册、或被守卫逻辑 wx.switchTab 劫持)。
做法:切页后立刻 currentPage() 反查 path,与目标 route 不符则判 FAIL 并重试。
缺这一步的脚本会产出"假绿"——图全有,但内容全是同一页。
⛔ 铁律三:路由表以 app.json 为唯一真源
不要用 fs.readdirSync(pages) 推导待截页面。文件系统里的目录可能已建但未注册到
app.json(或反之),两处不一致时会截到根本进不去的页。直接读 app.json 的
pages 数组 + tabBar.list。
⛔ 铁律四:JS 语法门禁必须查"同作用域重复 const"
node --check 和 vm.compileFunction 都可能漏判同作用域重复 const/let 声明
(按整文件单作用域处理)。而重复 const 是 SyntaxError,后果是整页 JS 无法编译
→ 整页白屏,自动化侧只表现为 page.xxx not exists,从截图反推极其困难。
scripts/check_js_syntax.js 用"块级作用域深度栈"近似:同一 { } 层内同名声明即判重。
取向是宁可误报(会打印行号,人工一眼可辨)也不漏报。
实战价值:写这个脚本的当天,改 shoot.js 就又犯了一次 const MANIFEST 重复声明,
当场被门禁抓出——门禁的价值就在这种时刻体现。
⛔ 铁律五:空输入守卫——Number('') === 0 是最危险的静默失败
Number('') // 0
isFinite(0) // true
isNaN('') // false
所以只写 if (!isFinite(x)) throw 或 if (isNaN(x)) throw 的页面,在用户一个字段
都没填时会把空串静默当成 0 参与运算,产出一个"看起来像模像样、实则毫无意义"的结果。
实证:某坐标换算页 lat/lon/h 全空 → 点计算 → 输出 X=6378.137km, Y=0, Z=0。
若不看输入框,这张截图与真实计算结果完全无法区分。这是最危险的一类 bug。
正解:在项目自己的常量模块里放一个 numOf(v),把"空/纯空白/null"判为 NaN:
function numOf(v) {
if (v === null || v === undefined) return NaN;
if (typeof v === 'string' && v.trim() === '') return NaN;
return Number(v);
}
function hasNum(v) { return isFinite(numOf(v)); }
这样各页现有的 isNaN/isFinite 校验立刻生效,无需改动判断逻辑。改一处比逐页改划算。
⛔ 铁律六:CSS 变量三向核验(定义 / 引用 / fallback)
var(--x) 在 --x 未定义且无 fallback 时,整条声明失效(不是回退到初始值,
是声明作废)。这会静默吃掉圆角、字体、间距,视觉上表现为"某些卡片棱角分明、某些圆润",
从截图极难定位到具体哪条规则。
scripts/check_css_tokens.js 做三件事:
1. 扫全部 wxss 收集 --x: 定义
2. 扫全部 wxss 收集 var(--x) 引用
3. 区分有无 fallback——var(--x) 后紧跟 , 或 ) 即为无 fallback
顺带一类:单位和标签类需显式 white-space: nowrap。大数字旁的单位串
(如 X(km)、起始 RB)若未锁 nowrap,会在剩余宽度里从中间被折断,
且这类问题往往全站普遍存在(因为大家都会忘写这条)。
⛔ 铁律七:统计 class 绝不能用 \bbtn\b 这类词边界
本流水线最隐蔽的错误。 正则 /\bbtn\b/ 匹配时,btn-ghost 里的 btn 前面是词边界、
后面是 -(非词字符),也算匹配——于是 btn-ghost 被 btn 吞掉,
统计结果变成「.btn-primary 使用 0 次」,与事实完全相反。
曾据此得出「全站没有强调按钮,强调色是死令牌」的错误结论。
正解:先按 class="..." 属性切分,再对切分结果做精确字符串比对:
for (const m of wxml.matchAll(/class="([^"{}]+)"/g)) {
const cls = m[1].replace(/\{\{[^}]*\}\}/g, '').trim();
for (const c of cls.split(/\s+/)) {
if (c === 'btn-primary') count++; // 精确比对,不做子串/词边界匹配
}
}
反向教训同样重要:一次性探针脚本的结论不可信。本流水线曾用 4 个临时脚本分维度跑,
因类名列表不全会静默漏检(触摸区误报「0 处不足」),差点让一个真实 P0 漏过。
所有审计维度必须沉淀为单一常驻门禁并支持 --gate 退出码;临时探针只用于「探索」,
不能作为「结论来源」。
⛔ 铁律八:不要把复杂脚本内联进 shell
含中文 Windows 路径 + 多层反斜杠 + 反引号的代码,内联进 node -e 会被 Bash 层破坏:
\\\\→////,中文路径后的引号边界错乱 →SyntaxError: missing ) after argument list- 反引号被 shell 当命令替换执行,内容直接丢失(写文档/记忆时整段变空)
- 非捕获分组
(?:在传输中易丢右括号
一律写成 .js / .py 文件再执行,或用 Write 工具落盘。
另注:内联 String.replace(anchor, anchor.source) 里的 anchor.source 是正则字面量本身
而非匹配文本,必须用 m[0]。
⛔ 铁律九:模型读不了图时,改用静态量化轨,不要降低标准
当模型不支持图片输入(报 does not support reading images)或委派的视觉 agent 被限流中断时:
- 不要把视觉结论降级成「可能存在排版问题」这类空话
- 不要凭想象编造图上细节
- 应当切换到
scripts/ux_audit.js的量化轨,用代码 + WCAG 公式给出可复算的结论 - 在报告里显式申报「本轮未覆盖视觉观感」,并说明哪些维度靠代码取证、哪些需切模型补
详见 references/review-playbook.md 的「双轨审阅协议」。
⛔ 铁律十:把「项目约定」外置成配置,别硬编码进通用脚本
通用技能最容易腐化的方式,是把某次实战的项目约定写进脚本:某个按钮类名、某段中文
关键词、某个示例选择器。后果是换项目后静默退化——脚本不报错,只是推不出动作入口,
于是截图只截到空态,审阅结论悄悄失真。
做法:所有约定进 *.config.json,脚本读不到时必须显式警告而不是无声降级:
if (!fs.existsSync(CFG_FILE)) {
console.warn('! 未找到配置,--fill 将退化为「只调主计算」,结果态截图会失真。');
}
配套两条:
- class 精确比对(铁律七的延伸):解析
class="a b c"时必须split(/\s+/)后逐个
===,不要用「类名子串出现在属性里」判断。 - 需要 eval 表达式的场景,用
new Function('x0',...,expr)显式声明形参,
不要裸eval(expr)——eval在当前作用域里找不到那些形参,会抛ReferenceError。
<!-- evolve:rule-respect-host-decision -->
⛔ 铁律十三:宿主项目代码里写了决策理由时,不许擅自推翻
做优化前先读宿主项目自己的注释。很多产品决策的理由就写在代码注释里,
不知道理由就动手,等于把别人的设计推翻一遍。
实例:某项目 ntn 页留有注释:
> v6.2:只填数、不自动计算。原因:一键自动计算会让"示例数据"看起来像"默认有数据",
> 用户分不清是协议默认值还是自己填的。改由用户点「立即还原 →」显式触发。
我在不知情的情况下给另外 4 页做了「首访自动预填 + 自动计算」,转化数据是好看了,
但违反了全站一致性,最后全部回滚。
纪律:
1. 动手前 grep 一下宿主有没有相关注释/决策记录(关键词:原因 / because / v6. / 铁律 / 决策)
2. 发现自己的优化与既有决策冲突 → 停下来,把冲突和两条路的取舍摆给用户拍板
3. 交互/UI 口径的变更尤其如此:这是产品决策,不是 bug 修复
> 来源:2026-10-03 · 34 页 / 29 款工具小程序实战(20 轴 × 5 轮回归)
<!-- evolve:rule-verdict-by-exit-code -->
⛔ 铁律十四:判成败必须用「退出码 + 结论行数值」,不许用输出文本匹配
流水线 / 校验脚本判定子任务成败时,只能以退出码 + 脚本自己打印的结论行数值为准,禁止对 stdout 做 /FAIL|失败|✗/ 这类文本匹配。
误判现场:自进化引擎把 _selfcheck.js 的输出扫一遍,发现有“失败”字样即判失败——可那一行明明是「结果: 69 通过, 0 失败」。于是在毫无错误的情况下连续回滚三次。同类翻车:publish_check.js 输出的 ✎ 人工待办被当成错误。
正确写法:
run('_selfcheck.js', '技能自检', (out, code) => {
const m = out.match(/结果:\s*(\d+)\s*通过,\s*(\d+)\s*失败/);
if (!m) return { ok: false, tail: '未找到结论行;exit=' + code };
return { ok: +m[2] === 0 && code === 0, tail: m[0] };
});
三条规定:
- 每个被调度的脚本都应打出一行机器可解析的结论(如
结果: N 通过, M 失败)。
没有结论行的输出视为「无法判定」而非「失败」——两者含义不同,不要合并。 READY*(技术就绪,仅差人工签署信息)不算阻断,否则流水线永远转不了绿。- 诊断信息要打到 stderr 或完整保留,别截断。回滚三次都看不清真因,
正是因为错误信息在拼接时被裁掉了。
> 来源:2026-10-03 miniapp-launch-doctor v1.2 自进化闭环实测
<!-- evolve:rule-cross-check-independent -->
⛔ 铁律十五:自己的校验通过了,不代表产物是对的——必须用独立实现交叉验证
当校验器和被校验对象是同一个作者写的同一套逻辑时,它验证的是「我有没有把自己想说的话说一致」,不是「产物对不对」。
误判现场:package.js 手写 ZIP 容器,自带 CRC 复算并用 zlib.inflateRawSync 逐条还原比对,
输出「25/25 条目解压核对通过」。拿 Python 的 zipfile 一读:
zipfile.BadZipFile: Bad magic number for central directory
根因:写中央目录时先拼了全部 file header,再拼了全部文件名([...headers, ...names]),
没有逐个交错。自己的校验恰好按同样的顺序解析,于是手拉手一起错。
规则:
- 容器 / 协议类产物,必须用第三方实现验收。别嫌麻烦 —— Python
zipfile、unzip -t、
浏览器里打开,任选其一。作者自己的解析器只能算「中间过程检查」,不算验收。 - 一个实现的 bug 会同等地污染它自己的测试。测试若与实现共用算法 / 常量 / 顺序,
它就退化成一致性检查而非正确性检查。同一族错误还包括:
位域解码里用解码结果重建原始比特、序列化库用自己的反序列化验证自己的序列化。 - 交叉验证失败时,先怀疑自己。第一反应往往是「第三方库的锅吧」——
实际每一次都是自己错了。第三方实现被千万人跑过,你的几百行没有。
实操:把这一步写进流水线而不是靠记性。package.js 输出末尾已提示
「用独立工具打开一次」,别跳过。
> 来源:2026-10-03 package.js 手写 ZIP 被 python zipfile 判 Bad magic
<!-- evolve:rule-check-on-relative-path -->
⛔ 铁律十六:门禁判定必须作用在相对路径上,且必须在非默认安装路径下复跑一次
自用能过 ≠ 别人能用。 在自己的默认路径下全绿,搬到别人机器上可能全线假阻断。
翻车现场:publish_check.js 扫临时残留时写的是
files.filter(f => /(^|[\\\/])tmp_/.test(f)) // f 是绝对路径
把自己的技能拷到一个名叫 tmp_skill_filled 的目录再跑,Skill 下每个文件都被判为
「残留临时文件」——因为那个 tmp_ 匹配的是安装路径里的目录名,不是包内文件。
24 项假阻断。真要命的是:用户从市场下载后解压到 %TEMP%\\tmpA1B2\\ 下会立刻遇到,
而这个路径谁也控制不了。
规则:
- 任何「包里有什么 / 缺什么」的判定,一律先
path.relative(根, f)再匹配。
绝对路径只用于打开文件,不用于做结论。 - 写完门禁必须在「换了地方」的条件下复跑:改个名字、拷到临时目录、
换个盘符、甚至换台机器。默认路径下通过不能说明任何事。 - 断言所依赖的前置条件越隐蔽,越要写进测试用例。
这次若不是随手把临时目录命名为tmp_,这个 bug 会一路带进市场。
推广:这条同样适用于 $HOME、~、/usr/local 等会被展开的路径——
判「是否越界」之前,先想清楚这个路径是内容还是容器。
> 来源:2026-10-03 publish_check.js 在自定义 --skill 路径下 24 项假阻断
<!-- evolve:rule-gate-must-be-clearable -->
⛔ 铁律十七:人工待办清单必须可清空——写黑暗测试证明填上后能到 READY
把「机器填不了的东西」列成人工待办很容易,难的是证明它们填上之后门禁真的会转绿。
待办清单如果有一项永远清不掉,这道门禁就从「把关」退化为「装饰」。
本次连踩两个坑:
author: ""被判成已署名 —— YAML 取出来是字符串'""',直接判真假它恒为真。
修正:取值先剥引号再去空白(v.replace(/^"(.*)"$/,'$1').trim())。- 修正后填上
Skill Author <author@example.com>仍然被判未署名 ——
占位符正则里的<.*>把合法的「名 <邮箱>」格式当成模板占位符了。
修正:先摘掉含@的尖括号 token,再判占位符。
规则:
- 每列一项人工待办,就要配一条「填完会长什么样」的验收样例。
没有样例,等于没人验证过这条检查会不会误伤合法输入。 - 写黑暗测试跑一遍:复制技能到临时目录 → 按清单填伪值 → 跑门禁 →
断言输出变成READY · 可上架且待办归零。本次正是这样才发现两个 bug 的。 - 占位符检测要保守。宁可放过
<unknown>,也不要误判张三 <zhangsan@corp.com>。
误判的代价是用户改签名去迎合工具,本末倒置。 - 空值 ≠ 缺省。
""、' '、null、undefined、蹦出一个空数组——
它们语义不同,但在if (v)面前长得一样。逐个列出来再决定怎么判。
> 来源:2026-10-03 publish_check.js author 假通过与误伤邮箱格式
<!-- evolve:rule-multi-role-discipline -->
⛔ 铁律十八:多角色环测不许混口径——先选角色,按套餐分章,中途不换
本技能有 12 个专家角色(测试架构师 / UX 评审 / 用例生成 / 模糊测试 / 代码审计 / 隐私合规 /
性能兼容 / 审核员 / 恶意用户 / 测试开发 / 发布经理 / 缺陷分析)。用错角色 = 用错判据,
最典型失效:用 UX 评审的「观感」去判 WebView 安全风险,或用测试架构师的「风险矩阵」去写
界面修改建议——结论混着写,研发看了没法执行。
规则:
- 一次只加载一个角色。用
node scripts/roles.js --role=<id>取该角色完整 prompt,
投给模型时全程不切换其他角色口径。 - 「做上线前深度自测」按标准套餐分章输出(见
references/roles-index.md第二节):
① 测试架构师框范围 → ② UX 评审逐页 → ③ 用例生成补边界 → ④ 代码审计 + 隐私合规 →
⑤ 性能兼容 → ⑥ 审核员 + 恶意用户 → ⑦ 发布经理收口。每章开头标注本章用哪个角色。 - 所有角色共用「防敷衍条款」(证据位置 / 可执行建议 / 验收标准 / 不泛泛 / 不编造 /
优先级统一定义),由roles.js自动拼在每个角色 prompt 尾部,不许省略。 - 量化判据以
capability-spec.md+design-guidelines.md为准,不凭感觉。
性能数字(冷启动 ≤2s、主包 ≤2MB、setData ≤256KB)、隐私清单、类目资质、27 条体验评分权重,
全是硬基线,可在真机/脚本复算。 - 不一次倒完 80 条问题。UX 评审必须走四轮(对齐 → 页面 UI → 流程 → 汇总),
每轮独立交付与确认点(见references/roles-ux.md)。
> 来源:2026-10-04 用户把 12 个专家 prompt 整合进技能时,明确「不许混口径、按套餐分章」
<!-- evolve:iron-scan-chinese-aware -->
⛔ 铁律十九:扫描类检查对中文工程文案必须精准,且待评不计入总分
小程序扫描类检查(内容残留 / HTTPS / 交互规范 / 360 评分)在中文工程语境下极易误伤,必须遵守:
- HTTPS 外链:
http://后接schemas.openxmlformats.org/www.w3.org/purl.org/dc/officedocument等 XML/Office/DublinCore 命名空间 URI,是规范标识不是网络外链,不计明文泄漏(自研 xlsx 引擎大量出现)。 - 测试残留:只命中显式残留短语——
测试数据/测试账号/假数据/临时数据+ 英文\bmock\b \bdemo\b \btodo\b \bfixme\b \blocalhost\b;禁止裸命中「测试」(受众词)与「示例/示例数据」(「示例」一键填充功能的用户向文案)。英文 token 必须加词边界,否则nodeModules会被demo子串误中。 - 交互规范:
:active是微信合法按压态,不算缺陷,仅作 hover-class 增强建议,不扣分。 - 360 评分:子项 note 以「待评」开头(模板默认 / 仅留证据)不计入总分,否则把优质小程序打成假 D;结论须声明待评权重。
> 来源:某 5G/NTN 小程序实战,原 score360 把项目打成 datasafe=50 / content=0 / 整体假 D,已逐一修正。
> 来源:某 5G/NTN 小程序实战:score360 假阳性修正
使用流程
0. 首次配置
# 安装自动化库(放在工程内的脚本目录,不污染全局)
cd <root>/tests/.automator && npm i miniprogram-automator --no-save
Windows 中文批处理注意事项见 scripts/make_bat.py 头注释:GBK + CRLF + 无 BOM + 无
chcp,否则双击报"找不到路径"。改批处理请改 shoot.bat.new(UTF-8 源)再执行
python make_bat.py 转码。
1. 先跑静态门禁(比截图快几个数量级,改完代码先自检)
node scripts/ux_audit.js --root=<工程根> # 8 维度量化审计
node scripts/ux_audit.js --root=<工程根> --gate # 同上,P0 存在则 exit 1
node scripts/check_js_syntax.js # 重复 const 声明
node scripts/check_css_tokens.js # CSS 变量定义/引用/fallback
node scripts/check_empty_input.js # Number(this.data.x) 附近有无空判
node scripts/verify_empty_guard.js # 空输入守卫行为回归(断言级)
check_empty_input.js 是启发式扫描,会误报(正常范围校验、已用 numOf 的页面
都会被标出),必须人工裁定后再决定改不改。
不同项目的类名/令牌命名不同时,在工程根放 ux-audit.config.json 覆盖默认项:
{
"minTouch": 88,
"touchClasses": ["btn", "btn-primary", "my-chip"],
"heroClass": "result-hero",
"resetLabel": "清空重来",
"exemptPages": ["index", "list", "profile"]
}
2. 出图
先完全关闭微信开发者工具(普通实例不开 9420 端口),然后双击 scripts/shoot.bat。
或直接跑:
node scripts/shoot.js --fill
| 参数 | 作用 |
|---|---|
--fill |
先灌示例再触发主计算,截「结果态」。审阅 UI 必用,否则截的是空态、看不到结果区版式 |
--bottom |
长页再补一张滚到底的底屏(--fill 隐含此项) |
--only=a,b,c |
只截指定页,用于定点重跑 |
--force |
强制重截已存在的 PNG(默认断点续跑,跳过 >1KB 的) |
--list |
打印路由清单后退出 |
--fill 的三级回退:点示例卡片元素(选择器由 shoot.config.json 的 exSel 给)→ 调页面
*Sample 类方法 → 无示例则直接调主计算。主按钮上全是保存/分享/返回类处理器的页面会被判为
聚合页并跳过填充。
环境变量:WEAPP_ROOT(工程根)、WECHAT_CLI(开发者工具 cli.bat)、WORKBUDDY_NODE。
退出码:0 全成功 / 2 部分失败(重跑会只补失败页)/ 3 API 调用方式错误(脚本 bug)。
3. 对账与体检
node scripts/ui_audit.js
输出每页顶屏/底屏文件大小、fill 记录、路由校验结果。空白页判据:PNG 体积明显偏小基本
等于白屏——只有标题栏的空白页通常在 10KB 量级,而有内容的正常页明显更大。
这个阈值与页面复杂度相关,换项目后先跑一次看分布再定(--blank=<字节> 覆盖)。
--fill 后若仍显示"未灌示例"的页,通常不是脚本 bug,而是在 shoot.config.json 里
缺了该页的 exSel / sample / calc 线索。
4. 人工精读
按 manifest.json 逐页看图。重点不是"好不好看",而是"图上显示的东西是不是真的":
| 图上现象 | 对应铁律 |
|---|---|
| 输入框全空却有结果 | 铁律五(空输入守卫失效) |
| 大数字下方单位被拆成两行 | 铁律六(缺 white-space: nowrap) |
| 圆角/字体与同类卡片不一致 | 铁律六(CSS 变量无 fallback) |
| 整页只有标题栏 | 铁律四(同作用域重复声明) |
| 按钮文字看不清 / 星标看不见 | 量化轨 P0(对比度不足) |
| 点返回/收藏老是误触 | 量化轨 P0(触摸区 < 44pt) |
5. 量盒模型(把肉眼判断变成可复算判据)
截图是"看起来对不对"(依赖人眼),盒模型是"数字说话"(可复算、可进 CI)。
hero_probe.js 用 automator 真读渲染结果,专门量大数字区(hero)——那里是
"长数值被从中间劈开"这类缺陷的高发区,而这类缺陷在源码里完全看不出异常。
node scripts/hero_probe.js --root=<工程根> [--port=9420] [--out=<json>]
[--sel-n=.ui-hero-n] [--sel-u=.ui-hero-u] [--sel-b=.ui-hero-b]
输出 hero_measured.json:每页 num / unit / boxW×boxH / numW×numH / wrapped。
核心判据:wrapped = numH > 40px ⇒ 大数字已被折行。实测 56rpx 字号单行数值盒高
≈34px、折行 ≈48px+。退出码 2 表示有折行页,可直接进 CI 阻断。
三条纪律:
1. 数据源单一 —— 必须用 element.size() 拿真实盒模型,不要用"字符数 × 估算字宽"
从代码反推(等宽字体真实字宽只有渲染引擎知道)。
2. ⚠️ element.text() 与 size() 都是 async,漏 await 会拿到字符串
[object Promise],数据看似有值实则全废(脚本已加 assertNoPromise() 兜底)。
3. 某页"结果块未渲染"是探针覆盖盲区(该轮 fill 未触发计算),不是渲染缺陷——
报告里必须如实标注"N/M 页覆盖",不得写成"全站正常"。
多角色环测(12 个专家角色)
光有 UI 审阅不够——小程序上线前还要过自测、审核预检、隐私合规、性能兼容、用例生成这些关。
本技能把这些拆成 12 个有各自判据的专家角色,并配两个工具直接取用。
角色路由(速查)
| 用户说什么 | 角色 | 参考文件 |
|---|---|---|
| 截图看看 / 界面好不好看 | UX/UI 评审专家 | references/roles-ux.md |
| 上线前自测 / 环测 / 能不能上线 | 测试架构师 → 逐角色跑 | references/roles-test.md |
| 给我写测试用例 / 覆盖边界异常 | 用例生成专家 | references/roles-test.md |
| 搞破坏 / 模糊测试 / 会不会崩 | 模糊/破坏性测试专家 | references/roles-test.md |
| 审一下代码 / 有没有内存泄漏 | 代码审计专家 | references/roles-audit.md |
| 隐私合规吗 / 会不会被下架 | 隐私合规与安全审计专家 | references/roles-audit.md |
| 卡不卡 / 低端机能跑吗 / 暗黑模式 | 性能与兼容测试专家 | references/roles-audit.md |
| 会不会被拒审 / 类目对不对 | 微信审核员(预检) | references/roles-launch.md |
| 挑刺 / 恶意用户会怎么搞 | 恶意用户 + 审核员 + 弱网 + 小白 | references/roles-launch.md |
| 写自动化测试 / 跑 CI | 测试开发(automator + Jest) | references/roles-launch.md |
| 回归范围 / 上线门禁 | 发布经理 | references/roles-launch.md |
| 这段日志/截图/崩溃怎么了 | 缺陷分析专家 | references/roles-launch.md |
完整路由 + 上线前标准套餐顺序 + 能力量化总纲,见:
references/roles-index.md—— 角色总表、标准套餐、防敷衍条款、能力总纲指针references/capability-spec.md—— 量化能力总纲:性能指标、隐私/类目/资质、提审策略与驳回排查、工具矩阵、数据度量口径references/design-guidelines.md—— UX 设计判据基线:颜色/字号/间距/按钮 Token、27 条体验评分权重、导航/交互/暗黑模式、避坑
两个取用工具
# 1) 一键取某个角色的完整 prompt(自带角色设定 + 输出模板 + 防敷衍条款 + 优先级定义)
node scripts/roles.js --list
node scripts/roles.js --role=ux-reviewer
node scripts/roles.js --role=test-architect --out=out.md
# 2) 读目标工程 app.json,生成四类上线清单骨架(已填入真实页面/权限)
node scripts/gen_checklist.js --root="<小程序工程根>"
node scripts/gen_checklist.js --root="<工程根>" --out=checklist.md
gen_checklist.js 自动拉出 app.json 的真实 pages + 分包页,并扫描代码里的权限 API
(定位/相册/剪贴板等)与 Storage 键,生成 P0 冒烟 / 隐私权限 / 机型兼容 / 审核预检 四类清单。
表格已带真实条目,逐行打勾即可。
用法纪律(铁律十八)
先选角色 → 按标准套餐分章 → 中途不换口径。一次把 12 个角色结论混着倒出来,研发只看前 5 条。
UX 评审必须走四轮(对齐 → 页面 UI → 流程 → 汇总),每轮独立交付与确认点。
上线评分(360 度,把零散结论收口为总分)
多角色评审产出的是一堆 P0/P1 工单,但老板/PM 要的是「这个小程序能打几分」。
scripts/score360.js 把 references/scoring-360.md 的五维度权重固化成可计算模型,
把各角色结论收口为一个加权总分 + S/A/B/C/D 评级:
node scripts/score360.js --init --out=scorecard.json # 出评分卡模板(17 子项,默认 0 分)
node scripts/score360.js --auto <工程根> --card=scorecard.json # 静态扫描自动填客观项(HTTPS/硬编码密钥/测试残留/暗黑/:active)
node scripts/score360.js --compute --card=scorecard.json # 出维度分 + 总分 + 评级
权重模型(合计 100):技术性能 25 / 功能完整 20 / 用户体验 25 / 安全合规 20 / 运营健康 10。
维度分 = Σ(子项分×子项权重)/Σ(维度内权重)×100;总分 = Σ(子项分×子项权重)/100。
--auto 只能填客观可量化项(代码可扫的),NPS / 等保定级 / UV / 好评率等需人工或真机评定,
脚本会保留为「待评」——这是有意为之,避免用启发式数字冒充结论。
脚本清单
| 文件 | 作用 |
|---|---|
scripts/shoot.js |
逐页截图主体(DevTools 自动化)。去绝对化:工程根取 WEAPP_ROOT,缺省按脚本自身位置反推 |
scripts/ux_audit.js |
静态量化审计(对比度/触摸区/字号/组件/文案/安全区/键盘/分享),支持 --gate |
scripts/check_js_syntax.js |
重复声明检测 + 导出各页方法清单 |
scripts/check_css_tokens.js |
CSS 变量三向核验 |
scripts/check_empty_input.js |
空输入隐患启发式扫描(需人工裁定) |
scripts/verify_empty_guard.js |
空输入守卫回归:① 语义契约自检 ② 工程真实函数对照(配 verify-empty-guard.config.json) |
scripts/ui_audit.js |
manifest 体检表 + 空白页判定 |
scripts/hero_probe.js |
hero 盒模型探针:用 automator 真读大数字区 size()/text(),量出"长数值被折行"这类渲染缺陷(判据 numH>40px),可进 CI 阻断(退出码 2) |
scripts/check_shadow_bug.js |
模块遮蔽扫描:检出「require 别名被局部同名遮蔽」与「把数据对象当模块用 obj.<moduleFn>()」——后者现场症状是页面红字 xxx is not a function,属必崩级。⚠ 会误报(作用域不重叠/注释),命中须看盘核实 |
scripts/check_flex_overflow.js |
flex 溢出扫描:三类可复算判据 —— ①按钮行(判 max 不判 Σ,因 flex:1 是均分)②结果块头部(标题 vs 来源串让位)③hero 降档识别。溢出退出码 2 |
scripts/check_component_path.js |
组件路径与标签注册双检:①页面 json 的 usingComponents 是否用绝对路径 /components/...(相对路径会解析到 pages/<page>/下,编译期抛错整包挂起)②绝对路径指向的组件文件是否真实存在 ③wxml 自定义标签是否已注册(自动排除 scroll-view 等含 - 的原生组件,不误报)。纯静态、零成本、失败即挂包,建议纳入 CI |
scripts/_verify_component_path.js |
上一脚本的灵敏度验证(一次性,用完即删):注入 5 类故障(含 1 个正例防误报)→ 断言全被拦 → finally 还原。新增门禁检查后必跑,否则无法证明检查真的生效 |
scripts/pay/aipay402.js |
支付宝 Machine Pay(402 / A2M)核心库:issue402() 下发 Payment-Needed 账单(Base64URL + RSA2 八字段签名)、parseProof() 解析 Payment-Proof、verify()/verifyAndCheck() 验付 + 五道业务核对、confirm() 履约回执 ack、Ledger 订单持久化与幂等(双唯一约束 + 三账对账 + 失败不计费)。零外部依赖 |
scripts/pay/server.js |
付费资源服务端参考实现:把能力做成按次收费资源的最小完整形态,含资源目录、/_reconcile 对账端点。缺环境变量直接启动失败(不静默降级) |
scripts/pay/verify_402.js |
402 五项链路离线自检(56 项,零凭证,桩网关):账单字段/签名/防篡改、凭证解析 5 类非法输入、验付五道核对、幂等防重放、履约回执、失败不计费 |
scripts/pay/verify_server.js |
服务端端到端自检(16 项):真起服务 + 真发 HTTP 请求,验 402/404/无效凭证/不泄露内容/对账干净 |
scripts/pay/sandbox_acceptance.js |
沙箱真实验收(6 类必跑场景):需 7 项沙箱凭证。--preflight 只做环境自检不联网;--auto 跑 S2/S3/S4/S5;--full --payment-proof=<B64> --out-trade-no=<单号> 含 S1/S6。退出码有语义:0=全通过、1=有失败、2=缺凭证、3=有 N 项待人工——绝不谎报通过 |
scripts/pay/_smoke_run.js |
无凭证自证冒烟:用自签假凭证跑通验收全流程并自动清理私钥。真打网关拿到 isv.invalid-app-id 即证明网络链路/签名/请求格式正确。没配凭证也能先验证工具本身可用 |
scripts/shoot.bat |
Windows 双击入口(GBK + CRLF + 无 BOM) |
scripts/make_bat.py |
bat 转码器:GBK + CRLF + 无 BOM + 自检 |
scripts/gate_rounds.js |
20 轴一键回归:每轮一个独立验证轴,结果归档 tests/gate-rounds.jsonl;自动加载 scripts/checks/*.js 插件轴 |
scripts/evolve.js |
自进化引擎:把实战发现回流到 SKILL.md / checks/ / manifest.json,自带快照与失败回滚 |
scripts/publish_check.js |
上架就绪门禁:元数据、版本号一致、私有信息泄漏、脚本清单一致、自进化就绪 |
scripts/checks/*.js |
由 evolve.js 生成的可插拔验证轴(每个导出 { id, title, run(ctx) }),不需要改任何主脚本 |
scripts/package.js |
一键打包:产出可上传市场的 zip(剔除 evolution/backups/ 与 pending 队列)并做 CRC 复算,纯 Node 无外部依赖 |
scripts/roles.js |
多角色环测取 prompt:--list 列 12 角色,--role=<id> 输出完整 prompt(角色设定 + 输出模板 + 防敷衍条款 + 优先级定义),可 --out= 落盘 |
scripts/gen_checklist.js |
上线清单生成:读 app.json 拉真实页面 + 分包,扫权限/Storage API,产出 P0 冒烟 / 隐私权限 / 机型兼容 / 审核预检 四类清单骨架 |
scripts/score360.js |
360 度评分:把 scoring-360.md 权重固化成模型,--init 出卡、--auto 扫工程填客观项、--compute 出维度分 + 总分 + S/A/B/C/D 评级 |
scripts/_selfcheck.js |
发布前自检:语法 / 绝对路径泄漏 / 文档引用 / frontmatter / 编码 |
移植到新项目时
shoot.js不需要改代码:路由表读app.json,动作入口全靠shoot.config.json里的
exSel/primaryBtnClasses/ghostBtnClasses/exampleKeywords推断。
个别页面推断不出时,可在配置里按页键覆盖"<pageKey>": {"exSel": ".xxx", "calc": "onCalc"}。ux_audit.js必须配contrastCases为自己的实际色值,否则对比度一节只是在跑
示例自检、对你项目没有结论意义(脚本会在输出里显式警告这一点)。numOf要放进目标项目自己的常量模块(铁律:公共常量唯一真源)。- 空白页阈值与页面复杂度相关,换项目后先跑
ui_audit.js取基线分布再定--blank。 touchClasses/heroClass/resetBtnClasses若与项目命名不同,改配置不改脚本。- 所有脚本不硬编码任何项目名或本机路径:工程根取
WEAPP_ROOT或按脚本位置反推,
标题取app.json的navigationBarTitleText。
配置文件
三处支持外置配置,避免改脚本。
ux-audit.config.json(工程根)
{
"minTouch": 85,
"minFontSize": 20,
"touchClasses": ["btn", "btn-primary", "your-chip"],
"heroClass": "result-hero",
"emptyClasses": ["ui-empty", "empty"],
"resetBind": "onReset",
"resetLabel": "清空重来",
"resetBtnClasses": ["btn-ghost"],
"exemptPages": ["index", "list", "profile"],
"contrastCases": [
["白字 / 主按钮渐变最浅端", "#FFFFFF", "#你的主色", 4.5, "P0"],
["正文 / 白底", "#333333", "#FFFFFF", 4.5, "P1"]
]
}
exemptPages 填导航/首页/工具集页——它们无输入输出,不该被判「缺 hero / 缺重置」。
shoot.config.json(工程根,可选)
{
"exSel": ".your-sample-item",
"primaryBtnClasses": ["btn-primary", "calc-btn"],
"ghostBtnClasses": ["btn-ghost"],
"exampleKeywords": ["常用示例", "示例数据"],
"sampleMethodPattern": "Sample",
"skipCalcMethods": ["onSaveCard", "onBackHub", "onShareAppMessage"],
"ntn": { "calc": "onEphCalc", "sample": "onEphSample" }
}
exSel 指向「示例条目」的 CSS 选择器;primaryBtnClasses / ghostBtnClasses
决定如何从 WXML 静态解析出「主计算方法」和「示例方法」;顶层页面键用于覆盖该页推断。
verify-empty-guard.config.json(工程根,可选)
{
"numOfModule": "miniprogram/utils/consts.js",
"numOfExport": "numOf",
"calcModule": "miniprogram/utils/geo.js",
"calcExport": "llaToEcef",
"calcArgs": ["__empty__", "__empty__", "__empty__"],
"rangeChecks": ["!isFinite(x0) || Math.abs(x0) > 90"],
"caseValid": [["39.9", "116.4", "50"], ["0", "0", "0"]]
}
rangeChecks 的语义是「返回 true 即判定非法、应拦下」,用 x0/x1/x2 引用入参。
caseValid 用于证明接入守卫后合法输入的结果零变化。
自进化闭环(evolve.js)
本技能自带把实战经验写回自己的能力。一次审阅跑完,把踩到的坑沉淀成一条 finding,
下次同类项目直接具备这个认知,不需要重新踩。
发现(finding)协议
一行一条 JSON,写到 evolution/pending.jsonl:
{"id":"axis-share-brand","type":"check_axis","title":"分享标题必须带品牌前缀",
"code":"module.exports={id:'share-brand',title:'分享标题品牌前缀',run(ctx){/* ... */}}",
"source":"2026-10-03 · 34 页小程序实战","confidence":"high"}
| type | 回流去向 | 说明 |
|---|---|---|
iron_rule |
SKILL.md 铁律区,自动编号 |
必须遵守的硬纪律 |
env_pitfall |
SKILL.md「环境坑与沙箱限制」 |
环境层面的坑,和项目代码无关 |
doc |
SKILL.md 末尾 |
补充说明、方法论 |
check_axis |
scripts/checks/<id>.js |
可插拔验证轴,被 gate_rounds.js 自动加载 |
audit_rule |
同上 | 同上,语义不同的别名 |
meta |
manifest.json |
合并元数据(版本号、能力清单等) |
常用命令
node scripts/evolve.js --capture '{"id":"...","type":"env_pitfall","title":"...","body":"...","confidence":"high"}'
node scripts/evolve.js --list # 看有什么待回流
node scripts/evolve.js --apply # 回流 + 自动校验,失败整目录回滚
node scripts/evolve.js --auto # apply + verify 一条龙(无人值守)
node scripts/evolve.js --history # 已应用历史
node scripts/evolve.js --rollback # 回滚到最近快照
⛔ 铁律十一:进化不能把误判写进去
选择器选的是无人值守自动回流,所以引擎自带四道护栏,缺一不可:
confidence=low默认跳过(需--force)——拿不准的观察不许自动固化。- apply 前整目录快照到
evolution/backups/<ts>。evolution/自身不入快照(防套娃)。 - apply 后必验:跑
_selfcheck.js+publish_check.js+ 插件 JS 语法检查;
任一失败 → 整目录回滚并撤销本轮applied.jsonl记录,不留半截状态。 - 按 id 去重:已应用过的 finding 不会重复写,误判回滚后可修正重提。
⛔ 铁律十二:测试期望写错时改测试,不许改代码迎合
.workbuddy 实战里这条被违反了又纠正回来 3 次。判定顺序:
- 门禁报 FAIL → 先复核 assertions 本身对不对(是否漏豁免、是否用了错的处理器名、
是否把"优化项"当成了"阻断项")。 - 期望确实错了 → 改测试期望,并在报告中记录这一改动及理由。
- 期望没错 → 才是改实现。
直接改代码让红灯变绿,等于把门禁降级成摆设。
验证轴要区分「阻断项」和「优化项」
gate_rounds.js 里两类要分开统计:
- 阻断项:错误即缺陷,必须 FAIL(语法、路由缺失、CSS 变量未定义、对比度不达 AA、触摸区不足)
- 优化项:只报告覆盖率 / 缺口清单,不判 FAIL(示例入口覆盖、冷启动引导、分享钩子)
把优化项当阻断项,会让团队习惯性地"为了绿灯改数字"。
商业化上架(publish_check.js)
node scripts/publish_check.js --skill=<技能根> --pricing=paid
检查 11 组,输出分三档:阻断(必须由机器/作者修掉)、提示、人工待办。
| 组 | 内容 |
|---|---|
| 元数据 | SKILL.md / RELEASE.md / LICENSE / manifest.json 齐备 |
| 版本号一致 | frontmatter = manifest = RELEASE.md,且符合 semver |
| 付费声明 | frontmatter 有 pricing;manifest.pricing.model 与 --pricing 参数一致 |
| 私有信息 | 扫全目录(跳过 evolution/):Windows 盘符路径、真实用户名、客户/项目名痕迹 |
| 脚本健康 | 全部 JS 语法通过、无 BOM、无 *.new / tmp_* 残留 |
| 清单一致 | SKILL.md 脚本清单 ↔ scripts/ 实际文件双向对齐(漏登记 WARN,登记了不存在 FAIL) |
| 自进化就绪 | evolve.js + evolution/ + checks/ 在位 |
| 冒烟 | _selfcheck.js 必须全绿 |
判定输出:READY · 可上架 / READY* 技术就绪,还差 N 项人工信息 / BLOCKED。
上架前必做清单
- 填
SKILL.mdfrontmatter 的author(真实署名)与manifest.json的author - 填
manifest.json的pricing.sku node scripts/publish_check.js必须输出READYnode scripts/package.js --out=<路径>打包 —— 它已自动剔除evolution/backups/(快照可能含
私有路径)、pending.jsonl、node_modules/,并做完 CRC 复算- 用独立工具打开一次 ZIP(如
python -m zipfile -e/unzip -t),别只信打包器自己的自验 RELEASE.md写清版本、变更、付费范围与免费范围
> 授权判定由分发渠道侧完成,技能内不含任何运行时授权校验代码 —— 这是有意为之:
> 代码 MIT 开放,付费买的是分发、更新与维护,不是代码访问权(见 LICENSE 末尾说明)。
环境坑与沙箱限制
> 本节由 evolve.js 自动沉淀,均为实战踩过的坑。
<!-- evolve:env-reg-blocked -->
reg.exe 命中 Program Blacklist → 冷启动链路瘫(attach 路径不受影响)
沙箱把 reg.exe 列入程序黑名单。cli.bat 内部要用它定位安装信息,于是冷启动链路瘫痪:
PROGRAM BLOCKED BY SECURITY POLICY - The sandbox prevented a program on the
configured Program Blacklist from starting: reg.exe (系统目录路径已省略)
This block cannot be approved or bypassed from the current command.
Do NOT retry this program, launch it through another shell or script, or
attempt an equivalent workaround.
If the user wants to allow this program, they must remove it from
Security Center -> Command Security -> Program Blacklist.
⛔ 先分清「谁在拦」:别被tsbx_rules.json 带偏
排查时极易走错的一条路:去读 cli/vendor/sandbox/<ver>/tsbx_rules.json,
发现里面根本没有 reg.exe,于是得出"策略里没有它,拦截方在别处"却找不到下一跳。
真相是两者不在同一层:
| 文件 | 模型 | 与 reg.exe 的关系 |
|---|---|---|
sandbox/<ver>/tsbx_rules.json |
沙箱引擎白名单:white_process 仅 8 个浏览器,process_rules/registry_rules 全为空 |
无关。它不拦reg.exe |
cli/vendor/shim/broker-program-policy-check.cjs |
shim 层转发:把程序名交给宿主 broker,reason==='forbidden_program' → 退出码 126 |
真正的拦截点 |
即黑名单在宿主主进程内存态,磁盘上没有可编辑文件
(搜 .workbuddy 全量 json/db 只命中 traces 日志)。结论:改文件拿不掉,
只能用户在「安全中心 → 命令安全 → 程序黑名单」删条目。
⛔ 决定性判据:有没有「已经开着的自动化实例」
这是本条最容易误诊的地方。截图脚本通常长这样:
if (await portAlive(PORT)) { /* 只 connect,不 spawn */ }
else { cp.spawn('"...cli.bat" auto --project ... --auto-port 9420 ...') }
| 端口状态 | 走哪条路 | 是否碰 reg.exe | 结果 |
|---|---|---|---|
| 9420 活着 | attach,只connect | 完全不碰 | 正常出图 |
| 9420 dead | 冷启动 spawn cli.bat | 碰 → 被拦 | 链路瘫 |
⇒ 同一个工程、同一天,端口活着就能跑、端口一死就全瘫。
用户会描述成"昨晚能跑今天不行"、"策略变严了",实际变量是端口,不是策略。
排查动作(两条命令定生死):
netstat -ano | grep 9420# 有无LISTEN
tasklist /FI "IMAGENAME eq wechatdevtools.exe" /FO CSV # 注意:别用 //FI
⚠️ WeChatAppEx.exe 是微信客户端的渲染进程,不是开发者工具,
有几个它都不代表 DevTools 在跑。
恢复渲染轨的最省事办法
让用户先手动打开开发者工具并保持自动化端口开着,脚本就会走 attach 分支,
完全不用碰黑名单。这比"研究怎么删策略条目"快一个数量级。
具迷惑性的现象
cli 冷启动被拦时照样会打印
√ IDE server started successfully, listening on http://127.0.0.1:31932,
但随后探测 31932 与 9420 全部 closed,进程列表里也没有开发者工具。
拿到 "started successfully" 就以为成功,是这里最容易犯的错——必须回探端口 + 查进程。
已验证无效的三条绕过路线:
| 路线 | 结果 |
|---|---|
dangerouslyDisableSandbox 直跑 cli.bat |
照样被拦,属broker 硬策略,不可批准 |
直接 spawn 微信开发者工具.exe auto ... |
exe 被当 node 模块加载:Cannot find module '<安装目录>\auto' |
复刻 cli.bat(ELECTRON_RUN_AS_NODE=1 + electron exe + resources/app.asar.unpacked/js/common/cli/index.js) |
打印同样成功字样,进程依旧不存活 |
结论:遇冷启动被拦,不要再试绕过(也别浪费时间找策略文件),
先问用户"能不能开着 DevTools 让你attach",不行再按铁律九切静态量化轨,
并在报告里写清"渲染轨未闭环 + 根因 + 需要用户做什么"。
<!-- evolve:env-bash-shim -->
Bash shim 缺基础命令,cd 还会静默失效
ls / tail / head / dirname / sed / grep 全部 command not found;
每次命令还附带一行噪声 shim/shell-runtime-bash-env.sh: line 3: cd: null directory。
最坑的是 cd:它不报错到非零退出码,Exit Code 仍然是 0,
但工作目录根本没变。写成:
cd "工程根" && node ux_audit.js --gate # cd 失效 → 脚本在错误目录里跑
会得到"找不到 pages 目录"这类看似别的问题的报错,很容易误判。
规避:
- 脚本一律用 --root=<绝对路径> 传参,不依赖 cwd
- 列目录用内联 node fs.readdirSync,过滤输出用 node 管道,不要用 tail/head/grep
- 每两条 bash 命令之间没有状态继承,不要假设上一条的 cd 还在
<!-- evolve:env-shim-no-cp-dirty-repo -->
⛔ 连 cp 都没有 → 故障注入成功但还原失败,把脏状态留在工程里
这是本条最危险的下游坑,会让"验证门禁"这件事本身变成破坏操作。
# 想干的事:备份 → 注入故障 → 跑门禁 → 还原
cp a.json /tmp/bak.json && node inject.js && node gate.js; cp /tmp/bak.json a.json
# ↑ Exit 127,中断整条 && 链
shim 没有 cp(ls/tail/head/grep/sed/dirname/cd 同样没有)。
于是第一步就127 退出,后面全部不执行:
- 好一点:故障没注入,只是白跑一趟
- 坏一点(; 分隔而非 &&):故障注入了,还原没跑 → 脏状态留在工程里,
下一个读文件的人拿到的是你制造的假bug
正确写法:备份/还原一律用 node,且多步脚本必须 try/finally
const GOOD = fs.readFileSync(F, 'utf8');
try { fs.writeFileSync(F, BAD, 'utf8'); /* 注入 + 验证 */ }
finally { fs.writeFileSync(F, GOOD, 'utf8'); } // 无论成败都还原
判据:任何"改坏→验证→改回"的流程,只要还原步骤依赖 shell 命令,就不许执行。
把还原写进脚本的 finally,让正确性不依赖调用方的命令行。
<!-- evolve:env-node-e-backslash -->
node -e "..." 用双引号包时,反斜杠会被 shell 吞掉
在 Git Bash 里 node -e "...\s..." 中的 \s 会变成 /s,正则当场语法错误:
node -e "const s=fs.readFileSync(p,'utf8'); /title:\s*([^\n]*)/.test(s)"
// → SyntaxError: Invalid or unexpected token
// → Invalid regular expression: /d+)rpx/ (\d 丢了)
改用单引号外壳(node -e '...')可以缓解,但最稳的是直接把脚本写成文件再执行——
尤其当脚本里既有反斜杠又有中文路径时。
同理,Windows 命令参数会被路径转换破坏,
taskkill //PID / tasklist //FI 这类要加 MSYS_NO_PATHCONV=1。
<!-- evolve:env-edit-batch -->
Edit 批量编辑只有最后一个落盘,Read 还可能返回陈旧缓存
一条消息里发多个 Edit 调用:全部回显 "Successfully edited",但只有最后一个真正生效。
更糟的是 Edit 之后立即 Read/Grep 复查,可能读到陈旧缓存(显示未改),
于是误判成"没改成"又重复编辑,把文件改乱。
唯一可信的验证:绕过工具缓存直读磁盘
node -e "console.log(fs.readFileSync(path,'utf8'))"
纪律:改文件一次一个 Edit + 读盘复验,绝不依赖工具回显的 success 判断成败。
<!-- evolve:env-leak-scan-fp -->
私有信息泄漏扫描必配三件套,否则全是假阳性
给技能 / 仓库做「有没有泄漏本机路径、用户名、项目名」扫描时,朴素正则会全线假阳性。三件套缺一不可:
- LEAK_ALLOW 白名单:
Program Files、Program Files (x86)、System32下的系统工具、
厂商开发工具的cli.bat—— 这类系统/厂商约定的安装位置是文档化接口,
写进文档不算泄漏。不做白名单时初版扫出 12 处,全是这类。 - 盘符正则要加负向后顾:写成
(?<![A-Za-z0-9_])[A-Za-z]:\\...,
否则 CSS / 正则里的height:\s、font:\s会被误当成盘符路径。 - SELF_EXEMPT 自指豁免:扫描脚本自身的源码里必然写着它要扫的黑名单正则
(项目名 / 卷符),必然命中自己。把publish_check.js、_selfcheck.js列入豁免。
收敛效果:12 处 → 1 处 → 0 处。
判定原则:宁可变着法子追假阴性(真泄漏),也不要让假阳性淹没输出——
一旦输出里全是无关项,真泄漏就没人看了。
<!-- evolve:env-readdir-subdir -->
readdirSync 的结果喂给文件读 API 前,必须先判 isFile()
给 scripts/ 新增 checks/ 子目录后,_selfcheck.js 直接在启动阶段崩掉:
Error: EISDIR: illegal operation on a directory, read '...\scripts\checks'
原因:fs.readdirSync(dir) 返回的是名字数组,混着目录名;直接拿去 readFileSync 就 EISDIR。
当目录里恰好全是文件时完全不会暴露——典型的「加了个子目录才炸」。
正确写法:
const files = fs.readdirSync(dir, { withFileTypes: true })
.filter(e => e.isFile()).map(e => e.name);
推广:任何 readdirSync 结果在喂给 readFileSync / statSync(内容) 之前都要先判 isFile()。
这条在扫描第三方目录时尤其重要——别人的目录里有什么,你不该做假设。
<!-- evolve:env-devtools-9420-port -->
微信开发者工具 9420 自动化服务端口默认关闭(⚠️ 标题已收窄,见下方纠错)
截图轨依赖 miniprogram-automator attach 微信开发者工具的自动化服务端口(默认 9420)。该端口在默认安装下是关闭的,只有 IDE server(如 31932)监听,导致 node shoot.js 连接失败、截图轨无法闭环。
解法:用户在微信开发者工具 → 设置 → 安全设置 → 勾选「开启服务端口」,重启 CLI/IDE 后端口才监听。
兜底(铁律九):9420 仍不可达时,截图轨不得伪造结论,必须切静态量化轨(代码可复算 + ux-audit/score360),并在报告里明示根因与待闭环项。
> 来源:某 34 页小程序实战自测(9420CLOSED / 31932 OPEN)。
> ⚠️ 纠错(后补):「默认关闭」只是一种成因,不是通例。
> 实战复盘发现同一天里 9420 先是活着(attach 跑通 34/34)、后变 dead(冷启动撞 reg.exe 被拦),
> 用户据此得出"昨晚能跑今天不行"。真实变量是端口状态,不是策略变严。
> 所以排查时先测端口(netstat -ano | grep 9420 + tasklist 查 wechatdevtools.exe),
> 再决定往「开服务端口」还是「reg.exe 被拦」哪条线查——顺序反了会白折腾。
> 详见 env-reg-blocked 的「有没有已经开着的自动化实例」。
<!-- evolve:env-automator-bridge-dead -->
坑:端口开了、也连上了,但 DevTools 对任何命令都不回响应
上面那条只是「端口没开」。实战里还遇到过更隐蔽的一层,特征极具辨识度:
| 阶段 | 现象 |
|---|---|
cli auto --auto-port 9420 |
实例正常拉起,automator.connect() 握手成功(ws 连上,无报错) |
| 之后任何命令 | currentPage() / switchTab() 一律 timeout waiting for automator response,每次精确卡在 ~10s |
currentPage() 是最基础的探活命令,它都超时 ⇒ 命令桥接没响应。
> ⚠️ 不要急着判"协议不兼容"——本技能曾据此误判过一次,代价是 falsely 关闭整条渲染轨。
> miniprogram-automator 停在 0.12.1 已多年、社区早停更,但它和当前 DevTools 依然完全兼容:
> 实测同一台机、同一工程,shoot.js 一次跑通 34/34 页截图(warmup 仅 9s)。
> 协议不兼容是伪结论。真实原因几乎总是「你重写驱动脚本时,把已验证的那条链路改坏了」。
先做这三件事,再谈别的:
- 优先复用已验证脚本,不要重写驱动。 若工程内已有跑通过的截图脚本(常见于
tests/.automator/shoot.js、<项目>/tests/.screenshots/manifest.json),
原样再跑一遍。本例中:磁盘上存着 34 页截图 + manifest 记录ok=34, fail=0,
一眼就能证明"这条路本来能走",比重新推导快 10 倍。 - 查历史产物,别只查历史记忆。
conversation_search常搜不到细节(它按语义排序,
会优先返回"项目总览"类摘要而漏掉具体命令)。直接扫磁盘:
找manifest.json/*.png截图目录,看mtime确认"上次成功是什么时候",
再从同目录的.js反读出当时真正用的命令。 - 对比驱动脚本的差异,重点看这 4 处(本例的破口都在这里):
| 环节 | 坏写法(本例) | 好写法(已验证) |
|---|---|---|
| 依赖加载 |require('miniprogram-automator')裸require,解析不到 | 设NODE_PATH指向tests/.automator/node_modules|
| 预热 | 干等sleep后直接导航 |currentPage()+switchTab首页轮询到就绪(配WARMUP_MAX上限) |
| 每条命令 | 无超时 → 冷启动期整进程挂死 |withTimeout(promise, ms, tag)逐条兜底 |
| 页面栈 |navigateTo累积 → 撞 10 层上限 | 非 tabBar 页一律redirectTo(栈恒 1~2 层) |
两个仍然成立的真·排除项(可放心写进报告):
- 不是「首次编译慢」:探活拉到 15 次、覆盖 3 分钟以上仍每次精确 10s 超时。
- 不是「工程已在别处打开」:复制到另一路径再拉起,照样同样行为。
- 端口占用清理用 Get-NetTCPConnection -LocalPort 9420 + Stop-Process -Force
(别用 MSYS_NO_PATHCONV=1 taskkill 这种 env 前缀写法,Git Bash shim 下会被当成命令而失败)。
判定纪律:只有当「原样重跑已验证脚本」也失败、且上述 4 处差异都排除后,
才可写"工具链限制";且必须明示"该结论来自排除法,非直接证据"。
> 来源:某 34 页小程序渲染轨攻坚。先误判为协议不兼容,后经扫磁盘发现历史 34/34 成功产物,
> 原样复用 tests/.automator/shoot.js 一次跑通(该工程 shoot.js 头部注释完整记录了
> v1 卡死 / v2 参数错误 / v3 才跑通的三代踩坑,是极好的参考样本)。
<!-- evolve:env-page-json-component-path -->
⛔ 页面 json 的 usingComponents 必须用绝对路径,否则编译期整包挂起
这是本技能目前唯一一个"一个字符错、全项目打不开"的坑,且静态门禁极易漏过。
// ❌ 致命:微信把「页面 json」的组件路径解析为相对于【页面目录】
"tool-reco": "components/tool-reco/tool-reco"
// → 报 pages/redcap/components/tool-reco/tool-reco 路径下未找到组件
// → 且该错误在【编译期】抛出,整包挂起,不是单页降级
// ✅ 正确:前导斜杠 = 相对小程序根
"tool-reco": "/components/tool-reco/tool-reco"
为什么容易踩:app.json 的 usingComponents 里写 "components/xxx/xxx"(无 /)是对的
(它相对 root);但页面 json 复用这个写法就炸。抄模板时极易只抄值不抄 /。
变体:"../../components/xxx/xxx"(上跳)能解析成功,不会报错,
但与全站风格不一致、且 miniprogram_npm 复制后易失效——属技术债,也要改成 /。
门禁必须查这一项(静态可判、零成本、失败即挂包):
for (const [tag, val] of Object.entries(j.usingComponents || {})) {
if (!val.startsWith('/')) { E(`[组件] ${p}.json ${tag} 用相对路径「${val}」→ 会被解析到 pages/<page>/${val},请改为「/${val}」`); continue; }
const abs = path.join(ROOT, val);
const hit = fs.existsSync(abs + '.js') || fs.existsSync(abs + '.json') ||
(fs.existsSync(abs) && fs.existsSync(path.join(abs, 'index.js')));
if (!hit) E(`[组件] ${p}.json ${tag} 指向「${val}」但文件不存在`);
}
报错时务必把「会被解析到哪个错误路径」原文打出来,省掉一轮猜。
配套:新增页面 json 一律复制既有页面 json 当模板,不要手写。
<!-- evolve:env-custom-tag-native-fp -->
扫「自定义组件是否注册」时,含 - 的原生组件会满屏假报警
校验"wxml 里用到的自定义标签是否注册"时,正则常写成:
/<([a-z][a-zA-Z0-9]*-[a-zA-Z0-9-]+)[\s\/>]/g// 抓 <tool-reco> 这类
但微信原生组件大量含 -:scroll-view / movable-view / cover-view /
cover-image / page-meta / root-portal / match-media …… 全都会被捕获,
然后报一堆"未注册"。实测 74 处引用里 4 处是 scroll-view 误报。
必须维护 NATIVE 白名单(或反过来:维护"已注册组件"全集,只查不在其中的标签)。
判据:标签在 app.json 全局 usingComponents 或本页 json 里注册过 → 放行;
否则若属于原生集合 → 放行;都不满足才报。
<!-- evolve:env-gate-sensitivity -->
⛔ 门禁新增检查后,必须注入已知故障证明它真能拦住
写完一条新门禁,跑一遍绿了什么都不证明——可能它压根没生效、路径写错、
或者被try/catch 吞了。本项目一度有 7 项检查从未验证过灵敏度。
标准做法:写一次性脚本,注入 3 类典型故障 → 断言全部被拦(退出码非 0 + 报错行命中)
→ finally 还原 → 再断言回到干净态(退出码 0)。
const GOOD = fs.readFileSync(F, 'utf8');
const CASES = [{ name, bad: GOOD.replace(...) }, /* 相对路径 / 路径不存在 / 上跳路径 */];
try {
for (const c of CASES) {
if (c.bad === GOOD) { console.log('[SKIP] 替换未命中,用例本身失效'); fail++; continue; }
fs.writeFileSync(F, c.bad, 'utf8');
const r = runGate();
const caught = r.code !== 0 && /\[组件\]/.test(r.out);
...
}
} finally { fs.writeFileSync(F, GOOD, 'utf8'); } // 见env-shim-no-cp-dirty-repo
三条纪律:
1. 替换必须校验生效:if (c.bad === GOOD) 说明替换没命中,用例是假的 → 判FAIL 而非 PASS
2. 判据是"退出码 + 报错行标记",不是文本里有没有"失败"二字(见铁律十四)
3. 还原写进 finally,绝不依赖命令行(shim 无 cp,见上)
> 参考实现:_tools/_verify_component_path_gate.js(3 用例全 PASS,覆盖相对路径 /
> 绝对路径不存在 / 上跳路径三类)。
支付宝 Machine Pay 按量付费改造(HTTP 402 / A2M)
> 面向「想把本技能的能力按次/按量收费」的使用者。完整规范见 references/machine-pay-402.md,
> 本节只给决策所需的最小信息。
⛔ 先认清架构事实
本技能是纯本地 Node 脚本,本身不是支付服务端。 402 协议要求存在一个
「可被 AI Agent 访问的 HTTP 端点 + 持有商户私钥」的服务端;
shoot.js / score360.js 跑在用户机器上,不能也不应该自己变成收单端。
| 你想要的 | 用什么 | 需要服务端吗 |
|---|---|---|
| 按次/按量收费(每次调用扣一次钱) | Machine Pay(402)——本节内容 | 需要 |
| 按月/按年/按席位持续收费 | 支付宝 AI 订阅(Token Pay) | 需要 |
| 一次性买断/固定套餐 | 固定套餐即可 | 可选 |
不想部署服务端 → 用订阅或固定套餐,不需要本节这套东西。
五项链路(缺一即为不合格)
| # | 环节 | 实现 | 不做会怎样 |
|---|---|---|---|
| ① | 402 账单下发 | Seller.issue402() |
Agent 无从得知价格,直接失败 |
| ② | 携带凭证重试 | Seller.parseProof() |
付款后无法继续调资源 |
| ③ | 验付调用 | Seller.verify() / verifyAndCheck() |
凭客户端显示就发内容 = 资损 |
| ④ | 履约确认(ack) | Seller.confirm() |
平台无从对账,订单挂账 |
| ⑤ | 订单持久化与幂等 | Ledger |
重试造成重复计量 / 重复履约 |
⛔ 普通下单、异步通知、主动查单不能替代本协议。
⛔ 客户端回跳页、App 回跳、Agent 端"支付成功"展示都不是履约依据。
三条最容易造成资损的错误
- 客户端显示成功就发内容 —— 客户端可伪造,必须服务端
verify且active===true - 只看
code===10000就履约 —— 还要核amount/out_trade_no/resource_id
/trade_no是否重复,五项全过才发内容 - 同一凭证允许重试多次 —— 同一
trade_no只能履约一次
用法
# 离线自检(不需要任何凭证,72 项)
node scripts/pay/verify_402.js # 56 项:五项链路 + 幂等 + 失败不计费
node scripts/pay/verify_server.js # 16 项:真起服务的端到端行为
# 起服务(私钥等凭证只从环境变量注入,缺任一项直接启动失败)
node scripts/pay/_smoke_run.js --auto # 无凭证也能自证工具可用(假凭证打真网关)
node scripts/pay/server.js
curl -i http://127.0.0.1:8787/report/360 # → 402 + Payment-Needed
⛔ 离线自检只证明协议逻辑写对了,不能证明支付宝侧受理通过。
上线前必须用沙箱凭证跑完 6 类场景:支付成功 / 用户取消 / 接口超时 / 重复提交 / 验签失败 / 退款。
⛔ 生产私钥严禁写入客户端、明文提交仓库或打进技能包。
分层定价建议
| 层 | 内容 | 建议 |
|---|---|---|
| 免费 | 静态量化轨、20 轴门禁、12 角色 prompt、清单生成、360 评分、组件路径门禁 | 0(跑出真实结论 → 自发传播) |
| 付费 | DevTools 真实渲染轨、自进化引擎、按次生成体检报告 | 静态审计计量困难 → 订阅;渲染轨有真实算力开销 → 按次 |
依据官方口径:只能估算用量、无法留下可追溯计量记录时,不建议直接按量收费。
参考文档
references/roles-index.md—— 12 角色总表、上线前标准套餐顺序、防敷衍条款、能力总纲指针references/roles-ux.md—— 角色 1:UX/UI 评审专家(六维度 + 四轮流程 + 输出模板 + 高频 P0)references/roles-test.md—— 角色 2–4:测试架构师 / 用例生成 / 模糊破坏性测试references/roles-audit.md—— 角色 5–7:代码审计 / 隐私合规 / 性能兼容references/roles-launch.md—— 角色 8–12:审核员 / 恶意用户 / 测试开发 / 发布经理 / 缺陷分析references/capability-spec.md—— 量化能力总纲:性能/隐私/类目/提审策略/工具/数据度量references/design-guidelines.md—— UX 设计判据基线:令牌 + 27 条体验评分权重 + 暗黑模式 + 避坑references/scoring-360.md—— 360 度评分框架:五维度权重 + 各平台对比 + 30 分钟快评清单(被score360.js固化)references/review-playbook.md—— 双轨审阅协议、评审报告模板、严重级别定义、评分维度references/render-track-playbook.md—— 渲染轨实战手册:四步打通流程(含 4 个必备要素)、缺陷模式库(大数字被劈/说明贴边/标签列窄/单位掉行)、逐页目视审阅顺序与每页必看 5 点、盒模型探针用法、报告口径纪律references/competitor-benchmark.md—— 竞品对照方法(首屏优先级、联动下拉、典型值位置)references/machine-pay-402.md—— 支付宝 Machine Pay(HTTP 402)按量付费接入指南:架构事实(本地脚本 vs 支付服务端)、五项链路对照表、402 账单字段与 RSA2 签名规则、Payment-Proof 解析、验付五道业务核对、履约回执、订单持久化与幂等、环境变量与网关地址、6 类上线必跑场景、常见错误认知、分层定价建议