// ==UserScript== // @name 江苏开放全自动化【公益版】 // @namespace http://tampermonkey.net/ // @version 1.0.1 // @description 江苏开放全自动化【可加速及自动下一页】 // @author 一心向善 // @match *://xuexi.jsou.cn/* // @match *://*.jsou.cn/* // @run-at document-idle // @grant none // @noframes // @license MIT // ==/UserScript== /** * 江苏开放全自动化【公益版】辅助脚本 v1.0.1 * * v1.0→v1.0.1 修复: * 1. 持久化运行状态(localStorage)— 页面跳转后自动恢复"启动中"状态 * 2. 增加最小化/恢复按钮 * 3. 在持久化模式下自动重启 * */ (function () { // 注意:不要 use strict,否则给 XHR prototype 赋值会抛 TypeError // 非严格模式下赋值会静默失败,不影响批量调用原生 sendHeartBeatAjax() var LS_KEY = 'studyHelperV52'; // ============ 配置(从 localStorage 恢复)============ function loadConfig() { try { var saved = JSON.parse(localStorage.getItem(LS_KEY) || 'null'); if (saved) return saved; } catch (e) {} return null; } function saveConfig() { try { localStorage.setItem(LS_KEY, JSON.stringify(CONFIG)); } catch (e) {} } var CONFIG = Object.assign({ docHeartbeats: 600, batchInterval: 600, videoSpeed: 1.0, autoNextDelay: 1500, keepAliveInterval: 10000 }, loadConfig() || {}); var state = { running: false, processing: false, currentResourceType: '', currentActivityId: '', heartbeatCount: 0, successCount: 0, failCount: 0, startTime: null, onlineTimer: null, speedTimer: null, xhrHooked: false, heartbeatSent: 0, heartbeatTarget: 0, waitingForVideo: false, moObserver: null, batchStartTs: 0, minimized: false }; // 持久化运行状态 —— 如果上次是运行中,这次自动恢复 function checkAutoResume() { try { var saved = JSON.parse(localStorage.getItem(LS_KEY + '_running') || 'null'); if (saved && saved.running) { log('🔁 检测到上次运行中,自动恢复...', 'auto'); // 恢复配置 CONFIG = Object.assign(CONFIG, saved.config || {}); CONFIG.videoSpeed = parseFloat(CONFIG.videoSpeed) || 1.0; CONFIG.docHeartbeats = parseInt(CONFIG.docHeartbeats) || 40; // 更新面板控件 setTimeout(function () { var inp = document.getElementById('cfg-doc-hearts'); var sel = document.getElementById('cfg-video-speed'); if (inp) inp.value = CONFIG.docHeartbeats; if (sel) sel.value = String(CONFIG.videoSpeed); updateTimeLabels(); // 启动自动流程 startAuto(true); // autoResume=true 时不重复设置 CONFIG }, 1500); } } catch (e) {} } function persistRunning(running) { try { if (running) { localStorage.setItem(LS_KEY + '_running', JSON.stringify({ running: true, config: CONFIG, startTime: Date.now() })); } else { localStorage.removeItem(LS_KEY + '_running'); } } catch (e) {} } function log(msg, type) { var t = new Date().toLocaleTimeString(); var c = { success:'#52c41a', error:'#f5222d', warn:'#faad14', info:'#1890ff', batch:'#722ed1', auto:'#eb2f96' }[type] || '#333'; console.log('%c[' + t + '] ' + msg, 'color:' + c); } // ============ 核心工具 ============ function getResourceType() { try { if (typeof dataHeart !== 'undefined') { if (dataHeart.type == 21) return '文档'; if (dataHeart.type == 2 || dataHeart.type == 28) return '视频'; if (dataHeart.type == 1) return '资源'; } } catch (e) {} var v = getVideoElement(); if (v) return '视频'; return '资源'; } function getCurrentActivityId() { try { if (typeof dataHeart !== 'undefined' && dataHeart.activityId) return dataHeart.activityId; } catch (e) {} return ''; } function getVideoElement() { var videos = document.querySelectorAll('video'); for (var i = 0; i < videos.length; i++) { if (videos[i].offsetParent !== null || videos[i].duration > 0) return videos[i]; } if (videos.length > 0) return videos[0]; var iframes = document.querySelectorAll('iframe'); for (var j = 0; j < iframes.length; j++) { try { var d = iframes[j].contentDocument || iframes[j].contentWindow.document; if (d) { var v = d.querySelector('video'); if (v) return v; } } catch (e) {} } return null; } // ============ 心跳 ============ function fireOneHeartbeat() { // 🔒 停止后不再发任何心跳 if (!state.running) return false; try { if (typeof dataHeart !== 'undefined') { dataHeart.isResourcePage = true; if ((dataHeart.type == 2 || dataHeart.type == 28) && dataHeart.playStatus === false) { return false; } } if (typeof sendHeartBeatAjax === 'function') { sendHeartBeatAjax(); return true; } if (typeof $ !== 'undefined' && typeof heartbeatUrl !== 'undefined' && heartbeatUrl) { $.ajax({ type:'POST', url:heartbeatUrl, data:dataHeart, cache:false }); return true; } return false; } catch (e) { return false; } } function fireBatch(n, onDone) { var sent = 0, total = n, interval = CONFIG.batchInterval; state.heartbeatTarget = total; state.heartbeatSent = 0; state.batchStartTs = Date.now(); showProgressBar(); updateProgress(0, total); function sendNext() { // 🔒 停止后立刻中断整个批量流程 if (!state.running) { hideProgressBar(); log('🛑 批量心跳已被手动停止', 'warn'); return; } if (sent >= total) { state.heartbeatSent = total; state.heartbeatTarget = 0; hideProgressBar(); log('✅ 本课件心跳 ' + total + '/' + total + ' 完成', 'batch'); updatePanel(); if (typeof onDone === 'function') onDone(); return; } fireOneHeartbeat(); sent++; state.heartbeatCount++; state.heartbeatSent = sent; updateProgress(sent, total); if (sent % 20 === 0 || sent === total) { log('📊 心跳进度 ' + sent + '/' + total, 'info'); } updatePanel(); setTimeout(sendNext, interval); } sendNext(); } // ============ 进度条 ============ function showProgressBar() { var box = document.getElementById('progress-box'); if (box) box.style.display = 'block'; } function hideProgressBar() { var box = document.getElementById('progress-box'); if (box) box.style.display = 'none'; } function updateProgress(done, total) { var bar = document.getElementById('progress-bar'); var pct = document.getElementById('progress-pct'); var dEl = document.getElementById('progress-done'); var tEl = document.getElementById('progress-total'); var eta = document.getElementById('progress-eta'); if (bar) bar.style.width = Math.round(done/total*100) + '%'; if (pct) pct.textContent = Math.round(done/total*100) + '%'; if (dEl) dEl.textContent = done; if (tEl) tEl.textContent = total; if (eta) { var elapsed = (Date.now() - state.batchStartTs) / 1000; var avg = done > 0 ? elapsed / done : 0; var remain = Math.max(0, avg * (total - done)); eta.textContent = '≈ ' + Math.round(remain) + '秒剩余'; } } // ============ XHR 拦截(try/catch 保护)=========== function hookXHR() { if (state.xhrHooked) return; state.xhrHooked = true; try { var origOpen = XMLHttpRequest.prototype.open; var origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (m, u) { this._url = u; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function () { var self = this; this.addEventListener('load', function () { if (self._url && self._url.indexOf('heartbeat') > -1 && self._url.indexOf('learningBehavior') > -1) { try { var resp = JSON.parse(self.responseText); if (resp && resp.code === 'SUCCESS') { state.successCount++; var te = document.getElementById('stuResourceViewTime'); if (te) { var t = parseInt(te.dataset.time || te.getAttribute('data-time') || '0'); t += 30; te.dataset.time = t; if (typeof formatLength === 'function') te.innerText = formatLength(t); } } else { state.failCount++; } } catch (e) { state.successCount++; } updatePanel(); } }); this.addEventListener('error', function () { if (self._url && self._url.indexOf('heartbeat') > -1) { state.failCount++; updatePanel(); } }); return origSend.apply(this, arguments); }; log('🔌 XHR 拦截器已激活', 'info'); } catch (e) { log('🔌 XHR 拦截器跳过(Chrome 限制),不影响批量发心跳', 'warn'); } } // ============ 保活 ============ function keepAlive() { try { if (typeof inactiveSeconds !== 'undefined') inactiveSeconds = 0; if (typeof mouseIsOn !== 'undefined') mouseIsOn = true; if (typeof isHeartbeatStop !== 'undefined') isHeartbeatStop = false; if (typeof dataHeart !== 'undefined') dataHeart.isResourcePage = true; } catch (e) {} try { var evt = new MouseEvent('mousemove', { clientX: Math.random()*1000, clientY: Math.random()*500, bubbles: true }); document.dispatchEvent(evt); } catch (e) {} } // ============ 视频:自动倍速 + 自动播放 + MutationObserver 持久化 ============ function applyVideoToElement(v) { if (!v || v._helperApplied) return; v._helperApplied = true; v._endedBound = false; v.playbackRate = CONFIG.videoSpeed; try { v.muted = true; } catch (e) {} if (v.paused) { v.play().catch(function(){}); } log('🎬 自动应用倍速 ' + CONFIG.videoSpeed + 'x + 播放', 'success'); // 持续维持速度(每秒检查一次) var speedHold = setInterval(function () { if (!document.body.contains(v)) { clearInterval(speedHold); return; } if (Math.abs(v.playbackRate - CONFIG.videoSpeed) > 0.01) { v.playbackRate = CONFIG.videoSpeed; try { v.muted = true; } catch (e) {} } if (v.paused && state.running) { v.play().catch(function(){}); } }, 2000); // ended 事件:只绑一次 v.addEventListener('ended', function onEnded() { if (!state.running) return; if (v._endedBound) return; v._endedBound = true; log('📺 视频播放完毕,下一节', 'info'); state.waitingForVideo = false; setTimeout(autoNextResource, CONFIG.autoNextDelay); }); // 卡住检测 var lastTime = v.currentTime, stallCount = 0; var stallTimer = setInterval(function () { if (!document.body.contains(v)) { clearInterval(stallTimer); return; } if (v.paused || v.ended) return; if (Math.abs(v.currentTime - lastTime) < 0.5) { stallCount++; if (stallCount > 6) { log('⚠️ 视频长时间没进度,尝试跳过', 'warn'); try { v.currentTime = Math.max(0, v.duration - 2); } catch (e) {} stallCount = 0; } } else { stallCount = 0; } lastTime = v.currentTime; }, 2000); } function startVideoObserver() { if (state.moObserver) { state.moObserver.disconnect(); state.moObserver = null; } state.moObserver = new MutationObserver(function (mutations) { document.querySelectorAll('video').forEach(applyVideoToElement); document.querySelectorAll('iframe').forEach(function (iframe) { try { var d = iframe.contentDocument || iframe.contentWindow.document; if (d) d.querySelectorAll('video').forEach(applyVideoToElement); } catch (e) {} }); }); state.moObserver.observe(document.body, { childList: true, subtree: true }); document.querySelectorAll('video').forEach(applyVideoToElement); log('👀 MutationObserver 已启动,所有视频自动应用倍速 ' + CONFIG.videoSpeed + 'x', 'info'); } function stopVideoObserver() { if (state.moObserver) { state.moObserver.disconnect(); state.moObserver = null; } } function setupVideo() { var v = getVideoElement(); if (!v) { log('❌ 没找到视频元素', 'error'); return false; } applyVideoToElement(v); if (v.duration > 0 && (v.duration - v.currentTime) < 5) { log('⚠️ 视频接近结尾,直接下一节', 'warn'); setTimeout(autoNextResource, 1000); } return true; } function waitForVideoEnd() { if (!state.running) { state.waitingForVideo = false; return; } var v = getVideoElement(); if (!v) { state.waitingForVideo = false; return; } if (!v.paused && v.duration > 0 && v.currentTime < v.duration - 1) { setTimeout(waitForVideoEnd, 3000); return; } if (v.duration > 0 && v.currentTime >= v.duration - 1) { log('📺 视频播完,下一节', 'info'); state.waitingForVideo = false; setTimeout(autoNextResource, CONFIG.autoNextDelay); return; } if (v.paused) { v.play().catch(function(){}); setTimeout(waitForVideoEnd, 2000); return; } setTimeout(waitForVideoEnd, 3000); } // ============ 找下一节 ============ function autoNextResource() { log('🔍 正在查找下一节...', 'auto'); keepAlive(); var selectors = [ '.chapter-item', '.section-item', '.course-item', '.resource-item', '.catalog-item', '.menu-item', '.node-item', '.learn-item', 'li[data-id]', 'li[data-fid]', 'li.activity-item', 'li.node', '.node', '.tree-node', '.slick-current + .slick-slide' ]; var foundActive = false; for (var s = 0; s < selectors.length; s++) { var items = document.querySelectorAll(selectors[s]); if (items.length < 2) continue; for (var j = 0; j < items.length; j++) { var cls = (items[j].className || '') + ' ' + (items[j].getAttribute('class') || ''); var style = items[j].getAttribute('style') || ''; var isCurrent = cls.indexOf('active') > -1 || cls.indexOf('current') > -1 || cls.indexOf('playing') > -1 || cls.indexOf('selected') > -1 || style.indexOf('rgb(24, 144, 255)') > -1 || style.indexOf('color:#1890ff') > -1 || style.indexOf('color: rgb(24, 144, 255)') > -1; if (isCurrent) { foundActive = true; continue; } if (foundActive && items[j].offsetParent !== null && items[j].clientHeight > 0) { log('🎯 点击下一节: ' + (items[j].innerText || '').trim().substring(0, 40), 'auto'); items[j].click(); scheduleHandle(); return true; } } } var btns = document.querySelectorAll('a, button, span, div'); for (var k = 0; k < btns.length; k++) { var t = (btns[k].innerText || '').trim(); if (t === '下一页' || t === '下一节' || t === '下一讲' || t === '下一个' || t === '下一项' || t === '▶' || t === '›') { log('🎯 点击按钮: ' + t, 'auto'); btns[k].click(); scheduleHandle(); return true; } } log('❌ 没找到下一节', 'error'); return false; } function scheduleHandle() { state.processing = false; if (state.speedTimer) { clearInterval(state.speedTimer); state.speedTimer = null; } keepAlive(); setTimeout(function () { var newAct = getCurrentActivityId(); log('🔄 页面加载中... activityId: ' + (newAct ? newAct.substring(0,12)+'...' : '未获取'), 'info'); handleCurrentResource(); }, 3000); } // ============ 主流程 ============ function handleCurrentResource() { if (state.processing) return; state.processing = true; try { if (typeof dataHeart !== 'undefined') { dataHeart.isResourcePage = true; if (typeof isHeartbeatStop !== 'undefined') isHeartbeatStop = false; } } catch (e) {} var type = getResourceType(); state.currentResourceType = type; var actId = getCurrentActivityId(); state.currentActivityId = actId; log('═══════════════════════════════════', 'auto'); log('📚 开始处理: [' + type + '] activityId=' + (actId ? actId.substring(0,12) + '...' : '(页面未加载)'), 'auto'); if (!actId && type === '资源') { log('⏳ 等待页面加载(2秒后重试)...', 'info'); setTimeout(function () { state.processing = false; handleCurrentResource(); }, 2000); return; } if (type === '视频') { log('🎬 视频课件,启动自动播放...', 'auto'); var ok = setupVideo(); if (ok) { state.processing = false; if (!state.waitingForVideo) { state.waitingForVideo = true; waitForVideoEnd(); } } else { log('⚠️ 视频启动失败,10秒后跳到下一节', 'warn'); state.processing = false; setTimeout(autoNextResource, 10000); } } else { log('📄 ' + type + ' 课件,批量发 ' + CONFIG.docHeartbeats + ' 个心跳', 'batch'); fireBatch(CONFIG.docHeartbeats, function () { log('✅ 本节加时完成 → 下一节', 'auto'); state.processing = false; setTimeout(autoNextResource, CONFIG.autoNextDelay); }); } updatePanel(); } // ============ 控制面板 ============ function createPanel() { var old = document.getElementById('study-helper-panel'); if (old) old.remove(); var p = document.createElement('div'); p.id = 'study-helper-panel'; p.innerHTML = '