// ==UserScript== // @name 四川职业技术在线教育-自动刷课 // @namespace https://www.iwdjy.com/ // @version 0.1 // @description 倍速播放+API双模式自动完成课程。优先使用页面Vue实例+倍速播放,兼容性更强 // @author 流方 // @match https://www.iwdjy.com/course-learn* // @icon https://www.iwdjy.com/static/wending.ico // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // @connect nbv.iwdjy.com // @run-at document-end // @license MIT // ==/UserScript== // 你的实际代码从这里开始... (function() { 'use strict'; // ========== 配置 ========== var CONFIG = { API_BASE: 'https://nbv.iwdjy.com', // 倍速播放速率(1-16,越大越快,但也可能触发反作弊) PLAYBACK_RATE: 16, // 快进时的步长(秒),每次跳这么多,低于speed_play的+20阈值 SEEK_STEP: 18, // 每步间隔(ms) SEEK_INTERVAL: 2500, // 视频加载超时(ms) VIDEO_TIMEOUT: 30000, // 每课时间延迟(ms) LESSON_DELAY: 3000, // 显示面板 SHOW_PANEL: true, // 策略优先级: 'api'(直调接口) | 'vuerate'(Vue实例+倍速) | 'seekstep'(逐步快进) STRATEGY: 'vuerate', }; var STATE = { running: false, paused: false, currentIndex: 0, total: 0, lessons: [], courseId: null, stats: { ok: 0, fail: 0, skip: 0 }, strategy: '', }; // ========== 日志 ========== function log(msg, type) { type = type || 'info'; console.log('[AutoPlay v2]', msg); var el = document.getElementById('ap-log'); if (!el) return; var line = document.createElement('div'); line.className = 'log-line log-' + type; var t = new Date().toLocaleTimeString(); line.textContent = '[' + t + '] ' + msg; el.appendChild(line); el.scrollTop = el.scrollHeight; while (el.children.length > 80) el.removeChild(el.firstChild); } function updateStatus(s) { var el = document.getElementById('ap-status'); if (el) el.textContent = s; } function updateProgress(cur, total) { var d = document.getElementById('ap-detail'); var f = document.getElementById('ap-progress'); if (d) d.textContent = cur + ' / ' + total; if (f) f.style.width = total > 0 ? Math.round(cur / total * 100) + '%' : '0%'; } // ========== UI面板 ========== function createPanel() { var panel = document.createElement('div'); panel.id = 'iwdjy-autoplay-panel'; panel.innerHTML = '' + '

🤖 自动刷课 v2

