Files
ql-scripts/_probe_meta.js
zhuhjay 87debf37a9 chore(meta): 三个脚本统一青龙元数据注解,新增 _probe_meta.js 自检
继 3531437 修好 qqmusic 之后,把同一问题在另两个脚本上一并清掉 ——
它们原本都没写 name 注解(任务名会退回文件名),且 jd 脚本的 cron 写在块注释里
用了 */ 转义,青龙会把反斜杠一起提取出去。

改动(三个脚本统一成同一形态:文件最前面两行行注释):
  // cron: <表达式>
  // name: <中文任务名>

- jd/scrip_moving_env_enhance.js: 补 name 注解;cron 从块注释移到行注释
  (原来写的 \*/ 转义会产出非法表达式)
- xiequ/scrip_switch.js: 补 name 注解;正文两处日志里的「name: xxx」改成「账号 xxx」
  (青龙的任务名正则不锚定行首、取全文第一个命中,正文命中会盖掉注解)
- qqmusic/scrip_qqmusic_checkin.js: 注解从块注释改成行注释,与另两个统一
- 新增 _probe_meta.js: 扫描所有 scrip_*.js,复刻旧版(/name:\s*(.+)/ 不锚定)
  与新版(cli/src/internal/subscription 的 cronMetadata)两条提取逻辑,
  断言任务名不是代码片段、cron 是合法表达式。不带 scrip_ 前缀,订阅不会拉取

验证:_probe_meta.js RESULT: PASS(3 个文件,两个版本青龙提取都正确)/
三个脚本 node --check 全过 / qqmusic 签名对拍 6 组 PASS / 空 env 优雅跳过
2026-11-04 21:22:38 +08:00

107 lines
4.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* _probe_meta.js —— 校验所有 scrip_*.js 的青龙元数据注解是否会被正确提取
*
* 青龙订阅勾「自动添加定时任务」时,从脚本正文正则提取任务名与 cron:
* 任务名: /name:\s*(.+)/ 不锚定行首,取全文第一个命中
* cron: /\bcron:\s*([^\r\n]+)/ 不锚定行首,取全文第一个命中
* 两条都抓不到时:任务名 = 文件名,cron = 随机 M H * * *
*
* 用法: node _probe_meta.js
* 退出码 0 = 全部通过,1 = 有文件不合格
*/
'use strict'
const fs = require('fs')
const path = require('path')
const ROOT = __dirname
/** 递归找所有 scrip_*.js(跳过 node_modules / .git) */
function findScripts(dir, out = []) {
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
if (ent.name === 'node_modules' || ent.name === '.git') continue
const p = path.join(dir, ent.name)
if (ent.isDirectory()) findScripts(p, out)
else if (/^scrip_.*\.js$/.test(ent.name)) out.push(p)
}
return out
}
/** 复刻青龙旧版提取(用户实际部署的版本,2026-11-04 真机证实) */
function extractLegacy(contents, filename) {
const name = contents.match(/name:\s*(.+)/)?.[1]?.trim()
const cron = contents.match(/\bcron:\s*([^\r\n]+)/)?.[1]?.replace(/["',]+$/, '').trim()
return { name, cron, fallbackName: path.basename(filename) }
}
/** 复刻青龙新版 cli/src/internal/subscription/subscriptionRunner.ts 的 cronMetadata */
function extractCurrent(contents, filename) {
const name = contents.match(/new\s+Env\(\s*['"]([^'"\r\n]+)['"]/)?.[1]
|| contents.match(/^\s*(?:#\s*)?name:\s*(.+)$/m)?.[1]?.trim()
|| path.basename(filename)
const explicit = contents.match(/\bcron:\s*([^\r\n]+)/)?.[1]?.replace(/["',]+$/, '').trim()
|| contents.match(/\bcron\s+["']([^"']+)["']/)?.[1]
const matching = contents.split('\n')
.filter(item => item.includes(path.basename(filename)))
.map(item => item.match(/(?:[\d*](?:[\d*/,-]*[\d*])?\s+){4,5}[\d*](?:[\d*/,-]*[\d*])?/)?.[0]?.trim())
.filter(v => !!v)
const cron = [...new Set(matching)].sort()[0] || explicit
return { name, cron }
}
/** cron 表达式合法性:5 或 6 段,每段只允许数字与 * , - / */
const CRON_RE = /^[\d*][\d*,/-]*(\s+[\d*][\d*,/-]*){4,5}$/
const isValidCron = (c) => typeof c === 'string' && CRON_RE.test(c.trim())
let fails = 0
const scripts = findScripts(ROOT).sort()
console.log(`扫描到 ${scripts.length} 个 scrip_*.js\n`)
for (const file of scripts) {
const rel = path.relative(ROOT, file)
const contents = fs.readFileSync(file, 'utf8')
const legacy = extractLegacy(contents, file)
const current = extractCurrent(contents, file)
console.log(`── ${rel}`)
console.log(` 旧版青龙 name = ${JSON.stringify(legacy.name)}`)
console.log(` 旧版青龙 cron = ${JSON.stringify(legacy.cron)}`)
console.log(` 新版青龙 name = ${JSON.stringify(current.name)}`)
console.log(` 新版青龙 cron = ${JSON.stringify(current.cron)}`)
const problems = []
const warns = []
// 判据以**旧版青龙**为准:那是当前实际部署的版本(2026-11-04 真机证实 ——
// 任务名被建成 "u.hostname," 只可能来自不锚定行首的 /name:\s*(.+)/)。
if (!legacy.name) problems.push('旧版青龙抓不到任务名(会退回文件名)')
if (!legacy.cron) problems.push('旧版青龙抓不到 cron(会落随机 fallback)')
else if (!isValidCron(legacy.cron)) problems.push(`旧版青龙提取的 cron 非法: ${JSON.stringify(legacy.cron)}`)
if (!current.cron) problems.push('新版青龙抓不到 cron(会落随机 fallback)')
else if (!isValidCron(current.cron)) problems.push(`新版青龙提取的 cron 非法: ${JSON.stringify(current.cron)}`)
// 新版青龙的任务名只在两种情况会退回文件名:脚本不用 common.js 的 Env()。
// 那是**可接受的降级**(任务照样建、照样跑,只是名字是文件名),故只告警不判失败。
if (current.name === path.basename(file)) {
warns.push('新版青龙会退回文件名作任务名(本脚本不用 common.js 的 Env(),属可接受降级)')
}
// 任务名不该是代码片段(含点、括号、分号、花括号等)。
// 例外:恰好等于文件名的命中是新版青龙的**合法兜底**,不是代码片段误抓。
for (const [label, n] of [['旧版', legacy.name], ['新版', current.name]]) {
if (!n || n === path.basename(file)) continue
if (/[{}();=\[\]]|\.\w/.test(n)) problems.push(`${label}青龙抓到的任务名像代码片段: ${JSON.stringify(n)}`)
}
if (problems.length) {
fails++
problems.forEach(p => console.log(` ✗ ${p}`))
} else {
console.log(' ✓ 元数据提取正确')
}
warns.forEach(w => console.log(` ! ${w}`))
console.log('')
}
console.log(fails === 0 ? `RESULT: PASS(${scripts.length} 个文件)` : `RESULT: FAIL(${fails} 个文件不合格)`)
process.exit(fails === 0 ? 0 : 1)