// ==UserScript== // @name 高教在线刷课助手(1.0.9 完整版) // @namespace http://tampermonkey.net/ // @version 1.0.9 // @description 修复多图框问题,强化视频识别能力,支持自动处理课程内容 // @author Sweek // @match *://*.cqooc.com/* // @license GPLv3 // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // 单实例控制核心逻辑 const instanceKey = 'course_helper_single_instance_v109'; const existingInstance = GM_getValue(instanceKey, false); // 若已有实例运行,立即终止当前脚本 if (existingInstance) { console.log('高教在线刷课助手:已有实例运行,当前实例终止'); return; } // 清理所有残留图框 const cleanOldFrames = () => { const oldFrames = document.querySelectorAll('#course-helper-frame'); if (oldFrames.length > 0) { console.log(`清理了${oldFrames.length}个残留图框`); oldFrames.forEach(el => el.remove()); } }; cleanOldFrames(); // 设置实例标识及生命周期管理 GM_setValue(instanceKey, true); window.addEventListener('beforeunload', () => GM_setValue(instanceKey, false)); window.addEventListener('popstate', () => GM_setValue(instanceKey, false)); // 淡蓝色系样式 GM_addStyle(` #course-helper-frame { position: fixed !important; top: 20px !important; left: 20px !important; width: 420px !important; height: 540px !important; background: #ffffff !important; border: 1px solid #e0e8f0 !important; border-radius: 8px !important; box-shadow: 0 4px 20px rgba(74, 144, 226, 0.15) !important; z-index: 9999999 !important; overflow: hidden !important; font-family: "Segoe UI", "Microsoft YaHei", sans-serif !important; transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important; pointer-events: auto !important; } #frame-header { height: 50px !important; background: #4a90e2 !important; color: #ffffff !important; padding: 0 18px !important; display: flex !important; justify-content: space-between !important; align-items: center !important; cursor: move !important; user-select: none !important; pointer-events: auto !important; } #frame-title { font-size: 15px !important; font-weight: 600 !important; } #frame-version { font-size: 12px !important; opacity: 0.9 !important; margin-left: 8px !important; font-weight: normal !important; } #frame-controls { display: flex !important; gap: 8px !important; } .control-btn { width: 32px !important; height: 32px !important; border-radius: 4px !important; background: rgba(255, 255, 255, 0.2) !important; color: #ffffff !important; border: none !important; outline: none !important; cursor: pointer !important; font-size: 14px !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: background 0.2s !important; pointer-events: auto !important; } .control-btn:hover { background: rgba(255, 255, 255, 0.3) !important; } #frame-tabs { height: 45px !important; display: flex !important; border-bottom: 1px solid #f0f5f7 !important; background: #f7f9fc !important; pointer-events: auto !important; } .frame-tab { flex: 1 !important; display: flex !important; align-items: center !important; justify-content: center !important; font-size: 14px !important; color: #666666 !important; cursor: pointer !important; transition: all 0.2s !important; user-select: none !important; pointer-events: auto !important; } .frame-tab.active { color: #4a90e2 !important; font-weight: 500 !important; border-bottom: 2px solid #4a90e2 !important; background: #ffffff !important; } #frame-content { height: calc(100% - 95px) !important; overflow: hidden !important; pointer-events: auto !important; } .frame-panel { height: 100% !important; overflow-y: auto !important; padding: 15px !important; display: none !important; pointer-events: auto !important; } .frame-panel.active { display: block !important; } #status-panel p { margin: 0 0 12px 0 !important; line-height: 1.6 !important; font-size: 14px !important; color: #333333 !important; } #status-panel strong { color: #4a90e2 !important; } #log-panel { font-size: 13px !important; } .log-item { padding: 8px 0 !important; margin: 0 !important; border-bottom: 1px dashed #f0f5f7 !important; color: #555555 !important; line-height: 1.5 !important; } .log-time { color: #999999 !important; margin-right: 8px !important; } .log-content { color: #333333 !important; } .frame-panel::-webkit-scrollbar { width: 6px !important; } .frame-panel::-webkit-scrollbar-track { background: #f0f5f7 !important; } .frame-panel::-webkit-scrollbar-thumb { background: #c0d0e0 !important; border-radius: 3px !important; } .frame-panel::-webkit-scrollbar-thumb:hover { background: #a0b8d0 !important; } #course-helper-frame.minimized { height: 50px !important; box-shadow: 0 2px 10px rgba(74, 144, 226, 0.1) !important; } #course-helper-frame.minimized #frame-tabs, #course-helper-frame.minimized #frame-content { display: none !important; } `); // 图框状态管理 const frameManager = { element: null, isDragging: false, dragOffset: { x: 0, y: 0 }, isMinimized: false, version: '1.0.9', initFrame() { cleanOldFrames(); // 初始化前再次清理 this.element = document.createElement('div'); this.element.id = 'course-helper-frame'; this.element.innerHTML = `
高教在线刷课助手 v${this.version}
状态信息
执行日志

