feat(qoder): 适配国际版每日签到 — UMID 设备风控身份 + 国际版活动 schema

核心发现(实测):
- Qoder 国际版服务端只对带 Cosy-MachineToken/Code/Type 的请求下发签到活动,
  否则 campaigns 返回空数组(国内版无此限制)
- 国际版活动 actionType=VIEW_DETAILS(国内版 CLAIM_BENEFIT),claimStatus 一致
- 活动窗口 2026-09-01~10-31,每日 10:00 (UTC+8) 刷新,领取后 30 天有效

实现:
- 新增 umid.py:从官方 npm 包 @qoder-ai/qodercli 下载并提取内嵌 UMID 二进制
  (x86_64/arm64 ELF),试跑校验后缓存到 @appdata/AgentCheckin/umid/
- qoder.py:签到前自动确保风控身份,支持国际版/国内版双 schema 活动解析
- API:/api/umid/status、/api/umid/install、/api/accounts/refresh_risk
- 前端:设置页风控身份面板(下载/状态)、账号列表「刷新风控」按钮
- 诊断:测连接返回 risk_identity / campaigns_debug(原始字段)

验证:NAS 实测 Qoder 国际版从「活动列表为空」变为「今日已领取(75 积分)」
测试:37 后端 + 25 E2E + 9 前端,全部通过
This commit is contained in:
cbb
2026-10-19 12:04:44 +08:00
parent a3b84d5ce2
commit c263edc3e7
10 changed files with 484 additions and 17 deletions
+25 -3
View File
@@ -216,7 +216,10 @@ def main():
"print('exp:', qoder.jwt_exp(tok), 'uid:', qoder.jwt_uid(tok))\n"
"# global detection\n"
"print('global:', qoder.is_global({'domain': 'openapi.qoder.sh'}), qoder.is_global({'domain': 'openapi.qoder.com.cn'}))\n"
"# campaign parsing: claimable -> claim flow mocked\n"
"# 国际版 schema: actionType=VIEW_DETAILS + claimStatus\n"
"intl = [{'actionType': 'VIEW_DETAILS', 'claimStatus': 'CLAIMABLE', 'campaignId': 'i1'}]\n"
"print('intl_benefits:', len(qoder._benefits(intl)))\n"
"# 国内版 schema\n"
"campaigns = [{'actionType': 'CLAIM_BENEFIT', 'claimStatus': 'CLAIMABLE', 'campaignId': 'c1', 'benefit': {'amount': 100}}]\n"
"b = qoder._benefits(campaigns)\n"
"print('benefits:', len(b), 'claimable:', len([c for c in b if c.get('claimStatus') == 'CLAIMABLE']))\n"
@@ -230,8 +233,10 @@ def main():
check("qoder device headers", lines and lines[0] == "clientType=10 machineId=MID version=1.2.3 emptyToken=''", out + err[-200:])
check("qoder jwt helpers", len(lines) > 1 and lines[1] == "exp: 9999999999 uid: u123", out)
check("qoder global detection", len(lines) > 2 and lines[2] == "global: True False", out)
check("qoder campaign parsing", len(lines) > 3 and lines[3] == "benefits: 1 claimable: 1", out)
check("qoder already-claimed detection", len(lines) > 4 and lines[4] == "claimed_today: 1", out)
check("qoder campaign parsing", "benefits: 1 claimable: 1" in out, out)
intl_line = next((l for l in lines if l.startswith("intl_benefits:")), "")
check("qoder intl schema (VIEW_DETAILS)", intl_line == "intl_benefits: 1", out)
check("qoder already-claimed detection", "claimed_today: 1" in out, out)
print("== 13. engine: add qoder account + token masking ==")
rc, out, err = run_py(
@@ -245,6 +250,23 @@ def main():
check("engine add qoder", rc == 0 and "qoder_" in out and "MID" in out, out + err[-200:])
check("qoder token masked in dashboard", "masked: True" in out, out)
print("== 14. umid module (offline) ==")
rc, out, err = run_py(
"import umid\n"
"st = umid.status()\n"
"print('supported:', st['supported'], '| arch:', st['cpu_arch'])\n"
"print('installed:', st['installed'])\n"
"import io, tarfile\n"
"buf = io.BytesIO()\n"
"with tarfile.open(fileobj=buf, mode='w') as tf:\n"
" info = tarfile.TarInfo('package/bundle/qodercli.js'); info.size = 5\n"
" tf.addfile(info, io.BytesIO(b'hello'))\n"
"print('tar_extract_ok:', umid._extract_tar_entry(buf.getvalue(), 'package/bundle/qodercli.js') == b'hello')\n"
)
lines = out.splitlines() if rc == 0 else []
check("umid: arch supported", len(lines) > 0 and "supported: True" in lines[0], out + err[-200:])
check("umid: tar extraction", len(lines) > 2 and "tar_extract_ok: True" in lines[2], out)
print()
print("PASSED: %d FAILED: %d" % (len(PASS), len(FAIL)))
if FAIL: