// ==UserScript== // @name 雨课堂刷课助手 // @namespace http://tampermonkey.net/ // @version 1.0.1 // @description 针对雨课堂视频进行自动播放,配置题库自动答题 // @author 叶屿 // @license GPL3 // @antifeature payment 题库答题功能需要验证码(免费)或激活码(付费),视频播放等基础功能完全免费 // @match *://*.yuketang.cn/* // @match *://*.gdufemooc.cn/* // @match *://*exam.yuketang.cn/* // @match *://*-exam.yuketang.cn/* // @run-at document-start // @icon http://yuketang.cn/favicon.ico // @grant unsafeWindow // @grant GM_xmlhttpRequest // @connect qsy.iano.cn // @connect lyck6.cn // @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== /* * ========================================== * 雨课堂刷课助手 v1.0 * ========================================== * * 【功能说明】 * 1. 视频自动播放(支持倍速、静音、防暂停) * 2. 作业自动答题(OCR识别 + 题库查询) * 3. 考试自动答题(支持停止/继续) * 4. 双模式题库(免费验证码 / 付费激活码) * * 【积分购买】 * 联系微信:C919irt * 价格表:50积分=2元,100积分=4元,150积分=6元,200积分=8元,500积分=18元 * 说明:每次答题消耗1积分,积分永久有效 * * 【付费声明】 * 本脚本基础功能(视频播放、进度保存)完全免费 * 题库答题功能需要验证码(免费24小时)或激活码(付费永久) * 付费仅用于题库API调用成本,不强制购买 * * 【免责声明】 * 本脚本仅供学习交流使用,请勿用于违反学校规定或作弊行为 * 使用本脚本造成的任何后果由使用者自行承担 * * 【版权信息】 * 作者:叶屿 | 版本:v1.0 | 更新:2026-02-08 * * ========================================== */ (() => { 'use strict'; let panel; // UI 面板实例后置初始化 let isRunning = false; // 标记是否正在运行 let stopRequested = false; // 标记是否请求停止 // ---- 脚本配置,用户可修改 ---- const Config = { version: '1.0', // 版本号 playbackRate: 2, // 视频播放倍速 pptInterval: 3000, // ppt翻页间隔 storageKeys: { // 使用者勿动 progress: '[雨课堂脚本]刷课进度信息', deviceId: 'ykt_device_id', activationCode: 'ykt_activation_code', answerMode: 'ykt_answer_mode', // 答题模式:free/paid verifyValidUntil: 'ykt_verify_valid_until', // 验证码有效期 proClassCount: 'pro_lms_classCount', feature: 'ykt_feature_conf' // 是否开启题库作答/自动评论 } }; 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; }, 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; } }; // ---- 存储工具 ---- 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)); }, getDeviceId() { let deviceId = localStorage.getItem(Config.storageKeys.deviceId); if (!deviceId) { deviceId = 'ykt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); localStorage.setItem(Config.storageKeys.deviceId, deviceId); } return deviceId; }, getActivationCode() { return localStorage.getItem(Config.storageKeys.activationCode) || ''; }, setActivationCode(code) { localStorage.setItem(Config.storageKeys.activationCode, code); }, getAnswerMode() { return localStorage.getItem(Config.storageKeys.answerMode) || 'free'; }, setAnswerMode(mode) { localStorage.setItem(Config.storageKeys.answerMode, mode); }, getVerifyValidUntil() { return Number(localStorage.getItem(Config.storageKeys.verifyValidUntil)) || 0; }, setVerifyValidUntil(timestamp) { localStorage.setItem(Config.storageKeys.verifyValidUntil, timestamp); }, isVerifyValid() { const validUntil = this.getVerifyValidUntil(); return validUntil > Date.now() / 1000; }, 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)); } }; // ---- UI 面板 ---- function createPanel() { const iframe = document.createElement('iframe'); iframe.style.position = 'fixed'; iframe.style.top = '40px'; iframe.style.left = '40px'; iframe.style.width = '480px'; iframe.style.height = '520px'; iframe.style.zIndex = '999999'; iframe.style.border = '1px solid #a3a3a3'; iframe.style.borderRadius = '12px'; iframe.style.background = '#fff'; iframe.style.overflow = 'hidden'; iframe.style.boxShadow = '0 10px 40px rgba(0,0,0,0.2)'; iframe.setAttribute('frameborder', '0'); iframe.setAttribute('id', 'ykt-helper-iframe'); iframe.setAttribute('allowtransparency', 'true'); document.body.appendChild(iframe); const doc = iframe.contentDocument || iframe.contentWindow.document; doc.open(); doc.write(`
📚
雨课堂助手
💳
付费题库
需要激活码
🆓
免费题库
需要验证码
扫码获取验证码
扫码观看广告获取验证码
验证后免费使用24小时
`); 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'), settings: doc.getElementById('settings'), saveSettings: doc.getElementById('save_settings'), closeSettings: doc.getElementById('close_settings'), modePaid: doc.getElementById('mode-paid'), modeFree: doc.getElementById('mode-free'), activationCodeSection: doc.getElementById('activation-code-section'), activationCodeInput: doc.getElementById('activation_code'), activateBtn: doc.getElementById('activate_btn'), activateStatus: doc.getElementById('activate_status'), creditsDisplay: doc.getElementById('credits_display'), wechatCopy: doc.getElementById('wechat_copy'), feedbackWechat: doc.getElementById('feedback_wechat'), verifyCodeSection: doc.getElementById('verify-code-section'), verifyCodeInput: doc.getElementById('verify_code'), verifyBtn: doc.getElementById('verify_btn'), verifyStatus: doc.getElementById('verify_status'), 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(); }); // 缩小按钮拖拽 ui.miniBasic.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(); e.stopPropagation(); // 防止触发点击事件 }); 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('雨课堂助手 v1.0\n作者:叶屿\n\n功能说明:\n- 自动播放视频/音频\n- 题库自动答题\n- 自动评论回复'); }); 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 queryCredits = async () => { const deviceId = Store.getDeviceId(); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://qsy.iano.cn/index.php?s=/api/question_bank/credits&device_id=${deviceId}`, timeout: 15000, onload: res => { if (res.status === 200) { try { const json = JSON.parse(res.responseText); if (json.code === 1) { resolve(json.data); } else { reject(json.msg || '查询失败'); } } catch (e) { reject('JSON 解析失败'); } } else { reject(`请求失败: HTTP ${res.status}`); } }, onerror: () => reject('网络错误'), ontimeout: () => reject('请求超时') }); }); }; // 更新积分显示 const updateCreditsDisplay = async () => { try { const data = await queryCredits(); ui.creditsDisplay.textContent = `${data.remaining_credits} 积分`; } catch (err) { ui.creditsDisplay.textContent = '-- 积分'; } }; // 验证验证码 const verifyCode = async (code) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'https://qsy.iano.cn/index.php?s=/api/code/verify', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: 'code=' + encodeURIComponent(code), timeout: 15000, onload: res => { if (res.status === 200) { try { const json = JSON.parse(res.responseText); if (json.code === 1 && json.data.valid) { resolve(json); } else { reject(json.msg || '验证码无效或已过期'); } } catch (e) { reject('JSON 解析失败'); } } else { reject(`请求失败: HTTP ${res.status}`); } }, onerror: () => reject('网络错误'), ontimeout: () => reject('请求超时') }); }); }; // 激活题库激活码 const activateCode = async (code, deviceId) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'https://qsy.iano.cn/index.php?s=/api/question_bank/activate', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ code: code, device_id: deviceId }), timeout: 15000, onload: res => { if (res.status === 200) { try { const json = JSON.parse(res.responseText); if (json.code === 1) { resolve(json); } else { reject(json.msg || '激活失败'); } } catch (e) { reject('JSON 解析失败'); } } else { reject(`请求失败: HTTP ${res.status}`); } }, onerror: () => reject('网络错误'), ontimeout: () => reject('请求超时') }); }); }; // 切换答题模式 const switchAnswerMode = (mode) => { Store.setAnswerMode(mode); if (mode === 'paid') { ui.modePaid.classList.add('selected'); ui.modeFree.classList.remove('selected'); ui.activationCodeSection.style.display = 'block'; ui.verifyCodeSection.style.display = 'none'; updateCreditsDisplay(); // 切换到付费模式时查询积分 } else { ui.modeFree.classList.add('selected'); ui.modePaid.classList.remove('selected'); ui.activationCodeSection.style.display = 'none'; ui.verifyCodeSection.style.display = 'block'; } }; // 绑定答题模式切换事件 ui.modePaid.addEventListener('click', () => switchAnswerMode('paid')); ui.modeFree.addEventListener('click', () => switchAnswerMode('free')); // 绑定微信号复制 ui.wechatCopy.addEventListener('click', () => { const wechat = 'C919irt'; const textarea = doc.createElement('textarea'); textarea.value = wechat; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; doc.body.appendChild(textarea); textarea.select(); try { doc.execCommand('copy'); ui.wechatCopy.textContent = '✅ 已复制'; setTimeout(() => { ui.wechatCopy.textContent = 'C919irt 📋'; }, 2000); } catch (err) { console.error('复制失败', err); } doc.body.removeChild(textarea); }); // 绑定反馈微信号复制 ui.feedbackWechat.addEventListener('click', () => { const wechat = 'C919irt'; const textarea = doc.createElement('textarea'); textarea.value = wechat; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; doc.body.appendChild(textarea); textarea.select(); try { doc.execCommand('copy'); ui.feedbackWechat.textContent = '✅ 已复制'; setTimeout(() => { ui.feedbackWechat.textContent = 'C919irt 📋'; }, 2000); } catch (err) { console.error('复制失败', err); } doc.body.removeChild(textarea); }); // 绑定激活按钮 ui.activateBtn.addEventListener('click', async () => { const code = ui.activationCodeInput.value.trim(); if (!code) { ui.activateStatus.className = 'verify-status error'; ui.activateStatus.textContent = '❌ 请输入激活码'; ui.activateStatus.style.display = 'block'; return; } ui.activateBtn.disabled = true; ui.activateBtn.textContent = '激活中...'; ui.activateStatus.style.display = 'none'; try { const deviceId = Store.getDeviceId(); const result = await activateCode(code, deviceId); Store.setActivationCode(code); ui.activateStatus.className = 'verify-status success'; ui.activateStatus.textContent = `✅ 激活成功!获得 ${result.data.credits} 积分`; ui.activateStatus.style.display = 'block'; ui.activationCodeInput.value = ''; await updateCreditsDisplay(); // 更新积分显示 log(`✅ 激活成功!获得 ${result.data.credits} 积分`); } catch (err) { ui.activateStatus.className = 'verify-status error'; ui.activateStatus.textContent = `❌ ${err}`; ui.activateStatus.style.display = 'block'; } finally { ui.activateBtn.disabled = false; ui.activateBtn.textContent = '激活'; } }); // 绑定验证码验证按钮 ui.verifyBtn.addEventListener('click', async () => { const vcode = ui.verifyCodeInput.value.trim(); if (!vcode || vcode.length !== 4) { ui.verifyStatus.className = 'verify-status error'; ui.verifyStatus.textContent = '❌ 请输入4位验证码'; ui.verifyStatus.style.display = 'block'; return; } ui.verifyBtn.disabled = true; ui.verifyBtn.textContent = '验证中...'; ui.verifyStatus.style.display = 'none'; try { const result = await verifyCode(vcode); Store.setVerifyValidUntil(result.data.valid_until); ui.verifyStatus.className = 'verify-status success'; ui.verifyStatus.textContent = `✅ 验证成功!有效期至 ${result.data.valid_until_str}`; ui.verifyStatus.style.display = 'block'; ui.verifyCodeInput.value = ''; ui.verifyCodeInput.disabled = true; log(`✅ 验证码验证成功!有效期至 ${result.data.valid_until_str}`); } catch (err) { ui.verifyStatus.className = 'verify-status error'; ui.verifyStatus.textContent = `❌ ${err}`; ui.verifyStatus.style.display = 'block'; } finally { ui.verifyBtn.disabled = false; ui.verifyBtn.textContent = '验证'; } }); // 支持回车键验证 ui.verifyCodeInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { ui.verifyBtn.click(); } }); // 检查验证码状态 const checkVerifyStatus = () => { if (Store.isVerifyValid()) { const validUntil = Store.getVerifyValidUntil(); const date = new Date(validUntil * 1000); const dateStr = date.toLocaleString('zh-CN'); ui.verifyStatus.className = 'verify-status success'; ui.verifyStatus.textContent = `✅ 已验证,有效期至 ${dateStr}`; ui.verifyCodeInput.disabled = true; ui.verifyBtn.disabled = true; } else { ui.verifyCodeInput.disabled = false; ui.verifyBtn.disabled = false; ui.verifyStatus.style.display = 'none'; } }; const loadAnswerMode = () => { const mode = Store.getAnswerMode(); switchAnswerMode(mode); if (mode === 'free') { checkVerifyStatus(); } else { updateCreditsDisplay(); // 付费模式加载时查询积分 } }; const loadActivationCode = () => { ui.activationCodeInput.value = Store.getActivationCode(); }; const loadFeatureConf = () => { const saved = Store.getFeatureConf(); ui.featureAutoAI.checked = saved.autoAI; ui.featureAutoComment.checked = saved.autoComment; }; loadAnswerMode(); loadActivationCode(); loadFeatureConf(); ui.btnSetting.onclick = () => { loadAnswerMode(); loadActivationCode(); loadFeatureConf(); ui.settings.style.display = 'block'; }; ui.closeSettings.onclick = () => { ui.settings.style.display = 'none'; }; ui.saveSettings.onclick = async () => { const mode = Store.getAnswerMode(); // 保存功能配置 const featureConf = { autoAI: ui.featureAutoAI.checked, autoComment: ui.featureAutoComment.checked }; Store.setFeatureConf(featureConf); // 检查激活状态并给出提示 if (featureConf.autoAI) { if (mode === 'paid') { try { const data = await queryCredits(); if (data.remaining_credits <= 0) { log('⚠️ 积分不足,请先购买激活码充值'); } else { log(`✅ 题库配置已保存 (付费模式,剩余 ${data.remaining_credits} 积分)`); } } catch (err) { log('⚠️ 题库配置已保存,但未检测到激活码,请先激活'); } } else { // 免费模式 if (!Store.isVerifyValid()) { log('⚠️ 题库配置已保存,但验证码未验证或已过期,请先验证'); } else { log('✅ 题库配置已保存 (免费模式)'); } } } else { log('✅ 题库配置已保存'); } ui.settings.style.display = 'none'; }; ui.btnClear.onclick = () => { Store.removeProgress(window.parent.location.href); localStorage.removeItem(Config.storageKeys.proClassCount); log('已清除当前课程的刷课进度缓存'); }; // 后面赋值给panel return { ...ui, log, setStartHandler(fn) { ui.btnStart.onclick = () => { // 如果正在运行,点击停止 if (isRunning) { stopRequested = true; log('⏸️ 正在停止...'); ui.btnStart.innerText = '停止中...'; ui.btnStart.disabled = true; return; } log('启动中...'); isRunning = true; stopRequested = false; // 检查是否是考试页面 const url = location.host; const path = location.pathname.split('/'); if (url.includes('exam.yuketang.cn') || path.includes('exam')) { ui.btnStart.innerText = '⏸️ 停止答题'; } else { ui.btnStart.innerText = '⏸️ 停止刷课'; } ui.btnStart.disabled = false; fn && fn(); }; }, resetStartButton(text = '开始刷课') { ui.btnStart.innerText = text; ui.btnStart.disabled = false; isRunning = false; stopRequested = false; } }; } // ---- 播放器工具 ---- const Player = { 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) { if (!video) return () => { }; const target = document.getElementsByClassName('play-btn-tip')[0]; if (!target) return () => { }; // 自动播放 const playVideo = () => { video.play().catch(e => { console.warn('自动播放失败:', e); setTimeout(playVideo, 3000); }); }; playVideo(); const observer = new MutationObserver(list => { for (const mutation of list) { if (mutation.type === 'childList' && target.innerText === '播放') { video.play(); } } }); observer.observe(target, { childList: true }); return () => 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); } }); } }; // ---- 防切屏 ---- 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: () => { } } }); } // ---- JSON Hook:拦截雨课堂服务器返回的题目数据 ---- let interceptedProblems = null; // 存储拦截到的题目数据 function setupJSONParseHook() { const originalParse = JSON.parse; JSON.parse = function (...args) { const result = originalParse.call(this, ...args); try { // 拦截雨课堂考试/作业的题目数据 if (result && result.data && result.data.problems && result.data.problems.length > 0) { interceptedProblems = parseYktProblems(result.data.problems, result.data.font); console.log('[雨课堂助手] 拦截到题目数据:', interceptedProblems.length, '题'); } } catch (e) { console.warn('[雨课堂助手] JSON Hook 解析异常:', e); } return result; }; } // 解析雨课堂题目数据(参考官方脚本的 parseYkt) function parseYktProblems(problems, font) { return problems.map(item => { const content = item.content || item; const typeText = content.TypeText || ''; const type = getQuestionType(typeText); const body = content.Body || ''; // 清理HTML标签,提取纯文本 const question = formatString(body); let options = []; if (type <= 1 && content.Options) { // 单选/多选:按 key 排序后提取选项文本 options = content.Options .sort((a, b) => (a.key || '').charCodeAt(0) - (b.key || '').charCodeAt(0)) .map(opt => formatString(opt.value || '')); } else if (type === 3 && content.Options) { // 判断题 options = ['正确', '错误']; } // 提取已有答案(如果有的话,用于结果页面) let answer = []; if (content.Answer) { if (Array.isArray(content.Answer)) { answer = content.Answer; } else if (typeof content.Answer === 'string') { answer = content.Answer.split(''); } } // 处理用户已答的答案 if (item.user && item.user.is_show_answer && item.user.answer) { if (type === 3) { answer = item.user.answer.map(a => a.replace('true', '正确').replace('false', '错误')); } else { answer = item.user.answer; } } return { qid: item.problem_id || '', question, options, type, answer }; }).filter(i => i.question); } // 题目类型映射(参考官方脚本) function getQuestionType(str) { if (!str) return 4; str = str.trim().replace(/\s+/g, ''); const TYPE = { '单选题': 0, '单选': 0, '单项选择题': 0, '单项选择': 0, '多选题': 1, '多选': 1, '多项选择题': 1, '多项选择': 1, '案例分析': 1, '填空题': 2, '填空': 2, '判断题': 3, '判断': 3, '对错题': 3, '判断正误': 3, '问答题': 4, '简答题': 4, '主观题': 4, '其它': 4 }; if (TYPE[str] !== undefined) return TYPE[str]; for (const key of Object.keys(TYPE)) { if (str.includes(key)) return TYPE[key]; } return 4; } // 格式化字符串(参考官方脚本的 formatString) function formatString(src) { if (!src) return ''; src = String(src); // 去除HTML标签(保留img标签信息) if (!src.includes('img') && !src.includes('iframe')) { const temp = document.createElement('div'); temp.innerHTML = src; src = temp.innerText || temp.textContent || ''; } // 全角转半角 src = src.replace(/[\uff01-\uff5e]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 65248) ); return src.replace(/\s+/g, ' ') .replace(/[""]/g, '"') .replace(/['']/g, "'") .replace(/。/g, '.') .replace(/[,.?:!;]$/, '') .trim(); } // 字符串相似度计算(编辑距离,参考官方脚本) function stringSimilarity(s, t) { if (!s || !t) return 0; if (s === t) return 100; const l = Math.max(s.length, t.length); const n = s.length, m = t.length; const d = []; for (let i = 0; i <= n; i++) { d[i] = []; d[i][0] = i; } for (let j = 0; j <= m; j++) { d[0][j] = j; } for (let i = 1; i <= n; i++) { for (let j = 1; j <= m; j++) { const cost = s.charAt(i - 1) === t.charAt(j - 1) ? 0 : 1; d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost); } } return Number(((1 - d[n][m] / l) * 100).toFixed(2)); } // ---- OCR & 题库API ---- const Solver = { // 直接从DOM读取题目(不再依赖OCR截图) readFromDOM(element) { if (!element) return { question: '', options: [], type: 4 }; // 读取题目文本 - 使用官方脚本的选择器 let questionText = ''; const questionEl = element.querySelector('h4') || element.querySelector('.problem-body') || element.querySelector('span:first-child'); if (questionEl) { questionText = formatString(questionEl.innerText || questionEl.textContent || ''); } if (!questionText) { questionText = formatString(element.innerText || element.textContent || ''); } // 清理题号和分数 questionText = questionText .replace(/^\d+[、.]\s*/, '') .replace(/[((](\d+(\.\d+)?分)[))]$/, '') .trim(); // 读取选项 let options = []; const optionContainer = element.querySelector('ul'); if (optionContainer) { const optionEls = optionContainer.querySelectorAll('li'); options = Array.from(optionEls).map(li => { const text = formatString(li.innerText || li.textContent || ''); return text.replace(/^[A-Ga-g][.、]\s*/, '').trim(); }).filter(t => t); } // 判断题目类型 const typeEl = element.querySelector('.item-type') || element.closest('.subject-item')?.querySelector('.item-type'); let type = 4; if (typeEl) { type = getQuestionType(typeEl.innerText || typeEl.textContent || ''); } else if (options.length === 2) { // 两个选项大概率是判断题 const joined = options.join(',').toLowerCase(); if (joined.includes('正确') || joined.includes('错误') || joined.includes('对') || joined.includes('错') || joined.includes('true') || joined.includes('false')) { type = 3; } } else if (options.length >= 3) { type = 0; // 默认单选 } return { question: questionText, options, type }; }, // 从拦截的数据中查找匹配的题目 findInterceptedQuestion(index) { if (!interceptedProblems || !interceptedProblems.length) return null; if (index >= 0 && index < interceptedProblems.length) { return interceptedProblems[index]; } return null; }, // 综合识别:优先用拦截数据,其次DOM读取,最后OCR async recognize(element, questionIndex) { // 方式1:从拦截的JSON数据中获取(最准确) const intercepted = this.findInterceptedQuestion(questionIndex); if (intercepted && intercepted.question && intercepted.question.length > 3) { panel.log(`📋 从服务器数据获取题目 (第${questionIndex + 1}题)`); return intercepted; } // 方式2:直接从DOM读取文本(快速,不需要OCR) const domResult = this.readFromDOM(element); if (domResult.question && domResult.question.length > 5) { panel.log(`📖 从页面DOM读取题目`); return domResult; } // 方式3:OCR识别(兜底方案) if (typeof html2canvas !== 'undefined' && typeof Tesseract !== 'undefined') { try { panel.log('📸 DOM读取失败,尝试OCR识别...'); 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'); const fullText = text.replace(/\s+/g, ' ').trim(); const lines = text.split('\n').map(l => l.trim()).filter(l => l); let options = []; const optionPattern = /^[A-F][.、::]?\s*(.+)/; for (const line of lines) { const match = line.match(optionPattern); if (match) options.push(match[1].trim()); } return { question: fullText, options, type: domResult.type }; } catch (err) { panel.log(`⚠️ OCR失败: ${err.message || '未知错误'}`); } } return { question: '', options: [], type: 4 }; }, async askQuestionBank(question, options = [], type = 4) { const mode = Store.getAnswerMode(); if (mode === 'free') { // 免费题库API return this.askFreeQuestionBank(question, options, type); } else { // 付费题库API return this.askPaidQuestionBank(question, options, type); } }, async askFreeQuestionBank(question, options = [], type = 4) { const API_URL = 'http://lyck6.cn/scriptService/api/autoFreeAnswer'; return new Promise((resolve, reject) => { panel.log('🔍 正在查询免费题库...'); GM_xmlhttpRequest({ method: 'POST', url: API_URL, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ question: question, options: options, type: type, location: '雨课堂' }), timeout: 15000, onload: res => { if (res.status === 200) { try { const json = JSON.parse(res.responseText); if (json.code === 0 && json.result) { const answers = json.result.answers || []; const success = json.result.success || false; // 处理答案格式 let finalAnswers = []; if (success && Array.isArray(answers) && answers.length > 0) { // 如果success为true,answers是索引数组,需要转换为选项 if (typeof answers[0] === 'number') { finalAnswers = answers.map(idx => { if (options[idx]) return options[idx]; // 如果没有提供options,返回字母 return String.fromCharCode(65 + idx); }); } else { finalAnswers = answers; } } else if (Array.isArray(answers) && answers.length > 0) { // 多个答案数组,取第一个 finalAnswers = answers[0] || []; } panel.log(`✅ 免费题库查询成功`); resolve({ answers: finalAnswers, remaining: -1 }); } else { const msg = json.message || '题库查询失败'; panel.log(`⚠️ ${msg}`); reject(msg); } } catch (e) { reject('JSON 解析失败'); } } else { const err = `请求失败: HTTP ${res.status}`; panel.log(err); reject(err); } }, onerror: () => reject('网络错误'), ontimeout: () => reject('请求超时') }); }); }, async askPaidQuestionBank(question, options = [], type = 4) { const API_URL = 'https://qsy.iano.cn/index.php?s=/api/question_bank/answer'; const deviceId = Store.getDeviceId(); return new Promise((resolve, reject) => { panel.log('🔍 正在查询付费题库...'); GM_xmlhttpRequest({ method: 'POST', url: API_URL, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ device_id: deviceId, question: question, options: options, type: type, location: '雨课堂' }), timeout: 15000, onload: res => { if (res.status === 200) { try { const json = JSON.parse(res.responseText); if (json.code === 1 && json.data && json.data.result) { const answers = json.data.result.answers || []; const remaining = json.data.remaining_credits || 0; panel.log(`✅ 付费题库查询成功,剩余积分: ${remaining}`); resolve({ answers, remaining }); } else { const msg = json.msg || '题库查询失败'; panel.log(`⚠️ ${msg}`); reject(msg); } } catch (e) { reject('JSON 解析失败'); } } else { const err = `请求失败: HTTP ${res.status}`; panel.log(err); reject(err); } }, onerror: () => reject('网络错误'), ontimeout: () => reject('请求超时') }); }); }, async autoSelectAndSubmit(answers, itemBodyElement) { if (!answers || !answers.length) { panel.log('⚠️ 未获取到答案,请人工检查'); return; } // 处理答案格式,使用改进的匹配逻辑 let targetIndices = []; const letterMap = { 'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6 }; // 获取页面上的选项文本,用于相似度匹配 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('ul'); const optionEls = listContainer ? listContainer.querySelectorAll('li') : []; const pageOptions = Array.from(optionEls).map(li => formatString(li.innerText || li.textContent || '').replace(/^[A-Ga-g][.、]\s*/, '').trim().toLowerCase() ); for (const answer of answers) { const answerStr = String(answer).trim(); const answerUpper = answerStr.toUpperCase(); const answerLower = answerStr.toLowerCase().replace(/\s/g, ''); // 1. 判断题处理 if (/^(对|正确|√|T|ri|true|是)$/i.test(answerStr)) { targetIndices = [0]; break; } if (/^(错|错误|×|F|不是|wr|false|否)$/i.test(answerStr)) { targetIndices = [1]; break; } // 2. 纯字母答案(如 "A", "AC", "BCD") if (/^[A-G]+$/.test(answerUpper) && answerUpper.length <= 7) { let isOrdered = true; for (let i = 1; i < answerUpper.length; i++) { if (answerUpper.charCodeAt(i) <= answerUpper.charCodeAt(i - 1)) { isOrdered = false; break; } } if (isOrdered) { for (const ch of answerUpper) { if (letterMap[ch] !== undefined && !targetIndices.includes(letterMap[ch])) { targetIndices.push(letterMap[ch]); } } continue; } } // 3. 精确匹配选项文本 const exactIdx = pageOptions.indexOf(answerLower); if (exactIdx >= 0 && !targetIndices.includes(exactIdx)) { targetIndices.push(exactIdx); continue; } // 4. 相似度匹配(当答案文本较长时) if (answerLower.length >= 5 && pageOptions.length > 0) { const ratings = pageOptions.map(opt => stringSimilarity(answerLower, opt)); const maxScore = Math.max(...ratings); if (maxScore > 65) { const bestIdx = ratings.indexOf(maxScore); if (!targetIndices.includes(bestIdx)) { targetIndices.push(bestIdx); } } } } if (!targetIndices.length) { panel.log(`⚠️ 无法解析答案: ${answers.join(', ')}`); return; } panel.log(`✅ 题库答案:${answers.join(', ')} → 选择第 ${targetIndices.map(i => String.fromCharCode(65 + i)).join('')} 项`); if (!listContainer) { panel.log('⚠️ 未找到选项容器'); return; } const options = listContainer.querySelectorAll('li'); 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('input') || options[idx]; clickable.click(); await Utils.sleep(300); } const submitBtn = (() => { const local = itemBodyElement.parentElement.querySelectorAll('.el-button--primary'); for (const btn of local) { if (btn.innerText.includes('提交')) return btn; } const global = document.querySelectorAll('.el-button.el-button--primary.el-button--medium'); for (const btn of global) { if (btn.innerText.includes('提交') && 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; } updateProgress(outside, inside = 0) { this.outside = outside; this.inside = inside; Store.setProgress(this.baseUrl, outside, inside); } async run() { this.panel.log(`检测到已播放到第 ${this.outside} 集,继续刷课...`); while (true) { // 检查是否请求停止 if (stopRequested) { this.panel.log('⏸️ 已停止刷课'); this.panel.resetStartButton('继续刷课'); return; } await this.autoSlide(); const list = document.querySelector('.logs-list')?.childNodes; if (!list || !list.length) { 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); 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'; this.panel.log(`刷课状态:第 ${this.outside + 1}/${list.length} 个,类型 ${type}`); 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); } } } 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(); 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(() => 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}项`; 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); } } this.updateProgress(this.outside + 1, 0); await Utils.sleep(1000); } async playAudioItem(item, title, idx) { this.panel.log(`开始播放音频:${title}`); item.click(); await Utils.sleep(2500); Player.applyMediaDefault(document.querySelector('audio')); const progressNode = document.querySelector('.progress-wrap')?.querySelector('.text'); await Utils.poll(() => 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(); 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.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) { const featureFlags = Store.getFeatureConf(); if (!featureFlags.autoComment) { this.panel.log('已关闭自动回复评论,跳过该项'); idx++; this.updateProgress(this.outside, idx); return idx; } this.panel.log(`开始处理${typeText}:${item.querySelector('h2')?.innerText || ''}`); item.click(); await Utils.sleep(1200); 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('已关闭题库自动答题,跳过该项'); idx++; this.updateProgress(this.outside, idx); return idx; } this.panel.log('进入作业,启动题库查询'); item.click(); await Utils.sleep(2000); // 等待拦截数据加载 await Utils.sleep(1000); let i = 0; while (true) { const items = document.querySelectorAll('.subject-item.J_order, .subject-item'); 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(1500); // 检查是否已答 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') || document.querySelector('.problem-body')?.parentElement; try { // 使用新的综合识别 const result = await Solver.recognize(targetEl, i); if (result && result.question && result.question.length > 3) { const { answers } = await Solver.askQuestionBank( result.question, result.options, result.type !== undefined ? result.type : 4 ); await Solver.autoSelectAndSubmit(answers, targetEl); } else { this.panel.log(`第 ${i + 1} 题识别失败`); } } catch (err) { this.panel.log(`题库查询失败:${err}`); } 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 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 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); Player.applySpeed(); const muteBtn = document.querySelector('.xt_video_player_common_icon'); muteBtn && muteBtn.click(); const stopObserve = Player.observePause(document.querySelector('video')); await Utils.poll(() => { const allTime = document.querySelector('.xt_video_player_current_time_display')?.innerText || ''; const [nowTime, totalTime] = allTime.split(' / '); return nowTime && totalTime && nowTime === totalTime; }, { interval: 800, timeout: await Utils.getDDL() }); stopObserve(); } } this.panel.log(`${className} 已播放完毕`); } else { const videoBox = document.querySelector('.video-box'); if (videoBox) { videoBox.click(); await Utils.sleep(1800); Player.applySpeed(); const muteBtn = document.querySelector('.xt_video_player_common_icon'); muteBtn && muteBtn.click(); await Utils.poll(() => { const times = document.querySelector('.xt_video_player_current_time_display')?.innerText || ''; const [nowTime, totalTime] = times.split(' / '); return nowTime && totalTime && nowTime === totalTime; }, { interval: 800, timeout: await Utils.getDDL() }); this.panel.log(`${className} 视频播放完毕`); } } this.updateProgress(this.outside + 1, 0); history.back(); await Utils.sleep(1000); } } // ---- 考试答题 Runner ---- class ExamRunner { constructor(panel) { this.panel = panel; } async run() { this.panel.log('🎯 开始自动答题...'); await Utils.sleep(2000); let retryCount = 0; const maxRetry = 5; let items = null; while (retryCount < maxRetry) { if (stopRequested) { this.panel.log('⏸️ 已停止答题'); this.panel.resetStartButton('继续答题'); return; } // 使用官方脚本的选择器 items = document.querySelectorAll('.exam-main--body .subject-item'); if (!items || items.length === 0) { items = document.querySelectorAll('.subject-item'); } if (!items || items.length === 0) { items = document.querySelectorAll('.container-problem .subject-item'); } if (items && items.length > 0) { this.panel.log(`找到 ${items.length} 道题目,开始答题`); break; } retryCount++; this.panel.log(`未找到题目,等待页面加载... (${retryCount}/${maxRetry})`); await Utils.sleep(3000); } if (!items || items.length === 0) { this.panel.log('❌ 未找到题目,请确认页面已加载完成'); this.panel.log('💡 提示:可能需要先点击"开始考试"按钮'); this.panel.resetStartButton('开始答题'); return; } let i = 0; while (i < items.length) { if (stopRequested) { this.panel.log('⏸️ 已停止答题'); this.panel.resetStartButton('继续答题'); return; } const item = items[i]; this.panel.log(`📝 正在处理第 ${i + 1}/${items.length} 题...`); item.scrollIntoView({ behavior: 'smooth', block: 'center' }); await Utils.sleep(800); // 检查是否已答题(使用官方脚本的 is-checked 选择器) if (item.querySelector('.is-checked')) { this.panel.log(`第 ${i + 1} 题已作答,跳过`); i++; continue; } // 查找题目内容区域 const questionArea = item.querySelector('.item-body') || item; try { // 使用新的综合识别方法(优先拦截数据 > DOM读取 > OCR) const result = await Solver.recognize(questionArea, i); if (result && result.question && result.question.length > 3) { // 查询题库 const { answers } = await Solver.askQuestionBank( result.question, result.options, result.type !== undefined ? result.type : 4 ); // 自动选择答案 await Solver.autoSelectAndSubmit(answers, questionArea); await Utils.sleep(1500); } else { this.panel.log(`第 ${i + 1} 题识别失败,跳过`); } } catch (err) { this.panel.log(`第 ${i + 1} 题查询失败:${err}`); } i++; await Utils.sleep(1000); } this.panel.log('🎉 答题完成!请检查并提交'); this.panel.resetStartButton('答题完成'); } } // ---- 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) { // 检查是否请求停止 if (stopRequested) { this.panel.log('⏸️ 已停止刷课'); this.panel.resetStartButton('继续刷课'); return; } 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('课程播放完毕 🎉'); this.panel.resetStartButton('刷完啦~'); break; } } } } // ---- 路由 ---- function start() { const url = location.host; const path = location.pathname.split('/'); const matchURL = `${url}${path[0]}/${path[1]}/${path[2]}`; panel.log(`正在匹配处理逻辑:${matchURL}`); // 检查是否是考试页面 if (url.includes('exam.yuketang.cn') || path.includes('exam')) { // 检查是否是结果页面 if (location.pathname.includes('/result/')) { panel.log('检测到考试结果页面,无法答题'); panel.log('💡 提示:此页面为已完成的考试结果,无法修改答案'); panel.resetStartButton('查看结果'); return; } panel.log('检测到考试页面,准备启动自动答题功能'); // 检查是否启用自动答题 const featureFlags = Store.getFeatureConf(); if (!featureFlags.autoAI) { panel.log('⚠️ 请先在【题库配置】中启用自动答题功能'); panel.resetStartButton('开始答题'); return; } // 检查验证/激活状态 const mode = Store.getAnswerMode(); if (mode === 'free') { // 免费模式,检查验证码 if (!Store.isVerifyValid()) { panel.log('⚠️ 验证码未验证或已过期,请先在【题库配置】中验证'); panel.resetStartButton('开始答题'); return; } panel.log('✅ 验证码有效,启动自动答题'); } else { // 付费模式,检查积分(不阻止启动,只提示) panel.log('✅ 付费模式,启动自动答题'); } // 启动考试答题 new ExamRunner(panel).run(); return; } 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/*'); panel.log('考试页面请点击【开始答题】启动自动答题'); } } // ---- 启动 ---- if (Utils.inIframe()) return; // 尽早设置 JSON Hook(在 document-start 阶段拦截数据) setupJSONParseHook(); // 等待 document.body 加载完成 const waitForBody = () => { return new Promise(resolve => { if (document.body) { resolve(); } else { const observer = new MutationObserver(() => { if (document.body) { observer.disconnect(); resolve(); } }); observer.observe(document.documentElement, { childList: true }); } }); }; // 初始化面板 (async () => { await waitForBody(); panel = createPanel(); // 初始信息已在HTML中显示,不需要额外log panel.setStartHandler(start); })(); })();