// ==UserScript==
// @name 📚 国开智能刷课助手
// @namespace http://tampermonkey.net/
// @version 2.1.1
// @description 国开自动刷:AI智能回帖/固定文本双模式、智能反检测,q反馈群:612441267
// @author lakay666
// @match *://lms.ouchn.cn/course/*
// @match *://lms.ouchn.cn/user/courses*
// @grant GM_notification
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect api.openai.com
// @connect api.anthropic.com
// @connect *
// @license GPL-3.0
// ==/UserScript==
(function() {
'use strict';
// ==================== 配置中心 ====================
const CONFIG = {
playbackRate: {
current: 1,
max: 4,
min: 0.5,
step: 0.5
},
intervals: {
loadCourse: 6000,
viewPage: 6000,
onlineVideo: 3000,
webLink: 3000,
forum: 3000,
material: 3000,
other: 3000,
randomFactor: 0.3
},
antiDetect: {
enabled: true,
randomScroll: true,
randomMouseMove: true,
scrollInterval: 15000,
mouseMoveInterval: 8000
},
features: {
autoForum: true,
autoMaterial: true,
autoVideo: true,
notification: true
},
forumReplyMode: 'fixed',
aiConfig: {
enabled: false,
apiUrl: 'https://api.openai.com/v1/chat/completions',
apiKey: '',
model: 'gpt-3.5-turbo',
maxTokens: 150,
temperature: 0.7,
promptTemplate: '请根据以下论坛帖子内容,写一段简短的学习回复(50-100字),要真诚、相关、有建设性:\n\n帖子标题:{title}\n帖子内容:{content}\n\n请直接输出回复内容,不要带任何前缀或说明。'
},
fixedReplies: [
'学习了,感谢分享!{randomEmoji}',
'这个知识点很有用,收藏了!{randomEmoji}',
'讲得很清楚,受益匪浅。{randomEmoji}',
'正好需要这个资料,谢谢!{randomEmoji}',
'学习了,对我帮助很大。{randomEmoji}',
'内容很精彩,感谢老师的分享!{randomEmoji}',
'这个案例很典型,值得深入研究。{randomEmoji}',
'理论与实践结合得很好,学习了!{randomEmoji}',
'思路清晰,讲解透彻,点赞!{randomEmoji}',
'很有启发性的内容,感谢分享!{randomEmoji}'
],
completionThreshold: 90,
retry: {
maxAttempts: 3,
delay: 2000
},
storagePrefix: 'gkbk_pro_'
};
// ==================== 工具函数 ====================
const Utils = {
async wait(baseMs) {
const random = baseMs * CONFIG.intervals.randomFactor * (Math.random() * 2 - 1);
const finalMs = Math.max(1000, Math.floor(baseMs + random));
return new Promise(resolve => setTimeout(resolve, finalMs));
},
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
storage: {
set(key, value) {
try {
localStorage.setItem(CONFIG.storagePrefix + key, JSON.stringify(value));
return true;
} catch (e) {
Logger.error('Storage set failed:', e);
return false;
}
},
get(key, defaultValue = null) {
try {
const item = localStorage.getItem(CONFIG.storagePrefix + key);
return item ? JSON.parse(item) : defaultValue;
} catch (e) {
Logger.error('Storage get failed:', e);
return defaultValue;
}
},
remove(key) {
localStorage.removeItem(CONFIG.storagePrefix + key);
}
},
getCourseIdFromUrl(url = window.location.href) {
const match = url.match(/\/course\/(\d+)/);
return match ? match[1] : null;
},
getActivityIdFromUrl(url = window.location.href) {
const match = url.match(/learning-activity\/(?:full-screen#?\/)?(\d+)/);
return match ? match[1] : null;
},
notify(title, body) {
if (!CONFIG.features.notification) return;
if (typeof GM_notification !== 'undefined') {
GM_notification({ title, text: body, timeout: 5000 });
} else if ('Notification' in window && Notification.permission === 'granted') {
new Notification(title, { body });
}
},
async requestNotifyPermission() {
if ('Notification' in window && Notification.permission === 'default') {
await Notification.requestPermission();
}
},
replaceVariables(text, variables = {}) {
const emojis = ['📚', '💡', '👍', '🌟', '🎯', '✨', '📝', '🔍', '💪', '🎓'];
const defaults = {
time: new Date().toLocaleString('zh-CN'),
randomEmoji: emojis[Math.floor(Math.random() * emojis.length)],
...variables
};
let result = text;
for (const [key, value] of Object.entries(defaults)) {
result = result.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
}
return result;
},
async request(options) {
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== 'undefined') {
GM_xmlhttpRequest({
...options,
onload: (response) => resolve(response),
onerror: (error) => reject(error),
ontimeout: () => reject(new Error('Request timeout'))
});
} else {
fetch(options.url, {
method: options.method || 'GET',
headers: options.headers,
body: options.data
}).then(r => r.text()).then(resolve).catch(reject);
}
});
}
};
// ==================== 日志系统 ====================
const Logger = {
levels: { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 },
currentLevel: 1,
log(level, ...args) {
if (level < this.currentLevel) return;
const prefix = `[国开助手][${Object.keys(this.levels)[level]}]`;
const method = level === 3 ? console.error : level === 2 ? console.warn : console.log;
method(prefix, ...args);
},
debug(...args) { this.log(0, ...args); },
info(...args) { this.log(1, ...args); },
warn(...args) { this.log(2, ...args); },
error(...args) { this.log(3, ...args); }
};
// ==================== 反检测模块 ====================
const AntiDetect = {
timers: [],
start() {
if (!CONFIG.antiDetect.enabled) return;
Logger.info('反检测模块已启动');
if (CONFIG.antiDetect.randomScroll) {
this.timers.push(setInterval(() => {
if (Math.random() > 0.6) {
const scrollY = Math.floor(Math.random() * 100) - 50;
window.scrollBy({ top: scrollY, behavior: 'smooth' });
}
}, CONFIG.antiDetect.scrollInterval));
}
if (CONFIG.antiDetect.randomMouseMove) {
this.timers.push(setInterval(() => {
if (Math.random() > 0.7) {
const event = new MouseEvent('mousemove', {
bubbles: true,
cancelable: true,
clientX: Math.random() * window.innerWidth,
clientY: Math.random() * window.innerHeight
});
document.dispatchEvent(event);
}
}, CONFIG.antiDetect.mouseMoveInterval));
}
},
stop() {
this.timers.forEach(t => clearInterval(t));
this.timers = [];
}
};
// ==================== 悬浮控制面板(可拖拽、可最小化,修复最小化消失) ====================
const ControlPanel = {
panel: null,
minimizedIcon: null,
isMinimized: false,
dragState: { dragging: false, startX: 0, startY: 0, startLeft: 0, startTop: 0 },
panelPosition: { left: null, top: null, right: null, bottom: null }, // 记录位置
create() {
if (document.getElementById('gkbk-control-panel')) return;
// 创建完整面板
const panel = document.createElement('div');
panel.id = 'gkbk-control-panel';
panel.innerHTML = `
`;
panel.style.cssText = `
position: fixed;
bottom: 30px;
right: 30px;
width: 320px;
background: rgba(255,255,255,0.98);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
z-index: 99999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
color: #333;
backdrop-filter: blur(10px);
transition: opacity 0.2s;
`;
document.body.appendChild(panel);
this.panel = panel;
// 创建最小化图标(固定右下角,避免位置计算错误)
const icon = document.createElement('div');
icon.id = 'gkbk-minimized-icon';
icon.innerHTML = '⚙️';
icon.style.cssText = `
position: fixed;
bottom: 30px;
right: 30px;
width: 50px;
height: 50px;
background: #2d8cf0;
color: white;
border-radius: 50%;
display: none;
align-items: center;
justify-content: center;
font-size: 28px;
box-shadow: 0 4px 16px rgba(45, 140, 240, 0.4);
cursor: pointer;
z-index: 100000;
transition: transform 0.2s, box-shadow 0.2s;
border: 2px solid white;
`;
icon.addEventListener('click', (e) => {
e.stopPropagation();
this.restore();
});
icon.addEventListener('mouseenter', () => {
icon.style.transform = 'scale(1.1)';
icon.style.boxShadow = '0 6px 20px rgba(45, 140, 240, 0.6)';
});
icon.addEventListener('mouseleave', () => {
icon.style.transform = 'scale(1)';
icon.style.boxShadow = '0 4px 16px rgba(45, 140, 240, 0.4)';
});
document.body.appendChild(icon);
this.minimizedIcon = icon;
this.bindEvents();
this.initDrag();
this.loadSavedConfig();
},
initDrag() {
const header = this.panel.querySelector('.gkbk-panel-header');
const panel = this.panel;
const onMouseMove = (e) => {
if (!this.dragState.dragging) return;
e.preventDefault();
const dx = e.clientX - this.dragState.startX;
const dy = e.clientY - this.dragState.startY;
let newLeft = this.dragState.startLeft + dx;
let newTop = this.dragState.startTop + dy;
const panelRect = panel.getBoundingClientRect();
newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - panelRect.width));
newTop = Math.max(0, Math.min(newTop, window.innerHeight - panelRect.height));
panel.style.left = newLeft + 'px';
panel.style.top = newTop + 'px';
panel.style.right = 'auto';
panel.style.bottom = 'auto';
};
const onMouseUp = () => {
this.dragState.dragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
panel.style.transition = '';
};
header.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('gkbk-minimize-btn')) return;
e.preventDefault();
const rect = panel.getBoundingClientRect();
this.dragState = {
dragging: true,
startX: e.clientX,
startY: e.clientY,
startLeft: rect.left,
startTop: rect.top
};
panel.style.transition = 'none';
panel.style.left = rect.left + 'px';
panel.style.top = rect.top + 'px';
panel.style.right = 'auto';
panel.style.bottom = 'auto';
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
},
bindEvents() {
const minimizeBtn = this.panel.querySelector('.gkbk-minimize-btn');
minimizeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.minimize();
});
const speedInput = document.getElementById('gkbk-speed');
const speedVal = document.getElementById('gkbk-speed-val');
const currentSpeed = document.getElementById('current-speed');
speedInput.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
speedVal.textContent = val;
currentSpeed.textContent = val + 'x';
CONFIG.playbackRate.current = val;
Utils.storage.set('config_playbackRate', val);
this.applySpeedToCurrentVideo(val);
});
document.getElementById('gkbk-forum').addEventListener('change', (e) => {
CONFIG.features.autoForum = e.target.checked;
Utils.storage.set('config_forum', e.target.checked);
});
document.getElementById('gkbk-video').addEventListener('change', (e) => {
CONFIG.features.autoVideo = e.target.checked;
});
document.getElementById('gkbk-antidetect').addEventListener('change', (e) => {
CONFIG.antiDetect.enabled = e.target.checked;
if (e.target.checked) AntiDetect.start(); else AntiDetect.stop();
});
document.getElementById('gkbk-notify').addEventListener('change', (e) => {
CONFIG.features.notification = e.target.checked;
});
const modeFixed = document.getElementById('mode-fixed');
const modeAi = document.getElementById('mode-ai');
const fixedSettings = document.getElementById('fixed-settings');
const aiSettings = document.getElementById('ai-settings');
const currentMode = document.getElementById('current-mode');
const updateMode = () => {
if (modeAi.checked) {
CONFIG.forumReplyMode = 'ai';
fixedSettings.style.display = 'none';
aiSettings.style.display = 'block';
currentMode.textContent = 'AI生成';
currentMode.style.color = '#2d8cf0';
} else {
CONFIG.forumReplyMode = 'fixed';
fixedSettings.style.display = 'block';
aiSettings.style.display = 'none';
currentMode.textContent = '固定文本';
currentMode.style.color = '#19be6b';
}
Utils.storage.set('config_reply_mode', CONFIG.forumReplyMode);
};
modeFixed.addEventListener('change', updateMode);
modeAi.addEventListener('change', updateMode);
const saveAiConfig = () => {
CONFIG.aiConfig.apiUrl = document.getElementById('ai-api-url').value;
CONFIG.aiConfig.apiKey = document.getElementById('ai-api-key').value;
CONFIG.aiConfig.model = document.getElementById('ai-model').value;
CONFIG.aiConfig.temperature = parseFloat(document.getElementById('ai-temp').value);
Utils.storage.set('config_ai', CONFIG.aiConfig);
};
['ai-api-url', 'ai-api-key', 'ai-model', 'ai-temp'].forEach(id => {
document.getElementById(id).addEventListener('change', saveAiConfig);
});
document.getElementById('fixed-replies').addEventListener('change', (e) => {
const replies = e.target.value.split('\n').filter(l => l.trim());
CONFIG.fixedReplies = replies.length > 0 ? replies : CONFIG.fixedReplies;
Utils.storage.set('config_fixed_replies', CONFIG.fixedReplies);
});
document.getElementById('test-ai-btn').addEventListener('click', async () => {
saveAiConfig();
const resultDiv = document.getElementById('ai-test-result');
resultDiv.style.display = 'block';
resultDiv.textContent = '测试中...';
resultDiv.style.color = '#ff9900';
try {
const test = await AIReply.generate('测试帖子标题', '这是一个测试帖子内容,用于验证AI接口是否正常工作。');
resultDiv.textContent = '✅ 测试成功: ' + test.substring(0, 30) + '...';
resultDiv.style.color = '#19be6b';
} catch (e) {
resultDiv.textContent = '❌ 测试失败: ' + e.message;
resultDiv.style.color = '#ed4014';
}
});
},
minimize() {
if (this.isMinimized) return;
const panel = this.panel;
const computedStyle = window.getComputedStyle(panel);
const left = computedStyle.left;
const top = computedStyle.top;
const right = computedStyle.right;
const bottom = computedStyle.bottom;
// 保存位置
if (left !== 'auto' && top !== 'auto') {
this.panelPosition.left = left;
this.panelPosition.top = top;
this.panelPosition.right = null;
this.panelPosition.bottom = null;
} else {
this.panelPosition.right = right;
this.panelPosition.bottom = bottom;
this.panelPosition.left = null;
this.panelPosition.top = null;
}
panel.style.display = 'none';
this.minimizedIcon.style.display = 'flex';
this.isMinimized = true;
},
restore() {
if (!this.isMinimized) return;
const panel = this.panel;
if (this.panelPosition.left && this.panelPosition.top) {
panel.style.left = this.panelPosition.left;
panel.style.top = this.panelPosition.top;
panel.style.right = 'auto';
panel.style.bottom = 'auto';
} else if (this.panelPosition.right && this.panelPosition.bottom) {
panel.style.right = this.panelPosition.right;
panel.style.bottom = this.panelPosition.bottom;
panel.style.left = 'auto';
panel.style.top = 'auto';
} else {
// 默认右下角
panel.style.right = '30px';
panel.style.bottom = '30px';
panel.style.left = 'auto';
panel.style.top = 'auto';
}
panel.style.display = 'block';
this.minimizedIcon.style.display = 'none';
this.isMinimized = false;
},
forceShow() {
if (this.panel) {
this.panel.style.display = 'block';
this.panel.style.right = '30px';
this.panel.style.bottom = '30px';
this.panel.style.left = 'auto';
this.panel.style.top = 'auto';
if (this.minimizedIcon) {
this.minimizedIcon.style.display = 'none';
}
this.isMinimized = false;
Logger.info('控制面板已强制恢复显示');
}
},
applySpeedToCurrentVideo(rate) {
const video = document.querySelector('video');
const audio = document.querySelector('audio');
const media = video || audio;
if (media) {
media.playbackRate = rate;
Logger.info(`倍速已调整为 ${rate}x`);
}
},
loadSavedConfig() {
const savedRate = Utils.storage.get('config_playbackRate');
if (savedRate !== null) {
CONFIG.playbackRate.current = savedRate;
document.getElementById('gkbk-speed').value = savedRate;
document.getElementById('gkbk-speed-val').textContent = savedRate;
document.getElementById('current-speed').textContent = savedRate + 'x';
}
const savedMode = Utils.storage.get('config_reply_mode');
if (savedMode === 'ai') {
document.getElementById('mode-ai').checked = true;
document.getElementById('mode-fixed').checked = false;
document.getElementById('fixed-settings').style.display = 'none';
document.getElementById('ai-settings').style.display = 'block';
document.getElementById('current-mode').textContent = 'AI生成';
CONFIG.forumReplyMode = 'ai';
}
const savedAi = Utils.storage.get('config_ai');
if (savedAi) {
Object.assign(CONFIG.aiConfig, savedAi);
document.getElementById('ai-api-url').value = CONFIG.aiConfig.apiUrl;
document.getElementById('ai-api-key').value = CONFIG.aiConfig.apiKey || '';
document.getElementById('ai-model').value = CONFIG.aiConfig.model;
document.getElementById('ai-temp').value = CONFIG.aiConfig.temperature;
}
const savedFixed = Utils.storage.get('config_fixed_replies');
if (savedFixed && savedFixed.length > 0) {
CONFIG.fixedReplies = savedFixed;
document.getElementById('fixed-replies').value = savedFixed.join('\n');
}
const savedForum = Utils.storage.get('config_forum');
if (savedForum !== null) {
CONFIG.features.autoForum = savedForum;
document.getElementById('gkbk-forum').checked = savedForum;
}
}
};
// ==================== AI回帖模块 ====================
const AIReply = {
async generate(title, content) {
if (!CONFIG.aiConfig.apiKey) throw new Error('未配置API密钥');
const prompt = CONFIG.aiConfig.promptTemplate
.replace('{title}', title)
.replace('{content}', content.substring(0, 500));
const payload = {
model: CONFIG.aiConfig.model,
messages: [{ role: 'user', content: prompt }],
max_tokens: CONFIG.aiConfig.maxTokens,
temperature: CONFIG.aiConfig.temperature
};
Logger.info('正在请求AI生成回复...', CONFIG.aiConfig.model);
try {
const response = await Utils.request({
method: 'POST',
url: CONFIG.aiConfig.apiUrl,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.aiConfig.apiKey}`
},
data: JSON.stringify(payload)
});
const data = JSON.parse(response.responseText);
if (data.choices && data.choices[0] && data.choices[0].message) {
return data.choices[0].message.content.trim();
} else if (data.error) {
throw new Error(data.error.message || 'AI接口返回错误');
} else {
throw new Error('无法解析AI响应');
}
} catch (e) {
Logger.error('AI生成失败:', e);
throw e;
}
},
getFallbackReply(title, content) {
return FixedReply.generate(title, content);
}
};
// ==================== 固定回帖模块 ====================
const FixedReply = {
generate(title, content) {
const text = (title + ' ' + content).toLowerCase();
const keywords = {
'资料': ['资料很详细,感谢分享!{randomEmoji}', '这个资料整理得很棒,学习了!{randomEmoji}'],
'视频': ['视频讲解很清晰,受益匪浅。{randomEmoji}', '看完视频收获很大,感谢老师!{randomEmoji}'],
'作业': ['作业要求很明确,准备开始动手了。{randomEmoji}', '作业布置得很合理,有助于巩固知识。{randomEmoji}'],
'考试': ['考试重点总结得很好,正在复习中。{randomEmoji}', '感谢分享考试经验,很有参考价值!{randomEmoji}'],
'问题': ['这个问题提得很好,我也有同样的疑惑。{randomEmoji}', '看到大家的讨论,思路清晰多了。{randomEmoji}'],
'谢谢': ['互相帮助,共同进步!{randomEmoji}', '有问题一起讨论,学习氛围很好。{randomEmoji}']
};
for (const [key, replies] of Object.entries(keywords)) {
if (text.includes(key)) {
const selected = replies[Math.floor(Math.random() * replies.length)];
return Utils.replaceVariables(selected, { course: Utils.getCourseIdFromUrl() });
}
}
const defaultReply = CONFIG.fixedReplies[Math.floor(Math.random() * CONFIG.fixedReplies.length)];
return Utils.replaceVariables(defaultReply, { course: Utils.getCourseIdFromUrl() });
}
};
// ==================== DOM 操作封装 ====================
const DOM = {
async waitFor(selector, timeout = 10000, interval = 500) {
const start = Date.now();
while (Date.now() - start < timeout) {
const el = document.querySelector(selector);
if (el) return el;
await Utils.sleep(interval);
}
return null;
},
async waitForAll(selector, timeout = 10000, interval = 500) {
const start = Date.now();
while (Date.now() - start < timeout) {
const els = document.querySelectorAll(selector);
if (els.length > 0) return els;
await Utils.sleep(interval);
}
return null;
},
async click(selector, timeout = 5000) {
const el = await this.waitFor(selector, timeout);
if (el) {
el.click();
return true;
}
return false;
},
getText(selector) {
const el = document.querySelector(selector);
return el ? el.textContent.trim() : '';
},
extractPostContent() {
const titleSelectors = ['.topic-title', '.post-title', '.thread-title', '.discussion-title', 'h1', 'h2', '.title'];
const contentSelectors = ['.topic-content', '.post-content', '.thread-content', '.discussion-content', '.content', 'article', '.main-text'];
let title = '';
let content = '';
for (const sel of titleSelectors) {
const el = document.querySelector(sel);
if (el && el.textContent.trim()) {
title = el.textContent.trim();
break;
}
}
for (const sel of contentSelectors) {
const el = document.querySelector(sel);
if (el && el.textContent.trim()) {
content = el.textContent.trim();
break;
}
}
if (!content) {
const allText = document.querySelectorAll('p, div');
let maxLen = 0;
for (const el of allText) {
const text = el.textContent.trim();
if (text.length > maxLen && text.length < 1000) {
maxLen = text.length;
content = text;
}
}
}
return { title: title || '无标题', content: content || '内容很精彩,学习了!' };
}
};
// ==================== 核心任务处理器 ====================
const TaskHandlers = {
async handleVideo() {
if (!CONFIG.features.autoVideo) {
Logger.info('自动视频已关闭,跳过');
await Utils.wait(CONFIG.intervals.viewPage);
await Bot.returnToCoursePage();
return;
}
Logger.info('正在处理视频/音频任务...');
const playBtnSelectors = [
'.mvp-toggle-play',
'.vjs-big-play-button',
'.play-button',
'button[title*="播放"]',
'button[aria-label*="播放"]',
'.video-play-btn'
];
let playBtn = null;
for (const sel of playBtnSelectors) {
playBtn = await DOM.waitFor(sel, 5000);
if (playBtn) break;
}
const video = await DOM.waitFor('video', 15000);
const audio = !video ? await DOM.waitFor('audio', 5000) : null;
const media = video || audio;
if (!media) {
Logger.warn('未找到视频/音频元素,按页面类型处理');
await Utils.wait(CONFIG.intervals.viewPage);
await Bot.returnToCoursePage();
return;
}
if (playBtn) {
Logger.info('找到原生播放按钮,模拟点击启动播放');
playBtn.click();
} else {
Logger.info('未找到播放按钮,直接调用 media.play()');
media.playbackRate = CONFIG.playbackRate.current;
media.volume = 0;
media.muted = true;
const playPromise = media.play();
if (playPromise) playPromise.catch(e => Logger.warn('自动播放被阻止:', e));
}
const duration = media.duration || 0;
const targetRate = CONFIG.playbackRate.current;
Logger.info(`媒体时长: ${Math.floor(duration/60)}分, 目标倍速: ${targetRate}x`);
const resumeViaButton = async () => {
if (window.GKBK_PAUSED || window.GKBK_STOPPED) return;
const btn = document.querySelector('.mvp-toggle-play') ||
document.querySelector('.vjs-big-play-button') ||
document.querySelector('button[title*="播放"]');
if (btn) {
Logger.info('视频暂停,通过点击播放按钮恢复');
btn.click();
} else {
Logger.warn('未找到播放按钮,尝试直接play');
media.play().catch(() => {});
}
};
media.addEventListener('pause', () => {
if (!window.GKBK_PAUSED && !window.GKBK_STOPPED) {
Logger.warn('视频被暂停,尝试通过按钮恢复');
resumeViaButton();
}
});
media.addEventListener('ended', () => {
Logger.info('媒体播放结束');
Bot.returnToCoursePage();
});
const checkTimer = setInterval(() => {
if (window.GKBK_STOPPED) {
clearInterval(checkTimer);
return;
}
if (Math.abs(media.playbackRate - targetRate) > 0.1) {
media.playbackRate = targetRate;
}
if (media.paused && !window.GKBK_PAUSED) {
resumeViaButton();
}
}, CONFIG.intervals.onlineVideo);
if (duration > 0 && media.currentTime >= duration - 1) {
Logger.info('视频似乎已经看完');
clearInterval(checkTimer);
setTimeout(() => Bot.returnToCoursePage(), 3000);
}
},
async handlePage() {
Logger.info('处理页面浏览任务...');
await Utils.wait(CONFIG.intervals.viewPage);
await Bot.returnToCoursePage();
},
async handleWebLink() {
Logger.info('处理线上链接任务...');
const btn = await DOM.waitFor('.open-link-button', 10000);
if (btn) {
btn.target = '_self';
btn.href = 'javascript:void(0);';
btn.click();
}
await Utils.wait(CONFIG.intervals.webLink);
await Bot.returnToCoursePage();
},
async handleMaterial() {
if (!CONFIG.features.autoMaterial) {
await Utils.wait(CONFIG.intervals.material);
await Bot.returnToCoursePage();
return;
}
Logger.info('处理参考资料任务...');
try {
const activityId = Utils.getActivityIdFromUrl();
if (!activityId) throw new Error('无法获取活动ID');
const res = await $.ajax({
url: `https://lms.ouchn.cn/api/activities/${activityId}`,
type: 'GET'
});
if (res.uploads && res.uploads.length > 0) {
for (const upload of res.uploads) {
await Utils.wait(CONFIG.intervals.material);
await $.ajax({
url: `https://lms.ouchn.cn/api/course/activities-read/${activityId}`,
type: 'POST',
data: JSON.stringify({ upload_id: upload.id }),
contentType: 'application/json'
});
Logger.debug(`标记资料 ${upload.id} 已读`);
}
}
} catch (e) {
Logger.error('标记资料失败:', e);
}
await Utils.wait(CONFIG.intervals.material);
await Bot.returnToCoursePage();
},
async handleForum() {
if (!CONFIG.features.autoForum) {
Logger.info('自动回帖已关闭,跳过');
await Utils.wait(CONFIG.intervals.forum);
await Bot.returnToCoursePage();
return;
}
Logger.info(`处理论坛任务,当前模式: ${CONFIG.forumReplyMode === 'ai' ? 'AI生成' : '固定文本'}`);
await Utils.wait(CONFIG.intervals.forum * 2);
try {
const selectors = [
'.topic-title', '.post-title', '.thread-title',
'.discussion-title', '.title',
'.topic-list a', '.discussion-list a',
'.item a', 'a[href*="topic"]', 'a[href*="discussion"]'
];
let postLink = null;
for (const sel of selectors) {
const els = document.querySelectorAll(sel);
for (const el of els) {
if (el.offsetParent !== null && el.href) {
postLink = el;
break;
}
}
if (postLink) break;
}
if (postLink) {
Logger.info('找到帖子,点击进入...');
postLink.click();
await Utils.sleep(5000);
await this.tryReplyInCurrentPage();
} else {
Logger.warn('未找到帖子,跳过');
await Bot.returnToCoursePage();
}
} catch (e) {
Logger.error('论坛处理失败:', e);
await Bot.returnToCoursePage();
}
},
async tryReplyInCurrentPage() {
const editor = await DOM.waitFor('[contenteditable="true"]', 5000);
if (!editor) return false;
const submitBtn = await this.findSubmitButton();
if (!submitBtn) return false;
const { title, content } = DOM.extractPostContent();
Logger.info(`提取到帖子: "${title.substring(0, 30)}..."`);
let replyContent = '';
let replyType = '';
if (CONFIG.forumReplyMode === 'ai' && CONFIG.aiConfig.apiKey) {
try {
replyContent = await AIReply.generate(title, content);
replyType = 'ai';
Logger.info('AI生成回复成功');
} catch (e) {
Logger.warn('AI生成失败,使用固定文本兜底:', e.message);
replyContent = FixedReply.generate(title, content);
replyType = 'fixed';
}
} else {
replyContent = FixedReply.generate(title, content);
replyType = 'fixed';
Logger.info('使用固定文本回复');
}
editor.innerHTML = `${replyContent}
`;
editor.focus();
editor.dispatchEvent(new Event('input', { bubbles: true }));
await Utils.sleep(1000);
if (!editor.textContent.trim() || editor.textContent.trim().length < 5) {
editor.textContent = replyContent;
}
submitBtn.click();
Logger.info(`已提交回帖 (${replyType})`);
await Utils.wait(CONFIG.intervals.forum);
await Bot.returnToCoursePage();
return true;
},
async findSubmitButton() {
const selectors = [
'button.ivu-btn-primary',
'button[type="button"].ivu-btn-primary',
'button.submit-reply', 'button.post-reply',
'.reply-footer button', '.post-btn'
];
for (const sel of selectors) {
const btn = document.querySelector(sel);
if (btn && (btn.textContent.includes('发表') || btn.textContent.includes('回帖') ||
btn.textContent.includes('提交') || btn.textContent.includes('回复'))) {
return btn;
}
}
const allBtns = document.querySelectorAll('button.ivu-btn-primary');
return allBtns[allBtns.length - 1] || null;
}
};
// ==================== 主控制器 ====================
const Bot = {
async init() {
Logger.info('国开智能刷课助手 v2.1.1 初始化...');
const savedRate = Utils.storage.get('config_playbackRate');
if (savedRate) CONFIG.playbackRate.current = savedRate;
Utils.requestNotifyPermission();
ControlPanel.create();
AntiDetect.start();
if (window.GKBK_STOPPED) {
Logger.info('脚本处于停止状态,本次不执行');
return;
}
await Utils.sleep(2000);
await this.route();
},
async route() {
const url = window.location.href;
if (url.includes('/user/courses')) {
await this.handleCourseCenter();
} else if (url.includes('/learning-activity/') && !url.includes('full-screen')) {
await this.handleForumDetail();
} else if (url.match(/\/course\/\d+\/ng.*#\/$/)) {
await this.handleCourseIndex();
} else if (url.includes('learning-activity/full-screen')) {
await this.handleTaskPage();
}
},
async handleCourseCenter() {
Logger.info('正在课程中心寻找未完成课程...');
const completedCourses = Utils.storage.get('completed_courses', []);
await Utils.wait(CONFIG.intervals.loadCourse * 2);
const cardSelectors = [
'.my-course-list .course-item',
'.course-list .course-item',
'.el-card.course-item',
'.course-panel',
'[class*="course-item"]',
'.el-card'
];
let cards = null;
for (const sel of cardSelectors) {
cards = await DOM.waitForAll(sel, 5000);
if (cards && cards.length > 0) break;
}
if (!cards || cards.length === 0) {
const links = document.querySelectorAll('a[href*="/course/"]');
for (const link of links) {
const courseId = Utils.getCourseIdFromUrl(link.href);
if (!courseId || completedCourses.includes(courseId)) continue;
const progress = this.extractProgress(link.parentElement);
if (progress < CONFIG.completionThreshold) {
Logger.info(`找到未完成课程(ID:${courseId}), 进度:${progress}%`);
link.click();
return;
}
}
Logger.info('所有课程似乎已完成');
Utils.notify('刷课完成', '没有找到进度低于90%的课程');
return;
}
for (const card of cards) {
const link = card.querySelector('a[href*="/course/"]') || card.querySelector('a');
if (!link) continue;
const courseId = Utils.getCourseIdFromUrl(link.href);
if (!courseId || completedCourses.includes(courseId)) continue;
const progress = this.extractProgress(card);
Logger.info(`课程 ${courseId} 进度: ${progress}%`);
if (progress < CONFIG.completionThreshold) {
Logger.info(`进入课程: ${courseId}`);
link.click();
return;
}
}
Logger.info('所有课程已刷完或进度达标');
Utils.notify('刷课完成', '课程中心所有课程进度已达90%以上');
},
extractProgress(element) {
if (!element) return 100;
const text = element.textContent;
const match = text.match(/(\d+)%/);
return match ? parseInt(match[1]) : 100;
},
async handleCourseIndex() {
Logger.info('正在课程首页处理...');
const courseId = Utils.getCourseIdFromUrl();
if (!courseId) {
Logger.error('无法获取课程ID');
return;
}
await this.expandAllTasks();
const tasks = await DOM.waitForAll('.learning-activity .clickable-area', 10000);
if (!tasks) {
Logger.error('未找到课程任务');
return;
}
for (const task of tasks) {
const typeIcon = task.querySelector('i.font[original-title]');
const type = typeIcon ? typeIcon.getAttribute('original-title') : '';
const typeEum = this.getTypeEum(type);
if (!typeEum) continue;
const statusEl = task.querySelector('.ivu-tooltip-inner b');
const isCompleted = statusEl && statusEl.textContent === '已完成';
if (!isCompleted) {
Logger.info(`发现未完成任务: ${type} (${typeEum})`);
Utils.storage.set(`typeEum-${courseId}`, typeEum);
Utils.storage.set('last_task', { courseId, timestamp: Date.now() });
task.click();
return;
}
}
Logger.info(`课程 ${courseId} 所有任务已完成`);
const completed = Utils.storage.get('completed_courses', []);
if (!completed.includes(courseId)) {
completed.push(courseId);
Utils.storage.set('completed_courses', completed);
}
Utils.notify('课程完成', `课程 ${courseId} 所有任务已刷完`);
await this.returnToCourseCenter();
},
async expandAllTasks() {
let attempts = 0;
while (attempts < 5) {
const collapsed = document.querySelector('i.font-toggle-all-collapsed');
const expanded = document.querySelector('i.font-toggle-all-expanded');
if (collapsed && !expanded) {
collapsed.click();
await Utils.sleep(2000);
} else if (expanded) {
Logger.info('课程任务已展开');
return;
}
attempts++;
await Utils.sleep(1000);
}
},
getTypeEum(type) {
const map = {
'页面': 'page',
'音视频教材': 'online_video',
'线上链接': 'web_link',
'讨论': 'forum',
'参考资料': 'material'
};
return map[type] || null;
},
async handleTaskPage() {
const courseId = Utils.getCourseIdFromUrl();
const activityId = Utils.getActivityIdFromUrl();
const typeEum = Utils.storage.get(`typeEum-${courseId}`);
if (!activityId || !typeEum) {
Logger.error('缺少活动ID或类型信息');
await this.returnToCoursePage();
return;
}
Logger.info(`处理任务: ${typeEum}, Activity: ${activityId}`);
await this.reportLearning(activityId, typeEum);
switch (typeEum) {
case 'page':
await TaskHandlers.handlePage();
break;
case 'online_video':
await TaskHandlers.handleVideo();
break;
case 'web_link':
await TaskHandlers.handleWebLink();
break;
case 'forum':
await TaskHandlers.handleForum();
break;
case 'material':
await TaskHandlers.handleMaterial();
break;
default:
Logger.warn(`未知任务类型: ${typeEum}`);
await Utils.wait(CONFIG.intervals.other);
await this.returnToCoursePage();
}
},
async handleForumDetail() {
Logger.info('在论坛详情页,尝试回帖...');
const success = await TaskHandlers.tryReplyInCurrentPage();
if (!success) {
Logger.warn('论坛详情页处理失败,返回');
await this.returnToCoursePage();
}
},
async reportLearning(activityId, activityType) {
try {
const duration = Math.ceil(Math.random() * 300 + 40);
const payload = {
activity_id: activityId,
activity_type: activityType,
browser: 'chrome',
course_id: window.globalData?.course?.id || '',
course_code: window.globalData?.course?.courseCode || '',
course_name: window.globalData?.course?.name || '',
org_id: window.globalData?.course?.orgId || '',
org_name: window.globalData?.user?.orgName || '',
dep_id: window.globalData?.dept?.id || '',
dep_name: window.globalData?.dept?.name || '',
dep_code: window.globalData?.dept?.code || '',
user_agent: navigator.userAgent,
user_id: window.globalData?.user?.id || '',
user_name: window.globalData?.user?.name || '',
user_no: window.globalData?.user?.userNo || '',
visit_duration: duration
};
await $.ajax({
url: 'https://lms.ouchn.cn/statistics/api/user-visits',
type: 'POST',
data: JSON.stringify(payload),
contentType: 'text/plain;charset=UTF-8'
});
Logger.debug('学习行为上报成功');
} catch (e) {
Logger.warn('学习行为上报失败:', e);
}
},
async returnToCoursePage() {
await Utils.sleep(2000);
const backBtn = await DOM.waitFor('a.full-screen-mode-back', 3000);
if (backBtn) {
backBtn.click();
} else {
history.back();
}
},
async returnToCourseCenter() {
await Utils.sleep(2000);
window.location.href = 'https://lms.ouchn.cn/user/courses#/';
}
};
// ==================== 启动 ====================
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => Bot.init());
} else {
Bot.init();
}
window.GKBK = {
status: () => ({ mode: CONFIG.forumReplyMode, speed: CONFIG.playbackRate.current }),
config: CONFIG,
testReply: async (mode = 'fixed') => {
CONFIG.forumReplyMode = mode;
const { title, content } = DOM.extractPostContent();
if (mode === 'ai') {
return await AIReply.generate(title, content);
} else {
return FixedReply.generate(title, content);
}
}
};
// 暴露应急恢复方法到全局
window.GKBK_forceShowPanel = () => ControlPanel.forceShow();
})();