// ==UserScript== // @name 舒华E学堂 - 防挂机自动验证 // @namespace http://tampermonkey.net/ // @version 3.1 // @description 舒华E学堂 yunxuetang视频学习防挂机,自动检测并点击防挂机验证弹窗 // @author Shy // @match *://www.ext.shuhua.com/* // @grant none // @run-at document-idle // @compatible chrome Chrome 90+ // @compatible firefox Firefox 90+ // @compatible edge Edge 90+ // @compatible opera Opera 80+ // @compatible safari Safari 14+ // @compatible brave All // @compatible vivaldi All // @tag 舒华 // @tag E学堂 // ==/UserScript== (function () { 'use strict'; // ==================== 浏览器兼容性检查 ==================== // 兼容所有主流现代浏览器(Chrome / Firefox / Edge / Opera / Safari / Brave / Vivaldi) const ua = navigator.userAgent; const isChrome = /Chrome\/\d+/.test(ua) && !/Edg\//.test(ua) && !/OPR\//.test(ua); const isEdge = /Edg\//.test(ua); const isFirefox = /Firefox\/\d+/.test(ua); const isOpera = /OPR\/\d+/.test(ua) || /Opera\//.test(ua); const isSafari = /Safari\//.test(ua) && !/Chrome\//.test(ua) && !/Chromium\//.test(ua); const browserName = isChrome ? 'Chrome' : isEdge ? 'Edge' : isFirefox ? 'Firefox' : isOpera ? 'Opera' : isSafari ? 'Safari' : 'Unknown'; // 检测是否为现代浏览器(支持 Userscript 的主流浏览器均支持) const isModernBrowser = isChrome || isEdge || isFirefox || isOpera || isSafari; if (!isModernBrowser) { console.warn( '[防挂机] 未识别的浏览器,脚本仍会尝试运行。检测到:', ua ); } // ==================== 配置区 ==================== const CONFIG = { // MutationObserver 扫描后延迟执行(毫秒) observeDelay: 500, // 轮询兜底间隔(毫秒) pollInterval: 1500, // 按钮点击后冷却时间(毫秒),防连续误触 clickCooldown: 3000, // 是否开启模拟活动(防止触发挂机检测) simulateActivity: true, // 模拟活动间隔(毫秒) activityInterval: 30000, // 调试模式 debug: true, // ---- 按钮文本匹配(正则,忽略空白) ---- buttonPatterns: [ /^确\s*定$/, /^确\s*认$/, /^知\s*道\s*了$/, /^我\s*知\s*道\s*了$/, /^继\s*续\s*学\s*习$/, /^继\s*续\s*观\s*看$/, /^立\s*即\s*学\s*习$/, /^返\s*回\s*学\s*习$/, /^返\s*回\s*课\s*程$/, /^我\s*在\s*学\s*习$/, /^我\s*还\s*在\s*学\s*习$/, /^我\s*在/, /^我\s*还\s*在/, /^关\s*闭$/, /^好\s*的$/, /^好$/, /^yes$/i, /^ok$/i, /^confirm$/i, /^确\s*定\s*关\s*闭$/, /^确\s*定\s*退\s*出$/, /^取\s*消$/, ], // ---- 弹窗容器选择器(按优先级排列) ---- dialogSelectors: [ // Element UI 弹窗(yunxuetang 默认 UI 框架) '.el-message-box', '.el-dialog__wrapper', '.el-dialog', // YXT 自有组件 '.yxt-dialog', '.yxt-modal', '.yxt-message-box', '.yxt-confirm', '.yxt-popup', '.yxt-overlay', // 通用选择器 '[role="dialog"]', '[role="alertdialog"]', '.el-overlay', '.modal', '.dialog', '.popup', '.overlay', '.mask', '.shade', // 弹窗包裹层 '.v-modal', '.el-message-box__wrapper', '.yxt-dialog__wrapper', '.yxt-message-box__wrapper', // 课程/学习相关弹窗 '[class*="verify"]', '[class*="idle"]', '[class*="timeout"]', '[class*="confirm"]', '[class*="alert"]', '[class*="tip-box"]', '[class*="popup"]', '[class*="dialog"]', '[class*="modal"]', ], // ---- 排除选择器 ---- excludeSelectors: [ 'video', 'audio', '.video-control', '.player-control', '.controls-bar', '.el-slider', ], }; // ==================== 状态 ==================== let lastClickTime = 0; let clickCount = 0; let activityTimer = null; let pollTimer = null; let observer = null; // ==================== 浏览器兼容性工具 ==================== // Element.matches 兼容性补丁 const _matches = Element.prototype.matches || Element.prototype.webkitMatchesSelector || Element.prototype.msMatchesSelector || function (selector) { return Array.from(document.querySelectorAll(selector)).indexOf(this) !== -1; }; // Element.closest 兼容性补丁(Safari < 13.1) const _closest = Element.prototype.closest || function (selector) { let el = this; while (el && el.nodeType === 1) { if (_matches.call(el, selector)) return el; el = el.parentElement || el.parentNode; } return null; }; // document.contains 兼容性补丁 const _contains = document.contains ? document.contains.bind(document) : function (node) { return document.documentElement.contains(node); }; // ==================== 工具函数 ==================== function log(...args) { if (CONFIG.debug) { console.log( '%c[防挂机]', 'color: #4CAF50; font-weight: bold; font-size: 12px;', new Date().toLocaleTimeString(), ...args ); } } function warn(...args) { console.warn( '%c[防挂机]', 'color: #FF9800; font-weight: bold;', ...args ); } function isInCooldown() { return Date.now() - lastClickTime < CONFIG.clickCooldown; } function matchesButtonPattern(text) { if (!text) return false; const cleaned = text.replace(/\s+/g, '').replace(/\n/g, ''); return CONFIG.buttonPatterns.some((pat) => pat.test(cleaned)); } function isExcluded(element) { for (const sel of CONFIG.excludeSelectors) { if (_closest.call(element, sel)) return true; } return false; } function isVisible(element) { if (!element || !element.nodeType) return false; // 检查元素是否在 DOM 中 if (!_contains(element)) return false; // 检查计算样式 try { const style = window.getComputedStyle(element); if ( style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0' || style.pointerEvents === 'none' ) { return false; } // 检查尺寸 const rect = element.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0) return false; } catch (e) { return false; } return true; } // ==================== 模拟活动(防触发) ==================== function simulateActivity() { if (!CONFIG.simulateActivity) return; try { // 模拟鼠标移动 const events = [ new MouseEvent('mousemove', { bubbles: true, clientX: Math.random() * window.innerWidth, clientY: Math.random() * window.innerHeight, }), new MouseEvent('mousedown', { bubbles: true }), new MouseEvent('mouseup', { bubbles: true }), new KeyboardEvent('keydown', { bubbles: true, key: 'Shift', keyCode: 16, }), new KeyboardEvent('keyup', { bubbles: true, key: 'Shift', keyCode: 16, }), ]; // 随机选一个事件触发 const event = events[Math.floor(Math.random() * events.length)]; document.dispatchEvent(event); log('已模拟用户活动'); } catch (e) { // 忽略模拟失败 } } // ==================== 核心检测逻辑 ==================== /** * 在指定容器内查找可点击的确认按钮 */ function findButtonInContainer(container) { if (!container || !isVisible(container)) return null; if (isExcluded(container)) return null; // 查找所有可能的按钮元素 const selectors = [ 'button', '.el-button', '.yxt-button', '.yxt-btn', 'a', 'span', 'div', 'input[type="button"]', 'input[type="submit"]', ]; for (const sel of selectors) { const elements = container.querySelectorAll(sel); for (const el of elements) { if (!isVisible(el)) continue; if (isExcluded(el)) continue; const text = el.textContent || el.value || el.innerText || ''; if (matchesButtonPattern(text)) { // 额外检查:确保不是取消按钮(如果同时有确认按钮) return el; } } } return null; } /** * 策略1:在已知弹窗容器内查找按钮 */ function findButtonInDialogs() { for (const selector of CONFIG.dialogSelectors) { try { const dialogs = document.querySelectorAll(selector); for (const dialog of dialogs) { if (!isVisible(dialog)) continue; const button = findButtonInContainer(dialog); if (button) { log(`策略1命中: 在 "${selector}" 中找到按钮`, button.textContent); return button; } } } catch (e) { // 选择器可能无效,跳过 } } return null; } /** * 策略2:全局搜索(在覆盖层/遮罩层中的按钮) */ function findButtonInOverlay() { // 查找可能的覆盖层元素 const overlaySelectors = [ '[style*="position: fixed"]', '[style*="position:fixed"]', '[style*="z-index"]', '.v-modal', '.el-overlay', ]; for (const sel of overlaySelectors) { try { const overlays = document.querySelectorAll(sel); for (const overlay of overlays) { if (!isVisible(overlay)) continue; const style = window.getComputedStyle(overlay); // 检查是否是覆盖层(高 z-index + 固定/绝对定位) const zIndex = parseInt(style.zIndex, 10); if (zIndex >= 100 && (style.position === 'fixed' || style.position === 'absolute')) { const button = findButtonInContainer(overlay); if (button) { log('策略2命中: 在覆盖层中找到按钮', button.textContent); return button; } // 也检查覆盖层的父元素 if (overlay.parentElement) { const parentButton = findButtonInContainer(overlay.parentElement); if (parentButton) { log('策略2命中: 在覆盖层父元素中找到按钮', parentButton.textContent); return parentButton; } } } } } catch (e) { // 跳过 } } return null; } /** * 策略3:Vue 实例检测(云学堂使用 Vue 2 / Vue 3) */ function findButtonViaVue() { try { const app = document.querySelector('#app'); if (!app) return null; // Vue 2: app.__vue__ if (app.__vue__) { const result = searchVueTree(app.__vue__); if (result) { log('策略3命中: 通过 Vue 2 实例找到弹窗'); return result; } } // Vue 3: app.__vue_app__ 或内部 _instance if (app.__vue_app__) { const result = searchVue3Tree(app.__vue_app__); if (result) { log('策略3命中: 通过 Vue 3 实例找到弹窗'); return result; } } // Vue 3 备选:遍历 __vue_app__._container if (app.__vue_app__ && app.__vue_app__._container) { const container = app.__vue_app__._container; if (container.__vue_app__) { const result = searchVue3Tree(container.__vue_app__); if (result) return result; } } } catch (e) { // Vue 可能未挂载 } return null; } function searchVueTree(vm, depth = 0) { if (depth > 10) return null; if (!vm) return null; try { const data = vm.$data || {}; const keys = Object.keys(data); for (const key of keys) { if ( key.toLowerCase().includes('dialog') || key.toLowerCase().includes('modal') || key.toLowerCase().includes('visible') || key.toLowerCase().includes('show') ) { if (data[key] === true) { const el = vm.$el; if (el && isVisible(el)) { const button = findButtonInContainer(el); if (button) return button; } } } } if (vm.$children) { for (const child of vm.$children) { const result = searchVueTree(child, depth + 1); if (result) return result; } } } catch (e) { // 忽略 } return null; } /** * Vue 3 组件树搜索(通过 vnode / subTree) */ function searchVue3Tree(app, depth = 0) { if (depth > 10) return null; if (!app) return null; try { // Vue 3 root component instance const root = app._instance; if (root) { const result = searchVue3Instance(root, 0); if (result) return result; } } catch (e) { // 忽略 } return null; } function searchVue3Instance(instance, depth) { if (depth > 10 || !instance) return null; try { // 检查 setupState / data / props 中的弹窗相关状态 const sources = [instance.setupState, instance.data, instance.props]; for (const source of sources) { if (!source) continue; const keys = Object.keys(source); for (const key of keys) { if ( key.toLowerCase().includes('dialog') || key.toLowerCase().includes('modal') || key.toLowerCase().includes('visible') || key.toLowerCase().includes('show') || key.toLowerCase().includes('open') ) { try { const val = source[key]; if (val === true || val === 'visible') { const el = instance.vnode?.el || instance.el; if (el && isVisible(el)) { const button = findButtonInContainer(el); if (button) return button; } } } catch (e) { // 忽略 getter 错误 } } } } // 递归搜索子组件 const subTree = instance.subTree; if (subTree && subTree.component) { const result = searchVue3Instance(subTree.component, depth + 1); if (result) return result; } if (subTree && subTree.children) { for (const child of (Array.isArray(subTree.children) ? subTree.children : [])) { if (child && child.component) { const result = searchVue3Instance(child.component, depth + 1); if (result) return result; } } } } catch (e) { // 忽略 } return null; } /** * 策略4:检测 Element UI MessageBox(云学堂常用) */ function findButtonInElMessageBox() { try { // Element UI MessageBox 会创建 .el-message-box 元素 const msgBox = document.querySelector('.el-message-box'); if (msgBox && isVisible(msgBox)) { const buttons = msgBox.querySelectorAll('.el-message-box__btns button'); for (const btn of buttons) { if (isVisible(btn)) { // 通常第一个按钮是确认按钮 log('策略4命中: Element UI MessageBox', btn.textContent); return btn; } } } // 也检查 Element UI Dialog const dialog = document.querySelector('.el-dialog'); if (dialog && isVisible(dialog)) { const footer = dialog.querySelector('.el-dialog__footer'); if (footer) { const buttons = footer.querySelectorAll('button'); for (const btn of buttons) { if (isVisible(btn)) { log('策略5命中: Element UI Dialog 按钮', btn.textContent); return btn; } } } } } catch (e) { // 忽略 } return null; } // ==================== 调度器 ==================== function tryAutoVerify() { if (isInCooldown()) return; let button = null; // 按优先级尝试各策略 button = findButtonInElMessageBox(); if (!button) button = findButtonInDialogs(); if (!button) button = findButtonInOverlay(); if (!button) button = findButtonViaVue(); if (button) { clickCount++; lastClickTime = Date.now(); const text = (button.textContent || button.value || '').trim(); log(`>>> 第 ${clickCount} 次自动点击: "${text}"`); // 模拟真实点击 try { // 先触发 hover 事件 button.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); button.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); // 短暂延迟后点击 setTimeout(() => { button.click(); button.dispatchEvent( new MouseEvent('click', { bubbles: true, cancelable: true }) ); log('>>> 点击完成'); }, 100); } catch (e) { // 回退:直接点击 button.click(); log('>>> 直接点击完成'); } } } // ==================== MutationObserver ==================== function startObserver() { observer = new MutationObserver((mutations) => { let shouldCheck = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { // 检查新增节点是否是弹窗 for (const sel of CONFIG.dialogSelectors) { try { if (_matches.call(node, sel)) { shouldCheck = true; break; } if (node.querySelector && node.querySelector(sel)) { shouldCheck = true; break; } } catch (e) { // 忽略 } } if (shouldCheck) break; } } } if (shouldCheck) break; } if (shouldCheck) { log('检测到可能的弹窗变化,延迟检查...'); setTimeout(tryAutoVerify, CONFIG.observeDelay); } }); observer.observe(document.body, { childList: true, subtree: true, }); log('MutationObserver 已启动'); } // ==================== 轮询兜底 ==================== function startPolling() { pollTimer = setInterval(() => { tryAutoVerify(); }, CONFIG.pollInterval); log(`轮询兜底已启动 (间隔 ${CONFIG.pollInterval}ms)`); } // ==================== 模拟活动定时器 ==================== function startActivitySimulator() { if (!CONFIG.simulateActivity) return; activityTimer = setInterval(() => { simulateActivity(); }, CONFIG.activityInterval); log(`活动模拟器已启动 (间隔 ${CONFIG.activityInterval}ms)`); } // ==================== 清理 ==================== function cleanup() { if (observer) observer.disconnect(); if (pollTimer) clearInterval(pollTimer); if (activityTimer) clearInterval(activityTimer); log('已清理所有定时器和观察者'); } // ==================== 启动 ==================== function init() { log('========================================'); log('舒华E学堂 防挂机自动验证脚本 v3.0'); log('浏览器:', browserName); log('当前页面:', location.href); log('========================================'); startObserver(); startPolling(); startActivitySimulator(); tryAutoVerify(); window.addEventListener('beforeunload', cleanup); // 暴露调试接口 window.__antiIdle = { config: CONFIG, tryAutoVerify, simulateActivity, cleanup, getStats: () => ({ clickCount, lastClickTime: lastClickTime ? new Date(lastClickTime).toLocaleTimeString() : 'none', debug: CONFIG.debug, simulateActivity: CONFIG.simulateActivity, }), toggleDebug: () => { CONFIG.debug = !CONFIG.debug; log('调试模式:', CONFIG.debug ? '开启' : '关闭'); return CONFIG.debug; }, toggleActivity: () => { CONFIG.simulateActivity = !CONFIG.simulateActivity; if (CONFIG.simulateActivity) { startActivitySimulator(); } else { clearInterval(activityTimer); } log('活动模拟:', CONFIG.simulateActivity ? '开启' : '关闭'); return CONFIG.simulateActivity; }, // 手动添加按钮关键词 addButtonPattern: (pattern) => { CONFIG.buttonPatterns.push(new RegExp(pattern)); log('已添加按钮关键词:', pattern); }, }; log('脚本初始化完成,等待验证弹窗...'); } // 启动 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();