// ==UserScript== // @name 新疆干部网络学院在线学习助手 // @namespace https://card.wlxy.live/links/09A67934 // @version 1.0 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @description 支持新疆干部在线(xjgbzx.cn)专题班一键学习,自动完成未完成视频学时,5倍速快速高效完成,免费体验与 Pro 授权,安全稳定 // @antifeature payment 免费体验3个视频章节,升级Pro不限 // @antifeature membership 需云端授权 // @match https://www.xjgbzx.cn/* // @match https://*.xjgbzx.cn/* // @connect oa15.ahzsksw.cn // @connect www.xjgbzx.cn // @connect xjgbzx.cn // @connect card.wlxy.live // @connect huaweicloudobs.ahjxjy.cn // @grant GM_xmlhttpRequest // @grant GM_info // @grant GM.xmlHttpRequest // @grant GM_openInTab // @run-at document-idle // @license All Rights Reserved // ==/UserScript== (function () { "use strict"; const SCRIPT_VERSION = "1.0"; const PRODUCT_NAME = "\u65b0\u7586\u5e72\u90e8\u5728\u7ebf\u5b66\u4e60\u52a9\u624b"; const BASE = location.origin; const REFERER = `${BASE}/pc/index.html`; const SIGNATURE = "dasdasfsd"; const DEFAULT_CLOUD_API_BASE = "https://oa15.ahzsksw.cn"; const PRO_BUY_URL = "https://card.wlxy.live/links/09A67934"; const PANEL_NOTICE_FALLBACK = "\u65b0\u7586\u5e72\u90e8\u5728\u7ebf\u52a9\u624b"; const PANEL_LOGO_URL = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const QQ_GROUP_NUMBER = "903117129"; const QQ_GROUP_LINK = "https://qun.qq.com/universal-share/share?ac=1&authKey=rxdL6YIJ0%2FxOEemjLqTGULvl5aAfJIVQcIvkvnwvmL%2FAmpFZnSafajYHgSXMUXvx&busi_data=eyJncm91cENvZGUiOiI5MDMxMTcxMjkiLCJ0b2tlbiI6IlB2dkFGSm5XRXBrSEhtQVFTUGQzdVNZakhNWDNMbW1kODA2enpoMi9obDh4SWp0YzBDODNFaGtwRU44Z0hyU0siLCJ1aW4iOiIxMjU0MzE1MTQifQ%3D%3D&data=9oyJixSPcigCQW-saV5eXlcMwV9C6J36XySx-rDHwVwNlofvRmd2ze5sLwFtHTbYbG4nAWIUrI0qftC6aTX9xg&svctype=4&tempid=h5_group_info"; const CLOUD_TOKEN_KEY = "xjgbzx_cloud_token_v1"; const CLOUD_LEASE_CACHE_KEY = "xjgbzx_cloud_lease_cache_v1"; const CLOUD_PRO_EXPIRE_CACHE_KEY = "xjgbzx_cloud_pro_expire_cache_v1"; const CLOUD_LAST_STATE_KEY = "xjgbzx_cloud_last_state_v1"; const PANEL_POS_KEY = "xjgbzx_panel_pos_v1"; const PANEL_COLLAPSED_KEY = "xjgbzx_panel_collapsed_v1"; const SK_QUEUE = "xjgbzx_queue"; const SK_CLASS = "xjgbzx_class_id"; const STEP_SEC = 60; const INTERVAL_SEC = 1; const FIRST_WAIT_SEC = 3; const CHEAT_WAIT_SEC = 8; const LOG_MAX = 200; const PROGRESS_LOG_THROTTLE_MS = 30000; const state = { enabled: false, stopFlag: false, running: false, classes: [], selectedClassId: String(localStorage.getItem(SK_CLASS) || ""), selectedIds: loadQueue(), courses: [], chapters: [], currentName: "", currentProgress: "0", currentCourseId: "", currentTask: "\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u5b66\u4e60", doneCount: 0, totalCount: 0, logLines: [], panelHint: "", panelRefreshing: false, lastProgressLogAt: 0, 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, cloudRevoked: false, freeVideoLimit: 3, freeUsedVideos: 0, panelNoticePath: "/api/xjgbzx/panel-notice", remotePanelNotice: "", proBuyUrl: PRO_BUY_URL, }; function loadQueue() { try { const raw = JSON.parse(localStorage.getItem(SK_QUEUE) || "[]"); return Array.isArray(raw) ? raw.map(String) : []; } catch (_) { return []; } } function saveQueue(ids) { state.selectedIds = (ids || []).map(String); localStorage.setItem(SK_QUEUE, JSON.stringify(state.selectedIds)); } function getSavedClassId() { return String(localStorage.getItem(SK_CLASS) || state.selectedClassId || ""); } function setSavedClassId(id) { state.selectedClassId = id != null && id !== "" ? String(id) : ""; if (state.selectedClassId) localStorage.setItem(SK_CLASS, state.selectedClassId); else localStorage.removeItem(SK_CLASS); return state.selectedClassId; } function detectClassIdFromUrl() { const href = String(location.href || ""); const hash = String(location.hash || ""); const search = String(location.search || ""); const patterns = [ /[?&#/]classId=(\d+)/i, /[?&#/]class_id=(\d+)/i, /\/class(?:Detail|Info|Course)?\/(\d+)/i, /class[=/_-](\d+)/i, ]; for (const re of patterns) { const m = href.match(re) || hash.match(re) || search.match(re); if (m && m[1]) return String(m[1]); } return ""; } function getCookie(name) { const m = document.cookie.match(new RegExp("(?:^|;\\s*)" + name + "=([^;]*)")); return m ? decodeURIComponent(m[1]) : ""; } function safeJsonParse(text, fallback) { if (text == null) return fallback; if (typeof text === "object") return text; try { return JSON.parse(String(text)); } catch (_) { return fallback; } } function parseUserInfoCookie() { const raw = getCookie("userInfo"); if (!raw) return null; try { const parts = String(raw).split("."); if (parts.length >= 5) { const jsonPart = parts.slice(4).join("."); const decoded = decodeURIComponent(jsonPart); const obj = safeJsonParse(decoded, null); if (obj && typeof obj === "object") return obj; } const brace = String(raw).indexOf("{"); if (brace >= 0) { const obj = safeJsonParse(decodeURIComponent(raw.slice(brace)), null); if (obj && typeof obj === "object") return obj; } } catch (_) {} return null; } function getLearningUserId() { const info = parseUserInfoCookie(); if (info) { const id = info.id != null ? info.id : info.userId != null ? info.userId : info.user_id; if (id != null && String(id).trim()) return String(id).trim(); } const raw = getCookie("userInfo"); if (raw) { const m = String(raw).match(/^[^.]+\.PC\.(\d+)\./i) || String(raw).match(/\.PC\.(\d+)\./i); if (m) return m[1]; } return ""; } function isLoggedIn() { return !!(getCookie("userInfo") || getCookie("JSESSIONID") || getLearningUserId()); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function sleepInterruptible(ms) { const end = Date.now() + Math.max(0, Number(ms) || 0); while (Date.now() < end) { if (state.stopFlag) return false; await sleep(Math.min(400, end - Date.now())); } return !state.stopFlag; } function escHtml(s) { return String(s ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function log(msg) { const dt = new Date().toLocaleTimeString("zh-CN", { hour12: false }); const line = `[${dt}] ${msg}`; state.logLines.unshift(line); if (state.logLines.length > LOG_MAX) state.logLines.length = LOG_MAX; console.log(`[\u65b0\u7586\u5e72\u90e8\u5728\u7ebf] ${msg}`); renderLog(); } function setHint(msg) { state.panelHint = msg || ""; const el = document.getElementById("xjgbzx-start-hint"); if (!el) return; if (!msg) { el.style.display = "none"; el.textContent = ""; return; } el.style.display = "block"; el.textContent = msg; } function clearPanelHint() { setHint(""); } function apiHeaders(withJsonBody) { const h = { Accept: "application/json, text/plain, */*", Referer: REFERER, Origin: BASE, signature: SIGNATURE, "Cache-Control": "no-cache", }; if (withJsonBody) h["Content-Type"] = "application/json"; return h; } function getGmXhr() { if (typeof GM_xmlhttpRequest === "function") return GM_xmlhttpRequest; if (typeof GM !== "undefined" && GM && typeof GM.xmlHttpRequest === "function") { return GM.xmlHttpRequest.bind(GM); } return null; } function gmRequest(method, url, { headers, body, withCredentials } = {}) { const gmXhr = getGmXhr(); if (!gmXhr) { return Promise.reject(new Error("\u5f53\u524d\u73af\u5883\u4e0d\u652f\u6301 GM_xmlhttpRequest\uff0c\u8bf7\u7528 Tampermonkey/ScriptCat")); } return new Promise((resolve, reject) => { gmXhr({ method, url, headers: headers || {}, data: body, anonymous: false, withCredentials: withCredentials !== false, timeout: 30000, responseType: "text", onload: (res) => resolve(res), onerror: () => reject(new Error("\u7f51\u7edc\u9519\u8bef " + url)), ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6 " + url)), }); }); } function gmRequestWithStatus(url, method, headers, data) { return gmRequest(method || "GET", url, { headers, body: data, withCredentials: false }).then((res) => ({ status: res.status, text: res.responseText || "", })); } async function apiRequest(method, path, { params, body } = {}) { let url = path.startsWith("http") ? path : `${BASE}${path}`; if (params && typeof params === "object") { const q = new URLSearchParams(); Object.keys(params).forEach((k) => { const v = params[k]; if (v !== undefined && v !== null) q.set(k, String(v)); }); const qs = q.toString(); if (qs) url += (url.includes("?") ? "&" : "?") + qs; } const hasBody = method !== "GET" && body !== undefined; const headers = apiHeaders(hasBody); const data = hasBody ? (typeof body === "string" ? body : JSON.stringify(body)) : undefined; let status = 0; let text = ""; const gmXhr = getGmXhr(); if (gmXhr) { const res = await gmRequest(method, url, { headers, body: data, withCredentials: true }); status = Number(res.status) || 0; text = String(res.responseText || ""); } else { const opts = { method, credentials: "include", headers }; if (hasBody) opts.body = data; const resp = await fetch(url, opts); status = resp.status; text = await resp.text(); } const json = safeJsonParse(text, null); if (status < 200 || status >= 300) { const err = new Error(`HTTP ${status}: ${(text || "").slice(0, 120)}`); err.http_status = status; err.raw = json || text; throw err; } if (!json || typeof json !== "object") throw new Error("\u54cd\u5e94\u4e0d\u662f JSON"); return json; } function apiGet(path, params) { return apiRequest("GET", path, { params }); } function apiPost(path, body, params) { return apiRequest("POST", path, { body: body ?? {}, params }); } async function fetchUncompletedClasses() { const all = []; for (let page = 1; page <= 50; page++) { const res = await apiGet("/trainee/api/class/uncompleted_list", { currentPage: page, pageSize: 10, year: "", }); if (res.code !== 0) throw new Error(res.message || "\u62c9\u53d6\u672a\u5b8c\u6210\u4e13\u9898\u73ed\u5931\u8d25"); const lst = (res.data && res.data.classes) || []; if (!lst.length) break; all.push(...lst); const pager = (res.data && res.data.pager) || {}; if (pager && pager.nextPageAvailable === false) break; if (lst.length < 10) break; } const uniq = {}; all.forEach((c) => { if (c && c.id != null) uniq[String(c.id)] = c; }); return Object.values(uniq); } async function fetchClassDetail(classId) { const res = await apiGet(`/trainee/api/class/detail/${classId}`); if (res.code !== 0) throw new Error(res.message || "\u62c9\u53d6\u4e13\u9898\u73ed\u8be6\u60c5\u5931\u8d25"); return (res.data && res.data.class_detail) || res.data || {}; } async function enrichClassesWithDetail(classes) { const list = classes || []; if (!list.length) return []; return Promise.all( list.map(async (c) => { try { const d = await fetchClassDetail(c.id); return Object.assign({}, c, d, { _detailOk: true }); } catch (_) { return Object.assign({}, c, { _detailOk: false }); } }) ); } async function fetchClassCoursePayload(classId) { const res = await apiGet(`/trainee/api/course/class_course/${classId}`); if (res.code !== 0) throw new Error(res.message || "\u62c9\u53d6\u73ed\u6b21\u8bfe\u7a0b\u5931\u8d25"); return res.data || {}; } function flattenClassCourses(payload, { onlyUncompleted } = { onlyUncompleted: false }) { const out = []; const groups = (payload && payload.course) || []; groups.forEach((group) => { const moduleName = (group && (group.module_name || group.name || group.title)) || ""; const keys = onlyUncompleted ? ["uncompleted_courseData"] : ["uncompleted_courseData", "completed_courseData"]; keys.forEach((key) => { const blocks = (group && group[key]) || []; blocks.forEach((block) => { const courses = (block && block.courses) || []; courses.forEach((c) => { if (!c || c.id == null) return; if (onlyUncompleted && Number(c.is_completed) === 1) return; out.push(Object.assign({}, c, { _module: moduleName, _classId: payload.class_id || "" })); }); }); }); }); const uniq = {}; out.forEach((c) => { const id = String(c.id); const prev = uniq[id]; if (!prev || Number(c.learning_progress || 0) >= Number(prev.learning_progress || 0)) { uniq[id] = c; } }); return Object.values(uniq); } async function fetchUncompletedCourses(classId) { const cid = classId != null && classId !== "" ? String(classId) : ""; if (!cid) return []; setSavedClassId(cid); const payload = await fetchClassCoursePayload(cid); return flattenClassCourses(payload, { onlyUncompleted: true }).map((c) => Object.assign({}, c, { _classId: cid }) ); } async function loadChaptersForClassIds(classIds) { const ids = (classIds || []).map(String).filter(Boolean); if (!ids.length) { state.chapters = []; return []; } const merged = []; for (const cid of ids) { const payload = await fetchClassCoursePayload(cid); merged.push( ...flattenClassCourses(payload, { onlyUncompleted: false }).map((c) => Object.assign({}, c, { _classId: cid }) ) ); } const uniq = {}; merged.forEach((c) => { const id = String(c.id); const prev = uniq[id]; if (!prev || Number(c.learning_progress || 0) >= Number(prev.learning_progress || 0)) { uniq[id] = c; } }); state.chapters = Object.values(uniq); return state.chapters; } async function fetchCourseDetail(courseId) { const res = await apiGet(`/trainee/api/course/detail/${courseId}`); if (res.code !== 0) throw new Error(res.message || "\u83b7\u53d6\u8bfe\u7a0b\u8be6\u60c5\u5931\u8d25"); return (res.data && res.data.course) || res.data; } async function fetchPlayToken(courseId) { const res = await apiGet(`/trainee/api/course/play/${courseId}`); if (res.code !== 0) throw new Error(res.message || "\u83b7\u53d6\u64ad\u653e\u51ed\u8bc1\u5931\u8d25"); const data = res.data || {}; return data.playCourse != null ? String(data.playCourse) : ""; } function collectScoCandidates(detail) { const sco = safeJsonParse(detail && detail.sco, {}); const manifest = safeJsonParse(detail && detail.manifest, []); const list = []; if (sco && Array.isArray(sco.scormData)) { sco.scormData.forEach((row) => { if (row && row.sco_id) list.push(row); }); } if (Array.isArray(manifest)) { manifest.forEach((row) => { if (row && row.sco_id) list.push(row); }); } return list; } function parseScoId() { return "res01"; } function parseLessonLocation(detail, preferredScoId) { const list = collectScoCandidates(detail); let row = null; if (preferredScoId) { row = list.find((r) => String(r.sco_id) === String(preferredScoId)) || null; } if (!row) { row = list.find((r) => String(r.sco_id) === "res01") || list.find((r) => /^res\d+/i.test(String(r.sco_id || ""))) || list[0] || null; } const raw = (row && row.lesson_location) || "0"; let current = parseInt(String(raw).trim(), 10); if (!Number.isFinite(current)) current = 0; const durationMin = Number(detail && detail.duration) || 0; const durSec = durationMin * 60; if (durSec > 0 && current > durSec * 3 && current >= 1000) { current = Math.floor(current / 1000); } return current; } function getCloudApiBase() { return DEFAULT_CLOUD_API_BASE; } function formatLeaseExpireText(expSec) { const ex = Number(expSec || 0); if (!Number.isFinite(ex) || ex <= 0) return "—"; const d = new Date(ex * 1000); if (Number.isNaN(d.getTime())) return "—"; return d.toLocaleString("zh-CN", { hour12: false }); } function resolveLeaseExpireSec(data) { const raw = data?.exp ?? data?.expire_at ?? data?.expires_at ?? data?.lease_exp ?? 0; const n = Number(raw || 0); if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000); return Number.isFinite(n) ? Math.floor(n) : 0; } function resolveProExpireSec(data) { const raw = data?.pro_expires_at ?? data?.proExpiresAt ?? data?.pro_expire_at ?? 0; const n = Number(raw || 0); if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000); return Number.isFinite(n) ? Math.floor(n) : 0; } function readCloudLeaseCache() { try { const raw = localStorage.getItem(CLOUD_LEASE_CACHE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") return null; const lease = String(parsed.lease || "").trim(); const exp = Number(parsed.exp || 0); if (!lease || !Number.isFinite(exp) || exp <= 0) return null; return { lease, exp, tier: String(parsed.tier || "").trim(), freeVideoLimit: Number(parsed.freeVideoLimit ?? parsed.free_video_limit ?? 3), freeUsedVideos: Number(parsed.freeUsedVideos ?? parsed.free_used_videos ?? 0), proExpireAt: Number(parsed.proExpireAt ?? parsed.pro_expire_at ?? 0), }; } catch (_) { return null; } } function writeCloudLeaseCache(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 readCloudLastState() { try { const raw = localStorage.getItem(CLOUD_LAST_STATE_KEY); if (!raw) return null; const p = JSON.parse(raw); if (!p || typeof p !== "object") return null; return { tier: String(p.tier || "").trim(), freeVideoLimit: Number(p.freeVideoLimit ?? p.free_video_limit ?? 3), freeUsedVideos: Number(p.freeUsedVideos ?? p.free_used_videos ?? 0), }; } catch (_) { return null; } } function writeCloudLastState(partial) { try { const prev = readCloudLastState() || {}; localStorage.setItem( CLOUD_LAST_STATE_KEY, JSON.stringify(Object.assign({}, prev, partial || {}, { ts: Date.now() })) ); } catch (_) {} } function writeCloudProExpireCache(token, exp) { try { const tk = String(token || "").trim(); if (!tk || !exp) { localStorage.removeItem(CLOUD_PRO_EXPIRE_CACHE_KEY); return; } localStorage.setItem(CLOUD_PRO_EXPIRE_CACHE_KEY, JSON.stringify({ token: tk, exp: Math.floor(exp) })); } catch (_) {} } async function _rq(path, method, payload) { const base = getCloudApiBase(); const url = `${base}${path.startsWith("/") ? path : `/${path}`}`; const headers = { Accept: "application/json", "Content-Type": "application/json" }; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; if (state.cloudToken) headers.Authorization = `Bearer ${state.cloudToken}`; let reqPayload = payload; if (reqPayload && typeof reqPayload === "object") { reqPayload = Object.assign({}, reqPayload); if (path !== "/api/xjgbzx/lease" && state.cloudLease && reqPayload.lease == null) { reqPayload.lease = state.cloudLease; } } const body = reqPayload == null ? null : JSON.stringify(reqPayload); const res = await gmRequestWithStatus(url, method || "POST", headers, body); const text = String(res.text || "").trim(); let data = {}; if (text) data = JSON.parse(text); if (res.status < 200 || res.status >= 300) { const errCode = String(data.detail || data.message || data.code || `http_${res.status}`); throw new Error(errCode); } return data; } async function _el(forceRefresh) { const now = Math.floor(Date.now() / 1000); if (!forceRefresh && state.cloudLease && state.cloudLeaseExp - now > 60) return true; if (!forceRefresh) { const cached = readCloudLeaseCache(); if (cached && cached.exp - now > 60) { state.cloudLease = cached.lease; state.cloudLeaseExp = cached.exp; state.cloudProExpireAt = Number(cached.proExpireAt || 0); if (cached.tier) state.cloudTier = cached.tier; if (cached.freeVideoLimit > 0) state.freeVideoLimit = cached.freeVideoLimit; if (cached.freeUsedVideos >= 0) state.freeUsedVideos = cached.freeUsedVideos; updateCloudPanelUI(); return true; } } const luid = getLearningUserId(); if (!luid) throw new Error("\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u83b7\u53d6\u4e91\u7aef\u6388\u6743"); let data; try { data = await _rq("/api/xjgbzx/lease", "POST", { learning_user_id: luid }); state.cloudRevoked = false; } catch (e) { const em = String(e?.message || e); if (/(revoked|invalid token|expired)/i.test(em)) { state.cloudRevoked = true; state.cloudTier = "revoked"; state.cloudLease = ""; state.cloudLeaseExp = 0; writeCloudLeaseCache("", 0); updateCloudPanelUI(); } throw e; } const lease = String(data.lease || ""); const exp = resolveLeaseExpireSec(data); const tier = String(data.tier || "").trim() || (state.cloudToken ? "pro" : "free"); const proExpireAt = resolveProExpireSec(data); state.cloudLease = lease; state.cloudLeaseExp = exp; state.cloudProExpireAt = proExpireAt > 0 ? proExpireAt : 0; state.cloudTier = tier; state.freeVideoLimit = Number( data.free_video_limit ?? data.free_chapter_limit ?? data.freeVideoLimit ?? state.freeVideoLimit ?? 3 ); state.freeUsedVideos = Number( data.free_used_videos ?? data.free_used_chapters ?? data.freeUsedVideos ?? 0 ); writeCloudLeaseCache(lease, exp, { tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, proExpireAt: state.cloudProExpireAt, }); writeCloudLastState({ tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, }); if (tier === "pro" && state.cloudProExpireAt > 0) { writeCloudProExpireCache(state.cloudToken, state.cloudProExpireAt); } updateCloudPanelUI(); return !!lease; } async function cloudVerifyToken() { if (!state.cloudToken) return true; const base = getCloudApiBase(); const url = `${base}/api/license/verify`; const headers = { Accept: "application/json", Authorization: `Bearer ${state.cloudToken}` }; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; const res = await gmRequestWithStatus(url, "GET", headers, null); const text = String(res.text || "").trim(); let data = {}; if (text) data = JSON.parse(text); if (res.status < 200 || res.status >= 300 || !data.ok) { throw new Error(String(data.message || data.detail || "Token \u6821\u9a8c\u5931\u8d25")); } return true; } function syncCloudQuotaFromResponse(data) { if (!data || typeof data !== "object") return; if (data.tier) state.cloudTier = String(data.tier); const lim = data.free_video_limit ?? data.free_chapter_limit ?? data.limit; const used = data.free_used_videos ?? data.free_used_chapters ?? data.used; if (lim != null) state.freeVideoLimit = Number(lim); if (used != null) state.freeUsedVideos = Number(used); writeCloudLastState({ tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, }); updateCloudPanelUI(); } async function cloudConsumeChapter(courseId) { await _el(false); const data = await _rq("/api/xjgbzx/video/consume", "POST", { cs_id: String(courseId || ""), }); syncCloudQuotaFromResponse(data); return data; } async function cloudFinishChapter() { try { await _el(true); } catch (_) {} } function isCourseDonePayload(data) { if (!data || typeof data !== "object") return false; if (Number(data.is_completed) === 1) return true; const p = Number(data.learning_progress); return Number.isFinite(p) && p >= 100; } async function postUserCourse({ playCourse, userCourseId, scoId, lessonLocation, sessionTime }) { return apiPost("/trainee/index/user_course", { playCourse, user_course_id: String(userCourseId), scormData: [ { sco_id: scoId || "res01", lesson_location: String(Math.floor(lessonLocation)), session_time: Math.floor(sessionTime), }, ], }); } function maybeLogProgress(progress, name) { const now = Date.now(); if (now - (state.lastProgressLogAt || 0) < PROGRESS_LOG_THROTTLE_MS) return; state.lastProgressLogAt = now; log(`\u5b66\u4e60\u4e2d ${progress}% · ${name}`); } function recalcClassCompletedHours(classId) { const cid = String(classId || ""); if (!cid) return null; const cls = (state.classes || []).find((c) => String(c.id) === cid); if (!cls) return null; const chapters = (state.chapters || []).filter((c) => String(c._classId) === cid); if (!chapters.length) return cls; let done = 0; chapters.forEach((c) => { const h = Number(c.learning_hour) || 0; const p = Math.min(100, Math.max(0, Number(c.learning_progress) || 0)); if (Number(c.is_completed) === 1 || p >= 100) done += h; else done += (h * p) / 100; }); cls.completed_required_leaning_hour = done.toFixed(2); return cls; } function patchChapterProgressUI(courseId, prog) { const box = document.getElementById("xjgbzx-chapter-preview"); if (!box) return; const item = Array.from(box.querySelectorAll(".xjgbzx-ch-item")).find( (el) => el.getAttribute("data-cid") === String(courseId) ); if (!item) { renderChapterPreview(); return; } const p = Math.min(100, Math.max(0, Number(prog) || 0)); const done = p >= 100; const progEl = item.querySelector(".xjgbzx-ch-prog"); const statusEl = item.querySelector("[data-ch-status]"); if (progEl) { progEl.textContent = `${p.toFixed(1)}%`; progEl.className = done ? "xjgbzx-ch-prog xjgbzx-ch-done" : "xjgbzx-ch-prog"; } if (statusEl) statusEl.textContent = done ? "\u5df2\u5b8c\u6210" : "\u5b66\u4e60\u4e2d"; document.querySelectorAll("#xjgbzx-chapter-preview .xjgbzx-ch-item").forEach((el) => { el.classList.toggle("xjgbzx-ch-cur", el.getAttribute("data-cid") === String(courseId)); }); } function patchClassProgressUI(classId) { const cls = recalcClassCompletedHours(classId); if (!cls) return; const box = document.getElementById("xjgbzx-course-list"); if (!box) return; const item = Array.from(box.querySelectorAll(".xjgbzx-course-item")).find( (el) => el.getAttribute("data-class-id") === String(classId) ); if (!item) { renderCourseList(); return; } const pct = classHourProgress(cls); const doneHour = fmtHour(cls.completed_required_leaning_hour || 0); const bar = item.querySelector(".xjgbzx-class-bar > span"); const pctEl = item.querySelector("[data-class-pct]"); const doneEl = item.querySelector("[data-class-done]"); if (bar) bar.style.width = `${pct}%`; if (pctEl) pctEl.textContent = `\u5b66\u65f6\u8fdb\u5ea6 ${pct}%`; if (doneEl) doneEl.textContent = doneHour; } function syncLiveProgress(course, prog) { const courseId = String(course.id); const classId = String(course._classId || ""); const p = Math.min(100, Math.max(0, Number(prog) || 0)); const ch = (state.chapters || []).find((c) => String(c.id) === courseId); if (ch) { ch.learning_progress = p; if (p >= 100) ch.is_completed = 1; if (!ch._classId && classId) ch._classId = classId; } state.currentProgress = String(p); updateStatusUI(); patchChapterProgressUI(courseId, p); if (classId) patchClassProgressUI(classId); } async function studyOne(course) { const cid = course.id; const name = course.course_name || course.name || `\u8bfe\u7a0b${cid}`; let durationMin = Number(course.duration) || 0; state.currentName = name; state.currentCourseId = String(cid); state.currentTask = "\u51c6\u5907\u4e2d…"; state.currentProgress = String(course.learning_progress || 0); state.lastProgressLogAt = 0; updateStatusUI(); renderChapterPreview(); if (state.stopFlag) return false; const detail = await fetchCourseDetail(cid); if (!durationMin) durationMin = Number(detail.duration) || 0; const durationSec = durationMin > 0 ? durationMin * 60 : 0; const ucid = detail.user_course_id; if (ucid == null || ucid === "") { log(`\u6682\u65f6\u65e0\u6cd5\u5b66\u4e60\uff1a${name}`); return false; } const scoId = parseScoId(detail); const playCourse = await fetchPlayToken(cid); if (!playCourse) { log(`\u6682\u65f6\u65e0\u6cd5\u5b66\u4e60\uff1a${name}`); return false; } let current = parseLessonLocation(detail, scoId); const initProg = Number(course.learning_progress || detail.learning_progress || 0); if (durationSec > 0 && initProg > 0 && current < (durationSec * initProg) / 100) { current = Math.floor((durationSec * initProg) / 100); } if (isCourseDonePayload(detail) || initProg >= 100) { log(`\u5df2\u5b66\u5b8c\uff0c\u8fdb\u5165\u4e0b\u4e00\u95e8`); syncLiveProgress(course, 100); return true; } state.currentTask = "\u51c6\u5907\u5b66\u4e60…"; updateStatusUI(); try { await cloudConsumeChapter(cid); } catch (e) { const msg = String(e.message || e); if (/free_quota_exhausted/i.test(msg)) { try { await _el(true); } catch (_) {} log(`\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff08${state.freeUsedVideos}/${state.freeVideoLimit}\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`); setHint("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"); switchPanelTab("settings"); openProModal(); state.stopFlag = true; return false; } log(`\u6682\u65f6\u65e0\u6cd5\u5f00\u59cb\u5b66\u4e60\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5`); return false; } state.currentTask = "\u5b66\u4e60\u4e2d…"; updateStatusUI(); syncLiveProgress(course, initProg); log(`\u6b63\u5728\u5b66\u4e60\uff1a${name}`); if (!(await sleepInterruptible(FIRST_WAIT_SEC * 1000))) return false; const step = STEP_SEC; const wait = INTERVAL_SEC; while (!state.stopFlag) { current += step; let res; try { res = await postUserCourse({ playCourse, userCourseId: ucid, scoId, lessonLocation: current, sessionTime: step, }); } catch (e) { log(`\u7f51\u7edc\u6ce2\u52a8\uff0c\u6b63\u5728\u91cd\u8bd5…`); if (!(await sleepInterruptible(CHEAT_WAIT_SEC * 1000))) return false; continue; } const data = res && res.data != null && typeof res.data === "object" ? res.data : res; if (res && Number(res.code) !== 0 && res.code != null) { log(`\u5b66\u4e60\u7a0d\u6709\u5ef6\u8fdf\uff0c\u6b63\u5728\u91cd\u8bd5…`); if (!(await sleepInterruptible(CHEAT_WAIT_SEC * 1000))) return false; continue; } if (data && data.cheat) { log(`\u5e73\u53f0\u6821\u9a8c\u4e2d\uff0c\u8bf7\u7a0d\u5019…`); if (!(await sleepInterruptible(CHEAT_WAIT_SEC * 1000))) return false; continue; } const prog = data && data.learning_progress != null ? data.learning_progress : null; if (prog != null) { syncLiveProgress(course, prog); maybeLogProgress(prog, name); } if (isCourseDonePayload(data)) { syncLiveProgress(course, 100); log(`\u5df2\u5b8c\u6210\uff1a${name}`); state.currentTask = "\u672c\u8bfe\u5df2\u5b8c\u6210"; updateStatusUI(); await cloudFinishChapter(); return true; } if (!(await sleepInterruptible(wait * 1000))) return false; } await cloudFinishChapter(); return false; } async function ensureCloudReady() { if (!isLoggedIn()) { log("\u8bf7\u5148\u767b\u5f55\u65b0\u7586\u5e72\u90e8\u5728\u7ebf"); setHint("\u8bf7\u5148\u767b\u5f55\u65b0\u7586\u5e72\u90e8\u5728\u7ebf"); return false; } if (!getLearningUserId()) { log("\u8bf7\u91cd\u65b0\u767b\u5f55\u540e\u518d\u8bd5"); setHint("\u8bf7\u91cd\u65b0\u767b\u5f55\u540e\u518d\u8bd5"); return false; } try { if (state.cloudToken) await cloudVerifyToken(); await _el(false); } catch (err) { log("\u6388\u6743\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc\u6216 Token"); setHint("\u6388\u6743\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc\u6216 Token"); return false; } const tier = String(state.cloudTier || "").toLowerCase(); if (tier === "pro") return true; if (tier === "free") { if (Number(state.freeUsedVideos) >= Number(state.freeVideoLimit)) { log(`\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff08${state.freeUsedVideos}/${state.freeVideoLimit}\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`); setHint("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"); switchPanelTab("settings"); openProModal(); return false; } return true; } log("\u6388\u6743\u72b6\u6001\u5f02\u5e38\uff0c\u8bf7\u68c0\u67e5 Token"); return false; } function cloudServiceOrigin() { try { return new URL(getCloudApiBase()).origin; } catch (_) { return ""; } } async function fetchPanelNotice() { const origin = cloudServiceOrigin(); if (!origin) return; const path = String(state.panelNoticePath || "/api/xjgbzx/panel-notice"); const url = `${origin}${path.startsWith("/") ? path : `/${path}`}`; try { const res = await gmRequestWithStatus(url, "GET", {}, null); if (res.status >= 200 && res.status < 300) { state.remotePanelNotice = String(res.text || "").trim() || PANEL_NOTICE_FALLBACK; const el = document.querySelector("#xjgbzx-ann-text"); if (el) el.textContent = state.remotePanelNotice; } } catch (_) {} } async function fetchClientConfig() { try { const data = await gmRequestWithStatus( `${getCloudApiBase()}/api/xjgbzx/client-config`, "GET", { Accept: "application/json" }, null ); if (data.status >= 200 && data.status < 300 && data.text) { const j = JSON.parse(data.text); if (j?.panelNoticePath) state.panelNoticePath = String(j.panelNoticePath); if (j?.freeVideoLimit != null) state.freeVideoLimit = Number(j.freeVideoLimit) || 3; if (j?.proBuyUrl) state.proBuyUrl = String(j.proBuyUrl).trim() || PRO_BUY_URL; } } catch (_) {} await fetchPanelNotice(); updateCloudPanelUI(); } function getProBuyUrl() { return String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL; } function openProBuyPage() { const url = getProBuyUrl(); try { GM_openInTab(url, { active: true, insert: true, setParent: true }); } catch (_) { window.open(url, "_blank", "noopener,noreferrer"); } } function openProModal() { const vm = document.getElementById("xjgbzx-pro-modal"); if (vm) vm.style.display = "flex"; } function closeProModal() { const vm = document.getElementById("xjgbzx-pro-modal"); if (vm) vm.style.display = "none"; } function createProModal() { const old = document.getElementById("xjgbzx-pro-modal"); if (old) old.remove(); const modal = document.createElement("div"); modal.id = "xjgbzx-pro-modal"; modal.innerHTML = `
\u5f00\u901a Pro
Pro \u7528\u6237\u53ef\u65e0\u9650\u5236\u89c6\u9891\u7ae0\u8282\u5b66\u4e60\uff08\u514d\u8d39\u4f53\u9a8c\u9ed8\u8ba4 ${state.freeVideoLimit || 3} \u7ae0\uff09

