// ==UserScript== // @name 内蒙古智慧教育 - 自动刷课助手 // @namespace https://wlpx.nmgdata.org.cn/ // @version 2.6.0 // @description 自动完成内蒙古智慧教育平台(wlpx.nmgdata.org.cn)的在线课程视频学习。支持自动播放、2倍速静音、断点续播、多课程连续刷课,全程无需手动操作。 // @author SeniorDeveloper // @match https://wlpx.nmgdata.org.cn/* // @icon https://wlpx.nmgdata.org.cn/favicon.ico // @grant unsafeWindow // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_log // @license MIT // @tag 教育 // @tag 学习 // @tag 视频 // @tag 自动化 // @run-at document-end // ==/UserScript== (function () { 'use strict'; const CONFIG = { pauseRefreshDelay: 500, videoEndDelay: 2000, maxRefreshRetries: 3, pollInterval: 1000, debug: true, completionThreshold: 0.95, }; const STATE = { currentPage: null, courseId: null, videoId: null, isAutoMode: false, refreshCount: 0, pauseDetected: false, pauseTimer: null, observer: null, lastActivityTime: Date.now(), processedVideos: GM_getValue('processedVideos', {}), completedCourses: GM_getValue('completedCourses', []), courseList: GM_getValue('courseList', []), currentCourseIndex: GM_getValue('currentCourseIndex', 0), _videoCompleted: false, }; function log(...args) { if (CONFIG.debug) { console.log('[刷课助手]', ...args); } } function getPageType() { const hash = window.location.hash; if (!hash || hash === '#' || hash === '#/' || hash === '#/index') return 'course-index'; if (hash.includes('/course/index') || hash === '#/course/index') return 'course-index'; if (hash.includes('/course/detail')) return 'course-detail'; if (hash.includes('/video') || hash.includes('/play') || hash.includes('/watch')) { return 'video-player'; } return 'unknown'; } function getParamFromHash(key) { const hash = window.location.hash; const match = hash.match(new RegExp(`[?&]${key}=([^&]*)`)); return match ? decodeURIComponent(match[1]) : null; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function waitForElement(selector, timeout = 10000) { return new Promise((resolve, reject) => { const start = Date.now(); const check = () => { const el = document.querySelector(selector); if (el) { resolve(el); } else if (Date.now() - start > timeout) { reject(new Error(`等待元素超时: ${selector}`)); } else { setTimeout(check, 300); } }; check(); }); } function createControlPanel() { if (document.getElementById('study-helper-panel')) return; const panel = document.createElement('div'); panel.id = 'study-helper-panel'; panel.innerHTML = `
🎓 刷课助手 v2.5.5
就绪中...
等待检测页面...
📊 课程: -- 🎬 视频: --
✅ 已完成: 0 门课程
`; document.body.appendChild(panel); addPanelStyles(); bindPanelEvents(); } function addPanelStyles() { const style = document.createElement('style'); style.textContent = ` #study-helper-panel { position: fixed; top: 20px; right: 20px; z-index: 999999; width: 280px; background: #fff; border-radius: 12px; box-shadow: 0 8px 40px rgba(0,0,0,0.15); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; overflow: hidden; transition: transform 0.3s ease; } #study-helper-panel.minimized { transform: translateX(calc(100% - 40px)); } .sh-header { background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; padding: 10px 14px; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; } .sh-header span { font-weight: 600; font-size: 14px; } .sh-btn-toggle { background: rgba(255,255,255,0.2); border: none; color: #fff; width: 24px; height: 24px; border-radius: 50%; cursor: pointer; font-size: 16px; line-height: 1; display: flex; align-items: center; justify-content: center; } .sh-btn-toggle:hover { background: rgba(255,255,255,0.35); } .sh-body { padding: 12px 14px; } .sh-status { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } .sh-dot { width: 10px; height: 10px; border-radius: 50%; background: #909399; flex-shrink: 0; } .sh-dot.running { background: #67c23a; animation: sh-pulse 1.5s infinite; } .sh-dot.paused { background: #e6a23c; } @keyframes sh-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } .sh-page-info { font-size: 11px; color: #909399; margin-bottom: 10px; word-break: break-all; } .sh-btns { display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; } .sh-btn { flex: 1; min-width: 60px; padding: 6px 10px; border: none; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 500; white-space: nowrap; transition: all 0.2s; } .sh-btn-start { background: #67c23a; color: #fff; } .sh-btn-start:hover { background: #5daf34; } .sh-btn-stop { background: #f56c6c; color: #fff; } .sh-btn-stop:hover { background: #e04e4e; } .sh-btn-next { background: #409eff; color: #fff; } .sh-btn-next:hover { background: #3a8ee6; } .sh-stats { display: flex; gap: 12px; font-size: 12px; color: #606266; margin-bottom: 6px; } .sh-log { max-height: 120px; overflow-y: auto; font-size: 11px; color: #909399; border-top: 1px solid #ebeef5; padding-top: 6px; margin-top: 6px; } .sh-log div { padding: 2px 0; } `; document.head.appendChild(style); } function bindPanelEvents() { const panel = document.getElementById('study-helper-panel'); const btnToggle = panel.querySelector('.sh-btn-toggle'); const btnStart = document.getElementById('sh-btn-start'); const btnStop = document.getElementById('sh-btn-stop'); const btnNext = document.getElementById('sh-btn-next'); btnToggle.addEventListener('click', () => { panel.classList.toggle('minimized'); btnToggle.textContent = panel.classList.contains('minimized') ? '+' : '−'; }); const header = panel.querySelector('.sh-header'); let isDragging = false, startX, startY, startLeft, startTop; header.addEventListener('mousedown', (e) => { if (e.target === btnToggle) return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; panel.style.transition = 'none'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; panel.style.left = (startLeft + dx) + 'px'; panel.style.top = (startTop + dy) + 'px'; panel.style.right = 'auto'; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; panel.style.transition = ''; } }); btnStart.addEventListener('click', startAutoMode); btnStop.addEventListener('click', stopAutoMode); btnNext.addEventListener('click', skipCurrentVideo); const btnReset = document.getElementById('sh-btn-reset'); if (btnReset) { btnReset.addEventListener('click', () => { if (confirm('确定要清除所有完成记录吗?这将重置所有课程进度。')) { STATE.completedCourses = []; STATE.processedVideos = {}; GM_setValue('completedCourses', []); GM_setValue('processedVideos', {}); GM_setValue('courseList', []); GM_setValue('currentCourseIndex', 0); updateUI({ statCompleted: 0 }); addLog('🗑 所有记录已清除'); } }); } } function updateUI(data = {}) { const dot = document.getElementById('sh-dot'); const statusText = document.getElementById('sh-status-text'); const pageInfo = document.getElementById('sh-page-info'); const btnStart = document.getElementById('sh-btn-start'); const btnStop = document.getElementById('sh-btn-stop'); const statCourse = document.getElementById('sh-stat-course'); const statVideo = document.getElementById('sh-stat-video'); const statCompleted = document.getElementById('sh-stat-completed'); if (data.status) { statusText.textContent = data.status; dot.className = 'sh-dot'; if (data.status.includes('运行') || data.status.includes('播放')) { dot.classList.add('running'); } else if (data.status.includes('暂停') || data.status.includes('停止')) { dot.classList.add('paused'); } } if (data.pageInfo !== undefined) pageInfo.textContent = data.pageInfo; if (data.isRunning !== undefined) { btnStart.style.display = data.isRunning ? 'none' : ''; btnStop.style.display = data.isRunning ? '' : 'none'; } if (data.statCourse !== undefined) statCourse.textContent = data.statCourse; if (data.statVideo !== undefined) statVideo.textContent = data.statVideo; if (data.statCompleted !== undefined) statCompleted.textContent = data.statCompleted; } function addLog(msg) { log(msg); const logEl = document.getElementById('sh-log'); if (!logEl) return; logEl.style.display = 'block'; const div = document.createElement('div'); div.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; logEl.prepend(div); while (logEl.children.length > 50) logEl.lastChild.remove(); } function detectPage() { const pageType = getPageType(); STATE.currentPage = pageType; addLog(`🧭 当前HASH: ${window.location.hash} → 识别为: ${pageType}`); switch (pageType) { case 'course-index': STATE.courseId = null; STATE.videoId = null; updateUI({ status: '就绪', pageInfo: '📋 课程列表页', isRunning: STATE.isAutoMode, }); if (STATE.isAutoMode) handleCourseIndexPage(); break; case 'course-detail': STATE.courseId = getParamFromHash('id'); STATE.videoId = null; updateUI({ status: '就绪', pageInfo: `📖 课程详情 - ID: ${STATE.courseId}`, isRunning: STATE.isAutoMode, }); if (STATE.isAutoMode) handleCourseDetailPage(); break; case 'video-player': STATE.videoId = getParamFromHash('id'); updateUI({ status: '检测视频中...', pageInfo: `🎬 视频播放 - ID: ${STATE.videoId}`, isRunning: STATE.isAutoMode, }); if (STATE.isAutoMode) handleVideoPlayerPage(); break; default: updateUI({ status: '待机', pageInfo: '❓ 未知页面', isRunning: STATE.isAutoMode, }); } } function setupRouteWatcher() { let lastHash = window.location.hash; const onRouteChange = () => { const current = window.location.hash; if (current === lastHash) return; lastHash = current; log('路由变化:', current); STATE.refreshCount = 0; STATE.pauseDetected = false; clearAllTimers(); setTimeout(() => detectPage(), 500); }; window.addEventListener('hashchange', onRouteChange); const origPushState = history.pushState; history.pushState = function (...args) { origPushState.apply(this, args); setTimeout(onRouteChange, 500); }; const origReplaceState = history.replaceState; history.replaceState = function (...args) { origReplaceState.apply(this, args); setTimeout(onRouteChange, 500); }; } function clearAllTimers() { if (STATE.pauseTimer) { clearTimeout(STATE.pauseTimer); STATE.pauseTimer = null; } if (STATE._resumeInterval) { clearInterval(STATE._resumeInterval); STATE._resumeInterval = null; } if (STATE._progressMonitor) { clearInterval(STATE._progressMonitor); STATE._progressMonitor = null; } } let _indexPageHandling = false; async function handleCourseIndexPage() { if (_indexPageHandling) return; if (!STATE.isAutoMode) return; _indexPageHandling = true; try { addLog('📋 扫描课程列表...'); await sleep(1000); let courseCards = document.querySelectorAll('.course-card'); if (courseCards.length === 0) { courseCards = document.querySelectorAll('.index-course-contain .el-card, .course-card-contain .el-card'); addLog("Main selector empty, fallback to .el-card scan"); } const courses = []; courseCards.forEach(card => { let linkEl = card.closest('a.urlLink'); if (!linkEl && card.tagName === 'A') linkEl = card; if (!linkEl) linkEl = card.querySelector('a[href*="id="]'); const href = linkEl ? (linkEl.getAttribute('href') || '') : ''; const match = href.match(/id[=:](\d+)/); if (!match) return; const id = match[1]; const nameEl = card.querySelector('.course-name'); const name = nameEl ? nameEl.textContent.trim() : `课程${id}`; const progressTextEl = card.querySelector('.progress-text'); const progressRaw = progressTextEl ? progressTextEl.textContent.trim().replace(/[^0-9]/g, '') : ''; const progressBarEl = card.querySelector('.el-progress-bar__inner'); let progressPct = -1; if (progressBarEl) { const styleAttr = progressBarEl.getAttribute('style') || ''; const inlineMatch = styleAttr.match(/width:\s*(\d+)%?/); if (inlineMatch) { progressPct = parseInt(inlineMatch[1], 10); } if (progressPct < 0) { const computedWidth = getComputedStyle(progressBarEl).width; const parentWidth = getComputedStyle(progressBarEl.parentElement).width; if (parentWidth && computedWidth) { const pct = (parseFloat(computedWidth) / parseFloat(parentWidth)) * 100; if (!isNaN(pct)) progressPct = Math.round(pct); } } } if (progressPct < 0 && progressRaw) { progressPct = parseInt(progressRaw, 10) || 0; } if (progressPct < 0) progressPct = 0; const isCompleted = progressPct >= 100 || STATE.completedCourses.includes(id); courses.push({ id, name, progressPct, isCompleted }); }); STATE.courseList = courses; GM_setValue('courseList', courses); addLog(`找到 ${courses.length} 门课程`); if (courses.length === 0) { addLog('❌ 未找到课程卡片,尝试扩大扫描...'); return; } courses.forEach(c => { addLog(` [${c.id}] ${c.name}: ${c.progressPct}% ${c.isCompleted ? '✅已完成' : '🔄未完成'}`); }); let targetIndex = -1; for (let i = 0; i < courses.length; i++) { if (!courses[i].isCompleted) { targetIndex = i; break; } } if (targetIndex >= 0) { const course = courses[targetIndex]; STATE.currentCourseIndex = targetIndex; GM_setValue('currentCourseIndex', targetIndex); updateUI({ statCourse: `${targetIndex + 1}/${courses.length}`, statVideo: '--' }); addLog(`🎯 目标课程: [${course.id}] ${course.name} (进度: ${course.progressPct}%)`); setTimeout(() => { window.location.hash = `#/course/detail?id=${course.id}`; }, 1500); } else { const nextBtn = findNextPageButton(); if (nextBtn) { addLog('📄 当前页完成,翻到下一页...'); nextBtn.click(); setTimeout(() => { _indexPageHandling = false; handleCourseIndexPage(); }, 2000); return; } addLog('✅ 所有课程已完成!'); updateUI({ status: '全部完成', isRunning: false }); STATE.isAutoMode = false; } } finally { _indexPageHandling = false; } } function findNextPageButton() { const nextBtn = document.querySelector('.el-pagination .btn-next:not(.disabled):not([disabled])'); if (nextBtn && !nextBtn.disabled) return nextBtn; const allNext = document.querySelectorAll('[class*="next"]'); for (const btn of allNext) { if (!btn.disabled && !btn.classList.contains('disabled')) return btn; } const pagerNext = document.querySelector('.el-pager li:last-child:not(.disabled):not(.active)'); if (pagerNext) return pagerNext; return null; } let _detailPageHandling = false; async function handleCourseDetailPage() { if (_detailPageHandling) return; if (!STATE.isAutoMode) return; _detailPageHandling = true; try { STATE.completedCourses = GM_getValue('completedCourses', []); STATE.processedVideos = GM_getValue('processedVideos', {}); addLog(`📖 进入课程 ${STATE.courseId} 详情页 (已记录完成视频: ${JSON.stringify(STATE.processedVideos[STATE.courseId] || [])}, 已完成课程: ${STATE.completedCourses.length}门)`); if (STATE.completedCourses.includes(STATE.courseId)) { addLog(`⏭ 课程 ${STATE.courseId} 已在本地标记为完成,跳过`); setTimeout(() => { window.location.hash = '#/course/index'; }, 1000); return; } await sleep(1500); await clickDirectoryTab(); const videos = await collectVideoList(); if (videos.length === 0) { addLog('❌ 多次重试仍未找到视频列表,跳过此课程(不标记完成,保留进度)'); setTimeout(() => { window.location.hash = '#/course/index'; }, 2000); return; } const watchedCount = videos.filter(v => v.isWatched).length; addLog(`📂 本课程共 ${videos.length} 个视频 (已看: ${watchedCount}, 未看: ${videos.length - watchedCount})`); updateUI({ statVideo: `${watchedCount}/${videos.length}` }); const allProcessed = GM_getValue('processedVideos', {}); const processed = allProcessed[STATE.courseId] || []; addLog("🔍 processedVideos[" + STATE.courseId + "] = [" + processed.join(',') + "] (总数keys:" + Object.keys(allProcessed).length + ")"); const useProcessedFallback = processed.length > 0; let targetVideo = null; for (const v of videos) { const inProcessed = processed.includes(v.videoId); const shouldSkip = useProcessedFallback ? inProcessed : (inProcessed || v.isWatched); addLog(" 🔎 视频 " + v.videoId + " '" + v.name + "' processed=" + inProcessed + " isWatched=" + v.isWatched + " skip=" + shouldSkip + " (fallback=" + !useProcessedFallback + ")"); if (!shouldSkip) { targetVideo = v; break; } } if (targetVideo) { addLog(`🎬 开始学习: ${targetVideo.name} (ID: ${targetVideo.videoId})`); window.location.hash = `#/video?id=${targetVideo.videoId}`; } else { addLog(`✅ 课程 ${STATE.courseId} 全部视频已完成`); markCourseComplete(); setTimeout(() => { window.location.hash = '#/course/index'; }, 2000); } } finally { _detailPageHandling = false; } } async function clickDirectoryTab() { const videoTab = document.getElementById('tab-video'); if (videoTab) { videoTab.click(); await sleep(800); return; } const tabItems = document.querySelectorAll('.el-tabs__item'); for (const tab of tabItems) { if (tab.textContent.trim() === '目录' || tab.textContent.includes('目录')) { tab.click(); await sleep(800); return; } } const ariaTab = document.querySelector('[role="tab"][aria-controls*="video"]'); if (ariaTab) { ariaTab.click(); await sleep(800); } } async function collectVideoList(retryCount = 0) { if (retryCount > 1) { addLog('⚠️ 重试已达上限,放弃收集视频列表'); return []; } let videos = []; for (let round = 0; round < 3 && videos.length === 0; round++) { await sleep(round * 1000); const selectors = [ '.course-video-item', '.teacher-video-item', '[class*="video-item"]', '[class*="video-name-info"]', ]; let items = []; for (const sel of selectors) { const found = document.querySelectorAll(sel); if (found.length > 0) { items = found; addLog(`🔍 选择器 "${sel}" 匹配到 ${found.length} 个元素`); break; } } if (items.length === 0) { const nameEls = document.querySelectorAll('.video-name'); if (nameEls.length > 0) { items = []; nameEls.forEach(el => { const container = el.closest('a.urlLink') || el.closest('[class*="video"]') || el.parentElement.parentElement; if (container && !items.includes(container)) { items.push(container); } }); addLog(`🔍 以 .video-name 为锚点找到 ${items.length} 个容器`); } } for (const item of items) { const nameEl = item.querySelector('.video-name'); const name = nameEl ? nameEl.textContent.trim() : '未知视频'; let link = item.tagName === 'A' ? item : item.querySelector('a[href*="video"]'); if (!link) link = item.querySelector('a.urlLink'); if (!link) link = item.closest('a[href*="video"]'); const href = link ? (link.getAttribute('href') || '') : ''; const match = href.match(/id[=:](\d+)/); const videoId = match ? match[1] : null; if (!videoId) continue; const completeFlag = item.querySelector('.already-watch .video-length'); const completeFlagText = completeFlag ? completeFlag.textContent.trim() : ''; const watchIcon = item.querySelector('i[class*="el-icon-video-"]'); const iconClass = watchIcon ? watchIcon.className : ''; const itemClasses = item.className || ''; const watchEl = item.querySelector('.already-watch, .video-watch-time, [class*="watch"]'); const watchStatus = watchEl ? watchEl.textContent.trim() : ''; const isWatched = completeFlagText === '已看完' || watchStatus.includes('已看完') || watchStatus.includes('已学完') || watchStatus.includes('已完成') || watchStatus.includes('100%') || iconClass.includes('success') || iconClass.includes('finished') || iconClass.includes('check') || itemClasses.includes('finished') || itemClasses.includes('completed') || itemClasses.includes('watched'); if (!videos.some(v => v.videoId === videoId)) { addLog(` 📹 ${name} (ID:${videoId}) - ${iconClass || 'no-icon'} - ${watchStatus || '无文字'} → ${isWatched ? '✅已学完' : '🔄未学完'}`); videos.push({ videoId, name, isWatched, element: item }); } } } if (videos.length === 0) { addLog(`⚠️ 未找到视频列表,重试第 ${retryCount + 1} 次...`); await clickDirectoryTab(); await sleep(2000); return collectVideoList(retryCount + 1); } const seen = new Set(); const deduped = []; for (const v of videos) { if (!seen.has(v.videoId)) { seen.add(v.videoId); deduped.push(v); } } if (deduped.length !== videos.length) { addLog(`🧹 视频列表去重: ${videos.length} → ${deduped.length}`); } return deduped; } function markVideoComplete(videoId, overrideCourseId) { const courseId = overrideCourseId || STATE.courseId; addLog("📝 markVideoComplete: courseId=" + courseId + " videoId=" + videoId); if (!courseId || !videoId) { addLog(`⚠️ markVideoComplete 失败: courseId=${courseId} videoId=${videoId}`); return; } const processed = STATE.processedVideos[courseId] || []; if (!processed.includes(videoId)) { processed.push(videoId); STATE.processedVideos[courseId] = processed; GM_setValue('processedVideos', STATE.processedVideos); addLog(`✅ 标记视频完成: 课程${courseId}/视频${videoId} (本课程已看: ${processed.length})`); } } function markCourseComplete() { const courseId = STATE.courseId; if (!courseId) return; if (!STATE.completedCourses.includes(courseId)) { STATE.completedCourses.push(courseId); GM_setValue('completedCourses', STATE.completedCourses); updateUI({ statCompleted: STATE.completedCourses.length }); } delete STATE.processedVideos[courseId]; GM_setValue('processedVideos', STATE.processedVideos); addLog(`✅ 课程 ${courseId} 完成,累计完成 ${STATE.completedCourses.length} 门`); } let _videoPageHandling = false; async function handleVideoPlayerPage() { if (_videoPageHandling) return; if (!STATE.isAutoMode) return; _videoPageHandling = true; try { STATE._videoCompleted = false; sessionStorage.removeItem("__study_resume_time"); sessionStorage.removeItem("__study_resume_video"); addLog(`🎬 进入视频页面 ID: ${STATE.videoId} (课程: ${STATE.courseId || '?'})`); STATE.refreshCount = 0; const currentCourseId = STATE.courseId; const video = await waitForVideoElement(); if (!video) { addLog('❌ 未找到视频元素,2秒后刷新...'); setTimeout(() => location.reload(), 2000); return; } addLog('✅ 视频元素就绪'); updateUI({ status: '▶ 播放中', isRunning: true }); setupVideoListeners(video, currentCourseId); forcePlay(video); startProgressMonitor(video); } catch (err) { addLog(`❌ 视频初始化失败: ${err.message}`); setTimeout(() => location.reload(), 2000); } finally { _videoPageHandling = false; } } function forcePlay(video) { if (!video) return; video.muted = true; video.playbackRate = 2; video.defaultPlaybackRate = 2; const savedTime = parseFloat(sessionStorage.getItem('__study_resume_time')); if (savedTime && savedTime > 0) { video.currentTime = savedTime; sessionStorage.removeItem('__study_resume_time'); sessionStorage.removeItem('__study_resume_video'); addLog(`📍 恢复播放进度: ${savedTime.toFixed(0)}s`); } const tryPlay = () => { if (!video.paused) { video.playbackRate = 2; return; } video.muted = true; video.playbackRate = 2; video.play().then(() => { addLog('🔇 静音播放中 (2x)'); updateUI({ status: '▶ 播放中 (2x静音)' }); video.playbackRate = 2; }).catch(err => { addLog(`⚠️ play() 失败: ${err.name}`); }); }; tryPlay(); setTimeout(tryPlay, 500); setTimeout(tryPlay, 1500); setTimeout(tryPlay, 3000); setTimeout(tryPlay, 6000); const resumeInterval = setInterval(() => { if (!STATE.isAutoMode) { clearInterval(resumeInterval); return; } video.muted = true; video.playbackRate = 2; if (video.paused && !video.ended) { video.play().catch(() => {}); } }, 2000); STATE._resumeInterval = resumeInterval; window.__studyHelperVideo = video; } async function waitForVideoElement(timeout = 15000) { const start = Date.now(); while (Date.now() - start < timeout) { const el = document.querySelector('.course-video-player') || document.querySelector('video[src*=".mp4"]') || document.querySelector('video'); if (el && el.tagName === 'VIDEO') { if (el.src || el.currentSrc) { addLog(`🔍 找到视频: ${(el.src||'').substring(0,60)}...`); return el; } if (el.readyState >= 1) return el; } await sleep(500); } return null; } function setupVideoListeners(video, courseId) { if (video.setAttribute) { video.setAttribute('data-study-helper-bound', '1'); } video.addEventListener('pause', () => { if (!STATE.isAutoMode) return; if (video.ended) { log('✅ 视频播放结束'); handleVideoComplete(courseId); return; } const duration = video.duration || 0; const currentTime = video.currentTime || 0; if (duration > 0 && currentTime >= duration * CONFIG.completionThreshold) { log('✅ 视频接近结尾,视为完成'); handleVideoComplete(courseId); return; } addLog("Video paused (" + currentTime.toFixed(0) + "s/" + duration.toFixed(0) + "s) -> saving & refresh"); if (currentTime > 0) { sessionStorage.setItem("__study_resume_time", currentTime); sessionStorage.setItem("__study_resume_video", window.location.hash); } STATE.pauseTimer = setTimeout(() => location.reload(), CONFIG.pauseRefreshDelay); }); video.addEventListener('play', () => { if (STATE.pauseTimer) { clearTimeout(STATE.pauseTimer); STATE.pauseTimer = null; } updateUI({ status: '▶ 播放中 (2x静音)' }); }); video.addEventListener('ended', () => { log('🏁 视频播放结束'); handleVideoComplete(courseId); }); video.addEventListener('error', () => { addLog('❌ 视频错误,刷新...'); setTimeout(() => location.reload(), 500); }); video.addEventListener('loadedmetadata', () => { video.playbackRate = 2; video.defaultPlaybackRate = 2; }); } function startProgressMonitor(video) { let lastTime = 0; let stallCount = 0; const monitor = setInterval(() => { if (!STATE.isAutoMode) { clearInterval(monitor); return; } const v = window.__studyHelperVideo || video; if (!v) { clearInterval(monitor); return; } const currentTime = v.currentTime || 0; const duration = v.duration || 0; if (!v.paused && currentTime === lastTime && duration > 0) { stallCount++; if (stallCount > 15) { addLog('⚠️ 视频播放卡住,刷新页面...'); clearInterval(monitor); location.reload(); return; } } else { stallCount = 0; } lastTime = currentTime; if (duration > 0) { const pct = ((currentTime / duration) * 100).toFixed(0); const statVideoEl = document.getElementById('sh-stat-video'); if (statVideoEl) { statVideoEl.textContent = `${pct}%`; } } if (duration > 0 && currentTime >= duration * CONFIG.completionThreshold && !v.ended) { log('视频接近结束,等待正常结束或触发完成'); } }, 2000); STATE._progressMonitor = monitor; } async function handleVideoComplete(overrideCourseId) { if (STATE._videoCompleted) return; STATE._videoCompleted = true; clearAllTimers(); sessionStorage.removeItem('__study_resume_time'); sessionStorage.removeItem('__study_resume_video'); const courseId = overrideCourseId || STATE.courseId; addLog(`✅ 视频 ${STATE.videoId} 学习完毕 (课程: ${courseId || '?'})`); markVideoComplete(STATE.videoId, courseId); await sleep(CONFIG.videoEndDelay); if (courseId) { addLog(`📖 返回课程 ${courseId} 详情页`); window.location.hash = `#/course/detail?id=${courseId}`; } else { addLog('⚠️ 没有课程上下文,返回课程列表'); window.location.hash = '#/course/index'; } } function skipCurrentVideo() { if (!STATE.isAutoMode) { addLog('⚠️ 请先启动自动刷课模式'); return; } addLog(`⏭ 跳过当前视频 ${STATE.videoId}`); clearAllTimers(); markVideoComplete(STATE.videoId); const courseId = STATE.courseId; if (courseId) { window.location.hash = `#/course/detail?id=${courseId}`; } else { window.location.hash = '#/course/index'; } } function startAutoMode() { STATE.isAutoMode = true; addLog('🚀 自动刷课模式已启动'); updateUI({ isRunning: true }); STATE.refreshCount = 0; window.__playAllowed = true; setTimeout(() => { window.__playAllowed = false; }, 5000); detectPage(); } function stopAutoMode() { STATE.isAutoMode = false; clearAllTimers(); try { window.__studyHelperVideo?.pause(); } catch(e) {} delete window.__studyHelperVideo; addLog('⏹ 自动刷课模式已停止'); updateUI({ status: '已停止', isRunning: false }); } function setupMutationObserver() { STATE.observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { if (node.querySelector && ( node.querySelector('.index-course-contain') || node.querySelector('.course-card-contain') || node.querySelector('.course-detail-information') || node.querySelector('video') || node.querySelector('[class*="player"]') )) { log('检测到页面内容变化,重新检测页面类型...'); setTimeout(() => detectPage(), 500); return; } } } } } }); STATE.observer.observe(document.getElementById('app') || document.body, { childList: true, subtree: true, }); } function init() { log('========================================'); log(' 内蒙智慧教育刷课助手 v2.5.5 已加载'); log(' https://wlpx.nmgdata.org.cn/'); log('========================================'); createControlPanel(); setupRouteWatcher(); setupMutationObserver(); setTimeout(() => { detectPage(); updateUI({ statCompleted: STATE.completedCourses.length }); }, 1000); const savedAutoMode = GM_getValue('autoMode', false); if (savedAutoMode) { addLog('🔄 恢复自动刷课模式'); setTimeout(() => startAutoMode(), 2000); } window.addEventListener('beforeunload', () => { GM_setValue('autoMode', STATE.isAutoMode); GM_setValue('processedVideos', STATE.processedVideos); GM_setValue('completedCourses', STATE.completedCourses); GM_setValue('courseList', STATE.courseList); GM_setValue('currentCourseIndex', STATE.currentCourseIndex); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();