// ==UserScript== // @name 青马自动刷课助手(党课) // @namespace https://hnqmgc.17el.cn/ // @version 3.1.3 // @description 自动完成 hnqmgc.17el.cn 平台课程,高速播放+倍速控制+静音+自动循环 // @author Assistant // @match https://hnqmgc.17el.cn/* // @match https://qmbbs.17el.cn/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant unsafeWindow // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ==================== 持久化配置 ==================== function loadCfg() { return { speed: GM_getValue('cfgSpeed', 16), muted: GM_getValue('cfgMuted', true), autoMode: GM_getValue('autoMode', false), doneCourses: GM_getValue('doneCourses', []), }; } function saveSpeed(v) { GM_setValue('cfgSpeed', v); } function saveMuted(v) { GM_setValue('cfgMuted', v); } function saveAutoMode(v) { GM_setValue('autoMode', v); } function saveDoneCourses(v) { GM_setValue('doneCourses', v); } const Store = { done() { return GM_getValue('doneCourses', []); }, markDone(kcid, name) { const list = this.done(); if (!list.find(item => (item.kcid || item) === kcid)) { const now = new Date(); const pad = n => String(n).padStart(2, '0'); const time = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`; list.push({ kcid, name: name || '未知课程', time }); GM_setValue('doneCourses', list); } }, isDone(kcid) { return this.done().some(item => (item.kcid || item) === kcid); }, reset() { GM_deleteValue('doneCourses'); GM_deleteValue('autoMode'); }, setAuto(v) { GM_setValue('autoMode', v); }, isAuto() { return GM_getValue('autoMode', false); }, // 兼容旧格式:迁移纯字符串数组到新对象格式 migrate() { const list = GM_getValue('doneCourses', []); if (list.length > 0 && typeof list[0] === 'string') { GM_setValue('doneCourses', list.map(kcid => ({ kcid, name: '历史课程', time: '未知时间' }))); } }, }; // 启动时迁移旧数据 Store.migrate(); const cfg = loadCfg(); const log = (...a) => console.log('[刷课]', ...a); function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } // ==================== 多层速度绕过 ==================== // 第1层:存储原始setter引用 let _origPlaybackRateSet = null; const proto = HTMLMediaElement.prototype; const origDesc = Object.getOwnPropertyDescriptor(proto, 'playbackRate'); if (origDesc && origDesc.set) { _origPlaybackRateSet = origDesc.set; } // 第2层:劫持原型 getter → 始终返回安全值 if (origDesc && origDesc.get) { try { Object.defineProperty(proto, 'playbackRate', { get() { return 1.5; }, set(v) { try { _origPlaybackRateSet.call(this, v); } catch (e) {} }, configurable: true, enumerable: true, }); } catch (e) { log('⚠️ 原型劫持失败:', e.message); } } // 第3层:每出现一个video元素,在实例级别劫持(双重保险) function patchVideoInstance(v) { if (!v || v._patched) return; v._patched = true; try { Object.defineProperty(v, 'playbackRate', { get() { return 1.5; }, set(val) { try { _origPlaybackRateSet.call(this, val); } catch (e) {} }, configurable: true, }); } catch (e) { /* 实例劫持失败,原型劫持应该还在 */ } } // 第4层:拦截原生alert,阻止不需要的弹窗 if (typeof unsafeWindow.alert === 'function') { const _origAlert = unsafeWindow.alert.bind(unsafeWindow); unsafeWindow.alert = function (msg) { if (typeof msg === 'string') { if (msg.includes('倍速') || msg.includes('拖动')) { log('🛡️ 已拦截alert:', msg); return; } if (msg.includes('提交成功')) { log('🛡️ 已拦截alert(提交成功)'); return; // 文章课提交成功,不需要用户点确定 } } _origAlert(msg); }; } // 第5层:拦截layer.msg/layer.alert,阻止特定弹窗 function patchLayer() { const w = unsafeWindow; if (w.layer && !w.layer._patched) { const _origMsg = w.layer.msg; const _origAlert = w.layer.alert; w.layer.msg = function (content, options, end) { if (typeof content === 'string' && content.includes('拖动')) { log('🛡️ 已拦截layer.msg:', content); return 0; } return _origMsg.call(this, content, options, end); }; w.layer.alert = function (content, options, yes) { if (typeof content === 'string') { if (content.includes('拖动')) { log('🛡️ 已拦截layer.alert:', content); if (typeof yes === 'function') yes(0); return 0; } // 检测"视频观看程序已开启"错误 → 关闭弹窗,15秒后刷新重试 if (content.includes('视频观看程序已开启') || content.includes('关闭其他视频')) { log('🛡️ 检测到视频会话冲突,15秒后重试...'); if (typeof yes === 'function') yes(0); setTimeout(() => { location.reload(); }, 15000); return 0; } } return _origAlert.call(this, content, options, yes); }; w.layer._patched = true; } } // ==================== 弹窗自动点击 + 错误检测 ==================== function installDialogAutoClick() { new MutationObserver(() => { // 检测"视频观看程序已开启"错误弹窗 → 自动关闭并15秒后刷新 const allDivs = document.querySelectorAll('.layui-layer-dialog, .layui-layer-content'); allDivs.forEach(div => { if (div._errChecked) return; div._errChecked = true; const text = div.textContent || ''; if (text.includes('视频观看程序已开启') || text.includes('关闭其他视频')) { log('🛡️ DOM检测到视频会话冲突,15秒后刷新...'); const closeBtn = document.querySelector('.layui-layer-btn0, .layui-layer-btn a'); if (closeBtn) closeBtn.click(); setTimeout(() => { location.reload(); }, 15000); return; } }); // 匹配layerUI弹窗中所有按钮 → 自动点第一个 const btns = document.querySelectorAll('.layui-layer-btn a'); btns.forEach(btn => { if (btn._ac) return; btn._ac = true; const text = btn.textContent.trim(); log(`🖱️ 弹窗按钮: "${text}" → 自动点击`); if (btns[0] === btn) { setTimeout(() => btn.click(), 200); } }); }).observe(document.documentElement, { childList: true, subtree: true }); } // ==================== 阻止浏览器后台暂停视频 ==================== // 持续播放心跳:浏览器可能绕过 JS pause() 直接挂起视频, // 因此每 500ms 强制 play() 一次来对抗 let _keepAliveTimer = null; function startKeepAlive() { if (_keepAliveTimer) return; _keepAliveTimer = setInterval(() => { const videos = document.querySelectorAll('video'); videos.forEach(v => { if (!v._keepPatched) { // 劫持实例的 pause,作为第一道防线 const origPause = v.pause.bind(v); v.pause = function () { if (document.hidden) { console.log('[刷课] 阻止实例pause'); return; } return origPause(); }; v._keepPatched = true; } if (v.paused && v.src && v.duration > 0 && !v.ended) { // 后台时更激进地打日志 if (document.hidden) console.log('[刷课] 心跳恢复后台播放'); v.play().catch(() => {}); } }); }, 500); } function stopKeepAlive() { if (_keepAliveTimer) { clearInterval(_keepAliveTimer); _keepAliveTimer = null; } } // 页面恢复可见时立即恢复 document.addEventListener('visibilitychange', () => { if (!document.hidden) { document.querySelectorAll('video').forEach(v => { if (v.paused && v.src && v.duration > 0) { v.play().catch(() => {}); } }); } }); // 页面加载即启动心跳 startKeepAlive(); // ==================== 视频控制 ==================== function boostAllVideos(speed) { const videos = document.querySelectorAll('video'); videos.forEach(v => { patchVideoInstance(v); v.muted = loadCfg().muted; try { v.playbackRate = speed; } catch (e) {} }); } // ==================== 页面检测 ==================== function detectPage() { const u = location.href; if (u.includes('coursePlay')) return 'wrapper'; if (u.includes('/qmwbkc/')) return 'article'; // 文章课程 if (u.includes('play_125_') || (u.includes('play_') && u.includes('qmbbs'))) return 'player'; if (u.includes('personal_125') || u.includes('personal_')) return 'courseList'; if (u.includes('/grzx')) return 'grzx'; return 'unknown'; } // ==================== 课程列表页 ==================== // 记录连续"无课"次数,防止误判全部完成 let noCourseCount = 0; async function runCourseList() { log('📍 课程列表页'); // 劫持 goPlayCourse → 同窗口重定向 const w = unsafeWindow; if (typeof w.goPlayCourse === 'function' && !w.goPlayCourse._hijacked) { const origFn = w.goPlayCourse; w.goPlayCourse = function (kcid, me) { log(`🎯 重定向到课程 ${kcid}`); const url = 'https://qmbbs.17el.cn/coursePlay_125_' + kcid + '.shtml'; try { window.top.location.href = url; } catch (e) { location.href = url; } }; w.goPlayCourse._hijacked = true; log('✅ goPlayCourse已劫持'); } // 等待表格数据加载完成(AJAX可能还在请求中) const getCourses = () => { const rows = document.querySelectorAll('#tbody > tr'); const list = []; rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length < 6) return; // 跳过"暂无数据"行(colspan) const btn = row.querySelector('.cellButton'); if (!btn) return; list.push({ kcid: cells[0].textContent.trim(), name: cells[1].textContent.trim(), progress: cells[3].textContent.trim(), status: cells[4].textContent.trim(), btn: btn, }); }); return list; }; // 等待真实数据:检查是否有课程行,或分页总数是否为0 let courses = getCourses(); for (let i = 0; i < 60; i++) { if (courses.length > 0) break; // 已有数据 // 检查分页是否显示"总记录:0条"(真正没数据) const totalSpan = document.querySelector('[id*="total_span"]'); if (totalSpan) { const m = totalSpan.textContent.match(/总记录[::]\s*(\d+)/); if (m) { if (parseInt(m[1]) === 0) break; // 确实没数据 // 有总记录但表格为空 → 数据还在加载 log(`⏳ 等待表格数据... (总记录${m[1]}条)`); } } await sleep(500); courses = getCourses(); } log(`📚 当前页 ${courses.length} 门课`); // 过滤未完成 const todo = courses.filter(c => { if (Store.isDone(c.kcid)) return false; if (c.status === '已学完') { Store.markDone(c.kcid, c.name); return false; } if (c.progress === '100%') { Store.markDone(c.kcid, c.name); return false; } return true; }); log(`⏳ 还需完成: ${todo.length} 门 (已完成: ${Store.done().length})`); if (todo.length === 0) { noCourseCount++; // 尝试翻页 const nextPage = document.querySelector('.J-paginationjs-next:not(.disabled)'); if (nextPage) { noCourseCount = 0; log('📄 翻到下一页'); nextPage.click(); await sleep(2500); return runCourseList(); } // 当前页无课程,且没有下一页 if (courses.length === 0 && noCourseCount < 3) { // 表格可能还没加载完,等待重试 log(`⚠️ 表格可能未加载完成,等待重试 (${noCourseCount}/3)`); await sleep(3000); return runCourseList(); } // 确认全部完成(有课程但全学完了,或重试多次仍无数据) log('🎉 全部课程完成!'); Store.setAuto(false); updateStatus('全部完成!'); updateBtnState(); alert('🎉 恭喜!所有课程已刷完!'); return; } noCourseCount = 0; const next = todo[0]; updateStatus(`前往: ${next.name}`); log(`🚀 开始: [${next.kcid}] ${next.name} (${next.progress})`); await sleep(3000); // 等上一个视频会话过期 if (next.btn) { next.btn.click(); } else if (w.goPlayCourse) { w.goPlayCourse(next.kcid); } } // ==================== 文章课程页 ==================== async function runArticle() { log('📍 文章课程页'); // 获取课程ID(从URL参数) const u = location.href; const kcidMatch = u.match(/[?&]kcid=(\d+)/); const kcid = kcidMatch ? kcidMatch[1] : '?'; log(`📄 文章课程ID: ${kcid}`); // 等待完成按钮出现 updateStatus('文章课 - 等待加载...'); let btn = null; for (let i = 0; i < 60; i++) { btn = document.querySelector('#kwbtn'); if (btn) break; await sleep(1000); } if (!btn) { log('⚠️ 未找到完成按钮,可能已完成'); if (kcid !== '?') Store.markDone(kcid, '文章课程'); await sleep(2000); try { window.top.location.href = 'https://hnqmgc.17el.cn/grzx/'; } catch (e) { location.href = 'https://hnqmgc.17el.cn/grzx/'; } return; } log('📄 找到完成按钮,等待倒计时结束...'); // 监听倒计时:先等它开始(变为非00:00),再等它归零(回到00:00) const timerSpan = document.querySelector('#daojishi'); if (timerSpan) { // 阶段1:等待倒计时启动 let started = false; for (let i = 0; i < 30; i++) { const txt = timerSpan.textContent.trim(); if (txt !== '00:00') { started = true; break; } updateStatus(`文章课 - 等待倒计时启动...`); await sleep(1000); } // 如果30秒后仍未启动,刷新页面重试 if (!started) { log('⚠️ 倒计时未启动(firstajax可能失败),刷新重试'); updateStatus('倒计时异常,刷新...'); await sleep(2000); location.reload(); return; } // 阶段2:等待倒计时归零 for (let i = 0; i < 120; i++) { const txt = timerSpan.textContent.trim(); updateStatus(`文章课 - 倒计时 ${txt}`); if (txt === '00:00') break; await sleep(1000); } // 二次确认:等2秒再读一次,防止正好踩在00:00展示的边界 await sleep(2000); const confirmTxt = timerSpan.textContent.trim(); if (confirmTxt !== '00:00') { log(`⚠️ 倒计时未真正归零(当前${confirmTxt}),继续等待...`); updateStatus(`文章课 - 继续等待 (${confirmTxt})`); for (let i = 0; i < 30; i++) { const txt = timerSpan.textContent.trim(); if (txt === '00:00') break; await sleep(1000); } } log('📄 倒计时归零'); } else { log('⚠️ 未找到倒计时元素,等待10秒后点击'); updateStatus('文章课 - 等待10秒...'); await sleep(10000); } // 确保按钮可用 btn.disabled = false; btn.removeAttribute('disabled'); await sleep(500); log('🖱️ 点击完成学习按钮'); updateStatus('文章课 - 提交完成...'); btn.click(); // 如果 wykw() 没绑定或没触发,手动调用 if (typeof unsafeWindow.wykw === 'function') { unsafeWindow.wykw(); } // 等待 AJAX 完成 + 处理"提交成功"弹窗 await sleep(2000); const alertBtn = document.querySelector('.layui-layer-btn0, .layui-layer-btn a'); if (alertBtn) { alertBtn.click(); await sleep(500); } // 确认按钮已消失(提交成功后会hide) const btnAfter = document.querySelector('#kwbtn'); if (btnAfter && btnAfter.style.display !== 'none') { // 按钮还在,再点一次 log('⚠️ 按钮仍在,重试点击'); btnAfter.click(); if (typeof unsafeWindow.wykw === 'function') unsafeWindow.wykw(); await sleep(3000); } // 从父页面获取课程名称 let courseName = ''; try { const parentDoc = window.parent.document; const cnEl = parentDoc.querySelector('#courseName'); if (cnEl) courseName = cnEl.textContent.trim(); } catch (e) { /* ignore */ } if (kcid !== '?') Store.markDone(kcid, courseName || '文章课程'); log('🏠 返回个人中心'); updateStatus('返回列表...'); await sleep(2000); try { window.top.location.href = 'https://hnqmgc.17el.cn/grzx/'; } catch (e) { location.href = 'https://hnqmgc.17el.cn/grzx/'; } } // ==================== 播放器页 ==================== async function runPlayer() { log('📍 播放器页'); const kcid = (typeof currentKcId !== 'undefined') ? String(currentKcId) : (unsafeWindow.currentKcId) ? String(unsafeWindow.currentKcId) : '?'; log(`📖 课程ID: ${kcid}`); // 等待pathSource就绪 updateStatus('等待视频加载...'); for (let i = 0; i < 40; i++) { const ps = unsafeWindow.pathSource || window.pathSource; if (ps && ps.length > 0) break; await sleep(1000); } const getPathSource = () => unsafeWindow.pathSource || window.pathSource; const ps = getPathSource(); if (!ps || ps.length === 0) { log('⚠️ pathSource未就绪'); updateStatus('播放中...'); } else { log(`📑 ${ps.length} 个小节`); updateStatus(`学习中 (${ps.length}小节)`); } // 持续加速视频(动态读取配置,支持面板实时调倍速) const speedTimer = setInterval(() => boostAllVideos(loadCfg().speed), 1500); const speedObs = new MutationObserver(() => boostAllVideos(loadCfg().speed)); speedObs.observe(document.documentElement, { childList: true, subtree: true }); boostAllVideos(loadCfg().speed); // 网络错误检测:xgplayer报错时容器有 .xgplayer-is-error 类 → 自动刷新 const errorCheckTimer = setInterval(() => { const isErr = document.querySelector('.xgplayer-is-error'); if (isErr) { log('🔌 检测到xgplayer错误状态(网络波动黑屏),3秒后自动刷新...'); updateStatus('网络波动,即将刷新...'); clearInterval(errorCheckTimer); // 也尝试点击"刷新"按钮作为即时恢复 const refreshBtn = document.querySelector('.xgplayer-error-refresh'); if (refreshBtn) { refreshBtn.click(); } setTimeout(() => { location.reload(); }, 3000); } }, 5000); // 小节自动推进看门狗:不依赖心跳AJAX,直接检测视频状态 let lastSectionIdx = -1; const sectionWatchdog = setInterval(() => { const ps = getPathSource(); if (!ps || ps.length === 0) return; const video = document.querySelector('video'); // 找到当前正在播放的小节(process < 99 的第一个) let curIdx = -1; for (let i = 0; i < ps.length; i++) { if (Number(ps[i].process) < 99) { curIdx = i; break; } } // 如果所有小节都完成了,跳过 if (curIdx === -1) return; // 如果当前小节变化了(说明正常切换了),更新记录 if (curIdx !== lastSectionIdx && lastSectionIdx !== -1) { log(`📎 小节已切换: ${lastSectionIdx} → ${curIdx}`); } lastSectionIdx = curIdx; // 检测视频是否已结束但未触发切换 if (video && video.duration > 0 && video.ended) { log(`🔔 看门狗:视频已结束但小节${curIdx}未完成,强制跳转`); const nextIdx = curIdx + 1; if (nextIdx < ps.length) { unsafeWindow.onlyOne = 1; unsafeWindow.playByJid(ps[nextIdx], unsafeWindow.getStartBFSJ(ps[nextIdx].id)); } } // 检测视频长时间暂停(非用户操作,或页面被隐藏) if (video && video.paused && !video.ended && video.duration > 0 && video.currentTime > 0 && video.currentTime < video.duration - 5) { const now = Date.now(); if (!video._pauseSince) video._pauseSince = now; const timeout = document.hidden ? 3000 : 15000; // 后台3秒就恢复 if (now - video._pauseSince > timeout) { log('🔔 看门狗:视频暂停超过' + (timeout/1000) + '秒,尝试恢复播放(页面' + (document.hidden ? '隐藏' : '可见') + ')'); video._pauseSince = null; try { video.play(); } catch (e) {} } } else if (video) { video._pauseSince = null; } }, document.hidden ? 2000 : 4000); // 后台轮巡更频繁 // 监控完成 const checkDone = () => { const ps = getPathSource(); if (!ps || ps.length === 0) return false; return ps.every(item => (item.process >= 99 || item.process === 100)); }; await new Promise(resolve => { const iv = setInterval(() => { if (checkDone()) { clearInterval(iv); resolve(); } }, 3000); setTimeout(() => { clearInterval(iv); resolve(); }, 7200000); }); clearInterval(sectionWatchdog); log('🎯 所有小节完成!'); clearInterval(speedTimer); clearInterval(errorCheckTimer); speedObs.disconnect(); // 尝试获取课程名称(从父页面的#courseName) let courseName = ''; try { const parentDoc = window.parent.document; const cnEl = parentDoc.querySelector('#courseName'); if (cnEl) courseName = cnEl.textContent.trim(); } catch (e) { /* 跨域限制,忽略 */ } if (kcid !== '?') Store.markDone(kcid, courseName); log('🏠 返回个人中心'); updateStatus('返回列表,等待下一门...'); await sleep(3000); try { window.top.location.href = 'https://hnqmgc.17el.cn/grzx/'; } catch (e) { location.href = 'https://hnqmgc.17el.cn/grzx/'; } } // ==================== 控制面板 ==================== let panelStatusEl = null; let panelBtnEl = null; let panelSpeedEls = []; function updateStatus(msg) { if (panelStatusEl) panelStatusEl.textContent = msg; } function updateBtnState() { if (!panelBtnEl) return; if (Store.isAuto()) { panelBtnEl.textContent = '⏸ 停止自动刷课'; panelBtnEl.classList.add('stop'); } else { panelBtnEl.textContent = '▶ 开始自动刷课'; panelBtnEl.classList.remove('stop'); } } function updateSpeedUI() { const cur = loadCfg().speed; panelSpeedEls.forEach(el => { const spd = parseInt(el.dataset.speed); el.classList.toggle('active', spd === cur); }); } function createPanel(page) { if (page !== 'courseList' && page !== 'player' && page !== 'article') return; const savedPos = GM_getValue('panelPos', null); const curCfg = loadCfg(); const panel = document.createElement('div'); panel.id = 'as-panel'; panel.style.left = (savedPos && savedPos.left) ? savedPos.left : 'auto'; panel.style.right = (savedPos && !savedPos.left) ? '10px' : 'auto'; panel.style.top = (savedPos && savedPos.top) ? savedPos.top : '10px'; panel.innerHTML = `
自动刷课助手 v3 ⠿⠿
就绪
倍速
已绕过倍速限制 | 自动循环课程
`; function append() { if (document.body) { document.body.appendChild(panel); panelStatusEl = document.getElementById('as-st'); panelBtnEl = document.getElementById('as-go'); panelSpeedEls = Array.from(document.querySelectorAll('#as-speed-row button')); installDrag(); bindEvents(); updateBtnState(); updateSpeedUI(); updateLogBtn(); } else { setTimeout(append, 200); } } append(); // 更新日志按钮文字 function updateLogBtn() { const btn = document.getElementById('as-log-btn'); if (!btn) return; const n = Store.done().length; btn.textContent = n === 0 ? '📋 查看刷课日志' : `📋 刷课日志 (${n}门)`; } // ====== 拖拽功能 ====== function installDrag() { const handle = document.getElementById('as-drag'); if (!handle) return; let dragging = false, startX, startY, startLeft, startTop; handle.addEventListener('mousedown', function(e) { if (e.target.tagName === 'BUTTON' || e.target.closest('button')) return; dragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; // 切换为 left/top 定位 panel.style.right = 'auto'; panel.style.left = startLeft + 'px'; panel.style.top = startTop + 'px'; panel.style.transition = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', function(e) { if (!dragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; let newLeft = startLeft + dx; let newTop = startTop + dy; // 限制在视口内 const maxLeft = window.innerWidth - panel.offsetWidth - 10; const maxTop = window.innerHeight - 30; newLeft = Math.max(0, Math.min(newLeft, maxLeft)); newTop = Math.max(0, Math.min(newTop, maxTop)); panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px'; }); document.addEventListener('mouseup', function() { if (!dragging) return; dragging = false; panel.style.transition = ''; // 保存位置 GM_setValue('panelPos', { left: panel.style.left, top: panel.style.top, }); }); } function bindEvents() { const pageType = detectPage(); // 开始/停止按钮 const goBtn = document.getElementById('as-go'); if (pageType === 'player' || pageType === 'article') { // 播放器/文章页面:显示"返回列表"按钮 goBtn.textContent = '↩ 返回课程列表'; goBtn.classList.add('stop'); goBtn.addEventListener('click', () => { Store.setAuto(false); updateStatus('返回中...'); try { window.top.location.href = 'https://hnqmgc.17el.cn/grzx/'; } catch (e) { location.href = 'https://hnqmgc.17el.cn/grzx/'; } }); } else { // 课程列表页:开始/停止 goBtn.addEventListener('click', async () => { if (Store.isAuto()) { Store.setAuto(false); updateStatus('已停止'); updateBtnState(); return; } Store.setAuto(true); updateStatus('运行中...'); updateBtnState(); await runCourseList(); }); } // 倍速按钮 document.querySelectorAll('#as-speed-row button').forEach(btn => { btn.addEventListener('click', () => { const spd = parseInt(btn.dataset.speed); saveSpeed(spd); cfg.speed = spd; updateSpeedUI(); boostAllVideos(spd); updateStatus(`倍速已设为 ${spd}x`); }); }); // 静音切换 document.getElementById('as-mute').addEventListener('click', () => { const cur = loadCfg().muted; const next = !cur; saveMuted(next); cfg.muted = next; const btn = document.getElementById('as-mute'); btn.textContent = next ? '静音中' : '有声音'; btn.classList.toggle('muted', next); // 应用到所有video document.querySelectorAll('video').forEach(v => { v.muted = next; }); updateStatus(next ? '已静音' : '已开启声音'); }); // 重置 document.getElementById('as-rst').addEventListener('click', () => { if (confirm('确定重置所有已完成记录?')) { Store.reset(); updateStatus('已重置'); updateBtnState(); renderLog(); } }); // 日志展开/收起 const logDiv = document.getElementById('as-log'); const logBtn = document.getElementById('as-log-btn'); logBtn.addEventListener('click', () => { const isHidden = logDiv.style.display === 'none'; logDiv.style.display = isHidden ? 'block' : 'none'; logBtn.textContent = isHidden ? '📋 收起日志' : '📋 查看刷课日志'; if (isHidden) renderLog(); }); } function renderLog() { const logDiv = document.getElementById('as-log'); if (!logDiv) return; const list = Store.done(); if (list.length === 0) { logDiv.innerHTML = '
暂无记录
'; return; } // 按时间倒序显示最近20条 const recent = [...list].reverse().slice(0, 20); logDiv.innerHTML = recent.map((item, i) => { const num = list.length - i; let name = item.name || '未知课程'; // 课程名过长时截断 if (name.length > 25) name = name.substring(0, 25) + '...'; const kcid = item.kcid || item; const time = item.time || '未知时间'; return `
#${num} ${name}
ID:${kcid} | ${time}
`; }).join(''); if (list.length > 20) { logDiv.innerHTML += `
... 共 ${list.length} 条,仅显示最近20条
`; } document.getElementById('as-log-btn').textContent = `📋 ${list.length === 0 ? '查看刷课日志' : `刷课日志 (${list.length}门)`}`; } } // ==================== 主入口 ==================== async function main() { const page = detectPage(); log(`📍 页面: ${page} | ${location.href}`); switch (page) { case 'courseList': if (Store.isAuto()) { updateStatus('自动继续...'); await sleep(3000); await runCourseList(); } break; case 'player': await sleep(500); await runPlayer(); break; case 'article': await sleep(500); await runArticle(); break; case 'wrapper': break; case 'grzx': break; } } function init() { const page = detectPage(); // 播放器页面立即安装防护 if (page === 'player') { patchLayer(); installDialogAutoClick(); // 重写 currentFinish:跳过弹窗直接播放下一个小节 let _finishRetry = 0; const _overrideFinish = () => { const w = unsafeWindow; if (typeof w.currentFinish === 'function' && typeof w.playByJid === 'function' && !w.currentFinish._hijacked) { w.currentFinish = function () { const ps = w.pathSource || []; for (let i = 0; i < ps.length; i++) { if (Number(ps[i].process) < 99) { log(`⏭ 自动跳转小节: [${ps[i].id}] process=${ps[i].process}%`); w.onlyOne = 1; w.playByJid(ps[i], w.getStartBFSJ(ps[i].id)); return; } } log('📌 currentFinish: 所有小节已完成'); }; w.currentFinish._hijacked = true; log('✅ currentFinish已劫持(跳过弹窗自动下一节)'); } else if (_finishRetry < 30) { _finishRetry++; setTimeout(_overrideFinish, 500); } else { log('⚠️ currentFinish劫持超时,使用弹窗自动点击兜底'); } }; _overrideFinish(); // 监听video元素出现 new MutationObserver(() => { const videos = document.querySelectorAll('video'); videos.forEach(v => { patchVideoInstance(v); v.muted = loadCfg().muted; }); }).observe(document.documentElement, { childList: true, subtree: true }); } createPanel(page); if (page === 'player' || page === 'article') { main(); } else if (page === 'courseList' && Store.isAuto()) { updateStatus('自动继续刷课...'); main(); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();