// ==UserScript== // @name 学习通自动刷课·复习 // @namespace http://tampermonkey.net/ // @version 1.2.0 // @description 自动播放学习通视频 - 互动控制面板 - 倍速调节/模式切换/进度显示 - 支持刷课/复习模式 // @author you // @match https://mooc1.chaoxing.com/mycourse/studentstudy* // @match https://mooc1.chaoxing.com/mooc-ans/mycourse/studentstudy* // @icon https://www.chaoxing.com/favicon.ico // @grant GM_addStyle // @grant GM_log // @license MIT // ==/UserScript== (function () { 'use strict' /* ========================= 全局状态 ========================= */ var CTRL = { paused: false, pageTotal: 0, pageDone: 0, allTotal: 0, allDone: 0, currentTitle: '', status: '启动中...', } /* ========================= 配置 ========================= */ var CONFIG = { playbackRate: parseFloat(localStorage.getItem('xxt_rate') || '1'), skipCompleted: localStorage.getItem('xxt_mode') !== 'review', autoResumeMs: 800, minStayMs: 35000, stallThreshold: 4, breakInterval: 5, completionThreshold: 0.98, maxWaitForVideo: 15000, maxPages: 200, } function log() { console.log('[学习通]', ...arguments) } function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms) }) } function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1) + min) } function randomDelay(min, max) { return sleep(randomInt(min, max)) } /* ========================= 暂停检查 ========================= */ async function checkPause() { while (CTRL.paused) { await sleep(300) } } /* ========================= 互动控制面板 ========================= */ function initControlPanel() { GM_addStyle(` #xxt-panel { all: initial; position: fixed; top: 80px; right: 16px; z-index: 999999; width: 260px; background: rgba(28,28,32,0.94); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); font-family: -apple-system, "Microsoft YaHei", sans-serif; font-size: 13px; color: #e8e8e8; user-select: none; overflow: hidden; transition: opacity 0.2s; } #xxt-panel * { box-sizing: border-box; } #xxt-panel .phead { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; background: rgba(255,255,255,0.04); border-bottom: 1px solid rgba(255,255,255,0.06); cursor: pointer; } #xxt-panel .phead .ptitle { font-size: 14px; font-weight: 600; letter-spacing: 0.3px; display: flex; align-items: center; gap: 6px; } #xxt-panel .phead .pcollapse { font-size: 16px; opacity: 0.5; transition: transform 0.2s; } #xxt-panel .phead .pcollapse.open { transform: rotate(180deg); } #xxt-panel .pbody { padding: 12px 14px 14px; } #xxt-panel .pbody.collapsed { display: none; } #xxt-panel .prow { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } #xxt-panel .plabel { opacity: 0.6; font-size: 12px; } #xxt-panel .pbtns { display: flex; gap: 4px; } #xxt-panel .pbtn { padding: 3px 10px; border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; cursor: pointer; font-size: 12px; color: #ccc; background: rgba(255,255,255,0.04); transition: all 0.15s; } #xxt-panel .pbtn:hover { background: rgba(255,255,255,0.1); } #xxt-panel .pbtn.active { background: #4A9EFF; color: #fff; border-color: #4A9EFF; } #xxt-panel .pbtn.active.review { background: #FF9800; border-color: #FF9800; } #xxt-panel .pbtn.speed { min-width: 36px; text-align: center; } #xxt-panel .pbtn.speed.val { min-width: 44px; cursor: default; background: rgba(255,255,255,0.06); font-weight: 600; font-size: 13px; } #xxt-panel .pbtn.speed.val:hover { background: rgba(255,255,255,0.06); } #xxt-panel .psep { height: 1px; background: rgba(255,255,255,0.06); margin: 10px 0; } #xxt-panel .pprog { margin-bottom: 6px; } #xxt-panel .pprog .pbar { height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; margin-top: 4px; overflow: hidden; } #xxt-panel .pprog .pbar .pfill { height: 100%; width: 0%; border-radius: 2px; background: linear-gradient(90deg, #4A9EFF, #6CB4FF); transition: width 0.4s; } #xxt-panel .pstats { font-size: 11px; opacity: 0.5; line-height: 1.6; margin-top: 2px; } #xxt-panel .pcur { font-size: 12px; opacity: 0.7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 8px; line-height: 1.4; } #xxt-panel .pstatus { display: flex; align-items: center; gap: 6px; font-size: 12px; margin-bottom: 10px; } #xxt-panel .pstatus .pdot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: #4CAF50; } #xxt-panel .pstatus .pdot.paused { background: #FF9800; } #xxt-panel .pctrl { display: flex; gap: 6px; } #xxt-panel .pctrl .pbtn { flex: 1; text-align: center; padding: 5px 0; } #xxt-panel .pctrl .pbtn.primary { background: #4A9EFF; color: #fff; border-color: #4A9EFF; } #xxt-panel .pctrl .pbtn.primary:hover { background: #3A8EEF; } #xxt-panel .pctrl .pbtn.primary.warn { background: #FF9800; border-color: #FF9800; } #xxt-panel .pctrl .pbtn.primary.warn:hover { background: #E08700; } #xxt-panel .pctrl .pbtn.danger { background: #e74c3c; color: #fff; border-color: #e74c3c; } #xxt-panel .pctrl .pbtn.danger:hover { background: #c0392b; } `) if (document.getElementById('xxt-panel')) return var labels = { modeStudy: '刷课', modeReview: '复习', } var panel = document.createElement('div') panel.id = 'xxt-panel' /* ---- Header ---- */ var header = document.createElement('div') header.className = 'phead' header.innerHTML = '
📚 学习通助手
' header.addEventListener('click', function (e) { if (e.target.closest('.pctrl')) return body.classList.toggle('collapsed') header.querySelector('.pcollapse').classList.toggle('open') }) /* ---- Body ---- */ var body = document.createElement('div') body.className = 'pbody' /* Mode row */ var modeRow = createRow() modeRow.label.textContent = '模式' var modeStudy = createBtn(labels.modeStudy) var modeReview = createBtn(labels.modeReview) modeStudy.className = 'pbtn active' + (CONFIG.skipCompleted ? '' : ' review') modeReview.className = 'pbtn' + (CONFIG.skipCompleted ? '' : ' active review') modeStudy.addEventListener('click', function () { setMode('study') }) modeReview.addEventListener('click', function () { setMode('review') }) modeRow.btns.append(modeStudy, modeReview) body.appendChild(modeRow.row) /* Speed row */ var speedRow = createRow() speedRow.label.textContent = '倍速' var speedDown = createBtn('−') speedDown.className = 'pbtn speed' var speedVal = createBtn(CONFIG.playbackRate + 'x') speedVal.className = 'pbtn speed val' var speedUp = createBtn('+') speedUp.className = 'pbtn speed' speedDown.addEventListener('click', function () { adjustSpeed(-0.5) }) speedUp.addEventListener('click', function () { adjustSpeed(0.5) }) speedRow.btns.append(speedDown, speedVal, speedUp) body.appendChild(speedRow.row) body.appendChild(createSep()) /* Current task */ var curEl = document.createElement('div') curEl.className = 'pcur' curEl.textContent = '等待开始...' /* Progress bar */ var progWrap = document.createElement('div') progWrap.className = 'pprog' var progText = document.createElement('div') progText.style.cssText = 'display:flex;justify-content:space-between;font-size:11px;opacity:0.6' progText.innerHTML = '页进度0%' var barWrap = document.createElement('div') barWrap.className = 'pbar' var barFill = document.createElement('div') barFill.className = 'pfill' barWrap.appendChild(barFill) progWrap.append(progText, barWrap) /* Stats */ var statsEl = document.createElement('div') statsEl.className = 'pstats' statsEl.textContent = '页: 0/0 · 总计: 0/0' body.appendChild(curEl) body.appendChild(progWrap) body.appendChild(statsEl) body.appendChild(createSep()) /* Status */ var statusRow = document.createElement('div') statusRow.className = 'pstatus' var dot = document.createElement('span') dot.className = 'pdot' var statusText = document.createElement('span') statusText.textContent = '启动中...' statusRow.append(dot, statusText) /* Control buttons */ var ctrlRow = document.createElement('div') ctrlRow.className = 'pctrl' var pauseBtn = document.createElement('div') pauseBtn.className = 'pbtn primary' pauseBtn.textContent = '⏸ 暂停' var stopBtn = document.createElement('div') stopBtn.className = 'pbtn danger' stopBtn.textContent = '⏹ 停止' ctrlRow.append(pauseBtn, stopBtn) body.appendChild(statusRow) body.appendChild(ctrlRow) panel.append(header, body) document.body.appendChild(panel) /* ---- Helper functions ---- */ function createRow() { var row = document.createElement('div') row.className = 'prow' var label = document.createElement('span') label.className = 'plabel' var btns = document.createElement('div') btns.className = 'pbtns' row.append(label, btns) return { row: row, label: label, btns: btns } } function createBtn(text) { var b = document.createElement('span') b.className = 'pbtn' b.textContent = text return b } function createSep() { var s = document.createElement('div') s.className = 'psep' return s } function setMode(mode) { CONFIG.skipCompleted = (mode !== 'review') localStorage.setItem('xxt_mode', mode) modeStudy.className = 'pbtn' + (mode === 'study' ? ' active' : '') modeReview.className = 'pbtn' + (mode === 'review' ? ' active review' : '') log('模式切换: ' + (mode === 'review' ? '复习' : '刷课')) CTRL.status = '模式: ' + (mode === 'review' ? '复习' : '刷课') } function adjustSpeed(delta) { var speeds = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] var idx = speeds.indexOf(CONFIG.playbackRate) if (idx === -1) { CONFIG.playbackRate = Math.max(0.5, Math.min(2, Math.round((CONFIG.playbackRate + delta) * 4) / 4)) } else { idx = Math.max(0, Math.min(speeds.length - 1, idx + (delta > 0 ? 1 : -1))) CONFIG.playbackRate = speeds[idx] } localStorage.setItem('xxt_rate', String(CONFIG.playbackRate)) speedVal.textContent = CONFIG.playbackRate + 'x' /* 实时应用到当前视频 */ try { var iframe = document.getElementById('iframe') if (iframe) { var doc = iframe.contentDocument || iframe.contentWindow.document if (doc) { var vids = doc.querySelectorAll('video#video_html5_api') vids.forEach(function (v) { v.playbackRate = CONFIG.playbackRate }) } } } catch (e) {} log('倍速: ' + CONFIG.playbackRate + 'x') CTRL.status = '倍速: ' + CONFIG.playbackRate + 'x' } /* ---- UI update interval ---- */ function updatePanel() { var all = getEntries() var page = getPageEntries() var pageTotal = page.length var pageDone = countCompleted(page) var allTotal = all.length var allDone = countCompleted(all) if (pageTotal > 0) { var pct = Math.round(pageDone / pageTotal * 100) progText.querySelector('.pct').textContent = pct + '%' barFill.style.width = pct + '%' statsEl.textContent = '页: ' + pageDone + '/' + pageTotal + ' · 总计: ' + allDone + '/' + allTotal } curEl.textContent = CTRL.currentTitle || '—' if (CTRL.paused) { dot.className = 'pdot paused' statusText.textContent = '⏸ 已暂停' pauseBtn.textContent = '▶ 继续' pauseBtn.className = 'pbtn primary warn' } else { dot.className = 'pdot' statusText.textContent = CTRL.status || '运行中...' pauseBtn.textContent = '⏸ 暂停' pauseBtn.className = 'pbtn primary' } } setInterval(updatePanel, 800) /* ---- Pause / Stop ---- */ pauseBtn.addEventListener('click', function () { CTRL.paused = !CTRL.paused log(CTRL.paused ? '⏸ 已暂停' : '▶ 已恢复') }) stopBtn.addEventListener('click', function () { if (confirm('确定停止刷课?页面将刷新。')) { location.reload() } }) log('控制面板已创建') } /* ========================= 人类行为模拟 ========================= */ function simulateHumanBehavior() { try { window.scrollBy(0, randomInt(50, 300)) var iframe = document.getElementById('iframe') if (iframe) { var doc try { doc = iframe.contentDocument || iframe.contentWindow.document } catch (e) {} if (doc) { doc.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: randomInt(100, 900), clientY: randomInt(100, 500) })) } } } catch (e) {} } /* ========================= 页面导航辅助 ========================= */ async function clickEntry(entry) { if (entry.isActive) return entry.name.click() await randomDelay(2000, 4000) await waitForIframeReady() } async function findVideosWithRetry() { var v = findVideos() if (v.length === 0) { await sleep(3000) v = findVideos() } return v } function hasNextPage() { var btn = document.getElementById('prevNextFocusNext') return btn && btn.style.display !== 'none' && btn.offsetParent !== null } /* ========================= 进度上报 XHR 监控 ========================= */ var lastProgressTime = Date.now() function setupXHRMonitor() { var origOpen = XMLHttpRequest.prototype.open XMLHttpRequest.prototype.open = function (method, url) { this._xxtUrl = typeof url === 'string' ? url : '' return origOpen.apply(this, arguments) } var origSend = XMLHttpRequest.prototype.send XMLHttpRequest.prototype.send = function (body) { var self = this if (self._xxtUrl && /multimedia|video.*log|playlog|studyLog|userlog/i.test(self._xxtUrl)) { self.addEventListener('load', function () { if (self.status === 200) lastProgressTime = Date.now() }) } return origSend.apply(self, arguments) } } function isProgressStale() { return (Date.now() - lastProgressTime) > 3 * 60 * 1000 } /* ========================= 获取知识点列表 ========================= */ function getEntries() { var tree = document.getElementById('coursetree') if (!tree) return [] var names = tree.querySelectorAll('.posCatalog_level .posCatalog_name') var entries = [] names.forEach(function (name) { var parent = name.closest('.posCatalog_select') if (!parent) return entries.push({ el: parent, name: name, title: name.getAttribute('title') || name.textContent.trim(), isActive: parent.classList.contains('posCatalog_active'), isCompleted: !!parent.querySelector('.icon_Completed'), }) }) return entries } /* ========================= 等待元素出现 ========================= */ function waitForEl(selector, timeout, root) { if (!root) root = document return new Promise(function (resolve, reject) { var found = root.querySelector(selector) if (found) return resolve(found) var obs = new MutationObserver(function () { var el = root.querySelector(selector) if (el) { obs.disconnect(); resolve(el) } }) obs.observe(root, { childList: true, subtree: true }) setTimeout(function () { obs.disconnect() reject(new Error('timeout waiting for ' + selector)) }, timeout || CONFIG.maxWaitForVideo) }) } /* ========================= 等待 iframe 内容加载完成 ========================= */ function waitForIframeReady() { return new Promise(function (resolve) { var iframe = document.getElementById('iframe') if (!iframe) { resolve(); return } function check() { try { var doc = iframe.contentDocument || iframe.contentWindow.document if (doc && doc.readyState === 'complete') { resolve() return } } catch (e) { /* cross-origin */ } setTimeout(check, 300) } check() }) } /* ========================= 查找当前页面的视频 ========================= */ function findVideos() { var mainIframe = document.getElementById('iframe') if (!mainIframe) return [] var iframeDoc try { iframeDoc = mainIframe.contentDocument || mainIframe.contentWindow.document } catch (e) { return [] } if (!iframeDoc) return [] var videoFrames = iframeDoc.querySelectorAll('iframe.ans-insertvideo-online') var videos = [] videoFrames.forEach(function (vf) { try { var vDoc = vf.contentDocument || vf.contentWindow.document var video = vDoc && vDoc.querySelector('video#video_html5_api') if (video) videos.push({ frame: vf, video: video, doc: vDoc }) } catch (e) { /* 忽略跨域错误 */ } }) return videos } /* ========================= 播放一组视频 ========================= */ async function playAllVideos(videos) { if (videos.length === 0) return for (var i = 0; i < videos.length; i++) { await checkPause() var v = videos[i].video var frame = videos[i].frame var doc = videos[i].doc CTRL.status = '▶ 播放视频 ' + (i + 1) + '/' + videos.length + ' (' + CONFIG.playbackRate + 'x)' log('▶ 播放第 ' + (i + 1) + '/' + videos.length + ' 个视频 (倍速: ' + CONFIG.playbackRate + 'x)') applyAntiIdle(frame, doc) try { v.playbackRate = CONFIG.playbackRate await v.play() } catch (e) { log('⚠ 播放失败: ' + e.message + ',尝试点击播放按钮') try { var bigBtn = doc.querySelector('.vjs-big-play-button') if (bigBtn) bigBtn.click() await sleep(1000) } catch (e2) { /* 忽略 */ } } await monitorVideo(v) log('✅ 第 ' + (i + 1) + '/' + videos.length + ' 个视频完成') } } /* ========================= 监控单个视频进度 ========================= */ function monitorVideo(video) { return new Promise(function (resolve) { var noProgressCount = 0 var prevPct = 0 var timer = setInterval(function () { try { if (video.ended) { clearInterval(timer) resolve() return } if (video.paused) { video.play().catch(function () {}) } var pct = video.currentTime / video.duration if (pct >= CONFIG.completionThreshold) { clearInterval(timer) resolve() return } if (Math.floor(pct * 100) % 10 === 0 && Math.floor(pct * 100) !== Math.floor(prevPct * 100)) { CTRL.status = '▶ 视频 ' + Math.floor(pct * 100) + '%' log(' 进度: ' + Math.floor(pct * 100) + '%') } if (Math.abs(pct - prevPct) < 0.001) { noProgressCount++ if (noProgressCount > 30) { log('⚠ 进度停滞超过30秒,强制完成') clearInterval(timer) resolve() return } } else { noProgressCount = 0 } prevPct = pct } catch (e) { clearInterval(timer) resolve() } }, CONFIG.autoResumeMs) setTimeout(function () { clearInterval(timer) resolve() }, 2 * 60 * 60 * 1000) }) } /* ========================= 防挂机措施 ========================= */ function applyAntiIdle(frame, doc) { if (!doc) return try { Object.defineProperty(doc, 'hidden', { get: function () { return false } }) Object.defineProperty(doc, 'visibilityState', { get: function () { return 'visible' } }) } catch (e) {} try { var videos = doc.querySelectorAll('video') videos.forEach(function (v) { v.addEventListener('mouseleave', function (e) { e.stopPropagation() }, true) }) } catch (e) {} try { var win = frame.contentWindow if (win) { win.addEventListener('blur', function (e) { e.stopImmediatePropagation() }, true) doc.addEventListener('visibilitychange', function (e) { e.stopImmediatePropagation() }, true) } } catch (e) {} try { doc.addEventListener('keydown', function (e) { if (e.key === ' ' || e.key === 'Spacebar' || e.key === 'k') { e.stopPropagation() } }, true) } catch (e) {} } /* ========================= 处理"是否继续"弹窗 ========================= */ function setupPopupHandler() { var obs = new MutationObserver(function () { try { var btns = document.querySelectorAll( '.jb_btn, .btnBlue, .popBottom a, #popok, #hintOk, #toastok, .popMoveDele, .jb_btn_92' ) btns.forEach(function (btn) { if (btn.offsetParent === null) return var text = btn.textContent.trim() if (text === '继续' || text === '确定' || text === '知道了') { log('自动关闭弹窗: "' + text + '"') btn.click() } }) } catch (e) {} }) obs.observe(document.body, { childList: true, subtree: true }) log('弹窗监听已启动') } /* ========================= 覆盖主页面可见性 ========================= */ function overridePageVisibility() { try { Object.defineProperty(document, 'hidden', { get: function () { return false }, configurable: true, }) } catch (e) {} document.addEventListener('visibilitychange', function (e) { e.stopImmediatePropagation() }, true) log('可见性检测已绕过') } /* ========================= 获取当前章节的所有知识点 ========================= */ function getPageEntries() { var all = getEntries() if (all.length === 0) return all var activeIdx = all.findIndex(function (e) { return e.isActive }) if (activeIdx === -1) return all var chapterUl = all[activeIdx].el.closest('.posCatalog_level ul') if (!chapterUl) return all return all.filter(function (e) { return chapterUl.contains(e.el) }) } /* ========================= 计算已完成数量 ========================= */ function countCompleted(entries) { var c = 0 for (var i = 0; i < entries.length; i++) { if (entries[i].isCompleted) c++ } return c } /* ========================= 主流程 ========================= */ async function main() { log('═══ 开始 ═══') log('模式: ' + (CONFIG.skipCompleted ? '刷课(跳过已学)' : '复习(全部重新播放)')) log('倍速: ' + CONFIG.playbackRate + 'x') log('最低停留: ' + (CONFIG.minStayMs / 1000) + '秒/知识点') CTRL.status = '初始化...' /* 初始化 */ initControlPanel() setupPopupHandler() overridePageVisibility() setupXHRMonitor() /* 等待课程树加载 */ try { await waitForEl('#coursetree', 20000) } catch (e) { log('❌ 等待课程树超时') CTRL.status = '❌ 课程树加载超时' return } await sleep(2000) /* 主循环:逐页处理 */ var pageCount = 0 var itemsSinceBreak = 0 var stallCount = 0 var lastCompleted = 0 while (pageCount < CONFIG.maxPages) { await checkPause() pageCount++ log('\n——— 第 ' + pageCount + ' 轮处理 ———') var entries = getEntries() if (entries.length === 0) { log('❌ 未找到知识点,等待后重试...') CTRL.status = '等待课程树...' await sleep(3000) entries = getEntries() if (entries.length === 0) { log('❌ 退出'); break } } var pageEntries = getPageEntries() log('当前页共 ' + pageEntries.length + ' 个知识点') CTRL.pageTotal = pageEntries.length if (pageEntries.length === 0) { if (hasNextPage()) { document.getElementById('prevNextFocusNext').click() CTRL.status = '⏳ 翻页中...' await randomDelay(5000, 10000) continue } log('✅ 没有更多页面') break } lastCompleted = countCompleted(pageEntries) var startIdx = pageEntries.findIndex(function (e) { return e.isActive }) if (startIdx === -1) startIdx = 0 var processed = 0 for (var i = startIdx; i < pageEntries.length; i++) { await checkPause() var entry = pageEntries[i] CTRL.currentTitle = entry.title CTRL.pageDone = processed log('\n[' + (i + 1) + '/' + pageEntries.length + '] ' + entry.title) /* ---- 刷课模式:已学知识点短暂进入后跳过 ---- */ if (CONFIG.skipCompleted && entry.isCompleted) { log('⏭ 已学,进入停留后继续') CTRL.status = '⏭ 已学: ' + entry.title await clickEntry(entry) await randomDelay(2000, 4000) simulateHumanBehavior() processed++ continue } /* ---- 切换到该知识点 ---- */ CTRL.status = '➡ 加载: ' + entry.title await clickEntry(entry) await randomDelay(3000, 5000) var entryStartTime = Date.now() /* ---- 查找并播放视频 ---- */ var videos = await findVideosWithRetry() if (videos.length > 0) { await playAllVideos(videos) } else { log('📄 本知识点无视频') CTRL.status = '📄 无视频: ' + entry.title } /* ---- 最低停留时间 ---- */ var elapsed = Date.now() - entryStartTime if (elapsed < CONFIG.minStayMs) { var remaining = CONFIG.minStayMs - elapsed log('⏳ 最低停留剩余 ' + Math.floor(remaining / 1000) + ' 秒...') CTRL.status = '⏳ 停留中 ' + Math.floor(remaining / 1000) + 's' await sleep(remaining) } /* ---- 等待进度上报 ---- */ log('⏳ 等待进度上报...') CTRL.status = '⏳ 上报进度...' await randomDelay(5000, 10000) simulateHumanBehavior() processed++ /* ---- 进度停滞检测 ---- */ var freshPage = getPageEntries() var freshCount = countCompleted(freshPage) var madeProgress = freshCount > lastCompleted || !isProgressStale() if (freshCount > lastCompleted) { lastCompleted = freshCount } if (madeProgress) { stallCount = 0 } else { stallCount++ log('⚠ 无进度增长 (' + stallCount + '/' + CONFIG.stallThreshold + ')') CTRL.status = '⚠ 进度停滞 ' + stallCount + '/' + CONFIG.stallThreshold } if (stallCount >= CONFIG.stallThreshold) { log('⚠ 检测到进度停滞,刷新会话...') CTRL.status = '⚠ 刷新会话...' var pause = randomInt(30000, 60000) await sleep(pause) location.reload() return } /* ---- 长间隔休息 ---- */ itemsSinceBreak++ if (itemsSinceBreak >= CONFIG.breakInterval) { var breakDuration = randomInt(30000, 60000) log('⏸ 长休息 ' + Math.floor(breakDuration / 1000) + ' 秒...') CTRL.status = '⏸ 休息 ' + Math.floor(breakDuration / 1000) + 's' await sleep(breakDuration) simulateHumanBehavior() itemsSinceBreak = 0 } } /* ---- 当前页处理完毕,翻页 ---- */ log('\n📄 当前页处理完毕(' + processed + ' 个知识点)') CTRL.status = '📄 翻页中...' if (hasNextPage()) { document.getElementById('prevNextFocusNext').click() await randomDelay(6000, 12000) } else { log('✅ 没有更多页面,全部完成!') CTRL.status = '✅ 全部完成' break } } log('\n═══ 全部完成 ═══') CTRL.status = '✅ 全部完成' } /* ========================= 启动 ========================= */ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { setTimeout(main, 2000) }) } else { setTimeout(main, 2000) } })()