从体验券落地页 y.qq.com/m/vip/experience_card/dist/index.html 的
index.036b2e716.js 逆出 music.vip.VipFreeCardSvr 全部 7 个 method,
并在 u6 网关实测路由(GetCanNumConf 用假 key 也返回真实配额表
{1:1,2:1,3:2,4:3,5:3,6:5,7:5,8:7,9:10,10:15},说明该 method 不校验登录;
CreatCard / CheckCurrentCardList 返回 1000=未登录,确认需要登录态)。
语义(官方特权页):有效期内的豪华绿钻每 31 天可按等级赠送不同非会员
好友 3 天体验券,LV1-2 各 1 张、LV3 2 张、LV4-5 各 3 张、LV6-7 各 5 张、
LV8 7 张。卡只能送人,自己领不了自己的(CheckCard 的 ownerUin 判定);
好友非会员自然月限领 1 张。
实现:
- 新增 QMClient.shareCard(),接在 vipStation 之后,失败不拖垮主流程
(QmDead 照常上抛,其他异常只记日志)
- 默认只读:报「本月配额 N 张 / 已送 M 张 / 剩余 K 张」,不 CreatCard
- QM_SHARE_CARD=on 才在 remain>0 时 CreatCard,输出 cardid 分享链接
- 配额来源:本月卡 sum 优先;本月无卡时取 confInfo 最大档位作上限参考
(不拿 confInfo[0] 兜底 —— 表里没有 0 号档,会取到 undefined 让 remain 恒 0)
- run() 返回值新增 cardUrl,汇总与通知里带分享链接
验证:node --check 通过 / 签名对拍 6 组 PASS / 空 env 优雅跳过 /
5 个 mock 用例(只读/用完/建卡/无卡兜底)输出符合预期 /
_probe_meta 三元数据 PASS
135 lines
6.8 KiB
Markdown
135 lines
6.8 KiB
Markdown
# 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 加多条(注入自动 `&` 连接)。**单条格式 `<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` 才创建 VIP 体验券(分享卡)。默认 off = 只读探测「本月还能送几张」,不消耗配额 |
|
||
|
||
## 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 |
|
||
|
||
**卡只能送人,自己领不了自己的**(`CheckCard` 的 `ownerUin` 判定)。
|
||
好友领取限制:非会员自然月内限领 1 张,体验券仅周期内有效、不可累计。
|
||
|
||
### 接口清单
|
||
|
||
| method | 作用 | 需要登录 |
|
||
|---|---|---|
|
||
| `GetCanNumConf` | 全等级配额表 `{等级: 张数}` | ❌ |
|
||
| `CheckCurrentCardList` | 本月卡列表 + 已送出 uin 列表 | ✅ |
|
||
| `CreatCard` | 建卡,`param: {source: 0}` → 返回 `cardID` + `cardInfo.left` | ✅ |
|
||
| `CheckCard` | 查某张卡详情(`param: {cardID}`) | ✅ |
|
||
| `GetCard` | **好友领取**用(`param: {cardID, source}`) | ✅ |
|
||
|
||
`source` 是落地页 ADTAG 映射:`xqy`=1 / `cdy`=2 / `privilege`=3 / 其他=0。
|
||
|
||
错误码:`100205`=当月已领取过 · `100210`=体验券已被领取 · `100211`=领取失败。
|
||
|
||
### 用法
|
||
|
||
```bash
|
||
# 只读:看本月还能送几张(不建卡、不占配额)
|
||
node scrip_qqmusic_checkin.js
|
||
|
||
# 建卡:有剩余配额时创建一张,输出分享链接
|
||
QM_SHARE_CARD=on node scrip_qqmusic_checkin.js
|
||
```
|
||
|
||
建卡成功后脚本会打印:
|
||
|
||
```
|
||
[分享卡] 本月配额 5 张,已送出 1 张,剩余 4 张
|
||
[分享卡] 已创建:剩余可领 4 次
|
||
[分享卡] 分享链接:https://y.qq.com/m/vip/experience_card/dist/index.html?cardid=<cardID>
|
||
```
|
||
|
||
链接发给好友,对方打开后自己点「立即领取」即可(脚本不代领,代领会撞 `100205`/`100210`)。
|
||
|
||
**默认 off 的理由**:`CreatCard` 会真的占掉一张配额,而卡是给人送的、不是自己用的 ——
|
||
自动建卡只会堆出没人领的废卡。所以默认只报数,要送人时手动置 `QM_SHARE_CARD=on` 跑一次。
|
||
|
||
## 凭证获取(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_<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 注解独占一行
|
||
|
||
## 任务覆盖
|
||
|
||
| 层 | 内容 | 风险 |
|
||
|---|---|---|
|
||
| 任务中心 | 签到(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())"
|
||
```
|