// ==UserScript==
// @name 重庆高教在线自动刷课助手(增强版)
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description 重庆高教在线(cqooc.com)自动刷课脚本。功能:自动播放视频(防暂停、放完自动下一节),PPT课件自动停留120秒后切换,自动跳过作业和测验,支持后台挂机。
// @author 自定义
// @match *://*.cqooc.com/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant unsafeWindow
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ==================== 配置区域 ====================
const CONFIG = {
PPT_WAIT_TIME: 120000, // PPT停留时间(毫秒)120秒
VIDEO_CHECK_INTERVAL: 1000, // 视频状态检测间隔(毫秒)
PAGE_LOAD_DELAY: 2500, // 页面加载等待时间(毫秒)
AUTO_START: true, // 是否自动开始(无需手动点击按钮)
SKIP_TESTS: true, // 是否跳过作业和测验
PREVENT_PAUSE: true, // 是否开启防暂停
LOG_LEVEL: 'info', // 日志级别: debug|info|warn|error
};
// ==================== 日志工具 ====================
const Logger = {
prefix: '[高教刷课]',
colors: {
debug: '#888',
info: '#2196F3',
warn: '#FF9800',
error: '#f44336',
success: '#4CAF50'
},
log(level, message, data) {
const levels = ['debug', 'info', 'warn', 'error'];
if (levels.indexOf(level) < levels.indexOf(CONFIG.LOG_LEVEL)) return;
const color = this.colors[level] || '#333';
const time = new Date().toLocaleTimeString();
console.log(`%c${this.prefix}[${time}][${level.toUpperCase()}] ${message}`, `color:${color}`, data || '');
// 同时显示在页面控制面板上
addPanelLog(message, level);
},
debug(msg, data) { this.log('debug', msg, data); },
info(msg, data) { this.log('info', msg, data); },
warn(msg, data) { this.log('warn', msg, data); },
error(msg, data) { this.log('error', msg, data); },
success(msg, data) { this.log('info', `%c${msg}`, 'color:#4CAF50;font-weight:bold'); }
};
// ==================== 页面控制面板 ====================
let panelCreated = false;
let panelLogs = [];
function createControlPanel() {
if (panelCreated) return;
panelCreated = true;
GM_addStyle(`
#cqooc-panel {
position: fixed;
top: 80px;
right: 20px;
width: 320px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
z-index: 99999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow: hidden;
transition: all 0.3s ease;
}
#cqooc-panel.collapsed {
width: 180px;
height: 50px;
}
#cqooc-panel.collapsed #cqooc-body {
display: none;
}
#cqooc-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: rgba(0,0,0,0.2);
cursor: pointer;
user-select: none;
}
#cqooc-header-title {
color: #fff;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
#cqooc-header-status {
width: 10px;
height: 10px;
border-radius: 50%;
background: #4CAF50;
box-shadow: 0 0 8px #4CAF50;
animation: pulse 2s infinite;
}
#cqooc-header-status.stopped {
background: #f44336;
box-shadow: 0 0 8px #f44336;
animation: none;
}
#cqooc-header-status.paused {
background: #FF9800;
box-shadow: 0 0 8px #FF9800;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
#cqooc-toggle {
color: #fff;
font-size: 16px;
background: none;
border: none;
cursor: pointer;
}
#cqooc-body {
padding: 12px;
}
#cqooc-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 10px;
}
.cqooc-stat-box {
background: rgba(255,255,255,0.15);
border-radius: 8px;
padding: 8px;
text-align: center;
}
.cqooc-stat-label {
color: rgba(255,255,255,0.7);
font-size: 11px;
margin-bottom: 4px;
}
.cqooc-stat-value {
color: #fff;
font-size: 16px;
font-weight: 700;
}
#cqooc-current-task {
background: rgba(255,255,255,0.1);
border-radius: 8px;
padding: 8px;
margin-bottom: 10px;
color: #fff;
font-size: 12px;
min-height: 40px;
word-break: break-all;
}
#cqooc-logs {
background: rgba(0,0,0,0.3);
border-radius: 8px;
padding: 8px;
height: 150px;
overflow-y: auto;
font-size: 11px;
line-height: 1.5;
}
.cqooc-log-item {
color: rgba(255,255,255,0.9);
padding: 1px 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.cqooc-log-debug { color: #aaa; }
.cqooc-log-info { color: #64B5F6; }
.cqooc-log-warn { color: #FFB74D; }
.cqooc-log-error { color: #EF5350; }
.cqooc-log-success { color: #81C784; }
#cqooc-controls {
display: flex;
gap: 6px;
margin-top: 10px;
}
.cqooc-btn {
flex: 1;
padding: 6px 10px;
border: none;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.cqooc-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.cqooc-btn-start {
background: #4CAF50;
color: #fff;
}
.cqooc-btn-stop {
background: #f44336;
color: #fff;
}
.cqooc-btn-pause {
background: #FF9800;
color: #fff;
}
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.3);
border-radius: 2px;
}
`);
const panel = document.createElement('div');
panel.id = 'cqooc-panel';
panel.innerHTML = `
`;
document.body.appendChild(panel);
// 折叠/展开
let collapsed = false;
document.getElementById('cqooc-header').addEventListener('click', (e) => {
if (e.target.id === 'cqooc-toggle') {
collapsed = !collapsed;
panel.classList.toggle('collapsed', collapsed);
document.getElementById('cqooc-toggle').textContent = collapsed ? '+' : '−';
}
});
// 按钮事件
document.getElementById('cqooc-btn-start').addEventListener('click', () => {
if (!isRunning) {
startStudying();
}
});
document.getElementById('cqooc-btn-pause').addEventListener('click', () => {
togglePause();
});
document.getElementById('cqooc-btn-stop').addEventListener('click', () => {
stopStudying();
});
}
function addPanelLog(message, level = 'info') {
panelLogs.push({ message, level, time: new Date().toLocaleTimeString() });
if (panelLogs.length > 100) panelLogs.shift();
const logsContainer = document.getElementById('cqooc-logs');
if (!logsContainer) return;
const logItem = document.createElement('div');
logItem.className = `cqooc-log-item cqooc-log-${level}`;
logItem.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logsContainer.appendChild(logItem);
logsContainer.scrollTop = logsContainer.scrollHeight;
// 限制日志数量
while (logsContainer.children.length > 50) {
logsContainer.removeChild(logsContainer.firstChild);
}
}
function updatePanelStats(type, progress) {
const typeEl = document.getElementById('cqooc-stat-type');
const progressEl = document.getElementById('cqooc-stat-progress');
if (typeEl) typeEl.textContent = type || '-';
if (progressEl) progressEl.textContent = progress || '0%';
}
function updatePanelStatus(status) {
const statusEl = document.getElementById('cqooc-header-status');
if (statusEl) {
statusEl.className = '';
statusEl.classList.add(status);
}
}
function updateCurrentTask(text) {
const el = document.getElementById('cqooc-current-task');
if (el) el.textContent = text;
}
// ==================== 核心刷课逻辑 ====================
let isRunning = false;
let isPaused = false;
let shouldStop = false;
let taskQueue = [];
let completedCount = 0;
let totalCount = 0;
let currentVideo = null;
let pausePreventionInterval = null;
function delay(ms) {
return new Promise(resolve => {
const check = () => {
if (shouldStop) {
resolve();
return;
}
if (isPaused) {
setTimeout(check, 500);
return;
}
setTimeout(resolve, ms);
};
check();
});
}
// 等待页面元素加载
function waitForElement(selector, timeout = 15000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const el = document.querySelector(selector);
if (el) {
resolve(el);
return;
}
if (Date.now() - startTime > timeout) {
reject(new Error(`等待元素 ${selector} 超时`));
return;
}
setTimeout(check, 500);
};
check();
});
}
// 获取所有待学习的章节
function getAllTasks() {
// 新版高教在线选择器
let tasks = Array.from(document.querySelectorAll('.third-level-box'));
// 如果没找到,尝试旧版选择器
if (tasks.length === 0) {
const chaptersList = document.querySelector('ul.cont_list#chapters-list');
if (chaptersList) {
// 展开所有折叠的章节
const closedItems = chaptersList.querySelectorAll('li.item.one.close');
closedItems.forEach(item => item.click());
// 获取未完成的小节 (s0-s5表示未完成)
const regex = /item\s+ref_\d+\s+item\d+\s+two\s+s(0|1|2|3|4|5)/;
const allLi = chaptersList.querySelectorAll('li');
tasks = Array.from(allLi).filter(li => regex.test(li.className));
}
}
// 过滤掉作业和测验
if (CONFIG.SKIP_TESTS) {
tasks = tasks.filter(el => !/作业|测验/.test(el.innerText));
}
return tasks;
}
// 点击任务并等待加载
async function clickTaskAndWait(task) {
task.click();
await delay(CONFIG.PAGE_LOAD_DELAY);
// 等待内容区域加载
try {
await Promise.race([
waitForElement('video', 10000),
waitForElement('iframe', 10000),
waitForElement('.MPreview-box', 10000),
waitForElement('#videoPlayer', 10000)
]);
} catch (e) {
Logger.warn('内容加载等待超时,继续执行');
}
}
// 判断内容类型
function getContentType() {
const hasVideo = document.querySelector('video');
const hasVideoPlayer = document.querySelector('#videoPlayer');
const hasIframe = document.querySelector('iframe');
const hasMPreview = document.querySelector('.MPreview-box');
if (hasVideo || hasVideoPlayer) return 'video';
if (hasIframe || hasMPreview) return 'pdf';
// 通过slide类型判断(旧版)
const activeSlide = document.querySelector('ul.list li.active, ul.list li.current');
if (activeSlide) {
if (activeSlide.className.includes('v2')) return 'video';
if (activeSlide.className.includes('v1')) return 'pdf';
}
return 'unknown';
}
// ==================== 视频处理 ====================
async function handleVideo() {
Logger.info('开始处理视频任务');
updatePanelStats('视频', '0%');
updateCurrentTask('正在播放视频...');
const video = document.querySelector('video') || document.querySelector('#videoPlayer video');
if (!video) {
Logger.error('未找到视频元素');
return;
}
currentVideo = video;
return new Promise((resolve) => {
let resolved = false;
const safeResolve = () => {
if (!resolved) {
resolved = true;
cleanup();
resolve();
}
};
// 清理函数
function cleanup() {
video.removeEventListener('ended', onEnded);
video.removeEventListener('timeupdate', onTimeUpdate);
video.removeEventListener('error', onError);
video.onpause = null;
if (pausePreventionInterval) {
clearInterval(pausePreventionInterval);
pausePreventionInterval = null;
}
currentVideo = null;
}
// 播放完成监听
function onEnded() {
Logger.success('视频播放完成!');
completedCount++;
updatePanelStats('视频', '100%');
safeResolve();
}
// 进度更新
function onTimeUpdate() {
if (video.duration) {
const progress = (video.currentTime / video.duration * 100).toFixed(1);
updatePanelStats('视频', progress + '%');
updateCurrentTask(`视频播放中: ${formatTime(video.currentTime)} / ${formatTime(video.duration)} (${progress}%)`);
}
}
// 错误处理
function onError(e) {
Logger.error('视频播放出错', e);
safeResolve();
}
// 绑定事件
video.addEventListener('ended', onEnded);
video.addEventListener('timeupdate', onTimeUpdate);
video.addEventListener('error', onError);
// 防暂停处理
if (CONFIG.PREVENT_PAUSE) {
video.onpause = function() {
if (!isRunning || isPaused || video.ended) return;
Logger.debug('视频被暂停,尝试恢复播放');
setTimeout(() => {
if (video.paused && !video.ended) {
video.play().catch(err => Logger.debug('恢复播放失败:' + err));
}
}, 500);
};
// 额外保护:定时检查视频状态
pausePreventionInterval = setInterval(() => {
if (!isRunning || isPaused) return;
if (video.paused && !video.ended && video.readyState >= 2) {
Logger.debug('检测到视频暂停,自动恢复');
video.play().catch(() => {});
}
}, 3000);
}
// 尝试播放视频
function tryPlay() {
// 先尝试点击播放按钮
const playBtn = document.querySelector('.dplayer-mobile-play') ||
document.querySelector('.xgplayer-icon-play svg') ||
document.querySelector('.xgplayer-start') ||
document.querySelector('[class*="play"]');
if (playBtn) {
Logger.debug('点击播放按钮');
playBtn.click();
}
// 直接调用play方法
video.play().then(() => {
Logger.info('视频开始播放');
}).catch(err => {
Logger.warn('自动播放被阻止,尝试静音后播放');
video.muted = true;
video.play().catch(err2 => {
Logger.error('播放失败', err2);
});
});
}
// 确保视频可以播放
if (video.readyState >= 2) {
tryPlay();
} else {
video.addEventListener('canplay', tryPlay, { once: true });
// 超时兜底
setTimeout(tryPlay, 5000);
}
// 超时保护:如果10分钟还没播放完,强制跳过
setTimeout(() => {
if (!resolved) {
Logger.warn('视频播放超时,强制进入下一节');
safeResolve();
}
}, 600000);
});
}
// ==================== PPT/PDF处理 ====================
async function handlePDF() {
Logger.info(`开始处理PPT/PDF任务,将等待 ${CONFIG.PPT_WAIT_TIME / 1000} 秒`);
updatePanelStats('PPT', '等待中');
updateCurrentTask(`PPT/PPT课件学习中,剩余 ${CONFIG.PPT_WAIT_TIME / 1000} 秒...`);
const startTime = Date.now();
const totalWait = CONFIG.PPT_WAIT_TIME;
return new Promise((resolve) => {
let resolved = false;
const timer = setInterval(() => {
if (shouldStop) {
clearInterval(timer);
resolve();
return;
}
if (isPaused) {
return; // 暂停时不倒计时
}
const elapsed = Date.now() - startTime;
const remaining = Math.max(0, totalWait - elapsed);
const progress = ((elapsed / totalWait) * 100).toFixed(0);
updatePanelStats('PPT', progress + '%');
updateCurrentTask(`PPT课件学习中,剩余 ${Math.ceil(remaining / 1000)} 秒...`);
if (remaining <= 0) {
clearInterval(timer);
if (!resolved) {
resolved = true;
Logger.success('PPT课件停留时间结束,进入下一节');
completedCount++;
resolve();
}
}
}, 1000);
});
}
// ==================== 主控制流程 ====================
async function startStudying() {
if (isRunning) return;
isRunning = true;
isPaused = false;
shouldStop = false;
completedCount = 0;
updatePanelStatus('running');
document.getElementById('cqooc-btn-start').textContent = '运行中';
Logger.info('========== 开始自动刷课 ==========');
// 获取所有任务
const tasks = getAllTasks();
taskQueue = tasks;
totalCount = tasks.length;
document.getElementById('cqooc-stat-total').textContent = totalCount;
Logger.info(`共找到 ${totalCount} 个学习任务`);
if (totalCount === 0) {
Logger.warn('未找到可学习的任务,请确认已进入课程学习页面');
isRunning = false;
updatePanelStatus('stopped');
return;
}
// 找到当前正在进行的任务索引
const activeIndex = tasks.findIndex(el =>
el.classList.contains('active') ||
el.classList.contains('current')
);
const startIndex = activeIndex >= 0 ? activeIndex : 0;
Logger.info(`从第 ${startIndex + 1} 个任务开始执行`);
// 依次执行每个任务
for (let i = startIndex; i < tasks.length; i++) {
if (shouldStop) {
Logger.info('刷课已停止');
break;
}
while (isPaused) {
if (shouldStop) break;
await delay(500);
}
const task = tasks[i];
const taskName = task.innerText?.trim() || `任务 ${i + 1}`;
Logger.info(`--------------------------------`);
Logger.info(`[${i + 1}/${tasks.length}] 开始学习: ${taskName}`);
updateCurrentTask(`[${i + 1}/${tasks.length}] ${taskName}`);
document.getElementById('cqooc-stat-done').textContent = completedCount;
try {
// 点击任务并等待加载
await clickTaskAndWait(task);
// 等待一下确保内容完全加载
await delay(2000);
// 判断内容类型并处理
const contentType = getContentType();
Logger.info(`内容类型: ${contentType}`);
switch (contentType) {
case 'video':
await handleVideo();
break;
case 'pdf':
await handlePDF();
break;
default:
Logger.warn('未知内容类型,等待10秒后跳过');
updatePanelStats('未知', '-');
await delay(10000);
completedCount++;
break;
}
// 任务完成后等2秒再进入下一个
await delay(2000);
} catch (error) {
Logger.error(`处理任务时出错: ${error.message}`);
await delay(3000);
}
}
isRunning = false;
updatePanelStatus('stopped');
document.getElementById('cqooc-btn-start').textContent = '开始刷课';
if (!shouldStop) {
Logger.success('========== 所有课程学习完成! ==========');
updateCurrentTask('所有课程已完成!');
notify('刷课完成!所有章节已学习完毕', 5000);
}
}
function togglePause() {
if (!isRunning) return;
isPaused = !isPaused;
const btn = document.getElementById('cqooc-btn-pause');
if (isPaused) {
btn.textContent = '继续';
updatePanelStatus('paused');
Logger.info('已暂停');
if (currentVideo && !currentVideo.paused) {
currentVideo.pause();
}
} else {
btn.textContent = '暂停';
updatePanelStatus('running');
Logger.info('已恢复');
if (currentVideo && currentVideo.paused) {
currentVideo.play().catch(() => {});
}
}
}
function stopStudying() {
shouldStop = true;
isRunning = false;
isPaused = false;
updatePanelStatus('stopped');
document.getElementById('cqooc-btn-start').textContent = '开始刷课';
document.getElementById('cqooc-btn-pause').textContent = '暂停';
Logger.info('刷课已停止');
if (pausePreventionInterval) {
clearInterval(pausePreventionInterval);
pausePreventionInterval = null;
}
}
// ==================== 工具函数 ====================
function formatTime(seconds) {
if (!seconds || isNaN(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' + secs : secs}`;
}
function notify(text, time = 3000) {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
padding: 16px 32px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
z-index: 999999;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
animation: slideDown 0.3s ease;
`;
notification.textContent = text;
document.body.appendChild(notification);
const style = document.createElement('style');
style.textContent = `
@keyframes slideDown {
from { opacity: 0; transform: translateX(-50%) translateY(-20px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
`;
document.head.appendChild(style);
setTimeout(() => {
notification.style.animation = 'slideDown 0.3s ease reverse';
setTimeout(() => notification.remove(), 300);
}, time);
}
// ==================== 页面路由处理 ====================
let lastPath = location.pathname;
function handlePathChange() {
if (lastPath !== location.pathname) {
lastPath = location.pathname;
onPageChange();
}
}
function onPageChange() {
Logger.info('页面路由变化:', location.pathname);
if (location.pathname === '/course/detail/courseStudy' ||
location.pathname === '/learn/mooc/structure') {
// 进入课程学习页面
setTimeout(() => {
createControlPanel();
if (CONFIG.AUTO_START) {
setTimeout(() => startStudying(), 3000);
}
}, 2000);
} else {
stopStudying();
}
}
// 劫持 history API 监听路由变化
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function (...args) {
originalPushState.apply(this, args);
handlePathChange();
};
history.replaceState = function (...args) {
originalReplaceState.apply(this, args);
handlePathChange();
};
window.addEventListener('popstate', handlePathChange);
// ==================== 初始化入口 ====================
(async function init() {
Logger.info('重庆高教在线自动刷课助手已加载');
Logger.info('当前页面:', location.pathname);
// 无论哪个页面都创建面板
if (document.body) {
createControlPanel();
} else {
window.addEventListener('DOMContentLoaded', createControlPanel);
}
// 如果已经在课程学习页面,自动开始
if (location.pathname === '/course/detail/courseStudy' ||
location.pathname === '/learn/mooc/structure') {
Logger.info('检测到课程学习页面');
if (CONFIG.AUTO_START) {
setTimeout(() => {
Logger.info('3秒后自动开始刷课...');
startStudying();
}, 3000);
}
} else {
Logger.info('请先进入「在学课程」,然后点击进入课程学习页面');
notify('刷课助手已就绪,请进入课程学习页面后自动开始', 4000);
}
})();
})();