// ==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'] }); })();