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