// ==UserScript== // @name 中国大学MOOC 网课助手 // @namespace https://icourse163.org/ // @version 2.0.0 // @description 中国大学MOOC(慕课) 全功能助手:自动播放视频、解锁倍速限制、自动完成任务点、自动签到、自动答题 // @author 网课助手 // @match *://www.icourse163.org/* // @match *://icourse163.org/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_notification // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; // ==================== 用户配置区 ==================== const CONFIG = { // 视频倍速 (1 ~ 16,推荐 2 或 3) playRate: GM_getValue('playRate', 2), // 是否自动播放下一节 autoNext: GM_getValue('autoNext', true), // 是否自动完成课件/讨论任务点 autoDoc: GM_getValue('autoDoc', true), autoDiscuss: GM_getValue('autoDiscuss', true), // 是否自动答题(随堂测验) autoAnswer: GM_getValue('autoAnswer', true), // 答题延时 ms(模拟真人操作,不要设太低) answerDelay: GM_getValue('answerDelay', 800), // 是否静音(倍速刷课时推荐开启) mute: GM_getValue('mute', false), }; // ==================== 工具函数 ==================== const utils = { /** 获取 CSRF Token(从 cookie 中读取) */ getCsrf() { const match = document.cookie.match(/NTESSTUDYSI=([^;]+)/); return match ? decodeURIComponent(match[1]) : null; }, /** 等待元素出现 */ waitForElement(selector, timeout = 10000) { return new Promise((resolve, reject) => { const el = document.querySelector(selector); if (el) return resolve(el); const observer = new MutationObserver(() => { const found = document.querySelector(selector); if (found) { observer.disconnect(); resolve(found); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`等待元素超时: ${selector}`)); }, timeout); }); }, /** 延时 */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }, /** 向页面发送 POST 请求 */ async post(url, body, headers = {}) { const csrf = this.getCsrf(); const defaultHeaders = { 'content-type': 'application/x-www-form-urlencoded', 'edu-script-token': csrf || '', }; const res = await fetch(url, { method: 'POST', headers: { ...defaultHeaders, ...headers }, body, credentials: 'include', }); return res.json().catch(() => ({})); }, /** 日志 */ log(msg, type = 'info') { const colors = { info: '#4CAF50', warn: '#FF9800', error: '#F44336' }; console.log(`%c[MOOC助手] ${msg}`, `color:${colors[type] || '#4CAF50'};font-weight:bold;`); }, }; // ==================== 视频控制模块 ==================== const videoController = { video: null, timer: null, /** 初始化,监听视频元素 */ init() { this.timer = setInterval(() => this.attach(), 1500); }, /** 绑定到当前视频元素 */ attach() { const video = document.querySelector('video'); if (!video || video === this.video) return; this.video = video; utils.log(`检测到视频元素,设置倍速 ×${CONFIG.playRate}`); // 1. 设置倍速 this.setPlayRate(CONFIG.playRate); // 2. 静音设置 if (CONFIG.mute) video.muted = true; // 3. 覆盖 playbackRate setter,防止被平台重置 this.lockPlayRate(video); // 4. 视频暂停时自动恢复(处理随堂测验弹窗) video.addEventListener('pause', () => this.onPause(video), true); // 5. 视频结束时跳转下一节 video.addEventListener('ended', () => this.onEnded(), true); // 6. 定时刷新倍速(防止被平台重置) setInterval(() => { if (this.video && this.video.playbackRate !== CONFIG.playRate) { this.video.playbackRate = CONFIG.playRate; } }, 2000); }, /** 设置倍速 */ setPlayRate(rate) { if (!this.video) return; try { this.video.playbackRate = rate; } catch (e) { utils.log('设置倍速失败: ' + e.message, 'warn'); } }, /** 劫持 playbackRate 属性,防止平台将其重置 */ lockPlayRate(video) { const descriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate'); if (!descriptor) return; const originalSet = descriptor.set; Object.defineProperty(video, 'playbackRate', { get: descriptor.get, set(val) { // 只允许设置 >= CONFIG.playRate 的值,忽略低倍速设置 if (val < CONFIG.playRate) { originalSet.call(this, CONFIG.playRate); } else { originalSet.call(this, val); } }, configurable: true, }); }, /** 视频暂停处理(关闭弹窗后恢复播放) */ onPause(video) { setTimeout(() => { // 关闭随堂测验弹窗 const mask = document.querySelector('div.j-insetCt'); if (mask) { const parent = mask.parentElement; if (parent) parent.remove(); utils.log('关闭随堂测验弹窗,恢复播放'); } // 尝试恢复播放 if (video.paused) { video.play().catch(() => {}); } }, 500); }, /** 视频播放结束,自动跳下一节 */ onEnded() { if (!CONFIG.autoNext) return; utils.log('视频播放结束,准备跳转下一节...'); setTimeout(() => courseController.goNext(), 2000); }, }; // ==================== 课程任务点模块 ==================== const courseController = { /** 自动完成课件任务点 */ async finishDoc(unitId) { const csrf = utils.getCsrf(); if (!csrf || !unitId) return; utils.log(`自动完成课件任务点: ${unitId}`); // 发送1页到末页的学习记录 for (let page = 1; page <= 3; page++) { await utils.post( `/web/j/courseBean.saveMocContentLearn.rpc?csrfKey=${csrf}`, `dto={"unitId":${unitId},"pageNum":${page},"finished":${page >= 3},"contentType":3}` ); await utils.sleep(500); } }, /** 自动完成讨论任务点(标记已读) */ async finishDiscuss(unitId) { const csrf = utils.getCsrf(); if (!csrf || !unitId) return; utils.log(`自动完成讨论任务点: ${unitId}`); await utils.post( `/web/j/courseBean.saveMocContentLearn.rpc?csrfKey=${csrf}`, `dto={"unitId":${unitId},"pageNum":1,"finished":true,"contentType":5}` ); }, /** 跳转到下一节内容 */ goNext() { // 优先找"下一节"按钮 const nextSelectors = [ 'a.j-next', 'button.j-next', '[class*="next"]', 'a[data-next]', '.u-btn-next', ]; for (const sel of nextSelectors) { const btn = document.querySelector(sel); if (btn) { utils.log(`点击下一节按钮: ${sel}`); btn.click(); return; } } utils.log('未找到下一节按钮', 'warn'); }, /** 获取当前 unitId */ getCurrentUnitId() { const match = location.href.match(/contentId=(\d+)/); return match ? match[1] : null; }, /** 获取当前页面类型 */ getPageType() { const url = location.href; if (url.includes('video')) return 'video'; if (url.includes('doc') || url.includes('pdf')) return 'doc'; if (url.includes('discuss')) return 'discuss'; if (url.includes('test') || url.includes('quiz')) return 'test'; // 根据 DOM 特征判断 if (document.querySelector('video')) return 'video'; if (document.querySelector('.j-quizContainer, .f-quiz')) return 'test'; if (document.querySelector('.j-docContainer, iframe.doc-iframe')) return 'doc'; return 'unknown'; }, }; // ==================== 自动答题模块 ==================== const autoAnswer = { running: false, async run() { if (this.running || !CONFIG.autoAnswer) return; this.running = true; utils.log('开始自动答题...'); await utils.sleep(2000); // 获取所有题目 const questions = document.querySelectorAll('.j-question, .f-question, [class*="question"]'); if (!questions.length) { utils.log('未发现题目', 'warn'); this.running = false; return; } for (const q of questions) { await this.answerQuestion(q); await utils.sleep(CONFIG.answerDelay); } // 尝试提交 await utils.sleep(1000); this.submit(); this.running = false; }, /** 回答单道题目(选择题优先选第一项,简答题填写默认内容) */ async answerQuestion(qEl) { // 单选/多选题 const radios = qEl.querySelectorAll('input[type="radio"]'); const checkboxes = qEl.querySelectorAll('input[type="checkbox"]'); const textareas = qEl.querySelectorAll('textarea, input[type="text"]'); if (radios.length > 0) { // 选第一个未选中的选项 const unselected = Array.from(radios).find(r => !r.checked); if (unselected) { unselected.click(); utils.log('单选题:选择选项'); } } else if (checkboxes.length > 0) { // 多选题:勾选第一个 const first = checkboxes[0]; if (!first.checked) { first.click(); } } else if (textareas.length > 0) { // 文本题:填写占位内容 textareas[0].value = '自动填写'; textareas[0].dispatchEvent(new Event('input', { bubbles: true })); textareas[0].dispatchEvent(new Event('change', { bubbles: true })); } }, /** 提交答卷 */ submit() { const submitSelectors = [ 'button.j-submit', 'button[type="submit"]', 'a.j-submit', '[class*="submit"]', 'button.u-btn-sure', ]; for (const sel of submitSelectors) { const btn = document.querySelector(sel); if (btn) { utils.log(`提交答题: ${sel}`); btn.click(); return; } } }, }; // ==================== 签到模块 ==================== const signIn = { /** 检测并自动签到 */ async check() { await utils.sleep(3000); // 检查是否有签到弹窗 const signSelectors = [ 'button.j-sign', '[class*="sign"]', 'button[data-sign]', '.sign-btn', 'a.sign', ]; for (const sel of signSelectors) { const btn = document.querySelector(sel); if (btn && btn.offsetParent !== null) { utils.log(`检测到签到按钮,自动签到: ${sel}`); btn.click(); this.showNotice('已自动签到'); return; } } }, showNotice(msg) { try { GM_notification({ title: '🎓 MOOC助手', text: msg, timeout: 3000, }); } catch (e) {} }, }; // ==================== 控制面板 UI ==================== const panel = { el: null, init() { GM_addStyle(` #mooc-panel { position: fixed; top: 80px; right: 16px; z-index: 99999; background: rgba(20, 20, 30, 0.92); backdrop-filter: blur(8px); color: #fff; border-radius: 12px; padding: 14px 16px; min-width: 200px; font-size: 13px; font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; box-shadow: 0 4px 24px rgba(0,0,0,0.35); user-select: none; border: 1px solid rgba(255,255,255,0.1); transition: all 0.3s; } #mooc-panel .mp-title { font-size: 14px; font-weight: 700; color: #6ee7b7; margin-bottom: 10px; display: flex; align-items: center; gap: 6px; cursor: pointer; } #mooc-panel .mp-title span.badge { font-size: 10px; background: #6ee7b7; color: #000; border-radius: 4px; padding: 1px 5px; } #mooc-panel .mp-body { display: flex; flex-direction: column; gap: 7px; } #mooc-panel .mp-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; } #mooc-panel .mp-label { color: #ccc; } #mooc-panel .mp-toggle { width: 36px; height: 18px; background: #555; border-radius: 9px; position: relative; cursor: pointer; transition: background 0.2s; flex-shrink: 0; } #mooc-panel .mp-toggle.on { background: #6ee7b7; } #mooc-panel .mp-toggle::after { content: ''; width: 14px; height: 14px; background: #fff; border-radius: 50%; position: absolute; top: 2px; left: 2px; transition: left 0.2s; } #mooc-panel .mp-toggle.on::after { left: 20px; } #mooc-panel .mp-rate { display: flex; align-items: center; gap: 4px; } #mooc-panel .mp-rate-btn { background: rgba(255,255,255,0.1); border: none; color: #fff; border-radius: 4px; padding: 2px 8px; cursor: pointer; font-size: 12px; transition: background 0.2s; } #mooc-panel .mp-rate-btn:hover { background: rgba(255,255,255,0.25); } #mooc-panel .mp-rate-val { color: #6ee7b7; font-weight: 700; min-width: 28px; text-align: center; } #mooc-panel .mp-divider { height: 1px; background: rgba(255,255,255,0.1); margin: 4px 0; } #mooc-panel .mp-btn { background: linear-gradient(135deg, #6ee7b7, #3b82f6); border: none; color: #000; font-weight: 700; border-radius: 6px; padding: 5px 0; cursor: pointer; font-size: 12px; width: 100%; transition: opacity 0.2s; } #mooc-panel .mp-btn:hover { opacity: 0.85; } #mooc-panel .mp-status { color: #6ee7b7; font-size: 11px; text-align: center; min-height: 14px; } #mooc-panel.collapsed .mp-body { display: none; } `); const div = document.createElement('div'); div.id = 'mooc-panel'; div.innerHTML = `