| (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) { |
| |
| } |
| }; |
| } |
|
|
| 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) { |
| |
| } |
| } |
|
|
| 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(); |
| })(); |
|
|