继 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 优雅跳过
107 lines
4.9 KiB
JavaScript
107 lines
4.9 KiB
JavaScript
#!/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)
|