// ==UserScript== // @name 雨课堂刷课助手 // @namespace http://tampermonkey.net/ // @version 2.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 // @connect open.bigmodel.cn // @connect yuketang.cn // @connect gdufemooc.cn // @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 // @require https://cdn.jsdelivr.net/npm/opentype.js@1.3.4/dist/opentype.min.js // ==/UserScript== /* * ========================================== * 雨课堂刷课助手 v2.0.1 * ========================================== * * 【功能说明】 * 1. 视频自动播放(支持倍速、静音、防暂停) * 2. 作业自动答题(OCR识别 + 题库查询) * 3. 考试自动答题(支持停止/继续) * 4. 双模式题库(免费验证码 / 付费激活码) * * 【积分购买】 * 联系微信:C919irt * 价格表:50积分=2元,100积分=4元,150积分=6元,200积分=8元,500积分=18元 * 说明:每次答题消耗1积分,积分永久有效 * * 【付费声明】 * 本脚本基础功能(视频播放、进度保存)完全免费 * 题库答题功能需要验证码(免费24小时)或激活码(付费永久) * 付费仅用于题库API调用成本,不强制购买 * * 【免责声明】 * 本脚本仅供学习交流使用,请勿用于违反学校规定或作弊行为 * 使用本脚本造成的任何后果由使用者自行承担 * * 【版权信息】 * 作者:叶屿 | 版本:v2.0.1 | 更新:2026-03-10 * * ========================================== */ (() => { 'use strict'; let panel; // UI 面板实例后置初始化 let isRunning = false; // 标记是否正在运行 let stopRequested = false; // 标记是否请求停止 // ---- 脚本配置,用户可修改 ---- const Config = { version: '2.0.1', // 版本号 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', // 是否开启题库作答/自动评论 runState: 'ykt_run_state', aiApiKey: 'ykt_ai_api_key' } }; 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)); }, getRunState() { const raw = localStorage.getItem(Config.storageKeys.runState); return Utils.safeJSONParse(raw, null); }, setRunState(status) { const state = { status, lastActiveTime: Date.now() }; localStorage.setItem(Config.storageKeys.runState, JSON.stringify(state)); }, clearRunState() { localStorage.removeItem(Config.storageKeys.runState); }, touchRunState() { const state = this.getRunState(); if (state && state.status === 'running') { state.lastActiveTime = Date.now(); localStorage.setItem(Config.storageKeys.runState, JSON.stringify(state)); } }, getAIApiKey() { return localStorage.getItem(Config.storageKeys.aiApiKey) || ''; }, setAIApiKey(key) { localStorage.setItem(Config.storageKeys.aiApiKey, key); } }; // ---- 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(`
📚
雨课堂助手
💳
付费题库
需要激活码
🆓
免费题库
需要验证码
🤖
AI答题
需要API Key
扫码获取验证码
扫码观看广告获取验证码
验证后免费使用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'), modeAI: doc.getElementById('mode-ai'), aiConfigSection: doc.getElementById('ai-config-section'), aiApiKeyInput: doc.getElementById('ai_api_key'), 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'), miniIcon: doc.getElementById('mini-icon'), miniText: doc.getElementById('mini-text') }; 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 = '80px'; iframe.style.height = '64px'; }; 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('雨课堂助手 v2.0.1\n作者:叶屿\n\n功能说明:\n- 自动播放视频/音频\n- 题库自动答题\n- AI智能答题\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); ui.modePaid.classList.remove('selected'); ui.modeFree.classList.remove('selected'); ui.modeAI.classList.remove('selected'); ui.activationCodeSection.style.display = 'none'; ui.verifyCodeSection.style.display = 'none'; ui.aiConfigSection.style.display = 'none'; if (mode === 'paid') { ui.modePaid.classList.add('selected'); ui.activationCodeSection.style.display = 'block'; updateCreditsDisplay(); } else if (mode === 'ai') { ui.modeAI.classList.add('selected'); ui.aiConfigSection.style.display = 'block'; ui.aiApiKeyInput.value = Store.getAIApiKey(); } else { ui.modeFree.classList.add('selected'); ui.verifyCodeSection.style.display = 'block'; } }; // 绑定答题模式切换事件 ui.modePaid.addEventListener('click', () => switchAnswerMode('paid')); ui.modeFree.addEventListener('click', () => switchAnswerMode('free')); ui.modeAI.addEventListener('click', () => switchAnswerMode('ai')); // 绑定微信号复制 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); // 保存AI API Key if (Store.getAnswerMode() === 'ai') { Store.setAIApiKey(ui.aiApiKeyInput.value.trim()); } // 检查激活状态并给出提示 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 (mode === 'ai') { if (!Store.getAIApiKey()) { log('⚠️ 题库配置已保存,但未填写AI API Key'); } else { log('✅ 题库配置已保存 (AI答题模式)'); } } 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') || path.includes('exercise') || path.includes('homework')) { ui.btnStart.innerText = '⏸️ 停止答题'; } else { ui.btnStart.innerText = '⏸️ 停止刷课'; } ui.btnStart.disabled = false; Store.setRunState('running'); startMiniStatusUpdate(panel); fn && fn(); }; }, resetStartButton(text = '开始刷课') { ui.btnStart.innerText = text; ui.btnStart.disabled = false; if (text.includes('刷完') || text.includes('完成')) { Store.setRunState('completed'); } else if (text.includes('停止') || text.includes('继续')) { Store.setRunState('stopped'); } isRunning = false; stopRequested = false; }, updateMiniStatus(icon, text) { if (ui.miniIcon) ui.miniIcon.innerText = icon; if (ui.miniText) ui.miniText.innerText = text; } }; } // ---- 播放器工具 ---- 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.volume = 0; media.playbackRate = Config.playbackRate; const p = media.play(); if (p !== undefined) { p.catch(e => { if (e.name !== 'AbortError') console.warn('[雨课堂助手] applyMediaDefault play失败:', e); setTimeout(() => media.play().catch(() => {}), 1500); }); } }, observePause(video) { if (!video) return () => { }; const target = document.getElementsByClassName('play-btn-tip')[0]; if (!target) return () => { }; let isPlayPending = false; // 安全播放:防止重叠的 play() 调用导致 AbortError const safePlay = (delay = 500) => { if (isPlayPending) return; isPlayPending = true; setTimeout(() => { if (video.paused) { const p = video.play(); if (p !== undefined) { p.then(() => { isPlayPending = false; }) .catch(e => { isPlayPending = false; if (e.name !== 'AbortError') { console.warn('[雨课堂助手] 自动播放失败:', e); } setTimeout(() => safePlay(1500), 1500); }); } else { isPlayPending = false; } } else { isPlayPending = false; } }, delay); }; safePlay(1500); const observer = new MutationObserver(list => { for (const mutation of list) { if (mutation.type === 'childList' && target.innerText === '播放') { safePlay(800); } } }); 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: () => { } } }); } // ---- 字体反混淆 ---- let glyphHashMap = null; // SHA-1哈希 -> 原始Unicode码点 let fontCharMap = null; // 混淆字符 -> 原始字符 let glyphHashMapPromise = null; // 映射表加载Promise let deobfuscationPromise = null; // 反混淆进行中的Promise function loadGlyphHashMap() { glyphHashMapPromise = new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: 'https://qsy.iano.cn/original_glyph_to_uni_v2.json', onload: (res) => { try { glyphHashMap = JSON.parse(res.responseText); console.log('[雨课堂助手] 字形映射表加载成功, 共', Object.keys(glyphHashMap).length, '个字形'); resolve(glyphHashMap); } catch (e) { console.error('[雨课堂助手] 解析字形映射表失败:', e); reject(e); } }, onerror: (err) => { console.error('[雨课堂助手] 加载字形映射表失败:', err); reject(err); } }); }); return glyphHashMapPromise; } async function sha1Hex(str) { const data = new TextEncoder().encode(str); const hashBuffer = await crypto.subtle.digest('SHA-1', data); return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join(''); } async function buildFontCharMap(fontUrl) { if (!glyphHashMap) { console.warn('[雨课堂助手] 字形映射表未加载,跳过反混淆'); return {}; } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: fontUrl, responseType: 'arraybuffer', onload: async (res) => { try { const font = opentype.parse(res.response); const charMap = {}; let matchCount = 0; for (let i = 0; i < font.glyphs.length; i++) { const glyph = font.glyphs.get(i); if (!glyph.unicode || !glyph.path || !glyph.path.commands || glyph.path.commands.length === 0) continue; const pathStr = glyph.path.toPathData(); if (!pathStr) continue; const hash = await sha1Hex(pathStr); const originalUnicode = glyphHashMap[hash]; if (originalUnicode !== undefined && originalUnicode !== glyph.unicode) { charMap[String.fromCodePoint(glyph.unicode)] = String.fromCodePoint(originalUnicode); matchCount++; } } console.log('[雨课堂助手] 字体反混淆映射建立完成:', matchCount, '个字符需要还原'); fontCharMap = charMap; resolve(charMap); } catch (e) { console.error('[雨课堂助手] 解析混淆字体失败:', e); reject(e); } }, onerror: (err) => { console.error('[雨课堂助手] 下载混淆字体失败:', err); reject(err); } }); }); } function deobfuscateText(text) { if (!fontCharMap || !text) return text; return Array.from(text).map(ch => fontCharMap[ch] || ch).join(''); } // ---- 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) { const fontUrl = result.data.font; if (fontUrl) { deobfuscationPromise = (glyphHashMapPromise || Promise.resolve()).then(() => { if (!glyphHashMap) return null; return buildFontCharMap(fontUrl); }).then(charMap => { interceptedProblems = parseYktProblems(result.data.problems, charMap); console.log('[雨课堂助手] 拦截到题目数据(已反混淆):', interceptedProblems.length, '题'); }).catch(err => { console.warn('[雨课堂助手] 字体反混淆失败,使用原始数据:', err); interceptedProblems = parseYktProblems(result.data.problems, null); }); } else { interceptedProblems = parseYktProblems(result.data.problems, null); console.log('[雨课堂助手] 拦截到题目数据:', interceptedProblems.length, '题'); } } } catch (e) { console.warn('[雨课堂助手] JSON Hook 解析异常:', e); } return result; }; } // 解析雨课堂题目数据(参考官方脚本的 parseYkt) function parseYktProblems(problems, charMap) { const deobfuscate = (text) => { if (!charMap || !text) return text; return Array.from(text).map(ch => charMap[ch] || ch).join(''); }; return problems.map(item => { const content = item.content || item; const typeText = content.TypeText || ''; const type = getQuestionType(typeText); const body = content.Body || ''; const question = formatString(deobfuscate(body)); let options = []; if (type <= 1 && content.Options) { options = content.Options .sort((a, b) => (a.key || '').charCodeAt(0) - (b.key || '').charCodeAt(0)) .map(opt => formatString(deobfuscate(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(deobfuscateText(questionEl.innerText || questionEl.textContent || '')); } if (!questionText) { questionText = formatString(deobfuscateText(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(deobfuscateText(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) { // 等待字体反混淆完成 if (deobfuscationPromise) { try { await deobfuscationPromise; } catch (e) { /* already handled */ } deobfuscationPromise = null; } // 方式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 === 'ai') { return this.askAI(question, options, type); } else if (mode === 'free') { if (!Store.isVerifyValid()) { panel.log('⚠️ 验证码未验证或已过期,请先在【题库配置】中验证'); throw '验证码未验证或已过期'; } return this.askFreeQuestionBank(question, options, type); } else { 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是索引数组(如[0,1])或直接答案数组 if (typeof answers[0] === 'number') { finalAnswers = answers.map(idx => { if (options[idx]) return options[idx]; return String.fromCharCode(65 + idx); }); } else { finalAnswers = answers; } } else if (Array.isArray(answers) && answers.length > 0) { // success=false: answers是二维数组,如[["对"],["正确"]] // 取第一组答案(最佳匹配) if (Array.isArray(answers[0])) { finalAnswers = answers[0]; } else { // 兜底:如果不是二维数组,直接使用 finalAnswers = answers; } } if (!finalAnswers.length) { panel.log('⚠️ 免费题库未找到匹配答案'); reject('未找到匹配答案'); return; } 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('🔍 正在查询付费题库...'); const params = new URLSearchParams(); params.append('device_id', deviceId); params.append('question', question); params.append('type', type); params.append('location', '雨课堂'); if (Array.isArray(options)) options.forEach((opt, i) => params.append('options[' + i + ']', opt)); GM_xmlhttpRequest({ method: 'POST', url: API_URL, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: params.toString(), 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); console.error('[雨课堂助手] 付费题库错误响应:', res.status, (res.responseText?.match(/([^<]*)<\/title>/)?.[1] || '') + ' | ' + (res.responseText?.match(/<h1>([^<]*)<\/h1>/)?.[1] || '') + ' | ' + (res.responseText?.replace(/<[^>]*>/g, ' ').substring(0, 1000) || '')); reject(err); } }, onerror: (e) => { console.error('[雨课堂助手] 付费题库网络错误:', e); reject('网络错误'); }, ontimeout: () => reject('请求超时') }); }); }, async askAI(question, options = [], type = 4) { const apiKey = Store.getAIApiKey(); if (!apiKey) { throw '未配置AI API Key,请在【题库配置】中填写'; } // 构建题目类型描述 const typeNames = { 0: '单选题', 1: '多选题', 2: '填空题', 3: '判断题', 4: '其他' }; const typeName = typeNames[type] || '其他'; // 构建选项文本 let optionsText = ''; if (options.length > 0) { optionsText = '\n选项:\n' + options.map((opt, i) => `${String.fromCharCode(65 + i)}. ${opt}`).join('\n'); } const prompt = `你是一个答题助手。请直接给出答案,不要解释。 题目类型:${typeName} 题目:${question}${optionsText} 回答要求: - 单选题:只回答一个选项的完整内容(如选项A的内容是"经济全球化",就回答"经济全球化") - 多选题:回答所有正确选项的完整内容,用"||"分隔(如"经济全球化||科技进步") - 判断题:只回答"正确"或"错误" - 填空题:直接给出填空内容 - 不要回答选项字母(A/B/C/D),要回答选项的具体内容`; return new Promise((resolve, reject) => { panel.log('🤖 正在使用AI答题...'); GM_xmlhttpRequest({ method: 'POST', url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: 'glm-4-flash', messages: [ { role: 'user', content: prompt } ], temperature: 0.1, max_tokens: 200 }), timeout: 30000, onload: res => { if (res.status === 200) { try { const json = JSON.parse(res.responseText); if (json.choices && json.choices[0] && json.choices[0].message) { let aiAnswer = json.choices[0].message.content.trim(); // 清理AI回答中可能的多余内容 aiAnswer = aiAnswer.replace(/^答案[::]\s*/i, '').replace(/^答[::]\s*/i, '').trim(); let answers = []; if (type === 1 && aiAnswer.includes('||')) { // 多选题:用||分隔 answers = aiAnswer.split('||').map(a => a.trim()).filter(a => a); } else { answers = [aiAnswer]; } panel.log(`🤖 AI答案:${answers.join(', ')}`); resolve({ answers, remaining: -1 }); } else { reject('AI返回格式异常'); } } catch (e) { reject('AI响应解析失败'); } } else if (res.status === 401) { reject('API Key无效,请检查配置'); } else if (res.status === 429) { reject('请求过于频繁,请稍后重试'); } else { reject(`AI请求失败: HTTP ${res.status}`); } }, onerror: () => reject('AI网络错误'), ontimeout: () => reject('AI请求超时') }); }); }, async autoSelectAndSubmit(answers, itemBodyElement, decodedOptions) { 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 findOptionsList = (root) => { if (!root) return null; // 优先精确选择器 const selectors = [ 'ul.list-inline.pt10', '.list-inline.list-unstyled-radio', '.list-unstyled.list-unstyled-radio', 'ul.list-unstyled', 'ul.list-inline', 'ul.list', 'ul' ]; for (const sel of selectors) { const candidates = root.querySelectorAll(sel); for (const ul of candidates) { // 验证:必须有至少2个li子元素,且不是导航栏(排除只含按钮的ul) const lis = ul.querySelectorAll('li'); if (lis.length >= 2) { const hasButton = ul.querySelector('button.el-button'); const isNav = ul.closest('.problem-fixedbar') || ul.closest('.aside-body'); if (!hasButton && !isNav) return ul; } } } return null; }; let listContainer = findOptionsList(itemBodyElement); if (!listContainer) { const parent = itemBodyElement.closest('.subject-item') || itemBodyElement.parentElement; listContainer = findOptionsList(parent); } if (!listContainer) { const mainArea = document.querySelector('.container-problem') || document.querySelector('.exam-main--body') || document.querySelector('.container-body'); listContainer = findOptionsList(mainArea); } const optionEls = listContainer ? listContainer.querySelectorAll('li') : []; let pageOptions = Array.from(optionEls).map(li => formatString(li.innerText || li.textContent || '').replace(/^[A-Ga-g][.、\s]\s*/, '').trim().toLowerCase() ); // 如果有解码后的选项数据,优先使用(因为DOM文本可能是字体混淆的) if (Array.isArray(decodedOptions) && decodedOptions.length > 0) { pageOptions = decodedOptions.map(opt => String(opt).replace(/^[A-Ga-g][.、\s]\s*/, '').trim().toLowerCase()); console.log('[雨课堂助手] 使用解码选项:', pageOptions); } else if (optionEls.length > 0) { console.log('[雨课堂助手] 使用DOM选项:', pageOptions); } else { console.log('[雨课堂助手] 未找到选项容器'); } 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; } // 1.5 纯数字答案(0-based索引,如 0=A, 1=B, 2=C) if (/^\d+$/.test(answerStr)) { const idx = parseInt(answerStr, 10); if (idx >= 0 && idx <= 9 && !targetIndices.includes(idx)) { targetIndices.push(idx); continue; } } // 1.8 带前缀的字母答案(如 "E. 机体作为..." → E,"A、没有疾病" → A) const letterPrefixMatch = answerStr.match(/^([A-Ga-g])\s*[.、.::))]/); if (letterPrefixMatch) { const ch = letterPrefixMatch[1].toUpperCase(); if (letterMap[ch] !== undefined && !targetIndices.includes(letterMap[ch])) { targetIndices.push(letterMap[ch]); continue; } } // 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 >= 2 && pageOptions.length > 0) { const containIdx = pageOptions.findIndex(opt => opt.includes(answerLower) || answerLower.includes(opt)); if (containIdx >= 0 && !targetIndices.includes(containIdx)) { targetIndices.push(containIdx); continue; } } // 5. 相似度匹配 if (answerLower.length >= 2 && pageOptions.length > 0) { const ratings = pageOptions.map(opt => stringSimilarity(answerLower, opt)); const maxScore = Math.max(...ratings); if (maxScore > 60) { 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; } // Check if course is already completed const listItem = list[this.outside]; const itemText = listItem?.innerText || ''; if (itemText.includes('100%') || itemText.includes('\u5df2\u5b8c\u6210')) { this.panel.log(`\u7b2c ${this.outside + 1} \u4e2a\u5df2\u5b8c\u6210\uff0c\u8df3\u8fc7`); 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} 已过截止,进度不再增加,将直接跳过`); let video = document.querySelector('video'); let waitCount = 0; while (!video && waitCount < 15) { await Utils.sleep(1000); video = document.querySelector('video'); waitCount++; } if (video) { video.volume = 0; await Utils.sleep(1500); await video.play().catch(() => {}); } Player.applySpeed(); Player.mute(); const stopObserve = Player.observePause(video); await Utils.poll(() => isDeadline || Utils.isProgressDone(progressNode?.innerHTML), { interval: 5000, timeout: await Utils.getDDL() }); stopObserve(); // If progress not complete, replay at 1x speed if (!isDeadline && !Utils.isProgressDone(progressNode?.innerHTML) && video) { this.panel.log('\u8fdb\u5ea6\u672a\u6ee1\uff0c\u4ee51x\u901f\u5ea6\u91cd\u64ad...'); video.playbackRate = 1; video.currentTime = 0; await Utils.sleep(1000); await video.play().catch(() => {}); const stopObserve2 = Player.observePause(video); await Utils.poll(() => Utils.isProgressDone(progressNode?.innerHTML), { interval: 5000, timeout: await Utils.getDDL() }); stopObserve2(); } 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); let audio = document.querySelector('audio'); let waitCount = 0; while (!audio && waitCount < 15) { await Utils.sleep(1000); audio = document.querySelector('audio'); waitCount++; } Player.applyMediaDefault(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); let video = document.querySelector('video'); let waitCount = 0; while (!video && waitCount < 15) { await Utils.sleep(1000); video = document.querySelector('video'); waitCount++; } if (video) { video.volume = 0; await Utils.sleep(1500); await video.play().catch(() => {}); } Player.applySpeed(); Player.mute(); const stopObserve = Player.observePause(video); const progressNode = document.querySelector('.progress-wrap')?.querySelector('.text'); await Utils.poll(() => Utils.isProgressDone(progressNode?.innerHTML), { interval: 3000, timeout: await Utils.getDDL() }); stopObserve(); // If progress not complete, replay at 1x speed if (!Utils.isProgressDone(progressNode?.innerHTML) && video) { this.panel.log('\u8fdb\u5ea6\u672a\u6ee1\uff0c\u4ee51x\u901f\u5ea6\u91cd\u64ad...'); video.playbackRate = 1; video.currentTime = 0; await Utils.sleep(1000); await video.play().catch(() => {}); const stopObserve2 = Player.observePause(video); await Utils.poll(() => Utils.isProgressDone(progressNode?.innerHTML), { interval: 3000, timeout: await Utils.getDDL() }); stopObserve2(); } 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; } // 检查答题模式的有效性 const answerMode = Store.getAnswerMode(); if (answerMode === 'ai' && !Store.getAIApiKey()) { this.panel.log('⚠️ 未配置AI API Key,跳过自动答题'); this.panel.log('💡 请在【题库配置】中填写API Key'); idx++; this.updateProgress(this.outside, idx); return idx; } if (answerMode === 'free' && !Store.isVerifyValid()) { this.panel.log('⚠️ 验证码未验证或已过期,跳过自动答题'); 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, result.options); } 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); // 确定题目总数:优先用拦截数据 const totalFromData = interceptedProblems ? interceptedProblems.length : 0; // 查找侧边栏题号按钮(用于分页导航) let navButtons = null; const navSelectors = [ '.aside-body .subject-item', '.aside-body li', '.exam-aside .subject-item', '.sheet-box .item', '.sheet-list .item', '.answer-sheet .item' ]; for (const sel of navSelectors) { const found = document.querySelectorAll(sel); if (found && found.length > 1) { navButtons = found; break; } } // 也尝试通过侧边栏数字按钮查找 if (!navButtons || navButtons.length <= 1) { const allBtns = document.querySelectorAll('button, div[data-order]'); const numBtns = Array.from(allBtns).filter(b => { const t = b.textContent.trim(); return /^\d+$/.test(t) && parseInt(t) >= 1 && parseInt(t) <= 50; }); if (numBtns.length >= 5) navButtons = numBtns; } // 检测是否为分页模式 const isPaginated = navButtons && navButtons.length > 1; let totalQuestions = isPaginated ? navButtons.length : 0; // 非分页模式:尝试一次性找到所有题目 let allItems = null; if (!isPaginated) { let retryCount = 0; while (retryCount < 5) { allItems = document.querySelectorAll('.exam-main--body .subject-item'); if (!allItems.length) allItems = document.querySelectorAll('.subject-item'); if (allItems.length > 0) break; retryCount++; this.panel.log(`未找到题目,等待页面加载... (${retryCount}/5)`); await Utils.sleep(3000); } totalQuestions = allItems ? allItems.length : 0; } if (totalQuestions === 0 && totalFromData > 0) { totalQuestions = totalFromData; } if (totalQuestions === 0) { this.panel.log('❌ 未找到题目,请确认页面已加载完成'); this.panel.resetStartButton('开始答题'); return; } this.panel.log(`找到 ${totalQuestions} 道题目${isPaginated ? '(分页模式)' : ''},开始答题`); for (let i = 0; i < totalQuestions; i++) { if (stopRequested) { this.panel.log('⏸️ 已停止答题'); this.panel.resetStartButton('继续答题'); return; } this.panel.log(`📝 正在处理第 ${i + 1}/${totalQuestions} 题...`); // 分页模式:点击侧边栏题号导航 if (isPaginated && navButtons[i]) { navButtons[i].click(); await Utils.sleep(1200); } // 查找当前显示的题目区域(在主内容区,不在侧边栏) let subjectItem, questionArea; if (isPaginated) { // 优先在主内容区查找,排除侧边栏 subjectItem = document.querySelector('.container-problem .item-body') || document.querySelector('.exam-main--body .item-body') || document.querySelector('.container-body .item-body'); // 如果找到item-body,用它的父元素作为subjectItem if (subjectItem) { questionArea = subjectItem; subjectItem = subjectItem.closest('.subject-item') || subjectItem.parentElement || subjectItem; } else { // 回退:查找包含选项列表的区域 const lists = document.querySelectorAll('ul.list-inline'); for (const ul of lists) { const parent = ul.closest('.subject-item') || ul.closest('.item-body') || ul.parentElement; if (parent && !parent.closest('.aside-body') && !parent.closest('.exam-aside')) { subjectItem = parent.closest('.subject-item') || parent; questionArea = parent.querySelector('.item-body') || parent; break; } } } } else { subjectItem = allItems[i]; questionArea = subjectItem ? (subjectItem.querySelector('.item-body') || subjectItem) : null; if (subjectItem) { subjectItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); await Utils.sleep(800); } } if (!questionArea) { this.panel.log(`第 ${i + 1} 题未找到题目区域,跳过`); continue; } // 检查是否已答题 const checkEl = subjectItem || questionArea; if (checkEl.querySelector('.is-checked')) { this.panel.log(`第 ${i + 1} 题已作答,跳过`); continue; } try { const result = await Solver.recognize(questionArea, i); if (result && result.question && result.question.length > 3) { // 查询题库(带重试) let answers; try { const bankResult = await Solver.askQuestionBank( result.question, result.options, result.type !== undefined ? result.type : 4 ); answers = bankResult.answers; } catch (firstErr) { this.panel.log('\u26a0\ufe0f 首次查询失败,2秒后重试...'); await Utils.sleep(2000); const retryResult = await Solver.askQuestionBank( result.question, result.options, result.type !== undefined ? result.type : 4 ); answers = retryResult.answers; } // 自动选择答案(传递解码选项以避免字体混淆问题) await Solver.autoSelectAndSubmit(answers, subjectItem || questionArea, result.options); await Utils.sleep(1500); } else { this.panel.log(`第 ${i + 1} 题识别失败,跳过`); } } catch (err) { this.panel.log(`第 ${i + 1} 题查询失败:${err}`); } 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('课程不属于视频,已跳过'); } Store.touchRunState(); 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); Store.touchRunState(); nextBtn.dispatchEvent(new Event('click')); } else { localStorage.removeItem(Config.storageKeys.proClassCount); Store.setRunState('completed'); this.panel.log('课程播放完毕 🎉'); this.panel.resetStartButton('刷完啦~'); break; } } } } // ---- 最小化面板状态更新 ---- let miniTimer = null; function startMiniStatusUpdate(p) { if (miniTimer) return; miniTimer = setInterval(() => { if (!isRunning) { p.updateMiniStatus('📚', '雨课堂助手'); clearInterval(miniTimer); miniTimer = null; return; } const video = document.querySelector('video'); if (video && !video.paused && video.duration) { const remaining = video.duration - video.currentTime; const min = Math.floor(remaining / 60).toString().padStart(2, '0'); const sec = Math.floor(remaining % 60).toString().padStart(2, '0'); p.updateMiniStatus('▶️', `${min}:${sec}`); } else { p.updateMiniStatus('⏳', '下一节...'); } }, 1000); } // ---- 自动恢复判断 ---- function shouldAutoResume(state, url, running, now) { if (!state) return false; if (state.status !== 'running') return false; if (now - state.lastActiveTime > 1800000) return 'expired'; if (!url.includes('pro/lms')) return false; if (running) return false; return true; } function waitForDOMReady(p, timeout = 15000) { const selectors = ['.btn-next', '.header-bar', '.leaf-detail']; return new Promise(resolve => { if (selectors.some(s => document.querySelector(s))) { resolve(); return; } p.log('等待页面加载中...'); const start = Date.now(); const timer = setInterval(() => { if (selectors.some(s => document.querySelector(s)) || Date.now() - start > timeout) { clearInterval(timer); resolve(); } }, 500); }); } function setupRouteListener(p) { let lastUrl = location.href; let debounceTimer = null; const onRouteChange = () => { const currentUrl = location.href; if (currentUrl === lastUrl) return; lastUrl = currentUrl; clearTimeout(debounceTimer); debounceTimer = setTimeout(async () => { const result = shouldAutoResume(Store.getRunState(), currentUrl, isRunning, Date.now()); if (result === true) { p.log('检测到路由变化,自动恢复刷课中...'); await waitForDOMReady(p); if (!isRunning) { isRunning = true; stopRequested = false; p.btnStart.innerText = '⏸️ 停止刷课'; p.log('自动恢复刷课中...'); startMiniStatusUpdate(p); start(); } } }, 1000); }; window.addEventListener('popstate', onRouteChange); const origPush = history.pushState; const origReplace = history.replaceState; history.pushState = function(...args) { origPush.apply(this, args); onRouteChange(); }; history.replaceState = function(...args) { origReplace.apply(this, args); onRouteChange(); }; } // ---- 路由 ---- async 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') || path.includes('exercise') || path.includes('homework')) { // 检查是否是结果页面 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 === 'ai') { if (!Store.getAIApiKey()) { panel.log('⚠️ 未配置AI API Key,请先在【题库配置】中填写'); panel.resetStartButton('开始答题'); return; } panel.log('✅ AI答题模式,启动自动答题'); } else 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')) { // 视频播放页:.btn-next 可能异步加载,需要等待 if (location.pathname.includes('/video/')) { const found = await Utils.poll(() => document.querySelector('.btn-next'), { interval: 500, timeout: 10000 }); if (found) { new ProNewRunner(panel).run(); } else { panel.log('⚠️ 未找到播放控件,尝试刷新页面'); location.reload(); } } else 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(); loadGlyphHashMap(); // 预加载字形映射表 // 等待 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); // 自动恢复检测 const runState = Store.getRunState(); const resumeResult = shouldAutoResume(runState, location.href, isRunning, Date.now()); if (resumeResult === 'expired') { Store.clearRunState(); panel.log('⏰ 刷课状态已过期,请重新开始'); } else if (resumeResult === true) { panel.log('🔄 检测到未完成的刷课任务...'); await waitForDOMReady(panel); panel.log('🔄 自动恢复刷课中...'); isRunning = true; stopRequested = false; panel.btnStart.innerText = '⏸️ 停止刷课'; startMiniStatusUpdate(panel); start(); } // 启动 SPA 路由监听 setupRouteListener(panel); })(); })();