// ==UserScript== // @name 智慧树刷课助手1.4.5 // @namespace https://github.com/CXRunfree/Autovisor // @version 1.4.5 // @description 智慧树普通课程与 AI 课程自动播放、完成后切换下一资源、助教随堂题支持 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.5 (2026-10-17):公告请求按面板合并,替换显示并保留关闭状态,忽略已销毁面板的旧响应。 // v1.4.4 (2026-10-17):适配无标准弹窗类名的 AI 随堂练习及自定义选项; // 题后仅使用明确的继续学习操作,防止通用关闭图标/表单默认提交导致退出; // 当前视频未结束不按历史 100% 切课,安全提示出现后停止任务,停用合成鼠标事件。 // v1.4.3 (2026-10-15):助教随堂题可独立选择 AI、免费题库或付费题库; // 学习面板补齐验证码、付费激活及余额配置,不自动切换来源; // 请求期间持续校验免费有效期,同题重绘复用查询结果,换题/切换来源/停止取消旧流程。 // v1.4.2 (2026-10-15):保留平台根节点和异步弹层,避免白屏及误触退出; // AI 课程按实际播放结束切课,防旧视频状态串课;助教题接入 AI 并确认提交。 // 新增学习页 AI 配置入口,修复异步换题、SPA 切页和停止/重启生命周期。 /* * ========================================== * 免责声明 / 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, // 自动处理随堂弹题(AI 助教使用用户配置) autoPlay: 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(/\/+$/, '') || '/'; const hashPath = target.hash.replace(/^#/, '').split('?')[0].replace(/\/+$/, '') || '/'; return (path === '/' || path === '/index.html' || path === '/home') && (!target.hash || /^(?:\/|\/home|\/index|\/index.html)$/.test(hashPath)); } catch (e) { return false; } } function triggerLessonClick(item) { if (!item || !item.isConnected) return false; const clickTarget = isAICourseClass() ? (item.closest?.('a,button,[role="button"]') || item) : item; try { clickTarget.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' }); } catch (_) {} const anchor = isAICourseClass() ? clickTarget.closest?.('a[href]') : (clickTarget.matches?.('a[href]') ? clickTarget : clickTarget.querySelector?.('a[href]')); const href = anchor?.getAttribute('href') || anchor?.href || ''; // AI 资源卡里的首页/退出链接不能作为下一资源入口。取消默认事件 // 无法取消 Vue 的 router.push,必须在触发监听器之前拒绝这种链接。 if (isAICourseClass() && anchor && href && href !== '#') { try { const target = new URL(href, location.href); if (!/^https?:$/.test(target.protocol) || target.origin !== location.origin || isRootNavigationHref(target.href) || /\/(?:login|logout|passport)(?:\/|$)/i.test(target.pathname)) return false; } catch (_) { return false; } } 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 === 'ai.zhihuishu.com' && /^\/AIstudent(?:\/|$)/i.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); } }); }); // 同一张卡片的外层和 data-resource-* 子节点可能同时命中。 // 保持页面顺序并只保留外层卡片,避免一次切换被当成两课。 return items.filter(item => !items.some(other => other !== item && other.contains(item))) .sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1); } 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.filter(item => !items.some(other => other !== item && item.contains(other))) .sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1); } function getAICourseItems() { const resources = getAICourseResourceItems(); if (resources.length) return resources; return getAICoursePointItems(); } function getAICourseItemProgress(item) { if (!item) return 0; const studyStatus = item.getAttribute('data-study-status'); const status = studyStatus || item.getAttribute('data-status') || item.getAttribute('data-learn-status'); if (studyStatus === '1' || /^(done|finished|completed|已学|已完成)$/i.test(String(status || '').trim())) return 100; // schedule 可能是已观看秒数,通用 data-status=1 也可能仅代表可用状态。 const attrNames = ['data-progress', 'data-percent', 'aria-valuenow']; for (const name of attrNames) { const raw = item.getAttribute(name); if (raw !== null && /^\s*\d+(?:\.\d+)?\s*%?\s*$/.test(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; if (/^\s*\d+(?:\.\d+)?\s*%?\s*$/.test(raw)) return parseProgress(raw); } const statusNodes = [item, ...item.querySelectorAll('[class*="status"], [class*="progress"], .tag, .el-tag')]; if (statusNodes.some(node => /^(?:已学|已完成|学习完成|完成学习|100%)$/.test((node.textContent || '').trim()))) return 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"]', ]; const marked = items.filter(item => activeSelectors.some(selector => item.matches(selector)) || activeSelectors.slice(4).some(selector => item.querySelector(selector))); if (marked.length === 1) return marked[0]; // 多个目录都留有 active 时不能默认采用第一张旧卡片。 if (marked.length > 1) { const visible = marked.filter(isElementVisible); return visible.length === 1 ? visible[0] : null; } const video = Array.from($$('video')).find(isElementVisible); 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 null; } 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') && hasStandaloneExamQuestion('.examPaper_subject, .questionBox, .ques-detail')) || (aiExamHost && hasStandaloneExamQuestion('.questionContent .el-radio-group, .questionContent .el-checkbox-group, .questionContent ul.radio-view, .questionContent ul.checkbox-view')); } function hasStandaloneExamQuestion(selector) { return Array.from(document.querySelectorAll(selector)).some(node => isElementVisible(node) && !node.closest('.el-dialog, .el-message-box, [role="dialog"], .dialog-test, .ai-class-exercise-dialog') && !getStudyExerciseDialog()?.contains(node)); } 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) && hasStandaloneExamQuestion('.questionContent, .questionTitle, .exam-preview, [class*="question-content"]'); } // ===================== 平台提示保护 ===================== // 平台根节点和弹层都由框架管理,不能根据后代文字删除/隐藏它们。 // 错误提示的确认/关闭回调也可能导航到首页,因此只暂停自动操作。 const PLATFORM_SAFETY_PATTERN = /异常脚本|关闭插件|异常行为|安全验证|身份验证/; const PLATFORM_SAFETY_MESSAGE_PATTERN = /检测到(?:异常脚本|异常行为)|(?:请|先|需要|手动).{0,12}关闭插件|(?:请|需要|进行|完成).{0,12}(?:安全验证|身份验证)/; function getBlockingPlatformDialog() { const dialogs = Array.from(document.querySelectorAll('.el-dialog, .el-message-box, [role="dialog"], .ant-modal')); const exercise = getStudyExerciseDialog(); // 旧版播放器的提示也可能是自定义 div;仅以明确安全提示的短文本补充, // 不把包含整页内容的应用根节点当作弹窗,也不读取助手面板自身的状态。 document.querySelectorAll('p, div, span').forEach(node => { if (node.closest('#autovisor-panel, #av-exam-panel, .questionContent, .examPaper_subject, .questionBox, .ques-detail') || exercise?.contains(node)) return; const text = (node.textContent || '').replace(/\s+/g, ''); if (text.length < 200 && PLATFORM_SAFETY_MESSAGE_PATTERN.test(text) && isElementVisible(node)) dialogs.push(node); }); const blocking = dialogs .filter(dialog => { if (!isElementVisible(dialog) || isStudyExerciseDialog(dialog)) return false; const text = dialogTextWithoutControls(dialog); const fullText = (dialog.textContent || '').replace(/\s+/g, ''); return !text || /异常脚本|关闭插件|异常行为|登录|重新登录|网络(?:错误|异常)|加载失败|播放失败|操作失败|访问异常|安全验证|身份验证/.test(fullText); }); // 多层弹窗并存时,安全提示优先于尚在加载的空白提示。 return blocking.find(dialog => PLATFORM_SAFETY_PATTERN.test((dialog.textContent || '').replace(/\s+/g, ''))) || blocking[0] || null; } function hasBlockingPlatformDialog() { return !!getBlockingPlatformDialog(); } let lastPlatformDialog = null; let platformSafetyStopped = false; function checkPlatformDialogs() { const dialog = getBlockingPlatformDialog(); if (platformSafetyStopped) return true; if (!dialog) { lastPlatformDialog = null; return false; } completionSince = 0; if (PLATFORM_SAFETY_PATTERN.test((dialog.textContent || '').replace(/\s+/g, ''))) { platformSafetyStopped = true; stopAllTasks(); pauseForVerification(); log.warn('平台安全提示已出现,自动任务已停止;请按平台提示处理后手动重启'); updatePanelStatus('⏹ 平台安全提示,已停止;处理后手动重启'); return true; } if (dialog !== lastPlatformDialog) { lastPlatformDialog = dialog; log.warn('平台提示尚未处理,已暂停自动操作并保留原页面'); updatePanelStatus('⏸ 请先处理页面提示'); } return true; } // ===================== 核心功能模块 ===================== // --- 获取视频元素 --- 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 studyPlaybackBinding = null; const appliedPlaybackSpeeds = new WeakMap(); 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') || (isAICourseClass() ? item.getAttribute('data-uid') : ''); if (explicit) return explicit; if (isAICourseClass()) { const nestedId = item.querySelector('[data-resources-uid], [data-resource-uid], [data-knowledge-id], [data-point-id], [data-uid]'); if (nestedId) { const key = ['data-resources-uid', 'data-resource-uid', 'data-knowledge-id', 'data-point-id', 'data-uid'] .map(name => nestedId.getAttribute(name)).find(Boolean); if (key) return key; } // 百分比、已学时长会动态改变,不能并入课时身份导致播放器失去绑定。 const heading = item.querySelector('.title-text, [title], [class*="title"], [class*="name"]'); const title = heading?.getAttribute('title') || heading?.textContent; if (title?.trim()) return title.replace(/\s+/g, ' ').trim().slice(0, 120); } 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(canContinue = () => true) { await sleep(2000); if (!canContinue()) return; // 与定时清理共用规则,不能把随堂弹题的关闭图标当成普通提示。 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'); // AI 助教及消息容器可能承载题目,保留平台组件及其事件状态。 } 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) { video.pause(); } } // --- 静音控制 --- function setMute() { if (!config.soundOff || platformSafetyStopped) 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 || platformSafetyStopped || hasBlockingStudyDialog()) return; const speed = Math.max(0.5, Math.min(config.playbackSpeed, 1.8)); const source = video.currentSrc || video.src || ''; const applied = appliedPlaybackSpeeds.get(video); // 同一视频/同一配置仅尝试一次。平台若主动恢复倍速,尊重播放器状态, // 不再每五秒与平台反复争抢 playbackRate。 if (applied?.speed === speed && applied.source === source) return; appliedPlaybackSpeeds.set(video, { speed, source }); 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 || platformSafetyStopped) 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; const aiExerciseAttempts = new WeakMap(); const aiExerciseTransitions = new WeakMap(); const studyExerciseAnswerCache = new WeakMap(); function getStudyAnswerMode() { const mode = GM_getValue('av_study_answer_mode', 'ai'); return ['ai', 'free', 'paid'].includes(mode) ? mode : 'ai'; } function invalidateStudyExerciseAnswer() { exerciseGeneration++; const dialog = getStudyExerciseDialog(); if (dialog) { aiExerciseAttempts.delete(dialog); studyExerciseAnswerCache.delete(dialog); } } function setStudyAnswerMode(mode) { if (!['ai', 'free', 'paid'].includes(mode)) return false; if (mode !== getStudyAnswerMode()) { GM_setValue('av_study_answer_mode', mode); invalidateStudyExerciseAnswer(); } return true; } function isAiStudyExerciseDialog(dialog) { if (!dialog) return false; const heading = dialog.querySelector('.el-dialog__title, .dialog-title, .modal-title, h1, h2, h3'); const title = `${heading?.textContent || ''} ${(dialog.textContent || '').slice(0, 180)}`.replace(/\s+/g, ''); return dialog.matches('.ai-class-exercise-dialog') || /AI助教.{0,30}(?:出题|随堂|练习)|AI随堂(?:练习|测试)/i.test(title) || (isAICourseClass() && !!dialog.querySelector('.questionContent .el-radio-group, .questionContent .el-checkbox-group')); } function isExerciseMetaText(text) { const value = formatString(text || '').replace(/\s+/g, '').replace(/^\d+[.、.]?/, ''); return !value || /AI助教|AI随堂(?:练习|测试)|以下所有内容均由AI生成|请注意甄别|学习过程中有问题|随时提问/.test(value) || /^(?:[((【\[]?(?:单选题?|多选题?|判断题?|不定项选择题?)[))】\]]?)$/.test(value); } function getStudyExerciseCandidates() { const selectors = [ '.dialog-test', '.ai-class-exercise-dialog', '.el-dialog', '.el-message-box', '[role="dialog"]', '.ant-modal', '[class*="exercise"], [class*="practice"], [class*="question-modal"], [class*="dialog"], [class*="modal"], [class*="popup"]', ]; const seen = new Set(); const result = []; selectors.forEach(selector => document.querySelectorAll(selector).forEach(node => { if (!seen.has(node)) { seen.add(node); result.push(node); } })); // 某些版本的弹层没有 role 或固定类名。从准确的标题往上寻找 // 同时容纳选项及操作按钮的最小容器,不能直接返回应用根节点。 document.querySelectorAll('h1, h2, h3, h4, p, div, span').forEach(title => { if (title.children.length || !/^(?:AI随堂(?:练习|测试)|AI助教.{0,12}(?:出题|随堂练习))$/i.test((title.textContent || '').replace(/\s+/g, '')) || !isElementVisible(title)) return; let parent = title.parentElement; for (let depth = 0; parent && parent !== document.body && depth < 6; depth++, parent = parent.parentElement) { if (getAiExerciseChoices(parent).length < 2 || !findExerciseButton(parent, /^(?:提交(?:答案|作答)?|继续学习|继续播放|关闭练习)$/)) continue; if (!seen.has(parent)) { seen.add(parent); result.unshift(parent); } break; } }); return result; } function getAiExerciseChoices(dialog) { const selectors = [ '.topic-item', 'label.el-radio, label.el-checkbox, label.ant-radio-wrapper, label.ant-checkbox-wrapper', '[role="radio"], [role="checkbox"]', '.questionContent ul.radio-view > li, .questionContent ul.checkbox-view > li', '.option-item, .options-item, .answer-item, .choice-item, .radio-item, .checkbox-item', '.topic-list > li, .option-list > li, .options-list > li', '.option', 'label:has(input[type="radio"]), label:has(input[type="checkbox"])', ]; for (const selector of selectors) { const choices = Array.from(dialog.querySelectorAll(selector)).filter(isElementVisible) .filter(node => !node.closest('.answer-analysis, .analysis, .answer-result')); if (choices.length >= 2) return choices.filter(node => !choices.some(other => other !== node && other.contains(node))); } // 新版随堂练习使用普通 div,A/B 是选项前缀,原生 input 可能被隐藏。 const inputs = Array.from(dialog.querySelectorAll('input[type="radio"], input[type="checkbox"]')); const labelled = inputs.map(input => input.labels?.[0] || input.closest('label') || input.parentElement) .filter(node => node && isElementVisible(node) && node.textContent.trim()); if (labelled.length >= 2 && new Set(labelled).size === labelled.length) return labelled; const fallback = Array.from(dialog.querySelectorAll('li, label, div, [role="button"]')).filter(node => isElementVisible(node) && !node.closest('.answer-analysis, .analysis, .answer-result') && /^[A-H](?:[..、::))\s]+|(?=正确|错误|是|否|对|错))\S/.test((node.textContent || '').trim())); const choices = fallback.filter(node => !fallback.some(other => other !== node && node.contains(other))); const group = choices[0]?.parentElement; return choices.filter(node => node.parentElement === group); } function readAiExerciseQuestion(dialog) { const choices = getAiExerciseChoices(dialog); if (choices.length < 2) return null; const cleanOption = value => formatString(value).replace(/^\s*[A-H](?:[..、::))\s]+|(?=正确|错误|是|否|对|错))\s*/i, '').trim(); const options = choices.map(choice => cleanOption(choice.textContent || choice.getAttribute('aria-label') || (choice.labels && Array.from(choice.labels).map(label => label.textContent).join(' ')) || choice.value || '')); if (options.some(option => !option)) return null; const candidates = Array.from(dialog.querySelectorAll('.topic-title, .question-title, .questionTitle .centent-pre, .questionTitle, .question-stem, .stem, .subject-title, [class*="question"], [class*="subject"]')); const usableText = node => { if (!isElementVisible(node) || choices.some(choice => choice === node || choice.contains(node) || node.contains(choice))) return ''; if (node.closest('.el-dialog__header, .el-dialog__footer, .answer, .analysis, .answer-analysis, .answer-result, button, [role="button"]')) return ''; const text = formatString(node.textContent || '').trim(); if (text.length < 4 || isExerciseMetaText(text)) return ''; return text; }; let question = candidates.map(usableText).filter(Boolean).sort((a, b) => b.length - a.length)[0] || ''; if (!question) { const fallback = Array.from(dialog.querySelectorAll('p, div, span')); question = fallback.map(usableText).filter(Boolean).sort((a, b) => b.length - a.length)[0] || ''; } if (!question) return null; const text = (dialog.textContent || '').replace(/\s+/g, ''); const type = /多选|多项选择|不定项/.test(text) || choices.some(choice => choice.matches('.el-checkbox, [role="checkbox"], input[type="checkbox"]') || choice.querySelector('input[type="checkbox"]')) ? 1 : /判断题|是非题/.test(text) ? 3 : 0; const current = dialog.querySelector('.number.active, .number.current, .number.is-active, .number[aria-selected="true"]'); const contentKey = JSON.stringify([question, options, type]); const key = JSON.stringify([current?.textContent || '', contentKey]); return { question, options, choices, type, key, contentKey }; } function matchAiExerciseAnswers(question, answers) { const normalize = text => formatString(String(text || '')).replace(/\s+/g, '').toLowerCase(); const truth = text => /^(正确|对|是|√|true|t)$/i.test(normalize(text)) ? true : /^(错误|错|否|×|false|f)$/i.test(normalize(text)) ? false : null; const indices = new Set(); const normalizedAnswers = (Array.isArray(answers) ? answers : answers == null ? [] : [answers]).flatMap(answer => { const value = String(answer ?? '').trim(); if (question.options.some(option => normalize(option) === normalize(value))) return [value]; if (question.type === 1 && /^(?:[A-H]{2,8}|[A-H](?:\s*[,,、;;|/\s]\s*[A-H])+)$/.test(value.toUpperCase())) { return value.toUpperCase().match(/[A-H]/g) || []; } if (question.type === 1 && value.includes('||')) return value.split('||').map(part => part.trim()); return [value]; }); for (const answer of normalizedAnswers) { const value = String(answer || '').trim(); let index = -1; if (/^[A-H]$/i.test(value)) index = value.toUpperCase().charCodeAt(0) - 65; else { const matching = question.options.map((option, i) => normalize(option) === normalize(value) || (question.type === 3 && truth(value) !== null && truth(option) === truth(value)) ? i : -1).filter(i => i >= 0); if (matching.length === 1) index = matching[0]; } if (index < 0 || index >= question.choices.length) return null; indices.add(index); } if (!indices.size || (question.type !== 1 && indices.size !== 1)) return null; return indices; } async function answerAiStudyExercise(dialog, canContinue) { if (hasExerciseAnswer(dialog)) return true; const question = readAiExerciseQuestion(dialog); if (!question) { updatePanelStatus('⏳ 等待 AI 助教题目和选项加载'); return false; } const mode = getStudyAnswerMode(); const sourceName = { ai: 'AI', free: '免费题库', paid: '付费题库' }[mode]; const canAnswer = () => canContinue() && getStudyAnswerMode() === mode && (mode !== 'free' || isVerifyValid()); if (mode === 'free' && !isVerifyValid()) { updatePanelStatus('⏸ 请在助教答题设置中验证免费题库验证码'); return false; } if (!canAnswer()) return false; const previous = aiExerciseAttempts.get(dialog); if (previous?.key === question.key && previous.mode === mode && Date.now() < previous.retryAt) return false; const sameQuestionContent = () => canAnswer() && dialog.isConnected && isElementVisible(dialog) && readAiExerciseQuestion(dialog)?.key === question.key; const sameQuestion = () => sameQuestionContent() && question.choices.every(choice => choice.isConnected && dialog.contains(choice)); const confirmedOriginal = () => { if (!canAnswer() || !dialog.isConnected || !isElementVisible(dialog) || !hasExerciseAnswer(dialog)) return false; const current = readAiExerciseQuestion(dialog); return !current || current.key === question.key; }; aiExerciseAttempts.set(dialog, { key: question.key, mode, retryAt: Date.now() + 20000 }); updatePanelStatus(`正在使用${sourceName}回答助教随堂题`); const cached = studyExerciseAnswerCache.get(dialog); const reusable = cached && cached.key === question.key && cached.mode === mode && cached.route === location.href; const result = reusable ? cached.result : await searchAnswer(question.question, question.options, question.type, mode); // 提交未确认时复用本题答案;付费查询即便响应异常也不自动重复扣费。 // 同题重绘只取消旧节点点击,仍保留结果;换题、停止或切换来源不缓存旧请求。 if (!reusable && sameQuestionContent() && (mode === 'paid' || result?.answers?.length)) { studyExerciseAnswerCache.set(dialog, { key: question.key, mode, route: location.href, result }); } if (!sameQuestion()) return confirmedOriginal(); // 用户在查询期间自行提交时,以平台结果为准,不再覆盖答案。 if (hasExerciseAnswer(dialog)) return true; const targets = matchAiExerciseAnswers(question, result?.answers); if (!targets) { log.warn(`${sourceName}未取得可匹配的助教题答案,保留题窗`); updatePanelStatus(mode === 'ai' ? '⚠️ AI 暂无可用答案,请检查配置或切换来源' : `⚠️ ${sourceName}暂无可用答案,请切换来源或手动作答`); return false; } // 多选先清除错误勾选,单选由平台原生互斥;每一步都核对仍是原题。 const indices = question.type === 1 ? question.choices.map((_choice, i) => i) : [...targets]; for (const index of indices) { if (confirmedOriginal()) return true; if (!sameQuestion()) return false; if (hasExerciseAnswer(dialog)) return true; const choice = question.choices[index]; if (isExerciseChoiceSelected(choice) === targets.has(index)) continue; if (choice.disabled || choice.matches('.is-disabled, [aria-disabled="true"]') || choice.querySelector('input:disabled')) return false; activateExerciseChoice(choice, dialog); await sleep(150); } if (!sameQuestion()) return confirmedOriginal(); if (hasExerciseAnswer(dialog)) return true; const selected = question.choices.map(isExerciseChoiceSelected); if (!selected.every((checked, i) => checked === targets.has(i))) { updatePanelStatus('⚠️ AI 助教选项未选中,请检查题目'); return false; } const submit = findExerciseButton(dialog, /^(提交(?:答案|作答)?|确定|确认(?:答案|作答)?|完成作答)$/); if (submit) clickExerciseControl(submit, dialog); if (!await waitForExerciseAnswer(dialog, canAnswer, question.key)) { updatePanelStatus('⚠️ AI 助教题尚未确认提交,已保留题窗'); return false; } return true; } function isStudyExerciseDialog(dialog) { if (!dialog) return false; return dialog.matches('.dialog-test, .ai-class-exercise-dialog') || isAiStudyExerciseDialog(dialog) || !!dialog.querySelector('.topic-item') || (!!dialog.querySelector('.number') && /弹题|出题|随堂|练习/.test(dialog.textContent || '')); } function getStudyExerciseDialog() { return getStudyExerciseCandidates().find(dialog => { if (!isElementVisible(dialog) || !isStudyExerciseDialog(dialog)) return false; const text = (dialog.textContent || '').replace(/\s+/g, ''); const choices = getAiExerciseChoices(dialog); return choices.length >= 2 || dialog.matches('.dialog-test, .ai-class-exercise-dialog') || /AI助教.{0,30}(?:出题|随堂|练习)|AI随堂(?:练习|测试)/i.test(text) && !!findExerciseButton(dialog, /^(?:提交(?:答案|作答)?|继续学习|继续播放|关闭练习)$/); }) || null; } function clickExerciseControl(control, dialog) { if (!control || !dialog || !dialog.contains(control) || !control.isConnected || !isElementVisible(control) || platformSafetyStopped || hasBlockingPlatformDialog()) return false; const anchor = control.closest('a[href]'); if (anchor) { const href = anchor.getAttribute('href') || anchor.href || ''; if (isRootNavigationHref(href) || /\/(?:login|logout|passport)(?:\/|$)/i.test(href)) return false; } const guard = event => event.preventDefault(); control.addEventListener('click', guard, true); try { control.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); return true; } finally { control.removeEventListener('click', guard, true); } } function findExerciseCompletionControl(dialog) { return findExerciseButton(dialog, /^(?:关闭练习|继续学习|继续播放|返回视频)$/); } function activateExerciseChoice(choice, dialog) { // button/a 形式的自定义选项也不能提交外层表单或跳转; // 原生 input/label 保留默认动作,才能正常更新 checked 状态。 if (choice.closest('button, a[href]')) return clickExerciseControl(choice, dialog); choice.click(); return true; } function isUnansweredExerciseWarning(dialog) { return /未[作做]答.*(?:不能|无法)关闭/.test((dialog?.textContent || '').replace(/\s+/g, '')); } function hasBlockingStudyDialog() { return hasBlockingPlatformDialog() || !!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, .is-selected, .checked, .is-checked, .ant-radio-wrapper-checked, .ant-checkbox-wrapper-checked, [aria-checked="true"]') || !!choice.querySelector('input:checked, .is-checked, .ant-radio-checked, .ant-checkbox-checked, [aria-checked="true"]'); } function getExerciseQuestionKey(dialog) { if (isAiStudyExerciseDialog(dialog)) return readAiExerciseQuestion(dialog)?.contentKey || ''; 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。 const aiQuestion = isAiStudyExerciseDialog(dialog); const previousTransition = aiQuestion ? aiExerciseTransitions.get(dialog) : null; const active = number.matches('.active, .current, .is-active, [aria-selected="true"], [aria-current="true"]'); if (active && (!previousTransition || previousTransition.number !== number)) return true; const previousKey = getExerciseQuestionKey(dialog); let transition = previousTransition?.number === number ? previousTransition : null; if (aiQuestion && !transition) { const before = readAiExerciseQuestion(dialog); transition = { number, contentKey: before?.contentKey || previousKey, choices: before?.choices || [], results: Array.from(dialog.querySelectorAll('.answer, .answer-result, .question-result, .result-content, .answer-analysis, .analysis, .correct-answer, [role="status"]')) .filter(isElementVisible).map(node => ({ node, text: node.textContent || '' })) }; aiExerciseTransitions.set(dialog, transition); } if (!active) clickExerciseControl(number, dialog); for (let attempt = 0; attempt < 30; attempt++) { if (!canContinue() || !dialog.isConnected || !isElementVisible(dialog)) return false; if (aiQuestion) { const current = readAiExerciseQuestion(dialog); const bodyChanged = current && (current.contentKey !== transition.contentKey || (transition.choices.length > 0 && transition.choices.every(choice => !dialog.contains(choice)))); const staleResult = transition.results.some(({ node, text }) => dialog.contains(node) && isElementVisible(node) && (node.textContent || '') === text); if (bodyChanged && !staleResult) { aiExerciseTransitions.delete(dialog); return true; } } else { const key = getExerciseQuestionKey(dialog); if (key && key !== previousKey) return true; } await sleep(100); } return false; } function hasExerciseAnswer(dialog) { // .answer 是平台作答后的答案/解析区。仅看当前弹题内可见的结果, // 不能用页面其他题目的 .answer 或单纯勾选状态当作提交成功。 const aiDialog = isAiStudyExerciseDialog(dialog); const legacyResult = !aiDialog && Array.from(dialog.querySelectorAll('.answer')).some(answer => isElementVisible(answer) && !answer.querySelector('.topic-item') && !!((answer.textContent || '').trim() || answer.querySelector('img'))); if (legacyResult) return true; if (!aiDialog) return false; return Array.from(dialog.querySelectorAll('.answer, .answer-result, .question-result, .result-content, .answer-analysis, .analysis, .correct-answer, [role="status"]')) .filter(result => !getAiExerciseChoices(result).length) .some(result => isElementVisible(result) && /正确答案|参考答案|回答正确|回答错误|回答[对错]了|作答成功|提交成功|已作答|答案解析/.test(result.textContent || '')); } 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, questionKey = '') { for (let attempt = 0; attempt < (questionKey ? 40 : 20); attempt++) { if (!canContinue()) return false; if (!isElementVisible(dialog) || !dialog.isConnected) return true; const current = questionKey ? readAiExerciseQuestion(dialog) : null; if (questionKey && current && current.key !== questionKey) return false; if (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)) activateExerciseChoice(option, dialog); 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')) clickExerciseControl(submit, dialog); 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 || platformSafetyStopped) return; if (isHikeClass()) return; // 翻转课不支持自动答题 if (isVerificationVisible()) { verifyBlocking = true; pauseForVerification(); return; } const dialog = getStudyExerciseDialog(); if (!dialog) return; const generation = exerciseGeneration; const route = location.href; const canContinue = () => generation === exerciseGeneration && route === location.href && config.autoAnswer && !platformSafetyStopped && !isVerificationVisible() && !hasBlockingPlatformDialog(); exerciseInProgress = true; try { dismissExerciseWarning(); if (isFusionClass() && !isAiStudyExerciseDialog(dialog)) { 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; if (isAiStudyExerciseDialog(dialog)) { if (!await answerAiStudyExercise(dialog, canContinue)) return; 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; activateExerciseChoice(choice, dialog); await sleep(200); } } if (!canContinue()) return; // 部分版本选择后直接显示答案,其他版本必须点击提交/确定。 const submit = findExerciseButton(dialog, /^(提交(?:答案|作答)?|确定|确认(?:答案|作答)?|完成作答)$/); if (!hasExerciseAnswer(dialog) && submit) { clickExerciseControl(submit, dialog); } if (!await waitForExerciseAnswer(dialog, canContinue)) { if (canContinue()) { log.warn(`第 ${index + 1} 道弹题尚未确认作答,保留题窗等待处理`); updatePanelStatus('⚠️ 弹题未完成,请检查题目'); } return; } } if (!canContinue() || !isElementVisible(dialog) || !dialog.isConnected) return; const close = findExerciseCompletionControl(dialog); if (close) { clickExerciseControl(close, dialog); await sleep(350); if (!dialog.isConnected || !isElementVisible(dialog)) log.success('弹题已完成并关闭'); else updatePanelStatus('⏳ 等待弹题关闭'); } else { updatePanelStatus('✅ 弹题已提交,请手动关闭题窗后继续'); } } catch (e) { log.warn('处理弹题时出错: ' + e.message); } finally { exerciseInProgress = false; if (canContinue() && !hasBlockingStudyDialog()) await checkAndPlay(); } } 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(); } // 空白弹层可能仍在加载课程或题目。保留节点和遮罩供框架继续渲染, // 不点击确认、不发送 ESC,也不删除框架持有的 DOM。 function clearBlankDialogs() { return checkPlatformDialogs(); } // --- 关闭其他弹窗 --- function closePopups() { if (platformSafetyStopped || isVerificationVisible()) return; dismissExerciseWarning(); clearBlankDialogs(); if (hasBlockingPlatformDialog()) return; 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; } // AI 目录先切活动项、播放器后换源时,旧视频不能归属到新课时。 // 保留源和时间绑定,只有换源、换播放器或时间回到起点后才接管新课。 let aiPlaybackBinding = null; function bindLessonPlayback(previous, video, lessonKey) { const source = video.currentSrc || video.src || ''; const time = Number(video.currentTime) || 0; if (!previous || previous.video !== video || previous.source !== source) { // 视频可能先换源,目录下一帧才更新。为刚换的新源保留一次 // 短暂的重新归属机会,避免把它永久绑定到上一课。 const pendingLesson = previous?.lessonKey === lessonKey && !video.ended ? lessonKey : ''; return { video, source, lessonKey, time, ready: true, pendingLesson, changedAt: Date.now() }; } if (previous.lessonKey !== lessonKey) { const sourceArrivedFirst = previous.pendingLesson === previous.lessonKey && Date.now() - previous.changedAt < SWITCH_COOLDOWN_MS && !video.ended; previous.lessonKey = lessonKey; previous.ready = sourceArrivedFirst; previous.pendingLesson = ''; } if (!previous.ready && !video.ended && time + 0.25 < previous.time) previous.ready = true; previous.time = time; return previous; } function getAICoursePlayback(active) { const video = Array.from($$('video')).find(isElementVisible); const lessonKey = getLessonKey(active); if (!active || !lessonKey || !video) return null; aiPlaybackBinding = bindLessonPlayback(aiPlaybackBinding, video, lessonKey); return aiPlaybackBinding.ready ? video : null; } function isAICourseVideoItem(item) { return !!item && (item.matches('.basic-info-video-card-container') || /^(?:video|11|22)$/i.test(item.getAttribute('data-resource-type') || item.getAttribute('data-resources-data-type') || '') || !!item.querySelector('video')); } // --- 获取当前学习进度 --- function getCourseProgress() { const active = getActiveLesson(); if (!active) return '0%'; if (isAICourseClass()) { const video = getAICoursePlayback(active); if (video && Number.isFinite(video.duration) && video.duration > 0 && video.currentTime >= 0) { if (video.ended) return '100%'; const percent = Math.min(99.9, (video.currentTime / video.duration) * 100); return `${percent.toFixed(1)}%`; } if (isAICourseVideoItem(active) || Array.from($$('video')).some(isElementVisible)) return '0%'; 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() { if (isAICourseClass()) { const active = getActiveLesson(); if (!active) return false; const video = getAICoursePlayback(active); if (video) return video.ended === true; // 视频尚未加载/仍是上一课的源时,不采用历史目录进度跳课。 if (isAICourseVideoItem(active) || Array.from($$('video')).some(isElementVisible)) return false; return isAICourseItemComplete(active); } // 目录的 100% 可能来自上次观看。必须先确认当前播放器真正结束, // 并持续绑定课时和视频源,避免题后切课或旧视频 ended 串到下一课。 const video = getVideo(); const lessonKey = getLessonKey(); if (!video || !lessonKey) return false; studyPlaybackBinding = bindLessonPlayback(studyPlaybackBinding, video, lessonKey); return studyPlaybackBinding.ready && video.ended === true; } async function checkLessonProgress() { if (platformSafetyStopped || checkPlatformDialogs()) return; 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 (platformSafetyStopped || checkPlatformDialogs()) return; 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 = allItems.indexOf(active); if (currentIndex < 0) return; 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 (platformSafetyStopped || checkPlatformDialogs()) return; // 路由已离开课程时停止任务;history.back 会再次触发旧定时器, // 造成页面反复进出,不能用历史后退修补平台自身的跳转。 if (beforeUrl !== location.href && isRootNavigationHref(location.href)) { log.warn('检测到切换请求将返回站点首页,已阻止继续执行'); stopAllTasks(); updatePanelStatus('⏹ 已离开课程,请手动返回学习页面'); return; } const current = getActiveLesson(); if (current && (getLessonKey(current) !== oldKey || (!oldKey && current !== active))) { changed = true; break; } await sleep(250); } if (generation !== exerciseGeneration || platformSafetyStopped || checkPlatformDialogs()) return; if (changed && !isRootNavigationHref(location.href)) { afterLessonSwitch(); } else { log.warn('切换下一集后未确认活动课时变化'); updatePanelStatus('⚠️ 课时切换未确认'); } } catch (e) { log.warn('切换下一集失败: ' + e.message); updatePanelStatus('⚠️ 课时切换失败'); } finally { switchInProgress = false; } } // 切换课程后重新初始化 function afterLessonSwitch() { if (platformSafetyStopped || checkPlatformDialogs()) return; 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 = []; let taskGeneration = 0; function getRuntimePageMode() { return isExamPage() ? 'exam' : isStudyPage() ? 'study' : 'idle'; } function watchPageContext(generation) { let route = location.href; let mode = getRuntimePageMode(); // 独立于学习任务检测 SPA 地址和延迟习题组件;不改写平台 history。 // 仍纳入 timers,手动停止后不会因路由变化而自行复活。 const id = setInterval(() => { if (generation !== taskGeneration) return; const nextRoute = location.href; const nextMode = getRuntimePageMode(); if (route === nextRoute && mode === nextMode) return; route = nextRoute; mode = nextMode; main().catch(error => log.warn('页面切换初始化失败: ' + error.message)); }, 1000); timers.push(id); } function syncRuntimePanels(mode) { const studyPanel = $('#autovisor-panel'); const examPanel = $('#av-exam-panel'); if (studyPanel) studyPanel.style.display = mode === 'exam' ? 'none' : ''; if (examPanel) examPanel.style.display = mode === 'exam' ? '' : 'none'; const trial = $('#av-trial-modal'); if (trial) trial.hidden = true; } function addTimer(fn, interval) { const generation = taskGeneration; let running = false; const tick = async () => { if (running || platformSafetyStopped || generation !== taskGeneration || !isStudyPage() || isExamPage()) return; if (hasBlockingPlatformDialog()) { checkPlatformDialogs(); 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() { taskGeneration++; invalidateStudyExerciseAnswer(); completionSince = 0; 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 = `
用于助教随堂题,独立于考试页的答题来源。更换来源或重新保存配置后可重试当前题目。
模型配置与本脚本考试页共用,保存后用于助教题重试。
扫码按页面提示获取验证码,有效期以验证结果为准。到期后可重新验证。
启用付费题库后,查询答案会消耗积分。激活码绑定当前设备,刷新余额不会查询题目。