fix(notify): 平台回 204 被误判为失败 + 诊断工具改名 scrip_

三个改动:

1) sendNotify.js 的 webhook 判据从 statusCode !== 200 改成 2xx 一律成功。
   平台 /api/hooks/ql 按设计回 204(表示已接收且不写 event_logs),
   原判据把 204 当失败 → 每轮日志都打「自定义发送通知消息失败!!」,
   而投递其实成功了。这是纯假警报,也是这次误判的根因。
   同时把状态码打进日志,真失败时能直接看出是 401/403/400/429。

2) _diag_notify.js → scrip_diag_notify.js。
   订阅白名单是 scrip_,不带前缀的文件拉不下来,工具等于不存在。
   加 // cron: 0 0 1 1 * 与 // name:,避免青龙自动建任务时落随机 cron。
   正文补到六段:sendNotify 加载 / env / webhook 三前置条件与 body+headers
   解析复刻 / 直打平台按状态码定位 / pushplus txt-vs-html 模板对比 /
   完整 sendNotify 对照。

3) 文档补齐平台侧的两道硬校验:来源 IP 必须 127.0.0.1 或 172.16/12
   (走公网域名会变成反代地址 → 403),回环带 XFF 一律 403。
   给出指向平台的完整 env 写法(WEBHOOK_BODY 必须多行,单行 JSON
   会被 parseString 整段吞掉 —— 已实测解析结果为 0 个 key)。

验证:node --check(sendNotify + 诊断)/ 5 个 mock 状态码用例
(204/401/403/400/429)全部被正确识别 / _probe_meta 四文件 PASS
This commit is contained in:
2026-11-05 09:34:06 +08:00
parent 483bc29424
commit 3a711d0a47
5 changed files with 303 additions and 146 deletions

View File

@@ -20,6 +20,7 @@
| 京东Cookie辅助脚本 | `jd/scrip_moving_env_enhance.js` | 将JD CK进行重排序,保证所有账号都有助力与被助力的机会 | 需要修改脚本内域名(对应ql的域名) | `59 23 */3 * *` |
| 携趣白名单切换 | `xiequ/scrip_switch.js` | 多个携趣账号,多个IP,自动选择合适的携趣进行白名单切换 | `XQ_ACCOUNTS`<br />`XQ_WHITELIST` | `38 2,8,14,20 * * *` |
| QQ音乐自动签到 | `qqmusic/scrip_qqmusic_checkin.js` | QQ音乐会员成长值任务 + 会员福利站自动签到(签到/挂件/装扮/听书/节目/礼券/月里程碑/抽奖) | `SH_QQMUSIC_KEY`<br />(多账号同名多条,格式 `<uin>#<qm_keyst>`) | `1 0,6 * * *` |
| 通知链路诊断 | `scrip_diag_notify.js` | 排查「通知只收到标题」「自定义发送通知消息失败」:查 sendNotify 加载、env、webhook 前置条件、body 解析、直打平台、pushplus 模板对比 | 无(读现有通知 env) | `0 0 1 1 *`(等于不自动跑,手动执行) |
> **文件名约定**:订阅白名单是 `scrip_`,所有脚本文件必须带该前缀才会被拉取。
> 详见各目录下的 README。

View File

@@ -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, '<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))
}

View File

@@ -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` 换成 `<br>`。
微信侧只显示一行是**展示折叠**,点开消息卡片才看全文。
要确认是折叠还是真丢内容:`_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(说明穿过了反代)。
## 任务覆盖

228
scrip_diag_notify.js Normal file
View File

@@ -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, '<br>') : 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))
}

View File

@@ -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);