// ==UserScript== // @name 湖南人社-湖南云畔-全自动挂机助手 // @version 1.2.6 // @description 湖南人社-湖南云畔平台全自动辅助:后台防暂停、自动拦截图像验证码、自动答题、小节智能无缝连播、标准学时合规挂机(防拉回/防警告/防重播尾部/防退回个人中心/防空指针报错)、跨课程全自动连播! // @author meteroa // @match https://www.hnedu123.com.cn/personal-center/my-course* // @match https://www.hnedu123.com.cn/my-course-details* // @require https://scriptcat.org/lib/637/1.4.5/ajaxHooker.js#sha256=EGhGTDeet8zLCPnx8+72H15QYRfpTX4MbhyJ4lJZmyg= // @grant unsafeWindow // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_info // @grant GM_addStyle // @run-at document-start // @license MIT // ==/UserScript== (function () { 'use strict'; console.log('[湖南云畔助手] 脚本开始加载,初始化核心防护与拦截系统...'); // 跨页面标记:当前课程是否所有小节已全部完成 let isCurrentCourseFullyDone = false; // ========================================== // 1. 全局异常与崩盘吸收 (解决 TypeError reading 'data' 与 pause-error) // ========================================== window.addEventListener('unhandledrejection', function (e) { const msg = e.reason ? (e.reason.message || e.reason.toString() || '') : ''; if (msg.includes("reading 'data'") || msg.includes("reading 'fullScreen'") || msg.includes("reading 'video'") || msg.includes("reading 'removeChild'") || msg.includes('pause-error')) { e.preventDefault(); e.stopPropagation(); // console.warn('[湖南云畔助手] 已成功捕获并吸收平台内部空指针崩溃异常'); } }); window.addEventListener('error', function (e) { const msg = e.message || ''; if (msg.includes("reading 'data'") || msg.includes("reading 'fullScreen'") || msg.includes("reading 'video'") || msg.includes("reading 'removeChild'") || msg.includes('pause-error')) { e.preventDefault(); e.stopPropagation(); // console.warn('[湖南云畔助手] 已拦截平台内部未捕获异常,防止脚本或页面崩溃'); } }); // ========================================== // 2. 核心底层 Hook:彻底解决“离开窗口就暂停”与“倍速检测报警” // ========================================== function installCoreHooks() { try { // A. 欺骗页面:将 document.visibilityState 永久锁定为 'visible',document.hidden 永久为 false Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true }); Object.defineProperty(document, 'hidden', { get: () => false, configurable: true }); // B. 拦截全局 setInterval:彻底禁用页面内置的“请勿拖动进度条或倍速播放”检测定时器 const originalSetInterval = window.setInterval; window.setInterval = function (handler, timeout, ...args) { if (typeof handler === 'function') { const fnStr = handler.toString(); // 页面中的 getTimes 定时器会检查 currentTime - pauseTime > 20 或 VLlTL (0x1c3) 差距,弹警告并强制回退 if (fnStr.includes('pauseTime') || fnStr.includes('请勿拖动进度条') || fnStr.includes('倍速播放') || fnStr.includes('VLlTL') || (fnStr.includes('0x1c3') && timeout === 1000)) { console.log('[湖南云畔助手] 成功拦截并禁用了网页反倍速反拖拽检测定时器!'); // 返回一个无害的超长空定时器 ID return originalSetInterval(() => {}, 2147483647); } } return originalSetInterval.call(this, handler, timeout, ...args); }; // C. 拦截 EventTarget.prototype.addEventListener const originalAddEventListener = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = function (type, listener, options) { // 彻底阻止 visibilitychange 监听器注册,使页面的 handleVisibilityChange 无法被触发 if (type === 'visibilitychange') { return; } // 阻止窗口失焦 blur 暂停事件 if (type === 'blur' && (this === window || this === document)) { return; } // 拦截 video 元素的 ratechange 事件,强制锁定 1.0x 标准倍速,避免播放器重置速率异常 if (type === 'ratechange' && (this instanceof HTMLVideoElement || (this && this.tagName === 'VIDEO'))) { const wrappedListener = function (event) { if (this.playbackRate !== 1.0) { this.playbackRate = 1.0; event.stopImmediatePropagation(); event.stopPropagation(); return; } return listener.apply(this, arguments); }; return originalAddEventListener.call(this, type, wrappedListener, options); } return originalAddEventListener.call(this, type, listener, options); }; // 清空内联事件 try { Object.defineProperty(document, 'onvisibilitychange', { get: () => null, set: () => {}, configurable: true }); Object.defineProperty(window, 'onblur', { get: () => null, set: () => {}, configurable: true }); } catch (e) {} // D. 底层锁定 HTMLMediaElement.prototype.currentTime:彻底杜绝任何 JS 脚本强制倒退拉回进度 let currentMediaSrc = ''; let allowInitialSeek = true; const originalCurrentTimeDesc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'currentTime'); if (originalCurrentTimeDesc) { Object.defineProperty(HTMLMediaElement.prototype, 'currentTime', { get: function () { return originalCurrentTimeDesc.get.call(this); }, set: function (newVal) { const cur = originalCurrentTimeDesc.get.call(this); const src = this.currentSrc || this.src || ''; if (src && src !== currentMediaSrc) { currentMediaSrc = src; allowInitialSeek = true; } // 新视频初次加载定位(如从上一次观看记录恢复)或重置回开头予以放行 if (newVal <= 0.5 || (allowInitialSeek && (cur <= 1.0 || newVal >= cur))) { allowInitialSeek = false; return originalCurrentTimeDesc.set.call(this, newVal); } // 在同一视频播放过程中,若新设置的时间比当前进度倒退了 1.5 秒以上: // 判定为反作弊或反拖拽强制倒退拉回,予以坚决拦截! if (cur > 1.5 && newVal < cur - 1.5) { console.warn(`[湖南云畔助手] 核心防御生效:成功拦截反倍速拉回进度!当前: ${cur.toFixed(1)}s, 企图回退至: ${newVal.toFixed(1)}s`); return; } return originalCurrentTimeDesc.set.call(this, newVal); }, configurable: true }); } // E. DOM 实时消音盾:秒级销毁任何反倍速/反拖拽的 Element UI 弹窗 DOM const domObserver = new MutationObserver(mutations => { for (const m of mutations) { for (const node of m.addedNodes) { if (node.nodeType === 1) { // 坚决放行助手自身的任何 UI 元素 if (node.id === 'hnedu-panel-container' || node.id === 'hnedu-toggle-float-btn' || (node.id && node.id.startsWith('hnedu-')) || (typeof node.className === 'string' && node.className.includes('hnedu')) || (node.closest && node.closest('#hnedu-panel-container, #hnedu-toggle-float-btn')) || (node.querySelector && node.querySelector('#hnedu-panel-container, #hnedu-toggle-float-btn'))) { continue; } // 仅拦截 Element UI 弹窗与提示 const isElPopup = node.classList && ( node.classList.contains('el-message') || node.classList.contains('el-message-box__wrapper') || node.classList.contains('el-notification') || node.classList.contains('el-alert') ); const isElChild = node.closest && node.closest('.el-message, .el-message-box__wrapper, .el-notification, .el-alert'); if (isElPopup || isElChild) { const text = node.innerText || node.textContent || ''; if (text.includes('倍速') || text.includes('进度条') || text.includes('拖动') || text.includes('调整至') || text.includes('疑似') || text.includes('刷新后继续')) { const targetToHide = (node.closest && node.closest('.el-message, .el-message-box__wrapper, .el-notification, .el-alert')) || node; targetToHide.style.setProperty('display', 'none', 'important'); targetToHide.style.setProperty('visibility', 'hidden', 'important'); targetToHide.style.setProperty('opacity', '0', 'important'); targetToHide.style.setProperty('pointer-events', 'none', 'important'); targetToHide.style.setProperty('height', '0', 'important'); targetToHide.style.setProperty('margin', '0', 'important'); targetToHide.style.setProperty('padding', '0', 'important'); console.log('[湖南云畔助手] 实时隐形反倍速警告弹窗 DOM:', text.trim()); } } } } } }); if (document.documentElement) { domObserver.observe(document.documentElement, { childList: true, subtree: true }); } else { document.addEventListener('DOMContentLoaded', () => { domObserver.observe(document.documentElement, { childList: true, subtree: true }); }); } // F. 全局 Element UI $message 弹窗静默过滤 const installMessageShield = () => { const hookMsgObj = (obj) => { if (!obj || obj._speedShieldInstalled) return; obj._speedShieldInstalled = true; ['warning', 'error', 'info', 'success'].forEach(type => { if (typeof obj[type] === 'function') { const orig = obj[type]; obj[type] = function (opt, ...args) { const str = typeof opt === 'string' ? opt : (opt && opt.message ? opt.message : ''); if (str.includes('倍速') || str.includes('进度条') || str.includes('拖动') || str.includes('调整至')) { console.log(`[湖南云畔助手] 拦截到全局 $message.${type}:`, str); return; } return orig.call(this, opt, ...args); }; } }); }; if (window.$nuxt && window.$nuxt.constructor && window.$nuxt.constructor.prototype) { hookMsgObj(window.$nuxt.constructor.prototype.$message); } if (window.$nuxt && window.$nuxt.$message) { hookMsgObj(window.$nuxt.$message); } }; setInterval(installMessageShield, 500); console.log('[湖南云畔助手] 核心底层 Hook (防暂停 / 防拉回 / 防倍速警报 / 定时器拦截) 已成功生效!'); } catch (err) { console.error('[湖南云畔助手] Core Hooks 异常:', err); } } installCoreHooks(); // ========================================== // 3. 网络请求拦截:屏蔽验证码与自动提取题目答案 (绝不使用 abort 避免触发 axios 异常) // ========================================== window._latestQuestionAnswer = null; function installAjaxHooks() { if (typeof ajaxHooker === 'undefined') { console.warn('[湖南云畔助手] ajaxHooker 未加载,等待重试...'); return; } ajaxHooker.hook(request => { // A. 拦截随机随堂练习题目,获取题目数据并自动提取正确答案 (typeA = 1) if (request.url.includes('vedioValidQuestions/getQuestions')) { request.response = res => { try { const json = JSON.parse(res.responseText); if (json && json.data) { window._latestQuestionAnswer = json.data.correctAnswer; console.log('[湖南云畔助手] 拦截到答题题目,正确答案为:', window._latestQuestionAnswer); } } catch (e) { console.error('[湖南云畔助手] 解析题目数据失败:', e); } }; } // B. 拦截 60 秒轮询弹出的图像/验证码题目 (typeA = 2) // 该接口若返回 data: null,网页内部 logic (P.data && (...)) 将不会触发任何弹窗与暂停! else if (request.url.includes('vedioTransferQuestionsRecord/queryQuestion')) { request.response = res => { try { res.status = 200; res.responseText = JSON.stringify({ code: 200, msg: "success", data: null }); // console.log('[湖南云畔助手] 成功屏蔽中途图形/验证码弹窗请求'); } catch (e) { console.error('[湖南云畔助手] 屏蔽图形验证码异常:', e); } }; } // C. 监听学时记录接口响应,若服务端因异常或记录冲突拒绝入库,及时预警并执行自愈恢复 else if (request.url.includes('/course/courseStudyRecord/')) { request.response = res => { try { const txt = res.responseText || ''; const isRejected = res.status >= 400 || txt.includes('结束点') || txt.includes('不一致') || txt.includes('倍速') || txt.includes('疑似'); if (isRejected) { console.error('[湖南云畔助手] ⚠️ 服务端已拒绝本次学时入库!原因:', txt); logMsg('⚠️ 服务端学时记录异常!平台服务端严格校验 1:1 物理时长与进度连续性。'); const v = document.querySelector('video'); if (v && v.playbackRate !== 1.0) v.playbackRate = 1.0; if (txt.includes('结束点') || txt.includes('不一致')) { showToast('⚠️ 进度与服务端记录冲突!正在自动刷新页面以同步服务端真实进度...', 'error'); setTimeout(() => { location.reload(); }, 2500); } } } catch (e) {} }; } // D. 对任何废弃接口一律返回合法的 HTTP 200 空对象,绝不能 abort,彻底避免 axios 抛出 undefined reading data else if (request.url.includes('learning/learnVerify')) { request.response = res => { res.status = 200; res.responseText = JSON.stringify({ code: 0, msg: "ok", data: null }); }; } }); console.log('[湖南云畔助手] Ajax 请求拦截器已激活!'); } try { installAjaxHooks(); } catch (e) { console.error('[湖南云畔助手] 注册 AjaxHook 失败:', e); } // ========================================== // 4. 防退回个人中心:拦截 backShow 事件与非法路由跳转 // ========================================== function installBackShowGuard() { const hookComponentEmit = (comp) => { if (!comp) return; // 1. 劫持 $emit,屏蔽 backShow if (!comp._backShowGuarded && typeof comp.$emit === 'function') { comp._backShowGuarded = true; const origEmit = comp.$emit; comp.$emit = function (event, ...args) { if (event === 'backShow') { console.warn('[湖南云畔助手] 核心防御生效:成功拦截平台的 backShow 事件,已阻止非法回退个人中心!参数:', args); return this; } return origEmit.apply(this, [event, ...args]); }; if (comp._events && comp._events.backShow) { comp._events.backShow = []; } } // 2. 彻底屏蔽组件本身的 backShow 方法(防止父组件被直接调用或触发) if (typeof comp.backShow === 'function' && !comp.backShow._guarded) { comp.backShow = function (reason) { console.warn('[湖南云畔助手] 核心防御生效:成功屏蔽组件的 backShow 方法执行!reason:', reason); }; comp.backShow._guarded = true; } // 3. 彻底清除并禁用内置的 1 秒反倍速反拖拽计时器 timers (消除退出页面时的 video 空指针报错与倒退重播) if (comp.timers) { clearInterval(comp.timers); comp.timers = null; } if (!comp._timersGuarded) { comp._timersGuarded = true; let _t = null; try { Object.defineProperty(comp, 'timers', { get() { return _t; }, set(val) { if (val) clearInterval(val); _t = null; }, configurable: true }); } catch (e) {} } // 4. 动态劫持 pauseTime 和 VLlTL (0x1c3) 属性,使其永远跟随当前播放进度 // 彻底杜绝 updateCourseStudyRecordFn、videPlay、videPause 中的 20s 差距判定与视频倒退重播 if (!comp._propsGuarded && ('pauseTime' in comp || 'VLlTL' in comp || comp.dplayer)) { comp._propsGuarded = true; try { Object.defineProperty(comp, 'pauseTime', { get() { const v = document.querySelector('video'); return v ? Math.floor(v.currentTime) : 0; }, set() {}, configurable: true }); Object.defineProperty(comp, 'VLlTL', { get() { const v = document.querySelector('video'); return v ? Math.floor(v.currentTime) : 0; }, set() {}, configurable: true }); } catch (e) {} } // 5. 防御 exportFullFn:在 dplayer 已经被销毁或为 null 时防止抛出 fullScreen 空指针报错 if (typeof comp.exportFullFn === 'function' && !comp.exportFullFn._guarded) { const origExport = comp.exportFullFn; comp.exportFullFn = function (...args) { if (!this.dplayer || !this.dplayer.fullScreen) { return; } try { return origExport.apply(this, args); } catch (e) {} }; comp.exportFullFn._guarded = true; } // 6. 拦截组件内部与全局的 $message.warning 弹窗(静默吸收反倍速/反拖拽提示) const hookMsg = (msgObj) => { if (msgObj && !msgObj._speedGuarded) { msgObj._speedGuarded = true; const origWarn = msgObj.warning; msgObj.warning = function (msg, ...args) { const str = typeof msg === 'string' ? msg : (msg && msg.message ? msg.message : ''); if (str.includes('倍速') || str.includes('进度条') || str.includes('拖动') || str.includes('调整至') || str.includes('疑似')) { console.log('[湖南云畔助手] 成功拦截反倍速/反拖拽警告弹窗:', str); return; } return origWarn.call(this, msg, ...args); }; } }; hookMsg(comp.$message); if (window.$nuxt) hookMsg(window.$nuxt.$message); }; // 全局 Vue 原型链 $emit 劫持(确保所有尚未渲染或动态创建的组件全部受保护) const installVuePrototypeShield = () => { const getVueConstructor = () => { if (window.Vue && window.Vue.prototype) return window.Vue; if (window.$nuxt && window.$nuxt.constructor) { if (window.$nuxt.constructor.super && window.$nuxt.constructor.super.prototype) { return window.$nuxt.constructor.super; } return window.$nuxt.constructor; } return null; }; const Vue = getVueConstructor(); if (Vue && Vue.prototype && !Vue.prototype._backShowGuarded) { Vue.prototype._backShowGuarded = true; const origEmit = Vue.prototype.$emit; Vue.prototype.$emit = function (event, ...args) { if (event === 'backShow') { console.warn('[湖南云畔助手] 核心防御生效:全局 Vue 原型拦截到 backShow 事件,已彻底阻断回退个人中心!参数:', args); return this; } return origEmit.apply(this, [event, ...args]); }; } }; // 全局 VueRouter 原型链 push / replace 劫持(覆盖所有组件实例) const installRouterPrototypeShield = () => { if (!window.$nuxt || !window.$nuxt.$router) return; const routerProto = window.$nuxt.$router.constructor ? window.$nuxt.$router.constructor.prototype : null; if (routerProto && !routerProto._globalGuardInstalled) { routerProto._globalGuardInstalled = true; ['push', 'replace'].forEach(method => { const origMethod = routerProto[method]; if (typeof origMethod === 'function') { routerProto[method] = function (location, onComplete, onAbort) { const path = typeof location === 'string' ? location : (location && location.path ? location.path : ''); if (path.includes('/personal-center/my-course') && window.location.href.includes('/my-course-details')) { if (!isCurrentCourseFullyDone) { console.warn(`[湖南云畔助手] 核心防御生效:已成功阻断 VueRouter.prototype.${method} 回退到个人中心!目标:`, path); if (typeof onAbort === 'function') onAbort(); return Promise.resolve(); } } return origMethod.call(this, location, onComplete, onAbort); }; } }); } }; // 递归遍历并防护整棵 Vue 组件树 const traverseAndHookVueTree = (vm) => { if (!vm || typeof vm !== 'object') return; hookComponentEmit(vm); if (Array.isArray(vm.$children)) { for (const child of vm.$children) { traverseAndHookVueTree(child); } } }; // 持续对 DOM 上的 Vue 组件实例注入保护 (深度树遍历查找) const findAndHookVideoComp = () => { const video = document.querySelector('video'); if (video) { let el = video; while (el && el !== document.body) { if (el.__vue__) { let c = el.__vue__; while (c) { if ('pauseTime' in c || 'VLlTL' in c || c.dplayer) { hookComponentEmit(c); } c = c.$parent; } } el = el.parentElement; } } const candidates = document.querySelectorAll('.videoContainerBox, .dplayer, .video-container, .rightTree'); for (const cEl of candidates) { if (cEl.__vue__) { let c = cEl.__vue__; while (c) { if ('pauseTime' in c || 'VLlTL' in c || c.dplayer || c.$emit) { hookComponentEmit(c); } c = c.$parent; } } } }; setInterval(() => { installVuePrototypeShield(); installRouterPrototypeShield(); if (window.$nuxt) { traverseAndHookVueTree(window.$nuxt); } findAndHookVideoComp(); }, 500); } installBackShowGuard(); // ========================================== // 5. 全局辅助工具函数 // ========================================== const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); function logMsg(msg) { const time = new Date().toLocaleTimeString(); console.log(`[湖南云畔助手 ${time}] ${msg}`); const logBox = document.getElementById('hnedu-log-content'); if (logBox) { const line = document.createElement('div'); line.textContent = `[${time}] ${msg}`; line.style.marginBottom = '4px'; logBox.appendChild(line); logBox.scrollTop = logBox.scrollHeight; } } function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.textContent = message; toast.style.cssText = ` position: fixed; top: 25px; left: 50%; transform: translateX(-50%); background: ${type === 'success' ? '#2ecc71' : type === 'error' ? '#e74c3c' : '#3498db'}; color: #fff; padding: 10px 22px; border-radius: 20px; font-size: 14px; font-weight: 500; z-index: 999999999; box-shadow: 0 4px 15px rgba(0,0,0,0.25); transition: all 0.3s ease; pointer-events: none; `; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translate(-50%, -20px)'; setTimeout(() => toast.remove(), 400); }, 3000); } // ========================================== // 6. 自动化调度控制器 (Controller) // ========================================== class CourseController { constructor() { this.timer = null; this.isBusy = false; this.isAdvancing = false; this.lastRoute = location.href; this.initRouteWatcher(); this.startLoop(); } // 监听路由变更 initRouteWatcher() { const checkRoute = () => { if (location.href !== this.lastRoute) { this.lastRoute = location.href; // logMsg(`页面路由变更: ${location.pathname}`); this.onRouteChanged(); } }; window.addEventListener('popstate', checkRoute); const origPush = history.pushState; history.pushState = function (state, title, url) { if (url && typeof url === 'string' && url.includes('/personal-center/my-course') && window.location.href.includes('/my-course-details')) { if (!isCurrentCourseFullyDone) { console.warn('[湖南云畔助手] 拦截到非正常 history.pushState 回退个人中心,已阻止!'); return; } } const res = origPush.apply(this, arguments); checkRoute(); return res; }; const origReplace = history.replaceState; history.replaceState = function (state, title, url) { if (url && typeof url === 'string' && url.includes('/personal-center/my-course') && window.location.href.includes('/my-course-details')) { if (!isCurrentCourseFullyDone) { console.warn('[湖南云畔助手] 拦截到非正常 history.replaceState 回退个人中心,已阻止!'); return; } } const res = origReplace.apply(this, arguments); checkRoute(); return res; }; setInterval(checkRoute, 1000); } onRouteChanged() { createPanelDOM(); if (this.isCourseListPage()) { isCurrentCourseFullyDone = false; logMsg('当前处于课程列表页'); } else if (this.isCourseDetailPage()) { isCurrentCourseFullyDone = false; logMsg('当前处于课程播放详情页'); } } isCourseListPage() { return location.href.includes('/personal-center/my-course'); } isCourseDetailPage() { return location.href.includes('/my-course-details'); } startLoop() { if (this.timer) clearInterval(this.timer); this.timer = setInterval(async () => { // 周期性检查确保悬浮控制面板始终挂载在页面中 createPanelDOM(); const isAuto = GM_getValue('hnedu_autoRunning', true); if (!isAuto || this.isBusy) return; this.isBusy = true; try { if (this.isCourseDetailPage()) { await this.handleDetailPageLoop(); } else if (this.isCourseListPage()) { await this.handleListPageLoop(); } } catch (e) { console.error('[湖南云畔助手] 循环任务异常:', e); } finally { this.isBusy = false; } }, 1200); } // ---------------------------------------------------- // 6.1 课程详情播放页逻辑 // ---------------------------------------------------- async handleDetailPageLoop() { // A. 处理弹窗 (防暂停温馨提示 & 答题弹窗) await this.handleDialogs(); // B. 视频播放器维护 (保活、静音、倍速、防拖拽重置) this.maintainVideoPlayback(); // C. 检查当前视频是否结束并处理连播 await this.checkVideoEndedAndAdvance(); } async handleDialogs() { // 1. 关闭“温馨提示”离开窗口弹窗 const tipDialog = Array.from(document.querySelectorAll('.el-dialog')).find(d => { const title = d.querySelector('.el-dialog__title')?.innerText || ''; return title.includes('温馨提示') || d.innerText.includes('不要离开学习窗口'); }); if (tipDialog && tipDialog.style.display !== 'none' && tipDialog.parentElement.style.display !== 'none') { logMsg('检测到“不要离开窗口”温馨提示弹窗,正在自动关闭...'); const closeBtn = tipDialog.querySelector('.el-dialog__headerbtn') || tipDialog.querySelector('button'); if (closeBtn) closeBtn.click(); await sleep(400); } // 2. 处理“随机练习”答题弹窗 const examDialog = Array.from(document.querySelectorAll('.el-dialog')).find(d => { const title = d.querySelector('.el-dialog__title')?.innerText || ''; return title.includes('随机练习'); }); if (examDialog && examDialog.style.display !== 'none' && examDialog.parentElement.style.display !== 'none') { logMsg('检测到随堂练习题目弹窗,正在自动答题...'); const radios = examDialog.querySelectorAll('.el-radio'); let targetRadio = null; if (window._latestQuestionAnswer) { for (const r of radios) { const label = r.getAttribute('label') || r.querySelector('.el-radio__original')?.value || ''; const text = r.innerText.trim(); if (label === window._latestQuestionAnswer || text.startsWith(window._latestQuestionAnswer)) { targetRadio = r; break; } } } // 未匹配到时默认选第一个选项 if (!targetRadio && radios.length > 0) { targetRadio = radios[0]; } if (targetRadio) { targetRadio.click(); logMsg(`已选择题目选项: ${targetRadio.innerText.trim() || window._latestQuestionAnswer || 'A'}`); await sleep(300); const submitBtn = examDialog.querySelector('.submit') || Array.from(examDialog.querySelectorAll('button')).find(b => b.innerText.includes('提交')); if (submitBtn) { submitBtn.click(); logMsg('题目答案已提交'); await sleep(500); // 点击确认弹窗 (el-message-box) const msgConfirmBtn = document.querySelector('.el-message-box button'); if (msgConfirmBtn) msgConfirmBtn.click(); } } } // 3. 处理“学习记录结束点与上次学习记录不一致”冲突弹窗(自动刷新页面自愈同步) const conflictDialog = Array.from(document.querySelectorAll('.el-message-box__wrapper, .el-dialog__wrapper')).find(w => { const text = w.innerText || ''; return text.includes('学习记录结束点') || text.includes('结束点与上次学习记录不一致'); }); if (conflictDialog && conflictDialog.offsetParent !== null && conflictDialog.style.display !== 'none') { logMsg('检测到“学习记录结束点不一致”错误弹窗,正在自动刷新页面重新同步服务端真实进度...'); showToast('检测到进度冲突,正在刷新页面重新同步服务端真实进度...', 'error'); await sleep(2000); location.reload(); return; } // 4. 通用 Element UI 弹框确认 (如多端登录确认或操作提示) const msgBoxBtn = document.querySelector('.el-message-box__wrapper .el-button--primary'); if (msgBoxBtn && msgBoxBtn.offsetParent !== null) { msgBoxBtn.click(); } } maintainVideoPlayback() { const video = document.querySelector('video'); if (!video) return; // 静音播放确保浏览器背景播放不被策略拦截 if (!video.muted) { video.muted = true; video.volume = 0; } // 自动续播:若视频被暂停且未结束,自动恢复播放 if (video.paused && !video.ended) { // 如果当前时间已接近终点(容差 2.0 秒),不要再强行调用 play(),防止反复循环尾部 if (video.duration > 0 && (video.duration - video.currentTime) <= 2.0) { return; } video.play().catch(() => {}); } // 锁定标准 1.0x 正常倍速播放(平台服务端强制 1:1 物理学时校验) if (video.playbackRate !== 1.0) { video.playbackRate = 1.0; } } async checkVideoEndedAndAdvance() { const autoNext = GM_getValue('hnedu_autoNextVideo', true); if (!autoNext) return; const video = document.querySelector('video'); if (!video) return; // 判断视频是否播放完毕 (容差 2.0 秒) const isEnded = video.ended || (video.duration > 0 && Math.abs(video.currentTime - video.duration) <= 2.0); if (!isEnded) return; if (this.isAdvancing) return; this.isAdvancing = true; try { logMsg('当前小节视频播放完毕,正在准备进入下一小节...'); await sleep(1500); // 留出请求完成缓冲 // 查找右侧目录树节点 const treeNodes = Array.from(document.querySelectorAll('.rightTree .custom-tree-node')); if (!treeNodes || treeNodes.length === 0) return; // 筛选出所有真正的小节节点 (排除章标题 .treeZhang) const jieNodes = treeNodes.filter(n => n.querySelector('.treeJie') !== null); if (jieNodes.length === 0) return; // 查找当前正在播放小节的索引 const currentIndex = jieNodes.findIndex(n => n.querySelector('.selectTreeJie') !== null); let nextNode = null; let nextTitle = ''; // 1. 优先从当前小节之后寻找第一个未达到 100% 的小节 if (currentIndex !== -1) { for (let i = currentIndex + 1; i < jieNodes.length; i++) { const node = jieNodes[i]; const percentEl = node.querySelector('.numberPercent'); const percentText = percentEl ? percentEl.innerText.trim() : ''; const isDone = percentText.includes('100%') || parseInt(percentText, 10) >= 100; if (!isDone) { nextNode = node; nextTitle = node.querySelector('.treeJie')?.innerText.trim() || '下一小节'; break; } } } // 2. 如果后方全部学完,从头寻找前面是否有遗漏的小节 if (!nextNode && currentIndex > 0) { for (let i = 0; i < currentIndex; i++) { const node = jieNodes[i]; const percentEl = node.querySelector('.numberPercent'); const percentText = percentEl ? percentEl.innerText.trim() : ''; const isDone = percentText.includes('100%') || parseInt(percentText, 10) >= 100; if (!isDone) { nextNode = node; nextTitle = node.querySelector('.treeJie')?.innerText.trim() || '未学小节'; break; } } } // 3. 所有小节均已学完 if (!nextNode) { logMsg('当前课程的所有小节均已学完 (100%)!'); const autoCourse = GM_getValue('hnedu_autoNextCourse', true); if (autoCourse) { isCurrentCourseFullyDone = true; logMsg('开启了自动学习下一门课程,准备返回课程列表...'); showToast('本课程已全部学完,3秒后返回列表学习下一门...', 'success'); await sleep(3000); location.href = 'https://www.hnedu123.com.cn/personal-center/my-course?index=0'; } return; } // 4. 执行平滑切换 logMsg(`自动切换播放下一小节: 【${nextTitle}】`); showToast(`切换播放: ${nextTitle}`); const clickTarget = nextNode.querySelector('.treeJie') || nextNode; clickTarget.click(); await sleep(2500); const newVideo = document.querySelector('video'); if (newVideo) { newVideo.muted = true; newVideo.volume = 0; newVideo.play().catch(() => {}); } } finally { this.isAdvancing = false; } } // ---------------------------------------------------- // 6.2 课程列表页逻辑 (自动查找未学课程并进入) // ---------------------------------------------------- async handleListPageLoop() { const autoCourse = GM_getValue('hnedu_autoNextCourse', true); if (!autoCourse) return; const courseCards = document.querySelectorAll('.listBox ul li'); if (!courseCards || courseCards.length === 0) { return; } let unfinishedCard = null; let unfinishedTitle = ''; for (const card of courseCards) { const titleEl = card.querySelector('.courseTitle'); const progressEl = card.querySelector('.el-progress__text'); const progressText = progressEl ? progressEl.innerText.trim() : ''; const title = titleEl ? titleEl.innerText.trim() : '课程'; const isDone = progressText.includes('100%') || parseInt(progressText, 10) >= 100; if (!isDone) { if (!unfinishedCard) { unfinishedCard = card; unfinishedTitle = title; } } } if (unfinishedCard) { logMsg(`发现未完成课程: 【${unfinishedTitle}】,准备进入学习...`); showToast(`进入课程: ${unfinishedTitle}`); await sleep(1500); const studyBtn = unfinishedCard.querySelector('.studyBtn') || unfinishedCard.querySelector('.bottomBtn div'); if (studyBtn) { studyBtn.click(); await sleep(3000); } return; } // 当前页若全满,尝试翻页 const pagerNext = document.querySelector('.el-pager li.active + li.number'); const btnNext = document.querySelector('.btn-next:not([disabled])'); if (pagerNext || (btnNext && !btnNext.disabled)) { logMsg('当前页课程已全部学完,自动翻到下一页...'); showToast('正在翻到下一页课程...'); await sleep(1500); if (pagerNext) pagerNext.click(); else if (btnNext) btnNext.click(); await sleep(2000); return; } logMsg('恭喜!所有已选课程已全部学完!'); showToast('所有已选课程全部学完!', 'success'); GM_setValue('hnedu_autoRunning', false); updatePanelUI(); } } // ========================================== // 7. 现代化悬浮控制面板 (UI Panel) // ========================================== function injectPanelStyles() { GM_addStyle(` #hnedu-panel-container { position: fixed; bottom: 25px; right: 25px; width: 320px; background: #ffffff; border-radius: 14px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18); border: 1px solid #e1e8ed; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; z-index: 999999999; color: #2c3e50; overflow: hidden; transition: transform 0.3s ease, opacity 0.3s ease; user-select: none; } #hnedu-panel-header { background: linear-gradient(135deg, #2b5876 0%, #4e4376 100%); color: #ffffff; padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; cursor: move; } #hnedu-panel-header .title { font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 8px; } #hnedu-panel-header .badge { font-size: 11px; background: rgba(255, 255, 255, 0.25); padding: 2px 6px; border-radius: 10px; } #hnedu-panel-header .ctrl-btn { background: none; border: none; color: #ffffff; font-size: 16px; cursor: pointer; opacity: 0.8; padding: 0 4px; } #hnedu-panel-header .ctrl-btn:hover { opacity: 1; } #hnedu-panel-body { padding: 14px 16px; font-size: 13px; } .hnedu-status-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px dashed #edf2f7; } .hnedu-tag { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; } .tag-green { background: #e8f8f5; color: #27ae60; } .tag-gray { background: #f2f4f4; color: #7f8c8d; } .tag-blue { background: #ebf5fb; color: #2980b9; } .hnedu-opt-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .hnedu-opt-label { font-weight: 500; color: #34495e; } .hnedu-switch { position: relative; display: inline-block; width: 38px; height: 20px; } .hnedu-switch input { opacity: 0; width: 0; height: 0; } .hnedu-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .3s; border-radius: 20px; } .hnedu-slider:before { position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; } input:checked + .hnedu-slider { background-color: #2ecc71; } input:checked + .hnedu-slider:before { transform: translateX(18px); } .hnedu-main-btn { width: 100%; padding: 10px 0; border-radius: 8px; border: none; font-size: 14px; font-weight: 600; cursor: pointer; margin-top: 6px; transition: all 0.2s; } .btn-start { background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%); color: #fff; box-shadow: 0 4px 12px rgba(46, 204, 113, 0.35); } .btn-start:hover { filter: brightness(1.06); } .btn-stop { background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: #fff; box-shadow: 0 4px 12px rgba(230, 126, 34, 0.35); } .btn-stop:hover { filter: brightness(1.06); } #hnedu-log-content { margin-top: 10px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; height: 75px; padding: 6px 8px; font-size: 11px; color: #64748b; overflow-y: auto; font-family: Consolas, monospace; } #hnedu-toggle-float-btn { position: fixed; bottom: 25px; right: 25px; width: 48px; height: 48px; background: linear-gradient(135deg, #2b5876 0%, #4e4376 100%); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); cursor: pointer; z-index: 999999998; font-size: 20px; transition: transform 0.2s; } #hnedu-toggle-float-btn:hover { transform: scale(1.08); } `); } function createPanelDOM() { if (!document.body || document.getElementById('hnedu-panel-container')) return; const isAuto = GM_getValue('hnedu_autoRunning', true); GM_deleteValue('hnedu_playbackRate'); const nextVideo = GM_getValue('hnedu_autoNextVideo', true); const nextCourse = GM_getValue('hnedu_autoNextCourse', true); // 创建控制面板 const container = document.createElement('div'); container.id = 'hnedu-panel-container'; container.innerHTML = `
⚡ 云畔挂机助手 v${GM_info.script.version}
运行状态 ${isAuto ? '● 自动运行中' : '○ 已暂停'}
播放倍速 1.0x 标准学时
后台防暂停/防跳转 已锁定保护
弹窗验证/题目拦截 自动拦截与应答
视频播放完毕自动下一节
课程学完自动下一门
`; // 最小化恢复浮标 let floatBtn = document.getElementById('hnedu-toggle-float-btn'); if (!floatBtn) { floatBtn = document.createElement('div'); floatBtn.id = 'hnedu-toggle-float-btn'; floatBtn.innerHTML = '⚡'; floatBtn.title = '展开挂机助手面板'; floatBtn.style.display = 'none'; document.body.appendChild(floatBtn); } document.body.appendChild(container); // 绑定事件 const minBtn = document.getElementById('hnedu-btn-minimize'); if (minBtn) { minBtn.onclick = () => { container.style.display = 'none'; floatBtn.style.display = 'flex'; }; } floatBtn.onclick = () => { container.style.display = 'block'; floatBtn.style.display = 'none'; }; // 开关按钮 const toggleBtn = document.getElementById('hnedu-btn-toggle'); toggleBtn.onclick = () => { const current = GM_getValue('hnedu_autoRunning', true); const next = !current; GM_setValue('hnedu_autoRunning', next); updatePanelUI(); logMsg(next ? '已开启自动化挂机' : '已暂停挂机'); }; // 选项切换 document.getElementById('hnedu-chk-next-video').onchange = e => { GM_setValue('hnedu_autoNextVideo', e.target.checked); logMsg(`自动下一节: ${e.target.checked ? '开' : '关'}`); }; document.getElementById('hnedu-chk-next-course').onchange = e => { GM_setValue('hnedu_autoNextCourse', e.target.checked); logMsg(`自动下一门课程: ${e.target.checked ? '开' : '关'}`); }; // 面板简单拖拽 const header = document.getElementById('hnedu-panel-header'); let isDragging = false, startX, startY, initLeft, initTop; header.onmousedown = e => { if (e.target.tagName === 'BUTTON') return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = container.getBoundingClientRect(); initLeft = rect.left; initTop = rect.top; container.style.bottom = 'auto'; container.style.right = 'auto'; container.style.left = `${initLeft}px`; container.style.top = `${initTop}px`; document.onmousemove = ev => { if (!isDragging) return; const dx = ev.clientX - startX; const dy = ev.clientY - startY; container.style.left = `${initLeft + dx}px`; container.style.top = `${initTop + dy}px`; }; document.onmouseup = () => { isDragging = false; document.onmousemove = null; document.onmouseup = null; }; }; logMsg('助手面板加载完成,自动准备就绪'); } function updatePanelUI() { const isAuto = GM_getValue('hnedu_autoRunning', true); const statusBadge = document.getElementById('hnedu-status-badge'); const toggleBtn = document.getElementById('hnedu-btn-toggle'); if (statusBadge) { statusBadge.className = `hnedu-tag ${isAuto ? 'tag-green' : 'tag-gray'}`; statusBadge.textContent = isAuto ? '● 自动运行中' : '○ 已暂停'; } if (toggleBtn) { toggleBtn.className = `hnedu-main-btn ${isAuto ? 'btn-stop' : 'btn-start'}`; toggleBtn.textContent = isAuto ? '暂停自动化挂机' : '启动自动化挂机'; } } // ========================================== // 8. 入口主函数 (Init) // ========================================== function init() { injectPanelStyles(); createPanelDOM(); new CourseController(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();