feat(qqmusic): VIP 体验券(分享卡)配额查询与建卡
从体验券落地页 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
This commit is contained in:
@@ -15,6 +15,62 @@ QQ音乐会员成长值任务 + 会员福利站 自动签到,青龙面板 Node
|
|||||||
| `QM_KEY` / `QM_UIN` | 备选 | 单账号兼容形态(`SH_QQMUSIC_KEY` 优先) |
|
| `QM_KEY` / `QM_UIN` | 备选 | 单账号兼容形态(`SH_QQMUSIC_KEY` 优先) |
|
||||||
| `QM_CACHE_DIR` | 可选 | key 缓存目录,默认 `/ql/data/qqmusic_cache`(青龙)/ `qqmusic/.cache`(本地) |
|
| `QM_CACHE_DIR` | 可选 | key 缓存目录,默认 `/ql/data/qqmusic_cache`(青龙)/ `qqmusic/.cache`(本地) |
|
||||||
| `QM_PLAY_REPORT` | 可选 | `on` 才启用任务20播放上报(本版未实现模板,挂了开关占位) |
|
| `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 抓一次,之后无限续)
|
## 凭证获取(web 抓一次,之后无限续)
|
||||||
|
|
||||||
@@ -62,6 +118,7 @@ cron: 1 0,6 * * *
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 任务中心 | 签到(15) / 挂件(5) / 装扮(5) / 听书(5) / 节目StartTask | 低(等价 App 点击) |
|
| 任务中心 | 签到(15) / 挂件(5) / 装扮(5) / 听书(5) / 节目StartTask | 低(等价 App 点击) |
|
||||||
| 会员福利站 | 每日签到(15) / 礼券(+10心愿值) / 旅程激励 / 月里程碑 / 月度好礼 / 臻享抽奖 | 低(先探后做,无硬编码 ID) |
|
| 会员福利站 | 每日签到(15) / 礼券(+10心愿值) / 旅程激励 / 月里程碑 / 月度好礼 / 臻享抽奖 | 低(先探后做,无硬编码 ID) |
|
||||||
|
| VIP 体验券 | 分享卡配额查询 / 建卡(`QM_SHARE_CARD=on`) | 低(只读默认,不消耗配额) |
|
||||||
| 播放上报 | 任务20 完整 +10 需 App 正常收听(伪造统计默认关) | 中(默认关闭) |
|
| 播放上报 | 任务20 完整 +10 需 App 正常收听(伪造统计默认关) | 中(默认关闭) |
|
||||||
| 支付流水任务 | 4/5/6/12/14/18/21 | 不可伪造,不碰 |
|
| 支付流水任务 | 4/5/6/12/14/18/21 | 不可伪造,不碰 |
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,9 @@
|
|||||||
* QM_CACHE_DIR key 缓存目录覆盖(默认规则见上)
|
* QM_CACHE_DIR key 缓存目录覆盖(默认规则见上)
|
||||||
* QM_PLAY_REPORT 任务20播放上报开关(默认 off;伪造统计数据属中风险,
|
* QM_PLAY_REPORT 任务20播放上报开关(默认 off;伪造统计数据属中风险,
|
||||||
* 且需播放 XML 模板,本版未实现,仅挂 StartTask 标记)
|
* 且需播放 XML 模板,本版未实现,仅挂 StartTask 标记)
|
||||||
|
* QM_SHARE_CARD VIP 体验券(分享卡)开关,默认 off 只读探测不消耗配额。
|
||||||
|
* on = 配额有剩余且本月卡已领完时自动 CreatCard,输出可分享的领取链接。
|
||||||
|
* 卡是「赠送给非会员好友」的 3 天豪华绿钻,自己领不了自己的卡。
|
||||||
*/
|
*/
|
||||||
'use strict'
|
'use strict'
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
@@ -38,6 +41,10 @@ const SCRAMBLE = [89, 39, 179, 150, 218, 82, 58, 252, 177, 52, 186, 123, 120, 64
|
|||||||
const APP_COMM = { ct: '11', cv: '20090008' } // cosmeticcgi 系专用 comm
|
const APP_COMM = { ct: '11', cv: '20090008' } // cosmeticcgi 系专用 comm
|
||||||
const POLL_PROBE_MS = 15000 // 任务20 30005 时的一次性只读探测
|
const POLL_PROBE_MS = 15000 // 任务20 30005 时的一次性只读探测
|
||||||
const PLAY_REPORT = String(process.env.QM_PLAY_REPORT || '').toLowerCase() === 'on'
|
const PLAY_REPORT = String(process.env.QM_PLAY_REPORT || '').toLowerCase() === 'on'
|
||||||
|
// VIP 体验券(分享卡):默认只读探测。on 才在「配额有剩余 + 本月卡已用完」时 CreatCard。
|
||||||
|
const SHARE_CARD = String(process.env.QM_SHARE_CARD || '').toLowerCase() === 'on'
|
||||||
|
// 分享落地页:cardid 由 CreatCard 返回,好友打开后自己点领取(非会员自然月限领 1 张)
|
||||||
|
const CARD_LANDING = 'https://y.qq.com/m/vip/experience_card/dist/index.html'
|
||||||
|
|
||||||
function time33(s) {
|
function time33(s) {
|
||||||
let h = 5381
|
let h = 5381
|
||||||
@@ -436,9 +443,65 @@ class QMClient {
|
|||||||
return gained
|
return gained
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单账号全流程:续期 → 签到 → 任务动作 → 全量领取 → 福利站 */
|
/** VIP 体验券(分享卡)—— music.vip.VipFreeCardSvr,2026-11-04 从体验券落地页
|
||||||
|
* index.036b2e716.js 逆出,接口已在 u6 网关实测路由通(GetCanNumConf 用假 key
|
||||||
|
* 也返回真实配额表,说明该 method 不校验登录;CreatCard/CheckCurrentCardList 返回 1000)。
|
||||||
|
*
|
||||||
|
* 语义(官方特权页 https://y.qq.com/m/myvip/v6/privilege_detail.html?id=experience_card):
|
||||||
|
* 有效期内的豪华绿钻会员每 31 天可按等级赠送「不同非会员好友」3 天体验券,
|
||||||
|
* LV1-2 各 1 张、LV3 2 张、LV4-5 各 3 张、LV6-7 各 5 张、LV8 7 张。
|
||||||
|
* 卡只能送人,自己领不了自己的(CheckCard 的 ownerUin 判定)。
|
||||||
|
*
|
||||||
|
* 默认只读:只报「本月还能送几张」,不 CreatCard(CreatCard 会真的占配额)。
|
||||||
|
* QM_SHARE_CARD=on 时才在「配额有剩余 + 本月卡已发完/没卡」时创建一张新卡。 */
|
||||||
|
async shareCard() {
|
||||||
|
// ① 等级配额表(不校验登录,任何账号都能读到全等级档位;仅用于展示上限)
|
||||||
|
const conf = ((await this.api('music.vip.VipFreeCardSvr', 'GetCanNumConf', {})).data || {}).confInfo || {}
|
||||||
|
// ② 本月已发卡记录(需要登录态;1000 会在 api() 里抛 QmDead)
|
||||||
|
const listData = (await this.api('music.vip.VipFreeCardSvr', 'CheckCurrentCardList', {})).data || {}
|
||||||
|
const cards = listData.cardInfoList || []
|
||||||
|
const last = cards[cards.length - 1] || {}
|
||||||
|
const sentList = last.reciveUinList || [] // 服务端字段名就是 reciveUinList(拼写如此)
|
||||||
|
// 配额来源:本月卡的 sum(服务端实发配额)优先;没有卡时取 confInfo 的最大档位作上限参考。
|
||||||
|
// 注意不能拿 confInfo[0] 兜底 —— 表里没有 0 号档,取到 undefined 会让 remain 恒为 0。
|
||||||
|
const maxQuota = Math.max(0, ...Object.values(conf).map(Number).filter(Number.isFinite))
|
||||||
|
const hasCard = cards.length > 0 && Number.isFinite(Number(last.sum))
|
||||||
|
const quota = hasCard ? Number(last.sum) : maxQuota
|
||||||
|
const remain = hasCard ? Math.max(0, quota - sentList.length) : quota
|
||||||
|
|
||||||
|
if (hasCard) {
|
||||||
|
log(`[分享卡] 本月配额 ${quota} 张,已送出 ${sentList.length} 张,剩余 ${remain} 张`)
|
||||||
|
} else {
|
||||||
|
log(`[分享卡] 本月还没建过卡(等级上限参考 ${maxQuota} 张)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SHARE_CARD) {
|
||||||
|
log('[分享卡] 只读模式(QM_SHARE_CARD 未置 on),不创建新卡')
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (remain <= 0) {
|
||||||
|
log('[分享卡] 本月配额已用完,下月再送')
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ③ 有配额才建卡:CreatCard 返回 cardID + cardInfo.left(该卡可被领次数)
|
||||||
|
const created = (await this.api('music.vip.VipFreeCardSvr', 'CreatCard', { source: 0 })).data || {}
|
||||||
|
const cardId = created.cardID
|
||||||
|
if (!cardId) {
|
||||||
|
log('[分享卡] CreatCard 未返回 cardID,跳过')
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const left = (created.cardInfo || {}).left
|
||||||
|
const url = `${CARD_LANDING}?cardid=${cardId}`
|
||||||
|
log(`[分享卡] 已创建:剩余可领 ${left != null ? left : '?'} 次`)
|
||||||
|
log(`[分享卡] 分享链接:${url}`)
|
||||||
|
this.shareCardUrl = url // 供汇总输出
|
||||||
|
return 0 // 不产成长值,不计入 total
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单账号全流程:续期 → 签到 → 任务动作 → 全量领取 → 福利站 → 分享卡 */
|
||||||
async run() {
|
async run() {
|
||||||
const out = { total: 0, dead: false }
|
const out = { total: 0, dead: false, cardUrl: '' }
|
||||||
try {
|
try {
|
||||||
await this.refresh()
|
await this.refresh()
|
||||||
const st = await this.state()
|
const st = await this.state()
|
||||||
@@ -475,6 +538,11 @@ class QMClient {
|
|||||||
if (e instanceof QmDead) throw e // 失效语义不被福利站吞掉
|
if (e instanceof QmDead) throw e // 失效语义不被福利站吞掉
|
||||||
log(`[福利站] 运行异常: ${e.message}`)
|
log(`[福利站] 运行异常: ${e.message}`)
|
||||||
}
|
}
|
||||||
|
try { await this.shareCard() } catch (e) {
|
||||||
|
if (e instanceof QmDead) throw e // 登录态失效照常上抛
|
||||||
|
log(`[分享卡] 运行异常: ${e.message}`) // 接口改版/风控不拖垮主流程
|
||||||
|
}
|
||||||
|
out.cardUrl = this.shareCardUrl || ''
|
||||||
log(`[QQ音乐] 本次新增成长值合计: ${out.total}`)
|
log(`[QQ音乐] 本次新增成长值合计: ${out.total}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof QmDead) {
|
if (e instanceof QmDead) {
|
||||||
@@ -557,6 +625,7 @@ async function main() {
|
|||||||
blocks.push(r.dead
|
blocks.push(r.dead
|
||||||
? `🔴 ${a.uin}:登录态失效,请重新在网页抓 qm_keyst 更新 env`
|
? `🔴 ${a.uin}:登录态失效,请重新在网页抓 qm_keyst 更新 env`
|
||||||
: `🟢 ${a.uin}:本次新增成长值 ${r.total}`)
|
: `🟢 ${a.uin}:本次新增成长值 ${r.total}`)
|
||||||
|
if (r.cardUrl) blocks.push(`🎁 ${a.uin}:分享卡 ${r.cardUrl}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(`[QQ音乐] 账号 ${a.uin} 运行异常: ${e.message}`)
|
log(`[QQ音乐] 账号 ${a.uin} 运行异常: ${e.message}`)
|
||||||
blocks.push(`🔴 ${a.uin}:运行异常 ${e.message}`)
|
blocks.push(`🔴 ${a.uin}:运行异常 ${e.message}`)
|
||||||
|
|||||||
Reference in New Issue
Block a user