// ==UserScript== // @name 网课全自动助手 (elearnmooc) // @name:en elearnmooc_helper // @icon https://www.elearnmooc.com/favicon.ico // @namespace https://github.com/Relianttt // @version 1.2 // @description elearnmooc 网课全自动助手:图形化控制面板,支持自动倍速播放、静音、自动连播下一节、智能处理结束弹窗、列表页自动检索未完成任务 // @description:en Auto course helper for elearnmooc: GUI control panel with auto playback speed, mute, auto-next, smart popup handling, and auto-scan for incomplete tasks // @author reliant // @license MIT // @icon https://www.elearnmooc.com/favicon.ico // @match *://www.elearnmooc.com/* // @match *://elearnmooc.com/* // @grant GM_getValue // @grant GM_setValue // @run-at document-end // @noframes // ==/UserScript== (function () { 'use strict'; // --- 存储封装(GM 优先,fallback localStorage)--- function decodeStoredValue(raw, fallback) { if (raw === undefined) return fallback; // GM/localStorage 可能返回 string / number / boolean / object(历史版本可能直接存 object) if (typeof raw !== 'string') return raw; try { return JSON.parse(raw); } catch { // 兼容历史“非 JSON 字符串”场景 return raw; } } function encodeStoredValue(value) { // 统一编码为 JSON 字符串,保证跨脚本管理器类型一致 if (value === undefined) return undefined; try { return JSON.stringify(value); } catch { return undefined; } } function storageGet(key, fallback) { // 1) GM(全站统一) try { if (typeof GM_getValue === 'function') { const raw = GM_getValue(key); if (raw !== undefined) return decodeStoredValue(raw, fallback); } } catch { } // 2) localStorage(同域 fallback,并尝试自动迁移到 GM) try { const raw = localStorage.getItem(key); if (raw !== null) { const decoded = decodeStoredValue(raw, fallback); try { if (typeof GM_setValue === 'function') { const enc = encodeStoredValue(decoded); if (enc !== undefined) GM_setValue(key, enc); } } catch { } return decoded; } } catch { } return fallback; } function storageSet(key, value) { if (value === undefined) { // Keep the old value rather than accidentally wiping user config due to a bug upstream. try { console.warn('[mooc-helper] storageSet: value is undefined; skip write for key:', key); } catch { } return; } const enc = encodeStoredValue(value); if (enc === undefined) { // e.g. circular structure passed in try { console.warn('[mooc-helper] storageSet: failed to encode value; skip write for key:', key); } catch { } return; } // GM + localStorage 双写:GM 负责全站统一,localStorage 负责无 GM 环境下可用 try { if (typeof GM_setValue === 'function') GM_setValue(key, enc); } catch { } try { localStorage.setItem(key, enc); } catch { } } // --- 配置存储键名 --- const STORAGE_KEY = 'mooc_auto_helper_config'; // --- 默认配置 --- const defaultConfig = { speed: 2.0, isMuted: true, autoNext: true, autoScan: true }; // --- 加载/保存配置 --- function loadConfig() { const saved = storageGet(STORAGE_KEY, null); if (saved && typeof saved === 'object' && !Array.isArray(saved)) { return { ...defaultConfig, ...saved }; } return { ...defaultConfig }; } function saveConfig() { storageSet(STORAGE_KEY, config); } // --- 全局配置(从存储加载)--- let config = loadConfig(); // --- 模拟鼠标点击 --- function simulateClick(element) { // 获取元素位置用于事件坐标 const rect = element.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; // 事件通用配置 const eventOptions = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y, screenX: x, screenY: y, button: 0, buttons: 1 }; // 1. 先聚焦元素 if (element.focus) element.focus(); // 2. 派发 pointerdown 事件(现代浏览器) try { element.dispatchEvent(new PointerEvent('pointerdown', eventOptions)); } catch (e) { } // 3. 派发 mousedown 事件 element.dispatchEvent(new MouseEvent('mousedown', eventOptions)); // 4. 派发 pointerup 事件 try { element.dispatchEvent(new PointerEvent('pointerup', eventOptions)); } catch (e) { } // 5. 派发 mouseup 事件 element.dispatchEvent(new MouseEvent('mouseup', eventOptions)); // 6. 派发 click 事件 element.dispatchEvent(new MouseEvent('click', eventOptions)); // 7. 最后调用原生 click 方法(双重保险) if (element.click) element.click(); } // --- UI 界面渲染 --- const POS_KEY = 'mooc_auto_helper_pos'; const savedPos = (() => { const raw = storageGet(POS_KEY, null); if (raw && Number.isFinite(raw.top) && Number.isFinite(raw.left)) return raw; return null; })(); const panel = document.createElement('div'); Object.assign(panel.style, { position: 'fixed', top: savedPos ? savedPos.top + 'px' : '120px', left: savedPos ? savedPos.left + 'px' : 'auto', right: savedPos ? 'auto' : '20px', zIndex: '99999', width: '210px', padding: '15px', background: '#fff', border: '2px solid #007bff', borderRadius: '10px', boxShadow: '0 4px 15px rgba(0,0,0,0.2)', fontFamily: 'sans-serif', userSelect: 'none' }); panel.innerHTML = `

