// ==UserScript== // @name 全新国开免费自动刷课 // @namespace 小石头 // @description 支持自动访问线上链接、查看资料附件、观看视频、自动查看页面、自动参与发帖回帖。 // @version 2.7.1 // @author 小石头 // @icon data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20128%20128'%3E%3Crect%20x='6'%20y='6'%20width='116'%20height='116'%20rx='26'%20fill='%2300b96b'/%3E%3Crect%20x='51'%20y='36'%20width='26'%20height='56'%20fill='none'%20stroke='%23ffffff'%20stroke-width='8'/%3E%3C/svg%3E // @match *://lms.ouchn.cn/course/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @connect keyt.cn // @original-author 小石头 // @original-license GPL-1.0 // @original-script http://one.ouchn.cn/ // @license GPL-1.0 // @source http://one.ouchn.cn/ // @note 2.7.1:标题改为"国开学习",按钮改为"捐赠",填入卡密通配置 // ==/UserScript== (() => { "use strict"; // ============================================================ // 配置区 // ============================================================ const CONFIG = { videoSpeed: 2, randomDelayMin: 5000, randomDelayMax: 8000, autoPlayVideo: true, muteVideo: true, autoNext: false, nextDelay: 3000, enablePanel: true, repeatLearned: false, activityInterval: 0, unlocked: false }; const ACTIVITY_INTERVAL_STEPS = [ { label: '0秒', sec: 0 }, { label: '3秒', sec: 3 }, { label: '5秒', sec: 5 }, { label: '10秒', sec: 10 }, { label: '30秒', sec: 30 }, { label: '1分钟', sec: 60 }, { label: '2分钟', sec: 120 } ]; const REPEAT_SKIP_TYPES = ['forum']; // ============================================================ // 卡密通网络验证配置 // ============================================================ const KEYT_USER = 'cjtfky123'; const KEYT_APP = 'a'; const KEYT_SECRET = '1cc35a6aa4b3b60f82100301574ca9a9'; const KEYT_BASE = `https://www.keyt.cn/kami/${KEYT_USER}/check.php`; const AUTH_CACHE_KEY = 'ouchn_auth_cache'; const MACHINE_ID_KEY = 'ouchn_machine_id'; const HEARTBEAT_MS = 50000; const HEARTBEAT_MAX_FAIL = 5; const TIME_TOLERANCE = 120; // ============================================================ // MD5 纯 JS 实现 // ============================================================ const MD5 = (function () { function safeAdd(x, y) { const lsw = (x & 0xFFFF) + (y & 0xFFFF); const msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xFFFF); } function bitRotateLeft(num, cnt) { return (num << cnt) | (num >>> (32 - cnt)); } function md5cmn(q, a, b, x, s, t) { return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b); } function md5ff(a, b, c, d, x, s, t) { return md5cmn((b & c) | (~b & d), a, b, x, s, t); } function md5gg(a, b, c, d, x, s, t) { return md5cmn((b & d) | (c & ~d), a, b, x, s, t); } function md5hh(a, b, c, d, x, s, t) { return md5cmn(b ^ c ^ d, a, b, x, s, t); } function md5ii(a, b, c, d, x, s, t) { return md5cmn(c ^ (b | ~d), a, b, x, s, t); } function binlMD5(x, len) { x[len >> 5] |= 0x80 << (len % 32); x[(((len + 64) >>> 9) << 4) + 14] = len; let a = 1732584193, b = -271733879, c = -1732584194, d = 271733878; for (let i = 0; i < x.length; i += 16) { const olda = a, oldb = b, oldc = c, oldd = d; a = md5ff(a, b, c, d, x[i], 7, -680876936); d = md5ff(d, a, b, c, x[i + 1], 12, -389564586); c = md5ff(c, d, a, b, x[i + 2], 17, 606105819); b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330); a = md5ff(a, b, c, d, x[i + 4], 7, -176418897); d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426); c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341); b = md5ff(b, c, d, a, x[i + 7], 22, -45705983); a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416); d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417); c = md5ff(c, d, a, b, x[i + 10], 17, -42063); b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162); a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682); d = md5ff(d, a, b, c, x[i + 13], 12, -40341101); c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290); b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329); a = md5gg(a, b, c, d, x[i + 1], 5, -165796510); d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632); c = md5gg(c, d, a, b, x[i + 11], 14, 643717713); b = md5gg(b, c, d, a, x[i], 20, -373897302); a = md5gg(a, b, c, d, x[i + 5], 5, -701558691); d = md5gg(d, a, b, c, x[i + 10], 9, 38016083); c = md5gg(c, d, a, b, x[i + 15], 14, -660478335); b = md5gg(b, c, d, a, x[i + 4], 20, -405537848); a = md5gg(a, b, c, d, x[i + 9], 5, 568446438); d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690); c = md5gg(c, d, a, b, x[i + 3], 14, -187363961); b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501); a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467); d = md5gg(d, a, b, c, x[i + 2], 9, -51403784); c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473); b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734); a = md5hh(a, b, c, d, x[i + 5], 4, -378558); d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463); c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562); b = md5hh(b, c, d, a, x[i + 14], 23, -35309556); a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060); d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353); c = md5hh(c, d, a, b, x[i + 7], 16, -155497632); b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640); a = md5hh(a, b, c, d, x[i + 13], 4, 681279174); d = md5hh(d, a, b, c, x[i], 11, -358537222); c = md5hh(c, d, a, b, x[i + 3], 16, -722521979); b = md5hh(b, c, d, a, x[i + 6], 23, 76029189); a = md5hh(a, b, c, d, x[i + 9], 4, -640364487); d = md5hh(d, a, b, c, x[i + 12], 11, -421815835); c = md5hh(c, d, a, b, x[i + 15], 16, 530742520); b = md5hh(b, c, d, a, x[i + 2], 23, -995338651); a = md5ii(a, b, c, d, x[i], 6, -198630844); d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415); c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905); b = md5ii(b, c, d, a, x[i + 5], 21, -57434055); a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571); d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606); c = md5ii(c, d, a, b, x[i + 10], 15, -1051523); b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799); a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359); d = md5ii(d, a, b, c, x[i + 15], 10, -30611744); c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380); b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649); a = md5ii(a, b, c, d, x[i + 4], 6, -145523070); d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379); c = md5ii(c, d, a, b, x[i + 2], 15, 718787259); b = md5ii(b, c, d, a, x[i + 9], 21, -343485551); a = safeAdd(a, olda); b = safeAdd(b, oldb); c = safeAdd(c, oldc); d = safeAdd(d, oldd); } return [a, b, c, d]; } function binl2rstr(input) { let output = ''; for (let i = 0; i < input.length * 32; i += 8) { output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF); } return output; } function rstr2binl(input) { const output = []; output[(input.length >> 2) - 1] = undefined; for (let i = 0; i < output.length; i++) output[i] = 0; for (let i = 0; i < input.length * 8; i += 8) { output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32); } return output; } function rstrMD5(s) { return binl2rstr(binlMD5(rstr2binl(s), s.length * 8)); } function rstr2hex(input) { const hexTab = '0123456789abcdef'; let output = ''; for (let i = 0; i < input.length; i++) { const x = input.charCodeAt(i); output += hexTab.charAt((x >>> 4) & 0x0F) + hexTab.charAt(x & 0x0F); } return output; } function str2rstrUTF8(input) { return unescape(encodeURIComponent(input)); } return function (s) { return rstr2hex(rstrMD5(str2rstrUTF8(s))); }; })(); // ============================================================ // 卡密通验证:工具函数 // ============================================================ const nowTs = () => Math.floor(Date.now() / 1000); function getMachineId() { let id = GM_getValue(MACHINE_ID_KEY, ''); if (!id) { id = 'OUCHN-' + Date.now().toString(36).toUpperCase() + '-' + Math.random().toString(36).slice(2, 10).toUpperCase(); GM_setValue(MACHINE_ID_KEY, id); } return id + 'MAC'; } function verifyResponse(raw) { if (!raw) return null; const signIndex = raw.indexOf('|sign='); if (signIndex === -1) return null; const body = raw.substring(0, signIndex); const sign = raw.substring(signIndex + 6); if (MD5(body + KEYT_SECRET) !== sign) return null; const lastPipe = body.lastIndexOf('|'); if (lastPipe === -1) return null; const serverTs = parseInt(body.substring(lastPipe + 1), 10); if (Math.abs(nowTs() - serverTs) > TIME_TOLERANCE) return null; return body.substring(0, lastPipe); } function gmGet(url) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url, headers: { 'Cache-Control': 'no-cache' }, timeout: 12000, onload: (res) => resolve(res.status === 200 ? res.responseText : null), onerror: () => resolve(null), ontimeout: () => resolve(null) }); }); } async function fetchCardSwitch() { for (let i = 0; i < 3; i++) { const url = `${KEYT_BASE}?act=get_switch&app=${KEYT_APP}&t=${nowTs()}`; const raw = await gmGet(url); if (raw) { const biz = verifyResponse(raw); if (biz && biz.indexOf('CARD_ON') !== -1) return 'CARD_ON'; if (biz && biz.indexOf('CARD_OFF') !== -1) return 'CARD_OFF'; } await new Promise(r => setTimeout(r, 500)); } return 'CARD_ON'; } async function verifyCard(card, mac) { const url = `${KEYT_BASE}?card=${encodeURIComponent(card)}&mac=${encodeURIComponent(mac)}&app=${KEYT_APP}&heart=1&t=${nowTs()}`; const raw = await gmGet(url); return verifyResponse(raw); } function parseRemain(biz) { if (!biz) return null; const parts = biz.split('|'); if (parts.length >= 3) { return { days: parseInt(parts[2], 10) || 0, minutes: parts.length >= 4 ? (parseInt(parts[3], 10) || 0) : 0 }; } if (parts.length === 2 && parts[1] === 'permanent') { return { days: 99999, minutes: 0 }; } return null; } let heartbeatTimer = null; let heartbeatFail = 0; let currentCard = ''; let currentMac = ''; function stopHeartbeat() { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } } function startHeartbeat(panel) { stopHeartbeat(); heartbeatFail = 0; heartbeatTimer = setInterval(async () => { if (!currentCard || !currentMac) return; const biz = await verifyCard(currentCard, currentMac); if (biz && biz.startsWith('ok|')) { heartbeatFail = 0; const info = parseRemain(biz); if (info && panel) { panel.log(`💓 心跳正常 · 剩余 ${info.days}天 ${info.minutes}分钟`, 'info'); } } else { heartbeatFail++; if (panel) panel.log(`⚠️ 心跳异常 ${heartbeatFail}/${HEARTBEAT_MAX_FAIL}`, 'warning'); if (heartbeatFail >= HEARTBEAT_MAX_FAIL) { stopHeartbeat(); if (panel) panel.log('❌ 心跳连续失败,已锁定功能', 'error'); CONFIG.unlocked = false; } } }, HEARTBEAT_MS); } // ============================================================ // LMS API // ============================================================ 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 }; }, 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.user.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.orgName, 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`, {}) .done((data) => resolve(data)) .fail(() => resolve(null)); }); }, 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 }); }); } }; 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 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 = `
🎬 国开学习 v2.0 正式版
⚙️参数设置
视频倍速
自动播放
静音播放
学习间隔 0秒
0s3s5s 10s30s1m 2m
📋运行日志
清空
重复刷课
❤️ 感谢支持

