// ==UserScript== // @name 雨课堂刷课助手 // @namespace http://tampermonkey.net/ // @version 3.0.4 // @description 针对雨课堂视频进行自动播放,配置AI自动答题 // @author 风之子 // @license GPL3 // @match *://*.yuketang.cn/* // @match *://*.gdufemooc.cn/* // @run-at document-start // @icon http://yuketang.cn/favicon.ico // @grant unsafeWindow // @grant GM_xmlhttpRequest // @connect api.openai.com // @connect api.moonshot.cn // @connect api.deepseek.com // @connect dashscope.aliyuncs.com // @connect api.anthropic.com // @connect * // @connect cdn.jsdelivr.net // @connect unpkg.com // @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js // @require https://unpkg.com/tesseract.js@v2.1.0/dist/tesseract.min.js // ==/UserScript== (() => { 'use strict'; let panel; // UI 面板实例后置初始化 // ---- 脚本配置,用户可修改 ---- const Config = { version: '3.0.4', // 版本号 playbackRate: 2, // 视频播放倍速 pptInterval: 3000, // ppt翻页间隔 storageKeys: { // 使用者勿动 progress: '[雨课堂脚本]刷课进度信息', ai: 'ykt_ai_conf', proClassCount: 'pro_lms_classCount', feature: 'ykt_feature_conf', // 是否开启AI作答/自动评论 pendingAutoStart: 'ykt_pending_auto_start' } }; const Utils = { // 短暂睡眠,等待网页加载 sleep: (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms)), // 将一个 JSON 字符串解析为 JavaScript 对象 safeJSONParse(value, fallback) { try { return JSON.parse(value); } catch (_) { return fallback; } }, // 每隔一段时间检查某个条件是否满足(通过 checker 函数),如果满足就成功返回;如果超时仍未满足,就失败返回 poll(checker, { interval = 1000, timeout = 20000 } = {}) { return new Promise(resolve => { const start = Date.now(); const timer = setInterval(() => { if (checker()) { clearInterval(timer); resolve(true); return; } if (Date.now() - start > timeout) { clearInterval(timer); resolve(false); } }, interval); }); }, // 使用UI课程完成度来判别是否完成课程 isProgressDone(text) { if (!text) return false; return text.includes('100%') || text.includes('99%') || text.includes('98%') || text.includes('已完成'); }, // 主要是规避firefox会创建多个iframe的问题 inIframe() { return window.top !== window.self; }, // 下滑到最底部,触发课程加载 scrollToBottom(containerSelector) { const el = document.querySelector(containerSelector); if (el) el.scrollTop = el.scrollHeight; }, getCurrentClassroomId() { const query = new URLSearchParams(location.search); const queryId = query.get('classroom_id'); if (queryId) return queryId; const path = location.pathname; return path.match(/^\/ai-workspace\/lms-graph\/([^/]+)/)?.[1] || path.match(/^\/v2\/web\/studentLog\/([^/]+)/)?.[1] || ''; }, isSupportedLearningPage() { const path = location.pathname; return path.includes('/ai-workspace/lms-graph/') || path.includes('/v2/web/') || path.includes('/pro/lms/'); }, waitForMountTarget(timeout = 15000) { const getTarget = () => document.body || document.documentElement; const existing = getTarget(); if (existing) return Promise.resolve(existing); return new Promise(resolve => { let done = false; const finish = () => { if (done) return; done = true; observer.disconnect(); clearTimeout(timer); resolve(getTarget()); }; const observer = new MutationObserver(() => { if (getTarget()) finish(); }); observer.observe(document, { childList: true, subtree: true }); document.addEventListener('DOMContentLoaded', finish, { once: true }); window.addEventListener('load', finish, { once: true }); const timer = setTimeout(finish, timeout); }); }, async getDDL() { const element = document.querySelector('video') || document.querySelector('audio'); const fallback = 180_000; if (!element) return fallback; let duration = Number(element.duration); if (!Number.isFinite(duration) || duration <= 0) { await new Promise(resolve => element.addEventListener('loadedmetadata', resolve, { once: true })); duration = Number(element.duration); } const elementDurationMs = duration * 1000; // 转为秒 const timeout = Math.max(elementDurationMs * 3, 10_000); // 至少 10 秒(防极短视频); return timeout; }, // 关闭雨课堂的挂机/离开检测弹窗,避免遮罩拦截刷课流程 dismissPopups() { const wrappers = document.querySelectorAll('.el-dialog__wrapper, .el-message-box__wrapper'); for (const wrapper of wrappers) { const style = getComputedStyle(wrapper); const rect = wrapper.getBoundingClientRect(); if (style.display === 'none' || style.visibility === 'hidden' || rect.width === 0) continue; const text = wrapper.innerText || ''; const buttons = [...wrapper.querySelectorAll('button')]; const clickBtn = label => { const btn = buttons.find(b => (b.innerText || '').trim().includes(label)); if (btn) btn.click(); }; if (text.includes('好好学习') || text.includes('继续观看')) { clickBtn('继续观看'); } else if (text.includes('报告老师')) { clickBtn('取消'); } } } }; // ---- 存储工具 ---- const Store = { getProgress(url) { const raw = localStorage.getItem(Config.storageKeys.progress); const all = Utils.safeJSONParse(raw, {}) || { url: { outside: 0, inside: 0 } }; if (!all[url]) { all[url] = { outside: 0, inside: 0 }; localStorage.setItem(Config.storageKeys.progress, JSON.stringify(all)); } return { all, current: all[url] }; }, setProgress(url, outside, inside = 0) { const raw = localStorage.getItem(Config.storageKeys.progress); const all = Utils.safeJSONParse(raw, {}); all[url] = { outside, inside }; localStorage.setItem(Config.storageKeys.progress, JSON.stringify(all)); }, removeProgress(url) { const raw = localStorage.getItem(Config.storageKeys.progress); const all = Utils.safeJSONParse(raw, {}); delete all[url]; localStorage.setItem(Config.storageKeys.progress, JSON.stringify(all)); }, getAIConf() { const raw = localStorage.getItem(Config.storageKeys.ai); const saved = Utils.safeJSONParse(raw, {}) || {}; const conf = { url: saved.url ?? "https://api.deepseek.com/chat/completions", key: saved.key ?? "sk-xxxxxxx", model: saved.model ?? "deepseek-chat", apiFormat: saved.apiFormat ?? "openai", // openai 或 anthropic authMethod: saved.authMethod ?? "bearer", // bearer 或 x-api-key }; localStorage.setItem(Config.storageKeys.ai, JSON.stringify(conf)); return conf; }, setAIConf(conf) { localStorage.setItem(Config.storageKeys.ai, JSON.stringify(conf)); }, getProClassCount() { const value = localStorage.getItem(Config.storageKeys.proClassCount); return value ? Number(value) : 1; }, setProClassCount(count) { localStorage.setItem(Config.storageKeys.proClassCount, count); }, getFeatureConf() { const raw = localStorage.getItem(Config.storageKeys.feature); const saved = Utils.safeJSONParse(raw, {}) || {}; const conf = { autoAI: saved.autoAI ?? false, autoComment: saved.autoComment ?? false, }; localStorage.setItem(Config.storageKeys.feature, JSON.stringify(conf)); return conf; }, setFeatureConf(conf) { localStorage.setItem(Config.storageKeys.feature, JSON.stringify(conf)); }, getPendingAutoStart() { const raw = localStorage.getItem(Config.storageKeys.pendingAutoStart); const saved = Utils.safeJSONParse(raw, null); if (!saved || !saved.classroomId || !saved.ts) return null; if (Date.now() - saved.ts > 30 * 60 * 1000) { localStorage.removeItem(Config.storageKeys.pendingAutoStart); return null; } return saved; }, setPendingAutoStart(classroomId = '', returnUrl = '') { if (!classroomId) return; const prev = this.getPendingAutoStart() || {}; localStorage.setItem(Config.storageKeys.pendingAutoStart, JSON.stringify({ classroomId, returnUrl: returnUrl || prev.returnUrl || '', ts: Date.now() })); }, clearPendingAutoStart() { localStorage.removeItem(Config.storageKeys.pendingAutoStart); } }; // ---- UI 面板 ---- function createPanel() { const iframe = document.createElement('iframe'); iframe.style.position = 'fixed'; iframe.style.top = '40px'; iframe.style.left = '40px'; iframe.style.width = '520px'; iframe.style.height = '340px'; iframe.style.zIndex = '999999'; iframe.style.border = '1px solid #a3a3a3'; iframe.style.borderRadius = '10px'; iframe.style.background = '#fff'; iframe.style.overflow = 'hidden'; iframe.style.boxShadow = '6px 4px 17px 2px #000000'; iframe.setAttribute('frameborder', '0'); iframe.setAttribute('id', 'ykt-helper-iframe'); iframe.setAttribute('allowtransparency', 'true'); const mountTarget = document.body || document.documentElement; if (!mountTarget) { throw new Error('面板挂载点不存在'); } mountTarget.appendChild(iframe); const doc = iframe.contentDocument || iframe.contentWindow.document; doc.open(); doc.write(`
展开
`); doc.close(); const ui = { iframe, doc, panel: doc.getElementById('panel'), header: doc.getElementById('header'), info: doc.getElementById('info'), btnStart: doc.getElementById('btn-start'), btnClear: doc.getElementById('btn-clear'), btnSetting: doc.getElementById('btn-setting'), btnStop: doc.getElementById('btn-stop'), btnReload: doc.getElementById('btn-reload'), settings: doc.getElementById('settings'), saveSettings: doc.getElementById('save_settings'), closeSettings: doc.getElementById('close_settings'), aiUrlInput: doc.getElementById('ai_url'), aiKeyInput: doc.getElementById('ai_key'), aiModelInput: doc.getElementById('ai_model'), aiFormatSelect: doc.getElementById('ai_format'), authMethodSelect: doc.getElementById('auth_method'), featureAutoAI: doc.getElementById('feature_auto_ai'), featureAutoComment: doc.getElementById('feature_auto_comment'), minimality: doc.getElementById('minimality'), question: doc.getElementById('question'), miniBasic: doc.getElementById('mini-basic') }; let isDragging = false; let startX = 0, startY = 0, startLeft = 0, startTop = 0; const hostWindow = window.parent || window; const onMove = e => { if (!isDragging) return; const deltaX = e.screenX - startX; const deltaY = e.screenY - startY; const maxLeft = Math.max(0, hostWindow.innerWidth - iframe.offsetWidth); const maxTop = Math.max(0, hostWindow.innerHeight - iframe.offsetHeight); iframe.style.left = Math.min(Math.max(0, startLeft + deltaX), maxLeft) + 'px'; iframe.style.top = Math.min(Math.max(0, startTop + deltaY), maxTop) + 'px'; }; const stopDrag = () => { if (!isDragging) return; isDragging = false; iframe.style.transition = ''; doc.body.style.userSelect = ''; }; ui.header.addEventListener('mousedown', e => { isDragging = true; startX = e.screenX; startY = e.screenY; startLeft = parseFloat(iframe.style.left) || 0; startTop = parseFloat(iframe.style.top) || 0; iframe.style.transition = 'none'; doc.body.style.userSelect = 'none'; e.preventDefault(); }); doc.addEventListener('mousemove', onMove); hostWindow.addEventListener('mousemove', onMove); doc.addEventListener('mouseup', stopDrag); hostWindow.addEventListener('mouseup', stopDrag); hostWindow.addEventListener('blur', stopDrag); const normalSize = { width: parseFloat(iframe.style.width), height: parseFloat(iframe.style.height) }; const miniSize = 64; let isMinimized = false; const enterMini = () => { if (isMinimized) return; isMinimized = true; ui.panel.style.display = 'none'; ui.miniBasic.classList.add('show'); iframe.style.width = miniSize + 'px'; iframe.style.height = miniSize + 'px'; }; const exitMini = () => { if (!isMinimized) return; isMinimized = false; ui.panel.style.display = ''; ui.miniBasic.classList.remove('show'); iframe.style.width = normalSize.width + 'px'; iframe.style.height = normalSize.height + 'px'; }; ui.minimality.addEventListener('click', enterMini); ui.miniBasic.addEventListener('click', exitMini); ui.question.addEventListener('click', () => { window.parent.alert('作者:niuwh.cn(重构版 by Codex)'); }); const log = message => { const li = doc.createElement('li'); li.innerText = message; ui.info.appendChild(li); if (ui.info.lastElementChild) ui.info.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' }); }; const defaultAI = { url: 'https://api.deepseek.com/chat/completions', key: 'sk-xxxxxxx', model: 'deepseek-chat', apiFormat: 'openai', authMethod: 'bearer' }; const loadAIConf = () => { const saved = Store.getAIConf(); ui.aiUrlInput.value = saved.url || defaultAI.url; ui.aiKeyInput.value = saved.key || defaultAI.key; ui.aiModelInput.value = saved.model || defaultAI.model; ui.aiFormatSelect.value = saved.apiFormat || defaultAI.apiFormat; ui.authMethodSelect.value = saved.authMethod || defaultAI.authMethod; }; const loadFeatureConf = () => { const saved = Store.getFeatureConf(); ui.featureAutoAI.checked = saved.autoAI; ui.featureAutoComment.checked = saved.autoComment; }; loadAIConf(); loadFeatureConf(); ui.btnSetting.onclick = () => { loadAIConf(); loadFeatureConf(); ui.settings.style.display = 'block'; }; ui.closeSettings.onclick = () => { ui.settings.style.display = 'none'; }; ui.saveSettings.onclick = () => { const conf = { url: ui.aiUrlInput.value.trim(), key: ui.aiKeyInput.value.trim(), model: ui.aiModelInput.value.trim(), apiFormat: ui.aiFormatSelect.value, authMethod: ui.authMethodSelect.value }; Store.setAIConf(conf); const featureConf = { autoAI: ui.featureAutoAI.checked, autoComment: ui.featureAutoComment.checked }; Store.setFeatureConf(featureConf); ui.settings.style.display = 'none'; log('✅ AI 配置已保存'); }; ui.btnClear.onclick = () => { Store.removeProgress(window.parent.location.href); localStorage.removeItem(Config.storageKeys.proClassCount); Store.clearPendingAutoStart(); log('已清除当前课程的刷课进度缓存'); }; // 停止刷课:清除自动恢复标记后刷新页面,刷新后脚本回到空闲状态(进度缓存保留) ui.btnStop.onclick = () => { Store.clearPendingAutoStart(); log('已停止刷课,页面即将刷新'); window.parent.location.reload(); }; // 重新加载:重建自动恢复标记后刷新页面,刷新后自动恢复刷课(停止后点击同样生效) ui.btnReload.onclick = () => { Store.setPendingAutoStart(Utils.getCurrentClassroomId()); log('正在重新加载脚本...'); window.parent.location.reload(); }; let startHandler = null; let running = false; const invokeStart = () => { if (running) { log('已在刷课中,忽略重复启动'); return; } running = true; log('启动中...'); ui.btnStart.innerText = '刷课中...'; startHandler && startHandler(); }; // 后面赋值给panel return { ...ui, log, setStartHandler(fn) { startHandler = fn; ui.btnStart.onclick = invokeStart; }, start() { invokeStart(); }, resetStartButton(text = '开始刷课') { ui.btnStart.innerText = text; if (text !== '刷课中...') running = false; } }; } // ---- 播放器工具 ---- const Player = { isNearEnd(media, threshold = 1) { if (!media) return false; const duration = Number(media.duration || 0); const currentTime = Number(media.currentTime || 0); return Number.isFinite(duration) && duration > 1 && currentTime > 0 && duration - currentTime <= threshold; }, applySpeed() { const rate = Config.playbackRate; const speedBtn = document.querySelector('xt-speedlist xt-button') || document.getElementsByTagName('xt-speedlist')[0]?.firstElementChild?.firstElementChild; const speedWrap = document.getElementsByTagName('xt-speedbutton')[0]; if (speedBtn && speedWrap) { speedBtn.setAttribute('data-speed', rate); speedBtn.setAttribute('keyt', `${rate}.00`); speedBtn.innerText = `${rate}.00X`; const mousemove = document.createEvent('MouseEvent'); mousemove.initMouseEvent('mousemove', true, true, unsafeWindow, 0, 10, 10, 10, 10, 0, 0, 0, 0, 0, null); speedWrap.dispatchEvent(mousemove); speedBtn.click(); } else if (document.querySelector('video')) { document.querySelector('video').playbackRate = rate; } }, mute() { const muteBtn = document.querySelector('#video-box > div > xt-wrap > xt-controls > xt-inner > xt-volumebutton > xt-icon'); if (muteBtn) muteBtn.click(); const video = document.querySelector('video'); if (video) video.volume = 0; }, applyMediaDefault(media) { if (!media) return; media.play(); media.volume = 0; media.playbackRate = Config.playbackRate; }, observePause(video, shouldResume = () => true) { if (!video) return () => { }; const canResume = () => shouldResume() && !video.ended && !this.isNearEnd(video); // 自动播放 const playVideo = () => { if (!canResume()) return; video.play().catch(e => { if (!canResume()) return; console.warn('自动播放失败:', e); setTimeout(playVideo, 3000); }); }; playVideo(); // 直接监听 pause 事件,不依赖播放器 UI 元素 const onPause = () => { if (canResume()) playVideo(); }; video.addEventListener('pause', onPause); // 定时兜底:防止 pause 事件被拦截 const timer = setInterval(() => { if (video.paused && canResume()) playVideo(); }, 5000); // 播放器 UI 观察:按钮被点击暂停时 tip 变为「播放」 const target = document.getElementsByClassName('play-btn-tip')[0]; let observer = null; if (target) { observer = new MutationObserver(list => { for (const mutation of list) { if (mutation.type === 'childList' && target.innerText === '播放' && canResume()) { video.play(); } } }); observer.observe(target, { childList: true }); } return () => { video.removeEventListener('pause', onPause); clearInterval(timer); if (observer) observer.disconnect(); }; }, waitForEnd(media, timeout = 0) { return new Promise(resolve => { if (!media) return resolve(); if (media.ended) return resolve(); let timer; const onEnded = () => { clearTimeout(timer); resolve(); }; media.addEventListener('ended', onEnded, { once: true }); if (timeout > 0) { timer = setTimeout(() => { media.removeEventListener('ended', onEnded); resolve(); }, timeout); } }); } }; // ---- ai-workspace 路由工具 ---- const AiWorkspace = { normalizeText(text) { return String(text || '').replace(/\s+/g, ' ').trim(); }, isVisibleElement(element) { if (!element || element.nodeType !== 1) return false; const view = element.ownerDocument?.defaultView || window; const style = view.getComputedStyle(element); const rect = element.getBoundingClientRect(); return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0; }, getRoute() { const match = location.pathname.match(/^\/ai-workspace\/lms-graph\/([^/]+)\/([^/]+)\/([^/?#]+)/); if (!match) return null; const [, classroomId, type, leafId] = match; const query = new URLSearchParams(location.search); return { classroomId, type, leafId, nodeId: query.get('node_id') || '' }; }, getMediaCandidates() { return [...document.querySelectorAll('video, audio')].filter(media => { if (!(media instanceof HTMLMediaElement)) return false; const rect = media.getBoundingClientRect(); const isVisible = rect.width > 0 && rect.height > 0; return isVisible || media.tagName.toLowerCase() === 'audio'; }); }, getMedia() { const candidates = this.getMediaCandidates(); if (!candidates.length) return document.querySelector('video') || document.querySelector('audio'); const score = media => { const rect = media.getBoundingClientRect(); const area = rect.width * rect.height; const playingBoost = !media.paused && !media.ended ? 1_000_000 : 0; const currentBoost = Number(media.currentTime || 0); return playingBoost + area + currentBoost; }; return [...candidates].sort((a, b) => score(b) - score(a))[0]; }, isPlayerDone(media, { startTime = 0, minPlayedDelta = 0 } = {}) { if (!media) return false; const currentTime = Number(media?.currentTime || 0); const duration = Number(media?.duration || 0); const playedDelta = Math.max(0, currentTime - startTime); if (playedDelta < minPlayedDelta) return false; if (media?.ended) return true; if (duration > 1 && currentTime > 0 && duration - currentTime <= 1) return true; const display = document.querySelector('.xt_video_player_current_time_display')?.innerText?.trim() || ''; const [current, total] = display.split(' / ').map(text => text?.trim()); return Boolean(playedDelta >= minPlayedDelta && current && total && current === total); }, keepAlive(shouldResume = () => true) { let lastMedia = null; const tick = () => { if (!shouldResume()) return; const media = this.getMedia(); if (!media) return; if (lastMedia !== media) { lastMedia = media; media.addEventListener('pause', tick); } media.muted = true; media.defaultMuted = true; media.volume = 0; media.playbackRate = Config.playbackRate; if (media.paused && !media.ended && !Player.isNearEnd(media)) { media.play().catch(() => { }); } }; const timer = setInterval(tick, 500); document.addEventListener('visibilitychange', tick); window.addEventListener('focus', tick); tick(); return () => { clearInterval(timer); if (lastMedia) lastMedia.removeEventListener('pause', tick); document.removeEventListener('visibilitychange', tick); window.removeEventListener('focus', tick); }; }, getActiveLeafTitle() { return document.querySelector('.leaf-item.is-active')?.innerText?.replace(/\s+/g, ' ').trim() || ''; }, getExerciseDocument() { const localHasExercise = document.querySelector('#app .container-body .container-problem') || document.querySelector('#app .container-problem') || document.querySelector('.container-problem'); if (localHasExercise) return document; const frames = [...document.querySelectorAll('iframe')]; for (const frame of frames) { try { const doc = frame.contentDocument; if (!doc?.body) continue; if ( doc.querySelector('.container-problem') || doc.querySelector('.subject-item') || doc.querySelector('.item-body') ) { return doc; } } catch (_) { // ignore cross-document access failures } } return null; }, getExerciseContainer() { const exerciseDoc = this.getExerciseDocument(); return exerciseDoc?.querySelector('#app .container-body .container-problem') || exerciseDoc?.querySelector('#app .container-problem') || exerciseDoc?.querySelector('.container-problem') || null; }, getExerciseQuestionTabs(root = this.getExerciseContainer()) { if (!root) return []; const selectors = [ '.subject-item.J_order', '.subject-item', '.problem-index-item', '.question-index-item', '[class*="subject-item"]', '[class*="problem-index"]', '[class*="question-index"]' ].join(','); const all = [...root.querySelectorAll(selectors)]; return all.filter((el, index, arr) => { if (!this.isVisibleElement(el)) return false; if (arr.indexOf(el) !== index) return false; const text = this.normalizeText(el.innerText); return text && text.length <= 20; }); }, getExerciseQuestionBody(root = this.getExerciseContainer()) { if (!root) return null; const itemType = root.querySelector('.item-type'); if (itemType?.parentElement && this.isVisibleElement(itemType.parentElement)) return itemType.parentElement; const selectors = [ '.item-body', '.problem-content', '.question-content', '.problem-main', '.problem-body', '.question-body', '[class*="problem-content"]', '[class*="question-content"]', '[class*="problem-body"]', '[class*="question-body"]' ]; for (const selector of selectors) { const match = [...root.querySelectorAll(selector)].find(el => this.isVisibleElement(el)); if (match) return match; } return root; }, isExerciseAnswered(root = this.getExerciseContainer()) { if (!root) return false; const disabled = root.querySelector('.el-button.el-button--info.is-disabled.is-plain') || root.querySelector('button[disabled]'); if (disabled) return true; const statusSelectors = [ '.result', '.status', '.answer-status', '[class*="result"]', '[class*="status"]' ]; for (const selector of statusSelectors) { const statusNode = [...root.querySelectorAll(selector)] .find(el => this.isVisibleElement(el) && /已完成|已作答|已提交|回答正确|回答错误/.test(this.normalizeText(el.innerText))); if (statusNode) return true; } return false; }, getExerciseActionButton(root = this.getExerciseContainer(), pattern = /提交|保存|确认|确定|下一题|下一道|下一步|完成本题/) { if (!root) return null; const selectors = 'button, .el-button, [role="button"], [class*="button"]'; const nodes = [ ...root.querySelectorAll(selectors), ...document.querySelectorAll(selectors) ]; return nodes.find(el => this.isVisibleElement(el) && pattern.test(this.normalizeText(el.innerText))); } }; // ---- 防切屏 ---- function preventScreenCheck() { const win = unsafeWindow; const blackList = new Set(['visibilitychange', 'blur', 'pagehide']); win._addEventListener = win.addEventListener; win.addEventListener = (...args) => blackList.has(args[0]) ? undefined : win._addEventListener(...args); document._addEventListener = document.addEventListener; document.addEventListener = (...args) => blackList.has(args[0]) ? undefined : document._addEventListener(...args); Object.defineProperties(document, { hidden: { value: false }, visibilityState: { value: 'visible' }, hasFocus: { value: () => true }, onvisibilitychange: { get: () => undefined, set: () => { } }, onblur: { get: () => undefined, set: () => { } } }); Object.defineProperties(win, { onblur: { get: () => undefined, set: () => { } }, onpagehide: { get: () => undefined, set: () => { } } }); } // ---- OCR & AI ---- const Solver = { async recognize(element) { if (!element) return '无元素'; try { panel.log('正在截图...'); const canvas = await html2canvas(element, { useCORS: true, logging: false, scale: 2, backgroundColor: '#ffffff' }); panel.log('正在 OCR 识别 (首轮较慢)...'); const { data: { text } } = await Tesseract.recognize(canvas, 'chi_sim', { logger: m => { if (m.status === 'downloading tesseract lang') { console.log(`正在下载语言包 ${(m.progress * 100).toFixed(0)}%`); } } }); return text.replace(/\s+/g, ' ').trim(); } catch (err) { console.error('OCR error:', err); panel.log(`OCR 失败: ${err.message || '网络错误'}`); return 'OCR识别出错'; } }, async askAI(ocrText, optionCount = 0) { const saved = Store.getAIConf(); const API_URL = saved.url; const API_KEY = saved.key; const MODEL_NAME = saved.model; const API_FORMAT = saved.apiFormat || 'openai'; const AUTH_METHOD = saved.authMethod || 'bearer'; return new Promise((resolve, reject) => { if (!API_KEY || API_KEY.includes('sk-xxxx')) { const msg = '⚠️ 请在 [AI配置] 中填写有效的 API Key'; panel.log(msg); reject(msg); return; } const maxChar = String.fromCharCode(65 + optionCount - 1); const rangeStr = optionCount ? `A-${maxChar}` : 'A-D'; const prompt = ` 你是专业做题助手,请分析 OCR 文本,判断题型后给出答案。 强约束: 1) 本题只有 ${optionCount || '若干'} 个选项,范围 ${rangeStr} 2) 忽略 OCR 错误的选项字母,按出现顺序映射 A/B/C/D... 3) 输出格式必须包含“正确答案:”前缀,例如 正确答案:A 或 正确答案:ABD 或 正确答案:对/错 题目内容: ${ocrText} `; const systemPrompt = "你是一个只输出答案的助手。判断题输出'对'或'错',选择题输出字母。"; // 构建认证 header const authHeader = AUTH_METHOD === 'x-api-key' ? { 'x-api-key': API_KEY } : { 'Authorization': `Bearer ${API_KEY}` }; if (API_FORMAT === 'anthropic') { // Anthropic API 格式 const headers = { 'Content-Type': 'application/json', ...authHeader }; // 只有原生 Anthropic API 才需要 anthropic-version,代理通常不需要 if (API_URL.includes('api.anthropic.com')) { headers['anthropic-version'] = '2023-06-01'; } const requestBody = { model: MODEL_NAME, max_tokens: 1024, system: systemPrompt, messages: [ { role: 'user', content: prompt } ] }; // 调试日志 console.log('[AI请求] URL:', API_URL); console.log('[AI请求] Headers:', headers); console.log('[AI请求] Body:', requestBody); panel.log(`请求 ${API_URL}...`); GM_xmlhttpRequest({ method: 'POST', url: API_URL, headers, data: JSON.stringify(requestBody), data: JSON.stringify({ model: MODEL_NAME, max_tokens: 1024, system: systemPrompt, messages: [ { role: 'user', content: prompt } ] }), timeout: 120000, // 120秒,思考模型需要更长响应时间 onload: res => { console.log('[AI响应] Status:', res.status); console.log('[AI响应] Response:', res.responseText); if (res.status === 200) { try { const json = JSON.parse(res.responseText); // Anthropic 返回格式: content[0].text const answerText = json.content?.[0]?.text || json.choices?.[0]?.message?.content; resolve(answerText); } catch (e) { reject('JSON 解析失败'); } } else { const err = `请求失败: HTTP ${res.status} - ${res.responseText}`; panel.log(err); reject(err); } }, onerror: () => reject('网络错误'), ontimeout: () => reject('请求超时') }); } else { // OpenAI API 格式(默认) GM_xmlhttpRequest({ method: 'POST', url: API_URL, headers: { 'Content-Type': 'application/json', ...authHeader }, data: JSON.stringify({ model: MODEL_NAME, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ], temperature: 0.1 }), timeout: 120000, // 120秒,思考模型需要更长响应时间 onload: res => { if (res.status === 200) { try { const json = JSON.parse(res.responseText); const answerText = json.choices[0].message.content; resolve(answerText); } catch (e) { reject('JSON 解析失败'); } } else { const err = `请求失败: HTTP ${res.status}`; panel.log(err); reject(err); } }, onerror: () => reject('网络错误'), ontimeout: () => reject('请求超时') }); } }); }, async autoSelectAndSubmit(aiResponse, itemBodyElement) { const match = aiResponse.match(/(?:正确)?答案[::]?\s*([A-F]+(?:[,,][A-F]+)*|[对错]|正确|错误)/i); if (!match) { panel.log('⚠️ 未提取到有效选项,请人工检查'); return; } let answerRaw = match[1].replace(/[,,]/g, '').trim(); const map = { 'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5 }; let targetIndices = []; if (answerRaw === '对' || answerRaw === '正确') { targetIndices = [0]; } else if (answerRaw === '错' || answerRaw === '错误') { targetIndices = [1]; } else { for (const char of answerRaw.toUpperCase()) { if (map[char] !== undefined) targetIndices.push(map[char]); } } if (!targetIndices.length) return; panel.log(`✅ AI 建议选:${answerRaw}`); const listContainer = itemBodyElement.querySelector('.list-inline.list-unstyled-radio') || itemBodyElement.querySelector('.list-unstyled.list-unstyled-radio') || itemBodyElement.querySelector('.list-unstyled') || itemBodyElement.querySelector('ul.list') || itemBodyElement.querySelector('[class*="option-list"]') || itemBodyElement.querySelector('[class*="answer-list"]') || itemBodyElement.querySelector('ul') || itemBodyElement.querySelector('[role="radiogroup"]'); if (!listContainer) { panel.log('⚠️ 未找到选项容器'); return; } const options = listContainer.querySelectorAll('li, .option-item, .answer-item, [class*="option-item"], [class*="answer-item"]'); for (const idx of targetIndices) { if (!options[idx]) continue; const clickable = options[idx].querySelector('label.el-radio') || options[idx].querySelector('label.el-checkbox') || options[idx].querySelector('.el-radio__label') || options[idx].querySelector('.el-checkbox__label') || options[idx].querySelector('[role="radio"]') || options[idx].querySelector('[role="checkbox"]') || options[idx].querySelector('input') || options[idx]; clickable.click(); await Utils.sleep(150); } const submitBtn = (() => { const ownerDocument = itemBodyElement.ownerDocument || document; const roots = [itemBodyElement.parentElement, itemBodyElement, ownerDocument].filter(Boolean); const matchText = text => /提交|保存|确认|确定|提交答案/.test(text); for (const root of roots) { const local = root.querySelectorAll('button, .el-button, [role="button"]'); for (const btn of local) { if (btn.offsetParent !== null && matchText(btn.innerText || '')) return btn; } } const global = ownerDocument.querySelectorAll('.el-button.el-button--primary.el-button--medium'); for (const btn of global) { if (matchText(btn.innerText || '') && btn.offsetParent !== null) return btn; } return null; })(); if (submitBtn) { panel.log('正在提交...'); submitBtn.click(); } else { panel.log('⚠️ 未找到提交按钮,请手动提交'); } } }; // ---- v2 逻辑 ---- class V2Runner { constructor(panel) { this.panel = panel; this.baseUrl = location.href; const { current } = Store.getProgress(this.baseUrl); this.outside = current.outside; this.inside = current.inside; this.shouldStop = false; } updateProgress(outside, inside = 0) { this.outside = outside; this.inside = inside; Store.setProgress(this.baseUrl, outside, inside); } async waitForExternalHandoff(timeout = 1200) { await Utils.sleep(timeout); if (document.visibilityState === 'hidden' || !document.hasFocus()) { this.shouldStop = true; this.panel.log('已交给新页面继续,返回目录页后会自动续跑'); return true; } return false; } checkCompletionStatus(statusBox, statusText) { // 1. 检查明确的完成状态文本 if (statusText.includes('已完成') || statusText.includes('已读')) { return true; } // 2. 检查明确的未完成状态文本 if (statusText.includes('未开始') || statusText.includes('未读') || statusText.includes('进行中')) { return false; } // 3. 检查学习进度数字比例 const progressMatch = statusText.match(/(\d+)\/(\d+)/); if (progressMatch) { const [, current, total] = progressMatch; const currentNum = parseInt(current, 10); const totalNum = parseInt(total, 10); // 根据数字进度判断:相等且大于0表示已完成 return currentNum === totalNum && totalNum > 0; } // 默认返回false(未完成) return false; } async run() { this.panel.log(`检测到已播放到第 ${this.outside} 集,继续刷课...`); // 在课件页恢复时直接续播当前内容,不重新走列表流程 if (location.pathname.includes('/studentCards/')) { const videoBox = document.querySelector('.video-box'); const boxText = videoBox?.innerText || ''; if ((videoBox || document.querySelector('video')) && !boxText.includes('已完成')) { this.panel.log('检测到当前课件页,直接续播当前内容'); await this.waitCoursewareVideo(); history.back(); await Utils.sleep(1000); } } while (true) { await this.autoSlide(); const list = document.querySelector('.logs-list')?.childNodes; if (!list || !list.length) { // 可能停留在课件页:跳回目录页继续,避免无限重试 const pending = Store.getPendingAutoStart(); const returnUrl = pending?.returnUrl || (pending?.classroomId ? `/v2/web/studentLog/${pending.classroomId}` : ''); if (returnUrl && !location.pathname.includes('/studentLog/')) { this.panel.log('当前页面无课程列表,返回目录页继续'); location.href = returnUrl; return; } this.panel.log('未找到课程列表,稍后重试'); await Utils.sleep(2000); continue; } console.log(`当前集数:${this.outside}/全部集数${list.length}`); if (this.outside >= list.length) { this.panel.log('课程刷完啦 🎉'); this.panel.resetStartButton('刷完啦~'); Store.removeProgress(this.baseUrl); Store.clearPendingAutoStart(); break; } const course = list[this.outside]?.querySelector('.content-box')?.querySelector('section'); if (!course) { this.panel.log('未找到当前课程节点,跳过'); this.updateProgress(this.outside + 1, 0); continue; } const type = course.querySelector('.tag')?.querySelector('use')?.getAttribute('xlink:href') || 'piliang'; const title = course.querySelector('h2')?.innerText?.trim() || `第${this.outside + 1}项`; // 预检查完成状态 const statusBox = course.querySelector('.statistics-box .aside'); const statusText = statusBox?.innerText || ''; // 判断是否已完成 let isCompleted = this.checkCompletionStatus(statusBox, statusText); if (isCompleted) { this.panel.log(`✅ ${title} 已完成,跳过`); this.updateProgress(this.outside + 1, 0); continue; } this.panel.log(`刷课状态:第 ${this.outside + 1}/${list.length} 个,类型 ${type},标题:${title}`); if (type.includes('shipin')) { await this.handleVideo(course); } else if (type.includes('piliang')) { await this.handleBatch(course, list); } else if (type.includes('ketang')) { await this.handleClassroom(course); } else if (type.includes('kejian')) { await this.handleCourseware(course); } else if (type.includes('kaoshi')) { this.panel.log('考试区域脚本会被屏蔽,已跳过'); this.updateProgress(this.outside + 1, 0); } else { this.panel.log('非视频/批量/课件/考试,已跳过'); this.updateProgress(this.outside + 1, 0); } if (this.shouldStop) return; } } async autoSlide() { const frequency = Math.floor((this.outside + 1) / 20) + 1; for (let i = 0; i < frequency; i++) { Utils.scrollToBottom('.viewContainer'); await Utils.sleep(800); } } async handleVideo(course) { course.click(); if (await this.waitForExternalHandoff(1500)) return; await Utils.sleep(3000); const progressNode = document.querySelector('.progress-wrap')?.querySelector('.text'); const title = document.querySelector('.title')?.innerText || '视频'; const isDeadline = document.querySelector('.box')?.innerText.includes('已过考核截止时间'); if (isDeadline) this.panel.log(`${title} 已过截止,进度不再增加,将直接跳过`); Player.applySpeed(); Player.mute(); const stopObserve = Player.observePause(document.querySelector('video')); await Utils.poll(() => { Utils.dismissPopups(); return isDeadline || Utils.isProgressDone(progressNode?.innerHTML); }, { interval: 5000, timeout: await Utils.getDDL() }); stopObserve(); this.updateProgress(this.outside + 1, 0); history.back(); await Utils.sleep(1200); } async handleBatch(course, list) { const expandBtn = course.querySelector('.sub-info')?.querySelector('.gray')?.querySelector('span'); if (!expandBtn) { this.panel.log('未找到批量展开按钮,跳过'); this.updateProgress(this.outside + 1, 0); return; } expandBtn.click(); await Utils.sleep(1200); const activities = list[this.outside]?.querySelector('.leaf_list__wrap')?.querySelectorAll('.activity__wrap') || []; let idx = this.inside; this.panel.log(`进入批量区,内部进度 ${idx}/${activities.length}`); while (idx < activities.length) { const item = activities[idx]; if (!item) break; const tagText = item.querySelector('.tag')?.innerText || ''; const tagHref = item.querySelector('.tag')?.querySelector('use')?.getAttribute('xlink:href') || ''; const title = item.querySelector('h2')?.innerText || `第${idx + 1}项`; // 检查当前项目的完成状态 const statusBox = item.querySelector('.statistics-box .aside'); const statusText = statusBox?.innerText || ''; const isCompleted = this.checkCompletionStatus(statusBox, statusText); if (isCompleted) { this.panel.log(`✅ ${title} 已完成,跳过`); idx++; this.updateProgress(this.outside, idx); continue; } if (tagText === '音频') { idx = await this.playAudioItem(item, title, idx); } else if (tagHref.includes('shipin')) { idx = await this.playVideoItem(item, title, idx); } else if (tagHref.includes('tuwen') || tagHref.includes('taolun')) { idx = await this.autoCommentItem(item, tagHref.includes('tuwen') ? '图文' : '讨论', idx); } else if (tagHref.includes('zuoye')) { idx = await this.handleHomework(item, idx); } else { this.panel.log(`类型未知,已跳过:${title}`); idx++; this.updateProgress(this.outside, idx); } if (this.shouldStop) return; } this.updateProgress(this.outside + 1, 0); await Utils.sleep(1000); } async playAudioItem(item, title, idx) { this.panel.log(`开始播放音频:${title}`); item.click(); if (await this.waitForExternalHandoff()) return idx; await Utils.sleep(2500); Player.applyMediaDefault(document.querySelector('audio')); const progressNode = document.querySelector('.progress-wrap')?.querySelector('.text'); await Utils.poll(() => { Utils.dismissPopups(); return Utils.isProgressDone(progressNode?.innerHTML); }, { interval: 3000, timeout: await Utils.getDDL() }); this.panel.log(`${title} 播放完成`); idx++; this.updateProgress(this.outside, idx); history.back(); await Utils.sleep(1500); return idx; } async playVideoItem(item, title, idx) { this.panel.log(`开始播放视频:${title}`); item.click(); if (await this.waitForExternalHandoff()) return idx; await Utils.sleep(2500); Player.applySpeed(); Player.mute(); const stopObserve = Player.observePause(document.querySelector('video')); const progressNode = document.querySelector('.progress-wrap')?.querySelector('.text'); await Utils.poll(() => { Utils.dismissPopups(); return Utils.isProgressDone(progressNode?.innerHTML); }, { interval: 3000, timeout: await Utils.getDDL() }); stopObserve(); this.panel.log(`${title} 播放完成`); idx++; this.updateProgress(this.outside, idx); history.back(); await Utils.sleep(1500); return idx; } async autoCommentItem(item, typeText, idx) { this.panel.log(`开始处理${typeText}:${item.querySelector('h2')?.innerText || ''}`); item.click(); await Utils.sleep(1200); // 检查是否开启自动评论功能 const featureFlags = Store.getFeatureConf(); if (!featureFlags.autoComment) { this.panel.log(`${typeText}已查看,但未开启自动回复功能`); idx++; this.updateProgress(this.outside, idx); history.back(); await Utils.sleep(1000); return idx; } // 开启了自动评论功能,执行评论逻辑 window.scrollTo(0, document.body.scrollHeight); await Utils.sleep(800); window.scrollTo(0, 0); const commentSelectors = ['#new_discuss .new_discuss_list .cont_detail', '.new_discuss_list dd .cont_detail', '.cont_detail.word-break']; let firstComment = ''; for (let retry = 0; retry < 30 && !firstComment; retry++) { for (const sel of commentSelectors) { const list = document.querySelectorAll(sel); for (const node of list) { if (node?.innerText?.trim()) { firstComment = node.innerText.trim(); break; } } if (firstComment) break; } if (!firstComment) await Utils.sleep(500); } if (!firstComment) { this.panel.log('未找到评论内容,跳过该项'); } else { const input = document.querySelector('.el-textarea__inner'); if (input) { input.value = firstComment; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); await Utils.sleep(800); const sendBtn = document.querySelector('.el-button.submitComment') || document.querySelector('.publish_discuss .postBtn button') || document.querySelector('.el-button--primary'); if (sendBtn && !sendBtn.disabled && !sendBtn.classList.contains('is-disabled')) { sendBtn.click(); this.panel.log(`已在${typeText}区发表评论`); } else { this.panel.log('发送按钮不可用或不存在'); } } else { this.panel.log('未找到评论输入框,跳过'); } } idx++; this.updateProgress(this.outside, idx); history.back(); await Utils.sleep(1000); return idx; } async handleHomework(item, idx) { const featureFlags = Store.getFeatureConf(); if (!featureFlags.autoAI) { this.panel.log('已关闭AI自动答题,跳过该项'); idx++; this.updateProgress(this.outside, idx); return idx; } this.panel.log('进入作业,启动 OCR + AI'); item.click(); await Utils.sleep(1500); let i = 0; const maxRetry = 3; // 最大重试次数 while (true) { const items = document.querySelectorAll('.subject-item.J_order'); if (i >= items.length) { this.panel.log(`所有题目处理完毕,共 ${items.length} 题,准备交卷`); break; } const listItem = items[i]; listItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); listItem.click(); await Utils.sleep(1800); const disabled = document.querySelectorAll('.el-button.el-button--info.is-disabled.is-plain'); if (disabled.length > 0) { this.panel.log(`第 ${i + 1} 题已完成,跳过...`); i++; continue; } const targetEl = document.querySelector('.item-type')?.parentElement || document.querySelector('.item-body'); let optionCount = 0; const listContainer = targetEl?.querySelector('.list-inline.list-unstyled-radio') || targetEl?.querySelector('.list-unstyled.list-unstyled-radio') || targetEl?.querySelector('ul.list'); if (listContainer) optionCount = listContainer.querySelectorAll('li').length; const ocrResult = await Solver.recognize(targetEl); if (ocrResult && ocrResult.length > 5) { let retryCount = 0; let success = false; while (retryCount < maxRetry && !success) { try { if (retryCount > 0) { this.panel.log(`🔄 第 ${i + 1} 题重试 ${retryCount}/${maxRetry}...`); } panel.log('🤖 请求 AI 获取答案...'); const aiText = await Solver.askAI(ocrResult, optionCount); await Solver.autoSelectAndSubmit(aiText, targetEl); success = true; } catch (err) { retryCount++; this.panel.log(`AI 答题失败:${err}`); if (retryCount < maxRetry) { this.panel.log(`等待 5 秒后重试...`); await Utils.sleep(5000); } else { this.panel.log(`⚠️ 第 ${i + 1} 题重试 ${maxRetry} 次后仍失败,跳过`); } } } } await Utils.sleep(1500); i++; } idx++; this.updateProgress(this.outside, idx); history.back(); await Utils.sleep(1200); return idx; } async handleClassroom(course) { this.panel.log('进入课堂模式...'); course.click(); await Utils.sleep(5000); const iframe = document.querySelector('iframe.lesson-report-mobile'); if (!iframe || !iframe.contentDocument) { this.panel.log('未找到课堂 iframe,跳过'); this.updateProgress(this.outside + 1, 0); return; } const video = iframe.contentDocument.querySelector('video'); const audio = iframe.contentDocument.querySelector('audio'); if (video) { Player.applyMediaDefault(video); await Player.waitForEnd(video); } if (audio) { Player.applyMediaDefault(audio); await Player.waitForEnd(audio); } this.updateProgress(this.outside + 1, 0); history.go(-1); await Utils.sleep(1200); } // 等待课件视频播放完毕;播放器被关闭(弹窗关闭/元素销毁)时自动重新打开 async waitCoursewareVideo() { const deadline = await Utils.getDDL(); const start = Date.now(); let boundVideo = null; let stopObserve = () => { }; let reopenAttempts = 0; try { while (Date.now() - start < deadline) { Utils.dismissPopups(); const video = document.querySelector('video'); const display = document.querySelector('.xt_video_player_current_time_display'); if (!video) { // 播放器被关闭或视频元素被销毁,重新打开视频框 const videoBox = document.querySelector('.video-box'); if (videoBox && !videoBox.innerText.includes('已完成')) { this.panel.log('播放器被关闭,正在重新打开'); videoBox.click(); boundVideo = null; } reopenAttempts++; if (reopenAttempts >= 4) { this.panel.log('播放器恢复失败,刷新页面重试'); location.reload(); return false; } await Utils.sleep(2000); continue; } reopenAttempts = 0; if (!display) { // 播放器加载中,等待渲染 await Utils.sleep(800); continue; } if (video !== boundVideo) { stopObserve(); Player.applySpeed(); Player.mute(); boundVideo = video; stopObserve = Player.observePause(video); } const times = display.innerText || ''; const [nowTime, totalTime] = times.split(' / '); if (nowTime && totalTime && nowTime === totalTime) return true; await Utils.sleep(800); } return false; } finally { stopObserve(); } } async handleCourseware(course) { const tableData = course.parentNode?.parentNode?.parentNode?.__vue__?.tableData; const deadlinePassed = (tableData?.deadline || tableData?.end) ? (tableData.deadline < Date.now() || tableData.end < Date.now()) : false; if (deadlinePassed) { this.panel.log(`${course.querySelector('h2')?.innerText || '课件'} 已结课,跳过`); this.updateProgress(this.outside + 1, 0); return; } course.click(); await Utils.sleep(3000); // 检测"查看课件"按钮(课件概况页专用) const checkBtn = document.querySelector('.ppt_img_box .check') || document.querySelector('p.check'); if (checkBtn && checkBtn.innerText?.trim() === '查看课件') { this.panel.log('检测到"查看课件"按钮,正在点击...'); checkBtn.click(); await Utils.sleep(2000); } const classType = document.querySelector('.el-card__header')?.innerText || ''; const className = document.querySelector('.dialog-header')?.firstElementChild?.innerText || '课件'; if (classType.includes('PPT')) { const slides = document.querySelector('.swiper-wrapper')?.children || []; this.panel.log(`开始播放 PPT:${className}`); for (let i = 0; i < slides.length; i++) { slides[i].click(); this.panel.log(`${className}:第 ${i + 1} 张`); await Utils.sleep(Config.pptInterval); } await Utils.sleep(Config.pptInterval); const videoBoxes = document.querySelectorAll('.video-box'); if (videoBoxes?.length) { this.panel.log('PPT 中有视频,继续播放'); for (let i = 0; i < videoBoxes.length; i++) { if (videoBoxes[i].innerText === '已完成') { this.panel.log(`第 ${i + 1} 个视频已完成,跳过`); continue; } videoBoxes[i].click(); await Utils.sleep(2000); await this.waitCoursewareVideo(); } } this.panel.log(`${className} 已播放完毕`); } else { const videoBox = document.querySelector('.video-box'); if (videoBox) { videoBox.click(); await Utils.sleep(1800); await this.waitCoursewareVideo(); this.panel.log(`${className} 视频播放完毕`); } } this.updateProgress(this.outside + 1, 0); history.back(); await Utils.sleep(1000); } } // ---- pro/lms 旧版(仅做转发) ---- class ProOldRunner { constructor(panel) { this.panel = panel; } run() { this.panel.log('准备打开新标签页...'); const leafDetail = document.querySelectorAll('.leaf-detail'); let classCount = Store.getProClassCount() - 1; while (leafDetail[classCount] && !leafDetail[classCount].firstChild.querySelector('i').className.includes('shipin')) { classCount++; Store.setProClassCount(classCount + 1); this.panel.log('课程不属于视频,已跳过'); } leafDetail[classCount]?.click(); } } // ---- pro/lms 新版(主要逻辑) ---- class ProNewRunner { constructor(panel) { this.panel = panel; } async run() { preventScreenCheck(); let classCount = Store.getProClassCount(); while (true) { this.panel.log(`准备播放第 ${classCount} 集...`); await Utils.sleep(2000); const className = document.querySelector('.header-bar')?.firstElementChild?.innerText || ''; const classType = document.querySelector('.header-bar')?.firstElementChild?.firstElementChild?.getAttribute('class') || ''; const classStatus = document.querySelector('#app > div.app_index-wrapper > div.wrap > div.viewContainer.heightAbsolutely > div > div > div > div > section.title')?.lastElementChild?.innerText || ''; if (classType.includes('tuwen') && !classStatus.includes('已读')) { this.panel.log(`正在阅读:${className}`); await Utils.sleep(2000); } else if (classType.includes('taolun')) { this.panel.log(`讨论区暂不自动发帖,${className}`); await Utils.sleep(2000); } else if (classType.includes('shipin') && !classStatus.includes('100%')) { this.panel.log(`2s 后开始播放:${className}`); await Utils.sleep(2000); let statusTimer; let videoTimer; try { statusTimer = setInterval(() => { const status = document.querySelector('#app > div.app_index-wrapper > div.wrap > div.viewContainer.heightAbsolutely > div > div > div > div > section.title')?.lastElementChild?.innerText || ''; if (status.includes('100%') || status.includes('99%') || status.includes('98%') || status.includes('已完成')) { this.panel.log(`${className} 播放完毕`); clearInterval(statusTimer); statusTimer = null; } }, 200); const videoWaitStart = Date.now(); videoTimer = setInterval(() => { const video = document.querySelector('video'); if (video) { setTimeout(() => { Player.applySpeed(); Player.mute(); Player.observePause(video); }, 2000); clearInterval(videoTimer); videoTimer = null; } else if (Date.now() - videoWaitStart > 20000) { location.reload(); } }, 5000); await Utils.sleep(8000); await Utils.poll(() => { const status = document.querySelector('#app > div.app_index-wrapper > div.wrap > div.viewContainer.heightAbsolutely > div > div > div > div > section.title')?.lastElementChild?.innerText || ''; return status.includes('100%') || status.includes('99%') || status.includes('98%') || status.includes('已完成'); }, { interval: 1000, timeout: await Utils.getDDL() }); } finally { if (statusTimer) clearInterval(statusTimer); if (videoTimer) clearInterval(videoTimer); } } else if (classType.includes('zuoye')) { this.panel.log(`进入作业:${className}(暂无自动答题)`); await Utils.sleep(2000); } else if (classType.includes('kaoshi')) { this.panel.log(`进入考试:${className}(不会自动答题)`); await Utils.sleep(2000); } else if (classType.includes('ketang')) { this.panel.log(`进入课堂:${className}(暂无自动功能)`); await Utils.sleep(2000); } else { this.panel.log(`已看过:${className}`); await Utils.sleep(2000); } this.panel.log(`第 ${classCount} 集播放完毕`); classCount++; Store.setProClassCount(classCount); const nextBtn = document.querySelector('.btn-next'); if (nextBtn) { const event1 = new Event('mousemove', { bubbles: true }); event1.clientX = 9999; event1.clientY = 9999; nextBtn.dispatchEvent(event1); nextBtn.dispatchEvent(new Event('click')); } else { localStorage.removeItem(Config.storageKeys.proClassCount); this.panel.log('课程播放完毕 🎉'); Store.clearPendingAutoStart(); this.panel.resetStartButton('开始刷课'); break; } } } } // ---- ai-workspace 新版学习空间 ---- class AiWorkspaceRunner { constructor(panel) { this.panel = panel; } getExerciseQuestionLabel(root) { const tabs = AiWorkspace.getExerciseQuestionTabs(root); const active = tabs.find(tab => /active|current|selected|is-active/.test(tab.className)); return AiWorkspace.normalizeText(active?.innerText || ''); } getReturnUrl() { const pending = Store.getPendingAutoStart(); const route = AiWorkspace.getRoute(); if (!pending || !route) return ''; if (pending.classroomId !== route.classroomId) return ''; return pending.returnUrl || ''; } getSourceWindow() { try { if (!window.opener || window.opener.closed) return null; if (window.opener.location.origin !== location.origin) return null; return window.opener; } catch (_) { return null; } } async returnToSource() { const returnUrl = this.getReturnUrl(); if (!returnUrl) return false; this.panel.log('媒体播放完成,返回课程目录页继续匹配'); await Utils.sleep(1200); const sourceWindow = this.getSourceWindow(); if (sourceWindow) { try { sourceWindow.location.href = returnUrl; sourceWindow.focus(); window.close(); return true; } catch (_) { // ignore and fallback } } if (location.href !== returnUrl) { location.href = returnUrl; } else { history.back(); } return true; } async handleMedia(route) { const title = AiWorkspace.getActiveLeafTitle() || `${route.type} ${route.leafId}`; this.panel.log(`开始播放:${title}`); const ready = await Utils.poll(() => Boolean(AiWorkspace.getMedia()), { interval: 500, timeout: 20000 }); let media = AiWorkspace.getMedia(); if (!ready || !media) { this.panel.log('未找到视频/音频元素,停止当前轮次'); return false; } const playbackState = { completed: false }; const shouldResume = () => !playbackState.completed; let stopObserve = () => { }; if (media.tagName.toLowerCase() === 'video') { Player.applySpeed(); Player.mute(); stopObserve = Player.observePause(media, shouldResume); } else { Player.applyMediaDefault(media); } const stopKeepAlive = AiWorkspace.keepAlive(shouldResume); this.panel.log(`已接管播放器:${media.tagName.toLowerCase()},目标倍速 ${Config.playbackRate}x,静音开启`); try { let startTime = Number(media.currentTime || 0); const started = await Utils.poll(() => { const currentMedia = AiWorkspace.getMedia(); if (currentMedia) media = currentMedia; if (!media) return false; const currentTime = Number(media.currentTime || 0); return currentTime > startTime + 0.5 || (!media.paused && media.readyState >= 2 && currentTime > startTime + 0.2); }, { interval: 500, timeout: 15000 }); if (!started) { this.panel.log('未确认到视频实际开始播放,停止当前轮次'); return false; } startTime = Number(media.currentTime || 0); let resolveEnded; const endedPromise = new Promise(resolve => { resolveEnded = resolve; }); const onEnded = () => { playbackState.completed = true; resolveEnded(true); }; media.addEventListener('ended', onEnded); const done = await Promise.race([ endedPromise, Utils.poll(() => { if (playbackState.completed) return true; const currentMedia = AiWorkspace.getMedia(); if (currentMedia) media = currentMedia; if (AiWorkspace.isPlayerDone(media, { startTime, minPlayedDelta: 3 })) { playbackState.completed = true; return true; } return false; }, { interval: 1000, timeout: await Utils.getDDL() }) ]); media.removeEventListener('ended', onEnded); playbackState.completed = true; if (!done) { this.panel.log('等待播放完成超时,停止当前轮次'); return false; } } finally { stopObserve(); stopKeepAlive(); } this.panel.log(`${title} 播放完成`); return true; } async solveExerciseQuestion(root, label = '') { const questionRoot = AiWorkspace.getExerciseQuestionBody(root); if (!questionRoot) { this.panel.log('未找到题目容器,停止当前轮次'); return false; } if (AiWorkspace.isExerciseAnswered(questionRoot)) { this.panel.log(`${label || '当前题目'} 已完成,跳过`); return true; } const listContainer = questionRoot.querySelector('.list-inline.list-unstyled-radio') || questionRoot.querySelector('.list-unstyled.list-unstyled-radio') || questionRoot.querySelector('.list-unstyled') || questionRoot.querySelector('ul.list') || questionRoot.querySelector('[class*="option-list"]') || questionRoot.querySelector('[class*="answer-list"]') || questionRoot.querySelector('ul') || questionRoot.querySelector('[role="radiogroup"]'); const optionCount = listContainer ? listContainer.querySelectorAll('li, .option-item, .answer-item, [class*="option-item"], [class*="answer-item"]').length : 0; if (!optionCount) { this.panel.log(`${label || '当前题目'} 未找到选项,跳过`); return false; } const ocrResult = await Solver.recognize(questionRoot); if (!ocrResult || ocrResult.length <= 5) { this.panel.log(`${label || '当前题目'} OCR 结果过短,跳过`); return false; } const maxRetry = 3; for (let retryCount = 0; retryCount < maxRetry; retryCount++) { try { if (retryCount > 0) this.panel.log(`${label || '当前题目'} 重试 ${retryCount}/${maxRetry - 1}`); this.panel.log('🤖 请求 AI 获取答案...'); const aiText = await Solver.askAI(ocrResult, optionCount); await Solver.autoSelectAndSubmit(aiText, questionRoot); await Utils.sleep(1200); return true; } catch (err) { this.panel.log(`AI 答题失败:${err}`); if (retryCount < maxRetry - 1) await Utils.sleep(5000); } } return false; } async advanceExerciseQuestion(root, previousFingerprint = '') { const currentRoot = AiWorkspace.getExerciseContainer() || root; const nextBtn = AiWorkspace.getExerciseActionButton(currentRoot, /下一题|下一道|下一步/); if (!nextBtn) return false; nextBtn.click(); return Utils.poll(() => { const latestRoot = AiWorkspace.getExerciseContainer() || currentRoot; const questionRoot = AiWorkspace.getExerciseQuestionBody(latestRoot); const fingerprint = AiWorkspace.normalizeText(questionRoot?.innerText || '').slice(0, 120); return fingerprint && fingerprint !== previousFingerprint; }, { interval: 500, timeout: 5000 }); } async handleExercise(route) { const featureFlags = Store.getFeatureConf(); if (!featureFlags.autoAI) { this.panel.log('已关闭 AI 自动答题,作业将直接跳过'); return true; } const ready = await Utils.poll(() => Boolean(AiWorkspace.getExerciseContainer()), { interval: 500, timeout: 20000 }); const root = AiWorkspace.getExerciseContainer(); if (!ready || !root) { this.panel.log('未找到作业容器,停止当前轮次'); return false; } this.panel.log(`开始处理作业:${AiWorkspace.getActiveLeafTitle() || route.leafId}`); const tabs = AiWorkspace.getExerciseQuestionTabs(root); if (tabs.length) { this.panel.log(`检测到题目索引 ${tabs.length} 个,按题号顺序作答`); for (let i = 0; i < tabs.length; i++) { const currentRoot = AiWorkspace.getExerciseContainer() || root; const currentTabs = AiWorkspace.getExerciseQuestionTabs(currentRoot); const currentTab = currentTabs[i]; if (!currentTab) break; currentTab.click(); await Utils.sleep(1200); await this.solveExerciseQuestion(AiWorkspace.getExerciseContainer() || currentRoot, `第 ${i + 1} 题`); } return true; } this.panel.log('未找到题号列表,尝试只处理当前题并按下一题推进'); let previousFingerprint = ''; for (let i = 0; i < 20; i++) { const currentRoot = AiWorkspace.getExerciseContainer() || root; const questionRoot = AiWorkspace.getExerciseQuestionBody(currentRoot); const fingerprint = AiWorkspace.normalizeText(questionRoot?.innerText || '').slice(0, 120); if (!fingerprint) break; if (i > 0 && fingerprint === previousFingerprint) break; await this.solveExerciseQuestion(currentRoot, this.getExerciseQuestionLabel(currentRoot) || `第 ${i + 1} 题`); previousFingerprint = fingerprint; const moved = await this.advanceExerciseQuestion(currentRoot, fingerprint); if (!moved) break; } return true; } async run() { preventScreenCheck(); const route = AiWorkspace.getRoute(); if (!route) { this.panel.log('当前页面已离开 ai-workspace/lms-graph'); return; } if (!route.leafId) { this.panel.log('未能识别当前知识点'); return; } let ok = false; if (route.type === 'video' || route.type === 'audio') { ok = await this.handleMedia(route); } else if (route.type === 'exercise') { ok = await this.handleExercise(route); } else { this.panel.log(`当前类型为 ${route.type},最小方案暂不自动处理`); return; } if (!ok) return; await this.returnToSource(); } } // ---- 路由 ---- function start() { const classroomId = Utils.getCurrentClassroomId(); const returnUrl = location.pathname.includes('/v2/web/studentLog/') ? location.href : ''; Store.setPendingAutoStart(classroomId, returnUrl); const aiRoute = AiWorkspace.getRoute(); if (aiRoute) { panel.log(`正在匹配处理逻辑:ai-workspace/lms-graph/${aiRoute.type}`); new AiWorkspaceRunner(panel).run(); return; } const url = location.host; const path = location.pathname.split('/'); const matchURL = `${url}${path[0]}/${path[1]}/${path[2]}`; panel.log(`正在匹配处理逻辑:${matchURL}`); if (matchURL.includes('yuketang.cn/v2/web') || matchURL.includes('gdufemooc.cn/v2/web')) { new V2Runner(panel).run(); } else if (matchURL.includes('yuketang.cn/pro/lms') || matchURL.includes('gdufemooc.cn/pro/lms')) { if (document.querySelector('.btn-next')) { new ProNewRunner(panel).run(); } else { new ProOldRunner(panel).run(); } } else { panel.resetStartButton('开始刷课'); panel.log('当前页面非刷课页面,应匹配 */v2/web/*、*/pro/lms/* 或 */ai-workspace/lms-graph/*'); } } // ---- 启动 ---- async function boot() { if (Utils.inIframe()) return; await Utils.waitForMountTarget(); try { panel = createPanel(); panel.log(`雨课堂刷课助手 v${Config.version} 已加载`); panel.setStartHandler(start); const pendingAutoStart = Store.getPendingAutoStart(); const currentClassroomId = Utils.getCurrentClassroomId(); if ( pendingAutoStart && Utils.isSupportedLearningPage() && currentClassroomId && pendingAutoStart.classroomId === currentClassroomId ) { panel.log(`检测到跨页面跳转,自动恢复刷课:课堂 ${currentClassroomId}`); setTimeout(() => panel.start(), 1200); } } catch (err) { console.error('面板初始化失败:', err); } } boot(); })();