⠿ 网课自动助手

识别中...
`; document.body.appendChild(panel); // --- 初始化时 clamp 位置到当前视口 --- if (savedPos) { const maxLeft = window.innerWidth - panel.offsetWidth; const maxTop = window.innerHeight - panel.offsetHeight; panel.style.left = Math.max(0, Math.min(savedPos.left, maxLeft)) + 'px'; panel.style.top = Math.max(0, Math.min(savedPos.top, maxTop)) + 'px'; } // --- 拖拽逻辑(使用 Pointer Events 兼容触屏)--- const dragHandle = panel.querySelector('#panelDragHandle'); let isDragging = false, dragOffsetX = 0, dragOffsetY = 0; dragHandle.addEventListener('pointerdown', (e) => { if (e.button !== 0) return; // 仅响应左键/主指 isDragging = true; dragOffsetX = e.clientX - panel.getBoundingClientRect().left; dragOffsetY = e.clientY - panel.getBoundingClientRect().top; dragHandle.setPointerCapture(e.pointerId); e.preventDefault(); }); document.addEventListener('pointermove', (e) => { if (!isDragging) return; let newLeft = e.clientX - dragOffsetX; let newTop = e.clientY - dragOffsetY; // 边界约束 newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - panel.offsetWidth)); newTop = Math.max(0, Math.min(newTop, window.innerHeight - panel.offsetHeight)); panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px'; panel.style.right = 'auto'; }); function endDrag() { if (!isDragging) return; isDragging = false; // 保存位置 storageSet(POS_KEY, { top: parseInt(panel.style.top), left: parseInt(panel.style.left) }); } document.addEventListener('pointerup', endDrag); document.addEventListener('pointercancel', endDrag); // --- 获取 UI 元素 --- const speedRange = panel.querySelector('#speedRange'); const speedVal = panel.querySelector('#speedVal'); const statusInfo = panel.querySelector('#statusInfo'); const muteCheck = panel.querySelector('#muteCheck'); const nextCheck = panel.querySelector('#nextCheck'); const scanCheck = panel.querySelector('#scanCheck'); // --- 从配置恢复 UI 状态 --- speedRange.value = config.speed; speedVal.innerText = config.speed; muteCheck.checked = config.isMuted; nextCheck.checked = config.autoNext; scanCheck.checked = config.autoScan; // --- 绑定事件:更改时保存配置 --- speedRange.oninput = () => { config.speed = parseFloat(speedRange.value); speedVal.innerText = config.speed; saveConfig(); }; muteCheck.onchange = () => { config.isMuted = muteCheck.checked; saveConfig(); }; nextCheck.onchange = () => { config.autoNext = nextCheck.checked; saveConfig(); }; scanCheck.onchange = () => { config.autoScan = scanCheck.checked; saveConfig(); }; // --- 核心逻辑 --- function mainLoop() { const video = document.querySelector('video.videoplayer'); const nextBtn = document.querySelector('.next_chapter'); // 优先检测场景 B:处理结束弹窗 // 优先检查 alertbox_group 内的按钮 let confirmBackBtn = document.querySelector('div.alertbox_group button.theme_2'); // 如果没找到,尝试查找其他弹窗结构中的确定按钮 if (!confirmBackBtn) { // 查找所有 theme_2 按钮,检查是否在弹窗中 const allTheme2Btns = document.querySelectorAll('button.theme_2'); for (let btn of allTheme2Btns) { // 检查按钮文本是否为"确定",且附近有弹窗提示文字 if (btn.innerText.includes("确定")) { // 检查是否在弹窗容器中(不是笔记区等其他区域) const parent = btn.closest('.alertbox, .layer, .modal, .popup, .dialog, [class*="alert"], [class*="layer"]'); if (parent) { confirmBackBtn = btn; break; } // 或者检查页面上是否有"返回课程内容"相关文字 if (document.body.innerText.includes("是否返回课程内容") || document.body.innerText.includes("返回列表")) { confirmBackBtn = btn; break; } } } } if (confirmBackBtn && confirmBackBtn.innerText.includes("确定")) { statusInfo.innerText = "状态: 正在自动返回列表"; // 从当前 URL 获取 courseId 和 termId const urlParams = new URLSearchParams(window.location.search); const courseId = urlParams.get('courseId'); const termId = urlParams.get('termId'); if (courseId && termId) { const targetUrl = `/pages/learning/videoCourseware.jsp?courseId=${courseId}&termId=${termId}`; window.location.href = targetUrl; } else { // 如果获取不到参数,降级为模拟点击 simulateClick(confirmBackBtn); } return; } // 场景 A:视频播放中 if (video) { statusInfo.innerText = "状态: 正在监控播放器"; video.muted = muteCheck.checked; if (video.playbackRate !== config.speed) video.playbackRate = config.speed; if (video.paused && !video.ended) video.play().catch(() => { }); video.onended = () => { if (nextCheck.checked && nextBtn && !nextBtn.disabled) { nextBtn.click(); } }; return; } // 场景 C:列表页扫描与自动展开 if (scanCheck.checked) { const statusLabels = document.querySelectorAll('.loadStatus'); for (let label of statusLabels) { if (label.innerText.includes("未完成")) { const chapterHeader = label.closest('.chapter_title_box'); if (chapterHeader) { const parent = chapterHeader.parentElement; const contentArea = parent.querySelector('.chapter_content'); if (contentArea && (contentArea.style.display === 'none' || getComputedStyle(contentArea).display === 'none')) { statusInfo.innerText = "状态: 正在展开未完成章节..."; chapterHeader.click(); return; } if (contentArea) { const allPlayIcons = contentArea.querySelectorAll('i.fa-play-circle.video_play_icon'); for (let icon of allPlayIcons) { if (icon.style.display !== 'none') { // 使用模拟点击触发播放 statusInfo.innerText = "状态: 发现未播放任务,正在进入..."; const markerName = 'data-mooc-helper-click-target'; const markerValue = String(Date.now()) + Math.random().toString(16).slice(2); icon.setAttribute(markerName, markerValue); const injected = document.createElement('script'); injected.textContent = "(function(){var icon=document.querySelector('[data-mooc-helper-click-target=\"" + markerValue + "\"]');if(!icon)return;var jq=window.jQuery;if(jq&&jq.fn&&typeof jq.fn.triggerHandler==='function'){jq(icon).triggerHandler('click');}else if(jq&&jq.fn&&typeof jq.fn.trigger==='function'){jq(icon).trigger('click');}else if(icon.click){icon.click();}})();"; (document.documentElement || document.body).appendChild(injected); injected.remove(); icon.removeAttribute(markerName); return; } } } } } } statusInfo.innerText = "状态: 扫描中/暂无未完任务"; } } // 每 2.5 秒执行一次 setInterval(mainLoop, 2500); })();