// ==UserScript== // @name ✨超星学习通智能阅读助手(URL检测自动跳转版) // @version 1.0.1 // @description 超星学习通智能阅读增强工具 📚,支持逐段 / 整页双模式自动阅读 ⏩,结合实时监听与定时检查实现 URL 变化精准检测 🔍,自动识别并跳转下一章节 ➡️,提供全键盘控制(K 开始 / Z 暂停 / S 设置 / R 跳转)⌨️ 及个性化配置中心 ⚙️,适配超星全平台课程页面,提升阅读效率与学习体验 ✨ // @author 伏黑甚而 // @match *://*.chaoxing.com/* // @match *://mooc1-*.chaoxing.com/* // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant unsafeWindow // @connect chaoxing.com // @run-at document-end // @icon http://pan-yz.chaoxing.com/favicon.ico // ==/UserScript== (function() { 'use strict'; // 全局配置 const CONFIG = { scrollSpeed: parseFloat(GM_getValue('scrollSpeed', 0.5)), scrollMode: GM_getValue('scrollMode', 'paragraph'), autoStart: GM_getValue('autoStart', true), restartAfterFinish: GM_getValue('restartAfterFinish', true), showTips: GM_getValue('showTips', true), debugMode: GM_getValue('debugMode', true), autoRedirect: GM_getValue('autoRedirect', true), maxIframeDepth: 10, urlChangeDetection: true, urlChangeDebounce: 500, monitorNextButton: true, nextButtonSelector: '#right1', urlCheckInterval: 1000, // 新增:URL检查间隔(ms) }; // 状态管理 const STATE = { isRunning: false, isPaused: false, currentChapter: 0, totalChapters: 0, contentElements: [], currentElementIndex: 0, lastButton: null, lastUrl: window.location.href, urlChangeTimer: null, urlCheckTimer: null, // 新增:URL检查定时器 nextButtonObserver: null, }; // 日志与通知 function log(message, level = 'info') { if (!CONFIG.debugMode && level !== 'error') return; console.log(`[超星阅读助手] [${level.toUpperCase()}] ${message}`); } function notify(message, type = 'info') { const icons = { info: 'ℹ️', success: '✅', warning: '⚠️', error: '❌' }; GM_notification({ title: `${icons[type]} 超星阅读助手`, text: message, silent: true, timeout: 5000 }); log(message, type); } // DOM工具 function waitForElement(selector, timeout = 15000) { return new Promise(resolve => { const interval = setInterval(() => { const el = document.querySelector(selector); if (el) { clearInterval(interval); resolve(el); } }, 100); setTimeout(() => clearInterval(interval), timeout); }); } // 页面操作类 class PageOperator { constructor() { this.init(); this.setupUrlChangeDetection(); this.setupNextButtonMonitoring(); this.startUrlCheckTimer(); // 新增:启动URL定时检查 } init() { this.bindEvents(); this.detectPageType(); log(`脚本初始化,版本:${GM_info.script.version}`); } // 新增:启动URL定时检查 startUrlCheckTimer() { if (STATE.urlCheckTimer) clearInterval(STATE.urlCheckTimer); STATE.urlCheckTimer = setInterval(() => { if (window.location.href !== STATE.lastUrl) { log('定时检测到URL变化'); this.handleUrlChange(); } }, CONFIG.urlCheckInterval); log(`URL定时检查已启动,间隔:${CONFIG.urlCheckInterval}ms`); } setupNextButtonMonitoring() { if (!CONFIG.monitorNextButton) return; this.findAndBindNextButton(); const observer = new MutationObserver(() => { this.findAndBindNextButton(); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true }); STATE.nextButtonObserver = observer; log('下一节按钮监控已启用'); } findAndBindNextButton() { try { const nextButton = document.querySelector(CONFIG.nextButtonSelector); if (nextButton && !nextButton._hasClickHandler) { log('找到下一节按钮,绑定点击事件'); const originalOnClick = nextButton.getAttribute('onclick'); nextButton.addEventListener('click', (e) => { log('检测到下一节按钮点击'); if (originalOnClick) { eval(originalOnClick); } this.onNextButtonClicked(); }); nextButton._hasClickHandler = true; log('下一节按钮点击事件绑定成功'); } } catch (error) { log(`绑定下一节按钮失败: ${error.message}`, 'error'); } } onNextButtonClicked() { notify('检测到章节切换,准备加载新内容...'); const wasRunning = STATE.isRunning; const wasPaused = STATE.isPaused; this.resetState(); this.showModal(`
正在加载下一章内容...
请稍候...
`); setTimeout(() => { this.hideModal(); this.detectPageType(); if (wasRunning && !wasPaused) { setTimeout(() => { if (this.isReadingPage()) { this.startAutoRead(); } }, 2000); } }, 3000); } // 增强URL变化检测 setupUrlChangeDetection() { if (!CONFIG.urlChangeDetection) return; // 监听hashchange事件 window.addEventListener('hashchange', () => { log('检测到hashchange事件'); this.handleUrlChange(); }); // 监听popstate事件 window.addEventListener('popstate', () => { log('检测到popstate事件'); this.handleUrlChange(); }); // 使用MutationObserver监听DOM变化 const observer = new MutationObserver(() => { if (window.location.href !== STATE.lastUrl) { log('MutationObserver检测到URL变化'); this.handleUrlChange(); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true }); log('URL变化检测已启用(实时监听+定时检查)'); } handleUrlChange() { const newUrl = window.location.href; if (newUrl !== STATE.lastUrl) { log(`URL变化: ${STATE.lastUrl} → ${newUrl}`); STATE.lastUrl = newUrl; clearTimeout(STATE.urlChangeTimer); STATE.urlChangeTimer = setTimeout(() => { this.resetState(); this.detectPageType(); if (this.isReadingPage() && CONFIG.autoStart) { setTimeout(() => this.startAutoRead(), 2000); } }, CONFIG.urlChangeDebounce); } } resetState() { STATE.isRunning = false; STATE.isPaused = false; STATE.contentElements = []; STATE.currentElementIndex = 0; log('状态已重置'); } bindEvents() { document.addEventListener('keydown', e => { if (e.key === 'k') this.startAutoRead(); if (e.key === 'z') this.pauseAutoRead(); if (e.key === 's') this.showSettings(); if (e.key === 'r') this.toggleAutoRedirect(); }); } detectPageType() { const oldTips = document.querySelector('#auto-read-tips'); if (oldTips) oldTips.remove(); if (this.isReadingPage()) { log('检测到阅读页面'); this.initReading(); } else if (this.isCoursePage()) { log('检测到课程主页'); this.addAutoRedirectButton(); CONFIG.autoRedirect && this.detectReadingTasks(); } else if (location.href.includes('/mooc-ans/course/')) { log('检测到课程目录/任务列表页面 (新)'); this.handleCourseCatalogPage(); } else if (this.isTaskPage()) { log('检测到任务页面 (旧)'); this.handleGenericTaskPage(); } } isReadingPage() { return location.href.includes('/ztnodedetailcontroller/visitnodedetail'); } isCoursePage() { return location.href.includes('/mooc-ans/mycourse/studentstudy'); } isTaskPage() { return location.href.includes('pageHeader=0'); } initReading() { this.showUsageTips(); this.detectChapterInfo(); this.collectContentElements(); if (CONFIG.autoStart) { setTimeout(() => { if (this.isReadingPage()) { this.startAutoRead(); } }, 2000); } } startAutoRead() { if (STATE.isRunning && !STATE.isPaused) return; STATE.isRunning = true; STATE.isPaused = false; if (CONFIG.scrollMode === 'paragraph') { this.startParagraphScroll(); } else { this.startPageScroll(); } this.showStatus(); notify(`开始阅读 (${CONFIG.scrollSpeed}秒/${CONFIG.scrollMode === 'paragraph' ? '段落' : '页'})`); } pauseAutoRead() { if (!STATE.isRunning) return; STATE.isPaused = true; clearTimeout(this.scrollTimer); this.showStatus(); notify('阅读已暂停'); } showStatus() { const status = STATE.isPaused ? '已暂停' : '阅读中'; const progress = STATE.totalChapters > 0 ? `第 ${STATE.currentChapter}/${STATE.totalChapters} 章` : '章节信息未知'; this.showModal(`状态: ${status}
${progress}
模式: ${CONFIG.scrollMode === 'paragraph' ? '段落阅读' : '页面阅读'}
速度: ${CONFIG.scrollSpeed.toFixed(1)}秒/${CONFIG.scrollMode === 'paragraph' ? '段落' : '页'}
按 Z 键暂停 / 按 K 键继续
`); setTimeout(() => this.hideModal(), 3000); } startParagraphScroll() { this.collectContentElements(); if (STATE.contentElements.length === 0) { notify('未检测到段落,临时切换整页模式...', 'warning'); CONFIG.scrollMode = 'page'; this.startPageScroll(); // 启动内容监听器 const observer = new MutationObserver(() => { this.collectContentElements(); if (STATE.contentElements.length > 0) { observer.disconnect(); notify('检测到段落内容,恢复逐段模式', 'success'); CONFIG.scrollMode = 'paragraph'; if (!STATE.isPaused) { this.startParagraphScroll(); } } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true }); return; } this.scrollToNextElement(); } collectContentElements() { const selectors = [ 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img', 'video', 'iframe', '.content', '.text-block', '.article-content' ]; STATE.contentElements = []; selectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(el => { if (el.offsetHeight > 20 && el.offsetWidth > 20) { STATE.contentElements.push(el); } }); }); STATE.contentElements.sort((a, b) => { return a.getBoundingClientRect().top - b.getBoundingClientRect().top; }); log(`找到 ${STATE.contentElements.length} 个内容元素`); } scrollToNextElement() { if (STATE.isPaused) return; if (STATE.currentElementIndex >= STATE.contentElements.length) { this.onChapterComplete(); return; } const element = STATE.contentElements[STATE.currentElementIndex]; if (CONFIG.debugMode) { element.style.outline = '2px solid red'; setTimeout(() => element.style.outline = '', 1000); } element.scrollIntoView({ behavior: 'smooth', block: 'center' }); const baseTime = parseFloat(CONFIG.scrollSpeed) * 1000; let waitTime = element.tagName === 'IMG' || element.tagName === 'VIDEO' ? baseTime * 1.5 : baseTime; STATE.currentElementIndex++; this.scrollTimer = setTimeout(() => this.scrollToNextElement(), waitTime); } startPageScroll() { const scrollSpeed = parseFloat(CONFIG.scrollSpeed); const totalHeight = document.documentElement.scrollHeight - window.innerHeight; const scrollStep = totalHeight / (scrollSpeed * 10); let currentTop = 0; clearInterval(this.scrollTimer); this.scrollTimer = setInterval(() => { if (STATE.isPaused) return; currentTop += scrollStep; if (currentTop >= totalHeight) { clearInterval(this.scrollTimer); this.onChapterComplete(); } else { window.scrollTo({ top: currentTop, behavior: 'smooth' }); } }, 100); } onChapterComplete() { clearTimeout(this.scrollTimer); STATE.currentElementIndex = 0; STATE.contentElements = []; this.findNextButton() .then(nextButton => { if (nextButton) { notify('正在加载下一章...'); nextButton.style.outline = '3px solid green'; setTimeout(() => nextButton.style.outline = '', 2000); nextButton.click(); STATE.currentChapter++; } else { if (CONFIG.restartAfterFinish) { notify('已到达最后一章,即将从头开始', 'warning'); setTimeout(() => this.goToFirstChapter(), 3000); } else { STATE.isRunning = false; notify('全部阅读完成!', 'success'); } } }) .catch(error => { notify(`章节切换错误: ${error.message}`, 'error'); log(error.stack, 'error'); }); } findNextButton() { return new Promise(resolve => { const selectors = [ CONFIG.nextButtonSelector, '.nodeItem.r i', '.next-page-btn', 'a:contains("下一章")', 'button:contains("下一章")', 'a[title="下一章"]', '.reader__control--next', '.uxp-pager-next', 'button[aria-label*="下一章"]', '.next-btn:visible' ]; for (const selector of selectors) { try { let element = document.querySelector(selector); if (!element && selector.includes(':contains(')) { const text = selector.match(/:contains\("(.*)"\)/)[1]; const allElements = document.querySelectorAll('a, button'); element = Array.from(allElements).find(el => el.textContent.includes(text)); } if (element) { log(`找到下一章按钮: ${selector}`); return resolve(element); } } catch (error) { // 继续尝试下一个选择器 } } log('未找到下一章按钮,尝试通用选择器', 'warning'); const genericElements = document.querySelectorAll('*'); for (const el of genericElements) { if (el.textContent.includes('下一章') && el.offsetWidth > 0 && el.offsetHeight > 0) { log('找到下一章按钮(通用选择器)'); return resolve(el); } } log('未找到下一章按钮', 'warning'); resolve(null); }); } goToFirstChapter() { try { const firstChapter = document.querySelector('.course_section .chapterText, .catalog-item:first-child'); if (firstChapter) { firstChapter.click(); setTimeout(() => { STATE.currentChapter = 1; if (CONFIG.scrollMode === 'paragraph') { this.collectContentElements(); } if (!STATE.isPaused) { this.scrollToNextElement(); } }, 3000); } else { notify('未找到目录,无法重新开始', 'error'); STATE.isRunning = false; } } catch (error) { notify(`跳转错误: ${error.message}`, 'error'); STATE.isRunning = false; } } // 自动跳转相关方法 async detectReadingTasks() { log('开始检测阅读任务'); const found = await this.detectIframes(document, 0); if (!found) { await new Promise(resolve => setTimeout(resolve, 500)); if (!STATE.lastButton) { notify('未找到阅读任务', 'warning'); } } else { log('成功找到并处理阅读按钮', 'info'); } } detectIframes(parentDoc, depth) { return new Promise(async resolve => { if (depth > CONFIG.maxIframeDepth) { log(`达到最大iframe深度(${CONFIG.maxIframeDepth})`, 'info'); return resolve(false); } log(`检测第${depth}层文档`); try { await waitForElement('#chapterContent, .content, .text-block, .article-content, body', 15000); log(`第${depth}层文档内容加载完毕,准备查找按钮`); await new Promise(resolve => setTimeout(resolve, 500)); } catch (error) { log(`等待第${depth}层文档内容加载超时: ${error.message}`, 'warning'); } const buttons = this.findReadingButtons(parentDoc); if (buttons.length > 0) { log(`在第${depth}层找到${buttons.length}个阅读按钮`); const success = await this.triggerButtonClick(buttons[0], parentDoc); if (success) { return resolve(true); } } else { log(`在第${depth}层文档未找到阅读按钮`, 'info'); } const iframes = parentDoc.querySelectorAll('iframe'); for (const iframe of iframes) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; if (iframeDoc) { const foundInIframe = await this.detectIframes(iframeDoc, depth + 1); if (foundInIframe) { return resolve(true); } } else { log('无法获取iframe内容', 'warning'); } } catch (error) { log(`访问iframe失败: ${error.message}`, 'error'); } } resolve(false); }); } findReadingButtons(doc) { const buttonTexts = ['去阅读', '开始阅读', '立即阅读', '阅读全文', '进入阅读']; const buttonElements = []; // 先尝试精确匹配 buttonTexts.forEach(text => { const elements = doc.querySelectorAll('a, button, span, div'); elements.forEach(el => { if (el.textContent.trim() === text && el.offsetWidth > 0 && el.offsetHeight > 0 && getComputedStyle(el).display !== 'none') { buttonElements.push(el); } }); }); // 如果没找到,尝试模糊匹配 if (buttonElements.length === 0) { buttonTexts.forEach(text => { const elements = doc.querySelectorAll('a, button, span, div'); elements.forEach(el => { if (el.textContent.includes(text) && el.offsetWidth > 0 && el.offsetHeight > 0 && getComputedStyle(el).display !== 'none') { buttonElements.push(el); } }); }); } return buttonElements; } triggerButtonClick(button, doc) { return new Promise(async resolve => { if (!button) { log('没有提供按钮元素', 'error'); return resolve(false); } try { button.style.outline = '3px solid red'; button.style.transition = 'outline 0.3s ease'; STATE.lastButton = button; button.scrollIntoView({ block: 'center', behavior: 'smooth' }); await new Promise(resolve => setTimeout(resolve, 500)); button.click(); log('尝试点击按钮', 'info'); notify('已尝试点击阅读按钮'); resolve(true); } catch (error) { log(`点击按钮失败: ${error.message}`, 'error'); notify('点击按钮失败', 'error'); resolve(false); } finally { setTimeout(() => { if (STATE.lastButton) STATE.lastButton.style.outline = ''; }, 2000); } }); } _extractHref(element) { try { if (element.href && element.href.includes('chaoxing.com')) { return element.href; } const parentLink = element.closest('a'); if (parentLink && parentLink.href && parentLink.href.includes('chaoxing.com')) { return parentLink.href; } const dataHref = element.getAttribute('data-href'); if (dataHref && dataHref.includes('chaoxing.com')) { return dataHref; } const onclick = element.getAttribute('onclick'); if (onclick) { const hrefMatch = onclick.match(/window\.location\.href=['"]([^'"]+)['"]/); if (hrefMatch && hrefMatch[1] && hrefMatch[1].includes('chaoxing.com')) { return hrefMatch[1]; } const goToMatch = onclick.match(/goTo\(['"]([^'"]+)['"]\)/); if (goToMatch && goToMatch[1] && goToMatch[1].includes('chaoxing.com')) { return goToMatch[1]; } } return null; } catch (error) { log(`提取链接失败: ${error.message}`, 'error'); return null; } } // 自动跳转控制按钮 addAutoRedirectButton() { try { const controlButton = document.createElement('div'); controlButton.id = 'auto-redirect-control'; controlButton.style.cssText = ` position: fixed; top: 20px; right: 20px; background: rgba(0,0,0,0.7); color: white; padding: 10px; border-radius: 5px; z-index: 9999; font-family: Arial, sans-serif; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 5px; `; const updateButtonState = () => { controlButton.innerHTML = ` ${CONFIG.autoRedirect ? '🟢' : '⚪'} 自动跳转: ${CONFIG.autoRedirect ? '开启' : '关闭'} `; }; updateButtonState(); controlButton.addEventListener('click', () => { CONFIG.autoRedirect = !CONFIG.autoRedirect; GM_setValue('autoRedirect', CONFIG.autoRedirect); updateButtonState(); notify(`自动跳转已${CONFIG.autoRedirect ? '开启' : '关闭'}`); if (CONFIG.autoRedirect) { setTimeout(() => this.detectReadingTasks(), 1000); } }); document.body.appendChild(controlButton); } catch (error) { log(`添加自动跳转控制按钮失败: ${error.message}`, 'error'); } } // 键盘事件初始化 initKeyboardEvents() { if (!window.location.href.includes('/ztnodedetailcontroller/visitnodedetail')) return; document.addEventListener('keydown', (e) => { if (e.key.toLowerCase() === 'k') { e.preventDefault(); this.startAutoRead(); } if (e.key.toLowerCase() === 'z') { e.preventDefault(); this.pauseAutoRead(); } if (e.key.toLowerCase() === 's') { e.preventDefault(); this.showSettings(); } if (e.key.toLowerCase() === 'r') { e.preventDefault(); CONFIG.autoRedirect = !CONFIG.autoRedirect; GM_setValue('autoRedirect', CONFIG.autoRedirect); notify(`自动跳转已${CONFIG.autoRedirect ? '开启' : '关闭'}`); this.showUsageTips(); } }); } // 设置菜单 showSettings() { const html = `生活不易,猪猪叹气 —— 赏口饲料,让我少气!🐷✨
超星阅读助手快捷键:
• K: 开始/继续阅读
• Z: 暂停阅读
• S: 显示设置
• R: 切换自动跳转 (${CONFIG.autoRedirect ? '开启' : '关闭'})