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:
@@ -177,6 +177,62 @@ def install(force=False):
|
||||
# 调用
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def run_runtime_info(env_code, account=""):
|
||||
"""以指定 env 直接运行 runtime-info,返回 (完整输出 JSON, error)。
|
||||
|
||||
env_code: 0=国内版 ONLINE,3=国际版(客户端默认),4=国际版(CLI 默认)。
|
||||
返回的 JSON 除 machineToken/Code/Type 外通常还有 vmInfo 等字段。
|
||||
"""
|
||||
info = installed()
|
||||
if not info:
|
||||
return None, "未安装 runtime-info(请先在设置里下载)"
|
||||
try:
|
||||
stdin_data = json.dumps({"account": str(account or "")}) + "\n"
|
||||
p = subprocess.run([BIN_PATH, str(env_code), "--account-stdin"],
|
||||
input=stdin_data, capture_output=True, text=True, timeout=30)
|
||||
if p.returncode != 0 or not p.stdout.strip():
|
||||
return None, "runtime-info 执行失败(退出码 %s)%s" % (p.returncode, (p.stderr or "")[:120])
|
||||
line = p.stdout.strip().split("\n")[0]
|
||||
data = json.loads(line)
|
||||
if not isinstance(data, dict):
|
||||
return None, "runtime-info 输出不是 JSON 对象"
|
||||
return data, None
|
||||
except subprocess.TimeoutExpired:
|
||||
return None, "runtime-info 执行超时"
|
||||
except Exception as e:
|
||||
return None, "runtime-info 调用异常:%s" % e
|
||||
|
||||
|
||||
def run_runtime_info_raw(args, stdin_data=None):
|
||||
"""按给定 argv / stdin 调用 runtime-info,返回 (JSON, error)。
|
||||
|
||||
用途:对照不同的 env 取值与「是否带 --account-stdin」(客户端 Linux 分支
|
||||
只用 `runtime-info <env>`,完全不喂 stdin)。
|
||||
"""
|
||||
info = installed()
|
||||
if not info:
|
||||
return None, "未安装 runtime-info(请先在设置里下载)"
|
||||
try:
|
||||
argv = [BIN_PATH] + [str(a) for a in args]
|
||||
if stdin_data is None:
|
||||
p = subprocess.run(argv, stdin=subprocess.DEVNULL,
|
||||
capture_output=True, text=True, timeout=30)
|
||||
else:
|
||||
p = subprocess.run(argv, input=stdin_data,
|
||||
capture_output=True, text=True, timeout=30)
|
||||
if p.returncode != 0 or not p.stdout.strip():
|
||||
return None, "退出码 %s %s" % (p.returncode, (p.stderr or "")[:160])
|
||||
line = p.stdout.strip().split("\n")[0]
|
||||
data = json.loads(line)
|
||||
if not isinstance(data, dict):
|
||||
return None, "输出不是 JSON 对象"
|
||||
return data, None
|
||||
except subprocess.TimeoutExpired:
|
||||
return None, "超时"
|
||||
except Exception as e:
|
||||
return None, "异常:%s" % e
|
||||
|
||||
|
||||
def risk_identity(provider="qoder", account="", force_install=False):
|
||||
"""生成设备风控身份。返回 ({"machineToken","machineCode","machineType"}, error).
|
||||
|
||||
@@ -188,28 +244,18 @@ def risk_identity(provider="qoder", account="", force_install=False):
|
||||
if not ok:
|
||||
return None, msg
|
||||
info = installed()
|
||||
env = str(RISK_ENV.get(provider, 0))
|
||||
try:
|
||||
stdin_data = json.dumps({"account": str(account or "")}) + "\n"
|
||||
p = subprocess.run([BIN_PATH, env, "--account-stdin"],
|
||||
input=stdin_data, capture_output=True, text=True, timeout=30)
|
||||
if p.returncode != 0 or not p.stdout.strip():
|
||||
return None, "runtime-info 执行失败(退出码 %s)%s" % (p.returncode, (p.stderr or "")[:120])
|
||||
line = p.stdout.strip().split("\n")[0]
|
||||
data = json.loads(line)
|
||||
out = {}
|
||||
for k in ("machineToken", "machineCode", "machineType"):
|
||||
v = data.get(k)
|
||||
if not isinstance(v, str) or not v.strip():
|
||||
return None, "runtime-info 输出缺少 %s" % k
|
||||
out[k] = v.strip()
|
||||
if data.get("vmInfo"):
|
||||
out["vmInfo"] = data["vmInfo"]
|
||||
return out, None
|
||||
except subprocess.TimeoutExpired:
|
||||
return None, "runtime-info 执行超时"
|
||||
except Exception as e:
|
||||
return None, "runtime-info 调用异常:%s" % e
|
||||
data, err = run_runtime_info(RISK_ENV.get(provider, 0), account)
|
||||
if err:
|
||||
return None, err
|
||||
out = {}
|
||||
for k in ("machineToken", "machineCode", "machineType"):
|
||||
v = data.get(k)
|
||||
if not isinstance(v, str) or not v.strip():
|
||||
return None, "runtime-info 输出缺少 %s" % k
|
||||
out[k] = v.strip()
|
||||
if data.get("vmInfo"):
|
||||
out["vmInfo"] = data["vmInfo"]
|
||||
return out, None
|
||||
|
||||
|
||||
def status():
|
||||
|
||||
Reference in New Issue
Block a user