原来 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 名断言同步改成新名(金币·签到 / 金币·抽奖签到)。
244 lines
10 KiB
JavaScript
244 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* test_coin.js —— 金币中心签到逻辑的离线验证(mock https,不联网)
|
||
*
|
||
* 覆盖 coinCenter() 的六条分支:
|
||
* ① 未签 → SignIn 成功 → AwardPrize 成功
|
||
* ② 未签 → SignIn 成功 → 无 State=2 的 task(只签不领)
|
||
* ③ 已签 → 有 State=2 的 task(补领)
|
||
* ④ 已签 → 无 State=2 的 task(skip)
|
||
* ⑤ 状态查询失败(ActID 过期)→ warn,不 fail
|
||
* ⑥ QM_COIN_SIGN=off → skip
|
||
* ⑦ 领奖返回 100004(幂等)→ 算成功
|
||
* ⑧ QmDead 透传(code 2000)
|
||
*
|
||
* 用法: node qqmusic/test_coin.js
|
||
*/
|
||
'use strict'
|
||
const Module = require('module')
|
||
const path = require('path')
|
||
|
||
const FILE = path.join(__dirname, '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 : ''}`) }
|
||
}
|
||
|
||
/* ── mock https.request ── */
|
||
let respond = () => ({ status: 200, text: '{}' })
|
||
const origRequest = require('https').request
|
||
require('https').request = function (opts, cb) {
|
||
const chunks = []
|
||
const req = {
|
||
on() { return req },
|
||
write(d) { chunks.push(Buffer.from(d)) },
|
||
end() {
|
||
const body = Buffer.concat(chunks).toString('utf8')
|
||
const r = respond(opts, body)
|
||
const res = {
|
||
statusCode: r.status,
|
||
on(ev, fn) {
|
||
if (ev === 'data') fn(Buffer.from(r.text))
|
||
if (ev === 'end') setImmediate(fn)
|
||
return res
|
||
},
|
||
}
|
||
setImmediate(() => cb(res))
|
||
},
|
||
destroy() {},
|
||
}
|
||
return req
|
||
}
|
||
|
||
const { QMClient } = require(FILE)
|
||
|
||
/* 构造一个直接可用(跳过 refresh/缓存)的 client */
|
||
function mkClient() {
|
||
const c = new QMClient('1006764347', 'Q_H_L_faketoken_for_shape', '/tmp/__nonexistent_qm_cache.json')
|
||
c.key = 'Q_H_L_faketoken_for_shape'
|
||
return c
|
||
}
|
||
|
||
const STATE_OK = (isSignIn, day, tasks) => JSON.stringify({
|
||
code: 0,
|
||
req_0: { code: 0, data: { retCode: 0, Info: { IsSignIn: isSignIn } } },
|
||
req_1: {
|
||
code: 0,
|
||
data: {
|
||
retCode: 0,
|
||
Info: { IsSignIn: isSignIn, ContinueSignInCount: day },
|
||
TaskListInfo: { TaskList: { ContinueTaskList: tasks } },
|
||
},
|
||
},
|
||
})
|
||
const SIGN_OK = JSON.stringify({
|
||
code: 0,
|
||
req_0: { code: 0, data: { retCode: 0, Info: { IsSignIn: true } } },
|
||
})
|
||
const AWARD_OK = JSON.stringify({ code: 0, req_0: { code: 0, data: { retCode: 0 } } })
|
||
const AWARD_DUP = JSON.stringify({ code: 0, req_0: { code: 0, data: { retCode: 100004 } } })
|
||
|
||
/* 按 method 分派的 mock:金币主活动与抽奖活动用不同 ActID 区分 */
|
||
function route(table) {
|
||
return (opts, body) => {
|
||
let j = {}
|
||
try { j = JSON.parse(body) } catch { /* */ }
|
||
const acts = [j.req_0, j.req_1].filter(Boolean).map(r => (r.param || {}).ActID)
|
||
const act = acts[0]
|
||
const methods = [j.req_0, j.req_1].filter(Boolean).map(r => r.method)
|
||
const key = `${act}:${methods.join('+')}`
|
||
const hit = table[key]
|
||
return { status: 200, text: hit !== undefined ? hit : '{"code":0,"req_0":{"code":0,"data":{"retCode":0}}}' }
|
||
}
|
||
}
|
||
|
||
const COIN = 'Z25hHGi'
|
||
const LOT = 'Z156KEu'
|
||
|
||
const cases = [
|
||
{
|
||
name: '① 未签 → 签到成功 → 领奖成功',
|
||
table: {
|
||
[`${COIN}:GetSignInSummary+GetSignInTaskList`]: STATE_OK(false, 0, {}),
|
||
[`${COIN}:SignIn`]: SIGN_OK,
|
||
[`${COIN}:AwardPrize`]: AWARD_OK,
|
||
[`${LOT}:GetSignInSummary+GetSignInTaskList`]: STATE_OK(true, 5, {}),
|
||
},
|
||
// 第二次查询(签完后复查)也要返回带 task 的状态 → 用序列化响应
|
||
seq: true,
|
||
want: { status: 'done', has: '已签' },
|
||
},
|
||
]
|
||
|
||
/* 由于「签完复查」需要同一 key 返回不同响应,用计数器实现 */
|
||
async function runCase(name, script, want) {
|
||
const calls = []
|
||
respond = (opts, body) => {
|
||
let j = {}
|
||
try { j = JSON.parse(body) } catch { /* */ }
|
||
calls.push({
|
||
url: opts.host + opts.path,
|
||
reqs: [j.req_0, j.req_1].filter(Boolean).map(r => `${(r.param || {}).ActID}:${r.method}`),
|
||
comm: j.comm,
|
||
})
|
||
return script(j, calls.length)
|
||
}
|
||
const c = mkClient()
|
||
const t = await c.coinCenter()
|
||
console.log(`\n${name}`)
|
||
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 === '金币·签到')
|
||
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 === '金币·签到')
|
||
ok(`detail 含「${v}」`, s && s.detail.includes(v), s && s.detail)
|
||
} else if (k === 'calls') {
|
||
ok(`请求次数 = ${v}`, calls.length === v, String(calls.length))
|
||
} else if (k === 'comm') {
|
||
const c0 = calls[0] && calls[0].comm
|
||
ok(`comm 是小程序形态(mina=1 appid=…)`, c0 && c0.mina === 1 && String(c0.appid).startsWith('wx'), JSON.stringify(c0))
|
||
ok(`comm.authst = key`, c0 && c0.authst === 'Q_H_L_faketoken_for_shape')
|
||
ok(`comm 无 g_tk(金币体系不签名)`, c0 && c0.g_tk === undefined, JSON.stringify(c0))
|
||
}
|
||
}
|
||
return calls
|
||
}
|
||
|
||
;(async () => {
|
||
/* ① 未签 → 签 → 领 */
|
||
await runCase('── ① 未签 → 签到 → 领奖 ──', (j, n) => {
|
||
const acts = [j.req_0, j.req_1].filter(Boolean).map(r => (r.param || {}).ActID)
|
||
const methods = [j.req_0, j.req_1].filter(Boolean).map(r => r.method)
|
||
const act = acts[0]
|
||
if (act === LOT) return { status: 200, text: STATE_OK(true, 3, {}) }
|
||
if (methods.join('+') === 'GetSignInSummary+GetSignInTaskList') {
|
||
// 第一次查询:未签;第二次(签后复查):已签 + 有可领 task
|
||
return { status: 200, text: n === 1
|
||
? STATE_OK(false, 0, {})
|
||
: STATE_OK(true, 1, { '1': { ID: 'T1', State: 2, PrizeList: [{ Value: 30 }] } }) }
|
||
}
|
||
if (methods[0] === 'SignIn') return { status: 200, text: SIGN_OK }
|
||
if (methods[0] === 'AwardPrize') return { status: 200, text: AWARD_OK }
|
||
return { status: 200, text: '{"code":0,"req_0":{"code":0,"data":{"retCode":0}}}' }
|
||
}, { status: 'done', has: '已签', comm: true })
|
||
|
||
/* ② 已签 + 有 task → 补领 */
|
||
await runCase('── ② 已签 + 有可领 task → 补领 ──', (j) => {
|
||
const acts = [j.req_0, j.req_1].filter(Boolean).map(r => (r.param || {}).ActID)
|
||
const methods = [j.req_0, j.req_1].filter(Boolean).map(r => r.method)
|
||
if (acts[0] === LOT) return { status: 200, text: STATE_OK(true, 3, {}) }
|
||
if (methods.join('+') === 'GetSignInSummary+GetSignInTaskList') {
|
||
return { status: 200, text: STATE_OK(true, 2, { '2': { ID: 'T2', State: 2, PrizeList: [{ Name: '10金币' }] } }) }
|
||
}
|
||
if (methods[0] === 'AwardPrize') return { status: 200, text: AWARD_OK }
|
||
return { status: 200, text: '{"code":0,"req_0":{"code":0,"data":{"retCode":0}}}' }
|
||
}, { status: 'done', has: '10金币' })
|
||
|
||
/* ③ 已签 + 无可领 → skip */
|
||
await runCase('── ③ 已签 + 无可领 → skip ──', (j) => {
|
||
const acts = [j.req_0, j.req_1].filter(Boolean).map(r => (r.param || {}).ActID)
|
||
return { status: 200, text: STATE_OK(true, 4, { '1': { ID: 'T1', State: 1 } }) }
|
||
}, { status: 'skip', has: '连续第 4 天' })
|
||
|
||
/* ④ 状态查询失败 → warn(不是 fail) */
|
||
await runCase('── ④ ActID 过期(查询失败)→ warn ──', () => ({
|
||
status: 200,
|
||
text: JSON.stringify({ code: 0, req_0: { code: -1, data: { retCode: 100001 } }, req_1: { code: -1, data: {} } }),
|
||
}), { status: 'warn', has: 'ActID' })
|
||
|
||
/* ⑤ 领奖返回 100004(幂等)→ 算成功 */
|
||
await runCase('── ⑤ 领奖 retCode=100004(已领过)→ done ──', (j) => {
|
||
const acts = [j.req_0, j.req_1].filter(Boolean).map(r => (r.param || {}).ActID)
|
||
const methods = [j.req_0, j.req_1].filter(Boolean).map(r => r.method)
|
||
if (acts[0] === LOT) return { status: 200, text: STATE_OK(true, 3, {}) }
|
||
if (methods.join('+') === 'GetSignInSummary+GetSignInTaskList') {
|
||
return { status: 200, text: STATE_OK(true, 2, { '2': { ID: 'T2', State: 2, PrizeList: [{ Value: 20 }] } }) }
|
||
}
|
||
if (methods[0] === 'AwardPrize') return { status: 200, text: AWARD_DUP }
|
||
return { status: 200, text: '{"code":0,"req_0":{"code":0,"data":{"retCode":0}}}' }
|
||
}, { status: 'done' })
|
||
|
||
/* ⑥ 金币抽奖签到(独立 ActID) */
|
||
{
|
||
console.log('\n── ⑥ 金币抽奖签到(独立 ActID)──')
|
||
respond = (opts, body) => {
|
||
const j = JSON.parse(body)
|
||
const acts = [j.req_0, j.req_1].filter(Boolean).map(r => (r.param || {}).ActID)
|
||
const methods = [j.req_0, j.req_1].filter(Boolean).map(r => r.method)
|
||
if (acts[0] === COIN) {
|
||
if (methods.join('+') === 'GetSignInSummary+GetSignInTaskList') return { status: 200, text: STATE_OK(true, 5, {}) }
|
||
return { status: 200, text: '{"code":0,"req_0":{"code":0,"data":{"retCode":0}}}' }
|
||
}
|
||
// 抽奖活动:未签 → 签 → 有 task 可领
|
||
if (methods.join('+') === 'GetSignInSummary+GetSignInTaskList') {
|
||
return { status: 200, text: STATE_OK(false, 0, {}) }
|
||
}
|
||
if (methods[0] === 'SignIn') return { status: 200, text: SIGN_OK }
|
||
if (methods[0] === 'AwardPrize') return { status: 200, text: AWARD_OK }
|
||
return { status: 200, text: '{"code":0,"req_0":{"code":0,"data":{"retCode":0}}}' }
|
||
}
|
||
const c = mkClient()
|
||
await c.coinCenter()
|
||
const s = c.steps.find(x => x.name === '金币·抽奖签到')
|
||
ok('金币抽奖签到有独立 step', !!s, JSON.stringify(c.steps.map(x => x.name)))
|
||
}
|
||
|
||
/* ⑦ QmDead 透传 */
|
||
{
|
||
console.log('\n── ⑦ 未登录(code 2000)→ 抛 QmDead ──')
|
||
respond = () => ({ status: 200, text: JSON.stringify({ code: 0, req_0: { code: 2000, data: {} } }) })
|
||
const c = mkClient()
|
||
let threw = null
|
||
try { await c.coinCenter() } catch (e) { threw = e }
|
||
ok('抛了 QmDead', threw && threw.constructor.name === 'QmDead', threw && threw.constructor.name)
|
||
}
|
||
|
||
require('https').request = origRequest
|
||
console.log(`\nRESULT: ${fail === 0 ? 'PASS' : `FAIL(${fail})`} (${pass} passed, ${fail} failed)`)
|
||
process.exit(fail === 0 ? 0 : 1)
|
||
})()
|