// ==UserScript== // @name 国家智慧教育平台-学习助手(优化版) // @namespace http://tampermonkey.net/ // @version 5.2 // @description 真实结构解析+状态图标;启动强制跳到第一个未完成;轮询切课、跳过已学、进度条、倍速伪装、弹窗自动关、后台保活、可拖动精简面板 // @author 寻道书生 // @match *://basic.smartedu.cn/* // @match *://www.smartedu.cn/* // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; let config = { speed: 2.0, autoScroll: true, autoNext: true, autoMute: true, autoPlay: true, keepAlive: true, closeModal: true, showStat: true, skipDone: true, resume: true, autoStart: true }; let scrollRan = false; let bindEnded = false; let currentVideo = null; let skipCount = 0; let curEntry = null; let globalIdx = -1; let lastClickEntry = null; let lastClickTime = 0; let lastSwitchTime = 0; let lastUserClickTime = 0; let observer = null; // ============ 精简面板 ============ function createPanel(){ const panel = document.createElement('div'); panel.id = "studyPanel"; panel.style=` position:fixed;top:80px;right:20px;z-index:9999; background:#fff;border:1px solid #e8e8e8;border-radius:10px; padding:0;width:250px;font-size:12px;box-shadow:0 4px 16px #0001a; font-family:system-ui,sans-serif;overflow:hidden; `; panel.innerHTML=`
🎓 学习助手按住拖动
倍速
📊 进度 统计中...
▶ 正在: -
⏭ 下一节: -
自动从第一个未完成开始
播放完自动下一节
自动展开全部目录
后台保活(切标签不暂停)
`; document.body.appendChild(panel); // 拖拽 (function(){ const header = document.getElementById('panelHeader'); if(!header) return; try{ const pos = JSON.parse(localStorage.getItem('smartedu_panel_pos')||'null'); if(pos){ panel.style.left = pos.left+'px'; panel.style.top = pos.top+'px'; panel.style.right = 'auto'; } }catch(e){} let dragging = false, ox = 0, oy = 0; header.addEventListener('mousedown', (e)=>{ dragging = true; ox = e.clientX - panel.offsetLeft; oy = e.clientY - panel.offsetTop; panel.style.right = 'auto'; e.preventDefault(); }); document.addEventListener('mousemove', (e)=>{ if(!dragging) return; let x = e.clientX - ox, y = e.clientY - oy; x = Math.max(0, Math.min(x, window.innerWidth - panel.offsetWidth)); y = Math.max(0, Math.min(y, window.innerHeight - panel.offsetHeight)); panel.style.left = x+'px'; panel.style.top = y+'px'; }); document.addEventListener('mouseup', ()=>{ if(!dragging) return; dragging = false; try{ localStorage.setItem('smartedu_panel_pos', JSON.stringify({left: panel.offsetLeft, top: panel.offsetTop})); }catch(e){} }); })(); document.getElementById('setSpeedBtn').onclick = ()=>{ const val = parseFloat(document.getElementById('speedInput').value); if(val>=0.5 && val<=10){ config.speed = val; applySpeed(currentVideo); document.getElementById('feedbackLine').textContent = '✅ 倍速已设为 '+val+'x(平台显示1x)'; }else{ document.getElementById('feedbackLine').textContent = '⚠️ 倍速需在 0.5~10 之间'; } }; document.getElementById('cbAutoStart').onchange = e=>config.autoStart = e.target.checked; document.getElementById('cbNext').onchange = e=>config.autoNext = e.target.checked; document.getElementById('cbScroll').onchange = e=>config.autoScroll = e.target.checked; document.getElementById('cbKeep').onchange = e=>config.keepAlive = e.target.checked; document.getElementById('locateBtn').onclick = ()=>{ const cur = findCurrentEntry(true); if(cur){ document.getElementById('feedbackLine').textContent = '🎯 已定位第'+(globalIdx+1)+'节: '+(cur.textContent||'').replace(/\s+/g,' ').trim().slice(0,26); } else { document.getElementById('feedbackLine').textContent = '⚠️ 未定位到当前课,请先点开一节课'; } refreshNowLine(); }; document.getElementById('diagBtn').onclick = ()=>{ const lines = diagCourses(); const ta = document.getElementById('diagText'); ta.value = lines; document.getElementById('diagArea').style.display = ''; document.getElementById('copyBtn').style.display = ''; document.getElementById('feedbackLine').textContent = '已输出 '+lines.split('\n').length+' 行,复制后发我'; }; document.getElementById('copyBtn').onclick = ()=>{ const ta = document.getElementById('diagText'); ta.select(); ta.setSelectionRange(0, 99999); try{ document.execCommand('copy'); document.getElementById('feedbackLine').textContent = '✅ 已复制,粘贴发给助手即可'; }catch(e){ document.getElementById('feedbackLine').textContent = '复制失败,请手动全选复制'; } }; document.getElementById('clearBtn').onclick = ()=>{ try{ localStorage.removeItem('smartedu_progress'); localStorage.removeItem('smartedu_played'); }catch(e){} globalIdx = -1; curEntry = null; skipCount = 0; document.getElementById('feedbackLine').textContent = '🧹 已清除断点和已播记录'; updateStat(); } } // ============ 目录解析(真实结构) ============ function getAllEntries(){ const list = []; document.querySelectorAll('.resource-item.resource-item-train').forEach(el=>{ const t = (el.textContent||'').trim(); if(t.length < 2) return; if(el.querySelector('video,audio')) return; list.push(el); }); return list; } function getModulePath(el){ const parts = []; let p = el.parentElement; while(p && p !== document.body){ if(p.classList && p.classList.contains('fish-collapse-item')){ const h = p.querySelector(':scope > .fish-collapse-header'); if(h && (h.textContent||'').trim()){ parts.unshift((h.textContent||'').trim().slice(0,18)); } } p = p.parentElement; } return parts.join('>'); } function getItemState(el){ if(!el) return 'todo'; if(el.querySelector('.icon_checkbox_fill')) return 'done'; if(el.querySelector('.icon_processing_fill')) return 'processing'; return 'todo'; } function isDone(el){ if(getItemState(el) === 'done') return true; return getPlayedSet().has((el.textContent||'').trim()); } function isUnfinished(el){ return el && !isDone(el); } function ensureEntryVisible(entry){ const headers = []; let p = entry.parentElement; while(p){ if(p.classList && p.classList.contains('fish-collapse-item')){ const h = p.querySelector(':scope > .fish-collapse-header'); if(h && !/active/.test(h.className)) headers.unshift(h); } p = p.parentElement; if(p && p.classList && p.classList.contains('fish-collapse')) break; } headers.forEach(h=>h.click()); } // ============ 定位 ============ function findCurrentEntry(forceCalib){ const all = getAllEntries(); const active = document.querySelector('.resource-item-active'); if(active){ const i = all.indexOf(active); if(i >= 0){ globalIdx = i; curEntry = active; return active; } } if(globalIdx >= 0 && all[globalIdx] && document.body.contains(all[globalIdx])){ curEntry = all[globalIdx]; return curEntry; } if(forceCalib && all.length) return null; return null; } function findFirstUnfinished(fromIdx){ const all = getAllEntries(); const start = fromIdx >= 0 ? fromIdx : 0; for(let i=start; i= 0) globalIdx = idx; curEntry = entry; verifyPlayback(); } function verifyPlayback(){ setTimeout(()=>{ const v = document.querySelector('video'); if(v && !v.paused && v.currentTime > 0) return; if(v && v.paused && v.readyState >= 2 && config.autoPlay){ v.play().catch(()=>{}); } if(!v){ const cur = findCurrentEntry(); if(cur){ doClick(cur); } } }, 6000); } function installClickTracker(){ document.addEventListener('click', (e)=>{ const item = e.target.closest('.resource-item'); if(item){ lastUserClickTime = Date.now(); const all = getAllEntries(); const i = all.indexOf(item); if(i>=0){ globalIdx = i; curEntry = item; } } }, true); } // ============ 进度保存/统计 ============ function saveProgress(){ if(globalIdx>=0){ try{ localStorage.setItem('smartedu_progress', JSON.stringify({idx:globalIdx})); }catch(e){} } } function loadProgress(){ try{ const d = JSON.parse(localStorage.getItem('smartedu_progress')||'null'); if(d && d.idx>=0) return d.idx; }catch(e){} return -1; } function getPlayedSet(){ try{ return new Set(JSON.parse(localStorage.getItem('smartedu_played')||'[]')); }catch(e){ return new Set(); } } function markPlayed(entry){ if(!entry) return; try{ const s = getPlayedSet(); s.add((entry.textContent||'').trim()); localStorage.setItem('smartedu_played', JSON.stringify(Array.from(s))); }catch(e){} } function collectCourses(){ const all = getAllEntries(); let done = 0, todo = 0; all.forEach(el=>{ if(isDone(el)) done++; else todo++; }); return {done, todo, total: all.length}; } function refreshNowLine(){ const nowLine = document.getElementById('nowLine'); const nextLine = document.getElementById('nextLine'); if(!nowLine) return; const cur = findCurrentEntry(); if(cur){ nowLine.textContent = '▶ 正在: '+(cur.textContent||'').replace(/\s+/g,' ').trim().slice(0,22); const n = findNextLesson(); nextLine.textContent = n.next ? '⏭ 下一节: '+(n.next.textContent||'').replace(/\s+/g,' ').trim().slice(0,22) : '⏭ 下一节: '+n.reason; }else{ nowLine.textContent = '▶ 正在: 未定位(先手动点开一课)'; nextLine.textContent = '⏭ 下一节: -'; } } function updateStat(feedback){ if(!config.showStat) return; const statLine = document.getElementById('statLine'); if(!statLine) return; const s = collectCourses(); const pct = s.total ? Math.round(s.done/s.total*100) : 0; document.getElementById('progBar').style.width = pct+'%'; statLine.textContent = `已学${s.done} · 未学${s.todo} · 总${s.total} (${pct}%)`; refreshNowLine(); const fbLine = document.getElementById('feedbackLine'); if(feedback && fbLine){ fbLine.innerHTML = feedback; fbLine.style.animation = 'none'; fbLine.offsetHeight; fbLine.style.animation = 'fbFlash 1s'; if(!document.getElementById('fbFlashStyle')){ const st = document.createElement('style'); st.id = 'fbFlashStyle'; st.textContent = '@keyframes fbFlash{0%{background:#fff3cd;}100%{background:transparent;}}'; document.head.appendChild(st); } } } function diagCourses(){ const all = getAllEntries(); const lines = []; lines.push('===== A. 全局顺序条目链(真实DOM顺序) ====='); let lastPath = ''; all.forEach((el,i)=>{ const path = getModulePath(el); const t = (el.textContent||'').replace(/\s+/g,' ').trim().slice(0,42); const st = getItemState(el); const mark = st==='done' ? '✓' : (st==='processing' ? '🔄' : '○'); if(path && path !== lastPath){ lines.push(`—— [${path}]`); lastPath = path; } lines.push(`${i+1}.${mark} ${t}`); }); lines.push(`共 ${all.length} 节`); lines.push(''); lines.push('===== B. 定位信息 ====='); const cur = findCurrentEntry(); lines.push('当前: ' + (cur ? (cur.textContent||'').replace(/\s+/g,' ').trim().slice(0,40) : '未定位')); lines.push('记忆索引: ' + (globalIdx>=0 ? '第'+(globalIdx+1)+'节' : '无')); const first = findFirstUnfinished(0); lines.push('第一个未完成(从最上面): ' + (first>=0 ? '第'+(first+1)+'节 '+(all[first].textContent||'').replace(/\s+/g,' ').trim().slice(0,30) : '无(已全部刷完)')); return lines.slice(0,400).join('\n'); } // ============ 视频倍速伪装 ============ function getSpeedDesc(obj){ while(obj && obj !== Object.prototype){ const d = Object.getOwnPropertyDescriptor(obj, 'playbackRate'); if(d) return d; obj = Object.getPrototypeOf(obj); } return null; } function hijackSpeed(video){ if(!video || video.__hijacked) return; video.__hijacked = true; const desc = getSpeedDesc(video); if(desc && desc.set){ Object.defineProperty(video, 'playbackRate', { configurable: true, get(){ return 1; }, set(){ desc.set.call(this, config.speed); } }); desc.set.call(video, config.speed); } if(config.autoMute) video.muted = true; } function applySpeed(video){ if(!video) return; const desc = getSpeedDesc(video); if(desc && desc.set) desc.set.call(video, config.speed); } function tryAutoPlay(){ if(!config.autoPlay) return; const video = currentVideo || document.querySelector('video'); if(!video) return; if(video.paused && !video.ended && video.readyState >= 2){ video.play().catch(()=>{}); } } function keepPageAlive() { if(!config.keepAlive) return; Object.defineProperty(document, 'visibilityState', { value: 'visible', writable: false }); Object.defineProperty(document, 'hidden', { value: false, writable: false }); document.dispatchEvent(new Event('visibilitychange')); if(currentVideo && currentVideo.paused) tryAutoPlay(); } function expandAllChapters(){ document.querySelectorAll('.fish-collapse-item').forEach(item=>{ if(item.classList.contains('fish-collapse-item-active')) return; const h = item.querySelector(':scope > .fish-collapse-header'); if(h && !h.__expanded){ h.__expanded = true; h.click(); } }); document.querySelectorAll('[class*="expand"][aria-expanded="false"],[class*="unfold"],[class*="more"][class*="btn"]').forEach(el=>{ if(!el.__expanded){ el.__expanded = true; el.click(); } }); } function scrollCatalogContainer(){ document.querySelectorAll('[class*="catalog"],[class*="menu"],[class*="sidebar"],[class*="tree"],[class*="list"],[class*="directory"],[class*="wrap"]').forEach(c=>{ if(c.scrollHeight > c.clientHeight + 100){ c.scrollTop = c.scrollHeight; } }); window.scrollTo(0, document.body.scrollHeight); window.dispatchEvent(new WheelEvent('wheel', {deltaY: 1, deltaMode: 0})); } function autoScrollPage() { if (!config.autoScroll || scrollRan) return; scrollRan = true; expandAllChapters(); let lastCount = -1; let stableCount = 0; let timer = setInterval(()=>{ expandAllChapters(); scrollCatalogContainer(); const count = getAllEntries().length; if(count === lastCount){ stableCount++; } else { stableCount = 0; lastCount = count; } if(stableCount >= 6){ clearInterval(timer); window.scrollTo({top: 0, behavior: 'auto'}); updateStat(); } }, 800); setTimeout(()=>{ clearInterval(timer); window.scrollTo({top: 0, behavior: 'auto'}); updateStat(); }, 40000); } function closeDetectModal(){ if(!config.closeModal) return; const keywords = /倍速|暂停学习|学习已暂停|关闭倍速播放|检测到|学时|须学习完|学习完课程|完整观看|未学习完|看完视频/i; const containers = [ '.el-message-box','.el-dialog','.el-notification','.ant-modal', '.modal','.popup','.toast', '[class*="fish-message"]','[class*="fish-dialog"]','[class*="fish-modal"]', '[class*="message-box"]','[class*="dialog"]' ]; document.querySelectorAll(containers.join(',')).forEach(el=>{ if(!el.innerText || !keywords.test(el.innerText)) return; const btns = el.querySelectorAll('button'); let hit = null; btns.forEach(b=>{ if(/知道|确定|关闭|OK|好的|取消/.test((b.textContent||'').trim())) hit = b; }); if(hit){ hit.click(); return; } const closeBtn = el.querySelector('.el-dialog__headerbtn,.el-message-box__close,.close,[aria-label="关闭"],[class*="headerbtn"],[class*="close-btn"],[class*="modal-close"]'); if(closeBtn){ closeBtn.click(); return; } el.style.display='none'; el.remove(); }); } // ============ 自动切下一节 ============ function autoNextVideo() { if (!config.autoNext) return; const video = currentVideo; if(!video) return; if(bindEnded) return; bindEnded = true; video.addEventListener('ended', videoEnd); } function watchForEnd(){ const v = currentVideo; if(!v || !v.duration || v.duration === Infinity) return; if(v.ended || (v.currentTime >= v.duration - 1)){ if(!v.__endFired){ v.__endFired = true; setTimeout(videoEnd, 300); } } } function videoEnd(){ if(Date.now() - lastSwitchTime < 3000) return; lastSwitchTime = Date.now(); if(!config.autoNext) return; markPlayed(findCurrentEntry()); saveProgress(); const r = findNextLesson(); if(r.next){ clickEntry(r.next); globalIdx = r.idx; scrollRan = false; bindEnded = false; currentVideo = null; const s = collectCourses(); updateStat(`✅ 刷完一节!已学 ${s.done}/${s.total}(${Math.round(s.done/s.total*100)}%),继续~`); setTimeout(()=>{ ensureVideo(); autoScrollPage(); tryAutoPlay(); updateStat(); },1800); } else { updateStat('🏁 ' + r.reason); } } // ============ 跳过已学完 ============ function skipDoneLesson(){ if(!config.skipDone) return; const cur = findCurrentEntry(); if(!cur) return; if(isUnfinished(cur)) return; const total = collectCourses().total || 50; if(skipCount >= total + 5){ updateStat('🏁 检测到已全部刷完,停止跳转'); return; } skipCount++; const r = findNextLesson(); if(r.next){ markPlayed(cur); saveProgress(); clickEntry(r.next); globalIdx = r.idx; scrollRan = false; bindEnded = false; currentVideo = null; updateStat('⏭️ 已学完,跳到下一个未完成: 第'+(r.idx+1)+'节'); setTimeout(()=>{ ensureVideo(); autoScrollPage(); tryAutoPlay(); updateStat(); },1500); } else { updateStat('🏁 ' + r.reason); } } function ensureVideo(){ const video = document.querySelector('video'); if(!video) return; if(video !== currentVideo){ currentVideo = video; video.__endFired = false; hijackSpeed(video); autoNextVideo(); setTimeout(skipDoneLesson, 3000); } } // ============ 启动:强制从第一个未完成开始 ============ function startSmart(){ if(!config.autoStart) return; let attempts = 0; function tryStart(){ attempts++; const all = getAllEntries(); if(!all.length || all.length < 30){ if(attempts < 25){ setTimeout(tryStart, 1000); return; } } const firstIdx = findFirstUnfinished(0); if(firstIdx < 0){ updateStat('🏁 全部课程已刷完!'); return; } if(Date.now() - lastUserClickTime < 30000) return; const cur = findCurrentEntry(); const curIdx = cur ? all.indexOf(cur) : -1; if(curIdx === firstIdx){ updateStat('▶ 已在第一个未完成: 第'+(firstIdx+1)+'节'); return; } clickEntry(all[firstIdx]); globalIdx = firstIdx; ensureEntryVisible(all[firstIdx]); autoScrollPage(); tryAutoPlay(); updateStat('🎯 已从最上面定位到第一个未完成: 第'+(firstIdx+1)+'节 '+(all[firstIdx].textContent||'').replace(/\s+/g,' ').trim().slice(0,24)); } setTimeout(tryStart, 4000); } function watchCatalog(){ if(observer) return; const target = document.querySelector('.fish-collapse') || document.body; observer = new MutationObserver(()=>{ clearTimeout(observer.__t); observer.__t = setTimeout(()=>{ expandAllChapters(); updateStat(); refreshNowLine(); }, 600); }); observer.observe(target, {childList:true, subtree:true}); } function init(){ if(!document.getElementById("studyPanel")) createPanel(); installClickTracker(); ensureVideo(); autoScrollPage(); tryAutoPlay(); closeDetectModal(); updateStat(); watchCatalog(); startSmart(); } window.addEventListener('load', init); setInterval(()=>{ ensureVideo(); tryAutoPlay(); watchForEnd(); keepPageAlive(); closeDetectModal(); refreshNowLine(); updateStat(); }, 1000); })();