diff --git a/README.md b/README.md index cddd322..c8f608b 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ | 京东Cookie辅助脚本 | `jd/scrip_moving_env_enhance.js` | 将JD CK进行重排序,保证所有账号都有助力与被助力的机会 | 需要修改脚本内域名(对应ql的域名) | `59 23 */3 * *` | | 携趣白名单切换 | `xiequ/scrip_switch.js` | 多个携趣账号,多个IP,自动选择合适的携趣进行白名单切换 | `XQ_ACCOUNTS`
`XQ_WHITELIST` | `38 2,8,14,20 * * *` | | QQ音乐自动签到 | `qqmusic/scrip_qqmusic_checkin.js` | QQ音乐会员成长值任务 + 会员福利站自动签到(签到/挂件/装扮/听书/节目/礼券/月里程碑/抽奖) | `SH_QQMUSIC_KEY`
(多账号同名多条,格式 `#`) | `1 0,6 * * *` | +| 通知链路诊断 | `scrip_diag_notify.js` | 排查「通知只收到标题」「自定义发送通知消息失败」:查 sendNotify 加载、env、webhook 前置条件、body 解析、直打平台、pushplus 模板对比 | 无(读现有通知 env) | `0 0 1 1 *`(等于不自动跑,手动执行) | > **文件名约定**:订阅白名单是 `scrip_`,所有脚本文件必须带该前缀才会被拉取。 > 详见各目录下的 README。 diff --git a/_diag_notify.js b/_diag_notify.js deleted file mode 100644 index 69c9c86..0000000 --- a/_diag_notify.js +++ /dev/null @@ -1,125 +0,0 @@ -#!/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 6bad646..f634e38 100644 --- a/qqmusic/README.md +++ b/qqmusic/README.md @@ -181,30 +181,57 @@ cron: 1 0,6 * * * ## 通知发不出去怎么查 -`node _diag_notify.js`(放在仓库根,跟 `sendNotify.js` 同级)一次跑完查五件事: +```bash +# 青龙里手动跑一次(cron 设成 1 月 1 日,不会自动跑) +node scrip_diag_notify.js +``` + +六段逐项定位,token 全程脱敏: ``` -1. sendNotify 能不能加载 ← 最常见:容器里缺 got,require 抛错被静默吞掉 -2. 通知 env 有没有读到 ← 青龙「环境变量」是否真的保存了 -3. webhook 前置条件 + body 解析 ← 见下方两个坑 -4. 直连 pushplus 发 txt/html 两条 ← 对比哪个模板正文完整 -5. 走一遍 sendNotify ← 与 4 对比,定位是通道问题还是展示问题 +1. sendNotify 加载 ← 容器缺 got 时 require 抛错,被静默吞掉 +2. 通知 env ← 青龙「环境变量」是否真的保存了 +3. webhook 配置分析 ← 三个前置条件 + body/headers 解析复刻 +4. 直打平台 /api/hooks/ql ← 按状态码定位卡在哪一道校验 +5. 直连 pushplus 模板对比 ← txt vs html,确认是不是展示折叠 +6. 完整 sendNotify ← 与第 4 节对照,区分通道问题与展示问题 +``` + +### 坑 0:平台回 204,原版 sendNotify 判成失败(已修) + +`/api/hooks/ql` 按设计返回 **204**(表示已接收,且不写 event_logs), +而原版 `sendNotify.js` 的判据是 `resp.statusCode !== 200`: + +```js +if (resp.statusCode !== 200) { + console.log(`自定义发送通知消息失败!!\n${resp.body}`); // 204 也走这里 +} +``` + +**所以「自定义发送通知消息失败!!」是假警报 —— 平台其实收到了。** +已改成 2xx 一律算成功,并把状态码打进日志: + +```js +if (resp.statusCode < 200 || resp.statusCode >= 300) { + console.log(`自定义发送通知消息失败!!HTTP ${resp.statusCode}\n${resp.body}`); +} else { + console.log(`自定义发送通知消息成功🎉。HTTP ${resp.statusCode}\n${resp.body || ''}`); +} ``` ### 坑 1:`require('../sendNotify')` 静默失败 -脚本里原本是 `catch { /* 仅控制台 */ }`,缺 `got` 时**一条通知都发不出去且日志无痕**。 -现在会打一行: +原本 `catch { /* 仅控制台 */ }`,缺 `got` 时**一条通知都发不出去且日志无痕**。现在会打一行: ``` [通知] sendNotify 不可用(Cannot find module 'got'),本轮不发推送 ``` -修:`docker exec -it qinglong pnpm install got`(sendNotify 只依赖 got + 内置模块)。 +修:`docker exec -it qinglong pnpm install got` ### 坑 2:WEBHOOK_BODY 写成单行 JSON 会被解析成空 -`sendNotify` 的 `parseString` 用 `/(\w+):\s*((?:(?!\n\w+:).)*)/g` 解析,**靠换行 + `\w+:` 断值**。 +`sendNotify` 的 `parseString` 用 `/(\w+):\s*((?:(?!\n\w+:).)*)/g`,**靠换行 + `\w+:` 断值**。 单行 JSON 没有换行,整个 body 被吞掉,实测解析结果 `{}`(0 个 key): ```js @@ -212,19 +239,41 @@ cron: 1 0,6 * * * 'title:$title\ncontent:$content' → 两个 key ✓ ``` -body 畸形 → 对方返回非 200 → 日志出现 `自定义发送通知消息失败!!`。 +body 变成 `{}` → 平台 `JSON.parse` 得到空对象 → 400「title 与 content 不能同时为空」。 -**三个前置条件缺一不可**(`webhookNotify` 逐行 return): +### 坑 3:webhook 三个前置条件,缺一个就静默跳过 + +`webhookNotify` 开头逐行 return,**不发请求也不报错**: 1. `WEBHOOK_URL` 或 `WEBHOOK_BODY` **至少一个**含 `$title` -2. `WEBHOOK_METHOD` **必须显式设**(空则走 else 分支 resolve,不发请求也不报错) -3. body 能被 `parseString` 正确解析(多行,或干脆用 URL 传参) +2. `WEBHOOK_METHOD` **必须显式设**(空则走 else 分支 resolve) +3. body 能被 `parseString` 正确解析(见坑 2) -### 坑 3:pushplus 的 html 模板可能只显示标题 +### 坑 4:pushplus 只显示一行 ≠ 内容丢了 -`sendNotify` 的 pushplus 通道固定发 `template` 缺省值(html),并把 `\n` 换成 `
`。 -微信侧只显示一行是**展示折叠**,点开消息卡片才看全文。 -要确认是折叠还是真丢内容:`_diag_notify.js` 第 4 节会同时发 txt 和 html 两条,对比即可。 +微信通知卡片默认只展示开头,点开才展开全文。要确认是折叠还是真丢: +第 5 节会同时发 `txt` 和 `html` 两条,对比哪个正文完整。 + +- txt 完整 / html 一行 → 把 pushplus 目标改成 `template=txt` +- 两条都一行 → 微信卡片折叠,点开即全文,不是 bug + +### 指向平台的完整配置 + +```bash +WEBHOOK_URL=https://<平台域名>/api/hooks/ql +WEBHOOK_METHOD=POST +WEBHOOK_CONTENT_TYPE=application/json +WEBHOOK_BODY=title:$title +content:$content +WEBHOOK_HEADERS=Content-Type: application/json +x-sh-hook-token: <平台 QL_HOOK_TOKEN> +``` + +平台侧还有两道硬校验(`hookGuard`): + +- **来源 IP 必须是 127.0.0.1 或 172.16/12**(docker 内网)。青龙与平台不同容器时, + 走公网域名会让源 IP 变成反代地址 → 403。要用**容器内网地址**(如 `http://script-helper:3000`)。 +- 回环地址带 `X-Forwarded-For` 一律 403(说明穿过了反代)。 ## 任务覆盖 diff --git a/scrip_diag_notify.js b/scrip_diag_notify.js new file mode 100644 index 0000000..59a44ad --- /dev/null +++ b/scrip_diag_notify.js @@ -0,0 +1,228 @@ +#!/usr/bin/env node +// cron: 0 0 1 1 * +// name: QQ音乐通知诊断 +/** + * 通知链路诊断(青龙里跑:node scrip_diag_notify.js) + * + * 专治「pushplus 只收到一行标题」和「自定义发送通知消息失败」这两个现象。 + * 逐段定位:sendNotify 加载 → env → webhook 配置 → 直打平台 /api/hooks/ql + * → 直连 pushplus 对比模板 → 走一遍完整 sendNotify。 + * + * token 一律脱敏。只读配置 + 发测试消息,不改任何东西。 + * 手动跑一次即可,cron 设成 1 月 1 日(等于不自动跑)。 + */ +'use strict' + +const https = require('https') +const http = require('http') +const mask = (v, n = 6) => (!v ? '(未设)' : `${v.slice(0, n)}…(len=${v.length})`) +const hr = t => console.log(`\n══════════ ${t} ══════════`) + +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') + +/** 发一个请求,返回 {status, headers, body};超时/网络错误也返回而不抛 */ +function req(url, { method = 'POST', headers = {}, body } = {}) { + return new Promise((resolve) => { + let u + try { u = new URL(url) } catch (e) { return resolve({ status: 0, headers: {}, body: `URL 非法: ${e.message}` }) } + const payload = body == null ? '' : (typeof body === 'string' ? body : JSON.stringify(body)) + const lib = u.protocol === 'http:' ? http : https + const r = lib.request({ + host: u.hostname, port: u.port || (u.protocol === 'http:' ? 80 : 443), + path: u.pathname + u.search, method, + headers: { ...headers, ...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}) }, + timeout: 15000, + }, (res) => { + const cs = [] + res.on('data', c => cs.push(c)) + res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(cs).toString('utf8') })) + }) + r.on('timeout', () => { r.destroy(); resolve({ status: 0, headers: {}, body: 'TIMEOUT(15s)' }) }) + r.on('error', e => resolve({ status: 0, headers: {}, body: e.message })) + if (payload) r.write(payload) + r.end() + }) +} + +/* ────────────────── 1. sendNotify 加载 ────────────────── */ +hr('1. sendNotify 加载') +let sn = null +try { + sn = require('./sendNotify') + console.log(' ✓ 加载成功,sendNotify =', typeof sn.sendNotify) +} catch (e) { + console.log(' ✗ 加载失败:', e.message.split('\n')[0]) + console.log(' → 脚本里 require 失败会打一行日志并跳过推送,一条都发不出') + console.log(' → 修:docker exec -it qinglong pnpm install got') +} + +/* ────────────────── 2. 通知 env ────────────────── */ +hr('2. 通知 env') +const ENVS = ['PUSH_PLUS_TOKEN', 'PUSH_PLUS_USER', 'WEBHOOK_URL', 'WEBHOOK_BODY', + 'WEBHOOK_METHOD', 'WEBHOOK_CONTENT_TYPE', 'WEBHOOK_HEADERS', 'SKIP_PUSH_TITLE'] +const SENSITIVE = /TOKEN|URL|BODY|HEADERS/i +let envHit = 0 +for (const k of ENVS) { + const v = process.env[k] + if (v) envHit++ + console.log(` ${v ? '✓' : '·'} ${k.padEnd(20)} ${v ? (SENSITIVE.test(k) ? mask(v) : v) : '(未设)'}`) +} +if (!envHit) console.log(' ⚠️ 一个都没读到 —— 青龙「环境变量」是否保存/是否启用') + +/* ────────────────── 3. webhook 配置分析 ────────────────── */ +hr('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 || '' +const WH = process.env.WEBHOOK_HEADERS || '' + +console.log(' URL 含 $title :', WU.includes('$title')) +console.log(' BODY 含 $title:', WB.includes('$title')) +console.log(' METHOD :', WM || '(空 → 静默跳过,不发也不报错)') +console.log(' CONTENT_TYPE :', WCT || '(空)') + +// 三个前置条件(webhookNotify 逐行 return,缺一个就静默跳过) +if (!WU.includes('$title') && !WB.includes('$title')) { + console.log('\n ✗ 条件① 不过:URL 与 BODY 都没有 $title → 第一行就 resolve(),什么都不发') +} +if (!WM) { + console.log('\n ✗ 条件② 不过:WEBHOOK_METHOD 为空 → 走 else 分支 resolve(),什么都不发') +} + +// body 解析复刻:parseString 靠「换行 + \w+:」断值,单行 JSON 会被整段吞掉 +if (WB) { + 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(' 解析出:', keys.length ? keys.join(', ') : '(0 个 key)') + if (keys.length === 0) { + console.log(' ✗ 条件③ 不过:BODY 非空但解析出 0 个 key') + console.log(' 单行 JSON 会被整段吞掉(已实测),最终发出去的是 {}') + console.log(' → 平台 JSON.parse("{}") 得到空对象 → 400 title 与 content 不能同时为空') + console.log(' 修:BODY 改成多行 —— title:$title 换行 content:$content') + } +} + +// headers 解析复刻:split('\n'),若 env 里是字面量 \n(两字符)就分不开 +if (WH) { + console.log('\n --- HEADERS 解析复刻(sendNotify 的 parseHeaders)---') + const lines = WH.split('\n') + console.log(' 按换行切出:', lines.length, '行', lines.length === 1 && WH.includes('\\n') ? '(⚠️ 疑似字面量 \\n)' : '') + for (const line of lines) { + const i = line.indexOf(':') + if (i < 0) { console.log(' ✗ 无冒号,被丢弃:', line.slice(0, 40)); continue } + const k = line.slice(0, i).trim().toLowerCase() + const v = line.slice(i + 1).trim() + console.log(` ${k} = ${/token|key|secret/i.test(k) ? mask(v) : v}`) + if (v.includes('\\n')) console.log(' ⚠️ 值里含字面量 \\n —— 整行没被切开,token 会带上后半段') + } +} + +/* ────────────────── 4. 直打平台 /api/hooks/ql ────────────────── */ +hr('4. 直打平台 /api/hooks/ql') +if (!WU) { + console.log(' · WEBHOOK_URL 未设,跳过') +} else { + // 还原 sendNotify 实际会请求的 URL($title/$content 替换后) + const finalUrl = WU.replaceAll('$title', encodeURIComponent(TITLE)).replaceAll('$content', encodeURIComponent(DESP)) + let host = '?', path = '?' + try { const u = new URL(finalUrl); host = u.host; path = u.pathname } catch { /* ignore */ } + console.log(' 目标:', host + path) + console.log(' query 长度:', (() => { try { return new URL(finalUrl).search.length } catch { return '?' } })(), + '(把正文塞进 URL 会超长,平台只读 body)') + + // 复刻 parseHeaders + const hdrs = {} + if (WH) for (const line of WH.split('\n')) { + const i = line.indexOf(':') + if (i > 0) hdrs[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim() + } + + ;(async () => { + console.log('\n ── 用 sendNotify 相同的头 + 一个最小合法 body 试一次 ──') + const probe = { title: '诊断探针', content: '来自 scrip_diag_notify.js 的连通性测试,可忽略' } + const r1 = await req(finalUrl, { + method: WM || 'POST', + headers: { 'Content-Type': 'application/json', ...hdrs }, + body: JSON.stringify(probe), + }) + console.log(` HTTP ${r1.status} body=${r1.body.slice(0, 200) || '(空)'}`) + if (r1.status === 204) { + console.log(' ✓ 平台已接收(204 = 成功,且不写 event_logs)') + console.log(' ⚠️ 但 sendNotify 的成功判据是 statusCode !== 200 → 204 会被它当成失败') + console.log(' 「自定义发送通知消息失败!!」这句是假警报,实际投递成功了') + } else if (r1.status === 401) { + console.log(' ✗ 401:x-sh-hook-token 不对,或 WEBHOOK_HEADERS 没被正确解析') + } else if (r1.status === 403) { + console.log(' ✗ 403:来源 IP 不在白名单(只放 127.0.0.1 与 172.16/12),或回环带了 XFF') + } else if (r1.status === 400) { + console.log(' ✗ 400:body 不是合法 JSON,或 title/content 全空') + } else if (r1.status === 429) { + console.log(' ✗ 429:超过 60/分 或 1000/天') + } else if (r1.status === 0) { + console.log(' ✗ 连不上:', r1.body, '(容器网络/域名解析)') + } + + console.log('\n ── 再发一条含中文与换行的真实正文(验证 JSON 转义)──') + const r2 = await req(finalUrl, { + method: WM || 'POST', + headers: { 'Content-Type': 'application/json', ...hdrs }, + body: JSON.stringify({ title: TITLE, content: DESP }), + }) + console.log(` HTTP ${r2.status} body=${r2.body.slice(0, 200) || '(空)'}`) + if (r2.status === 204) console.log(' ✓ 完整正文(含换行/中文/emoji)平台也收下了') + else if (r2.status !== r1.status) console.log(' ⚠️ 与上一条状态不同 → 正文内容触发了额外校验') + + /* ────────── 5. 直连 pushplus 模板对比 ────────── */ + hr('5. 直连 pushplus(模板对比)') + const PT = process.env.PUSH_PLUS_TOKEN || '' + if (!PT) { + console.log(' · PUSH_PLUS_TOKEN 未设,跳过') + } else { + for (const tpl of ['txt', 'html']) { + const content = tpl === 'html' ? DESP.replace(/\n/g, '
') : DESP + const r = await req('https://www.pushplus.plus/send', { + headers: { 'Content-Type': 'application/json' }, + body: { 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 || ''} content=${content.length}字`) + } + console.log('\n → 微信里收到两条,对比哪条正文完整:') + console.log(' · txt 完整 / html 只有一行 → 换 template=txt') + console.log(' · 两条都只有一行 → 微信通知卡片本来就折叠,点开才展开全文') + console.log(' · 两条都收不到 → PUSH_PLUS_TOKEN 或实名状态有问题') + } + + /* ────────── 6. 走一遍完整 sendNotify ────────── */ + hr('6. 完整 sendNotify') + if (!sn) { + console.log(' 跳过(未加载)') + } else { + console.log(' 调用 sendNotify("QQ音乐签到", desp)…\n') + await sn.sendNotify(TITLE, DESP) + console.log('\n 对照第 4 节:') + console.log(' · 若这里出现「自定义发送通知消息失败」但第 4 节是 204 → 假警报,投递成功') + console.log(' · 若第 4 节 401/403 → webhook 头或 IP 的问题,与 sendNotify 无关') + } + hr('完成') + })().catch(e => console.log(' 异常:', e.message)) +} diff --git a/sendNotify.js b/sendNotify.js index 1a448da..384056b 100644 --- a/sendNotify.js +++ b/sendNotify.js @@ -1310,10 +1310,14 @@ function webhookNotify(text, desp) { ).replaceAll('$content', encodeURIComponent(desp)); got(formatUrl, options).then((resp) => { try { - if (resp.statusCode !== 200) { - console.log(`自定义发送通知消息失败!!\n${resp.body}`); + // [本地补丁 2026-11-05] 原版判据是 resp.statusCode !== 200,把 204 也算失败。 + // 平台 /api/hooks/ql 按设计回 204(表示已接收且不写 event_logs),于是每轮 + // 都打印「自定义发送通知消息失败!!」—— 投递其实成功,纯假警报。 + // 改成 2xx 一律算成功,并把状态码打进日志,方便区分真失败。 + if (resp.statusCode < 200 || resp.statusCode >= 300) { + console.log(`自定义发送通知消息失败!!HTTP ${resp.statusCode}\n${resp.body}`); } else { - console.log(`自定义发送通知消息成功🎉。\n${resp.body}`); + console.log(`自定义发送通知消息成功🎉。HTTP ${resp.statusCode}\n${resp.body || ''}`); } } catch (e) { $.logErr(e, resp);