feat(qqmusic): 通知改成逐项明细 + 结论标题
之前通知只有一行「本次新增成长值 N」,看不出这轮到底做了什么、 哪些没做成、要不要人工介入。 改动: - QMClient 新增 steps 数组与 step(name,status,detail) 记录点, status 四态:done=做到 / warn=部分完成留待下次 / fail=失败 / skip=无需处理(今日已完成或未开放) - verdict() 汇总:有 fail→fail,有 warn→warn,其余 done - 每个动作补上 step():任务中心签到、挂件、装扮、听书行为、 节目开始标记、四项领奖、福利站签到/礼券/旅程激励/月里程碑/ 月度好礼/臻享抽奖、分享卡 - 新增 renderAccount():done 逐行列,warn/fail 逐行列,skip 折叠成 一行(避免十几个「今日已完成」淹没有效信息),分享卡链接单独一行 - 通知标题按结论给:✅ 全部完成 / ⚠️ 有未完成项 - 登录态失效保持 🔴 单列并直接给处理指引,不进明细列表 设计取舍:skip 折叠而不是省略 —— 折叠能看出「今天该做的都做了」, 省略会让人怀疑是不是漏跑了。done/warn/fail 永不折叠。 验证:node --check / 签名对拍 6 组 PASS / verdict 三态用例 / 渲染两例(含卡片链接与不含)输出符合预期 / _probe_meta 三元数据 PASS
This commit is contained in:
@@ -140,6 +140,45 @@ cron: 1 0,6 * * *
|
|||||||
- `https.request` 的选项用 `host:`(Node 的 `hostname` 别名)而不是 `hostname:`
|
- `https.request` 的选项用 `host:`(Node 的 `hostname` 别名)而不是 `hostname:`
|
||||||
- cron 注解独占一行
|
- cron 注解独占一行
|
||||||
|
|
||||||
|
## 通知内容
|
||||||
|
|
||||||
|
每轮跑完推送一条,格式固定:
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ QQ音乐签到 · 全部完成
|
||||||
|
|
||||||
|
🟢 123456789:成长值 +35
|
||||||
|
✓ 任务中心签到 +15
|
||||||
|
✓ 挂件 retCode 0
|
||||||
|
✓ 领奖·头像挂件 +10
|
||||||
|
✓ 福利站签到 5成长值(本月 12 天)
|
||||||
|
△ 领奖·收听节目 未计次,待 App 内收听
|
||||||
|
· 无需处理 3 项:福利站礼券、月里程碑、分享卡
|
||||||
|
🎁 分享卡 https://y.qq.com/m/vip/experience_card/dist/index.html?cardid=xxx
|
||||||
|
|
||||||
|
———
|
||||||
|
轮次: 00:01 / 06:02 双轮(任务20跨天滚动 + 福利站兜底)
|
||||||
|
节目任务(20)依赖 App 内正常收听计入(播放上报未启用)
|
||||||
|
时间: 2026/11/4 23:10:05
|
||||||
|
```
|
||||||
|
|
||||||
|
**标题就是结论**:`✅ 全部完成` / `⚠️ 有未完成项`,不用点开就知道要不要管。
|
||||||
|
|
||||||
|
每个账号下分四类:
|
||||||
|
|
||||||
|
| 符号 | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| `✓` | 做到了(带结果,如 `+15`、`retCode 0`) |
|
||||||
|
| `△` | 部分完成 / 留待下次(如任务20未计次、写锁) |
|
||||||
|
| `✗` | 失败(接口报错、code 非预期) |
|
||||||
|
| `·` | 无需处理(今日已完成 / 未开放),**折叠成一行**,不占版面 |
|
||||||
|
|
||||||
|
判定规则:有任何 `✗` → 标题变 `⚠️`;只有 `△` → 仍算完成(有遗留但不影响);
|
||||||
|
全 `✓`/`·` → `✅`。登录态失效单独用 🔴 并直接给处理指引,不进明细列表。
|
||||||
|
|
||||||
|
明细来源是 `QMClient.steps`:每个动作调一次 `step(name, status, detail)`,
|
||||||
|
`verdict()` 汇总。新增任务时记得补一句 `step()`,否则它在通知里不可见。
|
||||||
|
|
||||||
## 任务覆盖
|
## 任务覆盖
|
||||||
|
|
||||||
| 层 | 内容 | 风险 |
|
| 层 | 内容 | 风险 |
|
||||||
|
|||||||
@@ -124,6 +124,21 @@ class QMClient {
|
|||||||
this.rescue = { openid: '', access_token: '' } // QQ 互联二级救活凭证
|
this.rescue = { openid: '', access_token: '' } // QQ 互联二级救活凭证
|
||||||
this.key = key
|
this.key = key
|
||||||
this.loadCache() // 缓存 key 优先于 env(qmcommon 同语义)
|
this.loadCache() // 缓存 key 优先于 env(qmcommon 同语义)
|
||||||
|
this.steps = [] // 本次执行明细,供汇总通知渲染
|
||||||
|
this.shareCardUrl = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 记录一步执行结果。status: done=做到 / skip=无需做(已完成或未开放)
|
||||||
|
* warn=部分完成(留待下次)/ fail=失败。只进通知,不替代 log。 */
|
||||||
|
step(name, status, detail) {
|
||||||
|
this.steps.push({ name, status, detail: String(detail ?? '') })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 本次是否「完成」:有 fail 就是未完成,有 warn 是部分完成,其余算完成 */
|
||||||
|
verdict() {
|
||||||
|
if (this.steps.some(s => s.status === 'fail')) return 'fail'
|
||||||
|
if (this.steps.some(s => s.status === 'warn')) return 'warn'
|
||||||
|
return 'done'
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCache() {
|
loadCache() {
|
||||||
@@ -235,9 +250,11 @@ class QMClient {
|
|||||||
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}` : '已签')
|
||||||
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())
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +262,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跳过'); 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 成对制造当日动作
|
||||||
@@ -254,6 +271,7 @@ class QMClient {
|
|||||||
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}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 任务9 装扮:UseCosmetic 当前使用中的装扮(cosmeticcgi 专用 comm) */
|
/** 任务9 装扮:UseCosmetic 当前使用中的装扮(cosmeticcgi 专用 comm) */
|
||||||
@@ -265,10 +283,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跳过'); 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}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 领取循环:20020 冷却3s重领 / 30005 任务20只读探测一次 / 10006 写锁即停 / 30009 已领 */
|
/** 领取循环:20020 冷却3s重领 / 30005 任务20只读探测一次 / 10006 写锁即停 / 30009 已领 */
|
||||||
@@ -278,7 +297,7 @@ 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}: 今日已完成`); 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' }) // 领取钥匙
|
||||||
}
|
}
|
||||||
@@ -301,6 +320,7 @@ class QMClient {
|
|||||||
code = 30009
|
code = 30009
|
||||||
} else {
|
} else {
|
||||||
log(`[任务] ${name}: state 仍 ${v2 || 'N'},留待下次运行`)
|
log(`[任务] ${name}: state 仍 ${v2 || 'N'},留待下次运行`)
|
||||||
|
this.step(`领奖·${name}`, 'warn', '未计次,待 App 内收听')
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,14 +328,18 @@ class QMClient {
|
|||||||
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}`)
|
||||||
await sleep(3000) // 成功发放后 ~2s 冷却,拉开下一个
|
await sleep(3000) // 成功发放后 ~2s 冷却,拉开下一个
|
||||||
} else if (code === 30009) {
|
} else if (code === 30009) {
|
||||||
log(`[任务] ${name}: 今日已完成`)
|
log(`[任务] ${name}: 今日已完成`)
|
||||||
|
this.step(`领奖·${name}`, 'skip', '今日已完成')
|
||||||
} else if (code === 10006) {
|
} else if (code === 10006) {
|
||||||
log(`[任务] ${name}: 10006 写操作锁,停止本账号领取(次日自动恢复)`)
|
log(`[任务] ${name}: 10006 写操作锁,停止本账号领取(次日自动恢复)`)
|
||||||
|
this.step(`领奖·${name}`, 'warn', '10006 写锁,次日恢复')
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
log(`[任务] ${name}: code:${code}(未识别,留待下次运行)`)
|
log(`[任务] ${name}: code:${code}(未识别,留待下次运行)`)
|
||||||
|
this.step(`领奖·${name}`, 'warn', `code ${code}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return total
|
return total
|
||||||
@@ -339,6 +363,7 @@ class QMClient {
|
|||||||
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} 天)`)
|
||||||
} 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) {
|
||||||
@@ -347,6 +372,7 @@ class QMClient {
|
|||||||
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} 天)`)
|
||||||
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) {
|
||||||
@@ -355,8 +381,10 @@ class QMClient {
|
|||||||
}
|
}
|
||||||
} else if (r.code === 14822150) {
|
} else if (r.code === 14822150) {
|
||||||
log(`[福利站] 今日已签到(幂等码,本月 ${monthDays} 天)`)
|
log(`[福利站] 今日已签到(幂等码,本月 ${monthDays} 天)`)
|
||||||
|
this.step('福利站签到', 'skip', '今日已签')
|
||||||
} else {
|
} else {
|
||||||
log(`[福利站] 签到异常 code:${r.code}`)
|
log(`[福利站] 签到异常 code:${r.code}`)
|
||||||
|
this.step('福利站签到', 'fail', `code ${r.code}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,14 +403,23 @@ class QMClient {
|
|||||||
if (ok.length) {
|
if (ok.length) {
|
||||||
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) log(`[礼券] ${pick.couponName} 领取成功 +${t502.taskReward || 10}心愿值`)
|
if (r.code === 0) {
|
||||||
else if (r.code === 14816772) log('[礼券] 今日已领取')
|
log(`[礼券] ${pick.couponName} 领取成功 +${t502.taskReward || 10}心愿值`)
|
||||||
else log(`[礼券] code:${r.code}`)
|
this.step('福利站礼券', 'done', pick.couponName)
|
||||||
|
} else if (r.code === 14816772) {
|
||||||
|
log('[礼券] 今日已领取')
|
||||||
|
this.step('福利站礼券', 'skip', '今日已领')
|
||||||
|
} else {
|
||||||
|
log(`[礼券] code:${r.code}`)
|
||||||
|
this.step('福利站礼券', 'warn', `code ${r.code}`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log(`[礼券] 券池今日无可领券(${infos.length} 张全非可领态)`)
|
log(`[礼券] 券池今日无可领券(${infos.length} 张全非可领态)`)
|
||||||
|
this.step('福利站礼券', 'skip', '券池无可领')
|
||||||
}
|
}
|
||||||
} else if (t502) {
|
} else if (t502) {
|
||||||
log('[礼券] 今日已完成')
|
log('[礼券] 今日已完成')
|
||||||
|
this.step('福利站礼券', 'skip', '今日已完成')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 旅程激励:心愿值达标档位实时领取(ClaimIncentives 幂等)----
|
// ---- 旅程激励:心愿值达标档位实时领取(ClaimIncentives 幂等)----
|
||||||
@@ -390,11 +427,21 @@ class QMClient {
|
|||||||
const unlock = (idata.incentives || []).filter(x => x.status === 1)
|
const unlock = (idata.incentives || []).filter(x => x.status === 1)
|
||||||
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) log(`[激励] 领取成功:${unlock.map(x => `${x.name}(${x.score}分)`).join('、')}`)
|
if (r.code === 0) {
|
||||||
else log(`[激励] code:${r.code}`)
|
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}`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const nxt = (idata.incentives || []).find(x => x.status === 5)
|
const nxt = (idata.incentives || []).find(x => x.status === 5)
|
||||||
if (nxt) log(`[激励] 心愿值${idata.currentScore || 0}/${nxt.score}(下档 ${nxt.name})`)
|
if (nxt) {
|
||||||
|
log(`[激励] 心愿值${idata.currentScore || 0}/${nxt.score}(下档 ${nxt.name})`)
|
||||||
|
this.step('旅程激励', 'skip', `心愿值 ${idata.currentScore || 0}/${nxt.score}`)
|
||||||
|
} else {
|
||||||
|
this.step('旅程激励', 'skip', '无待领档位')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 月度里程碑:签满目标天数即领(探针先行,14822144=未达标幂等)----
|
// ---- 月度里程碑:签满目标天数即领(探针先行,14822144=未达标幂等)----
|
||||||
@@ -406,43 +453,59 @@ class QMClient {
|
|||||||
const status = target.incentiveStatus
|
const status = target.incentiveStatus
|
||||||
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) log(`[月里程碑] 领取:${target.incentiveName}`)
|
if (r.code === 0) {
|
||||||
else log(`[月里程碑] code:${r.code}(${signedCnt} 天达标待领)`)
|
log(`[月里程碑] 领取:${target.incentiveName}`)
|
||||||
|
this.step('月里程碑', 'done', target.incentiveName)
|
||||||
|
} else {
|
||||||
|
log(`[月里程碑] 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} 天`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 月度好礼:poolStatus 3=可领 且库存>0 才领;补货时刻实读打印 ----
|
// ---- 月度好礼:poolStatus 3=可领 且库存>0 才领;补货时刻实读打印 ----
|
||||||
const gpData = (await this.api('music.vipcgi.VipRenewalMonthGiftCgi', 'QueryVipRenewalMonthlyGiftPool', {})).data || {}
|
const gpData = (await this.api('music.vipcgi.VipRenewalMonthGiftCgi', 'QueryVipRenewalMonthlyGiftPool', {})).data || {}
|
||||||
|
const gotGifts = [], noStock = []
|
||||||
for (const p of gpData.giftPools || []) {
|
for (const p of gpData.giftPools || []) {
|
||||||
const g = p.gift || {}
|
const g = p.gift || {}
|
||||||
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}`)
|
if (r.code === 0) { log(`[月礼] 领取:${g.giftName}`); gotGifts.push(g.giftName) }
|
||||||
else log(`[月礼] 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)`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 || {}
|
const dd = (await this.api('music.vipcgi.VipRenewalLotteryCgi', 'QueryVipRenewDrawInfo', {})).data || {}
|
||||||
let draws = dd.lotteryCount || 0
|
let draws = dd.lotteryCount || 0
|
||||||
|
const prizes = []
|
||||||
while (draws > 0) {
|
while (draws > 0) {
|
||||||
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 || '成功')
|
||||||
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('、')}`)
|
||||||
|
else this.step('臻享抽奖', 'skip', '无抽奖券')
|
||||||
|
|
||||||
log(`[福利站] ${ymd} 巡检完成`)
|
log(`[福利站] ${ymd} 巡检完成`)
|
||||||
return gained
|
return gained
|
||||||
}
|
}
|
||||||
@@ -486,7 +549,6 @@ class QMClient {
|
|||||||
log(hasCard
|
log(hasCard
|
||||||
? `[分享卡] 本月配额 ${quota} 张,已被领 ${sentList.length} 张,还能送 ${canSend} 张`
|
? `[分享卡] 本月配额 ${quota} 张,已被领 ${sentList.length} 张,还能送 ${canSend} 张`
|
||||||
: `[分享卡] 本月还没建过卡(等级上限参考 ${maxQuota} 张)`)
|
: `[分享卡] 本月还没建过卡(等级上限参考 ${maxQuota} 张)`)
|
||||||
|
|
||||||
// ③ 复用优先:扫本月每张卡,cardLeft>0 的直接返回链接(不建新卡)
|
// ③ 复用优先:扫本月每张卡,cardLeft>0 的直接返回链接(不建新卡)
|
||||||
for (const c of cards) {
|
for (const c of cards) {
|
||||||
const cid = c.cardID || c.cardId
|
const cid = c.cardID || c.cardId
|
||||||
@@ -506,16 +568,19 @@ class QMClient {
|
|||||||
log(`[分享卡] 复用本月未领完的卡:还能被领 ${left} 次(不新建、不占新配额)`)
|
log(`[分享卡] 复用本月未领完的卡:还能被领 ${left} 次(不新建、不占新配额)`)
|
||||||
log(`[分享卡] 分享链接:${url}`)
|
log(`[分享卡] 分享链接:${url}`)
|
||||||
this.shareCardUrl = url
|
this.shareCardUrl = url
|
||||||
|
this.step('分享卡', 'done', `复用本月卡,还能被领 ${left} 次`)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!force && !SHARE_CARD) {
|
if (!force && !SHARE_CARD) {
|
||||||
log('[分享卡] 只读模式(QM_SHARE_CARD 未置 on),不创建新卡')
|
log('[分享卡] 只读模式(QM_SHARE_CARD 未置 on),不创建新卡')
|
||||||
|
this.step('分享卡', 'skip', `只读,还能送 ${canSend} 张(QM_SHARE_CARD 未开)`)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if (canSend <= 0) {
|
if (canSend <= 0) {
|
||||||
log('[分享卡] 本月配额已用完,下月再送')
|
log('[分享卡] 本月配额已用完,下月再送')
|
||||||
|
this.step('分享卡', 'skip', '本月配额已用完')
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +589,7 @@ class QMClient {
|
|||||||
const cardId = created.cardID
|
const cardId = created.cardID
|
||||||
if (!cardId) {
|
if (!cardId) {
|
||||||
log('[分享卡] CreatCard 未返回 cardID,跳过')
|
log('[分享卡] CreatCard 未返回 cardID,跳过')
|
||||||
|
this.step('分享卡', 'warn', 'CreatCard 未返回 cardID')
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
const left = (created.cardInfo || {}).left
|
const left = (created.cardInfo || {}).left
|
||||||
@@ -531,6 +597,7 @@ class QMClient {
|
|||||||
log(`[分享卡] 已新建:还能被领 ${left != null ? left : '?'} 次`)
|
log(`[分享卡] 已新建:还能被领 ${left != null ? left : '?'} 次`)
|
||||||
log(`[分享卡] 分享链接:${url}`)
|
log(`[分享卡] 分享链接:${url}`)
|
||||||
this.shareCardUrl = url // 供汇总输出
|
this.shareCardUrl = url // 供汇总输出
|
||||||
|
this.step('分享卡', 'done', `新建 1 张,还能被领 ${left != null ? left : '?'} 次`)
|
||||||
return 0 // 不产成长值,不计入 total
|
return 0 // 不产成长值,不计入 total
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,15 +610,20 @@ 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('[签到] 今日已签到')
|
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', '今日无需动作')
|
||||||
if (st['9'] === 'N') await this.cosmetic()
|
if (st['9'] === 'N') await this.cosmetic()
|
||||||
|
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', '已上报')
|
||||||
|
} else {
|
||||||
|
this.step('听书行为', 'skip', '今日无需动作')
|
||||||
}
|
}
|
||||||
if (st['20'] === 'N' || !signed) {
|
if (st['20'] === 'N' || !signed) {
|
||||||
// 任务20跨天不重置:当日未签时 20:Y 是昨日残留,仍挂开始标记
|
// 任务20跨天不重置:当日未签时 20:Y 是昨日残留,仍挂开始标记
|
||||||
@@ -561,9 +633,13 @@ class QMClient {
|
|||||||
? '[节目] 开始标记已挂(播放上报未实现于本版,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}` : '已挂')
|
||||||
} else {
|
} else {
|
||||||
log(`[节目] StartTask 返回 code:${r.code},无法开始`)
|
log(`[节目] StartTask 返回 code:${r.code},无法开始`)
|
||||||
|
this.step('节目开始标记', 'warn', `code ${r.code}`)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.step('节目开始标记', 'skip', '今日无需动作')
|
||||||
}
|
}
|
||||||
|
|
||||||
const stNow = await this.state()
|
const stNow = await this.state()
|
||||||
@@ -572,16 +648,22 @@ class QMClient {
|
|||||||
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)
|
||||||
}
|
}
|
||||||
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}`) // 接口改版/风控不拖垮主流程
|
||||||
|
this.step('分享卡', 'fail', e.message)
|
||||||
}
|
}
|
||||||
out.cardUrl = this.shareCardUrl || ''
|
out.cardUrl = this.shareCardUrl || ''
|
||||||
|
out.steps = this.steps
|
||||||
|
out.verdict = this.verdict()
|
||||||
log(`[QQ音乐] 本次新增成长值合计: ${out.total}`)
|
log(`[QQ音乐] 本次新增成长值合计: ${out.total}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof QmDead) {
|
if (e instanceof QmDead) {
|
||||||
out.dead = true
|
out.dead = true
|
||||||
|
out.steps = this.steps
|
||||||
|
out.verdict = 'dead'
|
||||||
log(`[QQ音乐] 登录态失效(2000): ${e.message}`)
|
log(`[QQ音乐] 登录态失效(2000): ${e.message}`)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
@@ -691,6 +773,23 @@ function pushCardResult(results) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 把一次执行的 steps 渲染成通知正文。只列「做到了」和「没做到」,
|
||||||
|
* 全部 skip 的项折叠成一行,避免通知被十几行「今日已完成」淹没。 */
|
||||||
|
function renderAccount(uin, r) {
|
||||||
|
const icon = { done: '✓', warn: '△', fail: '✗', skip: '·' }
|
||||||
|
const did = r.steps.filter(s => s.status === 'done')
|
||||||
|
const warn = r.steps.filter(s => s.status === 'warn' || s.status === 'fail')
|
||||||
|
const skip = r.steps.filter(s => s.status === 'skip')
|
||||||
|
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 (r.cardUrl) lines.push(` 🎁 分享卡 ${r.cardUrl}`)
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// ---- 手动取卡模式(青龙「运行」按钮直接点,给人送卡用)----
|
// ---- 手动取卡模式(青龙「运行」按钮直接点,给人送卡用)----
|
||||||
// 触发:task qqmusic/scrip_qqmusic_checkin.js -- card
|
// 触发:task qqmusic/scrip_qqmusic_checkin.js -- card
|
||||||
@@ -736,22 +835,29 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const blocks = []
|
const blocks = []
|
||||||
|
let allDone = true
|
||||||
for (const a of accounts) {
|
for (const a of accounts) {
|
||||||
log(`\n===== 账号 ${a.uin} =====`)
|
log(`\n===== 账号 ${a.uin} =====`)
|
||||||
try {
|
try {
|
||||||
const cli = new QMClient(a.uin, a.key, path.join(dir, `qm_${a.uin}.json`))
|
const cli = new QMClient(a.uin, a.key, path.join(dir, `qm_${a.uin}.json`))
|
||||||
const r = await cli.run()
|
const r = await cli.run()
|
||||||
blocks.push(r.dead
|
if (r.dead) {
|
||||||
? `🔴 ${a.uin}:登录态失效,请重新在网页抓 qm_keyst 更新 env`
|
allDone = false
|
||||||
: `🟢 ${a.uin}:本次新增成长值 ${r.total}`)
|
blocks.push(`🔴 ${a.uin}:登录态失效\n └ 请重新在网页抓 qm_keyst 更新 env`)
|
||||||
if (r.cardUrl) blocks.push(`🎁 ${a.uin}:分享卡 ${r.cardUrl}`)
|
continue
|
||||||
|
}
|
||||||
|
if (r.verdict === 'fail') allDone = false
|
||||||
|
blocks.push(renderAccount(a.uin, r))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
allDone = false
|
||||||
log(`[QQ音乐] 账号 ${a.uin} 运行异常: ${e.message}`)
|
log(`[QQ音乐] 账号 ${a.uin} 运行异常: ${e.message}`)
|
||||||
blocks.push(`🔴 ${a.uin}:运行异常 ${e.message}`)
|
blocks.push(`🔴 ${a.uin}:运行异常\n └ ${e.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const desp = blocks.join('\n\n')
|
const head = allDone ? '✅ QQ音乐签到 · 全部完成' : '⚠️ QQ音乐签到 · 有未完成项'
|
||||||
+ `\n\n轮次说明: 00:01 / 06:02 双轮(任务20跨天滚动 + 福利站兜底)`
|
const desp = [head, '', ...blocks].join('\n')
|
||||||
|
+ `\n\n———`
|
||||||
|
+ `\n轮次: 00:01 / 06:02 双轮(任务20跨天滚动 + 福利站兜底)`
|
||||||
+ `\n节目任务(20)依赖 App 内正常收听计入(播放上报未启用)`
|
+ `\n节目任务(20)依赖 App 内正常收听计入(播放上报未启用)`
|
||||||
+ `\n时间: ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`
|
+ `\n时间: ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`
|
||||||
let sendNotifyFn = null
|
let sendNotifyFn = null
|
||||||
|
|||||||
Reference in New Issue
Block a user