Files
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

148 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""Full user-journey test against the REAL NAS installation."""
import json
import subprocess
import sys
import time
import urllib.request
import urllib.error
BASE = "https://192.168.31.10:5667/cgi/ThirdParty/AgentCheckin/index.cgi"
PASS, FAIL = [], []
def check(name, cond, extra=""):
(PASS if cond else FAIL).append(name)
print((" PASS " if cond else " FAIL ") + name + ("" if cond else " " + str(extra)[:300]))
def cookie():
r = subprocess.run(["bash", "/tmp/get_cookie.sh"], capture_output=True, text=True, timeout=60)
return r.stdout.strip()
def api(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
if data:
req.add_header("Content-Type", "application/json")
import ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
import http.cookiejar
opener = urllib.request.build_opener(
urllib.request.HTTPSHandler(context=ctx),
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
# inject ost cookie manually
req.add_header("Cookie", "ost=" + COOKIE)
try:
with opener.open(req, timeout=60) as resp:
return resp.status, json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read().decode("utf-8"))
COOKIE = cookie()
print("cookie acquired:", COOKIE[:16], "...")
print("== 1. dashboard state ==")
st, d = api("GET", "/api/state")
check("GET /api/state", st == 200 and d.get("today") is not None, (st, d))
check("daemon alive", d.get("daemon_alive") is True, d.get("daemon_alive"))
check("next run scheduled", bool(d.get("next_run_at")), d.get("next_run_at"))
print(" daemon:", d.get("daemon"), "| next:", d.get("next_run_at"))
print("== 2. add TRAE account (fake token) ==")
st, d = api("POST", "/api/accounts/add_trae", {
"name": "NAS 测试账号",
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyUmVnaW9uIjp7InJlZ2lvbiI6IkNOIn19.sig",
})
check("add trae account", st == 200 and d.get("ok") is True, (st, d))
trae_id = d.get("account", {}).get("id")
check("token masked in response", "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9" not in json.dumps(d), d)
print("== 3. add WorkBuddy account (manual, fake token) ==")
st, d = api("POST", "/api/accounts/add_wb_manual", {
"name": "NAS 测试 WB", "access_token": "fake-wb-token-xyz", "domain": "codebuddy.cn",
})
check("add wb account", st == 200 and d.get("ok") is True, (st, d))
wb_id = d.get("account", {}).get("id")
print("== 4. account list ==")
st, d = api("GET", "/api/accounts")
check("2 accounts listed", len(d.get("accounts", [])) == 2, d)
check("no raw tokens anywhere", "fake-wb-token-xyz" not in json.dumps(d), d)
print("== 5. trigger check-in (立即签到) ==")
st, d = api("POST", "/api/checkin", {})
check("checkin triggered", st == 200 and d.get("ok") is True, (st, d))
print(" mode:", d.get("mode"))
print("== 6. wait for daemon to execute ==")
deadline = time.time() + 60
results = {}
while time.time() < deadline:
st, d = api("GET", "/api/state")
tr = d.get("today_results") or {}
if trae_id in tr and wb_id in tr:
results = tr
break
time.sleep(2)
check("both accounts ran", bool(results), d.get("today_results"))
if results:
for aid, r in results.items():
print(" %s (%s): %s — %s" % (r.get("name"), r.get("platform"), r.get("status"), r.get("message")))
check("trae handled gracefully (no_session expected)",
results.get(trae_id, {}).get("status") in ("no_session", "fail", "ok", "already", "retry_later"),
results.get(trae_id))
check("wb handled gracefully",
results.get(wb_id, {}).get("status") in ("no_session", "fail", "ok", "already", "retry_later"),
results.get(wb_id))
print("== 7. logs ==")
st, d = api("GET", "/api/logs?lines=30")
check("logs returned", st == 200 and len(d.get("lines", [])) > 0, (st, d))
for line in d.get("lines", [])[-4:]:
print(" ", line)
print("== 8. settings update ==")
st, d = api("POST", "/api/settings", {
"schedule_times": ["00:05", "09:00", "12:30", "19:00", "22:00"],
"webhook_url": "", "notify_on": ["fail", "success"],
})
check("settings saved", st == 200 and "22:00" in d["settings"]["schedule_times"], (st, d))
st, d = api("GET", "/api/state")
check("settings reflected in state", "22:00" in (d.get("settings") or {}).get("schedule_times", []), d.get("settings"))
print("== 9. account disable/enable/delete ==")
st, d = api("POST", "/api/accounts/update", {"id": wb_id, "enabled": False})
check("disable account", st == 200 and d["account"]["enabled"] is False, (st, d))
st, d = api("POST", "/api/accounts/update", {"id": wb_id, "enabled": True})
check("enable account", st == 200 and d["account"]["enabled"] is True, (st, d))
st, d = api("POST", "/api/accounts/delete", {"id": wb_id})
check("delete wb account", st == 200 and d.get("ok") is True, (st, d))
st, d = api("POST", "/api/accounts/delete", {"id": trae_id})
check("delete trae account", st == 200 and d.get("ok") is True, (st, d))
st, d = api("GET", "/api/accounts")
check("accounts empty again", len(d.get("accounts", [])) == 0, d)
print("== 10. static assets ==")
import ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
for path, marker in [("/", "Agent 签到助手"), ("/assets/app.js", "AgentCheckin"),
("/assets/style.css", "--primary"), ("/assets/logo.svg", "<svg")]:
req = urllib.request.Request(BASE + path)
req.add_header("Cookie", "ost=" + COOKIE)
with urllib.request.urlopen(req, timeout=20, context=ctx) as r:
body = r.read().decode("utf-8", "replace")
check("GET %s" % path, r.status == 200 and marker in body, body[:100])
print()
print("PASSED: %d FAILED: %d" % (len(PASS), len(FAIL)))
if FAIL:
print("failures:", FAIL)
sys.exit(1 if FAIL else 0)