Files
fnos-agent-checkin/AgentCheckin/app/www/assets/app.js
T
AgentCheckin 556e45d642 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 前端全绿
2026-10-19 12:49:06 +08:00

623 lines
26 KiB
JavaScript
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.
/* AgentCheckin frontend — vanilla JS, no dependencies */
(function () {
"use strict";
var state = null;
var pollTimer = null;
var oauthState = null;
var oauthTimer = null;
var logTimer = null;
var $ = function (id) { return document.getElementById(id); };
// ---------------------------------------------------------------- helpers
function toast(msg, kind) {
var t = $("toast");
t.textContent = msg;
t.className = "toast" + (kind ? " " + kind : "");
clearTimeout(t._timer);
t._timer = setTimeout(function () { t.classList.add("hidden"); }, 3200);
}
function api(method, path, body) {
var opts = { method: method, headers: {} };
if (body !== undefined && body !== null) {
opts.headers["Content-Type"] = "application/json";
opts.body = JSON.stringify(body);
}
return fetch("api" + path, opts).then(function (r) {
return r.json().catch(function () { return {}; });
}).then(function (d) {
if (d && d.error) throw new Error(d.error);
return d;
});
}
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
});
}
var STATUS = {
ok: { label: "签到成功", cls: "ok", icon: "✅" },
ok_unverified: { label: "已领取·待确认", cls: "ok_unverified", icon: "✅" },
already: { label: "今日已签", cls: "already", icon: "☑️" },
inactive: { label: "未开放签到", cls: "inactive", icon: "➖" },
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: "⏳" }
};
function platformName(p) { return p === "trae" ? "TRAE" : (p === "qoder" ? "Qoder" : "WorkBuddy"); }
// ---------------------------------------------------------------- tabs
document.querySelectorAll(".tab").forEach(function (btn) {
btn.addEventListener("click", function () {
document.querySelectorAll(".tab").forEach(function (b) { b.classList.remove("active"); });
document.querySelectorAll(".tabpage").forEach(function (p) { p.classList.remove("active"); });
btn.classList.add("active");
$("tab-" + btn.dataset.tab).classList.add("active");
if (btn.dataset.tab === "logs") loadLogs();
if (btn.dataset.tab === "accounts") renderAccounts();
if (btn.dataset.tab === "settings") openSettingsTab();
});
});
// ---------------------------------------------------------------- dashboard
function renderTop() {
var alive = state && state.daemon_alive;
var chip = $("daemon-chip");
chip.innerHTML = '<span class="dot"></span>守护进程 ' + (alive ? "运行中" : "已停止");
chip.className = "chip " + (alive ? "chip-green" : "chip-red");
var nr = $("nextrun-chip");
nr.textContent = "下次签到 " + ((state && state.next_run_at) || "—");
nr.className = "chip " + (state && state.next_run_at ? "chip-blue" : "chip-gray");
}
function renderCards() {
var wrap = $("dash-cards");
var accounts = (state && state.accounts) || [];
if (!accounts.length) {
wrap.innerHTML = '<div class="panel" style="grid-column:1/-1;text-align:center;color:var(--muted)">' +
"暂无账号,请先到「账号管理」添加 TRAE / WorkBuddy 账号</div>";
return;
}
wrap.innerHTML = accounts.map(function (a) {
var st = a.today && a.today.status ? STATUS[a.today.status] : null;
var statusHtml = st
? '<span class="status-chip ' + st.cls + '">' + st.icon + " " + st.label + "</span>"
: '<span class="status-chip pending">⏳ 今日未签</span>';
var credits = "";
if (a.last_result && typeof a.last_result.credits === "number") {
credits = "剩余积分 <b>" + a.last_result.credits + "</b>";
} else if (a.last_result && a.last_result.points) {
credits = "本次 +<b>" + a.last_result.points + "</b>";
}
var msg = a.today && a.today.message ? esc(a.today.message) : "";
return (
'<div class="acct-card' + (a.enabled ? "" : " disabled") + '">' +
'<div class="acct-head"><span class="platform-badge ' + esc(a.platform) + '">' + platformName(a.platform) + "</span>" +
'<span class="acct-name">' + esc(a.name) + "</span></div>" +
'<div class="acct-status">' + statusHtml + (msg ? '<span class="muted">' + msg + "</span>" : "") + "</div>" +
'<div class="acct-meta">' + (credits ? "<span>" + credits + "</span>" : "") +
(a.last_result ? "<span>最近运行 " + esc(a.last_result.time || "") + "</span>" : "") +
(!a.enabled ? "<span>已禁用</span>" : "") + "</div>" +
'<div class="acct-actions">' +
'<button class="btn btn-sm" data-act="run" data-id="' + esc(a.id) + '">立即签到</button>' +
'<button class="btn btn-sm" data-act="test" data-id="' + esc(a.id) + '">测连接</button>' +
"</div></div>"
);
}).join("");
wrap.querySelectorAll("button[data-act]").forEach(function (btn) {
btn.addEventListener("click", function () {
if (btn.dataset.act === "run") runCheckin(btn.dataset.id, btn);
else testAccount(btn.dataset.id, btn);
});
});
}
var lastStateJson = "";
function refreshState(force) {
return api("GET", "/state").then(function (d) {
var json = JSON.stringify(d);
if (!force && json === lastStateJson) return; // 数据没变,跳过重渲染
lastStateJson = json;
state = d;
renderTop();
renderCards();
}).catch(function (e) { /* keep old state on transient errors */ });
}
// ---------------------------------------------------------------- run / test
var runningBtn = null;
function runCheckin(accountId, btn) {
if (btn) { btn.disabled = true; btn.textContent = "执行中…"; runningBtn = btn; }
api("POST", "/checkin", accountId ? { account_id: accountId } : {})
.then(function (d) {
if (d.mode === "trigger") {
toast("已触发签到,守护进程正在执行…");
waitForRun(accountId);
} else {
toast("签到完成", "ok");
refreshState();
}
})
.catch(function (e) { toast(e.message, "err"); })
.finally(function () {
if (runningBtn) { runningBtn.disabled = false; runningBtn.textContent = "立即签到"; runningBtn = null; }
});
}
function waitForRun(accountId) {
// poll until today_results contains a fresh entry for the target
var started = Date.now();
var before = JSON.stringify((state && state.today_results) || {});
(function poll() {
if (Date.now() - started > 90000) { refreshState(); return; }
refreshState().then(function () {
var after = JSON.stringify((state && state.today_results) || {});
var done = after !== before && (!accountId || (state.today_results || {})[accountId]);
if (done) {
var r = accountId ? state.today_results[accountId] : null;
if (r) toast(platformName(r.platform) + " " + r.name + ":" + (r.message || r.status), r.status === "fail" || r.status === "no_session" ? "err" : "ok");
else toast("签到完成", "ok");
} else {
setTimeout(poll, 3000);
}
});
})();
}
function testAccount(id, btn) {
btn.disabled = true; btn.textContent = "检测中…";
api("POST", "/accounts/test", { id: id })
.then(function (d) {
var parts = [];
if (d.refresh) parts.push("续期:" + d.refresh);
if (typeof d.checked_in === "boolean") parts.push("今日" + (d.checked_in ? "已签" : "未签"));
if (d.enable === false) parts.push("活动未开放");
if (d.credits && typeof d.credits.remain === "number") parts.push("剩余积分 " + d.credits.remain);
else if (typeof d.credits === "number") parts.push("积分 " + d.credits);
if (d.hint) parts.push(d.hint);
if (d.error) parts.push("错误:" + d.error);
toast(parts.join(";") || "检测完成", d.error ? "err" : "ok");
})
.catch(function (e) { toast(e.message, "err"); })
.finally(function () { btn.disabled = false; btn.textContent = "测连接"; });
}
// ---------------------------------------------------------------- accounts tab
function renderAccounts() {
var wrap = $("account-table-wrap");
var accounts = (state && state.accounts) || [];
if (!accounts.length) {
wrap.innerHTML = '<p class="muted">暂无账号,点击右上角「添加账号」。</p>';
return;
}
var rows = accounts.map(function (a) {
var st = a.today && a.today.status ? STATUS[a.today.status] : null;
return "<tr>" +
"<td><span class='platform-badge " + esc(a.platform) + "'>" + platformName(a.platform) + "</span></td>" +
"<td>" + esc(a.name) + (a.need_reauth ? ' <span class="status-chip no_session">需重新授权</span>' : "") +
(a.platform === "qoder" ? '<div class="muted" style="font-size:11px">' + esc((a.auth && a.auth.domain) || "") + "</div>" : "") + "</td>" +
"<td>" + esc(a.auth.accessToken || "") + "</td>" +
"<td>" + (st ? st.icon + " " + st.label : "⏳ 今日未签") + "</td>" +
"<td>" + (a.last_result && typeof a.last_result.credits === "number" ? a.last_result.credits : "—") + "</td>" +
"<td>" + (a.enabled ? "启用" : "禁用") + "</td>" +
'<td class="actions">' +
'<button class="btn btn-sm" data-a2="run" data-id="' + esc(a.id) + '">签到</button>' +
(a.platform === "qoder" ? '<button class="btn btn-sm" data-a2="fixdomain" data-id="' + esc(a.id) + '" title="自动探测 Token 所属站点并切换">修复站点</button>' +
'<button class="btn btn-sm" data-a2="risk" data-id="' + esc(a.id) + '" title="重新生成设备风控身份(活动列表为空时使用)">刷新风控</button>' : "") +
'<button class="btn btn-sm" data-a2="toggle" data-id="' + esc(a.id) + '">' + (a.enabled ? "禁用" : "启用") + "</button>" +
'<button class="btn btn-sm btn-danger" data-a2="del" data-id="' + esc(a.id) + '">删除</button>' +
"</td></tr>";
}).join("");
wrap.innerHTML = '<table class="tbl"><thead><tr><th>平台</th><th>名称</th><th>Token</th><th>今日</th><th>积分</th><th>状态</th><th>操作</th></tr></thead><tbody>' + rows + "</tbody></table>";
wrap.querySelectorAll("button[data-a2]").forEach(function (btn) {
btn.addEventListener("click", function () {
var id = btn.dataset.id, act = btn.dataset.a2;
if (act === "run") runCheckin(id, btn);
else if (act === "risk") {
btn.disabled = true; btn.textContent = "生成中…";
api("POST", "/accounts/refresh_risk", { id: id })
.then(function (d) {
if (d.ok) { toast("风控身份已更新", "ok"); refreshState(); }
else { toast(d.error || "生成失败", "err"); }
})
.catch(function (e) { toast(e.message, "err"); })
.finally(function () { btn.disabled = false; btn.textContent = "刷新风控"; });
}
else if (act === "fixdomain") {
btn.disabled = true; btn.textContent = "探测中…";
api("POST", "/accounts/fix_domain", { id: id })
.then(function (d) {
if (d.ok) { toast("已切换到 " + d.domain, "ok"); refreshState(); }
else { toast(d.error || "探测失败", "err"); }
})
.catch(function (e) { toast(e.message, "err"); })
.finally(function () { btn.disabled = false; btn.textContent = "修复站点"; });
}
else if (act === "toggle") {
var acc = state.accounts.find(function (x) { return x.id === id; });
api("POST", "/accounts/update", { id: id, enabled: !acc.enabled })
.then(function () { refreshState(); }).catch(function (e) { toast(e.message, "err"); });
} else if (act === "del") {
if (!confirm("确定删除该账号?")) return;
api("POST", "/accounts/delete", { id: id })
.then(function () { toast("已删除", "ok"); refreshState(); })
.catch(function (e) { toast(e.message, "err"); });
}
});
});
}
// ---------------------------------------------------------------- add modal
function openModal() { $("modal-add").classList.remove("hidden"); }
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); });
});
$("modal-add").addEventListener("click", function (e) {
if (e.target === this) closeModal("modal-add");
});
$("btn-add-account").addEventListener("click", openModal);
document.querySelectorAll(".mtab").forEach(function (btn) {
btn.addEventListener("click", function () {
document.querySelectorAll(".mtab").forEach(function (b) { b.classList.remove("active"); });
document.querySelectorAll(".mtabpage").forEach(function (p) { p.classList.remove("active"); });
btn.classList.add("active");
$("mtab-" + btn.dataset.mtab).classList.add("active");
stopOauth();
});
});
function addMsg(msg, ok) {
var m = $("add-msg");
m.textContent = msg;
m.className = "msg " + (ok ? "ok" : "err");
}
$("btn-wb-manual-add").addEventListener("click", function () {
var token = $("wb-token").value.trim();
if (!token) return addMsg("请填写 Access Token", false);
api("POST", "/accounts/add_wb_manual", {
name: $("wb-name").value.trim(),
access_token: token,
refresh_token: $("wb-refresh").value.trim(),
domain: $("wb-domain").value
}).then(function () {
toast("WorkBuddy 账号已添加", "ok");
closeModal("modal-add"); refreshState();
}).catch(function (e) { addMsg(e.message, false); });
});
$("btn-trae-add").addEventListener("click", function () {
var token = $("trae-token").value.trim();
if (!token) return addMsg("请填写 Access Token", false);
api("POST", "/accounts/add_trae", {
name: $("trae-name").value.trim(),
access_token: token,
refresh_token: $("trae-refresh").value.trim()
}).then(function () {
toast("TRAE 账号已添加", "ok");
closeModal("modal-add"); refreshState();
}).catch(function (e) { addMsg(e.message, false); });
});
// ---- 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);
if (token.length < 10) return addMsg("Access Token 仅 " + token.length + " 字符,明显不完整,请重新复制", false);
var rt0 = $("qoder-refresh").value.trim();
if (rt0 && rt0.length < 10) return addMsg("Refresh Token 仅 " + rt0.length + " 字符,明显不完整,请重新复制", false);
var dev = {};
var map = {
"qoder-machine-id": "machineId",
"qoder-machine-token": "machineToken",
"qoder-machine-code": "machineCode",
"qoder-machine-type": "machineType",
"qoder-version": "version",
};
Object.keys(map).forEach(function (id) {
var v = $(id).value.trim();
if (v) dev[map[id]] = v;
});
api("POST", "/accounts/add_qoder", {
name: $("qoder-name").value.trim(),
access_token: token,
refresh_token: $("qoder-refresh").value.trim(),
domain: $("qoder-domain").value,
device: dev
}).then(function () {
toast("Qoder 账号已添加", "ok");
closeModal("modal-add"); refreshState();
}).catch(function (e) { addMsg(e.message, false); });
});
// ---- workbuddy oauth flow
function stopOauth() {
if (oauthTimer) { clearInterval(oauthTimer); oauthTimer = null; }
oauthState = null;
$("oauth-box").classList.add("hidden");
}
$("btn-oauth-begin").addEventListener("click", function () {
this.disabled = true;
api("POST", "/wb_oauth_begin", {})
.then(function (d) {
oauthState = d.state;
$("oauth-url").value = d.login_url;
$("oauth-box").classList.remove("hidden");
$("oauth-status").textContent = "等待登录…(链接 " + Math.round(d.ttl / 60) + " 分钟内有效)";
oauthTimer = setInterval(pollOauth, 2000);
})
.catch(function (e) { addMsg(e.message, false); })
.finally(function () { this.disabled = false; }.bind(this));
});
$("btn-oauth-copy").addEventListener("click", function () {
var input = $("oauth-url");
input.select();
var done = function () { toast("链接已复制", "ok"); };
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(input.value).then(done, function () { document.execCommand("copy"); done(); });
} else { document.execCommand("copy"); done(); }
});
function pollOauth() {
if (!oauthState) return;
api("GET", "/wb_oauth_poll?state=" + encodeURIComponent(oauthState))
.then(function (d) {
if (d.status === "done") {
stopOauth();
toast("WorkBuddy 账号已绑定", "ok");
closeModal("modal-add");
refreshState();
}
})
.catch(function (e) {
$("oauth-status").textContent = e.message;
stopOauth();
});
}
// ---------------------------------------------------------------- logs
function loadLogs() {
api("GET", "/logs?lines=300").then(function (d) {
$("log-view").textContent = (d.lines || []).join("\n") || "暂无日志";
$("log-view").scrollTop = $("log-view").scrollHeight;
}).catch(function () {
$("log-view").textContent = "日志加载失败";
});
}
$("btn-refresh-logs").addEventListener("click", loadLogs);
setInterval(function () {
if (document.hidden) return;
if ($("log-autorefresh").checked && $("tab-logs").classList.contains("active")) loadLogs();
}, 15000);
// ---------------------------------------------------------------- settings
// Local editing copy: initialised from server state ONLY when the tab is
// opened (and there are no unsaved edits). The 10s state poll must never
// overwrite it, otherwise in-progress edits are silently discarded.
var settings = null;
var settingsDirty = false;
function loadUmidStatus() {
api("GET", "/umid/status").then(function (d) {
var el = $("umid-status");
if (!el) return;
if (!d.supported) {
el.textContent = "当前 CPU 架构 " + d.cpu_arch + " 暂不支持自动生成(需 x86_64 / arm64)";
} else if (d.installed) {
el.textContent = "✅ 已安装 v" + d.version + "(" + d.arch + ",安装于 " + (d.installedAt || "-") + ")";
} else {
el.textContent = "⚠️ 未安装 —— 点击下方按钮下载(约 30MB)";
}
}).catch(function () {});
}
var umidBtn = $("btn-umid-install");
if (umidBtn) {
umidBtn.addEventListener("click", function () {
umidBtn.disabled = true;
umidBtn.textContent = "下载中(约 1-3 分钟)…";
$("umid-msg").textContent = "";
$("umid-msg").className = "msg";
api("POST", "/umid/install", { force: true })
.then(function (d) {
$("umid-msg").textContent = d.message || (d.ok ? "已安装" : "失败");
$("umid-msg").className = "msg " + (d.ok ? "ok" : "err");
toast(d.ok ? "风控组件已就绪" : (d.message || "下载失败"), d.ok ? "ok" : "err");
loadUmidStatus();
})
.catch(function (e) {
$("umid-msg").textContent = e.message;
$("umid-msg").className = "msg err";
})
.finally(function () {
umidBtn.disabled = false;
umidBtn.textContent = "下载 / 重新获取风控组件";
});
});
}
function openSettingsTab() {
if (!settingsDirty) {
settings = JSON.parse(JSON.stringify(
(state && state.settings) || { schedule_times: [], webhook_url: "", notify_on: [] }));
}
renderSettings();
loadUmidStatus();
}
function renderSettings() {
// renders ONLY from the local copy — never re-reads from `state` here
settings = settings || { schedule_times: [], webhook_url: "", notify_on: [] };
var chips = $("time-chips");
chips.innerHTML = "";
(settings.schedule_times || []).forEach(function (t) {
var el = document.createElement("span");
el.className = "time-chip";
el.innerHTML = esc(t) + '<button title="删除">×</button>';
el.querySelector("button").addEventListener("click", function () {
settings.schedule_times = settings.schedule_times.filter(function (x) { return x !== t; });
settingsDirty = true;
renderSettings();
});
chips.appendChild(el);
});
$("webhook-url").value = settings.webhook_url || "";
$("webhook-type").value = settings.webhook_type || "auto";
$("notify-fail").checked = (settings.notify_on || []).indexOf("fail") >= 0;
$("notify-success").checked = (settings.notify_on || []).indexOf("success") >= 0;
}
$("btn-add-time").addEventListener("click", function () {
var v = $("new-time").value;
if (!v) { toast("请先选择要添加的时间", "err"); return; }
settings.schedule_times = settings.schedule_times || [];
if (settings.schedule_times.indexOf(v) >= 0) {
toast("该时间已存在", "err");
return;
}
settings.schedule_times.push(v);
settings.schedule_times.sort();
settingsDirty = true;
renderSettings();
$("new-time").value = "";
});
$("btn-save-settings").addEventListener("click", function () {
var times = (settings.schedule_times || []).slice().sort();
if (!times.length) { $("settings-msg").textContent = "至少保留一个签到时间点"; $("settings-msg").className = "msg err"; return; }
var notify = [];
if ($("notify-fail").checked) notify.push("fail");
if ($("notify-success").checked) notify.push("success");
api("POST", "/settings", {
schedule_times: times,
webhook_url: $("webhook-url").value.trim(),
webhook_type: $("webhook-type").value,
notify_on: notify
}).then(function () {
settingsDirty = false;
$("settings-msg").textContent = "已保存";
$("settings-msg").className = "msg ok";
toast("设置已保存", "ok");
refreshState();
}).catch(function (e) {
$("settings-msg").textContent = e.message;
$("settings-msg").className = "msg err";
});
});
$("btn-test-notify").addEventListener("click", function () {
var url = $("webhook-url").value.trim();
if (!url) { $("settings-msg").textContent = "请先填写 Webhook 地址"; $("settings-msg").className = "msg err"; return; }
// save first so the daemon/notify uses the new url, then fire a real run
var notify = [];
if ($("notify-fail").checked) notify.push("fail");
if ($("notify-success").checked) notify.push("success");
api("POST", "/settings", { webhook_url: url, webhook_type: $("webhook-type").value, notify_on: notify })
.then(function () {
settingsDirty = false;
return api("POST", "/checkin", {});
})
.then(function () {
$("settings-msg").textContent = "已触发一次签到,通知将随结果发送";
$("settings-msg").className = "msg ok";
waitForRun(null);
})
.catch(function (e) {
$("settings-msg").textContent = e.message;
$("settings-msg").className = "msg err";
});
});
// ---------------------------------------------------------------- run all
$("btn-run-all").addEventListener("click", function () { runCheckin(null, null); });
// ---------------------------------------------------------------- boot
var STATE_POLL_MS = 30000;
function boot() {
refreshState(true);
renderAccounts();
pollTimer = setInterval(function () {
if (document.hidden) return; // 页面/iframe 不可见时暂停轮询
refreshState().then(renderAccountsIfVisible);
}, STATE_POLL_MS);
document.addEventListener("visibilitychange", function () {
if (!document.hidden) refreshState(true).then(renderAccountsIfVisible);
});
}
function renderAccountsIfVisible() {
if ($("tab-accounts").classList.contains("active")) renderAccounts();
}
boot();
})();