// ==UserScript== // @name 甘肃专业技术教育学习助手 // @namespace https://gsadjy.sccchina.net/ // @version 1.0 // @description 甘肃专业技术教育(安德职业培训):勾选课程自助学习;免费体验 1 门课,专业版激活后 30 天有效,免费考试,1:1常速模式和效率模式自由选择。 // @author 柠檬真酸 // @match https://gsadjy.sccchina.net/* // @match https://*.sccchina.net/* // @match https://*.sccchina.net/mizar/* // @grant GM_xmlhttpRequest // @grant GM_info // @grant GM_openInTab // @connect gsadjy.sccchina.net // @connect *.sccchina.net // @connect oa40.ahzsksw.cn // @connect *.ahzsksw.cn // @connect mizarfile.chinaedu.net // @connect huaweicloudobs.ahjxjy.cn // @run-at document-idle // ==/UserScript== (function () { "use strict"; if (!/\/mizar\//i.test(location.pathname) && !/gsadjy\.sccchina\.net$/i.test(location.host)) { return; } const SCRIPT_VERSION = (() => { try { if (typeof GM_info !== "undefined" && GM_info?.script?.version) { return String(GM_info.script.version); } } catch (_) {} return "1.0"; })(); const SCRIPT_NAME = "\u7518\u8083\u4e13\u4e1a\u6280\u672f\u6559\u80b2\u5b66\u4e60\u52a9\u624b"; function secToMin(sec) { return Math.max(0, Math.ceil((Number(sec) || 0) / 60)); } function progressMinText(pos, total) { return `${secToMin(pos)}/${secToMin(total)} \u5206\u949f`; } const ORIGIN = location.origin; const BASE = "/mizar"; const SIGN_SALT = "52c203760cf28798a44f6ac4"; const PANEL_LOGO_URL = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const DEFAULT_CLOUD_API_BASE = "https://oa40.ahzsksw.cn"; const CLOUD_TOKEN_KEY = "gsadjy_cloud_token_v1"; const CLOUD_LEASE_CACHE_KEY = "gsadjy_cloud_lease_cache_v1"; const CLOUD_CLIENT_CFG_KEY = "gsadjy_client_cfg_v1"; const LEARNING_UID_KEY = "gsadjy_learning_uid_v1"; const PRO_BUY_URL = "https://card.wlxy.live/details/7621DC2D"; const KEY_QUEUE = "gsadjy_queue_v1"; const KEY_DONE = "gsadjy_done_v1"; const KEY_MODE = "gsadjy_mode_v1"; const KEY_POS = "gsadjy_panel_pos_v1"; const KEY_COLLAPSED = "gsadjy_panel_collapsed_v1"; const MODE_EFF = "efficiency"; const MODE_RT = "realtime"; let EFF_PARTS = 3; let EFF_WAIT_MS = 1500; let RT_STEP = 60; let RT_WAIT_MS = 60000; let LOCK_EVERY_MS = 20000; let FREE_COURSE_LIMIT_DEFAULT = 1; const EXAM_ANSWER_BY_QID = { "d64bee48-59ab-4fbb-8010-8b87168886d6": "1", "c568dfd3-1c5c-40ef-a6be-c4e5a03392c1": "4", "2a7547e3-e478-4c7e-bfb7-6b601994be99": "3", "6dcd4a78-d149-4a36-8433-fcc21e74ba4f": "2", "cf46fc99-a1a3-48ba-9ed0-05ce48e50a26": "4", }; const EXAM_ANSWER_BANK = [ { match: "\u4e2d\u56fd\u5171\u4ea7\u515a\u6700\u5927\u7684\u4f18\u52bf", answerText: "\u5bc6\u5207\u8054\u7cfb\u7fa4\u4f17", fallbackValue: "1" }, { match: "\u4f5c\u98ce\u5efa\u8bbe\u7684\u5386\u7a0b\u5206\u4e3a", answerText: "\u4e09", fallbackValue: "4" }, { match: "\u5341\u9879\u89c4\u5b9a", answerText: "\u56db\u5ddd\u7701\u59d4\u7701\u653f\u5e9c", fallbackValue: "3" }, { match: "\u516b\u9879\u89c4\u5b9a", answerText: "2012", fallbackValue: "2" }, { match: "\u515a\u7684\u5341\u4e5d\u5927\u548c\u4e8c\u5341\u5927\u540e", answerText: "\u4e24\u6b21", fallbackValue: "4" }, ]; const state = { busy: false, stopFlag: false, trains: [], courses: [], queue: loadJson(KEY_QUEUE, []), mode: localStorage.getItem(KEY_MODE) || MODE_EFF, logLines: [], currentCourse: "", currentTask: "\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u5b66\u4e60", userName: "", learningUserId: localStorage.getItem(LEARNING_UID_KEY) || "", examBusy: false, examActivities: [], cloudToken: localStorage.getItem(CLOUD_TOKEN_KEY) || "", cloudLease: "", cloudTier: "free", cloudProExpireAt: 0, freeCourseLimit: FREE_COURSE_LIMIT_DEFAULT, freeUsedCourses: 0, canExam: false, activationTtlDays: 30, proBuyUrl: PRO_BUY_URL, doneIds: new Set(loadJson("gsadjy_done_v1", [])), panelNotice: "", chapterCache: Object.create(null), }; function loadJson(k, def) { try { const v = JSON.parse(localStorage.getItem(k) || "null"); return v == null ? def : v; } catch (_) { return def; } } function saveJson(k, v) { localStorage.setItem(k, JSON.stringify(v)); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function escapeHtml(s) { return String(s || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function rememberDone(id) { if (!id) return; state.doneIds.add(id); saveJson(KEY_DONE, [...state.doneIds]); } function log(msg, level) { const line = { text: `[${new Date().toLocaleTimeString()}] ${String(msg || "").trim()}`, level: level || "", }; state.logLines.unshift(line); if (state.logLines.length > 200) state.logLines.length = 200; const box = document.getElementById("gs-run-log"); if (box) { box.innerHTML = state.logLines .slice(0, 80) .map((t) => { const row = typeof t === "string" ? { text: t, level: "" } : t; const cls = row.level === "warn" ? " gs-log-warn" : ""; return `
${escapeHtml(row.text)}
`; }) .join(""); } console.log("[\u5b89\u8fbe\u52a9\u624b]", msg); } function showProAlert(text) { const el = document.getElementById("gs-pro-alert"); if (!el) return; el.style.display = "block"; el.innerHTML = `${escapeHtml(text)} \u7acb\u5373\u5f00\u901a\u4e13\u4e1a\u7248`; } function getCloudApiBase() { try { const cfg = loadJson(CLOUD_CLIENT_CFG_KEY, null); if (cfg && cfg.cloudApiBase) return String(cfg.cloudApiBase).replace(/\/$/, ""); } catch (_) {} return DEFAULT_CLOUD_API_BASE; } function resolveLearningUserId() { if (state.learningUserId) return state.learningUserId; const html = document.documentElement ? document.documentElement.innerHTML : ""; const m = html.match(/setUserId['"]?\s*,\s*['"]([0-9a-f-]{36})['"]/i) || html.match(/userId['"]\s*[:=]\s*['"]([0-9a-f-]{36})['"]/i); if (m) { state.learningUserId = m[1]; localStorage.setItem(LEARNING_UID_KEY, state.learningUserId); return state.learningUserId; } let uid = localStorage.getItem(LEARNING_UID_KEY) || ""; if (!uid) { uid = "local-" + Math.random().toString(36).slice(2) + Date.now().toString(36); localStorage.setItem(LEARNING_UID_KEY, uid); } state.learningUserId = uid; return uid; } function cloudHttp(path, method, payload) { const base = getCloudApiBase(); const url = `${base}${path.startsWith("/") ? path : "/" + path}`; const headers = { Accept: "application/json", "Content-Type": "application/json" }; const luid = resolveLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; if (state.cloudToken) headers.Authorization = "Bearer " + state.cloudToken; return new Promise((resolve, reject) => { const done = (status, text) => resolve({ status, text: text || "" }); if (typeof GM_xmlhttpRequest === "function") { GM_xmlhttpRequest({ method: method || "GET", url, headers, data: payload == null ? undefined : JSON.stringify(payload), timeout: 30000, onload: (r) => done(r.status, r.responseText || ""), onerror: () => reject(new Error("\u4e91\u7aef\u7f51\u7edc\u9519\u8bef")), ontimeout: () => reject(new Error("\u4e91\u7aef\u8d85\u65f6")), }); } else { fetch(url, { method: method || "GET", headers, body: payload == null ? undefined : JSON.stringify(payload), }) .then(async (r) => done(r.status, await r.text())) .catch(() => reject(new Error("\u4e91\u7aef\u7f51\u7edc\u9519\u8bef"))); } }); } async function cloudJson(path, method, payload) { const res = await cloudHttp(path, method, payload); let j = null; try { j = JSON.parse(res.text || "{}"); } catch (_) { throw new Error("\u4e91\u7aef\u54cd\u5e94\u5f02\u5e38"); } if (res.status >= 400 || (j && j.ok === false)) { throw new Error((j && (j.message || j.detail)) || "\u4e91\u7aef\u8bf7\u6c42\u5931\u8d25"); } return j; } function applyClientConfig(cfg) { if (!cfg || typeof cfg !== "object") return; saveJson(CLOUD_CLIENT_CFG_KEY, cfg); if (cfg.freeCourseLimit != null || cfg.free_course_limit != null) { state.freeCourseLimit = Number(cfg.freeCourseLimit ?? cfg.free_course_limit) || 1; } if (cfg.activationTtlDays != null || cfg.activation_ttl_days != null) { state.activationTtlDays = Number(cfg.activationTtlDays ?? cfg.activation_ttl_days) || 30; } if (cfg.proBuyUrl) state.proBuyUrl = String(cfg.proBuyUrl); if (cfg.effParts != null) EFF_PARTS = Math.max(1, Number(cfg.effParts) || 3); if (cfg.effWaitMs != null) EFF_WAIT_MS = Math.max(200, Number(cfg.effWaitMs) || 1500); if (cfg.rtStep != null) RT_STEP = Math.max(1, Number(cfg.rtStep) || 60); if (cfg.rtWaitMs != null) RT_WAIT_MS = Math.max(1000, Number(cfg.rtWaitMs) || 60000); if (cfg.lockEveryMs != null) LOCK_EVERY_MS = Math.max(5000, Number(cfg.lockEveryMs) || 20000); } async function fetchClientConfig() { try { const data = await cloudJson("/api/gsadjy/client-config", "GET", null); applyClientConfig(data); } catch (_) { const cached = loadJson(CLOUD_CLIENT_CFG_KEY, null); if (cached) applyClientConfig(cached); } } async function fetchPanelNotice() { try { const res = await cloudHttp("/api/gsadjy/panel-notice", "GET", null); if (res.status < 400 && res.text) { state.panelNotice = String(res.text).trim(); const el = document.getElementById("gs-ann-text"); if (el && state.panelNotice) el.textContent = state.panelNotice; } } catch (_) {} } async function _cl(force) { if (!force && state.cloudLease) return state.cloudLease; const cached = loadJson(CLOUD_LEASE_CACHE_KEY, null); if (!force && cached && cached.lease && cached.exp && Date.now() / 1000 < cached.exp - 30) { state.cloudLease = cached.lease; state.cloudTier = cached.tier || "free"; state.freeCourseLimit = Number(cached.free_course_limit) || state.freeCourseLimit; state.freeUsedCourses = Number(cached.free_used_courses) || 0; state.cloudProExpireAt = Number(cached.pro_expires_at) || 0; state.canExam = cached.can_exam === true || state.cloudTier === "pro"; return state.cloudLease; } const data = await cloudJson("/api/gsadjy/lease", "POST", { learning_user_id: resolveLearningUserId(), name: state.userName || "", userName: state.userName || "", }); state.cloudLease = data.lease || ""; state.cloudTier = String(data.tier || "free"); state.freeCourseLimit = Number(data.free_course_limit) || state.freeCourseLimit; state.freeUsedCourses = Number(data.free_used_courses) || 0; state.cloudProExpireAt = Number(data.pro_expires_at) || 0; state.canExam = data.can_exam === true || state.cloudTier === "pro"; saveJson(CLOUD_LEASE_CACHE_KEY, { lease: state.cloudLease, exp: data.exp, tier: state.cloudTier, free_course_limit: state.freeCourseLimit, free_used_courses: state.freeUsedCourses, pro_expires_at: state.cloudProExpireAt, can_exam: state.canExam, }); return state.cloudLease; } async function consumeFreeCourse(course) { await _cl(false); if (state.cloudTier === "pro") return true; const data = await cloudJson("/api/gsadjy/course/consume", "POST", { lease: state.cloudLease, course_id: course.trainCourseId || course.id, }); if (data.free_used_courses != null) state.freeUsedCourses = Number(data.free_used_courses); if (data.free_course_limit != null) state.freeCourseLimit = Number(data.free_course_limit); return true; } async function saveCloudToken(token) { state.cloudToken = String(token || "").trim(); if (state.cloudToken) localStorage.setItem(CLOUD_TOKEN_KEY, state.cloudToken); else localStorage.removeItem(CLOUD_TOKEN_KEY); localStorage.removeItem(CLOUD_LEASE_CACHE_KEY); state.cloudLease = ""; if (state.cloudToken) { await cloudJson("/api/license/verify", "GET", null); } await _cl(true); updatePanel(); } function sha1HexUpper(str) { function rotl(n, s) { return (n << s) | (n >>> (32 - s)); } function toUtf8(s) { const out = []; for (let i = 0; i < s.length; i++) { const a = s.charCodeAt(i); if (a >= 1 && a <= 127) out.push(a); else if (a > 2047) { out.push(0xe0 | ((a >> 12) & 15), 0x80 | ((a >> 6) & 63), 0x80 | (a & 63)); } else { out.push(0xc0 | ((a >> 6) & 31), 0x80 | (a & 63)); } } return out; } const msg = toUtf8(String(str)); const l = msg.length; const nBlocks = (((l + 8) >>> 6) + 1) * 16; const arr = new Array(nBlocks).fill(0); for (let i = 0; i < l; i++) arr[i >> 2] |= msg[i] << (24 - (i % 4) * 8); arr[l >> 2] |= 0x80 << (24 - (l % 4) * 8); arr[nBlocks - 1] = l * 8; let h0 = 1732584193, h1 = -271733879, h2 = -1732584194, h3 = 271733878, h4 = -1009589776; const w = new Array(80); for (let i = 0; i < nBlocks; i += 16) { for (let t = 0; t < 80; t++) { if (t < 16) w[t] = arr[i + t] | 0; else w[t] = rotl(w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16], 1); } let a = h0, b = h1, c = h2, d = h3, e = h4; for (let t = 0; t < 80; t++) { const f = t < 20 ? (b & c) | (~b & d) : t < 40 ? b ^ c ^ d : t < 60 ? (b & c) | (b & d) | (c & d) : b ^ c ^ d; const k = t < 20 ? 1518500249 : t < 40 ? 1859775393 : t < 60 ? -1894007588 : -899497514; const temp = (rotl(a, 5) + f + e + k + w[t]) | 0; e = d; d = c; c = rotl(b, 30); b = a; a = temp; } h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0; h4 = (h4 + e) | 0; } const hex = (n) => ("00000000" + (n >>> 0).toString(16)).slice(-8); return (hex(h0) + hex(h1) + hex(h2) + hex(h3) + hex(h4)).toUpperCase(); } function makeSign(params) { const keys = Object.keys(params).sort(); let o = SIGN_SALT; for (const k of keys) { const v = params[k]; o += k + (v == null ? "" : String(v)); } o += SIGN_SALT; return sha1HexUpper(o); } function toForm(obj) { return Object.keys(obj) .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k] == null ? "" : obj[k])}`) .join("&"); } async function http(method, path, body, opts) { opts = opts || {}; const url = path.startsWith("http") ? path : ORIGIN + path; const headers = Object.assign( { Accept: opts.accept || "application/json, text/plain, */*" }, opts.headers || {} ); if (!opts.asDocument) { headers["X-Requested-With"] = headers["X-Requested-With"] || "XMLHttpRequest"; } if (body != null && !headers["Content-Type"]) { headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8"; } if (typeof GM_xmlhttpRequest === "function") { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method, url, headers, data: body == null ? undefined : body, timeout: 60000, anonymous: false, withCredentials: true, onload: (res) => resolve({ status: res.status, text: res.responseText || "", finalUrl: res.finalUrl || url, }), onerror: () => reject(new Error("\u7f51\u7edc\u9519\u8bef")), ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6")), }); }); } const res = await fetch(url, { method, credentials: "include", headers, body: body == null ? undefined : body, redirect: "follow", }); return { status: res.status, text: await res.text(), finalUrl: res.url }; } async function apiSigned(path, fields) { const params = Object.assign({}, fields || {}, { format: "json", timestamp: String(Date.now()), }); params.sign = makeSign(params); const res = await http("POST", path, toForm(params), { headers: { CeduAjax: "1" }, }); let json; try { json = JSON.parse(res.text); } catch (_) { throw new Error("\u63a5\u53e3\u54cd\u5e94\u975e JSON"); } const code = String(json?.result?.code ?? json?.code ?? ""); if (code && code !== "1") { throw new Error(json?.result?.message || json?.message || "\u63a5\u53e3\u5931\u8d25"); } return json?.result?.data ?? json?.data ?? json; } async function checkLogin() { if (state.courses.length || state.trains.length) return true; try { const year = String(new Date().getFullYear()); const data = await apiSigned(`${BASE}/studyspace/course.api`, { studyState: "1", timeValue: year, searchValue: "", }); if (data && typeof data === "object") { const el = document.querySelector(".userName, #realName, #userName"); if (el) state.userName = String(el.textContent || el.value || "").trim() || state.userName; return true; } } catch (_) {} try { const res = await http("POST", `${BASE}/login/ajaxCheckLoginInfo.do`, "", { headers: { CeduAjax: "1" }, accept: "*/*", }); const t = String(res.text || "").trim(); if (/^login\.process$/i.test(t) || /"login"\s*:\s*true/i.test(t)) return true; try { const j = JSON.parse(t); if (j && (j.realName || j.userName || j.name)) { state.userName = j.realName || j.userName || j.name || ""; return true; } if (j?.login === false) return false; } catch (_) {} if (/\u672a\u767b\u5f55|login\.do|no\s*login/i.test(t)) return false; return t.length > 0 && !/login\.html/i.test(t); } catch (_) { return false; } } async function fetchTrains() { const year = String(new Date().getFullYear()); const data = await apiSigned(`${BASE}/studyspace/course.api`, { studyState: "1", timeValue: year, searchValue: "", }); const list = [] .concat(data?.publicList || []) .concat(data?.selectedClassHourList || []) .concat(data?.usableClassHourList || []); state.trains = list.map((t) => ({ userTrainId: t.id, trainId: t.trainId, name: t.name || t.trainId, process: t.process || "0%", classHour: t.classHour, finished: Number(t.finished) === 1, studyState: t.studyState, })); return state.trains; } function parseTrainCourses(html, train) { const out = []; const byKey = new Map(); const userCourseByTrainCourse = Object.create(null); const reUc = /showMoreRecordInfo\(\s*'([0-9a-f-]{36})'\s*,\s*'([0-9a-f-]{36})'\s*\)/gi; let m; while ((m = reUc.exec(html))) { userCourseByTrainCourse[m[2]] = m[1]; } const reStudy = /study\(\s*'([0-9a-f-]{36})'\s*,\s*'([0-9a-f-]{36})'\s*,\s*'(\d+)'\s*\)/gi; while ((m = reStudy.exec(html))) { const trainCourseId = m[1]; const courseVersionId = m[2]; const key = trainCourseId + "|" + courseVersionId; const tagStart = html.lastIndexOf("", m.index); const tag = tagStart >= 0 && tagEnd > tagStart ? html.slice(tagStart, tagEnd + 1) : ""; const titleM = tag.match(/\btitle\s*=\s*["']([^"']{2,200})["']/i); const title = titleM ? String(titleM[1] || "").trim() : ""; const around = html.slice(Math.max(0, m.index - 200), m.index + 1400); const hourM = around.match(/\u5b66\u65f6\s*]*>\s*([\d.]+)\s*<\/em>/i); const pctM = around.match(/width\s*:\s*(\d+)\s*%[\s\S]{0,200}?(\d+)\s*%/); const pctNear = around.match(/(?:\u89c6\u9891\u8fdb\u5ea6|\u5b66\u4e60\u8fdb\u5ea6)[\s\S]{0,280}?(\d+)\s*%/); const pct = Math.max( pctM ? Number(pctM[2] || pctM[1] || 0) : 0, pctNear ? Number(pctNear[1] || 0) : 0 ); const started = /\u7ee7\u7eed\u5b66\u4e60/.test(around); const finishedText = /\u5df2\u5b8c\u6210|\u5df2\u5b66\u5b8c|\u5b66\u4e60\u5b8c\u6bd5/.test(around); const prev = byKey.get(key); if (!prev) { byKey.set(key, { id: key, trainId: train.trainId, userTrainId: train.userTrainId, trainName: train.name, trainCourseId, courseVersionId, userCourseId: userCourseByTrainCourse[trainCourseId] || "", title: title || trainCourseId.slice(0, 8), classHour: hourM ? Number(hourM[1]) : 0, progress: pct, done: pct >= 100 || finishedText, started, }); continue; } if (title && (prev.title === trainCourseId.slice(0, 8) || title.length > prev.title.length)) { prev.title = title; } if (!prev.classHour && hourM) prev.classHour = Number(hourM[1]); if (pct > prev.progress) prev.progress = pct; if (started) prev.started = true; if (finishedText || pct >= 100) prev.done = true; if (!prev.userCourseId && userCourseByTrainCourse[trainCourseId]) { prev.userCourseId = userCourseByTrainCourse[trainCourseId]; } } for (const row of byKey.values()) { if (state.doneIds.has(row.id) || row.progress >= 100) row.done = true; if (row.done) rememberDone(row.id); out.push(row); } return out; } async function fetchAllCourses() { const trains = await fetchTrains(); const all = []; for (const tr of trains) { if (!tr.trainId || !tr.userTrainId) continue; const url = `${BASE}/studentspace/study/listTrainCourse.do?trainId=${encodeURIComponent( tr.trainId )}&userTrainId=${encodeURIComponent(tr.userTrainId)}`; const res = await http("GET", url, null, { accept: "text/html,*/*", asDocument: true, }); const list = parseTrainCourses(res.text || "", tr); all.push(...list); } state.courses = all; return all; } function parseHidden(html) { const out = {}; const re = /<(?:input|INPUT)[^>]*\bid=["']([^"']+)["'][^>]*\bvalue=["']([^"']*)["'][^>]*>/gi; let m; while ((m = re.exec(html))) out[m[1]] = m[2]; const re2 = /<(?:input|INPUT)[^>]*\bvalue=["']([^"']*)["'][^>]*\bid=["']([^"']+)["'][^>]*>/gi; while ((m = re2.exec(html))) if (out[m[2]] == null) out[m[2]] = m[1]; const pick = (name) => { if (out[name]) return; const r1 = new RegExp(`\\bid=["']${name}["'][^>]*\\bvalue=["']([^"']*)["']`, "i"); const r2 = new RegExp(`\\bvalue=["']([^"']*)["'][^>]*\\bid=["']${name}["']`, "i"); const mm = html.match(r1) || html.match(r2); if (mm) out[name] = mm[1]; }; [ "userCourseId", "trainId", "trainCourseId", "courseVersionId", "courseActivityId", "id", "videoPosition", "token", "type", "courseActivityProcessOpen", "courseTopicCode", "arrangementId", "answerPaperRecordId", "studentTestActivityScoreId", "userExamId", "deviceId", ].forEach(pick); return out; } function parseBeginStudyVideos(html) { const out = []; const byId = new Map(); const re = /beginStudy\(\s*'([^']+)'\s*,\s*'1'\s*,\s*'([0-9a-f-]{36})'\s*\)/gi; let m; while ((m = re.exec(html))) { const activityId = m[2]; const around = html.slice(Math.max(0, m.index - 600), m.index + 80); const lenM = around.match(/]*>\s*(\d{1,2}):(\d{2})\s*<\/em>\s*\/\s*(\d{1,2}):(\d{2})/); const duration = lenM ? Number(lenM[3]) * 60 + Number(lenM[4]) : 0; const titleM = around.match(/activityNameSpan[^>]*title="([^"]+)"/); const title = (titleM && titleM[1]) || ""; const exist = byId.get(activityId); if (exist) { if (!exist.duration && duration) exist.duration = duration; if (!exist.title && title) exist.title = title; continue; } const row = { topicCode: m[1], activityId, duration, title }; byId.set(activityId, row); out.push(row); } return out; } async function openVideoPage(course, baseFields, act) { const qs = "userCourseId=" + encodeURIComponent(baseFields.userCourseId) + "&trainId=" + encodeURIComponent(baseFields.trainId || course.trainId || "") + "&trainCourseId=" + encodeURIComponent(baseFields.trainCourseId || course.trainCourseId || "") + "&courseVersionId=" + encodeURIComponent(baseFields.courseVersionId || course.courseVersionId || "") + "&courseTopicCode=" + encodeURIComponent(act.topicCode || "001") + "&courseActivityId=" + encodeURIComponent(act.activityId); const res = await http("GET", `${BASE}/study/activity/video/study.do?${qs}`, null, { accept: "text/html,*/*", asDocument: true, }); const fields = Object.assign({}, baseFields, parseHidden(res.text || "")); if (!fields.userCourseId || !fields.id || !fields.courseActivityId) { if (/\u8bf7\u5148\u767b\u5f55|login\.do|login\.html/i.test(res.text || "")) { throw new Error("\u65e0\u6cd5\u8fdb\u5165\u8bfe\u4ef6\uff08\u4f1a\u8bdd\u5931\u6548\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55\uff09"); } throw new Error("\u65e0\u6cd5\u8fdb\u5165\u8bfe\u4ef6\uff08\u7f3a\u89c6\u9891\u8d44\u6e90 id\uff09"); } const pos = Number(fields.videoPosition || fields.curVideoPosition || 0) || 0; let duration = Number(act.duration) || 0; if (duration < 60 && course.progress > 0 && pos > 0) { duration = Math.ceil((pos / course.progress) * 100); } if (duration < 60) { const perAct = Math.max( 1, Math.round((Number(course.classHour) || 1) / Math.max(1, course._videoCount || 1)) ); duration = Math.max(600, Math.round(perAct * 45 * 60)); } if (pos >= duration - 3) { duration = Math.max(duration, pos + 30); } return { fields, position: pos, duration, actTitle: act.title }; } async function openStudySession(course) { const entry = `${BASE}/study/index/index.do?trainCourseId=${encodeURIComponent( course.trainCourseId )}&courseVersionId=${encodeURIComponent(course.courseVersionId)}`; const res = await http("GET", entry, null, { accept: "text/html,*/*", asDocument: true, }); const html = res.text || ""; if (/\u8bf7\u5148\u767b\u5f55|login\.do|login\.html/i.test(html) && !/id=["']userCourseId["']/i.test(html)) { throw new Error("\u65e0\u6cd5\u8fdb\u5165\u8bfe\u4ef6\uff08\u4f1a\u8bdd\u5931\u6548\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55\uff09"); } const baseFields = parseHidden(html); if (!baseFields.userCourseId && course.userCourseId) { baseFields.userCourseId = course.userCourseId; } if (!baseFields.trainId && course.trainId) baseFields.trainId = course.trainId; if (!baseFields.trainCourseId) baseFields.trainCourseId = course.trainCourseId; if (!baseFields.courseVersionId) { baseFields.courseVersionId = course.courseVersionId; } if (!baseFields.userCourseId) { throw new Error("\u65e0\u6cd5\u8fdb\u5165\u8bfe\u4ef6\uff08\u76ee\u5f55\u9875\u65e0 userCourseId\uff09"); } const videos = parseBeginStudyVideos(html); if (!videos.length) { throw new Error("\u65e0\u6cd5\u8fdb\u5165\u8bfe\u4ef6\uff08\u65e0\u89c6\u9891\u6d3b\u52a8\uff0c\u8bf7\u786e\u8ba4\u5df2\u9009\u8bfe\uff09"); } course._videoCount = videos.length; return { baseFields, videos }; } async function postVideo(path, fields, extra) { const body = Object.assign( { type: fields.type || "2", courseActivityProcessOpen: fields.courseActivityProcessOpen || "2", id: fields.id, userCourseId: fields.userCourseId, trainId: fields.trainId, trainCourseId: fields.trainCourseId, courseVersionId: fields.courseVersionId, courseTopicCode: fields.courseTopicCode || "001", courseActivityId: fields.courseActivityId, token: fields.token || "00000000-0000-0000-0000-000000000000", rand: String(Math.random()), }, extra || {} ); const res = await http("POST", path, toForm(body), { headers: { CeduAjax: "1" }, accept: "*/*", }); const t = String(res.text || "").trim(); if (t !== "1" && t !== '"1"' && !/^1\b/.test(t)) { throw new Error(`\u5b66\u4e60\u540c\u6b65\u5931\u8d25\uff1a${t.slice(0, 80) || res.status}`); } return true; } async function studyOneCourse(course) { state.currentCourse = course.title; state.currentTask = "\u8fdb\u5165\u8bfe\u4ef6"; updatePanel(); log(`\u5f00\u59cb\uff1a${course.title}`); try { await consumeFreeCourse(course); } catch (e) { const msg = String(e.message || e); if (/free_quota|\u914d\u989d|403/i.test(msg)) { const tip = `\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${state.freeUsedCourses}/${state.freeCourseLimit} \u95e8\uff09\u3002\u8bf7\u5f00\u901a\u4e13\u4e1a\u7248\u540e\u7ee7\u7eed\u5b66\u4e60\u3002`; showProAlert(tip); throw new Error(tip); } throw e; } const sess = await openStudySession(course); state.chapterCache[course.id] = sess.videos || []; const efficiency = state.mode !== MODE_RT; const waitMs = efficiency ? EFF_WAIT_MS : RT_WAIT_MS; for (let i = 0; i < sess.videos.length; i++) { if (state.stopFlag) break; const act = sess.videos[i]; const label = act.title || `${course.title}·\u89c6\u9891${i + 1}/${sess.videos.length}`; state.currentTask = `\u8fdb\u5165 ${label}`; updatePanel(); const video = await openVideoPage(course, sess.baseFields, act); let position = video.position; const duration = video.duration; const fields = video.fields; log(`${label} \u5df2\u5b66 ${progressMinText(position, duration)}`); if (position >= duration - 3) { log(`${label} \u5df2\u5b66\u5b8c\uff0c\u8df3\u8fc7`); continue; } let lastLock = 0; let partsLeft = EFF_PARTS; let lastShownMin = -1; while (!state.stopFlag && position < duration - 3) { const now = Date.now(); if (now - lastLock >= LOCK_EVERY_MS) { await postVideo(`${BASE}/study/activity/video/refreshLock.do`, fields, {}); lastLock = now; } const remain = Math.max(1, duration - position); const chunk = efficiency ? Math.ceil(remain / Math.max(1, partsLeft)) : Math.min(RT_STEP, remain); if (efficiency) partsLeft = Math.max(1, partsLeft - 1); position = Math.min(duration, position + chunk); const prog = progressMinText(position, duration); state.currentTask = `\u5df2\u5b66 ${prog}`; updatePanel(); await postVideo(`${BASE}/study/activity/video/record.do`, fields, { position: String(Math.floor(position)), length: String(Math.floor(chunk)), }); const shownMin = secToMin(position); if (shownMin !== lastShownMin || position >= duration - 3) { log(`${label} \u5df2\u5b66 ${prog}`); lastShownMin = shownMin; } if (position >= duration - 3) break; await sleep(waitMs); } } if (!state.stopFlag) { course.progress = 100; course.done = true; rememberDone(course.id); state.queue = state.queue.filter((id) => id !== course.id); saveJson(KEY_QUEUE, state.queue); log(`\u5b8c\u6210\uff1a${course.title}`); } } async function runQueue() { if (state.busy) return; state.busy = true; state.stopFlag = false; updatePanel(); try { if (!(await checkLogin())) { log("\u8bf7\u5148\u767b\u5f55\u5e73\u53f0"); return; } try { await _cl(true); } catch (e) { log("\u6388\u6743\u6821\u9a8c\u5931\u8d25\uff1a" + (e.message || e)); return; } if (!state.courses.length) await fetchAllCourses(); let q = state.queue .map((id) => state.courses.find((c) => c.id === id)) .filter(Boolean) .filter((c) => !c.done); if (!q.length) { log("\u8bf7\u5148\u52fe\u9009\u672a\u5b8c\u6210\u8bfe\u7a0b"); return; } if (state.cloudTier !== "pro" && q.length > state.freeCourseLimit) { log(`\u514d\u8d39\u7248\u6bcf\u6b21\u6700\u591a\u5b66\u4e60 ${state.freeCourseLimit} \u95e8\uff0c\u5df2\u81ea\u52a8\u622a\u53d6\u961f\u5217`); q = q.slice(0, state.freeCourseLimit); } for (const c of q) { if (state.stopFlag) break; try { await studyOneCourse(c); } catch (e) { const em = String(e.message || e); if (/\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c|\u8bf7\u5f00\u901a\u4e13\u4e1a\u7248/.test(em)) log(em, "warn"); else log(`\u5931\u8d25\uff1a${c.title} · ${em}`); } renderCourseList(); renderChapterPreview(); } log(state.stopFlag ? "\u5df2\u505c\u6b62" : "\u961f\u5217\u8dd1\u5b8c"); } catch (e) { log("\u8fd0\u884c\u5931\u8d25\uff1a" + (e.message || e)); } finally { state.busy = false; state.currentTask = "\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u5b66\u4e60"; updatePanel(); } } function parseExamActivityList(html, train, activityPageUrl) { const doc = new DOMParser().parseFromString(html, "text/html"); const hidden = {}; doc.querySelectorAll("input[type=hidden]").forEach((el) => { const id = el.getAttribute("id") || el.getAttribute("name"); if (id) hidden[id] = String(el.value || "").trim(); }); const trainId = hidden.trainId || train.trainId || ""; const trainCourseId = hidden.trainCourseId || ""; const courseVersionId = hidden.courseVersionId || ""; const userCourseId = hidden.userCourseId || ""; const out = []; doc.querySelectorAll("a[onclick*='beginStudy']").forEach((a) => { const onclick = a.getAttribute("onclick") || ""; const m = onclick.match(/beginStudy\(\s*'([^']+)'\s*,\s*'([0-9a-f-]{36})'\s*\)/i); if (!m) return; const dd = a.closest("dd") || a.closest("dl") || doc.body; const ddText = dd ? String(dd.textContent || "") : ""; const submitM = ddText.match(/\u63d0\u4ea4\u6b21\u6570[\uff1a:]\s*(\d+)\s*\/\s*(\d+)/); const doneM = ddText.match(/\u901a\u8fc7\u6570\s*(\d+)\s*\u4efd\s*\/\s*(\d+)\s*\u4efd/); out.push({ trainId, trainName: train.name || "", userTrainId: train.userTrainId || "", trainCourseId, courseVersionId, userCourseId, courseTopicCode: m[1], courseActivityId: m[2], submitted: submitM ? Number(submitM[1]) : 0, maxSubmit: submitM ? Number(submitM[2]) : 0, passed: doneM ? Number(doneM[1]) : 0, total: doneM ? Number(doneM[2]) : 1, pageUrl: activityPageUrl, }); }); return out; } async function fetchAllExamActivities() { const trains = await fetchTrains(); const all = []; for (const tr of trains) { if (!tr.trainId) continue; const url = `${BASE}/studentspace/study/listTrainActivity.do?trainId=${encodeURIComponent( tr.trainId )}&activityType=5`; try { const res = await http("GET", url, null, { accept: "text/html,*/*", asDocument: true, }); const list = parseExamActivityList(res.text || "", tr, url); all.push(...list); log(`\u57f9\u8bad\u73ed\u300c${tr.name}\u300d\u8003\u8bd5\u6d3b\u52a8 ${list.length} \u4e2a`); } catch (e) { log(`\u62c9\u53d6\u57f9\u8bad\u73ed\u300c${tr.name}\u300d\u8003\u8bd5\u6d3b\u52a8\u5931\u8d25\uff1a${e.message || e}`); } } state.examActivities = all; return all; } function parseExamContextFromHtml(html) { const doc = new DOMParser().parseFromString(html, "text/html"); const pick = (id) => { const el = doc.getElementById(id); return el ? String(el.value || "").trim() : ""; }; const questions = []; doc.querySelectorAll(".exercise[id^='questionTag']").forEach((ex) => { const tag = ex.querySelector("[questionId]"); const questionId = tag ? String(tag.getAttribute("questionId") || "").trim() : ""; if (!questionId) return; const p = ex.querySelector(".titleArea p"); const questionText = p ? String(p.textContent || "").trim() : ""; const options = []; ex.querySelectorAll(`input[name="answer_${questionId}"]`).forEach((inp) => { const li = inp.closest("li"); const label = li ? li.querySelector(".hand span") || li.querySelector("label") : null; options.push({ value: String(inp.value || "").trim(), text: label ? String(label.textContent || "").trim() : "", }); }); questions.push({ questionId, questionText, options }); }); return { arrangementId: pick("arrangementId"), answerPaperRecordId: pick("answerPaperRecordId"), studentTestActivityScoreId: pick("studentTestActivityScoreId"), userExamId: pick("userExamId"), questions, }; } function findExamRule(questionText) { const t = String(questionText || ""); return EXAM_ANSWER_BANK.find((r) => t.includes(r.match)) || null; } async function runOneExamFromApi(act) { const studyQs = "userCourseId=" + encodeURIComponent(act.userCourseId) + "&trainId=" + encodeURIComponent(act.trainId) + "&trainCourseId=" + encodeURIComponent(act.trainCourseId) + "&courseVersionId=" + encodeURIComponent(act.courseVersionId) + "&courseTopicCode=" + encodeURIComponent(act.courseTopicCode || "001") + "&courseActivityId=" + encodeURIComponent(act.courseActivityId); const studyRes = await http("GET", `${BASE}/study/activity/exam/study.do?${studyQs}`, null, { accept: "text/html,*/*", asDocument: true, }); const studyFields = parseHidden(studyRes.text || ""); const dvcId = studyFields.deviceId || Math.random().toString(16).slice(2) + Date.now().toString(16); const otsQs = "userCourseId=" + encodeURIComponent(act.userCourseId) + "&trainId=" + encodeURIComponent(act.trainId) + "&trainCourseId=" + encodeURIComponent(act.trainCourseId) + "&courseVersionId=" + encodeURIComponent(act.courseVersionId) + "&courseTopicCode=" + encodeURIComponent(act.courseTopicCode || "001") + "&courseActivityId=" + encodeURIComponent(act.courseActivityId) + "&dvcId=" + encodeURIComponent(dvcId); const otsRes = await http("GET", `${BASE}/study/activity/exam/studyOts.do?${otsQs}`, null, { accept: "text/html,*/*", asDocument: true, }); const ctx = parseExamContextFromHtml(otsRes.text || ""); if (!ctx.answerPaperRecordId || !ctx.arrangementId || !ctx.studentTestActivityScoreId) { throw new Error("\u65e0\u6cd5\u89e3\u6790\u8003\u8bd5\u4e0a\u4e0b\u6587\uff08\u53ef\u80fd\u65e0\u8003\u8bd5\u673a\u4f1a/\u5df2\u901a\u8fc7/\u672a\u767b\u5f55\uff09"); } if (!ctx.questions.length) { throw new Error("\u672a\u89e3\u6790\u5230\u9898\u76ee"); } const answerList = ctx.questions.map((q) => { let value = ""; if (EXAM_ANSWER_BY_QID[q.questionId]) { value = EXAM_ANSWER_BY_QID[q.questionId]; } else { const rule = findExamRule(q.questionText); if (rule && rule.answerText) { const hit = q.options.find( (o) => o.text.includes(rule.answerText) || rule.answerText.includes(o.text) ); if (hit) value = hit.value; } if (!value && rule && rule.fallbackValue) { const hit = q.options.find((o) => o.value === rule.fallbackValue); if (hit) value = hit.value; } } if (!value && q.options.length) value = q.options[0].value; return { questionId: q.questionId, content: JSON.stringify({ id: value }), }; }); log(`\u672c\u5377 ${answerList.length} \u9898\u5df2\u4f5c\u7b54\uff0c\u63d0\u4ea4\u4e2d…`); const paperAnswerResultJson = JSON.stringify({ answerPaperRecordId: ctx.answerPaperRecordId, questionAnswerList: answerList, }); const body = toForm({ arrangementId: ctx.arrangementId, answerPaperRecordId: ctx.answerPaperRecordId, studentTestActivityScoreId: ctx.studentTestActivityScoreId, paperAnswerResultJson, resourcePackageId: ctx.userExamId, temp: "0", rand: String(Math.random()), }); const res = await http("POST", `${BASE}/study/activity/exam/submitExam.do`, body, { headers: { CeduAjax: "1" }, accept: "*/*", }); const text = String(res.text || "").trim(); if (text === "1" || text.includes('"1"') || text.includes("\u6210\u529f")) { return { ok: true, text }; } return { ok: false, text }; } async function runAllExamsFromHome() { if (state.examBusy) return; state.examBusy = true; updatePanel(); try { log("\u5f00\u59cb\u4e00\u952e\u8003\u8bd5"); if (!(await checkLogin())) { log("\u8bf7\u5148\u767b\u5f55\u5e73\u53f0"); return; } const activities = await fetchAllExamActivities(); if (!activities.length) { log("\u6ca1\u6709\u627e\u5230\u8003\u8bd5\u6d3b\u52a8"); return; } const todo = activities.filter((a) => { if (!a.total) return true; return a.passed < a.total; }); log(`\u5171 ${activities.length} \u4e2a\u8003\u8bd5\uff0c\u5f85\u8dd1 ${todo.length} \u4e2a`); let okCount = 0; let failCount = 0; for (let i = 0; i < todo.length; i++) { const act = todo[i]; const label = `${act.trainName || "\u57f9\u8bad\u73ed"} · ${act.courseTopicCode} · ${act.courseActivityId.slice(0, 8)}`; log(`[${i + 1}/${todo.length}] ${label}`); try { const r = await runOneExamFromApi(act); if (r.ok) { okCount++; log(` ✅ \u4ea4\u5377\u6210\u529f`); } else { failCount++; log(` ⚠️ \u4ea4\u5377\u8fd4\u56de\uff1a${r.text.slice(0, 120)}`); } } catch (e) { failCount++; log(` ❌ \u5931\u8d25\uff1a${e.message || e}`); } await sleep(800); } log(`\u4e00\u952e\u8003\u8bd5\u5b8c\u6210\uff1a\u6210\u529f ${okCount}\uff0c\u5931\u8d25 ${failCount}`); } catch (e) { log("\u4e00\u952e\u8003\u8bd5\u5f02\u5e38\uff1a" + (e.message || e)); } finally { state.examBusy = false; updatePanel(); } } function injectStyles() { if (document.getElementById("gs-panel-style")) return; const st = document.createElement("style"); st.id = "gs-panel-style"; st.textContent = ` #gs-auto-panel{position:fixed;right:20px;top:80px;z-index:2147483000;width:412px;display:flex;flex-direction:column;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#0f172a;overflow:hidden;max-height:min(92vh,780px)} #gs-auto-panel.gs-collapsed{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14)} #gs-auto-panel.gs-collapsed #gs-panel-body,#gs-auto-panel.gs-collapsed .gs-footer-extra,#gs-auto-panel.gs-collapsed #gs-panel-footer{display:none} #gs-panel-header{flex:0 0 auto;padding:8px 11px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none} #gs-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1} #gs-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:contain;box-shadow:0 2px 9px rgba(15,23,42,.11);border:1px solid rgba(148,163,184,.45);background:#fff;flex:0 0 auto;padding:2px;box-sizing:border-box} #gs-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;display:flex;align-items:flex-start;gap:5px;flex-wrap:wrap} .gs-ver{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0} #gs-panel-sub{display:flex;flex-wrap:wrap;align-items:center;gap:3px 5px;margin-top:3px} .gs-chip{font-size:11px;color:#7c2d12;background:rgba(255,255,255,.62);padding:2px 7px;border-radius:999px;border:1px solid rgba(180,83,9,.18);font-weight:700} .gs-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28)} #gs-panel-controls{display:flex;gap:5px} #gs-panel-controls button{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1);font-weight:900} #gs-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px} .gs-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px} .gs-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;line-height:1.28} .gs-status-label{font-size:11px;color:#64748b;font-weight:700} #gs-status{padding:2px 7px;border-radius:999px;font-weight:900;font-size:11px;background:#fff;border:1px solid #cbd5e1} .gs-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;margin-top:3px} .gs-metrics em{font-style:normal;font-weight:900;color:#0f172a} .gs-pct{font-size:14px;font-weight:900;color:#0369a1} .gs-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px} .gs-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb)} .gs-tabbar{display:flex;gap:6px;margin-bottom:7px} .gs-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px} .gs-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent} .gs-pane{display:none}.gs-pane.active{display:block} .gs-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;gap:6px} .gs-list-title{font-size:12px;color:#64748b;font-weight:700} .gs-mini{border:1px solid #cbd5e1;background:#fff;color:#64748b;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;cursor:pointer} #gs-course-list,#gs-run-log,#gs-chapter-list{max-height:220px;overflow-y:auto;background:#f1f5f9;border:1px solid #dbe4f0;border-radius:11px;padding:6px} .gs-ch-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;overflow:hidden} .gs-ch-course:last-child{margin-bottom:0} .gs-ch-title{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700} .gs-ch-item{padding:5px 9px;font-size:11px;display:flex;justify-content:space-between;gap:7px;border-bottom:1px dashed #eef2f7;color:#0f172a} .gs-ch-item:last-child{border-bottom:none} .gs-ch-meta{flex:0 0 auto;color:#64748b;font-size:10px} .gs-course-item{display:flex;gap:8px;align-items:flex-start;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:8px;margin-bottom:6px;cursor:pointer} .gs-course-item:last-child{margin-bottom:0} .gs-course-item.is-checked{border-color:#86efac;background:#f0fdf4} .gs-course-item.is-done{opacity:.72;background:#e5e7eb;border-color:#cbd5e1;cursor:not-allowed} .gs-course-item.is-done .gs-name,.gs-course-item.is-done .gs-course-meta{color:#94a3b8} .gs-course-item.is-done input{pointer-events:none} .gs-log-warn{background:#fff7ed;color:#9a3412;font-weight:800;border:1px solid #fdba74;border-radius:8px;padding:8px;margin:4px 0} .gs-pro-alert{display:none;margin-bottom:8px;padding:8px 10px;border-radius:10px;background:#fff1f2;border:1px solid #fb7185;color:#9f1239;font-size:12px;font-weight:800;line-height:1.45} .gs-pro-alert a{color:#be123c;margin-left:4px} #gs-auto-panel .gs-course-item input[type="checkbox"]{ margin:3px 0 0 !important;padding:0 !important;accent-color:#2563eb; width:16px !important;height:16px !important;min-width:16px !important;min-height:16px !important; flex:0 0 auto !important;opacity:1 !important;visibility:visible !important; display:inline-block !important;appearance:auto !important;-webkit-appearance:checkbox !important; position:relative !important;z-index:2 !important;cursor:pointer !important; clip:auto !important;transform:none !important;pointer-events:auto !important; } .gs-course-item .gs-course-main{flex:1;min-width:0;cursor:pointer} .gs-course-item span.gs-name{font-size:12px;color:#0f172a;font-weight:700} .gs-course-meta{display:block;font-size:11px;color:#64748b;margin-top:3px} .gs-cbar{display:block;height:4px;background:#e2e8f0;border-radius:2px;margin-top:6px;overflow:hidden} .gs-btn-row{display:flex;gap:7px} .gs-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px} .gs-btn:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed} .gs-btn-start{background:#16a34a}.gs-btn-stop{background:#ef4444} .gs-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a} .gs-btn-exam{background:#7c3aed} .gs-mode-row{display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap} .gs-mode-opt{flex:1;min-width:140px;display:flex;align-items:center;gap:6px;font-size:12px;font-weight:700;color:#334155;background:#f8fafc;border:1px solid #e2e8f0;border-radius:9px;padding:7px 9px;cursor:pointer} .gs-mode-opt.is-on{border-color:#93c5fd;background:#eff6ff;color:#1e3a8a} .gs-mode-opt input{accent-color:#2563eb;margin:0} .gs-pro-banner{margin:0 0 8px;padding:9px 10px;border-radius:11px;background:linear-gradient(135deg,#fff7ed,#fef3c7);border:1px solid #fdba74} .gs-pro-title{font-size:12px;font-weight:900;color:#9a3412;margin-bottom:3px} .gs-pro-desc{font-size:11px;color:#78350f;line-height:1.38;margin-bottom:7px} .gs-pro-buy{display:inline-block;text-decoration:none;background:linear-gradient(135deg,#ea580c,#f59e0b);color:#fff;padding:6px 12px;border-radius:999px;font-size:11px;font-weight:800} .gs-meta-row{display:flex;justify-content:space-between;gap:7px;align-items:center;font-size:12px;margin-bottom:6px} .gs-meta-label{color:#64748b;flex:0 0 auto} .gs-meta-value{font-weight:700;text-align:right} .gs-token-input{flex:1;min-width:0;border:1px solid #cbd5e1;border-radius:8px;padding:5px 8px;font-size:12px;background:#fff;color:#0f172a} .gs-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;user-select:text} .gs-empty{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900} .gs-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0} .gs-ann{position:relative;font-size:12px;color:#334155;line-height:1.42;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:11px;padding:8px 9px 8px 11px} .gs-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-radius:11px 0 0 11px} #gs-panel-footer{padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px} `; document.head.appendChild(st); } function createPanel() { if (document.getElementById("gs-auto-panel")) return; injectStyles(); const panel = document.createElement("div"); panel.id = "gs-auto-panel"; panel.innerHTML = `
${SCRIPT_NAME}v${SCRIPT_VERSION}
\u52fe\u9009\u8bfe\u7a0b\u81ea\u52a8\u5b66\u4e60\u6548\u7387\u6a21\u5f0f
\u8fd0\u884c\u72b6\u6001\u5df2\u505c\u6b62
\u5df2\u9009 0 \u95e8 · \u65e0
0%
\u5f53\u524d\u4efb\u52a1\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u5b66\u4e60
\u52fe\u9009\u5f85\u5b66\u8bfe\u7a0b
\u767b\u5f55\u540e\u70b9\u5237\u65b0\u52a0\u8f7d\u8bfe\u7a0b
\u7ae0\u8282 / \u8bfe\u4ef6
\u52fe\u9009\u8bfe\u7a0b\u540e\u67e5\u770b\u7ae0\u8282
\u5347\u7ea7\u4e13\u4e1a\u7248
\u514d\u8d39\u4f53\u9a8c 1 \u95e8\u8bfe\u3002\u4e13\u4e1a\u7248\u6fc0\u6d3b\u540e 30 \u5929\u6709\u6548\uff0c\u4e0d\u9650\u8bfe\u7a0b\u3002
\u7acb\u5373\u8d2d\u4e70\u4e13\u4e1a\u7248
\u4e13\u4e1a\u7248
\u4f1a\u5458\u72b6\u6001\u52a0\u8f7d\u4e2d…
\u6fc0\u6d3b\u7801
\u5b66\u4e60\u6a21\u5f0f
`; document.documentElement.appendChild(panel); const saved = loadJson(KEY_POS, null); if (saved && saved.left != null) { panel.style.right = "auto"; panel.style.left = saved.left + "px"; panel.style.top = (saved.top || 72) + "px"; } if (localStorage.getItem(KEY_COLLAPSED) === "1") panel.classList.add("gs-collapsed"); enableDrag(panel); panel.querySelector("#gs-btn-min")?.addEventListener("click", (e) => { e.stopPropagation(); panel.classList.toggle("gs-collapsed"); localStorage.setItem(KEY_COLLAPSED, panel.classList.contains("gs-collapsed") ? "1" : "0"); }); panel.querySelectorAll(".gs-tab-btn").forEach((btn) => { btn.addEventListener("click", () => { panel.querySelectorAll(".gs-tab-btn").forEach((b) => b.classList.toggle("active", b === btn)); panel.querySelectorAll(".gs-pane").forEach((p) => p.classList.toggle("active", p.dataset.pane === btn.dataset.tab) ); if (btn.dataset.tab === "chapters") void renderChapterPreview(); }); }); panel.querySelectorAll('input[name="gs-mode"]').forEach((inp) => { inp.addEventListener("change", () => { if (!inp.checked) return; state.mode = inp.value === MODE_RT ? MODE_RT : MODE_EFF; localStorage.setItem(KEY_MODE, state.mode); updatePanel(); }); }); const tokInp = document.getElementById("gs-cloud-token"); if (tokInp) tokInp.value = state.cloudToken || ""; document.getElementById("gs-save-token")?.addEventListener("click", async () => { const tok = String(tokInp?.value || "").trim(); try { await saveCloudToken(tok); log(tok ? "\u6fc0\u6d3b\u7801\u5df2\u4fdd\u5b58" : "\u5df2\u6e05\u9664\u6fc0\u6d3b\u7801"); } catch (e) { log("\u4fdd\u5b58\u5931\u8d25\uff1a" + (e.message || e)); updatePanel(); } }); document.getElementById("gs-clear-token")?.addEventListener("click", async () => { if (tokInp) tokInp.value = ""; state.cloudTier = "free"; state.canExam = false; state.cloudProExpireAt = 0; try { await saveCloudToken(""); log("\u5df2\u6e05\u9664\u6fc0\u6d3b\u7801"); } catch (e) { log("\u6e05\u9664\u5931\u8d25\uff1a" + (e.message || e)); updatePanel(); } }); document.getElementById("gs-refresh")?.addEventListener("click", () => void refreshCourses()); document.getElementById("gs-ch-refresh")?.addEventListener("click", () => void renderChapterPreview(true)); document.getElementById("gs-sel-none")?.addEventListener("click", () => { state.queue = []; saveJson(KEY_QUEUE, state.queue); renderCourseList(); updatePanel(); }); document.getElementById("gs-sel-undone")?.addEventListener("click", () => { state.queue = state.courses.filter((c) => !c.done).map((c) => c.id); saveJson(KEY_QUEUE, state.queue); renderCourseList(); updatePanel(); }); document.getElementById("gs-start")?.addEventListener("click", () => void runQueue()); document.getElementById("gs-stop")?.addEventListener("click", () => { state.stopFlag = true; log("\u6b63\u5728\u505c\u6b62…"); }); document.getElementById("gs-exam-all")?.addEventListener("click", () => void runAllExamsFromHome()); } function enableDrag(panel) { const header = panel.querySelector("#gs-panel-header"); if (!header) return; let sx = 0, sy = 0, ol = 0, ot = 0, dragging = false; header.addEventListener("mousedown", (e) => { if (e.target.closest("button")) return; dragging = true; const r = panel.getBoundingClientRect(); sx = e.clientX; sy = e.clientY; ol = r.left; ot = r.top; e.preventDefault(); }); window.addEventListener("mousemove", (e) => { if (!dragging) return; panel.style.right = "auto"; panel.style.left = ol + e.clientX - sx + "px"; panel.style.top = ot + e.clientY - sy + "px"; }); window.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; saveJson(KEY_POS, { left: parseInt(panel.style.left || "0", 10) || 0, top: parseInt(panel.style.top || "0", 10) || 0, }); }); } function fmtDur(sec) { const n = Number(sec) || 0; if (n < 60) return n ? n + "\u79d2" : ""; const m = Math.floor(n / 60); const s = n % 60; return s ? m + "\u5206" + s + "\u79d2" : m + "\u5206\u949f"; } async function renderChapterPreview(force) { const box = document.getElementById("gs-chapter-list"); if (!box) return; const picked = state.courses.filter((c) => state.queue.includes(c.id)); const list = picked.length ? picked : state.courses.slice(0, 8); if (!list.length) { box.innerHTML = `
\u6682\u65e0\u8bfe\u7a0b
`; return; } box.innerHTML = `
\u6b63\u5728\u8bfb\u53d6\u7ae0\u8282…
`; const blocks = []; for (const c of list) { if (force) c._chapters = null; if (!c._chapters) { try { const entry = `${BASE}/study/index/index.do?trainCourseId=${encodeURIComponent( c.trainCourseId )}&courseVersionId=${encodeURIComponent(c.courseVersionId)}`; const res = await http("GET", entry, null, { accept: "text/html,*/*", asDocument: true, }); const videos = parseBeginStudyVideos(res.text || ""); c._chapters = videos.length ? videos : [{ title: "\u672a\u8bfb\u5230\u8bfe\u4ef6", duration: 0, activityId: "" }]; } catch (e) { c._chapters = [{ title: "\u8bfb\u53d6\u5931\u8d25", duration: 0, activityId: "" }]; } } const items = c._chapters .map((v, i) => { const name = v.title || `\u8bfe\u4ef6 ${i + 1}`; const meta = fmtDur(v.duration); return `
${escapeHtml(name)}${escapeHtml( meta )}
`; }) .join(""); blocks.push( `
${escapeHtml(c.title)}
${items}
` ); } box.innerHTML = blocks.join(""); } function renderCourseList() { const box = document.getElementById("gs-course-list"); if (!box) return; if (!state.courses.length) { box.innerHTML = `
\u6682\u65e0\u8bfe\u7a0b\uff0c\u70b9\u5237\u65b0
`; return; } box.innerHTML = state.courses .map((c, idx) => { const on = !c.done && state.queue.includes(c.id); const checked = on ? "checked" : ""; const pct = Math.max(0, Math.min(100, Number(c.progress) || 0)); const cid = "gs-c-" + idx; return `
`; }) .join(""); box.querySelectorAll("input[type=checkbox]").forEach((inp) => { inp.addEventListener("change", () => { if (inp.disabled) return; const id = inp.getAttribute("data-id"); if (state.doneIds.has(id) || state.courses.some((c) => c.id === id && c.done)) { inp.checked = false; return; } const set = new Set(state.queue); if (inp.checked) set.add(id); else set.delete(id); state.queue = [...set]; saveJson(KEY_QUEUE, state.queue); const row = inp.closest(".gs-course-item"); if (row) row.classList.toggle("is-checked", inp.checked); updatePanel(); }); }); } function updatePanel() { const st = document.getElementById("gs-status"); const start = document.getElementById("gs-start"); const stop = document.getElementById("gs-stop"); const examAll = document.getElementById("gs-exam-all"); if (st) st.textContent = state.busy ? "\u8fd0\u884c\u4e2d" : state.examBusy ? "\u8003\u8bd5\u4e2d" : "\u5df2\u505c\u6b62"; if (start) start.disabled = !!state.busy || !!state.examBusy; if (stop) stop.disabled = !state.busy; if (examAll) examAll.disabled = !!state.examBusy || !!state.busy; const cc = document.getElementById("gs-cur-course"); const tk = document.getElementById("gs-cur-task"); if (cc) cc.textContent = state.currentCourse || "\u65e0"; if (tk) tk.textContent = state.currentTask || ""; const n = document.getElementById("gs-queue-n"); const n2 = document.getElementById("gs-queue-n2"); if (n) n.textContent = String(state.queue.length); if (n2) n2.textContent = String(state.queue.length); const picked = state.courses.filter((c) => state.queue.includes(c.id)); const avg = picked.length ? Math.round( picked.reduce((s, c) => s + (Number(c.progress) || 0), 0) / picked.length ) : 0; const pctEl = document.getElementById("gs-pct"); const bar = document.getElementById("gs-bar"); if (pctEl) pctEl.textContent = avg + "%"; if (bar) bar.style.width = avg + "%"; const tier = document.getElementById("gs-cloud-tier"); if (tier) { if (state.cloudTier === "pro") { const until = state.cloudProExpireAt ? new Date(state.cloudProExpireAt * 1000).toLocaleDateString() : ""; tier.textContent = until ? `\u4e13\u4e1a\u7248\uff08\u81f3 ${until}\uff09` : "\u4e13\u4e1a\u7248"; } else { tier.textContent = `\u514d\u8d39\u7248\uff08${state.freeUsedCourses || 0}/${state.freeCourseLimit || 1} \u95e8\uff09`; } } const buy = document.getElementById("gs-pro-buy"); if (buy && state.proBuyUrl) buy.href = state.proBuyUrl; const chip = document.getElementById("gs-mode-chip"); if (chip) chip.textContent = state.mode === MODE_RT ? "1:1 \u5e38\u901f" : "\u6548\u7387\u6a21\u5f0f"; const effLab = document.getElementById("gs-mode-eff-lab"); const rtLab = document.getElementById("gs-mode-rt-lab"); if (effLab) effLab.classList.toggle("is-on", state.mode === MODE_EFF); if (rtLab) rtLab.classList.toggle("is-on", state.mode === MODE_RT); const modeInp = document.querySelector( `input[name="gs-mode"][value="${state.mode}"]` ); if (modeInp) modeInp.checked = true; } async function refreshCourses() { try { log("\u52a0\u8f7d\u8bfe\u7a0b…"); await fetchAllCourses(); const ids = new Set(state.courses.map((c) => c.id)); state.queue = state.queue.filter((id) => ids.has(id) && !state.doneIds.has(id)); state.queue = state.queue.filter((id) => { const c = state.courses.find((x) => x.id === id); return c && !c.done; }); if (!state.queue.length) { state.queue = state.courses.filter((c) => !c.done).map((c) => c.id); } saveJson(KEY_QUEUE, state.queue); renderCourseList(); updatePanel(); log(`\u5df2\u52a0\u8f7d ${state.courses.length} \u95e8\u8bfe`); } catch (e) { log("\u52a0\u8f7d\u5931\u8d25\uff1a" + (e.message || e)); const box = document.getElementById("gs-course-list"); if (box) box.innerHTML = `
\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55
`; } } function boot() { createPanel(); updatePanel(); void (async () => { await Promise.all([fetchPanelNotice(), fetchClientConfig()]); updatePanel(); await checkLogin(); updatePanel(); await refreshCourses(); if (/\/mizar\/studentspace\/study\/listStuding\.do/i.test(location.pathname) || /\/mizar\/portal\/index\.do/i.test(location.pathname)) { log("\u68c0\u6d4b\u5230\u5b66\u4e60\u7a7a\u95f4\u9996\u9875\uff0c\u53ef\u70b9“\u4e00\u952e\u8003\u8bd5\uff08\u5168\u90e8\u57f9\u8bad\u73ed\uff09”"); } })(); } if (document.body) boot(); else document.addEventListener("DOMContentLoaded", boot); })();