// ==UserScript== // @name 新国开/国家开放大学/一网一免费自动刷课 // @namespace 一心向善 // @description 支持自动访问线上链接、查看资料附件、观看视频、自动查看页面、自动参与发帖回帖。调用内部接口实现! // @version 2.2.3 // @author 一心向善 // @match *://lms.ouchn.cn/course/* // @original-author 一心向善 // @original-license GPL-1.0 // @license GPL-1.0 // @grant none // ==/UserScript== (() => { 'use strict'; // ============ 配置 ============ const CONFIG = { videoSpeed: 2, randomDelayMin: 5000, randomDelayMax: 8000, autoPlayVideo: true, muteVideo: true, autoNext: false, nextDelay: 3000, enablePanel: true, statChunk: 300, // 观看时长统计分片:每 300 秒一片(降低请求数避免限流) statInterval: 3000 // 统计请求间隔(毫秒) }; const notificationTypes = { material: '参考资料', web_link: '线上链接', online_video: '音视频教材', slide: '微课', lesson: '录播教材', homework: '作业', forum: '讨论区', chatroom: 'iSlide 直播', questionnaire: '调查问卷', page: '页面', course_invite: '課程邀請', scorm: 'SCORM' }; const delay = (ms) => new Promise((r) => setTimeout(r, ms)); const sleep = async (min = CONFIG.randomDelayMin, max = CONFIG.randomDelayMax) => delay(Math.floor(Math.random() * (max - min)) + min); // ============ API ============ const API = { getGlobalData() { const g = window.globalData || {}; return { course: g.course || {}, user: g.user || {}, dept: g.dept || {}, courseRoles: g.courseRoles || ['student'] }; }, // 统计上报:与官方前端完全一致的 text/plain + 同源 Cookie // 429/404/5xx/网络错误 统一重试(平台偶发路由到旧节点会返回 404,重试即可成功) postStatistics(url, body, attempt = 1) { const fullUrl = url.startsWith('http') ? url : `${location.origin}${url}`; const MAX_ATTEMPTS = 4; return fetch(fullUrl, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, body }).then(async (res) => { if (res.ok) return { status: res.status }; // 429/404/408/5xx 可重试 const retryable = res.status === 429 || res.status === 404 || res.status === 408 || res.status >= 500; if (retryable && attempt < MAX_ATTEMPTS) { await delay(1500 * attempt); // 1.5s, 3s, 4.5s return API.postStatistics(url, body, attempt + 1); } let msg = ''; try { msg = (await res.text()).slice(0, 100); } catch (e) {} throw new Error(`请求失败 [HTTP ${res.status}]${msg ? ' ' + msg : ''}`); }).catch(async (err) => { // 网络层错误(Failed to fetch 等)同样重试 if (attempt < MAX_ATTEMPTS) { await delay(1500 * attempt); return API.postStatistics(url, body, attempt + 1); } throw err; }); }, // 学习访问时长 user-visits(字段与官方逐一对齐) addLearningBehavior(activityId, activityType = 'online_video', visitDuration = 60) { const { course, user, dept } = this.getGlobalData(); const masterCourseId = course.masterCourseId || course.id; const body = JSON.stringify({ user_id: String(user.id), org_id: user.orgId, course_id: course.id, activity_id: activityId, visit_duration: visitDuration, is_teacher: false, browser: 'chrome', user_agent: navigator.userAgent, master_course_id: masterCourseId, activity_type: activityType, auto_interval: true, org_name: user.orgName, org_code: user.orgCode, user_no: user.userNo, user_name: user.name, course_code: course.courseCode, course_name: course.name, dep_id: dept.id, dep_name: dept.name, dep_code: dept.code }); return this.postStatistics('/statistics/api/user-visits', body); }, // 视频观看时长 online-videos(字段与官方逐一对齐) addVideoLearningRecords({ start_at, end_at, duration, activity_id, upload_id, module_id }) { const { course, user, dept } = this.getGlobalData(); const masterCourseId = course.masterCourseId || course.id; const body = JSON.stringify({ user_id: user.id, org_id: user.orgId, course_id: course.id, module_id: module_id ?? null, is_final: null, is_formative: null, activity_id: activity_id, upload_id: upload_id, reply_id: null, comment_id: null, forum_type: '', action_type: 'play', is_teacher: false, is_student: true, ts: Date.now(), user_agent: navigator.userAgent, meeting_type: 'online_video', start_at: start_at, end_at: end_at, duration: duration ?? (end_at - start_at), master_course_id: masterCourseId, org_name: user.orgName, org_code: user.orgCode, user_no: user.userNo, user_name: user.name, course_code: course.courseCode, course_name: course.name, dep_id: dept.id, dep_name: dept.name, dep_code: dept.code }); return this.postStatistics('/statistics/api/online-videos', body); }, // 学习活动行为 postLearningActiVities(activityId, activityType, isOpen, activityName = null) { const { course, user, dept, courseRoles } = this.getGlobalData(); const payload = JSON.stringify({ org_id: user.orgId, user_id: user.id, course_id: course.id, enrollment_role: courseRoles[0], is_teacher: false, activity_id: activityId, activity_type: activityType, activity_name: activityName, module: null, action: isOpen ? 'open' : 'close', ts: Date.now(), user_agent: navigator.userAgent, mode: 'normal', channel: 'web', target_info: {}, master_course_id: course.id, org_name: user.orgName, org_code: user.orgCode, user_no: user.userNo, user_name: user.name, course_code: course.courseCode, course_name: course.name, dep_id: dept.id, dep_name: dept.name, dep_code: dept.code }); return new Promise((resolve) => { $.ajax({ url: `${location.origin}/statistics/api/learning-activity`, data: payload, type: 'POST', contentType: 'application/json', dataType: 'JSON', success: resolve, error: (xhr, st, et) => resolve(xhr) // 失败也不阻断 }); }); }, // 活动完成上报(决定"已完成"的关键接口) postActivitiesRead(activityId, extraData = {}) { return new Promise((resolve, reject) => { $.ajax({ type: 'POST', url: `${location.origin}/api/course/activities-read/${activityId}`, contentType: 'application/json', dataType: 'JSON', data: JSON.stringify(extraData), success: resolve, error: (xhr) => reject(new Error(`活动完成上报失败 [HTTP ${xhr.status}]`)) }); }); }, getCategoryId(activityId) { return new Promise((resolve) => { $.get(`${location.origin}/api/forum/${activityId}/category?fields=id`, {}, resolve); }); }, postForum(categoryId, { title, content } = {}) { const t = title || `好好学习${Date.now()}`; const c = content || `

