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:
2026-11-07 18:58:15 +08:00
parent a9acafa3ba
commit d081ac66da
3 changed files with 266 additions and 126 deletions

113
qqmusic/test_render.js Normal file
View 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)