// ==UserScript==
// @name 优学院DGUT版
// @version 1.6
// @description 适配DGUT优学院(自动静音播放、自动做练习题、自动翻页、修改播放速率)
// @author Linus
// @match https://ua.dgut.edu.cn/learnCourse/learnCourse.html*
// @icon https://lms.dgut.edu.cn/ulearning/favicon.ico
// @grant GM_xmlhttpRequest
// @license MIT
// @namespace https://greasyfork.org/users/1540778
// @downloadURL https://update.greasyfork.icu/scripts/556678/%E4%BC%98%E5%AD%A6%E9%99%A2DGUT%E7%89%88.user.js
// @updateURL https://update.greasyfork.icu/scripts/556678/%E4%BC%98%E5%AD%A6%E9%99%A2DGUT%E7%89%88.meta.js
// ==/UserScript==
(function () {
'use strict';
/* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* 优学院自动静音播放、自动做练习题、自动翻页、修改播放速率脚本(适配东莞理工学院)
* 重要提醒:使用风险自负,避免高倍速/长时间挂机,建议非核心课程使用,仅限个人学习使用,禁止商用
* 基于作者Brush-JIM的脚本“优学院自动静音播放、自动做练习题、自动翻页、修改播放速率(改)”
* 和作者 luluzzy. 的脚本“DGUT Ulearning Tool”(MIT协议)二次开发重构
* 原脚本链接:https://greasyfork.org/zh-CN/scripts/555722-dgut-ulearning-tool
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*/
// 配置管理中心
const AppConfig = {
playbackRate: 1.5,
autoPlay: true,
autoMute: true,
autoAdjustRate: true,
autoFillAnswers: true,
showAnswers: true,
autoAnswerSingle: true,
autoAnswerMulti: true,
autoAnswerJudge: true,
autoAnswerBlank: true,
maxRetryCount: 7,
getStorageKey: (key) => `ulearn_${key}`,
load: function () {
const prefix = 'ulearn_';
Object.keys(this).forEach(key => {
if (typeof this[key] !== 'function' && key !== 'maxRetryCount') {
const stored = localStorage.getItem(prefix + key);
if (stored !== null) {
this[key] = stored === 'true' || (stored !== 'false' && stored);
}
}
});
},
save: function () {
const prefix = 'ulearn_';
Object.keys(this).forEach(key => {
if (typeof this[key] !== 'function' && key !== 'maxRetryCount') {
localStorage.setItem(prefix + key, this[key]);
}
});
}
};
// 状态管理
const AppState = {
isPaused: false,
isRestarting: false,
isNavigating: false,
answerInProgress: false,
modalChecking: false,
nextPageRetry: 0,
reset: function () {
this.nextPageRetry = 0;
this.isNavigating = false;
}
};
// 日志工具
const Logger = {
element: null,
init: function (el) {
this.element = el;
},
log: function (message) {
const timestamp = new Date().toLocaleTimeString();
const logMsg = `[${timestamp}] ${message}`;
console.log(`DGUT助手: ${logMsg}`);
if (this.element) {
this.element.innerHTML += `${logMsg}
`;
this.element.scrollTop = this.element.scrollHeight;
}
}
};
// 答案处理服务
class AnswerService {
getQuestionType(questionEl) {
const typeTag = questionEl.querySelector('.question-type-tag');
if (!typeTag) return null;
const typeText = typeTag.textContent.trim();
if (typeText.includes('单选题')) return 'single';
if (typeText.includes('多选题')) return 'multiple';
if (typeText.includes('判断题')) return 'judge';
if (typeText.includes('填空题')) return 'blank';
return null;
}
processQuestion(questionId, answers) {
if (AppState.isPaused || AppState.isRestarting) return;
const questionEl = document.querySelector(`#question${questionId}`);
if (!questionEl) {
Logger.log(`未找到问题容器: ${questionId}`);
return;
}
const type = this.getQuestionType(questionEl);
if (!type) {
Logger.log(`无法识别题型: ${questionId}`);
return;
}
const handlers = {
single: () => this.handleChoice(questionEl, answers),
multiple: () => this.handleChoice(questionEl, answers),
judge: () => this.handleJudge(questionEl, answers),
blank: () => this.handleBlank(questionEl, answers)
};
if (handlers[type]) {
handlers[type]();
} else {
Logger.log(`不支持的题型: ${type}`);
}
}
handleChoice(questionEl, answers) {
const options = questionEl.querySelectorAll('.choice-item, .option-item, .question-option');
if (!options.length) {
Logger.log(`选择题选项未找到: ${questionEl.id}`);
return;
}
options.forEach(option => {
const optionLabel = option.querySelector('.option')?.textContent?.trim().replace('.', '') ||
option.querySelector('.option-letter')?.textContent?.trim() ||
option.querySelector('span:first-child')?.textContent?.trim().replace('.', '');
if (optionLabel && answers.includes(optionLabel)) {
const selector = option.querySelector('.checkbox, .option-checkbox, .radio');
if (selector && !selector.classList.contains('selected')) {
option.click();
if (!selector.classList.contains('selected')) {
selector.click();
}
Logger.log(`选中选项: ${optionLabel}`);
}
}
});
}
handleJudge(questionEl, answers) {
const isCorrect = String(answers) === 'true';
const btnSelector = isCorrect ? '.right-btn' : '.wrong-btn';
const judgeBtn = questionEl.querySelector(btnSelector);
if (judgeBtn && !judgeBtn.classList.contains('selected')) {
judgeBtn.click();
Logger.log(`判断题选择: ${isCorrect ? '正确' : '错误'}`);
}
}
handleBlank(questionEl, answers) {
const inputs = questionEl.querySelectorAll('textarea, .blank-input');
answers.forEach((ans, idx) => {
if (inputs[idx]) {
const cleanedAns = this.cleanHtml(this.escapeHtml(ans));
inputs[idx].value = cleanedAns;
$(inputs[idx]).trigger('change');
}
});
Logger.log(`填空题已填充: ${answers.join('; ')}`);
}
escapeHtml(str) {
const entities = { 'lt': '<', 'gt': '>', 'nbsp': ' ', 'amp': '&', 'quot': '"' };
return str.replace(/&(lt|gt|nbsp|amp|quot);/ig, (_, key) => entities[key]);
}
cleanHtml(str) {
return str.replace(/(<[^>]+>|\\n|\\r)/g, ' ');
}
}
// 视频控制服务
class VideoController {
constructor() {
this.observer = null;
}
init() {
this.setupVideoMonitoring();
}
setupVideoMonitoring() {
this.observer = new MutationObserver((mutations) => {
if (!AppState.isPaused && !AppState.isRestarting) {
this.processVideos();
this.checkModals();
}
});
this.observer.observe(document.body, { childList: true, subtree: true });
}
processVideos(slept = false) {
if (AppState.isPaused || AppState.isRestarting || !AppConfig.autoPlay) return;
if (AppState.answerInProgress) {
setTimeout(() => this.processVideos(true), 1000);
return;
}
if (!slept) {
setTimeout(() => this.processVideos(true), 3000);
return;
}
const video = document.querySelector("video, mediaelementwrapper video:first-child");
if (video) {
video.playbackRate = AppConfig.playbackRate;
if (AppConfig.autoMute && !video.muted) {
video.muted = true;
Logger.log("视频已静音");
}
}
const videoWrappers = $('mediaelementwrapper video:first-child');
const statusIndicators = $('.video-bottom span:first-child');
if (videoWrappers.length === 0) {
PageNavigator.goNext();
return;
}
if (videoWrappers.length !== statusIndicators.length) {
PageNavigator.goNext();
return;
}
const videoStates = [];
$(videoWrappers).each((idx, el) => {
const state = $(statusIndicators[idx]).attr('data-bind');
videoStates.push({
element: el,
completed: state === 'text: $root.i18nMessageText().finished',
currentTime: 0
});
});
videoStates.forEach((state, idx) => {
state.element.addEventListener('ended', () => {
videoStates[idx].completed = true;
Logger.log("视频播放完成");
PageNavigator.goNext();
}, { once: true });
});
this.controlPlayback(videoStates);
}
controlPlayback(videoStates) {
if (AppState.isPaused || AppState.isRestarting) return;
if (videoStates.length !== $('mediaelementwrapper video:first-child').length) {
this.processVideos();
return;
}
videoStates.forEach(state => {
state.element.playbackRate = AppConfig.playbackRate;
});
for (let i = 0; i < videoStates.length; i++) {
if (videoStates[i].element !== $('mediaelementwrapper video:first-child')[i]) {
this.processVideos();
return;
}
if (!videoStates[i].completed) {
const targetVideo = (i > 0 && !videoStates[i - 1].completed) ? videoStates[i - 1] : videoStates[i];
if (targetVideo.element.paused || targetVideo.currentTime === targetVideo.element.currentTime) {
targetVideo.element.currentTime = Math.max(0, targetVideo.element.currentTime - 3);
targetVideo.element.play().catch(err => {
Logger.log(`播放失败: ${err.message}`);
AppRestarter.restart();
});
}
targetVideo.currentTime = targetVideo.element.currentTime;
if (AppConfig.autoMute && !targetVideo.element.muted) {
targetVideo.element.muted = true;
}
if (AppConfig.autoAdjustRate && targetVideo.element.playbackRate !== AppConfig.playbackRate) {
targetVideo.element.playbackRate = AppConfig.playbackRate;
}
setTimeout(() => this.controlPlayback(videoStates), 500);
return;
}
}
PageNavigator.goNext();
}
checkModals(slept = false) {
if (AppState.isPaused || AppState.isRestarting || AppState.answerInProgress) return;
if (!slept) {
setTimeout(() => this.checkModals(true), 2000);
return;
}
AppState.modalChecking = true;
const questionPanel = $('.question-wrapper');
if (questionPanel.length > 0 && AppConfig.autoFillAnswers) {
AnswerProcessor.processQuiz();
AppState.modalChecking = false;
return;
}
const statModal = $('#statModal');
if (statModal.length > 0) {
const buttons = statModal[0].getElementsByTagName('button');
if (buttons.length >= 2) buttons[1].click();
}
const errorIndicator = $('.mobile-video-error');
if (errorIndicator && errorIndicator.css('display') !== 'none') {
$('.try-again').click();
Logger.log("检测到视频错误,已尝试重试");
}
const alertModal = document.getElementById('alertModal');
if (alertModal && alertModal.className.includes('in')) {
const operations = $('.modal-operation').children();
if (operations.length >= 2) {
operations[AppConfig.autoFillAnswers ? 0 : 1].click();
} else {
const continueBtn = $('.btn-submit');
continueBtn.each((_, btn) => {
if ($(btn).text() !== '提交') $(btn).click();
});
}
if (AppConfig.autoFillAnswers) AnswerProcessor.processQuiz();
}
AppState.modalChecking = false;
}
}
// 页面导航器
const PageNavigator = {
goNext: function () {
if (AppState.isNavigating || AppState.isPaused || AppState.isRestarting ||
AppState.answerInProgress || !AppConfig.autoPlay || AppState.modalChecking) {
return;
}
Logger.log("尝试导航至下一页");
const nextButtons = $('.mobile-next-page-btn, .next-btn, .btn-next, .nextVideoBtn');
if (nextButtons.length === 0) {
AppState.nextPageRetry++;
Logger.log(`未找到下一页按钮(${AppState.nextPageRetry}/${AppConfig.maxRetryCount})`);
if (AppState.nextPageRetry >= AppConfig.maxRetryCount) {
AppState.isPaused = true;
document.getElementById('toggleScript').innerText = '▶️ 继续运行';
document.getElementById('toggleScript').style.backgroundColor = 'rgba(46, 204, 113, 0.5)';
Logger.log(`连续${AppConfig.maxRetryCount}次未找到下一页,已暂停`);
}
return;
}
AppState.nextPageRetry = 0;
AppState.isNavigating = true;
Logger.log("锁定导航状态,防止重复操作");
nextButtons.each((_, btn) => {
if (!$(btn).hasClass('disabled')) {
btn.click();
Logger.log("已点击下一页按钮");
}
});
setTimeout(() => {
Logger.log("导航完成,解除锁定");
AppState.isNavigating = false;
setTimeout(() => {
if (!AppState.isPaused && !AppState.isRestarting) {
videoController.processVideos();
videoController.checkModals();
}
}, 1000);
}, 3000);
}
};
// 答案处理器
const AnswerProcessor = {
processQuiz: function () {
if (AppState.isPaused || AppState.isRestarting || AppState.answerInProgress || !AppConfig.autoFillAnswers) {
return;
}
AppState.answerInProgress = true;
Logger.log("检测到测验页面,开始处理答案");
let questionIds = [];
const questionPanels = $('.question-wrapper');
const pageItems = $('.page-item');
const waitForQuestions = setInterval(() => {
const currentPanels = $('.question-wrapper');
if (currentPanels.length > 0) {
clearInterval(waitForQuestions);
currentPanels.each((_, panel) => {
const id = $(panel).attr('id');
if (id && id.startsWith('question')) {
questionIds.push(id.replace('question', ''));
} else {
Logger.log("发现无效问题ID,已跳过");
}
});
questionIds = [...new Set(questionIds)];
Logger.log(`共检测到 ${questionIds.length} 道题目`);
let pageId = '';
let found = false;
pageItems.each((_, item) => {
if (found) return;
const pageName = $(item).find('.page-name');
if (pageName.length > 0 && pageName[0].className.includes('active')) {
const idAttr = $(item).attr('id');
pageId = idAttr.slice(idAttr.search(/\d/g));
found = true;
}
});
if (!found) {
AppState.answerInProgress = false;
PageNavigator.goNext();
return;
}
if (questionIds.length === 0) {
Logger.log("未发现有效题目,跳转至下一页");
AppState.answerInProgress = false;
PageNavigator.goNext();
return;
}
const answerService = new AnswerService();
const total = questionIds.length;
let processed = 0;
const processNext = (index) => {
if (index >= total) {
Logger.log(`所有 ${total} 道题目处理完毕`);
setTimeout(() => {
if (AppConfig.autoPlay) {
$('textarea, .blank-input').trigger('change');
const submitBtn = $('.btn-submit');
if (submitBtn.length > 0) {
submitBtn.click();
Logger.log("已提交答案");
}
const videos = $('video').filter((_, v) => v.src !== "");
if (videos.length === 0) {
AppState.answerInProgress = false;
PageNavigator.goNext();
return;
}
}
AppState.answerInProgress = false;
}, 1000);
return;
}
const qId = questionIds[index];
Logger.log(`处理第 ${index + 1}/${total} 题 (ID: ${qId})`);
this.fetchAnswer(qId, pageId, answerService, () => {
processed++;
Logger.log(`第 ${index + 1} 题处理完成 (${processed}/${total})`);
processNext(index + 1);
});
};
processNext(0);
}
}, 500);
setTimeout(() => {
clearInterval(waitForQuestions);
if (questionIds.length === 0) {
Logger.log("超时未检测到题目,跳转至下一页");
AppState.answerInProgress = false;
PageNavigator.goNext();
}
}, 5000);
},
fetchAnswer: function (questionId, parentId, answerService, callback) {
if (AppState.isPaused || AppState.isRestarting) {
callback();
return;
}
const auth = this.getAuthorization();
if (!auth) {
Logger.log("获取认证信息失败,无法请求答案");
callback();
return;
}
GM_xmlhttpRequest({
method: "GET",
url: `https://ua.dgut.edu.cn/uaapi/questionAnswer/${questionId}?parentId=${parentId}`,
headers: {
"UA-AUTHORIZATION": auth,
"X-Requested-With": "XMLHttpRequest",
"Referer": window.location.href
},
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
const answers = data.correctAnswerList || data.answer || [];
Logger.log(`题目 ${questionId} 答案: ${answers}`);
if (answers.length > 0) {
answerService.processQuestion(questionId, answers);
} else {
Logger.log(`题目 ${questionId} 未找到答案`);
}
} catch (err) {
Logger.log(`解析答案失败: ${err.message}`);
console.error("答案解析错误:", err);
} finally {
callback();
}
},
onerror: (err) => {
Logger.log(`请求答案失败: ${err.message}`);
console.error("答案请求错误:", err);
callback();
}
});
},
getAuthorization: function () {
return document.cookie.split(";")
.map(c => c.trim().split("="))
.find(([k]) => k === "AUTHORIZATION")?.[1] || "";
}
};
// 应用重启器
const AppRestarter = {
restart: function () {
if (AppState.isRestarting) return;
AppState.isRestarting = true;
Logger.log("检测到异常,尝试重启服务...");
const toggleBtn = document.getElementById('toggleScript');
if (!AppState.isPaused) {
toggleBtn.click();
}
setTimeout(() => {
Logger.log("重启中,恢复服务...");
toggleBtn.click();
setTimeout(() => {
AppState.isRestarting = false;
Logger.log("服务重启完成");
}, 1000);
}, 2000);
}
};
// UI组件
class UIController {
constructor() {
this.panel = null;
}
render() {
this.loadStyles();
this.createPanel();
this.bindEvents();
AppConfig.load();
this.syncConfigToUI();
}
loadStyles() {
const style = document.createElement('style');
style.textContent = `
.ulearn-panel {position:fixed;top:100px;right:30px;z-index:999999;background:rgba(0,0,0,0.7);color:#fff;padding:10px 12px;border-radius:8px;font-size:14px;width:320px;cursor:move}
.ulearn-panel:hover {opacity:0.95}
.drag-handle {cursor:move;padding:5px;text-align:center;background:rgba(255,255,255,0.1);border-radius:4px;margin-bottom:8px}
.panel-title {text-align:center;margin:5px 0;font-weight:bold}
.control-btn {margin:4px;padding:3px 8px;cursor:pointer;border:none;border-radius:3px;background:rgba(255,255,255,0.2);color:white}
.control-btn:hover {background:rgba(255,255,255,0.3)}
.log-container {height:150px;overflow:auto;background:#111;padding:4px;border-radius:4px;font-size:12px;line-height:1.5;margin-top:8px}
.setting-group {padding-left:15px;margin:5px 0}
.setting-item {position:relative;list-style:none;margin:5px 0}
.setting-input {position:absolute;right:5px}
.section-title {font-weight:bold;margin-top:10px;margin-bottom:5px;padding-bottom:3px;border-bottom:1px solid rgba(255,255,255,0.2)}
.pause-state {background:rgba(231, 76, 60, 0.5)!important;margin-top:5px;}
.resume-state {background:rgba(46, 204, 113, 0.5)!important;margin-top:5px;}
`;
document.head.appendChild(style);
}
createPanel() {
const panel = document.createElement('div');
panel.className = 'ulearn-panel';
panel.innerHTML = `
提示:播放失败时将自动重启