// ==UserScript== // @name WeLearn 自动刷时长脚本(终极防挂机版) // @namespace http://tampermonkey.net/ // @version 7.0.1 // @description 全量播放+面板+题目穿插+持久化+防30分钟挂机+焦点伪造 // @match https://welearn.sflep.com/student/StudyCourse.aspx* // @grant none // @run-at document-idle // @license MIT // ==/UserScript== (function() { 'use strict'; // ========== 配置 ========== const SCROLL_INTERVAL_MS = 2000; const CHECK_PLAY_END_MS = 2000; const CHAPTER_WAIT_MS = 8000; const ELEMENT_WAIT_MAX = 60; const ELEMENT_WAIT_STEP = 1000; const MEDIA_LOAD_TIMEOUT = 20000; const PROGRESS_TIMEOUT = 20000; const MAX_RETRIES = 2; const PROGRESS_STALL_COUNT = 4; const MIN_PLAY_DURATION = 5000; const MAX_RECOVER_ATTEMPTS = 3; const MAX_CHAPTER_INDEX = 4; const TOPIC_REVIEW_INTERVAL_MIN = 2; const TOPIC_REVIEW_INTERVAL_MAX = 3; const TOPIC_READ_DURATION_MIN = 5000; const TOPIC_READ_DURATION_MAX = 10000; const MOUSEOVER_INTERVAL = 30000; // 每30秒模拟鼠标悬停一次,防止30分钟超时 const LS_KEY = 'welearn_auto_course_data'; // ========== 全局状态 ========== let scrollTaskRunning = false; let scrollAbortController = null; let viewportKeeperRAF = null; let viewportKeeperInterval = null; let audioContextUnlocked = false; let mouseoverIntervalId = null; // 定时模拟鼠标悬停 let state = { completedRounds: 0, currentChapterIdx: 0, remainingMediaQueue: [], openedTopics: [] // 已打开题目标题列表 }; let isLoopRunning = false; let abortController = null; let currentActivity = '空闲'; let panelContainer = null; // ========== 工具函数 ========== const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; const sleep = (ms, signal) => new Promise((resolve, reject) => { const timer = setTimeout(resolve, ms); if (signal) signal.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('Aborted')); }); }); async function waitForElement(selector, scope, timeout = ELEMENT_WAIT_MAX) { for (let i = 0; i < timeout; i++) { if (!scope) { await sleep(ELEMENT_WAIT_STEP); continue; } const el = scope.querySelector(selector); if (el) return el; await sleep(ELEMENT_WAIT_STEP); } return null; } function getIframeDocument() { const iframe = document.querySelector('iframe[src*="e-textbook"], iframe[src*="readoor"]'); return iframe?.contentDocument || null; } function getScrollContainer() { const doc = getIframeDocument(); return doc?.querySelector('.text_middle_center') || doc?.querySelector('.el-scrollbar__wrap') || null; } function getTopicTitle(topicElement) { const titleEl = topicElement.querySelector('.title_name') || topicElement.querySelector('.el-collapse-item__title'); return titleEl ? titleEl.textContent.trim() : ''; } function getRealVideoItems() { const doc = getIframeDocument(); if (!doc) return []; const allItems = Array.from(doc.querySelectorAll('.el-collapse-item')); return allItems.filter(item => { const hasCover = item.querySelector('.def_img_box'); const hasVideoBox = item.querySelector('.video_box'); const isExam = item.querySelector('[class*="exam"]') || item.innerHTML.includes('submitQuestion') || item.querySelector('.exam-box') || item.querySelector('.test-box'); return (hasCover || hasVideoBox) && !isExam; }); } function getAudioItems() { const doc = getIframeDocument(); return doc ? Array.from(doc.querySelectorAll('.audio_deft_box')) : []; } function getNonMediaCollapseItems() { const doc = getIframeDocument(); if (!doc) return []; const allItems = Array.from(doc.querySelectorAll('.el-collapse-item')); const videoItems = getRealVideoItems(); return allItems.filter(item => { const isVideo = videoItems.includes(item); const containsAudioBox = item.querySelector('.audio_deft_box'); return !isVideo && !containsAudioBox; }); } function getAllChapterLeafNodes() { const doc = getIframeDocument(); return doc ? Array.from(doc.querySelectorAll('.el-tree-node.is_chapter.noChildren')) : []; } // ========== localStorage ========== function loadState() { try { const data = localStorage.getItem(LS_KEY); if (data) { const parsed = JSON.parse(data); state.completedRounds = parsed.completedRounds || 0; state.currentChapterIdx = parsed.currentChapterIdx ?? 0; state.remainingMediaQueue = parsed.remainingMediaQueue || []; state.openedTopics = parsed.openedTopics || []; } } catch (e) {} } function saveState() { try { localStorage.setItem(LS_KEY, JSON.stringify(state)); } catch (e) {} } function markTopicOpened(topicElement) { const title = getTopicTitle(topicElement); if (title && !state.openedTopics.includes(title)) { state.openedTopics.push(title); saveState(); } } // ========== 焦点伪造 + 鼠标悬停模拟 ========== function setupFocusSpoofing() { try { Object.defineProperty(document, 'hasFocus', { value: () => true, writable: false, configurable: false }); } catch (e) {} window.addEventListener('blur', e => { e.stopImmediatePropagation(); window.focus(); }, true); window.addEventListener('focus', e => e.stopImmediatePropagation(), true); setTimeout(() => { const doc = getIframeDocument(); if (doc?.defaultView) { const win = doc.defaultView; try { Object.defineProperty(win.document, 'hasFocus', { value: () => true, writable: false, configurable: false }); } catch (e) {} win.addEventListener('blur', e => { e.stopImmediatePropagation(); win.focus(); }, true); win.addEventListener('focus', e => e.stopImmediatePropagation(), true); } }, 2000); } function startMouseoverSimulation() { if (mouseoverIntervalId) return; mouseoverIntervalId = setInterval(() => { const doc = getIframeDocument(); if (doc?.body) { // 派发 mouseover 到 body,触发 parent.document.body.click() doc.body.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true, view: doc.defaultView })); // 直接调用父页面的 body.click() 兜底 try { if (parent && parent.document && parent.document.body) { parent.document.body.click(); } } catch (e) {} } }, MOUSEOVER_INTERVAL); } function stopMouseoverSimulation() { if (mouseoverIntervalId) { clearInterval(mouseoverIntervalId); mouseoverIntervalId = null; } } // ========== 防离开检测 ========== function preventLeavingDetection() { try { Object.defineProperty(document, 'hidden', { get: () => false, configurable: false }); Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: false }); } catch (e) {} window.addEventListener('visibilitychange', e => e.stopImmediatePropagation(), true); } function preventLeavingInIframe(doc) { if (!doc) return; try { Object.defineProperty(doc, 'hidden', { get: () => false, configurable: false }); Object.defineProperty(doc, 'visibilityState', { get: () => 'visible', configurable: false }); } catch (e) {} doc.addEventListener('visibilitychange', e => e.stopImmediatePropagation(), true); } // ========== 音频上下文解锁 ========== function unlockAudioContexts() { if (typeof AudioContext !== 'undefined') { if (!window.__audioContexts) window.__audioContexts = []; window.__audioContexts.forEach(ctx => { if (ctx.state === 'suspended') ctx.resume().catch(() => {}); }); } audioContextUnlocked = true; } function hijackAudioContextCreation(win) { if (!win?.AudioContext) return; const Orig = win.AudioContext; win.AudioContext = function(...args) { const ctx = new Orig(...args); if (!win.__audioContexts) win.__audioContexts = []; win.__audioContexts.push(ctx); return ctx; }; win.AudioContext.prototype = Orig.prototype; } // ========== 章节操作 ========== async function ensureDrawerOpen() { const doc = getIframeDocument(); if (!doc) return; if (doc.querySelector('.el-drawer.open')) return; const menuBtn = await waitForElement('.ymhicon.ymh-ebookopen', doc, 5); if (menuBtn) { menuBtn.click(); await sleep(2000); } } async function clickChapterNode(chapterEl) { const doc = getIframeDocument(); if (!doc || !chapterEl) return false; await ensureDrawerOpen(); chapterEl.scrollIntoView?.({ block: 'nearest', behavior: 'auto' }); await sleep(300); const content = chapterEl.querySelector('.el-tree-node__content'); if (content) { content.click(); return true; } return false; } async function goToChapter(idx) { const chapters = getAllChapterLeafNodes(); if (idx < 0 || idx >= chapters.length) return false; const success = await clickChapterNode(chapters[idx]); if (success) { state.currentChapterIdx = idx; const videos = getRealVideoItems(); const audios = getAudioItems(); const fullList = []; videos.forEach((v, i) => fullList.push({ type: 'video', index: i })); audios.forEach((a, i) => fullList.push({ type: 'audio', index: i })); state.remainingMediaQueue = fullList; saveState(); currentActivity = `进入第${idx+1}章`; } return success; } // ========== 滚动控制 ========== function performRealScroll(container) { const max = container.scrollHeight - container.clientHeight; if (max <= 0) return; const targetY = randomInt(0, max); const delta = targetY > container.scrollTop ? 120 : -120; container.dispatchEvent(new MouseEvent('mousemove', { clientX: 100, clientY: 200, bubbles: true, cancelable: true, view: container.ownerDocument.defaultView })); container.dispatchEvent(new WheelEvent('wheel', { deltaX: 0, deltaY: delta, deltaMode: 0, bubbles: true, cancelable: true, view: container.ownerDocument.defaultView })); container.scrollTop = targetY; } function startScrollTask() { if (scrollTaskRunning) return; scrollTaskRunning = true; scrollAbortController = new AbortController(); (async () => { while (!scrollAbortController.signal.aborted) { const container = getScrollContainer(); if (container) performRealScroll(container); try { await sleep(SCROLL_INTERVAL_MS + randomInt(0, 1000), scrollAbortController.signal); } catch { break; } } scrollTaskRunning = false; })(); } function stopScrollTask() { scrollAbortController?.abort(); scrollAbortController = null; } // ========== 视口守护 ========== function startViewportKeeper(element) { stopViewportKeeper(); const keep = () => { if (element?.isConnected) { element.scrollIntoView({ behavior: 'auto', block: 'center' }); viewportKeeperRAF = requestAnimationFrame(keep); } }; viewportKeeperRAF = requestAnimationFrame(keep); viewportKeeperInterval = setInterval(() => { if (element?.isConnected) element.scrollIntoView({ behavior: 'auto', block: 'center' }); }, 2000); } function stopViewportKeeper() { if (viewportKeeperRAF) { cancelAnimationFrame(viewportKeeperRAF); viewportKeeperRAF = null; } if (viewportKeeperInterval) { clearInterval(viewportKeeperInterval); viewportKeeperInterval = null; } } // ========== 进度检测 ========== function getProgress(mediaEl, audioBox) { if (mediaEl?.duration && !isNaN(mediaEl.currentTime)) return mediaEl.currentTime; if (audioBox) { const played = audioBox.querySelector('.aplayer-played'); if (played) { const w = parseFloat(played.style.width); if (!isNaN(w)) return w; } } return null; } async function waitForRealPlaybackStart(mediaEl, audioBox, doc) { const start = getProgress(mediaEl, audioBox); if (start !== null && start > 0) return true; const t0 = Date.now(); while (Date.now() - t0 < PROGRESS_TIMEOUT) { await sleep(1000); const now = getProgress(mediaEl, audioBox); if (now !== null && now > (start || 0)) return true; if (mediaEl?.ended) return true; if (mediaEl?.paused) mediaEl.play().catch(() => {}); } return false; } // ========== 媒体播放 ========== async function playSingleVideo(videoItem) { const doc = getIframeDocument(); if (!doc) return false; if (!videoItem.classList.contains('is-active')) { videoItem.querySelector('.el-collapse-item__header')?.click(); await sleep(1500); } videoItem.querySelector('.def_img_box')?.click(); await sleep(2000); let video = null; const st = Date.now(); while (Date.now() - st < MEDIA_LOAD_TIMEOUT) { video = doc.querySelector('video'); if (video?.readyState >= 2 && video.duration > 0) break; await sleep(1000); } if (!video) return false; video.muted = true; stopScrollTask(); video.scrollIntoView({ behavior: 'smooth', block: 'center' }); startViewportKeeper(video); currentActivity = '播放视频中...'; let ok = false; for (let r = 0; r <= MAX_RETRIES; r++) { video.play().catch(() => {}); await sleep(1000); if (await waitForRealPlaybackStart(video, null, doc)) { ok = true; break; } if (r < MAX_RETRIES) { video.pause(); await sleep(500); } } stopViewportKeeper(); if (!ok) { startScrollTask(); return false; } while (!video.ended) { if (video.paused && !video.ended) video.play().catch(() => {}); await sleep(CHECK_PLAY_END_MS); } startScrollTask(); return true; } async function playSingleAudio(audioItem) { const doc = getIframeDocument(); if (!doc) return false; stopScrollTask(); audioItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); startViewportKeeper(audioItem); const playBtn = audioItem.querySelector('.playAudio'); if (!playBtn) { stopViewportKeeper(); startScrollTask(); return false; } playBtn.click(); await sleep(2000); const audioBox = await waitForElement('.audio_play_box', doc, 5); if (audioBox) { const btn = audioBox.querySelector('.aplayer-button'); if (btn && !btn.classList.contains('aplayer-pause')) btn.click(); await sleep(1000); } let audio = null; const st = Date.now(); while (Date.now() - st < MEDIA_LOAD_TIMEOUT) { audio = doc.querySelector('audio'); if (audio?.readyState >= 2 && audio.duration > 0) break; await sleep(1000); } if (audio) { audio.muted = true; audio.play().catch(() => {}); } let ok = false; for (let r = 0; r <= MAX_RETRIES; r++) { if (await waitForRealPlaybackStart(audio, audioBox, doc)) { ok = true; break; } if (r < MAX_RETRIES) { audioItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); const btn = audioBox?.querySelector('.aplayer-button'); if (btn && !btn.classList.contains('aplayer-pause')) btn.click(); if (audio) { audio.muted = true; audio.play().catch(() => {}); } await sleep(1000); } } if (!ok) { stopViewportKeeper(); startScrollTask(); return false; } currentActivity = '播放音频中...'; const playStart = Date.now(); let stall = 0, lastProg = getProgress(audio, audioBox), recovers = 0; while (true) { await sleep(CHECK_PLAY_END_MS); const prog = getProgress(audio, audioBox); if (audio?.ended) break; if (prog !== null && lastProg !== null && Math.abs(prog - lastProg) < 0.1) stall++; else stall = 0; if (stall >= PROGRESS_STALL_COUNT && (Date.now() - playStart) > MIN_PLAY_DURATION) break; if (prog !== null && prog >= 99.5) break; if ((audio?.paused && !audio?.ended) || (prog === null || prog === 0) && !audio?.ended) { if (recovers < MAX_RECOVER_ATTEMPTS) { audioItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); const btn = audioBox?.querySelector('.aplayer-button'); if (btn && !btn.classList.contains('aplayer-pause')) btn.click(); if (audio) { audio.muted = true; audio.play().catch(() => {}); } recovers++; stall = 0; await sleep(2000); lastProg = getProgress(audio, audioBox); continue; } else break; } lastProg = prog; } stopViewportKeeper(); startScrollTask(); return true; } // ========== 题目复习 ========== async function performTopicReview() { const doc = getIframeDocument(); if (!doc) return; const allTopics = getNonMediaCollapseItems(); if (allTopics.length === 0) return; let targetTopic = null; if (state.openedTopics.length > 0) { const openedInDOM = allTopics.filter(t => state.openedTopics.includes(getTopicTitle(t))); if (openedInDOM.length > 0) { targetTopic = openedInDOM[randomInt(0, openedInDOM.length - 1)]; } } if (!targetTopic) { const unopened = allTopics.find(t => !state.openedTopics.includes(getTopicTitle(t))); if (unopened) { targetTopic = unopened; markTopicOpened(targetTopic); } else { targetTopic = allTopics[randomInt(0, allTopics.length - 1)]; } } if (!targetTopic) return; const title = getTopicTitle(targetTopic); currentActivity = `复习题目: ${title.substring(0, 20)}`; await ensureDrawerOpen(); targetTopic.scrollIntoView({ behavior: 'smooth', block: 'center' }); await sleep(1000); if (!targetTopic.classList.contains('is-active')) { targetTopic.querySelector('.el-collapse-item__header')?.click(); await sleep(1000); } const readTime = randomInt(TOPIC_READ_DURATION_MIN, TOPIC_READ_DURATION_MAX); currentActivity = `阅读题目: ${title.substring(0, 20)} (${readTime/1000}s)`; await sleep(readTime); if (targetTopic.classList.contains('is-active')) { targetTopic.querySelector('.el-collapse-item__header')?.click(); await sleep(500); } currentActivity = '完成题目复习'; } // ========== 章节媒体播放(含题目穿插) ========== async function playChapterMedia() { if (state.remainingMediaQueue.length === 0) return true; const queue = [...state.remainingMediaQueue]; for (let i = queue.length - 1; i > 0; i--) { const j = randomInt(0, i); [queue[i], queue[j]] = [queue[j], queue[i]]; } let mediaCount = 0; let reviewThreshold = randomInt(TOPIC_REVIEW_INTERVAL_MIN, TOPIC_REVIEW_INTERVAL_MAX); for (const item of queue) { if (abortController?.signal.aborted) return false; currentActivity = `播放 ${item.type} #${item.index}`; let success = false; if (item.type === 'video') { const videos = getRealVideoItems(); if (item.index < videos.length) success = await playSingleVideo(videos[item.index]); } else { const audios = getAudioItems(); if (item.index < audios.length) success = await playSingleAudio(audios[item.index]); } state.remainingMediaQueue = state.remainingMediaQueue.filter( m => !(m.type === item.type && m.index === item.index) ); saveState(); if (success) mediaCount++; if (mediaCount >= reviewThreshold) { await sleep(randomInt(1000, 3000)); await performTopicReview(); mediaCount = 0; reviewThreshold = randomInt(TOPIC_REVIEW_INTERVAL_MIN, TOPIC_REVIEW_INTERVAL_MAX); } await sleep(randomInt(1000, 3000)); } currentActivity = '章节媒体播放完毕'; return state.remainingMediaQueue.length === 0; } // ========== 主循环 ========== async function mainLoop() { isLoopRunning = true; abortController = new AbortController(); try { while (!abortController.signal.aborted) { const chapters = getAllChapterLeafNodes(); const target = Math.min(state.currentChapterIdx, MAX_CHAPTER_INDEX); if (!chapters.some(el => el.classList.contains('is-current')) || state.currentChapterIdx !== target) { await goToChapter(target); await sleep(CHAPTER_WAIT_MS); } const done = await playChapterMedia(); if (!done) break; const next = state.currentChapterIdx + 1; if (next <= MAX_CHAPTER_INDEX) { await goToChapter(next); } else { state.completedRounds++; saveState(); currentActivity = `完成一轮,共${state.completedRounds}轮,回到第一章`; await goToChapter(0); } if (!scrollTaskRunning) startScrollTask(); } } finally { isLoopRunning = false; } } function stopScript() { abortController?.abort(); stopScrollTask(); stopMouseoverSimulation(); currentActivity = '已暂停'; } async function resumeScript() { if (isLoopRunning) return; if (!getIframeDocument()) return; if (!scrollTaskRunning) startScrollTask(); startMouseoverSimulation(); await mainLoop(); } // ========== 面板 ========== function scrollToAndHighlight(el) { if (!el) return; const doc = getIframeDocument(); if (!doc) return; ensureDrawerOpen().then(() => { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); el.style.transition = 'box-shadow 0.3s'; el.style.boxShadow = '0 0 20px 5px #ffd700'; setTimeout(() => { el.style.boxShadow = ''; }, 2000); }); } function createPanel() { if (panelContainer) return; panelContainer = document.createElement('div'); panelContainer.id = 'welearn-panel'; panelContainer.innerHTML = `