// ==UserScript== // @name WeLearn 自动刷课脚本(小屏返回修复V3) // @namespace http://tampermonkey.net/ // @version 7.3 // @description 修复小屏二级页面返回:遮罩点击+history.back,全量播放+面板+持久化+防挂机 // @match https://welearn.sflep.com/student/StudyCourse.aspx* // @grant none // @run-at document-idle // ==/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; const RETURN_DELAY = 4000; // 等待后尝试返回 const RETURN_CHECK_INTERVAL = 500; 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')) : []; } // ========== 持久化 ========== 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) { doc.body.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true, view: doc.defaultView })); 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; } // ========== 小屏题目返回(增强版,支持遮罩点击与history.back) ========== async function clickBackButton(doc) { console.log('[小屏适配] 开始尝试返回...'); // 方法1:点击返回按钮系列 const backContainer = doc.querySelector('.flex.close'); if (backContainer) { const targets = [ backContainer, backContainer.querySelector('span'), backContainer.querySelector('i'), ].filter(Boolean); for (const target of targets) { console.log('[小屏适配] 点击', target.tagName, target.className, target.textContent?.trim().substring(0,10)); target.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: doc.defaultView })); target.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: doc.defaultView })); target.click(); await sleep(1000); if (!doc.querySelector('.flex.close')) { console.log('[小屏适配] 返回成功(按钮点击有效)'); return true; } } } // 方法2:点击遮罩层 .classNumberMask const mask = doc.querySelector('.classNumberMask'); if (mask) { console.log('[小屏适配] 尝试点击遮罩层'); mask.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: doc.defaultView })); mask.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: doc.defaultView })); mask.click(); await sleep(1000); if (!doc.querySelector('.flex.close')) { console.log('[小屏适配] 返回成功(遮罩点击有效)'); return true; } } // 方法3:调用 history.back() try { console.log('[小屏适配] 尝试 history.back()'); history.back(); await sleep(2000); if (!doc.querySelector('.flex.close')) { console.log('[小屏适配] 返回成功(history.back())'); return true; } } catch (e) { console.warn('[小屏适配] history.back() 失败', e); } return false; } async function handleTopicReturnIfNeeded() { const doc = getIframeDocument(); if (!doc) return false; const backBtn = doc.querySelector('.flex.close'); if (!backBtn) return false; console.log('[小屏适配] 检测到二级题目页面,将在', RETURN_DELAY/1000, '秒后自动返回'); currentActivity = `已进入题目页面,${RETURN_DELAY/1000}秒后返回`; await sleep(RETURN_DELAY); if (!doc.querySelector('.flex.close')) { console.log('[小屏适配] 页面已自动返回(无需操作)'); return true; } const clicked = await clickBackButton(doc); if (clicked) { await sleep(2000); return true; } console.warn('[小屏适配] 所有返回尝试均失败,强制等待后继续'); await sleep(5000); return false; } // ========== 题目复习 ========== 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)}`; console.log('[题目复习] 正在处理:', title); await ensureDrawerOpen(); targetTopic.scrollIntoView({ behavior: 'smooth', block: 'center' }); await sleep(1000); const header = targetTopic.querySelector('.el-collapse-item__header'); if (header) { console.log('[题目复习] 点击标题'); header.click(); await sleep(1000); } const isSmallScreen = await handleTopicReturnIfNeeded(); if (isSmallScreen) { currentActivity = '完成题目复习(小屏模式)'; return; } if (targetTopic.classList.contains('is-active')) { const readTime = randomInt(TOPIC_READ_DURATION_MIN, TOPIC_READ_DURATION_MAX); currentActivity = `阅读题目: ${title.substring(0, 20)} (${readTime/1000}s)`; await sleep(readTime); const headerClose = targetTopic.querySelector('.el-collapse-item__header'); if (headerClose && targetTopic.classList.contains('is-active')) { headerClose.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 = `

WeLearn 刷课面板

