/* 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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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 = '守护进程 ' + (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 = '
' + "暂无账号,请先到「账号管理」添加 TRAE / WorkBuddy 账号
"; return; } wrap.innerHTML = accounts.map(function (a) { var st = a.today && a.today.status ? STATUS[a.today.status] : null; var statusHtml = st ? '' + st.icon + " " + st.label + "" : '⏳ 今日未签'; var credits = ""; if (a.last_result && typeof a.last_result.credits === "number") { credits = "剩余积分 " + a.last_result.credits + ""; } else if (a.last_result && a.last_result.points) { credits = "本次 +" + a.last_result.points + ""; } var msg = a.today && a.today.message ? esc(a.today.message) : ""; return ( '
' + '
' + platformName(a.platform) + "" + '' + esc(a.name) + "
" + '
' + statusHtml + (msg ? '' + msg + "" : "") + "
" + '
' + (credits ? "" + credits + "" : "") + (a.last_result ? "最近运行 " + esc(a.last_result.time || "") + "" : "") + (!a.enabled ? "已禁用" : "") + "
" + '
' + '' + '' + "
" ); }).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 = '

暂无账号,点击右上角「添加账号」。

'; return; } var rows = accounts.map(function (a) { var st = a.today && a.today.status ? STATUS[a.today.status] : null; return "" + "" + platformName(a.platform) + "" + "" + esc(a.name) + (a.need_reauth ? ' 需重新授权' : "") + (a.platform === "qoder" ? '
' + esc((a.auth && a.auth.domain) || "") + "
" : "") + "" + "" + esc(a.auth.accessToken || "") + "" + "" + (st ? st.icon + " " + st.label : "⏳ 今日未签") + "" + "" + (a.last_result && typeof a.last_result.credits === "number" ? a.last_result.credits : "—") + "" + "" + (a.enabled ? "启用" : "禁用") + "" + '' + '' + (a.platform === "qoder" ? '' : "") + '" + '' + ""; }).join(""); wrap.innerHTML = '' + rows + "
平台名称Token今日积分状态操作
"; 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 === "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(); $("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); }); }); $("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 openSettingsTab() { if (!settingsDirty) { settings = JSON.parse(JSON.stringify( (state && state.settings) || { schedule_times: [], webhook_url: "", notify_on: [] })); } renderSettings(); } 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) + ''; 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(); })();