feat(qqmusic): QQ音乐会员成长值+福利站自动签到(zzc签名对拍验证)

- u6.y.qq.com 协议底座: zzc/time33 与 LDP924/auto-checkin qmcommon.py 逐字节对拍 PASS(6向量)
- 续期闭环: QQLogin 换新 key + openid/access_token 二级救活, 缓存存 /ql/data/qqmusic_cache(订阅git免疫)
- 任务中心三态机: 签到/挂件/装扮/听书/节目, Y零请求/W直领/N动作, 10006写锁即停
- 福利站全量: 每日签到/礼券/旅程激励/月里程碑/月度好礼/臻享抽奖, 先探后做
- env: SH_QQMUSIC_KEY 多账号(uin#key / cookie串 / 混排), code 1000/2000 归一失效语义
- 播放上报(任务20完整+10)默认关闭, 定时 1 0,6 * * * 双轮
This commit is contained in:
2026-11-04 00:38:19 +08:00
parent 21f4d40a0c
commit 89afcd1e60
5 changed files with 722 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""签名对拍(文件交换版,规避 Windows 子进程控制台编码坑):
本文件 = LDP924/auto-checkin qmcommon.py 的 zzc/time33 参考实现,生成期望值 JSON;
qqmusic/test_sign_run.js = 被测 Node 模块,读取 JSON 逐条比对并给出 PASS/FAIL。
两个实现任何一处不一致都会 MISMATCH —— 这是协议调用可用性的硬门槛。"""
import json
import subprocess
import sys
import os
from base64 import b64encode
from hashlib import sha1
SCRAMBLE = (89, 39, 179, 150, 218, 82, 58, 252, 177, 52,
186, 123, 120, 64, 242, 133, 143, 161, 121, 179)
def time33(s):
h = 5381
for c in s:
h += (h << 5) + ord(c)
h &= 0xFFFFFFFF
return h & 0x7FFFFFFF
def zzc(payload):
h = sha1(payload.encode()).hexdigest().upper()
p1 = ''.join(h[i] for i in (23, 14, 6, 36, 16, 7, 19))
p2 = ''.join(h[i] for i in (16, 1, 32, 12, 19, 27, 8, 5))
p3 = bytes(v ^ int(h[i * 2:i * 2 + 2], 16) for i, v in enumerate(SCRAMBLE))
mid = b64encode(p3).decode().replace('\\', '').replace('/', '').replace('+', '').replace('=', '')
return ('zzc' + p1 + mid + p2).lower()
PAYLOADS = [
'{"comm":{"g_tk":962138355,"uin":123456789,"format":"json","inCharset":"utf-8","outCharset":"utf-8","notice":0,"platform":"h5","needNewCode":1,"ct":23,"cv":0},"req_0":{"module":"music.lvz.LevelConfigSvr","method":"GetState","param":{}}}',
'{"comm":{},"req_0":{"module":"m","method":"x","param":{}}}',
'x',
'a' * 10000,
'',
'unicode测试中文签名输入⏰',
]
def main():
expect = {
'time33': time33('Q_H_L_testkey1234567890'),
'cases': [{'payload': p, 'zzc': zzc(p), 'len': len(zzc(p))} for p in PAYLOADS],
}
here = os.path.dirname(os.path.abspath(__file__))
expect_path = os.path.join(here, 'test_sign_expect.json')
with open(expect_path, 'w', encoding='utf-8') as f:
json.dump(expect, f, ensure_ascii=False)
out = subprocess.run(['node', os.path.join(here, 'test_sign_run.js')],
capture_output=True, text=True, shell=False, cwd=here,
encoding='utf-8', errors='replace')
sys.stdout.write(out.stdout)
if out.stderr:
sys.stderr.write(out.stderr)
return out.returncode
if __name__ == '__main__':
sys.exit(main())