// ==UserScript== // @name 全国煤炭行业现代远程教育培训网自动播放 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @author zhkali // @author 更新时间:2026-10-19 21:27:00 // @description 自动播放、自动下一节、普通弹窗处理、5分钟卡死恢复、随机刷新、完成通知 // @match https://www.coaledu.net/* // @match https://coaledu.net/* // @match http://www.coaledu.net/* // @match http://coaledu.net/* // @run-at document-idle // @grant GM_notification // ==/UserScript== (function () { 'use strict'; // ================================================== // 1. 参数配置 // ================================================== const CONFIG = { mainInterval: 3000, monitorInterval: 10000, stuckTimeout: 5 * 60 * 1000, playCooldown: 2500, actionCooldown: 2500, // 普通提示处理 handleContinuePrompt: true, // 立即观看按钮处理 handleWatchButton: true, // 随机刷新:保留原版4至6分钟机制 autoRefresh: true, minRefreshMinutes: 4, maxRefreshMinutes: 6, // 发生课程切换或播放恢复后,至少等待此时间再刷新 refreshConflictDelay: 30 * 1000, // 验证提示期间每隔多久重新检查刷新条件 verificationRetryDelay: 60 * 1000, debug: true }; const SELECTORS = { currentCourse: 'a.nowrap.on.subset-class', courseLink: 'a.nowrap.subset-class', progress: '.finish-percent', video: 'video', playButton: '.ccH5PlayBtn', watchButton: '.info-button.watch', // 仅检测需要人工处理的验证提示 verification: [ '.verifyFaceMask', '.playback-guard-message-box' ] }; // ================================================== // 2. 运行状态 // ================================================== let activeCourseKey = ''; let completedCourseKey = ''; let activatedCourseKey = ''; let lastActionAt = 0; let lastPlayAttemptAt = 0; let lastCriticalActionAt = 0; let lastMovementAt = Date.now(); let lastVideoTime = null; let lastProgress = null; let verificationNotified = false; let finishedNotified = false; let missingCourseLogged = false; let refreshTimer = null; const clickedWatchButtons = new WeakSet(); const clickedContinueButtons = new WeakSet(); // ================================================== // 3. 日志、通知和页面状态 // ================================================== function log(...args) { if (CONFIG.debug) { console.log('[煤炭教育助手]', ...args); } } function notify(message, title = '课程助手') { log(message); try { if (typeof GM_notification === 'function') { GM_notification({ title, text: message, timeout: 5000 }); } } catch (error) { console.warn('[煤炭教育助手] 通知失败:', error); } } function showStatus(message) { let panel = document.getElementById( 'coaledu-auto-play-status' ); if (!panel) { panel = document.createElement('div'); panel.id = 'coaledu-auto-play-status'; Object.assign(panel.style, { position: 'fixed', right: '12px', bottom: '12px', zIndex: '2147483647', maxWidth: '360px', padding: '10px 14px', borderRadius: '6px', background: '#263238', color: '#fff', fontSize: '13px', lineHeight: '1.5', boxShadow: '0 2px 8px rgba(0,0,0,.25)', pointerEvents: 'none' }); document.body.appendChild(panel); } panel.textContent = message; } function isVisible(element) { if (!element || !element.isConnected) { return false; } const style = getComputedStyle(element); return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity) !== 0 && element.getClientRects().length > 0; } function markCriticalAction(reason) { lastCriticalActionAt = Date.now(); log('关键操作:', reason); } // ================================================== // 4. 课程识别和进度读取 // ================================================== function getCurrentCourse() { return document.querySelector( SELECTORS.currentCourse ); } function getCourseKey(link) { if (!link) return ''; // 同时使用 href 与文字,避免多个 # 链接被当成同一课程 const href = link.getAttribute('href') || ''; const text = (link.textContent || '').trim(); return `${href}|${text}`; } function getCourseProgress(courseLi) { if (!courseLi) return null; const element = courseLi.querySelector( SELECTORS.progress ); if (!element) return null; const text = element.textContent.trim(); const match = text.match(/(\d+(?:\.\d+)?)/); return match ? Number(match[1]) : null; } function getVideo() { return document.querySelector(SELECTORS.video); } function getVideoTime() { const video = getVideo(); if (!video || !Number.isFinite(video.currentTime)) { return null; } return video.currentTime; } // ================================================== // 5. 验证与风控提示检测 // ================================================== function needsManualCheck() { const detected = SELECTORS.verification.some( selector => { return Array.from( document.querySelectorAll(selector) ).some(isVisible); } ); // 不将通用 .ccMask 单独判定为验证弹窗, // 因为它也可能是普通播放器遮罩。 if (detected) { if (!verificationNotified) { notify( '检测到身份验证或风控提示,请手动处理后继续。', '需要人工处理' ); verificationNotified = true; } showStatus('检测到验证提示,请手动处理'); return true; } verificationNotified = false; return false; } // ================================================== // 6. 播放器控制与自动播放 // ================================================== function clickPlayerButton() { if (needsManualCheck()) return false; const button = document.querySelector( SELECTORS.playButton ); if (isVisible(button)) { button.click(); log('已尝试点击播放器播放按钮'); return true; } return false; } function tryPlay(force = false) { if (needsManualCheck()) return; const now = Date.now(); if ( !force && now - lastPlayAttemptAt < CONFIG.playCooldown ) { return; } lastPlayAttemptAt = now; const video = getVideo(); if (video && video.paused) { try { const result = video.play(); if ( result && typeof result.catch === 'function' ) { result.catch(error => { log('视频自动播放未成功:', error); if (!needsManualCheck()) { clickPlayerButton(); } }); } return; } catch (error) { log('调用 video.play() 失败:', error); clickPlayerButton(); return; } } // 页面尚未找到 video 时,尝试播放器播放按钮 if (!video) { clickPlayerButton(); } } // ================================================== // 7. 普通“继续”提示 // ================================================== function handleContinuePrompt() { if (!CONFIG.handleContinuePrompt) return; if (needsManualCheck()) return; const elements = document.querySelectorAll( 'button, button span' ); for (const element of elements) { if (!isVisible(element)) continue; const button = element.matches('button') ? element : element.closest('button'); if (!button || !isVisible(button)) continue; if (clickedContinueButtons.has(button)) continue; // 不触碰人脸验证或风控弹窗内的按钮 if ( button.closest( '.verifyFaceMask, .playback-guard-message-box' ) ) { continue; } const text = ( button.innerText || button.textContent || '' ).trim(); if (text.includes('继续')) { clickedContinueButtons.add(button); button.click(); log('已点击普通“继续”按钮'); break; } } } // ================================================== // 8. “立即观看”按钮 // ================================================== function handleWatchButton() { if (!CONFIG.handleWatchButton) return; if (needsManualCheck()) return; const button = document.querySelector( SELECTORS.watchButton ); if (!isVisible(button)) return; if (clickedWatchButtons.has(button)) return; // 同一个 DOM 按钮只自动尝试一次 clickedWatchButtons.add(button); log('检测到“立即观看”按钮'); setTimeout(() => { if (!needsManualCheck() && isVisible(button)) { button.click(); log('已点击“立即观看”按钮'); } }, 500); } // ================================================== // 9. 查找下一节课程 // ================================================== function findNextCourse(courseLi) { if (!courseLi) return null; let sibling = courseLi.nextElementSibling; while (sibling) { if (sibling.matches('li')) { const link = sibling.querySelector( SELECTORS.courseLink ); if (link) return link; } sibling = sibling.nextElementSibling; } return null; } // ================================================== // 10. 主逻辑:播放、切换、完成通知 // ================================================== function mainLoop() { if (needsManualCheck()) return; handleContinuePrompt(); handleWatchButton(); const current = getCurrentCourse(); if (!current) { showStatus('脚本已运行,但暂未识别当前课程'); if (!missingCourseLogged) { log( '未找到当前课程选择器:', SELECTORS.currentCourse ); log('当前网址:', location.href); missingCourseLogged = true; } return; } missingCourseLogged = false; const courseLi = current.closest('li'); if (!courseLi) { log('已找到课程链接,但未找到所属 li 元素'); return; } const courseKey = getCourseKey(current); const progress = getCourseProgress(courseLi); const now = Date.now(); // 当前课程发生变化时,重置进度监控基准 if (courseKey !== activeCourseKey) { activeCourseKey = courseKey; lastMovementAt = now; lastVideoTime = getVideoTime(); lastProgress = progress; markCriticalAction('识别到课程切换'); log('当前课程:', current.textContent.trim()); showStatus( '正在运行:' + current.textContent.trim() ); // 新课程首次识别时,如果尚无视频和播放器按钮, // 只尝试点击当前课程一次,避免反复重载。 if ( !getVideo() && !document.querySelector(SELECTORS.playButton) && activatedCourseKey !== courseKey ) { activatedCourseKey = courseKey; log('尚未找到播放器,尝试打开当前课程'); current.click(); setTimeout(() => { if (!needsManualCheck()) { tryPlay(true); } }, 1500); return; } } // 进度明确达到100%后,才切换下一节 if (progress !== null && progress >= 100) { if (completedCourseKey === courseKey) { return; } if (now - lastActionAt < CONFIG.actionCooldown) { return; } const next = findNextCourse(courseLi); if (next) { completedCourseKey = courseKey; lastActionAt = now; markCriticalAction('切换下一节课程'); log( '当前课程已完成,切换下一节:', next.textContent.trim() ); next.click(); setTimeout(() => { if (!needsManualCheck()) { tryPlay(true); } }, 1500); } else { completedCourseKey = courseKey; if (!finishedNotified) { finishedNotified = true; notify( '当前课程列表已完成。', '学习结束' ); showStatus('当前课程列表已完成'); } } return; } // 当前课程尚未完成时,尝试恢复正常播放 tryPlay(); } // ================================================== // 11. 5分钟停滞检测与恢复 // ================================================== function monitorProgress() { if (needsManualCheck()) { // 验证期间暂停停滞计时 lastMovementAt = Date.now(); lastVideoTime = null; return; } const current = getCurrentCourse(); if (!current) return; const courseKey = getCourseKey(current); if (courseKey !== activeCourseKey) return; const courseLi = current.closest('li'); const progress = getCourseProgress(courseLi); const videoTime = getVideoTime(); let moved = false; // 检测实际视频播放时间变化 if ( videoTime !== null && lastVideoTime !== null && videoTime > lastVideoTime + 0.2 ) { moved = true; } // 检测网站显示的课程完成百分比变化 if ( progress !== null && lastProgress !== null && progress > lastProgress ) { moved = true; } if (moved) { lastMovementAt = Date.now(); log('已检测到视频或课程进度变化'); } if (videoTime !== null) { lastVideoTime = videoTime; } if (progress !== null) { lastProgress = progress; } if ( Date.now() - lastMovementAt >= CONFIG.stuckTimeout ) { log('超过5分钟没有检测到进度变化,尝试恢复播放'); markCriticalAction('尝试恢复停滞视频'); tryPlay(true); notify( '超过5分钟没有检测到播放进度,已尝试恢复。请检查播放器、网络或验证提示。', '播放停滞提醒' ); // 防止接下来的每10秒都重复恢复 lastMovementAt = Date.now(); } } // ================================================== // 12. 独立随机刷新调度器 // ================================================== function getRandomRefreshDelay() { const min = CONFIG.minRefreshMinutes * 60 * 1000; const max = CONFIG.maxRefreshMinutes * 60 * 1000; return Math.floor( Math.random() * (max - min + 1) ) + min; } function scheduleRefresh(delay = null) { if (!CONFIG.autoRefresh) return; if (finishedNotified) return; if (refreshTimer !== null) { clearTimeout(refreshTimer); refreshTimer = null; } const nextDelay = delay === null ? getRandomRefreshDelay() : delay; log( '下一次刷新定时检查约在', Math.round(nextDelay / 60000), '分钟后' ); refreshTimer = setTimeout(() => { refreshTimer = null; // 验证提示出现时不刷新,延后再检查 if (needsManualCheck()) { scheduleRefresh( CONFIG.verificationRetryDelay ); return; } // 防止刷新与课程切换或播放恢复同时发生 const timeSinceCriticalAction = Date.now() - lastCriticalActionAt; if ( lastCriticalActionAt > 0 && timeSinceCriticalAction < CONFIG.refreshConflictDelay ) { const remaining = CONFIG.refreshConflictDelay - timeSinceCriticalAction; log( '刚发生切换或恢复操作,延后刷新', Math.ceil(remaining / 1000), '秒' ); scheduleRefresh(remaining + 1000); return; } if (finishedNotified) { log('课程列表已完成,停止刷新'); return; } log('执行独立定时刷新'); location.reload(); }, nextDelay); } // ================================================== // 13. 启动 // ================================================== if (!/coursePlay\.html/i.test(location.pathname)) { console.log( '[煤炭教育助手] 当前不是 coursePlay.html 页面,不执行。' ); return; } log('脚本初始化成功'); log('当前网址:', location.href); showStatus('课程助手已启动,正在检测页面'); mainLoop(); setInterval(mainLoop, CONFIG.mainInterval); setInterval(monitorProgress, CONFIG.monitorInterval); if (CONFIG.autoRefresh) { scheduleRefresh(); } })(); // 更新时间:2026-10-19 21:30:00