Files
ql-scripts/_diag_notify.js
zhuhjay 483bc29424 fix(qqmusic): 通知发送失败不再静默 + 新增通知诊断工具
现象:青龙日志里「自定义发送通知消息失败!!」,pushplus 只收到一行标题。

根因排查(三处,都已实测复现):
1) 脚本里 require('../sendNotify') 是 catch{} 静默吞掉 —— 容器缺 got 时
   一条通知都发不出去且日志无痕,只能靠猜。改成打印一行原因。
2) sendNotify 的 parseString 用 /(\w+):\s*((?:(?!\n\w+:).)*)/g 解析
   WEBHOOK_BODY,靠「换行 + \w+:」断值。单行 JSON 没有换行,整个 body
   被吞掉 —— 实测 '{"title":"$title",...}' 解析结果为 {}(0 个 key)。
   畸形 body → 对方返回非 200 → 正是「自定义发送通知消息失败」的来源。
3) webhookNotify 有三个前置条件,缺一个就静默 resolve(不发也不报错):
   URL/BODY 至少一个含 $title、WEBHOOK_METHOD 必须显式设、body 能被解析。

新增 _diag_notify.js(仓库根,与 sendNotify.js 同级):
- 查 sendNotify 能否加载、通知 env 是否读到(token 脱敏)
- 复刻 parseString,直接告出 WEBHOOK_BODY 会解析成什么
- 直连 pushplus 发 txt 与 html 两条,对比哪个模板正文完整
- 最后走一遍完整 sendNotify,与直连结果对照
用来区分「通道没发」和「微信侧展示折叠」两件事。

验证:node --check / 签名对拍 6 组 PASS / 加载失败时日志文案实测输出
/ _probe_meta 三元数据 PASS
2026-11-05 08:54:26 +08:00

