// ==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 = `
⚙️ 刷课控制面板
💬 回帖模式
预设回复库(随机选择):
变量: {time} {course} {randomEmoji}
状态: 运行中 | 回帖模式: 固定文本
`; 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); } } }; })();