// ==UserScript== // @name 【效率版】武汉理工大学网络教育学习助手 // @namespace https://card.wlxy.live/details/A9C371DB // @version 1.1 // @description 武汉理工大学网络教育(wljy.whut.edu.cn)学习辅助:一键快读课程时长完成、作业满分完成,考试答题辅助待后期添加;免费体验1门课学时,Pro不限课程与作业;Pro激活后30天有效。 // @antifeature payment 免费体验1门课学时,升级Pro不限课程与作业 // @antifeature membership 需云端授权 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @match https://wljy.whut.edu.cn/* // @noframes // @connect wljy.whut.edu.cn // @connect oa19.ahzsksw.cn // @connect card.wlxy.live // @connect huaweicloudobs.ahjxjy.cn // @grant GM_info // @grant GM_xmlhttpRequest // @run-at document-idle // @tag 武汉理工大学 // @tag 网络教育 // @tag 学习辅助 // @tag 课程时长 // @tag 作业完成 // @license All Rights Reserved // ==/UserScript== (function () { "use strict"; try { if (window.top !== window.self) return; } catch (_) { return; } const BOOT_KEY = "__WHUT_WLJY_HELPER_BOOTED__"; if (window[BOOT_KEY]) return; window[BOOT_KEY] = true; const SCRIPT_VERSION = (() => { try { if (typeof GM_info !== "undefined" && GM_info?.script?.version) { return String(GM_info.script.version); } } catch (_) {} return "1.6.16"; })(); const API_BASE = location.origin + "/prod-api"; const PANEL_POS_KEY = "whut_wljy_panel_pos_v1"; const PANEL_COLLAPSED_KEY = "whut_wljy_panel_collapsed_v1"; const QUEUE_KEY = "whut_wljy_course_queue_v1"; const CHUNK_KEY = "whut_wljy_eff_chunk_sec_v2"; const GAP_KEY = "whut_wljy_eff_gap_ms_v1"; const HW_MIN_SCORE_KEY = "whut_wljy_hw_min_score_v2"; const CLOUD_TOKEN_KEY = "whut_wljy_cloud_token_v1"; const CLOUD_LEASE_CACHE_KEY = "whut_wljy_cloud_lease_cache_v1"; const PANEL_LOGO_URL = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const DEFAULT_CLOUD_API_BASE = "https://oa19.ahzsksw.cn"; const PRO_BUY_URL = "https://card.wlxy.live/details/A9C371DB"; const PANEL_NOTICE = "\u514d\u8d39\u53ef\u4f53\u9a8c 1 \u95e8\u8bfe\u5b66\u65f6\uff1bPro \u53ef\u5904\u7406\u5168\u90e8\u8bfe\u7a0b\u4e0e\u4f5c\u4e1a\u3002\u5b66\u5b8c\u5b66\u65f6\u540e\u81ea\u52a8\u505a\u4f5c\u4e1a\u3002"; const OFFICIAL_TICK_SEC = 5; const DEFAULT_CHUNK_SEC = 3600; const MAX_CHUNK_SEC = 3600; const COURSE_EXTRA_MIN = 2; const COURSE_EXTRA_SEC = COURSE_EXTRA_MIN * 60; const DEFAULT_GAP_MS = 1000; const COURSE_GAP_MS = 3 * 1000; const REFRESH_EVERY_N = 8; const HW_ANSWER_GAP_MS = 200; const DEFAULT_HW_MIN_SCORE = 100; const state = { courses: [], selected: new Set(loadQueue()), panelRefreshing: false, logLines: [], userName: "", userId: "", enabled: false, runBusy: false, runMode: "", // study | homework | pipeline currentCourse: "", currentTask: "", queueTotal: 0, queueDone: 0, stopFlag: false, cloudApiBase: DEFAULT_CLOUD_API_BASE, cloudToken: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(), cloudTier: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim() ? "unknown" : "free", cloudLease: "", cloudLeaseExp: 0, cloudProExpireAt: 0, freeCourseLimit: 1, freeUsedCourses: 0, remotePanelNotice: "", proBuyUrl: PRO_BUY_URL, }; function getHwMinScore() { const v = parseInt(localStorage.getItem(HW_MIN_SCORE_KEY) || String(DEFAULT_HW_MIN_SCORE), 10); if (!Number.isFinite(v)) return DEFAULT_HW_MIN_SCORE; return Math.min(100, Math.max(0, v)); } function loadQueue() { try { const raw = JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]"); return Array.isArray(raw) ? raw.map(String) : []; } catch (_) { return []; } } function saveQueue() { localStorage.setItem(QUEUE_KEY, JSON.stringify([...state.selected])); } function getChunkSec() { const v = parseInt(localStorage.getItem(CHUNK_KEY) || String(DEFAULT_CHUNK_SEC), 10); if (!Number.isFinite(v)) return DEFAULT_CHUNK_SEC; return Math.min(MAX_CHUNK_SEC, Math.max(OFFICIAL_TICK_SEC, v)); } function getGapMs() { const v = parseInt(localStorage.getItem(GAP_KEY) || String(DEFAULT_GAP_MS), 10); if (!Number.isFinite(v)) return DEFAULT_GAP_MS; return Math.min(10000, Math.max(300, v)); } function escHtml(s) { return String(s || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function _gpb() { return String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL; } function _opb() { try { window.open(_gpb(), "_blank", "noopener,noreferrer"); } catch (_) { location.href = _gpb(); } } function _swd(opts) { const o = opts || {}; const title = o.title || "\u63d0\u793a"; const message = o.message || ""; const okText = o.okText || "\u786e\u5b9a"; const cancelText = o.cancelText == null ? "" : o.cancelText; const showBuy = !!o.showBuy; return new Promise((resolve) => { const old = document.getElementById("whut-dialog-root"); if (old) old.remove(); const root = document.createElement("div"); root.id = "whut-dialog-root"; root.innerHTML = `
`; const close = (v) => { root.remove(); resolve(v); }; root.addEventListener("click", (e) => { const act = e.target?.closest?.("[data-act]")?.getAttribute("data-act"); if (act === "mask" || act === "cancel") close(false); else if (act === "ok") close(true); else if (act === "buy") { _opb(); close("buy"); } }); document.documentElement.appendChild(root); root.querySelector('[data-act="ok"]')?.focus(); }); } function _swa(message, title) { return _swd({ title: title || "\u63d0\u793a", message, okText: "\u77e5\u9053\u4e86" }); } function _swc(message, title) { return _swd({ title: title || "\u8bf7\u786e\u8ba4", message, okText: "\u786e\u5b9a", cancelText: "\u53d6\u6d88", }); } function _sph(message) { return _swd({ title: "\u5f00\u901a\u4e13\u4e1a\u7248", message: message || "\u5f00\u901a\u4e13\u4e1a\u7248\u540e\u53ef\u5904\u7406\u5168\u90e8\u8bfe\u7a0b\u4e0e\u4f5c\u4e1a\u3002", okText: "\u7a0d\u540e\u518d\u8bf4", showBuy: true, }); } function log(msg) { const line = `[${new Date().toLocaleTimeString()}] ${msg}`; state.logLines.unshift(line); if (state.logLines.length > 220) state.logLines.length = 220; const box = document.getElementById("whut-run-log"); if (box) { box.innerHTML = state.logLines .map((t) => `
${escHtml(t)}
`) .join(""); } } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function getToken() { try { const m = document.cookie.match(/(?:^|;\s*)Stu-Token=([^;]+)/); if (m && m[1]) return decodeURIComponent(m[1]).trim(); } catch (_) {} const keys = ["Stu-Token", "stu-token", "Admin-Token", "token", "access_token"]; for (const k of keys) { try { const v = localStorage.getItem(k); if (v && String(v).trim()) return String(v).trim().replace(/^Bearer\s+/i, ""); } catch (_) {} try { const v = sessionStorage.getItem(k); if (v && String(v).trim()) return String(v).trim().replace(/^Bearer\s+/i, ""); } catch (_) {} } return ""; } function decodeJwtPayload(token) { try { const part = String(token || "").split(".")[1]; if (!part) return null; const b64 = part.replace(/-/g, "+").replace(/_/g, "/"); const json = decodeURIComponent( atob(b64) .split("") .map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)) .join("") ); return JSON.parse(json); } catch (_) { return null; } } function getUserId() { if (state.userId) return state.userId; const payload = decodeJwtPayload(getToken()); const id = payload?.userId || payload?.usrId || payload?.userid || ""; if (id) state.userId = String(id); return state.userId; } function _af(path, options = {}) { const token = getToken(); const headers = Object.assign( { Accept: "application/json, text/plain, */*" }, options.headers || {} ); if (token) headers.Authorization = "Bearer " + token; if (options.body && !headers["Content-Type"]) { headers["Content-Type"] = "application/json;charset=UTF-8"; } return fetch(API_BASE + path, { method: options.method || "GET", credentials: "include", headers, body: options.body, }).then(async (res) => { const text = await res.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch (_) { throw new Error("\u63a5\u53e3\u8fd4\u56de\u975e JSON\uff1a" + path); } if (!res.ok) throw new Error(`HTTP ${res.status} ${path}`); if (data && typeof data.code !== "undefined" && Number(data.code) !== 200) { throw new Error(data.msg || `\u63a5\u53e3\u9519\u8bef code=${data.code}`); } return data; }); } function _gca() { return DEFAULT_CLOUD_API_BASE; } function _rcl() { try { const parsed = JSON.parse(localStorage.getItem(CLOUD_LEASE_CACHE_KEY) || "null"); const lease = String(parsed?.lease || "").trim(); const exp = Number(parsed?.exp || 0); if (!lease || !Number.isFinite(exp) || exp * 1000 <= Date.now() + 15000) return null; return parsed; } catch (_) { return null; } } function _wcl(lease, exp, extra) { if (!lease || !exp) { localStorage.removeItem(CLOUD_LEASE_CACHE_KEY); return; } localStorage.setItem( CLOUD_LEASE_CACHE_KEY, JSON.stringify(Object.assign({ lease, exp }, extra || {})) ); } function _ch(path, method, payload) { const base = _gca(); const url = `${base}${path.startsWith("/") ? path : `/${path}`}`; const headers = { Accept: "application/json", "Content-Type": "application/json" }; const luid = getUserId(); if (luid) headers["x-learning-user-id"] = luid; if (state.cloudToken) headers.Authorization = `Bearer ${state.cloudToken}`; const body = payload == null ? null : JSON.stringify(payload); return new Promise((resolve, reject) => { const req = { method: method || "GET", url, headers, timeout: 60000, withCredentials: false, responseType: "text", onload: (res) => resolve({ status: res.status, text: res.responseText || "" }), onerror: () => reject(new Error("\u7f51\u7edc\u5f02\u5e38")), ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6")), }; if (body != null) req.data = body; if (typeof GM_xmlhttpRequest === "function") { GM_xmlhttpRequest(req); return; } fetch(url, { method: req.method, headers, body }) .then(async (r) => resolve({ status: r.status, text: await r.text() })) .catch(() => reject(new Error("\u7f51\u7edc\u5f02\u5e38"))); }); } async function _crj(path, method, payload) { const res = await _ch(path, method, payload); const raw = String(res.text || "").trim(); let data = null; if (raw) { try { data = JSON.parse(raw); } catch (_) { throw new Error(`\u670d\u52a1\u5f02\u5e38(HTTP ${res.status})`); } } if (res.status >= 400) { const rawMsg = data?.message || data?.detail || `HTTP ${res.status}`; const err = new Error(_fcm(rawMsg)); err.status = res.status; err.data = data; throw err; } return data || {}; } function _fcm(msg) { const s = String(msg || "").trim(); const map = { "invalid token": "\u6388\u6743\u7801\u65e0\u6548", invalid_token: "\u6388\u6743\u7801\u65e0\u6548", revoked: "\u6388\u6743\u7801\u5df2\u505c\u7528", expired: "\u6388\u6743\u7801\u5df2\u8fc7\u671f", expired_after_activation: "\u6388\u6743\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u5f00\u901a", token_bound_to_other_learning_user: "\u6388\u6743\u7801\u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7", "missing Authorization": "\u672a\u63d0\u4f9b\u6388\u6743\u4fe1\u606f", "invalid Authorization scheme": "\u6388\u6743\u683c\u5f0f\u4e0d\u6b63\u786e", missing_learning_user_id: "\u7f3a\u5c11\u8d26\u53f7\u4fe1\u606f\uff0c\u8bf7\u5148\u767b\u5f55", "missing learning_user_id": "\u7f3a\u5c11\u8d26\u53f7\u4fe1\u606f\uff0c\u8bf7\u5148\u767b\u5f55", free_course_quota_exceeded: "\u514d\u8d39\u4f53\u9a8c\u6b21\u6570\u5df2\u7528\u5b8c\uff0c\u8bf7\u5f00\u901a\u4e13\u4e1a\u7248", pro_only: "\u5f53\u524d\u8d26\u53f7\u65e0\u4f5c\u4e1a\u6743\u9650\uff0c\u8bf7\u5f00\u901a\u4e13\u4e1a\u7248", invalid_lease: "\u4f1a\u8bdd\u5df2\u5931\u6548\uff0c\u8bf7\u91cd\u8bd5", rate_limited: "\u8bf7\u6c42\u8fc7\u4e8e\u9891\u7e41\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5", qbank_lookup_error: "\u67e5\u8be2\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5", qbank_upsert_error: "\u540c\u6b65\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5", }; if (map[s]) return map[s]; const lower = s.toLowerCase(); if (map[lower]) return map[lower]; if (/invalid\s*token/i.test(s)) return "\u6388\u6743\u7801\u65e0\u6548"; if (/expired/i.test(s)) return "\u6388\u6743\u7801\u5df2\u8fc7\u671f"; if (/revoked/i.test(s)) return "\u6388\u6743\u7801\u5df2\u505c\u7528"; return s || "\u6388\u6743\u5931\u8d25"; } async function _ecl(force) { const cached = !force ? _rcl() : null; if (cached?.lease) { state.cloudLease = cached.lease; state.cloudLeaseExp = Number(cached.exp || 0); if (cached.tier) state.cloudTier = String(cached.tier); if (cached.free_course_limit != null) state.freeCourseLimit = Number(cached.free_course_limit) || 1; if (cached.free_used_courses != null) state.freeUsedCourses = Number(cached.free_used_courses) || 0; if (cached.pro_expires_at) state.cloudProExpireAt = Number(cached.pro_expires_at) || 0; return true; } const luid = getUserId(); if (!luid) throw new Error("\u65e0\u6cd5\u89e3\u6790\u8d26\u53f7\uff0c\u8bf7\u5148\u767b\u5f55"); const data = await _crj("/api/whut/lease", "POST", { learning_user_id: String(luid), name: state.userName || "", userName: state.userName || "", }); const lease = String(data.lease || ""); const exp = Number(data.exp || 0); if (!lease) throw new Error("\u6388\u6743\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"); state.cloudLease = lease; state.cloudLeaseExp = exp; state.cloudTier = String(data.tier || "free"); state.freeCourseLimit = Number(data.free_course_limit != null ? data.free_course_limit : 1) || 1; state.freeUsedCourses = Number(data.free_used_courses != null ? data.free_used_courses : 0) || 0; state.cloudProExpireAt = Number(data.pro_expires_at || 0) || 0; if (data.proBuyUrl) { const u = String(data.proBuyUrl).trim(); if (u && !/^https?:\/\/card\.wlxy\.live\/?$/i.test(u)) state.proBuyUrl = u; } _wcl(lease, exp, { tier: state.cloudTier, free_course_limit: state.freeCourseLimit, free_used_courses: state.freeUsedCourses, pro_expires_at: state.cloudProExpireAt, }); _rcm(); return true; } async function _cfc(course) { await _ecl(false); if (state.cloudTier === "pro") return { ok: true, unlimited: true }; const data = await _crj("/api/whut/course/consume", "POST", { lease: state.cloudLease, cour_no: String(course.courNo || ""), cour_name: String(course.courName || ""), }); if (data.free_used_courses != null) state.freeUsedCourses = Number(data.free_used_courses) || 0; _wcl(state.cloudLease, state.cloudLeaseExp, { tier: state.cloudTier, free_course_limit: state.freeCourseLimit, free_used_courses: state.freeUsedCourses, pro_expires_at: state.cloudProExpireAt, }); _rcm(); return data; } async function _cql(stems) { if (!stems.length || state.cloudTier !== "pro") return []; await _ecl(false); const data = await _crj("/api/whut/qbank/lookup", "POST", { lease: state.cloudLease, stems, }); return Array.isArray(data.items) ? data.items : []; } async function _cqu(items) { if (!items.length || state.cloudTier !== "pro") return 0; await _ecl(false); const CHUNK = 40; let saved = 0; for (let i = 0; i < items.length; i += CHUNK) { const chunk = items.slice(i, i + CHUNK); const payloadItems = chunk.map((it) => ({ stem: it.stem, answer_text: it.answer_text || (it.answer_letter ? `__LETTER__${it.answer_letter}` : ""), answer_letter: it.answer_letter || "", paper_name: it.paper_name || "", cour_no: it.cour_no || "", source: it.source || "whut_hw", })); try { const data = await _crj("/api/whut/qbank/upsert", "POST", { lease: state.cloudLease, items: payloadItems, }); saved += Number(data.saved || 0); } catch (e) { if (String(e.message || "").includes("invalid_lease") || e.status === 401) { await _ecl(true); const data = await _crj("/api/whut/qbank/upsert", "POST", { lease: state.cloudLease, items: payloadItems, }); saved += Number(data.saved || 0); } else { throw e; } } } return saved; } async function _rpn() { try { const res = await _ch("/api/whut/panel-notice", "GET", null); if (res.status === 200 && res.text) { state.remotePanelNotice = String(res.text).trim(); const el = document.querySelector("#whut-wljy-panel .whut-ann"); if (el && state.remotePanelNotice) el.textContent = state.remotePanelNotice; } } catch (_) {} } function _rcm() { const set = (id, text) => { const el = document.getElementById(id); if (el) el.textContent = text; }; const tierLabel = state.cloudTier === "pro" ? state.cloudProExpireAt ? `\u4e13\u4e1a\u7248\uff08\u81f3 ${new Date(state.cloudProExpireAt * 1000).toLocaleDateString()}\uff09` : "\u4e13\u4e1a\u7248" : state.cloudTier === "unknown" ? "\u6821\u9a8c\u4e2d…" : `\u514d\u8d39\u7248\uff08\u5df2\u7528 ${state.freeUsedCourses}/${state.freeCourseLimit} \u95e8\uff09`; set("whut-cloud-tier", tierLabel); const buy = document.getElementById("whut-pro-buy"); if (buy) buy.href = _gpb(); const banner = document.getElementById("whut-pro-banner"); if (banner) banner.style.display = _ipu() ? "none" : ""; } function _ipu() { return state.cloudTier === "pro"; } function parseCourseTimeMin(v) { if (v == null || v === "" || v === "-") return 0; const n = Number(v); return Number.isFinite(n) && n > 0 ? n : 0; } function parseMinutesField(v) { if (v == null || v === "" || v === "-") return null; if (typeof v === "string" && /\u5206\u949f/.test(v)) { const m = v.replace(/,/g, "").match(/([\d.]+)/); return m ? Number(m[1]) : null; } const n = Number(v); if (!Number.isFinite(n) || n < 0) return null; return n; } function parseLearnPercent(v) { if (v == null || v === "" || v === "-") return null; const n = Number(String(v).replace("%", "")); if (!Number.isFinite(n) || n < 0) return null; return Math.min(100, n); } function formatScore(v) { if (v == null || v === "" || v === "-") return "—"; const n = Number(v); if (!Number.isFinite(n)) return String(v); if (Number.isInteger(n)) return String(n); return String(Math.round(n * 100) / 100); } function formatDurationLabel(min) { if (min == null || min === "" || !Number.isFinite(Number(min))) return "—"; const n = Number(min); const t = Math.round(n * 100) / 100; return `${t} \u5206\u949f`; } function parseScheduleDoneMin(text) { const s = String(text || ""); const m = s.replace(/,/g, "").match(/([\d.]+)/); if (!m) return null; const n = Number(m[1]); return Number.isFinite(n) ? n : null; } function _nc(raw) { const courNo = String(raw.courNo ?? raw.courseNo ?? "").trim(); const learnedMin = parseMinutesField(raw.state); const courseTimeMin = parseCourseTimeMin(raw.courseTime); const learnPercent = parseLearnPercent(raw.learnPercent); return { courNo, courName: String(raw.courName || raw.courseName || courNo || "\u672a\u547d\u540d\u8bfe\u7a0b"), courseTimeMin, courseTimeRaw: raw.courseTime, requireMin: courseTimeMin, doneMin: learnedMin != null ? learnedMin : 0, doneLabel: "", score: raw.score, state: raw.state, status: raw.status, learnPercent, learnedMin: learnedMin != null ? learnedMin : 0, teacher: raw.teacher || "", checkMod: raw.checkMod || "", studyForm: raw.studyForm || "", majorName: raw.majorName || "", courseCateporyName: raw.courseCateporyName || "", homeworkOne: null, homeworkTwo: null, homeworkResult: null, coursewareResult: null, snsResult: null, testResult: null, maxResult: null, noHomework: null, homeworkTaskCount: null, }; } function _msi(courses, scheduleData) { const data = scheduleData?.data || scheduleData || {}; const reqMap = new Map(); const doneMap = new Map(); const reqList = data?.learningProcess?.courseName || []; if (Array.isArray(reqList)) { for (const it of reqList) { const no = String(it.courNo || "").trim(); if (!no) continue; const req = parseCourseTimeMin(it.courseTime); if (req > 0) reqMap.set(no, req); const doneRaw = parseMinutesField(it.stuCourseLearnTime); if (doneRaw != null && doneRaw > 1) doneMap.set(no, doneRaw); } } const doneList = data?.learningcourses || []; if (Array.isArray(doneList)) { for (const it of doneList) { const no = String(it.value5 || it.courNo || "").trim(); if (!no) continue; const done = parseScheduleDoneMin(it.value3); if (done != null) { doneMap.set(no, done); const c = courses.find((x) => x.courNo === no); if (c) c.doneLabel = String(it.value3 || "").trim(); } } } for (const c of courses) { if (reqMap.has(c.courNo)) { c.requireMin = reqMap.get(c.courNo); if (!(c.courseTimeMin > 0)) c.courseTimeMin = c.requireMin; } else { c.requireMin = c.courseTimeMin || 0; } if (doneMap.has(c.courNo)) { c.doneMin = doneMap.get(c.courNo); } else { c.doneMin = Number(c.learnedMin) || 0; } if (!c.doneLabel) c.doneLabel = formatDurationLabel(c.doneMin); } } function _mei(courses, examData) { const list = examData?.data?.examResult || examData?.examResult || []; if (!Array.isArray(list)) return; const map = new Map(); for (const it of list) { const no = String(it.courNo || "").trim(); if (no) map.set(no, it); } for (const c of courses) { const ex = map.get(c.courNo); if (!ex) continue; c.courseCateporyName = ex.courseCateporyName || c.courseCateporyName; c.homeworkOne = ex.homeworkOne; c.homeworkTwo = ex.homeworkTwo; c.homeworkResult = ex.homeworkResult; c.coursewareResult = ex.coursewareResult; c.snsResult = ex.snsResult; c.testResult = ex.testResult; c.maxResult = ex.maxResult; } } function targetSec(course) { if (!(course.courseTimeMin > 0)) return 0; return Math.round(course.courseTimeMin * 60) + COURSE_EXTRA_SEC; } function targetMin(course) { return targetSec(course) / 60; } function progressPct(course) { const need = targetSec(course); if (need > 0) { const learnedSec = Math.round((Number(course.learnedMin) || 0) * 60); return Math.min(100, Math.round((learnedSec / need) * 100)); } if (course.learnPercent != null) return Math.round(course.learnPercent); return 0; } function platformPct(course) { if (course.learnPercent != null) return Math.round(course.learnPercent); if (course.courseTimeMin > 0) { return Math.min(100, Math.round(((Number(course.learnedMin) || 0) / course.courseTimeMin) * 100)); } return 0; } function remainingSec(course) { const need = targetSec(course); if (!need) return 0; const learned = Math.round((Number(course.learnedMin) || 0) * 60); return Math.max(0, need - learned); } function _icd(course) { if (!targetSec(course)) return true; if (course.learnPercent != null && course.learnPercent >= 100) return true; if ((Number(course.learnedMin) || 0) + 1e-6 >= targetMin(course)) return true; if (remainingSec(course) <= OFFICIAL_TICK_SEC) return true; return false; } function _ipc(course) { const cate = String(course?.courseCateporyName || ""); if (/\u5b9e\u8df5/.test(cate)) return true; return course?.noHomework === true; } function _hns(course) { const req = Number(course?.requireMin || course?.courseTimeMin || 0); return !(req > 0) && !(targetSec(course) > 0); } function _icfd(course) { if (_ipc(course) && _hns(course)) return true; if (course.noHomework === true && _hns(course)) return true; const studyOk = !(targetSec(course) > 0) || _icd(course); if (!studyOk) return false; if (course.noHomework === true) return true; if (course.noHomework == null) { return false; } const hw = Number(course.homeworkResult); if (!Number.isFinite(hw)) return false; return hw + 1e-6 >= getHwMinScore(); } function scoreEmpty(v) { return v == null || v === "" || v === "-" || v === "—"; } function inferNoHomeworkFromExam(course) { return ( scoreEmpty(course.homeworkOne) && scoreEmpty(course.homeworkTwo) && (scoreEmpty(course.homeworkResult) || Number(course.homeworkResult) === 0) ); } function _pds() { let changed = false; for (const c of state.courses) { if (_icfd(c) && state.selected.has(c.courNo)) { state.selected.delete(c.courNo); changed = true; } } if (changed) saveQueue(); } async function _fui() { try { const data = await _af("/student/user/getInfo"); const u = data?.user || data?.data || {}; state.userName = String(u.nickName || u.userName || u.stuName || u.name || "").trim(); const uid = u.userId || u.stuId || u.userNo || ""; if (uid) state.userId = String(uid); if (!state.userId) getUserId(); } catch (_) { state.userName = ""; getUserId(); } } function _acs() { const valid = new Set(state.courses.map((c) => c.courNo)); state.selected = new Set([...state.selected].filter((id) => valid.has(id))); if (!state.selected.size && state.courses.length) { state.courses.forEach((c) => state.selected.add(c.courNo)); } saveQueue(); } async function _fcl() { const data = await _af("/student/eduStudent/studentCourse"); const list = data?.data?.mycourse || data?.data?.myCourse || data?.data || []; if (!Array.isArray(list)) throw new Error("\u8bfe\u7a0b\u5217\u8868\u683c\u5f0f\u5f02\u5e38"); state.courses = list.map(_nc).filter((c) => c.courNo); _acs(); return state.courses; } async function _ecd() { const stamp = (state._enrichStamp = (state._enrichStamp || 0) + 1); const [scheduleData, examData] = await Promise.all([ _af("/student/studySchedule/findMySchedule").catch(() => null), _af("/student/MyExamResults/listEduStuExamResults").catch(() => null), ]); if (stamp !== state._enrichStamp) return false; if (scheduleData) _msi(state.courses, scheduleData); if (examData) _mei(state.courses, examData); _ra(); await _ehf(state.courses, stamp); if (stamp !== state._enrichStamp) return false; _ra(); return true; } async function _ehf(courses, stamp) { const list = Array.isArray(courses) ? courses : []; let idx = 0; const workers = Math.min(4, Math.max(1, list.length)); async function worker() { while (idx < list.length) { if (stamp != null && stamp !== state._enrichStamp) return; const c = list[idx++]; if (!c) continue; try { const tasks = await fetchCourseTasks(c.courNo); const hwTasks = (tasks || []).filter((t) => t && (t.paperNo || t.ctpName || t.ctpNo)); c.homeworkTaskCount = hwTasks.length; c.noHomework = hwTasks.length === 0; } catch (_) { c.noHomework = inferNoHomeworkFromExam(c); c.homeworkTaskCount = c.noHomework ? 0 : null; } } } await Promise.all(Array.from({ length: workers }, () => worker())); } function _sc() { return state.courses.filter((c) => state.selected.has(c.courNo)); } async function _plt(courNo, studyTimeSec) { const body = { courNo: Number(courNo) || courNo, studyTime: Math.max(1, Math.round(Number(studyTimeSec) || OFFICIAL_TICK_SEC)), }; return _af("/student/eduStudent/updateLearnTime", { method: "POST", body: JSON.stringify(body), }); } async function _scp(courNo) { const c = state.courses.find((x) => x.courNo === String(courNo)); if (!c) return; try { const data = await _af("/student/eduStudent/studentCourse"); const list = data?.data?.mycourse || []; const raw = list.find((x) => String(x.courNo) === String(courNo)); if (!raw) return; const next = _nc(raw); c.state = next.state; c.learnedMin = next.learnedMin; c.learnPercent = next.learnPercent; c.courseTimeMin = next.courseTimeMin || c.courseTimeMin; c.courseTimeRaw = next.courseTimeRaw; if (c.courseTimeMin > 0) c.requireMin = c.courseTimeMin; c.doneMin = Number(c.learnedMin) || 0; c.doneLabel = formatDurationLabel(c.doneMin); } catch (_) {} } async function _reo(course) { const chunk = getChunkSec(); const gap = getGapMs(); state.currentCourse = course.courName; await _scp(course.courNo); if (!targetSec(course)) { log(`\u8df3\u8fc7\u300c${course.courName}\u300d\uff1a\u65e0\u9700\u5b66\u65f6`); state.currentTask = "\u65e0\u9700\u5b66\u65f6\uff0c\u8df3\u8fc7"; _rrs(); return { skipped: true }; } if (_icd(course)) { log(`\u8df3\u8fc7\u300c${course.courName}\u300d\uff1a\u5b66\u65f6\u5df2\u5b8c\u6210`); state.currentTask = "\u5df2\u5b8c\u6210"; _rrs(); return { skipped: true }; } let rem = remainingSec(course); log(`\u5f00\u59cb\u5b66\u4e60\u300c${course.courName}\u300d`); let ticks = 0; let submitted = 0; while (!state.stopFlag && rem > 0) { const send = Math.min(chunk, rem, MAX_CHUNK_SEC); state.currentTask = `\u5b66\u4e60\u4e2d · ${progressPct(course)}%`; _rrs(); try { await _plt(course.courNo, send); submitted += send; course.learnedMin = (course.learnedMin || 0) + send / 60; rem = Math.max(0, rem - send); ticks += 1; if (ticks % REFRESH_EVERY_N === 0) { await _scp(course.courNo); rem = remainingSec(course); log(`\u300c${course.courName}\u300d\u5b66\u65f6\u8fdb\u5ea6\u5df2\u66f4\u65b0`); } } catch (e) { log(`\u63d0\u4ea4\u5931\u8d25\uff1a${e.message || e}\uff0c\u7a0d\u540e\u91cd\u8bd5`); await sleep(3000); continue; } if (rem <= 0) break; await sleep(gap); } await _scp(course.courNo); const done = _icd(course) || remainingSec(course) <= OFFICIAL_TICK_SEC; log(done ? `\u300c${course.courName}\u300d\u5b66\u65f6\u5df2\u5b8c\u6210` : `\u5df2\u505c\u6b62\u300c${course.courName}\u300d`); state.currentTask = done ? "\u672c\u8bfe\u5b8c\u6210" : "\u5df2\u505c\u6b62"; _rrs(); return { skipped: false, done }; } async function _sef() { if (state.runBusy) return; if (!getToken()) { log("\u8bf7\u5148\u767b\u5f55\u5b66\u751f\u7aef"); return; } getUserId(); try { await _ecl(true); } catch (e) { log(`\u6388\u6743\u5931\u8d25\uff1a${e.message || e}`); return; } const queue = _sc().filter((c) => targetSec(c) > 0); if (!queue.length) { log("\u8bf7\u52fe\u9009\u6709\u5b66\u65f6\u76ee\u6807\u7684\u8bfe\u7a0b"); return; } if (!_ipu() && queue.length > state.freeCourseLimit) { const msg = `\u514d\u8d39\u7248\u4ec5\u53ef\u4f53\u9a8c ${state.freeCourseLimit} \u95e8\u8bfe\u5b66\u65f6\uff0c\u8bf7\u51cf\u5c11\u52fe\u9009\u6216\u5f00\u901a\u4e13\u4e1a\u7248`; log(msg); _sph(msg); return; } state.enabled = true; state.stopFlag = false; state.runBusy = true; state.runMode = "study"; state.queueTotal = queue.length; state.queueDone = 0; _rrs(); log( `\u5f00\u59cb\u5904\u7406\u5b66\u65f6\uff1a${queue.length} \u95e8\u8bfe` ); try { for (let i = 0; i < queue.length; i++) { if (state.stopFlag) break; const course = queue[i]; await _scp(course.courNo); if (!_icd(course) && targetSec(course) > 0) { try { await _cfc(course); } catch (e) { log(`\u8df3\u8fc7\u300c${course.courName}\u300d\uff1a${e.message || e}`); continue; } } const result = await _reo(course); if (!result.skipped && (result.done || _icd(course))) { state.queueDone += 1; } else if (result.skipped && _icd(course)) { state.queueDone += 1; } _rrs(); _rcl2(); if (state.stopFlag) break; if (i < queue.length - 1) { state.currentTask = "\u8bfe\u95f4\u7a0d\u5019…"; _rrs(); const until = Date.now() + COURSE_GAP_MS; while (!state.stopFlag && Date.now() < until) { await sleep(500); } } } log(state.stopFlag ? "\u5b66\u65f6\u5904\u7406\u5df2\u505c\u6b62" : "\u5b66\u65f6\u5904\u7406\u5df2\u5b8c\u6210"); } catch (e) { log(`\u5b66\u65f6\u5904\u7406\u5f02\u5e38\uff1a${e.message || e}`); } finally { state.enabled = false; state.runBusy = false; state.runMode = ""; state.currentCourse = ""; state.currentTask = state.stopFlag ? "\u5df2\u505c\u6b62" : "\u7a7a\u95f2"; _rrs(); try { await _fcl(); _ra(); _ecd().catch(() => {}); } catch (_) { _ra(); } } } function _tef() { state.stopFlag = true; state.enabled = false; state.currentTask = "\u6b63\u5728\u505c\u6b62…"; _rrs(); log("\u5df2\u505c\u6b62"); } const HW_MAX_ROUNDS = 12; function flattenQuestions(paperData) { const out = []; const details = paperData?.paperDetail || []; for (const block of details) { const list = block?.questionList || []; const queType = String(block?.queType ?? block?.type ?? ""); const blockTitle = String(block?.title || block?.name || ""); for (const q of list) { out.push(Object.assign({}, q, { _queType: queType, _blockTitle: blockTitle })); } } return out; } function normalizeQueContent(qOrText) { const raw = typeof qOrText === "string" ? qOrText : String(qOrText?.queContent || qOrText?.subQueContent || ""); return raw .replace(/<[^>]+>/g, "") .replace(/ /gi, " ") .replace(/&[a-z]+;/gi, "") .replace(/\s+/g, "") .replace(/^\d+[\.\u3001\uff0e]/g, "") .slice(0, 240); } function normalizeOptionText(text) { return String(text || "") .replace(/<[^>]+>/g, "") .replace(/ /gi, " ") .replace(/&[a-z]+;/gi, "") .replace(/\s+/g, "") .slice(0, 200); } function parseAnswerLetters(raw) { const s = String(raw || "").trim().toUpperCase(); if (!s) return ""; const letters = [...s].filter((c) => c >= "A" && c <= "Z"); return [...new Set(letters)].sort().join(""); } function formatStuAnswer(letters, q) { const L = parseAnswerLetters(letters); if (!L) return ""; if (L.length > 1 || isMultiQuestion(q)) return [...L].join(","); return L; } function isMultiQuestion(q) { const t = String(q?._queType || q?.queType || "").trim(); const title = String(q?._blockTitle || q?.title || ""); if (t === "3" || /\u591a\u9009|\u591a\u9879/.test(title)) return true; return false; } function optionTextByLetter(q, letter) { const L = String(letter || "") .trim() .toUpperCase(); const opts = q?.answerList || []; for (const a of opts) { if (String(a.optionNo || "").trim().toUpperCase() === L) { return normalizeOptionText(a.option); } } return ""; } function optionTextsByLetters(q, letters) { const L = parseAnswerLetters(letters); const texts = []; for (const ch of L) { const t = optionTextByLetter(q, ch); if (t) texts.push(t); } return texts.join("|||"); } function letterByOptionText(q, answerText) { const want = normalizeOptionText(answerText); if (!want) return ""; const opts = q?.answerList || []; for (const a of opts) { if (normalizeOptionText(a.option) === want) { return String(a.optionNo || "") .trim() .toUpperCase(); } } return ""; } function lettersByOptionTexts(q, joined) { const parts = String(joined || "") .split("|||") .map((x) => normalizeOptionText(x)) .filter(Boolean); if (!parts.length) return ""; const found = []; for (const p of parts) { const L = letterByOptionText(q, p); if (L) found.push(L); } return [...new Set(found)].sort().join(""); } function _cab() { return { byQueNo: new Map(), byContent: new Map() }; } function bankSize(bank) { return Math.max(bank.byQueNo.size, bank.byContent.size); } function putBankEntry(bank, queNo, content, letter, text) { const L = parseAnswerLetters(letter); const T = String(text || "").trim(); if (!L && !T) return false; const entry = { letter: L || "", text: T || "" }; const shouldWrite = (prev) => { if (!prev) return true; const pL = parseAnswerLetters(prev.letter); if (L.length > pL.length) return true; // 更完整的多选 if (L.length < pL.length) return false; // 勿用残缺覆盖完整 const pParts = String(prev.text || "").split("|||").filter(Boolean).length; const nParts = String(T || "").split("|||").filter(Boolean).length; if (nParts > pParts) return true; if (nParts < pParts) return false; return true; }; let added = false; if (queNo) { const prev = bank.byQueNo.get(queNo); if (shouldWrite(prev)) { if (!prev) added = true; bank.byQueNo.set(queNo, entry); } } if (content) { const prev = bank.byContent.get(content); if (shouldWrite(prev)) { if (!prev) added = true; bank.byContent.set(content, entry); } } return added; } function mergeAnswersFromPaper(bank, paperData) { let added = 0; let multi = 0; for (const q of flattenQuestions(paperData)) { const letters = parseAnswerLetters(q.correctAnswer || q.standard || q.rightAnswer || ""); if (!letters) continue; if (letters.length > 1) multi += 1; const queNo = String(q.queNo || "").trim(); const content = normalizeQueContent(q); const text = optionTextsByLetters(q, letters); if (putBankEntry(bank, queNo, content, letters, text)) added += 1; } return { added, multi }; } function lookupAnswerEntry(q, bank) { const queNo = String(q.queNo || "").trim(); if (queNo && bank.byQueNo.has(queNo)) return bank.byQueNo.get(queNo); const content = normalizeQueContent(q); if (content && bank.byContent.has(content)) return bank.byContent.get(content); return null; } function resolveStuAnswer(q, bank) { const entry = lookupAnswerEntry(q, bank); if (!entry) return ""; const fromLetter = parseAnswerLetters(entry.letter); let fromText = ""; if (entry.text) { fromText = lettersByOptionTexts(q, entry.text); if (!fromText && !String(entry.text).includes("|||")) { fromText = letterByOptionText(q, entry.text) || ""; } } let ans = fromLetter.length >= fromText.length ? fromLetter : fromText; if (isMultiQuestion(q) && ans.length < 2) return ""; return ans; } function pickFallbackOption(q) { const opts = (q.answerList || []).map((a) => String(a.optionNo || "").trim()).filter(Boolean); return opts[0] || "A"; } async function _pcb(bank, questions) { if (!_ipu()) return 0; const stems = []; const stemSet = new Set(); for (const q of questions || []) { const s = normalizeQueContent(q); if (s && !stemSet.has(s) && !bank.byContent.has(s)) { stemSet.add(s); stems.push(s); } } if (!stems.length) return 0; try { const items = await _cql(stems); let n = 0; for (const it of items) { if (!it?.found && !(it?.answer_text || it?.answer_letter)) continue; if (!(it.answer_text || it.answer_letter)) continue; const stem = normalizeQueContent(it.stem || ""); if (!stem) continue; putBankEntry(bank, "", stem, it.answer_letter || "", it.answer_text || ""); n += 1; } return n; } catch (e) { return 0; } } async function _pbc(bank, meta) { if (!_ipu()) return 0; const items = []; for (const [stem, entry] of bank.byContent.entries()) { if (!stem || !(entry?.text || entry?.letter)) continue; items.push({ stem, answer_text: entry.text || "", answer_letter: entry.letter || "", paper_name: meta?.paper_name || "", cour_no: meta?.cour_no || "", source: "whut_hw", }); } if (!items.length) return 0; try { return await _cqu(items); } catch (e) { return 0; } } async function fetchCourseTasks(courNo) { const data = await _af( `/student/eduStudent/getCourseTasksNew?courNo=${encodeURIComponent(courNo)}` ); const list = data?.data?.list || []; return Array.isArray(list) ? list : []; } async function fetchExerciseHistory(courNo) { const data = await _af( `/student/eduStudent/exerciseHistory?courNo=${encodeURIComponent(courNo)}` ); const list = data?.data?.list || []; return Array.isArray(list) ? list : []; } async function checkLearnTime(courNo) { return _af(`/student/eduStudent/checkLearnTime?courNo=${encodeURIComponent(courNo)}`, { method: "POST", body: "{}", }); } async function startExercise(courNo, paperNo) { const data = await _af("/student/eduStudent/startMyExerciseNew", { method: "POST", body: JSON.stringify({ courNo: String(courNo), paperNo: Number(paperNo) }), }); const instNo = data?.data?.data?.instNo || data?.data?.instNo; if (!instNo) throw new Error("\u672a\u62ff\u5230\u7ec3\u4e60\u5b9e\u4f8b instNo"); return String(instNo); } async function showQuestionsNew(instNo) { const data = await _af("/student/eduStudent/showExerciseQuestionsNew", { method: "POST", body: JSON.stringify({ instNo: String(instNo), type: "1" }), }); return data?.data || {}; } async function showQuestionsReview(instNo) { const data = await _af("/student/eduStudent/showExerciseQuestions", { method: "POST", body: JSON.stringify({ instNo: String(instNo) }), }); return data?.data || {}; } async function saveStuAnswer(instNo, queNo, stuAnswer, q) { const userId = getUserId(); if (!userId) throw new Error("\u65e0\u6cd5\u8bc6\u522b\u8d26\u53f7"); const multi = isMultiQuestion(q) || parseAnswerLetters(stuAnswer).length > 1; const path = multi ? "/student/eduStudent/saveStuAnswerOfMultipleSelect" : "/student/eduStudent/saveStuAnswerOfSingleSelect"; return _af(path, { method: "POST", body: JSON.stringify({ userId: String(userId), queNo: String(queNo), stuAnswer: String(stuAnswer), instNo: String(instNo), }), }); } async function submitExercise(instNo) { await _af("/student/eduStudent/checkStuAnswerNew", { method: "POST", body: JSON.stringify({ instNo: String(instNo) }), }); await _af("/student/eduStudent/calcExercisePoints", { method: "POST", body: JSON.stringify({ instNo: String(instNo) }), }); } async function refreshHomeworkInfo(courNo, paperNo) { const data = await _af("/student/eduStudent/refreshInfo", { method: "POST", body: JSON.stringify({ courNo: String(courNo), paperNo: Number(paperNo) }), }); return data?.data || {}; } async function _fas(instNo, questions, bank, fillUnknown) { let known = 0; let guess = 0; for (const q of questions) { if (state.stopFlag) throw new Error("\u5df2\u505c\u6b62"); const queNo = String(q.queNo || "").trim(); if (!queNo) continue; let ans = resolveStuAnswer(q, bank); if (!ans) { if (!fillUnknown) continue; ans = pickFallbackOption(q); guess += 1; } else { known += 1; } const payload = formatStuAnswer(ans, q) || ans; await saveStuAnswer(instNo, queNo, payload, q); if ((known + guess) % 5 === 0) { state.currentTask = `\u5df2\u7b54 ${known + guess}/${questions.length}\uff08\u77e5 ${known}\uff09`; _rrs(); } await sleep(HW_ANSWER_GAP_MS); } await submitExercise(instNo); return { known, guess, total: questions.length }; } async function seedBankFromHistory(courNo, task, bank) { const hist = await fetchExerciseHistory(courNo); const name = String(task.ctpName || ""); const candidates = hist .filter((h) => { const n = String(h.instName || ""); return !name || n === name || n.includes(name) || name.includes(n); }) .sort((a, b) => Number(b.userScore || 0) - Number(a.userScore || 0)); let used = 0; for (const h of candidates.slice(0, 5)) { if (!h.instNo) continue; try { const paper = await showQuestionsReview(h.instNo); const before = bankSize(bank); mergeAnswersFromPaper(bank, paper); if (bankSize(bank) > before) { used += 1; } } catch (e) { } } return used; } async function _roh(courNo, task) { const paperNo = task.paperNo; const name = task.ctpName || `paper-${paperNo}`; let best = Number(task.maxScore || 0); const minScore = getHwMinScore(); state.currentTask = name; _rrs(); if (best >= minScore) { return { skipped: true, score: best }; } const bank = _cab(); await seedBankFromHistory(courNo, task, bank); for (let round = 1; round <= HW_MAX_ROUNDS; round++) { if (state.stopFlag) return { skipped: true, score: best }; if (best >= minScore) break; state.currentTask = name; _rrs(); await checkLearnTime(courNo); const instNo = await startExercise(courNo, paperNo); const paper = await showQuestionsNew(instNo); const questions = flattenQuestions(paper); if (!questions.length) throw new Error("\u9898\u76ee\u5217\u8868\u4e3a\u7a7a"); await _pcb(bank, questions); await _fas(instNo, questions, bank, true); const review = await showQuestionsReview(instNo); mergeAnswersFromPaper(bank, review); await _pbc(bank, { paper_name: name, cour_no: String(courNo) }); const info = await refreshHomeworkInfo(courNo, paperNo); const maxScore = Number(info.maxScore != null ? info.maxScore : best); if (Number.isFinite(maxScore)) best = Math.max(best, maxScore); if (best >= minScore) break; await sleep(600); } if (best >= minScore) { log(`\u300c${name}\u300d\u5df2\u5b8c\u6210\uff0c${best} \u5206`); } return { skipped: false, score: best }; } async function _rhc(course) { let tasks = []; try { tasks = await fetchCourseTasks(course.courNo); } catch (e) { log(`\u83b7\u53d6\u4f5c\u4e1a\u5217\u8868\u5931\u8d25\u300c${course.courName}\u300d\uff1a${e.message || e}`); return; } if (!tasks.length) { course.noHomework = true; course.homeworkTaskCount = 0; return; } course.noHomework = false; course.homeworkTaskCount = tasks.length; for (const task of tasks) { if (state.stopFlag) break; if (task.btnDisabled) { log(`\u8df3\u8fc7\u300c${task.ctpName}\u300d\uff1a\u6309\u94ae\u4e0d\u53ef\u7528`); continue; } try { await _roh(course.courNo, task); } catch (e) { log(`\u4f5c\u4e1a\u5931\u8d25\u300c${task.ctpName || task.paperNo}\u300d\uff1a${e.message || e}`); } await sleep(800); } } async function _shw() { if (state.runBusy) return; if (!getToken()) { log("\u8bf7\u5148\u767b\u5f55\u5b66\u751f\u7aef"); return; } getUserId(); if (!getUserId()) { log("\u65e0\u6cd5\u8bc6\u522b\u8d26\u53f7\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55"); return; } try { await _ecl(true); } catch (e) { log(`\u6388\u6743\u5931\u8d25\uff1a${e.message || e}`); return; } if (!_ipu()) { const msg = "\u4f5c\u4e1a\u529f\u80fd\u9700\u5f00\u901a\u4e13\u4e1a\u7248"; log(msg); _sph(msg); return; } const queue = _sc(); if (!queue.length) { log("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b"); return; } state.enabled = true; state.stopFlag = false; state.runBusy = true; state.runMode = "homework"; state.queueTotal = queue.length; state.queueDone = 0; _rrs(); log(`\u5f00\u59cb\u5904\u7406\u4f5c\u4e1a\uff1a${queue.length} \u95e8\u8bfe\uff0c\u76ee\u6807 ${getHwMinScore()} \u5206`); try { for (let i = 0; i < queue.length; i++) { if (state.stopFlag) break; const course = queue[i]; state.currentCourse = course.courName; _rrs(); await _rhc(course); state.queueDone += 1; _rrs(); _rcl2(); } log(state.stopFlag ? "\u4f5c\u4e1a\u5df2\u505c\u6b62" : "\u4f5c\u4e1a\u5904\u7406\u5b8c\u6210"); } catch (e) { log(`\u4f5c\u4e1a\u5904\u7406\u5f02\u5e38\uff1a${e.message || e}`); } finally { state.enabled = false; state.runBusy = false; state.runMode = ""; state.currentCourse = ""; state.currentTask = state.stopFlag ? "\u5df2\u505c\u6b62" : "\u7a7a\u95f2"; _rrs(); _ra(); } } async function _spl() { if (state.runBusy) return; if (!getToken()) { log("\u8bf7\u5148\u767b\u5f55\u5b66\u751f\u7aef"); return; } getUserId(); try { await _ecl(true); } catch (e) { log(`\u6388\u6743\u5931\u8d25\uff1a${e.message || e}`); return; } const queue = _sc(); if (!queue.length) { log("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b"); return; } if (!_ipu() && queue.length > state.freeCourseLimit) { const msg = `\u514d\u8d39\u7248\u4ec5\u53ef\u4f53\u9a8c ${state.freeCourseLimit} \u95e8\u8bfe\uff0c\u8bf7\u51cf\u5c11\u52fe\u9009\u6216\u5f00\u901a\u4e13\u4e1a\u7248`; log(msg); _sph(msg); return; } state.enabled = true; state.stopFlag = false; state.runBusy = true; state.runMode = "pipeline"; state.queueTotal = queue.length; state.queueDone = 0; _rrs(); log( _ipu() ? `\u5f00\u59cb\uff1a${queue.length} \u95e8\u8bfe\uff08\u5b66\u65f6\u4e0e\u4f5c\u4e1a\uff09` : `\u5f00\u59cb\uff1a${queue.length} \u95e8\u8bfe\uff08\u514d\u8d39\u7248\u4ec5\u5b66\u65f6\uff09` ); try { for (let i = 0; i < queue.length; i++) { if (state.stopFlag) break; const course = queue[i]; state.currentCourse = course.courName; _rrs(); await _scp(course.courNo); if (targetSec(course) > 0 && !_icd(course)) { try { await _cfc(course); await _reo(course); } catch (e) { log(`\u5b66\u65f6\u8df3\u8fc7\u300c${course.courName}\u300d\uff1a${e.message || e}`); } } else if (targetSec(course) > 0) { log(`\u300c${course.courName}\u300d\u5b66\u65f6\u5df2\u5b8c\u6210`); } if (state.stopFlag) break; if (_ipu()) { log(`\u300c${course.courName}\u300d\u5f00\u59cb\u4f5c\u4e1a`); await _rhc(course); } state.queueDone += 1; _rrs(); _rcl2(); if (state.stopFlag) break; if (i < queue.length - 1) { state.currentTask = "\u8bfe\u95f4\u7a0d\u5019…"; _rrs(); const until = Date.now() + COURSE_GAP_MS; while (!state.stopFlag && Date.now() < until) { await sleep(500); } } } log(state.stopFlag ? "\u5df2\u505c\u6b62" : "\u5168\u90e8\u5904\u7406\u5b8c\u6210"); } catch (e) { log(`\u5904\u7406\u5f02\u5e38\uff1a${e.message || e}`); } finally { state.enabled = false; state.runBusy = false; state.runMode = ""; state.currentCourse = ""; state.currentTask = state.stopFlag ? "\u5df2\u505c\u6b62" : "\u7a7a\u95f2"; _rrs(); try { await _fcl(); _ra(); _ecd().catch(() => {}); } catch (_) { _ra(); } } } function readPanelCollapsed() { const v = localStorage.getItem(PANEL_COLLAPSED_KEY); return v == null ? false : v === "1"; } function writePanelCollapsed(collapsed) { localStorage.setItem(PANEL_COLLAPSED_KEY, collapsed ? "1" : "0"); } function readPanelPos() { try { return JSON.parse(localStorage.getItem(PANEL_POS_KEY) || "null"); } catch (_) { return null; } } function writePanelPos(left, top) { localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left, top })); } function clampPanelInViewport(panel) { if (!panel) return; const rect = panel.getBoundingClientRect(); const maxLeft = Math.max(0, window.innerWidth - panel.offsetWidth); const maxTop = Math.max(0, window.innerHeight - panel.offsetHeight); const left = Math.max(0, Math.min(maxLeft, rect.left)); const top = Math.max(0, Math.min(maxTop, rect.top)); panel.style.right = "auto"; panel.style.bottom = "auto"; panel.style.left = `${left}px`; panel.style.top = `${top}px`; writePanelPos(left, top); } function applyPanelCollapsed(panel, collapsed) { const btnMin = panel.querySelector("#whut-btn-min"); const btnMax = panel.querySelector("#whut-btn-max"); panel.classList.toggle("whut-panel-min", !!collapsed); panel.classList.toggle("whut-panel-max", !collapsed); if (btnMin) btnMin.style.display = collapsed ? "none" : ""; if (btnMax) btnMax.style.display = collapsed ? "" : "none"; clampPanelInViewport(panel); } function _epd(panel) { const header = panel.querySelector("#whut-panel-header"); if (!header) return; let dragging = false; let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; header.addEventListener("mousedown", (e) => { if (e.target?.closest("#whut-panel-controls, .whut-panel-ctl")) return; dragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; panel.style.right = "auto"; panel.style.bottom = "auto"; e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (!dragging) return; const left = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, startLeft + (e.clientX - startX))); const top = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, startTop + (e.clientY - startY))); panel.style.left = `${left}px`; panel.style.top = `${top}px`; }); document.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; const rect = panel.getBoundingClientRect(); writePanelPos(rect.left, rect.top); }); } function _ips() { const old = document.getElementById("whut-wljy-panel-style"); if (old) old.remove(); const st = document.createElement("style"); st.id = "whut-wljy-panel-style"; st.textContent = ` #whut-wljy-panel{position:fixed;right:16px;top:64px;z-index:999999;width:360px;display:flex;flex-direction:column;background:linear-gradient(180deg,#eef3f9 0%,#f7f9fc 42%,#f3f6fa 100%);border:1px solid #cfd9e6;border-radius:14px;box-shadow:0 12px 32px rgba(15,23,42,.14);font-family:"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;font-size:11px;color:#0f172a;overflow:hidden;} #whut-wljy-panel.whut-panel-max{max-height:min(88vh,720px);} #whut-wljy-panel.whut-panel-min #whut-panel-body,#whut-wljy-panel.whut-panel-min .whut-footer-extra{display:none !important;} #whut-panel-header{padding:7px 9px;background:linear-gradient(135deg,#1e3a5f 0%,#245b8a 55%,#2f7aa8 100%);border-bottom:1px solid rgba(255,255,255,.12);display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;} #whut-panel-brand{display:flex;align-items:center;gap:8px;min-width:0;flex:1;} #whut-panel-logo{width:26px;height:26px;border-radius:8px;object-fit:contain;background:rgba(255,255,255,.95);border:1px solid rgba(255,255,255,.35);box-shadow:0 1px 4px rgba(0,0,0,.1);} #whut-panel-title{font-size:12px;font-weight:800;color:#fff;line-height:1.25;display:flex;gap:5px;flex-wrap:wrap;align-items:center;} .whut-panel-title-text{flex:1 1 10em;min-width:0;letter-spacing:.1px;} .whut-panel-version{font-size:10px;font-weight:800;color:#dbeafe;padding:1px 6px;border-radius:999px;background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.22);} #whut-panel-sub{display:flex;flex-wrap:wrap;gap:3px;margin-top:3px;} .whut-sub-chip{font-size:10px;color:#e0f2fe;background:rgba(255,255,255,.12);padding:1px 6px;border-radius:999px;border:1px solid rgba(255,255,255,.18);font-weight:700;} .whut-sub-chip-em{color:#ecfdf5;background:rgba(16,185,129,.22);border-color:rgba(167,243,208,.35);} #whut-panel-controls{display:flex;gap:4px;} .whut-panel-ctl{border:none;background:rgba(255,255,255,.16);color:#fff;width:24px;height:24px;border-radius:999px;cursor:pointer;font-weight:900;font-size:12px;} .whut-panel-ctl:hover{background:rgba(255,255,255,.28);} #whut-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:7px;} .whut-card{background:rgba(255,255,255,.92);border:1px solid #d7e0ec;border-radius:11px;padding:7px 8px;margin-bottom:7px;box-shadow:0 1px 2px rgba(15,23,42,.04);} .whut-meta-row{display:flex;justify-content:space-between;gap:6px;font-size:11px;margin-bottom:3px;align-items:flex-start;} .whut-meta-label{color:#64748b;flex:0 0 auto;} .whut-meta-value{font-weight:700;text-align:right;word-break:break-all;color:#0f172a;} .whut-status-row{display:flex;justify-content:space-between;align-items:center;gap:6px;} #whut-run-status{padding:2px 8px;border-radius:999px;font-weight:800;font-size:10px;border:1px solid #cbd5e1;background:#fff;color:#475569;} #whut-run-status.on{background:#ecfdf5;border-color:#6ee7b7;color:#047857;} .whut-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;} .whut-progress-bar>span{display:block;height:100%;background:linear-gradient(90deg,#0ea5e9,#2563eb);width:0;transition:width .25s ease;} .whut-tabbar{display:flex;gap:5px;margin-bottom:6px;} .whut-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px 3px;border-radius:8px;cursor:pointer;font-weight:700;font-size:11px;} .whut-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0284c7);color:#fff;border-color:transparent;box-shadow:0 3px 8px rgba(37,99,235,.22);} .whut-pane{display:none;}.whut-pane.active{display:block;} .whut-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:5px;gap:6px;} .whut-list-title{font-size:11px;color:#64748b;font-weight:700;} .whut-list-tag{font-size:10px;color:#1e40af;background:#dbeafe;border:1px solid #93c5fd;border-radius:999px;padding:1px 6px;font-weight:700;} .whut-toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:4px 6px;margin-bottom:5px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;} .whut-check-mini{display:inline-flex;align-items:center;gap:4px;font-size:11px;color:#334155;font-weight:700;cursor:pointer;user-select:none;} .whut-check-mini input{width:12px;height:12px;accent-color:#2563eb;cursor:pointer;} .whut-refresh-mini{border:none;background:transparent;color:#1d4ed8;font-size:11px;font-weight:800;cursor:pointer;padding:0;text-decoration:underline;text-underline-offset:2px;} .whut-refresh-mini:disabled{opacity:.45;cursor:not-allowed;} #whut-course-list{max-height:220px;overflow-y:auto;display:flex;flex-direction:column;gap:6px;padding:1px;background:transparent;border:none;} #whut-run-log{max-height:220px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:10px;padding:4px;} .whut-empty-state{padding:18px 8px;text-align:center;color:#94a3b8;font-size:12px;font-weight:600;} .whut-course-item{display:grid;grid-template-columns:18px 1fr;gap:8px 9px;align-items:start;padding:9px 10px;border:1px solid #e3ebf4;background:#fff;border-radius:12px;margin:0;box-shadow:0 1px 2px rgba(15,23,42,.03);transition:border-color .15s,box-shadow .15s,background .15s;} .whut-course-item:hover{border-color:#c7dbf5;box-shadow:0 3px 10px rgba(37,99,235,.07);} .whut-course-item.is-done{background:#f8fafc;border-color:#e8eef5;box-shadow:none;cursor:default;} .whut-course-item.is-done:hover{border-color:#e8eef5;box-shadow:none;} .whut-course-item > input{width:14px;height:14px;margin:2px 0 0;accent-color:#2563eb;cursor:pointer;flex-shrink:0;} .whut-course-item.is-done > input{cursor:not-allowed;accent-color:#94a3b8;} .whut-course-main{min-width:0;grid-column:2;} .whut-course-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;} .whut-course-name{font-weight:600;font-size:11px;line-height:1.35;color:#1e293b;letter-spacing:0;} .whut-course-item.is-done .whut-course-name{color:#64748b;font-weight:600;} .whut-done-badge{flex:0 0 auto;font-size:9px;font-weight:700;line-height:1;color:#059669;background:#ecfdf5;border:1px solid #a7f3d0;border-radius:999px;padding:2px 6px;white-space:nowrap;} .whut-chip-cate{background:linear-gradient(135deg,#eff6ff,#dbeafe)!important;border-color:#93c5fd!important;color:#1d4ed8!important;font-weight:800;} .whut-chip-cate em{color:#1d4ed8!important;font-weight:800;} .whut-course-item.is-practice:not(.is-done){border-color:#bfdbfe;} .whut-course-item.is-done .whut-chip-cate{background:#e2e8f0!important;border-color:#cbd5e1!important;} .whut-course-item.is-done .whut-chip-cate em{color:#475569!important;} .whut-course-sub{display:flex;flex-wrap:wrap;gap:3px 8px;margin-top:3px;font-size:10px;line-height:1.3;color:#64748b;} .whut-course-sub span{display:inline-flex;align-items:baseline;gap:3px;} .whut-course-sub b{font-weight:600;color:#334155;font-variant-numeric:tabular-nums;} .whut-course-item.is-done .whut-course-sub{color:#94a3b8;} .whut-course-item.is-done .whut-course-sub b{color:#64748b;} .whut-score-row{display:flex;flex-wrap:wrap;gap:3px;margin-top:5px;} .whut-score-chip{display:inline-flex;align-items:center;gap:3px;font-size:9px;font-weight:600;color:#64748b;background:#f8fafc;border:1px solid #e8eef5;border-radius:999px;padding:2px 6px;line-height:1.2;} .whut-score-chip i{font-style:normal;font-weight:600;color:#94a3b8;} .whut-score-chip em{font-style:normal;color:#1d4ed8;font-weight:700;font-variant-numeric:tabular-nums;} .whut-course-item.is-done .whut-score-chip{background:#f1f5f9;border-color:#e2e8f0;color:#94a3b8;} .whut-course-item.is-done .whut-score-chip i{color:#94a3b8;} .whut-course-item.is-done .whut-score-chip em{color:#64748b;} .whut-btn-row{display:flex;gap:6px;margin-top:6px;flex-wrap:wrap;} .whut-btn{flex:1;border:none;color:#fff;padding:7px 8px;border-radius:9px;cursor:pointer;font-weight:800;font-size:12px;min-width:80px;} .whut-btn-start{background:linear-gradient(135deg,#16a34a,#15803d);box-shadow:0 4px 10px rgba(22,163,74,.24);} .whut-btn-stop{background:linear-gradient(135deg,#ef4444,#dc2626);box-shadow:0 4px 10px rgba(239,68,68,.18);} .whut-btn-pro{background:linear-gradient(135deg,#f59e0b,#ea580c);box-shadow:0 4px 12px rgba(234,88,12,.28);color:#fff;} .whut-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;box-shadow:none;} .whut-btn-mini{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;border:1px solid #cbd5e1;background:#fff;color:#334155;border-radius:7px;padding:2px 8px;font-size:11px;font-weight:700;line-height:1.2;white-space:nowrap;cursor:pointer;min-width:0;box-shadow:none;} .whut-btn:disabled{opacity:.55;cursor:not-allowed;box-shadow:none;} .whut-pro-banner{margin-bottom:8px;padding:10px;border-radius:12px;background:linear-gradient(135deg,#fff7ed,#ffedd5 55%,#fed7aa);border:1px solid #fdba74;box-shadow:0 4px 14px rgba(234,88,12,.12);} .whut-pro-banner-title{font-size:13px;font-weight:800;color:#9a3412;line-height:1.3;} .whut-pro-banner-desc{margin-top:4px;font-size:11px;color:#9a3412;line-height:1.45;opacity:.92;} .whut-pro-banner .whut-btn-pro{margin-top:8px;width:100%;padding:9px 10px;font-size:13px;border-radius:10px;text-decoration:none;display:flex;align-items:center;justify-content:center;} .whut-footer-extra{padding:6px 8px;background:rgba(248,250,252,.95);border-top:1px solid #e2e8f0;} .whut-ann{position:relative;font-size:11px;color:#334155;line-height:1.4;background:linear-gradient(180deg,#fffdf7,#fff8eb);border:1px solid #fcd34d;border-radius:10px;padding:7px 8px 7px 10px;} .whut-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-radius:10px 0 0 10px;} .whut-log-row{padding:3px 5px;border-bottom:1px dashed #d4deea;font-size:11px;line-height:1.4;} .whut-linkish{color:#1d4ed8;cursor:pointer;text-decoration:none;font-weight:700;} #whut-dialog-root{position:fixed;inset:0;z-index:1000001;display:flex;align-items:center;justify-content:center;padding:16px;font-family:"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;} .whut-dialog-mask{position:absolute;inset:0;background:rgba(15,23,42,.45);backdrop-filter:blur(2px);} .whut-dialog-card{position:relative;z-index:1;width:min(320px,92vw);background:#fff;border-radius:14px;border:1px solid #d7e0ec;box-shadow:0 18px 48px rgba(15,23,42,.28);overflow:hidden;} .whut-dialog-head{padding:11px 14px;background:linear-gradient(135deg,#1e3a5f 0%,#245b8a 55%,#2f7aa8 100%);color:#fff;font-size:13px;font-weight:800;} .whut-dialog-body{padding:14px;font-size:12.5px;line-height:1.55;color:#334155;white-space:pre-wrap;word-break:break-word;} .whut-dialog-actions{display:flex;flex-wrap:wrap;gap:8px;padding:0 14px 14px;} .whut-dialog-actions .whut-btn{flex:1;min-width:88px;} `; document.head.appendChild(st); } function syncSelectToolbar() { const all = document.getElementById("whut-check-all"); const none = document.getElementById("whut-check-none"); if (!all && !none) return; const selectable = state.courses.filter((c) => !_icfd(c)); const total = selectable.length; const sel = selectable.filter((c) => state.selected.has(c.courNo)).length; if (all) { all.checked = total > 0 && sel === total; all.indeterminate = sel > 0 && sel < total; all.disabled = !!state.runBusy || total === 0; } if (none) { none.checked = state.courses.length > 0 && state.selected.size === 0; none.disabled = !!state.runBusy; } } function _rcl2() { const box = document.getElementById("whut-course-list"); if (!box) return; if (!state.courses.length) { box.innerHTML = `
${getToken() ? "\u6682\u65e0\u8bfe\u7a0b" : "\u8bf7\u5148\u767b\u5f55\u5b66\u751f\u7aef"}
`; syncSelectToolbar(); return; } _pds(); box.innerHTML = state.courses .map((c) => { const fullyDone = _icfd(c); const checked = !fullyDone && state.selected.has(c.courNo) ? "checked" : ""; const reqLabel = c.requireMin > 0 ? formatDurationLabel(c.requireMin) : "—"; const doneLabel = c.doneLabel || formatDurationLabel(c.doneMin); const noHw = c.noHomework === true || (_ipc(c) && _hns(c)); const noStudy = _hns(c); const cateLabel = String(c.courseCateporyName || "").trim() || "\u5b9e\u8df5\u73af\u8282\u8bfe\u7a0b"; let chipHtml = ""; if (noHw || (_ipc(c) && noStudy)) { const parts = [`${escHtml(cateLabel)}`]; if (noStudy) parts.push(`\u65e0\u5b66\u65f6`); parts.push(`\u65e0\u4f5c\u4e1a`); if (!noStudy) { parts.push( `\u8bfe\u4ef6${escHtml(formatScore(c.coursewareResult))}` ); } chipHtml = parts.join(""); } else { chipHtml = [ ["\u4f5c\u4e1a\u4e00", formatScore(c.homeworkOne)], ["\u4f5c\u4e1a\u4e8c", formatScore(c.homeworkTwo)], ["\u5747\u5206", formatScore(c.homeworkResult)], ["\u8bfe\u4ef6", formatScore(c.coursewareResult)], ["\u4e92\u52a8", formatScore(c.snsResult)], ] .map(([k, v]) => `${escHtml(k)}${escHtml(v)}`) .join(""); } const badge = fullyDone ? `\u5df2\u5b8c\u6210` : ""; const practiceCls = _ipc(c) || noHw ? " is-practice" : ""; return ` `; }) .join(""); box.querySelectorAll("input[type=checkbox][data-cour]").forEach((el) => { el.addEventListener("change", () => { if (el.disabled) return; const id = el.getAttribute("data-cour"); if (el.checked) state.selected.add(id); else state.selected.delete(id); saveQueue(); _rst(); }); }); syncSelectToolbar(); } function _cp() { const old = document.getElementById("whut-wljy-panel"); if (old) old.remove(); _ips(); const panel = document.createElement("div"); panel.id = "whut-wljy-panel"; panel.className = "whut-panel-max"; panel.innerHTML = `
\u6b66\u6c49\u7406\u5de5\u5927\u5b66\u7f51\u7edc\u6559\u80b2\u5b66\u4e60\u52a9\u624b v${SCRIPT_VERSION}
\u5b66\u65f6\u5b66\u4e60 \u4f5c\u4e1a\u5904\u7406
\u8fd0\u884c\u72b6\u6001 \u5df2\u505c\u6b62
\u961f\u5217\u8fdb\u5ea60/0
\u6211\u7684\u8bfe\u7a0b \u672a\u52a0\u8f7d
\u767b\u5f55\u540e\u81ea\u52a8\u52a0\u8f7d
\u5f53\u524d\u8bfe\u7a0b\u65e0
\u5f53\u524d\u4efb\u52a1\u70b9\u51fb\u5f00\u59cb\u540e\u5904\u7406\u5b66\u65f6\u4e0e\u4f5c\u4e1a
\u8d26\u53f7
\u4f1a\u5458\u72b6\u6001
\u5df2\u9009\u8bfe\u7a0b
\u8fd0\u884c\u65e5\u5fd7
\u5347\u7ea7\u4e13\u4e1a\u7248
\u89e3\u9501\u5168\u90e8\u8bfe\u7a0b\u5b66\u65f6\u4e0e\u4f5c\u4e1a\u5904\u7406\uff0c\u6388\u6743\u7801\u7c98\u8d34\u540e\u7acb\u5373\u751f\u6548\u3002
\u7acb\u5373\u8d2d\u4e70\u4e13\u4e1a\u7248
\u4f1a\u5458\u6388\u6743Pro
\u6388\u6743\u7801
`; document.documentElement.appendChild(panel); const pos = readPanelPos(); if (pos && Number.isFinite(pos.left) && Number.isFinite(pos.top)) { panel.style.right = "auto"; panel.style.left = `${pos.left}px`; panel.style.top = `${pos.top}px`; } applyPanelCollapsed(panel, readPanelCollapsed()); _epd(panel); panel.querySelector("#whut-btn-min")?.addEventListener("click", () => { writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); panel.querySelector("#whut-btn-max")?.addEventListener("click", () => { writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); panel.querySelectorAll(".whut-tab-btn").forEach((btn) => { btn.addEventListener("click", () => { const tab = btn.getAttribute("data-tab"); panel.querySelectorAll(".whut-tab-btn").forEach((b) => b.classList.toggle("active", b === btn)); panel.querySelectorAll(".whut-pane").forEach((p) => { p.classList.toggle("active", p.getAttribute("data-pane") === tab); }); }); }); panel.querySelector("#whut-clear-log")?.addEventListener("click", () => { state.logLines = []; const box = document.getElementById("whut-run-log"); if (box) box.innerHTML = ""; }); panel.querySelector("#whut-btn-refresh")?.addEventListener("click", () => _rfa(true)); panel.querySelector("#whut-check-all")?.addEventListener("change", (e) => { const on = !!e.target.checked; if (on) { state.courses.forEach((c) => { if (!_icfd(c)) state.selected.add(c.courNo); }); const none = document.getElementById("whut-check-none"); if (none) none.checked = false; } else { state.selected.clear(); } saveQueue(); _ra(); }); panel.querySelector("#whut-check-none")?.addEventListener("change", (e) => { if (!e.target.checked) return; state.selected.clear(); const all = document.getElementById("whut-check-all"); if (all) { all.checked = false; all.indeterminate = false; } saveQueue(); _ra(); }); panel.querySelector("#whut-btn-pipeline")?.addEventListener("click", () => _spl()); panel.querySelector("#whut-btn-stop")?.addEventListener("click", () => _tef()); panel.querySelector("#whut-btn-save-token")?.addEventListener("click", async () => { const tok = String(document.getElementById("whut-cloud-token")?.value || "").trim(); state.cloudToken = tok; if (tok) localStorage.setItem(CLOUD_TOKEN_KEY, tok); else localStorage.removeItem(CLOUD_TOKEN_KEY); _wcl("", 0); state.cloudLease = ""; state.cloudTier = tok ? "unknown" : "free"; try { getUserId(); await _ecl(true); log(_ipu() ? "\u4e13\u4e1a\u7248\u5df2\u5f00\u901a" : "\u5f53\u524d\u4e3a\u514d\u8d39\u4f53\u9a8c"); if (!_ipu() && tok) { await _swa("\u6388\u6743\u7801\u672a\u751f\u6548\uff0c\u8bf7\u786e\u8ba4\u662f\u5426\u6b63\u786e\u6216\u5df2\u8fc7\u671f\u3002", "\u6388\u6743\u63d0\u793a"); } } catch (e) { state.cloudTier = "free"; _wcl("", 0); state.cloudLease = ""; const tip = _fcm(e.message || e); log(`\u6388\u6743\u6821\u9a8c\u5931\u8d25\uff1a${tip}`); await _sph(tip); } _rcm(); }); panel.querySelector("#whut-btn-clear-token")?.addEventListener("click", async () => { const ok = await _swc("\u786e\u5b9a\u6e05\u9664\u5f53\u524d\u6388\u6743\u7801\u5417\uff1f", "\u6e05\u9664\u6388\u6743"); if (!ok) return; state.cloudToken = ""; localStorage.removeItem(CLOUD_TOKEN_KEY); _wcl("", 0); state.cloudLease = ""; state.cloudTier = "free"; const inp = document.getElementById("whut-cloud-token"); if (inp) inp.value = ""; _rcm(); log("\u5df2\u6e05\u9664\u6388\u6743\u7801"); }); const tokInp = panel.querySelector("#whut-cloud-token"); if (tokInp) tokInp.value = state.cloudToken || ""; return panel; } function _rrs() { const badge = document.getElementById("whut-run-status"); if (badge) { const label = state.runBusy ? "\u5b66\u4e60\u4e2d" : "\u5df2\u505c\u6b62"; badge.textContent = label; badge.classList.toggle("on", !!state.runBusy); } const set = (id, text) => { const el = document.getElementById(id); if (el) el.textContent = text; }; set("whut-current-course", state.currentCourse || "\u65e0"); set("whut-current-task", state.currentTask || "\u70b9\u51fb\u5f00\u59cb\u540e\u5904\u7406\u5b66\u65f6\u4e0e\u4f5c\u4e1a"); set("whut-queue-progress-text", `${state.queueDone}/${state.queueTotal}`); const bar = document.getElementById("whut-queue-progress-bar"); if (bar) { const pct = state.queueTotal ? Math.round((state.queueDone / state.queueTotal) * 100) : 0; bar.style.width = `${pct}%`; } const btnPipe = document.getElementById("whut-btn-pipeline"); const btnStop = document.getElementById("whut-btn-stop"); if (btnPipe) btnPipe.disabled = !!state.runBusy; if (btnStop) btnStop.disabled = !state.runBusy; } function _rst() { const set = (id, text) => { const el = document.getElementById(id); if (el) el.textContent = text; }; set("whut-user-name", state.userName || (getToken() ? "\u5df2\u767b\u5f55" : "\u672a\u767b\u5f55")); set("whut-sel-summary", `${state.selected.size} / ${state.courses.length} \u95e8`); _rcm(); const tag = document.getElementById("whut-course-tag"); if (tag) tag.textContent = state.courses.length ? `${state.courses.length} \u95e8\u8bfe` : "\u672a\u52a0\u8f7d"; const busy = state.panelRefreshing || state.runBusy; const btnRefresh = document.getElementById("whut-btn-refresh"); if (btnRefresh) btnRefresh.disabled = busy; _rrs(); syncSelectToolbar(); } function _ra() { _rst(); _rcl2(); } async function _rfa(manual) { if (state.panelRefreshing) return; state.panelRefreshing = true; _rst(); try { if (!getToken()) { log("\u8bf7\u5148\u767b\u5f55\u5b66\u751f\u7aef"); state.courses = []; _ra(); return; } if (manual) log("\u6b63\u5728\u5237\u65b0\u8bfe\u7a0b\u5217\u8868…"); await _fui(); try { await _ecl(false); } catch (_) {} await _rpn(); await _fcl(); log(`\u8bfe\u7a0b\u5217\u8868\u5df2\u52a0\u8f7d\uff1a${state.courses.length} \u95e8`); log(`\u5df2\u9009 ${state.selected.size} \u95e8\u8bfe`); _ra(); state.panelRefreshing = false; _rst(); _ecd().catch(() => {}); return; } catch (e) { log(`\u5237\u65b0\u5931\u8d25\uff1a${e.message || e}`); _ra(); } finally { state.panelRefreshing = false; _rst(); } } function boot() { _cp(); log("\u52a9\u624b\u5df2\u542f\u52a8"); _rfa(false); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } })();