// ==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 = `
就绪中...
等待检测页面...
📊 课程: --
🎬 视频: --
✅ 已完成: 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();
}
})();