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:
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user