// ==UserScript== // @name 内蒙古网络学院 课程进度助手 // @namespace local.nmg.auto // @version 3.1.2 // @description 在内蒙古网络学院输入专题名称后自动扫描课程、缓存课程ID,并按章节顺序推进课程播放。使用方法:登录后输入专题名并点击“扫描并开始”;数据仅保存在本机浏览器,不向第三方上传用户信息。 // @author 本地脚本作者 // @license MIT // @tag 内蒙古网络学院 // @tag 课程进度 // @tag 自动播放 // @tag 学习工具 // @match https://wlxy.nmgdj.gov.cn/* // @run-at document-start // @grant unsafeWindow // @noframes // ==/UserScript== (function () { 'use strict'; const W = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; const STORE_KEY = 'nmgAuto.v5'; const OLD_STORE_KEY = 'nmgAuto.v3'; const API_COLUMNS = '/api/v5/edu/category/get-special-columns'; const API_COURSES = '/api/v5/edu/course/get-course-column'; const BASE_HEADERS = { 'Content-Type': 'application/json', 'x-device-id': 'Web', 'x-tenant-id': 'nmg', 'x-app-id': '51cc0b63b9dc4eb5a8210263f2be0667', 'x-client-id': '51cc0b63b9dc4eb5a8210263f2be0667' }; // 输入关键词 -> 专题编号的别名表,仅用于快捷匹配和同类专题消歧。 // 专题完整名称由站点专题接口提供,这里只保留少量关键词。 const COLUMN_ALIASES = { '自贸': '600062', '自由贸易': '600062', '自贸区': '600062', '自由贸易试验区': '600062', '内蒙古自贸试验区': '600062', '铸牢': '600018', '共同体': '600018', '民族共同体': '600018', '中华民族共同体': '600018', '数字经济': '600054', '知识产权': '600050', '党内法规': '600047', '科技创新': '600046', '党纪': '600048', '廉洁文化': '600051', '千万工程': '600015', '乡村振兴': '600015', '法治思想': '600016', '营商环境': '600019', '应急管理': '600020', '国家安全观': '600022', '黄河流域': '600023', '中国式现代化': '600024', '人民民主': '600026', '统一大市场': '600027', '数字产业化': '600028', '产业数字化': '600028', '共同富裕': '600029', '党的建设': '600030', '人才工作': '600003', '干部队伍': '600031', '自我革命': '600032', '金融发展': '600033', '共青团': '600011', '合规经营': '600035', '国企': '600035', '碳中和': '600007', '碳达峰': '600007', '十四五': '600005', '2022两会': '600001', '二十大': '600000' }; const FALLBACK_TITLES = { '600018': '铸牢中华民族共同体意识', '600062': '中国(内蒙古)自由贸易试验区建设' }; function defaultState() { return { speed: 1.5, running: false, columnCode: '', columnName: '', courseIndex: 0, courseId: '', pendingScan: null, catalog: {}, completed: {}, message: '未开始', updatedAt: Date.now() }; } function loadState() { const base = defaultState(); try { const raw = W.localStorage.getItem(STORE_KEY); if (raw) { const parsed = JSON.parse(raw) || {}; const savedSpeed = Number(parsed.speed) || base.speed; return { ...base, ...parsed, speed: Math.min(2, Math.max(1, savedSpeed)), catalog: parsed.catalog && typeof parsed.catalog === 'object' ? parsed.catalog : {}, completed: parsed.completed && typeof parsed.completed === 'object' ? parsed.completed : {}, pendingScan: parsed.pendingScan || null }; } const oldRaw = W.localStorage.getItem(OLD_STORE_KEY); if (oldRaw) { const old = JSON.parse(oldRaw) || {}; const oldSpeed = Number(old.speed) || base.speed; return { ...base, speed: Math.min(2, Math.max(1, oldSpeed)), completed: old.completed && typeof old.completed === 'object' ? old.completed : {}, message: '已迁移旧版记录' }; } } catch (error) { console.warn('[网院刷课] 读取缓存失败', error); } return base; } const state = loadState(); let monitorTimer = null; let panel = null; let statusNode = null; let currentNode = null; let inputNode = null; let speedNode = null; let collapsed = false; let endedSince = 0; let chapterRetryAt = 0; let scanWatchTimer = null; let lastNoVideoNotice = 0; function saveState() { state.updatedAt = Date.now(); try { W.localStorage.setItem(STORE_KEY, JSON.stringify(state)); } catch (error) { console.warn('[网院刷课] 保存缓存失败', error); } } function normalizeText(value) { return String(value || '') .toLowerCase() .replace(/[\s()()【】[\]“”‘’'"·—\-_/\\,,。::;;]/g, ''); } function longestCommonSubsequence(a, b) { const row = new Array(b.length + 1).fill(0); for (let i = 1; i <= a.length; i += 1) { let previous = 0; for (let j = 1; j <= b.length; j += 1) { const old = row[j]; row[j] = a[i - 1] === b[j - 1] ? previous + 1 : Math.max(row[j], row[j - 1]); previous = old; } } return row[b.length]; } function columnScore(column, query) { const q = normalizeText(query); const name = normalizeText(column.n); const aliases = (column.a || []).map(normalizeText); if (!q) return 0; if (q === column.c || q === name || aliases.includes(q)) return 100; if (name.includes(q) || aliases.some((alias) => alias.includes(q))) return 84; if (q.includes(name) || aliases.some((alias) => q.includes(alias))) return 76; const overlap = longestCommonSubsequence(q, name); return Math.round((overlap / Math.max(q.length, 1)) * 68); } function resolveColumn(query, columns) { const raw = String(query || '').trim(); const numeric = raw.match(/\d{5,}/); if (numeric) { const byCode = columns.find((column) => String(column.c) === numeric[0]); if (byCode) return byCode; return { c: numeric[0], n: FALLBACK_TITLES[numeric[0]] || numeric[0], a: [] }; } const aliasCode = COLUMN_ALIASES[normalizeText(raw)]; if (aliasCode) { const byAlias = columns.find((column) => String(column.c) === aliasCode); if (byAlias) return byAlias; return { c: aliasCode, n: FALLBACK_TITLES[aliasCode] || aliasCode, a: [] }; } let best = null; let bestScore = 0; columns.forEach((column) => { const score = columnScore(column, raw); if (score > bestScore) { best = column; bestScore = score; } }); return bestScore >= 36 ? best : null; } function mergeColumns(remoteColumns) { const map = new Map(); (remoteColumns || []).forEach((column) => { const code = String(column.code || column.c || column.id || ''); const name = column.name || column.n || ''; if (!code) return; map.set(code, { c: code, n: name || FALLBACK_TITLES[code] || code, a: column.a || [] }); }); return [...map.values()]; } async function loadRemoteColumns() { try { const response = await W.fetch(API_COLUMNS, { method: 'POST', credentials: 'include', headers: BASE_HEADERS, body: JSON.stringify({ page: 1, limit: 100 }) }); const data = await response.json(); return Array.isArray(data.data) ? data.data : []; } catch (error) { console.warn('[网院刷课] 读取专题列表失败,将使用内置名称', error); return []; } } function parseBody(body) { if (!body) return {}; if (typeof body === 'string') { try { return JSON.parse(body); } catch (error) { return {}; } } if (typeof FormData !== 'undefined' && body instanceof FormData) { return Object.fromEntries(body.entries()); } return body || {}; } function isCourseListUrl(url) { return String(url || '').includes(API_COURSES); } function pickCourseArray(root) { const queue = [{ value: root, depth: 0 }]; const seen = new Set(); while (queue.length) { const { value, depth } = queue.shift(); if (!value || depth > 6 || seen.has(value)) continue; if (typeof value === 'object') seen.add(value); if (Array.isArray(value)) { if (!value.length) return value; const objectItems = value.filter((item) => item && typeof item === 'object'); if (objectItems.length === value.length) { const looksLikeCourses = objectItems.some((item) => item.id || item.courseId || item.name || item.title); if (looksLikeCourses) return value; } value.slice(0, 5).forEach((item) => queue.push({ value: item, depth: depth + 1 })); continue; } ['data', 'list', 'rows', 'records', 'items', 'courseList', 'courses'].forEach((key) => { if (value[key]) queue.push({ value: value[key], depth: depth + 1 }); }); } return []; } function normalizeCourses(items) { const map = new Map(); (items || []).forEach((item) => { if (!item || typeof item !== 'object') return; const course = item.course && typeof item.course === 'object' ? item.course : {}; const id = String(item.id || item.courseId || course.id || '').trim(); if (!id) return; const title = item.name || item.title || item.courseName || course.name || course.title || ''; const credit = item.credit || item.courseCredit || course.credit || ''; map.set(id, { id, title: String(title || '未命名课程'), credit: String(credit || ''), isExam: Boolean(item.isExam || course.isExam), isSignIn: Boolean(item.isSignIn || course.isSignIn) }); }); return [...map.values()]; } function rememberCatalog(code, name, courses, source) { const cleanCode = String(code || ''); if (!cleanCode || !courses.length) return; const old = state.catalog[cleanCode] || {}; state.catalog[cleanCode] = { code: cleanCode, name: name || old.name || FALLBACK_TITLES[cleanCode] || cleanCode, courses, source: source || 'scan', scannedAt: Date.now() }; saveState(); updatePanel(); } function catalogFor(code) { const catalog = state.catalog[String(code || '')]; return catalog && Array.isArray(catalog.courses) && catalog.courses.length ? catalog : null; } function handleCourseResponse(url, body, text) { if (!isCourseListUrl(url) || !text) return; try { const request = parseBody(body); const code = String(request.categoryCode || request.columnCode || request.code || ''); const parsed = JSON.parse(text); const courses = normalizeCourses(pickCourseArray(parsed)); if (!code || !courses.length) return; rememberCatalog(code, state.pendingScan && String(state.pendingScan.code) === code ? state.pendingScan.name : '', courses, 'scan'); if (state.pendingScan && String(state.pendingScan.code) === code) { state.pendingScan = null; state.running = true; state.columnCode = code; state.columnName = catalogFor(code) ? catalogFor(code).name : code; state.courseIndex = 0; saveState(); setStatus(`扫描完成,共 ${courses.length} 门课程,正在开始`, 'success'); window.setTimeout(() => startColumn(code, state.columnName), 700); } } catch (error) { console.warn('[网院刷课] 解析课程列表失败', error); } } function installNetworkCapture() { try { const OriginalXHR = W.XMLHttpRequest; if (OriginalXHR && !OriginalXHR.__nmgAutoPatched) { const originalOpen = OriginalXHR.prototype.open; const originalSend = OriginalXHR.prototype.send; OriginalXHR.prototype.open = function (method, url) { this.__nmgAutoUrl = String(url || ''); return originalOpen.apply(this, arguments); }; OriginalXHR.prototype.send = function (body) { this.__nmgAutoBody = body; if (isCourseListUrl(this.__nmgAutoUrl)) { this.addEventListener('load', () => { try { if (!this.responseType || this.responseType === 'text' || this.responseType === 'json') { const text = this.responseType === 'json' ? JSON.stringify(this.response) : this.responseText; handleCourseResponse(this.__nmgAutoUrl, this.__nmgAutoBody, text); } } catch (error) { console.warn('[网院刷课] 读取课程接口失败', error); } }); } return originalSend.apply(this, arguments); }; OriginalXHR.__nmgAutoPatched = true; } } catch (error) { console.warn('[网院刷课] 无法监听页面请求', error); } try { const originalFetch = W.fetch; if (originalFetch && !originalFetch.__nmgAutoPatched) { const wrappedFetch = function (input, init) { const url = typeof input === 'string' ? input : input && input.url ? input.url : ''; return originalFetch.apply(this, arguments).then((response) => { if (isCourseListUrl(url)) { const directBody = init && init.body ? init.body : ''; const bodyPromise = directBody ? Promise.resolve(directBody) : input && typeof input.clone === 'function' ? input.clone().text() : Promise.resolve(''); Promise.all([response.clone().text(), bodyPromise]).then(([text, body]) => { handleCourseResponse(url, body, text); }).catch(() => {}); } return response; }); }; wrappedFetch.__nmgAutoPatched = true; W.fetch = wrappedFetch; } } catch (error) { console.warn('[网院刷课] 无法监听页面网络请求', error); } } function setStatus(message, type) { state.message = message || ''; if (statusNode) { statusNode.textContent = state.message; statusNode.dataset.type = type || ''; } updateCurrent(); } function updateCurrent() { if (!currentNode) return; const catalog = catalogFor(state.columnCode); if (!state.running || !catalog) { currentNode.textContent = state.pendingScan ? `正在扫描:${state.pendingScan.name || state.pendingScan.code}` : '等待输入专题名称'; return; } const course = catalog.courses[state.courseIndex] || null; if (!course) { currentNode.textContent = `专栏:${state.columnName || state.columnCode} · 课程已到末尾`; return; } const video = document.querySelector('video'); const percent = isCourseCompletedInDom() ? ' · 已学完' : video && Number.isFinite(video.duration) && video.duration > 0 ? ` · ${Math.min(100, Math.max(0, video.currentTime / video.duration * 100)).toFixed(1)}%` : ''; currentNode.textContent = `专栏:${state.columnName || state.columnCode} · 第 ${state.courseIndex + 1}/${catalog.courses.length} 门${percent}\n${course.title || course.id}`; } function updatePanel() { updateCurrent(); if (speedNode) speedNode.value = String(state.speed); if (statusNode && state.message) { statusNode.textContent = state.message; } } function detailUrl(code, course) { const params = new URLSearchParams({ classId: '', isExam: course.isExam ? '1' : '', credit: course.credit || '', score: '0', semester: '', categoryCode: String(code) }); return `/detail/${encodeURIComponent(course.id)}?${params.toString()}`; } function navigateToCourse(index) { const catalog = catalogFor(state.columnCode); if (!catalog) { scanColumn({ c: state.columnCode, n: state.columnName }); return; } const course = catalog.courses[index]; if (!course) { state.running = false; state.courseId = ''; saveState(); setStatus('这个专题的课程已全部处理完', 'success'); return; } state.courseIndex = index; state.courseId = course.id; state.running = true; saveState(); setStatus(`正在打开第 ${index + 1}/${catalog.courses.length} 门课程`); W.location.href = detailUrl(state.columnCode, course); } function startColumn(code, name) { const cleanCode = String(code || ''); const catalog = catalogFor(cleanCode); if (!catalog) { scanColumn({ c: cleanCode, n: name || FALLBACK_TITLES[cleanCode] || cleanCode }); return; } stopMonitor(false); state.columnCode = cleanCode; state.columnName = name || catalog.name || cleanCode; state.pendingScan = null; state.running = true; let startIndex = 0; if (catalog.courses.some((course) => !state.completed[course.id])) { startIndex = catalog.courses.findIndex((course) => !state.completed[course.id]); } else { state.running = false; state.courseIndex = 0; state.courseId = ''; saveState(); setStatus('缓存记录显示这个专题已全部完成', 'success'); return; } state.courseIndex = startIndex; saveState(); navigateToCourse(startIndex); } function scanColumn(column) { const code = String(column && column.c || column && column.code || ''); const name = column && (column.n || column.name) || FALLBACK_TITLES[code] || code; if (!code) return; stopMonitor(false); state.running = false; state.columnCode = code; state.columnName = name; state.courseIndex = 0; state.courseId = ''; state.pendingScan = { code, name, startedAt: Date.now() }; saveState(); setStatus(`正在扫描“${name}”的课程,请稍等`); W.location.href = `/column/${encodeURIComponent(code)}?title=${encodeURIComponent(name)}`; } async function startFromInput() { const query = inputNode ? inputNode.value.trim() : ''; if (!query) { setStatus('请先输入专题名称', 'error'); inputNode && inputNode.focus(); return; } setStatus('正在查找专题...'); let column = resolveColumn(query, mergeColumns([])); if (!column) { const remote = await loadRemoteColumns(); column = resolveColumn(query, mergeColumns(remote)); } if (!column) { setStatus(`没有找到“${query}”,请把专题名称写完整一些`, 'error'); return; } const cached = catalogFor(column.c); if (cached) { startColumn(column.c, column.n); return; } scanColumn(column); } function parseChapterProgress() { const nodes = [...document.querySelectorAll('[class*="chapter--"]')]; const node = nodes.find((element) => /课程章节/.test(element.innerText || '')); const text = node ? node.innerText : document.body ? document.body.innerText : ''; const match = String(text || '').match(/课程章节\s*[((]\s*(\d+)\s*\/\s*(\d+)\s*[))]/); if (!match) return null; return { done: Number(match[1]), total: Number(match[2]) }; } function isCourseCompletedInDom() { const chapter = parseChapterProgress(); if (chapter && chapter.total > 0 && chapter.done >= chapter.total) { const items = visibleChapterItems(); if (!items.length) return true; return items.every((element) => /已学完|100%/.test(element.innerText || '')); } const items = visibleChapterItems(); return items.length > 0 && items.every((element) => { const text = element.innerText || ''; return /已学完/.test(text) && /100%/.test(text); }); } function visibleChapterItems() { return [...document.querySelectorAll('[class*="player_side_item--"]')].filter((element) => { const rect = element.getBoundingClientRect(); return rect.width > 80 && rect.height > 30; }); } function advanceChapter() { const items = visibleChapterItems(); const activeIndex = items.findIndex((element) => /active/.test(element.className) || /学习中|播放中/.test(element.innerText || '')); const next = items[activeIndex + 1]; if (!next) return false; try { try { if (W.MouseEvent) { next.dispatchEvent(new W.MouseEvent('mousedown', { bubbles: true, cancelable: true, view: W })); next.dispatchEvent(new W.MouseEvent('mouseup', { bubbles: true, cancelable: true, view: W })); next.dispatchEvent(new W.MouseEvent('click', { bubbles: true, cancelable: true, view: W })); } } catch (eventError) { console.warn('[网院刷课] 章节事件模拟失败,尝试直接点击', eventError); } if (typeof next.click === 'function') next.click(); setStatus('正在进入下一章节'); return true; } catch (error) { console.warn('[网院刷课] 切换章节失败', error); return false; } } function markCurrentComplete(fromDomCheck) { const catalog = catalogFor(state.columnCode); if (!catalog) return; const course = catalog.courses[state.courseIndex]; if (!course) return; const code = state.columnCode; state.completed[course.id] = Date.now(); state.courseIndex += 1; state.courseId = ''; saveState(); stopMonitor(false); endedSince = 0; if (state.courseIndex >= catalog.courses.length) { state.running = false; saveState(); setStatus(`“${state.columnName}”全部课程已播放完成`, 'success'); return; } setStatus(`第 ${state.courseIndex} 门完成,准备下一门`, 'success'); const nextIndex = state.courseIndex; window.setTimeout(() => { if (state.running && state.columnCode === code && state.courseIndex === nextIndex) { navigateToCourse(nextIndex); } }, fromDomCheck ? 1200 : 4000); } function stopMonitor(pauseVideo) { if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; } endedSince = 0; if (pauseVideo) { const video = document.querySelector('video'); if (video) video.pause(); } } function handleNoVideo(course) { const bodyText = document.body ? document.body.innerText : ''; if (/登录|login/i.test(location.pathname) || /请先登录|重新登录/.test(bodyText)) { state.running = false; saveState(); setStatus('登录状态失效,请重新登录', 'error'); return; } if (/考试|答题|签到/.test(bodyText)) { const code = state.columnCode; state.completed[course.id] = Date.now(); state.courseIndex += 1; saveState(); stopMonitor(false); setStatus('此课程需要考试或签到,已跳过', 'error'); const nextIndex = state.courseIndex; window.setTimeout(() => { if (state.running && state.columnCode === code && state.courseIndex === nextIndex) { navigateToCourse(nextIndex); } }, 1500); return; } if (Date.now() - lastNoVideoNotice > 10000) { lastNoVideoNotice = Date.now(); setStatus('播放器还未加载,继续等待', 'error'); } } function monitorCourse() { if (monitorTimer || !state.running) return; const catalog = catalogFor(state.columnCode); const course = catalog && catalog.courses[state.courseIndex]; if (!course) return; let noVideoSince = Date.now(); monitorTimer = window.setInterval(() => { if (!state.running) { stopMonitor(false); return; } if (isCourseCompletedInDom()) { setStatus('检测到本门课程已学完,准备下一门', 'success'); markCurrentComplete(true); return; } const video = document.querySelector('video'); if (!video) { if (Date.now() - noVideoSince > 35000) handleNoVideo(course); return; } noVideoSince = Date.now(); try { video.muted = true; if (Math.abs(Number(video.playbackRate) - Number(state.speed)) > 0.05) { video.playbackRate = Number(state.speed); } if (video.paused && !video.ended) { const playPromise = video.play(); if (playPromise && typeof playPromise.catch === 'function') playPromise.catch(() => {}); } } catch (error) { console.warn('[网院刷课] 播放器控制失败', error); } updateCurrent(); const duration = Number(video.duration); const atEnd = video.ended || (Number.isFinite(duration) && duration > 10 && video.currentTime >= duration - 1.2); if (!atEnd) { endedSince = 0; return; } if (!endedSince) endedSince = Date.now(); const chapter = parseChapterProgress(); const chapterUnfinished = chapter && chapter.total > 0 && chapter.done < chapter.total; const items = visibleChapterItems(); const activeIndex = items.findIndex((element) => /active/.test(element.className) || /学习中|播放中/.test(element.innerText || '')); const lastChapter = items.length <= 1 || activeIndex < 0 || activeIndex >= items.length - 1; if (chapterUnfinished && !lastChapter && Date.now() - endedSince > 3200) { if (Date.now() - chapterRetryAt > 8000) { chapterRetryAt = Date.now(); advanceChapter(); } endedSince = Date.now() - 3000; return; } if ((!chapterUnfinished || lastChapter) && Date.now() - endedSince > 3200) { markCurrentComplete(); } }, 1000); } function buildPanel() { if (!document.body || panel) return; const oldPanel = document.getElementById('nmgAutoPanel'); if (oldPanel) oldPanel.remove(); const style = document.createElement('style'); style.textContent = ` #nmgAutoV3 { position: fixed; right: 16px; bottom: 16px; z-index: 2147483647; width: 372px; max-width: calc(100vw - 24px); box-sizing: border-box; border: 1px solid #d9d9d9; border-radius: 8px; background: #fff; color: #1f2329; box-shadow: 0 8px 28px rgba(0,0,0,.18); font: 13px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif; } #nmgAutoV3 * { box-sizing: border-box; } #nmgAutoV3 .nmg-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid #eee; font-weight: 700; } #nmgAutoV3 .nmg-collapse { border: 0; background: transparent; color: #666; cursor: pointer; font-size: 13px; } #nmgAutoV3 .nmg-body { padding: 10px 12px 12px; } #nmgAutoV3 .nmg-status { min-height: 21px; margin-bottom: 8px; color: #555; } #nmgAutoV3 .nmg-status[data-type="success"] { color: #0d7a3f; } #nmgAutoV3 .nmg-status[data-type="error"] { color: #c23b22; } #nmgAutoV3 .nmg-row { display: flex; gap: 8px; } #nmgAutoV3 input, #nmgAutoV3 select { min-width: 0; height: 34px; border: 1px solid #cfd6dd; border-radius: 6px; background: #fff; color: #1f2329; padding: 0 9px; outline: none; } #nmgAutoV3 input:focus, #nmgAutoV3 select:focus { border-color: #1677ff; box-shadow: 0 0 0 2px rgba(22,119,255,.12); } #nmgAutoV3 input { flex: 1; } #nmgAutoV3 button.nmg-primary { height: 34px; border: 0; border-radius: 6px; background: #1677ff; color: #fff; padding: 0 12px; cursor: pointer; white-space: nowrap; } #nmgAutoV3 button.nmg-secondary { height: 30px; border: 1px solid #cfd6dd; border-radius: 6px; background: #fff; color: #333; padding: 0 9px; cursor: pointer; } #nmgAutoV3 .nmg-current { min-height: 42px; margin-top: 9px; padding: 8px 9px; border-radius: 6px; background: #f6f8fa; color: #444; white-space: pre-line; } #nmgAutoV3 .nmg-controls { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 9px; } #nmgAutoV3 .nmg-speed { display: flex; align-items: center; gap: 6px; color: #555; } #nmgAutoV3 .nmg-speed select { width: 74px; } #nmgAutoV3 .nmg-hint { margin-top: 8px; color: #8a8f99; font-size: 12px; } #nmgAutoV3.nmg-collapsed .nmg-body { display: none; } `; document.head.appendChild(style); panel = document.createElement('div'); panel.id = 'nmgAutoV3'; panel.innerHTML = `
内蒙古网院 · 课程进度助手
未开始
等待输入专题名称
保持本标签页处于前台,脚本会按章节和课程顺序自动播放。
`; document.body.appendChild(panel); statusNode = panel.querySelector('.nmg-status'); currentNode = panel.querySelector('.nmg-current'); inputNode = panel.querySelector('input'); speedNode = panel.querySelector('select'); panel.querySelector('.nmg-primary').addEventListener('click', startFromInput); panel.querySelector('.nmg-collapse').addEventListener('click', () => { collapsed = !collapsed; panel.classList.toggle('nmg-collapsed', collapsed); panel.querySelector('.nmg-collapse').textContent = collapsed ? '展开' : '收起'; }); inputNode.addEventListener('keydown', (event) => { if (event.key === 'Enter') startFromInput(); }); speedNode.addEventListener('change', () => { state.speed = Math.min(2, Math.max(1, Number(speedNode.value) || 1.5)); saveState(); const video = document.querySelector('video'); if (video) video.playbackRate = state.speed; setStatus(`倍速已切换为 ${state.speed}x`); }); panel.querySelector('.nmg-stop').addEventListener('click', () => { state.running = false; state.pendingScan = null; saveState(); stopMonitor(true); setStatus('已停止'); }); panel.querySelector('.nmg-rescan').addEventListener('click', () => { if (!state.columnCode) { setStatus('请先输入并开始一个专题', 'error'); return; } delete state.catalog[state.columnCode]; state.running = false; state.pendingScan = null; saveState(); const column = { c: state.columnCode, n: state.columnName || state.columnCode }; scanColumn(column); }); panel.querySelector('.nmg-clear').addEventListener('click', () => { state.catalog = {}; state.completed = {}; state.columnCode = ''; state.columnName = ''; state.courseIndex = 0; state.courseId = ''; state.pendingScan = null; state.running = false; saveState(); stopMonitor(true); setStatus('缓存已清空'); }); updatePanel(); } function resumeIfNeeded() { if (!state.columnCode) return; const pending = state.pendingScan; const scanningPath = location.pathname.startsWith('/column/') || location.pathname.startsWith('/columnDetail'); if (pending && scanningPath && String(pending.code) === String(state.columnCode)) { setStatus(`正在扫描“${pending.name}”的课程,请稍等`); if (scanWatchTimer) clearTimeout(scanWatchTimer); scanWatchTimer = window.setTimeout(() => { if (!state.pendingScan) return; setStatus('没有扫描到课程,请点“重新扫描”重试', 'error'); }, 15000); return; } if (!state.running) return; const catalog = catalogFor(state.columnCode); if (!catalog) { scanColumn({ c: state.columnCode, n: state.columnName }); return; } const path = location.pathname; const current = catalog.courses[state.courseIndex]; if (path.startsWith('/detail/') && current && location.pathname.includes(encodeURIComponent(current.id))) { monitorCourse(); setStatus(`继续播放:${current.title || current.id}`); return; } navigateToCourse(state.courseIndex); } function init() { installNetworkCapture(); const start = () => { buildPanel(); resumeIfNeeded(); }; if (document.body) start(); else document.addEventListener('DOMContentLoaded', start, { once: true }); } init(); })();