// ==UserScript== // @name 国培学习自动助手 // @namespace https://example.local/ablesky-auto-learn // @version 0.4.1 // @license CC-BY-NC-SA // @description 只学习指定“我的学习”栏目,在课程列表页自动进入未完成课程,多分集课程会逐集学习,全部完成后返回列表继续下一个栏目。 // @match https://study.enaea.edu.cn/* // ==/UserScript== (function () { 'use strict'; /** * 说明: * 1. 脚本只会点击页面中符合课程列表结构的学习链接。 * 2. 脚本仅在 https://study.enaea.edu.cn/* 下运行。 * 交流 QQ 群:882909961 */ const CONFIG = { // 只处理“我的学习”左侧导航中的这 6 个栏目,其他栏目如案例撰写、在线考试、研修活动等都会跳过。 targetStudyTitles: [ '心有大我、至诚报国的理想信念', '言为士则、行为世范的道德情操', '启智润心、因材施教的育人智慧', '勤学笃行、求是创新的躬耕态度', '乐教爱生、甘于奉献的仁爱之心', '胸怀天下、以文化人的弘道追求', ], // 左侧“我的学习”导航选择器。 sidebarSelector: '#sideNav, .g-sidebar, .sidebar_left', sidebarLinkSelector: '#sideNav a[title], .g-sidebar a[title], .sidebar_left a[title]', // 课程列表页选择器,根据你提供的 HTML 编写。 courseLinkSelector: 'a.golearn.saveStuCourse, a.golearn[data-progress], a.saveStuCourse[data-progress]', titleSelector: '.course-title', progressTextSelector: '.progressvalue', // 学习页分集选择器,根据你提供的课程分集 HTML 编写。 coursewareItemSelector: '.cvtb-MCK-course-content', coursewareProgressSelector: '.cvtb-MCK-CsCt-studyProgress, [id^="J_studyProgress_"]', coursewareTitleSelector: '.cvtb-MCK-CsCt-title', // 学习页地址识别,例如 /viewerforccvideo.do?courseId=... learningUrlPattern: /viewerforccvideo\.do/i, // 延迟参数:给动态页面一点加载时间,避免太快点击。 startDelayMs: 1800, clickDelayMs: 1200, scanIntervalMs: 2000, maxListWaitMs: 30000, // 学习页行为。 autoMuteVideo: true, autoPlayVideo: true, finishThreshold: 0.98, afterFinishDelayMs: 3500, completedSkipMs: 2 * 60 * 1000, coursewareClickCooldownMs: 5000, autoConfirmDialogIntervalMs: 5000, // 临时存储前缀。 storagePrefix: 'ablesky_auto_learn_helper_', }; const state = { startedAt: Date.now(), timer: null, lastCoursewareClickAt: 0, lastCoursewareClickId: '', returning: false, navigatingSection: false, lastDialogConfirmAt: 0, }; function storageKey(name) { return CONFIG.storagePrefix + name; } function log(...args) { console.log('[国培学习自动助手]', ...args); setStatus(args.map(String).join(' ')); } function setStatus(text) { let panel = document.getElementById('ablesky-auto-learn-status'); if (!panel) { panel = document.createElement('div'); panel.id = 'ablesky-auto-learn-status'; panel.style.cssText = [ 'position:fixed', 'right:12px', 'bottom:12px', 'z-index:2147483647', 'max-width:360px', 'padding:8px 10px', 'border-radius:6px', 'background:rgba(0,0,0,.72)', 'color:#fff', 'font-size:12px', 'line-height:1.45', 'font-family:Arial,"Microsoft YaHei",sans-serif', 'box-shadow:0 2px 10px rgba(0,0,0,.25)', ].join(';'); document.documentElement.appendChild(panel); } panel.textContent = text; } function parsePercent(value) { if (value == null) return NaN; const match = String(value).match(/(\d+(?:\.\d+)?)\s*%?/); return match ? Number(match[1]) : NaN; } function normalizeTitle(value) { return String(value || '').replace(/\s+/g, '').trim(); } function isTargetStudyTitle(title) { const normalized = normalizeTitle(title); return CONFIG.targetStudyTitles.some((target) => normalizeTitle(target) === normalized); } function getTargetSidebarLinks() { return Array.from(document.querySelectorAll(CONFIG.sidebarLinkSelector)) .map((link) => ({ link, title: (link.getAttribute('title') || link.textContent || '').trim(), href: link.href || link.getAttribute('href') || '', })) .filter((item) => item.title && isTargetStudyTitle(item.title)); } function getCurrentSidebarTarget() { const links = getTargetSidebarLinks(); if (!links.length) return null; const active = links.find((item) => item.link.classList.contains('active')); if (active) return active; const currentUrl = location.href; return links.find((item) => item.href && currentUrl.indexOf(item.href) >= 0) || null; } function getCompletedSectionMap() { try { return JSON.parse(sessionStorage.getItem(storageKey('completedSections')) || '{}') || {}; } catch (error) { return {}; } } function rememberCompletedSection(title) { if (!title) return; const completed = getCompletedSectionMap(); completed[normalizeTitle(title)] = Date.now(); sessionStorage.setItem(storageKey('completedSections'), JSON.stringify(completed)); } function isCompletedSection(title) { if (!title) return false; const completed = getCompletedSectionMap(); return Boolean(completed[normalizeTitle(title)]); } function navigateToSidebarTarget(target) { if (!target || !target.href || state.navigatingSection) return false; state.navigatingSection = true; clearInterval(state.timer); log(`切换到左侧“我的学习”栏目:${target.title}`); setTimeout(() => { location.href = target.href; }, CONFIG.clickDelayMs); return true; } function navigateToNextStudySection(currentTitle) { const links = getTargetSidebarLinks(); if (!links.length) return false; const normalizedCurrent = normalizeTitle(currentTitle); const currentIndex = links.findIndex((item) => normalizeTitle(item.title) === normalizedCurrent); const orderedLinks = currentIndex >= 0 ? links.slice(currentIndex + 1).concat(links.slice(0, currentIndex + 1)) : links; const next = orderedLinks.find((item) => !isCompletedSection(item.title)); if (next) return navigateToSidebarTarget(next); log('指定的 6 个“我的学习”栏目都已检查完成。'); return false; } function ensureAllowedStudySection() { const sidebarLinks = getTargetSidebarLinks(); if (!sidebarLinks.length) { // 学习播放页通常没有左侧导航,这里不拦截。 return true; } const currentTarget = getCurrentSidebarTarget(); if (currentTarget && isTargetStudyTitle(currentTarget.title)) { return true; } const next = sidebarLinks.find((item) => !isCompletedSection(item.title)) || sidebarLinks[0]; log('当前不在指定学习栏目内,将自动进入第一个需要学习的栏目。'); navigateToSidebarTarget(next); return false; } function getCourseProgress(link) { const fromData = parsePercent(link.getAttribute('data-progress')); if (!Number.isNaN(fromData)) return fromData; const row = link.closest('tr'); const progressNode = row && row.querySelector(CONFIG.progressTextSelector); const fromText = parsePercent(progressNode && progressNode.textContent); return Number.isNaN(fromText) ? 0 : fromText; } function getCourseTitle(link) { const row = link.closest('tr'); const titleNode = row && row.querySelector(CONFIG.titleSelector); return ( (titleNode && (titleNode.getAttribute('title') || titleNode.textContent)) || link.getAttribute('title') || link.getAttribute('courseid-id') || link.getAttribute('data-id') || '未命名课程' ).trim(); } function getCourseIdentity(link) { return ( link.getAttribute('courseid-id') || link.getAttribute('data-id') || link.getAttribute('data-syllabusid') || getCourseTitle(link) ); } function getRecentlyCompletedMap() { try { return JSON.parse(sessionStorage.getItem(storageKey('recentlyCompleted')) || '{}') || {}; } catch (error) { return {}; } } function rememberRecentlyCompleted(identity) { if (!identity) return; const completed = getRecentlyCompletedMap(); completed[identity] = Date.now(); sessionStorage.setItem(storageKey('recentlyCompleted'), JSON.stringify(completed)); } function isRecentlyCompleted(identity) { if (!identity) return false; const completed = getRecentlyCompletedMap(); const completedAt = completed[identity]; return completedAt && Date.now() - completedAt < CONFIG.completedSkipMs; } function rememberListPage() { if (!CONFIG.learningUrlPattern.test(location.href)) { sessionStorage.setItem(storageKey('lastListUrl'), location.href); } } function clickElement(element) { element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true, view: window })); element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window })); element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window })); element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); } function handleStudyConfirmDialog() { const dialog = document.querySelector('.dialog-box'); if (!dialog) return false; const now = Date.now(); if (now - state.lastDialogConfirmAt < CONFIG.autoConfirmDialogIntervalMs) return false; const button = document.querySelector('.dialog-button-container button') || document.querySelector('.dialog-button-container a') || document.querySelector('.dialog-button-container input') || document.querySelector('.dialog-button-container span') || document.querySelector('.dialog-button-container')?.children?.[0]; if (!button) return false; state.lastDialogConfirmAt = now; log('检测到学习确认弹窗,已自动点击确认继续学习。'); clickElement(button); return true; } function findIncompleteCourses() { return Array.from(document.querySelectorAll(CONFIG.courseLinkSelector)) .map((link) => ({ link, progress: getCourseProgress(link), title: getCourseTitle(link), courseId: link.getAttribute('courseid-id') || '', dataId: link.getAttribute('data-id') || '', syllabusId: link.getAttribute('data-syllabusid') || '', identity: getCourseIdentity(link), })) .filter((item) => item.progress < 100) .filter((item) => !isRecentlyCompleted(item.identity)) .filter((item) => item.link.offsetParent !== null || item.link.getClientRects().length > 0); } function handleCourseListPage() { if (!ensureAllowedStudySection()) return; rememberListPage(); const incomplete = findIncompleteCourses(); if (!incomplete.length) { const courseLinks = document.querySelectorAll(CONFIG.courseLinkSelector); if (!courseLinks.length && Date.now() - state.startedAt < CONFIG.maxListWaitMs) { log('正在等待课程列表加载...'); return; } clearInterval(state.timer); const currentTarget = getCurrentSidebarTarget(); if (currentTarget) { rememberCompletedSection(currentTarget.title); log(`栏目“${currentTarget.title}”没有找到未完成课程,准备检查下一个指定栏目。`); navigateToNextStudySection(currentTarget.title); return; } log('没有找到未完成课程。当前可见课程可能已全部完成,或这里不是课程列表页。'); return; } clearInterval(state.timer); const next = incomplete[0]; sessionStorage.setItem(storageKey('currentCourseTitle'), next.title); sessionStorage.setItem(storageKey('currentCourseId'), next.identity || ''); log(`准备进入未完成课程:${next.title}(${next.progress}%)`); setTimeout(() => { clickElement(next.link); }, CONFIG.clickDelayMs); } function tryAutoPlayVideo() { const videos = Array.from(document.querySelectorAll('video')); if (!videos.length) { log('已进入学习页,正在等待视频播放器加载...'); return false; } for (const video of videos) { if (CONFIG.autoMuteVideo) video.muted = true; if (CONFIG.autoPlayVideo && video.paused) { const playPromise = video.play(); if (playPromise && typeof playPromise.catch === 'function') { playPromise.catch((error) => { log('浏览器阻止了自动播放,请手动点击一次视频开始播放。', error && error.message ? error.message : ''); }); } } bindVideoFinish(video); } log(`已找到视频播放器(${videos.length} 个),已尝试自动播放。`); return true; } function bindVideoFinish(video) { if (video.dataset.ableskyAutoLearnBound === '1') return; video.dataset.ableskyAutoLearnBound = '1'; const onMaybeFinished = () => { const duration = Number(video.duration || 0); const current = Number(video.currentTime || 0); if (duration > 0 && (video.ended || current / duration >= CONFIG.finishThreshold)) { log('视频播放接近完成,等待平台进度更新到 100%...'); video.removeEventListener('timeupdate', onMaybeFinished); video.removeEventListener('ended', onMaybeFinished); setTimeout(() => { handleLearningProgressState(); }, CONFIG.afterFinishDelayMs); } }; video.addEventListener('timeupdate', onMaybeFinished); video.addEventListener('ended', onMaybeFinished); } function getLearningProgressItems() { const progressNodes = Array.from(document.querySelectorAll(CONFIG.coursewareProgressSelector)); return progressNodes.map((node) => ({ node, progress: parsePercent(node.textContent), item: node.closest(CONFIG.coursewareItemSelector), id: node.closest(CONFIG.coursewareItemSelector) && node.closest(CONFIG.coursewareItemSelector).getAttribute('data-id'), title: getCoursewareTitle(node.closest(CONFIG.coursewareItemSelector)), })); } function getCoursewareTitle(item) { if (!item) return '未知分集'; const titleNode = item.querySelector(CONFIG.coursewareTitleSelector); return ( item.getAttribute('title') || (titleNode && titleNode.textContent) || item.getAttribute('data-id') || '未知分集' ).trim(); } function isLearningProgressComplete() { const progressItems = getLearningProgressItems(); if (!progressItems.length) return false; return progressItems.every((item) => !Number.isNaN(item.progress) && item.progress >= 100); } function getCurrentCourseware(progressItems) { return progressItems.find((item) => item.item && item.item.classList.contains('current')) || null; } function findFirstIncompleteCourseware(progressItems) { return progressItems.find((item) => Number.isNaN(item.progress) || item.progress < 100) || null; } function clickCoursewareItem(target) { if (!target || !target.item) return false; const now = Date.now(); const targetId = target.id || target.title || ''; if ( state.lastCoursewareClickId === targetId && now - state.lastCoursewareClickAt < CONFIG.coursewareClickCooldownMs ) { return false; } state.lastCoursewareClickId = targetId; state.lastCoursewareClickAt = now; log(`切换到未完成分集:${target.title}(${Number.isNaN(target.progress) ? 0 : target.progress}%)`); clickElement(target.item); // 有些播放器切换分集后不会自动播放,稍等页面切换完成后再尝试播放。 setTimeout(tryAutoPlayVideo, 1800); setTimeout(tryAutoPlayVideo, 3500); return true; } function finishCourseAndReturnToList() { if (state.returning) return; state.returning = true; const currentCourseId = sessionStorage.getItem(storageKey('currentCourseId')) || ''; rememberRecentlyCompleted(currentCourseId); log('当前课程所有分集均已达到 100%,返回课程列表继续查找下一个任务...'); clearInterval(state.timer); setTimeout(returnToListOrContinue, CONFIG.afterFinishDelayMs); } function handleLearningProgressState() { const progressItems = getLearningProgressItems(); if (!progressItems.length) return; const currentItem = getCurrentCourseware(progressItems); const firstIncomplete = findFirstIncompleteCourseware(progressItems); // 多分集课程:只有所有分集都 100% 后才返回列表。 if (!firstIncomplete || isLearningProgressComplete()) { finishCourseAndReturnToList(); return; } // 如果当前选中的分集已经 100%,或者页面默认停在已完成分集,就切换到第一个未完成分集。 if (!currentItem || currentItem.progress >= 100) { clickCoursewareItem(firstIncomplete); } } function monitorLearningProgress() { const progressItems = getLearningProgressItems(); if (!progressItems.length) { return; } const currentItem = getCurrentCourseware(progressItems) || progressItems[0]; const progress = Number.isNaN(currentItem.progress) ? 0 : currentItem.progress; const total = progressItems.length; const completed = progressItems.filter((item) => !Number.isNaN(item.progress) && item.progress >= 100).length; log(`当前分集:${currentItem.title},进度:${progress}%;本课程分集完成:${completed}/${total}`); handleLearningProgressState(); } function clickPossibleCompletionButton() { // 常见的完成、继续、返回按钮。如果后续你提供完成页 HTML,可以继续补充更精确的选择器。 const candidates = Array.from(document.querySelectorAll('a, button, input[type="button"], input[type="submit"]')); const matched = candidates.find((node) => { const text = ((node.textContent || node.value || node.title || '') + '').trim(); return /返回|继续学习|下一课|下一节|完成|确定/.test(text); }); if (matched) { log('点击可能的完成/继续/返回按钮:', (matched.textContent || matched.value || matched.title || '').trim()); clickElement(matched); return true; } return false; } function returnToListOrContinue() { if (clickPossibleCompletionButton()) return; const listUrl = sessionStorage.getItem(storageKey('lastListUrl')); if (listUrl) { location.href = listUrl; return; } history.back(); } function handleLearningPage() { const title = sessionStorage.getItem(storageKey('currentCourseTitle')) || '当前课程'; log(`已打开学习页:${title}`); state.timer = setInterval(() => { tryAutoPlayVideo(); monitorLearningProgress(); }, CONFIG.scanIntervalMs); tryAutoPlayVideo(); monitorLearningProgress(); } function boot() { setTimeout(() => { setInterval(handleStudyConfirmDialog, CONFIG.autoConfirmDialogIntervalMs); if (CONFIG.learningUrlPattern.test(location.href)) { handleLearningPage(); } else { state.timer = setInterval(handleCourseListPage, CONFIG.scanIntervalMs); handleCourseListPage(); } }, CONFIG.startDelayMs); } boot(); })();