feat(qoder): 弄清国际版每日 100 Credits 的「按设备下发」规则 + 一键设备授权登录

排查结论(本机对真实接口做了 30+ 组对照实验)
- GET /sash/api/v1/me/campaigns 只返回一条常驻 VIEW_DETAILS 广告
  (act-20260901-493,BOGO 订阅推广,claimStatus=CLAIMED 与签到无关)
- 无论 Cosy-MachineToken/Code/Type 用 env 0/3/4/prod、裸调用/带 stdin、
  是否附 Cosy-MachineId/OS/Hostname/Origin/Referer/X-*、是否带 clientType,
  都拿不到 CLAIM_BEFIT 轮次;账号本人 balances/achievements 正常
- 三个社区实现(qoder2api-hub、dsh-connect-qoder、CreditDaddy)一致:
  每日轮次按「登录过官方客户端的那台设备」下发,NAS 侧另造的设备看不到它

修复
- _benefits() 改为只认 actionType=CLAIM_BEFIT(不再把 VIEW_DETAILS 当签到),
  窗口过期也排除;claimStatus 未知按可领处理(领取接口幂等)
- 两段式取活动:先裸取拿 uid → 按 uid 生成风控身份 → 再带设备身份重取
- 状态文案改为「未下发每日签到活动」而不是误导性的「今日已领取」

新功能
- Qoder 一键登录(OAuth 设备授权 / PKCE S256):应用内点按钮 → 浏览器确认一次,
  Token 直接绑定本机设备;同 uid 已存在则更新凭证而不是新增重复账号
- 手工导入桌面端设备身份接口 /api/qoder/import_device
- 诊断接口:/api/qoder/{probe,experiment,growth,matrix,desktop}

测试:39 后端 + 25 E2E + 9 前端全绿
This commit is contained in:
AgentCheckin
2026-10-19 12:49:06 +08:00
parent c263edc3e7
commit 556e45d642
11 changed files with 943 additions and 70 deletions
+15 -3
View File
@@ -216,9 +216,19 @@ 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"
"# 国际版 schema: actionType=VIEW_DETAILS + claimStatus\n"
"intl = [{'actionType': 'VIEW_DETAILS', 'claimStatus': 'CLAIMABLE', 'campaignId': 'i1'}]\n"
"# 国际版同一账号还挂着 VIEW_DETAILS 的订阅推广(claimStatus=CLAIMED,无 benefit)\n"
"# —— 它不是签到活动,必须被排除\n"
"intl = [{'actionType': 'VIEW_DETAILS', 'claimStatus': 'CLAIMED', 'campaignId': 'i1'}]\n"
"print('intl_benefits:', len(qoder._benefits(intl)))\n"
"# 每日轮次:actionType=CLAIM_BENEFIT、窗口开放\n"
"now0 = int(time.time())\n"
"daily = [{'actionType': 'CLAIM_BENEFIT', 'claimStatus': 'CLAIMABLE', 'campaignId': 'd1',\n"
" 'startAt': now0 - 60, 'endAt': now0 + 600}]\n"
"print('daily_benefits:', len(qoder._benefits(daily)))\n"
"# 过期轮次要排除\n"
"expired = [{'actionType': 'CLAIM_BENEFIT', 'claimStatus': 'CLAIMABLE', 'campaignId': 'd2',\n"
" 'startAt': now0 - 600, 'endAt': now0 - 60}]\n"
"print('expired_benefits:', len(qoder._benefits(expired)))\n"
"# 国内版 schema\n"
"campaigns = [{'actionType': 'CLAIM_BENEFIT', 'claimStatus': 'CLAIMABLE', 'campaignId': 'c1', 'benefit': {'amount': 100}}]\n"
"b = qoder._benefits(campaigns)\n"
@@ -235,7 +245,9 @@ def main():
check("qoder global detection", len(lines) > 2 and lines[2] == "global: True False", 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 intl: VIEW_DETAILS excluded", intl_line == "intl_benefits: 0", out)
check("qoder daily CLAIM_BENEFIT kept", "daily_benefits: 1" in out, out)
check("qoder expired round excluded", "expired_benefits: 0" in out, out)
check("qoder already-claimed detection", "claimed_today: 1" in out, out)
print("== 13. engine: add qoder account + token masking ==")