From 483bc29424b7178bc79bc25b080c98bd6e873210 Mon Sep 17 00:00:00 2001 From: zhuhjay Date: Thu, 5 Nov 2026 08:54:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(qqmusic):=20=E9=80=9A=E7=9F=A5=E5=8F=91?= =?UTF-8?q?=E9=80=81=E5=A4=B1=E8=B4=A5=E4=B8=8D=E5=86=8D=E9=9D=99=E9=BB=98?= =?UTF-8?q?=20+=20=E6=96=B0=E5=A2=9E=E9=80=9A=E7=9F=A5=E8=AF=8A=E6=96=AD?= =?UTF-8?q?=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:青龙日志里「自定义发送通知消息失败!!」,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 --- _diag_notify.js | 125 +++++++++++++++++++++++++++++++ qqmusic/README.md | 47 ++++++++++++ qqmusic/scrip_qqmusic_checkin.js | 5 +- 3 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 _diag_notify.js diff --git a/_diag_notify.js b/_diag_notify.js new file mode 100644 index 0000000..69c9c86 --- /dev/null +++ b/_diag_notify.js @@ -0,0 +1,125 @@ +#!/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, '
') : 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)) +} diff --git a/qqmusic/README.md b/qqmusic/README.md index 95cd091..6bad646 100644 --- a/qqmusic/README.md +++ b/qqmusic/README.md @@ -179,6 +179,53 @@ cron: 1 0,6 * * * 明细来源是 `QMClient.steps`:每个动作调一次 `step(name, status, detail)`, `verdict()` 汇总。新增任务时记得补一句 `step()`,否则它在通知里不可见。 +## 通知发不出去怎么查 + +`node _diag_notify.js`(放在仓库根,跟 `sendNotify.js` 同级)一次跑完查五件事: + +``` +1. sendNotify 能不能加载 ← 最常见:容器里缺 got,require 抛错被静默吞掉 +2. 通知 env 有没有读到 ← 青龙「环境变量」是否真的保存了 +3. webhook 前置条件 + body 解析 ← 见下方两个坑 +4. 直连 pushplus 发 txt/html 两条 ← 对比哪个模板正文完整 +5. 走一遍 sendNotify ← 与 4 对比,定位是通道问题还是展示问题 +``` + +### 坑 1:`require('../sendNotify')` 静默失败 + +脚本里原本是 `catch { /* 仅控制台 */ }`,缺 `got` 时**一条通知都发不出去且日志无痕**。 +现在会打一行: + +``` +[通知] sendNotify 不可用(Cannot find module 'got'),本轮不发推送 +``` + +修:`docker exec -it qinglong pnpm install got`(sendNotify 只依赖 got + 内置模块)。 + +### 坑 2:WEBHOOK_BODY 写成单行 JSON 会被解析成空 + +`sendNotify` 的 `parseString` 用 `/(\w+):\s*((?:(?!\n\w+:).)*)/g` 解析,**靠换行 + `\w+:` 断值**。 +单行 JSON 没有换行,整个 body 被吞掉,实测解析结果 `{}`(0 个 key): + +```js +'{"title":"$title","content":"$content"}' → {} ✗ +'title:$title\ncontent:$content' → 两个 key ✓ +``` + +body 畸形 → 对方返回非 200 → 日志出现 `自定义发送通知消息失败!!`。 + +**三个前置条件缺一不可**(`webhookNotify` 逐行 return): + +1. `WEBHOOK_URL` 或 `WEBHOOK_BODY` **至少一个**含 `$title` +2. `WEBHOOK_METHOD` **必须显式设**(空则走 else 分支 resolve,不发请求也不报错) +3. body 能被 `parseString` 正确解析(多行,或干脆用 URL 传参) + +### 坑 3:pushplus 的 html 模板可能只显示标题 + +`sendNotify` 的 pushplus 通道固定发 `template` 缺省值(html),并把 `\n` 换成 `
`。 +微信侧只显示一行是**展示折叠**,点开消息卡片才看全文。 +要确认是折叠还是真丢内容:`_diag_notify.js` 第 4 节会同时发 txt 和 html 两条,对比即可。 + ## 任务覆盖 | 层 | 内容 | 风险 | diff --git a/qqmusic/scrip_qqmusic_checkin.js b/qqmusic/scrip_qqmusic_checkin.js index 1c85b9f..08774da 100644 --- a/qqmusic/scrip_qqmusic_checkin.js +++ b/qqmusic/scrip_qqmusic_checkin.js @@ -861,7 +861,10 @@ async function main() { + `\n节目任务(20)依赖 App 内正常收听计入(播放上报未启用)` + `\n时间: ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}` let sendNotifyFn = null - try { sendNotifyFn = require('../sendNotify').sendNotify } catch { /* 无 sendNotify 时仅控制台 */ } + try { sendNotifyFn = require('../sendNotify').sendNotify } catch (e) { + // 静默吞掉会让「通知没发出去」完全不可见 —— 必须留一行,否则只能靠猜 + log(`[通知] sendNotify 不可用(${e.message.split('\n')[0]}),本轮不发推送`) + } if (sendNotifyFn) { try { await sendNotifyFn('QQ音乐签到', desp) } catch (e) { log(`[通知] 推送失败: ${e.message}`) } }