// ==UserScript== // @name AES云上授课助手 // @namespace https://scriptcat.org/ // @version 1.0.120-DevPreview 3 // @description AES云上授课助手1.0.120开发灰度预览版3.0 - 针对出头科技云上教学系统的自动刷课脚本 // @Tag 网课自动化 // @author 绫白 // @match https://csjs.web2.superchutou.com/* // @match https://*.superchutou.com/* // @grant GM_notification // @grant GM_log // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // ==================== 配置项 ==================== const CONFIG = { playbackRate: 1.5, autoMute: true, enableBackgroundPlay: true, checkInterval: 2000, startWaitTime: 2000, nextVideoDelay: 2000, showNotification: true, debug: true }; // ==================== 工具函数 ==================== function log(message, type = 'info') { const prefix = '[AES助手]'; const timestamp = new Date().toLocaleTimeString(); const fullMessage = `${prefix} [${timestamp}] ${message}`; if (CONFIG.debug) { const styles = { info: 'color: #2196F3', success: 'color: #4CAF50', warn: 'color: #FF9800', error: 'color: #F44336' }; console.log(`%c${fullMessage}`, styles[type] || styles.info); } GM_log(fullMessage, type); } function notify(title, text, timeout = 5000) { if (CONFIG.showNotification) GM_notification({ title, text, timeout }); log(`${title}: ${text}`, 'success'); } // ==================== 独立矢量LOGO (防ID冲突) ==================== const LOGO_SVG = ``; // ==================== 全局CSS提取 (解决CSP拦截) ==================== const PANEL_CSS = ` #aes-panel { position: fixed; top: 80px; right: 20px; width: 320px; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); z-index: 999999; font-family: '汉仪文黑85W', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #fff; overflow: hidden; transition: all 0.3s ease; user-select: none; } #aes-panel.collapsed { width: 50px; height: 50px; border-radius: 50%; cursor: pointer; } #aes-panel.collapsed .panel-content { display: none; } #aes-panel.collapsed .panel-header { padding: 0; justify-content: center; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } #aes-panel.collapsed .panel-title { display: none; } #aes-panel.collapsed .toggle-btn { display: none; } #aes-panel.collapsed .collapsed-icon { display: flex !important; } .aes-logo { vertical-align: middle; margin-right: 6px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2)); } #aes-panel.collapsed .aes-logo { width: 26px; height: 26px; margin: 0; } .panel-header { background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; cursor: move; } .panel-title { font-size: 14px; font-weight: 600; display: flex; align-items: center; } .collapsed-icon { display: none; align-items: center; justify-content: center; width: 100%; height: 100%; } .toggle-btn { background: rgba(255,255,255,0.2); border: none; color: #fff; width: 24px; height: 24px; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; font-size: 16px; line-height: 1; } .toggle-btn:hover { background: rgba(255,255,255,0.3); } .panel-content { padding: 16px; } .status-card { background: rgba(255,255,255,0.08); border-radius: 12px; padding: 12px; margin-bottom: 12px; } .status-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .status-label { font-size: 12px; color: #888; } .status-value { font-size: 13px; font-weight: 500; display: flex; align-items: center; gap: 4px; } .status-running { color: #4CAF50; } .status-stopped { color: #ff9800; } .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #4CAF50; animation: pulse 1.5s infinite; } .status-dot.stopped { background: #ff9800; animation: none; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .progress-bar { height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; margin: 8px 0; } .progress-fill { height: 100%; background: linear-gradient(90deg, #4CAF50, #8BC34A); border-radius: 3px; transition: width 0.3s ease; } .progress-text { display: flex; justify-content: space-between; font-size: 11px; color: #888; } .video-info { background: rgba(255,255,255,0.05); border-radius: 8px; padding: 10px; margin-top: 8px; } .video-title { font-size: 13px; font-weight: 500; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .video-index { font-size: 11px; color: #888; } .control-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 12px; } .ctrl-btn { background: rgba(255,255,255,0.1); border: none; color: #fff; padding: 10px 6px; border-radius: 10px; cursor: pointer; transition: all 0.2s; font-size: 16px; } .ctrl-btn:hover { background: rgba(255,255,255,0.2); transform: translateY(-1px); } .ctrl-btn.active { background: linear-gradient(135deg, #4CAF50, #8BC34A); } .ctrl-btn:disabled { opacity: 0.5; cursor: not-allowed; } .speed-section { margin-bottom: 12px; } .speed-header { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 12px; } .speed-current { color: #667eea; font-weight: 600; } .speed-buttons { display: flex; gap: 6px; } .speed-btn { flex: 1; background: rgba(255,255,255,0.1); border: none; color: #fff; padding: 8px 4px; border-radius: 8px; cursor: pointer; font-size: 12px; transition: all 0.2s; } .speed-btn:hover { background: rgba(255,255,255,0.2); } .speed-btn.active { background: linear-gradient(135deg, #667eea, #764ba2); } .stats-row { display: flex; justify-content: space-around; padding: 12px 0; border-top: 1px solid rgba(255,255,255,0.1); } .stat-item { text-align: center; } .stat-value { font-size: 18px; font-weight: 600; color: #fff; } .stat-label { font-size: 10px; color: #888; margin-top: 2px; } .stat-value.highlight { color: #4CAF50; } .footer-credit { text-align: center; padding: 10px 0 6px; border-top: 1px solid rgba(255,255,255,0.08); margin-top: 8px; } .footer-credit-text { font-size: 10px; color: #555; line-height: 1.5; } .footer-credit-text span { color: #4facfe; font-weight: bold; } @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) { #aes-panel { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .stats-row, .footer-credit { border-top: none; position: relative; } .stats-row::after, .footer-credit::after { content: ''; position: absolute; left: 0; top: 0; width: 200%; height: 200%; transform-origin: 0 0; transform: scale(0.5); pointer-events: none; box-sizing: border-box; } .stats-row::after { border-top: 1px solid rgba(255,255,255,0.1); } .footer-credit::after { border-top: 1px solid rgba(255,255,255,0.08); } } `; // ==================== UI面板类 ==================== class ControlPanel { constructor(manager) { this.manager = manager; this.panel = null; this.isCollapsed = false; this.isDragging = false; this.dragOffset = { x: 0, y: 0 }; this.updateTimer = null; this.init(); } init() { GM_addStyle(PANEL_CSS); this.createPanel(); this.bindEvents(); this.startUpdate(); log('控制面板已创建(安全模式)', 'success'); } createPanel() { const container = document.createElement('div'); container.id = 'aes-panel-container'; container.innerHTML = `
${LOGO_SVG}AES云上授课助手 ${LOGO_SVG}
运行状态 运行中
00:00 0% 00:00
加载中...
第 0/0 个视频
播放速度 1.5x
0
已完成
0
剩余
0%
总进度
`; document.body.appendChild(container); this.panel = document.getElementById('aes-panel'); } bindEvents() { document.getElementById('toggle-btn').addEventListener('click', (e) => { e.stopPropagation(); this.toggleCollapse(); }); this.panel.querySelector('.panel-header').addEventListener('dblclick', () => this.toggleCollapse()); const header = this.panel.querySelector('.panel-header'); header.addEventListener('mousedown', (e) => { if (this.isCollapsed) return; this.isDragging = true; const rect = this.panel.getBoundingClientRect(); this.dragOffset.x = e.clientX - rect.left; this.dragOffset.y = e.clientY - rect.top; this.panel.style.transition = 'none'; }); document.addEventListener('mousemove', (e) => { if (!this.isDragging) return; this.panel.style.left = Math.max(0, e.clientX - this.dragOffset.x) + 'px'; this.panel.style.top = Math.max(0, e.clientY - this.dragOffset.y) + 'px'; this.panel.style.right = 'auto'; }); document.addEventListener('mouseup', () => { this.isDragging = false; this.panel.style.transition = ''; }); document.getElementById('btn-play').addEventListener('click', () => this.manager.togglePlay()); document.getElementById('btn-prev').addEventListener('click', () => this.manager.playPrevVideo()); document.getElementById('btn-next').addEventListener('click', () => this.manager.playNextVideo()); document.getElementById('btn-mute').addEventListener('click', () => this.manager.toggleMute()); document.querySelectorAll('.speed-btn').forEach(btn => { btn.addEventListener('click', () => { const speed = parseFloat(btn.dataset.speed); this.manager.setSpeed(speed); this.updateSpeedButtons(speed); }); }); } toggleCollapse() { this.isCollapsed = !this.isCollapsed; this.panel.classList.toggle('collapsed', this.isCollapsed); document.getElementById('toggle-btn').textContent = this.isCollapsed ? '+' : '−'; } updateSpeedButtons(speed) { document.querySelectorAll('.speed-btn').forEach(btn => btn.classList.toggle('active', parseFloat(btn.dataset.speed) === speed)); document.getElementById('speed-display').textContent = speed + 'x'; } startUpdate() { this.updateTimer = setInterval(() => this.updateDisplay(), 500); } updateDisplay() { const video = this.manager.video; if (!video) return; const currentTime = video.currentTime || 0; const duration = video.duration || 0; const progress = duration > 0 ? (currentTime / duration * 100) : 0; const isPaused = video.paused; const statusDot = document.getElementById('status-dot'); const statusLabel = document.getElementById('status-label'); const playBtn = document.getElementById('btn-play'); if (isPaused) { statusDot.classList.add('stopped'); statusLabel.textContent = '已暂停'; playBtn.textContent = '▶'; playBtn.classList.remove('active'); } else { statusDot.classList.remove('stopped'); statusLabel.textContent = '运行中'; playBtn.textContent = '⏸'; playBtn.classList.add('active'); } document.getElementById('progress-fill').style.width = progress + '%'; document.getElementById('current-time').textContent = this.manager.formatTime(currentTime); document.getElementById('total-time').textContent = this.manager.formatTime(duration); document.getElementById('progress-percent').textContent = progress.toFixed(0) + '%'; const videoItems = this.manager.getAllVideoItems(); const total = videoItems.length; const safeIndex = Math.max(0, this.manager.currentVideoIndex); const displayCurrentIndex = Math.max(1, this.manager.currentVideoIndex + 1); if (videoItems[this.manager.currentVideoIndex]) { document.getElementById('video-title').textContent = videoItems[this.manager.currentVideoIndex].textContent?.trim().split('时长')[0] || ''; } document.getElementById('video-index').textContent = `第 ${displayCurrentIndex}/${total} 个视频`; document.getElementById('stat-completed').textContent = safeIndex; document.getElementById('stat-remaining').textContent = Math.max(0, total - safeIndex); document.getElementById('stat-percent').textContent = total > 0 ? ((safeIndex / total) * 100).toFixed(0) + '%' : '0%'; const muteBtn = document.getElementById('btn-mute'); muteBtn.textContent = video.muted ? '🔇' : '🔊'; muteBtn.classList.toggle('active', video.muted); } destroy() { if (this.updateTimer) clearInterval(this.updateTimer); document.getElementById('aes-panel-container')?.remove(); } } // ==================== 核心管理类 ==================== class AutoPlayManager { constructor() { this.video = null; this.isPlaying = false; this.isCompleted = false; this.checkTimer = null; this.currentVideoIndex = -1; this.totalVideos = 0; this.panel = null; this.observer = null; this.init(); } init() { log('AES智能云上刷课助手 v1.0.120 [开发灰度预览版] 启动', 'info'); const savedRate = GM_getValue('playbackRate'); if (savedRate) CONFIG.playbackRate = savedRate; const savedMute = GM_getValue('autoMute'); if (savedMute !== undefined) CONFIG.autoMute = savedMute; this.registerMenuCommands(); if (CONFIG.enableBackgroundPlay) this.setupAntiPause(); this.setupRouteListener(); this.panel = new ControlPanel(this); this.tryInitPlayer(); } registerMenuCommands() { // 【BUG修复】弃用箭头函数与首字符Emoji,使用 .bind(this) 强绑定上下文防止沙箱剥离 GM_registerMenuCommand('开始/暂停', this.togglePlay.bind(this)); GM_registerMenuCommand('下一课', this.playNextVideo.bind(this)); GM_registerMenuCommand('查看进度', this.showProgress.bind(this)); } setupAntiPause() { try { Object.defineProperty(document, 'hidden', { get: () => false, configurable: true }); Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true }); } catch (e) {} const forcePlay = () => { if (this.video && this.isPlaying && this.video.paused && !this.video.ended) { this.video.play().catch(() => {}); } }; document.addEventListener('visibilitychange', forcePlay); window.addEventListener('blur', forcePlay); document.addEventListener('mouseleave', forcePlay); } setupRouteListener() { let lastHash = location.hash; window.addEventListener('hashchange', () => { if (location.hash !== lastHash) { lastHash = location.hash; log('页面切换: ' + location.hash, 'info'); this.onRouteChange(); } }); } onRouteChange() { this.isCompleted = false; this.isPlaying = false; this.currentVideoIndex = -1; if (this.checkTimer) { clearInterval(this.checkTimer); this.checkTimer = null; } if (this.observer) { this.observer.disconnect(); this.observer = null; } setTimeout(() => { this.updateCurrentVideoIndex(); this.tryInitPlayer(); }, 2000); } getAllVideoItems() { return Array.from(document.querySelectorAll('ul.ant-list-items > div > a')); } getCurrentVideoId() { const match = location.hash.match(/#\/video\/([^/]+)/); return match ? match[1] : null; } updateCurrentVideoIndex() { const videoItems = this.getAllVideoItems(); const currentId = this.getCurrentVideoId(); this.totalVideos = videoItems.length; if (currentId) { videoItems.forEach((item, index) => { if ((item.getAttribute('href') || '').includes(currentId)) this.currentVideoIndex = index; }); } log(`当前视频索引: ${this.currentVideoIndex + 1}/${this.totalVideos}`, 'info'); } tryInitPlayer() { if (!location.hash.includes('/video/')) return; log('正在查找视频播放器...', 'info'); const findVideo = () => { let v = document.querySelector('#CuPlayer video') || document.querySelector('.vjs-tech') || document.querySelector('video'); if (!v) { try { const iframes = document.querySelectorAll('iframe'); for (let iframe of iframes) { if (iframe.contentDocument) { v = iframe.contentDocument.querySelector('video'); if (v) break; } } } catch (e) {} } return v; }; if (findVideo()) { this.video = findVideo(); this.onPlayerReady(); return; } this.observer = new MutationObserver((mutations, obs) => { const v = findVideo(); if (v) { obs.disconnect(); this.observer = null; this.video = v; this.onPlayerReady(); } }); this.observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { if (this.observer) { this.observer.disconnect(); this.observer = null; if (!this.video) log('未找到视频播放器(超时)', 'error'); } }, 15000); } onPlayerReady() { log('视频播放器已就绪', 'success'); this.updateCurrentVideoIndex(); this.lockPlaybackRate(this.video); if (CONFIG.autoMute) this.video.muted = true; this.setupVideoEvents(); setTimeout(() => this.startAutoPlay(), CONFIG.startWaitTime); } lockPlaybackRate(videoElement) { try { Object.defineProperty(HTMLVideoElement.prototype, 'playbackRate', { get: function() { return this._aes_rate || 1.0; }, set: function(val) { if (this._aes_setting) { this._aes_rate = val; this._aes_setting = false; } else { this._aes_rate = CONFIG.playbackRate; } Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate').set.call(this, this._aes_rate); }, configurable: true }); videoElement._aes_setting = true; videoElement.playbackRate = CONFIG.playbackRate; log(`已启用倍速防护锁: ${CONFIG.playbackRate}x`, 'success'); } catch (e) { log('倍速防护锁安装失败,启用降级防护', 'warn'); videoElement.addEventListener('ratechange', () => { if (Math.abs(videoElement.playbackRate - CONFIG.playbackRate) > 0.01) { videoElement.playbackRate = CONFIG.playbackRate; } }); videoElement.playbackRate = CONFIG.playbackRate; } } applyPlaybackRate() { if (this.video) { this.video._aes_setting = true; this.video.playbackRate = CONFIG.playbackRate; } } setupVideoEvents() { if (!this.video) return; this.video.addEventListener('play', () => { this.isPlaying = true; }); this.video.addEventListener('pause', () => { if (!this.video.ended && this.isPlaying) { setTimeout(() => { if (this.video && this.video.paused && !this.video.ended) this.video.play().catch(() => {}); }, 300); } }); this.video.addEventListener('ended', () => this.onVideoEnded()); this.video.addEventListener('error', () => { log('视频播放错误,尝试重载', 'error'); setTimeout(() => { if (this.video) { this.video.load(); this.video.play().catch(() => {}); } }, 3000); }); } startAutoPlay() { if (!this.video) { this.tryInitPlayer(); return; } this.applyPlaybackRate(); if (CONFIG.autoMute) this.video.muted = true; this.video.play().then(() => { this.isPlaying = true; notify('刷课已开始', `第 ${this.currentVideoIndex + 1}/${this.getAllVideoItems().length} 个视频`); this.startStatusCheck(); }).catch(err => { log('自动播放失败,强制静音重试: ' + err.message, 'error'); this.video.muted = true; this.video.play().catch(() => log('请手动点击播放一次以激活', 'error')); }); } stopAutoPlay() { if (this.checkTimer) { clearInterval(this.checkTimer); this.checkTimer = null; } if (this.video) this.video.pause(); this.isPlaying = false; notify('刷课已停止', '脚本已暂停运行'); } togglePlay() { if (this.video) this.video.paused ? this.video.play().catch(() => {}) : this.video.pause(); } toggleMute() { if (!this.video) return; this.video.muted = !this.video.muted; CONFIG.autoMute = this.video.muted; GM_setValue('autoMute', CONFIG.autoMute); } setSpeed(speed) { CONFIG.playbackRate = speed; GM_setValue('playbackRate', speed); this.applyPlaybackRate(); notify('速度已设置', `播放速度: ${speed}x`); } startStatusCheck() { if (this.checkTimer) clearInterval(this.checkTimer); this.checkTimer = setInterval(() => this.checkVideoStatus(), CONFIG.checkInterval); } checkVideoStatus() { if (!this.video) { this.tryInitPlayer(); return; } if (this.video.paused && !this.video.ended && this.isPlaying) this.video.play().catch(() => {}); } onVideoEnded() { if (this.isCompleted) return; this.isCompleted = true; log('视频播放完成!', 'success'); notify('课程已完成', `第 ${this.currentVideoIndex + 1}/${this.getAllVideoItems().length} 个视频播放完成`); if (this.checkTimer) { clearInterval(this.checkTimer); this.checkTimer = null; } setTimeout(() => this.playNextVideo(), CONFIG.nextVideoDelay); } playNextVideo() { this.updateCurrentVideoIndex(); const videoItems = this.getAllVideoItems(); if (videoItems.length === 0) { notify('切换失败', '未找到课程目录'); return; } const nextIndex = this.currentVideoIndex + 1; if (nextIndex >= videoItems.length) { notify('🎉 刷课完成!', '所有视频已播放完毕'); return; } const nextVideo = videoItems[nextIndex]; const videoName = nextVideo.textContent?.trim().split('时长')[0] || `第${nextIndex + 1}个视频`; let currentHash = location.hash; nextVideo.click(); setTimeout(() => { if (location.hash === currentHash) { const href = nextVideo.getAttribute('href'); if (href) { log('点击切换失效,执行强制Hash跳转', 'warn'); location.hash = href; } } }, 500); notify('切换视频', `正在播放: ${videoName}`); this.isCompleted = false; this.isPlaying = false; this.video = null; setTimeout(() => { this.updateCurrentVideoIndex(); this.tryInitPlayer(); }, 3000); } playPrevVideo() { this.updateCurrentVideoIndex(); const videoItems = this.getAllVideoItems(); if (videoItems.length === 0) return; const prevIndex = this.currentVideoIndex - 1; if (prevIndex < 0) { notify('提示', '已经是第一个视频了'); return; } const prevVideo = videoItems[prevIndex]; let currentHash = location.hash; prevVideo.click(); setTimeout(() => { if (location.hash === currentHash) { const href = prevVideo.getAttribute('href'); if (href) location.hash = href; } }, 500); this.isCompleted = false; this.isPlaying = false; this.video = null; setTimeout(() => { this.updateCurrentVideoIndex(); this.tryInitPlayer(); }, 3000); } showProgress() { const videoItems = this.getAllVideoItems(); this.updateCurrentVideoIndex(); const current = this.currentVideoIndex + 1; const total = videoItems.length; const percent = total > 0 ? ((current / total) * 100).toFixed(1) : 0; let timeInfo = this.video ? `\n当前: ${this.formatTime(this.video.currentTime)} / ${this.formatTime(this.video.duration)}` : ''; notify('当前进度', `第 ${current}/${total} 个视频 (${percent}%)${timeInfo}`); } formatTime(seconds) { if (!seconds || isNaN(seconds)) return '00:00'; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); return h > 0 ? `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}` : `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; } } // ==================== 启动 ==================== if (document.readyState === 'complete') { new AutoPlayManager(); } else { window.addEventListener('load', () => new AutoPlayManager()); } })();