好好学习,天天向上。${Date.now()}

`; return new Promise((resolve, reject) => { $.ajax({ type: 'POST', url: `${location.origin}/api/topics`, contentType: 'application/json', dataType: 'JSON', data: JSON.stringify({ title: t, content: c, category_id: categoryId, uploads: [] }), success: resolve, error: (xhr) => reject(new Error(`发帖失败 [HTTP ${xhr.status}]`)) }); }); } }; // ============ 视频播放 ============ const VideoPlayer = { async waitForVideo(timeout = 15000) { const start = Date.now(); while (Date.now() - start < timeout) { const v = this.findVideo(); if (v) return v; await delay(500); } return null; }, findVideo() { let v = document.querySelector('video'); if (v) return v; // 同源 iframe 内查找 const frames = document.querySelectorAll('iframe'); for (const f of frames) { try { const doc = f.contentDocument; if (doc) { const iv = doc.querySelector('video'); if (iv) return iv; } } catch (e) {} } return null; }, async playUntilComplete(video, onProgress) { video.playbackRate = CONFIG.videoSpeed; video.muted = CONFIG.muteVideo; try { await video.play(); } catch (e) {} const total = video.duration || 0; const maxWaitMs = Math.min(180000, Math.max((total * 1000) / CONFIG.videoSpeed, 15000)); const startTime = Date.now(); let last = -1; while (Date.now() - startTime < maxWaitMs) { if (video.ended || video.currentTime >= total * 0.95) { return { ok: true, duration: total || video.currentTime }; } const progress = total > 0 ? Math.round((video.currentTime / total) * 100) : 0; if (progress >= last + 20) { onProgress && onProgress(progress, video.currentTime, total); last = progress; } await delay(Math.max(1000, 3000 / CONFIG.videoSpeed)); if (video.paused && !video.ended && video.currentTime < total * 0.95) { try { await video.play(); } catch (e) {} } } return { ok: false, duration: video.currentTime || total }; } }; // ============ 控制面板 ============ class LogPanel { constructor() { this.el = null; this.onStartCb = null; } mount() { if (document.getElementById('guokaiPanel')) return; const wrapper = document.querySelector('.wrapper') || document.body; wrapper.insertAdjacentHTML('beforeend', this.html()); this.el = document.getElementById('guokaiPanel'); this.bindEvents(); this.makeDraggable(); } html() { return `
🎬 国开视频助手
⚙️ 功能区
视频倍速
自动播放
静音播放
📋 运行日志
清空
答题或批量合作,联系Q:640105435
Q群1:949193546 | Q群2:756253160
`; } bindEvents() { const $el = this.el; $el.querySelector('#gkClose').onclick = () => $el.remove(); $el.querySelector('#gkMin').onclick = () => { const body = $el.querySelector('#guokaiBody'); const footer = $el.querySelector('#gkFooter'); const hidden = body.style.display === 'none'; body.style.display = hidden ? 'flex' : 'none'; footer.style.display = hidden ? 'block' : 'none'; }; $el.querySelectorAll('.speed-btn').forEach((btn) => { btn.onclick = () => { $el.querySelectorAll('.speed-btn').forEach((b) => b.classList.remove('active')); btn.classList.add('active'); CONFIG.videoSpeed = parseFloat(btn.dataset.speed); const v = VideoPlayer.findVideo(); if (v) v.playbackRate = CONFIG.videoSpeed; this.log(`倍速已切换为 ${CONFIG.videoSpeed}x`, 'info'); }; }); $el.querySelector('#gkAutoPlay').onchange = (e) => { CONFIG.autoPlayVideo = e.target.checked; this.log(`自动播放: ${CONFIG.autoPlayVideo ? '开启' : '关闭'}`, 'info'); }; $el.querySelector('#gkMute').onchange = (e) => { CONFIG.muteVideo = e.target.checked; const v = VideoPlayer.findVideo(); if (v) v.muted = CONFIG.muteVideo; this.log(`静音播放: ${CONFIG.muteVideo ? '开启' : '关闭'}`, 'info'); }; $el.querySelector('#gkClear').onclick = () => { $el.querySelector('#gkLog').innerHTML = ''; }; $el.querySelector('#gkStartBtn').onclick = () => { if (this.onStartCb) this.onStartCb(); }; } makeDraggable() { const header = this.el.querySelector('#guokaiHeader'); let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false; header.addEventListener('mousedown', (e) => { dragging = true; sx = e.clientX; sy = e.clientY; const r = this.el.getBoundingClientRect(); ox = r.left; oy = r.top; this.el.style.right = 'auto'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!dragging) return; this.el.style.left = `${ox + e.clientX - sx}px`; this.el.style.top = `${oy + e.clientY - sy}px`; }); document.addEventListener('mouseup', () => { dragging = false; }); } onStart(cb) { this.onStartCb = cb; } setStartBtnDisabled(disabled) { const btn = this.el && this.el.querySelector('#gkStartBtn'); if (btn) { btn.disabled = disabled; btn.textContent = disabled ? '⏳ 刷课中...' : '🚀 开始刷课'; } } setProgress(pct) { const fill = this.el && this.el.querySelector('#gkProgressFill'); if (fill) fill.style.width = `${Math.max(0, Math.min(100, pct))}%`; } log(msg, type = 'info') { console.log('[国开助手]', msg); if (!this.el) return; const box = this.el.querySelector('#gkLog'); if (!box) return; const now = new Date().toLocaleTimeString('zh-CN', { hour12: false }); const div = document.createElement('div'); div.className = `log-item ${type}`; div.innerHTML = `${now}${msg}`; box.appendChild(div); box.scrollTop = box.scrollHeight; while (box.children.length > 300) box.removeChild(box.firstChild); } } // ============ 自动刷课 ============ class CourseAutoLearn { constructor(courseId) { this.courseId = courseId; this.panel = new LogPanel(); this.panel.mount(); this.running = false; } start() { if (this.running) { this.panel.log('正在运行中,请勿重复点击', 'warning'); return; } this.running = true; this.panel.setStartBtnDisabled(true); this.run().catch((e) => { this.panel.log(`❌ 运行异常: ${e.message}`, 'error'); console.error(e); }).finally(() => { this.running = false; this.panel.setStartBtnDisabled(false); }); } async run() { this.panel.log('===== 开始刷课 =====', 'info'); const before = await this.getCompleteness(); const completedIds = (before.completed_result?.completed?.learning_activity || []).map(Number); this.panel.log(`当前课程进度: ${before.study_completeness ?? 0}%`, 'info'); const modules = await this.getModules(); for (const module of modules) { this.panel.log(`📚 模块: ${module.name} (${module.id})`, 'info'); let activities = []; try { activities = await this.getAllActivities(module.id); } catch (e) { this.panel.log(` 模块活动获取失败: ${e.message}`, 'warning'); continue; } for (const act of activities) { if (completedIds.includes(Number(act.id))) { this.panel.log(` ✓ 已完成跳过: ${act.title}`, 'success'); continue; } try { await this.handleActivity(act, module); this.panel.log(` ✅ 完成: ${act.title}`, 'success'); } catch (e) { this.panel.log(` ❌ 失败: ${act.title} - ${e.message}`, 'error'); } await sleep(); } } const after = await this.getCompleteness(); this.panel.log(`🎉 全部处理完毕,课程进度: ${before.study_completeness ?? 0}% → ${after.study_completeness ?? 0}%`, 'success'); this.panel.setProgress(after.study_completeness ?? 100); } async getCompleteness() { return new Promise((resolve) => { $.get(`${location.origin}/api/course/${this.courseId}/my-completeness`, (data) => resolve(data || {})); }); } async getModules() { return new Promise((resolve) => { $.get(`${location.origin}/api/courses/${this.courseId}/modules`, (data) => { resolve((data && data.modules) || []); }); }); } async getAllActivities(moduleId) { return new Promise((resolve, reject) => { $.get(`${location.origin}/api/course/${this.courseId}/all-activities?module_ids=[${moduleId}]&activity_types=learning_activities,exams,classrooms`, (data) => resolve((data && data.learning_activities) || [])) .fail((xhr) => reject(new Error(`HTTP ${xhr.status}`))); }); } async handleActivity(activity, module) { const { id, title, type, is_open, uploads, syllabus_id } = activity; try { await API.postLearningActiVities(id, type, is_open, title); } catch (e) {} switch (type) { case 'page': case 'web_link': await API.postActivitiesRead(id); break; case 'online_video': await this.handleVideoActivity(id, title, uploads, syllabus_id, module && module.id); break; case 'material': for (const u of (uploads || [])) { try { await API.postActivitiesRead(id, { upload_id: u.id }); } catch (e) {} } break; case 'forum': { const cat = await API.getCategoryId(id); const catId = cat && cat.topic_category && cat.topic_category.id; if (catId) await API.postForum(catId); break; } default: this.panel.log(` ⚠️ 暂不支持的类型: ${notificationTypes[type] || type}`, 'warning'); } } async handleVideoActivity(activityId, title, uploads, syllabusId, moduleId) { const panel = this.panel; panel.log(` 📋 视频: ${title}`, 'info'); let actualDuration = 0; try { await API.postActivitiesRead(activityId); } catch (e) {} // 1. 真实播放视频(若当前页面有播放器) if (CONFIG.autoPlayVideo) { const video = await VideoPlayer.waitForVideo(8000); if (video) { try { const res = await VideoPlayer.playUntilComplete(video, (p, cur, total) => { panel.log(` 播放进度: ${p}% (${Math.round(cur)}/${Math.round(total)}秒)`, 'info'); }); actualDuration = Math.round(res.duration || video.duration || 0); panel.log(res.ok ? ` 🎬 视频播放完成 (${actualDuration}秒)` : ` ⏱️ 播放超时,已播放 ${actualDuration}秒`, res.ok ? 'success' : 'warning'); } catch (e) { panel.log(` 📺 播放异常: ${e.message}`, 'error'); actualDuration = Math.round(video.duration || 0); } } else { // 无播放器(课程列表页)→ 走纯接口上报 console.log('[国开助手] 当前页面无播放器,使用接口上报视角时长'); } } // 2. 观看时长统计(分片心跳,限速+失败冷却) const uploadList = Array.isArray(uploads) ? uploads : []; let videoOk = 0, videoFail = 0, visitOk = 0, visitFail = 0, lastErr = ''; let reported = 0; for (const upload of uploadList) { for (const vinfo of (upload.videos || [])) { const duration = Math.round(vinfo.duration || actualDuration || 0); if (duration <= 0) continue; const CHUNK = CONFIG.statChunk; for (let start = 0; start < duration; start += CHUNK) { const end = Math.min(start + CHUNK, duration); try { await API.addVideoLearningRecords({ activity_id: activityId, upload_id: upload.id, module_id: moduleId, start_at: start, end_at: end, duration: end - start }); videoOk++; } catch (e) { videoFail++; lastErr = e.message; console.warn('[国开助手] online-videos 上报失败:', e.message); await delay(10000); // 失败冷却,避免连续触发限流 } try { await API.addLearningBehavior(activityId, 'online_video', end - start); visitOk++; } catch (e) { visitFail++; lastErr = e.message; console.warn('[国开助手] user-visits 上报失败:', e.message); } await delay(CONFIG.statInterval); } // 3. 完成上报(决定"已完成") try { await API.postActivitiesRead(activityId, { start: 0, end: duration }); reported++; } catch (e) { console.warn('[国开助手] 完成上报失败:', e.message); } } } if (videoFail === 0 && visitFail === 0) { panel.log(` 📈 观看时长统计已上报 (视频${videoOk}片/访问${visitOk}片)`, 'success'); } else { panel.log(` 📈 观看时长统计: 视频 ${videoOk}成功/${videoFail}失败, 访问 ${visitOk}成功/${visitFail}失败${lastErr ? ' (' + lastErr + ')' : ''}`, 'info'); } if (reported > 0) { panel.log(` 📊 已报告观看进度 0-${actualDuration || '?'}秒`, 'success'); } } } // ============ 初始化 & SPA 监听 ============ let currentCourseId = null; let currentLearn = null; function getCourseId() { const input = document.querySelector('#courseId'); if (input && input.value) return input.value; const m = location.pathname.match(/\/course\/(\d+)/); return m ? m[1] : null; } function initPanel() { const courseId = getCourseId(); if (!courseId) return; if (!CONFIG.enablePanel) return; if (currentLearn && currentCourseId === courseId && document.getElementById('guokaiPanel')) return; currentCourseId = courseId; currentLearn = new CourseAutoLearn(courseId); currentLearn.panel.log(`🎓 课程ID: ${courseId}`, 'info'); currentLearn.panel.log('💡 视频播放页面点击"开始刷课"按钮开始自动学习才有效果', 'info'); currentLearn.panel.onStart(() => currentLearn.start()); } function boot() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(initPanel, 500)); } else { setTimeout(initPanel, 500); } // SPA 路由变化监听 let lastUrl = location.href; setInterval(() => { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(initPanel, 800); } if (!document.getElementById('guokaiPanel')) { currentLearn = null; initPanel(); } }, 1500); } boot(); })();