Files
fnos-agent-checkin/AgentCheckin/app/www/index.html
T
cbb c263edc3e7 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 前端,全部通过
2026-10-19 12:04:44 +08:00

227 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Agent 签到助手</title>
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<header class="topbar">
<div class="brand">
<img src="assets/logo.svg" alt="" class="brand-logo">
<div>
<h1>Agent 签到助手</h1>
<p class="sub">TRAE · WorkBuddy 每日积分自动签到</p>
</div>
</div>
<div class="topbar-right">
<span id="daemon-chip" class="chip chip-gray">守护进程 —</span>
<span id="nextrun-chip" class="chip chip-blue">下次签到 —</span>
<button id="btn-run-all" class="btn btn-primary">立即签到</button>
</div>
</header>
<nav class="tabs">
<button class="tab active" data-tab="dashboard">仪表盘</button>
<button class="tab" data-tab="accounts">账号管理</button>
<button class="tab" data-tab="logs">签到记录</button>
<button class="tab" data-tab="settings">设置</button>
</nav>
<main>
<!-- ======================= 仪表盘 ======================= -->
<section id="tab-dashboard" class="tabpage active">
<div id="dash-cards" class="card-grid"></div>
<div class="panel">
<h2>使用说明</h2>
<ul class="hint-list">
<li><b>TRAE</b>(字节跳动·国内版):每日自动领取 <b>200</b> Work 专属积分。</li>
<li><b>WorkBuddy / CodeBuddy</b>(腾讯):7 天循环签到,第 1-6 天每天 100 分,第 7 天 1000 分。</li>
<li><b>Qoder</b>(国内版):每日自动领取 <b>100</b> Credits(活动非零点准时下发,多时段自动补领)。</li>
<li>签到由 NAS 本地守护进程自动执行,失败会在下一个时段自动补签; Token 过期会标记需重新授权。</li>
<li>所有凭据仅保存在本机 <code>@appdata/AgentCheckin</code>,不会上传到任何第三方。</li>
</ul>
</div>
</section>
<!-- ======================= 账号管理 ======================= -->
<section id="tab-accounts" class="tabpage">
<div class="panel">
<div class="panel-head">
<h2>账号列表</h2>
<button class="btn btn-primary" id="btn-add-account">+ 添加账号</button>
</div>
<div id="account-table-wrap"></div>
</div>
</section>
<!-- ======================= 签到记录 ======================= -->
<section id="tab-logs" class="tabpage">
<div class="panel">
<div class="panel-head">
<h2>运行日志</h2>
<div>
<label class="switch-label"><input type="checkbox" id="log-autorefresh" checked> 自动刷新</label>
<button class="btn" id="btn-refresh-logs">刷新</button>
</div>
</div>
<pre id="log-view" class="log-view">加载中…</pre>
</div>
</section>
<!-- ======================= 设置 ======================= -->
<section id="tab-settings" class="tabpage">
<div class="panel">
<h2>签到时间</h2>
<p class="muted">每天在这些时间点自动检查并签到(已签会自动跳过)。错过时段后在守护进程启动时会自动补跑。</p>
<div id="time-chips" class="chips"></div>
<div class="row">
<input type="time" id="new-time" class="input input-time">
<button class="btn" id="btn-add-time">添加时间点</button>
</div>
</div>
<div class="panel">
<h2>Qoder 风控身份(UMID)</h2>
<p class="muted">Qoder 国际版只对带设备风控身份的请求下发签到活动。首次使用需下载官方组件(约 30MB,仅一次)。</p>
<div id="umid-status" class="muted">检测中…</div>
<div class="row">
<button class="btn" id="btn-umid-install">下载 / 重新获取风控组件</button>
</div>
<div id="umid-msg" class="msg"></div>
</div>
<div class="panel">
<h2>消息通知(可选)</h2>
<p class="muted">签到后向 Webhook 发送 POST JSON(<code>{"title","content","text","msgtype"}</code>),兼容企业微信/钉钉/飞书/Bark 等机器人。留空则不通知。</p>
<input type="url" id="webhook-url" class="input input-wide" placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...">
<label style="margin-top:12px">通知平台
<select id="webhook-type" class="input">
<option value="auto">自动识别(推荐)</option>
<option value="wecom">企业微信机器人</option>
<option value="dingtalk">钉钉机器人</option>
<option value="feishu">飞书机器人</option>
<option value="bark">Bark(iOS 推送)</option>
<option value="generic">通用 JSON(Server酱 / PushPlus 等)</option>
</select>
</label>
<div class="row">
<label class="switch-label"><input type="checkbox" id="notify-fail" checked> 失败时通知</label>
<label class="switch-label"><input type="checkbox" id="notify-success"> 成功时也通知</label>
</div>
<div class="row">
<button class="btn btn-primary" id="btn-save-settings">保存设置</button>
<button class="btn" id="btn-test-notify">发送测试通知</button>
</div>
<div id="settings-msg" class="msg"></div>
</div>
</section>
</main>
<!-- ======================= 添加账号弹窗 ======================= -->
<div id="modal-add" class="modal-mask hidden">
<div class="modal">
<div class="modal-head">
<h3>添加账号</h3>
<button class="modal-close" data-close="modal-add">×</button>
</div>
<div class="modal-tabs">
<button class="mtab active" data-mtab="wb-oauth">WorkBuddy 网页授权</button>
<button class="mtab" data-mtab="wb-manual">WorkBuddy 手动</button>
<button class="mtab" data-mtab="trae">TRAE</button>
<button class="mtab" data-mtab="qoder">Qoder</button>
</div>
<div id="mtab-wb-oauth" class="mtabpage active">
<p>点击下方按钮生成授权链接,在浏览器中打开并登录腾讯 CodeBuddy / WorkBuddy 账号,本页面会自动完成绑定。</p>
<button class="btn btn-primary" id="btn-oauth-begin">生成授权链接</button>
<div id="oauth-box" class="hidden">
<p>请在浏览器中打开以下链接完成登录(10 分钟内有效):</p>
<div class="row">
<input id="oauth-url" class="input input-wide" readonly>
<button class="btn" id="btn-oauth-copy">复制</button>
</div>
<p id="oauth-status" class="muted">等待登录…</p>
</div>
</div>
<div id="mtab-wb-manual" class="mtabpage">
<label>名称(可选)<input id="wb-name" class="input input-wide" placeholder="我的 CodeBuddy"></label>
<label>Access Token <span class="req">*</span><input id="wb-token" class="input input-wide" placeholder="accessToken"></label>
<label>Refresh Token(可选,用于自动续期)<input id="wb-refresh" class="input input-wide" placeholder="refreshToken"></label>
<label>区域
<select id="wb-domain" class="input">
<option value="codebuddy.cn">国内版 codebuddy.cn(支持签到)</option>
<option value="workbuddy.ai">国际版 workbuddy.ai(无签到)</option>
</select>
</label>
<button class="btn btn-primary" id="btn-wb-manual-add">添加</button>
</div>
<div id="mtab-trae" class="mtabpage">
<label>名称(可选)<input id="trae-name" class="input input-wide" placeholder="TRAE 主账号"></label>
<label>Access Token <span class="req">*</span><input id="trae-token" class="input input-wide" placeholder="eyJ 开头的 JWT"></label>
<label>Refresh Token(可选)<input id="trae-refresh" class="input input-wide" placeholder="可选"></label>
<details class="help">
<summary>如何获取 TRAE Token?</summary>
<div class="help-body">
<p>在登录了 TRAE 国内版客户端的电脑上,凭据保存在:</p>
<p><code>Windows: %APPDATA%\Trae CN\User\globalStorage\storage.json</code></p>
<p>其中键 <code>iCubeAuthInfo://icube.cloudide</code> 的值为加密串,解密后可得 <code>token</code>(即 Access Token,JWT 格式)。</p>
<p>也可使用社区工具 <code>trae_get_token.py</code>(Trae-AutoCheckin 项目)一键提取。</p>
<p class="muted">Token 约 60 天有效;过期后在本页重新粘贴即可。</p>
</div>
</details>
<button class="btn btn-primary" id="btn-trae-add">添加</button>
</div>
<div id="mtab-qoder" class="mtabpage">
<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>
<label>站点区域(重要:两个站点 Token 不互通,选错会 401)
<select id="qoder-domain" class="input">
<option value="openapi.qoder.com.cn">国内版 openapi.qoder.com.cn(qoder.cn 客户端)</option>
<option value="openapi.qoder.sh">国际版 openapi.qoder.sh(qoder.com 客户端)</option>
</select>
</label>
<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>方式 B · 浏览器抓包(最快)</b></p>
<p>浏览器登录 Qoder 网页版 → F12 开发者工具 → 网络 → 点页面里任意功能,任选一个请求 → 在请求头里找到 <code>authorization: Bearer dt-...</code></p>
<p><b>复制技巧</b>:右键该值 →「Copy value」复制完整内容,粘贴后删掉开头的 <code>Bearer </code>。</p>
<p><b>注意</b>:Qoder 的 <code>dt-</code> Token 原生就只有 <b>27/28 个字符</b>(不是截断,这是它的原生格式),<code>drt-</code> 刷新串同理。</p>
<p><b>关键</b>:国内版(qoder.cn)与国际版(qoder.com)的 Token <b>不互通</b>——用哪个客户端登录提的 Token,上方「站点区域」就要选对应的那个,选错会直接 401。拿不准可以先用「测连接」,它会自动探测 Token 属于哪个站点。</p>
<p><b>方式 C · 本机客户端提取</b></p>
<p>在装了并登录 Qoder 客户端的 Windows 上运行社区工具 <code>01_extract.py</code>(GitHub: wallechfox/qoder-checkin),自动解密 <code>auth.v1.dat</code> 取出 Token 和设备标识。</p>
<p class="muted">Token 有效期较短,过期后重新获取即可;填了 Refresh Token 的应用会自动续期。</p>
<p><b>关于风控身份(重要)</b></p>
<p>Qoder 服务端只对携带设备风控身份(<code>Cosy-MachineToken/Code/Type</code>)的请求下发「每天领 100 Credits」活动,否则活动列表为空。</p>
<p>本应用会自动从官方 npm 包 <code>@qoder-ai/qodercli</code> 提取 UMID 组件并生成身份(首次约需下载 30MB,之后缓存复用)。可在「设置 → Qoder 风控身份」查看/重新获取。</p>
</div>
</details>
<details class="help">
<summary>设备标识(可选,领取提示风控时填写)</summary>
<div class="help-body">
<p>Qoder 签到需要 Cosy-* 设备标识。一般情况留空即可;若提示风控/领取失败,用方式 C 的工具提取后填入:</p>
<div class="row"><input id="qoder-machine-id" class="input input-wide" placeholder="Cosy-MachineId"></div>
<div class="row"><input id="qoder-machine-token" class="input input-wide" placeholder="Cosy-MachineToken"></div>
<div class="row"><input id="qoder-machine-code" class="input input-wide" placeholder="Cosy-MachineCode"></div>
<div class="row"><input id="qoder-machine-type" class="input input-wide" placeholder="Cosy-MachineType"></div>
<div class="row"><input id="qoder-version" class="input input-wide" placeholder="Cosy-Version(客户端版本号)"></div>
</div>
</details>
<button class="btn btn-primary" id="btn-qoder-add">添加</button>
</div>
<div id="add-msg" class="msg"></div>
</div>
</div>
<div id="toast" class="toast hidden"></div>
<script src="assets/app.js"></script>
</body>
</html>