1.购买服务器阿里云:服务器购买地址https://t.aliyun.com/U/DT4XYh若失效,可用地址
阿里云:
服务器购买地址
https://t.aliyun.com/U/DT4XYh若失效,可用地址
https://www.aliyun.com/activity/wuying/dj?source=5176.29345612&userCode=49hts92d腾讯云:
https://curl.qcloud.com/wJpWmSfU若失效,可用地址
https://cloud.tencent.com/act/cps/redirect?redirect=2446&cps_key=ad201ee2ef3b771157f72ee5464b1fea&from=console华为云
https://activity.huaweicloud.com/cps.html?fromacct=64b5cf7cc11b4840bb4ed2ea0b2f4468&utm_source=V1g3MDY4NTY=&utm_medium=cps&utm_campaign=2019052.部署教程
3.代码如下
let console_config={maxLogs: 1000, // 最大日志数量autoScroll: true, // 自动滚动到最新日志showTimestamp: true, // 显示时间戳videoPlayRate: 2.0 // 视频播放速率}let my_console;/*** 获取课程视频的观看信息* @param targetUrl 发送请求的地址*/function hijackXMLHTTPRequest(targetUrl) {const originalOpen = XMLHttpRequest.prototype.open;const originalSend = XMLHttpRequest.prototype.send;// 重写open方法,用于检测特定的请求URLXMLHttpRequest.prototype.open = function (method, url, async, user, password) {// 先调用原始方法originalOpen.apply(this, arguments);// 检查URL是否匹配要拦截的目标if (url && url.includes(targetUrl)) {// 监听readystatechange事件以获取响应this.addEventListener('readystatechange', function () {if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {try {// 服务器返回的数据在this.responseText中,通常是JSONconst responseData = JSON.parse(this.responseText);// 将数据存储到全局变量供页面其他脚本使用window._interceptedVideoLogData = responseData;} catch (e) {console.error('解析响应数据时出错:', e);}}});}};}hijackXMLHTTPRequest('/video-log/detail/');/*** 劫持雨课堂心跳发送api,利用Web Worker代替雨课堂原有js的定时器精准控制心跳发送,从而绕过浏览器针对后台页面的节流限制*/function hijackHeartbeat() {// 劫持 setIntervalconst _originalSetInterval = unsafeWindow.setInterval;unsafeWindow.setInterval = function(callback, interval, ...args) {//根据 interval参数来过滤心跳的定时器,心跳的定时器为5000msif (interval === 5000) {console.log("[定时器监听] 发现 setInterval: ", interval, "ms", "回调函数: ", callback);unsafeWindow.heartBeat = callback;unsafeWindow.heartContext=this;unsafeWindow.heartBeatArgs=args;return;}return _originalSetInterval.call(this, callback, interval, ...args);};//在主线程中创建Web Workerconst heartbeatBlob = new Blob([`const interval = 5000; // 10秒间隔setInterval(function() {postMessage({ type: 'tick'});}, interval);`], { type: 'application/javascript' });const heartbeatWorker = new Worker(URL.createObjectURL(heartbeatBlob));heartbeatWorker.onmessage = function(e) {if (e.data.type === 'tick') {// 收到Worker的定时信号,触发心跳发送逻辑if (unsafeWindow.heartBeat && unsafeWindow.heartContext&&my_console) {unsafeWindow.heartBeat.apply(unsafeWindow.heartContext,unsafeWindow.heartBeatArgs);}}};}//启动心跳劫持hijackHeartbeat();/*** css样式注入*/GM_addStyle(`#console-container {position: fixed;top: 20px; right: 20px;width: 450px;background: rgba(0,0,0,0.85);color: #fff;border: 1px solid #444;font-family: monospace;z-index: 99999;display: block}.console-header {padding: 8px;background: #333;cursor: move;}.console-switch {float: right;background: transparent;border: none;color: white;cursor: pointer;}.console-body {padding: 8px;height: 300px;overflow: auto;}.log-entry {margin-bottom: 4px;};/*** 防止雨课堂切屏检测*/function preventScreenCheck() {const blackList = new Set(["visibilitychange", "blur", "focus", "pagehide", "pageshow"]); // 需要拦截的事件类型// 保存原生方法const originalAddEventListener = window.EventTarget.prototype.addEventListener;const originalRemoveEventListener = window.EventTarget.prototype.removeEventListener;// 劫持 EventTarget.prototype.addEventListenerwindow.EventTarget.prototype.addEventListener = function(type, listener, options) {if (blackList.has(type)) {return undefined; // 直接返回undefined,阻止监听器被添加}return originalAddEventListener.call(this, type, listener, options);};// 劫持 EventTarget.prototype.removeEventListenerwindow.EventTarget.prototype.removeEventListener = function(type, listener, options) {if (blackList.has(type)) {return undefined;}return originalRemoveEventListener.call(this, type, listener, options);};// 伪造关键属性:让检测代码读取到"永远处于焦点和前台"的状态Object.defineProperties(document, {'hidden': {get: () => false,configurable: false,enumerable: true},'visibilityState': {get: () => 'visible',configurable: false,enumerable: true},'hasFocus': {value: () => true,configurable: false,writable: false},// 拦截对onvisibilitychange等的赋值'onvisibilitychange': {get: () => undefined,set: () => {},configurable: false,enumerable: true},'onblur': {get: () => undefined,set: () => {},configurable: false,enumerable: true},'onfocus': {get: () => undefined,set: () => {},configurable: false,enumerable: true},'onpagehide': {get: () => undefined,set: () => {},configurable: false,enumerable: true},'onpageshow': {get: () => undefined,set: () => {},configurable: false,enumerable: true}});// 伪造 window 对象的相关属性Object.defineProperties(window, {'onblur': {get: () => undefined,set: () => {},configurable: false,enumerable: true},'onfocus': {get: () => undefined,set: () => {},configurable: false,enumerable: true},'onpagehide': {get: () => undefined,set: () => {},configurable: false,enumerable: true},'onpageshow': {get: () => undefined,set: () => {},configurable: false,enumerable: true}});}/*** 鼠标滑动模拟器,模拟真人鼠标滑动*/class MouseSliderSimulator {constructor(options = {}) {// 合并配置参数this.config = {interval: options.interval || 3000, // 滑动间隔(毫秒)moveSteps: options.moveSteps || 8, // 每次滑动的步数maxOffset: options.maxOffset || 12, // 每次滑动的最大偏移量(像素)container: options.container || document, // 目标容器选择器autoStart: options.autoStart || false // 是否自动启动};this.containerElement = null;this.moveInterval = null;this.lastX = 0;this.lastY = 0;// 用户活动检测this.isUserActive = false;// 用户活动检测定时器this.userActivityTimer = null;// 修复: 提前绑定方法,确保this指向实例this._handleUserActivity = this._handleUserActivity.bind(this);// 初始化this.init();// 如果配置为自动启动,则开始滑动if (this.config.autoStart) {this.start();}}// 初始化方法init() {this.containerElement = this.config.container;if (!this.containerElement) {console.error(`目标对象 "${this.config.container}" 为空!`);return false;}// 获取容器边界并设置初始位置const containerRect = this.containerElement.getBoundingClientRect();this.lastX = containerRect.left + containerRect.width / 2;this.lastY = containerRect.top + containerRect.height / 2;// 立即移动鼠标到初始位置this.triggerMouseMove(this.lastX, this.lastY);this._setupUserActivityMonitoring();return true;}// 启动滑动模拟start() {if (!this.containerElement) {if (!this.init()) {return false;}}// 清除现有的定时器(防止重复启动)this.stop();this.moveInterval = setInterval(() => {this.slideMouse();}, this.config.interval);return true;}// 执行单次滑动slideMouse() {const containerRect = this.containerElement.getBoundingClientRect();// 生成带随机偏移的目标位置let targetX = this.lastX + (Math.random() * 2 - 1) * this.config.maxOffset;let targetY = this.lastY + (Math.random() * 2 - 1) * this.config.maxOffset;// 确保目标位置在容器边界内targetX = Math.max(containerRect.left, Math.min(containerRect.left + containerRect.width, targetX));targetY = Math.max(containerRect.top, Math.min(containerRect.top + containerRect.height, targetY));// 贝塞尔曲线控制点(增加随机轨迹)const controlX = this.lastX + (Math.random() - 0.5) * this.config.maxOffset * 2;const controlY = this.lastY + (Math.random() - 0.5) * this.config.maxOffset * 2;// 分步滑动(沿贝塞尔曲线路径)for (let i = 0; i <= this.config.moveSteps; i++) {setTimeout(() => {const t = i / this.config.moveSteps;// 二次贝塞尔曲线计算const stepX = Math.round((1-t)**2 * this.lastX + 2*(1-t)*t*controlX + t**2*targetX);const stepY = Math.round((1-t)**2 * this.lastY + 2*(1-t)*t*controlY + t**2*targetY);this.triggerMouseMove(stepX, stepY);// 更新最后位置if (i === this.config.moveSteps) {this.lastX = stepX;this.lastY = stepY;}}, i * 50); // 步间延迟}}// 触发鼠标移动事件triggerMouseMove(x, y) {const event = new MouseEvent('mousemove', {clientX: x,clientY: y,bubbles: true,});document.dispatchEvent(event);}// 停止滑动stop() {if (this.moveInterval) {clearInterval(this.moveInterval);this.moveInterval = null;}}// 更新配置updateConfig(newConfig) {Object.assign(this.config, newConfig);// 如果容器选择器有变化,重新初始化if (newConfig.container && newConfig.container !== this.config.container) {this.init();}return this;}// 销毁实例,清理资源destroy() {this.stop();// 移除事件监听器document.removeEventListener('mousemove', this._handleUserActivity);document.removeEventListener('keydown', this._handleUserActivity);document.removeEventListener('click', this._handleUserActivity);// 清除用户活动定时器if (this.userActivityTimer) {clearTimeout(this.userActivityTimer);}this.containerElement = null;}// 监听真人操作事件_setupUserActivityMonitoring() {document.addEventListener('mousemove', this._handleUserActivity);document.addEventListener('keydown', this._handleUserActivity);document.addEventListener('click', this._handleUserActivity);// 可根据需要添加其他事件监听}// 事件处理函数_handleUserActivity(event) {// 检查事件是否由用户真实操作触发if (event.isTrusted) {// 立即停止鼠标模拟this.stop();// 标记用户为活跃状态this.isUserActive = true;// 清除现有的定时器(如果存在)if (this.userActivityTimer) {clearTimeout(this.userActivityTimer);}// 设置一个新的定时器,3秒后认为用户不再活跃this.userActivityTimer = setTimeout(() => {this.isUserActive = false;this.start();}, 3000); // 3秒无操作后重置}}}/*** 控制台类的实现*/class Console {constructor(options = {}) {// 默认配置this.config = {maxLogs: 1000,// 最大日志数量autoScroll: true,// 自动滚动到最新日志showTimestamp: true,// 显示时间戳videoPlayRate: 2.0,// 视频播放速率...options// 用户自定义配置覆盖默认配置};// 创建控制台容器this.container = document.createElement('div');this.container.id = 'console-container';// 创建标题栏this.header = document.createElement('div');this.header.className = 'console-header';this.header.innerHTML = `<span class="console-title">Console</span><button class="console-switch">收起</button>`;// 创建日志区域this.logContainer = document.createElement('div');this.logContainer.className = 'console-body';// 组装元素this.container.appendChild(this.header);this.container.appendChild(this.logContainer);document.body.appendChild(this.container);//给头部的console-close按钮添加点击隐藏事件this.header.querySelector('.console-switch').addEventListener('click', () => this.switch());// 给控制台容器添加拖拽功能this._setupDrag(this.header);// 日志存储this.logs = [];}// 核心日志方法log(...args) {this._addEntry('log', 'INFO', '#d4d4d4', ...args);}warn(...args) {this._addEntry('warn', 'WARN', '#d7ba7d', ...args);}error(...args) {this._addEntry('error', 'ERROR', '#f44747', ...args);}clear() {this.logContainer.innerHTML = '';this.logs = [];}show() {this.logContainer.style.height = '300px';this.header.querySelector('.console-switch').innerText = "收起"}hide() {this.logContainer.style.height = '0px';this.header.querySelector('.console-switch').innerText = "展开"}switch() {if (this.logContainer.style.height === "0px") {this.show()} else {this.hide()}}// 内部实现_addEntry(type, label, color, ...messages) {// 创建日志条目const logEntry = document.createElement('div');logEntry.className = `log-entry log-${type}`;// 如果设置中显示时间戳,则添加时间戳元素if (this.config.showTimestamp) {const timeSpan = document.createElement('span');timeSpan.className = 'log-time';timeSpan.textContent = new Date().toLocaleTimeString();logEntry.appendChild(timeSpan);}// 添加日志标签const labelSpan = document.createElement('span');labelSpan.textContent = `[${label}] `;labelSpan.style.color = color;logEntry.appendChild(labelSpan);// 处理消息内容messages.forEach(msg => {const contentSpan = document.createElement('span');if (msg.nodeType===1){contentSpan.appendChild(msg);} else if(typeof msg === 'object'){contentSpan.textContent = JSON.stringify(msg, null, 2);} else {contentSpan.textContent = msg;}contentSpan.style.color = color;logEntry.appendChild(contentSpan);logEntry.appendChild(document.createTextNode(' '));});// 添加到容器this.logContainer.appendChild(logEntry);this.logs.push(logEntry);// 自动滚动if (this.config.autoScroll) {this.logContainer.scrollTop = this.logContainer.scrollHeight;}// 日志数量限制if (this.logs.length > this.config.maxLogs) {this.logs.shift().remove();}}// 拖拽功能实现_setupDrag(header) {let isDragging = false;let offsetX, offsetY;// 鼠标按下事件,开始拖拽header.addEventListener('mousedown', (e) => {isDragging = true;offsetX = e.clientX - this.container.getBoundingClientRect().left;offsetY = e.clientY - this.container.getBoundingClientRect().top;this.container.style.cursor = 'grabbing';});document.addEventListener('mousemove', (e) => {if (!isDragging) return;this.container.style.left = (e.clientX - offsetX) + 'px';this.container.style.top = (e.clientY - offsetY) + 'px';this.container.style.right = 'unset';this.container.style.bottom = 'unset';});document.addEventListener('mouseup', () => {isDragging = false;this.container.style.cursor = 'default';});}}/*** 模拟人类点击事件,欺骗某些对事件检测严格的元素* @param element*/const simulateHumanClick = (element) => {const rect = element.getBoundingClientRect();const events = [{type: 'mousemove', x: rect.left-10, y: rect.top-10},{type: 'mousemove', x: rect.left+5, y: rect.top+5},{type: 'mousedown'},{type: 'mouseup'},{type: 'click'}];events.forEach(e => {const event = new MouseEvent(e.type, {bubbles: true,clientX: e.x || rect.left + rect.width/2,clientY: e.y || rect.top + rect.height/2});element.dispatchEvent(event);});};/*** dom元素查找的可靠实现* @param selector 选择器字符串* @param targetContainer 可选,默认为document。指定在哪个容器内查找元素* @param timeout 可选,默认为500ms。dom树变动的最长等待时间* @param baseDelay 可选,默认为10ms。每次重试的延迟时间,指数增长* @param maxRetries 可选,默认为5。最大重试次数* @returns {Promise<unknown>} Promise对象,解析为找到的元素或错误信息*/function waitForElement(selector, targetContainer = document, timeout = 1000, baseDelay = 10, maxRetries = 10) {return new Promise((resolve, reject) => {let retryCount = 0;function attempt() {// 1. 立即检查const element = targetContainer.querySelector(selector);if (element) {return resolve(element);}// 2. 设置单次观察的超时let timeoutId = setTimeout(() => {observer.disconnect();onTimeout();}, timeout);// 3. 启动Observer监听const observer = new MutationObserver((mutations) => {});observer.observe(targetContainer, { childList: true, subtree: true });// 4. 处理单次观察超时的函数function onTimeout() {retryCount++;if (retryCount <= maxRetries) {// 等待指数退避时间后,进行下一次attemptsetTimeout(attempt, baseDelay * Math.pow(2, retryCount - 1));} else {reject(new Error(`查找元素${selector}失败,重试次数已达上限,请检查网络连接。`));}}}// 开始第一次尝试attempt();});}/*** 页面为选择课程主页时执行的逻辑*/function selectLessonPageLogic(){my_console.log("当前页面为课程选择页面,正在查找所有的课程...");let app = document.getElementById("app");waitForElement(".grid",app).then(element=>{setTimeout(()=>{let lessonsName=element.querySelectorAll("h1");my_console.log("查找结果:");let names=[];lessonsName.forEach(node=>{names.push(node.textContent.trim());})my_console.log(names);my_console.warn("请选择对应的课程进行学习,脚本会自动开始刷课");},1000);}).catch(err=>{my_console.error(err);});}/*** 课程中选择学习内容才处理逻辑*/function selectLessonItemPageLogic(){my_console.log("当前页面为课程页面,正在寻找第一个未完成的课程...");let app = document.getElementById("app");waitForElement(".main-box",app).then((element)=>{setTimeout(()=>{let states= element.querySelectorAll(".chapter-list>.content>.el-tooltip>.progress-time>.progress-wrap>.item");let i;for(i =0;i<states.length;i++){if(states[i].textContent!=="已完成"){my_console.log("已找到,开始学习...");states[i].click();break;}}if(i===states.length){my_console.log("全部课程已经学完!");}},1000);}).catch(err=>{my_console.error(err);});}/*** 自动播放视频*/function autoPlayVideo(){GM_addStyle(`.el-dialog__wrapper{display:none !important;}`)/*** 对视频进行区间播放,跳过已经播放过的视频*/function intervalPlay(){let watchedInterval=window._interceptedVideoLogData.data.heartbeat.result;my_console.log("已观看区间:"+JSON.stringify(watchedInterval));let currentInterval=0;let end=currentInterval<watchedInterval.length?watchedInterval[currentInterval]['s']:videoElement.duration;videoElement.currentTime=0;//在主线程中创建Web Workerconst videoFlushBlob = new Blob([`const interval = 10000; // 10秒间隔setInterval(function() {self.postMessage({ type: 'check'});}, interval);` ], { type: 'application/javascript' });videoElement.play();const videoFlushWorker = new Worker(URL.createObjectURL(videoFlushBlob));videoFlushWorker.postMessage({type:'launch'});videoFlushWorker.onmessage = function(e) {if (e.data.type === 'check') {my_console.log("当前视频时间:"+videoElement.currentTime);// 收到Worker的定时信号if(videoElement.currentTime<=videoElement.duration&&videoElement.currentTime>=end){my_console.log("跳过已经观看过的区间:"+watchedInterval[currentInterval]['s']+"-"+watchedInterval[currentInterval]['e']);videoElement.currentTime=watchedInterval[currentInterval]['e'];currentInterval++;end=currentInterval<watchedInterval.length?watchedInterval[currentInterval]['s']:videoElement.duration;}}};}my_console.log("当前页面为视频播放页面,正在自动播放视频...");let videoElement;//获取雨课堂的最高层静态divlet app=document.getElementById('app');let callback=()=>{//如果该视频已经是完成状态了,则直接跳转下一个waitForElement(".title-fr>.progress-wrap>.item>.text",app).then(finish=>{if(finish.innerText==="完成度:100%"){my_console.log("当前课程已经学完,即将跳转至下一个课程...");let nextButton=document.querySelector('span.btn-next.ml20.pointer');simulateHumanClick(nextButton);}}).catch(err=>{location.reload();});//打印当前课程名称waitForElement('.title-fl > span',app).then(title=>{my_console.log("当前课程:"+title.innerText);}).catch(err=>{my_console.error("课程名出错:"+err);location.reload();});//选择视频播放速率let rateListPromise=waitForElement('.xt_video_player_common_list',app);let rateButtonPromise=waitForElement('xt-speedbutton',app);//两个dom元素必须都要获取到Promise.all([rateListPromise,rateButtonPromise]).then(([rateList,rateButton])=>{//如果用户设定的速率并不在原有速率列表中,将速率列表中的第一个速率改成对应速率if (![0.5, 1.0, 1.25, 1.5, 2.0].includes(console_config.videoPlayRate)){let newVideoPlayRate = rateList.childNodes[0];newVideoPlayRate.setAttribute('data-speed', console_config.videoPlayRate);newVideoPlayRate.setAttribute('keyt',console_config.videoPlayRate);newVideoPlayRate.innerText=console_config.videoPlayRate.toFixed(2)+"x";}//鼠标移动到速率选择上let rateButtonRect = rateButton.getBoundingClientRect();const mouseMove = new MouseEvent('mousemove', {bubbles: true,cancelable: true,clientX: (rateButtonRect.left+rateButtonRect.right)/2, // 相对于视口的X坐标clientY: (rateButtonRect.top+rateButtonRect.bottom)/2 // 相对于视口的Y坐标});rateButton.dispatchEvent(mouseMove);//选择速率的第一个rateList.children[0].click();//静音播放视频waitForElement('xt-controls > xt-inner > xt-volumebutton > xt-icon',app).then((mute=>{//静音播放视频mute.click();intervalPlay();})).catch(err=>{my_console.error("静音:"+err);location.reload();})}).catch(err=>{my_console.error("速率调整:"+err);location.reload();})}//寻找视频元素,并对其进行操作waitForElement("video.xt_video_player",app).then(element=>{videoElement=element;//触发获取视频详情apiwaitForElement('.log-detail').then(element=>{element.click();waitForElement('.v-modal').then(element => {element.remove();})})//监听视频暂停事件,重新播放视频videoElement.addEventListener("pause",()=>{if (videoElement.currentTime <= videoElement.duration - 1){videoElement.play();}});//监听视频播放完毕事件,自动跳转至下一个视频videoElement.addEventListener("ended",()=>{let nextButton=document.querySelector('span.btn-next.ml20.pointer');if(nextButton){my_console.log("当前视频播放完毕,3s后播放下一个视频...");const nextVideoTimer = new Blob([`const interval = 3000; // 3秒间隔setTimeout(function() {self.postMessage({ type: 'next'});}, interval);` ], { type: 'application/javascript' });const nextVideoWorker = new Worker(URL.createObjectURL(nextVideoTimer));nextVideoWorker.onmessage = function(e) {if (e.data.type === 'next') {simulateHumanClick(nextButton);}};}else{my_console.log("最后一个视频播放完毕,本课程已经结束!");}});let mouseSliderConfig={container: app,autoStart: true}window.mouseSliderSimulator = new MouseSliderSimulator(mouseSliderConfig);const callbackTimer = new Blob([`const interval = 1000; // 3秒间隔setTimeout(function() {self.postMessage({ type: 'callback'});}, interval);` ], { type: 'application/javascript' });const callbackWorker = new Worker(URL.createObjectURL(callbackTimer));callbackWorker.onmessage = function(e) {if (e.data.type === 'callback') {callback();}};}).catch(err=>{my_console.error("寻找视频元素"+err);location.reload();});}//当路由变动时,所需要进行的操作function onRouteChange(path) {my_console.clear();if(window.mouseSliderSimulator){window.mouseSliderSimulator.destroy();}start(path);}/*** 劫持跳转页面的行为,重新匹配目的地址的操作逻辑*/function hackHistoryApi(){/*** History API* 包含 pushState()和 replaceState()方法,用于主动操作浏览器历史记录栈:* pushState():新增历史记录条目(URL 变化但页面不刷新)* replaceState():替换当前历史记录条目(常用于静默更新 URL)* 本质是开发者主动控制路由的工具。* popstate事件* 属于被动监听机制,在用户触发浏览器行为(如点击前进/后退按钮)或调用 history.back()/forward()时自动触发。* 本质是对用户导航行为的响应。*///劫持原有的History APIconst _originalPushState = history.pushState;const _originalReplaceState = history.replaceState;//重写History API.增加路由改变时的行为逻辑history.pushState = function (state, title, url) {//执行原始的 pushState 方法const result = _originalPushState.apply(this, arguments);onRouteChange(url)return result;};history.replaceState = function (state, title, url) {// 执行原始的 replaceState 方法const result = _originalReplaceState.apply(this, arguments);onRouteChange(url);return result;};//修复:以上的History Api劫持不能作用于浏览器的前进/后退,因此监听 popstate 事件(右键前进/后退触发)window.addEventListener('popstate', () => {onRouteChange(window.location.pathname);});}//正则表达式匹配规则class Regex{static videoPathRegex = /^\/pro\/[^\/]+(\/[^\/]+)*\/video\/[^\/]+$/;static lessonPathRegex = /^\/pro(\/.*)?\/studycontent$/;static host="/pro/courselist";}//网页不同路由的处理逻辑匹配function start(currentPath){//页面匹配处理逻辑:查找表const pathHandler=[{ condition: path => path === Regex.host, action: selectLessonPageLogic },{ condition: path => Regex.lessonPathRegex.test(path), action: selectLessonItemPageLogic },{ condition: path => Regex.videoPathRegex.test(path), action: autoPlayVideo },{ condition: path => true, action: ()=>{my_console.error("未知路径,暂未开发对应的功能,请进入学习空间")}}]//页面匹配处理my_console.log("当前路径:"+currentPath);for(const{condition,action} of pathHandler){if(condition(currentPath)){action();break;}}}/*** 主程序*/(function(){'use strict';preventScreenCheck();hackHistoryApi();window.addEventListener('DOMContentLoaded', function() {my_console=new Console(console_config);my_console.log(welcome);const currentPath = window.location.pathname;start(currentPath);});})()
解析
这是一个在雨课堂网页端自动化刷课脚本。它会自动识别你当前所处的课程页面,查找未完成的小节,自动进入视频页、调整播放速率、静音播放、监看播放进度并在结尾自动跳到下一节。脚本同时带一个页面内的浮动"控制台",用于显示运行日志、拖拽定位与一键收起。
主要方法
页面路由匹配与分发(
start()+ 路由劫持)
监听并拦截前端路由变更(包含前进/后退),根据 URL 判定当前是"课程列表""小节列表"还是"视频播放"页面,然后分发到对应逻辑函数。元素等待与稳健查询(
waitForElement())
对指定选择器做多次、指数退避的 DOM 查询;结合MutationObserver等待页面渲染完成,避免"元素未就绪"导致的操作失败。自动学习流程(
selectLessonPageLogic() / selectLessonItemPageLogic() / autoPlayVideo())
在课程列表页罗列课程标题、提示用户进入;
在小节列表页自动定位第一个未完成的小节并进入;
在视频页自动设置播放速率与静音、监听播放结束并跳转下一节,且定期检查已看区间,避免重复观看。
浮动日志面板(
Console类)
页面右上角自带的"控制台",支持拖拽、自动滚动、时间戳、收起/展开等,统一输出脚本运行状态与提示信息。"人类操作"模拟(
simulateHumanClick() & MouseSliderSimulator)
通过合成鼠标事件与缓慢随机移动,尽量让点击/移动行为更像真人操作,降低被简单前端规则识别为脚本的概率(但不保证安全)。
页面"前台"与心跳维系
以前端层面的手段,尽量让页面维持"可见/有焦点"的状态,并以更稳定的方式定时触发心跳/进度上报,避免后台标签页被节流影响(不展开细节,以免造成不当用途)。请求拦截读取状态
监听与课程进度相关的请求响应,从返回数据中拿到"已观看区间",据此跳过重复片段。
注意:
本文部分变量已做脱敏处理,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。技术层面需要提供帮助,可以通过打赏的方式进行探讨。
没有评论:
发表评论