Pro \u6743\u76ca

\u63d0\u793a\uff1aPro \u6743\u9650\u7ed1\u5b9a\u5f53\u524d\u767b\u5f55\u8d26\u53f7\uff0c\u8bf7\u52ff\u4e0e\u4ed6\u4eba\u5171\u7528 Token\u3002

\u5f00\u901a\u65b9\u5f0f

1) \u52a0\u5165 QQ \u7fa4 ${QQ_GROUP_NUMBER}\uff0c\u8054\u7cfb\u7ba1\u7406\u5458\u83b7\u53d6 Token

2) \u524d\u5f80\u8d2d\u4e70\u9875\u83b7\u53d6 Token\uff0c\u81ea\u52a9\u53d1\u8d27

\u8d2d\u5f97 Token \u540e\uff1a\u5728\u8bbe\u7f6e\u9875 Token \u8f93\u5165\u6846\u7c98\u8d34\uff0c\u70b9\u51fb\u300c\u4fdd\u5b58\u5e76\u6821\u9a8c\u300d\u3002
\u91cd\u8981\uff1a\u8bf7\u52ff\u968f\u610f\u6cc4\u9732 Token\uff0c\u907f\u514d\u8d26\u53f7\u88ab\u591a\u4eba\u5171\u7528\u5bfc\u81f4\u5931\u6548\u3002
`; document.body.appendChild(modal); modal.addEventListener("click", (e) => { if (e.target === modal) closeProModal(); }); modal.querySelector("#xjgbzx-pro-close")?.addEventListener("click", closeProModal); modal.querySelector("#xjgbzx-pro-buy-link")?.addEventListener("click", openProBuyPage); } function switchPanelTab(name) { document.querySelectorAll(".xjgbzx-tab-btn").forEach((el) => { el.classList.toggle("active", el.dataset.tab === name); }); document.querySelectorAll(".xjgbzx-pane").forEach((el) => { el.classList.toggle("active", el.dataset.pane === name); }); } function formatCloudTierText(tier) { if (state.cloudRevoked) return "\u5df2\u7981\u7528"; const t = String(tier || "").trim().toLowerCase(); if (t === "pro") return "Pro \u4f1a\u5458"; if (t === "free") return "\u514d\u8d39\u4f53\u9a8c"; if (t === "revoked") return "\u5df2\u7981\u7528"; if (t === "unknown") return "\u672a\u6821\u9a8c"; return tier ? String(tier) : "\u672a\u6821\u9a8c"; } function updateCloudPanelUI() { const tierEl = document.querySelector("#xjgbzx-cloud-tier"); const freeEl = document.querySelector("#xjgbzx-cloud-free"); const freeLabelEl = document.querySelector("#xjgbzx-cloud-free-label"); const tokenInput = document.querySelector("#xjgbzx-cloud-token"); if (tierEl) { tierEl.textContent = formatCloudTierText(state.cloudTier); tierEl.style.color = state.cloudRevoked || String(state.cloudTier || "").toLowerCase() === "revoked" ? "#dc2626" : "#0f172a"; } if (state.cloudRevoked) { if (freeLabelEl) freeLabelEl.textContent = "\u6388\u6743\u72b6\u6001"; if (freeEl) freeEl.textContent = "Token \u5df2\u7981\u7528"; } else if (String(state.cloudTier || "").toLowerCase() === "pro") { if (freeLabelEl) freeLabelEl.textContent = "Pro \u5230\u671f"; if (freeEl) freeEl.textContent = formatLeaseExpireText(state.cloudProExpireAt || state.cloudLeaseExp); } else { if (freeLabelEl) freeLabelEl.textContent = "\u514d\u8d39\u4f53\u9a8c\u89c6\u9891"; if (freeEl) freeEl.textContent = `${state.freeUsedVideos}/${state.freeVideoLimit}`; } if (tokenInput && tokenInput !== document.activeElement) tokenInput.value = state.cloudToken || ""; } async function runQueue() { if (state.running) return; state.running = true; state.stopFlag = false; state.enabled = true; updateStatusUI(); try { if (!(await ensureCloudReady())) return; const selectedClassIds = state.selectedIds.slice(); if (!selectedClassIds.length) { setHint("\u8bf7\u5148\u52fe\u9009\u81f3\u5c11\u4e00\u4e2a\u4e13\u9898\u73ed\u518d\u70b9\u5f00\u59cb"); log("\u672a\u52fe\u9009\u4e13\u9898\u73ed"); return; } clearPanelHint(); log("\u5f00\u59cb\u5b66\u4e60"); const queue = []; for (const classId of selectedClassIds) { if (state.stopFlag) break; const cls = (state.classes || []).find((c) => String(c.id) === String(classId)); const clsName = (cls && cls.class_name) || `\u4e13\u9898\u73ed${classId}`; const courses = await fetchUncompletedCourses(classId); if (!courses.length) { log(`\u300c${clsName}\u300d\u5df2\u5168\u90e8\u5b66\u5b8c`); continue; } log(`\u5f00\u59cb\u5b66\u4e60\u300c${clsName}\u300d\uff08\u5171 ${courses.length} \u95e8\uff09`); courses.forEach((c) => { queue.push(Object.assign({}, c, { _classId: String(classId), _className: clsName })); }); } if (!queue.length) { setHint("\u6240\u9009\u4e13\u9898\u73ed\u6682\u65e0\u672a\u5b8c\u6210\u8bfe\u7a0b"); log("\u5f53\u524d\u6ca1\u6709\u9700\u8981\u5b66\u4e60\u7684\u8bfe\u7a0b"); return; } state.courses = queue; state.totalCount = queue.length; state.doneCount = 0; updateStatusUI(); await loadChaptersForClassIds(selectedClassIds); renderChapterPreview(); switchPanelTab("chapter"); const remainByClass = {}; queue.forEach((c) => { const id = String(c._classId || ""); if (id) remainByClass[id] = (remainByClass[id] || 0) + 1; }); for (const course of queue) { if (state.stopFlag) break; if (String(state.cloudTier || "").toLowerCase() === "free") { if (Number(state.freeUsedVideos) >= Number(state.freeVideoLimit)) { log(`\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff08${state.freeUsedVideos}/${state.freeVideoLimit}\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`); switchPanelTab("settings"); openProModal(); break; } } const ok = await studyOne(course); if (ok) { state.doneCount += 1; const classId = String(course._classId || ""); if (classId && remainByClass[classId] != null) { remainByClass[classId] -= 1; if (remainByClass[classId] <= 0) { saveQueue(state.selectedIds.filter((id) => String(id) !== classId)); state.classes = (state.classes || []).filter((c) => String(c.id) !== classId); renderCourseList(); } } } updateStatusUI(); } if (state.stopFlag) { log("\u5df2\u505c\u6b62"); state.currentName = "\u5df2\u505c\u6b62"; state.currentTask = "\u5df2\u505c\u6b62"; } else { log("\u5168\u90e8\u5b66\u4e60\u5b8c\u6210"); state.currentName = "\u5168\u90e8\u5b8c\u6210"; state.currentTask = "\u5168\u90e8\u5b8c\u6210"; } } catch (e) { log(`\u8fd0\u884c\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5`); setHint(String(e.message || e)); } finally { state.running = false; state.enabled = false; updateStatusUI(); } } function stopRun() { state.stopFlag = true; state.enabled = false; log("\u6b63\u5728\u505c\u6b62…"); updateStatusUI(); } async function refreshCourses() { try { if (!isLoggedIn()) { setHint("\u8bf7\u5148\u767b\u5f55\u65b0\u7586\u5e72\u90e8\u5728\u7ebf"); state.classes = []; state.chapters = []; renderCourseList(); renderChapterPreview(); return; } state.panelRefreshing = true; updateStatusUI(); clearPanelHint(); log("\u6b63\u5728\u5237\u65b0\u8bfe\u7a0b…"); state.classes = await enrichClassesWithDetail(await fetchUncompletedClasses()); const valid = new Set(state.classes.map((c) => String(c.id))); let selected = state.selectedIds.filter((id) => valid.has(String(id))); if (!selected.length) { const fromUrl = detectClassIdFromUrl(); if (fromUrl && valid.has(fromUrl)) selected = [fromUrl]; } saveQueue(selected); if (selected[0]) setSavedClassId(selected[0]); renderCourseList(); const chapterIds = selected.length ? selected : state.classes[0] ? [String(state.classes[0].id)] : []; await loadChaptersForClassIds(chapterIds); renderChapterPreview(); log(`\u5df2\u52a0\u8f7d ${state.classes.length} \u4e2a\u4e13\u9898\u73ed`); if (!state.classes.length) setHint("\u6682\u65e0\u672a\u5b8c\u6210\u4e13\u9898\u73ed"); } catch (e) { log(`\u5237\u65b0\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5`); setHint(String(e.message || e)); } finally { state.panelRefreshing = false; updateStatusUI(); } } function injectStyles() { if (document.getElementById("xjgbzx-auto-style")) return; const style = document.createElement("style"); style.id = "xjgbzx-auto-style"; style.textContent = ` #xjgbzx-auto-panel{position:fixed;right:20px;top:80px;z-index:999999;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,820px);} #xjgbzx-auto-panel.xjgbzx-panel-min #xjgbzx-panel-body,#xjgbzx-auto-panel.xjgbzx-panel-min #xjgbzx-panel-footer,#xjgbzx-auto-panel.xjgbzx-panel-min .xjgbzx-footer-extra{display:none !important;} #xjgbzx-auto-panel.xjgbzx-panel-min #xjgbzx-panel-header{border-bottom:none;} #xjgbzx-panel-header{flex:0 0 auto;padding:8px 11px;background:linear-gradient(180deg,#eef6ff,#e2effc);border-bottom:1px solid #cfe0f5;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;} #xjgbzx-panel-brand{display:flex;gap:9px;align-items:center;min-width:0;flex:1;} #xjgbzx-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:cover;border:1px solid rgba(148,163,184,.45);background:#fff;flex:0 0 auto;} #xjgbzx-panel-title{font-size:13px;font-weight:900;color:#0f172a;line-height:1.26;display:flex;align-items:flex-start;gap:5px;flex-wrap:wrap;} .xjgbzx-panel-title-text{flex:1 1 12em;min-width:0;} .xjgbzx-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;} #xjgbzx-panel-sub{display:flex;flex-wrap:wrap;gap:3px 5px;margin-top:3px;} .xjgbzx-sub-chip{font-size:11px;color:#1e3a8a;background:rgba(255,255,255,.7);padding:2px 7px;border-radius:999px;border:1px solid rgba(37,99,235,.18);font-weight:700;} .xjgbzx-sub-chip-em{color:#1d4ed8;background:rgba(219,234,254,.9);border-color:rgba(37,99,235,.28);} #xjgbzx-panel-controls{display:flex;gap:5px;flex:0 0 auto;} .xjgbzx-panel-ctl{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-size:15px;font-weight:900;padding:0;} #xjgbzx-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;} .xjgbzx-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .xjgbzx-card-status{padding:6px 8px;} .xjgbzx-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;line-height:1.28;} .xjgbzx-status-label{font-size:11px;color:#64748b;font-weight:700;} #xjgbzx-auto-status{padding:2px 7px;border-radius:999px;font-weight:900;font-size:11px;background:#fff;border:1px solid #cbd5e1;} #xjgbzx-auto-status.on{color:#166534;background:#dcfce7;border-color:#86efac;} .xjgbzx-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;} .xjgbzx-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .xjgbzx-progress-pct{font-size:14px;font-weight:900;color:#0369a1;} .xjgbzx-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;} .xjgbzx-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;} .xjgbzx-tabbar{display:flex;gap:6px;margin-bottom:7px;} .xjgbzx-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px 4px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;} .xjgbzx-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .xjgbzx-pane{display:none;}.xjgbzx-pane.active{display:block;} .xjgbzx-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;} .xjgbzx-list-title{font-size:12px;color:#64748b;font-weight:700;} .xjgbzx-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;} .xjgbzx-list-head-actions{display:flex;align-items:center;gap:5px;} .xjgbzx-log-clear-btn,.xjgbzx-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#64748b;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;cursor:pointer;} #xjgbzx-course-list,#xjgbzx-chapter-preview,#xjgbzx-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} .xjgbzx-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;} .xjgbzx-course-item{display:flex;gap:7px;align-items:flex-start;padding:8px 7px;border-radius:8px;border:1px solid transparent;} .xjgbzx-course-item:hover{background:#fff;border-color:#dbe4f0;} .xjgbzx-course-item label{flex:1;cursor:pointer;line-height:1.35;min-width:0;} .xjgbzx-course-name{font-weight:700;display:block;} .xjgbzx-course-meta{display:block;font-size:11px;color:#64748b;margin-top:3px;line-height:1.45;} .xjgbzx-course-meta .hl{color:#ea580c;font-weight:700;} .xjgbzx-course-meta .req{color:#dc2626;font-weight:700;} .xjgbzx-class-bar{height:4px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:5px;} .xjgbzx-class-bar>span{display:block;height:100%;background:linear-gradient(90deg,#fb923c,#ea580c);width:0;} .xjgbzx-course-foot{display:flex;justify-content:space-between;gap:8px;margin-top:3px;font-size:11px;color:#64748b;} .xjgbzx-course-foot .done{color:#ea580c;font-weight:700;} .xjgbzx-ch-item{padding:7px 8px;border-bottom:1px solid #e8eef6;} .xjgbzx-ch-item:last-child{border-bottom:none;} .xjgbzx-ch-title{font-weight:700;line-height:1.35;margin-bottom:3px;} .xjgbzx-ch-meta{font-size:11px;color:#64748b;display:flex;flex-wrap:wrap;gap:6px 10px;} .xjgbzx-ch-prog{font-weight:800;color:#1d4ed8;} .xjgbzx-ch-done{color:#15803d;} .xjgbzx-ch-cur{background:#eff6ff;border-radius:8px;} .xjgbzx-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;} .xjgbzx-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;align-items:center;} .xjgbzx-meta-label{color:#64748b;} .xjgbzx-meta-value{font-weight:700;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:260px;} .xjgbzx-auth-badge{padding:2px 7px;border-radius:999px;border:1px solid #cbd5e1;font-size:11px;font-weight:900;} .xjgbzx-hidden{display:none !important;} .xjgbzx-btn-row{display:flex;gap:7px;margin-bottom:6px;} .xjgbzx-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;} .xjgbzx-btn-start{background:#16a34a;}.xjgbzx-btn-stop{background:#ef4444;} .xjgbzx-btn:disabled,.xjgbzx-btn-off{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;} .xjgbzx-btn-pro{flex:0 0 auto;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;border:none;padding:4px 8px;font-size:12px;border-radius:10px;cursor:pointer;font-weight:800;} .xjgbzx-start-hint{font-size:11px;color:#b45309;background:#fffbeb;border:1px solid #fcd34d;border-radius:7px;padding:5px 7px;margin-bottom:5px;text-align:center;font-weight:600;} .xjgbzx-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;} .xjgbzx-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;} .xjgbzx-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;} .xjgbzx-qq-row{display:flex;align-items:center;justify-content:space-between;gap:9px;margin-top:5px;} .xjgbzx-qq-text{font-size:12px;color:#475569;} .xjgbzx-qq-title{font-size:13px;font-weight:800;color:#0f172a;} .xjgbzx-qq-btn{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:5px 12px;border-radius:999px;font-size:12px;font-weight:800;cursor:pointer;} #xjgbzx-panel-footer{padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;} #xjgbzx-pro-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000001;display:none;align-items:center;justify-content:center;padding:20px;} .xjgbzx-pro-card{width:min(560px,92vw);max-height:88vh;overflow:auto;background:#fff;border-radius:18px;border:1px solid #dbe4f0;box-shadow:0 18px 46px rgba(15,23,42,.25);padding:16px;} .xjgbzx-pro-title{font-size:24px;font-weight:900;color:#0f172a;} .xjgbzx-pro-sub{font-size:13px;color:#64748b;margin-top:4px;line-height:1.45;} .xjgbzx-pro-sec{margin-top:12px;border:1px solid #dbe4f0;border-radius:12px;padding:12px;background:#f8fafc;} .xjgbzx-pro-sec h4{margin:0 0 8px;font-size:15px;color:#0f172a;} .xjgbzx-pro-sec p{margin:4px 0;font-size:13px;color:#334155;line-height:1.5;} .xjgbzx-pro-sec ul{margin:6px 0 0 18px;padding:0;} .xjgbzx-pro-sec li{margin:4px 0;font-size:13px;color:#334155;} .xjgbzx-pro-buy{display:flex;flex-direction:column;gap:10px;margin-top:8px;} .xjgbzx-pro-muted{color:#64748b;font-size:12px;line-height:1.5;} .xjgbzx-pro-buy-btn{display:inline-block;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 16px;border-radius:12px;font-size:14px;font-weight:800;border:none;cursor:pointer;align-self:flex-start;} .xjgbzx-pro-tip{margin-top:8px;background:#fef3c7;border:1px solid #fcd34d;border-radius:10px;padding:8px 10px;font-size:13px;color:#92400e;font-weight:700;line-height:1.45;} .xjgbzx-pro-tip-info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:600;} .xjgbzx-pro-actions{margin-top:14px;display:flex;justify-content:flex-end;} .xjgbzx-pro-close{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 18px;border-radius:12px;font-size:14px;font-weight:800;cursor:pointer;} `; document.documentElement.appendChild(style); } function fmtHour(v) { if (v == null || v === "") return "0"; return String(v); } function classHourProgress(c) { const need = Number(c.required_leaning_hour || c.required_period || c.learning_hour || 0); const done = Number(c.completed_required_leaning_hour || 0); if (!(need > 0)) return 0; return Math.max(0, Math.min(100, Math.round((done / need) * 100))); } function renderCourseList() { const box = document.getElementById("xjgbzx-course-list"); if (!box) return; const list = state.classes || []; if (!list.length) { box.innerHTML = `
${ isLoggedIn() ? "\u6682\u65e0\u672a\u5b8c\u6210\u4e13\u9898\u73ed" : "\u8bf7\u5148\u767b\u5f55\u540e\u5237\u65b0" }
`; return; } const selected = new Set(state.selectedIds.map(String)); box.innerHTML = list .map((c) => { const id = String(c.id); const name = escHtml(c.class_name || `\u4e13\u9898\u73ed${id}`); const begin = c.begin_date || "-"; const end = c.end_date || "-"; const reqNum = c.required_course_num != null ? c.required_course_num : c.course_num; const reqHour = fmtHour(c.required_leaning_hour || c.required_period || c.learning_hour); const eleNum = c.elective_course_num != null ? c.elective_course_num : 0; const eleHour = fmtHour(c.elective_leaning_hour != null ? c.elective_leaning_hour : 0); const doneHour = fmtHour(c.completed_required_leaning_hour || 0); const pct = classHourProgress(c); const checked = selected.has(id) ? "checked" : ""; return `
`; }) .join(""); box.querySelectorAll('input[type="checkbox"]').forEach((cb) => { cb.addEventListener("change", () => { const ids = Array.from(box.querySelectorAll('input[type="checkbox"]:checked')).map((el) => String(el.getAttribute("data-id")) ); saveQueue(ids); if (ids[0]) setSavedClassId(ids[0]); if (!state.running) { loadChaptersForClassIds(ids.length ? ids : state.classes[0] ? [String(state.classes[0].id)] : []) .then(() => renderChapterPreview()) .catch(() => {}); } updateQueueFooter(); }); }); updateQueueFooter(); } function renderChapterPreview() { const box = document.getElementById("xjgbzx-chapter-preview"); if (!box) return; const list = state.chapters || []; if (!list.length) { box.innerHTML = `
\u6682\u65e0\u7ae0\u8282\uff0c\u8bf7\u52fe\u9009\u4e13\u9898\u73ed\u6216\u70b9\u5237\u65b0
`; return; } const curId = state.currentCourseId != null ? String(state.currentCourseId) : ""; box.innerHTML = list .map((c) => { const id = String(c.id); const name = escHtml(c.course_name || c.name || `\u8bfe\u7a0b${id}`); const prog = Number(c.learning_progress || 0); const done = Number(c.is_completed) === 1 || prog >= 100; const hour = c.learning_hour != null ? `${c.learning_hour}\u5b66\u65f6` : ""; const dur = c.duration != null ? `${c.duration}\u5206\u949f` : ""; const lecturer = c.lecturer ? escHtml(c.lecturer) : ""; const mod = c._module ? escHtml(c._module) : ""; const curCls = id === curId ? " xjgbzx-ch-cur" : ""; const progCls = done ? "xjgbzx-ch-prog xjgbzx-ch-done" : "xjgbzx-ch-prog"; return `
${name}
${prog.toFixed(1)}% ${done ? "\u5df2\u5b8c\u6210" : "\u672a\u5b8c\u6210"} ${hour ? `${escHtml(hour)}` : ""} ${dur ? `${escHtml(dur)}` : ""} ${lecturer ? `${lecturer}` : ""} ${mod ? `${mod}` : ""}
`; }) .join(""); } function renderLog() { const box = document.getElementById("xjgbzx-run-log"); if (!box) return; if (!state.logLines.length) { box.innerHTML = `
\u6682\u65e0\u65e5\u5fd7
`; return; } box.innerHTML = state.logLines.map((l) => `
${escHtml(l)}
`).join(""); } function updateQueueFooter() { const el = document.getElementById("xjgbzx-queue-text"); if (!el) return; const n = state.selectedIds.length; el.textContent = n ? `\u5df2\u9009 ${n} \u4e2a\u4e13\u9898\u73ed` : "\u672a\u9009\u62e9\u4e13\u9898\u73ed"; } function updateStatusUI() { const status = document.getElementById("xjgbzx-auto-status"); const startBtn = document.getElementById("xjgbzx-start"); const stopBtn = document.getElementById("xjgbzx-stop"); const doneEl = document.getElementById("xjgbzx-queue-done"); const totalEl = document.getElementById("xjgbzx-queue-total"); const pctEl = document.getElementById("xjgbzx-queue-percent"); const bar = document.getElementById("xjgbzx-queue-progress"); const cur = document.getElementById("xjgbzx-current-course"); const task = document.getElementById("xjgbzx-current-task"); const prog = document.getElementById("xjgbzx-current-progress"); const running = state.running; if (status) { if (state.panelRefreshing) status.textContent = "\u5237\u65b0\u4e2d…"; else status.textContent = running ? (state.stopFlag ? "\u6b63\u5728\u505c\u6b62" : "\u8fd0\u884c\u4e2d") : "\u5df2\u505c\u6b62"; status.classList.toggle("on", running && !state.stopFlag); } if (startBtn) { startBtn.disabled = running; startBtn.classList.toggle("xjgbzx-btn-off", running); } if (stopBtn) { stopBtn.disabled = !running; stopBtn.classList.toggle("xjgbzx-btn-off", !running); } const total = state.totalCount || 0; const done = state.doneCount || 0; const pct = total > 0 ? Math.round((done / total) * 100) : 0; if (doneEl) doneEl.textContent = String(done); if (totalEl) totalEl.textContent = String(total); if (pctEl) pctEl.textContent = `${pct}%`; if (bar) bar.style.width = `${pct}%`; if (cur) { cur.textContent = state.currentName || "\u65e0"; cur.title = state.currentName || ""; } if (task) { task.textContent = state.currentTask || "\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u5b66\u4e60"; task.title = state.currentTask || ""; } if (prog) prog.textContent = `${state.currentProgress || 0}%`; updateCloudPanelUI(); updateQueueFooter(); } 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 applyPanelCollapsed(panel, collapsed) { const btnMin = panel.querySelector("#xjgbzx-btn-min"); const btnMax = panel.querySelector("#xjgbzx-btn-max"); panel.classList.toggle("xjgbzx-panel-min", !!collapsed); if (btnMin) btnMin.style.display = collapsed ? "none" : ""; if (btnMax) btnMax.style.display = collapsed ? "" : "none"; } function restorePanelPos(panel) { try { const raw = JSON.parse(localStorage.getItem(PANEL_POS_KEY) || "null"); if (!raw || typeof raw.left !== "number" || typeof raw.top !== "number") return; panel.style.left = `${raw.left}px`; panel.style.top = `${raw.top}px`; panel.style.right = "auto"; } catch (_) {} } function enableDrag(panel) { const header = panel.querySelector("#xjgbzx-panel-header"); if (!header) return; let dragging = false; let ox = 0; let oy = 0; header.addEventListener("mousedown", (e) => { if (e.target.closest("button")) return; dragging = true; const rect = panel.getBoundingClientRect(); ox = e.clientX - rect.left; oy = e.clientY - rect.top; e.preventDefault(); }); window.addEventListener("mousemove", (e) => { if (!dragging) return; const left = Math.max(0, Math.min(window.innerWidth - 80, e.clientX - ox)); const top = Math.max(0, Math.min(window.innerHeight - 40, e.clientY - oy)); panel.style.left = `${left}px`; panel.style.top = `${top}px`; panel.style.right = "auto"; }); window.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; const rect = panel.getBoundingClientRect(); localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left: rect.left, top: rect.top })); }); } function loadPanelLogo() { const img = document.getElementById("xjgbzx-panel-logo"); if (!img || !PANEL_LOGO_URL) return; const gmXhr = getGmXhr(); if (!gmXhr) return; gmXhr({ method: "GET", url: PANEL_LOGO_URL, responseType: "blob", anonymous: true, timeout: 20000, onload: (res) => { try { if (res.status < 200 || res.status >= 300 || !res.response) return; const blob = res.response; if (!(blob instanceof Blob) || !blob.size) return; img.src = URL.createObjectURL(blob); } catch (_) {} }, onerror: () => {}, ontimeout: () => {}, }); } function createPanel() { const old = document.getElementById("xjgbzx-auto-panel"); if (old) old.remove(); injectStyles(); const panel = document.createElement("div"); panel.id = "xjgbzx-auto-panel"; panel.innerHTML = `
${PRODUCT_NAME} v${SCRIPT_VERSION}
\u4e00\u952e\u5168\u81ea\u52a8\u5b8c\u6210 5\u500d\u901f\u64ad\u653e \u7701\u65f6\u7701\u5fc3
\u8fd0\u884c\u72b6\u6001 \u5df2\u505c\u6b62
0/0 \u89c6\u9891\u5df2\u5b66\u4e60
0%
\u672a\u5b8c\u6210\u4e13\u9898\u73ed
\u8bfe\u7a0b\u5217\u8868
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d\u4e13\u9898\u73ed
\u7ae0\u8282\u9884\u89c8 \u5b9e\u65f6\u8fdb\u5ea6
\u8bf7\u52fe\u9009\u4e13\u9898\u73ed
\u8fd0\u884c\u65e5\u5fd7
\u5b9e\u65f6\u65e5\u5fd7
\u6388\u6743\u4e0e\u9009\u9879 \u8bbe\u7f6e
\u7528\u6237\u7c7b\u578b\u672a\u6821\u9a8c
\u514d\u8d39\u4f53\u9a8c\u89c6\u98910/3
Token
\u4e91\u7aef\u670d\u52a1${DEFAULT_CLOUD_API_BASE}
\u5f53\u524d\u8bfe\u7a0b\u65e0
\u5f53\u524d\u8fdb\u5ea60%
\u5f53\u524d\u4efb\u52a1\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u5b66\u4e60
`; document.documentElement.appendChild(panel); createProModal(); loadPanelLogo(); restorePanelPos(panel); applyPanelCollapsed(panel, readPanelCollapsed()); enableDrag(panel); document.getElementById("xjgbzx-btn-min")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); document.getElementById("xjgbzx-btn-max")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); panel.querySelectorAll(".xjgbzx-tab-btn").forEach((btn) => { btn.addEventListener("click", () => { switchPanelTab(btn.dataset.tab); if (btn.dataset.tab === "chapter") renderChapterPreview(); }); }); document.getElementById("xjgbzx-refresh")?.addEventListener("click", () => { if (state.running) { setHint("\u8fd0\u884c\u4e2d\u8bf7\u5148\u505c\u6b62\u518d\u5237\u65b0"); return; } refreshCourses(); }); document.getElementById("xjgbzx-clear-log")?.addEventListener("click", () => { state.logLines = []; renderLog(); }); document.getElementById("xjgbzx-start")?.addEventListener("click", () => { if (state.running) return; runQueue(); }); document.getElementById("xjgbzx-stop")?.addEventListener("click", () => stopRun()); document.getElementById("xjgbzx-join-qq")?.addEventListener("click", () => { try { GM_openInTab(QQ_GROUP_LINK, { active: true, insert: true, setParent: true }); } catch (_) { window.open(QQ_GROUP_LINK, "_blank"); } }); document.getElementById("xjgbzx-open-pro")?.addEventListener("click", () => openProModal()); document.getElementById("xjgbzx-cloud-save")?.addEventListener("click", async () => { const input = document.getElementById("xjgbzx-cloud-token"); state.cloudToken = String((input && input.value) || "").trim(); state.cloudRevoked = false; localStorage.setItem(CLOUD_TOKEN_KEY, state.cloudToken); writeCloudLeaseCache("", 0); state.cloudLease = ""; state.cloudLeaseExp = 0; try { if (state.cloudToken) await cloudVerifyToken(); await _el(true); log("\u6388\u6743\u5df2\u66f4\u65b0"); } catch (err) { log("\u6821\u9a8c\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5 Token"); } updateCloudPanelUI(); }); const last = readCloudLastState(); if (last) { if (last.tier) state.cloudTier = last.tier; if (last.freeVideoLimit > 0) state.freeVideoLimit = last.freeVideoLimit; if (last.freeUsedVideos >= 0) state.freeUsedVideos = last.freeUsedVideos; } updateStatusUI(); renderLog(); renderChapterPreview(); void fetchClientConfig(); if (isLoggedIn()) { refreshCourses(); void _el(false).catch(() => {}); } else { setHint("\u8bf7\u5148\u767b\u5f55\u65b0\u7586\u5e72\u90e8\u5728\u7ebf"); log("\u672a\u767b\u5f55\uff0c\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d\u4e13\u9898\u73ed"); } } function boot() { if (!/xjgbzx\.cn$/i.test(location.hostname) && !/\.xjgbzx\.cn$/i.test(location.hostname)) return; if (window.__xjgbzxAutoBooted) return; window.__xjgbzxAutoBooted = true; createPanel(); setInterval(() => { if (isLoggedIn()) _el(false).catch(() => {}); }, 10 * 60 * 1000); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } })();