// ==UserScript==
// @name 雨课堂刷课助手
// @namespace http://tampermonkey.net/
// @version 1.0.1
// @description 针对雨课堂视频进行自动播放,配置题库自动答题
// @author 叶屿
// @license GPL3
// @antifeature payment 题库答题功能需要验证码(免费)或激活码(付费),视频播放等基础功能完全免费
// @match *://*.yuketang.cn/*
// @match *://*.gdufemooc.cn/*
// @match *://*exam.yuketang.cn/*
// @match *://*-exam.yuketang.cn/*
// @run-at document-start
// @icon http://yuketang.cn/favicon.ico
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @connect qsy.iano.cn
// @connect lyck6.cn
// @connect cdn.jsdelivr.net
// @connect unpkg.com
// @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// @require https://unpkg.com/tesseract.js@v2.1.0/dist/tesseract.min.js
// ==/UserScript==
/*
* ==========================================
* 雨课堂刷课助手 v1.0
* ==========================================
*
* 【功能说明】
* 1. 视频自动播放(支持倍速、静音、防暂停)
* 2. 作业自动答题(OCR识别 + 题库查询)
* 3. 考试自动答题(支持停止/继续)
* 4. 双模式题库(免费验证码 / 付费激活码)
*
* 【积分购买】
* 联系微信:C919irt
* 价格表:50积分=2元,100积分=4元,150积分=6元,200积分=8元,500积分=18元
* 说明:每次答题消耗1积分,积分永久有效
*
* 【付费声明】
* 本脚本基础功能(视频播放、进度保存)完全免费
* 题库答题功能需要验证码(免费24小时)或激活码(付费永久)
* 付费仅用于题库API调用成本,不强制购买
*
* 【免责声明】
* 本脚本仅供学习交流使用,请勿用于违反学校规定或作弊行为
* 使用本脚本造成的任何后果由使用者自行承担
*
* 【版权信息】
* 作者:叶屿 | 版本:v1.0 | 更新:2026-02-08
*
* ==========================================
*/
(() => {
'use strict';
let panel; // UI 面板实例后置初始化
let isRunning = false; // 标记是否正在运行
let stopRequested = false; // 标记是否请求停止
// ---- 脚本配置,用户可修改 ----
const Config = {
version: '1.0', // 版本号
playbackRate: 2, // 视频播放倍速
pptInterval: 3000, // ppt翻页间隔
storageKeys: { // 使用者勿动
progress: '[雨课堂脚本]刷课进度信息',
deviceId: 'ykt_device_id',
activationCode: 'ykt_activation_code',
answerMode: 'ykt_answer_mode', // 答题模式:free/paid
verifyValidUntil: 'ykt_verify_valid_until', // 验证码有效期
proClassCount: 'pro_lms_classCount',
feature: 'ykt_feature_conf' // 是否开启题库作答/自动评论
}
};
const Utils = {
// 短暂睡眠,等待网页加载
sleep: (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms)),
// 将一个 JSON 字符串解析为 JavaScript 对象
safeJSONParse(value, fallback) {
try {
return JSON.parse(value);
} catch (_) {
return fallback;
}
},
// 每隔一段时间检查某个条件是否满足(通过 checker 函数),如果满足就成功返回;如果超时仍未满足,就失败返回
poll(checker, { interval = 1000, timeout = 20000 } = {}) {
return new Promise(resolve => {
const start = Date.now();
const timer = setInterval(() => {
if (checker()) {
clearInterval(timer);
resolve(true);
return;
}
if (Date.now() - start > timeout) {
clearInterval(timer);
resolve(false);
}
}, interval);
});
},
// 使用UI课程完成度来判别是否完成课程
isProgressDone(text) {
if (!text) return false;
return text.includes('100%') || text.includes('99%') || text.includes('98%') || text.includes('已完成');
},
// 主要是规避firefox会创建多个iframe的问题
inIframe() {
return window.top !== window.self;
},
// 下滑到最底部,触发课程加载
scrollToBottom(containerSelector) {
const el = document.querySelector(containerSelector);
if (el) el.scrollTop = el.scrollHeight;
},
async getDDL() {
const element = document.querySelector('video') || document.querySelector('audio');
const fallback = 180_000;
if (!element) return fallback;
let duration = Number(element.duration);
if (!Number.isFinite(duration) || duration <= 0) {
await new Promise(resolve => element.addEventListener('loadedmetadata', resolve, { once: true }));
duration = Number(element.duration);
}
const elementDurationMs = duration * 1000; // 转为秒
const timeout = Math.max(elementDurationMs * 3, 10_000); // 至少 10 秒(防极短视频);
return timeout;
}
};
// ---- 存储工具 ----
const Store = {
getProgress(url) {
const raw = localStorage.getItem(Config.storageKeys.progress);
const all = Utils.safeJSONParse(raw, {}) || { url: { outside: 0, inside: 0 } };
if (!all[url]) {
all[url] = { outside: 0, inside: 0 };
localStorage.setItem(Config.storageKeys.progress, JSON.stringify(all));
}
return { all, current: all[url] };
},
setProgress(url, outside, inside = 0) {
const raw = localStorage.getItem(Config.storageKeys.progress);
const all = Utils.safeJSONParse(raw, {});
all[url] = { outside, inside };
localStorage.setItem(Config.storageKeys.progress, JSON.stringify(all));
},
removeProgress(url) {
const raw = localStorage.getItem(Config.storageKeys.progress);
const all = Utils.safeJSONParse(raw, {});
delete all[url];
localStorage.setItem(Config.storageKeys.progress, JSON.stringify(all));
},
getDeviceId() {
let deviceId = localStorage.getItem(Config.storageKeys.deviceId);
if (!deviceId) {
deviceId = 'ykt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
localStorage.setItem(Config.storageKeys.deviceId, deviceId);
}
return deviceId;
},
getActivationCode() {
return localStorage.getItem(Config.storageKeys.activationCode) || '';
},
setActivationCode(code) {
localStorage.setItem(Config.storageKeys.activationCode, code);
},
getAnswerMode() {
return localStorage.getItem(Config.storageKeys.answerMode) || 'free';
},
setAnswerMode(mode) {
localStorage.setItem(Config.storageKeys.answerMode, mode);
},
getVerifyValidUntil() {
return Number(localStorage.getItem(Config.storageKeys.verifyValidUntil)) || 0;
},
setVerifyValidUntil(timestamp) {
localStorage.setItem(Config.storageKeys.verifyValidUntil, timestamp);
},
isVerifyValid() {
const validUntil = this.getVerifyValidUntil();
return validUntil > Date.now() / 1000;
},
getProClassCount() {
const value = localStorage.getItem(Config.storageKeys.proClassCount);
return value ? Number(value) : 1;
},
setProClassCount(count) {
localStorage.setItem(Config.storageKeys.proClassCount, count);
},
getFeatureConf() {
const raw = localStorage.getItem(Config.storageKeys.feature);
const saved = Utils.safeJSONParse(raw, {}) || {};
const conf = {
autoAI: saved.autoAI ?? false,
autoComment: saved.autoComment ?? false,
};
localStorage.setItem(Config.storageKeys.feature, JSON.stringify(conf));
return conf;
},
setFeatureConf(conf) {
localStorage.setItem(Config.storageKeys.feature, JSON.stringify(conf));
}
};
// ---- UI 面板 ----
function createPanel() {
const iframe = document.createElement('iframe');
iframe.style.position = 'fixed';
iframe.style.top = '40px';
iframe.style.left = '40px';
iframe.style.width = '480px';
iframe.style.height = '520px';
iframe.style.zIndex = '999999';
iframe.style.border = '1px solid #a3a3a3';
iframe.style.borderRadius = '12px';
iframe.style.background = '#fff';
iframe.style.overflow = 'hidden';
iframe.style.boxShadow = '0 10px 40px rgba(0,0,0,0.2)';
iframe.setAttribute('frameborder', '0');
iframe.setAttribute('id', 'ykt-helper-iframe');
iframe.setAttribute('allowtransparency', 'true');
document.body.appendChild(iframe);
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(`
⚠️ 有BUG及时反馈需提供账号
联系微信:C919irt 📋
- ✅ 脚本已加载 v1.0
- 📖 题库自动答题模式
- ⚠️ 自动答题需要配置激活
- 💡 点击【题库配置】开始
- 🚀 雨课堂助手已启动
`);
doc.close();
const ui = {
iframe,
doc,
panel: doc.getElementById('panel'),
header: doc.getElementById('header'),
info: doc.getElementById('info'),
btnStart: doc.getElementById('btn-start'),
btnClear: doc.getElementById('btn-clear'),
btnSetting: doc.getElementById('btn-setting'),
settings: doc.getElementById('settings'),
saveSettings: doc.getElementById('save_settings'),
closeSettings: doc.getElementById('close_settings'),
modePaid: doc.getElementById('mode-paid'),
modeFree: doc.getElementById('mode-free'),
activationCodeSection: doc.getElementById('activation-code-section'),
activationCodeInput: doc.getElementById('activation_code'),
activateBtn: doc.getElementById('activate_btn'),
activateStatus: doc.getElementById('activate_status'),
creditsDisplay: doc.getElementById('credits_display'),
wechatCopy: doc.getElementById('wechat_copy'),
feedbackWechat: doc.getElementById('feedback_wechat'),
verifyCodeSection: doc.getElementById('verify-code-section'),
verifyCodeInput: doc.getElementById('verify_code'),
verifyBtn: doc.getElementById('verify_btn'),
verifyStatus: doc.getElementById('verify_status'),
featureAutoAI: doc.getElementById('feature_auto_ai'),
featureAutoComment: doc.getElementById('feature_auto_comment'),
minimality: doc.getElementById('minimality'),
question: doc.getElementById('question'),
miniBasic: doc.getElementById('mini-basic')
};
let isDragging = false;
let startX = 0, startY = 0, startLeft = 0, startTop = 0;
const hostWindow = window.parent || window;
const onMove = e => {
if (!isDragging) return;
const deltaX = e.screenX - startX;
const deltaY = e.screenY - startY;
const maxLeft = Math.max(0, hostWindow.innerWidth - iframe.offsetWidth);
const maxTop = Math.max(0, hostWindow.innerHeight - iframe.offsetHeight);
iframe.style.left = Math.min(Math.max(0, startLeft + deltaX), maxLeft) + 'px';
iframe.style.top = Math.min(Math.max(0, startTop + deltaY), maxTop) + 'px';
};
const stopDrag = () => {
if (!isDragging) return;
isDragging = false;
iframe.style.transition = '';
doc.body.style.userSelect = '';
};
// 标题栏拖拽
ui.header.addEventListener('mousedown', e => {
isDragging = true;
startX = e.screenX;
startY = e.screenY;
startLeft = parseFloat(iframe.style.left) || 0;
startTop = parseFloat(iframe.style.top) || 0;
iframe.style.transition = 'none';
doc.body.style.userSelect = 'none';
e.preventDefault();
});
// 缩小按钮拖拽
ui.miniBasic.addEventListener('mousedown', e => {
isDragging = true;
startX = e.screenX;
startY = e.screenY;
startLeft = parseFloat(iframe.style.left) || 0;
startTop = parseFloat(iframe.style.top) || 0;
iframe.style.transition = 'none';
doc.body.style.userSelect = 'none';
e.preventDefault();
e.stopPropagation(); // 防止触发点击事件
});
doc.addEventListener('mousemove', onMove);
hostWindow.addEventListener('mousemove', onMove);
doc.addEventListener('mouseup', stopDrag);
hostWindow.addEventListener('mouseup', stopDrag);
hostWindow.addEventListener('blur', stopDrag);
const normalSize = { width: parseFloat(iframe.style.width), height: parseFloat(iframe.style.height) };
const miniSize = 64;
let isMinimized = false;
const enterMini = () => {
if (isMinimized) return;
isMinimized = true;
ui.panel.style.display = 'none';
ui.miniBasic.classList.add('show');
iframe.style.width = miniSize + 'px';
iframe.style.height = miniSize + 'px';
};
const exitMini = () => {
if (!isMinimized) return;
isMinimized = false;
ui.panel.style.display = '';
ui.miniBasic.classList.remove('show');
iframe.style.width = normalSize.width + 'px';
iframe.style.height = normalSize.height + 'px';
};
ui.minimality.addEventListener('click', enterMini);
ui.miniBasic.addEventListener('click', exitMini);
ui.question.addEventListener('click', () => {
window.parent.alert('雨课堂助手 v1.0\n作者:叶屿\n\n功能说明:\n- 自动播放视频/音频\n- 题库自动答题\n- 自动评论回复');
});
const log = message => {
const li = doc.createElement('li');
li.innerText = message;
ui.info.appendChild(li);
if (ui.info.lastElementChild) ui.info.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
};
// 查询积分
const queryCredits = async () => {
const deviceId = Store.getDeviceId();
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://qsy.iano.cn/index.php?s=/api/question_bank/credits&device_id=${deviceId}`,
timeout: 15000,
onload: res => {
if (res.status === 200) {
try {
const json = JSON.parse(res.responseText);
if (json.code === 1) {
resolve(json.data);
} else {
reject(json.msg || '查询失败');
}
} catch (e) {
reject('JSON 解析失败');
}
} else {
reject(`请求失败: HTTP ${res.status}`);
}
},
onerror: () => reject('网络错误'),
ontimeout: () => reject('请求超时')
});
});
};
// 更新积分显示
const updateCreditsDisplay = async () => {
try {
const data = await queryCredits();
ui.creditsDisplay.textContent = `${data.remaining_credits} 积分`;
} catch (err) {
ui.creditsDisplay.textContent = '-- 积分';
}
};
// 验证验证码
const verifyCode = async (code) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: 'https://qsy.iano.cn/index.php?s=/api/code/verify',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: 'code=' + encodeURIComponent(code),
timeout: 15000,
onload: res => {
if (res.status === 200) {
try {
const json = JSON.parse(res.responseText);
if (json.code === 1 && json.data.valid) {
resolve(json);
} else {
reject(json.msg || '验证码无效或已过期');
}
} catch (e) {
reject('JSON 解析失败');
}
} else {
reject(`请求失败: HTTP ${res.status}`);
}
},
onerror: () => reject('网络错误'),
ontimeout: () => reject('请求超时')
});
});
};
// 激活题库激活码
const activateCode = async (code, deviceId) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: 'https://qsy.iano.cn/index.php?s=/api/question_bank/activate',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({
code: code,
device_id: deviceId
}),
timeout: 15000,
onload: res => {
if (res.status === 200) {
try {
const json = JSON.parse(res.responseText);
if (json.code === 1) {
resolve(json);
} else {
reject(json.msg || '激活失败');
}
} catch (e) {
reject('JSON 解析失败');
}
} else {
reject(`请求失败: HTTP ${res.status}`);
}
},
onerror: () => reject('网络错误'),
ontimeout: () => reject('请求超时')
});
});
};
// 切换答题模式
const switchAnswerMode = (mode) => {
Store.setAnswerMode(mode);
if (mode === 'paid') {
ui.modePaid.classList.add('selected');
ui.modeFree.classList.remove('selected');
ui.activationCodeSection.style.display = 'block';
ui.verifyCodeSection.style.display = 'none';
updateCreditsDisplay(); // 切换到付费模式时查询积分
} else {
ui.modeFree.classList.add('selected');
ui.modePaid.classList.remove('selected');
ui.activationCodeSection.style.display = 'none';
ui.verifyCodeSection.style.display = 'block';
}
};
// 绑定答题模式切换事件
ui.modePaid.addEventListener('click', () => switchAnswerMode('paid'));
ui.modeFree.addEventListener('click', () => switchAnswerMode('free'));
// 绑定微信号复制
ui.wechatCopy.addEventListener('click', () => {
const wechat = 'C919irt';
const textarea = doc.createElement('textarea');
textarea.value = wechat;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
doc.body.appendChild(textarea);
textarea.select();
try {
doc.execCommand('copy');
ui.wechatCopy.textContent = '✅ 已复制';
setTimeout(() => {
ui.wechatCopy.textContent = 'C919irt 📋';
}, 2000);
} catch (err) {
console.error('复制失败', err);
}
doc.body.removeChild(textarea);
});
// 绑定反馈微信号复制
ui.feedbackWechat.addEventListener('click', () => {
const wechat = 'C919irt';
const textarea = doc.createElement('textarea');
textarea.value = wechat;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
doc.body.appendChild(textarea);
textarea.select();
try {
doc.execCommand('copy');
ui.feedbackWechat.textContent = '✅ 已复制';
setTimeout(() => {
ui.feedbackWechat.textContent = 'C919irt 📋';
}, 2000);
} catch (err) {
console.error('复制失败', err);
}
doc.body.removeChild(textarea);
});
// 绑定激活按钮
ui.activateBtn.addEventListener('click', async () => {
const code = ui.activationCodeInput.value.trim();
if (!code) {
ui.activateStatus.className = 'verify-status error';
ui.activateStatus.textContent = '❌ 请输入激活码';
ui.activateStatus.style.display = 'block';
return;
}
ui.activateBtn.disabled = true;
ui.activateBtn.textContent = '激活中...';
ui.activateStatus.style.display = 'none';
try {
const deviceId = Store.getDeviceId();
const result = await activateCode(code, deviceId);
Store.setActivationCode(code);
ui.activateStatus.className = 'verify-status success';
ui.activateStatus.textContent = `✅ 激活成功!获得 ${result.data.credits} 积分`;
ui.activateStatus.style.display = 'block';
ui.activationCodeInput.value = '';
await updateCreditsDisplay(); // 更新积分显示
log(`✅ 激活成功!获得 ${result.data.credits} 积分`);
} catch (err) {
ui.activateStatus.className = 'verify-status error';
ui.activateStatus.textContent = `❌ ${err}`;
ui.activateStatus.style.display = 'block';
} finally {
ui.activateBtn.disabled = false;
ui.activateBtn.textContent = '激活';
}
});
// 绑定验证码验证按钮
ui.verifyBtn.addEventListener('click', async () => {
const vcode = ui.verifyCodeInput.value.trim();
if (!vcode || vcode.length !== 4) {
ui.verifyStatus.className = 'verify-status error';
ui.verifyStatus.textContent = '❌ 请输入4位验证码';
ui.verifyStatus.style.display = 'block';
return;
}
ui.verifyBtn.disabled = true;
ui.verifyBtn.textContent = '验证中...';
ui.verifyStatus.style.display = 'none';
try {
const result = await verifyCode(vcode);
Store.setVerifyValidUntil(result.data.valid_until);
ui.verifyStatus.className = 'verify-status success';
ui.verifyStatus.textContent = `✅ 验证成功!有效期至 ${result.data.valid_until_str}`;
ui.verifyStatus.style.display = 'block';
ui.verifyCodeInput.value = '';
ui.verifyCodeInput.disabled = true;
log(`✅ 验证码验证成功!有效期至 ${result.data.valid_until_str}`);
} catch (err) {
ui.verifyStatus.className = 'verify-status error';
ui.verifyStatus.textContent = `❌ ${err}`;
ui.verifyStatus.style.display = 'block';
} finally {
ui.verifyBtn.disabled = false;
ui.verifyBtn.textContent = '验证';
}
});
// 支持回车键验证
ui.verifyCodeInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
ui.verifyBtn.click();
}
});
// 检查验证码状态
const checkVerifyStatus = () => {
if (Store.isVerifyValid()) {
const validUntil = Store.getVerifyValidUntil();
const date = new Date(validUntil * 1000);
const dateStr = date.toLocaleString('zh-CN');
ui.verifyStatus.className = 'verify-status success';
ui.verifyStatus.textContent = `✅ 已验证,有效期至 ${dateStr}`;
ui.verifyCodeInput.disabled = true;
ui.verifyBtn.disabled = true;
} else {
ui.verifyCodeInput.disabled = false;
ui.verifyBtn.disabled = false;
ui.verifyStatus.style.display = 'none';
}
};
const loadAnswerMode = () => {
const mode = Store.getAnswerMode();
switchAnswerMode(mode);
if (mode === 'free') {
checkVerifyStatus();
} else {
updateCreditsDisplay(); // 付费模式加载时查询积分
}
};
const loadActivationCode = () => {
ui.activationCodeInput.value = Store.getActivationCode();
};
const loadFeatureConf = () => {
const saved = Store.getFeatureConf();
ui.featureAutoAI.checked = saved.autoAI;
ui.featureAutoComment.checked = saved.autoComment;
};
loadAnswerMode();
loadActivationCode();
loadFeatureConf();
ui.btnSetting.onclick = () => {
loadAnswerMode();
loadActivationCode();
loadFeatureConf();
ui.settings.style.display = 'block';
};
ui.closeSettings.onclick = () => {
ui.settings.style.display = 'none';
};
ui.saveSettings.onclick = async () => {
const mode = Store.getAnswerMode();
// 保存功能配置
const featureConf = {
autoAI: ui.featureAutoAI.checked,
autoComment: ui.featureAutoComment.checked
};
Store.setFeatureConf(featureConf);
// 检查激活状态并给出提示
if (featureConf.autoAI) {
if (mode === 'paid') {
try {
const data = await queryCredits();
if (data.remaining_credits <= 0) {
log('⚠️ 积分不足,请先购买激活码充值');
} else {
log(`✅ 题库配置已保存 (付费模式,剩余 ${data.remaining_credits} 积分)`);
}
} catch (err) {
log('⚠️ 题库配置已保存,但未检测到激活码,请先激活');
}
} else {
// 免费模式
if (!Store.isVerifyValid()) {
log('⚠️ 题库配置已保存,但验证码未验证或已过期,请先验证');
} else {
log('✅ 题库配置已保存 (免费模式)');
}
}
} else {
log('✅ 题库配置已保存');
}
ui.settings.style.display = 'none';
};
ui.btnClear.onclick = () => {
Store.removeProgress(window.parent.location.href);
localStorage.removeItem(Config.storageKeys.proClassCount);
log('已清除当前课程的刷课进度缓存');
};
// 后面赋值给panel
return {
...ui,
log,
setStartHandler(fn) {
ui.btnStart.onclick = () => {
// 如果正在运行,点击停止
if (isRunning) {
stopRequested = true;
log('⏸️ 正在停止...');
ui.btnStart.innerText = '停止中...';
ui.btnStart.disabled = true;
return;
}
log('启动中...');
isRunning = true;
stopRequested = false;
// 检查是否是考试页面
const url = location.host;
const path = location.pathname.split('/');
if (url.includes('exam.yuketang.cn') || path.includes('exam')) {
ui.btnStart.innerText = '⏸️ 停止答题';
} else {
ui.btnStart.innerText = '⏸️ 停止刷课';
}
ui.btnStart.disabled = false;
fn && fn();
};
},
resetStartButton(text = '开始刷课') {
ui.btnStart.innerText = text;
ui.btnStart.disabled = false;
isRunning = false;
stopRequested = false;
}
};
}
// ---- 播放器工具 ----
const Player = {
applySpeed() {
const rate = Config.playbackRate;
const speedBtn = document.querySelector('xt-speedlist xt-button') || document.getElementsByTagName('xt-speedlist')[0]?.firstElementChild?.firstElementChild;
const speedWrap = document.getElementsByTagName('xt-speedbutton')[0];
if (speedBtn && speedWrap) {
speedBtn.setAttribute('data-speed', rate);
speedBtn.setAttribute('keyt', `${rate}.00`);
speedBtn.innerText = `${rate}.00X`;
const mousemove = document.createEvent('MouseEvent');
mousemove.initMouseEvent('mousemove', true, true, unsafeWindow, 0, 10, 10, 10, 10, 0, 0, 0, 0, 0, null);
speedWrap.dispatchEvent(mousemove);
speedBtn.click();
} else if (document.querySelector('video')) {
document.querySelector('video').playbackRate = rate;
}
},
mute() {
const muteBtn = document.querySelector('#video-box > div > xt-wrap > xt-controls > xt-inner > xt-volumebutton > xt-icon');
if (muteBtn) muteBtn.click();
const video = document.querySelector('video');
if (video) video.volume = 0;
},
applyMediaDefault(media) {
if (!media) return;
media.play();
media.volume = 0;
media.playbackRate = Config.playbackRate;
},
observePause(video) {
if (!video) return () => { };
const target = document.getElementsByClassName('play-btn-tip')[0];
if (!target) return () => { };
// 自动播放
const playVideo = () => {
video.play().catch(e => {
console.warn('自动播放失败:', e);
setTimeout(playVideo, 3000);
});
};
playVideo();
const observer = new MutationObserver(list => {
for (const mutation of list) {
if (mutation.type === 'childList' && target.innerText === '播放') {
video.play();
}
}
});
observer.observe(target, { childList: true });
return () => observer.disconnect();
},
waitForEnd(media, timeout = 0) {
return new Promise(resolve => {
if (!media) return resolve();
if (media.ended) return resolve();
let timer;
const onEnded = () => {
clearTimeout(timer);
resolve();
};
media.addEventListener('ended', onEnded, { once: true });
if (timeout > 0) {
timer = setTimeout(() => {
media.removeEventListener('ended', onEnded);
resolve();
}, timeout);
}
});
}
};
// ---- 防切屏 ----
function preventScreenCheck() {
const win = unsafeWindow;
const blackList = new Set(['visibilitychange', 'blur', 'pagehide']);
win._addEventListener = win.addEventListener;
win.addEventListener = (...args) => blackList.has(args[0]) ? undefined : win._addEventListener(...args);
document._addEventListener = document.addEventListener;
document.addEventListener = (...args) => blackList.has(args[0]) ? undefined : document._addEventListener(...args);
Object.defineProperties(document, {
hidden: { value: false },
visibilityState: { value: 'visible' },
hasFocus: { value: () => true },
onvisibilitychange: { get: () => undefined, set: () => { } },
onblur: { get: () => undefined, set: () => { } }
});
Object.defineProperties(win, {
onblur: { get: () => undefined, set: () => { } },
onpagehide: { get: () => undefined, set: () => { } }
});
}
// ---- JSON Hook:拦截雨课堂服务器返回的题目数据 ----
let interceptedProblems = null; // 存储拦截到的题目数据
function setupJSONParseHook() {
const originalParse = JSON.parse;
JSON.parse = function (...args) {
const result = originalParse.call(this, ...args);
try {
// 拦截雨课堂考试/作业的题目数据
if (result && result.data && result.data.problems && result.data.problems.length > 0) {
interceptedProblems = parseYktProblems(result.data.problems, result.data.font);
console.log('[雨课堂助手] 拦截到题目数据:', interceptedProblems.length, '题');
}
} catch (e) {
console.warn('[雨课堂助手] JSON Hook 解析异常:', e);
}
return result;
};
}
// 解析雨课堂题目数据(参考官方脚本的 parseYkt)
function parseYktProblems(problems, font) {
return problems.map(item => {
const content = item.content || item;
const typeText = content.TypeText || '';
const type = getQuestionType(typeText);
const body = content.Body || '';
// 清理HTML标签,提取纯文本
const question = formatString(body);
let options = [];
if (type <= 1 && content.Options) {
// 单选/多选:按 key 排序后提取选项文本
options = content.Options
.sort((a, b) => (a.key || '').charCodeAt(0) - (b.key || '').charCodeAt(0))
.map(opt => formatString(opt.value || ''));
} else if (type === 3 && content.Options) {
// 判断题
options = ['正确', '错误'];
}
// 提取已有答案(如果有的话,用于结果页面)
let answer = [];
if (content.Answer) {
if (Array.isArray(content.Answer)) {
answer = content.Answer;
} else if (typeof content.Answer === 'string') {
answer = content.Answer.split('');
}
}
// 处理用户已答的答案
if (item.user && item.user.is_show_answer && item.user.answer) {
if (type === 3) {
answer = item.user.answer.map(a => a.replace('true', '正确').replace('false', '错误'));
} else {
answer = item.user.answer;
}
}
return {
qid: item.problem_id || '',
question,
options,
type,
answer
};
}).filter(i => i.question);
}
// 题目类型映射(参考官方脚本)
function getQuestionType(str) {
if (!str) return 4;
str = str.trim().replace(/\s+/g, '');
const TYPE = {
'单选题': 0, '单选': 0, '单项选择题': 0, '单项选择': 0,
'多选题': 1, '多选': 1, '多项选择题': 1, '多项选择': 1, '案例分析': 1,
'填空题': 2, '填空': 2,
'判断题': 3, '判断': 3, '对错题': 3, '判断正误': 3,
'问答题': 4, '简答题': 4, '主观题': 4, '其它': 4
};
if (TYPE[str] !== undefined) return TYPE[str];
for (const key of Object.keys(TYPE)) {
if (str.includes(key)) return TYPE[key];
}
return 4;
}
// 格式化字符串(参考官方脚本的 formatString)
function formatString(src) {
if (!src) return '';
src = String(src);
// 去除HTML标签(保留img标签信息)
if (!src.includes('img') && !src.includes('iframe')) {
const temp = document.createElement('div');
temp.innerHTML = src;
src = temp.innerText || temp.textContent || '';
}
// 全角转半角
src = src.replace(/[\uff01-\uff5e]/g, ch =>
String.fromCharCode(ch.charCodeAt(0) - 65248)
);
return src.replace(/\s+/g, ' ')
.replace(/[""]/g, '"')
.replace(/['']/g, "'")
.replace(/。/g, '.')
.replace(/[,.?:!;]$/, '')
.trim();
}
// 字符串相似度计算(编辑距离,参考官方脚本)
function stringSimilarity(s, t) {
if (!s || !t) return 0;
if (s === t) return 100;
const l = Math.max(s.length, t.length);
const n = s.length, m = t.length;
const d = [];
for (let i = 0; i <= n; i++) { d[i] = []; d[i][0] = i; }
for (let j = 0; j <= m; j++) { d[0][j] = j; }
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= m; j++) {
const cost = s.charAt(i - 1) === t.charAt(j - 1) ? 0 : 1;
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
}
}
return Number(((1 - d[n][m] / l) * 100).toFixed(2));
}
// ---- OCR & 题库API ----
const Solver = {
// 直接从DOM读取题目(不再依赖OCR截图)
readFromDOM(element) {
if (!element) return { question: '', options: [], type: 4 };
// 读取题目文本 - 使用官方脚本的选择器
let questionText = '';
const questionEl = element.querySelector('h4') ||
element.querySelector('.problem-body') ||
element.querySelector('span:first-child');
if (questionEl) {
questionText = formatString(questionEl.innerText || questionEl.textContent || '');
}
if (!questionText) {
questionText = formatString(element.innerText || element.textContent || '');
}
// 清理题号和分数
questionText = questionText
.replace(/^\d+[、.]\s*/, '')
.replace(/[((](\d+(\.\d+)?分)[))]$/, '')
.trim();
// 读取选项
let options = [];
const optionContainer = element.querySelector('ul');
if (optionContainer) {
const optionEls = optionContainer.querySelectorAll('li');
options = Array.from(optionEls).map(li => {
const text = formatString(li.innerText || li.textContent || '');
return text.replace(/^[A-Ga-g][.、]\s*/, '').trim();
}).filter(t => t);
}
// 判断题目类型
const typeEl = element.querySelector('.item-type') ||
element.closest('.subject-item')?.querySelector('.item-type');
let type = 4;
if (typeEl) {
type = getQuestionType(typeEl.innerText || typeEl.textContent || '');
} else if (options.length === 2) {
// 两个选项大概率是判断题
const joined = options.join(',').toLowerCase();
if (joined.includes('正确') || joined.includes('错误') ||
joined.includes('对') || joined.includes('错') ||
joined.includes('true') || joined.includes('false')) {
type = 3;
}
} else if (options.length >= 3) {
type = 0; // 默认单选
}
return { question: questionText, options, type };
},
// 从拦截的数据中查找匹配的题目
findInterceptedQuestion(index) {
if (!interceptedProblems || !interceptedProblems.length) return null;
if (index >= 0 && index < interceptedProblems.length) {
return interceptedProblems[index];
}
return null;
},
// 综合识别:优先用拦截数据,其次DOM读取,最后OCR
async recognize(element, questionIndex) {
// 方式1:从拦截的JSON数据中获取(最准确)
const intercepted = this.findInterceptedQuestion(questionIndex);
if (intercepted && intercepted.question && intercepted.question.length > 3) {
panel.log(`📋 从服务器数据获取题目 (第${questionIndex + 1}题)`);
return intercepted;
}
// 方式2:直接从DOM读取文本(快速,不需要OCR)
const domResult = this.readFromDOM(element);
if (domResult.question && domResult.question.length > 5) {
panel.log(`📖 从页面DOM读取题目`);
return domResult;
}
// 方式3:OCR识别(兜底方案)
if (typeof html2canvas !== 'undefined' && typeof Tesseract !== 'undefined') {
try {
panel.log('📸 DOM读取失败,尝试OCR识别...');
const canvas = await html2canvas(element, {
useCORS: true, logging: false, scale: 2, backgroundColor: '#ffffff'
});
panel.log('🔍 正在OCR识别...');
const { data: { text } } = await Tesseract.recognize(canvas, 'chi_sim');
const fullText = text.replace(/\s+/g, ' ').trim();
const lines = text.split('\n').map(l => l.trim()).filter(l => l);
let options = [];
const optionPattern = /^[A-F][.、::]?\s*(.+)/;
for (const line of lines) {
const match = line.match(optionPattern);
if (match) options.push(match[1].trim());
}
return { question: fullText, options, type: domResult.type };
} catch (err) {
panel.log(`⚠️ OCR失败: ${err.message || '未知错误'}`);
}
}
return { question: '', options: [], type: 4 };
},
async askQuestionBank(question, options = [], type = 4) {
const mode = Store.getAnswerMode();
if (mode === 'free') {
// 免费题库API
return this.askFreeQuestionBank(question, options, type);
} else {
// 付费题库API
return this.askPaidQuestionBank(question, options, type);
}
},
async askFreeQuestionBank(question, options = [], type = 4) {
const API_URL = 'http://lyck6.cn/scriptService/api/autoFreeAnswer';
return new Promise((resolve, reject) => {
panel.log('🔍 正在查询免费题库...');
GM_xmlhttpRequest({
method: 'POST',
url: API_URL,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({
question: question,
options: options,
type: type,
location: '雨课堂'
}),
timeout: 15000,
onload: res => {
if (res.status === 200) {
try {
const json = JSON.parse(res.responseText);
if (json.code === 0 && json.result) {
const answers = json.result.answers || [];
const success = json.result.success || false;
// 处理答案格式
let finalAnswers = [];
if (success && Array.isArray(answers) && answers.length > 0) {
// 如果success为true,answers是索引数组,需要转换为选项
if (typeof answers[0] === 'number') {
finalAnswers = answers.map(idx => {
if (options[idx]) return options[idx];
// 如果没有提供options,返回字母
return String.fromCharCode(65 + idx);
});
} else {
finalAnswers = answers;
}
} else if (Array.isArray(answers) && answers.length > 0) {
// 多个答案数组,取第一个
finalAnswers = answers[0] || [];
}
panel.log(`✅ 免费题库查询成功`);
resolve({ answers: finalAnswers, remaining: -1 });
} else {
const msg = json.message || '题库查询失败';
panel.log(`⚠️ ${msg}`);
reject(msg);
}
} catch (e) {
reject('JSON 解析失败');
}
} else {
const err = `请求失败: HTTP ${res.status}`;
panel.log(err);
reject(err);
}
},
onerror: () => reject('网络错误'),
ontimeout: () => reject('请求超时')
});
});
},
async askPaidQuestionBank(question, options = [], type = 4) {
const API_URL = 'https://qsy.iano.cn/index.php?s=/api/question_bank/answer';
const deviceId = Store.getDeviceId();
return new Promise((resolve, reject) => {
panel.log('🔍 正在查询付费题库...');
GM_xmlhttpRequest({
method: 'POST',
url: API_URL,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({
device_id: deviceId,
question: question,
options: options,
type: type,
location: '雨课堂'
}),
timeout: 15000,
onload: res => {
if (res.status === 200) {
try {
const json = JSON.parse(res.responseText);
if (json.code === 1 && json.data && json.data.result) {
const answers = json.data.result.answers || [];
const remaining = json.data.remaining_credits || 0;
panel.log(`✅ 付费题库查询成功,剩余积分: ${remaining}`);
resolve({ answers, remaining });
} else {
const msg = json.msg || '题库查询失败';
panel.log(`⚠️ ${msg}`);
reject(msg);
}
} catch (e) {
reject('JSON 解析失败');
}
} else {
const err = `请求失败: HTTP ${res.status}`;
panel.log(err);
reject(err);
}
},
onerror: () => reject('网络错误'),
ontimeout: () => reject('请求超时')
});
});
},
async autoSelectAndSubmit(answers, itemBodyElement) {
if (!answers || !answers.length) {
panel.log('⚠️ 未获取到答案,请人工检查');
return;
}
// 处理答案格式,使用改进的匹配逻辑
let targetIndices = [];
const letterMap = { 'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6 };
// 获取页面上的选项文本,用于相似度匹配
const listContainer = itemBodyElement.querySelector('.list-inline.list-unstyled-radio') ||
itemBodyElement.querySelector('.list-unstyled.list-unstyled-radio') ||
itemBodyElement.querySelector('.list-unstyled') ||
itemBodyElement.querySelector('ul.list') ||
itemBodyElement.querySelector('ul');
const optionEls = listContainer ? listContainer.querySelectorAll('li') : [];
const pageOptions = Array.from(optionEls).map(li =>
formatString(li.innerText || li.textContent || '').replace(/^[A-Ga-g][.、]\s*/, '').trim().toLowerCase()
);
for (const answer of answers) {
const answerStr = String(answer).trim();
const answerUpper = answerStr.toUpperCase();
const answerLower = answerStr.toLowerCase().replace(/\s/g, '');
// 1. 判断题处理
if (/^(对|正确|√|T|ri|true|是)$/i.test(answerStr)) {
targetIndices = [0];
break;
}
if (/^(错|错误|×|F|不是|wr|false|否)$/i.test(answerStr)) {
targetIndices = [1];
break;
}
// 2. 纯字母答案(如 "A", "AC", "BCD")
if (/^[A-G]+$/.test(answerUpper) && answerUpper.length <= 7) {
let isOrdered = true;
for (let i = 1; i < answerUpper.length; i++) {
if (answerUpper.charCodeAt(i) <= answerUpper.charCodeAt(i - 1)) {
isOrdered = false;
break;
}
}
if (isOrdered) {
for (const ch of answerUpper) {
if (letterMap[ch] !== undefined && !targetIndices.includes(letterMap[ch])) {
targetIndices.push(letterMap[ch]);
}
}
continue;
}
}
// 3. 精确匹配选项文本
const exactIdx = pageOptions.indexOf(answerLower);
if (exactIdx >= 0 && !targetIndices.includes(exactIdx)) {
targetIndices.push(exactIdx);
continue;
}
// 4. 相似度匹配(当答案文本较长时)
if (answerLower.length >= 5 && pageOptions.length > 0) {
const ratings = pageOptions.map(opt => stringSimilarity(answerLower, opt));
const maxScore = Math.max(...ratings);
if (maxScore > 65) {
const bestIdx = ratings.indexOf(maxScore);
if (!targetIndices.includes(bestIdx)) {
targetIndices.push(bestIdx);
}
}
}
}
if (!targetIndices.length) {
panel.log(`⚠️ 无法解析答案: ${answers.join(', ')}`);
return;
}
panel.log(`✅ 题库答案:${answers.join(', ')} → 选择第 ${targetIndices.map(i => String.fromCharCode(65 + i)).join('')} 项`);
if (!listContainer) {
panel.log('⚠️ 未找到选项容器');
return;
}
const options = listContainer.querySelectorAll('li');
for (const idx of targetIndices) {
if (!options[idx]) continue;
const clickable = options[idx].querySelector('label.el-radio') ||
options[idx].querySelector('label.el-checkbox') ||
options[idx].querySelector('.el-radio__label') ||
options[idx].querySelector('.el-checkbox__label') ||
options[idx].querySelector('input') ||
options[idx];
clickable.click();
await Utils.sleep(300);
}
const submitBtn = (() => {
const local = itemBodyElement.parentElement.querySelectorAll('.el-button--primary');
for (const btn of local) {
if (btn.innerText.includes('提交')) return btn;
}
const global = document.querySelectorAll('.el-button.el-button--primary.el-button--medium');
for (const btn of global) {
if (btn.innerText.includes('提交') && btn.offsetParent !== null) return btn;
}
return null;
})();
if (submitBtn) {
panel.log('正在提交...');
submitBtn.click();
} else {
panel.log('⚠️ 未找到提交按钮,请手动提交');
}
}
};
// ---- v2 逻辑 ----
class V2Runner {
constructor(panel) {
this.panel = panel;
this.baseUrl = location.href;
const { current } = Store.getProgress(this.baseUrl);
this.outside = current.outside;
this.inside = current.inside;
}
updateProgress(outside, inside = 0) {
this.outside = outside;
this.inside = inside;
Store.setProgress(this.baseUrl, outside, inside);
}
async run() {
this.panel.log(`检测到已播放到第 ${this.outside} 集,继续刷课...`);
while (true) {
// 检查是否请求停止
if (stopRequested) {
this.panel.log('⏸️ 已停止刷课');
this.panel.resetStartButton('继续刷课');
return;
}
await this.autoSlide();
const list = document.querySelector('.logs-list')?.childNodes;
if (!list || !list.length) {
this.panel.log('未找到课程列表,稍后重试');
await Utils.sleep(2000);
continue;
}
console.log(`当前集数:${this.outside}/全部集数${list.length}`);
if (this.outside >= list.length) {
this.panel.log('课程刷完啦 🎉');
this.panel.resetStartButton('刷完啦~');
Store.removeProgress(this.baseUrl);
break;
}
const course = list[this.outside]?.querySelector('.content-box')?.querySelector('section');
if (!course) {
this.panel.log('未找到当前课程节点,跳过');
this.updateProgress(this.outside + 1, 0);
continue;
}
const type = course.querySelector('.tag')?.querySelector('use')?.getAttribute('xlink:href') || 'piliang';
this.panel.log(`刷课状态:第 ${this.outside + 1}/${list.length} 个,类型 ${type}`);
if (type.includes('shipin')) {
await this.handleVideo(course);
} else if (type.includes('piliang')) {
await this.handleBatch(course, list);
} else if (type.includes('ketang')) {
await this.handleClassroom(course);
} else if (type.includes('kejian')) {
await this.handleCourseware(course);
} else if (type.includes('kaoshi')) {
this.panel.log('考试区域脚本会被屏蔽,已跳过');
this.updateProgress(this.outside + 1, 0);
} else {
this.panel.log('非视频/批量/课件/考试,已跳过');
this.updateProgress(this.outside + 1, 0);
}
}
}
async autoSlide() {
const frequency = Math.floor((this.outside + 1) / 20) + 1;
for (let i = 0; i < frequency; i++) {
Utils.scrollToBottom('.viewContainer');
await Utils.sleep(800);
}
}
async handleVideo(course) {
course.click();
await Utils.sleep(3000);
const progressNode = document.querySelector('.progress-wrap')?.querySelector('.text');
const title = document.querySelector('.title')?.innerText || '视频';
const isDeadline = document.querySelector('.box')?.innerText.includes('已过考核截止时间');
if (isDeadline) this.panel.log(`${title} 已过截止,进度不再增加,将直接跳过`);
Player.applySpeed();
Player.mute();
const stopObserve = Player.observePause(document.querySelector('video'));
await Utils.poll(() => isDeadline || Utils.isProgressDone(progressNode?.innerHTML), { interval: 5000, timeout: await Utils.getDDL() });
stopObserve();
this.updateProgress(this.outside + 1, 0);
history.back();
await Utils.sleep(1200);
}
async handleBatch(course, list) {
const expandBtn = course.querySelector('.sub-info')?.querySelector('.gray')?.querySelector('span');
if (!expandBtn) {
this.panel.log('未找到批量展开按钮,跳过');
this.updateProgress(this.outside + 1, 0);
return;
}
expandBtn.click();
await Utils.sleep(1200);
const activities = list[this.outside]?.querySelector('.leaf_list__wrap')?.querySelectorAll('.activity__wrap') || [];
let idx = this.inside;
this.panel.log(`进入批量区,内部进度 ${idx}/${activities.length}`);
while (idx < activities.length) {
const item = activities[idx];
if (!item) break;
const tagText = item.querySelector('.tag')?.innerText || '';
const tagHref = item.querySelector('.tag')?.querySelector('use')?.getAttribute('xlink:href') || '';
const title = item.querySelector('h2')?.innerText || `第${idx + 1}项`;
if (tagText === '音频') {
idx = await this.playAudioItem(item, title, idx);
} else if (tagHref.includes('shipin')) {
idx = await this.playVideoItem(item, title, idx);
} else if (tagHref.includes('tuwen') || tagHref.includes('taolun')) {
idx = await this.autoCommentItem(item, tagHref.includes('tuwen') ? '图文' : '讨论', idx);
} else if (tagHref.includes('zuoye')) {
idx = await this.handleHomework(item, idx);
} else {
this.panel.log(`类型未知,已跳过:${title}`);
idx++;
this.updateProgress(this.outside, idx);
}
}
this.updateProgress(this.outside + 1, 0);
await Utils.sleep(1000);
}
async playAudioItem(item, title, idx) {
this.panel.log(`开始播放音频:${title}`);
item.click();
await Utils.sleep(2500);
Player.applyMediaDefault(document.querySelector('audio'));
const progressNode = document.querySelector('.progress-wrap')?.querySelector('.text');
await Utils.poll(() => Utils.isProgressDone(progressNode?.innerHTML), { interval: 3000, timeout: await Utils.getDDL() });
this.panel.log(`${title} 播放完成`);
idx++;
this.updateProgress(this.outside, idx);
history.back();
await Utils.sleep(1500);
return idx;
}
async playVideoItem(item, title, idx) {
this.panel.log(`开始播放视频:${title}`);
item.click();
await Utils.sleep(2500);
Player.applySpeed();
Player.mute();
const stopObserve = Player.observePause(document.querySelector('video'));
const progressNode = document.querySelector('.progress-wrap')?.querySelector('.text');
await Utils.poll(() => Utils.isProgressDone(progressNode?.innerHTML), { interval: 3000, timeout: await Utils.getDDL() });
stopObserve();
this.panel.log(`${title} 播放完成`);
idx++;
this.updateProgress(this.outside, idx);
history.back();
await Utils.sleep(1500);
return idx;
}
async autoCommentItem(item, typeText, idx) {
const featureFlags = Store.getFeatureConf();
if (!featureFlags.autoComment) {
this.panel.log('已关闭自动回复评论,跳过该项');
idx++;
this.updateProgress(this.outside, idx);
return idx;
}
this.panel.log(`开始处理${typeText}:${item.querySelector('h2')?.innerText || ''}`);
item.click();
await Utils.sleep(1200);
window.scrollTo(0, document.body.scrollHeight);
await Utils.sleep(800);
window.scrollTo(0, 0);
const commentSelectors = ['#new_discuss .new_discuss_list .cont_detail', '.new_discuss_list dd .cont_detail', '.cont_detail.word-break'];
let firstComment = '';
for (let retry = 0; retry < 30 && !firstComment; retry++) {
for (const sel of commentSelectors) {
const list = document.querySelectorAll(sel);
for (const node of list) {
if (node?.innerText?.trim()) {
firstComment = node.innerText.trim();
break;
}
}
if (firstComment) break;
}
if (!firstComment) await Utils.sleep(500);
}
if (!firstComment) {
this.panel.log('未找到评论内容,跳过该项');
} else {
const input = document.querySelector('.el-textarea__inner');
if (input) {
input.value = firstComment;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
await Utils.sleep(800);
const sendBtn = document.querySelector('.el-button.submitComment') ||
document.querySelector('.publish_discuss .postBtn button') ||
document.querySelector('.el-button--primary');
if (sendBtn && !sendBtn.disabled && !sendBtn.classList.contains('is-disabled')) {
sendBtn.click();
this.panel.log(`已在${typeText}区发表评论`);
} else {
this.panel.log('发送按钮不可用或不存在');
}
} else {
this.panel.log('未找到评论输入框,跳过');
}
}
idx++;
this.updateProgress(this.outside, idx);
history.back();
await Utils.sleep(1000);
return idx;
}
async handleHomework(item, idx) {
const featureFlags = Store.getFeatureConf();
if (!featureFlags.autoAI) {
this.panel.log('已关闭题库自动答题,跳过该项');
idx++;
this.updateProgress(this.outside, idx);
return idx;
}
this.panel.log('进入作业,启动题库查询');
item.click();
await Utils.sleep(2000);
// 等待拦截数据加载
await Utils.sleep(1000);
let i = 0;
while (true) {
const items = document.querySelectorAll('.subject-item.J_order, .subject-item');
if (i >= items.length) {
this.panel.log(`所有题目处理完毕,共 ${items.length} 题,准备交卷`);
break;
}
const listItem = items[i];
listItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
listItem.click();
await Utils.sleep(1500);
// 检查是否已答
const disabled = document.querySelectorAll('.el-button.el-button--info.is-disabled.is-plain');
if (disabled.length > 0) {
this.panel.log(`第 ${i + 1} 题已完成,跳过`);
i++;
continue;
}
const targetEl = document.querySelector('.item-type')?.parentElement ||
document.querySelector('.item-body') ||
document.querySelector('.problem-body')?.parentElement;
try {
// 使用新的综合识别
const result = await Solver.recognize(targetEl, i);
if (result && result.question && result.question.length > 3) {
const { answers } = await Solver.askQuestionBank(
result.question,
result.options,
result.type !== undefined ? result.type : 4
);
await Solver.autoSelectAndSubmit(answers, targetEl);
} else {
this.panel.log(`第 ${i + 1} 题识别失败`);
}
} catch (err) {
this.panel.log(`题库查询失败:${err}`);
}
await Utils.sleep(1500);
i++;
}
idx++;
this.updateProgress(this.outside, idx);
history.back();
await Utils.sleep(1200);
return idx;
}
async handleClassroom(course) {
this.panel.log('进入课堂模式...');
course.click();
await Utils.sleep(5000);
const iframe = document.querySelector('iframe.lesson-report-mobile');
if (!iframe || !iframe.contentDocument) {
this.panel.log('未找到课堂 iframe,跳过');
this.updateProgress(this.outside + 1, 0);
return;
}
const video = iframe.contentDocument.querySelector('video');
const audio = iframe.contentDocument.querySelector('audio');
if (video) {
Player.applyMediaDefault(video);
await Player.waitForEnd(video);
}
if (audio) {
Player.applyMediaDefault(audio);
await Player.waitForEnd(audio);
}
this.updateProgress(this.outside + 1, 0);
history.go(-1);
await Utils.sleep(1200);
}
async handleCourseware(course) {
const tableData = course.parentNode?.parentNode?.parentNode?.__vue__?.tableData;
const deadlinePassed = (tableData?.deadline || tableData?.end) ? (tableData.deadline < Date.now() || tableData.end < Date.now()) : false;
if (deadlinePassed) {
this.panel.log(`${course.querySelector('h2')?.innerText || '课件'} 已结课,跳过`);
this.updateProgress(this.outside + 1, 0);
return;
}
course.click();
await Utils.sleep(3000);
const classType = document.querySelector('.el-card__header')?.innerText || '';
const className = document.querySelector('.dialog-header')?.firstElementChild?.innerText || '课件';
if (classType.includes('PPT')) {
const slides = document.querySelector('.swiper-wrapper')?.children || [];
this.panel.log(`开始播放 PPT:${className}`);
for (let i = 0; i < slides.length; i++) {
slides[i].click();
this.panel.log(`${className}:第 ${i + 1} 张`);
await Utils.sleep(Config.pptInterval);
}
await Utils.sleep(Config.pptInterval);
const videoBoxes = document.querySelectorAll('.video-box');
if (videoBoxes?.length) {
this.panel.log('PPT 中有视频,继续播放');
for (let i = 0; i < videoBoxes.length; i++) {
if (videoBoxes[i].innerText === '已完成') {
this.panel.log(`第 ${i + 1} 个视频已完成,跳过`);
continue;
}
videoBoxes[i].click();
await Utils.sleep(2000);
Player.applySpeed();
const muteBtn = document.querySelector('.xt_video_player_common_icon');
muteBtn && muteBtn.click();
const stopObserve = Player.observePause(document.querySelector('video'));
await Utils.poll(() => {
const allTime = document.querySelector('.xt_video_player_current_time_display')?.innerText || '';
const [nowTime, totalTime] = allTime.split(' / ');
return nowTime && totalTime && nowTime === totalTime;
}, { interval: 800, timeout: await Utils.getDDL() });
stopObserve();
}
}
this.panel.log(`${className} 已播放完毕`);
} else {
const videoBox = document.querySelector('.video-box');
if (videoBox) {
videoBox.click();
await Utils.sleep(1800);
Player.applySpeed();
const muteBtn = document.querySelector('.xt_video_player_common_icon');
muteBtn && muteBtn.click();
await Utils.poll(() => {
const times = document.querySelector('.xt_video_player_current_time_display')?.innerText || '';
const [nowTime, totalTime] = times.split(' / ');
return nowTime && totalTime && nowTime === totalTime;
}, { interval: 800, timeout: await Utils.getDDL() });
this.panel.log(`${className} 视频播放完毕`);
}
}
this.updateProgress(this.outside + 1, 0);
history.back();
await Utils.sleep(1000);
}
}
// ---- 考试答题 Runner ----
class ExamRunner {
constructor(panel) {
this.panel = panel;
}
async run() {
this.panel.log('🎯 开始自动答题...');
await Utils.sleep(2000);
let retryCount = 0;
const maxRetry = 5;
let items = null;
while (retryCount < maxRetry) {
if (stopRequested) {
this.panel.log('⏸️ 已停止答题');
this.panel.resetStartButton('继续答题');
return;
}
// 使用官方脚本的选择器
items = document.querySelectorAll('.exam-main--body .subject-item');
if (!items || items.length === 0) {
items = document.querySelectorAll('.subject-item');
}
if (!items || items.length === 0) {
items = document.querySelectorAll('.container-problem .subject-item');
}
if (items && items.length > 0) {
this.panel.log(`找到 ${items.length} 道题目,开始答题`);
break;
}
retryCount++;
this.panel.log(`未找到题目,等待页面加载... (${retryCount}/${maxRetry})`);
await Utils.sleep(3000);
}
if (!items || items.length === 0) {
this.panel.log('❌ 未找到题目,请确认页面已加载完成');
this.panel.log('💡 提示:可能需要先点击"开始考试"按钮');
this.panel.resetStartButton('开始答题');
return;
}
let i = 0;
while (i < items.length) {
if (stopRequested) {
this.panel.log('⏸️ 已停止答题');
this.panel.resetStartButton('继续答题');
return;
}
const item = items[i];
this.panel.log(`📝 正在处理第 ${i + 1}/${items.length} 题...`);
item.scrollIntoView({ behavior: 'smooth', block: 'center' });
await Utils.sleep(800);
// 检查是否已答题(使用官方脚本的 is-checked 选择器)
if (item.querySelector('.is-checked')) {
this.panel.log(`第 ${i + 1} 题已作答,跳过`);
i++;
continue;
}
// 查找题目内容区域
const questionArea = item.querySelector('.item-body') || item;
try {
// 使用新的综合识别方法(优先拦截数据 > DOM读取 > OCR)
const result = await Solver.recognize(questionArea, i);
if (result && result.question && result.question.length > 3) {
// 查询题库
const { answers } = await Solver.askQuestionBank(
result.question,
result.options,
result.type !== undefined ? result.type : 4
);
// 自动选择答案
await Solver.autoSelectAndSubmit(answers, questionArea);
await Utils.sleep(1500);
} else {
this.panel.log(`第 ${i + 1} 题识别失败,跳过`);
}
} catch (err) {
this.panel.log(`第 ${i + 1} 题查询失败:${err}`);
}
i++;
await Utils.sleep(1000);
}
this.panel.log('🎉 答题完成!请检查并提交');
this.panel.resetStartButton('答题完成');
}
}
// ---- pro/lms 旧版(仅做转发) ----
class ProOldRunner {
constructor(panel) {
this.panel = panel;
}
run() {
this.panel.log('准备打开新标签页...');
const leafDetail = document.querySelectorAll('.leaf-detail');
let classCount = Store.getProClassCount() - 1;
while (leafDetail[classCount] && !leafDetail[classCount].firstChild.querySelector('i').className.includes('shipin')) {
classCount++;
Store.setProClassCount(classCount + 1);
this.panel.log('课程不属于视频,已跳过');
}
leafDetail[classCount]?.click();
}
}
// ---- pro/lms 新版(主要逻辑) ----
class ProNewRunner {
constructor(panel) {
this.panel = panel;
}
async run() {
preventScreenCheck();
let classCount = Store.getProClassCount();
while (true) {
// 检查是否请求停止
if (stopRequested) {
this.panel.log('⏸️ 已停止刷课');
this.panel.resetStartButton('继续刷课');
return;
}
this.panel.log(`准备播放第 ${classCount} 集...`);
await Utils.sleep(2000);
const className = document.querySelector('.header-bar')?.firstElementChild?.innerText || '';
const classType = document.querySelector('.header-bar')?.firstElementChild?.firstElementChild?.getAttribute('class') || '';
const classStatus = document.querySelector('#app > div.app_index-wrapper > div.wrap > div.viewContainer.heightAbsolutely > div > div > div > div > section.title')?.lastElementChild?.innerText || '';
if (classType.includes('tuwen') && !classStatus.includes('已读')) {
this.panel.log(`正在阅读:${className}`);
await Utils.sleep(2000);
} else if (classType.includes('taolun')) {
this.panel.log(`讨论区暂不自动发帖,${className}`);
await Utils.sleep(2000);
} else if (classType.includes('shipin') && !classStatus.includes('100%')) {
this.panel.log(`2s 后开始播放:${className}`);
await Utils.sleep(2000);
let statusTimer;
let videoTimer;
try {
statusTimer = setInterval(() => {
const status = document.querySelector('#app > div.app_index-wrapper > div.wrap > div.viewContainer.heightAbsolutely > div > div > div > div > section.title')?.lastElementChild?.innerText || '';
if (status.includes('100%') || status.includes('99%') || status.includes('98%') || status.includes('已完成')) {
this.panel.log(`${className} 播放完毕`);
clearInterval(statusTimer);
statusTimer = null;
}
}, 200);
const videoWaitStart = Date.now();
videoTimer = setInterval(() => {
const video = document.querySelector('video');
if (video) {
setTimeout(() => {
Player.applySpeed();
Player.mute();
Player.observePause(video);
}, 2000);
clearInterval(videoTimer);
videoTimer = null;
} else if (Date.now() - videoWaitStart > 20000) {
location.reload();
}
}, 5000);
await Utils.sleep(8000);
await Utils.poll(() => {
const status = document.querySelector('#app > div.app_index-wrapper > div.wrap > div.viewContainer.heightAbsolutely > div > div > div > div > section.title')?.lastElementChild?.innerText || '';
return status.includes('100%') || status.includes('99%') || status.includes('98%') || status.includes('已完成');
}, { interval: 1000, timeout: await Utils.getDDL() });
} finally {
if (statusTimer) clearInterval(statusTimer);
if (videoTimer) clearInterval(videoTimer);
}
} else if (classType.includes('zuoye')) {
this.panel.log(`进入作业:${className}(暂无自动答题)`);
await Utils.sleep(2000);
} else if (classType.includes('kaoshi')) {
this.panel.log(`进入考试:${className}(不会自动答题)`);
await Utils.sleep(2000);
} else if (classType.includes('ketang')) {
this.panel.log(`进入课堂:${className}(暂无自动功能)`);
await Utils.sleep(2000);
} else {
this.panel.log(`已看过:${className}`);
await Utils.sleep(2000);
}
this.panel.log(`第 ${classCount} 集播放完毕`);
classCount++;
Store.setProClassCount(classCount);
const nextBtn = document.querySelector('.btn-next');
if (nextBtn) {
const event1 = new Event('mousemove', { bubbles: true });
event1.clientX = 9999;
event1.clientY = 9999;
nextBtn.dispatchEvent(event1);
nextBtn.dispatchEvent(new Event('click'));
} else {
localStorage.removeItem(Config.storageKeys.proClassCount);
this.panel.log('课程播放完毕 🎉');
this.panel.resetStartButton('刷完啦~');
break;
}
}
}
}
// ---- 路由 ----
function start() {
const url = location.host;
const path = location.pathname.split('/');
const matchURL = `${url}${path[0]}/${path[1]}/${path[2]}`;
panel.log(`正在匹配处理逻辑:${matchURL}`);
// 检查是否是考试页面
if (url.includes('exam.yuketang.cn') || path.includes('exam')) {
// 检查是否是结果页面
if (location.pathname.includes('/result/')) {
panel.log('检测到考试结果页面,无法答题');
panel.log('💡 提示:此页面为已完成的考试结果,无法修改答案');
panel.resetStartButton('查看结果');
return;
}
panel.log('检测到考试页面,准备启动自动答题功能');
// 检查是否启用自动答题
const featureFlags = Store.getFeatureConf();
if (!featureFlags.autoAI) {
panel.log('⚠️ 请先在【题库配置】中启用自动答题功能');
panel.resetStartButton('开始答题');
return;
}
// 检查验证/激活状态
const mode = Store.getAnswerMode();
if (mode === 'free') {
// 免费模式,检查验证码
if (!Store.isVerifyValid()) {
panel.log('⚠️ 验证码未验证或已过期,请先在【题库配置】中验证');
panel.resetStartButton('开始答题');
return;
}
panel.log('✅ 验证码有效,启动自动答题');
} else {
// 付费模式,检查积分(不阻止启动,只提示)
panel.log('✅ 付费模式,启动自动答题');
}
// 启动考试答题
new ExamRunner(panel).run();
return;
}
if (matchURL.includes('yuketang.cn/v2/web') || matchURL.includes('gdufemooc.cn/v2/web')) {
new V2Runner(panel).run();
} else if (matchURL.includes('yuketang.cn/pro/lms') || matchURL.includes('gdufemooc.cn/pro/lms')) {
if (document.querySelector('.btn-next')) {
new ProNewRunner(panel).run();
} else {
new ProOldRunner(panel).run();
}
} else {
panel.resetStartButton('开始刷课');
panel.log('当前页面非刷课页面,应匹配 */v2/web/* 或 */pro/lms/*');
panel.log('考试页面请点击【开始答题】启动自动答题');
}
}
// ---- 启动 ----
if (Utils.inIframe()) return;
// 尽早设置 JSON Hook(在 document-start 阶段拦截数据)
setupJSONParseHook();
// 等待 document.body 加载完成
const waitForBody = () => {
return new Promise(resolve => {
if (document.body) {
resolve();
} else {
const observer = new MutationObserver(() => {
if (document.body) {
observer.disconnect();
resolve();
}
});
observer.observe(document.documentElement, { childList: true });
}
});
};
// 初始化面板
(async () => {
await waitForBody();
panel = createPanel();
// 初始信息已在HTML中显示,不需要额外log
panel.setStartHandler(start);
})();
})();