fix(qqmusic): 通知改为脚本自实现,断开对青龙 sendNotify 的依赖
真机定位(2026-11-07):仓库里的 sendNotify.js 每轮都被覆盖,改它不生效。
青龙 shell/update.sh 的 gen_list_repo 复制顺序是三步,后一步盖前一步:
407: cp -f $file_notify_js "${dir_scripts}/${uniq_path}" 面板自带的 sendNotify.js
415: cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}" 仓库的(覆盖①)
421: cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" /ql/data/deps/*(再覆盖②)
而 share.sh:228 在初始化时把面板自带的 sample/notify.js 拷成了 /ql/data/deps/sendNotify.js。
③ 排在最后 → 最终文件永远是面板自带那份,204 修复永远不生效。
改为脚本内联实现 notify(),零依赖(只用内置 http/https):
- httpPost / parseHookHeaders / buildHookBody / notify 四个函数,约 90 行
- 2xx 一律算成功(204 不再误判)
- buildHookBody 对 application/json 先试 JSON.parse,不合法就 JSON.stringify 重建,
内容里带引号/换行/反斜杠都安全(青龙 parseString 那个坑绕开了)
- 双通道:webhook + pushplus 直发,任一路成功即 ok
- 两路都没配时打一行明确提示,不再静默
main() 里删掉 require('../sendNotify'),改成 await notify('QQ音乐签到', desp)。
第四参 author 的追加逻辑也一并没了,正文不再带「本通知 By:github.com/whyour/qinglong」。
新增 test_notify.js:38 条断言
- parseHookHeaders 八种输入(含字面量 \n 不切开)
- buildHookBody 三种特殊字符(双引号/反斜杠/制表符)
- notify 六种状态码(200/204/201 通过,401/403/400/429 失败)
- 无配置、双通道部分失败
端到端已验证:mock 服务收到的 content 与原始 desp 逐字相同(10 行完整),
title 正确,header 带 token,HTTP 204 判为成功。
README 重写「通知发不出去怎么查」:坑 0 改成 deps 覆盖(附验证命令),
坑 1-3 保留但标注为「若又接回 sendNotify 才需注意」,新增 pushplus 展示限制的
三种官方解法与代价对照。
This commit is contained in:
170
qqmusic/test_notify.js
Normal file
170
qqmusic/test_notify.js
Normal file
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* test_notify.js —— 验证脚本自带的 notify() 实现(不依赖青龙 sendNotify)
|
||||
*
|
||||
* 覆盖:
|
||||
* 1. parseHookHeaders —— 多行 / 空行 / 无冒号 / 重复 key
|
||||
* 2. buildHookBody —— JSON 内容含引号换行时的转义、urlencoded、纯文本
|
||||
* 3. notify —— mock 服务测 200/204/401/403/400/429 六种状态码
|
||||
* 4. 无配置 —— 两路都没配时的行为
|
||||
*
|
||||
* 用法: node qqmusic/test_notify.js
|
||||
* 退出码 0 = 全过
|
||||
*/
|
||||
'use strict'
|
||||
const http = require('http')
|
||||
|
||||
let pass = 0, fail = 0
|
||||
const ok = (name, cond, extra = '') => {
|
||||
if (cond) { pass++; console.log(` ✓ ${name}`) }
|
||||
else { fail++; console.log(` ✗ ${name}${extra ? ' — ' + extra : ''}`) }
|
||||
}
|
||||
const eq = (name, got, want) => ok(name, got === want, `got=${JSON.stringify(got)} want=${JSON.stringify(want)}`)
|
||||
|
||||
/* ══════════ 1-2. 纯函数(不需要 env) ══════════ */
|
||||
const { buildHookBody, parseHookHeaders } = require('./scrip_qqmusic_checkin.js')
|
||||
|
||||
console.log('\n── 1. parseHookHeaders ──')
|
||||
eq('单行', JSON.stringify(parseHookHeaders('X-A: 1')), '{"X-A":"1"}')
|
||||
eq('两行', JSON.stringify(parseHookHeaders('X-A: 1\nX-B: 2')), '{"X-A":"1","X-B":"2"}')
|
||||
eq('空行忽略', JSON.stringify(parseHookHeaders('X-A: 1\n\n\nX-B: 2')), '{"X-A":"1","X-B":"2"}')
|
||||
eq('无冒号忽略', JSON.stringify(parseHookHeaders('garbage\nX-A: 1')), '{"X-A":"1"}')
|
||||
eq('值含冒号', JSON.stringify(parseHookHeaders('X-A: a:b:c')), '{"X-A":"a:b:c"}')
|
||||
eq('空串', JSON.stringify(parseHookHeaders('')), '{}')
|
||||
eq('值前后空格被 trim', JSON.stringify(parseHookHeaders(' X-A : v ')), '{"X-A":"v"}')
|
||||
// 字面量 \n(两个字符)不该被切开 —— 这正是青龙 sendNotify 的坑
|
||||
eq('字面量 \\n 不切开', Object.keys(parseHookHeaders('X-A: 1\\nX-B: 2')).length, 1)
|
||||
|
||||
console.log('\n── 2. buildHookBody(默认 JSON 模板)──')
|
||||
{
|
||||
const b = buildHookBody('标题', '第一行\n第二行')
|
||||
let j = null
|
||||
try { j = JSON.parse(b) } catch { /* 下面判 */ }
|
||||
ok('产出是合法 JSON', j !== null, b)
|
||||
eq('title 正确', j && j.title, '标题')
|
||||
eq('content 换行保留', j && j.content, '第一行\n第二行')
|
||||
}
|
||||
{
|
||||
// 内容里带引号 —— 占位替换后必须仍然合法
|
||||
const b = buildHookBody('T', '他说"你好"然后走了')
|
||||
let j = null
|
||||
try { j = JSON.parse(b) } catch { /* */ }
|
||||
ok('内容含双引号仍是合法 JSON', j !== null, b)
|
||||
eq('引号原样保留', j && j.content, '他说"你好"然后走了')
|
||||
}
|
||||
{
|
||||
// 内容里带反斜杠与制表符
|
||||
const b = buildHookBody('T', 'C:\\path\\to\tfile')
|
||||
let j = null
|
||||
try { j = JSON.parse(b) } catch { /* */ }
|
||||
ok('含反斜杠/制表符仍合法', j !== null, b)
|
||||
eq('反斜杠保留', j && j.content, 'C:\\path\\to\tfile')
|
||||
}
|
||||
|
||||
/* ══════════ 3. notify —— mock 服务 ══════════ */
|
||||
console.log('\n── 3. notify(mock 服务,六种状态码)──')
|
||||
|
||||
// 注意:WEBHOOK_URL 等是模块加载时读的常量,所以要改 env 后重新 require
|
||||
const MOD = require.resolve('./scrip_qqmusic_checkin.js')
|
||||
function reloadWith(env) {
|
||||
for (const k of ['WEBHOOK_URL', 'WEBHOOK_BODY', 'WEBHOOK_METHOD', 'WEBHOOK_CONTENT_TYPE',
|
||||
'WEBHOOK_HEADERS', 'PUSH_PLUS_TOKEN', 'PUSH_PLUS_USER', 'PUSH_PLUS_TEMPLATE']) delete process.env[k]
|
||||
Object.assign(process.env, env)
|
||||
delete require.cache[MOD]
|
||||
return require(MOD)
|
||||
}
|
||||
|
||||
function startServer(status, body = '') {
|
||||
return new Promise((resolve) => {
|
||||
const seen = []
|
||||
const srv = http.createServer((req, res) => {
|
||||
const chunks = []
|
||||
req.on('data', c => chunks.push(c))
|
||||
req.on('end', () => {
|
||||
seen.push({ method: req.method, url: req.url, headers: req.headers, body: Buffer.concat(chunks).toString('utf8') })
|
||||
res.writeHead(status, { 'Content-Type': 'application/json' })
|
||||
res.end(body)
|
||||
})
|
||||
})
|
||||
srv.listen(0, '127.0.0.1', () => resolve({ srv, port: srv.address().port, seen }))
|
||||
})
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
const cases = [
|
||||
{ status: 204, wantOk: true, label: '204 平台正常返回(★ 旧版误判为失败的就是这个)' },
|
||||
{ status: 200, wantOk: true, label: '200 普通成功' },
|
||||
{ status: 201, wantOk: true, label: '201 也是 2xx' },
|
||||
{ status: 401, wantOk: false, label: '401 token 错' },
|
||||
{ status: 403, wantOk: false, label: '403 IP 不在白名单' },
|
||||
{ status: 400, wantOk: false, label: '400 body 畸形' },
|
||||
{ status: 429, wantOk: false, label: '429 超频' },
|
||||
]
|
||||
|
||||
for (const c of cases) {
|
||||
const { srv, port, seen } = await startServer(c.status)
|
||||
const m = reloadWith({
|
||||
WEBHOOK_URL: `http://127.0.0.1:${port}/api/hooks/ql`,
|
||||
WEBHOOK_METHOD: 'POST',
|
||||
WEBHOOK_CONTENT_TYPE: 'application/json',
|
||||
WEBHOOK_BODY: 'title:$title\ncontent:$content',
|
||||
WEBHOOK_HEADERS: 'X-Sh-Hook-Token: tok123\nX-Sh-Hook-Id: qinglong',
|
||||
})
|
||||
// 静音 notify 内部的 log
|
||||
const origLog = console.log
|
||||
console.log = () => {}
|
||||
const r = await m.notify('QQ音乐签到', '✅ 全部完成\n ✓ 任务中心签到 +15')
|
||||
console.log = origLog
|
||||
|
||||
const wh = r.channels.find(x => x.name === 'webhook')
|
||||
ok(`${c.status} → ${c.wantOk ? 'ok' : 'not ok'}(${c.label})`, wh.ok === c.wantOk, JSON.stringify(wh))
|
||||
if (c.status === 204 || c.status === 200) {
|
||||
ok(` 请求确实发出去了`, seen.length === 1, `seen=${seen.length}`)
|
||||
const sent = seen[0]
|
||||
ok(` method = POST`, sent.method === 'POST', sent.method)
|
||||
ok(` header 带 token`, sent.headers['x-sh-hook-token'] === 'tok123', sent.headers['x-sh-hook-token'])
|
||||
let j = null
|
||||
try { j = JSON.parse(sent.body) } catch { /* */ }
|
||||
ok(` body 是合法 JSON`, j !== null, sent.body.slice(0, 120))
|
||||
eq(` title`, j && j.title, 'QQ音乐签到')
|
||||
ok(` content 含明细`, !!(j && j.content && j.content.includes('任务中心签到')), j && j.content)
|
||||
}
|
||||
srv.close()
|
||||
}
|
||||
|
||||
/* 4. 无配置 */
|
||||
console.log('\n── 4. 两路都没配 ──')
|
||||
{
|
||||
const m = reloadWith({})
|
||||
const origLog = console.log
|
||||
const lines = []
|
||||
console.log = (...a) => lines.push(a.join(' '))
|
||||
const r = await m.notify('T', 'C')
|
||||
console.log = origLog
|
||||
ok('ok = false', r.ok === false)
|
||||
ok('提示「未配置任何通道」', lines.some(l => l.includes('未配置任何通道')), lines.join(' | '))
|
||||
}
|
||||
|
||||
/* 5. 只有 webhook 挂、pushplus 通 —— 不应因一路失败而整体失败 */
|
||||
console.log('\n── 5. 双通道:webhook 失败但 pushplus 成功 ──')
|
||||
{
|
||||
const { srv, port } = await startServer(500)
|
||||
const m = reloadWith({
|
||||
WEBHOOK_URL: `http://127.0.0.1:${port}/x`,
|
||||
WEBHOOK_METHOD: 'POST',
|
||||
PUSH_PLUS_TOKEN: 'faketoken_for_shape_only',
|
||||
})
|
||||
// pushplus 会真的发出去 —— 这里只校验 webhook 那一半,避免污染真实账号
|
||||
const origLog = console.log
|
||||
console.log = () => {}
|
||||
const r = await m.notify('T', 'C')
|
||||
console.log = origLog
|
||||
const wh = r.channels.find(x => x.name === 'webhook')
|
||||
ok('webhook 记为失败', wh.ok === false, JSON.stringify(wh))
|
||||
ok('channels 里有两路', r.channels.length === 2, JSON.stringify(r.channels.map(c => c.name)))
|
||||
srv.close()
|
||||
}
|
||||
|
||||
console.log(`\nRESULT: ${fail === 0 ? 'PASS' : `FAIL(${fail})`} (${pass} passed, ${fail} failed)`)
|
||||
process.exit(fail === 0 ? 0 : 1)
|
||||
})()
|
||||
Reference in New Issue
Block a user