diff --git a/qqmusic/README.md b/qqmusic/README.md index 4bcc23c..06f59fc 100644 --- a/qqmusic/README.md +++ b/qqmusic/README.md @@ -15,6 +15,62 @@ QQ音乐会员成长值任务 + 会员福利站 自动签到,青龙面板 Node | `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= +``` + +链接发给好友,对方打开后自己点「立即领取」即可(脚本不代领,代领会撞 `100205`/`100210`)。 + +**默认 off 的理由**:`CreatCard` 会真的占掉一张配额,而卡是给人送的、不是自己用的 —— +自动建卡只会堆出没人领的废卡。所以默认只报数,要送人时手动置 `QM_SHARE_CARD=on` 跑一次。 ## 凭证获取(web 抓一次,之后无限续) @@ -62,6 +118,7 @@ cron: 1 0,6 * * * |---|---|---| | 任务中心 | 签到(15) / 挂件(5) / 装扮(5) / 听书(5) / 节目StartTask | 低(等价 App 点击) | | 会员福利站 | 每日签到(15) / 礼券(+10心愿值) / 旅程激励 / 月里程碑 / 月度好礼 / 臻享抽奖 | 低(先探后做,无硬编码 ID) | +| VIP 体验券 | 分享卡配额查询 / 建卡(`QM_SHARE_CARD=on`) | 低(只读默认,不消耗配额) | | 播放上报 | 任务20 完整 +10 需 App 正常收听(伪造统计默认关) | 中(默认关闭) | | 支付流水任务 | 4/5/6/12/14/18/21 | 不可伪造,不碰 | diff --git a/qqmusic/scrip_qqmusic_checkin.js b/qqmusic/scrip_qqmusic_checkin.js index b09ac49..45d5f76 100644 --- a/qqmusic/scrip_qqmusic_checkin.js +++ b/qqmusic/scrip_qqmusic_checkin.js @@ -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}`)