// ==UserScript== // @name 广开网络教学平台/广东开放大学【最高16倍】自动刷课 // @namespace http://course.ougd.cn/ // @version 2.5.1 // @description 广东开放大学课程视频自动播放、倍速、自动滚动文档、自动切换下一个学习任务,模拟观看行为 // @author GDOU Assistant // @match *://course.ougd.cn/* // @grant none // @run-at document-idle // @license All Rights Reserved // ==/UserScript== (function() { 'use strict'; // ========== iframe 处理 ========== // 脚本只在顶层页面运行,通过 contentWindow 注入 hook 到视频 iframe(preview.php) if (window.top !== window) return; // ========== 全局单例锁 - 防止重复执行 ========== if (window.__GDOUScriptInitialized__) return; window.__GDOUScriptInitialized__ = true; // ========== 劫持 addEventListener,阻止 beforeunload 事件注册(防止跳转时弹出确认框)========== (function() { var originalAddEventListener = window.addEventListener; window.addEventListener = function(type, listener, options) { // 拦截 beforeunload 事件的注册 if (type === 'beforeunload') { console.log('[GDOU] 拦截了 beforeunload 事件注册'); return; // 不注册,直接返回 } return originalAddEventListener.call(this, type, listener, options); }; // 同样劫持 document.addEventListener var docOriginalAddEventListener = document.addEventListener; document.addEventListener = function(type, listener, options) { if (type === 'beforeunload') { console.log('[GDOU] 拦截了 document.beforeunload 事件注册'); return; } return docOriginalAddEventListener.call(this, type, listener, options); }; // 立即清除已存在的 beforeunload window.onbeforeunload = null; document.onbeforeunload = null; console.log('[GDOU] 已初始化 beforeunload 拦截器'); })(); // ========== 时间欺骗 Hook(让平台的时间检测代码看到加速后的时间)========== // 原理:视频以 16x 播放时,currentTime 增量 = 80s/5s真实秒 // 但 Date.now() 报告真实时间增量 = 5s,服务器发现 80 > 5 → 判定倍速 → 拒绝 // 修复:Hook Date.now() 让它也报告 80s 增量,与 currentTime 增量匹配 (function() { var origDateNow = Date.now; var origPerfNow = (window.performance && performance.now) ? performance.now.bind(performance) : null; var hookState = { active: false, speed: 1, offset: 0, // 累计偏移量(ms) lastRealTime: null // 上次采样真实时间 }; // 计算加速后的时间 function getAcceleratedTime() { var realNow = origDateNow(); if (hookState.active) { if (hookState.lastRealTime !== null) { var realDelta = realNow - hookState.lastRealTime; if (realDelta > 0 && realDelta < 60000) { // 忽略异常大的间隔 hookState.offset += realDelta * (hookState.speed - 1); } } hookState.lastRealTime = realNow; } return realNow + hookState.offset; } // Hook Date.now() Date.now = getAcceleratedTime; // Hook new Date() 无参构造 var OrigDate = window.Date; function PatchedDate() { if (arguments.length === 0) { return new OrigDate(getAcceleratedTime()); } return new (Function.prototype.bind.apply(OrigDate, [null].concat(Array.prototype.slice.call(arguments)))); } PatchedDate.prototype = OrigDate.prototype; PatchedDate.now = getAcceleratedTime; PatchedDate.parse = OrigDate.parse; PatchedDate.UTC = OrigDate.UTC; try { PatchedDate.__proto__ = OrigDate.__proto__; } catch(e) {} window.Date = PatchedDate; // Hook performance.now() var perfOffset = 0; var perfLastReal = null; if (origPerfNow) { performance.now = function() { var realNow = origPerfNow(); if (hookState.active) { if (perfLastReal !== null) { var realDelta = realNow - perfLastReal; if (realDelta > 0 && realDelta < 60000) { perfOffset += realDelta * (hookState.speed - 1); } } perfLastReal = realNow; } return realNow + perfOffset; }; } // 暴露控制接口 window.__GDOU_TIME_HOOK__ = { start: function(speed) { hookState.speed = speed; hookState.lastRealTime = origDateNow(); perfLastReal = origPerfNow ? origPerfNow() : null; hookState.active = true; console.log('[GDOU] 时间加速Hook已启动: ' + speed + 'x (Date.now/performance.now已加速)'); }, stop: function() { if (hookState.active) { var realNow = origDateNow(); var realDelta = realNow - hookState.lastRealTime; if (realDelta > 0 && realDelta < 60000) { hookState.offset += realDelta * (hookState.speed - 1); } } hookState.active = false; hookState.speed = 1; console.log('[GDOU] 时间加速Hook已停止'); }, isRunning: function() { return hookState.active; }, getSpeed: function() { return hookState.speed; } }; console.log('[GDOU] 时间欺骗Hook已安装(Date.now/Date/performance.now)'); })(); // ========== 核心修复:拦截并篡改进度上报数据 ========== // 平台代码(preview.php 内联): // data[0].args.total_time = totaltime + (current_time - start_play) // data[0].args.current = player.currentTime() // data[0].args.duration = player.duration() // data[0].methodname = "mod_ougdresource_set_progress" // 服务器判断 total_time >= duration * 70% 才标记完成 // 修复:拦截 XHR.send,把 total_time 篡改为 duration(已看满全程) // 关键问题:油猴/ScriptCat 沙箱环境中,XMLHttpRequest.prototype.send 是不可配置不可重写的, // 而且沙箱的 XMLHttpRequest 和页面真实的 XMLHttpRequest 是不同对象。 // 无论直接赋值还是 Object.defineProperty 都会报 "Cannot redefine property: send" 错误。 // 解决方案:通过注入