// ==UserScript== // @name chd刷课(chd) // @namespace http://tampermonkey.net/ // @version 423 // @description 解决自动播放被阻止问题,确保视频持续播放,60分钟自动刷新,拦截弹窗,自动完成所有课程。 // @author chd // @match *.hnsydwpx.cn/* // @grant GM_addStyle // @grant GM_log // @grant GM_notification // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @run-at document-idle // @connect 192.168.1.107 // @license MIT // @downloadURL none // ==/UserScript== (function () { "use strict"; // 配置参数 const config = { checkInterval: 10000, interactionWait: 3000, maxRetry: 300, debugMode: true, countdownDuration: 60 * 60, // 60分钟(秒数) dataAttribute1: "li[data-v-290b612e]", dataAttribute2: "div[data-v-6a18900e]", }; // 添加UI指示器 GM_addStyle(` .script-indicator { position: fixed; top: 20px; right: 1400px; background: linear-gradient(135deg, rgba(0,0,0,0.8), #B8860B); color: white; padding: 15px 22px; border-radius: 10px; box-shadow: 0 2px 5px rgba(0,0,0,0.4); z-index: 9999; font-size: 25px; } .script-indicator.error { background: #F44336; } .pro-label { position: absolute; bottom: -10px; right: 10px; background: rgba(0, 0, 0, 0.5); color: #FFD700; padding: 5px 10px; border-radius: 5px; font-size: 14px; font-weight: bold; opacity: 0.8; } .countdown-display { font-size: 14px; margin-top: 5px; opacity: 0.8; } `); const indicator = document.createElement("div"); indicator.className = "script-indicator"; indicator.innerHTML = '刷课脚本已经启动啦
Pro
'; document.body.appendChild(indicator); // 倒计时管理器 class CountdownManager { constructor() { this.timer = null; this.startTime = null; this.remaining = GM_getValue( "countdownRemaining", config.countdownDuration ); this.init(); } init() { logDebug("CountdownManager: 初始化开始"); this.updateDisplay(); if (!GM_getValue("countdownRunning", false)) { GM_setValue("countdownRunning", true); this.startTime = Date.now(); this.start(); } else { const elapsed = Math.floor( (Date.now() - GM_getValue("countdownStartTime")) / 1000 ); this.remaining = Math.max(config.countdownDuration - elapsed, 0); this.start(); } logDebug("CountdownManager: 初始化完成"); } start() { logDebug("CountdownManager: 开始倒计时"); GM_setValue("countdownStartTime", Date.now()); this.timer = setInterval(() => { this.remaining--; GM_setValue("countdownRemaining", this.remaining); if (this.remaining <= 0) { this.handleTimeout(); return; } this.updateDisplay(); }, 1000); } updateDisplay() { const minutes = Math.floor(this.remaining / 60); const seconds = this.remaining % 60; indicator.querySelector( ".countdown-display" ).textContent = `下次刷新: ${minutes .toString() .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; } handleTimeout() { logDebug("CountdownManager: 倒计时结束,处理超时"); clearInterval(this.timer); GM_setValue("countdownRunning", false); GM_setValue("countdownRemaining", config.countdownDuration); if (1) { logDebug("CountdownManager: 60分钟倒计时结束,返回课程中心"); window.location.href = "https://www.hnsydwpx.cn/mineCourse"; } else { // 如果在课程中心页面则重新开始倒计时 this.remaining = config.countdownDuration; this.init(); } } notify(message) { if (config.debugMode) { GM_notification({ title: `[倒计时通知]`, text: message, timeout: 5000, }); } } } // 解决自动播放问题的视频控制器 class VideoController { constructor() { this.player = null; this.retryCount = 0; this.isWaitingInteraction = false; logDebug("VideoController: 初始化开始"); this.init(); } async init() { try { this.player = await this.waitForElement("#coursePlayer video"); this.addFakeInteractionLayer(); this.startMonitoring(); logDebug("VideoController: 初始化完成"); } catch (error) { logDebug(`VideoController: 初始化失败: ${error.message}`); indicator.classList.add("error"); indicator.textContent = "脚本初始化失败"; } } // 添加伪交互层解决自动播放限制 addFakeInteractionLayer() { logDebug("VideoController: 添加伪交互层"); GM_addStyle(` .interaction-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: transparent; z-index: 9998; cursor: pointer; } .interaction-notice { position: fixed; bottom: 80px; right: 20px; background: rgba(0,0,0,0.7); color: white; padding: 10px 15px; border-radius: 4px; z-index: 9999; max-width: 300px; font-size: 14px; text-align: center; box-shadow: 0 2px 10px rgba(0,0,0,0.5); } `); // 创建覆盖层 const overlay = document.createElement("div"); overlay.className = "interaction-overlay"; overlay.onclick = () => this.handleUserInteraction(); document.body.appendChild(overlay); // 添加提示 const notice = document.createElement("div"); notice.className = "interaction-notice"; notice.innerHTML = "点击页面任意位置激活自动播放功能
3秒后自动尝试播放"; document.body.appendChild(notice); this.isWaitingInteraction = true; setTimeout(() => { if (this.isWaitingInteraction) { this.handleUserInteraction(); notice.innerHTML = "已自动激活播放功能"; setTimeout(() => notice.remove(), 2000); } }, config.interactionWait); } // 处理用户交互 handleUserInteraction() { if (!this.isWaitingInteraction) return; logDebug("VideoController: 处理用户交互"); this.isWaitingInteraction = false; document.querySelector(".interaction-overlay")?.remove(); document.querySelector(".interaction-notice")?.remove(); // 首次播放需要用户触发 this.playVideo() .then(() => { logDebug(`VideoController: 用户交互后自动播放已启动`); }) .catch((error) => { logDebug(`VideoController: 交互后播放失败: ${error}`); this.notify(`交互后播放失败: ${error}`, "error"); }); } // 新增检查覆盖层的方法 intervalCheck() { removeSpecificOverlays(); this.checkPlayingProgress(); } // 开始监控 startMonitoring() { logDebug("VideoController: 开始监控视频播放状态"); this.monitorInterval = setInterval(() => { if ( !this.isWaitingInteraction && this.player.paused && !this.player.ended ) { logDebug("VideoController: 监控到视频播放状态异常"); this.playVideo(); logDebug("VideoController: 恢复视频播放"); } // 检查覆盖层 this.intervalCheck(); }, config.checkInterval); // 监听视频事件 this.player.addEventListener("pause", () => { if (!this.isWaitingInteraction) { logDebug("VideoController: 检测到视频等待交互,尝试移除交互层"); this.playVideo(); removeSpecificOverlays(); } }); this.player.addEventListener("ended", () => { logDebug("VideoController: 当前视频播放完毕"); this.nextChapter(); }); } // 播放视频(处理自动播放限制) async playVideo() { if (this.retryCount >= config.maxRetry) { logDebug("VideoController: 达到最大重试次数,停止尝试播放"); GM_notification({ title: "自动播放被阻止", text: "请手动点击播放按钮", timeout: 5000, }); indicator.classList.add("error"); indicator.textContent = "自动播放被阻止"; return; } try { const playPromise = this.player.play(); if (playPromise !== undefined) { await playPromise; this.retryCount = 0; indicator.classList.remove("error"); indicator.innerHTML = '刷课运行中……
Pro
'; logDebug("VideoController: 视频播放成功"); } } catch (error) { this.retryCount++; this.notify( `播放失败 (${this.retryCount}/${config.maxRetry}): ${error}`, "error" ); logDebug( `VideoController: 播放失败 (${this.retryCount}/${config.maxRetry}): ${error}` ); // 尝试通过点击按钮播放 const playBtn = await this.waitForElement( ".xgplayer-play", document, 1000 ).catch(() => null); if (playBtn) { playBtn.click(); logDebug("VideoController: 已尝试点击播放按钮"); } // 直接尝试静音播放 if (this.retryCount >= 1) { this.player.muted = true; this.player.play().catch((e) => { logDebug(`VideoController: 静音播放也失败: ${e}`); }); } } } checkPlayingProgress() { const items = document.querySelectorAll("li[data-v-290b612e]"); const ifChangeClass = false; for (let item of items) { if (item.className === "playlist_li_active") { const progressElement = item.querySelector(".progress"); const progress = progressElement ? progressElement.textContent.trim() : null; if (typeof progress === "string" && progress.includes("%")) { // 将百分数转换为数值 const progressValue = parseFloat(progress.replace("%", "")); if (progressValue === 100) { // item.click(); // 点击后等待视频加载并开始播放 // setTimeout(() => this.playVideo(), 10000); console.log(`VideoController: 当前已满100%,切换章节`); this.nextChapter(); } } return; } } } // 切换到下一章节 nextChapter() { logDebug("VideoController: 当前视频播放完毕,尝试切换到下一章节"); const items = document.querySelectorAll(config.dataAttribute1); for (let item of items) { const progressElement = item.querySelector(".progress"); const progress = progressElement ? progressElement.textContent.trim() : null; logDebug(`章节进度文本内容: ${progress}`); if (typeof progress === "string" && progress.includes("%")) { // 将百分数转换为数值 const progressValue = parseFloat(progress.replace("%", "")); logDebug(`当前章节进度数值: ${progressValue}`); if (progressValue < 98) { item.click(); // 点击后等待视频加载并开始播放 setTimeout(() => this.playVideo(), 10000); logDebug( `VideoController: 已切换到章节: ${ item.querySelector(".name").textContent }` ); return; } } } window.location.href = "https://www.hnsydwpx.cn/mineCourse"; logDebug("VideoController: 所有章节已完成,返回课程中心"); } // 等待元素出现 waitForElement(selector, parent = document, timeout = 10000) { logDebug(`VideoController: 开始等待元素 ${selector} 出现`); return new Promise((resolve, reject) => { const startTime = Date.now(); const check = () => { const el = parent.querySelector(selector); if (el) { logDebug(`VideoController: 元素 ${selector} 已找到`); resolve(el); } else if (Date.now() - startTime < timeout) { setTimeout(check, 500); } else { logDebug(`VideoController: 元素 ${selector} 未找到`); reject(new Error(`元素未找到: ${selector}`)); } }; check(); }); } // 弹窗通知 notify(message, type = "info") { if (config.debugMode) { GM_notification({ title: `[视频控制]`, text: message, timeout: 5000, }); } } } // 查找包含“防作弊问答”的元素并获取其 Base64 编码 // 该函数用于在文档中通过 XPath 表达式查找包含“防作弊问答”文本的元素, // 然后在其祖先元素中查找 img 元素,并获取其 Base64 编码形式的图片数据 function getBase64FromAntiCheatImage() { logDebug( "getBase64FromAntiCheatImage: 开始查找防作弊问答图片并获取 Base64 编码" ); const xpath = "//span[contains(text(), '防作弊问答')]"; try { // 使用 document.evaluate 方法根据给定的 XPath 表达式在文档中查找元素 // 这里指定返回第一个匹配的有序节点 const result = document.evaluate( xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ); const antiCheatElement = result.singleNodeValue; if (antiCheatElement) { let parentElement = antiCheatElement.parentElement; while (parentElement) { const imgElement = parentElement.querySelector("img"); // 检查找到的 img 元素的 src 属性是否以"data:image"开头, // 如果是,则表示是 Base64 编码的图片数据,提取并返回 if (imgElement && imgElement.src.startsWith("data:image")) { logDebug( "getBase64FromAntiCheatImage: 已获取到防作弊问答图片的 Base64 编码" ); return imgElement.src.split(",")[1]; } parentElement = parentElement.parentElement; } } } catch (error) { logDebug( `getBase64FromAntiCheatImage: 在使用 XPath 查找元素时出错: ${error}` ); } logDebug("getBase64FromAntiCheatImage: 未找到防作弊问答图片的 Base64 编码"); return null; } // 构造发送到后端的请求数据 // 该函数接受一个 Base64 编码的图片数据,构造一个包含图片数据和 OCR 配置选项的对象 function buildRequestData(base64Image) { logDebug("buildRequestData: 开始构造发送到后端的请求数据"); const options = { lang: "eng", oem: 3, psm: 7, }; try { // 检查传入的 base64Image 是否为空,如果为空则抛出错误 if (!base64Image) { throw new Error("传入的 base64Image 为空"); } logDebug("buildRequestData: 请求数据构造完成"); return { image: base64Image, options: options, }; } catch (error) { logDebug(`buildRequestData: 构造请求数据时出错: ${error}`); throw error; } } // 发送请求到后端进行 OCR 识别 // 该函数接受构造好的请求数据,通过 GM_xmlhttpRequest 发送 POST 请求到后端服务器, // 并根据响应结果进行处理,返回识别结果和算式计算结果的对象 function sendOCRRequest(requestData) { logDebug("sendOCRRequest: 开始发送请求到后端进行 OCR 识别"); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "http://192.168.1.107:3000/recognize", headers: { "Content-Type": "application/json", }, data: JSON.stringify(requestData), onload: function (response) { if (response.status === 200) { try { // 将服务端返回的响应文本解析为 JSON 格式 const responseData = JSON.parse(response.responseText); const recognitionResult = responseData.text; const calculationResult = responseData.calculationResult; logDebug("sendOCRRequest: 后端 OCR 识别请求成功,已获取结果"); resolve({ recognitionResult, calculationResult }); } catch (parseError) { logDebug(`sendOCRRequest: 解析服务端响应时出错: ${parseError}`); reject(new Error("解析服务端响应失败")); } } else { logDebug( `sendOCRRequest: 请求失败: ${response.statusText},状态码: ${response.status}` ); reject( new Error( `请求失败: ${response.statusText},状态码: ${response.status}` ) ); } }, onerror: function (error) { logDebug(`sendOCRRequest: 发送请求时出错: ${error}`); reject( new Error(`请求出错: ${error},可能是网络连接问题或服务器不可达`) ); }, }); }); } // 根据计算结果点击相应的答案 // 该函数接受一个计算结果,在页面中查找所有符合特定样式的 radio 标签, // 如果标签的文本内容或其关联的 input 的值与计算结果匹配,则点击该 radio 标签的 input 元素 function clickCorrespondingAnswer(calculatedResult) { logDebug("clickCorrespondingAnswer: 开始根据计算结果点击相应的答案"); try { const radioLabels = document.querySelectorAll( "label.el-radio.is-bordered.el-radio--large" ); radioLabels.forEach((label) => { const labelText = label.querySelector(".el-radio__label").textContent; logDebug(`clickCorrespondingAnswer: 检查答案标签文本: ${labelText}`); const inputValue = label.querySelector( "input.el-radio__original" ).value; logDebug(`clickCorrespondingAnswer: 检查答案输入值: ${inputValue}`); if ( labelText === calculatedResult.toString() || inputValue === calculatedResult.toString() ) { const inputElement = label.querySelector("input.el-radio__original"); inputElement.click(); logDebug(`clickCorrespondingAnswer: 已点击答案: ${labelText}`); } }); } catch (error) { logDebug(`clickCorrespondingAnswer: 点击答案时出错: ${error}`); } } function removeSpecificOverlays(videoController) { // 获取所有匹配的元素,这里是 class 为".el-overlay"的元素 const overlays = document.querySelectorAll(".el-overlay"); // 遍历并删除每个元素 overlays.forEach((overlay) => { // 检查元素的 display 属性是否不为 none,即存在覆盖层 if (overlay.style.display !== "none") { logDebug("removeSpecificOverlays: 检测到覆盖层不存在display属性"); // 检查元素内部是否有特定的内容,这里检查 class 为".el-dialog__title"的元素的文本内容 const dialogTitle = overlay.querySelector( ".el-dialog__title, .el-message-box__title" ); if (dialogTitle && dialogTitle.textContent === "防作弊问答") { logDebug("removeSpecificOverlays: 检测到防作弊问答"); const base64Image = getBase64FromAntiCheatImage(); if (base64Image) { const requestData = buildRequestData(base64Image); sendOCRRequest(requestData) .then(({ recognitionResult, calculationResult }) => { logDebug( `removeSpecificOverlays: 防作弊问答 OCR 识别结果: ${recognitionResult}` ); if (calculationResult !== null) { clickCorrespondingAnswer(calculationResult); } else { logDebug("removeSpecificOverlays: 未成功计算算式结果"); } }) .catch((error) => { logDebug( `removeSpecificOverlays: 处理 OCR 请求结果时出错: ${error}` ); }); } else { logDebug("removeSpecificOverlays: 未找到“防作弊问答”相关区域"); } } else { try { logDebug( `removeSpecificOverlays: 非防作弊问答覆盖层": ${dialogTitle.textContent}` ); overlay.remove(); logDebug("removeSpecificOverlays: 已移除非防作弊问答覆盖层"); } catch (error) { logDebug( `removeSpecificOverlays: 移除非防作弊问答覆盖层出错: ${error}` ); } } // 检查 videoController 是否存在 if (videoController) { videoController.playVideo(); // this.playVideo(); } else { logDebug( "removeSpecificOverlays: videoController 未定义,无法调用 playVideo 方法" ); } } }); } // 页面初始化 function check2425(items, index) { logDebug("check2425: 开始检查课程章节"); items[index].click(); setTimeout(() => { //先检查元素是否存在 const lists = document.querySelectorAll( ".el-tab-pane .el-row .list_title" ); let button; if (lists.length) { if (index) logProcess("2024年章节未完成,程序优先学习2024年章节!"); for (let list of lists) { let text; if ( (text = list.querySelector(".el-progress__text span").innerText) === "100%" ) continue; logProcess(`第一个未学完的视频进度为:${text}`); button = list.querySelector("button"); try { button.click(); logProcess("已点击未学完视频的播放按钮"); break; } catch (clickError) { logDebug(`check2425: 点击操作失败: ${clickError}`); } } } else { logProcess("2024已经完成,学习2025年课程!"); if (!index) logProcess("全部完成啦!"); else check2425(items, 0); } new VideoController(); }, 2000); } function init() { logDebug("init: 脚本初始化开始"); // 初始化倒计时 new CountdownManager(); // 只在对视频页面启用 if ( location.pathname.includes("/videoPlayback") || location.pathname.includes("/getcourseDetails") ) { logProcess("当前页面为课程学习界面,启动视频控制"); new VideoController(); } else if (!location.pathname.includes("/mineCourse")) { logProcess("当前页面不是课程中心,重定向到课程中心"); window.location.href = "https://www.hnsydwpx.cn/mineCourse"; } else if (location.pathname.includes("/mineCourse")) { setTimeout(() => { logProcess("进入课程中心页面,开始选择课程章节"); const items = document.querySelectorAll( `.years ${config.dataAttribute2}` ); check2425(items, 1); }, 2000); } logDebug("init: 脚本初始化完成"); } // 日志输出函数,区分调试和进程提示 function logDebug(message) { if (config.debugMode) { const now = new Date(); const timestamp = now.toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", }); console.log(`[DEBUG] [${timestamp}] ${message}`); } } function logProcess(message) { const now = new Date(); const timestamp = now.toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", }); console.log(`[PROCESS] [${timestamp}] ${message}`); } // 启动脚本 if (document.readyState === "complete") { init(); } else { window.addEventListener("load", init); } })();