// ==UserScript== // @name 湖南人社-湖南云畔-全自动挂机助手 // @version 1.1.0 // @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('[湖南云畔助手] 脚本开始加载,初始化核心防护与拦截系统...'); // ========================================== // 1. 核心底层 Hook:彻底解决“离开窗口就暂停” // ========================================== function installAntiBlurHooks() { try { // 欺骗页面:将 document.visibilityState 永久锁定为 'visible',document.hidden 永久为 false Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true }); Object.defineProperty(document, 'hidden', { get: () => false, configurable: true }); // 拦截可能挂载在 document 或 window 上的事件监听 const originalAddEventListener = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = function (type, listener, options) { // 彻底阻止 visibilitychange 监听器注册,使页面的 handleVisibilityChange 无法被触发 if (type === 'visibilitychange') { // console.log('[湖南云畔助手] 成功拦截 visibilitychange 监听注册'); return; } // 阻止窗口失焦 blur 暂停事件 if (type === 'blur' && (this === window || this === document)) { // console.log('[湖南云畔助手] 成功拦截 window.blur 监听注册'); return; } // 拦截 video 元素的 ratechange 事件,防止播放器因倍速检测重置速率或暂停 if (type === 'ratechange' && (this instanceof HTMLVideoElement || this.tagName === 'VIDEO')) { const wrappedListener = function (event) { const targetRate = GM_getValue('hnedu_playbackRate', 1.0); if (targetRate > 1.0) { event.stopImmediatePropagation(); 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) {} console.log('[湖南云畔助手] 后台防暂停 (Visibility / Blur) 底层 Hook 注入成功!'); } catch (err) { console.error('[湖南云畔助手] AntiBlur Hook 异常:', err); } } installAntiBlurHooks(); // ========================================== // 2. 网络请求拦截:屏蔽验证码与自动提取题目答案 // ========================================== window._latestQuestionAnswer = null; function installAjaxHooks() { if (typeof ajaxHooker === 'undefined') { console.warn('[湖南云畔助手] ajaxHooker 未加载,等待重试...'); return; } ajaxHooker.hook(request => { // 拦截随机随堂练习题目,获取题目数据并自动提取正确答案 (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); } }; } // 拦截 60 秒轮询弹出的图像/验证码题目 (typeA = 2) // 该接口若返回 data: null,网页内部 logic (P.data && (...)) 将不会触发任何弹窗与暂停! else if (request.url.includes('vedioTransferQuestionsRecord/queryQuestion')) { request.response = res => { try { res.responseText = JSON.stringify({ code: 200, msg: "success", data: null }); console.log('[湖南云畔助手] 成功屏蔽中途图形/验证码弹窗请求'); } catch (e) { console.error('[湖南云畔助手] 屏蔽图形验证码异常:', e); } }; } // 屏蔽旧接口可能残留的频控或验证请求 else if (request.url.includes('learning/learnVerify')) { request.abort = true; } }); console.log('[湖南云畔助手] Ajax 请求拦截器已激活!'); } try { installAjaxHooks(); } catch (e) { console.error('[湖南云畔助手] 注册 AjaxHook 失败:', e); } // ========================================== // 3. 全局辅助工具函数 // ========================================== 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); } // ========================================== // 4. 自动化调度控制器 (Controller) // ========================================== class CourseController { constructor() { this.timer = null; this.isBusy = false; this.lastRoute = location.href; this.initRouteWatcher(); this.startLoop(); } // 监听 SPA 路由切换 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 () { const res = origPush.apply(this, arguments); checkRoute(); return res; }; const origReplace = history.replaceState; history.replaceState = function () { const res = origReplace.apply(this, arguments); checkRoute(); return res; }; setInterval(checkRoute, 1000); } onRouteChanged() { if (this.isCourseListPage()) { logMsg('当前处于课程列表页'); } else if (this.isCourseDetailPage()) { 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 () => { 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; } }, 1500); } // ---------------------------------------------------- // 4.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(500); } // 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. 通用 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) { // 如果正在倒计时或答题中,可稍缓,否则立即播放 video.play().catch(() => {}); } // 设置倍速播放 const targetRate = GM_getValue('hnedu_playbackRate', 1.0); if (video.playbackRate !== targetRate) { video.playbackRate = targetRate; } // 同步 Vue 组件内部的 pauseTime,彻底杜绝网页内置“请勿拖动进度条或倍速播放”重置机制 try { const box = document.querySelector('.videoContainerBox'); const comp = box?.__vue__ || document.querySelector('.rightTree')?.__vue__; if (comp && typeof comp.pauseTime !== 'undefined') { comp.pauseTime = Math.floor(video.currentTime); } } catch (e) {} } async checkVideoEndedAndAdvance() { const autoNext = GM_getValue('hnedu_autoNextVideo', true); if (!autoNext) return; const video = document.querySelector('video'); if (!video) return; // 判断视频是否播放完毕 const isEnded = video.ended || (video.duration > 0 && Math.abs(video.currentTime - video.duration) < 1.0); if (!isEnded) return; logMsg('当前小节视频播放完毕,正在保存进度并查找下一小节...'); await sleep(2000); // 等待平台 updateCourseStudyRecord 提交完成 // 查找右侧目录树节点 const treeNodes = document.querySelectorAll('.rightTree .custom-tree-node'); if (!treeNodes || treeNodes.length === 0) return; let nextNode = null; let nextTitle = ''; let allCompleted = true; for (const node of treeNodes) { const jie = node.querySelector('.treeJie'); if (!jie) continue; // 忽略章标题 (.treeZhang) const percentEl = node.querySelector('.numberPercent'); const percentText = percentEl ? percentEl.innerText.trim() : ''; const isDone = percentText.includes('100%') || parseInt(percentText, 10) >= 100; if (!isDone) { allCompleted = false; if (!nextNode) { nextNode = node; nextTitle = jie.innerText.trim(); } } } if (allCompleted) { logMsg('当前课程的所有小节均已学完 (100%)!'); const autoCourse = GM_getValue('hnedu_autoNextCourse', true); if (autoCourse) { logMsg('开启了自动学习下一门课程,准备返回课程列表...'); showToast('本课程已全部学完,3秒后返回列表学习下一门...', 'success'); await sleep(3000); location.href = 'https://www.hnedu123.com.cn/personal-center/my-course?index=0'; } return; } if (nextNode) { const isCurrentActive = nextNode.querySelector('.selectTreeJie') !== null; if (!isCurrentActive) { logMsg(`自动切换播放下一小节: 【${nextTitle}】`); showToast(`切换播放: ${nextTitle}`); const clickTarget = nextNode.querySelector('.treeJie') || nextNode; clickTarget.click(); await sleep(2000); const newVideo = document.querySelector('video'); if (newVideo) { newVideo.muted = true; newVideo.volume = 0; newVideo.play().catch(() => {}); } } } } // ---------------------------------------------------- // 4.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(); } } // ========================================== // 5. 现代化悬浮控制面板 (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-speed-group { display: flex; gap: 5px; } .hnedu-speed-btn { border: 1px solid #dcdfe6; background: #ffffff; color: #606266; padding: 3px 8px; border-radius: 6px; font-size: 12px; cursor: pointer; transition: all 0.2s; } .hnedu-speed-btn.active { background: #4e4376; color: #ffffff; border-color: #4e4376; } .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.getElementById('hnedu-panel-container')) return; const isAuto = GM_getValue('hnedu_autoRunning', true); const rate = GM_getValue('hnedu_playbackRate', 1.0); 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 ? '● 自动运行中' : '○ 已暂停'}
离开窗口防暂停 已持续保护
弹窗验证/题目拦截 自动拦截与应答
播放倍速
视频播放完毕自动下一节
课程学完自动下一门
`; // 最小化恢复浮标 const floatBtn = document.createElement('div'); floatBtn.id = 'hnedu-toggle-float-btn'; floatBtn.innerHTML = '⚡'; floatBtn.title = '展开挂机助手面板'; floatBtn.style.display = 'none'; document.body.appendChild(container); document.body.appendChild(floatBtn); // 绑定事件 document.getElementById('hnedu-btn-minimize').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 ? '已开启自动化挂机' : '已暂停挂机'); }; // 倍速切换 container.querySelectorAll('.hnedu-speed-btn').forEach(btn => { btn.onclick = () => { const s = parseFloat(btn.getAttribute('data-speed')); GM_setValue('hnedu_playbackRate', s); container.querySelectorAll('.hnedu-speed-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); logMsg(`已设置播放倍速为: ${s}x`); }; }); // 选项切换 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 ? '暂停自动化挂机' : '启动自动化挂机'; } } // ========================================== // 6. 入口主函数 (Init) // ========================================== function init() { injectPanelStyles(); createPanelDOM(); new CourseController(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();