图框已初始化完成

版本号:v${this.version}

功能:自动处理课程内容,支持视频、PPT、PDF

优化点:

• 强化视频识别能力(支持iframe嵌套视频)

• 解决多图框问题

• 增加视频查找重试机制

[${this.getCurrentTime()}] 完整版启动成功(v${this.version})
`; document.body.appendChild(this.element); this.bindEvents(); this.log('图框初始化完成,单实例控制生效'); }, bindEvents() { const header = this.element.querySelector('#frame-header'); const minimizeBtn = this.element.querySelector('#minimize-btn'); const resetBtn = this.element.querySelector('#reset-btn'); const tabs = this.element.querySelectorAll('.frame-tab'); // 拖拽事件 header.addEventListener('mousedown', (e) => this.startDrag(e)); document.addEventListener('mousemove', (e) => this.handleDrag(e)); document.addEventListener('mouseup', () => this.stopDrag()); document.addEventListener('mouseleave', () => this.stopDrag()); // 最小化按钮 minimizeBtn.addEventListener('click', () => this.toggleMinimize()); // 重置位置 resetBtn.addEventListener('click', () => this.resetPosition()); // 标签切换 tabs.forEach(tab => { tab.addEventListener('click', () => this.switchTab(tab)); }); }, startDrag(e) { if (e.button !== 0) return; e.preventDefault(); e.stopPropagation(); const rect = this.element.getBoundingClientRect(); this.isDragging = true; this.dragOffset.x = e.clientX - rect.left; this.dragOffset.y = e.clientY - rect.top; this.element.style.boxShadow = '0 6px 25px rgba(74, 144, 226, 0.25)'; this.element.style.transform = 'scale(1.01)'; this.log('开始拖拽图框'); }, handleDrag(e) { if (!this.isDragging) return; e.preventDefault(); const newX = e.clientX - this.dragOffset.x; const newY = e.clientY - this.dragOffset.y; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const frameWidth = this.element.offsetWidth; const frameHeight = this.element.offsetHeight; const constrainedX = Math.max(0, Math.min(newX, viewportWidth - frameWidth)); const constrainedY = Math.max(0, Math.min(newY, viewportHeight - frameHeight)); this.element.style.left = `${constrainedX}px`; this.element.style.top = `${constrainedY}px`; this.element.style.transform = 'none'; }, stopDrag() { if (this.isDragging) { this.isDragging = false; this.element.style.boxShadow = '0 4px 20px rgba(74, 144, 226, 0.15)'; this.log('图框拖拽结束'); } }, toggleMinimize() { this.isMinimized = !this.isMinimized; const btn = this.element.querySelector('#minimize-btn'); if (this.isMinimized) { this.element.classList.add('minimized'); btn.textContent = '□'; btn.title = '还原'; this.log('图框已最小化'); } else { this.element.classList.remove('minimized'); btn.textContent = '—'; btn.title = '最小化'; this.log('图框已还原'); } }, resetPosition() { this.element.style.top = '20px'; this.element.style.left = '20px'; this.element.style.transform = 'none'; this.log('图框位置已重置'); }, switchTab(tab) { if (tab.classList.contains('active') || this.isMinimized) return; this.element.querySelectorAll('.frame-tab').forEach(t => t.classList.remove('active')); this.element.querySelectorAll('.frame-panel').forEach(panel => panel.classList.remove('active')); tab.classList.add('active'); const targetPanel = tab.dataset.panel; this.element.querySelector(`#${targetPanel}-panel`).classList.add('active'); this.log(`切换到【${tab.textContent}】面板`); }, getCurrentTime() { const date = new Date(); return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); }, log(message) { if (this.isMinimized) return; const logPanel = this.element.querySelector('#log-panel'); const logItem = document.createElement('div'); logItem.className = 'log-item'; logItem.innerHTML = ` [${this.getCurrentTime()}] ${message} `; logPanel.appendChild(logItem); logPanel.scrollTop = logPanel.scrollHeight; }, updateStatus(content) { if (this.isMinimized) return; const statusPanel = this.element.querySelector('#status-panel'); const contentArray = Array.isArray(content) ? content : [content]; statusPanel.innerHTML = contentArray.map(item => `

