// ==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();
}
})();