# QQ音乐自动签到(scrip_qqmusic_checkin.js) QQ音乐会员成长值任务 + 会员福利站 自动签到,青龙面板 Node 零依赖。 协议底座与 [LDP924/auto-checkin](https://github.com/LDP924/auto-checkin)(Python)逐字节同源: `u6.y.qq.com/cgi-bin/musics.fcg` 网关 + `zzc` 签名(sha1 重组 + 置乱表 + b64 去尾)+ `qm_keyst` cookie。 > **文件名必须带 `scrip_` 前缀**:本仓库的青龙订阅规则白名单是 `scrip_`, > 不带前缀的文件不会被订阅拉取(脚本、测试、文档同一约定)。 ## 环境变量 | 变量 | 必填 | 说明 | |---|---|---| | `SH_QQMUSIC_KEY` | ✅ | 多账号:青龙同名 env 加多条(注入自动 `&` 连接)。**单条格式 `#`**,如 `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= 👉 复制这条链接发给对方: https://y.qq.com/m/vip/experience_card/dist/index.html?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 抓一次,之后无限续) 1. 浏览器打开 [y.qq.com](https://y.qq.com) 登录(QQ/微信均可) 2. F12 → Application → Cookies → `https://y.qq.com` → 复制 `qm_keyst` 与 `uin`(`o123456789` 去掉 `o`) 3. 青龙新建环境变量 `SH_QQMUSIC_KEY`,值 `uin#qm_keyst` **为什么抓一次就够**:签名/`g_tk` 全是本地算法(无盐、无设备绑定、无动态 token); `qm_keyst` 名义 72h,但续期接口 `QQConnectLogin.LoginServer.QQLogin` 旧 key 不吊销—— 脚本每次运行先续期,新 key 写缓存滚动,环境变量永远不用再动。 只有脚本断跑超 72h 且缓存丢失(推送出现 🔴 2000)才需要重新抓。 ## 缓存与订阅安全 续期产物存 `/ql/data/qqmusic_cache/qm_.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 文档 · 如何直接在服务号中显示推送内容](https://pushplus.plus/doc/help/showmessage.html) 所以通知栏能看到的只有:**标题 + 正文第一行**。后面的明细一律折叠。 三条官方解法(都要额外成本): | 方案 | 代价 | |---|---| | 在服务号发「激活消息」 | 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`。 ```js await sendNotifyFn(title, desp, {}, '') ``` ## 通知发不出去怎么查 ```bash # 青龙里手动跑一次(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`: ```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` 时**一条通知都发不出去且日志无痕**。现在会打一行: ``` [通知] 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): ```js '{"title":"$title","content":"$content"}' → {} ✗ 'title:$title\ncontent:$content' → 两个 key ✓ ``` body 变成 `{}` → 平台 `JSON.parse` 得到空对象 → 400「title 与 content 不能同时为空」。 ### 坑 3:webhook 三个前置条件,缺一个就静默跳过 `webhookNotify` 开头逐行 return,**不发请求也不报错**: 1. `WEBHOOK_URL` 或 `WEBHOOK_BODY` **至少一个**含 `$title` 2. `WEBHOOK_METHOD` **必须显式设**(空则走 else 分支 resolve) 3. body 能被 `parseString` 正确解析(见坑 2) ### 坑 4:pushplus 只显示一行 ≠ 内容丢了 微信通知卡片默认只展示开头,点开才展开全文。要确认是折叠还是真丢: 第 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(说明穿过了反代)。 ## 任务覆盖 | 层 | 内容 | 风险 | |---|---|---| | 任务中心 | 签到(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` 登录态失效推送提示。 ## 本地测试 ```bash 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())" ```