// ==UserScript==
// @name 广东教师继续教育(免费全自动挂机,1倍速)
// @namespace http://tampermonkey.net/zzzzzzys_广东教师继续教育
// @version 1.0.7-free
// @copyright zzzzzzys.All Rights Reserved.
// @description 广东教师继续教育公需课全自动挂机,1倍速,防挂机检测,无限制看完所有课程。
// @author zzzzzzys (已优化免费)
// @match https://jsxx.gdedu.gov.cn/*study/*course/*
// @match https://jsxx.gdedu.gov.cn/*study/course/*
// @match https://jsxx.gds.edu.cn/*
// @match https://jsxx.gds.edu.cn/*study/*course*
// @require https://fastly.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js
// @resource https://cdn.staticfile.org/limonte-sweetalert2/11.7.1/sweetalert2.min.css
// @require https://fastly.jsdelivr.net/npm/sweetalert2@11.12.2/dist/sweetalert2.all.min.js
// @grant unsafeWindow
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_info
// @grant GM_addStyle
// @run-at document-end
// @license
// ==/UserScript==
class Runner {
constructor() {
this.runner = null
this.run()
}
run() {
const url = location.href;
if (url.includes("study/course")) {
this.runner = new Course("channel-gdedu")
}
}
}
class Course {
constructor(channel = "channel-my") {
this.panel = new AuthWindow({
VIPBtnText: "全自动挂机(免费版)",
VIPInfo: "所有课程自动连续播放(1倍速)"
})
this.channel = channel
this.VIP = true
this.running = false
this.antiIdleTimer = null
this.init()
}
init() {
// 隐藏付费相关元素
this.hideAuthElements();
this.panel.setTip("全功能已启用(免费版),自动连续播放所有课程(1倍速)");
this.panel.setOnBegin(() => {
if (!this.running) {
this.running = true
console.log("免费全自动运行中...")
// 启动防挂机心跳:每15分钟轻微滚动一次
this.startAntiIdle();
this.run().then(r => {
this.running = false
this.stopAntiIdle();
})
}
})
// 三秒后自动开始
Swal.fire({
title: "提示",
text: "脚本三秒后自动开始!将自动完成所有课程",
icon: 'info',
timer: 3000,
confirmButtonText: '确定',
timerProgressBar: true,
willClose: () => {
this.panel.startAutomation()
}
}).catch(e => {
this.panel.startAutomation()
});
}
hideAuthElements() {
if (this.panel.authInput) this.panel.authInput.container.style.display = 'none';
if (this.panel.verifyBtn) this.panel.verifyBtn.style.display = 'none';
if (this.panel.vipBtn) this.panel.vipBtn.style.display = 'none';
const links = this.panel.container.querySelectorAll('.auth-link');
links.forEach(link => link.style.display = 'none');
}
startAntiIdle() {
this.antiIdleTimer = setInterval(() => {
window.scrollBy(0, 1);
console.log('防挂机心跳');
}, 15 * 60 * 1000);
}
stopAntiIdle() {
if (this.antiIdleTimer) {
clearInterval(this.antiIdleTimer);
this.antiIdleTimer = null;
}
}
goNextVideo() {
try {
if (typeof unsafeWindow.goNext === 'function') {
unsafeWindow.goNext();
} else {
throw new Error('goNext not found');
}
} catch (e) {
const cur = document.querySelector('.section.z-crt');
if (cur) {
const all = Array.from(document.querySelectorAll('.section'));
const idx = all.indexOf(cur);
if (idx !== -1 && idx < all.length - 1) {
all[idx + 1].click();
}
}
}
}
async run() {
try {
// 外层循环,每次重新获取当前课程列表
while (true) {
const sections = await Utils.getStudyNode('.section', 'nodeList', 5000);
if (!sections || sections.length === 0) {
console.log('未找到课程节点,可能已完成');
break;
}
// 寻找当前激活的课程
let currentSection = null;
for (let i = 0; i < sections.length; i++) {
if (sections[i].classList.contains('z-crt')) {
currentSection = sections[i];
break;
}
}
if (!currentSection) {
console.log('未找到激活课程,尝试选择第一个未完成项');
for (let i = 0; i < sections.length; i++) {
if (!sections[i].classList.contains('completed')) {
currentSection = sections[i];
break;
}
}
if (!currentSection) break; // 所有课程都已完成
}
console.log('当前课程:', currentSection.querySelector('span')?.innerText);
// 检查是否已经完成
const completed = await this.checkStatus();
if (completed) {
console.log('已完成,跳转下一个');
this.goNextVideo();
await sleep(3000);
continue;
}
// 获取视频元素
const video = await Utils.getStudyNode('video', 'node', 10000);
if (!video) {
console.error('视频元素未找到,刷新页面');
location.reload();
return;
}
video.muted = true;
video.volume = 0;
video.currentTime = 0;
await video.play();
video.currentTime = 0;
// 等待视频播放完成
await this.waitForVideoEnd(video);
// 弹出跳转提示
await Swal.fire({
title: "完成当前课程",
text: '5秒后自动跳转下一个',
icon: 'success',
confirmButtonColor: "#FF4DAFFF",
confirmButtonText: "确定",
timer: 5000,
timerProgressBar: true,
allowOutsideClick: false,
allowEscapeKey: false,
});
this.goNextVideo();
await sleep(3000);
}
this.finish();
} catch (e) {
console.error(e);
Swal.fire({
title: "出错",
text: e.toString(),
icon: 'error',
confirmButtonColor: "#FF4DAFFF",
confirmButtonText: "确定",
});
}
}
sendMsg(msg) {
const channel = new BroadcastChannel(this.channel);
channel.postMessage(msg);
}
finish() {
if (Swal) {
this.sendMsg('finish');
Swal.fire({
title: "学习完成!",
text: `全部课程已学完!`,
icon: 'success',
confirmButtonColor: "#FF4DAFFF",
confirmButtonText: "确定",
timerProgressBar: true,
timer: 0,
willClose: () => { }
});
setTimeout(() => {
window.close();
}, 10000);
}
}
async waitForVideoEnd(video) {
return new Promise(resolve => {
const checkInterval = setInterval(async () => {
try {
if (video && video.paused) {
await video.play();
}
if (!video.src) {
console.error("视频源丢失,重新加载页面");
clearInterval(checkInterval);
location.reload();
return;
}
// 自动关闭答题弹窗
try {
if ($('#questionDiv').length) {
$('#questionDiv').stopTime('C');
$('.mylayer-closeico').trigger('click');
}
} catch (e) { }
const done = await this.checkStatus();
if (done) {
clearInterval(checkInterval);
resolve();
}
} catch (e) {
console.error("监控循环出错", e);
clearInterval(checkInterval);
setTimeout(() => location.reload(), 3000);
}
}, 3000);
video.addEventListener('ended', () => {
clearInterval(checkInterval);
resolve();
}, { once: true });
});
}
async checkStatus() {
try {
const dom = await Utils.getStudyNode('.g-study-dt', 'node', 3000);
const time = document.querySelector('#viewTimeTxt');
if (!dom || !time) return false;
const requireText = dom.querySelector('span')?.innerText;
if (!requireText) return false;
const require = parseInt(requireText);
const current = parseInt(time.innerText);
if (isNaN(require) || isNaN(current)) return false;
return current >= require;
} catch (e) {
console.error("检查状态出错", e);
return false;
}
}
}
class Utils {
static async getStudyNode(selector, type = 'node', timeout = 10000) {
return new Promise((resolve, reject) => {
if (!['node', 'nodeList'].includes(type)) {
reject('Invalid type parameter. Expected "node" or "nodeList"');
return;
}
const cleanup = (timeoutId, intervalId) => {
clearTimeout(timeoutId);
clearInterval(intervalId);
};
const handleSuccess = (result, timeoutId, intervalId) => {
console.log(`${selector} ready!`);
cleanup(timeoutId, intervalId);
resolve(result);
};
const handleFailure = (timeoutId, intervalId) => {
cleanup(timeoutId, intervalId);
resolve(null);
};
const checkNode = () => {
try {
if (type === 'node') {
return document.querySelector(selector);
}
const nodes = document.querySelectorAll(selector);
return nodes.length > 0 ? nodes : null;
} catch (error) {
reject('节点检查错误:', error)
}
};
const intervalId = setInterval(() => {
const result = checkNode();
if (result) {
handleSuccess(result, timeoutId, intervalId);
} else {
console.log(`等待节点: ${selector}...`);
}
}, 1000);
const timeoutId = setTimeout(() => {
console.error(`节点获取超时: ${selector}`);
handleFailure(timeoutId, intervalId);
}, timeout);
});
}
}
class AuthWindow {
constructor({ VIPBtnText = "全自动挂机", VIPInfo = "免费全功能版" }) {
this.storageKey = 'AuthData';
this.injectGlobalStyles();
this.initDOM();
this.show();
this.setVIPBtnText(VIPBtnText);
this.setTip(VIPInfo);
}
injectGlobalStyles() {
GM_addStyle(`
.auth-window { position: fixed; bottom: 10px; right: 10px; z-index: 9999; background: white; padding: 24px; border-radius: 12px; box-shadow: 0 6px 30px rgba(0,0,0,0.15); border: 1px solid #e4e7ed; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-width: 320px; transform: translateY(20px); opacity: 0; transition: all 0.3s ease; } .auth-window.visible { transform: translateY(0); opacity: 1; } .auth-title { margin: 0 0 16px; font-size: 20px; color: #2c3e50; font-weight: 600; display: flex; align-items: center; gap: 8px; } .auth-version { font-size: 12px; color: #95a5a6; font-weight: normal; } .auth-tip { margin: 0 0 20px; color: #2ecc71; font-size: 14px; line-height: 1.5; } .input-group { margin-bottom: 18px; } .input-label { display: block; margin-bottom: 6px; color: #34495e; font-size: 14px; font-weight: 500; } .input-field { width: 80%; padding: 10px 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; transition: border-color 0.2s; } .input-field:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 3px rgba(52,152,219,0.1); } .auth-button { width: 100%; padding: 12px; background: #3498db; color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; } .auth-button:hover { background: #2980b9; transform: translateY(-1px); } .auth-button:active { transform: translateY(0); } .error-message { color: #e74c3c; font-size: 13px; margin-top: 8px; padding: 8px; background: #fdeded; border-radius: 6px; display: none; } .control-panel { opacity: 1; transform: translateY(10px); transition: all 0.3s ease; } .control-panel.visible { opacity: 1; transform: translateY(0); } .auth-button[disabled] { background: #bdc3c7 !important; cursor: not-allowed; } .window-toggle:hover .toggle-icon { animation: bounce 0.6s; } .toggle-icon { width: 20px; height: 20px; transition: transform 0.3s ease; } @keyframes bounce { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(4px); } }
.vip-btn { width: 100%; position: relative; padding: 12px 24px; border: none; border-radius: 8px; background: linear-gradient(135deg, #ffd700 0%, #ffd900 30%, #ffae00 70%, #ff8c00 100%); color: #2c1a00; font-weight: 600; cursor: pointer; transition: all 0.3s; overflow: hidden; box-shadow: 0 4px 15px rgba(255, 174, 0, 0.3); } .vip-btn:hover { transform: translateY(-2px); } .vip-text { background: linear-gradient(45deg, #2c1a00, #5a3a00); -webkit-background-clip: text; background-clip: text; color: transparent; display: inline-block; }
`);
GM_addStyle(` div.swal2-container { all: initial !important; position: fixed !important; z-index: 999999 !important; inset: 0 !important; display: flex !important; align-items: center !important; justify-content: center !important; background: rgba(0,0,0,0.4) !important; } .swal2-popup { all: initial !important; max-width: 600px !important; width: 90vw !important; min-width: 300px !important; position: relative !important; box-sizing: border-box !important; padding: 20px !important; background: white !important; border-radius: 8px !important; font-family: Arial !important; animation: none !important; }`);
}
initDOM() {
this.container = document.createElement('div');
this.container.className = 'auth-window';
const title = document.createElement('h3');
title.className = 'auth-title';
title.innerHTML = `
脚本控制台v${GM_info.script.version}
`;
const tip = document.createElement('p');
tip.className = 'auth-tip';
tip.textContent = '初始化...';
this.tip = tip;
this.authInput = this.createInput(' 授权密钥', 'password', '#auth');
const authLink1 = this.createLink('authLink1', '#', '获取授权链接1');
const authLink2 = this.createLink('authLink2', '#', '获取授权链接2');
this.verifyBtn = document.createElement('button');
this.verifyBtn.className = 'auth-button';
this.verifyBtn.innerHTML = `验证授权码`;
this.verifyBtn.onclick = () => this.handleVerify();
this.controlPanel = document.createElement('div');
this.controlPanel.className = 'control-panel';
this.controlPanel.style.marginTop = '20px';
this.vipBtn = document.createElement('button');
this.vipBtn.className = 'vip-btn';
this.vipBtn.innerHTML = `高级功能-全自动挂机`;
this.vipBtn.onclick = () => this.handleVIPClick();
this.timerDisplay = document.createElement('div');
this.timerDisplay.textContent = '运行时间: 00:00:00';
this.timerDisplay.style.color = '#2ecc71';
this.startBtn = document.createElement('button');
this.startBtn.className = 'auth-button';
this.startBtn.style.backgroundColor = '#2ecc71';
this.startBtn.innerHTML = `▶ 开始运行`;
this.startBtn.onclick = () => this.startAutomation();
this.errorBox = document.createElement('div');
this.errorBox.className = 'error-message';
this.controlPanel.append(this.vipBtn, this.timerDisplay, this.startBtn);
this.container.append(title, tip, this.authInput.container, authLink1, authLink2, this.verifyBtn, this.controlPanel, this.errorBox);
document.body.appendChild(this.container);
this.initControlBtn();
}
initControlBtn() {
this.toggleBtn = document.createElement('button');
this.toggleBtn.className = 'window-toggle';
this.toggleBtn.innerHTML = '☰';
this.toggleBtn.style.cssText = 'position:fixed;right:30px;bottom:30px;z-index:9999999;';
this.toggleBtn.onclick = () => {
const hidden = this.container.style.display === 'none';
this.container.style.display = hidden ? 'block' : 'none';
};
document.body.appendChild(this.toggleBtn);
}
startAutomation() {
if (!this.isRunning) {
this.startTime = Date.now();
this.isRunning = true;
this.startBtn.textContent = '运行中...';
this.startBtn.disabled = true;
this.timer = setInterval(() => {
const elapsed = Date.now() - this.startTime;
const h = Math.floor(elapsed / 3600000).toString().padStart(2,'0');
const m = Math.floor((elapsed % 3600000) / 60000).toString().padStart(2,'0');
const s = Math.floor((elapsed % 60000) / 1000).toString().padStart(2,'0');
this.timerDisplay.textContent = `运行时间: ${h}:${m}:${s}`;
}, 1000);
if (this.begin) this.begin();
}
}
createInput(label, type, id) {
const c = document.createElement('div'); c.className = 'input-group';
const l = document.createElement('label'); l.textContent = label;
const i = document.createElement('input'); i.className = 'input-field'; i.type = type; i.id = id;
c.appendChild(l); c.appendChild(i);
return { container: c, input: i };
}
createLink(id, href, text) {
const a = document.createElement('a');
a.className = 'auth-link'; a.href = href; a.textContent = text;
return a;
}
show() { setTimeout(() => this.container.classList.add('visible'), 100); }
handleVerify() { Swal.fire({title:'提示',text:'免费版无需授权',icon:'info'}); }
handleVIPClick() { Swal.fire({title:'提示',text:'所有功能已免费开放',icon:'info'}); }
setTip(text) { this.tip.innerText = text; }
setVIPBtnText(text) { this.vipBtn.innerHTML = text; }
setOnVerifyCallback(cb) { this.onVerify = cb; }
setOnBegin(cb) { this.begin = cb; }
setOnVIP(cb) { this.vipCallback = cb; }
}
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
new Runner();