// ==UserScript==
// @name AES云上授课助手
// @namespace https://scriptcat.org/
// @version 1.0.110-Release
// @description AES云上授课助手1.0.110正式版 - 针对出头科技云上教学系统的自动刷课脚本,修复反暂停、倍速锁定、CSP拦截等问题
// @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');
}
// ==================== 全局CSS提取 (解决CSP拦截) ====================
const PANEL_CSS = `
/* 【调整】全局字体应用汉仪文黑85W,并保留系统字体作为兼容回退 */
#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; }
.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; gap: 6px; }
.panel-title::before { content: '⚡'; }
.collapsed-icon { display: none; font-size: 22px; 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; }
/* 【调整】PowerBy重点显示:更纯正的蓝色,并加粗 */
.footer-credit-text span { color: #4facfe; font-weight: bold; }
`;
// ==================== 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 = `
`;
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 currentIndex = this.manager.currentVideoIndex + 1;
const total = videoItems.length;
if (videoItems[this.manager.currentVideoIndex]) {
document.getElementById('video-title').textContent = videoItems[this.manager.currentVideoIndex].textContent?.trim().split('时长')[0] || '';
}
document.getElementById('video-index').textContent = `第 ${currentIndex}/${total} 个视频`;
document.getElementById('stat-completed').textContent = this.manager.currentVideoIndex;
document.getElementById('stat-remaining').textContent = total - this.manager.currentVideoIndex;
document.getElementById('stat-percent').textContent = total > 0 ? ((this.manager.currentVideoIndex / 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.1 稳定版启动', '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() {
GM_registerMenuCommand('▶️ 开始/暂停', () => this.togglePlay());
GM_registerMenuCommand('⏭️ 下一课', () => this.playNextVideo());
GM_registerMenuCommand('📊 查看进度', () => this.showProgress());
}
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) { /* 跨域iframe会报错,忽略 */ }
}
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());
}
})();