diff --git a/qqmusic/scrip_qqmusic_checkin.js b/qqmusic/scrip_qqmusic_checkin.js index dba98b0..c71271a 100644 --- a/qqmusic/scrip_qqmusic_checkin.js +++ b/qqmusic/scrip_qqmusic_checkin.js @@ -329,12 +329,12 @@ class QMClient { if (data.Ret === 0) { const gotN = Number(data.Total) const got = Number.isFinite(gotN) && gotN > 0 ? gotN : null - log(`[签到] 签到成功${got ? ` 成长值+${got}` : '(成长值实值见 App 明细)'}`) - this.step('任务中心签到', 'done', got ? `+${got}` : '已签') + log(`[任务中心·签到] 签到成功${got ? ` 成长值+${got}` : '(成长值实值见 App 明细)'}`) + this.step('任务中心·签到', 'done', got ? `+${got}` : '已签') return got || 0 } - log(`[签到] Ret:${data.Ret} ${data.Msg || ''}`) - this.step('任务中心签到', 'fail', `Ret ${data.Ret} ${data.Msg || ''}`.trim()) + log(`[任务中心·签到] Ret:${data.Ret} ${data.Msg || ''}`) + this.step('任务中心·签到', 'fail', `Ret ${data.Ret} ${data.Msg || ''}`.trim()) return 0 } @@ -342,7 +342,7 @@ class QMClient { async pendant() { const d = await this.api('music.vip.PendantUserSvr', 'QueryUserPendant', {}) const pid = (d.data || {}).pendant && d.data.pendant.id - if (!pid) { log('[挂件] 无挂件库存,任务8跳过'); this.step('挂件', 'skip', '无库存'); return } + if (!pid) { log('[任务中心·挂件] 无挂件库存,任务8跳过'); this.step('任务中心·挂件', 'skip', '无库存'); return } let r = await this.api('music.vip.PendantUserSvr', 'UsePendant', { id: Number(pid) }) let rc = Number((r.data || {}).retCode) if (rc === 42502) { // 已佩戴 → Cancel→Use 成对制造当日动作 @@ -350,8 +350,8 @@ class QMClient { r = await this.api('music.vip.PendantUserSvr', 'UsePendant', { id: Number(pid) }) rc = Number((r.data || {}).retCode) } - log(`[挂件] UsePendant retCode:${rc}`) - this.step('挂件', rc === 0 ? 'done' : 'warn', `retCode ${rc}`) + log(`[任务中心·挂件] UsePendant retCode:${rc}`) + this.step('任务中心·挂件', rc === 0 ? 'done' : 'warn', `retCode ${rc}`) } /** 任务9 装扮:UseCosmetic 当前使用中的装扮(cosmeticcgi 专用 comm) */ @@ -363,11 +363,11 @@ class QMClient { const d = await this.api('music.cosmeticcgi.UserCosmeticCgi', 'GetUserCosmetic', { category: 'subject' }, comm) const items = ((d.data || {}).cosmetics) || [] const cur = items.find(c => Number(c.state) === 1) || items[0] - if (!cur) { log('[装扮] subject 类目无装扮,任务9跳过'); this.step('装扮', 'skip', '无 subject 装扮'); return } + if (!cur) { log('[任务中心·装扮] subject 类目无装扮,任务9跳过'); this.step('任务中心·装扮', 'skip', '无 subject 装扮'); return } const r = await this.api('music.cosmeticcgi.UserCosmeticCgi', 'UseCosmetic', { category: 'subject', itemID: cur.id, manufacture: 'vivo' }, comm) - log(`[装扮] UseCosmetic code:${r.code} (id=${cur.id})`) - this.step('装扮', r.code === 0 ? 'done' : 'warn', `code ${r.code}`) + log(`[任务中心·装扮] UseCosmetic code:${r.code} (id=${cur.id})`) + this.step('任务中心·装扮', r.code === 0 ? 'done' : 'warn', `code ${r.code}`) } /** 领取循环:20020 冷却3s重领 / 30005 任务20只读探测一次 / 10006 写锁即停 / 30009 已领 */ @@ -377,19 +377,19 @@ class QMClient { for (const [tid, name] of CLAIMS) { const k = String(tid) const v = stNow[k] - if (v === 'Y') { log(`[任务] ${name}: 今日已完成`); this.step(`领奖·${name}`, 'skip', '今日已完成'); continue } + if (v === 'Y') { log(`[任务中心·领奖] ${name}: 今日已完成`); this.step(`领奖·${name}`, 'skip', '今日已完成'); continue } if (tid === 20 && v === 'W') { await this.api('music.lvz.LevelConfigSvr', 'StartTask', { ID: '20' }) // 领取钥匙 } let d = await this.api('music.lvz.LevelConfigSvr', 'IssueAward', { ID: k }) let code = d.code if (code === 20020) { - log(`[任务] ${name}: 20020 冷却,3s 后原地重领`) + log(`[任务中心·领奖] ${name}: 20020 冷却,3s 后原地重领`) await sleep(3000) d = await this.api('music.lvz.LevelConfigSvr', 'IssueAward', { ID: k }) code = d.code } else if (code === 30005 && tid === 20) { - log(`[任务] ${name}: 30005 未计次(本版无播放上报,仅 ${POLL_PROBE_MS / 1000}s 只读探测一次)`) + log(`[任务中心·领奖] ${name}: 30005 未计次(本版无播放上报,仅 ${POLL_PROBE_MS / 1000}s 只读探测一次)`) await sleep(POLL_PROBE_MS) const v2 = (await this.state())[k] if (v2 === 'W') { @@ -399,26 +399,26 @@ class QMClient { } else if (v2 === 'Y') { code = 30009 } else { - log(`[任务] ${name}: state 仍 ${v2 || 'N'},留待下次运行`) + log(`[任务中心·领奖] ${name}: state 仍 ${v2 || 'N'},留待下次运行`) this.step(`领奖·${name}`, 'warn', '未计次,待 App 内收听') continue } } if (code === 0) { const got = (d.data && d.data.DrawScores) || 0 - log(`[任务] ${name}: 领取成功 +${got}`) + log(`[任务中心·领奖] ${name}: 领取成功 +${got}`) total += got this.step(`领奖·${name}`, 'done', `+${got}`) await sleep(3000) // 成功发放后 ~2s 冷却,拉开下一个 } else if (code === 30009) { - log(`[任务] ${name}: 今日已完成`) + log(`[任务中心·领奖] ${name}: 今日已完成`) this.step(`领奖·${name}`, 'skip', '今日已完成') } else if (code === 10006) { - log(`[任务] ${name}: 10006 写操作锁,停止本账号领取(次日自动恢复)`) + log(`[任务中心·领奖] ${name}: 10006 写操作锁,停止本账号领取(次日自动恢复)`) this.step(`领奖·${name}`, 'warn', '10006 写锁,次日恢复') break } else { - log(`[任务] ${name}: code:${code}(未识别,留待下次运行)`) + log(`[任务中心·领奖] ${name}: code:${code}(未识别,留待下次运行)`) this.step(`领奖·${name}`, 'warn', `code ${code}`) } } @@ -442,29 +442,29 @@ class QMClient { monthDays = (d.data || {}).signInDays || signedCnt const today = days.find(x => x.day === day) if (today && today.signInStatus === 2) { - log(`[福利站] 今日已签到(本月 ${monthDays} 天)`) - this.step('福利站签到', 'skip', `今日已签(本月 ${monthDays} 天)`) + log(`[福利站·签到] 今日已签到(本月 ${monthDays} 天)`) + this.step('福利站·签到', 'skip', `今日已签(本月 ${monthDays} 天)`) } else { const r = await this.api('music.vipcgi.RenewalPlaySignInCgi', 'RenewalPlaySignIn', {}) if (r.code === 0) { const data = r.data || {} const name = (data.todayIncentive || {}).incentiveName || '奖励' - log(`[福利站] 签到成功:${name}(本月 ${monthDays + 1} 天)`) + log(`[福利站·签到] 签到成功:${name}(本月 ${monthDays + 1} 天)`) const m = /^(\d+)成长值/.exec(name) if (m) gained += Number(m[1]) - this.step('福利站签到', 'done', `${name}(本月 ${monthDays + 1} 天)`) + this.step('福利站·签到', 'done', `${name}(本月 ${monthDays + 1} 天)`) for (const mk of ['weekMilestoneIncentive', 'monthMilestoneIncentive']) { const inc = data[mk] if (inc && inc.incentiveName) { - log(`[福利站] ${mk.startsWith('week') ? '周' : '月'}里程碑达成:${inc.incentiveName}`) + log(`[福利站·${mk.startsWith('week') ? '周' : '月'}里程碑] 达成:${inc.incentiveName}`) } } } else if (r.code === 14822150) { - log(`[福利站] 今日已签到(幂等码,本月 ${monthDays} 天)`) - this.step('福利站签到', 'skip', '今日已签') + log(`[福利站·签到] 今日已签到(幂等码,本月 ${monthDays} 天)`) + this.step('福利站·签到', 'skip', '今日已签') } else { - log(`[福利站] 签到异常 code:${r.code}`) - this.step('福利站签到', 'fail', `code ${r.code}`) + log(`[福利站·签到] 异常 code:${r.code}`) + this.step('福利站·签到', 'fail', `code ${r.code}`) } } @@ -484,22 +484,22 @@ class QMClient { const pick = ok[day % ok.length] const r = await this.api('music.vipcgi.VipRenewalCouponCgi', 'ClaimVipRenewCoupon', { couponID: pick.couponID }) if (r.code === 0) { - log(`[礼券] ${pick.couponName} 领取成功 +${t502.taskReward || 10}心愿值`) - this.step('福利站礼券', 'done', pick.couponName) + log(`[福利站·礼券] ${pick.couponName} 领取成功 +${t502.taskReward || 10}心愿值`) + this.step('福利站·礼券', 'done', pick.couponName) } else if (r.code === 14816772) { - log('[礼券] 今日已领取') - this.step('福利站礼券', 'skip', '今日已领') + log('[福利站·礼券] 今日已领取') + this.step('福利站·礼券', 'skip', '今日已领') } else { - log(`[礼券] code:${r.code}`) - this.step('福利站礼券', 'warn', `code ${r.code}`) + log(`[福利站·礼券] code:${r.code}`) + this.step('福利站·礼券', 'warn', `code ${r.code}`) } } else { - log(`[礼券] 券池今日无可领券(${infos.length} 张全非可领态)`) - this.step('福利站礼券', 'skip', '券池无可领') + log(`[福利站·礼券] 券池今日无可领券(${infos.length} 张全非可领态)`) + this.step('福利站·礼券', 'skip', '券池无可领') } } else if (t502) { - log('[礼券] 今日已完成') - this.step('福利站礼券', 'skip', '今日已完成') + log('[福利站·礼券] 今日已完成') + this.step('福利站·礼券', 'skip', '今日已完成') } // ---- 旅程激励:心愿值达标档位实时领取(ClaimIncentives 幂等)---- @@ -508,19 +508,19 @@ class QMClient { if (unlock.length) { const r = await this.api('music.vipcgi.StarWishLotteryCgi', 'ClaimIncentives', { seasonID: season }) if (r.code === 0) { - log(`[激励] 领取成功:${unlock.map(x => `${x.name}(${x.score}分)`).join('、')}`) - this.step('旅程激励', 'done', unlock.map(x => x.name).join('、')) + log(`[福利站·激励] 领取成功:${unlock.map(x => `${x.name}(${x.score}分)`).join('、')}`) + this.step('福利站·旅程激励', 'done', unlock.map(x => x.name).join('、')) } else { - log(`[激励] code:${r.code}`) - this.step('旅程激励', 'warn', `code ${r.code}`) + log(`[福利站·激励] code:${r.code}`) + this.step('福利站·旅程激励', 'warn', `code ${r.code}`) } } else { const nxt = (idata.incentives || []).find(x => x.status === 5) if (nxt) { - log(`[激励] 心愿值${idata.currentScore || 0}/${nxt.score}(下档 ${nxt.name})`) - this.step('旅程激励', 'skip', `心愿值 ${idata.currentScore || 0}/${nxt.score}`) + log(`[福利站·激励] 心愿值${idata.currentScore || 0}/${nxt.score}(下档 ${nxt.name})`) + this.step('福利站·旅程激励', 'skip', `心愿值 ${idata.currentScore || 0}/${nxt.score}`) } else { - this.step('旅程激励', 'skip', '无待领档位') + this.step('福利站·旅程激励', 'skip', '无待领档位') } } @@ -534,15 +534,15 @@ class QMClient { if (signedCnt >= need || status === 2 || status === 3) { const r = await this.api('music.vipcgi.RenewalPlaySignInCgi', 'ClaimMilestoneIncentive', { month }) if (r.code === 0) { - log(`[月里程碑] 领取:${target.incentiveName}`) - this.step('月里程碑', 'done', target.incentiveName) + log(`[福利站·月里程碑] 领取:${target.incentiveName}`) + this.step('福利站·月里程碑', 'done', target.incentiveName) } else { - log(`[月里程碑] code:${r.code}(${signedCnt} 天达标待领)`) - this.step('月里程碑', 'warn', `code ${r.code}(${signedCnt} 天达标)`) + log(`[福利站·月里程碑] code:${r.code}(${signedCnt} 天达标待领)`) + this.step('福利站·月里程碑', 'warn', `code ${r.code}(${signedCnt} 天达标)`) } } else { - log(`[月里程碑] ${signedCnt}/${need} 天(${target.incentiveName})`) - this.step('月里程碑', 'skip', `${signedCnt}/${need} 天`) + log(`[福利站·月里程碑] ${signedCnt}/${need} 天(${target.incentiveName})`) + this.step('福利站·月里程碑', 'skip', `${signedCnt}/${need} 天`) } } @@ -554,18 +554,18 @@ class QMClient { if (p.poolStatus === 3 && (g.availableStock || 0) > 0) { const r = await this.api('music.vipcgi.VipRenewalMonthGiftCgi', 'ReceiveVipRenewalMonthlyGift', { giftPoolType: p.poolType, tab: 'cjhy' }) - if (r.code === 0) { log(`[月礼] 领取:${g.giftName}`); gotGifts.push(g.giftName) } - else { log(`[月礼] code:${r.code}`); noStock.push(`${g.giftName}(code ${r.code})`) } + if (r.code === 0) { log(`[福利站·月礼] 领取:${g.giftName}`); gotGifts.push(g.giftName) } + else { log(`[福利站·月礼] code:${r.code}`); noStock.push(`${g.giftName}(code ${r.code})`) } } else if (p.poolStatus === 3) { const ts = g.nextStockRefreshTime || 0 const nxt = ts ? new Date(ts * 1000).toTimeString().slice(0, 5) : '未知' - log(`[月礼] ${g.giftName} 今日库存0(${nxt} 补货)`) + log(`[福利站·月礼] ${g.giftName} 今日库存0(${nxt} 补货)`) noStock.push(`${g.giftName}(库存0)`) } } - if (gotGifts.length) this.step('月度好礼', 'done', gotGifts.join('、')) - else if (noStock.length) this.step('月度好礼', 'skip', noStock.join('、')) - else this.step('月度好礼', 'skip', '无可用礼品池') + if (gotGifts.length) this.step('福利站·月礼', 'done', gotGifts.join('、')) + else if (noStock.length) this.step('福利站·月礼', 'skip', noStock.join('、')) + else this.step('福利站·月礼', 'skip', '无可用礼品池') // ---- 臻享抽奖:有抽奖券才抽,抽到清零或失败即停 ---- const dd = (await this.api('music.vipcgi.VipRenewalLotteryCgi', 'QueryVipRenewDrawInfo', {})).data || {} @@ -575,18 +575,18 @@ class QMClient { const r = await this.api('music.vipcgi.VipRenewalLotteryCgi', 'DrawVipRenewActivity', { lotteryPageType: 1 }) if (r.code === 0) { const prize = ((r.data || {}).prize || {}).prizeName - log(`[抽奖] ${prize || '成功'}`) + log(`[福利站·抽奖] ${prize || '成功'}`) prizes.push(prize || '成功') draws -= 1 } else { - log(`[抽奖] code:${r.code},停止`) + log(`[福利站·抽奖] code:${r.code},停止`) break } } - if (prizes.length) this.step('臻享抽奖', 'done', `${prizes.length} 次:${prizes.join('、')}`) - else this.step('臻享抽奖', 'skip', '无抽奖券') + if (prizes.length) this.step('福利站·臻享抽奖', 'done', `${prizes.length} 次:${prizes.join('、')}`) + else this.step('福利站·臻享抽奖', 'skip', '无抽奖券') - log(`[福利站] ${ymd} 巡检完成`) + log(`[福利站] ${ymd} 巡检完成(签到/礼券/激励/里程碑/月礼/抽奖)`) return gained } @@ -601,15 +601,16 @@ class QMClient { * 不做的:金币抽奖(drawCoinLottery,消耗金币抽奖券)、红包雨、每日任务列表、 * 定时金币 —— 那些要么花金币要么要伪造播放/浏览行为,风险与收益不成比例。 */ async coinCenter() { - if (!COIN_SIGN) { this.step('金币签到', 'skip', 'QM_COIN_SIGN=off'); return 0 } + if (!COIN_SIGN) { this.step('金币·签到', 'skip', 'QM_COIN_SIGN=off'); return 0 } + log(`[金币·签到] 开始:ActID=${COIN_ACT_ID} ScenesID=${COIN_SCENE_ID}`) let st = await this.coinSignState(COIN_ACT_ID, COIN_SCENE_ID) if (!st.ok) { // ActID 会随活动换季失效。这里不打 fail(不是登录态问题,也不是脚本 bug), // 用 warn 并把「改哪个 env」写进 detail,看到通知就知道怎么修。 - log(`[金币] 状态查询失败 code=${st.sCode}/${st.tCode} ret=${st.sRet}/${st.tRet}`) - log(`[金币] 若持续失败,多半是活动换季导致 ActID 失效 —— 抓包看 ActID,改 QM_COIN_ACT_ID`) - this.step('金币签到', 'warn', `查询失败 code ${st.sCode}/${st.tCode}(可能 ActID 过期)`) + log(`[金币·签到] 状态查询失败 code=${st.sCode}/${st.tCode} ret=${st.sRet}/${st.tRet}`) + log(`[金币·签到] 若持续失败,多半是活动换季导致 ActID 失效 —— 抓包看 ActID,改 QM_COIN_ACT_ID`) + this.step('金币·签到', 'warn', `查询失败 code ${st.sCode}/${st.tCode}(可能 ActID 过期)`) return 0 } @@ -620,14 +621,14 @@ class QMClient { const okSign = d.code === 0 && req.code === 0 && Number(data.retCode ?? 0) === 0 && data.Info && data.Info.IsSignIn if (!okSign) { - log(`[金币] 签到失败 code=${req.code} ret=${data.retCode}`) - this.step('金币签到', 'warn', `签到失败 code ${req.code} ret ${data.retCode}`) + log(`[金币·签到] 签到失败 code=${req.code} ret=${data.retCode}`) + this.step('金币·签到', 'warn', `签到失败 code ${req.code} ret ${data.retCode}`) return 0 } signedNow = true - log('[金币] 签到成功') + log('[金币·签到] 签到成功') st = await this.coinSignState(COIN_ACT_ID, COIN_SCENE_ID) - if (!st.ok) { this.step('金币签到', 'done', '已签(状态复查失败)'); return 0 } + if (!st.ok) { this.step('金币·签到', 'done', '已签(状态复查失败)'); return 0 } } // 连续签到第几天 → 用于在 TaskList 里定位「今天该领的那个」 @@ -649,27 +650,28 @@ class QMClient { // 100004 = 已领过(幂等),算成功 const okAward = d.code === 0 && req.code === 0 && (ret === 0 || ret === 100004) if (okAward) { - log(`[金币] ${signedNow ? '签到并领取' : '补领'}成功${rewardName ? `:${rewardName}` : ''}`) - this.step('金币签到', 'done', `${signedNow ? '已签 + ' : ''}${rewardName || '已领'}`) + log(`[金币·签到] ${signedNow ? '签到并领取' : '补领'}成功${rewardName ? `:${rewardName}` : ''}`) + this.step('金币·签到', 'done', `${signedNow ? '已签 + ' : ''}${rewardName || '已领'}`) } else { - log(`[金币] 领奖失败 code=${req.code} ret=${ret}`) - this.step('金币签到', 'warn', `已签,领奖失败 ret ${ret}`) + log(`[金币·签到] 领奖失败 code=${req.code} ret=${ret}`) + this.step('金币·签到', 'warn', `已签,领奖失败 ret ${ret}`) } } else if (st.info.IsSignIn) { const nth = day ? `连续第 ${day} 天` : '' - log(`[金币] 今日已签到${nth ? `(${nth})` : ''}`) - this.step('金币签到', 'skip', `今日已签${nth ? `(${nth})` : ''}`) + log(`[金币·签到] 今日已签到${nth ? `(${nth})` : ''}`) + this.step('金币·签到', 'skip', `今日已签${nth ? `(${nth})` : ''}`) } else { - log('[金币] 签到状态未确认') - this.step('金币签到', 'warn', '签到状态未确认') + log('[金币·签到] 签到状态未确认') + this.step('金币·签到', 'warn', '签到状态未确认') } // ---- 金币抽奖签到(另一个 ActID,独立活动)---- // 只做「签到 + 领奖」,不碰 drawCoinLottery(那个要消耗抽奖券去抽,属于花金币) try { + log(`[金币·抽奖签到] 开始:ActID=${LOTTERY_ACT_ID}`) let ls = await this.coinSignState(LOTTERY_ACT_ID, '') if (!ls.ok) { - log(`[金币抽奖] 状态查询失败 code=${ls.sCode}/${ls.tCode},跳过`) + log(`[金币·抽奖签到] 状态查询失败 code=${ls.sCode}/${ls.tCode},跳过`) } else { let lotterySigned = false if (!ls.info.IsSignIn) { @@ -688,23 +690,23 @@ class QMClient { const req = d.req_0 || {}, data = req.data || {} const ret = Number(data.retCode ?? 0) if (d.code === 0 && req.code === 0 && (ret === 0 || ret === 100004)) { - this.step('金币抽奖签到', 'done', lotterySigned ? '已签 + 已领' : '已领') + this.step('金币·抽奖签到', 'done', lotterySigned ? '已签 + 已领' : '已领') } else { - this.step('金币抽奖签到', 'warn', `领奖失败 ret ${ret}`) + this.step('金币·抽奖签到', 'warn', `领奖失败 ret ${ret}`) } } else if (lotterySigned) { // 签了但当场没查到可领项(接口有延迟)→ 也记一笔,别让「做过的事」在通知里消失 - this.step('金币抽奖签到', 'done', '已签(奖励待下次领)') + this.step('金币·抽奖签到', 'done', '已签(奖励待下次领)') } else if (ls.ok && ls.info.IsSignIn) { - this.step('金币抽奖签到', 'skip', '今日已签') + this.step('金币·抽奖签到', 'skip', '今日已签') } else { - this.step('金币抽奖签到', 'skip', '无需处理') + this.step('金币·抽奖签到', 'skip', '无需处理') } } } catch (e) { if (e instanceof QmDead) throw e - log(`[金币抽奖] 异常: ${e.message}`) - this.step('金币抽奖签到', 'warn', e.message) + log(`[金币·抽奖签到] 异常: ${e.message}`) + this.step('金币·抽奖签到', 'warn', e.message) } return 0 // 金币不计入成长值合计 @@ -801,7 +803,7 @@ class QMClient { return 0 // 不产成长值,不计入 total } - /** 单账号全流程:续期 → 签到 → 任务动作 → 全量领取 → 福利站 → 分享卡 */ + /** 单账号全流程:续期 → 任务中心 → 福利站 → 金币中心 → 分享卡 */ async run() { const out = { total: 0, dead: false, cardUrl: '' } try { @@ -810,53 +812,60 @@ class QMClient { const signed = await this.signQry() log(`[状态] 签到:${signed ? '已签' : '未签'} 挂件:${st['8']} 装扮:${st['9']} 听书:${st['30']} 节目:${st['20']}`) - if (signed) { log('[签到] 今日已签到'); this.step('任务中心签到', 'skip', '今日已签') } + // 三套签到体系各自独立,用段落标题分开 —— 日志里一眼能看出哪套做了什么 + log('\n───── ① 任务中心(成长值)─────') + if (signed) { log('[任务中心·签到] 今日已签到'); this.step('任务中心·签到', 'skip', '今日已签') } else out.total += await this.signDaily() // 动作语义:N=执行动作 / W=只领(能否领取决于行为已计次)/ Y=零请求(防风控) if (st['8'] === 'N') await this.pendant() - else this.step('挂件', 'skip', '今日无需动作') + else this.step('任务中心·挂件', 'skip', '今日无需动作') if (st['9'] === 'N') await this.cosmetic() - else this.step('装扮', 'skip', '今日无需动作') + else this.step('任务中心·装扮', 'skip', '今日无需动作') if (st['30'] === 'N') { await this.api('music.lvz.LevelConfigSvr', 'RecordBehavior', { ID: '30' }) - log('[听书] 行为上报完成') - this.step('听书行为', 'done', '已上报') + log('[任务中心·听书] 行为上报完成') + this.step('任务中心·听书', 'done', '已上报') } else { - this.step('听书行为', 'skip', '今日无需动作') + this.step('任务中心·听书', 'skip', '今日无需动作') } if (st['20'] === 'N' || !signed) { // 任务20跨天不重置:当日未签时 20:Y 是昨日残留,仍挂开始标记 const r = (await this.api('music.lvz.LevelConfigSvr', 'StartTask', { ID: '20' })).data || {} if (Number(r.code) === 1) { log(PLAY_REPORT - ? '[节目] 开始标记已挂(播放上报未实现于本版,App 内正常收听计入)' - : '[节目] 开始标记已挂(播放上报未启用,App 内正常收听计入)') - if (r.count != null) log(`[节目] 当前播放计数: ${r.count}`) - this.step('节目开始标记', 'done', r.count != null ? `计数 ${r.count}` : '已挂') + ? '[任务中心·节目] 开始标记已挂(播放上报未实现于本版,App 内正常收听计入)' + : '[任务中心·节目] 开始标记已挂(播放上报未启用,App 内正常收听计入)') + if (r.count != null) log(`[任务中心·节目] 当前播放计数: ${r.count}`) + this.step('任务中心·节目', 'done', r.count != null ? `计数 ${r.count}` : '已挂') } else { - log(`[节目] StartTask 返回 code:${r.code},无法开始`) - this.step('节目开始标记', 'warn', `code ${r.code}`) + log(`[任务中心·节目] StartTask 返回 code:${r.code},无法开始`) + this.step('任务中心·节目', 'warn', `code ${r.code}`) } } else { - this.step('节目开始标记', 'skip', '今日无需动作') + this.step('任务中心·节目', 'skip', '今日无需动作') } const stNow = await this.state() out.total += await this.claimAll(stNow) + log('\n───── ② 会员福利站(成长值 + 权益)─────') try { out.total += await this.vipStation() } catch (e) { if (e instanceof QmDead) throw e // 失效语义不被福利站吞掉 log(`[福利站] 运行异常: ${e.message}`) this.step('福利站', 'fail', e.message) } + // 金币中心:独立协议(musicu.fcg + 小程序 comm),给金币不给成长值。 // 放在福利站之后、分享卡之前 —— 金币体系挂了不影响前两套,也不拖累取卡。 + log('\n───── ③ 金币中心(金币,不计入成长值)─────') try { await this.coinCenter() } catch (e) { if (e instanceof QmDead) throw e - log(`[金币] 运行异常: ${e.message}`) - this.step('金币签到', 'fail', e.message) + log(`[金币·签到] 运行异常: ${e.message}`) + this.step('金币·签到', 'fail', e.message) } + + log('\n───── ④ VIP 体验券(分享卡)─────') try { await this.shareCard() } catch (e) { if (e instanceof QmDead) throw e // 登录态失效照常上抛 log(`[分享卡] 运行异常: ${e.message}`) // 接口改版/风控不拖垮主流程 @@ -865,7 +874,8 @@ class QMClient { out.cardUrl = this.shareCardUrl || '' out.steps = this.steps out.verdict = this.verdict() - log(`[QQ音乐] 本次新增成长值合计: ${out.total}`) + log(`\n[QQ音乐] 本次新增成长值合计: ${out.total}`) + log(`[QQ音乐] 金币中心为独立体系,不产成长值(明细见上方 ③ 段)`) } catch (e) { if (e instanceof QmDead) { out.dead = true @@ -984,30 +994,47 @@ function pushCardResult(results) { * 全部 skip 的项折叠成一行,避免通知被十几行「今日已完成」淹没。 */ function renderAccount(uin, r) { const icon = { done: '✓', warn: '△', fail: '✗', skip: '·' } - // 金币体系的 step 名字都以「金币」开头 —— 单独成组,别混进成长值里让人误读 - const isCoin = s => /^金币/.test(s.name) - const coin = r.steps.filter(isCoin) - const rest = r.steps.filter(s => !isCoin(s)) - const did = rest.filter(s => s.status === 'done') - const warn = rest.filter(s => s.status === 'warn' || s.status === 'fail') - const skip = rest.filter(s => s.status === 'skip') + // step 名统一成「体系·子项」两段(2026-11-07 改),按体系分组渲染 —— + // 通知里必须一眼看出「哪个签到完成了」,不能把所有 done 平铺成一堆。 + const group = s => { + const m = /^(任务中心|福利站|金币|分享卡)/.exec(s.name) + return m ? m[1] : '其他' + } + const line = s => ` ${icon[s.status] || '·'} ${s.name.replace(/^[^·]+·/, '')}${s.detail ? ` ${s.detail}` : ''}` + const lines = [`${r.verdict === 'done' ? '🟢' : '🟡'} ${uin}:成长值 +${r.total}`] - for (const s of did) lines.push(` ${icon.done} ${s.name}${s.detail ? ` ${s.detail}` : ''}`) - for (const s of warn) lines.push(` ${icon[s.status]} ${s.name}${s.detail ? ` ${s.detail}` : ''}`) - if (skip.length) { - lines.push(` ${icon.skip} 无需处理 ${skip.length} 项:${skip.map(s => s.name).join('、')}`) - } - // 金币单独一行:它不加成长值,混在上面会被当成成长值的一部分 - if (coin.length) { - const doneCoin = coin.filter(s => s.status === 'done') - const badCoin = coin.filter(s => s.status === 'warn' || s.status === 'fail') - if (doneCoin.length) { - lines.push(` 💰 金币:${doneCoin.map(s => `${s.name.replace(/^金币/, '') || '签到'}${s.detail ? ` ${s.detail}` : ''}`).join(' / ')}`) + + const ORDER = [ + ['任务中心', '📈 任务中心(成长值)'], + ['福利站', '🎁 会员福利站(成长值 + 权益)'], + ['金币', '💰 金币中心(金币,不计成长值)'], + ['分享卡', '🎫 VIP 体验券'], + ['其他', '📌 其他'], + ] + + for (const [key, title] of ORDER) { + const items = r.steps.filter(s => group(s) === key) + if (!items.length) continue + + const done = items.filter(s => s.status === 'done') + const bad = items.filter(s => s.status === 'warn' || s.status === 'fail') + const skip = items.filter(s => s.status === 'skip') + + // 整个体系都没做事(全 skip)→ 收成一行,别占屏 + if (!done.length && !bad.length && skip.length) { + lines.push(` ${title}:${skip.map(s => s.name.replace(/^[^·]+·/, '')).join('、')} 今日无需处理`) + continue + } + + lines.push(` ${title}`) + for (const s of done) lines.push(line(s)) + for (const s of bad) lines.push(line(s)) + if (skip.length) { + lines.push(` · 无需处理 ${skip.length} 项:${skip.map(s => s.name.replace(/^[^·]+·/, '')).join('、')}`) } - for (const s of badCoin) lines.push(` ${icon[s.status]} ${s.name}${s.detail ? ` ${s.detail}` : ''}`) - if (!doneCoin.length && !badCoin.length) lines.push(` · 金币:今日已签`) } - if (r.cardUrl) lines.push(` 🎁 分享卡 ${r.cardUrl}`) + + if (r.cardUrl) lines.push(` 🔗 领卡链接 ${r.cardUrl}`) return lines.join('\n') } diff --git a/qqmusic/test_coin.js b/qqmusic/test_coin.js index 14487a5..1804394 100644 --- a/qqmusic/test_coin.js +++ b/qqmusic/test_coin.js @@ -131,10 +131,10 @@ async function runCase(name, script, want) { ok(`返回 0(金币不计入成长值)`, t === 0, String(t)) for (const [k, v] of Object.entries(want)) { if (k === 'status') { - const s = c.steps.find(x => x.name === '金币签到') + const s = c.steps.find(x => x.name === '金币·签到') ok(`step 存在且 status=${v}`, s && s.status === v, JSON.stringify(c.steps.map(x => `${x.name}:${x.status}`))) } else if (k === 'has') { - const s = c.steps.find(x => x.name === '金币签到') + const s = c.steps.find(x => x.name === '金币·签到') ok(`detail 含「${v}」`, s && s.detail.includes(v), s && s.detail) } else if (k === 'calls') { ok(`请求次数 = ${v}`, calls.length === v, String(calls.length)) @@ -223,7 +223,7 @@ async function runCase(name, script, want) { } const c = mkClient() await c.coinCenter() - const s = c.steps.find(x => x.name === '金币抽奖签到') + const s = c.steps.find(x => x.name === '金币·抽奖签到') ok('金币抽奖签到有独立 step', !!s, JSON.stringify(c.steps.map(x => x.name))) } diff --git a/qqmusic/test_render.js b/qqmusic/test_render.js new file mode 100644 index 0000000..76f6b72 --- /dev/null +++ b/qqmusic/test_render.js @@ -0,0 +1,113 @@ +#!/usr/bin/env node +/** + * test_render.js —— 通知正文渲染的分组与「哪个签到完成了」可见性 + * + * 2026-11-07 需求:执行日志/通知里必须一眼看出「是什么签到完成了」。 + * 这个测试冻结 renderAccount 的分组契约: + * 1. 三套体系各自成组,组标题带 emoji + * 2. 组内每条都带「子项名 + detail」,不能只剩 detail + * 3. 整组全 skip → 收成一行 + * 4. 金币组明确标注「不计成长值」,不与成长值混算 + * 5. 分享卡链接单独一行 + * + * 用法: node qqmusic/test_render.js + */ +'use strict' +const { renderAccount } = require('./scrip_qqmusic_checkin.js') + +let pass = 0, fail = 0 +const ok = (n, c, e = '') => { + if (c) { pass++; console.log(` ✓ ${n}`) } + else { fail++; console.log(` ✗ ${n}${e ? ' — ' + e : ''}`) } +} +const has = (out, s, label) => ok(label || `含「${s}」`, out.includes(s), out) +const not = (out, s, label) => ok(label || `不含「${s}」`, !out.includes(s), out) + +/* ── 1. 三套体系都有收获 ── */ +console.log('\n── 1. 三套体系都有收获 ──') +const r1 = renderAccount('1006764347', { + total: 45, verdict: 'done', cardUrl: '', + steps: [ + { name: '任务中心·签到', status: 'done', detail: '+15' }, + { name: '任务中心·挂件', status: 'done', detail: '已佩戴' }, + { name: '任务中心·装扮', status: 'skip', detail: '' }, + { name: '福利站·签到', status: 'done', detail: '15成长值(本月 8 天)' }, + { name: '福利站·礼券', status: 'done', detail: '+10 心愿值' }, + { name: '福利站·月礼', status: 'skip', detail: '' }, + { name: '金币·签到', status: 'done', detail: '已签 + 30 金币' }, + { name: '金币·抽奖签到', status: 'done', detail: '已签 + 已领' }, + ], +}) +console.log(r1.split('\n').map(l => ' │ ' + l).join('\n')) + +has(r1, '📈 任务中心', '有任务中心组标题') +has(r1, '🎁 会员福利站', '有福利站组标题') +has(r1, '💰 金币中心', '有金币中心组标题') +has(r1, '不计成长值', '金币组标注「不计成长值」') +has(r1, '✓ 签到 +15', '任务中心签到带子项名 + detail') +has(r1, '✓ 礼券 +10 心愿值', '福利站礼券带子项名 + detail') +has(r1, '✓ 签到 已签 + 30 金币', '金币签到带子项名 + detail') +has(r1, '✓ 抽奖签到', '金币抽奖签到也在') +has(r1, '无需处理 1 项:装扮', '任务中心 skip 收成一行') +has(r1, '无需处理 1 项:月礼', '福利站 skip 收成一行') +// 关键:不能出现「只有 detail 没有名字」的行 +const detailOnly = r1.split('\n').filter(l => /^\s+[✓△✗·] (已签|\+|已领|已佩戴)/.test(l)) +ok('没有「只有 detail 没有子项名」的行', detailOnly.length === 0, JSON.stringify(detailOnly)) + +/* ── 2. 整组全 skip → 收成一行 ── */ +console.log('\n── 2. 金币全已签(整组 skip)──') +const r2 = renderAccount('1006764347', { + total: 30, verdict: 'done', cardUrl: '', + steps: [ + { name: '任务中心·签到', status: 'done', detail: '+15' }, + { name: '金币·签到', status: 'skip', detail: '今日已签(连续第 7 天)' }, + { name: '金币·抽奖签到', status: 'skip', detail: '今日已签' }, + ], +}) +console.log(r2.split('\n').map(l => ' │ ' + l).join('\n')) +has(r2, '💰 金币中心', '金币组还在') +has(r2, '今日无需处理', '整组 skip 收成一行') +has(r2, '签到、抽奖签到', '两个子项都列出来了') +not(r2, '✓ 签到 今日已签', '没有多余的成功行') + +/* ── 3. 金币 ActID 过期 → warn 单独列 ── */ +console.log('\n── 3. 金币 ActID 过期 ──') +const r3 = renderAccount('1006764347', { + total: 30, verdict: 'warn', cardUrl: '', + steps: [ + { name: '任务中心·签到', status: 'done', detail: '+15' }, + { name: '金币·签到', status: 'warn', detail: '查询失败 code -1/-1(可能 ActID 过期)' }, + ], +}) +console.log(r3.split('\n').map(l => ' │ ' + l).join('\n')) +has(r3, '🟡', 'verdict=warn 用黄脸') +has(r3, '△ 签到 查询失败', 'warn 带子项名 + detail') +has(r3, 'ActID', 'detail 里能看出是 ActID 问题') + +/* ── 4. 分享卡链接 ── */ +console.log('\n── 4. 分享卡链接 ──') +const r4 = renderAccount('1006764347', { + total: 30, verdict: 'done', cardUrl: 'https://y.qq.com/m/vip/experience_card/dist/index.html?cardid=ABC', + steps: [{ name: '分享卡', status: 'done', detail: '新建 1 张,还能被领 3 次' }], +}) +console.log(r4.split('\n').map(l => ' │ ' + l).join('\n')) +has(r4, '🔗 领卡链接 https://y.qq.com', '链接单独一行且有前缀') +has(r4, '🎫 VIP 体验券', '有分享卡组标题') + +/* ── 5. 空 steps 不崩 ── */ +console.log('\n── 5. 空 steps ──') +const r5 = renderAccount('1006764347', { total: 0, verdict: 'done', cardUrl: '', steps: [] }) +ok('不抛错', typeof r5 === 'string') +has(r5, '成长值 +0', '首行仍有成长值') + +/* ── 6. 未知前缀落到「其他」组 ── */ +console.log('\n── 6. 未知前缀 → 其他组 ──') +const r6 = renderAccount('1006764347', { + total: 0, verdict: 'done', cardUrl: '', + steps: [{ name: '某个新功能', status: 'done', detail: 'ok' }], +}) +has(r6, '📌 其他', '落到「其他」组') +has(r6, '某个新功能', '名字保留完整(没有 · 可切)') + +console.log(`\nRESULT: ${fail === 0 ? 'PASS' : `FAIL(${fail})`} (${pass} passed, ${fail} failed)`) +process.exit(fail === 0 ? 0 : 1)