// ==UserScript== // @name StudyFlow 学习进度助手 // @namespace local.studyflow // @version 2.1.18 // @description 独立的 HTML5 视频控制、续播、记录、笔记与自动刷课工具(不含答题功能) // @author YOUR_NAME // @license MIT // @match *://*.chaoxing.com/* // @match *://*.neauce.com/* // @match *://*.zhihuishu.com/* // @match *://*.edu.cn/* // @require https://cdn.jsdelivr.net/npm/lucide@0.468.0/dist/umd/lucide.min.js // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @connect gg.ffwks.top // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const APP_ID = 'studyflow-root'; const CHANNEL = 'studyflow:v1'; const SETTINGS_KEY = 'studyflow:settings'; const HISTORY_KEY = 'studyflow:history'; const NOTE_PREFIX = 'studyflow:note:'; const POSITION_PREFIX = 'studyflow:position:'; const isTopFrame = window.top === window.self; const frameId = createId(); const attachedVideos = new WeakSet(); const restoredVideos = new WeakSet(); // 广告地址(混淆存储:base64 + 反转,避免源码直接暴露服务器地址;仅作者可解) function decodeAdUrl(encoded) { try { const b64 = String(encoded || '').split('').reverse().join(''); if (typeof atob === 'function') return atob(b64); if (typeof Buffer === 'function') return Buffer.from(b64, 'base64').toString('utf8'); return ''; } catch (e) { return ''; } } const AD_URL_ENCODED = 'u92cq5CZh9CcvRnLzt2dmZmLnd2LvoDc0RHa'; const DEFAULT_SETTINGS = Object.freeze({ playbackRate: 1.25, muted: false, rememberPosition: true, autoNextChapter: true, pauseWhenHidden: false, panelOpen: true, // 后台挂机:切出页面/最小化后继续刷课(平台暂停的视频会自动恢复) backgroundMode: true, // 自动刷课(不含答题) autoStudy: true, autoMute: true, autoResume: true, skipVideoQuiz: true, faceWait: false, // 广告(远程 JSON 配置,可远程更改广告内容) adEnabled: true, adUrl: decodeAdUrl(AD_URL_ENCODED) }); let settings = Object.assign({}, DEFAULT_SETTINGS, readValue(SETTINGS_KEY, {})); let activeVideo = null; let lastPositionSave = 0; // 自动刷课共享状态(每帧一个实例;跨帧用户暂停标记挂在 video 元素本身上) const autoState = { running: false, ownsNavigation: false, drivenVideos: new Set(), lockedRates: new WeakMap(), timers: [] }; function createId() { if (window.crypto && typeof window.crypto.randomUUID === 'function') { return window.crypto.randomUUID(); } return `frame-${Date.now()}-${Math.random().toString(36).slice(2)}`; } function readValue(key, fallback) { try { return typeof GM_getValue === 'function' ? GM_getValue(key, fallback) : fallback; } catch (_error) { return fallback; } } function writeValue(key, value) { try { if (typeof GM_setValue === 'function') GM_setValue(key, value); } catch (_error) { // Storage failures must not break video controls. } } function stableHash(input) { let hash = 2166136261; for (let index = 0; index < input.length; index += 1) { hash ^= input.charCodeAt(index); hash = Math.imul(hash, 16777619); } return (hash >>> 0).toString(36); } function pageKey() { return stableHash(`${location.origin}${location.pathname}${document.title}`); } function videoKey(video) { const source = (video.currentSrc || video.src || video.poster || '').split('?')[0]; return stableHash(`${location.origin}${location.pathname}|${source}|${Math.round(video.duration || 0)}`); } function formatTime(seconds) { const value = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0; const hours = Math.floor(value / 3600); const minutes = Math.floor((value % 3600) / 60); const rest = value % 60; const core = `${String(minutes).padStart(2, '0')}:${String(rest).padStart(2, '0')}`; return hours ? `${hours}:${core}` : core; } function getVideoScore(video) { const rect = video.getBoundingClientRect(); const visibleArea = Math.max(0, rect.width) * Math.max(0, rect.height); return visibleArea + (!video.paused && !video.ended ? 10000000 : 0); } function selectVideo() { const candidates = Array.from(document.querySelectorAll('video')); if (!candidates.length) return null; candidates.sort((left, right) => getVideoScore(right) - getVideoScore(left)); return candidates[0]; } function attachVideo(video) { if (attachedVideos.has(video)) return; attachedVideos.add(video); const events = ['play', 'pause', 'ended', 'loadedmetadata', 'ratechange', 'volumechange', 'durationchange']; events.forEach((eventName) => video.addEventListener(eventName, () => { activeVideo = selectVideo() || video; if (eventName === 'loadedmetadata') restorePosition(video); if (eventName === 'ended') { clearPosition(video); video.__sfUserPaused = false; } reportState(eventName); }, { passive: true })); video.addEventListener('timeupdate', () => { activeVideo = selectVideo() || video; savePosition(video); reportState('timeupdate'); }, { passive: true }); applyVideoPreferences(video); if (video.readyState >= 1) restorePosition(video); } function scanVideos() { document.querySelectorAll('video').forEach(attachVideo); activeVideo = selectVideo(); reportState('scan'); } function applyVideoPreferences(video) { if (!video) return; unlockAutoRate(video); video.playbackRate = Number(settings.playbackRate) || 1; if (!autoState.drivenVideos.has(video)) video.muted = Boolean(settings.muted); } function restorePosition(video) { if (!settings.rememberPosition || restoredVideos.has(video) || !Number.isFinite(video.duration)) return; restoredVideos.add(video); const saved = Number(readValue(`${POSITION_PREFIX}${videoKey(video)}`, 0)); if (saved > 5 && saved < video.duration - 5) video.currentTime = saved; } function savePosition(video) { if (!settings.rememberPosition || !Number.isFinite(video.currentTime)) return; const now = Date.now(); if (now - lastPositionSave < 5000) return; lastPositionSave = now; if (video.currentTime > 3 && (!video.duration || video.currentTime < video.duration - 3)) { writeValue(`${POSITION_PREFIX}${videoKey(video)}`, Math.floor(video.currentTime)); } } function clearPosition(video) { writeValue(`${POSITION_PREFIX}${videoKey(video)}`, 0); addHistory(video); } function addHistory(video) { const title = String(document.title || '课程视频').trim().slice(0, 80); const history = readValue(HISTORY_KEY, []); const next = [{ title, url: location.href, completedAt: Date.now(), duration: Math.round(video.duration || 0) }].concat(Array.isArray(history) ? history.filter((item) => item.url !== location.href) : []).slice(0, 8); writeValue(HISTORY_KEY, next); } function currentState() { const video = selectVideo(); activeVideo = video; return { frameId, hasVideo: Boolean(video), title: String(document.title || location.hostname).trim().slice(0, 100), playing: Boolean(video && !video.paused && !video.ended), ended: Boolean(video && video.ended), currentTime: video ? Number(video.currentTime || 0) : 0, duration: video ? Number(video.duration || 0) : 0, playbackRate: video ? Number(video.playbackRate || 1) : Number(settings.playbackRate || 1), muted: video ? Boolean(video.muted) : Boolean(settings.muted), updatedAt: Date.now() }; } function reportState(reason) { const message = { channel: CHANNEL, type: 'state', reason, state: currentState() }; if (isTopFrame) { window.dispatchEvent(new CustomEvent(`${CHANNEL}:local-state`, { detail: message })); } else { window.top.postMessage(message, '*'); } } function isVisibleControl(element) { if (!(element instanceof HTMLElement) || element.closest(`#${APP_ID}`)) return false; if (element.matches(':disabled, [aria-disabled="true"]')) return false; const rect = element.getBoundingClientRect(); const style = getComputedStyle(element); return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden'; } function findPlatformNextControl(doc) { const directSelectors = [ '#prevNextFocusNext', '.jb_btn.jb_btn_92.fr.fs14.nextChapter', '.nextChapter', '[data-action="next-chapter"]', '[data-role="next-chapter"]' ]; for (const selector of directSelectors) { const candidate = doc.querySelector(selector); if (candidate && isVisibleControl(candidate)) return candidate; } const labelPattern = /^(下一章|下一节|下一课|下一课程|下一单元|下一个视频|下一个资源|next chapter|next lesson|next section|next video)(?:[\s::>|›»→-].{0,48})?$/i; const labelMatches = Array.from(doc.querySelectorAll('a, button, [role="button"], [onclick], [tabindex]')) .filter((element) => { if (!isVisibleControl(element)) return false; const label = [element.getAttribute('aria-label'), element.getAttribute('title'), element.innerText, element.value] .find(Boolean); return labelPattern.test(String(label || '').replace(/\s+/g, ' ').trim()); }); const outermostMatches = labelMatches.filter((element) => !labelMatches.some((other) => other !== element && other.contains(element))); if (outermostMatches.length === 1) return outermostMatches[0]; const chapterLists = [ { items: 'li.video', current: 'li.video.current_play' }, { items: '.section-item-collapse-info', current: '.section-item-collapse-info.active' }, { items: '.resources-list .resources-item', current: '.resources-list .resources-item.active, .resources-list .resources-item.current' }, { items: '.clearfix.video', current: '.clearfix.video.activeNode, .clearfix.video.active' }, { items: '[class*="card-container"]', current: '[class*="card-container"].active' }, { items: '.child-main', current: '.child-main.current' }, { items: '.chapter-content-second', current: '.chapter-content-second.current' }, { items: '.chapter-content .chapter-item', current: '.chapter-content .chapter-item.current' }, { items: '.chapter-item', current: '.chapter-item.current' }, { items: '.file-item', current: '.file-item.active' }, { items: '.el-tree-node', current: '.el-tree-node.is-current' } ]; for (const list of chapterLists) { const current = doc.querySelector(list.current); if (!current) continue; const items = Array.from(doc.querySelectorAll(list.items)); const index = items.indexOf(current); if (index < 0) continue; const next = items.slice(index + 1).find(isVisibleControl); if (next) return next; } return null; } function updateSettings(patch) { settings = Object.assign({}, settings, patch); writeValue(SETTINGS_KEY, settings); document.querySelectorAll('video').forEach(applyVideoPreferences); if (Object.prototype.hasOwnProperty.call(patch, 'autoStudy') && isTopFrame) { if (settings.autoStudy) { startAutoStudy(); } else { stopAutoStudy(); } } if ((Object.prototype.hasOwnProperty.call(patch, 'adEnabled') || Object.prototype.hasOwnProperty.call(patch, 'adUrl')) && ui) { renderAdArea(); } } function runCommand(command, value) { if (command === 'advance-next') { const nextControl = findPlatformNextControl(document); if (nextControl) { nextControl.click(); reportState('next-clicked'); } else { reportState('next-not-found'); } return; } const video = selectVideo(); if (command === 'set-setting') { updateSettings(value || {}); reportState(command); return; } if (!video) { reportState('no-video'); return; } if (command === 'toggle-play') { if (video.paused || video.ended) { video.__sfUserPaused = false; video.play().catch(() => reportState('play-blocked')); } else { video.__sfUserPaused = true; video.pause(); } } else if (command === 'seek-by') { video.currentTime = Math.max(0, Math.min(video.duration || Infinity, video.currentTime + Number(value || 0))); } else if (command === 'seek-to') { video.currentTime = Math.max(0, Math.min(video.duration || Infinity, Number(value || 0))); } else if (command === 'set-rate') { updateSettings({ playbackRate: Number(value) || 1 }); if (video) { unlockAutoRate(video); setBestRate(video); // 强制锁定(智慧树等播放器软设置可能被重置,锁定后保证生效且不被播放器改回) lockAutoRate(video, Number(value) || 1); } } else if (command === 'set-muted') { updateSettings({ muted: Boolean(value) }); } reportState(command); } window.addEventListener('message', (event) => { const message = event.data; if (!message || message.channel !== CHANNEL || message.type !== 'command') return; if (message.target && message.target !== frameId) return; runCommand(message.command, message.value); }); document.addEventListener('visibilitychange', () => { if (settings.pauseWhenHidden && document.hidden) { const video = selectVideo(); if (video && !video.paused) video.pause(); } }); let scanTimer = 0; const observer = new MutationObserver((records) => { const hasMediaChange = records.some((record) => { const changedNodes = Array.from(record.addedNodes).concat(Array.from(record.removedNodes)); return changedNodes.some((node) => { if (!(node instanceof Element)) return false; return node.matches('video, iframe') || Boolean(node.querySelector('video, iframe')); }); }); if (!hasMediaChange) return; window.clearTimeout(scanTimer); scanTimer = window.setTimeout(scanVideos, 120); }); observer.observe(document.documentElement, { childList: true, subtree: true }); scanVideos(); window.setInterval(scanVideos, 3000); installRateMenuFollow(); installBackgroundKeepalive(); if (!isTopFrame) return; const frameStates = new Map(); let selectedFrameId = frameId; let ui = null; let noteSaveTimer = 0; const autoAdvanceFrames = new Set(); function acceptState(message, source) { frameStates.set(message.state.frameId, { state: message.state, source }); const current = frameStates.get(selectedFrameId); if (!current || !current.state.hasVideo || message.state.playing) selectedFrameId = message.state.frameId; if (message.reason === 'loadedmetadata' || message.reason === 'play') { autoAdvanceFrames.delete(message.state.frameId); } if (message.reason === 'ended') advanceAfterVideo(message.state.frameId); if (message.reason === 'next-clicked') showToast('已切换到下一章/下一节'); if (message.reason === 'next-not-found') showToast('视频已结束,当前页未找到可识别的下一章'); renderState(); } window.addEventListener(`${CHANNEL}:local-state`, (event) => acceptState(event.detail, window)); window.addEventListener('message', (event) => { const message = event.data; if (!message || message.channel !== CHANNEL || message.type !== 'state') return; acceptState(message, event.source); }); function selectedEntry() { const fresh = Array.from(frameStates.values()).filter((entry) => Date.now() - entry.state.updatedAt < 10000); const playing = fresh.find((entry) => entry.state.playing); if (playing) return playing; return frameStates.get(selectedFrameId) || fresh.find((entry) => entry.state.hasVideo) || fresh[0] || null; } function sendCommand(command, value) { const entry = selectedEntry(); if (!entry || !entry.state.hasVideo) { showToast('当前页面未发现视频'); return; } if (entry.source === window) { runCommand(command, value); } else { entry.source.postMessage({ channel: CHANNEL, type: 'command', target: entry.state.frameId, command, value }, '*'); } } function findNextChapterControl() { return findPlatformNextControl(document); } function advanceAfterVideo(sourceFrameId) { if (autoState.ownsNavigation) return; // 自动刷课驱动接管跳转 if (!settings.autoNextChapter || autoAdvanceFrames.has(sourceFrameId)) return; autoAdvanceFrames.add(sourceFrameId); window.setTimeout(() => { if (!settings.autoNextChapter) return; const nextControl = findNextChapterControl(); if (!nextControl) { const entry = frameStates.get(sourceFrameId); if (entry && entry.source !== window) { entry.source.postMessage({ channel: CHANNEL, type: 'command', target: sourceFrameId, command: 'advance-next' }, '*'); return; } showToast('视频已结束,页面未识别到唯一的下一章/下一节按钮'); return; } const label = String(nextControl.innerText || nextControl.getAttribute('aria-label') || '下一章').trim(); nextControl.click(); showToast(`正在切换:${label.slice(0, 28)}`); }, 500); } function injectStyle() { const style = document.createElement('style'); style.textContent = ` #${APP_ID}, #${APP_ID} * { box-sizing: border-box; letter-spacing: 0; } #${APP_ID} { position: fixed; inset: 0; z-index: 2147483646; pointer-events: none; font-family: Inter, "Microsoft YaHei", Arial, sans-serif; color: #1f2933; } #${APP_ID} button, #${APP_ID} input, #${APP_ID} textarea { font: inherit; } #${APP_ID} .sf-launcher { pointer-events: auto; position: fixed; right: 18px; top: 44%; display: none; align-items: center; gap: 6px; padding: 8px 14px; border: 1px solid #171b20; border-radius: 20px; color: #fff; background: #171b20; box-shadow: 0 8px 22px rgba(23,27,32,.22); cursor: pointer; font-size: 12px; font-weight: 700; line-height: 1; z-index: 2147483647; } #${APP_ID} .sf-launcher .sf-launcher-label { display: inline-block; white-space: nowrap; } #${APP_ID} .sf-panel { pointer-events: auto; position: fixed; top: 18px; right: 18px; width: min(370px, calc(100vw - 36px)); max-height: calc(100vh - 36px); overflow: hidden; background: #fff; border: 1px solid #dce1e5; border-radius: 8px; box-shadow: 0 16px 42px rgba(31,41,51,.18); } #${APP_ID} .sf-header { display: grid; grid-template-columns: 36px minmax(0,1fr) 34px; gap: 10px; align-items: center; min-height: 68px; padding: 12px 14px; color: #fff; background: #171b20; } #${APP_ID} .sf-mark { width: 34px; height: 34px; border-radius: 7px; display: grid; place-items: center; color: #132019; background: #7ee2a8; } #${APP_ID} .sf-brand { min-width: 0; } #${APP_ID} .sf-brand strong { display: block; font-size: 15px; line-height: 20px; font-weight: 700; } #${APP_ID} .sf-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #bfc8ce; font-size: 12px; line-height: 18px; } #${APP_ID} .sf-icon-btn { width: 34px; height: 34px; padding: 0; border: 1px solid #dce1e5; border-radius: 7px; display: inline-grid; place-items: center; color: #34414b; background: #fff; cursor: pointer; transition: background .16s ease, border-color .16s ease, color .16s ease; } #${APP_ID} .sf-icon-btn:hover { color: #111827; border-color: #aeb8bf; background: #f4f6f7; } #${APP_ID} .sf-header .sf-icon-btn { color: #dfe6e9; border-color: #3b444a; background: transparent; } #${APP_ID} .sf-header .sf-icon-btn:hover { color: #fff; background: #2b3338; } #${APP_ID} .sf-body { max-height: calc(100vh - 104px); overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; } #${APP_ID} .sf-status { display: grid; grid-template-columns: auto minmax(0,1fr) auto; gap: 8px; align-items: center; padding: 12px 14px; border-bottom: 1px solid #edf0f2; background: #f8faf9; } #${APP_ID} .sf-dot { width: 9px; height: 9px; border-radius: 50%; background: #c2c9ce; } #${APP_ID} .sf-dot.is-playing { background: #22a960; box-shadow: 0 0 0 4px rgba(34,169,96,.12); } #${APP_ID} .sf-state-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; color: #44515a; } #${APP_ID} .sf-rate-label { color: #8a5700; background: #fff1c7; border-radius: 4px; padding: 3px 6px; font-size: 12px; font-weight: 700; } #${APP_ID} .sf-section { padding: 14px; border-bottom: 1px solid #edf0f2; } #${APP_ID} .sf-section:last-child { border-bottom: 0; } #${APP_ID} .sf-progress { width: 100%; height: 5px; accent-color: #22a960; cursor: pointer; } #${APP_ID} .sf-time { display: flex; justify-content: space-between; margin-top: 6px; color: #7a858d; font-size: 12px; font-variant-numeric: tabular-nums; } #${APP_ID} .sf-transport { display: grid; grid-template-columns: repeat(3, 42px); justify-content: center; gap: 12px; margin-top: 12px; } #${APP_ID} .sf-transport .sf-icon-btn { width: 42px; height: 42px; } #${APP_ID} .sf-transport .sf-primary { color: #fff; border-color: #171b20; background: #171b20; } #${APP_ID} .sf-transport .sf-primary:hover { color: #fff; border-color: #30383d; background: #30383d; } #${APP_ID} .sf-label { display: block; margin-bottom: 8px; color: #66727a; font-size: 12px; font-weight: 700; } #${APP_ID} .sf-segmented { display: grid; grid-template-columns: repeat(5, minmax(0,1fr)); padding: 3px; border: 1px solid #dce1e5; border-radius: 7px; background: #f4f6f7; } #${APP_ID} .sf-speed { min-width: 0; height: 30px; border: 0; border-radius: 5px; color: #5d6870; background: transparent; cursor: pointer; font-size: 12px; } #${APP_ID} .sf-speed.is-active { color: #17251d; background: #fff; box-shadow: 0 1px 4px rgba(31,41,51,.12); font-weight: 700; } #${APP_ID} .sf-option { display: flex; align-items: center; justify-content: space-between; min-height: 38px; gap: 16px; color: #3f4b53; font-size: 13px; } #${APP_ID} .sf-switch { position: relative; width: 38px; height: 22px; flex: 0 0 auto; } #${APP_ID} .sf-switch input { position: absolute; opacity: 0; pointer-events: none; } #${APP_ID} .sf-switch span { display: block; width: 100%; height: 100%; border-radius: 11px; background: #c7ced3; cursor: pointer; transition: background .16s ease; } #${APP_ID} .sf-switch span::after { content: ""; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(31,41,51,.25); transition: transform .16s ease; } #${APP_ID} .sf-switch input:checked + span { background: #22a960; } #${APP_ID} .sf-switch input:checked + span::after { transform: translateX(16px); } #${APP_ID} .sf-note { width: 100%; min-height: 92px; resize: vertical; border: 1px solid #dce1e5; border-radius: 7px; padding: 9px 10px; color: #26323a; background: #fbfcfc; outline: none; line-height: 1.55; } #${APP_ID} .sf-note:focus { border-color: #22a960; box-shadow: 0 0 0 3px rgba(34,169,96,.10); background: #fff; } #${APP_ID} .sf-note-state { min-height: 18px; margin-top: 5px; text-align: right; color: #879199; font-size: 11px; } #${APP_ID} .sf-toast { position: absolute; left: 50%; bottom: 18px; transform: translateX(-50%); max-width: calc(100% - 28px); padding: 8px 11px; border-radius: 6px; color: #fff; background: #1f2933; box-shadow: 0 8px 20px rgba(31,41,51,.2); font-size: 12px; opacity: 0; pointer-events: none; transition: opacity .18s ease; } #${APP_ID} .sf-toast.is-visible { opacity: 1; } #${APP_ID} .sf-ad-section { padding: 8px 14px; } #${APP_ID} .sf-ad-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 4px; } #${APP_ID} .sf-ad-head .sf-label { margin-bottom: 0; } #${APP_ID} .sf-ad-toggle { min-height: 0; font-size: 12px; color: #66727a; } #${APP_ID} .sf-ad-toggle .sf-switch { width: 32px; height: 18px; } #${APP_ID} .sf-ad-toggle .sf-switch span::after { width: 12px; height: 12px; } #${APP_ID} .sf-ad-toggle .sf-switch input:checked + span::after { transform: translateX(14px); } #${APP_ID} .sf-ad-body { min-height: 44px; } #${APP_ID} .sf-ad-placeholder { display: flex; align-items: center; justify-content: center; min-height: 44px; padding: 8px; border: 1px dashed #d3dade; border-radius: 7px; color: #98a2a8; font-size: 12px; background: #fafbfb; cursor: default; } #${APP_ID} .sf-ad-placeholder.is-action { cursor: pointer; } #${APP_ID} .sf-ad-placeholder.is-action:hover { color: #22a960; border-color: #9ad9b5; background: #f4faf6; } #${APP_ID} .sf-ad-item { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border: 1px solid #e4e9ec; border-radius: 7px; color: inherit; text-decoration: none; background: #fff; } #${APP_ID} .sf-ad-item:hover { border-color: #b9cfc1; box-shadow: 0 2px 8px rgba(31,41,51,.08); } #${APP_ID} .sf-ad-item .sf-ad-img { width: 72px; height: 44px; object-fit: cover; border-radius: 5px; flex: 0 0 auto; margin-bottom: 0; } #${APP_ID} .sf-ad-info { min-width: 0; flex: 1 1 auto; } #${APP_ID} .sf-ad-title { color: #1f2933; font-size: 13px; font-weight: 700; line-height: 18px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #${APP_ID} .sf-ad-text { margin-top: 3px; color: #66727a; font-size: 12px; line-height: 16px; white-space: pre-line; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } #${APP_ID} .sf-ad-btn { display: inline-block; margin-top: 5px; padding: 3px 9px; border-radius: 5px; color: #fff; background: #22a960; font-size: 11px; font-weight: 700; cursor: pointer; } #${APP_ID} .sf-ad-item:hover .sf-ad-btn { background: #1c9553; } #${APP_ID}[data-open="false"] .sf-panel { display: none; } #${APP_ID}[data-open="false"] .sf-launcher { display: grid; } #${APP_ID} svg { width: 17px; height: 17px; stroke-width: 2; } @media (max-width: 480px) { #${APP_ID} .sf-panel { top: auto; right: 10px; bottom: 10px; width: calc(100vw - 20px); max-height: calc(100vh - 20px); } #${APP_ID} .sf-body { max-height: calc(100vh - 88px); } #${APP_ID} .sf-launcher { right: 12px; top: auto; bottom: 18px; } } `; document.head.appendChild(style); } function icon(name) { const fallback = { 'activity': '●', 'panel-right-open': '◀', 'panel-right-close': '▶', 'rotate-ccw': '↺', 'rotate-cw': '↻', 'play': '▶', 'pause': 'Ⅱ', 'refresh-cw': '↻' }[name] || '•'; return `${fallback}`; } function refreshIcons() { if (typeof lucide === 'undefined' || typeof lucide.createIcons !== 'function') return; try { lucide.createIcons({ attrs: { 'aria-hidden': 'true' } }); } catch (_error) { // Text fallbacks remain visible if the icon bundle is unavailable. } } function buildUI() { injectStyle(); const root = document.createElement('div'); root.id = APP_ID; root.dataset.open = String(Boolean(settings.panelOpen)); root.innerHTML = ` ${icon('panel-right-open')}StudyFlow `; document.body.appendChild(root); ui = { root, title: root.querySelector('.sf-title'), stateText: root.querySelector('.sf-state-text'), stateDot: root.querySelector('.sf-dot'), rateLabel: root.querySelector('.sf-rate-label'), progress: root.querySelector('.sf-progress'), current: root.querySelector('.sf-current'), duration: root.querySelector('.sf-duration'), playButton: root.querySelector('[data-action="play"]'), note: root.querySelector('.sf-note'), noteState: root.querySelector('.sf-note-state'), toast: root.querySelector('.sf-toast'), adBody: root.querySelector('.sf-ad-body'), adSection: root.querySelector('.sf-ad-section') }; bindUI(); refreshIcons(); renderSettings(); loadNote(); renderState(); renderAdArea(); } function bindUI() { ui.root.addEventListener('click', (event) => { const actionNode = event.target.closest('[data-action]'); const rateNode = event.target.closest('[data-rate]'); if (rateNode) sendCommand('set-rate', Number(rateNode.dataset.rate)); if (!actionNode) return; const action = actionNode.dataset.action; if (action === 'open' || action === 'close') { const open = action === 'open'; ui.root.dataset.open = String(open); updateSettings({ panelOpen: open }); showToast(open ? '面板已展开' : '面板已收起,点右下角「StudyFlow」可重新打开'); } else if (action === 'play') { sendCommand('toggle-play'); } else if (action === 'back') { sendCommand('seek-by', -10); } else if (action === 'forward') { sendCommand('seek-by', 10); } else if (action === 'ad-refresh') { loadRemoteAd(true); } }); ui.root.querySelectorAll('[data-setting]').forEach((input) => { input.addEventListener('change', () => { const patch = { [input.dataset.setting]: input.checked }; updateSettings(patch); sendSettingToSelected(patch); }); }); ui.progress.addEventListener('change', () => sendCommand('seek-to', Number(ui.progress.value))); ui.note.addEventListener('input', () => { window.clearTimeout(noteSaveTimer); ui.noteState.textContent = '编辑中'; noteSaveTimer = window.setTimeout(saveNote, 450); }); } function sendSettingToSelected(patch) { const entry = selectedEntry(); if (!entry || entry.source === window) return; entry.source.postMessage({ channel: CHANNEL, type: 'command', target: entry.state.frameId, command: 'set-setting', value: patch }, '*'); } function renderSettings() { if (!ui) return; ui.root.dataset.open = String(Boolean(settings.panelOpen)); ui.root.querySelectorAll('[data-setting]').forEach((input) => { input.checked = Boolean(settings[input.dataset.setting]); }); } function renderState() { if (!ui) return; const entry = selectedEntry(); const state = entry ? entry.state : null; const hasVideo = Boolean(state && state.hasVideo); const playing = Boolean(hasVideo && state.playing); const current = hasVideo ? state.currentTime : 0; const duration = hasVideo ? state.duration : 0; const rate = hasVideo ? state.playbackRate : settings.playbackRate; ui.title.textContent = state ? state.title : '等待视频'; ui.stateText.textContent = hasVideo ? (playing ? '正在播放' : state.ended ? '播放完成' : '已暂停') : '当前页面未发现视频'; ui.stateDot.classList.toggle('is-playing', playing); ui.rateLabel.textContent = `${Number(rate || 1).toFixed(2)}x`; ui.progress.max = String(Math.max(1, duration)); ui.progress.value = String(Math.min(current, duration || current)); ui.progress.disabled = !hasVideo; ui.current.textContent = formatTime(current); ui.duration.textContent = formatTime(duration); const playState = playing ? 'pause' : 'play'; if (ui.playButton.dataset.state !== playState) { ui.playButton.dataset.state = playState; ui.playButton.innerHTML = icon(playState); refreshIcons(); } ui.playButton.title = playing ? '暂停' : '播放'; ui.playButton.setAttribute('aria-label', playing ? '暂停' : '播放'); ui.root.querySelectorAll('[data-rate]').forEach((button) => { button.classList.toggle('is-active', Math.abs(Number(button.dataset.rate) - Number(rate)) < 0.01); }); } // ==================== 广告(远程 JSON 配置,改远程文件即可换广告) ==================== let adData = null; // 最近一次成功解析的广告数据 let adLoadedAt = 0; let adStatus = 'idle'; // idle | loading | ok | error function adMsg(text) { const el = document.createElement('div'); el.className = 'sf-ad-placeholder'; el.textContent = text; return el; } function hideAdSection() { if (ui && ui.adSection) ui.adSection.style.display = 'none'; } function showAdSection() { if (ui && ui.adSection) ui.adSection.style.display = ''; } function renderAdArea() { if (!ui || !ui.adBody) return; const body = ui.adBody; body.innerHTML = ''; // 广告地址仅在脚本内部配置,不在面板暴露;未配置或关闭时整个广告区隐藏 if (!settings.adEnabled || !settings.adUrl) { hideAdSection(); return; } if (adStatus === 'loading' || adStatus === 'idle') { showAdSection(); loadRemoteAd(false); return; } if (adStatus === 'error' || !adData) { showAdSection(); body.appendChild(adMsg(adStatus === 'error' ? '广告加载失败' : '')); return; } const item = adData; if (item.enabled === false) { hideAdSection(); return; } const expireTime = item.expireAt ? new Date(item.expireAt).getTime() : NaN; if (Number.isFinite(expireTime) && expireTime < Date.now()) { hideAdSection(); return; } showAdSection(); const link = document.createElement('a'); link.className = 'sf-ad-item'; if (item.link) { link.href = item.link; link.target = '_blank'; link.rel = 'noopener'; } if (item.image) { const img = document.createElement('img'); img.className = 'sf-ad-img'; img.src = item.image; img.alt = item.title || '广告'; link.appendChild(img); } const info = document.createElement('div'); info.className = 'sf-ad-info'; if (item.title) { const t = document.createElement('div'); t.className = 'sf-ad-title'; t.textContent = item.title; info.appendChild(t); } if (item.text) { const p = document.createElement('div'); p.className = 'sf-ad-text'; p.textContent = item.text; info.appendChild(p); } if (item.link) { const btn = document.createElement('span'); btn.className = 'sf-ad-btn'; btn.textContent = '跳转地址'; info.appendChild(btn); } link.appendChild(info); body.appendChild(link); } function loadRemoteAd(force) { if (!settings.adUrl || !settings.adEnabled) return; if (!force && adData && Date.now() - adLoadedAt < 300000) return; // 5 分钟内不重复拉取 if (typeof GM_xmlhttpRequest !== 'function') { adStatus = 'error'; if (ui && ui.adBody) { ui.adBody.innerHTML = ''; ui.adBody.appendChild(adMsg('当前环境不支持广告拉取(需油猴 GM_xmlhttpRequest 权限)')); } return; } adStatus = 'loading'; try { GM_xmlhttpRequest({ method: 'GET', url: settings.adUrl, timeout: 8000, onload: (res) => { try { const data = JSON.parse(res.responseText || ''); adData = (data && typeof data === 'object' && !Array.isArray(data)) ? data : null; adStatus = 'ok'; } catch (e) { adData = null; adStatus = 'error'; } adLoadedAt = Date.now(); renderAdArea(); }, onerror: () => { adData = null; adStatus = 'error'; adLoadedAt = Date.now(); renderAdArea(); }, ontimeout: () => { adData = null; adStatus = 'error'; adLoadedAt = Date.now(); renderAdArea(); } }); } catch (e) { adData = null; adStatus = 'error'; renderAdArea(); } } function loadNote() { ui.note.value = String(readValue(`${NOTE_PREFIX}${pageKey()}`, '')); ui.noteState.textContent = ui.note.value ? '已保存' : ''; } function saveNote() { writeValue(`${NOTE_PREFIX}${pageKey()}`, ui.note.value); ui.noteState.textContent = '已保存'; } let toastTimer = 0; function showToast(message) { if (!ui) return; window.clearTimeout(toastTimer); ui.toast.textContent = message; ui.toast.classList.add('is-visible'); toastTimer = window.setTimeout(() => ui.toast.classList.remove('is-visible'), 1800); } function openPanel() { if (!ui) return; ui.root.dataset.open = 'true'; updateSettings({ panelOpen: true }); } if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('StudyFlow:打开控制台', openPanel); GM_registerMenuCommand('StudyFlow:清除本页续播位置', () => { const video = selectVideo(); if (video) clearPosition(video); showToast('本页续播位置已清除'); }); } /* ================================================================ * 自动刷课模块(不含答题功能) * 覆盖:超星章节学习页任务点、智慧树新版学习页 / 智能课程 / 共享课 * ================================================================ */ // ---------- 基础工具 ---------- function sfSleep(seconds) { return new Promise((resolve) => setTimeout(resolve, Math.max(0, (Number(seconds) || 0)) * 1000)); } function sfWaitFor(fn, timeout, interval) { return new Promise((resolve, reject) => { const start = Date.now(); const timer = setInterval(() => { let result = null; try { result = fn(); } catch (e) { result = null; } if (result) { clearInterval(timer); resolve(result); return; } if (Date.now() - start > (timeout || 10000)) { clearInterval(timer); reject(new Error('等待超时')); } }, interval || 300); }); } function clickEl(el) { if (!el) return; try { if (typeof el.click === 'function') { el.click(); } else { el.dispatchEvent(new MouseEvent('click', { bubbles: true })); } } catch (e) { /* ignore */ } } function safeTopWindow() { try { return window.top; } catch (e) { return window; } } function safeTopDocument() { try { return safeTopWindow().document; } catch (e) { return document; } } function autoToast(message) { // 提示词已按需求停用(自动刷课功能完整保留,未移除) // 原提示逻辑(需要时取消注释即可恢复): // try { if (typeof showToast === 'function') showToast(message); } catch (e) { /* ignore */ } // try { console.log('[StudyFlow]', message); } catch (e) { /* ignore */ } } function autoLog(message) { // 日志提示已按需求停用(自动刷课功能完整保留,未移除) // 原日志逻辑(需要时取消注释即可恢复): // try { console.log('[StudyFlow]', message); } catch (e) { /* ignore */ } } // ---------- 倍速锁定 ---------- function lockAutoRate(video, rate) { if (!video || !settings.autoStudy) return; const value = (Number(rate) > 0 && Number(rate) <= 16) ? Number(rate) : 1; try { video.playbackRate = value; } catch (e) { /* ignore */ } try { Object.defineProperty(video, 'playbackRate', { configurable: true, get: function () { return autoState.lockedRates.get(video) || value; }, set: function () { /* 阻止外部修改 */ } }); autoState.lockedRates.set(video, value); autoState.drivenVideos.add(video); } catch (e) { /* ignore */ } } function unlockAutoRate(video) { if (!video) return; if (autoState.lockedRates.has(video)) { try { delete video.playbackRate; } catch (e) { /* ignore */ } autoState.lockedRates.delete(video); } autoState.drivenVideos.delete(video); } function setBestRate(video) { const desired = Number(settings.playbackRate) || 1; const rates = [desired, 2, 1.75, 1.5, 1.25].filter(function (r, i, arr) { return arr.indexOf(r) === i; }); for (let i = 0; i < rates.length; i += 1) { try { video.playbackRate = rates[i]; if (video.playbackRate === rates[i]) return; } catch (e) { /* ignore */ } } } function getCappedRate(cap) { const rate = Number(settings.playbackRate) || 1; return Math.min(rate, cap || 2); } // ---------- 播放器内倍速菜单跟随 ---------- // 自动刷课用 Object.defineProperty 劫持 playbackRate(set 为空)锁定倍速, // 这会让播放器自带的倍速菜单点了没反应。用户在播放器内点倍速菜单时, // 解除劫持让播放器正常写入,并按新值重新锁定(对齐 ABC hookVedio 的锁定行为)。 const RATE_MENU_SEL = [ '.vjs-playback-rate .vjs-menu-item', '.vjs-menu-content .vjs-menu-item', '.vjs-playback-rate-value', '[class*="speed"] li', '[class*="rate"] li', '.speedBox li', '.rateBox li' ].join(', '); function installRateMenuFollow() { try { document.addEventListener('click', (event) => { const target = event.target; const item = target && target.closest ? target.closest(RATE_MENU_SEL) : null; if (!item) return; const video = document.querySelector('video'); if (!video) return; // 解除倍速劫持,让播放器能正常写入用户选择的倍速 unlockAutoRate(video); const followOnce = () => { video.removeEventListener('ratechange', followOnce); const r = Number(video.playbackRate) || 1; if (r > 0 && r <= 16) { updateSettings({ playbackRate: r }); if (settings.autoStudy) lockAutoRate(video, r); } }; video.addEventListener('ratechange', followOnce); }, true); } catch (e) { /* ignore */ } } // ---------- 后台挂机 ---------- // 切出页面/最小化后继续刷课:平台因页面隐藏而暂停视频时自动恢复; // 心跳定时器兜底(浏览器后台节流或平台不触发 pause 事件也能拉回播放)。 function installBackgroundKeepalive() { try { document.addEventListener('visibilitychange', () => { if (!settings.backgroundMode || !document.hidden) return; const video = selectVideo(); if (video && video.paused && !video.ended && !video.__sfUserPaused) { video.play().catch(() => { /* ignore */ }); } }); window.setInterval(() => { if (!settings.backgroundMode) return; const video = selectVideo(); if (video && video.paused && !video.ended && !video.__sfUserPaused) { video.play().catch(() => { /* ignore */ }); } }, 30000); } catch (e) { /* ignore */ } } // ---------- 登录页/非学习页自动折叠 ---------- // 登录页、课程列表页等没有学习内容,完整面板会挡住页面表单; // 检测到非学习页时自动收起成右下角小圆点,进入学习页再按用户设置展开。 function isLearningPage() { const host = (location.hostname || '').toLowerCase(); const href = location.href || ''; if (host.endsWith('chaoxing.com')) { return /mycourse\/studentstudy|chapterId=/.test(href) && !/login|passport|verify|register/.test(href); } if (host.endsWith('zhihuishu.com')) { return /\/learnPage\b|\/learning\b|\/course2cStudy\b|onlineh5/.test(href) && !/login|passport|sso|register/.test(href); } return false; } function installLoginAutoFold() { try { window.setInterval(() => { if (!ui || !ui.root) return; const open = isLearningPage() ? Boolean(settings.panelOpen) : false; if (ui.root.dataset.open !== String(open)) { ui.root.dataset.open = String(open); } }, 2000); } catch (e) { /* ignore */ } } // ---------- 页面识别 ---------- function detectAutoDriver() { const host = String(location.hostname || '').toLowerCase(); const path = String(location.pathname || ''); const isZhs = host === 'zhihuishu.com' || host.endsWith('.zhihuishu.com'); const isCx = host === 'chaoxing.com' || host.endsWith('.chaoxing.com') || host === 'neauce.com' || host.endsWith('.neauce.com') || host.endsWith('.edu.cn'); if (isZhs) { if ((host.indexOf('ai-smart-course-student-pro') >= 0 || host.indexOf('smartcoursestudent') >= 0) && (path.indexOf('/learnPage') >= 0 || path.indexOf('/singleCourse') >= 0)) return 'zhs-smart'; if (host.indexOf('studyplush5') >= 0 || host.indexOf('studywisdomh5') >= 0 || host.indexOf('wisdom-mooc') >= 0) return 'zhs-shared'; if (host.indexOf('onlineh5') >= 0) return 'zhs-shared'; if (path.indexOf('/learnPage') >= 0) return 'zhs-learn'; if (path.indexOf('/portals_h5/2clearning.html') >= 0) return 'zhs-2c'; return 'generic'; } if (isCx) { if (path.indexOf('/mycourse/studentstudy') >= 0 || path.indexOf('/knowledge/cards') >= 0) return 'cx-chapter'; return 'generic'; } return 'generic'; } // ---------- 超星测验/作业/答题类任务点识别(不做题,仅放行不阻塞翻章) ---------- function isCxQuizTask(frame) { try { const src = String((frame && (frame.src || (frame.getAttribute && frame.getAttribute('src')))) || ''); return /(api(\d)?\/work|mooc\d?\/work|modules\/work|doHomeWork|doHomework|reVersionTest|\/exam\/|testExam|homework|quiz)/.test(src); } catch (e) { return false; } } // ---------- 人脸识别(超星) ---------- const SF_FACE_TEXTS = [ '人脸信息采集', '采集人脸信息', '请使用手机APP采集人脸信息后继续学习', '请扫描下方二维码进入' ]; const SF_FACE_GENERIC_TEXTS = ['人脸识别', '继续学习']; function elVisible(el) { if (!el) return false; try { const style = el.ownerDocument.defaultView.getComputedStyle(el); if (style && (style.display === 'none' || style.visibility === 'hidden')) return false; } catch (e) { /* ignore */ } return el.offsetParent !== null || (typeof el.getClientRects === 'function' && el.getClientRects().length > 0); } function hasFaceInDocs(docs) { for (let i = 0; i < docs.length; i += 1) { const doc = docs[i]; if (!doc || !doc.body) continue; try { if (Array.prototype.some.call(doc.querySelectorAll('#fcqrimg'), (el) => el.getAttribute('src') && elVisible(el))) return true; if (Array.prototype.some.call(doc.querySelectorAll('.chapterVideoFaceMaskDiv'), elVisible)) return true; } catch (e) { /* ignore */ } const bodyText = doc.body.innerText || doc.body.textContent || ''; if (SF_FACE_TEXTS.some((t) => bodyText.indexOf(t) >= 0)) return true; try { const found = Array.prototype.some.call(doc.querySelectorAll('div,section,article,body'), (el) => { if (!elVisible(el)) return false; const text = el.innerText || el.textContent || ''; return SF_FACE_GENERIC_TEXTS.some((t) => text.indexOf(t) >= 0) && (el.querySelector('canvas,img,svg') || text.indexOf('人脸') >= 0); }); if (found) return true; } catch (e) { /* ignore */ } } return false; } async function waitFaceFor(docs) { if (!settings.faceWait || !hasFaceInDocs(docs)) return; autoToast('检测到人脸识别,请手动完成后脚本将继续'); const start = Date.now(); while (autoState.running && settings.faceWait && hasFaceInDocs(docs) && Date.now() - start < 600000) { await sfSleep(3); } } // ---------- 通用辅助 ---------- function getAllNestedIframes(root) { const result = []; const queue = Array.prototype.slice.call((root || document).querySelectorAll('iframe')); while (queue.length) { const frame = queue.shift(); result.push(frame); try { if (frame.contentDocument) { Array.prototype.slice.call(frame.contentDocument.querySelectorAll('iframe')).forEach((f) => queue.push(f)); } } catch (e) { /* ignore */ } } return result; } function waitIframeLoad(iframe, timeout) { return new Promise((resolve) => { const start = Date.now(); const timer = setInterval(() => { let ok = false; try { ok = Boolean(iframe.contentDocument) && iframe.contentDocument.readyState === 'complete'; } catch (e) { ok = false; } if (ok || Date.now() - start > (timeout || 30000)) { clearInterval(timer); resolve(); } }, 500); }); } function waitForMediaEl(timeout) { return sfWaitFor(() => { const media = document.querySelector('video,audio'); return (media && media.src) ? media : null; }, timeout || 5000, 200); } function waitZhsVideoEnd(video) { return new Promise((resolve) => { let resolved = false; let resumeTimer = null; let watchTimer = null; const done = () => { if (resolved) return; resolved = true; video.removeEventListener('ended', done); video.removeEventListener('pause', onPause); if (resumeTimer) clearTimeout(resumeTimer); if (watchTimer) clearInterval(watchTimer); resolve(); }; const onPause = () => { if (video.ended) return done(); if (video.duration > 0 && video.duration - video.currentTime <= 1) return done(); if (document.hidden && settings.pauseWhenHidden) return; if (settings.autoResume && !video.__sfUserPaused) { resumeTimer = setTimeout(() => { if (video && !video.ended && video.paused && settings.autoResume && !video.__sfUserPaused) { video.play().catch(() => { /* ignore */ }); } }, 2000); } }; video.addEventListener('ended', done, { once: true }); video.addEventListener('pause', onPause); watchTimer = setInterval(() => { if (!video.isConnected) done(); }, 5000); }); } function dismissReadNoteDialog() { const noteDialog = document.querySelector('.read-note-dialog'); if (noteDialog && getComputedStyle(noteDialog).display !== 'none') { const btn = noteDialog.querySelector('button'); if (btn) clickEl(btn); } } // ==================== 超星学习通章节学习页 ==================== const cxAutoDriver = { started: false, processing: false, lastUrl: '', lastAdvanceAt: 0, start: function () { if (this.started) return; this.started = true; const href = location.href; if (href.indexOf('mooc2=1') < 0) { const sep = href.indexOf('?') >= 0 ? '&' : '?'; location.href = href + sep + 'mooc2=1'; return; // 刷新后重新进入 } this.lastUrl = href; autoState.ownsNavigation = true; autoLog('进入超星章节学习页,自动刷课开始'); // 人脸识别提示(非阻塞) autoState.timers.push(setInterval(() => { if (!autoState.running) return; if (settings.faceWait && hasFaceInDocs([safeTopDocument(), document])) { autoToast('检测到人脸识别,请在页面内手动完成'); } }, 4000)); // URL 变化 autoState.timers.push(setInterval(() => { if (location.href !== this.lastUrl) { this.lastUrl = location.href; this.schedule(); } }, 2000)); // iframe 增删 try { this.observer = new MutationObserver(() => this.schedule()); this.observer.observe(document.documentElement, { childList: true, subtree: true }); } catch (e) { /* ignore */ } this.schedule(); }, schedule: function () { if (!autoState.running) return; clearTimeout(this.scheduleTimer); this.scheduleTimer = setTimeout(() => this.processTasks(), 400); }, processTasks: async function () { if (this.processing || !autoState.running) return; this.processing = true; try { const iframes = getAllNestedIframes(document); const tasks = iframes.filter((frame) => { try { const parent = frame.parentElement; // 任务点容器(.ans-job-icon)或测验/作业类 iframe(即使容器无任务图标)都纳入任务点 return Boolean(parent) && (Boolean(parent.querySelector('.ans-job-icon')) || isCxQuizTask(frame)); } catch (e) { return false; } }); if (!tasks.length) return; // 当前没有可处理的任务点 const unfinished = tasks.filter((frame) => { try { // 测验/作业/答题类任务点不做题也不等待,直接视为已放行,避免卡住不自动下一章 if (isCxQuizTask(frame)) return false; return !frame.parentElement.classList.contains('ans-job-finished'); } catch (e) { return true; } }); if (unfinished.length) { for (let i = 0; i < unfinished.length; i += 1) { if (!autoState.running) return; await this.processTask(unfinished[i]); } } else if (Date.now() - this.lastAdvanceAt > 10000) { // 全部任务点已完成 → 自动下一章 this.lastAdvanceAt = Date.now(); await this.nextChapter(); } } finally { this.processing = false; } }, processTask: async function (frame) { try { await waitIframeLoad(frame, 30000); } catch (e) { /* ignore */ } if (!autoState.running) return; let doc = null, win = null; try { doc = frame.contentDocument; win = frame.contentWindow; } catch (e) { /* ignore */ } if (!doc || !win) return; const src = frame.src || ''; try { if (src.indexOf('video') >= 0) { await this.processMedia('video', doc, frame); } else if (src.indexOf('audio') >= 0) { await this.processMedia('audio', doc, frame); } else if (/modules\/(ppt|doc|pptx|docx|pdf)/.test(src)) { await this.processPpt(win); } else if (src.indexOf('modules/innerbook') >= 0) { await this.processBook(win); } // 测验等任务点不做自动处理,直接放行 } catch (e) { autoLog('任务点处理异常: ' + e.message); } }, processMedia: async function (type, doc, frame) { let media = null; try { media = await sfWaitFor(() => doc.documentElement.querySelector(type), 30000, 500); } catch (e) { return; } if (!autoState.running) return; if (settings.faceWait) await waitFaceFor([safeTopDocument(), document, doc]); if (!autoState.running) return; const rateDisabled = isCxRateDisabled(doc); const finalRate = rateDisabled ? 1 : (Number(settings.playbackRate) || 1); try { media.pause(); } catch (e) { /* ignore */ } media.muted = settings.autoMute !== false; media.play().catch(() => { /* ignore */ }); lockAutoRate(media, finalRate); let quizTimer = null; if (settings.skipVideoQuiz) { quizTimer = setInterval(() => skipCxVideoQuiz(doc), 2000); } const onPause = () => { if (!settings.autoResume || media.ended || media.__sfUserPaused) return; if (document.hidden && settings.pauseWhenHidden) return; setTimeout(() => { if (media && !media.ended && media.paused && settings.autoResume && !media.__sfUserPaused) { if (settings.faceWait) { waitFaceFor([safeTopDocument(), document, doc]).then(() => media.play().catch(() => { /* ignore */ })); } else { media.play().catch(() => { /* ignore */ }); } } }, 2000); }; const onTimeUpdate = () => { if (finalRate > 1 && Number.isFinite(media.duration) && media.duration - media.currentTime < 10) { // 结尾恢复 1 倍速,保证任务点完成 unlockAutoRate(media); try { media.playbackRate = 1; } catch (e) { /* ignore */ } lockAutoRate(media, 1); } }; media.addEventListener('pause', onPause); if (finalRate > 1) media.addEventListener('timeupdate', onTimeUpdate); try { await waitCxTaskFinished(frame); } catch (e) { /* ignore */ } if (quizTimer) clearInterval(quizTimer); media.removeEventListener('pause', onPause); media.removeEventListener('timeupdate', onTimeUpdate); try { media.pause(); } catch (e) { /* ignore */ } unlockAutoRate(media); }, processPpt: function (win) { return new Promise((resolve) => { try { const iframe = win.document.querySelector('#panView'); if (iframe && iframe.contentWindow && iframe.contentWindow.document) { const pptWindow = iframe.contentWindow; const scrollHeight = pptWindow.document.body.scrollHeight; pptWindow.scrollTo({ top: scrollHeight, behavior: 'smooth' }); } } catch (e) { /* ignore */ } resolve(); }); }, processBook: function (win) { return new Promise((resolve) => { try { const topWin = safeTopWindow(); if (topWin && typeof topWin.onchangepage === 'function' && typeof win.getFrameAttr === 'function') { topWin.onchangepage(win.getFrameAttr('end')); } } catch (e) { /* ignore */ } resolve(); }); }, nextChapter: async function () { if (!autoState.running) return; if (!settings.autoNextChapter) { autoToast('自动下一章未开启,已停止'); return; } const topWin = safeTopWindow(); const topDoc = safeTopDocument(); // 1) 章节树跳转(第一优先):跳到当前章节之后的第一个未完成任务点章节。 // 新版超星无 PCount、且任务点未完成时平台会拦截“下一节”按钮, // 只有模拟点击章节树(onclick="getTeacherAjax(...)")能绕开拦截(ABC 同款路径)。 try { const chapters = Array.prototype.slice.call(topDoc.querySelectorAll('[onclick^="getTeacherAjax"]')).map((el) => { const match = (el.getAttribute('onclick') || '').match(/\('(.*)','(.*)','(.*)'\)/); const countEl = el.parentElement ? el.parentElement.querySelector('.jobUnfinishCount') : null; return { el, id: match ? match[3] : '', unfinish: Number.parseInt((countEl && countEl.value) || '0', 10) || 0 }; }).filter((c) => c.id); if (chapters.length) { const currentId = (topDoc.querySelector('#curChapterId') || {}).value || ''; const idx = chapters.findIndex((c) => c.id === currentId); const ordered = idx >= 0 ? chapters.slice(idx + 1).concat(chapters.slice(0, idx)) : chapters; const next = ordered.find((c) => c.unfinish !== 0); if (next) { const courseId = (topDoc.querySelector('#curCourseId') || {}).value; const clazzId = (topDoc.querySelector('#curClazzId') || {}).value; if (courseId && clazzId && typeof topWin.getTeacherAjax === 'function') { topWin.getTeacherAjax(courseId, clazzId, next.id); } else { next.el.click(); } autoToast('正在跳转到未完成章节'); return; } } } catch (e) { /* ignore */ } // 2) 平台翻章 API(兼容旧版页面) try { if (topWin.PCount && typeof topWin.PCount.next === 'function') { const courseId = topDoc.querySelector('#curCourseId'); const chapterId = topDoc.querySelector('#curChapterId'); const clazzId = topDoc.querySelector('#curClazzId'); const tabs = topDoc.querySelectorAll('#prev_tab .prev_ul li'); if (courseId && courseId.value && chapterId && chapterId.value && clazzId && clazzId.value) { topWin._preChapterId = chapterId.value; topWin.PCount.next(String(tabs.length), chapterId.value, courseId.value, clazzId.value, ''); autoToast('本章节已完成,正在跳转下一章'); return; } } } catch (e) { /* ignore */ } // 3) 可见的下一节按钮(跳过隐藏的按钮,优先 #prevNextFocusNext) try { const btnCandidates = [ document.querySelector('#prevNextFocusNext'), document.querySelector('.jb_btn.jb_btn_92.fr.fs14.nextChapter') ].filter(Boolean); const nextBtn = btnCandidates.find((b) => (b.style.display || getComputedStyle(b).display) !== 'none'); if (nextBtn) { nextBtn.click(); autoToast('本章节已完成,正在跳转下一章'); return; } } catch (e) { /* ignore */ } autoToast('未找到可识别的下一章/下一节按钮'); } }; function isCxRateDisabled(doc) { try { return doc.querySelectorAll('.vjs-playback-rate .vjs-menu-content .vjs-menu-item').length === 0; } catch (e) { return false; } } function skipCxVideoQuiz(doc) { try { const quiz = doc.querySelector('.ans-timelineobjects'); if (!quiz || quiz.offsetParent === null) return; const submit = doc.querySelector('#videoquiz-submit'); if (!submit || submit.style.display === 'none' || submit.offsetParent === null) return; quiz.style.display = 'none'; const bg = doc.querySelector('.ans-timelineobjectsbg'); if (bg) bg.style.display = 'none'; const video = doc.querySelector('video'); if (video) video.play().catch(() => { /* ignore */ }); } catch (e) { /* ignore */ } } function waitCxTaskFinished(frame) { return new Promise((resolve) => { let done = false; let guard = null; let observer = null; const finish = () => { if (done) return; done = true; if (guard) clearTimeout(guard); try { if (observer) observer.disconnect(); } catch (e) { /* ignore */ } resolve(); }; const parent = frame.parentElement; if (!parent || !parent.isConnected || parent.classList.contains('ans-job-finished')) { finish(); return; } try { observer = new MutationObserver(finish); observer.observe(parent, { attributes: true, attributeFilter: ['class'], subtree: true }); } catch (e) { finish(); return; } guard = setTimeout(finish, 60000); }); } // ==================== 智慧树新版学习页 /learnPage ==================== const zhsLearnDriver = { started: false, busy: false, lastUrl: '', initTimer: null, start: function () { if (this.started) return; this.started = true; autoState.ownsNavigation = true; this.lastUrl = location.href; autoLog('进入智慧树学习页,自动刷课开始'); autoState.timers.push(setInterval(() => { if (location.href !== this.lastUrl) { const wasLearn = this.lastUrl.indexOf('/learnPage') >= 0; this.lastUrl = location.href; if (location.href.indexOf('/learnPage') >= 0 && !wasLearn) { this.scheduleInit(3000); } } }, 1500)); this.scheduleInit(6000); }, scheduleInit: function (delay) { if (!autoState.running) return; clearTimeout(this.initTimer); this.initTimer = setTimeout(() => this.init(), delay || 1000); }, init: async function () { if (!autoState.running || this.busy) return; this.busy = true; try { await sfSleep(2); dismissReadNoteDialog(); try { await sfWaitFor(() => document.querySelector('.preview-content'), 15000, 300); } catch (e) { /* ignore */ } await this.runResources(); } catch (e) { autoLog('智慧树学习页初始化异常: ' + e.message); } finally { this.busy = false; } }, runResources: async function () { if (!autoState.running) return; let items = null; try { items = await sfWaitFor(() => { const list = Array.prototype.slice.call(document.querySelectorAll('.resources-list .resources-item')); return list.length ? list : null; }, 20000, 500); } catch (e) { await this.nextSection(); return; } const activeIndex = items.findIndex((el) => el.classList.contains('active')); const startIndex = activeIndex >= 0 ? activeIndex : 0; for (let i = startIndex; i < items.length; i += 1) { if (!autoState.running) return; const el = items[i]; if (i > startIndex || activeIndex < 0) { const clickTarget = el.querySelector('.basic-info-video-card-container') || el; clickEl(clickTarget); await sfSleep(3); } if (!autoState.running) return; const type = getZhsResourceType(el); autoLog('处理资源 [' + (i + 1) + '/' + items.length + '] 类型: ' + type); if (type === 'video') await this.processVideo(); else if (type === 'book') await this.processBook(); else if (type === 'ppt') await this.processPpt(); else await sfSleep(2); } await this.nextSection(); }, processVideo: async function () { try { await sfSleep(5); const video = document.querySelector('.preview-section .video-player-wrapper video') || document.querySelector('.preview-section video'); if (!video) return; video.muted = settings.autoMute !== false; video.play().catch(() => { /* ignore */ }); // 强制锁定倍速(软设置可能被智慧树播放器重置为 1x) lockAutoRate(video, getCappedRate(2)); await waitZhsVideoEnd(video); } catch (e) { /* ignore */ } }, processPpt: async function () { try { await sfSleep(2); const totalEl = document.querySelector('.preview-section .ppt-preview-container .page-total'); const totalPages = parseInt((totalEl && totalEl.innerText) || '0', 10) || 0; if (!totalPages) return; let guard = 0; while (autoState.running && guard < 200) { const containers = Array.prototype.slice.call(document.querySelectorAll('.catalogue-container .div-img-container')); const currentPage = containers.findIndex((c) => c.classList.contains('choose')) + 1 || 1; if (currentPage >= totalPages) break; const nextBtn = document.querySelector('.preview-section .ppt-preview-container .next-page-btn'); if (!nextBtn) break; clickEl(nextBtn); guard += 1; await sfSleep(5); } } catch (e) { /* ignore */ } }, processBook: async function () { try { try { await sfWaitFor(() => document.querySelector('.preview-section .book-mark .book-content'), 15000, 300); } catch (e) { return; } try { await sfWaitFor(() => document.querySelector('.preview-section .book-mark .canvas-wrapper'), 15000, 300); } catch (e) { return; } const pages = document.querySelectorAll('.preview-section .book-mark .canvas-wrapper'); if (!pages.length) return; const firstRect = pages[0].getBoundingClientRect(); const marginTop = parseInt(getComputedStyle(pages[0]).marginTop, 10) || 0; const pageHeight = Math.max(firstRect.height + marginTop, 100); const container = document.querySelector('.preview-section .book-mark .book-container'); if (!container) return; let guard = 0; while (autoState.running && guard < 300) { const target = Math.min(container.scrollTop + pageHeight, container.scrollHeight - container.clientHeight); container.scrollTo({ top: target, behavior: 'smooth' }); await sfSleep(1.5); if (container.scrollTop + container.clientHeight >= container.scrollHeight - 2) break; guard += 1; } } catch (e) { /* ignore */ } }, nextSection: async function () { if (!autoState.running) return; if (!settings.autoNextChapter) { autoToast('自动下一节未开启,已停止'); return; } const sections = Array.prototype.slice.call(document.querySelectorAll('.section-item-collapse-info')); const active = document.querySelector('.section-item-collapse-info.active'); if (!active) return; const idx = sections.indexOf(active); if (idx >= 0 && idx + 1 < sections.length) { clickEl(sections[idx + 1]); await sfSleep(5); await this.runResources(); } else { autoToast('当前课程所有章节已完成'); } } }; function getZhsResourceType(resourceEl) { const iconBox = resourceEl ? resourceEl.querySelector('.icon-box') : null; if (!iconBox) return 'unknown'; const classes = iconBox.className || ''; if (classes.indexOf('video') >= 0) return 'video'; if (classes.indexOf('book') >= 0) return 'book'; if (classes.indexOf('other') >= 0 || classes.indexOf('ppt') >= 0) return 'ppt'; return 'unknown'; } // ==================== 智慧树智能课程(新形态) ==================== const zhsSmartDriver = { started: false, busy: false, lastUrl: '', runTimer: null, lastJumpAt: 0, start: function () { if (this.started) return; this.started = true; autoState.ownsNavigation = true; if (location.href.indexOf('/singleCourse') >= 0) return; // 需要手动进入章节 this.lastUrl = location.href; autoLog('进入智慧树智能课程学习页,自动刷课开始'); autoState.timers.push(setInterval(() => { if (location.href !== this.lastUrl) { this.lastUrl = location.href; if (location.href.indexOf('/learnPage') >= 0) this.scheduleRun(1000); } }, 1500)); this.scheduleRun(6000); }, scheduleRun: function (delay) { if (!autoState.running) return; clearTimeout(this.runTimer); this.runTimer = setTimeout(() => this.run(), delay || 1000); }, run: async function () { if (!autoState.running || this.busy) return; this.busy = true; try { await sfSleep(5); dismissReadNoteDialog(); try { await sfWaitFor(() => document.querySelector('.section-item-collapse-info'), 15000, 300); } catch (e) { /* ignore */ } await this.doWork(); } catch (e) { autoLog('智慧树智能课程处理异常: ' + e.message); } finally { this.busy = false; } }, doWork: async function () { if (!autoState.running) return; let card = getZhsSmartActiveCard(); if (card && zhsCardFinished(card)) { card = getZhsSmartNextCard(); if (!card) { await this.nextSection(); return; } await clickZhsSmartCard(card); await sfSleep(3); card = getZhsSmartActiveCard() || card; } if (!card) { card = getZhsSmartNextCard(); if (!card && getZhsSmartCards().length) { await this.nextSection(); return; } if (card) { await clickZhsSmartCard(card); await sfSleep(3); card = getZhsSmartActiveCard() || card; } } if (!card) { await this.nextSection(); return; } const type = getZhsSmartType(card); autoLog('处理资源类型: ' + type); if (type === 'video') await this.processVideo(); else if (type === 'book') await this.processBook(); else if (type === 'ppt') await this.processPpt(); else { // 测验/答题等非学习类卡片:关掉可能弹出的弹窗后直接推进,不做题不等待 try { Array.prototype.forEach.call(document.querySelectorAll('.el-overlay,.el-dialog'), (d) => { d.style.display = 'none'; }); } catch (e) { /* ignore */ } await sfSleep(3); await this.processNext(); } }, processVideo: async function () { try { const wrapper = document.querySelector('.video-player-wrapper'); if (!wrapper || wrapper.style.display === 'none') throw new Error('视频未加载'); let video = null; try { video = await waitForMediaEl(10000); } catch (e) { throw new Error('未找到音视频元素'); } if (!autoState.running) return; video.currentTime = Math.max(video.currentTime, 1); video.volume = 0; video.muted = settings.autoMute !== false; // 强制锁定倍速(智慧树播放器软设置可能被重置) lockAutoRate(video, getCappedRate(1.5)); video.play().catch(() => { video.muted = true; video.play().catch(() => { /* ignore */ }); }); await waitZhsVideoEnd(video); await sfSleep(3); await this.processNext(); } catch (e) { await sfSleep(3); await this.processNext(); } }, processPpt: async function () { try { const container = document.querySelector('.preview-section .ppt-preview-container'); if (!container) { await sfSleep(3); await this.processNext(); return; } const getPageInfo = () => { let current = 0, total = 0; const cur = container.querySelector('.page-current'); const tot = container.querySelector('.page-total'); if (cur) current = parseInt((cur.textContent || '').trim(), 10) || 0; if (tot) total = parseInt((tot.textContent || '').trim(), 10) || 0; return [current, total]; }; let info = getPageInfo(); if (!info[0] || !info[1]) { await sfSleep(3); await this.processNext(); return; } let guard = 0; while (autoState.running && info[0] < info[1] && guard < 200) { clickEl(container.querySelector('.next-page-btn')); guard += 1; await sfSleep(4); info = getPageInfo(); } await this.processNext(); } catch (e) { await this.processNext(); } }, processBook: async function () { try { let container = null; try { container = await sfWaitFor(() => document.querySelector('.preview-section .book-mark .book-container'), 5000, 300); } catch (e) { await this.processNext(); return; } let guard = 0; while (autoState.running && container && container.scrollTop + container.clientHeight < container.scrollHeight - 2 && guard < 300) { container.scrollTo({ top: Math.min(container.scrollTop + Math.max(container.clientHeight * 0.8, 200), container.scrollHeight), behavior: 'smooth' }); guard += 1; await sfSleep(1.5); } await this.processNext(); } catch (e) { await this.processNext(); } }, processNext: async function () { if (!autoState.running) return; const next = getZhsSmartNextCard(); if (next) { await clickZhsSmartCard(next); await sfSleep(3); await this.doWork(); } else { await this.nextSection(); } }, nextSection: async function () { if (!autoState.running) return; if (!settings.autoNextChapter) { autoToast('自动下一节未开启,已停止'); return; } if (Date.now() - this.lastJumpAt < 10000) return; this.lastJumpAt = Date.now(); const next = getZhsSmartNextSection(); if (!next) { autoToast('所有必学章节已完成'); return; } clickEl(next); await sfSleep(5); await this.doWork(); } }; function getZhsSmartCards() { return Array.prototype.slice.call(document.querySelectorAll('[class*="card-container"]')); } function getZhsSmartActiveCard() { const cards = getZhsSmartCards(); for (let i = 0; i < cards.length; i += 1) { if (cards[i].classList.contains('active')) return cards[i]; } return null; } function zhsCardFinished(card) { if (!card) return false; const icon = card.querySelector('.finished-icon'); if (!icon) return false; const text = (icon.textContent || '').trim(); return !text || text.indexOf('已完成') >= 0; } function getZhsSmartNextCard() { const cards = getZhsSmartCards(); const activeIndex = cards.findIndex((c) => c.classList.contains('active')); const start = activeIndex >= 0 ? activeIndex + 1 : 0; for (let i = start; i < cards.length; i += 1) { if (!zhsCardFinished(cards[i])) return cards[i]; } return null; } function getZhsSmartType(card) { if (!card) return 'unknown'; const iconBox = card.querySelector('.icon-box'); const classes = (iconBox && iconBox.className) || ''; if (classes.indexOf('video') >= 0) return 'video'; if (classes.indexOf('book') >= 0) return 'book'; if (classes.indexOf('ppt') >= 0 || classes.indexOf('other') >= 0) return 'ppt'; if (classes.indexOf('text') >= 0) return 'text'; return 'unknown'; } function clickZhsSmartCard(card) { if (!card) return; const target = card.querySelector('.common-text') || card.querySelector('.video-title') || card.querySelector('.basic-info-video-card-container') || card; if (getZhsSmartType(card) === 'unknown') { const pageWindow = safeTopWindow(); const originalOpen = pageWindow.open; try { pageWindow.open = function () { return null; }; clickEl(target); setTimeout(() => { pageWindow.open = originalOpen; }, 3000); } catch (e) { clickEl(target); } } else { clickEl(target); } } function getZhsSmartNextSection() { const sections = Array.prototype.slice.call(document.querySelectorAll('.section-item-collapse-info')); const active = sections.find((s) => s.classList.contains('active')); const unfinished = sections.map((section) => { const el = section.querySelector('.collapse-info-progress .progress-text'); const text = (el && el.textContent) || ''; const match = text.replace(/\s/g, '').match(/(\d+)\/(\d+)/); if (!match) return null; return { section, progress: parseInt(match[1], 10), total: parseInt(match[2], 10) }; }).filter((x) => x && x.progress < x.total); if (!unfinished.length) return null; const activeIndex = unfinished.findIndex((x) => x.section === active); if (activeIndex >= 0) { return unfinished[activeIndex + 1] ? unfinished[activeIndex + 1].section : null; } return unfinished[0].section; } // ==================== 智慧树共享课 ==================== const zhsSharedDriver = { started: false, busy: false, switching: false, warned: false, lastJumpAt: 0, start: function () { if (this.started) return; this.started = true; autoState.ownsNavigation = true; autoLog('进入智慧树共享课学习页,自动刷课开始'); autoState.timers.push(setInterval(() => this.poll(), 2000)); this.schedulePlay(5000); }, schedulePlay: function (delay) { if (!autoState.running) return; clearTimeout(this.playTimer); this.playTimer = setTimeout(() => this.play(), delay || 1000); }, poll: function () { if (!autoState.running) return; const mastery = document.querySelector('.masterylevel-body'); if (mastery) { const close = document.querySelector('.header-box .close-box'); if (close) clickEl(close); } if (Array.prototype.some.call(document.querySelectorAll('.el-overlay .content'), (el) => (el.textContent || '').indexOf('保持良好的学习习惯') >= 0)) { if (!this.warned) { this.warned = true; autoToast('检测到学习习惯提示弹窗,已暂停自动切换,请手动处理后刷新'); } return; } this.warned = false; if (document.querySelector('.yidun_popup')) { autoToast('检测到验证码,请完成验证后继续'); } if (!this.busy && !this.switching && settings.autoNextChapter) { const current = document.querySelector('.chapter-content-second.current, .chapter-item.current'); if (current && current.querySelector('.finish-icon') && Date.now() - this.lastJumpAt > 10000) { this.lastJumpAt = Date.now(); this.nextChapter(); } } }, play: async function () { if (!autoState.running || this.busy) return; this.busy = true; try { try { await sfWaitFor(() => document.querySelector('.child-main'), 20000, 500); } catch (e) { /* ignore */ } Array.prototype.forEach.call(document.querySelectorAll('.el-overlay,.el-dialog'), (d) => { d.style.display = 'none'; }); this.fixControls(); await sfSleep(2); let video = null; try { video = await waitForMediaEl(15000); } catch (e) { /* ignore */ } if (!video) { await this.nextChapter(); return; } video.currentTime = Math.max(video.currentTime, 1); video.volume = 0; video.muted = settings.autoMute !== false; // 强制锁定倍速(智慧树共享课播放器软设置可能被重置) lockAutoRate(video, getCappedRate(1.5)); video.play().catch(() => { /* ignore */ }); await waitZhsVideoEnd(video); await sfSleep(2); await this.nextChapter(); } catch (e) { await sfSleep(2); await this.nextChapter(); } finally { this.busy = false; } }, nextChapter: async function () { if (!autoState.running || this.switching) return; if (!settings.autoNextChapter) { autoToast('自动切换章节未开启,已停止'); return; } this.switching = true; try { const items = getZhsSharedChapters(); if (!items.length) return; const current = document.querySelector('.chapter-content-second.current, .chapter-item.current'); const idx = items.indexOf(current); const next = idx >= 0 ? items[idx + 1] : items[0]; if (!next) { autoToast('所有章节视频已完成'); return; } clickEl(next); await sfSleep(5); this.schedulePlay(1000); } finally { this.switching = false; } }, fixControls: function () { const bar = document.querySelector('.controlsBar'); if (bar) { bar.style.display = 'block'; bar.style.zIndex = '2'; } } }; function getZhsSharedChapters() { const raw = Array.prototype.slice.call(document.querySelectorAll('.chapter-content .chapter-item')); const items = []; raw.forEach((el) => { const children = el.querySelectorAll('.chapter-content-second'); if (children.length) { Array.prototype.forEach.call(children, (c) => items.push(c)); } else { items.push(el); } }); return items.filter((el) => !el.querySelector('.finish-icon')); } // ==================== 智慧树兴趣课 2C(在线学堂入口页) ==================== // 2clearning.html 页面本身无视频("加入学习成功"后需进入在线学堂 learning 页); // 有视频直接刷,有 onlineh5 在线学堂链接则自动跳转进入,那里由 zhs-shared 接管。 const zhs2cDriver = { started: false, initTimer: null, start: function () { if (this.started) return; this.started = true; autoLog('进入智慧树兴趣课 2C 页面'); this.scheduleInit(3000); }, scheduleInit: function (delay) { if (!autoState.running) return; clearTimeout(this.initTimer); this.initTimer = setTimeout(() => this.init(), delay || 1000); }, init: async function () { if (!autoState.running) return; try { // 1) 页面直接有视频 → 播放 + 锁定倍速 + 等待结束 const video = document.querySelector('video'); if (video) { video.muted = settings.autoMute !== false; video.play().catch(() => { /* ignore */ }); lockAutoRate(video, getCappedRate(2)); await waitZhsVideoEnd(video); this.scheduleInit(3000); return; } // 2) 入口页提示"加入学习成功" → 自动进入在线学堂学习页 const links = Array.prototype.slice.call(document.querySelectorAll('a[href*="onlineh5"]')); const target = links[0]; if (target && target.href) { autoLog('自动进入在线学堂: ' + target.href); location.href = target.href; return; } // 3) 无视频无链接:稍后重试(课程页可能异步加载) this.scheduleInit(8000); } catch (e) { /* ignore */ } } }; // ==================== 通用兜底:任何含视频的课程页 ==================== function startGenericAuto() { autoState.timers.push(setInterval(() => { if (!autoState.running || !settings.autoStudy) return; const video = selectVideo(); if (!video) return; if (video.ended) { autoState.drivenVideos.delete(video); return; } if (video.paused && !video.__sfUserPaused) { video.muted = settings.autoMute !== false; video.play().catch(() => { /* ignore */ }); setBestRate(video); autoState.drivenVideos.add(video); } }, 3000)); } // ==================== 启动 / 停止 ==================== function startAutoStudy() { if (autoState.running || !settings.autoStudy) return; autoState.running = true; const driver = detectAutoDriver(); autoLog('自动刷课驱动: ' + driver); if (driver === 'cx-chapter') cxAutoDriver.start(); else if (driver === 'zhs-learn') zhsLearnDriver.start(); else if (driver === 'zhs-smart') zhsSmartDriver.start(); else if (driver === 'zhs-shared') zhsSharedDriver.start(); else if (driver === 'zhs-2c') zhs2cDriver.start(); else startGenericAuto(); } function stopAutoStudy() { autoState.running = false; autoState.ownsNavigation = false; autoState.timers.forEach((timer) => { try { clearInterval(timer); } catch (e) { /* ignore */ } }); autoState.timers.length = 0; autoState.drivenVideos.forEach((video) => { try { unlockAutoRate(video); } catch (e) { /* ignore */ } }); autoState.drivenVideos.clear(); autoLog('自动刷课已停止'); } if (document.body) { buildUI(); installLoginAutoFold(); if (settings.autoStudy) startAutoStudy(); } else { window.addEventListener('DOMContentLoaded', () => { buildUI(); installLoginAutoFold(); if (settings.autoStudy) startAutoStudy(); }, { once: true }); } })();