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:
@@ -24,6 +24,9 @@
|
||||
* QM_CACHE_DIR key 缓存目录覆盖(默认规则见上)
|
||||
* QM_PLAY_REPORT 任务20播放上报开关(默认 off;伪造统计数据属中风险,
|
||||
* 且需播放 XML 模板,本版未实现,仅挂 StartTask 标记)
|
||||
* QM_SHARE_CARD VIP 体验券(分享卡)开关,默认 off 只读探测不消耗配额。
|
||||
* on = 配额有剩余且本月卡已领完时自动 CreatCard,输出可分享的领取链接。
|
||||
* 卡是「赠送给非会员好友」的 3 天豪华绿钻,自己领不了自己的卡。
|
||||
*/
|
||||
'use strict'
|
||||
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 POLL_PROBE_MS = 15000 // 任务20 30005 时的一次性只读探测
|
||||
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) {
|
||||
let h = 5381
|
||||
@@ -436,9 +443,65 @@ class QMClient {
|
||||
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() {
|
||||
const out = { total: 0, dead: false }
|
||||
const out = { total: 0, dead: false, cardUrl: '' }
|
||||
try {
|
||||
await this.refresh()
|
||||
const st = await this.state()
|
||||
@@ -475,6 +538,11 @@ class QMClient {
|
||||
if (e instanceof QmDead) throw e // 失效语义不被福利站吞掉
|
||||
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}`)
|
||||
} catch (e) {
|
||||
if (e instanceof QmDead) {
|
||||
@@ -557,6 +625,7 @@ async function main() {
|
||||
blocks.push(r.dead
|
||||
? `🔴 ${a.uin}:登录态失效,请重新在网页抓 qm_keyst 更新 env`
|
||||
: `🟢 ${a.uin}:本次新增成长值 ${r.total}`)
|
||||
if (r.cardUrl) blocks.push(`🎁 ${a.uin}:分享卡 ${r.cardUrl}`)
|
||||
} catch (e) {
|
||||
log(`[QQ音乐] 账号 ${a.uin} 运行异常: ${e.message}`)
|
||||
blocks.push(`🔴 ${a.uin}:运行异常 ${e.message}`)
|
||||
|
||||
Reference in New Issue
Block a user