// ==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 = `
如果这个脚本帮到了你,欢迎捐赠支持作者~
联系方式:
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();
}
})();