// ==UserScript==
// @name 学习通自动刷课·复习
// @namespace http://tampermonkey.net/
// @version 1.3.7
// @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'
self.addEventListener('error', function (e) { console.error('[学习通助手] 全局错误:', e.message) })
self.addEventListener('unhandledrejection', function (e) { console.error('[学习通助手] 未处理拒绝:', e.reason) })
/* ========================= 全局状态 ========================= */
var CTRL = {
paused: false,
currentTitle: '',
status: '启动中...',
videoWatchSec: parseFloat(localStorage.getItem('xxt_watch_sec') || '0'),
sessionStart: Date.now(),
itemsProcessed: parseInt(localStorage.getItem('xxt_items') || '0'),
}
/* ========================= 版本 ========================= */
var SCRIPT_VERSION = '1.3.7'
/* ========================= 配置 ========================= */
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)) }
function saveStats() {
try {
localStorage.setItem('xxt_watch_sec', String(Math.round(CTRL.videoWatchSec)))
localStorage.setItem('xxt_items', String(CTRL.itemsProcessed))
} catch (e) {}
}
/* ========================= 暂停检查 ========================= */
async function checkPause() {
while (CTRL.paused) {
await sleep(300)
}
}
function formatDuration(sec) {
sec = Math.round(sec)
if (sec < 60) return sec + '秒'
if (sec < 3600) return Math.floor(sec / 60) + '分' + (sec % 60) + '秒'
var h = Math.floor(sec / 3600)
var m = Math.floor((sec % 3600) / 60)
return h + '小时' + m + '分'
}
/* ========================= 互动控制面板 ========================= */
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 .pprog .pbar .pfill.global {
background: linear-gradient(90deg, #8BC34A, #AED581);
}
#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 panel = document.createElement('div')
panel.id = 'xxt-panel'
/* ---- Header ---- */
var header = document.createElement('div')
header.className = 'phead'
header.innerHTML = '
📚 学习通助手 v' + SCRIPT_VERSION + '
▼
'
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('刷课')
var modeReview = createBtn('复习')
modeStudy.className = 'pbtn' + (CONFIG.skipCompleted ? ' active' : '')
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 = '等待开始...'
body.appendChild(curEl)
/* Helper to build a progress row */
function makeProgRow(label, cls) {
var wrap = document.createElement('div')
wrap.className = 'pprog'
var head = document.createElement('div')
head.style.cssText = 'display:flex;justify-content:space-between;font-size:11px;opacity:0.6'
head.innerHTML = '' + label + '0%'
var bar = document.createElement('div')
bar.className = 'pbar'
var fill = document.createElement('div')
fill.className = 'pfill ' + cls
bar.appendChild(fill)
wrap.append(head, bar)
return { el: wrap, pct: head.querySelector('.pct'), fill: fill }
}
/* Page progress */
var pageProg = makeProgRow('📄 页进度', '')
body.appendChild(pageProg.el)
/* Global progress */
var globalProg = makeProgRow('📊 总进度', 'global')
body.appendChild(globalProg.el)
/* Time info row */
var timeEl = document.createElement('div')
timeEl.className = 'pstats'
timeEl.textContent = '⏱ 已学: 0s · ⏳ 预计: —'
body.appendChild(timeEl)
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) idx = speeds.indexOf(1)
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(all)
var pageTotal = page.length
var pageDone = countCompleted(page)
var allTotal = all.length
var allDone = countCompleted(all)
/* Page progress */
if (pageTotal > 0) {
var pct = Math.round(pageDone / pageTotal * 100)
pageProg.pct.textContent = pct + '%'
pageProg.fill.style.width = pct + '%'
}
/* Global progress */
if (allTotal > 0) {
var gPct = Math.round(allDone / allTotal * 100)
globalProg.pct.textContent = allDone + '/' + allTotal + ' (' + gPct + '%)'
globalProg.fill.style.width = gPct + '%'
}
/* Time info */
var sessionSec = (Date.now() - CTRL.sessionStart) / 1000
var watchStr = formatDuration(CTRL.videoWatchSec)
var estimateStr = '—'
if (CTRL.itemsProcessed > 2 && allDone < allTotal) {
var avgSec = sessionSec / CTRL.itemsProcessed
var remaining = allTotal - allDone
var est = Math.round(avgSec * remaining)
estimateStr = formatDuration(est)
} else if (allDone >= allTotal) {
estimateStr = '✅ 已完成'
}
timeEl.textContent = '⏱ 已学 ' + watchStr + ' · 预计剩余 ' + estimateStr
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 maxAttempts = 8
for (var attempt = 0; attempt < maxAttempts; attempt++) {
var v = findVideos()
if (v.length > 0) return v
if (attempt < maxAttempts - 1) {
log('⏳ 等待视频加载 (' + (attempt + 1) + '/' + (maxAttempts - 1) + ')...')
await sleep(2000)
}
}
return []
}
function getNextBtn() {
var btn = document.getElementById('prevNextFocusNext')
return btn && btn.style.display !== 'none' && btn.offsetParent !== null ? btn : 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)
})
}
/* ========================= 课程树变化检测 ========================= */
var treeChanged = false
function setupTreeObserver() {
var tree = document.getElementById('coursetree')
if (!tree) return
var obs = new MutationObserver(function () { treeChanged = true })
obs.observe(tree, { childList: true, subtree: true, attributes: true })
}
/* ========================= 等待 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.muted = true
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(500) }
v.muted = true
await v.play()
} catch (e2) { /* 忽略 */ }
}
await monitorVideo(v)
log('✅ 第 ' + (i + 1) + '/' + videos.length + ' 个视频完成')
}
}
/* ========================= 监控单个视频进度 ========================= */
function monitorVideo(video) {
return new Promise(function (resolve) {
var prevPct = video.duration > 0 ? video.currentTime / video.duration : 0
var stallStart = 0
var timer = setInterval(function () {
try {
if (video.ended) {
clearInterval(timer)
resolve()
return
}
if (video.paused) {
video.muted = true
video.play().catch(function () {})
}
var pct = video.currentTime / video.duration
if (pct >= CONFIG.completionThreshold) {
clearInterval(timer)
resolve()
return
}
var delta = pct - prevPct
if (delta > 0) CTRL.videoWatchSec += delta * video.duration
var pctInt = Math.floor(pct * 100)
if (pctInt % 10 === 0 && pctInt !== Math.floor(prevPct * 100)) {
CTRL.status = '▶ 视频 ' + pctInt + '%'
log(' 进度: ' + pctInt + '%')
}
if (Math.abs(delta) < 0.001) {
if (stallStart === 0) stallStart = Date.now()
if (Date.now() - stallStart > 30000) {
log('⚠ 进度停滞超过30秒,强制完成')
clearInterval(timer)
resolve()
return
}
} else {
stallStart = 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 setupBackgroundManager() {
var wakeLock = null
async function requestWakeLock() {
try {
if ('wakeLock' in navigator) {
wakeLock = await navigator.wakeLock.request('screen')
wakeLock.addEventListener('release', function () { log('🔋 Wake Lock 释放') })
log('🔋 Wake Lock 已获取')
}
} catch (e) { log('🔋 Wake Lock:', e.message) }
}
requestWakeLock()
function recoverVideos() {
try {
var mainIframe = document.getElementById('iframe')
if (!mainIframe) return
var doc = mainIframe.contentDocument || mainIframe.contentWindow.document
if (!doc) return
var vids = doc.querySelectorAll('video#video_html5_api')
vids.forEach(function (v) {
if (v.paused && v.duration > 0 && v.currentTime < v.duration - 0.5) {
v.muted = true
v.play().catch(function () {})
}
})
} catch (e) {}
}
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') {
log('🔙 切回标签,立即恢复')
requestWakeLock()
recoverVideos()
}
})
window.addEventListener('focus', function () {
recoverVideos()
})
}
/* ========================= 获取当前章节的所有知识点 ========================= */
function getPageEntries(all) {
if (!all) 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() {
try {
log('═══ 开始 ═══')
log('模式: ' + (CONFIG.skipCompleted ? '刷课(跳过已学)' : '复习(全部重新播放)'))
log('倍速: ' + CONFIG.playbackRate + 'x')
log('最低停留: ' + (CONFIG.minStayMs / 1000) + '秒/知识点')
CTRL.status = '初始化...'
try { initControlPanel() } catch (e) { log('面板初始化失败:', e) }
try { setupPopupHandler() } catch (e) { log('弹窗监听失败:', e) }
try { overridePageVisibility() } catch (e) { log('可见性绕过失败:', e) }
try { setupXHRMonitor() } catch (e) { log('XHR监控失败:', e) }
try { setupBackgroundManager() } catch (e) { log('后台管理初始化失败:', e) }
window.addEventListener('beforeunload', saveStats)
setInterval(saveStats, 10000)
await waitForEl('#coursetree', 30000)
var treeEl = document.getElementById('coursetree')
if (treeEl) {
await waitForEl('.posCatalog_active', 20000, treeEl).catch(function () {})
}
setupTreeObserver()
await sleep(3000)
} catch (e) {
log('❌ 初始化失败:', e)
CTRL.status = '❌ 初始化失败'
return
}
/* 主循环:逐页处理 */
try {
var pageCount = 0
var itemsSinceBreak = 0
var stallCount = 0
var lastCompleted = 0
while (pageCount < CONFIG.maxPages) {
await checkPause()
if (treeChanged) {
treeChanged = false
log('⚠ 检测到课程树变化,重新扫描')
continue
}
pageCount++
log('\n——— 第 ' + pageCount + ' 轮处理 ———')
var entries = getEntries()
if (entries.length === 0) {
var waitRetries = 0
while (entries.length === 0 && waitRetries < 15) {
waitRetries++
log('⏳ 等待知识点加载 (' + waitRetries + '/15)...')
CTRL.status = '⏳ 等待课程树...'
await sleep(2000)
entries = getEntries()
}
if (entries.length === 0) { log('❌ 课程树为空,退出'); break }
}
var pageEntries = getPageEntries()
log('当前页共 ' + pageEntries.length + ' 个知识点')
if (pageEntries.length === 0) {
var nb = getNextBtn()
if (nb) {
nb.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()
if (treeChanged) {
treeChanged = false
log('⚠ 课程树变化,退出当前页处理')
break
}
var entry = pageEntries[i]
CTRL.currentTitle = entry.title
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)
/* ---- 最低停留时间(仅视频任务) ---- */
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)
} else {
log('📄 文档/附件,短停留后跳过')
CTRL.status = '📄 文件: ' + entry.title
/* 自动点击文件链接触发任务点 */
try {
var iframe = document.getElementById('iframe')
if (iframe) {
var doc = iframe.contentDocument || iframe.contentWindow.document
if (doc) {
/* 查找可打开的文件链接 */
var fileBtns = doc.querySelectorAll(
'a[href*="view"], a[href*="open"], a[href*="preview"], ' +
'a[href*=".pdf"], a[href*=".doc"], a[href*=".ppt"], a[href*=".xls"], a[href*=".txt"]'
)
fileBtns.forEach(function (el) { el.click() })
/* 查找文字按钮 */
doc.querySelectorAll('a, button, [class*="btn"], [class*="link"], [class*="down"]').forEach(function (el) {
var t = (el.textContent || '').trim()
if (t === '点击查看' || t === '查看' || t === '查看文件' || t === '打开' || t === '预览' || t === '下载') {
el.click()
}
})
/* 滚动到底部 */
if (doc.body) doc.body.scrollTop = doc.body.scrollHeight
}
}
} catch (e) {}
await randomDelay(6000, 10000)
}
simulateHumanBehavior()
processed++
CTRL.itemsProcessed++
/* ---- 进度停滞检测 ---- */
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 = '📄 翻页中...'
var nb = getNextBtn()
if (nb) {
nb.click()
await randomDelay(6000, 12000)
} else {
log('✅ 没有更多页面,全部完成!')
CTRL.status = '✅ 全部完成'
break
}
}
log('\n═══ 全部完成 ═══')
CTRL.status = '✅ 全部完成'
} catch (e) {
log('❌ 运行异常:', e)
CTRL.status = '❌ 异常: ' + (e.message || e)
}
}
/* ========================= 启动 ========================= */
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
setTimeout(main, 2000)
})
} else {
setTimeout(main, 2000)
}
})()