// ==UserScript== // @name 湖南大学江西专技平台 - 湖南大学国家级专业技术人员继续教育基地刷课助手(jxjy.hnu.edu.cn) // @version 1.0 // @description 湖南大学江西专技平台自动刷课,支持暂停/恢复 // @author 晓晓 // @match https://jxjy.hnu.edu.cn/* // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; var STATUS_KEY = 'hnuedu_running'; var WAIT_KEY = 'hnuedu_wait'; var CHAPTER_KEY = 'hnuedu_chapter'; function getRunning() { return GM_getValue(STATUS_KEY, false); } function setRunning(v) { GM_setValue(STATUS_KEY, v); } function getWait() { return GM_getValue(WAIT_KEY, false); } function setWait(v) { GM_setValue(WAIT_KEY, v); } var isRunning = false; var pollTimer = null; var loopTimer = null; // ==================== 日志 ==================== function log(msg, type) { type = type || 'info'; var prefix = { error: '❌', success: '✅', warning: '⚠️' }[type] || '🔍'; console.log('[湖大刷课]', prefix, msg); var el = document.getElementById('hnu-log'); if (el) { el.textContent = msg; } } // ==================== 页面检测 ==================== function isVisible(el) { if (!el) return false; var s = window.getComputedStyle(el); return s.display !== 'none' && s.visibility !== 'hidden' && el.getClientRects().length > 0; } function isFinished() { var tips = document.querySelector('.js-finish-tips'); return !!(tips && isVisible(tips)); } function isWatching() { var remain = document.querySelector('.js-remain-time'); return !!(remain && isVisible(remain)); } function isLearnPage() { return !!document.querySelector('.js-state-opration, .js-audio-convert-box, .js-learned-prompt'); } function parseChineseTime(text) { var total = 0; var h = text.match(/(\d+)\s*小时/); var m = text.match(/(\d+)\s*分/); var s = text.match(/(\d+)\s*秒/); if (h) total += parseInt(h[1], 10) * 3600; if (m) total += parseInt(m[1], 10) * 60; if (s) total += parseInt(s[1], 10); return total > 0 ? total : null; } function remainSeconds() { var remain = document.querySelector('.js-remain-time'); if (!remain) return null; var span = remain.querySelector('.color-danger') || remain.querySelector('span'); var text = span ? span.textContent.trim() : remain.textContent.trim(); return parseChineseTime(text); } // ==================== 模拟人工行为 ==================== function simulateMouseEnter(el) { var rect = el.getBoundingClientRect(); var base = { bubbles: true, cancelable: true, view: window, clientX: Math.round(rect.left + rect.width / 2), clientY: Math.round(rect.top + rect.height / 2) }; ['pointerover','mouseover','pointermove','mousemove','pointerenter','mouseenter'].forEach(function(type) { try { el.dispatchEvent(new MouseEvent(type, base)); } catch(e) {} }); } function isSamePage(url) { try { return new URL(url, location.href).pathname === location.pathname; } catch(e) { return false; } } function nextUrlFromDataContent() { var el = document.querySelector('.js-learned-prompt'); if (!el) return null; var html = el.getAttribute('data-content'); if (!html) return null; try { var doc = new DOMParser().parseFromString(html, 'text/html'); var btn = doc.querySelector('.learn-next-btn'); return btn ? btn.getAttribute('href') : null; } catch(e) { return null; } } function waitForPopoverButton(timeout) { return new Promise(function(resolve) { var start = Date.now(); function check() { var btn = document.querySelector('.popover .learn-next-btn, .js-learned-prompt .learn-next-btn'); if (btn && isVisible(btn)) return resolve(btn); if (Date.now() - start > timeout) return resolve(null); setTimeout(check, 300); } check(); }); } function sleep(ms) { return new Promise(function(r) { setTimeout(r, ms); }); } function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function humanDelay() { return sleep(rand(3000, 10000)); } async function gotoNextTask() { var promptBtn = document.querySelector('.js-learned-prompt'); if (!promptBtn) { log('未找到"任务完成"按钮'); return; } await humanDelay(); simulateMouseEnter(promptBtn); var nextBtn = await waitForPopoverButton(5000); if (!nextBtn) { simulateMouseEnter(promptBtn); nextBtn = await waitForPopoverButton(3000); } var nextUrl = nextBtn ? nextBtn.getAttribute('href') : nextUrlFromDataContent(); if (!nextUrl) { log('未找到下一任务链接'); return; } if (isSamePage(nextUrl)) { log('下一任务与当前页相同,跳过'); return; } await humanDelay(); if (nextBtn && document.contains(nextBtn)) { log('跳转下一任务: ' + nextUrl); simulateMouseEnter(nextBtn); nextBtn.click(); } else { log('直接跳转: ' + nextUrl); location.assign(nextUrl); } } function keepMediaPlaying() { var medias = Array.from(document.querySelectorAll('video, audio')); document.querySelectorAll('iframe').forEach(function(f) { try { if (f.contentDocument) medias.push.apply(medias, Array.from(f.contentDocument.querySelectorAll('video, audio'))); } catch(e) {} }); medias.forEach(function(m) { if (m.paused && !m.ended) { m.play().then(function() { log('已恢复暂停的媒体'); }).catch(function() {}); } }); } // ==================== 主循环 ==================== var noStatusStart = null; function tick() { if (!isRunning) return; keepMediaPlaying(); if (isFinished()) { log('本节已完成,准备跳转…'); gotoNextTask(); return; } if (isWatching()) { var sec = remainSeconds(); var display = sec !== null ? '剩余 ' + Math.floor(sec/60) + ' 分 ' + (sec%60) + ' 秒' : '学习中…'; log(display); noStatusStart = null; } else { if (!noStatusStart) noStatusStart = Date.now(); if (Date.now() - noStatusStart > 90000) { log('等待超时,尝试跳转…'); gotoNextTask(); noStatusStart = Date.now(); } else { log('等待页面加载…'); } } } function startLoop() { isRunning = true; setRunning(true); log('刷课已启动'); var badge = document.getElementById('hnu-badge'); if (badge) badge.textContent = '● 运行中'; var btn = document.getElementById('hnu-ctrl'); if (btn) btn.textContent = '暂停刷课'; var pollMin = 4000, pollMax = 8000; clearInterval(pollTimer); pollTimer = setInterval(function() { tick(); }, rand(pollMin, pollMax)); } function stopLoop() { isRunning = false; setRunning(false); clearInterval(pollTimer); log('刷课已暂停'); var badge = document.getElementById('hnu-badge'); if (badge) badge.textContent = '○ 已暂停'; var btn = document.getElementById('hnu-ctrl'); if (btn) btn.textContent = '开始刷课'; } function toggleLoop() { if (isRunning) stopLoop(); else startLoop(); } // ==================== UI面板 ==================== function createPanel() { var existing = document.getElementById('hnu-panel'); if (existing) existing.remove(); isRunning = getRunning(); var el = document.createElement('div'); el.id = 'hnu-panel'; el.innerHTML = '