Files
fnos-agent-checkin/AgentCheckin/app/backend/trae.py
T
cbb a3b84d5ce2 feat: AgentCheckin v1.2.2 — fnOS 应用:TRAE/WorkBuddy/Qoder 三平台每日积分自动签到
- 原生 fnOS 应用(CGI 入口 + Python 守护进程),依赖 python312 运行时
- 三平台签到:TRAE 每日 200 积分、WorkBuddy 7 天循环、Qoder 每日 100 Credits
- WorkBuddy 网页授权(OAuth 设备流)、TRAE icubeAuth 解密 Token、Qoder Cosy 设备头
- 多时段调度(默认 00:05/09:00/12:30/19:00)、启动补跑、幂等签到+回查确认
- 多平台 Webhook 通知(企业微信/钉钉/飞书/Bark/Server酱/PushPlus 自动识别)
- Qoder 双站点 Token 隔离自动探测与修复
- 前端:仪表盘/账号管理/签到记录/设置四个页签,30s 轮询+变化检测+隐藏暂停
- 测试:34 后端 + 25 E2E + 9 前端回归 + 真实时钟调度测试
2026-10-13 19:09:48 +08:00

197 lines
7.2 KiB
Python
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.
#!/usr/bin/env python3
"""TRAE (ByteDance, CN edition) daily check-in client.
Endpoints (reverse-engineered from the TRAE desktop client, see community
projects wb-trae-checkin / Trae-AutoCheckin):
base https://api.trae.cn
status POST /trae/api/v2/ug/checkin_credits/status body {"req_source":1}
claim POST /trae/api/v2/ug/checkin_credits/claim body {"req_source":1}
Auth: Authorization: Cloud-IDE-JWT <accessToken> (a JWT issued by the client).
The server also expects desktop-client headers including a 16-digit device id.
Result codes:
0 success (claim returns data.points, usually 200)
9095 already checked in today
9074 device id remembered by the server -> rotate to a fresh device id
1001/1002/401/403 session invalid -> re-login required
"""
import base64
import hashlib
import json
import time
from common import http_post_json, log
API_BASE = "https://api.trae.cn"
TRAE_VERSION = "1.107.1"
EP_STATUS = "/trae/api/v2/ug/checkin_credits/status"
EP_CLAIM = "/trae/api/v2/ug/checkin_credits/claim"
AUTH_FAIL_CODES = {1001, 1002, 401, 403}
def _b64url_decode(seg):
seg += "=" * (-len(seg) % 4)
return base64.urlsafe_b64decode(seg.encode("ascii"))
def jwt_payload(token):
"""Decode a JWT payload without verifying the signature."""
try:
parts = token.split(".")
if len(parts) < 2:
return {}
return json.loads(_b64url_decode(parts[1]).decode("utf-8"))
except Exception:
return {}
def user_region(account):
auth = account.get("auth", {})
if auth.get("region"):
return auth["region"]
payload = jwt_payload(auth.get("accessToken", ""))
region = (payload.get("userRegion") or {}).get("region")
return region or "CN"
# ---------------------------------------------------------------------------
# Device id (16 digits): stable per account, rotated on 9074
# ---------------------------------------------------------------------------
def stable_device_id(account_id):
digest = hashlib.sha256(("agentcheckin:%s" % account_id).encode("utf-8")).hexdigest()
n = int(digest[:13], 16)
return str(n).ljust(16, "0")[:16]
def rotate_device_id():
return (str(int(time.time() * 1000))[-13:] + str(int(time.time() * 1000) % 1000).zfill(3))[:16]
def device_id(account):
did = account.get("device_id")
if not did:
did = stable_device_id(account.get("id", "default"))
account["device_id"] = did
return did
def headers(account):
auth = account.get("auth", {})
return {
"Authorization": "Cloud-IDE-JWT %s" % auth.get("accessToken", ""),
"X-User-Region": user_region(account),
"User-Agent": "Trae/%s" % TRAE_VERSION,
"Content-Type": "application/json",
"x-device-id": device_id(account),
"x-device-type": "Windows",
"x-os-version": "10.0.19045",
"x-app-version": TRAE_VERSION,
}
# ---------------------------------------------------------------------------
# Check-in API
# ---------------------------------------------------------------------------
def query_status(account):
"""Return (checked, enable, credits, error). checked True/False/None."""
data, status, err = http_post_json(API_BASE + EP_STATUS, body={"req_source": 1},
headers=headers(account), timeout=20)
if err or data is None:
if status in (401, 403):
return None, None, None, "no_session"
return None, None, None, err or "status query failed"
if isinstance(data, dict):
code = data.get("code")
if code in AUTH_FAIL_CODES:
return None, None, None, "no_session"
if code == 0:
return (
bool(data.get("checked_in")),
data.get("enable") is not False,
data.get("credits"),
None,
)
return None, None, None, None
def claim(account):
"""Claim today's credits. Returns (status, message, points).
status: ok / already / fail / no_session / retry_later
"""
hdrs = headers(account)
data, status, err = http_post_json(API_BASE + EP_CLAIM, body={"req_source": 1},
headers=hdrs, timeout=20)
if status in (401, 403):
return "no_session", "登录态已失效,请重新获取 Token", 0
if data is None:
return "fail", "请求失败(HTTP %s:%s)" % (status, err), 0
if not isinstance(data, dict):
return "fail", "非 JSON 返回(HTTP %s)" % status, 0
code = data.get("code")
if code == 0:
d = data.get("data") if isinstance(data.get("data"), dict) else {}
return "ok", "签到成功", d.get("points") or 200
if code == 9095:
return "already", "今天已签到", 0
if code in AUTH_FAIL_CODES:
return "no_session", "登录态已失效(code=%s),请重新获取 Token" % code, 0
if code == 9074:
return "retry_later", "触发风控(9074),需更换设备号重试", 0
return "fail", "领取返回 code=%s %s" % (code, str(data.get("message") or data.get("msg") or "")[:80]), 0
def checkin_once(account):
"""Full single-account check-in: status -> claim -> verify.
Returns dict {status, message, points, credits}.
"""
checked, enable, credits, err = query_status(account)
if err == "no_session":
return {"status": "no_session", "message": "登录态已失效,请重新获取 Token", "points": 0}
if checked is True:
return {"status": "already", "message": "今日已签到",
"points": 0, "credits": credits}
if enable is False:
return {"status": "inactive", "message": "当前账号/时段未开放签到活动", "points": 0}
rotated = False
for attempt in range(2):
status, message, points = claim(account)
if status == "ok":
# verify server-side
for _ in range(3):
time.sleep(2)
v_checked, _, v_credits, _ = query_status(account)
if v_checked is True:
return {"status": "ok", "message": "签到成功(已回查确认)",
"points": points, "credits": v_credits}
if v_checked is False:
break
return {"status": "ok_unverified",
"message": "领取成功(+%s)但回查未确认" % points,
"points": points, "credits": credits}
if status == "already":
return {"status": "already", "message": "今天已签到", "points": 0,
"credits": credits}
if status == "no_session":
return {"status": "no_session", "message": message, "points": 0}
if status == "retry_later" and not rotated:
account["device_id"] = rotate_device_id()
rotated = True
log("trae account %s: 9074 -> rotated device id" % account.get("id"))
time.sleep(3)
continue
return {"status": "fail", "message": message, "points": 0}
return {"status": "retry_later", "message": "风控限流(9074),换设备号后仍失败,下个时段自动补签",
"points": 0}
def account_display(account):
return account.get("name") or (account.get("auth") or {}).get("nickname") or "TRAE 账号"