状态: 初始化
活动: 等待
章节: 0
轮次: 0
📋 待播媒体 (点击定位)
📝 题目列表 (点击展开并记录)
`; document.body.appendChild(panelContainer); document.getElementById('wl-stop').addEventListener('click', stopScript); document.getElementById('wl-resume').addEventListener('click', resumeScript); document.getElementById('wl-unlock').addEventListener('click', () => { unlockAudioContexts(); document.getElementById('wl-unlock').style.display = 'none'; if (!isLoopRunning) { startScrollTask(); startMouseoverSimulation(); mainLoop(); } }); setInterval(updatePanel, 2000); updatePanel(); } function updatePanel() { document.getElementById('wl-status').textContent = isLoopRunning ? '运行中' : (scrollTaskRunning ? '滚动中' : '空闲'); document.getElementById('wl-activity').textContent = currentActivity; document.getElementById('wl-chapter').textContent = state.currentChapterIdx + 1; document.getElementById('wl-rounds').textContent = state.completedRounds; if (!audioContextUnlocked && !isLoopRunning) { document.getElementById('wl-unlock').style.display = 'block'; } else { document.getElementById('wl-unlock').style.display = 'none'; } const mediaListEl = document.getElementById('wl-media-list'); mediaListEl.innerHTML = ''; const doc = getIframeDocument(); if (state.remainingMediaQueue.length === 0) { mediaListEl.innerHTML = '
无剩余媒体
'; } else { state.remainingMediaQueue.forEach(m => { const div = document.createElement('div'); div.className = 'task-item'; div.textContent = `${m.type} #${m.index}`; div.addEventListener('click', () => { if (!doc) return; let targetEl = null; if (m.type === 'video') { const videos = getRealVideoItems(); if (m.index < videos.length) targetEl = videos[m.index]; } else { const audios = getAudioItems(); if (m.index < audios.length) targetEl = audios[m.index]; } if (targetEl) { scrollToAndHighlight(targetEl); div.classList.add('highlight'); setTimeout(() => div.classList.remove('highlight'), 1500); } }); mediaListEl.appendChild(div); }); } const topicListEl = document.getElementById('wl-topic-list'); topicListEl.innerHTML = ''; if (!doc) { topicListEl.innerHTML = '
等待页面加载
'; return; } const topics = getNonMediaCollapseItems(); if (topics.length === 0) { topicListEl.innerHTML = '
无题目
'; } else { topics.forEach(topic => { const div = document.createElement('div'); div.className = 'task-item'; const title = getTopicTitle(topic); div.textContent = title.substring(0, 30); div.title = title; if (state.openedTopics.includes(title)) { div.classList.add('opened'); } div.addEventListener('click', () => { if (!topic.classList.contains('is-active')) { topic.querySelector('.el-collapse-item__header')?.click(); } scrollToAndHighlight(topic); markTopicOpened(topic); updatePanel(); div.classList.add('highlight'); setTimeout(() => div.classList.remove('highlight'), 1500); }); topicListEl.appendChild(div); }); } } // ========== 初始化 ========== async function init() { createPanel(); console.log('[脚本] 等待 iframe...'); while (!getIframeDocument()) await sleep(1000); const doc = getIframeDocument(); const win = doc.defaultView; loadState(); console.log(`[脚本] 已加载:轮次${state.completedRounds} 章节${state.currentChapterIdx+1}`); window.stopScript = stopScript; window.resumeScript = resumeScript; window.getCompletedRounds = () => state.completedRounds; win.stopScript = stopScript; win.resumeScript = resumeScript; win.getCompletedRounds = () => state.completedRounds; setupFocusSpoofing(); hijackAudioContextCreation(window); hijackAudioContextCreation(win); preventLeavingDetection(); preventLeavingInIframe(doc); await ensureDrawerOpen(); await waitForElement('.el-tree-node.is_chapter.noChildren', doc); const chapters = getAllChapterLeafNodes(); if (!chapters.some(el => el.classList.contains('is-current'))) { await goToChapter(state.currentChapterIdx); await sleep(CHAPTER_WAIT_MS); } if (!audioContextUnlocked) { document.getElementById('wl-unlock').style.display = 'block'; currentActivity = '等待点击解锁按钮'; } else { startScrollTask(); startMouseoverSimulation(); mainLoop(); } } window.addEventListener('load', () => setTimeout(init, 5000)); })();