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
+61 -2
View File
@@ -47,7 +47,7 @@
fail: { label: "签到失败", cls: "fail", icon: "❌" },
no_session: { label: "需重新授权", cls: "no_session", icon: "🔒" },
no_auth: { label: "缺少凭据", cls: "no_auth", icon: "🔒" },
retry_later: { label: "风控冷却", cls: "retry_later", icon: "⏳" }
retry_later: { label: "活动未下发·待重试", cls: "retry_later", icon: "⏳" }
};
function platformName(p) { return p === "trae" ? "TRAE" : (p === "qoder" ? "Qoder" : "WorkBuddy"); }
@@ -256,7 +256,7 @@
// ---------------------------------------------------------------- add modal
function openModal() { $("modal-add").classList.remove("hidden"); }
function closeModal(id) { $(id).classList.add("hidden"); stopOauth(); $("add-msg").textContent = ""; }
function closeModal(id) { $(id).classList.add("hidden"); stopOauth(); stopQdLogin(); $("add-msg").textContent = ""; }
document.querySelectorAll("[data-close]").forEach(function (b) {
b.addEventListener("click", function () { closeModal(b.dataset.close); });
@@ -310,6 +310,65 @@
});
// ---- qoder 一键登录(OAuth 设备授权,绑定本机设备)
var qdState = null, qdTimer = null;
function stopQdLogin() {
if (qdTimer) { clearInterval(qdTimer); qdTimer = null; }
qdState = null;
}
$("btn-qoder-login").addEventListener("click", function () {
var realm = $("qoder-domain").value.indexOf("com.cn") >= 0 ? "cn" : "intl";
var btn = this;
stopQdLogin();
btn.disabled = true;
$("qoder-login-link").innerHTML = "";
$("qoder-login-status").textContent = "正在申请授权链接…";
api("POST", "/qoder/login/start", { realm: realm })
.then(function (d) {
qdState = d.state;
var box = $("qoder-login-link");
box.innerHTML = "";
var a = document.createElement("a");
a.href = d.auth_url;
a.target = "_blank";
a.rel = "noopener";
a.textContent = "👉 点这里打开 Qoder 授权页";
box.appendChild(a);
var p = document.createElement("div");
p.className = "muted";
p.textContent = "(或在浏览器打开:" + d.auth_url + ")";
box.appendChild(p);
$("qoder-login-status").textContent =
"等待浏览器里确认授权…(" + Math.round(d.ttl / 60) + " 分钟内有效)";
qdTimer = setInterval(pollQdLogin, 3000);
})
.catch(function (e) { $("qoder-login-status").textContent = e.message; })
.finally(function () { btn.disabled = false; });
});
function pollQdLogin() {
if (!qdState) return;
api("GET", "/qoder/login/poll?state=" + encodeURIComponent(qdState))
.then(function (d) {
if (d.status === "ok") {
stopQdLogin();
$("qoder-login-status").textContent = "登录成功";
toast(d.message || "Qoder 账号已添加", "ok");
closeModal("modal-add");
refreshState();
} else if (d.status === "error") {
stopQdLogin();
$("qoder-login-status").textContent = d.message || "登录失败";
}
})
.catch(function (e) {
stopQdLogin();
$("qoder-login-status").textContent = e.message;
});
}
$("btn-qoder-add").addEventListener("click", function () {
var token = $("qoder-token").value.trim();
if (!token) return addMsg("请填写 Access Token", false);
+6
View File
@@ -239,3 +239,9 @@ label .input { margin-top: 6px; }
main { padding: 16px 12px 40px; }
.tab { padding: 10px 12px; }
}
/* Qoder 一键登录(设备授权) */
.qoder-oauth { margin-bottom: 14px; background: var(--bg2, #f6f8fa); }
.qoder-oauth a { color: var(--accent, #2563eb); font-weight: 600; text-decoration: none; }
.qoder-oauth a:hover { text-decoration: underline; }
.qoder-oauth .msg { white-space: pre-wrap; word-break: break-all; }
+10 -2
View File
@@ -175,6 +175,14 @@
</div>
<div id="mtab-qoder" class="mtabpage">
<div class="panel qoder-oauth">
<div class="row">
<button class="btn" id="btn-qoder-login">一键登录(设备授权,推荐)</button>
<span id="qoder-login-status" class="muted"></span>
</div>
<div id="qoder-login-link" class="msg"></div>
<p class="muted">点按钮 → 打开弹出的授权链接 → 在浏览器里确认一次即可自动加入账号。<b>不建议</b>再手工粘贴别处提取的 Token:国际版的「每天领 100 Credits」活动只下发给<b>这次登录的设备</b>,用别的机器提取的 Token 会看不到活动。</p>
</div>
<label>名称(可选)<input id="qoder-name" class="input input-wide" placeholder="Qoder 主账号"></label>
<label>Access Token <span class="req">*</span><input id="qoder-token" class="input input-wide" placeholder="dt- 开头的 JWT"></label>
<label>Refresh Token(可选,用于自动续期)<input id="qoder-refresh" class="input input-wide" placeholder="drt- 开头,可选"></label>
@@ -187,8 +195,8 @@
<details class="help" open>
<summary>如何获取 Qoder Token?(三种方式)</summary>
<div class="help-body">
<p><b>方式 A · 设备授权登录(推荐,无需抓包)</b></p>
<p>在电脑上下载社区工具 <code>qoder_login.py</code>(GitHub: L0NE-6/Qoder-Daily),运行 <code>python qoder_login.py</code>,在浏览器打开授权页确认一次,工具会输出 <code>access_token|refresh_token</code>,分别粘贴到上方即可。</p>
<p><b>方式 A · 一键登录(推荐,无需抓包)</b></p>
<p>直接点上方「一键登录(设备授权)」,在浏览器里确认一次即可。这种方式拿到的 Token 与<b>本机设备绑定</b>,国际版才能看到「每天领 100 Credits」活动。</p>
<p><b>方式 B · 浏览器抓包(最快)</b></p>
<p>浏览器登录 Qoder 网页版 → F12 开发者工具 → 网络 → 点页面里任意功能,任选一个请求 → 在请求头里找到 <code>authorization: Bearer dt-...</code></p>
<p><b>复制技巧</b>:右键该值 →「Copy value」复制完整内容,粘贴后删掉开头的 <code>Bearer </code>。</p>