// ==UserScript==
// @name 新国开/国家开放大学/一网一免费自动刷课
// @namespace 一心向善
// @description 支持自动访问线上链接、查看资料附件、观看视频、自动查看页面、自动参与发帖回帖。调用内部接口实现!
// @version 2.0.0
// @author 一心向善
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAC91BMVEUAAADVHiPaHx3YHyDYHx7YHyDXHx7ZHyHbHyHeIBvfITjaHyDaHyHZHyDZHyDaHyDZHx3NHADaHyDaHx7kIi/aHyDbHyHZHyDYHyDZHx7YHyHaHyDXHx7aHyDaHyDYHyLrIiTRHRjYHx7aHyHaHyHXHyH/JgDWHiHYHyHZHyDNHSrXHyDaHyHZHyHSHh3YIUL/JgDYHyDYHyDYHyDYHyDYHyHaHyHVHiDfICTNHSraHyDdICDXHyDaHyLZHyDaHyDZHyHXHyDVHiDZHx3WHhnFHC/YHyDZHyHYHxjbHx7aHx7ZHyDZHyDVHiPZHyDVHiT/JgDaHxHZHyDZHyDaHyHaHyDVHhW7GADYHyDWHyrYHyDaHyDaHx7ZHyHYHyDXHyDZHyDYHyDVHiPiIBjYHyzYHyHWHh7YHxvYHyGUEQDZHyHYHyHVHh3YHyHYHx7XHh3YHhPaHxndICfZHyDaHyDXHyEAAADYHx7cHx3YHyD/JgDFGgDXHyDYHyDbICTZHyDXHyDYHx7XHyDXHx7fIBnYHx7aHyDYHyTaHx3XHyDYHyDYHyDVHiPaHx7ZHxvZHyDZHyDWHh7WHh3aHx7aHyHZHyDNIFHoIirTHiTZHx7XHyDcHx3YHyDYHx7ZHyDYHyHYHyDXHyDfICHYHx7YHx7ZHyLbHyHYHyHYHyHaHyDVHiDVHhvZHyDZHyDXHx7bHyDZHyDcICHYHx7XHx7aHyDaHx7YHx7bHyDaHyHVHiHcHx3ZHx7ZHyDXHyHaHx7ZHx7aHyDUHh7ZHx7aHyDYHyHWHhnYHx7YHyHVHhvYHyDcICDaHyHcICHYHyHbHyDaHyHZHx7XHx7aHyHXHyDZHyDZHyDbHx7VHh7aHyDXHyHWHh7aHyDaHyDYHyDZHyDaHyHYHx7aHyDaHyHYHyDYHyDZHyDZHx7aHyDXHh3hISLVHh3bHx3XHyHYHyHbHyDcICHaHyDdICHeICHYHyDZHyDhISHkISLgICHfICHjISLbHyHiISH////ipcfUAAAA7nRSTlMAHE6Xvsm8i0YXBlOy6+erTATDPweH+ffXsp+bp8vifQkNqdyBMQEdZFEIq/qJFgUEh9Tj+/DsURIQPv23L9PYV7BHODAHwu8ZcxUpUkxHJQIQcKzwfA4DnBjuyTVN5M/FqxMNDwo/Ix4Cdr4h3H5YDyURj91FAfsseQMH2dUbmV1qrcYM5uE3beOvkCZJLvj7NVfAWEgECAnVegvN0Ziq08DeiItC9uR48jQu9mZs/fH3VZ7kIF/o408h57snleWNIFb8rhhzRhdy/ccybffviUnZrGU9Kyo0WWmG6P795JIfa7n5+b5yIhNMV08U6fjR/AAAAAFiS0dE/DwOo38AAAAHdElNRQfnARUIMQfLGMwuAAACTUlEQVQ4y2NgQABGJmYWVjZ2Dk4GbICLm4f33fsPHz58/MDHLyCIIc8h9O7Th89A8OXdp6/fvgsJo0qLiIp9BMl+/vBJXEJSSlpGVk5eAUleUekHRPujqauLWq9dPY5uAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIzLTAxLTIxVDA4OjQ5OjA3KzAwOjAwazr/FwAAACV0RVh0ZGF0ZTptb2ZpZnkAMjAyMy0wMS0yMVQwODo0OTowNyswMDowMDpnR6sAAAAASUVORK5CYII=
// @match *://lms.ouchn.cn/course/*
// @original-author 一心向善
// @original-license GPL-1.0
// @original-script http://one.ouchn.cn/
// @license GPL-1.0
// @source http://one.ouchn.cn/
// @note 1.1.0:修复控制面板失效问题
// 1.2.0:修复视频自动播放和倍速问题
// 1.3.0:重构控制面板样式,现代化UI设计
// ==/UserScript==
(() => {
"use strict";
const CONFIG = {
videoSpeed: 2,
randomDelayMin: 5000,
randomDelayMax: 8000,
autoPlayVideo: true,
muteVideo: true,
autoNext: false,
nextDelay: 3000,
enablePanel: true
};
const API = {
getGlobalData() {
return {
course: window.globalData?.course || {},
user: window.globalData?.user || {},
dept: window.globalData?.dept || {},
isOpenUniversity: window.globalData?.isOpenUniversity || true,
courseRoles: window.globalData?.courseRoles || ["student"],
deliveryOrg: window.globalData?.deliveryOrg || "ouchn",
useSinglePage: window.globalData?.useSinglePage ?? true,
expandActivityInfo: window.globalData?.expandActivityInfo ?? false
};
},
addLearningBehavior(activityId, activityType) {
const data = this.getGlobalData();
const duration = Math.ceil(300 * Math.random() + 40);
const payload = JSON.stringify({
activity_id: activityId,
activity_type: activityType,
browser: "chrome",
course_id: data.course.id,
course_code: data.course.courseCode,
course_name: data.course.name,
org_id: data.course.orgId,
org_name: data.user.orgName,
org_code: data.user.orgCode,
dep_id: data.dept.id,
dep_name: data.dept.name,
dep_code: data.dept.code,
user_agent: navigator.userAgent,
user_id: data.user.id,
user_name: data.user.name,
user_no: data.user.userNo,
visit_duration: duration
});
return new Promise((resolve, reject) => {
$.ajax({
url: `https://lms.ouchn.cn/statistics/api/user-visits`,
data: payload,
type: "POST",
cache: false,
contentType: "text/plain;charset=UTF-8",
complete: resolve,
error: reject
});
});
},
addVideoLearningRecords({ start_at, end_at, syllabus_id, activity_id, upload_id }) {
const data = this.getGlobalData();
const duration = Math.ceil(300 * Math.random() + 40);
const payload = JSON.stringify({
syllabus_id,
activity_id,
upload_id,
start_at,
end_at,
duration,
user_id: data.user.id,
org_id: data.user.orgId,
course_id: data.course.id,
is_teacher: false,
is_student: true,
ts: Date.now(),
user_agent: navigator.userAgent,
meeting_type: "online_video",
org_name: data.user.orgName,
org_code: data.course.orgCode,
user_no: data.user.userNo,
user_name: data.user.name,
course_code: data.course.courseCode,
course_name: data.course.name
});
return new Promise((resolve, reject) => {
$.ajax({
url: `https://lms.ouchn.cn/statistics/api/online-videos`,
data: payload,
type: "POST",
cache: false,
contentType: "text/plain;charset=UTF-8",
complete: resolve,
error: reject
});
});
},
postLearningActiVities(activityId, activityType, isOpen, activityName = null) {
const data = this.getGlobalData();
const payload = JSON.stringify({
org_id: data.user.orgId,
user_id: data.user.id,
course_id: data.course.id,
enrollment_role: data.courseRoles[0],
is_teacher: false,
activity_id: activityId,
activity_type: activityType,
activity_name: activityName,
module: null,
action: isOpen ? "open" : "close",
ts: new Date().getTime(),
user_agent: navigator.userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
mode: "normal",
channel: "web",
target_info: {},
master_course_id: data.course.id,
org_name: data.user.name,
org_code: data.user.orgCode,
user_no: data.user.userNo,
user_name: data.user.name,
course_code: data.course.courseCode,
course_name: data.course.name,
dep_id: data.dept.id,
dep_name: data.dept.name,
dep_code: data.dept.code
});
return new Promise((resolve, reject) => {
$.ajax({
url: `https://lms.ouchn.cn/statistics/api/learning-activity`,
data: payload,
type: "POST",
contentType: "application/json",
dataType: "JSON",
success: resolve,
error: reject
});
});
},
postActivitiesRead(activityId, extraData = {}) {
return new Promise((resolve, reject) => {
$.ajax({
type: "POST",
url: `https://lms.ouchn.cn/api/course/activities-read/${activityId}`,
contentType: "application/json",
dataType: "JSON",
data: JSON.stringify(extraData),
success: resolve,
error: reject
});
});
},
getCategoryId(activityId) {
return new Promise((resolve) => {
$.get(`https://lms.ouchn.cn/api/forum/${activityId}/category?fields=id`, {}, resolve);
});
},
postForum(categoryId, { title, content } = {}) {
const defaultTitle = `好好学习${Date.now()}`;
const defaultContent = `
好好学习,天天向上。${Date.now()}
`;
return new Promise((resolve, reject) => {
$.ajax({
type: "POST",
url: `https://lms.ouchn.cn/api/topics`,
contentType: "application/json",
dataType: "JSON",
data: JSON.stringify({
title: title || defaultTitle,
content: content || defaultContent,
category_id: categoryId,
uploads: []
}),
success: resolve,
error: reject
});
});
},
getActivities(activityId) {
return new Promise((resolve, reject) => {
$.ajax({
url: `https://lms.ouchn.cn/api/activities/${activityId}`,
type: "GET",
success: resolve,
error: reject
});
});
}
};
const notificationTypes = {
material: "参考资料",
web_link: "线上链接",
online_video: "音视频教材",
slide: "微课",
lesson: "录播教材",
homework: "作业",
forum: "讨论区",
chatroom: "iSlide 直播",
questionnaire: "调查问卷",
page: "页面",
course_invite: "課程邀請",
scorm: "SCORM"
};
const VideoPlayer = {
findVideoElement() {
const selectors = [
'video',
'.video-player video',
'.play-video video',
'video_html5',
'object video',
'iframe video'
];
for (const selector of selectors) {
const video = document.querySelector(selector);
if (video && video.tagName === 'VIDEO') {
return video;
}
}
const embeds = document.querySelectorAll('embed, object');
for (const embed of embeds) {
const video = embed.contentDocument?.querySelector('video');
if (video) return video;
}
return null;
},
async play(video, duration = 0) {
if (!video) {
console.warn('[视频播放器] 未找到视频元素');
return false;
}
try {
video.playbackRate = CONFIG.videoSpeed;
video.muted = CONFIG.muteVideo;
if (video.readyState < 3) {
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('视频加载超时')), 10000);
video.addEventListener('canplay', () => {
clearTimeout(timeout);
resolve();
}, { once: true });
});
}
await video.play();
console.log(`[视频播放器] 开始播放, 倍速: ${CONFIG.videoSpeed}x, 静音: ${CONFIG.muteVideo}`);
if (duration > 0) {
await new Promise(resolve => setTimeout(resolve, duration / CONFIG.videoSpeed));
video.pause();
}
return true;
} catch (err) {
console.error('[视频播放器] 播放失败:', err);
return false;
}
},
async playUntilComplete(video, onProgress) {
if (!video) {
console.warn('[视频播放器] 未找到视频元素');
return false;
}
try {
video.playbackRate = CONFIG.videoSpeed;
video.muted = CONFIG.muteVideo;
if (video.readyState < 3) {
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('视频加载超时')), 15000);
video.addEventListener('canplaythrough', () => {
clearTimeout(timeout);
resolve();
}, { once: true });
});
}
await video.play();
console.log(`[视频播放器] 开始播放直到完成, 倍速: ${CONFIG.videoSpeed}x`);
const videoDuration = video.duration || 0;
const maxWaitMs = Math.min(60000, Math.max(videoDuration * 1000 / CONFIG.videoSpeed, 10000));
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
if (video.ended) break;
const current = video.currentTime || 0;
const total = video.duration || 0;
const progress = total > 0 ? Math.round((current / total) * 100) : 0;
if (onProgress) {
onProgress({ current, total, progress });
}
await new Promise(resolve => setTimeout(resolve, 2000 / CONFIG.videoSpeed));
if (video.paused && !video.ended) {
try { await video.play(); } catch (e) {}
}
}
return true;
} catch (err) {
console.error('[视频播放器] 播放失败:', err);
return false;
}
},
getDuration(video) {
if (!video) return 0;
return video.duration && isFinite(video.duration) ? video.duration : 0;
},
async waitForVideo(maxWaitTime = 15000) {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
const video = this.findVideoElement();
if (video) return video;
await new Promise(resolve => setTimeout(resolve, 500));
}
return null;
}
};
class LogPanel {
constructor() {
this.panelHtml = `
`;
this.init();
}
init() {
const wrapper = document.querySelector(".wrapper") || document.body;
wrapper.insertAdjacentHTML('beforeend', this.panelHtml);
this.panel = document.getElementById("autoLearnPanel");
this.panelHeader = document.getElementById("panelHeader");
this.panelBody = document.getElementById("panelBody");
this.logContainer = document.getElementById("logContainer");
this.startBtn = document.getElementById("startBtn");
this.speedSelector = document.getElementById("speedSelector");
this.autoPlayCheck = document.getElementById("autoPlayCheck");
this.muteVideoCheck = document.getElementById("muteVideoCheck");
this.btnClose = document.getElementById("btnClose");
this.btnMinimize = document.getElementById("btnMinimize");
this.btnClearLog = document.getElementById("btnClearLog");
this.progressFill = document.getElementById("progressFill");
this.bindEvents();
}
bindEvents() {
this.btnClose.addEventListener('click', () => {
this.panel.style.transform = 'translateX(120%)';
setTimeout(() => {
this.panel.style.display = 'none';
}, 300);
});
this.btnMinimize.addEventListener('click', () => {
this.panelBody.style.display = this.panelBody.style.display === 'none' ? 'flex' : 'none';
this.panel.style.height = this.panelBody.style.display === 'none' ? '56px' : 'auto';
});
this.btnClearLog.addEventListener('click', () => {
this.logContainer.innerHTML = '';
});
let isDragging = false;
let offsetX, offsetY;
this.panelHeader.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('ctrl-btn')) return;
isDragging = true;
offsetX = e.clientX - this.panel.offsetLeft;
offsetY = e.clientY - this.panel.offsetTop;
this.panel.style.zIndex = '9999999999';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const x = e.clientX - offsetX;
const y = e.clientY - offsetY;
this.panel.style.left = Math.max(0, Math.min(window.innerWidth - this.panel.offsetWidth, x)) + 'px';
this.panel.style.top = Math.max(0, Math.min(window.innerHeight - this.panel.offsetHeight, y)) + 'px';
this.panel.style.right = 'auto';
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
this.speedSelector.addEventListener('click', (e) => {
const target = e.target.closest('.speed-btn');
if (!target) return;
const speed = parseFloat(target.dataset.speed);
CONFIG.videoSpeed = speed;
this.speedSelector.querySelectorAll('.speed-btn').forEach(btn => btn.classList.remove('active'));
target.classList.add('active');
this.log(`倍速已设置为 ${speed}x`, 'info');
});
this.autoPlayCheck.addEventListener('change', (e) => {
CONFIG.autoPlayVideo = e.target.checked;
this.log(`自动播放: ${CONFIG.autoPlayVideo ? '开启' : '关闭'}`, 'info');
});
this.muteVideoCheck.addEventListener('change', (e) => {
CONFIG.muteVideo = e.target.checked;
this.log(`静音播放: ${CONFIG.muteVideo ? '开启' : '关闭'}`, 'info');
});
}
log(message, type = 'info') {
const now = new Date();
const timeStr = now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const icons = {
info: 'ℹ️',
success: '✓',
warning: '⚠️',
error: '✗'
};
const item = document.createElement('div');
item.className = `log-item ${type}`;
item.innerHTML = `${timeStr}${icons[type] || '📝'}${message}`;
this.logContainer.appendChild(item);
this.logContainer.scrollTop = this.logContainer.scrollHeight;
}
setProgress(percent) {
this.progressFill.style.width = `${Math.min(100, Math.max(0, percent))}%`;
}
onStart(callback) {
this.startBtn.addEventListener('click', () => {
callback();
this.startBtn.innerHTML = '⏳刷课中...';
this.startBtn.disabled = true;
});
}
}
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const randomDelay = () => {
const ms = Math.floor(Math.random() * (CONFIG.randomDelayMax - CONFIG.randomDelayMin)) + CONFIG.randomDelayMin;
return delay(ms);
};
function findNextButton() {
const selectors = [
'button:contains("下一个")',
'a:contains("下一个")',
'.next-btn',
'.next',
'#next-btn',
'#next',
'button',
'a'
];
// 先用jQuery的 :contains 方式
try {
const $btn = $('button, a').filter((i, el) => {
const text = $(el).text().trim().replace(/\s+/g, '');
return text === '下一个' || text === '下一个>' || text === '下一个>';
});
if ($btn.length > 0) return $btn[0];
} catch (e) {}
// 再用原生DOM查找
const allButtons = document.querySelectorAll('button, a, [role="button"], [class*="next"]');
for (const btn of allButtons) {
const text = (btn.textContent || '').trim().replace(/\s+/g, '');
if (text === '下一个' || text === '下一个>' || /下一个/.test(text)) {
// 检查是否可见
const rect = btn.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
return btn;
}
}
}
// 用 XPath 找包含"下一个"的元素
try {
const xpath = '//*[contains(text(), "下一个") and (self::button or self::a or self::div or self::span)]';
const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
if (result.singleNodeValue) {
const node = result.singleNodeValue;
const rect = node.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) return node;
}
} catch (e) {}
return null;
}
function goNext(panel) {
const btn = findNextButton();
if (!btn) {
if (panel) panel.log('⚠️ 未找到"下一个"按钮', 'warning');
return false;
}
if (panel) panel.log(`➡️ 点击下一个...`, 'info');
// 触发点击
try {
btn.click();
} catch (e) {
// 用更可靠的方式触发点击
const event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
btn.dispatchEvent(event);
}
return true;
}
class CourseAutoLearn {
constructor(courseId) {
this.courseId = courseId;
this.panel = new LogPanel();
this.isRunning = false;
this.totalActivities = 0;
this.completedActivities = 0;
}
async start() {
this.isRunning = true;
this.completedActivities = 0;
this.totalActivities = 0;
this.panel.log('===== 智能刷课开始 =====', 'info');
this.panel.log(`当前配置: 倍速=${CONFIG.videoSpeed}x, 自动播放=${CONFIG.autoPlayVideo ? '开启' : '关闭'}, 自动下一个=${CONFIG.autoNext ? '开启' : '关闭'}`, 'info');
this.panel.setProgress(0);
try {
await this.processCourse();
} catch (err) {
this.panel.log(`执行出错: ${err.message}`, 'error');
console.error(err);
}
this.panel.log('===== 刷课完成 =====', 'success');
this.panel.setProgress(100);
this.isRunning = false;
// 自动切换到下一个任务
if (CONFIG.autoNext) {
this.panel.log(`⏳ ${CONFIG.nextDelay / 1000}秒后切换到下一个任务...`, 'info');
setTimeout(() => {
const success = goNext(this.panel);
if (success) {
this.panel.log('✅ 已切换到下一个任务,请等待页面加载...', 'success');
} else {
this.panel.log('⚠️ 无法自动切换,请手动点击"下一个"', 'warning');
// 重新激活开始按钮
this.panel.startBtn.innerHTML = '🔄重新开始';
this.panel.startBtn.disabled = false;
}
}, CONFIG.nextDelay);
} else {
setTimeout(() => {
this.panel.startBtn.innerHTML = '🔄重新开始';
this.panel.startBtn.disabled = false;
}, 1000);
}
}
async processCourse() {
const courseId = this.courseId;
const completenessData = await this.getCompleteness(courseId);
const startProgress = completenessData?.study_completeness || 0;
this.panel.log(`当前课程进度: ${startProgress}%`, 'info');
const modulesData = await this.getModules(courseId);
const modules = modulesData?.modules || [];
this.panel.log(`课程共 ${modules.length} 个模块`, 'info');
const completedActivities = completenessData?.completed_result?.completed?.learning_activity || [];
this.totalActivities = modules.reduce((sum, m) => sum + (m.activities_count || 0), 0) || modules.length * 3;
for (const module of modules) {
if (!this.isRunning) break;
await randomDelay();
this.panel.log(`━━━━ ${module.name} ━━━━`, 'info');
const activitiesData = await this.getModuleActivities(courseId, module.id);
const activities = activitiesData?.learning_activities || [];
for (const activity of activities) {
if (!this.isRunning) break;
const { title, id, type } = activity;
const isCompleted = completedActivities.includes(parseInt(id));
if (isCompleted) {
this.panel.log(`[跳过] ${title} (${notificationTypes[type] || type})`, 'warning');
this.completedActivities++;
this.updateProgress();
continue;
}
this.panel.log(`开始处理: ${title}`, 'info');
try {
await this.handleActivity(activity, module);
this.panel.log(`✅ 完成: ${title}`, 'success');
} catch (err) {
this.panel.log(`❌ 失败: ${title} - ${err.message}`, 'error');
}
this.completedActivities++;
this.updateProgress();
await randomDelay();
}
}
const endProgress = (await this.getCompleteness(courseId))?.study_completeness || 0;
this.panel.log(`🎉 刷课完成! 进度: ${startProgress}% → ${endProgress}%`, 'success');
}
updateProgress() {
if (this.totalActivities > 0) {
const percent = (this.completedActivities / this.totalActivities) * 100;
this.panel.setProgress(percent);
}
}
async handleActivity(activity, module) {
const { id, title, type, is_open, uploads, syllabus_id } = activity;
await API.postLearningActiVities(id, type, is_open, title);
await delay(500);
switch (type) {
case 'page':
await API.postActivitiesRead(id);
break;
case 'online_video':
await this.handleVideoActivity(id, title, uploads, syllabus_id);
break;
case 'material':
for (const upload of uploads) {
await API.postActivitiesRead(id, { upload_id: upload.id });
}
break;
case 'forum':
const categoryData = await API.getCategoryId(id);
if (categoryData?.topic_category?.id) {
await API.postForum(categoryData.topic_category.id);
}
break;
case 'web_link':
await API.postActivitiesRead(id);
break;
default:
this.panel.log(`⚠️ 不支持的活动类型: ${type}`, 'warning');
}
}
async handleVideoActivity(activityId, title, uploads, syllabusId) {
const panel = this.panel;
let actualDuration = 300;
panel.log(`📋 开始处理视频: ${title}`, 'info');
// 第一次调用 - 开始活动
await API.postLearningActiVities(activityId, 'online_video', true, title);
await API.postActivitiesRead(activityId);
if (CONFIG.autoPlayVideo) {
const video = await VideoPlayer.waitForVideo();
if (video) {
video.playbackRate = CONFIG.videoSpeed;
video.muted = CONFIG.muteVideo;
try {
await video.play();
actualDuration = video.duration || 300;
panel.log(`🎬 视频播放中, 时长: ${Math.round(actualDuration)}秒, 倍速: ${CONFIG.videoSpeed}x, ${CONFIG.muteVideo ? '已静音' : '有声'}`, 'info');
const maxWaitMs = Math.min(120000, Math.max(actualDuration * 1000 / CONFIG.videoSpeed, 15000));
const startTime = Date.now();
let lastProgressReport = 0;
let playedSuccessfully = false;
while (Date.now() - startTime < maxWaitMs) {
if (video.ended || video.currentTime >= (video.duration || actualDuration) * 0.95) {
playedSuccessfully = true;
break;
}
const current = video.currentTime || 0;
const total = video.duration || actualDuration;
const progress = total > 0 ? Math.round((current / total) * 100) : 0;
if (progress >= lastProgressReport + 20) {
panel.log(` 播放进度: ${progress}% (${Math.round(current)}/${Math.round(total)}秒)`, 'info');
lastProgressReport = progress;
}
await new Promise(resolve => setTimeout(resolve, 3000 / CONFIG.videoSpeed));
if (video.paused && !video.ended && video.currentTime < (video.duration || actualDuration) * 0.95) {
try { await video.play(); } catch (e) {}
}
}
if (playedSuccessfully) {
panel.log(`✅ 视频播放完成`, 'success');
actualDuration = Math.max(video.duration || actualDuration, video.currentTime || actualDuration);
} else {
panel.log(`⏱️ 播放超时,强制结束 (已播放 ${Math.round(video.currentTime || 0)}秒)`, 'warning');
actualDuration = Math.max(video.currentTime || actualDuration, actualDuration * 0.95);
}
} catch (err) {
panel.log(`📺 视频播放失败: ${err.message}`, 'error');
// 即使播放失败,也使用视频的实际时长
try {
actualDuration = video.duration || 300;
} catch (e) {
actualDuration = 300;
}
}
} else {
panel.log('📺 未找到视频元素,使用元数据时长', 'warning');
// 使用上传信息中的视频时长
if (uploads && uploads.length > 0 && uploads[0].videos && uploads[0].videos.length > 0) {
actualDuration = uploads[0].videos[0].duration || 300;
}
}
} else {
// 不自动播放时,使用元数据时长
if (uploads && uploads.length > 0 && uploads[0].videos && uploads[0].videos.length > 0) {
actualDuration = uploads[0].videos[0].duration || 300;
}
panel.log(`📺 跳过自动播放,使用时长: ${actualDuration}秒`, 'info');
}
// 上报视频观看记录
for (const upload of uploads) {
for (const videoInfo of upload.videos || []) {
const duration = videoInfo.duration || actualDuration;
await API.addVideoLearningRecords({
syllabus_id: syllabusId,
activity_id: activityId,
upload_id: upload.id,
start_at: 0,
end_at: Math.round(duration)
});
// 每个视频文件都单独上报一次完成
await API.postActivitiesRead(activityId, { start: 0, end: Math.round(duration) });
}
}
panel.log(`📊 已报告观看进度: 0 - ${Math.round(actualDuration)}秒`, 'success');
}
getCompleteness(courseId) {
return new Promise((resolve) => {
$.get(`https://lms.ouchn.cn/api/course/${courseId}/my-completeness`, ((data) => resolve(data)));
});
}
getModules(courseId) {
return new Promise((resolve) => {
$.get(`https://lms.ouchn.cn/api/courses/${courseId}/modules`, ((data) => resolve(data)));
});
}
getModuleActivities(courseId, moduleId) {
return new Promise((resolve) => {
$.get(`https://lms.ouchn.cn/api/course/${courseId}/all-activities?module_ids=[${moduleId}]&activity_types=learning_activities,exams,classrooms`, ((data) => resolve(data)));
});
}
}
let currentCourseId = null;
let currentAutoLearn = null;
function cleanupOldPanel() {
const oldPanel = document.getElementById('autoLearnPanel');
if (oldPanel) {
oldPanel.remove();
}
}
function init() {
// 清理旧面板
cleanupOldPanel();
const courseIdInput = document.querySelector("#courseId");
if (!courseIdInput) {
console.error('[刷课脚本] 未找到课程ID输入框 (#courseId),可能页面还在加载中');
setTimeout(init, 2000);
return;
}
const courseId = courseIdInput.value;
if (!courseId) {
console.error('[刷课脚本] 课程ID为空');
setTimeout(init, 2000);
return;
}
// 如果是同一个课程,不重新初始化
if (currentCourseId === courseId && currentAutoLearn && document.getElementById('autoLearnPanel')) {
console.log(`[刷课脚本] 已是同一课程 ${courseId},跳过重新初始化`);
return;
}
currentCourseId = courseId;
currentAutoLearn = new CourseAutoLearn(courseId);
currentAutoLearn.panel.log(`🎓 课程ID: ${courseId}`, 'info');
currentAutoLearn.panel.log(`💡 视频播放页面点击"开始刷课"按钮开始自动学习才有效果`, 'info');
currentAutoLearn.panel.onStart(() => {
currentAutoLearn.start();
});
}
// SPA 页面切换监听
let lastUrl = location.href;
const urlObserver = new MutationObserver(() => {
const currentUrl = location.href;
if (currentUrl !== lastUrl) {
console.log(`[刷课脚本] 检测到页面变化: ${lastUrl} -> ${currentUrl}`);
lastUrl = currentUrl;
setTimeout(init, 1500);
}
// 同时检查 courseId 是否发生变化(可能是页面内切换)
const courseIdInput = document.querySelector("#courseId");
if (courseIdInput && currentCourseId && courseIdInput.value !== currentCourseId) {
console.log(`[刷课脚本] 课程ID变化: ${currentCourseId} -> ${courseIdInput.value}`);
setTimeout(init, 1500);
}
});
// 监听 history API 变化(SPA常用)
const originalPushState = history.pushState;
history.pushState = function() {
originalPushState.apply(this, arguments);
setTimeout(() => {
if (CONFIG.autoNext) {
console.log('[刷课脚本] 检测到 pushState,准备重新初始化');
init();
}
}, 2000);
};
const originalReplaceState = history.replaceState;
history.replaceState = function() {
originalReplaceState.apply(this, arguments);
setTimeout(() => {
if (CONFIG.autoNext && !document.getElementById('autoLearnPanel')) {
console.log('[刷课脚本] 检测到 replaceState,面板已移除,准备重新初始化');
init();
}
}, 2000);
};
// 初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// 定时检查页面是否需要重新初始化(兜底方案)
setInterval(() => {
if (CONFIG.autoNext && !document.getElementById('autoLearnPanel')) {
const courseIdInput = document.querySelector("#courseId");
if (courseIdInput && courseIdInput.value) {
console.log('[刷课脚本] 定时检测: 面板不存在,重新初始化');
init();
}
}
}, 5000);
// DOM变化监听
urlObserver.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['value']
});
})();