// ==UserScript== // @name 智慧树刷课助手 // @namespace https://github.com/CXRunfree/Autovisor // @version 1.4.1 // @description 智慧树普通课程与 AI 课程自动播放、自动切换下一资源、跳过弹窗和弹题、自动静音、倍速播放、检测暂停续播、刷习惯分、习题 AI 答题 // @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/* // @match *://studentexamcomh5.zhihuishu.com/studentReviewTestOrExam/* // @match *://ai-smart-course-student-pro.zhihuishu.com/point/* // @match *://ai-smart-course-student-pro.zhihuishu.com/examPreview/* // @match *://ai-smart-course-student-pro.zhihuishu.com/singleCourse/knowledgeStudy/* // @match *://ai.zhihuishu.com/AIstudent/* // @match *://ai-smart-course-student-pro.zhihuishu.com/learnPage/* // @match *://hike-teaching-center.polymas.com/stu-hike/agent-course-hike/ai-course-center* // @match *://onlineweb.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'; // v1.4.1 (2026-10-12):弹题限定当前题窗、确认作答后关闭; // 弹题处理中阻止续播/切集,修复结束重播和折叠目录误判完成。 /* * ========================================== * 免责声明 / 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 clickTarget = isAICourseClass() ? (item.closest?.('a,button,[role="button"]') || item.querySelector?.('a,button,[role="button"]') || item) : item; try { clickTarget.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' }); } catch (_) {} const anchor = clickTarget.matches?.('a[href]') ? clickTarget : clickTarget.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 { clickTarget.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); clickTarget.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); clickTarget.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); anchor.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window, })); return true; } finally { anchor.removeEventListener('click', guard, true); } } try { clickTarget.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); clickTarget.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); clickTarget.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); clickTarget.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'); } // 智慧树 AI 课程使用独立的 learnPage/singleCourse 路由,目录和资源卡 // 也不再复用普通共享课的 .child-info/.clearfix.video 结构。这里只做 // 页面和 DOM 适配,不直接请求课程接口,避免影响平台自身的数据流。 function isAICourseClass() { const host = location.hostname || ''; const path = location.pathname || ''; return host === 'ai-smart-course-student-pro.zhihuishu.com' && (/\/learnPage\//.test(path) || /\/singleCourse\/knowledgeStudy\//.test(path)) || host === 'hike-teaching-center.polymas.com' && path.includes('/stu-hike/agent-course-hike/ai-course-center') || host === 'onlineweb.zhihuishu.com' && /(learn|study|course)/i.test(path); } function getAICourseResourceItems() { const selectors = [ '.resources-list .basic-info-video-card-container', '.resources-list [class*="basic-info"][class*="card-container"]', '.basic-info-video-card-container', '[class*="basic-info"][class*="card-container"]', '.resources-list [data-resources-uid]', '.resources-list [data-resource-uid]', '.resource-list .resource-card', '.resource-card[data-uid]', ]; const seen = new Set(); const items = []; selectors.forEach(selector => { document.querySelectorAll(selector).forEach(item => { if (!seen.has(item)) { seen.add(item); items.push(item); } }); }); return items; } function getAICoursePointItems() { const selectors = [ '.section-item-collapse-info', '.collapse-item-sub', '[data-knowledge-id]', '[data-point-id]', ]; const seen = new Set(); const items = []; selectors.forEach(selector => { document.querySelectorAll(selector).forEach(item => { if (!seen.has(item)) { seen.add(item); items.push(item); } }); }); return items; } function getAICourseItems() { const resources = getAICourseResourceItems(); if (resources.length) return resources; return getAICoursePointItems(); } function getAICourseItemProgress(item) { if (!item) return 0; const status = item.getAttribute('data-study-status') || item.getAttribute('data-status') || item.getAttribute('data-learn-status'); if (/^(1|done|finished|completed|已学|已完成)$/i.test(String(status || '').trim())) return 100; const attrNames = ['data-progress', 'data-schedule', 'data-percent', 'aria-valuenow']; for (const name of attrNames) { const raw = item.getAttribute(name); if (raw !== null && raw !== '') return parseProgress(raw); } const progress = item.querySelector('[aria-valuenow], [data-progress], [class*="progress"]'); if (progress) { const raw = progress.getAttribute('aria-valuenow') || progress.getAttribute('data-progress') || progress.textContent; const value = parseProgress(raw); if (value > 0) return value; } const text = (item.textContent || '').replace(/\s+/g, ''); if (/(已学|已完成|学习完成|完成学习|100%)/.test(text)) return 100; const ratio = text.match(/(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)/); if (ratio && Number(ratio[2]) > 0) return Math.max(0, Math.min(100, Number(ratio[1]) / Number(ratio[2]) * 100)); return 0; } function isAICourseItemComplete(item) { return getAICourseItemProgress(item) >= 100; } function getActiveAICourseItem() { const items = getAICourseItems(); if (!items.length) return null; const activeSelectors = [ '.active', '.current', '.selected', '.is-active', '.current-resource', '[aria-current="true"]', '[aria-selected="true"]', '[data-active="true"]', ]; for (const item of items) { if (activeSelectors.some(selector => item.matches(selector))) return item; } for (const item of items) { if (activeSelectors.slice(4).some(selector => item.querySelector(selector))) return item; } const video = getVideo(); if (video) { let current = video.closest('.basic-info-video-card-container, [class*="resource"], .resources-list'); if (current && !items.includes(current)) { current = items.find(item => item.contains(video) || item.contains(current)); } if (current) return current; } return items[0]; } 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') || isAICourseClass(); } function isExamPage() { const url = location.href; const aiExamHost = /ai-smart-course-student-pro\.zhihuishu\.com|studentexamcomh5\.zhihuishu\.com|ai\.zhihuishu\.com/i.test(location.hostname); const aiExamRoute = /\/studentReviewTestOrExam\/|\/examPreview\/|\/point\//i.test(location.pathname); // 只匹配实际做题页面,不匹配作业/考试列表页 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/') || (aiExamHost && aiExamRoute) || // 通用: 页面内存在题目容器(延迟检测) (url.includes('zhihuishu.com') && !!document.querySelector('.examPaper_subject, .questionBox, .ques-detail')) || (aiExamHost && !!document.querySelector('.questionContent .el-radio-group, .questionContent .el-checkbox-group, .questionContent ul.radio-view, .questionContent ul.checkbox-view')); } function isAiExamHost() { const hostMatched = /ai-smart-course-student-pro\.zhihuishu\.com|studentexamcomh5\.zhihuishu\.com|ai\.zhihuishu\.com/i.test(location.hostname); if (!hostMatched) return false; if (/\/studentReviewTestOrExam\/|\/examPreview\/|\/point\//i.test(location.pathname)) return true; // AI 课程把练习嵌在 singleCourse/knowledgeStudy 路由中,题目通常异步挂载。 // 只有检测到题目容器时才切到 AI 答题模式,避免普通学习页误显示答题面板。 return /\/singleCourse\/knowledgeStudy\//i.test(location.pathname) && !!document.querySelector('.questionContent, .questionTitle, .exam-preview, [class*="question-content"]'); } // ===================== 反检测模块 ===================== 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; let completionLessonKey = ''; let endedPlayback = null; 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 (isAICourseClass()) return getAICourseItems(); 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 (isAICourseClass()) return getActiveAICourseItem(); 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') || item.getAttribute('data-resources-uid') || item.getAttribute('data-resource-uid') || item.getAttribute('data-knowledge-id') || item.getAttribute('data-point-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 (isAICourseClass()) return isAICourseItemComplete(item); 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); // 与定时清理共用规则,不能把随堂弹题的关闭图标当成普通提示。 closePopups(); if (!isNewVersion() && !isHikeClass() && !isAICourseClass()) { // 自动夜间模式 (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; // 弹题仍待处理时平台会主动暂停;不要和弹题处理流程争抢播放。 if (exerciseInProgress || hasBlockingStudyDialog()) return; const video = getVideo(); // 对 ended 的视频调用 play() 会从头重播,让下一集检测错过结束状态。 if (video && video.paused && !video.ended) { try { await video.play(); log.info('检测到视频暂停,已恢复播放'); } catch (e) { // 自动播放可能被浏览器策略阻止 log.warn('自动播放被阻止,请手动点击播放'); } } } // --- 随堂弹题 --- let exerciseInProgress = false; let exerciseGeneration = 0; function isStudyExerciseDialog(dialog) { if (!dialog) return false; return dialog.matches('.dialog-test, .ai-class-exercise-dialog') || !!dialog.querySelector('.topic-item') || (!!dialog.querySelector('.number') && /弹题|出题|随堂|练习/.test(dialog.textContent || '')); } function getStudyExerciseDialog() { return Array.from($$('.dialog-test, .el-dialog, .el-message-box, .ai-class-exercise-dialog')) .find(dialog => isElementVisible(dialog) && isStudyExerciseDialog(dialog)) || null; } function isUnansweredExerciseWarning(dialog) { return /未[作做]答.*(?:不能|无法)关闭/.test((dialog?.textContent || '').replace(/\s+/g, '')); } function hasBlockingStudyDialog() { return !!getStudyExerciseDialog() || Array.from($$('.el-message-box, .el-dialog')) .some(dialog => isElementVisible(dialog) && !isStudyExerciseDialog(dialog) && isUnansweredExerciseWarning(dialog)); } function dismissExerciseWarning() { // 只关闭明确的“未作答不能关闭”提示,保留下面的题目继续处理。 Array.from($$('.el-message-box, .el-dialog')).forEach(dialog => { if (!isElementVisible(dialog) || isStudyExerciseDialog(dialog) || !isUnansweredExerciseWarning(dialog)) return; const close = dialog.querySelector('.el-message-box__headerbtn, .el-dialog__headerbtn, .iconfont.iconguanbi'); if (close && isElementVisible(close)) close.click(); }); } function isExerciseChoiceSelected(choice) { return choice.matches('input:checked, .active, .selected, .checked, .is-checked, [aria-checked="true"]') || !!choice.querySelector('input:checked, .is-checked, [aria-checked="true"]'); } function getExerciseQuestionKey(dialog) { return Array.from(dialog.querySelectorAll('.topic-title, .topic-item')).filter(isElementVisible) .map(node => (node.textContent || node.getAttribute('aria-label') || node.value || '').trim()).join('\n'); } async function showExerciseQuestion(dialog, number, canContinue) { // 题号可能先变蓝、题干稍后才返回。不能沿用上一题尚未消失的 .answer。 if (number.matches('.active, .current, .is-active, [aria-selected="true"], [aria-current="true"]')) return true; const previousKey = getExerciseQuestionKey(dialog); number.click(); for (let attempt = 0; attempt < 30; attempt++) { if (!canContinue() || !dialog.isConnected || !isElementVisible(dialog)) return false; const key = getExerciseQuestionKey(dialog); if (key && key !== previousKey) return true; await sleep(100); } return false; } function hasExerciseAnswer(dialog) { // .answer 是平台作答后的答案/解析区。仅看当前弹题内可见的结果, // 不能用页面其他题目的 .answer 或单纯勾选状态当作提交成功。 return Array.from(dialog.querySelectorAll('.answer')).some(answer => isElementVisible(answer) && !answer.querySelector('.topic-item') && !!((answer.textContent || '').trim() || answer.querySelector('img'))); } function findExerciseButton(dialog, textPattern) { return Array.from(dialog.querySelectorAll('button, .btn, [role="button"], .el-button')) .find(button => isElementVisible(button) && !button.disabled && !button.classList.contains('is-disabled') && button.getAttribute('aria-disabled') !== 'true' && textPattern.test((button.textContent || '').replace(/\s+/g, ''))); } async function waitForExerciseAnswer(dialog, canContinue) { for (let attempt = 0; attempt < 20; attempt++) { if (!canContinue()) return false; if (!isElementVisible(dialog) || !dialog.isConnected || hasExerciseAnswer(dialog)) return true; await sleep(100); } return false; } async function handleFusionExercise(canContinue = () => true) { if (!isFusionClass()) return false; const dialog = $('.ai-class-exercise-dialog'); if (!isElementVisible(dialog) || !canContinue()) return false; try { const option = dialog.querySelector('.option'); if (option && !isExerciseChoiceSelected(option)) option.click(); await sleep(600); if (!canContinue() || !isElementVisible(dialog)) return false; const submit = dialog.querySelector('.el-dialog__footer button.el-button'); if (submit && !submit.disabled && !submit.classList.contains('is-disabled')) submit.click(); await sleep(1000); if (!canContinue()) return false; log.info('融合课 AI 随堂练习已尝试提交'); return Boolean(option || submit); } catch (e) { log.warn('融合课 AI 随堂练习处理失败: ' + e.message); return false; } } async function skipQuestions() { if (!config.autoAnswer || exerciseInProgress) return; if (isHikeClass()) return; // 翻转课不支持自动答题 if (isVerificationVisible()) { verifyBlocking = true; pauseForVerification(); return; } const dialog = getStudyExerciseDialog(); if (!dialog) return; const generation = exerciseGeneration; const canContinue = () => generation === exerciseGeneration && config.autoAnswer && !isVerificationVisible(); exerciseInProgress = true; try { dismissExerciseWarning(); if (isFusionClass()) { await handleFusionExercise(canContinue); return; } // 目录和题目都使用 el-scrollbar__view,必须从当前可见题窗内找题。 const numbers = Array.from(dialog.querySelectorAll('.number')).filter(isElementVisible); const total = Math.max(1, numbers.length); log.info(`检测到 ${total} 道弹题`); for (let index = 0; index < total; index++) { if (!canContinue() || !dialog.isConnected || !isElementVisible(dialog)) return; const currentNumbers = Array.from(dialog.querySelectorAll('.number')).filter(isElementVisible); if (numbers.length > 1) { if (!currentNumbers[index]) return; if (!await showExerciseQuestion(dialog, currentNumbers[index], canContinue)) { if (canContinue()) updatePanelStatus('⏳ 弹题切换未确认,请检查题目'); return; } } if (!canContinue() || !dialog.isConnected || !isElementVisible(dialog)) return; if (hasExerciseAnswer(dialog)) continue; const choices = Array.from(dialog.querySelectorAll('.topic-item')).filter(isElementVisible); if (!choices.length) { updatePanelStatus('⏳ 等待弹题选项加载'); return; } // 随堂练习沿用选项触发作答的流程;单选只点一次,多选保留已有勾选。 // 点击后若平台已显示答案,不能继续点击下一选项把答案反选。 if (!choices.some(isExerciseChoiceSelected)) { const multiple = /多选|多项选择/.test(dialog.textContent || '') || !!dialog.querySelector('input[type="checkbox"], .el-checkbox'); for (const choice of choices.slice(0, multiple ? 2 : 1)) { if (!canContinue() || hasExerciseAnswer(dialog)) break; if (choice.disabled || choice.getAttribute('aria-disabled') === 'true') continue; choice.click(); await sleep(200); } } if (!canContinue()) return; // 部分版本选择后直接显示答案,其他版本必须点击提交/确定。 const submit = findExerciseButton(dialog, /^(提交(?:答案|作答)?|确定|确认(?:答案|作答)?|完成作答)$/); if (!hasExerciseAnswer(dialog) && submit) { submit.click(); } if (!await waitForExerciseAnswer(dialog, canContinue)) { if (canContinue()) { log.warn(`第 ${index + 1} 道弹题尚未确认作答,保留题窗等待处理`); updatePanelStatus('⚠️ 弹题未完成,请检查题目'); } return; } } if (!canContinue() || !isElementVisible(dialog) || !dialog.isConnected) return; const close = findExerciseButton(dialog, /^(关闭|继续学习|继续播放)$/) || Array.from(dialog.querySelectorAll('.el-dialog__headerbtn, .iconfont.iconguanbi')).find(isElementVisible); if (close) { close.click(); await sleep(350); if (!dialog.isConnected || !isElementVisible(dialog)) log.success('弹题已完成并关闭'); else updatePanelStatus('⏳ 等待弹题关闭'); } } catch (e) { log.warn('处理弹题时出错: ' + e.message); } finally { exerciseInProgress = false; if (canContinue() && !hasBlockingStudyDialog()) await checkAndPlay(); } } const blankDialogSince = new WeakMap(); function dialogTextWithoutControls(dialog) { if (!dialog) return ''; const clone = dialog.cloneNode(true); clone.querySelectorAll('button, .el-dialog__header, .el-message-box__header, .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() { if (exerciseInProgress || hasBlockingStudyDialog()) return false; 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; dismissExerciseWarning(); clearBlankDialogs(); // 不再对所有 .el-dialog 发送 ESC。课程切换/播放器状态弹窗也使用 // 该类名,ESC 会被平台解释成返回首页。仅处理明确的消息框关闭按钮。 document.querySelectorAll('.el-message-box').forEach(dialog => { if (!isElementVisible(dialog) || isStudyExerciseDialog(dialog) || isUnansweredExerciseWarning(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(); } }); document.querySelectorAll('.el-dialog').forEach(dialog => { if (!isElementVisible(dialog) || isStudyExerciseDialog(dialog) || isUnansweredExerciseWarning(dialog)) return; // 仅关闭已识别的学习时长提示,其他弹窗交给平台自身处理。 if (!/学习时长|学习时间.*(?:提醒|提示)|休息提醒/.test(dialogTextWithoutControls(dialog))) return; const close = dialog.querySelector('.iconfont.iconguanbi, .el-dialog__headerbtn'); if (close && isElementVisible(close)) close.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 (isAICourseClass()) { const video = getVideo(); if (video && (active.contains(video) || isElementVisible(video)) && Number.isFinite(video.duration) && video.duration > 0 && video.currentTime >= 0) { const percent = Math.min(100, (video.currentTime / video.duration) * 100); if (percent >= 99.5) return '100%'; return `${percent.toFixed(1)}%`; } return `${getAICourseItemProgress(active)}%`; } 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)); } function isCurrentLessonFinished() { // 平台目录的百分比可能晚于播放器结束更新,ended 同样是完成证据。 const video = getVideo(); const lessonKey = getLessonKey(); let ended = false; if (video?.ended && lessonKey) { const source = video.currentSrc || video.src || ''; if (!endedPlayback || endedPlayback.video !== video || endedPlayback.source !== source) { endedPlayback = { video, source, lessonKey }; } // 目录先切换、播放器后换源时,旧视频的 ended 不能算作新课时完成。 ended = endedPlayback.lessonKey === lessonKey; } else { endedPlayback = null; } return ended || parseProgress(getCourseProgress()) >= 100; } async function checkLessonProgress() { const progress = getCourseProgress(); updatePanelProgress(progress); if (isVerificationVisible()) { verifyBlocking = true; pauseForVerification(); completionSince = 0; return; } verifyBlocking = false; if (exerciseInProgress || hasBlockingStudyDialog()) { completionSince = 0; return; } monitorProgressHealth(parseProgress(progress)); const currentKey = getLessonKey(); if (currentKey !== completionLessonKey) { completionLessonKey = currentKey; completionSince = 0; } if (isCurrentLessonFinished()) { if (!completionSince) completionSince = Date.now(); if (Date.now() - completionSince >= 2000) await switchToNext(); } else { completionSince = 0; } } // --- 切换到下一集 --- async function switchToNext() { if (!config.autoNext || verifyBlocking || isVerificationVisible()) return; if (exerciseInProgress || hasBlockingStudyDialog()) return; if (switchInProgress || Date.now() < nextSwitchAllowedAt) return; if (!isCurrentLessonFinished()) 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)); if (!next) { log.success('🎉 所有课程已学习完毕!'); updatePanelStatus('🎉 全部完成!'); stopAllTasks(); return; } switchInProgress = true; const generation = exerciseGeneration; 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 (generation !== exerciseGeneration) return; // 若页面仍停留在原课程但链接已触发根路由,立即停止等待, // 避免继续操作首页上的同名元素。 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 (generation !== exerciseGeneration) return; 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; completionLessonKey = getLessonKey(); const title = getLessonTitle(); log.info(`正在学习: ${title}`); updatePanelStatus(`▶️ ${title}`); } // 获取当前课程名称 function getLessonTitle() { if (isAICourseClass()) { const active = getActiveLesson(); if (active) { const title = active.querySelector('.title-text, [title], [class*="title"], [class*="name"]'); const text = title?.getAttribute('title') || title?.textContent || active.textContent; if (text && text.trim()) return text.replace(/\s+/g, ' ').trim().slice(0, 120); } const heading = document.querySelector('.study-main h1, .knowledge-title, h1, h2'); return heading?.textContent?.trim() || '未知课程'; } 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() { exerciseGeneration++; 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.4.1
播放控制
⏩ 倍速
${config.playbackSpeed}x
🔇 静音
自动化
▶️ 自动播放
⏭️ 自动下一集
💬 跳过弹题
🏆 习惯分
运行中... --
`; document.body.appendChild(panel); panelStatusEl = $('#av-status'); panelProgressEl = $('#av-progress'); // 收起/展开 $('#av-collapse').addEventListener('click', () => { panel.classList.toggle('collapsed'); const collapsed = panel.classList.contains('collapsed'); $('#av-collapse').textContent = collapsed ? '▶' : '▼'; $('#av-collapse').setAttribute('aria-expanded', String(!collapsed)); $('#av-collapse').setAttribute('aria-label', collapsed ? '展开面板' : '收起面板'); keepPanelInViewport(panel); }); // 倍速控制 $('#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'), { constrainToViewport: true }); const onResize = () => { if (!panel.isConnected) { window.removeEventListener('resize', onResize); sizeObserver?.disconnect(); return; } keepPanelInViewport(panel); }; const sizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(onResize) : null; sizeObserver?.observe(panel); window.addEventListener('resize', onResize); } function keepPanelInViewport(el) { const rect = el.getBoundingClientRect(); const width = document.documentElement.clientWidth || window.innerWidth; const height = document.documentElement.clientHeight || window.innerHeight; const left = Math.max(12, Math.min(rect.left, width - rect.width - 12)); const top = Math.max(12, Math.min(rect.top, height - rect.height - 12)); if (Math.abs(left - rect.left) > 0.5 || Math.abs(top - rect.top) > 0.5) { el.style.left = left + 'px'; el.style.top = top + 'px'; el.style.right = 'auto'; } } function makeDraggable(el, handle, { constrainToViewport = false } = {}) { let offsetX = 0, offsetY = 0, isDragging = false; handle.addEventListener('mousedown', (e) => { if (e.button !== 0 || e.target.closest('button, a, input, select, textarea')) return; 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'; if (constrainToViewport) keepPanelInViewport(el); }); document.addEventListener('mouseup', () => { isDragging = false; }); } function updatePanelStatus(text) { if (panelStatusEl) panelStatusEl.textContent = text; } function updatePanelProgress(text) { if (panelProgressEl) panelProgressEl.textContent = text; } // ===================== 考试答题模块 ===================== // 普通课程仍保留原有手动搜题入口;AI 课程使用下方 OpenAI 兼容接口 // 直接作答,不接入第三方题库,也不上传用户身份信息。 const AI_PROVIDERS = { zhipu: { name: '智谱 GLM-4-Flash', model: 'glm-4-flash', url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', desc: '智谱 AI,中文题目适配较好' }, doubao: { name: '豆包 Doubao-lite', model: 'doubao-lite-32k', url: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions', desc: '火山引擎豆包模型(模型名可按控制台配置修改)' }, deepseek: { name: 'DeepSeek Flash(V4.1)', model: 'deepseek-flash', url: 'https://api.deepseek.com/chat/completions', thinking: 'disabled', desc: 'DeepSeek 官方 Flash 模型' }, deepseek_v4: { name: 'DeepSeek V4 Pro', model: 'deepseek-v4-pro', url: 'https://api.deepseek.com/chat/completions', thinking: 'disabled', desc: 'DeepSeek 官方 Pro 模型' } }; function readSharedAiConfig() { let raw = null; try { raw = GM_getValue('cx_answer_config', null); } catch (_) {} if (typeof raw === 'string') { try { raw = JSON.parse(raw); } catch (_) { raw = null; } } const shared = raw && typeof raw === 'object' ? raw : {}; // 雨课堂使用 localStorage 保存同一组 AI 配置;跨脚本复用时只读取 // 接口、模型和 Key,不读取任何题库/身份数据。 try { const provider = localStorage.getItem('ykt_ai_provider'); const apiKey = localStorage.getItem('ykt_ai_api_key'); const customUrl = localStorage.getItem('ykt_ai_custom_url'); const customModel = localStorage.getItem('ykt_ai_custom_model'); if (provider || apiKey || customUrl || customModel) { return Object.assign(shared, { aiProvider: provider || shared.aiProvider || '', aiApiKey: apiKey || shared.aiApiKey || '', aiCustomUrl: customUrl || shared.aiCustomUrl || '', aiCustomModel: customModel || shared.aiCustomModel || '' }); } } catch (_) {} return shared; } function loadExamAiConfig() { const shared = readSharedAiConfig(); let own = {}; try { own = GM_getValue('av_ai_config', {}) || {}; } catch (_) {} if (typeof own === 'string') { try { own = JSON.parse(own); } catch (_) { own = {}; } } const rawProvider = String(own.provider || shared.aiProvider || (isAiExamHost() ? 'deepseek' : 'zhipu')); const provider = (AI_PROVIDERS[rawProvider] || rawProvider === 'custom') ? rawProvider : 'deepseek'; return { provider, apiKey: String(own.apiKey || shared.aiApiKey || '').trim(), customUrl: String(own.customUrl || shared.aiCustomUrl || '').trim(), customModel: String(own.customModel || shared.aiCustomModel || '').trim(), model: String(own.model || '').trim() }; } function saveExamAiConfig(config) { const clean = { provider: String(config.provider || 'deepseek'), apiKey: String(config.apiKey || '').trim(), customUrl: String(config.customUrl || '').trim(), customModel: String(config.customModel || '').trim(), model: String(config.model || '').trim() }; try { GM_setValue('av_ai_config', clean); } catch (_) {} // 与学习通/雨课堂共用 API Key 和自定义接口配置,用户只需填写一次。 const shared = readSharedAiConfig(); shared.aiProvider = clean.provider; shared.aiApiKey = clean.apiKey; shared.aiCustomUrl = clean.customUrl; shared.aiCustomModel = clean.customModel; try { GM_setValue('cx_answer_config', JSON.stringify(shared)); } catch (_) {} try { localStorage.setItem('ykt_ai_provider', clean.provider); localStorage.setItem('ykt_ai_api_key', clean.apiKey); localStorage.setItem('ykt_ai_custom_url', clean.customUrl); localStorage.setItem('ykt_ai_custom_model', clean.customModel); } catch (_) {} return clean; } function getAiEndpoint(ai) { if (ai.provider !== 'custom') return AI_PROVIDERS[ai.provider]?.url || AI_PROVIDERS.deepseek.url; let raw = String(ai.customUrl || '').trim(); if (!raw) return ''; // 允许用户直接填写完整端点,并兼容末尾斜杠或查询参数。 if (/\/chat\/completions\/?(?:\?|$)/i.test(raw)) return raw.replace(/\/$/, ''); raw = raw.replace(/\/+$/, ''); return /\/v1$/i.test(raw) ? `${raw}/chat/completions` : `${raw}/v1/chat/completions`; } function getAiModel(ai) { if (ai.provider === 'custom') return String(ai.customModel || '').trim(); return String(ai.model || AI_PROVIDERS[ai.provider]?.model || '').trim(); } function parseAiResponseText(json) { const choice = json && Array.isArray(json.choices) ? json.choices[0] : null; const finishReason = String(choice?.finish_reason || '').toLowerCase(); if (['length', 'content_filter', 'tool_calls', 'aborted'].includes(finishReason)) { return ''; } let content = choice && choice.message ? choice.message.content : (choice && choice.text); if (Array.isArray(content)) content = content.map(x => typeof x === 'string' ? x : (x && x.text || '')).join(''); return String(content || '').trim(); } function parseAiAnswers(content, q) { let text = String(content || '').trim(); if (!text) return []; const fenced = text.match(/```(?:json|text)?\s*([\s\S]*?)```/i); if (fenced) text = fenced[1].trim(); try { const obj = JSON.parse(text); if (obj && typeof obj === 'object') text = String(obj.answer ?? obj.answers ?? obj.result ?? obj.output ?? '').trim(); } catch (_) {} text = text.replace(/^\s*(?:正确答案|最终答案|答案|答复|答)\s*[::]\s*/i, '').trim(); if (!text) return []; // 选择题优先解析 A、B、C 等标签,兼容 “A,C”/“A||C”/“选 A 和 C”。 if (q && (q.type === 0 || q.type === 1 || q.type === 3)) { const labels = text.match(/\b[A-H](?:\s*(?:[,,、|/&和与及]\s*)[A-H]){0,7}\b/i); if (labels && labels[0].replace(/[^A-H]/ig, '').length >= 1 && (/[答案选项选择]/i.test(content) || /^[A-H](?:\s*[,,、|/&和与及]\s*[A-H])*\s*$/i.test(text))) { const letters = labels[0].match(/[A-H]/ig) || []; const unique = [...new Set(letters.map(x => x.toUpperCase()))]; return q.type === 0 || q.type === 3 ? unique.slice(0, 1) : unique; } } // 主观题/填空答案可能自然包含逗号,只有显式 || 才表示多个空。 if (q && q.type >= 2) { return text.split(/\s*\|\|\s*/).map(x => x.trim()).filter(Boolean); } const pieces = text.split(/\s*(?:\|\||[,,、;;]|\s+和\s+|\s+与\s+)\s*/).map(x => x.trim()).filter(Boolean); return pieces.length ? pieces : [text]; } async function askAiAPI(question, options, type) { const ai = loadExamAiConfig(); const endpoint = getAiEndpoint(ai); const model = getAiModel(ai); if (!endpoint || !ai.apiKey || !model) { log.warn('[考试] AI 配置不完整,请在面板填写接口和 API Key'); return null; } const typeName = TYPE_NAMES[type] || '未知题型'; const optionLines = (options || []).map((o, i) => `${String.fromCharCode(65 + i)}. ${o}`).join('\n'); const prompt = [ '你是学习辅助答题助手。根据题干和选项给出最可能正确的答案。', '只输出 JSON,不要解释:{"answer":"A"};多选用逗号分隔(如 A,C);填空/简答直接写答案文本。', `题型:${typeName}`, `题干:${formatString(question)}`, optionLines ? `选项:\n${optionLines}` : '该题没有显式选项。' ].join('\n'); const payload = { model, temperature: 0.1, max_tokens: type >= 4 ? 800 : 256, stream: false, messages: [{ role: 'system', content: '只返回可解析的 JSON,不要输出 Markdown 或解释。' }, { role: 'user', content: prompt }] }; if (AI_PROVIDERS[ai.provider]?.thinking) payload.thinking = { type: AI_PROVIDERS[ai.provider].thinking }; const res = await gmRequest({ method: 'POST', url: endpoint, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${ai.apiKey}` }, data: JSON.stringify(payload), timeout: 60000 }); if (!res || res.status < 200 || res.status >= 300) { log.warn(`[考试] AI 请求失败${res ? ` (HTTP ${res.status})` : ''}`); return null; } try { const json = JSON.parse(res.responseText || '{}'); const content = parseAiResponseText(json); const answers = parseAiAnswers(content, { type }); return answers.length ? { answers, remaining: -1, source: `AI·${AI_PROVIDERS[ai.provider]?.name || '自定义接口'}` } : null; } catch (e) { log.warn('[考试] AI 响应解析失败: ' + e.message); return null; } } // --- 设备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 = [ { // 智慧树 AI 课程习题/考试(questionContent) root: '.questionContent', question: '.questionTitle .centent-pre, .questionTitle, .centent-pre', options: '.el-radio-group > label.el-radio .preStyle, .el-checkbox-group > label.el-checkbox .preStyle, ul.radio-view > li .preStyle, ul.checkbox-view > li .preStyle, .option-list li .preStyle', $options: '.el-radio-group > label.el-radio, .el-checkbox-group > label.el-checkbox, ul.radio-view > li, ul.checkbox-view > li, .option-list li', type: '.questionTitle, .letterSortNum', checked: '.is-checked, .is-checked input:checked, input:checked', }, { // 智慧树作业/考试 (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); let typeCode = typeEl ? getQuestionType(typeEl.textContent) : (activeLayout.root === '.questionContent' ? (container.querySelector('.el-checkbox-group, ul.checkbox-view') ? 1 : 0) : 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; if (activeLayout.root === '.questionContent' && typeCode >= 4 && useClickEls.length) { typeCode = container.querySelector('.el-checkbox-group, ul.checkbox-view') ? 1 : 0; } 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 = isAiExamHost() ? 'ai' : GM_getValue('av_exam_mode', 'free'); if (!['free', 'paid', 'ai'].includes(examMode)) examMode = isAiExamHost() ? 'ai' : '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 === 'ai') { return askAiAPI(cleanQ, cleanOpts, type); } 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 aiTab = $('#ae-mode-ai'); const freeArea = $('#ae-area-free'); const paidArea = $('#ae-area-paid'); const aiArea = $('#ae-area-ai'); [freeTab, paidTab, aiTab].forEach(tab => { if (tab) { tab.style.background = 'transparent'; tab.style.color = '#94a3b8'; } }); [freeArea, paidArea, aiArea].forEach(area => { if (area) area.style.display = 'none'; }); if (mode === 'ai') { aiTab.style.background = 'rgba(167,139,250,0.16)'; aiTab.style.color = '#c4b5fd'; aiArea.style.display = ''; setExamMode('ai'); } else 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')); $('#ae-mode-ai').addEventListener('click', () => switchModeUI('ai')); // 恢复上次模式 switchModeUI(examMode); // --- AI 配置 --- const aiCfg = loadExamAiConfig(); const aiProviderEl = $('#ae-ai-provider'); const aiKeyEl = $('#ae-ai-key'); const aiUrlEl = $('#ae-ai-url'); const aiModelEl = $('#ae-ai-model'); const aiCustomRow = $('#ae-ai-custom-row'); const aiModelRow = $('#ae-ai-model-row'); const aiHint = $('#ae-ai-hint'); const syncAiConfigUI = () => { const provider = aiProviderEl.value; const custom = provider === 'custom'; aiCustomRow.style.display = custom ? '' : 'none'; aiModelRow.style.display = custom ? '' : 'none'; const info = custom ? { desc: 'OpenAI 兼容接口,填写 Base URL、模型名称和 API Key。' } : AI_PROVIDERS[provider]; aiHint.textContent = info ? info.desc : ''; }; aiProviderEl.value = ['deepseek', 'deepseek_v4', 'doubao', 'zhipu', 'custom'].includes(aiCfg.provider) ? aiCfg.provider : 'deepseek'; aiKeyEl.value = aiCfg.apiKey; aiUrlEl.value = aiCfg.customUrl; aiModelEl.value = aiCfg.customModel; aiProviderEl.addEventListener('change', syncAiConfigUI); syncAiConfigUI(); $('#ae-ai-save').addEventListener('click', () => { const provider = aiProviderEl.value; if (!aiKeyEl.value.trim()) { $('#ae-ai-msg').textContent = '请填写 API Key'; $('#ae-ai-msg').style.color = '#f87171'; return; } if (provider === 'custom' && (!aiUrlEl.value.trim() || !aiModelEl.value.trim())) { $('#ae-ai-msg').textContent = '自定义接口需要填写地址和模型名称'; $('#ae-ai-msg').style.color = '#f87171'; return; } saveExamAiConfig({ provider, apiKey: aiKeyEl.value, customUrl: aiUrlEl.value, customModel: aiModelEl.value }); $('#ae-ai-msg').textContent = '✅ AI 配置已保存'; $('#ae-ai-msg').style.color = '#34d399'; }); // --- 免费验证码 --- // 恢复验证状态 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); // 练习组件可能在首次路由进入后才挂载;到面板创建前再判断一次, // 让 AI 课程的 singleCourse/knowledgeStudy 习题自动进入 AI 模式。 if (isAiExamHost()) { examMode = 'ai'; GM_setValue('av_exam_mode', 'ai'); } 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 { var parsedUrl = new URL(url, 'https://qsy.iano.cn/'); if (/^https?:$/.test(parsedUrl.protocol)) noticeImages.push(parsedUrl.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 link = document.createElement('a'); link.className = 'av-notice-image-link'; link.href = url; link.target = '_blank'; link.rel = 'noopener noreferrer'; link.title = '点击查看原图'; link.setAttribute('aria-label', '查看公告原图'); var img = document.createElement('img'); img.src = url; img.alt = '公告图片'; img.loading = 'lazy'; img.decoding = 'async'; img.referrerPolicy = 'no-referrer'; img.addEventListener('error', function() { link.style.display = 'none'; var tip = document.createElement('div'); tip.className = 'av-notice-image-fallback'; tip.textContent = '公告图片加载失败'; imageBox.appendChild(tip); }, { once: true }); link.appendChild(img); imageBox.appendChild(link); }); body.appendChild(imageBox); var imageHint = document.createElement('div'); imageHint.className = 'av-notice-image-hint'; imageHint.textContent = '点击图片查看原图'; body.appendChild(imageHint); } 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); } // AI 课程的习题组件由 SPA 异步挂载。路由刚进入时可能只有空壳, // 不能仅等待一次就落入学习面板,否则题目稍后出现时不会再创建答题面板。 if (isAICourseClass() && /\/(?:singleCourse\/knowledgeStudy|learnPage)\//i.test(location.pathname) && !document.querySelector('.questionContent, .questionTitle, .exam-preview, [class*="question-content"]')) { const questionDeadline = Date.now() + 8000; while (Date.now() < questionDeadline && !isExamPage() && !getVideo()) { await sleep(400); } } // 考试页走考试流程。AI 课程习题组件可能晚于首屏挂载,再补一次检测, // 避免把没有 video 的习题页误当成普通学习页。 let examPage = isExamPage(); if (examPage) { await examMain(); return; } if (!isStudyPage()) { log.info('当前页面非学习页,脚本待命中'); createPanel(); fetchAnnouncement(); updatePanelStatus('⏳ 等待进入学习/考试页面...'); return; } log.info('智慧树刷课助手启动!'); log.info(`页面类型: ${isAICourseClass() ? '智慧树 AI 课程' : 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(checkLessonProgress, 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(); })();