// ==UserScript== // @name 智慧培训平台视频助手(wlcb.axhpx.com) // @namespace https://wlcb.axhpx.com/ // @version 3.2.0 // @description 课程视频自动播放 + 自定义倍速 + 实时滚动日志。 // @author 一心向善 // @match https://wlcb.axhpx.com/student/* // @run-at document-idle // @grant none // @license All Rights Reserved // ==/UserScript== (function () { 'use strict'; /* ================= 配置区 ================= */ const CONFIG = { // 检测机制:每 100ms 检查「视频前进秒数 > 墙钟秒数 + 3」即判快进。 // 前台:4x 以内安全;后台标签页定时器被节流(最久 1 分钟/次),任何倍速都可能触发 → 默认 3x + 自动恢复兜底。 defaultRate: 3, // 默认倍速(稳定推荐) rates: [1, 1.5, 2, 2.5, 3, 4, 6, 8], // 下拉可选项(4x 及以上需保持页面前台) keepPlaying: true, // 被检测暂停时自动恢复播放 suppressWarnToast: true, // 自动隐藏“检测到快进行为”警告弹窗 autoEnterCourse: true, // 在“待学课程”页自动进入第一个课程 tickMs: 1000, logEverySec: 15 // 每 15 秒记录一次播放位置 }; /* =========================================== */ /* ---------- 实时滚动日志 ---------- */ let logBox = null, lastLogPos = 0, lastMsg = ''; function addLog(msg) { if (!logBox || msg === lastMsg) return; // 相同消息不重复刷 lastMsg = msg; const time = new Date().toLocaleTimeString('zh-CN', { hour12: false }); const line = document.createElement('div'); line.textContent = `[${time}] ${msg}`; logBox.appendChild(line); while (logBox.children.length > 60) logBox.removeChild(logBox.firstChild); logBox.scrollTop = logBox.scrollHeight; // 实时滚动到底部 } const log = (...a) => { console.log('%c[倍速助手]', 'color:#2196f3', ...a); }; const native = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate'); let desiredRate = parseFloat(localStorage.getItem('wlcb_rate')) || CONFIG.defaultRate; function findByText(selector, text) { return [...document.querySelectorAll(selector)] .find(el => el.offsetParent !== null && (el.innerText || '').trim().includes(text)); } const fmt = s => Math.floor(s / 60) + ':' + String(Math.floor(s % 60)).padStart(2, '0'); /* ---------- 监听平台心跳上报(只读观察,不修改) ---------- */ const origOpen = XMLHttpRequest.prototype.open, origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (m, u) { this.__url = u; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function () { if (this.__url && String(this.__url).includes('addOrUpdateLearningHours')) { this.addEventListener('load', () => { try { const r = JSON.parse(this.responseText); addLog(Number(r.code) === 200 ? '学时上报 ✓ 服务器已接收' : '学时上报 ✗ ' + (r.message || r.code)); } catch (e) { } }); } return origSend.apply(this, arguments); }; /* ---------- 抑制快进警告弹窗(仅隐藏提示,播放照常) ---------- */ if (CONFIG.suppressWarnToast) { new MutationObserver(() => { document.querySelectorAll('.el-message, [class*=message]').forEach(el => { if (/快进|禁止学习|禁止修改播放倍速/.test(el.textContent)) el.style.display = 'none'; }); }).observe(document.documentElement, { childList: true, subtree: true }); } /* ---------- 锁定倍速,对抗平台重置 ---------- */ function lockVideo(v) { if (v.__rateLocked) return; Object.defineProperty(v, 'playbackRate', { configurable: true, get() { return native.get.call(this); }, set() { native.set.call(this, desiredRate); } // 平台设任何值都强制写回目标倍速 }); v.__rateLocked = true; addLog('视频已锁定倍速 ' + desiredRate + 'x'); } function applyRate(v) { if (native.get.call(v) !== desiredRate) { native.set.call(v, desiredRate); addLog('倍速已应用: ' + desiredRate + 'x'); } } /* ---------- 控制面板(常驻所有页面,SPA 切换不消失) ---------- */ let rateLabel = null; function ensurePanel() { if (document.getElementById('wlcb-rate-panel')) return; const panel = document.createElement('div'); panel.id = 'wlcb-rate-panel'; panel.style.cssText = 'position:fixed;right:16px;top:100px;z-index:999999;width:230px;' + 'background:rgba(20,20,20,.88);color:#fff;padding:10px 12px;border-radius:10px;' + 'font:12px/1.6 sans-serif;box-shadow:0 2px 10px rgba(0,0,0,.4);user-select:none'; const title = document.createElement('div'); title.textContent = '倍速助手'; title.style.cssText = 'font-weight:bold;margin-bottom:4px'; const select = document.createElement('select'); select.style.cssText = 'width:100%;padding:2px 4px;border-radius:4px;color:#000;margin-bottom:4px'; CONFIG.rates.forEach(r => { const opt = document.createElement('option'); opt.value = r; opt.textContent = r + 'x' + (r === 3 ? ' (稳定推荐)' : r >= 4 ? ' (需前台)' : ''); if (r === desiredRate) opt.selected = true; select.appendChild(opt); }); select.onchange = () => { desiredRate = parseFloat(select.value); localStorage.setItem('wlcb_rate', desiredRate); const v = document.querySelector('video'); if (v) native.set.call(v, desiredRate); addLog('切换倍速: ' + desiredRate + 'x'); }; // 手动播放/暂停按钮(手动暂停后自动恢复不生效,再点一次恢复) const playBtn = document.createElement('button'); playBtn.textContent = '⏸ 暂停'; playBtn.style.cssText = 'width:100%;padding:4px;border:none;border-radius:4px;cursor:pointer;margin-bottom:4px;background:#4caf50;color:#fff'; playBtn.onclick = () => { const v = document.querySelector('video'); if (!v) { addLog('当前页面没有视频,请先进入课程播放页'); return; } if (allDone) { addLog('已全部完成,无需操作'); return; } if (v.paused) { manualHold = false; (document.querySelector('.vjs-big-play-button') || v).click?.(); v.play().catch(() => { }); playBtn.textContent = '⏸ 暂停'; addLog('手动播放'); } else { manualHold = true; v.pause(); playBtn.textContent = '▶ 播放'; addLog('手动暂停(自动恢复已挂起)'); } }; rateLabel = document.createElement('div'); rateLabel.style.cssText = 'color:#8fd18f;margin-bottom:4px'; logBox = document.createElement('div'); logBox.style.cssText = 'max-height:150px;overflow-y:auto;background:rgba(255,255,255,.08);' + 'border-radius:6px;padding:4px 6px;color:#ccc'; panel.append(title, select, playBtn, rateLabel, logBox); document.body.appendChild(panel); addLog('助手已启动,目标倍速 ' + desiredRate + 'x'); } /* ---------- 学习页 ---------- */ let lastLesson = ''; let endHold = false, allDone = false, manualHold = false; // 全部完成:暂停视频并停止一切自动操作 function stopAll(reason) { if (allDone) return; allDone = true; const v = document.querySelector('video'); if (v) { try { v.pause(); } catch (e) { } } addLog('✅ ' + reason + ',已自动停止,等待人工关闭'); const panel = document.getElementById('wlcb-rate-panel'); if (panel) panel.style.boxShadow = '0 0 0 2px #4caf50'; } // 播放结束监听:宽限期内等平台切换下一课时;若超时未切换、或同一视频从头重播(最后课时循环),判定全部完成 function watchEnded(v) { if (v.__endWatched) return; v.__endWatched = true; v.addEventListener('ended', () => { if (allDone) return; endHold = true; // 宽限期:禁止“暂停自动恢复”,避免把播完的视频拉起重播 addLog('本课时播放结束,等待平台切换下一课时…'); const src0 = v.src, el0 = v; let elapsed = 0; const iv = setInterval(() => { elapsed += 1; const cur = document.querySelector('video'); if (!cur) { if (elapsed > 20) { clearInterval(iv); endHold = false; } return; } // 新视频元素或换了片源 → 平台已切到下一课时,继续 if (cur !== el0 || (src0 && cur.src !== src0)) { clearInterval(iv); endHold = false; addLog('已切换到下一课时,继续播放'); return; } // 同一视频从头重播 → 最后一个课时在循环,全部已完成 if (!cur.paused && cur.currentTime > 1 && cur.currentTime < 10) { clearInterval(iv); stopAll('全部视频播放完成'); return; } // 宽限期结束仍未切换 → 视为全部完成 if (elapsed > 20) { clearInterval(iv); stopAll('全部视频播放完成'); } }, 1000); }); } function handleStudy() { const v = document.querySelector('video'); if (!v) return; lockVideo(v); applyRate(v); watchEnded(v); if (!v.muted) v.muted = true; // 课时切换检测(目录中当前选中项) const lesson = document.querySelector('.is-checked .el-tree-node__label, .el-tree-node.is-current .el-tree-node__label')?.textContent?.trim(); if (lesson && lesson !== lastLesson) { lastLesson = lesson; addLog('进入课时: ' + lesson); lastLogPos = 0; } if (CONFIG.keepPlaying && !endHold && !allDone && !manualHold && v.paused) { const bigPlay = document.querySelector('.vjs-big-play-button'); if (bigPlay) bigPlay.click(); else v.play().catch(() => { }); addLog('检测到暂停,已自动恢复播放'); } // 定期记录播放位置 if (!v.paused && v.currentTime - lastLogPos >= CONFIG.logEverySec * desiredRate) { lastLogPos = v.currentTime; addLog(`播放中 ${fmt(v.currentTime)}/${fmt(v.duration || 0)} @ ${desiredRate}x`); } if (rateLabel) rateLabel.textContent = '当前: ' + native.get.call(v) + 'x ' + fmt(v.currentTime) + '/' + fmt(v.duration || 0); } /* ---------- 待学课程页:自动进入 ---------- */ let entered = false, tabClicked = false; function handleCourseCenter() { if (!CONFIG.autoEnterCourse || entered) return; // 优先直接找课程按钮(当前已在待学课程列表则无需点标签) const btn = findByText('button', '继续学习') || findByText('button', '开始学习'); if (btn) { entered = true; btn.click(); addLog('自动进入课程…'); return; } // 列表还没出来时才点一次标签 if (!tabClicked) { const tab = findByText('button, .el-tabs__item', '待学课程'); if (tab) { tabClicked = true; tab.click(); addLog('已切换到「待学课程」'); } } } function tick() { try { ensurePanel(); const hash = location.hash; if (hash.includes('/courseStudy')) handleStudy(); else if (hash.includes('/center/course')) handleCourseCenter(); } catch (e) { log('异常:', e); } } setInterval(tick, CONFIG.tickMs); tick(); })();