// ==UserScript==
// @name 📚 国开智能刷课助手 Pro
// @namespace http://tampermonkey.net/
// @version 1.1.0
// @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: {
max: 4,
min: 1,
gradual: true,
gradualStep: 0.5,
gradualInterval: 5000
},
// 延迟设置
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,
statistics: true
},
// 回帖模式: 'ai' | 'fixed'
forumReplyMode: 'fixed',
// AI配置
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请直接输出回复内容,不要带任何前缀或说明。'
},
// 固定文本库(支持变量:{time}, {course}, {randomEmoji})
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;
},
// 通用HTTP请求(支持GM_xmlhttpRequest)
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
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);
if (window.GKBKConsole) {
window.GKBKConsole.addLog(Object.keys(this.levels)[level], args.join(' '));
}
},
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 Statistics = {
data: {
startTime: Date.now(),
todayDuration: Utils.storage.get('today_duration', 0),
lastDate: Utils.storage.get('last_date', new Date().toDateString()),
completedTasks: 0,
currentCourse: '',
currentProgress: 0,
aiReplies: 0,
fixedReplies: 0
},
init() {
const today = new Date().toDateString();
if (this.data.lastDate !== today) {
this.data.todayDuration = 0;
this.data.lastDate = today;
Utils.storage.set('today_duration', 0);
Utils.storage.set('last_date', today);
}
this.startDurationTracker();
if (CONFIG.features.statistics) {
this.createPanel();
}
},
startDurationTracker() {
setInterval(() => {
const now = Date.now();
const sessionDuration = Math.floor((now - this.data.startTime) / 1000);
this.data.todayDuration += 1;
if (sessionDuration % 60 === 0) {
Utils.storage.set('today_duration', this.data.todayDuration);
}
this.updatePanel();
}, 1000);
},
addCompletedTask(type = 'unknown') {
this.data.completedTasks++;
if (type === 'ai') this.data.aiReplies++;
if (type === 'fixed') this.data.fixedReplies++;
this.updatePanel();
},
setCurrentCourse(name, progress) {
this.data.currentCourse = name || '未知课程';
this.data.currentProgress = progress || 0;
this.updatePanel();
},
createPanel() {
if (document.getElementById('gkbk-stats-panel')) return;
const panel = document.createElement('div');
panel.id = 'gkbk-stats-panel';
panel.innerHTML = `
📊 学习统计
−
⏱️ 今日学习: 0分0秒
✅ 本次任务: 0 个
🤖 AI回帖: 0 |
📝 固定回帖: 0
📖 当前课程: -
📈 课程进度: 0%
国开智能刷课助手 Pro v3.1
`;
document.body.appendChild(panel);
document.getElementById('gkbk-minimize').addEventListener('click', () => {
const content = document.getElementById('gkbk-stats-content');
content.style.display = content.style.display === 'none' ? 'block' : 'none';
});
},
updatePanel() {
const todayTime = document.getElementById('gkbk-today-time');
const tasks = document.getElementById('gkbk-tasks');
const aiCount = document.getElementById('gkbk-ai-count');
const fixedCount = document.getElementById('gkbk-fixed-count');
const course = document.getElementById('gkbk-course');
const progress = document.getElementById('gkbk-progress');
const progressBar = document.getElementById('gkbk-progress-bar');
if (todayTime) {
const mins = Math.floor(this.data.todayDuration / 60);
const secs = this.data.todayDuration % 60;
todayTime.textContent = `${mins}分${secs}秒`;
}
if (tasks) tasks.textContent = this.data.completedTasks;
if (aiCount) aiCount.textContent = this.data.aiReplies;
if (fixedCount) fixedCount.textContent = this.data.fixedReplies;
if (course) course.textContent = this.data.currentCourse;
if (progress) progress.textContent = `${this.data.currentProgress}%`;
if (progressBar) progressBar.style.width = `${this.data.currentProgress}%`;
}
};
// ==================== 悬浮控制面板(增强版) ====================
const ControlPanel = {
create() {
if (document.getElementById('gkbk-control-panel')) return;
const panel = document.createElement('div');
panel.id = 'gkbk-control-panel';
panel.innerHTML = `
`;
document.body.appendChild(panel);
this.bindEvents();
this.loadSavedConfig();
},
bindEvents() {
// 速度滑块
const speedInput = document.getElementById('gkbk-speed');
const speedVal = document.getElementById('gkbk-speed-val');
speedInput.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
speedVal.textContent = val;
CONFIG.playbackRate.max = val;
Utils.storage.set('config_playbackRate', 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);
// AI配置保存
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);
});
// 测试AI
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';
}
});
// 暂停/继续
let isPaused = false;
document.getElementById('gkbk-pause').addEventListener('click', (e) => {
isPaused = !isPaused;
window.GKBK_PAUSED = isPaused;
e.target.textContent = isPaused ? '▶ 继续' : '⏸ 暂停';
e.target.style.background = isPaused ? '#19be6b' : '#ff9900';
document.getElementById('gkbk-status').textContent = isPaused ? '状态: 已暂停' : `状态: 运行中 | 回帖模式: ${modeAi.checked ? 'AI生成' : '固定文本'}`;
Logger.info(isPaused ? '用户暂停脚本' : '用户恢复脚本');
});
// 停止
document.getElementById('gkbk-stop').addEventListener('click', () => {
window.GKBK_STOPPED = true;
AntiDetect.stop();
Logger.info('用户停止脚本');
document.getElementById('gkbk-status').textContent = '状态: 已停止';
Utils.notify('刷课已停止', '脚本已被用户手动终止');
});
},
loadSavedConfig() {
// 恢复保存的配置
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;
}
},
// 备用:如果AI失败,使用智能固定文本
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 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;
}
const duration = media.duration || 0;
let targetRate = CONFIG.playbackRate.max;
if (duration < 300) targetRate = Math.min(2, targetRate);
else if (duration > 1800) targetRate = CONFIG.playbackRate.max;
Logger.info(`媒体时长: ${Math.floor(duration/60)}分, 目标倍速: ${targetRate}x`);
if (CONFIG.playbackRate.gradual) {
let currentRate = 1;
media.playbackRate = currentRate;
const speedTimer = setInterval(() => {
if (window.GKBK_PAUSED || window.GKBK_STOPPED) return;
if (currentRate < targetRate) {
currentRate = Math.min(currentRate + CONFIG.playbackRate.gradualStep, targetRate);
media.playbackRate = currentRate;
Logger.debug(`加速至 ${currentRate}x`);
} else {
clearInterval(speedTimer);
}
}, CONFIG.playbackRate.gradualInterval);
} else {
media.playbackRate = targetRate;
}
media.volume = 0;
media.muted = true;
const playPromise = media.play();
if (playPromise) playPromise.catch(e => Logger.warn('自动播放被阻止:', e));
media.addEventListener('pause', () => {
if (!window.GKBK_PAUSED && !window.GKBK_STOPPED) {
Logger.warn('视频被暂停,尝试恢复');
media.play().catch(() => {});
}
});
media.addEventListener('ended', () => {
Logger.info('媒体播放结束');
Statistics.addCompletedTask();
Bot.returnToCoursePage();
});
const checkTimer = setInterval(() => {
if (window.GKBK_STOPPED) {
clearInterval(checkTimer);
return;
}
if (media.paused && !window.GKBK_PAUSED) {
media.play().catch(() => {});
}
if (media.playbackRate < targetRate && !CONFIG.playbackRate.gradual) {
media.playbackRate = targetRate;
}
}, CONFIG.intervals.onlineVideo);
if (duration > 0 && media.currentTime >= duration - 1) {
Logger.info('视频似乎已经看完');
clearInterval(checkTimer);
Statistics.addCompletedTask();
setTimeout(() => Bot.returnToCoursePage(), 3000);
}
},
async handlePage() {
Logger.info('处理页面浏览任务...');
await Utils.wait(CONFIG.intervals.viewPage);
Statistics.addCompletedTask();
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);
Statistics.addCompletedTask();
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);
Statistics.addCompletedTask();
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) {
// 兜底:直接设置textContent
editor.textContent = replyContent;
}
submitBtn.click();
Logger.info(`已提交回帖 (${replyType})`);
await Utils.wait(CONFIG.intervals.forum);
Statistics.addCompletedTask(replyType);
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('国开智能刷课助手 Pro v1.1.0 初始化...');
const savedRate = Utils.storage.get('config_playbackRate');
if (savedRate) CONFIG.playbackRate.max = savedRate;
Utils.requestNotifyPermission();
ControlPanel.create();
Statistics.init();
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('正在课程中心寻找未完成课程...');
Statistics.setCurrentCourse('课程中心', 0);
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}%`);
Statistics.setCurrentCourse(`课程 ${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}`);
Statistics.setCurrentCourse(`课程 ${courseId}`, progress);
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;
}
Statistics.setCurrentCourse(`课程 ${courseId}`, 0);
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 = {
pause: () => { window.GKBK_PAUSED = true; Logger.info('已暂停'); },
resume: () => { window.GKBK_PAUSED = false; Logger.info('已恢复'); },
stop: () => { window.GKBK_STOPPED = true; AntiDetect.stop(); Logger.info('已停止'); },
status: () => ({ paused: window.GKBK_PAUSED, stopped: window.GKBK_STOPPED, mode: CONFIG.forumReplyMode }),
config: CONFIG,
statistics: Statistics.data,
// 手动触发回帖测试
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);
}
}
};
})();