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