fix(cmcc): parse_curl 头键大小写不敏感(Reqable 等导出 Cookie 大写 C)

号2素材实测踩到:mitm/Reqable 导出的请求头是 'Cookie'(大写),
parse_curl 的 headers.get('cookie') 大小写敏感 → 自检误报缺 cookie 头。
改用小写化视图做自检判定,原始头键仍原样保留(服务端/request 都大小写不敏感)。
This commit is contained in:
2026-11-07 18:50:04 +08:00
parent a9bdd048f5
commit 57a49ab7f5
4 changed files with 501 additions and 2 deletions

243
qqmusic/test_coin.js Normal file
View File

@@ -0,0 +1,243 @@
#!/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)
})()