如果这个脚本帮到了你,欢迎捐赠支持作者~

联系方式: QQ:3365137745
⚠️ 开启重复刷课?

重复刷课会将【已学过的活动】全部重新执行一遍:

  • 视频会重新播放
  • 学习记录会重复上报

确定要开启吗?

`; 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.pauseBtn = document.getElementById("pauseBtn"); this.restartBtn = document.getElementById("restartBtn"); 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.activityIntervalRange = document.getElementById("activityIntervalRange"); this.intervalValue = document.getElementById("intervalValue"); this.intervalLabels = document.querySelectorAll("#intervalLabels span"); this.repeatLearnedCheck = document.getElementById("repeatLearnedCheck"); this.donateBtn = document.getElementById("donateBtn"); this.donateModalMask = document.getElementById("donateModalMask"); this.donateModalOk = document.getElementById("donateModalOk"); this.donateModalCancel = document.getElementById("donateModalCancel"); this.repeatModalMask = document.getElementById("repeatModalMask"); this.repeatModalOk = document.getElementById("repeatModalOk"); this.repeatModalCancel = document.getElementById("repeatModalCancel"); this.authCodeInput = document.getElementById("authCodeInput"); this.authCodeTip = document.getElementById("authCodeTip"); this.bindEvents(); } setRunningState(running, paused = false) { if (running) { this.startBtn.disabled = true; this.pauseBtn.disabled = false; this.restartBtn.disabled = false; this.pauseBtn.innerHTML = paused ? '继续' : '暂停'; } else { this.startBtn.disabled = false; this.pauseBtn.disabled = true; this.restartBtn.disabled = true; this.pauseBtn.innerHTML = '暂停'; } } showRepeatModal() { this.repeatModalMask.classList.add('show'); } hideRepeatModal() { this.repeatModalMask.classList.remove('show'); } unlockAll() { CONFIG.unlocked = true; this.activityIntervalRange.disabled = false; this.intervalLabels.forEach(s => s.classList.remove('disabled')); this.repeatLearnedCheck.disabled = false; this.donateBtn.innerHTML = '已解锁'; this.log('🔓 已解锁:学习间隔 / 重复刷课', 'success'); } 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, 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, 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'); }); // ============== 卡密通验证 ============== const resetDonateModal = () => { if (this.authCodeInput) this.authCodeInput.value = ''; if (this.authCodeTip) { this.authCodeTip.textContent = ''; this.authCodeTip.classList.remove('show'); } if (this.authCodeInput) this.authCodeInput.classList.remove('is-error'); }; const showAuthError = (msg) => { if (!this.authCodeInput || !this.authCodeTip) return; this.authCodeInput.classList.add('is-error'); this.authCodeTip.textContent = msg; this.authCodeTip.classList.add('show'); }; const tryUnlock = async () => { if (!this.authCodeInput) return; const code = (this.authCodeInput.value || '').trim(); if (!code) { showAuthError('请输入授权码'); return; } this.donateModalOk.disabled = true; this.donateModalOk.textContent = '验证中...'; const sw = await fetchCardSwitch(); if (sw === 'CARD_OFF') { this.donateModalOk.disabled = false; this.donateModalOk.textContent = '确认'; this.donateModalMask.classList.remove('show'); resetDonateModal(); this.unlockAll(); this.log('🔓 卡密验证已关闭,直接放行', 'success'); return; } const mac = getMachineId(); const biz = await verifyCard(code, mac); this.donateModalOk.disabled = false; this.donateModalOk.textContent = '确认'; if (!biz) { showAuthError('验证失败:签名错误或网络异常'); return; } if (biz.startsWith('ok|')) { const info = parseRemain(biz); currentCard = code; currentMac = mac; GM_setValue(AUTH_CACHE_KEY, { card: code, mac, ts: Date.now() }); this.donateModalMask.classList.remove('show'); resetDonateModal(); this.unlockAll(); if (info) { this.log(`✅ 验证成功 · 剩余 ${info.days}天 ${info.minutes}分钟`, 'success'); } else { this.log('✅ 验证成功(终身有效)', 'success'); } startHeartbeat(this); } else if (biz.startsWith('error|')) { showAuthError(`验证失败:${biz.split('|')[1]}`); } else { showAuthError(`验证失败:${biz}`); } }; this.donateBtn.addEventListener('click', () => { if (CONFIG.unlocked) { this.log('已解锁,无需重复捐赠 ❤️', 'info'); return; } resetDonateModal(); this.donateModalMask.classList.add('show'); setTimeout(() => this.authCodeInput?.focus(), 100); }); this.donateModalCancel.addEventListener('click', () => { this.donateModalMask.classList.remove('show'); resetDonateModal(); }); this.donateModalOk.addEventListener('click', () => { tryUnlock(); }); this.authCodeInput?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); tryUnlock(); } }); this.authCodeInput?.addEventListener('input', () => { this.authCodeInput.classList.remove('is-error'); this.authCodeTip?.classList.remove('show'); }); this.donateModalMask.addEventListener('click', (e) => { if (e.target === this.donateModalMask) { this.donateModalMask.classList.remove('show'); resetDonateModal(); } }); // ============== 重复刷课开关 ============== this.repeatLearnedCheck.checked = CONFIG.repeatLearned; this.repeatLearnedCheck.disabled = true; CONFIG.repeatLearned = false; this.repeatLearnedCheck.addEventListener('change', (e) => { if (!CONFIG.unlocked) return; if (e.target.checked) { e.target.checked = false; this.showRepeatModal(); } else { CONFIG.repeatLearned = false; this.log('重复刷课: 关闭', 'info'); } }); this.repeatModalCancel.addEventListener('click', () => { this.hideRepeatModal(); this.log('重复刷课: 已取消开启', 'warning'); }); this.repeatModalOk.addEventListener('click', () => { this.hideRepeatModal(); this.repeatLearnedCheck.checked = true; CONFIG.repeatLearned = true; this.log('重复刷课: 开启', 'info'); }); this.repeatModalMask.addEventListener('click', (e) => { if (e.target === this.repeatModalMask) { this.hideRepeatModal(); this.log('重复刷课: 已取消开启', 'warning'); } }); // ============== 学习间隔 ============== const updateIntervalUI = () => { const idx = parseInt(this.activityIntervalRange.value, 10) || 0; const step = ACTIVITY_INTERVAL_STEPS[idx]; CONFIG.activityInterval = step.sec; this.intervalValue.textContent = step.label; this.intervalLabels.forEach(s => { s.classList.toggle('active', parseInt(s.dataset.idx, 10) === idx); }); }; this.activityIntervalRange.value = 0; this.activityIntervalRange.disabled = true; this.intervalLabels.forEach(s => s.classList.add('disabled')); this.activityIntervalRange.addEventListener('input', () => { if (!CONFIG.unlocked) return; updateIntervalUI(); }); this.intervalLabels.forEach(span => { span.addEventListener('click', () => { if (!CONFIG.unlocked) return; this.activityIntervalRange.value = span.dataset.idx; updateIntervalUI(); }); }); updateIntervalUI(); } 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.setRunningState(true, false); }); } onPause(callback) { this.pauseBtn.addEventListener('click', () => callback()); } onRestart(callback) { this.restartBtn.addEventListener('click', () => callback()); } } 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() { try { const $btn = $('button, a').filter((i, el) => { const text = $(el).text().trim().replace(/\s+/g, ''); return text === '下一个' || text === '下一个>'; }); if ($btn.length > 0) return $btn[0]; } catch (e) {} 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; } } 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.isPaused = false; this.totalActivities = 0; this.completedActivities = 0; } async waitIfPaused() { while (this.isPaused && this.isRunning) await delay(300); } pause() { if (!this.isRunning || this.isPaused) return; this.isPaused = true; this.panel.setRunningState(true, true); this.panel.log('⏸ 已暂停,点击"继续"恢复', 'warning'); } resume() { if (!this.isRunning || !this.isPaused) return; this.isPaused = false; this.panel.setRunningState(true, false); this.panel.log('▶ 已恢复运行', 'info'); } restart() { this.panel.log('🔄 正在重新开始…', 'warning'); this.isRunning = false; this.isPaused = false; setTimeout(() => { this.completedActivities = 0; this.totalActivities = 0; this.panel.setProgress(0); this.panel.startBtn.innerHTML = '刷课中'; this.panel.setRunningState(true, false); this.start(); }, 400); } async start() { this.isRunning = true; this.isPaused = false; this.completedActivities = 0; this.totalActivities = 0; this.panel.log('===== 智能刷课开始 =====', 'info'); this.panel.log(`当前配置: 倍速=${CONFIG.videoSpeed}x, 自动播放=${CONFIG.autoPlayVideo ? '开启' : '关闭'}, 自动下一个=${CONFIG.autoNext ? '开启' : '关闭'}, 重复学习=${CONFIG.repeatLearned ? '开启' : '关闭'}, 学习间隔=${CONFIG.activityInterval}秒(视频除外)`, '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; this.isPaused = false; this.panel.setRunningState(false); this.panel.startBtn.innerHTML = '🚀开始'; 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'); }, CONFIG.nextDelay); } } 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 this.waitIfPaused(); 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; await this.waitIfPaused(); const { title, id, type } = activity; const isCompleted = completedActivities.includes(parseInt(id)); if (isCompleted && !CONFIG.repeatLearned) { this.panel.log(`[跳过] ${title} (${notificationTypes[type] || type})`, 'warning'); this.completedActivities++; this.updateProgress(); continue; } this.panel.log( isCompleted ? `[已学过·重刷] ${title} (${notificationTypes[type] || type})` : `开始处理: ${title}`, 'info' ); try { await this.handleActivity(activity, isCompleted && CONFIG.repeatLearned); this.panel.log(`✅ 完成: ${title}`, 'success'); } catch (err) { this.panel.log(`❌ 失败: ${title} - ${err.message}`, 'error'); } this.completedActivities++; this.updateProgress(); if (type !== 'online_video' && CONFIG.activityInterval > 0) { this.panel.log(`⏱️ 学习间隔等待 ${CONFIG.activityInterval} 秒...`, 'info'); await delay(CONFIG.activityInterval * 1000); } 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, isRepeat = false) { const { id, title, type, is_open, uploads, syllabus_id } = activity; if (isRepeat && REPEAT_SKIP_TYPES.includes(type)) { this.panel.log(`ℹ️ 重刷模式:跳过${notificationTypes[type] || type} (${title})`, 'info'); return; } 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); const categoryId = categoryData?.topic_category?.id; if (categoryId) await API.postForum(categoryId); else this.panel.log(`⚠️ 讨论区未获取到 category_id: ${title}`, 'warning'); 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; const videoUploads = Array.isArray(uploads) ? uploads : []; 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 (!this.isRunning) { try { video.pause(); } catch (e) {} return; } await this.waitIfPaused(); 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 (videoUploads[0]?.videos?.[0]?.duration) actualDuration = videoUploads[0].videos[0].duration; } } else { if (videoUploads[0]?.videos?.[0]?.duration) actualDuration = videoUploads[0].videos[0].duration; panel.log(`📺 跳过自动播放,使用时长: ${actualDuration}秒`, 'info'); } if (!this.isRunning) return; for (const upload of videoUploads) { 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, reject) => { $.get(`https://lms.ouchn.cn/api/course/${courseId}/my-completeness`) .done((data) => resolve(data)) .fail((xhr, status, err) => reject(new Error(`my-completeness 请求失败: ${status || err}`))); }); } getModules(courseId) { return new Promise((resolve, reject) => { $.get(`https://lms.ouchn.cn/api/courses/${courseId}/modules`) .done((data) => resolve(data)) .fail((xhr, status, err) => reject(new Error(`modules 请求失败: ${status || err}`))); }); } getModuleActivities(courseId, moduleId) { return new Promise((resolve, reject) => { $.get(`https://lms.ouchn.cn/api/course/${courseId}/all-activities?module_ids=[${moduleId}]&activity_types=learning_activities,exams,classrooms`) .done((data) => resolve(data)) .fail((xhr, status, err) => reject(new Error(`all-activities 请求失败: ${status || err}`))); }); } } 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) { setTimeout(init, 2000); return; } const courseId = courseIdInput.value; if (!courseId) { setTimeout(init, 2000); return; } if (currentCourseId === courseId && currentAutoLearn && document.getElementById('autoLearnPanel')) { return; } currentCourseId = courseId; currentAutoLearn = new CourseAutoLearn(courseId); currentAutoLearn.panel.log(`🎓 课程ID: ${courseId}`, 'info'); currentAutoLearn.panel.log(`💡 点击"开始"按钮开始自动学习;首次使用请点"捐赠"`, 'info'); currentAutoLearn.panel.onStart(() => { currentAutoLearn.start(); }); currentAutoLearn.panel.onPause(() => { if (currentAutoLearn.isPaused) currentAutoLearn.resume(); else currentAutoLearn.pause(); }); currentAutoLearn.panel.onRestart(() => { currentAutoLearn.restart(); }); const cached = GM_getValue(AUTH_CACHE_KEY, null); if (cached && cached.card && cached.mac) { currentCard = cached.card; currentMac = cached.mac; currentAutoLearn.unlockAll(); currentAutoLearn.panel.log('✅ 已用缓存卡密自动解锁,正在恢复心跳…', 'success'); startHeartbeat(currentAutoLearn.panel); } } let lastUrl = location.href; function handleUrlChange(newUrl) { console.log(`[刷课脚本] 页面变化: ${lastUrl} -> ${newUrl}`); lastUrl = newUrl; setTimeout(init, 1500); } const originalPushState = history.pushState; history.pushState = function () { originalPushState.apply(this, arguments); if (location.href !== lastUrl) handleUrlChange(location.href); }; const originalReplaceState = history.replaceState; history.replaceState = function () { originalReplaceState.apply(this, arguments); if (location.href !== lastUrl) handleUrlChange(location.href); }; window.addEventListener('popstate', () => { if (location.href !== lastUrl) handleUrlChange(location.href); }); setInterval(() => { if (location.href !== lastUrl) { handleUrlChange(location.href); return; } const courseIdInput = document.querySelector("#courseId"); const liveCourseId = courseIdInput?.value; if (liveCourseId && currentCourseId && liveCourseId !== currentCourseId) { setTimeout(init, 1500); return; } if (!document.getElementById('autoLearnPanel') && liveCourseId) { init(); } }, 2000); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();