// ==UserScript== // @name 广东省干部培训网络学院 - 自动学习助手 // @namespace https://scriptcat.org/gdce-auto-study // @version 0.8.0 // @description 广东省干部培训网络学院自动学习脚本:视频监控、自动选课、自动切换课程、自动播放恢复 // @author ScriptCat Developer // @match https://gbpx.gd.gov.cn/gdceportal/study/* // @match https://gbpx.gd.gov.cn/gdceportal/Study/* // @match https://wcs1.shawcoder.xyz/gdcecw/play_pc/* // @grant GM_openInTab // @grant GM_setValue // @grant GM_getValue // @grant GM_addValueChangeListener // @grant GM_closeTab // @grant unsafeWindow // @run-at document-start // @license MIT // ==/UserScript== (function () { var pageWin = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; var _origAlert = pageWin.alert; var _origConfirm = pageWin.confirm; var _origPrompt = pageWin.prompt; pageWin.alert = function (msg) { console.log("[GDCE自动学习] 拦截alert: " + (msg || "").substring(0, 100)); }; pageWin.confirm = function (msg) { console.log("[GDCE自动学习] 拦截confirm: " + (msg || "").substring(0, 100) + " -> 自动返回true"); return true; }; pageWin.prompt = function (msg, def) { console.log("[GDCE自动学习] 拦截prompt: " + (msg || "").substring(0, 100)); return def || ""; }; var IS_VIDEO_PAGE = /shawcoder\.xyz/.test(location.hostname); var IS_STUDY_CENTER = /gbpx\.gd\.gov\.cn/.test(location.hostname); var _origWindowOpen = pageWin.open; function openInTab(url) { if (typeof GM_openInTab === "function") { console.log("[GDCE自动学习] 通过GM_openInTab打开: " + (url || "").substring(0, 80)); try { var tab = GM_openInTab(url, { active: true }); return tab || true; } catch (e) { console.log("[GDCE自动学习] GM_openInTab失败,回退到window.open: " + e.message); } } var win = _origWindowOpen.call(pageWin, url, "_blank"); if (!win) { console.log("[GDCE自动学习] window.open被弹窗拦截器阻止"); return false; } return win; } if (IS_VIDEO_PAGE) { var _lastAlertStatusTime = 0; var _layerAlertWatch = setInterval(function () { if (pageWin.layer && pageWin.layer.alert) { var origLayerAlert = pageWin.layer.alert; pageWin.layer.alert = function (msg, opts, callback) { console.log("[GDCE自动学习] 拦截layer.alert: " + (msg || "").substring(0, 100)); var idx = origLayerAlert.call(pageWin.layer, msg, opts, callback); setTimeout(function () { pageWin.layer.close(idx); }, 1500); var msgStr = String(msg || ""); var isCourseDone = msgStr.indexOf("不在学习中") !== -1 || msgStr.indexOf("已完成") !== -1; var alertStatus = isCourseDone ? "ended" : "error"; try { var now = Date.now(); if (typeof GM_setValue === "function" && now - _lastAlertStatusTime > 30000) { _lastAlertStatusTime = now; GM_setValue("gdce_video_status", { status: alertStatus, msg: msgStr, ts: now }); console.log("[GDCE自动学习] 已发送视频状态: " + alertStatus + " (layer.alert)"); } else { console.log("[GDCE自动学习] 忽略重复layer.alert状态发送(30秒防抖)"); } var msgType = isCourseDone ? "gdce-video-ended" : "gdce-video-error"; if (pageWin.parent && pageWin.parent !== pageWin.self) { pageWin.parent.postMessage({ type: msgType, msg: msgStr }, "*"); } if (pageWin.opener && !pageWin.opener.closed) { pageWin.opener.postMessage({ type: msgType, msg: msgStr }, "*"); } if (pageWin.top && pageWin.top !== pageWin.self) { pageWin.top.postMessage({ type: msgType, msg: msgStr }, "*"); } if (isCourseDone) { setTimeout(function () { console.log("[GDCE自动学习] 课程不在学习中,尝试关闭视频标签页(安全回退)"); if (typeof GM_closeTab === "function") { try { GM_closeTab(); } catch (e) {} } }, 10000); } } catch (e) {} return idx; }; clearInterval(_layerAlertWatch); } }, 500); setTimeout(function () { clearInterval(_layerAlertWatch); }, 30000); pageWin.open = function (url) { if (url && typeof url === "string") { if (url.indexOf("http") !== 0 && url.indexOf("//") !== 0) { try { url = new URL(url, location.href).href; } catch (e) {} } if (location.href.indexOf("playverif") !== -1 && url.indexOf("playdo") !== -1) { console.log("[GDCE自动学习] playverif页拦截window.open,在当前标签页导航到: " + url.substring(0, 80)); location.href = url; return null; } console.log("[GDCE自动学习] 视频页拦截window.open,通过GM_openInTab打开: " + url.substring(0, 80)); if (openInTab(url)) return null; } return _origWindowOpen.apply(this, arguments); }; } if (IS_STUDY_CENTER && window.self !== window.top) { pageWin.open = function (url) { if (url && typeof url === "string") { if (url.indexOf("http") !== 0 && url.indexOf("//") !== 0) { try { url = new URL(url, location.href).href; } catch (e) {} } console.log("[GDCE自动学习] iframe中拦截window.open,通过GM_openInTab打开: " + url.substring(0, 80)); if (openInTab(url)) return null; } return _origWindowOpen.apply(this, arguments); }; return; } var _isPopupLike = (pageWin.opener && !pageWin.opener.closed) || (window.self === window.top && /CourseDetail/i.test(location.href)); if (IS_STUDY_CENTER && _isPopupLike) { console.log("[GDCE自动学习] 检测到弹窗窗口/新标签页,URL: " + location.href.substring(0, 80)); function onReady() { var layerBtn = document.querySelector(".layui-layer-btn0, .layui-layer-close1"); if (layerBtn) layerBtn.click(); var btn = document.querySelector("#btnConfirm"); if (!btn) btn = document.querySelector("input[type='submit'][value='进入学习']"); if (btn) { console.log("[GDCE自动学习] 弹窗窗口:点击'进入学习'按钮"); btn.click(); } if (/CourseDetail/i.test(location.href)) { setTimeout(function () { console.log("[GDCE自动学习] CourseDetail页面,3秒后自动关闭"); pageWin.close(); }, 3000); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", onReady); } else { onReady(); } return; } var CONFIG = { CHECK_INTERVAL: 3000, PAUSE_CHECK_INTERVAL: 1500, STUCK_THRESHOLD: 15, MAX_RETRIES: 3, MAX_LOGS: 80, ACTION_DELAY: [800, 2000], SELECTORS: { secondIframe: "#secondIframe", thirdIframe: "#thirdIframe", dataMainIframe: "#dataMainIframe", courseTable: "#gvList", courseRow: "#gvList tr", continueStudyLink: "a.courseware-reed", completedCourseLink: "a.courseware-selected", courseNameLink: "a.courseName", enterStudyBtn: "#btnConfirm", navMyCourse: ".secondRouterLink", firstRouterLink: "span.firstRouterLink", childHrefItem: "span.childHref-item", childHrefContainer: "div.childHref", leftHrefChild: "div.leftHrefChild", creditedHours: "#ctl00_CPHMain_lblCredited_Num", requiredHours: "#ctl00_CPHMain_lblRequired_Num", layerConfirm: ".layui-layer-btn0", layerCancel: ".layui-layer-btn1", layerClose: ".layui-layer-close1", videoPlayer: "video" }, URL_PATTERNS: { studyCenter: /studyCenter\.aspx/, courseDetail: /CourseDetail\.aspx/, courseList: /thirdMain|myCourse|LearningCourse/, videoPlay: /shawcoder\.xyz|playverif|playdo/ }, STORAGE_PREFIX: "gdce_auto_study_" }; var Utils = { _logs: [], log: function (level, message) { var ts = new Date().toLocaleTimeString("zh-CN", { hour12: false }); this._logs.push({ timestamp: ts, level: level, message: message }); if (this._logs.length > CONFIG.MAX_LOGS) this._logs.shift(); var p = "[GDCE自动学习]"; if (level === "error") console.error(p + " [" + ts + "] " + message); else if (level === "warn") console.warn(p + " [" + ts + "] " + message); else console.log(p + " [" + ts + "] " + message); if (typeof StatusPanel !== "undefined" && StatusPanel._panel) StatusPanel.updateLogs(); }, info: function (m) { this.log("info", m); }, warn: function (m) { this.log("warn", m); }, error: function (m) { this.log("error", m); }, getLogs: function () { return this._logs.slice(); }, qsIframes: function (sel, root) { root = root || document; var el = root.querySelector(sel); if (el) return el; var frames = root.querySelectorAll("iframe"); for (var i = 0; i < frames.length; i++) { try { var doc = frames[i].contentDocument || (frames[i].contentWindow && frames[i].contentWindow.document); if (!doc) continue; var found = this.qsIframes(sel, doc); if (found) return found; } catch (e) { } } return null; }, qsaIframes: function (sel, root) { root = root || document; var results = Array.prototype.slice.call(root.querySelectorAll(sel)); var frames = root.querySelectorAll("iframe"); for (var i = 0; i < frames.length; i++) { try { var doc = frames[i].contentDocument || (frames[i].contentWindow && frames[i].contentWindow.document); if (!doc) continue; results = results.concat(this.qsaIframes(sel, doc)); } catch (e) { } } return results; }, getIframeDoc: function (iframeSelector) { var iframe = document.querySelector(iframeSelector); if (!iframe) return null; try { return iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document); } catch (e) { this.warn("无法访问iframe: " + iframeSelector); return null; } }, waitForElement: function (selector, timeout, root) { timeout = timeout || 30000; root = root || document; return new Promise(function (resolve, reject) { var el = root.querySelector(selector); if (el) return resolve(el); var observer = new MutationObserver(function () { var found = root.querySelector(selector); if (found) { observer.disconnect(); resolve(found); } }); observer.observe(root, { childList: true, subtree: true }); setTimeout(function () { observer.disconnect(); reject(new Error("等待元素超时: " + selector)); }, timeout); }); }, waitForElementIframes: function (selector, timeout) { timeout = timeout || 60000; var self = this; return new Promise(function (resolve, reject) { var el = self.qsIframes(selector); if (el) return resolve(el); var start = Date.now(); var timer = setInterval(function () { var found = self.qsIframes(selector); if (found) { clearInterval(timer); resolve(found); } else if (Date.now() - start > timeout) { clearInterval(timer); reject(new Error("等待元素超时(iframe穿透): " + selector)); } }, 1000); }); }, waitForElementDisappearIframes: function (selector, timeout) { timeout = timeout || 15000; var self = this; return new Promise(function (resolve) { var el = self.qsIframes(selector); if (!el) return resolve(true); var start = Date.now(); var timer = setInterval(function () { var found = self.qsIframes(selector); if (!found) { clearInterval(timer); resolve(true); } else if (Date.now() - start > timeout) { clearInterval(timer); resolve(false); } }, 500); }); }, findByText: function (tagName, text, root) { root = root || document; var els = root.querySelectorAll(tagName); for (var i = 0; i < els.length; i++) { if (els[i].textContent.indexOf(text) !== -1) return els[i]; } return null; }, findByTextIframes: function (tagName, text) { var all = this.qsaIframes(tagName); for (var i = 0; i < all.length; i++) { if (all[i].textContent.indexOf(text) !== -1) return all[i]; } return null; }, safeClick: function (element) { if (!element) return false; try { element.click(); return true; } catch (e) { this.error("点击元素失败: " + e.message); return false; } }, observeDOM: function (target, options, callback) { var observer = new MutationObserver(callback); observer.observe(target, options); return observer; }, sleep: function (ms) { return new Promise(function (r) { setTimeout(r, ms); }); }, randomDelay: function () { var min = CONFIG.ACTION_DELAY[0], max = CONFIG.ACTION_DELAY[1]; var ms = Math.floor(Math.random() * (max - min) + min); return this.sleep(ms); }, getConfig: function (key, defaultValue) { try { var raw = localStorage.getItem(CONFIG.STORAGE_PREFIX + key); return raw !== null ? JSON.parse(raw) : defaultValue; } catch (e) { return defaultValue; } }, setConfig: function (key, value) { try { localStorage.setItem(CONFIG.STORAGE_PREFIX + key, JSON.stringify(value)); } catch (e) { this.error("保存配置失败: " + e.message); } }, formatTime: function (seconds) { if (!seconds || isNaN(seconds)) return "00:00"; var h = Math.floor(seconds / 3600); var m = Math.floor((seconds % 3600) / 60); var s = Math.floor(seconds % 60); if (h > 0) return h + ":" + ("0" + m).slice(-2) + ":" + ("0" + s).slice(-2); return ("0" + m).slice(-2) + ":" + ("0" + s).slice(-2); } }; var PageDetector = { _currentPage: null, _isInIframe: false, init: function () { this._isInIframe = window.self !== window.top; this._currentPage = this.detectPageType(); Utils.info("页面检测 -> 类型: " + this._currentPage + ", iframe: " + this._isInIframe + ", URL: " + location.pathname); }, detectPageType: function () { var path = location.pathname; if (CONFIG.URL_PATTERNS.studyCenter.test(path)) return "studyCenter"; if (CONFIG.URL_PATTERNS.courseDetail.test(path)) return "courseDetail"; if (CONFIG.URL_PATTERNS.videoPlay.test(path)) return "videoPlay"; if (CONFIG.URL_PATTERNS.courseList.test(path)) return "courseList"; if (Utils.qsIframes(CONFIG.SELECTORS.videoPlayer)) return "videoPlay"; if (Utils.qsIframes(CONFIG.SELECTORS.enterStudyBtn)) return "courseDetail"; if (Utils.qsIframes(CONFIG.SELECTORS.courseTable)) return "courseList"; return "unknown"; }, get currentPage() { return this._currentPage; }, get isInIframe() { return this._isInIframe; }, isTopFrame: function () { return window.self === window.top; }, onPageChange: function (callback) { var lastHref = location.href; var self = this; var origPush = history.pushState; history.pushState = function () { origPush.apply(this, arguments); if (location.href !== lastHref) { lastHref = location.href; self._currentPage = self.detectPageType(); callback(self._currentPage); } }; window.addEventListener("popstate", function () { if (location.href !== lastHref) { lastHref = location.href; self._currentPage = self.detectPageType(); callback(self._currentPage); } }); var observer = new MutationObserver(function () { var newType = self.detectPageType(); if (newType !== self._currentPage && newType !== "unknown") { self._currentPage = newType; callback(newType); } }); observer.observe(document, { childList: true, subtree: true }); } }; var PageAnalyzer = { analyze: function () { Utils.info("========== 页面结构分析开始 =========="); this._dumpIframeTree(document, 0); this._findKeyElements(); this._dumpAllIframeContent(); this._dumpTableDetail(); Utils.info("========== 页面结构分析结束 =========="); }, _dumpIframeTree: function (doc, depth) { var indent = ""; for (var d = 0; d < depth; d++) indent += " "; var iframes = doc.querySelectorAll("iframe"); Utils.info(indent + "iframe数量: " + iframes.length); for (var i = 0; i < iframes.length; i++) { var iframe = iframes[i]; var src = iframe.src || iframe.getAttribute("src") || "(无src)"; var id = iframe.id || "(无id)"; var cls = iframe.className || "(无class)"; Utils.info(indent + " iframe[" + i + "] id=" + id + " class=" + cls + " src=" + src); try { var childDoc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document); if (childDoc) { this._dumpIframeTree(childDoc, depth + 1); } else { Utils.info(indent + " (无法访问 - 可能跨域)"); } } catch (e) { Utils.info(indent + " (无法访问: " + e.message + ")"); } } }, _findKeyElements: function () { var selectors = { "secondIframe": CONFIG.SELECTORS.secondIframe, "thirdIframe": CONFIG.SELECTORS.thirdIframe, "courseItem": CONFIG.SELECTORS.courseItem, "enterStudyBtn": CONFIG.SELECTORS.enterStudyBtn, "nextSectionBtn": CONFIG.SELECTORS.nextSectionBtn, "creditedHours": CONFIG.SELECTORS.creditedHours, "requiredHours": CONFIG.SELECTORS.requiredHours, "layerConfirm": CONFIG.SELECTORS.layerConfirm, "videoPlayer": CONFIG.SELECTORS.videoPlayer }; Utils.info("--- 选择器匹配结果 ---"); var keys = Object.keys(selectors); for (var i = 0; i < keys.length; i++) { var name = keys[i]; var sel = selectors[name]; var found = Utils.qsIframes(sel); var count = Utils.qsaIframes(sel).length; Utils.info(" " + name + " (" + sel + "): " + (found ? "找到" : "未找到") + (count > 0 ? " (共" + count + "个)" : "")); } Utils.info("--- 文本搜索结果 ---"); var textSearches = ["进入学习", "继续学习", "我的课程", "课程", "下一节", "下一课", "开始学习", "学习", "操作"]; for (var j = 0; j < textSearches.length; j++) { var text = textSearches[j]; var el = Utils.findByTextIframes("a, button, span, div, li, p, input", text); Utils.info(" 文本\"" + text + "\": " + (el ? "找到 " + this._describeElement(el) : "未找到")); } }, _dumpAllIframeContent: function () { Utils.info("--- 所有iframe详细内容 ---"); this._dumpDocContent(document, "顶层", 0); }, _dumpDocContent: function (doc, label, depth) { if (depth > 5) return; var indent = ""; for (var d = 0; d < depth; d++) indent += " "; try { Utils.info(indent + "[" + label + "] URL: " + (doc.location || doc.defaultView.location).href); } catch (e) { Utils.info(indent + "[" + label + "] URL: (无法获取)"); } var body = doc.body; if (!body) { Utils.info(indent + " (无body)"); return; } this._dumpElementTree(body, indent + " ", 0, 3); var iframes = doc.querySelectorAll("iframe"); for (var i = 0; i < iframes.length; i++) { try { var childDoc = iframes[i].contentDocument || (iframes[i].contentWindow && iframes[i].contentWindow.document); if (childDoc) { this._dumpDocContent(childDoc, "iframe[" + i + "] #" + (iframes[i].id || ""), depth + 1); } } catch (e) { Utils.info(indent + " iframe[" + i + "]: 无法访问"); } } }, _dumpElementTree: function (el, indent, depth, maxDepth) { if (depth > maxDepth) return; var children = el.children; for (var i = 0; i < children.length; i++) { var child = children[i]; var tag = child.tagName.toLowerCase(); if (tag === "script" || tag === "style" || tag === "link") continue; var id = child.id ? "#" + child.id : ""; var cls = child.className && typeof child.className === "string" ? "." + child.className.split(" ").join(".") : ""; var text = (child.textContent || "").trim().replace(/\s+/g, " ").substring(0, 80); var childCount = child.children.length; Utils.info(indent + tag + id + cls + (childCount > 0 ? " [" + childCount + "子元素]" : "") + " \"" + text + "\""); if (tag === "a" || tag === "button" || tag === "input" || tag === "video") { var outer = child.outerHTML.substring(0, 300); Utils.info(indent + " -> HTML: " + outer); } this._dumpElementTree(child, indent + " ", depth + 1, maxDepth); } }, _dumpTableDetail: function () { Utils.info("--- 表格详细分析(课程列表) ---"); var tables = Utils.qsaIframes("table"); Utils.info("找到 " + tables.length + " 个table元素"); for (var t = 0; t < tables.length; t++) { var table = tables[t]; var tid = table.id ? "#" + table.id : ""; var tcls = table.className ? "." + table.className.split(" ").join(".") : ""; Utils.info("table[" + t + "]" + tid + tcls); var rows = table.querySelectorAll("tr"); Utils.info(" 共 " + rows.length + " 行"); for (var r = 0; r < Math.min(rows.length, 10); r++) { var row = rows[r]; var cells = row.querySelectorAll("td, th"); var rowInfo = " 行[" + r + "]: "; for (var c = 0; c < cells.length; c++) { var cell = cells[c]; var cellText = (cell.textContent || "").trim().replace(/\s+/g, " ").substring(0, 40); rowInfo += "[" + cellText + "] "; } Utils.info(rowInfo); var links = row.querySelectorAll("a, button, input"); for (var l = 0; l < links.length; l++) { Utils.info(" 链接/按钮: " + links[l].outerHTML.substring(0, 300)); } } } }, _describeElement: function (el) { if (!el) return "(null)"; var tag = el.tagName || "?"; var id = el.id ? "#" + el.id : ""; var cls = el.className && typeof el.className === "string" ? "." + el.className.split(" ").join(".") : ""; var text = (el.textContent || "").trim().substring(0, 40); return tag + id + cls + " \"" + text + "\""; } }; function des(beinetkey, message, encrypt, mode, iv) { var spfunction1 = new Array(0x1010400, 0, 0x10000, 0x1010404, 0x1010004, 0x10404, 0x4, 0x10000, 0x400, 0x1010400, 0x1010404, 0x400, 0x1000404, 0x1010004, 0x1000000, 0x4, 0x404, 0x1000400, 0x1000400, 0x10400, 0x10400, 0x1010000, 0x1010000, 0x1000404, 0x10004, 0x1000004, 0x1000004, 0x10004, 0, 0x404, 0x10404, 0x1000000, 0x10000, 0x1010404, 0x4, 0x1010000, 0x1010400, 0x1000000, 0x1000000, 0x400, 0x1010004, 0x10000, 0x10400, 0x1000004, 0x400, 0x4, 0x1000404, 0x10404, 0x1010404, 0x10004, 0x1010000, 0x1000404, 0x1000004, 0x404, 0x10404, 0x1010400, 0x404, 0x1000400, 0x1000400, 0, 0x10004, 0x10400, 0, 0x1010004); var spfunction2 = new Array(-0x7fef7fe0, -0x7fff8000, 0x8000, 0x108020, 0x100000, 0x20, -0x7fefffe0, -0x7fff7fe0, -0x7fffffe0, -0x7fef7fe0, -0x7fef8000, -0x80000000, -0x7fff8000, 0x100000, 0x20, -0x7fefffe0, 0x108000, 0x100020, -0x7fff7fe0, 0, -0x80000000, 0x8000, 0x108020, -0x7ff00000, 0x100020, -0x7fffffe0, 0, 0x108000, 0x8020, -0x7fef8000, -0x7ff00000, 0x8020, 0, 0x108020, -0x7fefffe0, 0x100000, -0x7fff7fe0, -0x7ff00000, -0x7fef8000, 0x8000, -0x7ff00000, -0x7fff8000, 0x20, -0x7fef7fe0, 0x108020, 0x20, 0x8000, -0x80000000, 0x8020, -0x7fef8000, 0x100000, -0x7fffffe0, 0x100020, -0x7fff7fe0, -0x7fffffe0, 0x100020, 0x108000, 0, -0x7fff8000, 0x8020, -0x80000000, -0x7fefffe0, -0x7fef7fe0, 0x108000); var spfunction3 = new Array(0x208, 0x8020200, 0, 0x8020008, 0x8000200, 0, 0x20208, 0x8000200, 0x20008, 0x8000008, 0x8000008, 0x20000, 0x8020208, 0x20008, 0x8020000, 0x208, 0x8000000, 0x8, 0x8020200, 0x200, 0x20200, 0x8020000, 0x8020008, 0x20208, 0x8000208, 0x20200, 0x20000, 0x8000208, 0x8, 0x8020208, 0x200, 0x8000000, 0x8020200, 0x8000000, 0x20008, 0x208, 0x20000, 0x8020200, 0x8000200, 0, 0x200, 0x20008, 0x8020208, 0x8000200, 0x8000008, 0x200, 0, 0x8020008, 0x8000208, 0x20000, 0x8000000, 0x8020208, 0x8, 0x20208, 0x20200, 0x8000008, 0x8020000, 0x8000208, 0x208, 0x8020000, 0x20208, 0x8, 0x8020008, 0x20200); var spfunction4 = new Array(0x802001, 0x2081, 0x2081, 0x80, 0x802080, 0x800081, 0x800001, 0x2001, 0, 0x802000, 0x802000, 0x802081, 0x81, 0, 0x800080, 0x800001, 0x1, 0x2000, 0x800000, 0x802001, 0x80, 0x800000, 0x2001, 0x2080, 0x800081, 0x1, 0x2080, 0x800080, 0x2000, 0x802080, 0x802081, 0x81, 0x800080, 0x800001, 0x802000, 0x802081, 0x81, 0, 0, 0x802000, 0x2080, 0x800080, 0x800081, 0x1, 0x802001, 0x2081, 0x2081, 0x80, 0x802081, 0x81, 0x1, 0x2000, 0x800001, 0x2001, 0x802080, 0x800081, 0x2001, 0x2080, 0x800000, 0x802001, 0x80, 0x800000, 0x2000, 0x802080); var spfunction5 = new Array(0x100, 0x2080100, 0x2080000, 0x42000100, 0x80000, 0x100, 0x40000000, 0x2080000, 0x40080100, 0x80000, 0x2000100, 0x40080100, 0x42000100, 0x42080000, 0x80100, 0x40000000, 0x2000000, 0x40080000, 0x40080000, 0, 0x40000100, 0x42080100, 0x42080100, 0x2000100, 0x42080000, 0x40000100, 0, 0x42000000, 0x2080100, 0x2000000, 0x42000000, 0x80100, 0x80000, 0x42000100, 0x100, 0x2000000, 0x40000000, 0x2080000, 0x42000100, 0x40080100, 0x2000100, 0x40000000, 0x42080000, 0x2080100, 0x40080100, 0x100, 0x2000000, 0x42080000, 0x42080100, 0x80100, 0x42000000, 0x42080100, 0x2080000, 0, 0x40080000, 0x42000000, 0x80100, 0x2000100, 0x40000100, 0x80000, 0, 0x40080000, 0x2080100, 0x40000100); var spfunction6 = new Array(0x20000010, 0x20400000, 0x4000, 0x20404010, 0x20400000, 0x10, 0x20404010, 0x400000, 0x20004000, 0x404010, 0x400000, 0x20000010, 0x400010, 0x20004000, 0x20000000, 0x4010, 0, 0x400010, 0x20004010, 0x4000, 0x404000, 0x20004010, 0x10, 0x20400010, 0x20400010, 0, 0x404010, 0x20404000, 0x4010, 0x404000, 0x20404000, 0x20000000, 0x20004000, 0x10, 0x20400010, 0x404000, 0x20404010, 0x400000, 0x4010, 0x20000010, 0x400000, 0x20004000, 0x20000000, 0x4010, 0x20000010, 0x20404010, 0x404000, 0x20400000, 0x404010, 0x20404000, 0, 0x20400010, 0x10, 0x4000, 0x20400000, 0x404010, 0x4000, 0x400010, 0x20004010, 0, 0x20404000, 0x20000000, 0x400010, 0x20004010); var spfunction7 = new Array(0x200000, 0x4200002, 0x4000802, 0, 0x800, 0x4000802, 0x200802, 0x4200800, 0x4200802, 0x200000, 0, 0x4000002, 0x2, 0x4000000, 0x4200002, 0x802, 0x4000800, 0x200802, 0x200002, 0x4000800, 0x4000002, 0x4200000, 0x4200800, 0x200002, 0x4200000, 0x800, 0x802, 0x4200802, 0x200800, 0x2, 0x4000000, 0x200800, 0x4000000, 0x200800, 0x200000, 0x4000802, 0x4000802, 0x4200002, 0x4200002, 0x2, 0x200002, 0x4000000, 0x4000800, 0x200000, 0x4200800, 0x802, 0x200802, 0x4200800, 0x802, 0x4000002, 0x4200802, 0x4200000, 0x200800, 0, 0x2, 0x4200802, 0, 0x200802, 0x4200000, 0x800, 0x4000002, 0x4000800, 0x800, 0x200002); var spfunction8 = new Array(0x10001040, 0x1000, 0x40000, 0x10041040, 0x10000000, 0x10001040, 0x40, 0x10000000, 0x40040, 0x10040000, 0x10041040, 0x41000, 0x10041000, 0x41040, 0x1000, 0x40, 0x10040000, 0x10000040, 0x10001000, 0x1040, 0x41000, 0x40040, 0x10040040, 0x10041000, 0x1040, 0, 0, 0x10040040, 0x10000040, 0x10001000, 0x41040, 0x40000, 0x41040, 0x40000, 0x10041000, 0x1000, 0x40, 0x10040040, 0x1000, 0x41040, 0x10001000, 0x40, 0x10000040, 0x10040000, 0x10040040, 0x10000000, 0x40000, 0x10001040, 0, 0x10041040, 0x40040, 0x10000040, 0x10040000, 0x10001000, 0x10001040, 0, 0x10041040, 0x41000, 0x41000, 0x1040, 0x1040, 0x40040, 0x10000000, 0x10041000); var keys = des_createKeys(beinetkey); var m = 0, i, j, temp, temp2, right1, right2, left, right, looping; var cbcleft, cbcleft2, cbcright, cbcright2 var endloop, loopinc; var len = message.length; var chunk = 0; var iterations = keys.length == 32 ? 3 : 9; if (iterations == 3) { looping = encrypt ? new Array(0, 32, 2) : new Array(30, -2, -2); } else { looping = encrypt ? new Array(0, 32, 2, 62, 30, -2, 64, 96, 2) : new Array(94, 62, -2, 32, 64, 2, 30, -2, -2); } message += '\0\0\0\0\0\0\0\0'; result = ''; tempresult = ''; if (mode == 1) { cbcleft = (iv.charCodeAt(m++) << 24) | (iv.charCodeAt(m++) << 16) | (iv.charCodeAt(m++) << 8) | iv.charCodeAt(m++); cbcright = (iv.charCodeAt(m++) << 24) | (iv.charCodeAt(m++) << 16) | (iv.charCodeAt(m++) << 8) | iv.charCodeAt(m++); m = 0; } while (m < len) { if (encrypt) { left = (message.charCodeAt(m++) << 16) | message.charCodeAt(m++); right = (message.charCodeAt(m++) << 16) | message.charCodeAt(m++); } else { left = (message.charCodeAt(m++) << 24) | (message.charCodeAt(m++) << 16) | (message.charCodeAt(m++) << 8) | message.charCodeAt(m++); right = (message.charCodeAt(m++) << 24) | (message.charCodeAt(m++) << 16) | (message.charCodeAt(m++) << 8) | message.charCodeAt(m++); } if (mode == 1) { if (encrypt) { left ^= cbcleft; right ^= cbcright; } else { cbcleft2 = cbcleft; cbcright2 = cbcright; cbcleft = left; cbcright = right; } } temp = ((left >>> 4) ^ right) & 0x0f0f0f0f; right ^= temp; left ^= (temp << 4); temp = ((left >>> 16) ^ right) & 0x0000ffff; right ^= temp; left ^= (temp << 16); temp = ((right >>> 2) ^ left) & 0x33333333; left ^= temp; right ^= (temp << 2); temp = ((right >>> 8) ^ left) & 0x00ff00ff; left ^= temp; right ^= (temp << 8); temp = ((left >>> 1) ^ right) & 0x55555555; right ^= temp; left ^= (temp << 1); left = ((left << 1) | (left >>> 31)); right = ((right << 1) | (right >>> 31)); for (j = 0; j < iterations; j += 3) { endloop = looping[j + 1]; loopinc = looping[j + 2]; for (i = looping[j]; i != endloop; i += loopinc) { right1 = right ^ keys[i]; right2 = ((right >>> 4) | (right << 28)) ^ keys[i + 1]; temp = left; left = right; right = temp ^ (spfunction2[(right1 >>> 24) & 0x3f] | spfunction4[(right1 >>> 16) & 0x3f] | spfunction6[(right1 >>> 8) & 0x3f] | spfunction8[right1 & 0x3f] | spfunction1[(right2 >>> 24) & 0x3f] | spfunction3[(right2 >>> 16) & 0x3f] | spfunction5[(right2 >>> 8) & 0x3f] | spfunction7[right2 & 0x3f]); } temp = left; left = right; right = temp; } left = ((left >>> 1) | (left << 31)); right = ((right >>> 1) | (right << 31)); temp = ((left >>> 1) ^ right) & 0x55555555; right ^= temp; left ^= (temp << 1); temp = ((right >>> 8) ^ left) & 0x00ff00ff; left ^= temp; right ^= (temp << 8); temp = ((right >>> 2) ^ left) & 0x33333333; left ^= temp; right ^= (temp << 2); temp = ((left >>> 16) ^ right) & 0x0000ffff; right ^= temp; left ^= (temp << 16); temp = ((left >>> 4) ^ right) & 0x0f0f0f0f; right ^= temp; left ^= (temp << 4); if (mode == 1) { if (encrypt) { cbcleft = left; cbcright = right; } else { left ^= cbcleft2; right ^= cbcright2; } } if (encrypt) { tempresult += String.fromCharCode((left >>> 24), ((left >>> 16) & 0xff), ((left >>> 8) & 0xff), (left & 0xff), (right >>> 24), ((right >>> 16) & 0xff), ((right >>> 8) & 0xff), (right & 0xff)); } else { if (((left >>> 16) & 0xffff) != 0) { tempresult += String.fromCharCode(((left >>> 16) & 0xffff)); } if ((left & 0xffff) != 0) { tempresult += String.fromCharCode((left & 0xffff)); } if (((right >>> 16) & 0xffff) != 0) { tempresult += String.fromCharCode(((right >>> 16) & 0xffff)); } if ((right & 0xffff) != 0) { tempresult += String.fromCharCode((right & 0xffff)); } } encrypt ? chunk += 16 : chunk += 8; if (chunk == 512) { result += tempresult; tempresult = ''; chunk = 0; } } return result + tempresult; } function des_createKeys(beinetkey) { var pc2bytes0 = new Array(0, 0x4, 0x20000000, 0x20000004, 0x10000, 0x10004, 0x20010000, 0x20010004, 0x200, 0x204, 0x20000200, 0x20000204, 0x10200, 0x10204, 0x20010200, 0x20010204); var pc2bytes1 = new Array(0, 0x1, 0x100000, 0x100001, 0x4000000, 0x4000001, 0x4100000, 0x4100001, 0x100, 0x101, 0x100100, 0x100101, 0x4000100, 0x4000101, 0x4100100, 0x4100101); var pc2bytes2 = new Array(0, 0x8, 0x800, 0x808, 0x1000000, 0x1000008, 0x1000800, 0x1000808, 0, 0x8, 0x800, 0x808, 0x1000000, 0x1000008, 0x1000800, 0x1000808); var pc2bytes3 = new Array(0, 0x200000, 0x8000000, 0x8200000, 0x2000, 0x202000, 0x8002000, 0x8202000, 0x20000, 0x220000, 0x8020000, 0x8220000, 0x22000, 0x222000, 0x8022000, 0x8222000); var pc2bytes4 = new Array(0, 0x40000, 0x10, 0x40010, 0, 0x40000, 0x10, 0x40010, 0x1000, 0x41000, 0x1010, 0x41010, 0x1000, 0x41000, 0x1010, 0x41010); var pc2bytes5 = new Array(0, 0x400, 0x20, 0x420, 0, 0x400, 0x20, 0x420, 0x2000000, 0x2000400, 0x2000020, 0x2000420, 0x2000000, 0x2000400, 0x2000020, 0x2000420); var pc2bytes6 = new Array(0, 0x10000000, 0x80000, 0x10080000, 0x2, 0x10000002, 0x80002, 0x10080002, 0, 0x10000000, 0x80000, 0x10080000, 0x2, 0x10000002, 0x80002, 0x10080002); var pc2bytes7 = new Array(0, 0x10000, 0x800, 0x10800, 0x20000000, 0x20010000, 0x20000800, 0x20010800, 0x20000, 0x30000, 0x20800, 0x30800, 0x20020000, 0x20030000, 0x20020800, 0x20030800); var pc2bytes8 = new Array(0, 0x40000, 0, 0x40000, 0x2, 0x40002, 0x2, 0x40002, 0x2000000, 0x2040000, 0x2000000, 0x2040000, 0x2000002, 0x2040002, 0x2000002, 0x2040002); var pc2bytes9 = new Array(0, 0x10000000, 0x8, 0x10000008, 0, 0x10000000, 0x8, 0x10000008, 0x400, 0x10000400, 0x408, 0x10000408, 0x400, 0x10000400, 0x408, 0x10000408); var pc2bytes10 = new Array(0, 0x20, 0, 0x20, 0x100000, 0x100020, 0x100000, 0x100020, 0x2000, 0x2020, 0x2000, 0x2020, 0x102000, 0x102020, 0x102000, 0x102020); var pc2bytes11 = new Array(0, 0x1000000, 0x200, 0x1000200, 0x200000, 0x1200000, 0x200200, 0x1200200, 0x4000000, 0x5000000, 0x4000200, 0x5000200, 0x4200000, 0x5200000, 0x4200200, 0x5200200); var pc2bytes12 = new Array(0, 0x1000, 0x8000000, 0x8001000, 0x80000, 0x81000, 0x8080000, 0x8081000, 0x10, 0x1010, 0x8000010, 0x8001010, 0x80010, 0x81010, 0x8080010, 0x8081010); var pc2bytes13 = new Array(0, 0x4, 0x100, 0x104, 0, 0x4, 0x100, 0x104, 0x1, 0x5, 0x101, 0x105, 0x1, 0x5, 0x101, 0x105); var iterations = beinetkey.length >= 24 ? 3 : 1; var keys = new Array(32 * iterations); var shifts = new Array(0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0); var lefttemp, righttemp, m = 0, n = 0, temp; for (var j = 0; j < iterations; j++) { left = (beinetkey.charCodeAt(m++) << 24) | (beinetkey.charCodeAt(m++) << 16) | (beinetkey.charCodeAt(m++) << 8) | beinetkey.charCodeAt(m++); right = (beinetkey.charCodeAt(m++) << 24) | (beinetkey.charCodeAt(m++) << 16) | (beinetkey.charCodeAt(m++) << 8) | beinetkey.charCodeAt(m++); temp = ((left >>> 4) ^ right) & 0x0f0f0f0f; right ^= temp; left ^= (temp << 4); temp = ((right >>> -16) ^ left) & 0x0000ffff; left ^= temp; right ^= (temp << -16); temp = ((left >>> 2) ^ right) & 0x33333333; right ^= temp; left ^= (temp << 2); temp = ((right >>> -16) ^ left) & 0x0000ffff; left ^= temp; right ^= (temp << -16); temp = ((left >>> 1) ^ right) & 0x55555555; right ^= temp; left ^= (temp << 1); temp = ((right >>> 8) ^ left) & 0x00ff00ff; left ^= temp; right ^= (temp << 8); temp = ((left >>> 1) ^ right) & 0x55555555; right ^= temp; left ^= (temp << 1); temp = (left << 8) | ((right >>> 20) & 0x000000f0); left = (right << 24) | ((right << 8) & 0xff0000) | ((right >>> 8) & 0xff00) | ((right >>> 24) & 0xf0); right = temp; for (i = 0; i < shifts.length; i++) { if (shifts[i]) { left = (left << 2) | (left >>> 26); right = (right << 2) | (right >>> 26); } else { left = (left << 1) | (left >>> 27); right = (right << 1) | (right >>> 27); } left &= -0xf; right &= -0xf; lefttemp = pc2bytes0[left >>> 28] | pc2bytes1[(left >>> 24) & 0xf] | pc2bytes2[(left >>> 20) & 0xf] | pc2bytes3[(left >>> 16) & 0xf] | pc2bytes4[(left >>> 12) & 0xf] | pc2bytes5[(left >>> 8) & 0xf] | pc2bytes6[(left >>> 4) & 0xf]; righttemp = pc2bytes7[right >>> 28] | pc2bytes8[(right >>> 24) & 0xf] | pc2bytes9[(right >>> 20) & 0xf] | pc2bytes10[(right >>> 16) & 0xf] | pc2bytes11[(right >>> 12) & 0xf] | pc2bytes12[(right >>> 8) & 0xf] | pc2bytes13[(right >>> 4) & 0xf]; temp = ((righttemp >>> 16) ^ lefttemp) & 0x0000ffff; keys[n++] = lefttemp ^ temp; keys[n++] = righttemp ^ (temp << 16); } } return keys; } function stringToHex(s) { var r = ''; var hexes = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'); for (var i = 0; i < (s.length); i++) { r += hexes[s.charCodeAt(i) >> 4] + hexes[s.charCodeAt(i) & 0xf]; } return r; } function hexToString(s) { var r = ""; for (var i = 0; i < s.length; i += 2) { var sxx = parseInt(s.substring(i, i + 2), 16); r += String.fromCharCode(sxx); } return r; } function encMe(s, k) { return stringToHex(des(k, s, 1, 0)); } function uncMe(s, k) { return des(k, hexToString(s), 0, 0); } var VideoMonitor = { _video: null, _isMonitoring: false, _monitorTimer: null, _pauseCheckTimer: null, _lastProgress: 0, _stuckCount: 0, _isRateLocked: false, _onVideoEnd: null, _onError: null, _handlers: {}, _userInteracted: false, _autoplayRetries: 0, _maxAutoplayRetries: 30, _noSourceRetries: 0, _maxNoSourceRetries: 10, _lastHeartbeatTs: 0, HEARTBEAT_INTERVAL: 50000, init: function (onVideoEnd, onError) { this._onVideoEnd = onVideoEnd; this._onError = onError; Utils.info("视频监控模块初始化"); var self = this; var events = ["click", "keydown", "touchstart"]; events.forEach(function (evt) { document.addEventListener(evt, function () { self._userInteracted = true; }, { once: false, passive: true }); }); }, findVideo: function (timeout) { timeout = timeout || 60000; var self = this; Utils.info("正在查找视频元素..."); var video = Utils.qsIframes(CONFIG.SELECTORS.videoPlayer); if (video) { Utils.info("找到视频元素(即时)"); return Promise.resolve(video); } return Utils.waitForElementIframes(CONFIG.SELECTORS.videoPlayer, timeout) .then(function (v) { Utils.info("等待到视频元素出现"); return v; }) .catch(function () { Utils.warn("未找到视频元素"); return null; }); }, lockPlaybackRate: function (video) { if (this._isRateLocked) return; try { video.playbackRate = 1; var desc = Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, "playbackRate"); if (desc && desc.set) { var originalSetter = desc.set; Object.defineProperty(video, "playbackRate", { get: function () { return 1; }, set: function (val) { if (val !== 1) Utils.warn("检测到尝试修改播放速率为 " + val + "x,已阻止"); originalSetter.call(this, 1); }, configurable: true }); } this._isRateLocked = true; Utils.info("已锁定播放速率为1倍速"); } catch (e) { Utils.error("锁定播放速率失败: " + e.message); } }, unlockPlaybackRate: function (video) { if (!this._isRateLocked || !video) return; try { delete video.playbackRate; this._isRateLocked = false; Utils.info("已解锁播放速率"); } catch (e) { Utils.error("解锁播放速率失败: " + e.message); } }, startMonitoring: function () { if (this._isMonitoring) { Utils.info("视频监控已在运行中,跳过重复启动"); return; } var self = this; this.findVideo().then(function (video) { if (!video) { Utils.error("无法找到视频元素,监控未启动"); return; } self._video = video; self._isMonitoring = true; self._lastProgress = video.currentTime; self._stuckCount = 0; self._autoplayRetries = 0; self._noSourceRetries = 0; self.lockPlaybackRate(video); self._bindEvents(); self._monitorTimer = setInterval(function () { self._checkStatus(); }, CONFIG.CHECK_INTERVAL); self._pauseCheckTimer = setInterval(function () { self._checkAndResume(); }, CONFIG.PAUSE_CHECK_INTERVAL); Utils.info("视频监控已启动"); StatusPanel.updateVideoStatus("monitoring"); self._tryAutoplay(); }); }, stopMonitoring: function () { if (!this._isMonitoring) return; this._isMonitoring = false; clearInterval(this._monitorTimer); this._monitorTimer = null; clearInterval(this._pauseCheckTimer); this._pauseCheckTimer = null; if (this._video) { this.unlockPlaybackRate(this._video); this._unbindEvents(); } this._video = null; Utils.info("视频监控已停止"); StatusPanel.updateVideoStatus("stopped"); }, _tryAutoplay: function () { if (!this._video) return; if (!this._hasValidSource()) { Utils.warn("视频源无效,跳过自动播放"); return; } var self = this; if (this._userInteracted) { if (this._video.paused) { this._video.play().then(function () { Utils.info("自动播放成功"); }).catch(function (e) { Utils.warn("自动播放失败: " + e.message); }); } return; } if (this._video.paused && this._autoplayRetries < this._maxAutoplayRetries) { this._autoplayRetries++; var wasMuted = this._video.muted; this._video.muted = true; var playPromise = this._video.play(); if (playPromise && playPromise.then) { playPromise.then(function () { Utils.info("静音自动播放成功,等待用户交互后恢复声音"); self._waitForInteractionAndUnmute(); }).catch(function (e) { if (e.message && e.message.indexOf("no supported sources") !== -1) { if (self._handleNoSource()) return; } Utils.warn("静音自动播放也失败 (尝试 " + self._autoplayRetries + "/" + self._maxAutoplayRetries + "): " + e.message); self._video.muted = wasMuted; }); } } }, _waitForInteractionAndUnmute: function () { var self = this; if (this._userInteracted) { this._video.muted = false; Utils.info("已恢复视频声音"); return; } Utils.info("等待用户交互以恢复声音(请点击页面任意位置)..."); StatusPanel.showAlert("请点击页面任意位置以恢复视频声音"); var handler = function () { setTimeout(function () { if (self._video) { self._video.muted = false; Utils.info("用户已交互,恢复视频声音"); StatusPanel.hideAlert(); } }, 500); }; document.addEventListener("click", handler, { once: true }); document.addEventListener("keydown", handler, { once: true }); }, _bindEvents: function () { var v = this._video; if (!v) return; var self = this; var handlers = { play: function () { Utils.info("视频开始播放"); StatusPanel.updateVideoStatus("playing"); }, pause: function () { Utils.info("视频已暂停"); StatusPanel.updateVideoStatus("paused"); }, ended: function () { Utils.info("视频播放完毕"); StatusPanel.updateVideoStatus("ended"); self.stopMonitoring(); if (self._onVideoEnd) self._onVideoEnd(); }, error: function (e) { Utils.error("视频播放错误"); StatusPanel.updateVideoStatus("error"); if (self._onError) self._onError(e); }, waiting: function () { Utils.info("视频缓冲中..."); StatusPanel.updateVideoStatus("buffering"); }, playing: function () { Utils.info("视频恢复播放"); StatusPanel.updateVideoStatus("playing"); } }; var keys = Object.keys(handlers); for (var i = 0; i < keys.length; i++) { v.addEventListener(keys[i], handlers[keys[i]]); self._handlers[keys[i]] = handlers[keys[i]]; } }, _unbindEvents: function () { var v = this._video; if (!v) return; var keys = Object.keys(this._handlers); for (var i = 0; i < keys.length; i++) { v.removeEventListener(keys[i], this._handlers[keys[i]]); } this._handlers = {}; }, _isVideoCompleted: function () { if (!this._video) return false; var duration = this._video.duration; if (!duration || !isFinite(duration) || duration <= 0) return false; return this._video.currentTime >= duration - 1; }, _hasValidSource: function () { if (!this._video) return false; if (this._video.networkState === 3) return false; var hasSrc = !!this._video.src || this._video.querySelector("source"); if (!hasSrc) return false; var dur = this._video.duration; if (dur === 0 || (dur && !isFinite(dur))) return false; return true; }, _handleNoSource: function () { this._noSourceRetries++; if (this._noSourceRetries >= this._maxNoSourceRetries) { Utils.error("视频源无效(no supported sources),已重试" + this._noSourceRetries + "次,跳过此课程"); StatusPanel.showAlert("视频源无效,跳过此课程..."); this.stopMonitoring(); if (this._onVideoEnd) this._onVideoEnd(); return true; } return false; }, _sendHeartbeat: function () { var v = this._video; if (!v) return; this._lastHeartbeatTs = Date.now(); try { if (typeof GM_setValue === "function") { GM_setValue("gdce_video_heartbeat", { currentTime: v.currentTime, duration: v.duration, paused: v.paused, ts: this._lastHeartbeatTs }); } } catch (e) {} }, _checkStatus: function () { if (!this._video || !this._isMonitoring) return; if (Date.now() - this._lastHeartbeatTs > this.HEARTBEAT_INTERVAL) { this._sendHeartbeat(); } var currentTime = this._video.currentTime; var duration = this._video.duration; var paused = this._video.paused; var ended = this._video.ended; if (!this._hasValidSource() && !this._isVideoCompleted()) { if (this._handleNoSource()) return; } if (duration > 0) { var pct = Math.round((currentTime / duration) * 100); StatusPanel.updateProgress(pct, currentTime, duration); } if (this._isVideoCompleted() && !ended) { Utils.info("检测到视频已播放完毕(currentTime=" + currentTime.toFixed(2) + ", duration=" + duration.toFixed(2) + "),手动触发ended流程"); this.stopMonitoring(); if (this._onVideoEnd) this._onVideoEnd(); return; } if (!paused && !ended) { if (Math.abs(currentTime - this._lastProgress) < 0.5) { this._stuckCount++; if (this._stuckCount >= CONFIG.STUCK_THRESHOLD / (CONFIG.CHECK_INTERVAL / 1000)) { if (this._isVideoCompleted()) { Utils.info("视频已播完但卡住,直接触发ended"); this.stopMonitoring(); if (this._onVideoEnd) this._onVideoEnd(); return; } Utils.warn("视频可能卡住,尝试刷新恢复"); this._stuckCount = 0; this._tryRecover(); } } else { this._stuckCount = 0; } } this._lastProgress = currentTime; }, _checkAndResume: function () { if (!this._video || !this._isMonitoring) return; if (this._isVideoCompleted()) { Utils.info("视频已播放完毕,跳过恢复播放检查"); return; } if (!this._hasValidSource()) { if (this._handleNoSource()) return; Utils.warn("视频源可能无效(第" + this._noSourceRetries + "次检测),等待中..."); return; } if (this._noSourceRetries > 0) { this._noSourceRetries = 0; } if (this._video.paused && !this._video.ended) { Utils.info("检测到视频暂停,尝试恢复播放"); var self = this; var playPromise = this._video.play(); if (playPromise && playPromise.catch) { playPromise.then(function () { Utils.info("自动恢复播放成功"); }).catch(function (e) { if (e.message && e.message.indexOf("no supported sources") !== -1) { if (self._handleNoSource()) return; return; } if (e.name === "NotAllowedError" && !self._video.muted) { Utils.info("自动播放被阻止,尝试静音播放"); self._video.muted = true; self._video.play().then(function () { Utils.info("静音播放成功,等待用户交互恢复声音"); self._waitForInteractionAndUnmute(); }).catch(function (e2) { if (e2.message && e2.message.indexOf("no supported sources") !== -1) { if (self._handleNoSource()) return; return; } Utils.warn("静音播放也失败: " + e2.message); }); } else { Utils.warn("自动恢复播放失败: " + e.message); } }); } } }, _tryRecover: function () { if (!this._video) return; if (this._isVideoCompleted()) { Utils.info("视频已播放完毕,跳过恢复操作"); return; } if (!this._hasValidSource()) { if (this._handleNoSource()) return; Utils.warn("视频源无效,跳过恢复操作"); return; } try { this._video.pause(); var self = this; setTimeout(function () { if (!self._video) return; self._video.play().catch(function (e) { if (e.message && e.message.indexOf("no supported sources") !== -1) { if (self._handleNoSource()) return; } Utils.error("恢复播放失败: " + e.message); }); }, 1000); } catch (e) { Utils.error("恢复操作失败: " + e.message); } }, get isMonitoring() { return this._isMonitoring; } }; var CourseAutomation = { _isRunning: false, _retryCount: 0, TARGET_HOURS: 50, TARGET_REQUIRED_HOURS: 25, _targets: null, _creditedRequired: 0, _myCoursesRequiredHours: 0, _neededRequired: 0, _learnStartCredited: 0, _learnStartRequired: 0, _sessionGained: null, COURSE_CATEGORIES: [ "党的理论", "党性教育", "履职能力", "知识培训", "厅处长讲业务", "形势与政策", "红色广东", "新时代广东实践" ], _currentCategoryIndex: 0, _completedHours: 0, _myCourses: [], _myCoursesHours: 0, _neededHours: 0, _selectedCourses: [], _currentLearnIndex: 0, _phase: "idle", init: function () { Utils.info("课程自动化模块初始化(目标学时: " + this.TARGET_HOURS + ")"); this._startPopupWatcher(); }, _startPopupWatcher: function () { var self = this; setInterval(function () { self._tryDismissPopup(); }, 1500); }, _tryDismissPopup: function () { var selectors = [ CONFIG.SELECTORS.layerConfirm, CONFIG.SELECTORS.layerClose, ".layui-layer-btn0", ".layui-layer-btn a", ".layui-layer-close1" ]; for (var i = 0; i < selectors.length; i++) { try { var btn = Utils.qsIframes(selectors[i]); if (btn) { Utils.safeClick(btn); return; } btn = document.querySelector(selectors[i]); if (btn) { Utils.safeClick(btn); return; } } catch (e) { } } }, _handleLayerPopup: function () { setTimeout(function () { var confirmBtn = Utils.qsIframes(CONFIG.SELECTORS.layerConfirm); if (confirmBtn) { Utils.safeClick(confirmBtn); } var closeBtn = Utils.qsIframes(CONFIG.SELECTORS.layerClose); if (closeBtn) { Utils.safeClick(closeBtn); } confirmBtn = document.querySelector(CONFIG.SELECTORS.layerConfirm); if (confirmBtn) { Utils.safeClick(confirmBtn); } closeBtn = document.querySelector(CONFIG.SELECTORS.layerClose); if (closeBtn) { Utils.safeClick(closeBtn); } }, 1500); }, _dismissPopupAndContinue: function (waitMs) { waitMs = waitMs || 2000; var self = this; return new Promise(function (resolve) { var checkCount = 0; var maxChecks = Math.ceil(waitMs / 500) + 4; var timer = setInterval(function () { checkCount++; var dismissed = self._tryDismissPopupNow(); if (dismissed || checkCount >= maxChecks) { clearInterval(timer); setTimeout(resolve, 800); } }, 500); }); }, _tryDismissPopupNow: function () { var selectors = [ CONFIG.SELECTORS.layerConfirm, CONFIG.SELECTORS.layerClose, ".layui-layer-btn0", ".layui-layer-btn a", ".layui-layer-close1" ]; for (var i = 0; i < selectors.length; i++) { try { var btn = Utils.qsIframes(selectors[i]); if (btn) { Utils.safeClick(btn); return true; } btn = document.querySelector(selectors[i]); if (btn) { Utils.safeClick(btn); return true; } } catch (e) { } } return false; }, _extractCourseDetailUrl: function (link) { if (!link) return null; var href = link.getAttribute("href") || ""; var match = href.match(/javascript:w\(["']([^"']+)["']\)/); if (match && match[1]) { var url = match[1]; if (url.indexOf("CourseDetail") !== -1) { if (url.indexOf("http") !== 0) { try { var doc = link.ownerDocument; var baseURL = doc.location.href; url = new URL(url, baseURL).href; } catch (e) { url = "https://gbpx.gd.gov.cn/gdceportal/Study/" + url; } } return url; } } return null; }, _selectCourseViaFetch: function (course) { var self = this; var link = course.selectLink || course.continueLink; if (!link) return Promise.reject(new Error("无可用链接")); var courseDetailUrl = self._extractCourseDetailUrl(link); if (!courseDetailUrl) { var href = link.getAttribute("href") || ""; var pbMatch = href.match(/__doPostBack\(['"]([^'"]+)['"],\s*['"]([^'"]*)['"]\)/); if (pbMatch) { return self._selectCourseViaPostBack(link, pbMatch[1], pbMatch[2], course); } Utils.info("[选课] 链接格式无法解析,回退到直接点击: " + href.substring(0, 60)); Utils.safeClick(link); return self._dismissPopupAndContinue(3000); } Utils.info("[选课] 课程详情: " + courseDetailUrl); return fetch(courseDetailUrl, { method: "GET", credentials: "include" }).then(function (response) { if (!response.ok) throw new Error("请求失败: HTTP " + response.status); return response.text(); }).then(function (html) { var parser = new DOMParser(); var doc = parser.parseFromString(html, "text/html"); var viewstate = doc.querySelector("#__VIEWSTATE"); var viewstateGen = doc.querySelector("#__VIEWSTATEGENERATOR"); var eventVal = doc.querySelector("#__EVENTVALIDATION"); var btnConfirm = doc.querySelector("#btnConfirm"); if (!viewstate || !btnConfirm) { Utils.warn("[选课] 课程详情页缺少表单字段(可能已选过),跳过"); return "already_selected"; } var formData = new FormData(); formData.set("__VIEWSTATE", viewstate.value); formData.set("__VIEWSTATEGENERATOR", viewstateGen ? viewstateGen.value : ""); formData.set("__EVENTVALIDATION", eventVal ? eventVal.value : ""); formData.set("btnConfirm", "进入学习"); Utils.info("[选课] 提交选课到: " + courseDetailUrl); return fetch(courseDetailUrl, { method: "POST", body: formData, credentials: "include" }).then(function (postResponse) { if (postResponse.ok) { Utils.info("[选课] 选课请求已提交成功(HTTP " + postResponse.status + ")"); self._applySelected(course); return "success"; } else { Utils.warn("[选课] 提交返回非200状态: " + postResponse.status); return "fallback"; } }); }).then(function (result) { if (result === "fallback") { Utils.safeClick(link); self._applySelected(course); return self._dismissPopupAndContinue(3000); } return Utils.sleep(1500); }).catch(function (e) { Utils.warn("[选课] 请求失败: " + e.message + ",回退到直接点击"); Utils.safeClick(link); self._applySelected(course); return self._dismissPopupAndContinue(3000); }); }, _selectCourseViaPostBack: function (link, eventTarget, eventArgument, course) { var self = this; var doc = link.ownerDocument; var form = doc ? doc.querySelector("form") : null; if (!form) { Utils.safeClick(link); return self._dismissPopupAndContinue(3000); } var formData = new FormData(form); formData.set("__EVENTTARGET", eventTarget); formData.set("__EVENTARGUMENT", eventArgument); var actionUrl = form.getAttribute("action") || doc.location.href; if (actionUrl.indexOf("http") !== 0) { try { actionUrl = new URL(actionUrl, doc.location.href).href; } catch (e) { } } Utils.info("[选课] 提交到: " + actionUrl.substring(0, 80)); return fetch(actionUrl, { method: "POST", body: formData, credentials: "include" }).then(function (response) { if (response.ok) { Utils.info("[选课] 请求成功"); self._applySelected(course); return Utils.sleep(2000); } Utils.warn("[选课] 请求失败: HTTP " + response.status); Utils.safeClick(link); self._applySelected(course); return self._dismissPopupAndContinue(3000); }).catch(function (e) { Utils.warn("[选课] 请求失败: " + e.message); Utils.safeClick(link); self._applySelected(course); return self._dismissPopupAndContinue(3000); }); }, _extractVideoUrl: function (link) { if (!link) return null; var href = link.getAttribute("href") || ""; var match = href.match(/javascript:w\(["']([^"']+)["']\)/); if (match && match[1]) { var url = match[1].replace(/&/g, "&"); if (url.indexOf("shawcoder.xyz") !== -1 || url.indexOf("playverif") !== -1) { if (url.indexOf("http") !== 0) { try { var doc = link.ownerDocument; var baseURL = doc.location.href; url = new URL(url, baseURL).href; } catch (e) { url = "https://wcs1.shawcoder.xyz/gdcecw/play_pc/" + url; } } Utils.info("提取到视频URL: " + url.substring(0, 80) + "..."); return url; } return null; } if (href.indexOf("http") === 0 && (href.indexOf("shawcoder") !== -1 || href.indexOf("playverif") !== -1)) return href; return null; }, _parseCourseRow: function (row) { var cells = row.querySelectorAll("td"); if (cells.length < 5) return null; var numCols = cells.length; var nameCell = cells[0]; var hoursCell = cells[1]; var typeCell = cells[2]; var actionCellIndex = (numCols >= 7) ? 5 : 4; var actionCell = cells[actionCellIndex]; var name = (nameCell.textContent || "").trim(); var hours = parseFloat((hoursCell.textContent || "0").trim()) || 0; var type = (typeCell.textContent || "").trim(); var completedLink = row.querySelector(CONFIG.SELECTORS.completedCourseLink); var actionText = (actionCell.textContent || "").trim(); var isCompleted = !!completedLink || actionText.indexOf("已学") !== -1 || actionText.indexOf("已完成") !== -1; var continueLink = null; var videoUrl = null; var courseDetailLink = null; var allLinks = row.querySelectorAll("a, input[type='button'], input[type='submit']"); for (var j = 0; j < allLinks.length; j++) { var link = allLinks[j]; var linkText = (link.textContent || link.value || "").trim(); if (linkText.indexOf("继续学习") !== -1 || linkText.indexOf("开始学习") !== -1 || linkText.indexOf("进入学习") !== -1) { continueLink = link; videoUrl = this._extractVideoUrl(link); } else if (linkText.indexOf("进入选课") !== -1) { courseDetailLink = link; } else if (!continueLink && !courseDetailLink) { var detailUrl = this._extractCourseDetailUrl(link); if (detailUrl) { courseDetailLink = link; } } } var selectLink = null; if (courseDetailLink) { selectLink = courseDetailLink; } if (!selectLink && actionText.indexOf("进入选课") !== -1) { var actionLinks = actionCell.querySelectorAll("a"); if (actionLinks.length > 0) selectLink = actionLinks[0]; } var isRequired = type.indexOf("必修") !== -1; return { name: name, hours: hours, type: type, isRequired: isRequired, progress: isCompleted ? 100 : 0, isCompleted: isCompleted, continueLink: continueLink, videoUrl: videoUrl, selectLink: selectLink, row: row }; }, _scanCourseTable: function () { var table = Utils.qsIframes(CONFIG.SELECTORS.courseTable); if (!table) { Utils.warn("未找到课程表格 #gvList"); return []; } var rows = table.querySelectorAll("tr"); var courses = []; for (var i = 1; i < rows.length; i++) { var course = this._parseCourseRow(rows[i]); if (course) courses.push(course); } return courses; }, _waitForCourseTableAndScan: function (timeout) { timeout = timeout || 30000; var self = this; var selector = CONFIG.SELECTORS.courseTable; Utils.info("等待课程表格加载..."); return Utils.waitForElementDisappearIframes(selector, 8000).then(function (disappeared) { if (disappeared) Utils.info("旧课程表格已消失,等待新表格加载..."); else Utils.info("旧表格未消失,继续等待表格出现..."); return Utils.waitForElementIframes(selector, timeout); }).then(function (table) { Utils.info("课程表格已加载,开始扫描"); return Utils.sleep(1500).then(function () { return self._scanCourseTable(); }); }).catch(function (e) { Utils.warn("等待课程表格超时: " + e.message); return self._scanCourseTable(); }); }, _getPaginationInfo: function () { var info = { total: 0, currentPage: 0, totalPages: 0 }; var totalEl = Utils.qsIframes("#lblTotal"); var currentPageEl = Utils.qsIframes("#lblCurrentPage"); var pageEl = Utils.qsIframes("#lblPage"); if (totalEl) info.total = parseInt(totalEl.textContent.trim()) || 0; if (currentPageEl) info.currentPage = parseInt(currentPageEl.textContent.trim()) || 1; if (pageEl) info.totalPages = parseInt(pageEl.textContent.trim()) || 1; return info; }, _clickNextPage: function () { var nextBtn = Utils.qsIframes("#btnNextPage"); if (!nextBtn) nextBtn = Utils.qsIframes("input[type='submit'][name='btnNextPage']"); if (nextBtn) { Utils.info("点击下一页按钮"); Utils.safeClick(nextBtn); return true; } Utils.warn("未找到下一页按钮"); return false; }, _scanAllPages: function (timeout) { var self = this; var allCourses = []; function goToFirstPage() { var firstBtn = Utils.qsIframes("#btnFirstPage"); if (!firstBtn) firstBtn = Utils.qsIframes("input[type='submit'][name='btnFirstPage']"); if (firstBtn) { Utils.info("跳转到第1页"); Utils.safeClick(firstBtn); return Utils.sleep(3000); } return Utils.sleep(500); } function scanPage() { return self._waitForCourseTableAndScan(timeout).then(function (courses) { allCourses = allCourses.concat(courses); var pageInfo = self._getPaginationInfo(); Utils.info("当前第 " + pageInfo.currentPage + "/" + pageInfo.totalPages + " 页,本页 " + courses.length + " 门,累计 " + allCourses.length + " 门"); if (pageInfo.currentPage < pageInfo.totalPages) { Utils.info("还有下一页,翻页继续扫描..."); if (self._clickNextPage()) { return Utils.sleep(3000).then(function () { return scanPage(); }); } } return allCourses; }); } return goToFirstPage().then(function () { return scanPage(); }); }, getStudyHours: function () { var creditedEl = document.querySelector(CONFIG.SELECTORS.creditedHours); var requiredEl = document.querySelector(CONFIG.SELECTORS.requiredHours); if (!creditedEl) creditedEl = Utils.qsIframes(".courseware-des span, .courseware-des"); return { credited: creditedEl ? creditedEl.textContent.trim() : "0", required: requiredEl ? requiredEl.textContent.trim() : "50" }; }, _readTargets: function () { var total = this.TARGET_HOURS, reqMin = this.TARGET_REQUIRED_HOURS; try { var txt = document.body.innerText || ""; var m1 = txt.match(/总学时要求[::]?\s*([0-9.]+)\s*学时/); var m2 = txt.match(/必修课程至少\s*([0-9.]+)\s*学时/); if (m1) total = parseFloat(m1[1]) || total; if (m2) reqMin = parseFloat(m2[1]) || reqMin; var m3 = txt.match(/已完成学时[::]?\s*([0-9.]+)\s*学时(其中必修课程\s*([0-9.]+)\s*学时/); if (m3 && m3[2]) { var reqFromText = parseFloat(m3[2]); if (!isNaN(reqFromText) && reqFromText >= 0) { this._creditedRequired = reqFromText; this._creditedRequiredFromText = true; } } } catch (e) { } this._targets = { total: total, reqMin: reqMin }; Utils.info("[目标] 总学时 " + total + "(必修至少 " + reqMin + "),已获必修 " + (this._creditedRequired || 0) + (this._creditedRequiredFromText ? "(页面文案)" : "(待列表统计)")); return this._targets; }, hasReachedTargets: function () { var t = this._targets || this._readTargets(); var hours = this.getStudyHours(); var credited = parseFloat(hours.credited) || 0; var creditedReq = this._creditedRequired || 0; var totalOk = credited >= t.total; var reqOk = creditedReq >= t.reqMin; Utils.info("[达标检查] 总学时 " + credited + "/" + t.total + (totalOk ? "(已达标)" : "") + ",必修 " + creditedReq + "/" + t.reqMin + (reqOk ? "(已达标)" : "")); return { totalOk: totalOk, reqOk: reqOk, allOk: totalOk && reqOk }; }, hasReachedTargetHours: function () { return this.hasReachedTargets().allOk; }, _ensureCourseTab: function (tabName) { var candidates = Utils.qsaIframes("div,span,a"); var best = null; for (var i = 0; i < candidates.length; i++) { var txt = (candidates[i].textContent || "").trim(); if (txt === tabName) { best = candidates[i]; break; } if (!best && txt.indexOf(tabName) !== -1 && txt.length <= tabName.length + 2) best = candidates[i]; } if (best) { Utils.info("[导航] 切换到「" + tabName + "」标签"); Utils.safeClick(best); return Utils.sleep(2500).then(function () { return true; }); } Utils.warn("[导航] 未找到「" + tabName + "」标签,按当前列表继续"); return Utils.sleep(300); }, _scanLearnedHours: function () { var self = this; if (this._creditedRequiredFromText) { Utils.info("必修已获学时已从页面文案读取: " + this._creditedRequired + ",跳过已学课程扫描"); return Promise.resolve(this._creditedRequired); } Utils.info("=== 第2.5步:统计'已学课程'(计算必修已获学时) ==="); return this._ensureCourseTab("已学课程").then(function () { return self._scanAllPages(12000); }).then(function (courses) { var req = 0, total = 0; for (var i = 0; i < courses.length; i++) { total += courses[i].hours; if (courses[i].isRequired) req += courses[i].hours; } self._creditedRequired = Math.round(req * 100) / 100; self._creditedTotalLearned = Math.round(total * 100) / 100; Utils.info("已学课程统计: 必修 " + self._creditedRequired + " 学时,合计 " + self._creditedTotalLearned + " 学时(" + courses.length + " 门)"); return self._creditedRequired; }).catch(function (e) { Utils.warn("已学课程统计失败: " + e.message + "(必修已获按 " + (self._creditedRequired || 0) + " 处理)"); return self._creditedRequired || 0; }); }, _applySelected: function (course) { this._neededHours = Math.max(0, (this._neededHours || 0) - course.hours); if (course.isRequired) this._neededRequired = Math.max(0, (this._neededRequired || 0) - course.hours); Utils.info("[选课] 剩余缺口: 总 " + this._neededHours + " / 必修 " + this._neededRequired); }, _sortCoursesRequiredFirst: function (courses) { var arr = (courses || []).slice(); arr.sort(function (a, b) { return (b.isRequired ? 1 : 0) - (a.isRequired ? 1 : 0); }); return arr; }, _clickTopLevelMyCourse: function () { var links = document.querySelectorAll("span.firstRouterLink"); for (var i = 0; i < links.length; i++) { var text = (links[i].textContent || "").trim(); if (text.indexOf("我的课程") !== -1) { Utils.info("[导航] 点击顶层'我的课程'标签"); Utils.safeClick(links[i]); return true; } } Utils.warn("[导航] 未找到顶层'我的课程'标签"); return false; }, _clickTopLevelCategory: function (categoryName) { var links = document.querySelectorAll("span.firstRouterLink"); for (var i = 0; i < links.length; i++) { var text = (links[i].textContent || "").trim(); if (text.indexOf(categoryName) !== -1 || categoryName.indexOf(text) !== -1) { Utils.info("[导航] 点击顶层分类标签: " + text); Utils.safeClick(links[i]); return links[i]; } } return null; }, _readNavigationStructure: function () { var structure = []; var firstLinks = document.querySelectorAll("span.firstRouterLink"); if (firstLinks.length === 0) { firstLinks = Utils.qsaIframes(CONFIG.SELECTORS.firstRouterLink); } Utils.info("[导航] 找到 " + firstLinks.length + " 个一级分类链接"); for (var i = 0; i < firstLinks.length; i++) { var link = firstLinks[i]; var name = (link.textContent || "").trim(); var routerUrl = link.getAttribute("routerlink") || ""; Utils.info("[导航] 一级分类[" + i + "]: name='" + name + "' routerlink='" + routerUrl + "'"); var parent = link.parentElement; var childContainer = parent ? parent.querySelector(CONFIG.SELECTORS.childHrefContainer) : null; var children = []; if (childContainer) { var items = childContainer.querySelectorAll(CONFIG.SELECTORS.childHrefItem); for (var j = 0; j < items.length; j++) { var childName = (items[j].textContent || "").trim(); var childUrl = items[j].getAttribute("item") || ""; children.push({ name: childName, url: childUrl, element: items[j] }); Utils.info("[导航] 子分组[" + j + "]: name='" + childName + "' item='" + childUrl + "'"); } } structure.push({ name: name, url: routerUrl, element: link, children: children }); } return structure; }, _navigateToUrl: function (relativeUrl) { if (!relativeUrl) return false; var fullUrl = relativeUrl; if (relativeUrl.indexOf("http") !== 0) { fullUrl = "https://gbpx.gd.gov.cn/gdceportal/study/" + relativeUrl; } Utils.info("[导航] 导航到: " + fullUrl); var secondIframe = document.querySelector(CONFIG.SELECTORS.secondIframe); if (secondIframe) { secondIframe.src = fullUrl; return true; } secondIframe = Utils.qsIframes(CONFIG.SELECTORS.secondIframe); if (secondIframe) { secondIframe.src = fullUrl; return true; } Utils.warn("[导航] 未找到可导航的iframe"); return false; }, _clickNavAndNavigate: function (navElement, relativeUrl) { var self = this; var beforeSrc = this._getSecondIframeSrc(); Utils.safeClick(navElement); return Utils.sleep(3000).then(function () { var afterSrc = self._getSecondIframeSrc(); if (beforeSrc !== afterSrc) { Utils.info("[导航] 点击后secondIframe src已变化: " + (afterSrc || "").substring(0, 80)); return true; } Utils.info("[导航] 点击后secondIframe src未变化,尝试直接导航"); if (relativeUrl) { return self._navigateToUrl(relativeUrl); } return false; }); }, _getSecondIframeSrc: function () { var iframe = document.querySelector(CONFIG.SELECTORS.secondIframe); if (iframe) return iframe.src || ""; iframe = Utils.qsIframes(CONFIG.SELECTORS.secondIframe); if (iframe) return iframe.src || ""; return ""; }, autoSelectAndEnterCourse: function () { var self = this; var targets = this._readTargets(); if (!this._sessionGained) this._sessionGained = { total: 0, required: 0 }; this._phase = "scanning"; this._completedHours = parseFloat(this.getStudyHours().credited) || 0; Utils.info("=== 第1步:读取已完成学时: 总 " + this._completedHours + ",必修 " + (this._creditedRequired || 0) + " ==="); Utils.info("=== 第2步:扫描'我的课程'中未完成的课程(含分页) ==="); this._clickMyCourse().then(function () { return self._ensureCourseTab("在学课程"); }).then(function () { return self._scanAllPages(20000); }).then(function (courses) { self._myCourses = []; self._myCoursesHours = 0; self._myCoursesRequiredHours = 0; for (var i = 0; i < courses.length; i++) { var c = courses[i]; if (!c.isCompleted) { self._myCourses.push(c); self._myCoursesHours += c.hours; if (c.isRequired) self._myCoursesRequiredHours += c.hours; } } self._myCoursesHours = Math.round(self._myCoursesHours * 100) / 100; self._myCoursesRequiredHours = Math.round(self._myCoursesRequiredHours * 100) / 100; Utils.info("我的课程中未完成: " + self._myCourses.length + " 门,共 " + self._myCoursesHours + " 学时(必修 " + self._myCoursesRequiredHours + " 学时)"); return self._scanLearnedHours().then(function () { var creditedReq = self._creditedRequired || 0; var check = self.hasReachedTargets(); if (check.allOk) { Utils.info("总学时和必修学时均已达标,停止自动学习"); StatusPanel.showAlert("已达标:总 " + self._completedHours + "/" + targets.total + ",必修 " + creditedReq + "/" + targets.reqMin); self._phase = "idle"; return; } Utils.info("=== 第3步:学时缺口分析(总学时+必修双口径) ==="); Utils.info(" 目标: 总 " + targets.total + " 学时(必修至少 " + targets.reqMin + ")"); Utils.info(" 已获: 总 " + self._completedHours + " / 必修 " + creditedReq); Utils.info(" 在学未完成: 总 " + self._myCoursesHours + " / 必修 " + self._myCoursesRequiredHours); var needReq = Math.max(0, targets.reqMin - creditedReq - self._myCoursesRequiredHours); var needTotal = Math.max(0, targets.total - self._completedHours - self._myCoursesHours); self._neededRequired = Math.round(needReq * 100) / 100; self._neededHours = Math.round(Math.max(needTotal, needReq) * 100) / 100; Utils.info(" 修完在学后仍缺: 必修 " + self._neededRequired + " / 总计 " + Math.max(0, targets.total - self._completedHours - self._myCoursesHours) + "(选课按 " + self._neededHours + " 执行)"); if (self._neededHours <= 0 && self._neededRequired <= 0) { Utils.info("在学课程已足够达标,直接开始学习(必修优先)"); self._startLearningMyCourses(); } else { Utils.info("=== 第4步:从分类列表选课凑学时(必修优先) ==="); self._selectCoursesToFillGap(0); } }); }).catch(function (e) { Utils.error("扫描我的课程失败: " + e.message); self._selectCoursesToFillGap(0); }); }, _clickMyCourse: function () { var self = this; return new Promise(function (resolve) { Utils.info("点击'我的课程'标签..."); var clicked = self._clickTopLevelMyCourse(); if (clicked) { Utils.info("已点击顶层'我的课程',等待secondIframe加载..."); setTimeout(function () { var subTab = Utils.qsIframes(CONFIG.SELECTORS.navMyCourse); if (subTab) { Utils.info("点击secondIframe中的'我的课程'子标签"); Utils.safeClick(subTab); } setTimeout(resolve, 2000); }, 3000); } else { var myCourseTab = Utils.qsIframes(CONFIG.SELECTORS.navMyCourse); if (!myCourseTab) myCourseTab = Utils.findByTextIframes("span.firstRouterLink, a, span, div", "我的课程"); if (myCourseTab) { Utils.safeClick(myCourseTab); Utils.info("已点击iframe中的'我的课程'"); } else { Utils.warn("未找到'我的课程'标签"); } setTimeout(resolve, 2000); } }); }, _selectCoursesToFillGap: function (categoryIndex) { if (this._neededHours <= 0 && this._neededRequired <= 0) { Utils.info("已选够学时(总/必修双达标),回到我的课程开始学习"); this._goBackToMyCoursesAndLearn(); return; } if (categoryIndex >= this.COURSE_CATEGORIES.length) { Utils.warn("所有分类都已检查,仍未凑够学时(还差 " + this._neededHours + " 学时)"); this._goBackToMyCoursesAndLearn(); return; } this._currentCategoryIndex = categoryIndex; var categoryName = this.COURSE_CATEGORIES[categoryIndex]; Utils.info("=== 检查分类: " + categoryName + " (还差 " + this._neededHours + " 学时) ==="); var topEl = this._clickTopLevelCategory(categoryName); var self = this; if (topEl) { Utils.sleep(3000).then(function () { var navStructure = self._readNavigationStructure(); var navItem = null; for (var i = 0; i < navStructure.length; i++) { if (navStructure[i].name.indexOf(categoryName) !== -1 || categoryName.indexOf(navStructure[i].name) !== -1) { navItem = navStructure[i]; break; } } if (!navItem) { Utils.warn("未在导航中找到分类: " + categoryName); self._selectCoursesToFillGap(categoryIndex + 1); return; } if (navItem.children.length > 0) { self._selectFromSubCategoryToFillGap(categoryIndex, 0, navItem, navStructure); } else { self._selectCoursesFromCurrentCategory(categoryIndex); } }); } else { var navStructure = this._readNavigationStructure(); var navItem = null; for (var i = 0; i < navStructure.length; i++) { if (navStructure[i].name.indexOf(categoryName) !== -1 || categoryName.indexOf(navStructure[i].name) !== -1) { navItem = navStructure[i]; break; } } if (!navItem) { Utils.warn("未在导航中找到分类: " + categoryName); this._selectCoursesToFillGap(categoryIndex + 1); return; } this._clickNavAndNavigate(navItem.element, navItem.url).then(function () { if (navItem.children.length > 0) { self._selectFromSubCategoryToFillGap(categoryIndex, 0, navItem, navStructure); } else { self._selectCoursesFromCurrentCategory(categoryIndex); } }); } }, _selectFromSubCategoryToFillGap: function (categoryIndex, subIndex, navItem, navStructure) { if (this._neededHours <= 0 && this._neededRequired <= 0) { this._goBackToMyCoursesAndLearn(); return; } var self = this; var categoryName = this.COURSE_CATEGORIES[categoryIndex]; if (subIndex >= navItem.children.length) { Utils.info("分类'" + categoryName + "'的所有子分组已遍历完毕"); this._selectCoursesToFillGap(categoryIndex + 1); return; } var subItem = navItem.children[subIndex]; Utils.info("--- 子分组[" + subIndex + "/" + navItem.children.length + "]: " + subItem.name + " (还差 " + this._neededHours + " 学时) ---"); this._clickNavAndNavigate(subItem.element, subItem.url).then(function () { return self._selectCoursesFromCurrentCategory(categoryIndex); }).then(function () { self._selectFromSubCategoryToFillGap(categoryIndex, subIndex + 1, navItem, navStructure); }).catch(function (e) { Utils.error("子分组'" + subItem.name + "'选课失败: " + e.message); self._selectFromSubCategoryToFillGap(categoryIndex, subIndex + 1, navItem, navStructure); }); }, _selectCoursesFromCurrentCategory: function (categoryIndex) { var self = this; return this._scanAllPages(20000).then(function (courses) { var requiredCourses = []; var electiveCourses = []; for (var i = 0; i < courses.length; i++) { var c = courses[i]; if (c.isCompleted) continue; if (c.isRequired) requiredCourses.push(c); else electiveCourses.push(c); } var toSelect = requiredCourses.concat(electiveCourses); var selectedCount = 0; function needMore() { return self._neededHours > 0 || self._neededRequired > 0; } function selectNext(index) { if (index >= toSelect.length || !needMore()) { Utils.info("本分类选课完成,共选 " + selectedCount + " 门(剩余缺口: 总 " + Math.max(0, self._neededHours) + " / 必修 " + Math.max(0, self._neededRequired) + ")"); return Utils.sleep(2000); } var course = toSelect[index]; var wantThis = false; if (course.isRequired && self._neededRequired > 0) wantThis = true; else if (self._neededHours > 0 && self._neededRequired <= 0) wantThis = true; if (!wantThis) return selectNext(index + 1); if (course.selectLink) { Utils.info("选课: " + course.name + " (" + course.hours + "学时, " + course.type + ")"); selectedCount++; return self._selectCourseViaFetch(course).then(function () { return selectNext(index + 1); }).catch(function (e) { Utils.warn("选课失败,回退到点击方式: " + e.message); Utils.safeClick(course.selectLink); self._applySelected(course); return self._dismissPopupAndContinue(3000).then(function () { return selectNext(index + 1); }); }); } else if (course.continueLink || course.videoUrl) { Utils.info("课程已选但未完成,已在学习队列中: " + course.name); return selectNext(index + 1); } return selectNext(index + 1); } return selectNext(0); }); }, _goBackToMyCoursesAndLearn: function () { var self = this; this._phase = "learning"; Utils.info("=== 第5步:回到我的课程,开始逐个学习(必修优先) ==="); this._clickMyCourse().then(function () { return self._ensureCourseTab("在学课程"); }).then(function () { return self._scanAllPages(20000); }).then(function (courses) { self._selectedCourses = []; for (var i = 0; i < courses.length; i++) { if (!courses[i].isCompleted) { self._selectedCourses.push(courses[i]); } } self._selectedCourses = self._sortCoursesRequiredFirst(self._selectedCourses); Utils.info("我的课程中未完成共 " + self._selectedCourses.length + " 门(必修优先排序),开始学习"); self._currentLearnIndex = 0; self._learnStartCredited = parseFloat(self.getStudyHours().credited) || 0; self._learnStartRequired = self._creditedRequired || 0; self._sessionGained = { total: 0, required: 0 }; self._learnNextCourse(); }).catch(function (e) { Utils.error("回到我的课程失败: " + e.message); }); }, _startLearningMyCourses: function () { this._phase = "learning"; this._selectedCourses = this._sortCoursesRequiredFirst(this._myCourses); this._currentLearnIndex = 0; this._learnStartCredited = this._completedHours || (parseFloat(this.getStudyHours().credited) || 0); this._learnStartRequired = this._creditedRequired || 0; this._sessionGained = { total: 0, required: 0 }; Utils.info("我的课程中未完成共 " + this._selectedCourses.length + " 门(必修优先排序),开始学习"); this._learnNextCourse(); }, _learnNextCourse: function () { if (this._currentLearnIndex >= this._selectedCourses.length) { Utils.info("所有课程已学习完毕!"); StatusPanel.showAlert("所有课程已学习完毕!"); MainController._videoFinishedLock = false; MainController._videoPlaying = false; return; } if (MainController._videoPlaying) { Utils.info("上一个视频仍在播放中,10秒后重试..."); var self = this; setTimeout(function () { self._learnNextCourse(); }, 10000); return; } var course = this._selectedCourses[this._currentLearnIndex]; Utils.info("=== 学习课程[" + (this._currentLearnIndex + 1) + "/" + this._selectedCourses.length + "]: " + course.name + " (" + course.hours + "学时) ==="); var videoUrl = course.videoUrl; if (!videoUrl && course.continueLink) { videoUrl = this._extractVideoUrl(course.continueLink); } if (videoUrl) { Utils.info("通过openInTab打开视频: " + videoUrl.substring(0, 80)); MainController._videoPlaying = true; MainController._videoFinishedLock = false; var tabResult = openInTab(videoUrl); if (!tabResult) { Utils.warn("openInTab失败,回退到点击链接"); MainController._videoPlaying = false; Utils.safeClick(course.continueLink); } else { MainController._videoTab = (typeof tabResult === "object" && tabResult !== true) ? tabResult : null; MainController._lastVideoActivityTs = Date.now(); MainController._lastHeartbeatSeenTs = 0; MainController._startVideoWindowCheck(); } } else if (course.continueLink) { Utils.info("点击课程学习链接: " + course.name); Utils.safeClick(course.continueLink); } else if (course.selectLink) { var courseDetailUrl = this._extractCourseDetailUrl(course.selectLink); if (courseDetailUrl) { Utils.info("课程无直接视频链接,通过CourseDetail进入: " + courseDetailUrl.substring(0, 80)); var secondIframe = document.querySelector(CONFIG.SELECTORS.secondIframe); if (!secondIframe) secondIframe = Utils.qsIframes(CONFIG.SELECTORS.secondIframe); if (secondIframe) { secondIframe.src = courseDetailUrl; var self = this; Utils.info("已设置secondIframe.src为CourseDetail页,等待加载后点击'进入学习'..."); setTimeout(function () { self._clickEnterStudyInIframe(secondIframe); }, 5000); } else { Utils.warn("未找到secondIframe,跳过课程: " + course.name); this._currentLearnIndex++; this._learnNextCourse(); } } else { Utils.warn("课程没有可用的学习链接: " + course.name + ",跳过"); this._currentLearnIndex++; this._learnNextCourse(); } } else { Utils.warn("课程没有可用的学习链接: " + course.name + ",跳过"); this._currentLearnIndex++; this._learnNextCourse(); } }, _clickEnterStudyInIframe: function (iframe) { var self = this; try { var doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document); if (!doc) { Utils.warn("无法访问iframe内容,3秒后重试"); setTimeout(function () { self._clickEnterStudyInIframe(iframe); }, 3000); return; } var confirmBtn = doc.querySelector(CONFIG.SELECTORS.layerConfirm); if (confirmBtn) { Utils.info("iframe内检测到弹窗确认按钮,自动点击"); confirmBtn.click(); } var closeBtn = doc.querySelector(CONFIG.SELECTORS.layerClose); if (closeBtn) { Utils.info("iframe内检测到弹窗关闭按钮,自动关闭"); closeBtn.click(); } var btn = doc.querySelector(CONFIG.SELECTORS.enterStudyBtn); if (!btn) btn = doc.querySelector("input[type='submit'][value='进入学习']"); if (!btn) btn = doc.querySelector("input[type='submit'][name='btnConfirm']"); if (btn) { Utils.info("在iframe内找到'进入学习'按钮,点击"); btn.click(); Utils.info("已点击'进入学习'按钮,等待视频页面加载..."); setTimeout(function () { VideoMonitor.stopMonitoring(); var videoEl = Utils.qsIframes(CONFIG.SELECTORS.videoPlayer); if (videoEl) { Utils.info("找到同域视频元素,启动监控"); VideoMonitor.startMonitoring(); } else { Utils.info("视频在跨域iframe中播放,等待postMessage通知播放完毕"); } }, 5000); } else { Utils.warn("iframe内未找到'进入学习'按钮,3秒后重试"); setTimeout(function () { self._clickEnterStudyInIframe(iframe); }, 3000); } } catch (e) { Utils.warn("访问iframe内容失败: " + e.message + ",3秒后重试"); setTimeout(function () { self._clickEnterStudyInIframe(iframe); }, 3000); } }, onCourseLearned: function () { var doneCourse = this._selectedCourses[this._currentLearnIndex]; if (doneCourse && this._sessionGained) { this._sessionGained.total += doneCourse.hours; if (doneCourse.isRequired) this._sessionGained.required += doneCourse.hours; } this._currentLearnIndex++; Utils.info("课程学习完毕,准备学习下一门(" + this._currentLearnIndex + "/" + this._selectedCourses.length + ")"); var t = this._targets || this._readTargets(); var labelNow = parseFloat(this.getStudyHours().credited) || 0; var estTotal = Math.max(labelNow, (this._learnStartCredited || 0) + (this._sessionGained ? this._sessionGained.total : 0)); var estReq = (this._learnStartRequired || 0) + (this._sessionGained ? this._sessionGained.required : 0); if (estTotal >= t.total && estReq >= t.reqMin) { Utils.info("预计已达标(总 " + estTotal + "/" + t.total + ",必修 " + Math.round(estReq * 100) / 100 + "/" + t.reqMin + "),停止学习。以后续服务端统计为准,可刷新页面复核。"); StatusPanel.showAlert("已达标:总 " + estTotal + "/" + t.total + "(必修 " + Math.round(estReq * 100) / 100 + "/" + t.reqMin + ")"); MainController._videoFinishedLock = false; MainController._videoPlaying = false; this._phase = "idle"; return; } var self = this; setTimeout(function () { self._learnNextCourse(); }, 3000); }, clickNextSection: function () { var self = this; Utils.info("正在查找'下一节'按钮..."); var btn = Utils.findByTextIframes("a, button, span, div", "下一节"); if (!btn) btn = Utils.findByTextIframes("a, button, span, div", "下一课"); if (!btn) { Utils.warn("未找到'下一节'按钮"); return false; } Utils.randomDelay().then(function () { Utils.safeClick(btn); Utils.info("已点击'下一节'按钮"); self._handleLayerPopup(); }); return true; }, _findCourseInList: function (courses, name) { for (var i = 0; i < courses.length; i++) { if (courses[i].name.indexOf(name) !== -1 || name.indexOf(courses[i].name) !== -1) return courses[i]; } return null; }, checkMultipleCourses: function () { var videos = Utils.qsaIframes(CONFIG.SELECTORS.videoPlayer); if (videos.length > 1) { Utils.warn("检测到多个视频同时播放!"); StatusPanel.showAlert("警告:检测到多课程同时打开,请关闭多余课程!"); return true; } return false; }, start: function () { this._isRunning = true; Utils.info("课程自动化已启动"); }, stop: function () { this._isRunning = false; Utils.info("课程自动化已停止"); }, get isRunning() { return this._isRunning; } }; var StatusPanel = { _panel: null, _isMinimized: false, _isPaused: false, _videoStatus: "stopped", _progress: 0, _currentTime: 0, _duration: 0, _courseName: "", _alertMsg: "", _mode: "studyCenter", init: function () { if (typeof IS_VIDEO_PAGE !== "undefined" && IS_VIDEO_PAGE) { this._mode = "videoPlayer"; } else { this._mode = "studyCenter"; } this._createPanel(); this._bindPanelEvents(); Utils.info("状态面板已创建(模式: " + this._mode + ")"); }, _createPanel: function () { var style = document.createElement("style"); style.textContent = [ "#gdce-panel{position:fixed;top:20px;right:20px;width:320px;background:#1a1a2e;color:#e0e0e0;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,0.4);z-index:999999;font-family:'Microsoft YaHei',sans-serif;font-size:13px;transition:all 0.3s ease;overflow:hidden}", "#gdce-panel.minimized #gdce-panel-body{display:none!important}", "#gdce-panel.minimized #gdce-panel-controls{display:none!important}", "#gdce-panel.minimized{width:auto;height:auto;border-radius:8px;cursor:pointer}", "#gdce-panel-header{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;background:linear-gradient(135deg,#16213e,#0f3460);border-radius:12px 12px 0 0;cursor:move}", "#gdce-panel-title{font-weight:bold;font-size:14px;color:#e94560;line-height:1.4}", "#gdce-panel-title .gdce-subtitle{font-size:11px;color:#888;font-weight:normal}", "#gdce-panel-title a{color:#58a6ff;text-decoration:none}", "#gdce-panel-title a:hover{text-decoration:underline}", "#gdce-panel-controls{display:flex;gap:6px}", ".gdce-btn{background:none;border:1px solid #444;color:#ccc;border-radius:6px;padding:2px 8px;cursor:pointer;font-size:12px;transition:all 0.2s}", ".gdce-btn:hover{background:#e94560;color:#fff;border-color:#e94560}", ".gdce-btn.active{background:#e94560;color:#fff;border-color:#e94560}", "#gdce-panel-body{padding:12px 14px;max-height:400px;overflow-y:auto}", ".gdce-section{margin-bottom:10px}", ".gdce-section-title{font-size:11px;color:#888;text-transform:uppercase;margin-bottom:4px;letter-spacing:1px}", ".gdce-status-row{display:flex;justify-content:space-between;align-items:center;padding:3px 0}", ".gdce-status-value{color:#e94560;font-weight:bold}", "#gdce-progress-bar{width:100%;height:6px;background:#2a2a4a;border-radius:3px;overflow:hidden;margin-top:4px}", "#gdce-progress-fill{height:100%;background:linear-gradient(90deg,#e94560,#ff6b6b);border-radius:3px;transition:width 0.5s ease;width:0%}", "#gdce-log-container{max-height:150px;overflow-y:auto;background:#0d1117;border-radius:6px;padding:6px 8px;font-size:11px;line-height:1.6}", ".gdce-log-entry{border-bottom:1px solid #1a1a2e;padding:1px 0}", ".gdce-log-info{color:#58a6ff}", ".gdce-log-warn{color:#d29922}", ".gdce-log-error{color:#f85149}", "#gdce-alert{background:#f8514922;border:1px solid #f85149;border-radius:6px;padding:6px 10px;margin-top:8px;color:#f85149;font-size:12px;display:none}", "#gdce-panel::-webkit-scrollbar{width:4px}", "#gdce-panel::-webkit-scrollbar-thumb{background:#444;border-radius:2px}" ].join("\n"); document.head.appendChild(style); var panel = document.createElement("div"); panel.id = "gdce-panel"; var videoSection = ''; var hoursSection = ''; if (this._mode === "videoPlayer") { videoSection = [ '