让你的Moodle变聪明。
// ==UserScript==
// @name SuperMoodle
// @namespace http://tampermonkey.net/
// @version 7.0.0
// @description 让你的Moodle变聪明。
// @author Chengyu
// @match *://moodle.scnu.edu.cn/*
// @match *://sso.scnu.edu.cn/*
// @match *://*.scnu.edu.cn/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=scnu.edu.cn
// @license GNU GPLv3
// @grant unsafeWindow
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// ==/UserScript==
(function() {
'use strict';
const currentUrl = window.location.href;
const isTopWindow = window.self === window.top;
// ==========================================
// 全局设置:油猴账号密码保险箱 (来自 Script 2)
// ==========================================
const ssoAccount = GM_getValue('sso_account', '');
const ssoPassword = GM_getValue('sso_password', '');
GM_registerMenuCommand('⚙️ 设置/修改 SSO 账号密码 (用于全自动续期)', () => {
const acc = prompt('请输入华师统一认证账号(学号):', ssoAccount);
if (acc !== null) {
const pwd = prompt('请输入统一认证密码:', ssoPassword);
if (pwd !== null) {
GM_setValue('sso_account', acc.trim());
GM_setValue('sso_password', pwd.trim());
alert('✅ 账号保存成功!');
}
}
});
// =========================================================================
// 模块一:SSO 页面处理 (Iframe 或 顶层窗口)
// =========================================================================
if (currentUrl.includes('sso.scnu.edu.cn')) {
// 战场 B:SSO 确认页面 (auth.html) —— 自动点击“确定登录”
if (currentUrl.includes('openapi/auth.html') && !isTopWindow) {
console.log("%c[SSO 黑客] 🥷 发现确认登录页!正在强行点击确定...", "color: #00bcd4; font-weight: bold; background: #222;");
if (typeof unsafeWindow.gotoApp === 'function') {
unsafeWindow.gotoApp();
} else {
const confirmBtn = document.querySelector('a[href*="gotoApp"]');
if (confirmBtn) confirmBtn.click();
}
}
// 战场 C:SSO 登录页面 (user/login.html) —— 自动填表并提交
if (currentUrl.includes('user/login.html') && !isTopWindow) {
if (!ssoAccount || !ssoPassword) {
console.warn('[SSO 突击队] 缺少账号密码,无法自动爆破登录!请在油猴菜单栏设置。');
return;
}
console.log("%c[SSO 突击队] 💣 发现登录表单,正在填弹...", "color: #ff9800; font-weight: bold; background: #222;");
setTimeout(() => {
const accInput = document.getElementById('account');
const pwdInput = document.getElementById('password');
const loginBtn = document.getElementById('btn-password-login');
if (accInput && pwdInput && loginBtn) {
accInput.value = ssoAccount;
pwdInput.value = ssoPassword;
console.log(`%c[SSO 突击队] 账号注入成功,引爆表单!`, "color: #f44336; font-weight: bold;");
loginBtn.click();
}
}, 800);
}
return; // SSO 页面处理完毕,终止后续无关逻辑
}
// =========================================================================
// 模块二:H5P Iframe 嵌套处理 (后台潜伏逻辑)
// =========================================================================
if (!isTopWindow) {
const isH5PIframe = window.name === 'h5player' || currentUrl.includes('h5p');
if (isH5PIframe) {
const TARGET_PROGRESS = 0.95;
const CHECK_INTERVAL = 1000;
function getAllVideos(doc) {
let vids = Array.from(doc.querySelectorAll('video'));
const iframes = doc.querySelectorAll('iframe');
iframes.forEach(ifr => {
try {
if (ifr.contentDocument) vids = vids.concat(getAllVideos(ifr.contentDocument));
} catch (e) {}
});
return vids;
}
let videoInterval = setInterval(() => {
const videos = getAllVideos(document);
if (videos.length === 0) {
window.top.postMessage({ type: 'H5P_VIDEO_LOADING', msg: '穿透寻找内层视频...' }, '*');
return;
}
let mainVideo = null;
let maxDuration = 0;
videos.forEach(v => {
const d = v.duration;
if (d && !isNaN(d) && d > maxDuration) {
maxDuration = d;
mainVideo = v;
}
});
if (!mainVideo || maxDuration === 0) {
window.top.postMessage({ type: 'H5P_VIDEO_LOADING', msg: '等待 CDN 缓冲...' }, '*');
return;
}
if (mainVideo.paused) {
mainVideo.play().catch(e => {
mainVideo.muted = true;
mainVideo.play();
});
}
const currentTime = mainVideo.currentTime || 0;
const progress = currentTime / maxDuration;
window.top.postMessage({ type: 'H5P_VIDEO_PROGRESS', progress: progress, duration: maxDuration }, '*');
if (progress >= TARGET_PROGRESS) clearInterval(videoInterval);
}, CHECK_INTERVAL);
}
return; // 子页面逻辑结束
}
// =========================================================================
// ↓↓↓ 以下全部为 Moodle 顶层窗口 (Top Window) 逻辑 ↓↓↓
// =========================================================================
if (!currentUrl.includes('moodle.scnu.edu.cn')) return;
// ==========================================
// 模块三:后台保活机制 (Moodle Session)
// ==========================================
if (!currentUrl.includes('login/index.php')) {
const CHECK_INTERVAL = 3 * 60 * 1000;
const TIMEOUT_MS = 5000;
const MOODLE_SSO_ENTRY = 'https://sso.scnu.edu.cn/AccountService/openapi/auth.html?client_id=3f86b543c74eed80e7d72658699f6345&response_type=code&redirect_url=https://moodle.scnu.edu.cn/auth/sso/login.php';
console.log("%c[Moodle 综合版] 🚀 侦察与保活模块已挂载!", "color: #00e676; font-weight: bold; background: #222; padding: 4px;");
function showReviveToast() {
const toast = document.createElement('div');
toast.id = 'moodle-ghost-toast';
toast.className = 'alert alert-success alert-block fade in alert-dismissible';
toast.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 999999; box-shadow: 0 4px 12px rgba(0,0,0,0.15); font-weight: bold; padding-right: 40px;';
toast.innerHTML = `
🚀 <strong>系统提示:</strong>刚刚检测到您的登录已失效,插件已在后台为您<strong>狠狠敲打Moodle</strong>!
<br>
<span style="font-weight: normal; font-size: 0.9em;">如果发现页面上的按钮点击无效或报错,请点击 <a href="javascript:window.location.reload();" style="text-decoration: underline; color: #155724; font-weight: bold;">刷新当前页面</a></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close" onclick="this.parentElement.remove();" style="position: absolute; top: 0; right: 0; padding: 0.75rem 1.25rem; color: inherit; background: transparent; border: 0; cursor: pointer; font-size: 1.5rem; line-height: 1;">
<span aria-hidden="true">×</span>
</button>
`;
document.body.appendChild(toast);
setTimeout(() => { if (document.getElementById('moodle-ghost-toast')) document.getElementById('moodle-ghost-toast').remove(); }, 10000);
}
async function checkSessionAlive() {
try {
const res = await fetch('/my/?_=' + Date.now(), { cache: 'no-store', redirect: 'follow' });
const html = await res.text();
const isDead = html.includes('<a target="_self" href="https://moodle.scnu.edu.cn/login/index.php">Moodle Login Page</a>') || html.includes('login/index.php');
if (isDead) {
console.warn(`%c[Moodle 警报] 🚨 抓到死亡指纹!确认掉线!召唤隐藏 Iframe...`, "color: #ff5722; font-weight: bold;");
triggerSilentRenew();
}
} catch (e) { console.error('[Moodle 网络异常]', e); }
}
function triggerSilentRenew() {
const ghostFrame = document.createElement('iframe');
ghostFrame.style.display = 'none';
ghostFrame.id = 'ghost-sso-frame';
ghostFrame.src = MOODLE_SSO_ENTRY;
document.body.appendChild(ghostFrame);
let renewSuccess = false;
const pollTimer = setInterval(async () => {
try {
const frameUrl = ghostFrame.contentWindow.location.href;
if (frameUrl && frameUrl.includes('moodle.scnu.edu.cn')) {
clearInterval(pollTimer);
renewSuccess = true;
setTimeout(async () => {
await updatePageSesskey();
console.log(`%c[Moodle 奇迹] 🎯 Iframe 杀穿 SSO 归来!接应成功!`, "color: #00bcd4; font-weight: bold; font-size: 15px; background: #333; padding: 4px;");
showReviveToast();
ghostFrame.remove();
}, 800);
}
} catch (e) {}
}, 1000);
setTimeout(() => {
if (!renewSuccess) {
clearInterval(pollTimer);
console.error("%c[Moodle 彻底失败] ❌ 终极爆破超时!", "color: #f44336; font-weight: bold;");
ghostFrame.remove();
}
}, TIMEOUT_MS);
}
async function updatePageSesskey() {
try {
const res = await fetch('/my/?_=' + Date.now(), { cache: 'no-store' });
const html = await res.text();
const sesskeyMatch = html.match(/"sesskey":"([^"]+)"/);
if (sesskeyMatch && sesskeyMatch[1] && unsafeWindow.M && unsafeWindow.M.cfg) {
unsafeWindow.M.cfg.sesskey = sesskeyMatch[1];
}
} catch (e) {}
}
unsafeWindow.forceGhostReauth = triggerSilentRenew;
setTimeout(checkSessionAlive, 2000);
setInterval(checkSessionAlive, CHECK_INTERVAL);
}
// ==========================================
// 模块四:音频下载助手
// ==========================================
if (currentUrl.includes('mod/resource/view.php')) {
window.addEventListener('load', function() {
let audioSrc = "";
const sourceTag = document.querySelector('audio.vjs-tech source');
const fallbackLink = document.querySelector('.mediafallbacklink');
if (sourceTag) audioSrc = sourceTag.src;
else if (fallbackLink) audioSrc = fallbackLink.href;
if (!audioSrc) return;
const titleElement = document.querySelector('h1.h2') || document.querySelector('h2');
const fileName = titleElement ? titleElement.innerText.trim() + ".mp3" : "audio_download.mp3";
const downloadBtn = document.createElement('button');
downloadBtn.innerHTML = '💾 点击下载音频文件';
downloadBtn.style.cssText = `margin: 15px 0; padding: 10px 20px; background-color: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 16px; font-weight: bold; display: block; box-shadow: 0 2px 5px rgba(0,0,0,0.2);`;
downloadBtn.onclick = function() {
fetch(audioSrc)
.then(response => response.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
})
.catch(() => window.open(audioSrc, '_blank'));
};
const header = document.querySelector('.page-header-headings') || document.querySelector('.activity-header');
if (header) header.appendChild(downloadBtn);
else document.body.prepend(downloadBtn);
});
}
// ==========================================
// 模块五:水课自动连播 (H5P / 普通资源)
// ==========================================
const isH5PActivity = currentUrl.includes('h5pactivity');
const isFsResource = currentUrl.includes('fsresource');
if (isH5PActivity || isFsResource) {
const STORAGE_KEY = '__gemini_autoplay_enabled__';
let isEnabled = localStorage.getItem(STORAGE_KEY) === 'true';
const modeText = isH5PActivity ? "H5P 互动视频" : "普通视频资源";
// 注入专业版 CSS 样式
const style = document.createElement('style');
style.innerHTML = `
#gemini-ui-container { position: fixed; top: 80px; right: 20px; width: 290px; background: rgba(15, 23, 42, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; color: #e2e8f0; font-family: system-ui, -apple-system, sans-serif; font-size: 14px; z-index: 999999; box-shadow: 0 10px 30px rgba(0,0,0,0.5); overflow: hidden; transition: all 0.3s ease; }
.gemini-header { display: flex; align-items: center; padding: 12px 16px; background: rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255,255,255,0.05); font-weight: 600; font-size: 15px; letter-spacing: 0.5px; }
.gemini-dot { width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; margin-right: 10px; box-shadow: 0 0 8px transparent; transition: all 0.3s; }
.gemini-dot.active { background: #10b981; box-shadow: 0 0 8px #10b981; }
.gemini-dot.warning { background: #fbbf24; box-shadow: 0 0 8px #fbbf24; }
.gemini-body { padding: 16px; line-height: 1.6; min-height: 70px; }
.gemini-footer { display: flex; gap: 10px; padding: 12px 16px; background: rgba(0, 0, 0, 0.2); border-top: 1px solid rgba(255,255,255,0.05); }
.gemini-btn { flex: 1; padding: 8px 0; border: none; border-radius: 6px; font-weight: 500; cursor: pointer; transition: all 0.2s; font-size: 13px; }
.gemini-btn-primary { background: #0ea5e9; color: #fff; } .gemini-btn-primary:hover { background: #0284c7; }
.gemini-btn-secondary { background: #475569; color: #fff; } .gemini-btn-secondary:hover { background: #334155; }
.gemini-btn-danger { background: #ef4444; color: #fff; } .gemini-btn-danger:hover { background: #dc2626; }
.gemini-highlight { color: #38bdf8; font-weight: bold; } .gemini-success { color: #10b981; font-weight: bold; } .gemini-warning { color: #fbbf24; font-weight: bold; }
`;
document.head.appendChild(style);
const uiBox = document.createElement('div');
uiBox.id = 'gemini-ui-container';
document.body.appendChild(uiBox);
function renderUI() {
if (isEnabled) {
uiBox.innerHTML = `
<div class="gemini-header"><span class="gemini-dot active" id="gemini-status-dot"></span><span class="gemini-title">自动连播 (运行中)</span></div>
<div class="gemini-body" id="gemini-status-text">正在初始化监控...<br><span style="color:#94a3b8;font-size:12px;">模式: ${modeText}</span></div>
<div class="gemini-footer"><button id="btn-stop" class="gemini-btn gemini-btn-danger">停止连播</button></div>
`;
document.getElementById('btn-stop').addEventListener('click', () => {
localStorage.setItem(STORAGE_KEY, 'false');
isEnabled = false;
location.reload();
});
} else {
uiBox.innerHTML = `
<div class="gemini-header"><span class="gemini-dot"></span><span class="gemini-title">自动连播 (已暂停)</span></div>
<div class="gemini-body">检测到: <span class="gemini-highlight">${modeText}</span><br><span style="color:#94a3b8;font-size:13px;margin-top:5px;display:block;">点击启用后,后续剧集将全自动连播,无需再次确认。</span></div>
<div class="gemini-footer">
<button id="btn-start" class="gemini-btn gemini-btn-primary">🚀 启用连播</button>
<button id="btn-close" class="gemini-btn gemini-btn-secondary">关闭</button>
</div>
`;
document.getElementById('btn-start').addEventListener('click', () => {
localStorage.setItem(STORAGE_KEY, 'true');
isEnabled = true;
renderUI();
startScript();
});
document.getElementById('btn-close').addEventListener('click', () => { uiBox.style.display = 'none'; });
}
}
function updateStatus(html, isWarning = false) {
const statusEl = document.getElementById('gemini-status-text');
const dotEl = document.getElementById('gemini-status-dot');
if (statusEl) statusEl.innerHTML = html;
if (dotEl) dotEl.className = isWarning ? 'gemini-dot warning' : 'gemini-dot active';
}
renderUI();
if (isEnabled) startScript();
function getMoodleId(url) {
const match = url.match(/[?&]id=(\d+)/);
return match ? match[1] : null;
}
function jumpToNextId(reason = "视频看完啦!") {
const currentId = getMoodleId(currentUrl);
if (!currentId) { updateStatus(`<span class="gemini-warning">⚠️ 网址中未发现 id 参数,无法跳转</span>`, true); return; }
const nextId = parseInt(currentId) + 1;
const newUrl = currentUrl.replace(/([?&]id=)(\d+)/, (match, p1, p2) => p1 + nextId);
updateStatus(`<span class="gemini-success">✅ ${reason}</span><br>当前ID: ${currentId}<br>即将跳转: <span class="gemini-highlight">${nextId}</span><br><span style="color:#94a3b8;font-size:12px;">(2秒后自动执行...)</span>`);
setTimeout(() => { window.location.href = newUrl; }, 2000);
}
function startScript() {
if (isH5PActivity) {
const TARGET_PROGRESS = 0.95;
const NO_VIDEO_JUMP_DELAY = 8000;
let hasReceivedVideoMsg = false;
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'H5P_VIDEO_PROGRESS') {
hasReceivedVideoMsg = true;
const percent = (event.data.progress * 100).toFixed(1);
const totalMins = (event.data.duration / 60).toFixed(1);
updateStatus(`<span class="gemini-highlight">▶ 正在吃掉讨厌的H5P视频</span><br>视频长度: ${totalMins} 分钟<br>真实进度: <span class="gemini-success">${percent}%</span> (目标:95%)`);
if (event.data.progress >= TARGET_PROGRESS) jumpToNextId(`达到 95% 安全进度`);
} else if (event.data && event.data.type === 'H5P_VIDEO_LOADING') {
hasReceivedVideoMsg = true;
updateStatus(`<span class="gemini-warning">⏳ 锁定 H5P 组件</span><br>${event.data.msg}`, true);
}
});
setTimeout(() => { if (!hasReceivedVideoMsg) jumpToNextId("未探测到有效H5P视频内容,自动跳过"); }, NO_VIDEO_JUMP_DELAY);
} else if (isFsResource) {
const CHECK_INTERVAL = 800;
const JUMP_THRESHOLD = 0.998;
const NO_VIDEO_WAIT_TIME = 6;
let hasTriggeredJump = false;
let noVideoCounter = 0;
let videoFound = false;
try { window.alert = function() { return true; }; window.confirm = function() { return true; }; } catch (e) {}
function getPageProgress() {
const specificNodes = document.querySelectorAll('.num-bfjd, .number, .process-num');
for (let node of specificNodes) {
const text = node.innerText.trim();
if (/\d/.test(text) && !text.includes("需要") && !text.includes("达到")) {
const num = parseFloat(text);
if (!isNaN(num)) return num;
}
}
const bodyText = document.body.innerText;
const progressRegex = /(?:播放进度|观看进度|完成度|进度|score).{0,15}?(\d+(?:\.\d+)?)%/gi;
let match;
while ((match = progressRegex.exec(bodyText)) !== null) {
const numStr = match[1], fullMatchStr = match[0], index = match.index;
const contextStart = Math.max(0, index - 15);
const contextEnd = Math.min(bodyText.length, index + fullMatchStr.length + 20);
const context = bodyText.substring(contextStart, contextEnd);
if (context.includes("需要") || context.includes("达到") || context.includes("要求")) continue;
return parseFloat(numStr);
}
return null;
}
function tryPlayVideo(video) {
if (!video) return;
const playBtn = document.querySelector('.vjs-big-play-button') || document.querySelector('button[title="Play"]');
if (playBtn && playBtn.offsetParent) playBtn.click();
else video.play().catch(() => { video.muted = true; video.play(); });
}
setInterval(() => {
if (hasTriggeredJump) return;
const pageProgress = getPageProgress();
if (pageProgress !== null && pageProgress >= 100) {
hasTriggeredJump = true;
jumpToNextId(`检测到页面进度 ${pageProgress}%`);
return;
}
const video = document.querySelector('video');
if (!video) {
if (document.readyState !== 'complete') return;
noVideoCounter++;
if (!videoFound) updateStatus(`<span class="gemini-warning">🔍 正在寻找视频标签... (${noVideoCounter}s)</span>`, true);
if (noVideoCounter * (CHECK_INTERVAL/1000) >= NO_VIDEO_WAIT_TIME) {
hasTriggeredJump = true;
jumpToNextId("非视频资源页,自动跳过");
}
return;
}
videoFound = true;
noVideoCounter = 0;
const duration = video.duration || 0;
const currentTime = video.currentTime || 0;
const percent = duration > 0 ? (currentTime / duration) : 0;
const timeLeft = duration - currentTime;
const isVideoDone = video.ended || percent > JUMP_THRESHOLD || (duration > 0 && timeLeft < 0.5);
if (isVideoDone) {
if (pageProgress !== null && pageProgress < 100) {
updateStatus(`<span class="gemini-warning">⚠️ 视频已播完,但进度仅 ${pageProgress}%</span><br>正在重播刷时长...`, true);
video.currentTime = 0;
tryPlayVideo(video);
return;
}
const bodyText = document.body.innerText;
if (bodyText.includes("状态为未完成") || bodyText.includes("状态为:未完成")) {
updateStatus(`<span class="gemini-warning">⏳ 等待状态变更为“已完成”...</span>`, true);
return;
}
hasTriggeredJump = true;
jumpToNextId("视频播放完成");
} else {
if (video.paused) {
updateStatus(`<span class="gemini-warning">⏸️ 异常暂停,尝试恢复...</span>`, true);
tryPlayVideo(video);
} else {
let infoText = `<span class="gemini-highlight">▶ 视频监控中</span><br>播放进度: ${Math.floor(percent * 100)}%`;
if (pageProgress !== null) infoText += `<br>页面进度: <span class="gemini-success">${pageProgress}%</span>`;
updateStatus(infoText);
}
}
}, CHECK_INTERVAL);
}
}
}
})();