From 87debf37a97769886a93c445712c67e3c8659e23 Mon Sep 17 00:00:00 2001 From: zhuhjay Date: Wed, 4 Nov 2026 21:22:38 +0800 Subject: [PATCH] =?UTF-8?q?chore(meta):=20=E4=B8=89=E4=B8=AA=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E7=BB=9F=E4=B8=80=E9=9D=92=E9=BE=99=E5=85=83=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=B3=A8=E8=A7=A3=EF=BC=8C=E6=96=B0=E5=A2=9E=20=5Fpro?= =?UTF-8?q?be=5Fmeta.js=20=E8=87=AA=E6=A3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 继 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 优雅跳过 --- README.md | 32 ++++++++++ _probe_meta.js | 106 +++++++++++++++++++++++++++++++ jd/scrip_moving_env_enhance.js | 12 +++- qqmusic/scrip_qqmusic_checkin.js | 13 ++-- xiequ/scrip_switch.js | 12 ++-- 5 files changed, 160 insertions(+), 15 deletions(-) create mode 100644 _probe_meta.js diff --git a/README.md b/README.md index f0bdc5b..cddd322 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,38 @@ ql repo https://gitea.zhuhjay.cn/zhuhjay/ql-scripts.git "scrip_" "" "sendNotify|common" "js" ``` +## 青龙自动建任务的元数据约定(必读) + +订阅勾了「自动添加定时任务」时,青龙从**脚本正文**正则提取任务名与 cron。 +两条正则都**不锚定行首、取全文第一个命中**(2026-11-04 真机踩坑实录): + +| 项 | 青龙的提取规则 | 必须怎么写 | +|---|---|---| +| **任务名** | `/name:\s*(.+)/` | 文件最前面写一行 `// name: 中文任务名`。**全文任何位置都不能再出现「name + 冒号」的连写** —— `https.request({ hostname: u.hostname })` 会被抓成任务名 `u.hostname,`(已复现) | +| **cron** | `/\bcron:\s*([^\r\n]+)/`,**必须带冒号** | 写 `// cron: 1 0,6 * * *`,**独占一行、后面不跟任何注释**(同一行剩余文字会被一起吞进 schedule) | + +两个都抓不到时的兜底:任务名 = 文件名;cron = 随机 `M H * * *`(这就是那次任务被建成 `18 21 * * *` 的原因)。 + +**自检**: + +```bash +node _probe_meta.js # 扫描所有 scrip_*.js,复刻两个版本青龙的正则,验证提取结果 +``` + +`_probe_meta.js` 是开发期工具,不带 `scrip_` 前缀,不会被订阅拉取。 +新增脚本后跑一次,全 PASS 再提交。 + +### 各脚本的注解写法 + +```js +// cron: <表达式> +// name: <中文任务名> +/** 说明文字写这里,注意别写 "name:" 连写 */ +``` + +用**行注释**而不是块注释:块注释里 cron 的 `*/` 会提前闭合注释,必须写成 `*\/`, +而青龙会把反斜杠一起提取出去导致 cron 非法。 + ## 脚本说明 ### 使用方法 diff --git a/_probe_meta.js b/_probe_meta.js new file mode 100644 index 0000000..d8dc369 --- /dev/null +++ b/_probe_meta.js @@ -0,0 +1,106 @@ +#!/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) diff --git a/jd/scrip_moving_env_enhance.js b/jd/scrip_moving_env_enhance.js index c2179a7..900b252 100644 --- a/jd/scrip_moving_env_enhance.js +++ b/jd/scrip_moving_env_enhance.js @@ -1,10 +1,16 @@ +// cron: 59 23 */3 * * +// name: 京东Cookie辅助脚本 /** - * cron: 59 23 *\/3 * * - * * 京东Cookie辅助脚本(将JD CK进行重排序 保证所有账号都有助力与被助力的机会) + * + * 上面两行是青龙订阅自动建任务用的元数据注解,格式约定见仓库根 README: + * - 必须用行注释写在文件最前面(行首),不要塞进块注释 —— 块注释里 cron 的 + * 「星号斜杠」会被迫转义,青龙会把反斜杠一起提取出去。 + * - 全文不要出现第二处「name 加冒号」的连写:青龙的提取正则不锚定行首, + * 取的是全文第一个命中。 */ const { Env } = require('../common'); -const $ = new Env('京东Cookie辅助脚本(将JD CK进行重排序)'); +const $ = new Env('京东Cookie辅助脚本'); const fs = require('fs').promises; const fetch = require('node-fetch'); const envName = 'JD_COOKIE'; diff --git a/qqmusic/scrip_qqmusic_checkin.js b/qqmusic/scrip_qqmusic_checkin.js index 159f4f2..b09ac49 100644 --- a/qqmusic/scrip_qqmusic_checkin.js +++ b/qqmusic/scrip_qqmusic_checkin.js @@ -1,8 +1,11 @@ #!/usr/bin/env node +// cron: 1 0,6 * * * +// name: QQ音乐自动签到 /** - * name: QQ音乐自动签到 + * QQ音乐会员成长值 + 会员福利站 自动签到(青龙 Node 零依赖) * - * scrip_qqmusic_checkin.js —— QQ音乐会员成长值 + 会员福利站 自动签到(青龙 Node 零依赖) + * 上面两行是青龙订阅自动建任务用的元数据注解,格式约定见仓库根 README。 + * 定时说明: 00:01 + 06:02 双轮(任务20跨天滚动 + 福利站翻新兜底)。 * * 文件名前缀 scrip_ 是订阅白名单要求(ql repo 的 whitelist 参数 = "scrip_"), * 不带前缀的文件不会被青龙订阅拉取。 @@ -21,12 +24,6 @@ * QM_CACHE_DIR key 缓存目录覆盖(默认规则见上) * QM_PLAY_REPORT 任务20播放上报开关(默认 off;伪造统计数据属中风险, * 且需播放 XML 模板,本版未实现,仅挂 StartTask 标记) - * - * cron: 1 0,6 * * * - * - * 定时说明: 00:01 + 06:02 双轮(任务20跨天滚动 + 福利站翻新兜底)。 - * cron 注解必须**独占一行且后面不跟注释** —— 青龙的提取正则 - * /\bcron:\s*([^\r\n]+)/ 会把同一行剩下的文字一起吞进去,产出的 schedule 非法。 */ 'use strict' const fs = require('fs') diff --git a/xiequ/scrip_switch.js b/xiequ/scrip_switch.js index 00ab061..c30b8a5 100644 --- a/xiequ/scrip_switch.js +++ b/xiequ/scrip_switch.js @@ -1,7 +1,11 @@ +// cron: 38 2,8,14,20 * * * +// name: 携趣白名单切换 /** - * cron: 38 2,8,14,20 * * * - * * 携趣白名单切换(多个携趣,多个IP,多个IP选中一个合适的携趣进行切换) + * + * 上面两行是青龙订阅自动建任务用的元数据注解,格式约定见仓库根 README。 + * 本文件已把正文日志里的「name 加冒号」字样改成「账号」,避免青龙的提取正则 + * (不锚定行首、取全文第一个命中)抓到正文。 */ const { Env } = require('../common'); const $ = new Env('携趣小助手-白名单切换'); @@ -62,7 +66,7 @@ const apiTpl = { for (let tmpElement of tmp) { if (tmpElement?.res) { await fetch(apiTpl.delAll(tmpElement.info)); - $.log(`删除 name: ${tmpElement.info.comment} 下的所有白名单 -> `, tmpElement.res); + $.log(`删除账号 ${tmpElement.info.comment} 下的所有白名单 -> `, tmpElement.res); } } @@ -73,7 +77,7 @@ const apiTpl = { if (change) { for (const ip of ips) { await fetch(apiTpl.add(change.info, ip)); - $.log(`添加 name: ${change.info.comment} 白名单`); + $.log(`添加账号 ${change.info.comment} 白名单`); } } })()