(function () { const statusLabels = { pending: "排队中", running: "执行中", waiting_2fa: "等待短信验证码", cancel_requested: "停止中", completed: "已完成", stopped: "已停止", failed: "失败", idle: "未启动", }; const modeLabels = { stable: "稳定版", optimized: "优化版", }; const loginModeLabels = { unified: "统一认证", zhjw_direct: "教务直连", }; function renderLogLine(log) { const line = document.createElement("div"); const level = (log.level || "INFO").toLowerCase(); line.className = `log-line level-${level}`; const meta = document.createElement("span"); meta.className = "log-meta"; const owner = log.student_id || "system"; meta.textContent = `${log.created_at} | ${owner} | ${log.scope} | ${log.level}`; const message = document.createElement("span"); message.textContent = log.message || ""; line.append(meta, message); return line; } function bindLogStream() { const shell = document.querySelector("[data-log-stream-url]"); const consoleNode = document.getElementById("log-console"); if (!shell || !consoleNode || !window.EventSource) { return; } const streamUrl = shell.getAttribute("data-log-stream-url"); const eventSource = new EventSource(streamUrl); eventSource.onmessage = function (event) { try { const payload = JSON.parse(event.data); if (consoleNode.querySelector(".muted")) { consoleNode.innerHTML = ""; } consoleNode.appendChild(renderLogLine(payload)); while (consoleNode.children.length > 360) { consoleNode.removeChild(consoleNode.firstChild); } consoleNode.scrollTop = consoleNode.scrollHeight; } catch (_error) { // Ignore malformed frames and keep the stream alive. } }; } function modeLabel(mode) { return modeLabels[mode] || mode || "稳定版"; } function loginModeLabel(mode) { return loginModeLabels[mode] || mode || "统一认证"; } function normalizeLogin2faCode(value) { return String(value || "").replace(/\D/g, "").slice(0, 6); } function setFormMessage(node, message, level) { if (!node) { return; } node.className = `form-message ${level ? `level-${level}` : ""}`.trim(); node.textContent = message || ""; } function secondsText(value) { const seconds = Math.max(0, Number(value || 0)); return String(Math.ceil(seconds)); } async function submitLogin2faCode(taskId, code, scope) { const endpoint = scope === "admin" ? `/api/admin/tasks/${taskId}/login-2fa` : `/api/user/tasks/${taskId}/login-2fa`; const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", "X-Requested-With": "fetch", }, body: JSON.stringify({ code }), }); let payload = {}; try { payload = await response.json(); } catch (_error) { payload = { ok: false, message: "提交失败,请稍后重试。" }; } if (!response.ok || !payload.ok) { throw new Error(payload.message || "提交失败,请稍后重试。"); } return payload.message || "验证码已提交。"; } function updateUserLogin2fa(challenge) { const panel = document.getElementById("user-login-2fa-panel"); if (!panel) { return; } const phone = document.getElementById("user-login-2fa-phone"); const remaining = document.getElementById("user-login-2fa-remaining"); const form = document.getElementById("user-login-2fa-form"); const message = document.getElementById("user-login-2fa-message"); const input = form ? form.querySelector("input[name='code']") : null; const button = form ? form.querySelector("button[type='submit']") : null; const nextTaskId = challenge ? String(challenge.task_id) : ""; if (!challenge) { panel.hidden = true; panel.dataset.taskId = ""; if (input) { input.value = ""; input.disabled = false; } if (button) { button.disabled = false; } setFormMessage(message, "", ""); return; } if (panel.dataset.taskId !== nextTaskId) { if (input) { input.value = ""; input.disabled = false; } if (button) { button.disabled = false; } setFormMessage(message, "", ""); } panel.dataset.taskId = nextTaskId; panel.hidden = false; if (phone) { phone.textContent = challenge.phone_mask || "绑定手机"; } if (remaining) { remaining.textContent = secondsText(challenge.remaining_seconds); } } function createAdminLogin2faItem(taskId) { const item = document.createElement("section"); item.className = "login-2fa-item"; item.dataset.taskId = String(taskId); const info = document.createElement("div"); const title = document.createElement("h3"); const desc = document.createElement("p"); const meta = document.createElement("div"); meta.className = "login-2fa-meta"; info.append(title, desc, meta); const form = document.createElement("form"); form.className = "login-2fa-form"; form.dataset.login2faTaskId = String(taskId); const input = document.createElement("input"); input.type = "text"; input.name = "code"; input.inputMode = "numeric"; input.autocomplete = "one-time-code"; input.maxLength = 6; input.pattern = "[0-9]{6}"; input.placeholder = "6 位验证码"; input.required = true; const button = document.createElement("button"); button.type = "submit"; button.className = "btn btn-secondary"; button.textContent = "提交验证码"; const message = document.createElement("p"); message.className = "form-message"; form.append(input, button, message); item.append(info, form); return item; } function updateAdminLogin2fa(challenges) { const panel = document.getElementById("admin-login-2fa-panel"); const list = document.getElementById("admin-login-2fa-list"); if (!panel || !list) { return; } const activeChallenges = Array.isArray(challenges) ? challenges : []; panel.hidden = activeChallenges.length === 0; const activeTaskIds = new Set(activeChallenges.map((challenge) => String(challenge.task_id))); for (const item of Array.from(list.querySelectorAll(".login-2fa-item"))) { if (!activeTaskIds.has(item.dataset.taskId || "")) { item.remove(); } } for (const challenge of activeChallenges) { const taskId = String(challenge.task_id); let item = Array.from(list.querySelectorAll(".login-2fa-item")) .find((candidate) => candidate.dataset.taskId === taskId); if (!item) { item = createAdminLogin2faItem(taskId); list.appendChild(item); } const title = item.querySelector("h3"); const desc = item.querySelector("p"); const meta = item.querySelector(".login-2fa-meta"); if (title) { title.textContent = `${challenge.display_name || challenge.student_id || "用户"} 的登录认证`; } if (desc) { desc.textContent = `验证码已发送至 ${challenge.phone_mask || "绑定手机"},剩余 ${secondsText(challenge.remaining_seconds)} 秒。`; } if (meta) { meta.innerHTML = ""; for (const text of [`任务 #${challenge.task_id}`, `学号 ${challenge.student_id || "--"}`]) { const chip = document.createElement("span"); chip.className = "chip"; chip.textContent = text; meta.appendChild(chip); } } } } function updateUserStatus(data) { const taskText = document.getElementById("task-status-text"); const taskPill = document.getElementById("task-status-pill"); const courseCount = document.getElementById("course-count"); const taskAttempts = document.getElementById("task-attempts"); const taskAttemptsInline = document.getElementById("task-attempts-inline"); const taskErrors = document.getElementById("task-errors"); const taskErrorsInline = document.getElementById("task-errors-inline"); const taskLastError = document.getElementById("task-last-error"); const taskUpdatedAt = document.getElementById("task-updated-at"); const refreshInterval = document.getElementById("refresh-interval"); const requestedMode = document.getElementById("task-requested-mode"); const requestedModeInline = document.getElementById("task-requested-mode-inline"); const effectiveMode = document.getElementById("task-effective-mode"); const effectiveModeInline = document.getElementById("task-effective-mode-inline"); const loginModeInline = document.getElementById("task-login-mode-inline"); const proxyInline = document.getElementById("task-proxy-inline"); const task = data.task || null; const status = (task && task.status) || "idle"; if (taskText) { taskText.textContent = statusLabels[status] || status; } if (taskPill) { taskPill.className = `status-pill status-${status}`; taskPill.textContent = statusLabels[status] || status; } if (courseCount && Array.isArray(data.courses)) { courseCount.textContent = String(data.courses.length); } if (taskAttempts) { taskAttempts.textContent = String((task && task.total_attempts) || 0); } if (taskAttemptsInline) { taskAttemptsInline.textContent = String((task && task.total_attempts) || 0); } if (taskErrors) { taskErrors.textContent = String((task && task.total_errors) || 0); } if (taskErrorsInline) { taskErrorsInline.textContent = String((task && task.total_errors) || 0); } if (taskLastError) { taskLastError.textContent = (task && task.last_error) || "当前没有错误提示"; } if (taskUpdatedAt) { taskUpdatedAt.textContent = `最近更新时间:${(task && task.updated_at) || "暂无"}`; } if (requestedMode) { const proxyText = task && Number(task.use_proxy || 0) ? "启用" : "关闭"; requestedMode.textContent = `请求模式:${modeLabel(task && task.requested_mode)};登录方式:${loginModeLabel(task && task.login_mode)};代理:${proxyText}`; } if (requestedModeInline) { requestedModeInline.textContent = modeLabel(task && task.requested_mode); } if (effectiveMode) { effectiveMode.textContent = modeLabel(task && task.effective_mode); } if (effectiveModeInline) { effectiveModeInline.textContent = modeLabel(task && task.effective_mode); } if (loginModeInline) { loginModeInline.textContent = loginModeLabel(task && task.login_mode); } if (proxyInline) { proxyInline.textContent = task && Number(task.use_proxy || 0) ? "启用" : "关闭"; } if (refreshInterval && data.user && data.user.refresh_interval_seconds) { refreshInterval.textContent = `${data.user.refresh_interval_seconds} 秒`; } updateUserLogin2fa(data.login_2fa_challenge || null); } function updateAdminStatus(data) { const users = document.getElementById("stat-users"); const running = document.getElementById("stat-running"); const pending = document.getElementById("stat-pending"); const parallel = document.getElementById("parallel-limit-input"); if (users) { users.textContent = String(data.stats.users_count || 0); } if (running) { running.textContent = String(data.stats.running_count || 0); } if (pending) { pending.textContent = String(data.stats.pending_count || 0); } if (parallel) { parallel.value = String(data.parallel_limit || parallel.value); } updateAdminLogin2fa(data.login_2fa_challenges || []); } function bindStatusPolling() { const shell = document.querySelector("[data-status-url]"); if (!shell) { return; } const statusUrl = shell.getAttribute("data-status-url"); const isAdmin = shell.classList.contains("admin-dashboard"); async function refresh() { try { const response = await fetch(statusUrl, { headers: { "X-Requested-With": "fetch" } }); if (!response.ok) { return; } const payload = await response.json(); if (!payload.ok) { return; } if (isAdmin) { updateAdminStatus(payload); } else { updateUserStatus(payload); } } catch (_error) { // Leave the current UI state as-is if polling fails. } } refresh(); window.setInterval(refresh, 7000); } function bindLogin2faInteractions() { document.addEventListener("input", function (event) { const target = event.target; if (!(target instanceof HTMLInputElement) || target.name !== "code") { return; } const normalized = normalizeLogin2faCode(target.value); if (target.value !== normalized) { target.value = normalized; } }); const userForm = document.getElementById("user-login-2fa-form"); if (userForm) { userForm.addEventListener("submit", async function (event) { event.preventDefault(); const panel = document.getElementById("user-login-2fa-panel"); const message = document.getElementById("user-login-2fa-message"); const input = userForm.querySelector("input[name='code']"); const button = userForm.querySelector("button[type='submit']"); const code = normalizeLogin2faCode(input && input.value); const taskId = panel && panel.dataset.taskId; if (!taskId || code.length !== 6) { setFormMessage(message, "请输入 6 位短信验证码。", "warning"); return; } if (button) { button.disabled = true; } let submitted = false; try { await submitLogin2faCode(taskId, code, "user"); submitted = true; setFormMessage(message, "已提交,等待后台继续登录。", "success"); if (input) { input.value = ""; input.disabled = true; } } catch (error) { setFormMessage(message, error.message || "提交失败,请稍后重试。", "error"); } finally { if (button && !submitted) { button.disabled = false; } } }); } const adminList = document.getElementById("admin-login-2fa-list"); if (adminList) { adminList.addEventListener("submit", async function (event) { const form = event.target.closest("form[data-login2fa-task-id]"); if (!form) { return; } event.preventDefault(); const input = form.querySelector("input[name='code']"); const button = form.querySelector("button[type='submit']"); const message = form.querySelector(".form-message"); const code = normalizeLogin2faCode(input && input.value); const taskId = form.dataset.login2faTaskId; if (!taskId || code.length !== 6) { setFormMessage(message, "请输入 6 位短信验证码。", "warning"); return; } if (button) { button.disabled = true; } let submitted = false; try { await submitLogin2faCode(taskId, code, "admin"); submitted = true; setFormMessage(message, "已提交,等待后台继续登录。", "success"); if (input) { input.value = ""; input.disabled = true; } } catch (error) { setFormMessage(message, error.message || "提交失败,请稍后重试。", "error"); } finally { if (button && !submitted) { button.disabled = false; } } }); } } bindLogStream(); bindStatusPolling(); bindLogin2faInteractions(); })();