// ==UserScript== // @name 智慧树刷课助手 // @namespace https://github.com/CXRunfree/Autovisor // @version 1.3.1 // @description 智慧树网课自动播放、自动切换下一集、跳过弹窗和弹题、自动静音、倍速播放、检测暂停续播、刷习惯分、支持智慧共享课、翻转课和融合共享课、考试自动答题 // @author 叶屿 // @license GPL3 // @antifeature payment 题库答题功能需要激活码(付费),视频播放等基础功能完全免费 // @match *://passport.zhihuishu.com/* // @match *://studyh5.zhihuishu.com/* // @match *://studyvideoh5.zhihuishu.com/* // @match *://fusioncourseh5.zhihuishu.com/* // @match *://hike.zhihuishu.com/* // @match *://www.zhihuishu.com/* // @match *://studyservice-api.zhihuishu.com/* // @match *://examh5.zhihuishu.com/* // @match *://onlineexamh5new.zhihuishu.com/* // @match *://studentexambaseh5.zhihuishu.com/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @connect qsy.iano.cn // @connect lyck6.cn // @connect * // @run-at document-idle // ==/UserScript== (function () { 'use strict'; /* * ========================================== * 免责声明 / Disclaimer * ========================================== * * 本脚本仅供个人学习和技术研究使用,请勿用于任何违反 * 学校规章制度、考试纪律或法律法规的行为。 * * 使用本脚本即表示您同意以下条款: * 1. 本脚本不鼓励、不支持任何形式的考试作弊行为 * 2. 使用者应自行承担使用本脚本产生的一切后果 * 3. 作者不对因使用本脚本而导致的任何直接或间接 * 损失(包括但不限于学业处分、账号封禁等)承担责任 * 4. 本脚本不保证题库答案的准确性和完整性 * 5. 请遵守《中华人民共和国教育法》及相关法律法规 * 6. 请遵守所在学校的学术诚信规范 * * 如您不同意以上条款,请立即停止使用并删除本脚本。 * * This script is for personal learning and technical * research purposes only. The author assumes no * responsibility for any consequences. * ========================================== */ // ===================== 配置区 ===================== const DEFAULT_CONFIG = { playbackSpeed: 1.5, // 播放倍速 (0.5 ~ 1.8) soundOff: true, // 是否静音 autoNext: true, // 自动切换下一集 autoAnswer: true, // 自动跳过弹题 autoPlay: true, // 自动播放 & 暂停检测 habitScore: true, // 刷习惯分(模拟鼠标移动) autoExam: false, // 考试自动答题(默认关闭,手动点击搜题) examAutoFill: false, // 考试自动填入答案(默认关闭,需手动确认) showPanel: true, // 显示悬浮面板 }; // 从存储读取或使用默认配置 function loadConfig() { const saved = GM_getValue('autovisor_config', null); if (saved) { return Object.assign({}, DEFAULT_CONFIG, saved); } return Object.assign({}, DEFAULT_CONFIG); } function saveConfig(cfg) { GM_setValue('autovisor_config', cfg); } let config = loadConfig(); // ===================== 日志系统 ===================== const LOG_PREFIX = '[智慧树助手]'; const log = { info: (msg) => console.log(`%c${LOG_PREFIX} ${msg}`, 'color:#409EFF;font-weight:bold;'), warn: (msg) => console.warn(`%c${LOG_PREFIX} ${msg}`, 'color:#E6A23C;font-weight:bold;'), error: (msg) => console.error(`%c${LOG_PREFIX} ${msg}`, 'color:#F56C6C;font-weight:bold;'), success: (msg) => console.log(`%c${LOG_PREFIX} ${msg}`, 'color:#67C23A;font-weight:bold;'), }; // ===================== 工具函数 ===================== function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function $(selector) { return document.querySelector(selector); } function $$(selector) { return document.querySelectorAll(selector); } function safeClick(selector) { const el = typeof selector === 'string' ? $(selector) : selector; if (el) { el.click(); return true; } return false; } // 目录项有时是带有默认 href 的链接。直接调用 .click() 会在 // Vue/React 事件未接管时触发默认导航,智慧树会被带回课程首页。 // 仅拦截明确指向站点根页面的默认导航,保留目录组件自己的点击事件。 function isRootNavigationHref(href) { if (!href) return false; try { const target = new URL(href, location.href); if (target.origin !== location.origin) return false; const path = (target.pathname || '/').replace(/\/+$/, '') || '/'; return !target.hash && (path === '/' || path === '/index.html' || path === '/home'); } catch (e) { return false; } } function triggerLessonClick(item) { if (!item || !item.isConnected) return false; const anchor = item.matches?.('a[href]') ? item : item.querySelector?.('a[href]'); const href = anchor?.getAttribute('href') || anchor?.href || ''; if (anchor && isRootNavigationHref(href)) { // dispatchEvent 可让框架监听器正常收到点击;捕获阶段只取消 // 根首页的浏览器默认动作,避免 fallback href 改写当前课程状态。 const guard = (event) => event.preventDefault(); anchor.addEventListener('click', guard, true); try { anchor.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window, })); return true; } finally { anchor.removeEventListener('click', guard, true); } } try { item.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window, })); return true; } catch (e) { try { item.click(); return true; } catch (_) { return false; } } } function safeRemove(selector) { const el = $(selector); if (el) { el.remove(); return true; } return false; } // 判断页面类型 function isFusionClass() { return location.href.includes('fusioncourseh5.zhihuishu.com') || location.href.includes('fusioncourseh5') || !!document.querySelector('.chapter-item'); } function isNewVersion() { return isFusionClass(); } function isHikeClass() { return location.href.includes('hike.zhihuishu.com'); } function isStudyPage() { return location.href.includes('studyh5.zhihuishu.com') || location.href.includes('studyvideoh5.zhihuishu.com') || location.href.includes('fusioncourseh5.zhihuishu.com') || location.href.includes('hike.zhihuishu.com'); } function isExamPage() { const url = location.href; // 只匹配实际做题页面,不匹配作业/考试列表页 return url.includes('examh5.zhihuishu.com') || url.includes('onlineexamh5new.zhihuishu.com') || url.includes('studentexambaseh5.zhihuishu.com') || url.includes('/stuExamWeb.html') || url.includes('/webExamList/dohomework/') || url.includes('/webExamList/doexamination/') || // 通用: 页面内存在题目容器(延迟检测) (url.includes('zhihuishu.com') && !!document.querySelector('.examPaper_subject, .questionBox, .ques-detail')); } // ===================== 反检测模块 ===================== let antiCheatInitialized = false; // 温和策略:只拦截弹窗DOM,不修改原型链,不拦截XHR function hookAntiCheat() { if (antiCheatInitialized) return; const KEYWORDS = ['检测到异常脚本', '关闭插件', '异常行为']; // 1. MutationObserver 监听弹窗插入 const target = document.body || document.documentElement; if (!target) return; const observer = new MutationObserver((mutations) => { for (const m of mutations) { for (const node of m.addedNodes) { if (node.nodeType !== 1) continue; const text = (node.textContent || '').slice(0, 200); if (KEYWORDS.some(kw => text.includes(kw))) { node.style.display = 'none'; node.style.visibility = 'hidden'; node.style.opacity = '0'; node.style.pointerEvents = 'none'; try { node.remove(); } catch {} log.info('[反检测] 已拦截异常脚本弹窗'); } } } }); observer.observe(target, { childList: true, subtree: true }); // 2. 定时扫描关闭已有弹窗(只针对特定关键词的弹窗) setInterval(() => { document.querySelectorAll('.el-dialog, .el-message-box, [class*="modal"]').forEach(el => { const t = (el.textContent || '').slice(0, 300); if (KEYWORDS.some(kw => t.includes(kw))) { el.style.display = 'none'; try { el.remove(); } catch {} // 同时移除对应遮罩 const overlay = el.previousElementSibling; if (overlay && (overlay.classList.contains('el-overlay') || overlay.classList.contains('v-modal'))) { overlay.style.display = 'none'; try { overlay.remove(); } catch {} } log.info('[反检测] 已清除异常检测弹窗'); } }); }, 3000); antiCheatInitialized = true; log.info('[反检测] 反检测模块已激活(温和模式)'); } // ===================== 核心功能模块 ===================== // --- 获取视频元素 --- function getVideo() { return $('video'); } const STALL_TIMEOUT_MS = 120000; const SWITCH_COOLDOWN_MS = 8000; let switchInProgress = false; let nextSwitchAllowedAt = 0; let verifyBlocking = false; let progressHealth = { lessonKey: '', percent: -1, currentTime: -1, lastChangedAt: Date.now(), recoveryCount: 0, }; let completionSince = 0; function isElementVisible(element) { if (!element) return false; const style = getComputedStyle(element); const rect = element.getBoundingClientRect(); return style.display !== 'none' && style.visibility !== 'hidden' && Number.parseFloat(style.opacity || '1') > 0.05 && rect.width > 0 && rect.height > 0; } function isVerificationVisible() { return isElementVisible($('.yidun_modal__title')); } function parseProgress(raw) { const text = String(raw ?? '').replace(/\s+/g, ''); const match = text.match(/(\d+(?:\.\d+)?)%?/); if (!match) return 0; const value = Number(match[1]); if (!Number.isFinite(value)) return 0; return Math.max(0, Math.min(100, value)); } function formatProgress(raw) { const value = parseProgress(raw); return `${Number.isInteger(value) ? value : value.toFixed(1)}%`; } function getLessonItems() { if (isFusionClass()) return Array.from($$('.chapter-item')); if (isHikeClass()) return Array.from($$('.file-item')); const wisdomItems = Array.from($$('.child-info.hasvideo')); return wisdomItems.length ? wisdomItems : Array.from($$('.clearfix.video')); } function getActiveLesson() { if (isFusionClass()) { const activeChild = $('.chapter-content-second.current'); return activeChild ? activeChild.closest('.chapter-item') : null; } if (isHikeClass()) return $('.file-item.active'); if ($('.child-info.hasvideo')) return $('.child-info.hasvideo.current'); return $('.clearfix.video.current_play'); } function getLessonKey(item = getActiveLesson()) { if (!item) return ''; const explicit = item.getAttribute('data-id') || item.getAttribute('data-lesson-id'); if (explicit) return explicit; const title = item.querySelector('[title]')?.getAttribute('title'); return (title || item.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 120); } function isLessonComplete(item) { if (!item) return false; if (item.querySelector('.finish-icon, .icon-finish, .time_icofinish, .child-check')) { return true; } const progress = item.querySelector( '.el-progress[aria-valuenow], .progress-num, .rate, [role="progressbar"][aria-valuenow]' ); if (!progress) return false; const value = progress.getAttribute('aria-valuenow') ?? progress.textContent; return parseProgress(value) >= 100; } function resetProgressHealth() { progressHealth = { lessonKey: getLessonKey(), percent: parseProgress(getCourseProgress()), currentTime: Number(getVideo()?.currentTime) || 0, lastChangedAt: Date.now(), recoveryCount: 0, }; } function monitorProgressHealth(progress) { if (verifyBlocking || isVerificationVisible()) return; const video = getVideo(); if (!video || video.paused || video.ended || video.readyState < 2) { progressHealth.lastChangedAt = Date.now(); return; } const lessonKey = getLessonKey(); const currentTime = Number(video.currentTime); const moved = Number.isFinite(currentTime) && Number.isFinite(progressHealth.currentTime) && currentTime > progressHealth.currentTime + 0.25; if (lessonKey !== progressHealth.lessonKey || progress !== progressHealth.percent || moved) { progressHealth.lessonKey = lessonKey; progressHealth.percent = progress; progressHealth.currentTime = Number.isFinite(currentTime) ? currentTime : progressHealth.currentTime; progressHealth.lastChangedAt = Date.now(); progressHealth.recoveryCount = 0; return; } if (Date.now() - progressHealth.lastChangedAt < STALL_TIMEOUT_MS) return; if (progressHealth.recoveryCount === 0) { progressHealth.recoveryCount = 1; progressHealth.lastChangedAt = Date.now(); log.warn('视频和平台进度连续 120 秒未推进,正在尝试恢复播放'); updatePanelStatus('⚠️ 进度停滞,正在恢复...'); video.play().catch(() => {}); return; } log.error('视频和平台进度连续 120 秒未推进,已停止自动任务'); updatePanelStatus('⏹ 进度停滞,请手动检查页面'); stopAllTasks(); } // --- 页面优化:关闭弹窗、移除多余元素 --- async function optimizePage() { await sleep(2000); // 关闭学习时长弹窗 const initialClose = Array.from($$('.el-dialog .iconfont.iconguanbi, .el-message-box .iconfont.iconguanbi')) .find(isElementVisible); if (initialClose) initialClose.click(); if (!isNewVersion() && !isHikeClass()) { // 自动夜间模式 (18:00 ~ 7:00) const hour = new Date().getHours(); if (hour >= 18 || hour < 7) { const darkBtn = $('.Patternbtn-div'); if (darkBtn) darkBtn.click(); } // 移除无关元素 safeRemove('.exploreTip'); safeRemove('.ai-helper-Index2'); safeRemove('.aiMsg.once'); } log.info('页面优化完成'); } // --- 保存原始 pause,交给播放保活任务决定是否恢复 --- function overridePause() { const video = getVideo(); if (video && !video._pauseOverridden) { video._autovisorOriginalPause = video.pause.bind(video); video._pauseOverridden = true; log.info('已保存视频 pause 方法,由播放保活任务负责恢复'); } } function pauseForVerification() { const video = getVideo(); if (video && !video.paused && typeof video._autovisorOriginalPause === 'function') { video._autovisorOriginalPause(); } } // --- 静音控制 --- function setMute() { if (!config.soundOff) return; const video = getVideo(); if (video && video.volume !== 0) { video.volume = 0; const volumeBox = $('.volumeBox'); if (volumeBox) volumeBox.classList.add('volumeNone'); log.info('已静音'); } } // --- 倍速控制 --- function setSpeed() { const video = getVideo(); if (!video) return; const speed = Math.max(0.5, Math.min(config.playbackSpeed, 1.8)); if (video.playbackRate !== speed) { video.playbackRate = speed; const speedLabel = $('.speedBox span'); if (speedLabel) speedLabel.innerText = `X ${speed}`; log.info(`已设置倍速: ${speed}x`); } } // --- 检测暂停并续播 --- async function checkAndPlay() { if (!config.autoPlay) return; if (isVerificationVisible()) { verifyBlocking = true; pauseForVerification(); return; } verifyBlocking = false; const video = getVideo(); if (video && video.paused) { try { await video.play(); log.info('检测到视频暂停,已恢复播放'); } catch (e) { // 自动播放可能被浏览器策略阻止 log.warn('自动播放被阻止,请手动点击播放'); } } } // --- 跳过弹题 --- async function handleFusionExercise() { if (!isFusionClass()) return false; const dialog = $('.ai-class-exercise-dialog'); if (!isElementVisible(dialog)) return false; try { const option = dialog.querySelector('.option'); if (option) option.click(); await sleep(600); const submit = dialog.querySelector('.el-dialog__footer button.el-button'); if (submit) submit.click(); await sleep(1000); const video = getVideo(); if (video && video.paused && !isVerificationVisible()) { await video.play().catch(() => {}); } log.info('融合课 AI 随堂练习已尝试提交'); return Boolean(option || submit); } catch (e) { log.warn('融合课 AI 随堂练习处理失败: ' + e.message); return false; } } async function skipQuestions() { if (!config.autoAnswer) return; if (isHikeClass()) return; // 翻转课不支持自动答题 if (isVerificationVisible()) { verifyBlocking = true; pauseForVerification(); return; } if (isFusionClass()) { await handleFusionExercise(); return; } try { const quesContainer = $('.el-scrollbar__view'); if (!quesContainer) return; const questions = quesContainer.querySelectorAll('.number'); if (questions.length === 0) return; log.info(`检测到 ${questions.length} 道弹题`); for (const ques of questions) { ques.click(); await sleep(300); // 如果没有已选答案,随机选前两个 if (!$('.answer')) { const choices = $$('.topic-item'); for (let i = 0; i < Math.min(2, choices.length); i++) { choices[i].click(); await sleep(150); } } } // 按 ESC 关闭弹题对话框 await sleep(200); const dialog = quesContainer.closest('.el-dialog, .el-message-box'); if (dialog && dialog.querySelector('.number')) { dialog.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, keyCode: 27 })); log.success('弹题已跳过'); } } catch (e) { log.warn('跳过弹题时出错: ' + e.message); } } const blankDialogSince = new WeakMap(); function dialogTextWithoutControls(dialog) { if (!dialog) return ''; const clone = dialog.cloneNode(true); clone.querySelectorAll('button, .el-dialog__footer, .el-message-box__btns, .el-dialog__headerbtn, [aria-label="关闭"], [aria-label="Close"]').forEach(node => node.remove()); return (clone.textContent || '').replace(/\s+/g, ' ').trim(); } function removeDialogWithOverlay(dialog) { if (!dialog) return false; const overlays = new Set(); const parent = dialog.parentElement; if (parent) { parent.querySelectorAll('.v-modal, .el-overlay, .el-dialog__wrapper').forEach(node => { if (node !== dialog && !node.contains(dialog) && !dialog.contains(node)) overlays.add(node); }); } const previous = dialog.previousElementSibling; if (previous && previous.matches('.v-modal, .el-overlay')) overlays.add(previous); try { dialog.remove(); } catch (_) { dialog.style.display = 'none'; } overlays.forEach(node => { try { node.remove(); } catch (_) { node.style.display = 'none'; } }); return true; } // 平台偶发插入只有“确认”按钮、正文尚未加载的空白弹窗。不要按确认 // 或 ESC,让平台的兜底路由把课程送回首页;等待短暂的异步渲染后直接 // 移除该空壳及遮罩即可。 function clearBlankDialogs() { let removed = false; document.querySelectorAll('.el-dialog, .el-message-box').forEach(dialog => { if (!isElementVisible(dialog)) return; const text = dialogTextWithoutControls(dialog); const hasContentNode = dialog.querySelector('.el-dialog__body img, .el-dialog__body video, .el-dialog__body canvas, .el-dialog__body iframe, .el-message-box__message img, input, textarea'); if (text || hasContentNode) { blankDialogSince.delete(dialog); return; } const firstSeen = blankDialogSince.get(dialog) || Date.now(); blankDialogSince.set(dialog, firstSeen); if (Date.now() - firstSeen >= 1200) { removed = removeDialogWithOverlay(dialog) || removed; } }); return removed; } // --- 关闭其他弹窗 --- function closePopups() { if (isVerificationVisible()) return; if (isFusionClass() && isElementVisible($('.ai-class-exercise-dialog'))) return; clearBlankDialogs(); // 不再对所有 .el-dialog 发送 ESC。课程切换/播放器状态弹窗也使用 // 该类名,ESC 会被平台解释成返回首页。仅处理明确的消息框关闭按钮。 document.querySelectorAll('.el-message-box').forEach(dialog => { if (!isElementVisible(dialog)) return; const text = dialogTextWithoutControls(dialog); if (!text) return; if (/异常脚本|登录|网络错误|提示|通知|播放失败|操作失败|完成/.test(text)) { const close = dialog.querySelector('.el-message-box__headerbtn, .el-message-box__btns .el-button--primary'); if (close) close.click(); } }); const closeIcon = Array.from($$('.el-dialog .iconfont.iconguanbi, .el-message-box .iconfont.iconguanbi')) .find(isElementVisible); if (closeIcon) closeIcon.click(); } // --- 检测人机验证 --- function checkVerify() { const visible = isVerificationVisible(); if (visible) { verifyBlocking = true; pauseForVerification(); log.warn('⚠️ 检测到安全验证,请手动完成验证!'); updatePanelStatus('⚠️ 需要手动验证!'); return true; } if (verifyBlocking) { verifyBlocking = false; resetProgressHealth(); updatePanelStatus('▶️ 验证完成,恢复运行'); } return false; } // --- 习惯分:模拟鼠标在视频区域移动 --- function simulateMouseMove() { if (!config.habitScore) return; const videoArea = $('.videoArea') || getVideo(); if (!videoArea) return; const rect = videoArea.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return; const x = rect.left + rect.width / 2 + (Math.random() - 0.5) * 20; const y = rect.top + rect.height / 2 + (Math.random() - 0.5) * 20; videoArea.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: x, clientY: y, })); } // --- 获取当前学习进度 --- function getCourseProgress() { const active = getActiveLesson(); if (!active) return '0%'; if (isLessonComplete(active)) return '100%'; const progress = active.querySelector( '.el-progress[aria-valuenow], .progress-num, .rate, [role="progressbar"][aria-valuenow]' ); if (!progress) return '0%'; return formatProgress(progress.getAttribute('aria-valuenow') ?? progress.textContent); } // --- 获取未完成的课程列表 --- function getUnfinishedLessons() { return getLessonItems().filter(item => !isLessonComplete(item)); } // --- 切换到下一集 --- async function switchToNext() { if (!config.autoNext || verifyBlocking || isVerificationVisible()) return; if (switchInProgress || Date.now() < nextSwitchAllowedAt) return; if (parseProgress(getCourseProgress()) < 100) return; const allItems = getLessonItems(); if (allItems.length === 0) { log.warn('未识别到课程目录,暂不判断为全部完成'); updatePanelStatus('⚠️ 未识别到课程目录'); return; } const active = getActiveLesson(); if (!active) { log.warn('未识别到当前活动课时,暂不切换'); updatePanelStatus('⚠️ 未识别到当前课时'); return; } const currentIndex = Math.max(0, allItems.indexOf(active)); const ordered = [ ...allItems.slice(currentIndex + 1), ...allItems.slice(0, currentIndex + 1), ]; const next = ordered.find(item => item !== active && item.isConnected && !isLessonComplete(item) && isElementVisible(item)); if (!next) { log.success('🎉 所有课程已学习完毕!'); updatePanelStatus('🎉 全部完成!'); stopAllTasks(); return; } switchInProgress = true; nextSwitchAllowedAt = Date.now() + SWITCH_COOLDOWN_MS; const oldKey = getLessonKey(active); try { log.success('当前课程已完成,正在切换下一集...'); updatePanelStatus('🔄 正在切换下一集...'); const beforeUrl = location.href; if (!triggerLessonClick(next)) { throw new Error('目录项不可点击'); } log.info('已发送下一集切换请求'); const deadline = Date.now() + 8000; let changed = false; while (Date.now() < deadline) { // 若页面仍停留在原课程但链接已触发根路由,立即停止等待, // 避免继续操作首页上的同名元素。 if (beforeUrl !== location.href && isRootNavigationHref(location.href)) { log.warn('检测到切换请求将返回站点首页,已阻止继续执行'); try { history.back(); } catch (_) {} break; } const current = getActiveLesson(); if (current && (current !== active || getLessonKey(current) !== oldKey)) { changed = true; break; } await sleep(250); } if (changed && !isRootNavigationHref(location.href)) { afterLessonSwitch(); } else { log.warn('切换下一集后未确认活动课时变化'); updatePanelStatus('⚠️ 课时切换未确认'); } } catch (e) { log.warn('切换下一集失败: ' + e.message); updatePanelStatus('⚠️ 课时切换失败'); } finally { switchInProgress = false; } } // 切换课程后重新初始化 function afterLessonSwitch() { overridePause(); setMute(); setSpeed(); resetProgressHealth(); completionSince = 0; const title = getLessonTitle(); log.info(`正在学习: ${title}`); updatePanelStatus(`▶️ ${title}`); } // 获取当前课程名称 function getLessonTitle() { if (isFusionClass()) { const item = getActiveLesson(); const name = item?.querySelector('.item-name'); if (name) return name.getAttribute('title') || name.textContent.trim() || '未知课程'; } if (isHikeClass()) { const span = $('.file-item.active span[title]'); return span ? span.getAttribute('title') : '未知课程'; } else { const el = $('#lessonOrder'); return el ? el.getAttribute('title') : '未知课程'; } } // ===================== 定时任务管理 ===================== let timers = []; function addTimer(fn, interval) { let running = false; const tick = async () => { if (running) return; running = true; try { await fn(); } catch (e) { log.warn('定时任务执行失败: ' + e.message); } finally { running = false; } }; const id = setInterval(tick, interval); timers.push(id); return id; } function stopAllTasks() { timers.forEach(id => clearInterval(id)); timers = []; log.info('所有任务已停止'); } // ===================== 悬浮控制面板 ===================== let panelStatusEl = null; let panelProgressEl = null; function createPanel() { if (!config.showPanel) return; if ($('#autovisor-panel')) return; const panel = document.createElement('div'); panel.id = 'autovisor-panel'; panel.innerHTML = `
⚡ 智慧树助手 v1.2
▼
播放控制
⏩ 倍速
${config.playbackSpeed}x
🔇 静音
自动化
▶️ 自动播放
⏭️ 自动下一集
💬 跳过弹题
🏆 习惯分
运行中... --
`; document.body.appendChild(panel); panelStatusEl = $('#av-status'); panelProgressEl = $('#av-progress'); // 收起/展开 $('#av-collapse').addEventListener('click', () => { panel.classList.toggle('collapsed'); $('#av-collapse').textContent = panel.classList.contains('collapsed') ? '▶' : '▼'; }); // 倍速控制 $('#av-speed-down').addEventListener('click', () => { config.playbackSpeed = Math.max(0.5, +(config.playbackSpeed - 0.25).toFixed(2)); saveConfig(config); setSpeed(); $('#av-speed-val').textContent = config.playbackSpeed + 'x'; }); $('#av-speed-up').addEventListener('click', () => { config.playbackSpeed = Math.min(1.8, +(config.playbackSpeed + 0.25).toFixed(2)); saveConfig(config); setSpeed(); $('#av-speed-val').textContent = config.playbackSpeed + 'x'; }); // 静音切换 $('#av-mute-btn').addEventListener('click', () => { config.soundOff = !config.soundOff; saveConfig(config); const video = getVideo(); if (video) { video.volume = config.soundOff ? 0 : 0.5; const volumeBox = $('.volumeBox'); if (volumeBox) { if (config.soundOff) volumeBox.classList.add('volumeNone'); else volumeBox.classList.remove('volumeNone'); } } const muteBtn = $('#av-mute-btn'); muteBtn.textContent = config.soundOff ? '已静音' : '有声音'; muteBtn.className = 'av-btn ' + (config.soundOff ? 'av-on' : 'av-off'); }); // 开关切换 function toggleBtn(key, btnId) { $(btnId).addEventListener('click', () => { config[key] = !config[key]; saveConfig(config); const btn = $(btnId); btn.textContent = config[key] ? '已开启' : '已关闭'; btn.className = 'av-btn ' + (config[key] ? 'av-on' : 'av-off'); }); } toggleBtn('autoPlay', '#av-autoplay-btn'); toggleBtn('autoNext', '#av-autonext-btn'); toggleBtn('autoAnswer', '#av-autoanswer-btn'); toggleBtn('habitScore', '#av-habit-btn'); // 拖拽 makeDraggable(panel, $('#av-drag-handle')); } function makeDraggable(el, handle) { let offsetX = 0, offsetY = 0, isDragging = false; handle.addEventListener('mousedown', (e) => { isDragging = true; offsetX = e.clientX - el.getBoundingClientRect().left; offsetY = e.clientY - el.getBoundingClientRect().top; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; el.style.left = (e.clientX - offsetX) + 'px'; el.style.top = (e.clientY - offsetY) + 'px'; el.style.right = 'auto'; }); document.addEventListener('mouseup', () => { isDragging = false; }); } function updatePanelStatus(text) { if (panelStatusEl) panelStatusEl.textContent = text; } function updatePanelProgress(text) { if (panelProgressEl) panelProgressEl.textContent = text; } // ===================== 考试答题模块 ===================== // 对接 qsy.iano.cn 中转API + lyck6.cn 免费题库 // 借鉴万能脚本的选择器与答案匹配逻辑 // --- 设备ID --- function getDeviceId() { let id = GM_getValue('av_device_id', ''); if (!id) { id = 'av_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); GM_setValue('av_device_id', id); } return id; } // --- 文本格式化 (借鉴万能脚本 formatString) --- function formatString(src) { src = String(src); // HTML 实体解码 if (!src.includes('img') && !src.includes('iframe')) { const tmp = document.createElement('div'); tmp.innerHTML = src; src = tmp.textContent || tmp.innerText || ''; } // 全角转半角 src = src.replace(/[\uff01-\uff5e]/g, s => String.fromCharCode(s.charCodeAt(0) - 65248)); return src.replace(/\s+/g, ' ').replace(/[""]/g, '"').replace(/['']/g, "'") .replace(/。/g, '.').replace(/[,.?:!;]$/, '').trim(); } // 判断题匹配 (借鉴万能脚本) function isTrue(str) { return Boolean(String(str).match(/(^|,)(正确|是|对|√|T|ri|true|A)(,|$)/)); } function isFalse(str) { return Boolean(String(str).match(/(^|,)(错误|否|错|×|F|不是|wr|false|B)(,|$)/)); } function isPlainAnswer(answer) { if (!answer || answer.length > 8 || !/[A-Z]/.test(answer)) return false; let min = 0; for (let i = 0; i < answer.length; i++) { if (answer.charCodeAt(i) < min) return false; min = answer.charCodeAt(i); } return true; } // 答案文本相似度 (简化版) function answerSimilar(answer, options) { return options.map(opt => { const o = opt.replace(/\s/g, ''); const a = answer.replace(/\s/g, ''); if (o === a) return 100; if (o.includes(a) || a.includes(o)) return 85; let match = 0; const shorter = a.length < o.length ? a : o; const longer = a.length < o.length ? o : a; for (let i = 0; i < shorter.length; i++) { if (longer.includes(shorter[i])) match++; } return Math.round(match / longer.length * 100); }); } // --- 题型检测 (万能脚本 getQuestionType) --- function getQuestionType(typeText) { const t = String(typeText).trim(); if (/单选|单项选择|radio/i.test(t)) return 0; if (/多选|多项选择|checkbox|不定项/i.test(t)) return 1; if (/填空/i.test(t)) return 2; if (/判断|是非|对错/i.test(t)) return 3; if (/简答|问答|主观/i.test(t)) return 4; if (/名词解释/i.test(t)) return 5; if (/论述/i.test(t)) return 6; if (/计算/i.test(t)) return 7; if (/阅读理解|完型填空|听力/i.test(t)) return 8; return 4; // 默认 } const TYPE_NAMES = { 0: '单选', 1: '多选', 2: '填空', 3: '判断', 4: '简答', 5: '名词解释', 6: '论述', 7: '计算', 8: '其他' }; // --- 题目提取 (借鉴万能脚本的智慧树选择器) --- function extractExamQuestions() { const questions = []; // 按优先级尝试不同页面版本的选择器 const layouts = [ { // 智慧树作业/考试 (examPaper) root: '.examPaper_subject', question: '.subject_describe div, .smallStem_describe p', options: '.subject_node .nodeLab .node_detail', $options: '.subject_node .nodeLab .node_detail', type: '.subject_type span:first-child', checked: '.onChecked', }, { // 智慧树学分课作业 root: '.questionBox', question: '.questionContent', options: '.optionUl label .el-radio__label, .optionUl label .el-checkbox__label', $options: '.optionUl label', type: '.questionTit', checked: '.is-checked', }, { // 智慧树学分课考试 root: '.ques-detail', question: '.questionName .centent-pre', options: '.radio-view li .preStyle, .checkbox-views label .preStyle', $options: '.radio-view li, .checkbox-views label', type: '.letterSortNum', checked: '.is-checked', }, { // 通用回退 root: 'div[class*="subject"], div[class*="question"], div[class*="exam"]', question: '.subject_describe, .question-title, .ques-title, .topic-title, .stem, h3', options: '.subject_node, .option-item, .topic-item, label', $options: '.subject_node, .option-item, .topic-item, label', type: '.subject_type, .questionTit, .letterSortNum', checked: '.onChecked, .is-checked', } ]; let activeLayout = null; let containers = []; for (const layout of layouts) { containers = $$(layout.root); if (containers.length > 0) { activeLayout = layout; break; } } if (!activeLayout || containers.length === 0) { log.warn('[考试] 未检测到题目容器'); return []; } log.info(`[考试] 使用选择器: ${activeLayout.root}, 检测到 ${containers.length} 题`); // 跳过题型/分值标题的正则 const SKIP_HEADER = /^[\d\s\.\))、]*[【\[((]*[单多判填简论计][选断空答述算]|^\d+[\s\.、)\)]*\(?\d+分\)?$|^第?\d+题|^【.*题】/; containers.forEach((container, index) => { // 题目文本 — 跳过题型/分值标题,找到真正的问题 let questionText = ''; const qEls = container.querySelectorAll(activeLayout.question); for (const qEl of qEls) { const t = qEl.textContent.trim(); // 跳过:太短、题型标记如"1.【单选题】(2分)"、纯选项字母 if (t.length <= 5) continue; if (SKIP_HEADER.test(t) && t.length < 30) continue; if (/^[A-G][\..\s、]/.test(t)) continue; questionText = formatString(t); break; } // 备用:直接在container里找长文本段落 if (!questionText) { const ps = container.querySelectorAll('p, span, div'); for (const p of ps) { const t = p.textContent.trim(); if (t.length <= 8) continue; if (SKIP_HEADER.test(t) && t.length < 30) continue; if (/^[A-G][\..\s、]/.test(t)) continue; questionText = formatString(t); break; } } // 清理残留的题号和分值 if (questionText) { questionText = questionText .replace(/^[\d\s\.\))、]+/, '') .replace(/\(\s*\d+分\s*\)/g, '') .replace(/^[【\[((]*[单多判填简论计][选断空答述算][题】\]))]*\s*/g, '') .trim(); } if (!questionText || questionText.length < 4) return; // 题型 const typeEl = container.querySelector(activeLayout.type); const typeCode = typeEl ? getQuestionType(typeEl.textContent) : 4; // 选项文本 & 可点击元素 const optTexts = []; const $optEls = []; const optTextEls = container.querySelectorAll(activeLayout.options); const optClickEls = container.querySelectorAll(activeLayout.$options); const useClickEls = optClickEls.length >= optTextEls.length ? optClickEls : optTextEls; for (let i = 0; i < useClickEls.length; i++) { const textSrc = optTextEls[i] || useClickEls[i]; const raw = formatString(textSrc.textContent).replace(/^[A-Za-z][.\s、.]+/, '').trim(); optTexts.push(raw); $optEls.push(useClickEls[i]); } // 已选检测 const isChecked = (el) => { if (!activeLayout.checked) return false; return activeLayout.checked.split(',').some(sel => el.matches(sel.trim()) || el.querySelector(sel.trim())); }; questions.push({ index, el: container, text: questionText, type: typeCode, options: optTexts, optionEls: $optEls, isChecked, }); }); return questions; } // --- API调用 --- function gmRequest(opts) { return new Promise(resolve => { const timer = setTimeout(() => resolve(null), opts.timeout || 12000); try { GM_xmlhttpRequest({ ...opts, onload: (res) => { clearTimeout(timer); resolve(res); }, onerror: () => { clearTimeout(timer); resolve(null); }, ontimeout: () => { clearTimeout(timer); resolve(null); }, }); } catch { clearTimeout(timer); resolve(null); } }); } // 通用答案数组解析 function parseAnswersFromResult(resultObj, options) { if (!resultObj) return []; const raw = resultObj.answers || resultObj.answer || []; const success = resultObj.success !== false; let answers = []; if (typeof raw === 'string' && raw.trim()) { // 单个字符串答案 answers = [raw.trim()]; } else if (Array.isArray(raw) && raw.length > 0) { if (Array.isArray(raw[0])) { // 嵌套数组 [[答案1, 答案2]] answers = raw[0].map(a => String(a)); } else if (typeof raw[0] === 'number') { // 索引数组 [0, 2] => 对应选项 answers = raw.map(idx => options[idx] || String.fromCharCode(65 + idx)); } else { answers = raw.map(a => String(a)); } } return answers.filter(a => a && a.trim()); } // 你的中转API (qsy.iano.cn) async function askPaidAPI(question, options, type) { const deviceId = getDeviceId(); log.info(`[考试] 发送题目: "${question.slice(0, 40)}..." 选项:${options.length}个`); const res = await gmRequest({ method: 'POST', url: 'https://qsy.iano.cn/index.php?s=/api/question_bank/answer', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ device_id: deviceId, question: question, options: options, type: type, location: '智慧树' }), timeout: 12000, }); if (!res) { log.warn('[考试] 付费API无响应'); return null; } if (res.status !== 200) { log.warn(`[考试] 付费API HTTP ${res.status}`); return null; } try { const json = JSON.parse(res.responseText); log.info(`[考试] 付费API响应: code=${json.code}, msg=${json.msg || ''}`); if (json.code === 1 && json.data) { const result = json.data.result || json.data; const remaining = json.data.remaining_credits || 0; const answers = parseAnswersFromResult(result, options); if (answers.length > 0) { return { answers, remaining, source: '付费题库' }; } log.info('[考试] 付费API返回结果但无有效答案'); } if (json.code === 0) { log.warn(`[考试] 付费API: ${json.msg || '查询失败'}`); } } catch (e) { log.warn('[考试] 付费API解析错误: ' + e.message); } return null; } // 免费题库 (lyck6.cn, 与万能脚本同源) async function askFreeAPI(question, options, type) { const res = await gmRequest({ method: 'POST', url: 'http://lyck6.cn/scriptService/api/autoFreeAnswer', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ question: question, options: options, type: type, location: location.href }), timeout: 15000, }); if (!res) { log.warn('[考试] 免费API无响应'); return null; } if (res.status !== 200) { log.warn(`[考试] 免费API HTTP ${res.status}`); return null; } try { const json = JSON.parse(res.responseText); const result = json.result || json.data || json; const answers = parseAnswersFromResult(result, options); if (answers.length > 0) { return { answers, remaining: -1, source: '免费题库' }; } } catch (e) { log.warn('[考试] 免费API解析错误: ' + e.message); } return null; } // --- 当前题库模式: 'free' 或 'paid' --- let examMode = GM_getValue('av_exam_mode', 'free'); function setExamMode(mode) { examMode = mode; GM_setValue('av_exam_mode', mode); } // --- 免费验证码相关 --- function getVerifyState() { return GM_getValue('av_verify_state', { validUntil: 0 }); } function setVerifyState(state) { GM_setValue('av_verify_state', state); } function isVerifyValid() { const state = getVerifyState(); return state.validUntil && state.validUntil > Date.now() / 1000; } async function verifyCode(code) { const res = await gmRequest({ 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: 10000, }); if (!res || res.status !== 200) return { ok: false, msg: '网络错误' }; try { const json = JSON.parse(res.responseText); if (json.code === 1 && json.data) { const validUntil = json.data.valid_until || (Date.now() / 1000 + 86400); const validStr = json.data.valid_until_str || new Date(validUntil * 1000).toLocaleString(); setVerifyState({ validUntil }); return { ok: true, msg: `验证成功,有效期至 ${validStr}` }; } return { ok: false, msg: json.msg || '验证码无效或已过期' }; } catch { return { ok: false, msg: '解析错误' }; } } // 综合搜索: 根据模式选择题库 async function searchAnswer(question, options, type) { const cleanQ = formatString(question); if (!cleanQ || cleanQ.length < 4) return null; const cleanOpts = (options || []).map(o => formatString(o)); if (examMode === 'paid') { // 付费模式 log.info('[考试] 正在查询付费题库...'); const paidResult = await askPaidAPI(cleanQ, cleanOpts, type); if (paidResult && paidResult.answers.length > 0) { log.success(`[考试] 付费题库找到答案 (余${paidResult.remaining}积分)`); updateCreditsDisplay(paidResult.remaining); return paidResult; } } else { // 免费模式 if (!isVerifyValid()) { log.warn('[考试] 免费验证码未验证或已过期,请先验证'); return null; } log.info('[考试] 正在查询免费题库...'); const freeResult = await askFreeAPI(cleanQ, cleanOpts, type); if (freeResult && freeResult.answers.length > 0) { log.success('[考试] 免费题库找到答案'); return freeResult; } } return null; } // --- 智能答案匹配 & 填入 (借鉴万能脚本) --- function matchAndFill(q, result) { if (!result || !result.answers || result.answers.length === 0) return false; const answers = result.answers; let filled = false; if (q.type === 3) { // 判断题 const ansJoined = answers.join(','); for (let i = 0; i < q.optionEls.length; i++) { const shouldClick = (isTrue(ansJoined) && isFalse(q.options[i])) ? false : (isTrue(ansJoined) && !isFalse(q.options[i])) ? true : (isFalse(ansJoined) && isTrue(q.options[i])) ? false : (isFalse(ansJoined) && !isTrue(q.options[i])) ? true : false; // 更简单的逻辑: 如果答案为"对",点包含"对/正确/√"的选项 const optText = q.options[i] || ''; let match = false; if (isTrue(ansJoined)) match = isTrue(optText) || /对|正确|√|是|true/i.test(optText); else if (isFalse(ansJoined)) match = isFalse(optText) || /错|错误|×|否|false/i.test(optText); if (match && !q.isChecked(q.optionEls[i])) { q.optionEls[i].click(); filled = true; } } } else if (q.type === 2 || q.type === 4 || q.type === 5 || q.type === 6 || q.type === 7) { // 填空题/简答/论述等: 填入 textarea/input const inputs = q.el.querySelectorAll('input[type="text"], textarea'); const editorNames = q.el.querySelectorAll('textarea[name]'); if (editorNames.length > 0) { // 尝试 UEditor editorNames.forEach((ta, i) => { const val = answers[i] || answers[0] || ''; try { if (typeof UE !== 'undefined' && UE.getEditor) { UE.getEditor(ta.getAttribute('name')).setContent(val); filled = true; } } catch { } }); } if (!filled && inputs.length > 0) { inputs.forEach((inp, i) => { const val = answers[i] || answers[0] || ''; inp.value = val.trim(); inp.dispatchEvent(new Event('input', { bubbles: true })); inp.dispatchEvent(new Event('change', { bubbles: true })); }); filled = inputs.length > 0; } } else { // 单选(0)/多选(1)/其他选择题 const beautifulOptions = q.options.map(o => formatString(o).toLowerCase().replace(/\s/g, '')); let targetIndices = new Set(); for (const ans of answers) { // 1. 字母匹配 (A/B/C...) if (ans.length === 1 && isPlainAnswer(ans.toUpperCase())) { targetIndices.add(ans.toUpperCase().charCodeAt(0) - 65); continue; } // 2. 精确文本匹配 const val = formatString(ans).toLowerCase().replace(/\s/g, ''); const exactIdx = beautifulOptions.indexOf(val); if (exactIdx >= 0) { targetIndices.add(exactIdx); continue; } // 3. 模糊匹配 if (val.length >= 5) { const ratings = answerSimilar(val, beautifulOptions); const maxScore = Math.max(...ratings); if (maxScore > 65) { targetIndices.add(ratings.indexOf(maxScore)); } } } for (const idx of targetIndices) { if (idx >= 0 && idx < q.optionEls.length && !q.isChecked(q.optionEls[idx])) { q.optionEls[idx].click(); filled = true; } } } return filled; } // --- 积分管理 --- function updateCreditsDisplay(credits) { const el = $('#ae-credits'); if (el) el.textContent = credits >= 0 ? credits : '--'; } async function queryCredits() { const res = await gmRequest({ method: 'GET', url: `https://qsy.iano.cn/index.php?s=/api/question_bank/credits&device_id=${encodeURIComponent(getDeviceId())}`, timeout: 8000, }); if (!res || res.status !== 200) return 0; try { const json = JSON.parse(res.responseText); if (json.code === 1 && json.data) return json.data.remaining_credits || 0; } catch { } return 0; } async function activateCode(code) { const res = await gmRequest({ method: 'POST', url: 'https://qsy.iano.cn/index.php?s=/api/question_bank/activate', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ code, device_id: getDeviceId() }), timeout: 10000, }); if (!res || res.status !== 200) return { ok: false, msg: '网络错误' }; try { const json = JSON.parse(res.responseText); return { ok: json.code === 1, msg: json.msg || '', data: json.data }; } catch { return { ok: false, msg: '解析错误' }; } } // --- 考试面板 UI --- function createExamPanel() { if ($('#av-exam-panel')) return; const panel = document.createElement('div'); panel.id = 'av-exam-panel'; panel.innerHTML = `
📝 考试助手 ▼
扫码获取验证码
📱 扫码观看广告获取验证码
验证后免费使用24小时
0 已找到 0 未找到 0 总题数
⚠️ 仅供学习交流,请勿用于违规行为
使用者自行承担一切后果
`; document.body.appendChild(panel); // 领取付费题库测试弹窗(独立于面板,避免被面板滚动区域遮挡) const trialModal = document.createElement('div'); trialModal.id = 'av-trial-modal'; trialModal.hidden = true; trialModal.innerHTML = ` `; document.body.appendChild(trialModal); let trialPreviousFocus = null; const closeTrialModal = () => { trialModal.hidden = true; if (trialPreviousFocus && typeof trialPreviousFocus.focus === 'function') trialPreviousFocus.focus(); }; const openTrialModal = () => { trialPreviousFocus = document.activeElement; trialModal.hidden = false; const closeButton = trialModal.querySelector('.ae-trial-close'); if (closeButton) closeButton.focus(); }; $('#ae-trial-btn').addEventListener('click', openTrialModal); trialModal.querySelector('.ae-trial-close').addEventListener('click', closeTrialModal); trialModal.querySelector('.ae-trial-done').addEventListener('click', closeTrialModal); trialModal.addEventListener('click', (event) => { if (event.target === trialModal) closeTrialModal(); }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !trialModal.hidden) closeTrialModal(); }); // 收起/展开 $('#ae-collapse').addEventListener('click', () => { panel.classList.toggle('collapsed'); $('#ae-collapse').textContent = panel.classList.contains('collapsed') ? '▶' : '▼'; }); makeDraggable(panel, $('#ae-drag-handle')); // --- 模式切换 --- function switchModeUI(mode) { const freeTab = $('#ae-mode-free'); const paidTab = $('#ae-mode-paid'); const freeArea = $('#ae-area-free'); const paidArea = $('#ae-area-paid'); if (mode === 'paid') { freeTab.style.background = 'transparent'; freeTab.style.color = '#94a3b8'; paidTab.style.background = 'rgba(96,165,250,0.15)'; paidTab.style.color = '#60a5fa'; freeArea.style.display = 'none'; paidArea.style.display = ''; setExamMode('paid'); } else { paidTab.style.background = 'transparent'; paidTab.style.color = '#94a3b8'; freeTab.style.background = 'rgba(52,211,153,0.15)'; freeTab.style.color = '#34d399'; paidArea.style.display = 'none'; freeArea.style.display = ''; setExamMode('free'); } } $('#ae-mode-free').addEventListener('click', () => switchModeUI('free')); $('#ae-mode-paid').addEventListener('click', () => switchModeUI('paid')); // 恢复上次模式 switchModeUI(examMode); // --- 免费验证码 --- // 恢复验证状态 if (isVerifyValid()) { const state = getVerifyState(); const expStr = new Date(state.validUntil * 1000).toLocaleString(); $('#ae-verify-msg').textContent = `✅ 已验证,有效期至 ${expStr}`; $('#ae-verify-msg').style.color = '#34d399'; $('#ae-verify-code').disabled = true; $('#ae-verify-btn').disabled = true; } $('#ae-verify-btn').addEventListener('click', async () => { const code = ($('#ae-verify-code').value || '').trim(); if (!code || code.length !== 4) { $('#ae-verify-msg').textContent = '请输入4位验证码'; $('#ae-verify-msg').style.color = '#f87171'; return; } $('#ae-verify-btn').disabled = true; $('#ae-verify-btn').textContent = '验证中...'; const result = await verifyCode(code); $('#ae-verify-msg').textContent = result.ok ? `✅ ${result.msg}` : `❌ ${result.msg}`; $('#ae-verify-msg').style.color = result.ok ? '#34d399' : '#f87171'; if (result.ok) { $('#ae-verify-code').disabled = true; } else { $('#ae-verify-btn').disabled = false; } $('#ae-verify-btn').textContent = '验证'; }); // 回车验证 $('#ae-verify-code').addEventListener('keypress', (e) => { if (e.key === 'Enter') $('#ae-verify-btn').click(); }); // --- 付费激活码 --- $('#ae-activate-btn').addEventListener('click', async () => { const code = ($('#ae-code-input').value || '').trim(); if (!code) return; $('#ae-activate-msg').textContent = '激活中...'; const result = await activateCode(code); $('#ae-activate-msg').textContent = result.msg; $('#ae-activate-msg').style.color = result.ok ? '#34d399' : '#f87171'; if (result.ok) { const c = await queryCredits(); updateCreditsDisplay(c); } }); // --- 搜题/填入/停止 --- $('#ae-search-all').addEventListener('click', () => searchAllQuestions()); $('#ae-fill-all').addEventListener('click', () => fillAllAnswers()); $('#ae-stop').addEventListener('click', () => { isSearching = false; log.info('[考试] 已停止搜题'); }); // 付费模式查询积分 if (examMode === 'paid') { queryCredits().then(c => updateCreditsDisplay(c)); } } // --- 搜题与填入逻辑 --- let examResults = new Map(); let isSearching = false; async function searchAllQuestions() { if (isSearching) { log.warn('[考试] 正在搜题中...'); return; } isSearching = true; const questions = extractExamQuestions(); const listEl = $('#ae-question-list'); if (!listEl) { isSearching = false; return; } listEl.innerHTML = ''; examResults.clear(); $('#ae-total').textContent = questions.length; $('#ae-found').textContent = '0'; $('#ae-notfound').textContent = '0'; log.info(`[考试] 共 ${questions.length} 道题目,开始搜题...`); let found = 0, notFound = 0; for (let i = 0; i < questions.length; i++) { if (!isSearching) break; const q = questions[i]; const itemDiv = document.createElement('div'); itemDiv.className = 'ae-q-item searching'; itemDiv.id = `ae-q-${i}`; itemDiv.innerHTML = `
第${i + 1}题 [${TYPE_NAMES[q.type] || '未知'}]
${q.text}
☕ 搜索中...
`; listEl.appendChild(itemDiv); const result = await searchAnswer(q.text, q.options, q.type); const answerDiv = itemDiv.querySelector('.ae-q-answer'); const sourceDiv = itemDiv.querySelector('.ae-q-source'); if (result && result.answers.length > 0) { answerDiv.textContent = '✅ ' + result.answers.join(' | '); sourceDiv.textContent = '来源: ' + result.source; itemDiv.className = 'ae-q-item found'; found++; examResults.set(i, { question: q, result }); } else { answerDiv.textContent = '❌ 未找到答案'; answerDiv.className = 'ae-q-answer empty'; itemDiv.className = 'ae-q-item not-found'; notFound++; examResults.set(i, { question: q, result: null }); } $('#ae-found').textContent = found; $('#ae-notfound').textContent = notFound; await sleep(600); } // 绑定单题按钮 listEl.querySelectorAll('.ae-q-btn').forEach(btn => { btn.addEventListener('click', async (e) => { const action = e.target.dataset.action; const idx = parseInt(e.target.dataset.idx); if (action === 'retry') { const q = questions[idx]; const itemDiv = $(`#ae-q-${idx}`); if (!itemDiv) return; itemDiv.className = 'ae-q-item searching'; itemDiv.querySelector('.ae-q-answer').textContent = '☕ 重新搜索中...'; const result = await searchAnswer(q.text, q.options, q.type); const ad = itemDiv.querySelector('.ae-q-answer'); const sd = itemDiv.querySelector('.ae-q-source'); if (result && result.answers.length > 0) { ad.textContent = '✅ ' + result.answers.join(' | '); sd.textContent = '来源: ' + result.source; itemDiv.className = 'ae-q-item found'; examResults.set(idx, { question: q, result }); } else { ad.textContent = '❌ 未找到答案'; ad.className = 'ae-q-answer empty'; itemDiv.className = 'ae-q-item not-found'; } } else if (action === 'fill') { const data = examResults.get(idx); if (data && data.result) { const ok = matchAndFill(data.question, data.result); log.info(`[考试] 第${idx + 1}题 填入${ok ? '成功' : '失败'}`); } } }); }); isSearching = false; log.success(`[考试] 搜题完成! 找到:${found} 未找到:${notFound}`); if (config.examAutoFill && found > 0) { await sleep(1000); fillAllAnswers(); } } async function fillAllAnswers() { let filled = 0, failed = 0; for (const [idx, data] of examResults) { if (data.result) { const ok = matchAndFill(data.question, data.result); if (ok) filled++; else failed++; await sleep(400); } } log.success(`[考试] 填入完成! 成功:${filled} 失败:${failed}`); } // --- 考试页入口 --- async function examMain() { log.info('考试助手启动!'); await sleep(3000); createExamPanel(); if (config.autoExam) { await sleep(2000); searchAllQuestions(); } } // 获取并展示公告 function fetchAnnouncement() { try { GM_xmlhttpRequest({ method: 'GET', url: 'https://qsy.iano.cn/index.php?s=/admin/unified_manage/scriptannouncementapi', timeout: 5000, onload: function(resp) { try { var res = JSON.parse(resp.responseText); if (res.code === 1 && res.data && res.data.enabled) { var area = $('#av-notice-area'); if (!area) return; var colors = { info: '#60a5fa', warning: '#fbbf24', error: '#f87171', success: '#4ade80' }; var c = colors[res.data.type] || '#60a5fa'; var el = document.createElement('div'); el.className = 'av-notice'; var borders = { info:'rgba(96,165,250,0.3)', warning:'rgba(251,191,36,0.3)', error:'rgba(248,113,113,0.3)', success:'rgba(74,222,128,0.3)' }; el.style.borderColor = borders[res.data.type] || borders.info; var header = document.createElement('div'); header.className = 'av-notice-title'; var title = document.createElement('span'); title.style.color = c; title.innerHTML = '📢 '; title.appendChild(document.createTextNode(String(res.data.title || '公告'))); var close = document.createElement('span'); close.className = 'av-notice-close'; close.innerHTML = '×'; header.appendChild(title); header.appendChild(close); el.appendChild(header); // 公告图片可能由 images/image_url 字段返回,也可能直接写在正文(Markdown 或 img 标签)中。 // 旧实现只把 content 塞进 innerHTML,后台使用独立图片字段时图片会被完全丢弃。 var rawContent = String(res.data.content || ''); var noticeImages = []; var addImage = function(url) { url = String(url || '').trim(); if (!url) return; if (/^data:image\//i.test(url) || /^https?:\/\//i.test(url)) { noticeImages.push(url); return; } if (/^\/\//.test(url)) { noticeImages.push('https:' + url); return; } try { noticeImages.push(new URL(url, 'https://qsy.iano.cn/').href); } catch (_) {} }; var images = res.data.images; if (Array.isArray(images)) { images.forEach(addImage); } else if (typeof images === 'string' && images.trim()) { try { var parsedImages = JSON.parse(images); (Array.isArray(parsedImages) ? parsedImages : [images]).forEach(addImage); } catch (_) { images.split(/[\n,]+/).forEach(addImage); } } if (res.data.image_url) addImage(res.data.image_url); rawContent = rawContent.replace(/!\[[^\]]*\]\(\s*]+)>?(?:\s+["'][^)]*)?\)/g, function(_, url) { addImage(url); return ''; }); rawContent = rawContent.replace(/]*\bsrc\s*=\s*(?:["']([^"']+)["']|([^\s>]+))[^>]*>/gi, function(_, quotedUrl, bareUrl) { addImage(quotedUrl || bareUrl); return ''; }).trim(); noticeImages = noticeImages.filter(function(url, idx, arr) { return url && arr.indexOf(url) === idx; }).slice(0, 6); var body = document.createElement('div'); body.className = 'av-notice-body'; // 保留常见的换行/粗体等公告排版,同时移除脚本和事件属性。 var contentBox = document.createElement('div'); var contentSource = document.createElement('div'); contentSource.innerHTML = rawContent; contentSource.querySelectorAll('script,style,iframe,object,embed,link,meta').forEach(function(node) { node.remove(); }); contentSource.querySelectorAll('*').forEach(function(node) { Array.from(node.attributes).forEach(function(attr) { if (/^on/i.test(attr.name) || attr.name === 'style' || attr.name === 'src' || attr.name === 'href') { node.removeAttribute(attr.name); } }); }); contentBox.innerHTML = contentSource.innerHTML; if (contentBox.textContent.trim() || contentBox.querySelector('*')) body.appendChild(contentBox); if (noticeImages.length) { var imageBox = document.createElement('div'); imageBox.className = 'av-notice-images'; noticeImages.forEach(function(url) { var img = document.createElement('img'); img.src = url; img.alt = '公告图片'; img.loading = 'lazy'; img.decoding = 'async'; img.referrerPolicy = 'no-referrer'; img.addEventListener('click', function() { try { window.open(url, '_blank', 'noopener,noreferrer'); } catch (_) {} }); img.addEventListener('error', function() { img.style.display = 'none'; var tip = document.createElement('div'); tip.className = 'av-notice-image-fallback'; tip.textContent = '公告图片加载失败'; imageBox.appendChild(tip); }, { once: true }); imageBox.appendChild(img); }); body.appendChild(imageBox); } el.appendChild(body); area.appendChild(el); close.onclick = function(){ el.style.display='none'; }; } } catch(e) {} } }); } catch(e) {} } // ===================== 主流程 ===================== async function main() { // 首先激活反检测(必须在所有操作之前) try { hookAntiCheat(); } catch (e) { log.warn('反检测模块初始化异常: ' + e.message); } // 考试页走考试流程 if (isExamPage()) { await examMain(); return; } if (!isStudyPage()) { log.info('当前页面非学习页,脚本待命中'); createPanel(); fetchAnnouncement(); updatePanelStatus('⏳ 等待进入学习/考试页面...'); return; } log.info('智慧树刷课助手启动!'); log.info(`页面类型: ${isHikeClass() ? '翻转课' : isNewVersion() ? '智慧共享课(新版)' : '共享课'}`); // 等待页面加载 await sleep(3000); // 创建面板 createPanel(); // 获取公告 fetchAnnouncement(); // 页面优化 await optimizePage(); // 等待视频元素 await waitForVideo(); resetProgressHealth(); completionSince = 0; // 初始化视频设置 overridePause(); setMute(); setSpeed(); const title = getLessonTitle(); log.info(`正在学习: ${title}`); updatePanelStatus(`▶️ ${title}`); // 启动定时任务 // 1. 检测暂停并续播 (每3秒) addTimer(checkAndPlay, 3000); // 2. 视频设置维持(倍速、静音) (每5秒) addTimer(() => { overridePause(); setMute(); setSpeed(); }, 5000); // 3. 弹题检测 (每3秒) addTimer(skipQuestions, 3000); // 空白弹窗需要比普通提示更快清理,避免用户点击确认触发首页兜底路由。 addTimer(clearBlankDialogs, 1000); // 4. 弹窗关闭 (每4秒) addTimer(closePopups, 4000); // 5. 人机验证检测 (每5秒) addTimer(checkVerify, 5000); // 6. 习惯分模拟鼠标 (每8秒) addTimer(simulateMouseMove, 8000); // 7. 进度监控 & 自动切换 (每3秒) addTimer(async () => { const progress = getCourseProgress(); updatePanelProgress(progress); if (isVerificationVisible()) { verifyBlocking = true; pauseForVerification(); return; } verifyBlocking = false; monitorProgressHealth(parseProgress(progress)); if (parseProgress(progress) >= 100) { if (!completionSince) completionSince = Date.now(); if (Date.now() - completionSince >= 2000) { await switchToNext(); } } else { completionSince = 0; } }, 3000); log.success('所有任务模块已启动'); } async function waitForVideo() { let retries = 0; while (!getVideo() && retries < 30) { await sleep(1000); retries++; } if (getVideo()) { log.info('视频元素已就绪'); } else { log.warn('未找到视频元素,部分功能可能不可用'); } } // ===================== 油猴菜单命令 ===================== GM_registerMenuCommand('⚡ 重新启动脚本', () => { stopAllTasks(); const panel = $('#autovisor-panel'); if (panel) panel.remove(); main(); }); GM_registerMenuCommand('⏹ 停止脚本', () => { stopAllTasks(); updatePanelStatus('⏹ 脚本已停止'); log.info('脚本已手动停止'); }); GM_registerMenuCommand('🔄 重置配置', () => { config = Object.assign({}, DEFAULT_CONFIG); saveConfig(config); log.info('配置已重置为默认值'); location.reload(); }); // ===================== 启动 ===================== main(); })();