${item}

`).join(''); } }; // 内容类型判断(强化视频识别) function getContentType() { // 1. 优先检测视频相关元素 const videoElements = document.querySelectorAll('video, [data-type="video"], [class*="video"], [id*="video"]'); if (videoElements.length > 0) return 'video'; // 2. 检测视频播放按钮 const playButtons = document.querySelectorAll( '[class*="play"], [id*="play"], [title*="播放"], [aria-label*="播放"]' ); if (playButtons.length > 0) return 'video'; // 3. 检测PPT相关 const bodyText = document.body.innerText.toLowerCase(); if (bodyText.includes('ppt') || bodyText.includes('幻灯片') || bodyText.includes('powerpoint')) return 'ppt'; // 4. 检测PDF相关 if (bodyText.includes('pdf') || bodyText.includes('文档') || bodyText.includes('adobe')) return 'pdf'; return 'unknown'; } // 视频处理(增加重试和iframe检测) function handleVideo() { return new Promise(resolve => { frameManager.log('开始处理视频内容,尝试查找视频元素...'); let video = null; let retryCount = 0; const maxRetries = 10; // 10秒内重试 // 多维度查找视频元素 const findVideoElement = () => { // 直接查找video标签 video = document.querySelector('video'); if (video) return video; // 查找iframe中的视频(处理嵌套场景) const iframes = document.querySelectorAll('iframe'); for (const iframe of iframes) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; const iframeVideo = iframeDoc.querySelector('video'); if (iframeVideo) return iframeVideo; } catch (e) { // 跨域iframe无法访问,记录日志但不中断 frameManager.log('检测到跨域iframe,无法访问内部内容'); } } // 查找带video关键词的容器内的视频 const videoContainers = document.querySelectorAll('[class*="video"], [id*="video"]'); for (const container of videoContainers) { const containerVideo = container.querySelector('video'); if (containerVideo) return containerVideo; } return null; }; // 重试机制 const checkTimer = setInterval(() => { video = findVideoElement(); retryCount++; if (video) { clearInterval(checkTimer); frameManager.log(`视频元素找到(重试${retryCount}次)`); // 视频播放控制 video.playbackRate = 1.5; frameManager.log(`视频倍速已设置为:${video.playbackRate}x`); const playVideo = () => { if (video.paused && !video.ended) { video.play().catch(() => { video.muted = true; // 静音播放解决部分自动播放限制 video.play().catch(err => frameManager.log(`播放失败:${err.message}`)); }); } }; playVideo(); video.addEventListener('pause', playVideo); // 进度更新 const progressTimer = setInterval(() => { if (video.ended) { clearInterval(progressTimer); video.removeEventListener('pause', playVideo); frameManager.log('视频播放完成'); resolve(); } else { const progress = (video.currentTime / video.duration) * 100; frameManager.updateStatus([ `内容类型:视频`, `播放进度:${progress.toFixed(1)}%`, `当前时长:${formatTime(video.currentTime)}/${formatTime(video.duration)}`, `播放倍速:${video.playbackRate}x` ]); } }, 1000); } else if (retryCount >= maxRetries) { // 重试结束仍未找到,等待后跳过 clearInterval(checkTimer); frameManager.log(`重试${maxRetries}次仍未找到视频元素,将在25秒后跳过`); let countdown = 25; const waitTimer = setInterval(() => { countdown--; frameManager.updateStatus([ `内容类型:视频(未找到)`, `等待进度:${Math.round((25 - countdown)/25*100)}%`, `剩余时间:${countdown}秒`, `即将自动跳过至下一节` ]); if (countdown <= 0) { clearInterval(waitTimer); frameManager.log('视频未找到,已跳过'); resolve(); } }, 1000); } else { frameManager.log(`视频元素未找到,正在重试(${retryCount}/${maxRetries})`); frameManager.updateStatus([ `内容类型:视频(查找中)`, `重试次数:${retryCount}/${maxRetries}`, `请稍候...` ]); } }, 1000); }); } // PPT处理 function handlePPT() { return new Promise(resolve => { const totalSeconds = 35; frameManager.log(`开始处理PPT内容,${totalSeconds}秒后完成`); let remaining = totalSeconds; const timer = setInterval(() => { remaining--; const progress = ((totalSeconds - remaining) / totalSeconds) * 100; frameManager.updateStatus([ `内容类型:PPT`, `处理进度:${progress.toFixed(1)}%`, `剩余时间:${remaining}秒` ]); if (remaining <= 0) { clearInterval(timer); frameManager.log('PPT内容处理完成'); resolve(); } }, 1000); }); } // PDF处理 function handlePDF() { return new Promise(resolve => { const totalSeconds = 35; frameManager.log(`开始处理PDF内容,${totalSeconds}秒后完成`); let remaining = totalSeconds; const timer = setInterval(() => { remaining--; const progress = ((totalSeconds - remaining) / totalSeconds) * 100; frameManager.updateStatus([ `内容类型:PDF`, `处理进度:${progress.toFixed(1)}%`, `剩余时间:${remaining}秒` ]); if (remaining <= 0) { clearInterval(timer); frameManager.log('PDF内容处理完成'); resolve(); } }, 1000); }); } // 时间格式化 function formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; } // 处理当前内容 async function processCurrentContent() { const contentType = getContentType(); frameManager.log(`检测到内容类型:${contentType}`); switch (contentType) { case 'video': await handleVideo(); break; case 'ppt': await handlePPT(); break; case 'pdf': await handlePDF(); break; default: frameManager.log('未知内容类型,35秒后跳过'); frameManager.updateStatus([ `内容类型:未知`, `等待时间:35秒后自动跳过` ]); await new Promise(resolve => setTimeout(resolve, 35000)); break; } } // 任务处理流程 function startTaskProcessing() { if (!window.location.pathname.includes('/course/detail/courseStudy')) { frameManager.updateStatus([ '未检测到课程学习页面', '请先进入"在学课程"的学习页面', `版本号:v${frameManager.version}` ]); frameManager.log('未检测到课程学习页面'); return; } const tasks = Array.from(document.querySelectorAll('.third-level-box')) .filter(el => !el.innerText.toLowerCase().includes('作业') && !el.innerText.toLowerCase().includes('测验') && !el.innerText.toLowerCase().includes('考试')); if (tasks.length === 0) { frameManager.updateStatus(['未找到可处理的课程内容', '请确认已进入课程学习页面']); frameManager.log('未找到可处理的任务'); return; } const activeIndex = tasks.findIndex(el => el.classList.contains('active')); const taskQueue = tasks.slice(activeIndex > -1 ? activeIndex : 0); frameManager.log(`共发现${tasks.length}个任务,将从第${activeIndex + 1}个开始处理`); frameManager.updateStatus([ `总任务数:${tasks.length}个`, `剩余任务:${taskQueue.length}个`, `处理中...` ]); const processNextTask = () => { if (taskQueue.length === 0) { frameManager.log('所有任务处理完成'); frameManager.updateStatus(['所有课程内容已处理完成!']); return; } const nextTask = taskQueue.shift(); const taskName = nextTask.innerText.trim().substring(0, 25) + (nextTask.innerText.length > 25 ? '...' : ''); frameManager.log(`准备处理任务:${taskName}`); frameManager.updateStatus([`即将处理:${taskName}`, `剩余任务:${taskQueue.length}个`]); nextTask.click(); setTimeout(async () => { await processCurrentContent(); setTimeout(processNextTask, 3000); }, 5000); }; processNextTask(); } // 初始化执行 function init() { if (document.readyState !== 'complete') { setTimeout(init, 500); return; } cleanOldFrames(); frameManager.initFrame(); setTimeout(() => { startTaskProcessing(); }, 2000); } init(); })();