// ==UserScript==
// @name 【效率版】武汉理工大学高等学历终身教育学习助手
// @namespace https://card.wlxy.live/details/A9C371DB
// @version 1.2
// @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.18";
})();
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 = `
${escHtml(title)}
${escHtml(message)}
${showBuy ? `` : ""}
${cancelText ? `` : ""}
`;
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 _rcd(courNo, opts = {}) {
const withExam = opts.withExam !== false;
const withHwFlag = !!opts.withHomeworkFlag;
await _scp(courNo);
if (withExam) {
try {
const examData = await _af("/student/MyExamResults/listEduStuExamResults");
_mei(state.courses, examData);
} catch (_) {}
}
if (withHwFlag) {
const c = state.courses.find((x) => x.courNo === String(courNo));
if (c) {
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);
}
}
}
_pds();
_rcl2();
_rst();
_rrs();
}
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);
_rcl2();
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;
}
await _rcd(course.courNo, { withExam: true });
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;
await _rcd(course.courNo, { withExam: true, withHomeworkFlag: true });
}
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`);
}
await _rcd(course.courNo, { withExam: true });
if (state.stopFlag) break;
if (_ipu()) {
log(`\u300c${course.courName}\u300d\u5f00\u59cb\u4f5c\u4e1a`);
await _rhc(course);
await _rcd(course.courNo, { withExam: true, withHomeworkFlag: true });
}
state.queueDone += 1;
_rrs();
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 = `
\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();
}
})();