// ==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 = `
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