用户截图确认:pushplus 的微信卡片只显示「标题 QQ音乐签到 + 正文第一行」, 后面的明细全折叠。查 pushplus 官方文档,这是平台默认行为 —— 服务号模板消息默认不展示具体内容,必须点「查看详情」。 三条官方解法都要额外成本(发「激活消息」24h 内限 5 条 / 开会员 / 绑定自己认证的服务号每年 300 元),所以不改通道,改信息密度: - 标题从固定的「QQ音乐签到」改成三态:QQ音乐签到 ✅ / 🟡 / ⚠️ - 正文第一行从「✅ QQ音乐签到 · 全部完成」改成带数字的摘要: 「✅ 全部完成 · 成长值 +35」「✅ 已完成 · 成长值 +15 · 1 项待处理」 - 引入三态结论 state:done / warn(有 △ 但无 ✗,仍算完成)/ fail - 调用 sendNotify 时第四参传空串,去掉尾部「本通知 By:github.com/...」 标题不放数字的理由:平台抑制指纹是 sha256(kind|credId|title去数字), 数字本来就被 titleKeyOf 剥掉,带 +35 / +15 算同一指纹,抑制不会失效。 顺带修 main 里的累加:blocks 从数组改成 {body,total,warn}, 成长值合计与待处理项数在拼装前算好,摘要行才能直接用。 验证:node --check / 签名对拍 6 组 PASS / 5 个场景 (全好、有遗留、有失败、登录失效、双账号)的 state+标题+首行逐字比对 PASS / _probe_meta 四文件 PASS
QQ音乐自动签到(scrip_qqmusic_checkin.js)
QQ音乐会员成长值任务 + 会员福利站 自动签到,青龙面板 Node 零依赖。
协议底座与 LDP924/auto-checkin(Python)逐字节同源:
u6.y.qq.com/cgi-bin/musics.fcg 网关 + zzc 签名(sha1 重组 + 置乱表 + b64 去尾)+ qm_keyst cookie。
文件名必须带
scrip_前缀:本仓库的青龙订阅规则白名单是scrip_, 不带前缀的文件不会被订阅拉取(脚本、测试、文档同一约定)。
环境变量
| 变量 | 必填 | 说明 |
|---|---|---|
SH_QQMUSIC_KEY |
✅ | 多账号:青龙同名 env 加多条(注入自动 & 连接)。单条格式 <uin>#<qm_keyst>,如 123456789#Q_H_L_xxxx。也接受整串 cookie(含 qm_keyst= 与 uin=o...) |
QM_KEY / QM_UIN |
备选 | 单账号兼容形态(SH_QQMUSIC_KEY 优先) |
QM_CACHE_DIR |
可选 | key 缓存目录,默认 /ql/data/qqmusic_cache(青龙)/ qqmusic/.cache(本地) |
QM_PLAY_REPORT |
可选 | on 才启用任务20播放上报(本版未实现模板,挂了开关占位) |
QM_SHARE_CARD |
可选 | on 才让定时任务自动建卡。默认 off = 只读报数。手动取卡走 -- card,不受此开关限制 |
QM_CARD |
可选 | 1/on 等价于 -- card(不方便传参时用) |
VIP 体验券(分享卡)
music.vip.VipFreeCardSvr,2026-11-04 从体验券落地页
y.qq.com/m/vip/experience_card/dist/index.html 的 index.036b2e716.js 逆出。
接口在 u6 网关实测路由通(GetCanNumConf 用假 key 也返回真实配额表,说明该 method 不校验登录;
CreatCard / CheckCurrentCardList 返回 1000=未登录)。
这卡是什么
有效期内的豪华绿钻会员,每 31 天可按等级赠送给不同的非会员好友 3 天体验券:
| 等级 | LV1 | LV2 | LV3 | LV4 | LV5 | LV6 | LV7 | LV8 |
|---|---|---|---|---|---|---|---|---|
| 张数 | 1 | 1 | 2 | 3 | 3 | 5 | 5 | 7 |
(实测配额表还有 LV9=10、LV10=15)
卡只能送人,自己领不了自己的(CheckCard 的 ownerUin 判定)。
好友领取限制:非会员自然月内限领 1 张,体验券仅周期内有效、不可累计。
三个容易混淆的数
落地页上这三个数不是一回事,脚本输出里也分开报:
| 字段 | 含义 |
|---|---|
sum |
本月配额(你这个等级这个月一共能送几张) |
reciveUinList.length |
本月已被好友领走的张数 |
cardInfo.left |
单张卡还能被领几次(一张卡可以被多个人领,每次 -1) |
「还能送几张」= sum - reciveUinList.length。
left 是另一维度:它只说明这张卡还剩几次可被领,建卡本身就已经占掉一张配额。
两种模式
① 手动取卡(你要的场景)——青龙里点一次「运行」就吐链接:
task qqmusic/scrip_qqmusic_checkin.js -- card
或者环境变量 QM_CARD=1。
行为:先扫本月已有的卡,有 cardLeft>0 的直接复用(照抄落地页的
createCard(d ? cardId : "") 逻辑),只有卡都领完了才 CreatCard 建新的。
不会为了取一张卡把整个签到流程再跑一遍。
输出:
===== 取卡 账号 123456789 =====
[分享卡] 本月配额 5 张,已被领 1 张,还能送 4 张
[分享卡] 复用本月未领完的卡:还能被领 3 次(不新建、不占新配额)
[分享卡] 分享链接:https://y.qq.com/m/vip/experience_card/dist/index.html?cardid=<cardID>
👉 复制这条链接发给对方:
https://y.qq.com/m/vip/experience_card/dist/index.html?cardid=<cardID>
② 定时任务路径(默认只读)——每天签到顺带报个数,不建卡:
[分享卡] 本月配额 5 张,已被领 2 张,还能送 3 张
[分享卡] 只读模式(QM_SHARE_CARD 未置 on),不创建新卡
要让定时任务也自动建卡(不建议,会堆废卡)才设 QM_SHARE_CARD=on。
接口清单
| method | 作用 | 需要登录 |
|---|---|---|
GetCanNumConf |
全等级配额表 {等级: 张数} |
❌ |
CheckCurrentCardList |
本月卡列表 + 已被领 uin 列表 | ✅ |
CheckCard |
查某张卡详情 {cardID} → cardInfo.left / ownerUin |
✅ |
CreatCard |
建卡 {source: 0} → cardID + cardInfo.left |
✅ |
GetCard |
好友领取用 {cardID, source} |
✅ |
source 是落地页 ADTAG 映射:xqy=1 / cdy=2 / privilege=3 / 其他=0。
错误码:100205=当月已领取过 · 100210=体验券已被领取 · 100211=领取失败。
凭证获取(web 抓一次,之后无限续)
- 浏览器打开 y.qq.com 登录(QQ/微信均可)
- F12 → Application → Cookies →
https://y.qq.com→ 复制qm_keyst与uin(o123456789去掉o) - 青龙新建环境变量
SH_QQMUSIC_KEY,值uin#qm_keyst
为什么抓一次就够:签名/g_tk 全是本地算法(无盐、无设备绑定、无动态 token);
qm_keyst 名义 72h,但续期接口 QQConnectLogin.LoginServer.QQLogin 旧 key 不吊销——
脚本每次运行先续期,新 key 写缓存滚动,环境变量永远不用再动。
只有脚本断跑超 72h 且缓存丢失(推送出现 🔴 2000)才需要重新抓。
缓存与订阅安全
续期产物存 /ql/data/qqmusic_cache/qm_<uin>.json(订阅仓库之外)。
git 的 reset/clean 只动 tracked 文件,untracked 目录在 git pull 下天然安全;
且脚本读缓存优先、env 兜底,缓存丢失仅退化为"72h 内重新抓一次"。
定时
cron: 1 0,6 * * *
每天 00:01 + 06:02 双轮:任务20(收听节目)跨天滚动翻账本 + 福利站 0 点翻新兜底。
青龙自动建任务的元数据约定(踩坑记录 2026-11-04)
订阅勾了「自动添加定时任务」时,青龙从脚本内容里正则提取任务名与 cron,两个坑:
| 项 | 青龙的提取规则 | 必须怎么写 |
|---|---|---|
| 任务名 | /name:\s*(.+)/(不锚定行首),取全文第一个命中 |
脚本第 3 行写 * name: QQ音乐自动签到。全文任何位置都不能再出现「name+冒号」的连写——https.request({ hostname: u.hostname }) 会被抓成任务名 u.hostname,(真机已复现) |
| cron | /\bcron:\s*([^\r\n]+)/(必须带冒号) |
写 * cron: 1 0,6 * * *,且独占一行、后面不跟注释(同一行剩余文字会被一起吞进 schedule) |
两个都抓不到时的兜底:任务名 = 文件名 scrip_qqmusic_checkin.js;cron = 随机 M H * * *。
所以脚本里:
https.request的选项用host:(Node 的hostname别名)而不是hostname:- cron 注解独占一行
通知内容
为什么标题和首行才是关键
pushplus 的微信渠道用的是公众号模板消息,官方明确:
为了避免不必要的麻烦,使用 pushplus 的服务号推送模板消息默认是不展示具体的消息内容的, 需要点击链接后查看内容详情。 —— pushplus 文档 · 如何直接在服务号中显示推送内容
所以通知栏能看到的只有:标题 + 正文第一行。后面的明细一律折叠。
三条官方解法(都要额外成本):
| 方案 | 代价 |
|---|---|
| 在服务号发「激活消息」 | 24 小时内只有 5 条走客服消息,之后要重新激活 |
| 开通 pushplus 会员 | 付费,模板能多显示一部分 |
| 绑定自己的服务号 | 企业/个体工商户认证,每年 300 元 |
所以在不改通道的前提下,唯一能做的就是:把关键信息压进标题和首行。
现在的格式
标题:QQ音乐签到 ✅
首行:✅ 全部完成 · 成长值 +35
🟢 1006764347:成长值 +35
✓ 任务中心签到 +15
✓ 领奖·头像挂件 +10
· 无需处理 1 项:福利站礼券
———
轮次: 00:01 / 06:02 双轮
时间: 2026/11/5 20:10:33
折叠状态下就能看到 QQ音乐签到 ✅ 和 ✅ 全部完成 · 成长值 +35,够判断要不要点开。
三态结论
| state | 标题 | 首行 | 触发条件 |
|---|---|---|---|
done |
QQ音乐签到 ✅ |
✅ 全部完成 · 成长值 +N |
全 ✓ 或 · |
warn |
QQ音乐签到 🟡 |
✅ 已完成 · 成长值 +N · K 项待处理 |
有 △ 但无 ✗ |
fail |
QQ音乐签到 ⚠️ |
⚠️ 有未完成项 · 成长值 +N · K 项待处理 |
有 ✗ 或登录失效 |
warn 仍算「完成」——任务20 等 App 内收听属于正常遗留,不该报成失败。
明细的四个符号
| 符号 | 含义 |
|---|---|
✓ |
做到了(带结果,如 +15、retCode 0) |
△ |
部分完成 / 留待下次(任务20未计次、写锁) |
✗ |
失败(接口报错、code 非预期) |
· |
无需处理(今日已完成 / 未开放),折叠成一行 |
· 折叠而不是省略 —— 折叠能看出「今天该做的都做了」,省略会让人怀疑漏跑。
明细来源是 QMClient.steps:每个动作调一次 step(name, status, detail),
verdict() 汇总。新增任务时记得补一句 step(),否则它在通知里不可见。
两个细节
① 标题只放状态、不放数字。 平台的抑制指纹是
sha256(kind|credId|title.replace(/[\d\s]+/g, '')) —— 数字本来就会被剥掉,
标题带 +35 和 +15 算同一个指纹,不会破坏「1 小时同指纹只发一条」。
② 调用时第四参传空串,否则 sendNotify 会往正文尾部追加
本通知 By:https://github.com/whyour/qinglong,这行会一起进 notify_events.content。
await sendNotifyFn(title, desp, {}, '')
通知发不出去怎么查
# 青龙里手动跑一次(cron 设成 1 月 1 日,不会自动跑)
node scrip_diag_notify.js
六段逐项定位,token 全程脱敏:
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:
if (resp.statusCode !== 200) {
console.log(`自定义发送通知消息失败!!\n${resp.body}`); // 204 也走这里
}
所以「自定义发送通知消息失败!!」是假警报 —— 平台其实收到了。 已改成 2xx 一律算成功,并把状态码打进日志:
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 时一条通知都发不出去且日志无痕。现在会打一行:
[通知] sendNotify 不可用(Cannot find module 'got'),本轮不发推送
修:docker exec -it qinglong pnpm install got
坑 2:WEBHOOK_BODY 写成单行 JSON 会被解析成空
sendNotify 的 parseString 用 /(\w+):\s*((?:(?!\n\w+:).)*)/g,靠换行 + \w+: 断值。
单行 JSON 没有换行,整个 body 被吞掉,实测解析结果 {}(0 个 key):
'{"title":"$title","content":"$content"}' → {} ✗
'title:$title\ncontent:$content' → 两个 key ✓
body 变成 {} → 平台 JSON.parse 得到空对象 → 400「title 与 content 不能同时为空」。
坑 3:webhook 三个前置条件,缺一个就静默跳过
webhookNotify 开头逐行 return,不发请求也不报错:
WEBHOOK_URL或WEBHOOK_BODY至少一个含$titleWEBHOOK_METHOD必须显式设(空则走 else 分支 resolve)- body 能被
parseString正确解析(见坑 2)
坑 4:pushplus 只显示一行 ≠ 内容丢了
微信通知卡片默认只展示开头,点开才展开全文。要确认是折叠还是真丢:
第 5 节会同时发 txt 和 html 两条,对比哪个正文完整。
- txt 完整 / html 一行 → 把 pushplus 目标改成
template=txt - 两条都一行 → 微信卡片折叠,点开即全文,不是 bug
指向平台的完整配置
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(说明穿过了反代)。
任务覆盖
| 层 | 内容 | 风险 |
|---|---|---|
| 任务中心 | 签到(15) / 挂件(5) / 装扮(5) / 听书(5) / 节目StartTask | 低(等价 App 点击) |
| 会员福利站 | 每日签到(15) / 礼券(+10心愿值) / 旅程激励 / 月里程碑 / 月度好礼 / 臻享抽奖 | 低(先探后做,无硬编码 ID) |
| VIP 体验券 | 分享卡配额查询 / 建卡(QM_SHARE_CARD=on) |
低(只读默认,不消耗配额) |
| 播放上报 | 任务20 完整 +10 需 App 正常收听(伪造统计默认关) | 中(默认关闭) |
| 支付流水任务 | 4/5/6/12/14/18/21 | 不可伪造,不碰 |
状态三态机:Y=已完成零请求(防风控)/ W=进行中直接领 / N=执行动作。
错误码处置:20020 冷却3s重领 · 30005 只读探测后补领 · 10006 写锁即停次日恢复 · 2000 登录态失效推送提示。
本地测试
node --check scrip_qqmusic_checkin.js # 语法
node scrip_qqmusic_checkin.js # 无 env 时优雅跳过
SH_QQMUSIC_KEY='123#Q_H_L_x' node -e "console.log(require('./scrip_qqmusic_checkin').parseAccounts())"