' + '
↕ 拖动标题栏移动面板
' + '
⏳ 等待开始...
' + '
' + '
0 / 0
' + '
策略: --
' + '
' + '' + '' + '' + '
' + '' + '' + '
' + '
📋 脚本已加载,点击"开始"运行
'; document.body.appendChild(panel); // 按钮事件 document.getElementById('ap-btn-start').addEventListener('click', start); document.getElementById('ap-btn-pause').addEventListener('click', togglePause); document.getElementById('ap-btn-stop').addEventListener('click', stop); document.getElementById('ap-btn-strat').addEventListener('click', switchStrategy); document.getElementById('ap-btn-verify').addEventListener('click', verifyProgress); // 拖动功能 setupDrag(panel); } function setupDrag(panel) { var handle = document.getElementById('ap-drag-handle'); if (!handle) return; var isDragging = false; var startX, startY, startLeft, startTop; handle.addEventListener('mousedown', function(e) { // 忽略右键 if (e.button !== 0) return; isDragging = true; panel.classList.add('dragging'); // 记录初始鼠标位置和面板位置 startX = e.clientX; startY = e.clientY; var rect = panel.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; e.preventDefault(); }); document.addEventListener('mousemove', function(e) { if (!isDragging) return; var dx = e.clientX - startX; var dy = e.clientY - startY; var newLeft = startLeft + dx; var newTop = startTop + dy; // 边界限制 var maxLeft = window.innerWidth - panel.offsetWidth - 5; var maxTop = window.innerHeight - panel.offsetHeight - 5; newLeft = Math.max(5, Math.min(newLeft, maxLeft)); newTop = Math.max(5, Math.min(newTop, maxTop)); // 切换为 left/top 定位 panel.style.right = 'auto'; panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px'; }); document.addEventListener('mouseup', function() { if (isDragging) { isDragging = false; panel.classList.remove('dragging'); } }); // 触摸设备支持 handle.addEventListener('touchstart', function(e) { if (e.touches.length !== 1) return; isDragging = true; panel.classList.add('dragging'); startX = e.touches[0].clientX; startY = e.touches[0].clientY; var rect = panel.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; }, {passive: false}); document.addEventListener('touchmove', function(e) { if (!isDragging) return; e.preventDefault(); var dx = e.touches[0].clientX - startX; var dy = e.touches[0].clientY - startY; var newLeft = startLeft + dx; var newTop = startTop + dy; var maxLeft = window.innerWidth - panel.offsetWidth - 5; var maxTop = window.innerHeight - panel.offsetHeight - 5; newLeft = Math.max(5, Math.min(newLeft, maxLeft)); newTop = Math.max(5, Math.min(newTop, maxTop)); panel.style.right = 'auto'; panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px'; }, {passive: false}); document.addEventListener('touchend', function() { if (isDragging) { isDragging = false; panel.classList.remove('dragging'); } }); } // ========== 辅助 ========== function sleep(ms) { return new Promise(function(r) { setTimeout(r, ms); }); } function sleepWhilePaused() { return new Promise(function(resolve) { (function check() { if (!STATE.paused || !STATE.running) resolve(); else setTimeout(check, 500); })(); }); } function findVideo() { return document.querySelector('video'); } function getLessonLinks() { var all = document.querySelectorAll('a'); var result = []; for (var i = 0; i < all.length; i++) { if (/^课时\d+/.test(all[i].textContent.trim())) { result.push(all[i]); } } return result; } function findNextBtn() { var cl = document.querySelector('.check-learn'); if (!cl) return null; var spans = cl.querySelectorAll('span'); for (var i = 0; i < spans.length; i++) { if (spans[i].textContent.trim() === '下一节' && spans[i].className.indexOf('ok') >= 0) { return spans[i]; } } for (var j = 0; j < spans.length; j++) { if (spans[j].textContent.trim() === '下一节') return spans[j]; } return null; } // ========== 策略0: 直调API ========== function apiPost(path, data) { return new Promise(function(resolve, reject) { GM_xmlhttpRequest({ method: 'POST', url: CONFIG.API_BASE + path, headers: { 'Content-Type': 'application/json;charset=UTF-8', 'Accept': 'application/json, text/plain, */*', 'X-Requested-With': 'XMLHttpRequest', }, data: JSON.stringify(data), onload: function(r) { try { resolve(JSON.parse(r.responseText)); } catch(e) { reject(new Error('parse: ' + r.responseText.slice(0,100))); } }, onerror: function(e) { reject(new Error('network')); }, ontimeout: function() { reject(new Error('timeout')); }, timeout: 15000, }); }); } async function strategyApi(courseId) { STATE.strategy = 'api'; log('🔌 策略: 直调API (nbv.iwdjy.com)', 'info'); updateStrategyLabel(); var resp = await apiPost('/api/lesson/getLessonHour', { id: String(courseId) }); if (resp.code !== 200 || !resp.data) { log('❌ API获取课时失败: ' + JSON.stringify(resp).slice(0,100), 'error'); return false; } STATE.lessons = resp.data.data; STATE.total = STATE.lessons.length; log('✅ 获取到 ' + STATE.total + ' 个课时', 'success'); for (var i = 0; i < STATE.lessons.length; i++) { if (!STATE.running) break; if (STATE.paused) { await sleepWhilePaused(); if (!STATE.running) break; } var les = STATE.lessons[i]; STATE.currentIndex = i + 1; log('▶️ [' + (i+1) + '/' + STATE.total + '] ' + les.name + ' (' + les.play_time + 's)', 'info'); updateStatus('▶️ ' + les.name); try { var r1 = await apiPost('/api/lesson/saveUserHour', { hour_id: String(les.id), play_time: String(les.play_time) }); var r2 = await apiPost('/api/lesson/afterWatchCompleted', { lesson_id: String(courseId), hour_id: String(les.id), play_time: String(les.play_time) }); if (r1.code === 200 && r1.msg.indexOf('成功') >= 0 && r2.code === 200) { log(' ✅ ' + les.id + ' 完成!', 'success'); STATE.stats.ok++; } else if (r1.msg && r1.msg.indexOf('登陆') >= 0) { log(' ⚠️ 未登录,API策略失败', 'warn'); STATE.stats.fail++; updateProgress(i+1, STATE.total); return false; } else { log(' ⚠️ save=' + r1.msg + ' complete=' + r2.msg, 'warn'); STATE.stats.fail++; } } catch(e) { log(' ❌ 网络错误: ' + e.message, 'error'); STATE.stats.fail++; } updateProgress(i+1, STATE.total); if (i < STATE.lessons.length - 1) await sleep(CONFIG.LESSON_DELAY); } return true; } // ========== 策略1: Vue实例 + 倍速播放 ========== function getVueInstance() { try { // 尝试多种方式获取Vue实例 var app = document.querySelector('#app'); if (app && app.__vue__) return app.__vue__; // Vue 2 把实例挂在元素上 var all = document.querySelectorAll('[data-v-]'); for (var i = 0; i < Math.min(all.length, 20); i++) { if (all[i].__vue__) return all[i].__vue__; } } catch(e) {} return null; } async function strategyVueRate(courseId) { STATE.strategy = 'vuerate'; log('🖥️ 策略: 倍速播放(' + CONFIG.PLAYBACK_RATE + 'x) + 逐步快进', 'info'); updateStrategyLabel(); var links = getLessonLinks(); if (links.length === 0) { log('❌ 未找到课时列表', 'error'); return false; } STATE.total = links.length; STATE.currentIndex = 0; log('✅ 从页面识别到 ' + STATE.total + ' 个课时', 'success'); updateProgress(0, STATE.total); for (var i = 0; i < STATE.total; i++) { if (!STATE.running) break; if (STATE.paused) { await sleepWhilePaused(); if (!STATE.running) break; } STATE.currentIndex = i + 1; // 切换课时 if (i === 0) { // 第一个:点击课时链接 var fl = getLessonLinks(); if (fl.length > 0) { var v0 = findVideo(); if (!v0 || !v0.duration) { log(' 🖱️ 点击课时1', 'info'); fl[0].click(); await sleep(3000); } } } else { var nb = findNextBtn(); if (nb) { log(' ➡️ 点击下一节', 'info'); nb.click(); await sleep(4000); } else { log(' ⚠️ 未找到"下一节"按钮,可能已到末尾', 'warn'); break; } } // 获取当前文本 var curLinks = getLessonLinks(); var label = i < curLinks.length ? curLinks[i].textContent.trim().replace(/\s+/g, ' ') : '课时' + (i+1); log('▶️ [' + (i+1) + '/' + STATE.total + '] ' + label, 'info'); updateStatus('▶️ ' + label); // 等待视频加载 var video = null; var waitStart = Date.now(); while (Date.now() - waitStart < CONFIG.VIDEO_TIMEOUT) { if (!STATE.running) break; video = findVideo(); if (video && video.duration > 0 && video.readyState >= 2) break; await sleep(1000); } if (!video || !video.duration) { log(' ❌ 视频加载超时', 'error'); STATE.stats.fail++; updateProgress(i+1, STATE.total); continue; } var totalDuration = video.duration; log(' 🎬 视频就绪, 时长=' + Math.round(totalDuration) + 's', 'info'); // 混合策略: 倍速 + 逐步快进 var completed = await hybridFastForward(video, totalDuration); if (completed) { log(' ✅ 完成!', 'success'); STATE.stats.ok++; } else { log(' ❌ 完成失败', 'error'); STATE.stats.fail++; } updateProgress(i+1, STATE.total); await sleep(CONFIG.LESSON_DELAY); } return true; } async function hybridFastForward(video, totalDuration) { // 阶段1: 设置倍速,播放一小段自然触发saveUserHour try { video.playbackRate = CONFIG.PLAYBACK_RATE; } catch(e) {} video.muted = true; // 静音避免噪音 video.play(); log(' ⚡ 倍速=' + CONFIG.PLAYBACK_RATE + 'x, 静音播放中...', 'info'); // 阶段2: 逐步快进到结尾 // 使用较小的步长(18秒),小于speed_play的+20阈值 var current = video.currentTime; var targetTime = totalDuration - 8; // 留8秒让自然结束 while (current < targetTime && STATE.running) { current = Math.min(current + CONFIG.SEEK_STEP, targetTime); try { video.currentTime = current; } catch(e) {} await sleep(CONFIG.SEEK_INTERVAL); // 每5步报告一次进度 if (Math.floor(current / (CONFIG.SEEK_STEP * 5)) !== Math.floor((current - CONFIG.SEEK_STEP) / (CONFIG.SEEK_STEP * 5))) { var pct = Math.round(current / totalDuration * 100); updateStatus('⏩ ' + pct + '% (' + Math.round(current) + 's/' + Math.round(totalDuration) + 's)'); } } // 阶段3: 恢复1x速度,自然播放到结尾触发ended事件 try { video.playbackRate = 1; } catch(e) {} video.currentTime = totalDuration - 3; video.play(); log(' 🏁 接近结尾,等待自然结束...', 'info'); // 等待ended var waitEnd = Date.now(); while (Date.now() - waitEnd < 30000 && STATE.running) { if (video.ended) { log(' 🎉 视频自然结束!', 'success'); return true; } await sleep(1000); } // 如果还没结束,手动触发 if (!video.ended) { try { video.currentTime = totalDuration; await sleep(1500); video.dispatchEvent(new Event('ended', {bubbles: true})); log(' ⚡ 手动触发ended', 'info'); return true; } catch(e) { log(' ❌ 触发ended失败: ' + e.message, 'error'); return false; } } return true; } // ========== 验证进度 ========== async function verifyProgress() { log('🔍 开始验证进度...', 'info'); var courseId = new URLSearchParams(window.location.search).get('id'); if (!courseId) { log('❌ 无法获取课程ID', 'error'); return; } try { var resp = await apiPost('/api/lesson/getLessonHour', { id: String(courseId) }); if (resp.code !== 200) { log('❌ API失败', 'error'); return; } var list = resp.data.data; var done = 0; log('--- 进度报告 ---', 'info'); for (var i = 0; i < list.length; i++) { // 检查每个课时 try { var ur = await apiPost('/api/lesson/getUserHour', { hour_id: String(list[i].id) }); var isDone = ur.data && ur.data.is_complete == 1; if (isDone) done++; var pct = ur.data ? ur.data.show_play : '?'; log(' [' + list[i].id + '] ' + list[i].name + ' | ' + pct + ' | ' + (isDone ? '✅' : '❌'), isDone ? 'success' : 'warn'); } catch(e) { log(' [' + list[i].id + '] ' + list[i].name + ' | 查询失败', 'error'); } } log('📊 总计: ' + done + '/' + list.length + ' 已完成', done === list.length ? 'success' : 'info'); } catch(e) { log('❌ 验证失败: ' + e.message, 'error'); } } // ========== 主流程 ========== async function start() { if (STATE.running && !STATE.paused) { log('⚠️ 已在运行', 'warn'); return; } STATE.running = true; STATE.paused = false; STATE.stats = { ok: 0, fail: 0, skip: 0 }; STATE.currentIndex = 0; document.getElementById('ap-btn-pause').textContent = '⏸ 暂停'; var courseId = new URLSearchParams(window.location.search).get('id'); if (!courseId) { log('❌ 未找到课程ID参数', 'error'); return; } STATE.courseId = courseId; log('🚀 开始! 课程ID=' + courseId + ' 策略=' + CONFIG.STRATEGY, 'success'); var success = false; // 尝试主策略 if (CONFIG.STRATEGY === 'api') { success = await strategyApi(courseId); if (!success) { log('⚠️ API策略失败,自动切换到倍速策略...', 'warn'); CONFIG.STRATEGY = 'vuerate'; STATE.stats = { ok: 0, fail: 0, skip: 0 }; success = await strategyVueRate(courseId); } } else if (CONFIG.STRATEGY === 'vuerate') { success = await strategyVueRate(courseId); if (!success) { log('⚠️ 倍速策略失败,尝试API策略...', 'warn'); CONFIG.STRATEGY = 'api'; STATE.stats = { ok: 0, fail: 0, skip: 0 }; success = await strategyApi(courseId); } } else { success = await strategyVueRate(courseId); } finish(success); } function finish(success) { STATE.running = false; var msg = '🏁 ' + (success ? '完成!' : '中断') + ' 成功:' + STATE.stats.ok + ' 失败:' + STATE.stats.fail + ' 策略:' + STATE.strategy; log(msg, STATE.stats.fail === 0 ? 'success' : 'info'); updateStatus(msg); updateProgress(STATE.total, STATE.total); if (typeof GM_notification !== 'undefined') { GM_notification({ title: '文顶教育自动刷课', text: msg, timeout: 5000 }); } // 自动验证 log('🔍 自动验证中...', 'info'); setTimeout(verifyProgress, 3000); } function togglePause() { STATE.paused = !STATE.paused; var btn = document.getElementById('ap-btn-pause'); btn.textContent = STATE.paused ? '▶ 继续' : '⏸ 暂停'; log(STATE.paused ? '⏸ 已暂停' : '▶ 继续', 'info'); updateStatus(STATE.paused ? '⏸ 已暂停' : '▶ 运行中...'); } function stop() { STATE.running = false; STATE.paused = false; log('⏹ 已停止', 'info'); updateStatus('⏹ 已停止'); document.getElementById('ap-btn-pause').textContent = '⏸ 暂停'; } function switchStrategy() { var strategies = ['vuerate', 'api']; var idx = strategies.indexOf(CONFIG.STRATEGY); CONFIG.STRATEGY = strategies[(idx + 1) % strategies.length]; log('🔄 切换到: ' + CONFIG.STRATEGY, 'info'); updateStrategyLabel(); } function updateStrategyLabel() { var el = document.getElementById('ap-strategy'); if (el) el.textContent = '策略: ' + STATE.strategy + ' | 倍速: ' + CONFIG.PLAYBACK_RATE + 'x'; } // ========== 防暂停机制(强力版)========== function setupAntiPause() { // 1. 持续清除 window.onblur(页面代码用它暂停视频) setInterval(function() { if (window.onblur) window.onblur = null; }, 500); // 2. 强制覆盖 window.onblur — 用 defineProperty 让它只能设为我们允许的值 try { var _blur = null; Object.defineProperty(window, 'onblur', { get: function() { return _blur; }, set: function(fn) { // 包装一下:页面想设的 handler 被我们拦截 _blur = function(e) { // 不调用原始fn,什么都不做 console.log('[AutoPlay] 拦截 onblur 暂停'); }; }, configurable: true }); } catch(e) {} // 3. 监听 video 的 pause 事件,立即恢复 var watchVideoPause = function() { var v = findVideo(); if (v && !v._apPauseHooked) { v._apPauseHooked = true; v.addEventListener('pause', function() { if (!STATE.running || STATE.paused) return; if (this.ended) return; log(' 🔄 视频被暂停,立即恢复', 'info'); setTimeout(function() { v.play().catch(function(){}); }, 50); }); v.addEventListener('waiting', function() { if (!STATE.running || STATE.paused) return; // 缓冲等待也尝试恢复 setTimeout(function() { v.play().catch(function(){}); }, 200); }); } }; setInterval(watchVideoPause, 2000); // 4. 高频轮询:检测到暂停就恢复 setInterval(function() { if (!STATE.running || STATE.paused) return; var v = findVideo(); if (!v) return; // 确保新视频也加了 hook if (!v._apPauseHooked) watchVideoPause(); // 如果暂停了且未结束,强制播放 if (v.paused && !v.ended) { v.play().catch(function(){}); } }, 300); // 5. visibilitychange:Tab切回来时恢复 document.addEventListener('visibilitychange', function(e) { if (!STATE.running || STATE.paused) return; if (!document.hidden) { var v = findVideo(); if (v && v.paused && !v.ended) { v.play().catch(function(){}); } } e.stopImmediatePropagation(); }, true); // 6. 防止页面通过 removeEventListener 去掉我们的 blur 保护 var origRemoveEvt = EventTarget.prototype.removeEventListener; EventTarget.prototype.removeEventListener = function(type, listener, options) { // 如果页面试图移除 blur 相关的监听,忽略 if (type === 'blur' && this === window) { console.log('[AutoPlay] 拦截 removeEventListener(blur)'); return; } return origRemoveEvt.call(this, type, listener, options); }; // 7. 最高频监控:requestAnimationFrame 循环(仅页面可见时有效) (function rafLoop() { if (STATE.running && !STATE.paused) { var v = findVideo(); if (v && v.paused && !v.ended) { v.play().catch(function(){}); } } requestAnimationFrame(rafLoop); })(); log('🛡️ 防暂停(强力版)已启用', 'info'); } // ========== 初始化 ========== function init() { if (window.top !== window.self) return; // 立即启用防暂停 setupAntiPause(); updateStrategyLabel(); if (CONFIG.SHOW_PANEL) createPanel(); log('📋 文顶教育刷课脚本 v2.0 已加载', 'info'); log('💡 点击"开始"运行, 或切换策略后开始', 'info'); log('📌 策略说明: vuerate=倍速+快进(稳) | api=直调接口(快)', 'info'); log('🛡️ 已启用防失焦暂停保护', 'success'); // 不自动开始,等用户点击 } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();