126 lines
5.6 KiB
JavaScript
Raw 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
/**
* QQ音乐签到通知诊断(青龙里跑:cd /ql/data/scripts/ql-scripts && node _diag_notify.js)
*
* 一次跑完查五件事:
* 1. sendNotify 能不能加载
* 2. 通知 env 有没有读到
* 3. webhook 前置条件过不过 + body 会被解析成什么(sendNotify 的 parseString 有坑)
* 4. 直连 pushplus 发两次(txt / html 模板对比),确定是不是模板导致正文被折叠
* 5. 用 sendNotify 走一遍完整流程,对比它和直连的差别
*
* token 一律脱敏。只读配置 + 发测试消息,不改任何东西。
*/
'use strict'
const https = require('https')
const mask = (v, n = 6) => (!v ? '(未设)' : `${v.slice(0, n)}…(len=${v.length})`)
const TITLE = 'QQ音乐签到'
const DESP = [
'✅ QQ音乐签到 · 全部完成',
'',
'🟡 1006764347:成长值 +0',
' ✓ 节目开始标记 已挂',
' △ 领奖·主题装扮 code 30005',
' △ 领奖·收听节目 未计次,待 App 内收听',
' · 无需处理 12 项:任务中心签到、挂件、装扮、听书行为…',
'',
'———',
'轮次: 00:01 / 06:02 双轮',
'时间: ' + new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
].join('\n')
function post(url, body) {
return new Promise((resolve) => {
const u = new URL(url)
const payload = JSON.stringify(body)
const req = https.request({
host: u.hostname, port: u.port || 443, path: u.pathname + u.search, method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
timeout: 15000,
}, (res) => {
const cs = []
res.on('data', c => cs.push(c))
res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(cs).toString('utf8') }))
})
req.on('timeout', () => { req.destroy(); resolve({ status: 0, body: 'TIMEOUT' }) })
req.on('error', e => resolve({ status: 0, body: e.message }))
req.write(payload)
req.end()
})
}
console.log('══════════ 1. sendNotify 加载 ══════════')
let sn = null
try {
sn = require('./sendNotify')
console.log(' ✓ 加载成功')
} catch (e) {
console.log(' ✗ 加载失败:', e.message)
console.log(' → 脚本里 try{require}catch{} 静默吞掉,一条通知都发不出')
}
console.log('\n══════════ 2. 通知 env ══════════')
for (const k of ['PUSH_PLUS_TOKEN', 'PUSH_PLUS_USER', 'WEBHOOK_URL', 'WEBHOOK_BODY',
'WEBHOOK_METHOD', 'WEBHOOK_CONTENT_TYPE', 'WEBHOOK_HEADERS', 'SKIP_PUSH_TITLE']) {
const v = process.env[k]
const sensitive = /TOKEN|URL|BODY|HEADERS/i.test(k)
console.log(` ${v ? '✓' : '·'} ${k.padEnd(20)} ${v ? (sensitive ? mask(v) : v) : '(未设)'}`)
}
console.log('\n══════════ 3. webhook 前置条件 ══════════')
const WU = process.env.WEBHOOK_URL || ''
const WB = process.env.WEBHOOK_BODY || ''
const WM = process.env.WEBHOOK_METHOD || ''
const WCT = process.env.WEBHOOK_CONTENT_TYPE || ''
console.log(' URL 含 $title :', WU.includes('$title'))
console.log(' BODY 含 $title:', WB.includes('$title'))
console.log(' METHOD :', WM || '(空 → 不发请求)')
console.log(' CONTENT_TYPE :', WCT || '(空)')
if (WU.includes('$title') || WB.includes('$title')) {
console.log('\n --- body 解析复刻(sendNotify 的 parseString)---')
const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g
const matches = {}
let m
while ((m = regex.exec(WB)) !== null) { if (!matches[m[1]]) matches[m[1]] = m[2].trim() }
const keys = Object.keys(matches)
console.log(' 解析出的 key:', keys.length ? keys.join(', ') : '(一个都没有)')
if (keys.length === 0 && WB.trim()) {
console.log(' ✗✗ BODY 非空但解析出 0 个 key —— 单行 JSON 会被整段吞掉(已实测)')
console.log(' 这会让发出去的 body 畸形 → 对方返回 4xx → 「自定义发送通知消息失败」')
}
}
console.log('\n══════════ 4. 直连 pushplus(模板对比)══════════')
const PT = process.env.PUSH_PLUS_TOKEN || ''
if (!PT) {
console.log(' · PUSH_PLUS_TOKEN 未设,跳过')
} else {
;(async () => {
for (const tpl of ['txt', 'html']) {
const content = tpl === 'html' ? DESP.replace(/\n/g, '<br>') : DESP
const r = await post('https://www.pushplus.plus/send', {
token: PT, title: `${TITLE}(${tpl} 测试)`, content, template: tpl,
})
let j = {}
try { j = JSON.parse(r.body) } catch { /* 非 JSON */ }
console.log(` [${tpl}] HTTP ${r.status} code=${j.code} msg=${j.msg || ''}`)
console.log(` content 长度=${content.length} 行数=${DESP.split('\n').length}`)
}
console.log('\n → 两条都发到你微信了,对比哪个正文显示完整')
console.log(' → 若 txt 完整、html 只有一行 → pushplus 的 html 模板问题')
console.log(' → 若两条都只有一行 → 微信渠道本身折叠,点「查看详情」才展开')
console.log('\n══════════ 5. 走一遍 sendNotify ══════════')
if (!sn) { console.log(' 跳过(未加载)'); return }
console.log(' 调用 sendNotify("QQ音乐签到", desp)…\n')
await sn.sendNotify(TITLE, DESP)
console.log('\n══════════ 完成 ══════════')
console.log('对照上面 4 的直连结果:')
console.log(' · sendNotify 的 pushplus 也正常 → 两条都收到,问题在展示层')
console.log(' · sendNotify 报错 → 看报错文本,多半是 webhook 那一路')
})().catch(e => console.log(' 异常:', e.message))
}