feat(qqmusic): 取卡结果回传平台(QM_CARD_CALLBACK)

手动取卡目前要「登青龙 → 点运行 → 翻日志复制链接」。加一条可选回传:
脚本跑完把结果 POST 回平台,由平台转发到微信,手机上直接收到链接。

实现(脚本侧,零依赖 Node 内置 https):
- QM_CARD_CALLBACK  回传地址(如 https://<平台域名>/api/hooks/card)
- QM_CARD_TOKEN     鉴权 token,缺省回落 QL_HOOK_TOKEN
- 回传体只有 {cards:[{uin, ok, url|reason}], at},**不含 cookie/key** —— 走公网不泄凭证
- 回传失败只记日志,绝不让取卡本身算失败(卡已建出、链接已在日志里)
- 未配 QM_CARD_CALLBACK 时完全跳过,行为与之前一致
- 顺带把 pushCardResult 加进 module.exports,供单测直接调用

验证:node --check / 签名对拍 6 组 PASS / 4 个回传用例(未配跳过、正常 POST
含 token 头与 body 结构、URL 非法跳过、服务端 500 不抛)全 PASS /
_probe_meta 三元数据 PASS
This commit is contained in:
2026-11-04 23:31:43 +08:00
parent 656442e4d6
commit c7c341e10a

View File

@@ -45,6 +45,10 @@ const PLAY_REPORT = String(process.env.QM_PLAY_REPORT || '').toLowerCase() === '
const SHARE_CARD = String(process.env.QM_SHARE_CARD || '').toLowerCase() === 'on'
// 分享落地页:cardid 由 CreatCard 返回,好友打开后自己点领取(非会员自然月限领 1 张)
const CARD_LANDING = 'https://y.qq.com/m/vip/experience_card/dist/index.html'
// 取卡结果回传地址(平台侧 /api/hooks/card 或自建 n8n/Server酱 中转)。置空则只打印到日志。
const CARD_CALLBACK = String(process.env.QM_CARD_CALLBACK || '').trim()
// 回传鉴权 token:与平台 QL_HOOK_TOKEN 同一枚(平台 hookGuard 校验它)
const CARD_TOKEN = String(process.env.QM_CARD_TOKEN || process.env.QL_HOOK_TOKEN || '').trim()
function time33(s) {
let h = 5381
@@ -640,6 +644,53 @@ function parseAccounts() {
return list
}
/** 取卡结果回传平台(可选)。平台侧 /api/hooks/card 收到后按该用户的通知目标转发到微信。
* 设计要点:
* - 只回传「取卡结果」,不含 cookie/key —— 走公网也不泄露凭证。
* - 用 Node 内置 https,零依赖(与脚本其余部分一致)。
* - 失败只打日志,绝不因回传失败让取卡本身算失败(卡已经建出来了,链接在日志里)。
* - QM_CARD_CALLBACK 空 → 完全跳过(本地开发/不想接推送的场景)。 */
function pushCardResult(results) {
if (!CARD_CALLBACK) {
log('[回传] 未配置 QM_CARD_CALLBACK,结果仅打印在本日志里')
return Promise.resolve()
}
const payload = JSON.stringify({
cards: results,
at: new Date().toISOString(),
})
return new Promise((resolve) => {
let u
try { u = new URL(CARD_CALLBACK) } catch { log(`[回传] QM_CARD_CALLBACK 不是合法 URL,跳过`); return resolve() }
const lib = u.protocol === 'http:' ? require('http') : https
const req = lib.request({
host: u.hostname,
port: u.port || (u.protocol === 'http:' ? 80 : 443),
path: u.pathname + u.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(CARD_TOKEN ? { 'x-sh-hook-token': CARD_TOKEN } : {}),
'Content-Length': Buffer.byteLength(payload),
},
timeout: 15000,
}, (res) => {
const chunks = []
res.on('data', c => chunks.push(c))
res.on('end', () => {
const text = Buffer.concat(chunks).toString('utf8')
if (res.statusCode >= 200 && res.statusCode < 300) log(`[回传] 已推送(HTTP ${res.statusCode})`)
else log(`[回传] 失败 HTTP ${res.statusCode}: ${text.slice(0, 160)}`)
resolve()
})
})
req.on('timeout', () => { log('[回传] 超时,放弃(链接已在日志里)'); req.destroy(); resolve() })
req.on('error', (e) => { log(`[回传] 失败: ${e.message}(链接已在日志里)`); resolve() })
req.write(payload)
req.end()
})
}
async function main() {
// ---- 手动取卡模式(青龙「运行」按钮直接点,给人送卡用)----
// 触发:task qqmusic/scrip_qqmusic_checkin.js -- card
@@ -647,6 +698,7 @@ async function main() {
// 把 -- 后面的都收进 script_params,otask.sh 再 run_task_command node <脚本> <args>)。
// 也支持环境变量 QM_CARD=1(定时任务里不方便传参时用)。
// 只跑取卡,不跑签到:手动送卡时不想额外刷一遍签到接口。
// 置了 QM_CARD_CALLBACK 就把结果 POST 回平台,由平台转发到微信(见 pushCardResult)。
const cardOnly = process.argv.includes('card')
|| String(process.env.QM_CARD || '').toLowerCase() === 'on'
|| String(process.env.QM_CARD || '') === '1'
@@ -659,6 +711,7 @@ async function main() {
const dir = cacheDir()
if (cardOnly) {
const results = []
for (const a of accounts) {
log(`\n===== 取卡 账号 ${a.uin} =====`)
try {
@@ -667,14 +720,18 @@ async function main() {
await cli.shareCard(true) // force=true:无视 QM_SHARE_CARD 开关
if (cli.shareCardUrl) {
log(`\n👉 复制这条链接发给对方:\n${cli.shareCardUrl}\n`)
results.push({ uin: a.uin, ok: true, url: cli.shareCardUrl })
} else {
log('[分享卡] 本次没有可用的卡(配额用完 / 本月卡都已被领完)')
results.push({ uin: a.uin, ok: false, reason: '配额用完或本月卡都已被领完' })
}
} catch (e) {
if (e instanceof QmDead) log(`🔴 ${a.uin}:登录态失效,请重新抓 qm_keyst 更新 env`)
else log(`🔴 ${a.uin}:取卡异常 ${e.message}`)
const reason = e instanceof QmDead ? '登录态失效,请重新抓 qm_keyst 更新 env' : `取卡异常 ${e.message}`
log(`🔴 ${a.uin}:${reason}`)
results.push({ uin: a.uin, ok: false, reason })
}
}
await pushCardResult(results)
return
}
@@ -709,4 +766,4 @@ async function main() {
if (require.main === module) {
main().catch(e => { console.error('[QQ音乐] 未捕获异常:', e); process.exitCode = 1 })
}
module.exports = { time33, zzc, QMClient, parseAccounts }
module.exports = { time33, zzc, QMClient, parseAccounts, pushCardResult }