// ==UserScript== // @name 宝武学习助手 // @namespace http://tampermonkey.net/ // @version 2.1.1 // @description 1.新增[公开课/专区课]多课程自动学习模式;2.新增自动获取课程列表、课程成绩;3.自动静音、自动播放、自动下一节;4.自动完成学习时长;5.学习进度状态显示 // @author jinxhcn // @match *://learn.baowugroup.com/* // @icon https://learn.baowugroup.com/favicon.png // @require https://scriptcat.org/lib/7205/1.1.0/bw_learn_tool_api_lib.js?sha384-Y5iNeh0VIzymfz3E4Ab5mdrrUIFct9O4fWbKZusR4D7i6oak4FldGfWCx1/tn5VP // @require https://scriptcat.org/lib/7206/1.1.0/bw_learn_tool_gui_lib.js?sha384-OsZ+sFTzZKQTmp4GuTe382u2RPq+qTCdnEqiVXOQawkzXP9iixpxvg6kyGb79Kmd // @require https://scriptcat.org/lib/7207/1.1.0/bw_learn_tool_utl_lib.js?sha384-t2S9VqgtXtGgsqfU64j43ecbsyPqpR/TG1KahWlZwEWPdem6A1CM0CQnp8rvqqNc // @grant GM_info // @grant unsafeWindow // @grant GM_registerMenuCommand // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; const BWAPI = unsafeWindow.BWAPI; const BWGUI = unsafeWindow.BWGUI; const BWUTL = unsafeWindow.BWUTL; const log = (msg) => BWGUI.log(`[${new Date().toLocaleTimeString()}] ${msg}`); const DEFAULT_DATA = { url: { currentUrl: '', previousUrl: '' }, token: '', state: { isLogin: false, isAutoLearning: false, learningType: null, isVideoPage: false, isVideoMuted: false, isVideoPlaying: false }, publicCourses: [], zone: [], toLearnList: [] }; const AppData = BWUTL.createDataStore('bw_learn_app_data', DEFAULT_DATA); const getCurrentCourseGuid = () => { const hash = window.location.hash; if (!hash) return null; const q = hash.indexOf('?'); if (q === -1) return null; const params = new URLSearchParams(hash.substring(q + 1)); return params.get('guid') || null; }; const isCourseFinished = (score) => { if (!score) return false; const req = Number(score.requiredDuration); const comp = Number(score.completedDuration); return req > 0 && comp > req; }; const getUrlChange = (oldUrl) => { const newUrl = location.href; return newUrl !== oldUrl ? { previousUrl: oldUrl, currentUrl: newUrl } : null; }; const getTokenChange = (oldToken) => { const token = BWAPI.getToken(); return token !== oldToken ? token : null; }; const getVideoElement = () => document.querySelector('#xg-video video'); const getStateChanges = (oldState) => { const changes = {}; const isLogin = !location.href.includes('auth/login'); if (oldState.isLogin !== isLogin) { changes.isLogin = isLogin; log(`🔐 登录状态: ${isLogin ? '已登录' : '未登录'}`); } const video = getVideoElement(); const isVideoPage = !!video; const isVideoMuted = video?.muted ?? false; const isVideoPlaying = video ? !video.paused : false; if (oldState.isVideoPage !== isVideoPage || oldState.isVideoMuted !== isVideoMuted || oldState.isVideoPlaying !== isVideoPlaying) { Object.assign(changes, { isVideoPage, isVideoMuted, isVideoPlaying }); } if (!location.href.includes('courseStudy?') && !isVideoPage && oldState.isAutoLearning) { changes.isAutoLearning = false; log('❌ 自动学习已退出'); } if (isVideoPage && oldState.isAutoLearning) { const courseGuid = getCurrentCourseGuid(); const toLearnList = AppData.get('toLearnList') || []; const matched = toLearnList.find(item => String(item.courseGuid) === String(courseGuid)); const newType = matched ? (matched.courseType === '公开课' ? 'public' : matched.courseType === '专区课' ? 'zone' : null) : null; if (oldState.learningType !== newType) { changes.learningType = newType; log(`📚 学习类型已更新: ${newType || '未匹配'}`); } } else if (oldState.learningType !== null) { changes.learningType = null; } return Object.keys(changes).length > 0 ? changes : null; }; const updateAppDataState = () => { const oldUrl = AppData.get('url').currentUrl; const oldToken = AppData.get('token'); const oldState = AppData.get('state'); const updates = {}; const urlChange = getUrlChange(oldUrl); if (urlChange) updates.url = urlChange; const token = getTokenChange(oldToken); if (token !== null) updates.token = token; const stateChanges = getStateChanges(oldState); if (stateChanges) { if (stateChanges.isAutoLearning === false) updates.toLearnList = []; updates.state = stateChanges; } const currentIsVideoPage = stateChanges?.isVideoPage ?? oldState.isVideoPage; if (!location.href.includes('courseStudy?') && !currentIsVideoPage) { updates.toLearnList = []; } if (Object.keys(updates).length > 0) AppData.save(updates); return { ...oldState, ...(stateChanges || {}) }; }; const updateLearningStatusDisplay = (state) => { let text = '待学习'; let color = '#0b96db'; if (!state.isLogin) { text = '未登录'; color = '#f0ad4e'; } else if (state.isAutoLearning) { if (state.isVideoPage && state.isVideoPlaying) { if (state.learningType === 'zone') { text = '学习中·专区课'; } else if (state.learningType === 'public') { text = '学习中·公开课'; } else { text = '学习中'; } color = '#5cb85c'; } } else { if (state.isVideoPage && state.isVideoPlaying) { text = '学习中·单课程'; color = '#5cb85c'; } } BWGUI.updateStatus(text, color); }; const getPublicCoursesList = async () => { try { const allCourses = await BWAPI.fetchPublicCourses(''); const enriched = await BWAPI.enrichCoursesWithScores(allCourses); AppData.save({ publicCourses: enriched }); log('✅ 公开课课程加载完成'); showPublicCoursesTable(); } catch { log('❌ 公开课加载失败'); } }; const getZoneCoursesList = async () => { try { const zoneList = await BWAPI.fetchZoneList(''); const zoneMap = new Map(); zoneList.forEach(zone => { if (!zoneMap.has(zone.olClassNo)) { zoneMap.set(zone.olClassNo, { ...zone, zoneCourses: [] }); } }); const uniqueZones = Array.from(zoneMap.values()); await Promise.allSettled( uniqueZones.map(zone => BWAPI.fetchZoneCoursesDetail(zone.centerCode, zone.olClassNo, zone.olClassType) .then(courses => BWAPI.enrichCoursesWithScores(courses)) .then(enriched => { zone.zoneCourses = enriched; }) .catch(err => { log(`⚠️ 加载专区 ${zone.olClassNo} 课程失败: ${err}`); zone.zoneCourses = []; }) ) ); AppData.save({ zone: uniqueZones }); log(`✅ 专区课程加载完成,共 ${uniqueZones.length} 个专区`); showZoneCoursesTable(); } catch { log('❌ 专区课程加载失败'); } }; const showPublicCoursesTable = () => BWGUI.showPublicCoursesTable(AppData.get('publicCourses') || []); const showZoneCoursesTable = () => BWGUI.showZoneCoursesTable(AppData.get('zone') || []); const refreshPublicCourses = async () => { log('⏳ 正在获取公开课...'); await getPublicCoursesList(); const count = AppData.get('publicCourses')?.length || 0; log(`✅ 获取完成,共 ${count} 门公开课`); }; const refreshZoneCourses = async () => { log('⏳ 正在获取专区课程...'); await getZoneCoursesList(); const zones = AppData.get('zone') || []; const total = zones.reduce((sum, z) => sum + (z.zoneCourses?.length || 0), 0); log(`✅ 获取完成,共 ${zones.length} 个专区,${total} 门课程`); }; const learnCourses = async (type, loadFn, storageKey) => { let data = AppData.get(storageKey) || []; if (data.length === 0) { log('⏳ 课程数据为空,正在自动加载...'); try { await loadFn(); data = AppData.get(storageKey) || []; } catch { log('❌ 获取课程数据失败'); return; } } const unlearned = []; if (type === 'public') { data.forEach(c => { if (!isCourseFinished(c.score)) unlearned.push({ ...c, courseType: '公开课' }); }); } else { data.forEach(zone => { (zone.zoneCourses || []).forEach(c => { if (!isCourseFinished(c.score)) unlearned.push({ ...c, courseType: '专区课' }); }); }); } AppData.save({ toLearnList: unlearned }); BWGUI.showToLearnTable(unlearned, getCurrentCourseGuid()); log(`📋 待学习${type === 'public' ? '公开课' : '专区课'}共 ${unlearned.length} 门`); if (unlearned.length > 0) { const first = unlearned[0]; if (first.centerCode && first.courseGuid) { log(`🚀 学习课程: ${first.courseName}`); AppData.save({ state: { isAutoLearning: true } }); location.assign(`https://learn.baowugroup.com/#/courseStudy?centerCode=${first.centerCode}&guid=${first.courseGuid}`); } else { log('⚠️ 课程信息不完整,无法跳转到学习页面'); } } else { AppData.save({ state: { isAutoLearning: false } }); log(`🎉 所有${type === 'public' ? '公开课' : '专区课'}已完成,无需学习`); } }; const learnPublicCourses = () => learnCourses('public', getPublicCoursesList, 'publicCourses'); const learnZoneCourses = () => learnCourses('zone', getZoneCoursesList, 'zone'); const playCurrentVideo = (video) => { if (!video || video.ended || !video.paused) return false; const strategies = [ { sel: '.xgplayer-start', name: '中央播放按钮' }, { sel: '.xgplayer-play .xgplayer-icon', name: '控制栏播放图标' }, { sel: '#xg-video, .xgplayer', name: '视频画面区域' }, ]; for (const { sel, name } of strategies) { const el = document.querySelector(sel); if (el?.offsetParent) { el.click(); log(`▶️ 点击${name}`); return true; } } video.play().catch(() => { }); log('▶️ 自动点击播放'); return true; }; const tryClickNextChapter = () => { const btn = document.querySelector('.play-chapter button.el-button--primary'); if (btn?.offsetParent && !btn.disabled && btn.textContent.includes('播放下一节')) { log('⏭️ 自动点击 "播放下一节"'); btn.click(); return true; } return false; }; const cleanupVideoOverlays = () => { document.querySelector('.xgplayer-start')?.classList.add('hide'); const overlay = document.querySelector('.video-overlay'); if (overlay) overlay.style.display = 'none'; }; let fetchingCourseInfo = false; const ensureCurrentCourseInfo = async () => { if (fetchingCourseInfo) return; const hash = window.location.hash; if (!hash) return; const q = hash.indexOf('?'); if (q === -1) return; const params = new URLSearchParams(hash.substring(q + 1)); const centerCode = params.get('centerCode'); const courseGuid = params.get('guid'); if (!centerCode || !courseGuid) return; const list = AppData.get('toLearnList') || []; if (list.some(c => String(c.courseGuid) === String(courseGuid))) return; fetchingCourseInfo = true; try { const info = await BWAPI.fetchCourseInfoByVideoUrl(centerCode, courseGuid); if (info) { let score = null; try { score = await BWAPI.fetchCourseScore(info.centerCode, info.courseNo, info.olClassNo); } catch { } const state = AppData.get('state'); let courseType = '单课程'; if (state.isAutoLearning) { const existing = list.find(c => String(c.courseGuid) === String(courseGuid)); if (existing) courseType = existing.courseType; } const enriched = { ...info, courseType, score: score || { passScore: null, learnScore: null, requiredDuration: null, completedDuration: null } }; const currentList = AppData.get('toLearnList') || []; const filtered = currentList.filter(c => String(c.courseGuid) !== String(courseGuid)); filtered.push(enriched); AppData.save({ toLearnList: filtered }); log(`📋 课程信息已获取: ${enriched.courseName} (${courseType})`); BWGUI.showToLearnTable(filtered, getCurrentCourseGuid()); } } catch { log('⚠️ 课程信息获取失败'); } finally { fetchingCourseInfo = false; } }; const loop_2s = async () => { try { const state = updateAppDataState(); updateLearningStatusDisplay(state); BWGUI.setButtonsEnabled(state.isLogin && !state.isVideoPage); if (state.isVideoPage) { await ensureCurrentCourseInfo(); const toLearnList = AppData.get('toLearnList') || []; BWGUI.showToLearnTable(toLearnList, getCurrentCourseGuid()); const video = getVideoElement(); if (video && !state.isVideoMuted) { video.muted = true; log('🔇 视频已静音'); } if (video && !state.isVideoPlaying) { playCurrentVideo(video); } if (tryClickNextChapter()) return; if (video && !video.paused) { cleanupVideoOverlays(); } const allButtons = document.querySelectorAll('button.el-button'); const replayBtn = [...allButtons].find(btn => btn.textContent.includes('重新播放') && btn.offsetParent); if (replayBtn) { const currentGuid = getCurrentCourseGuid(); const currentCourse = toLearnList.find(c => String(c.courseGuid) === String(currentGuid)); if (currentCourse) { let latestScore = null; try { latestScore = await BWAPI.fetchCourseScoreInVideoPage( currentCourse.centerCode, currentCourse.courseNo, currentCourse.olClassNo ); } catch { } const updatedScore = latestScore || currentCourse.score; if (isCourseFinished(updatedScore)) { log('✅ 课程已完成,自动跳转下一节'); const updatedCourse = { ...currentCourse, score: updatedScore }; handleCourseCompletion(updatedCourse, toLearnList, state.isAutoLearning); return; } else { log('🔄 课程未完成,自动重播'); replayBtn.click(); return; } } else { log('🔄 检测到“重新播放”按钮,尝试自动重播'); replayBtn.click(); return; } } } } catch (err) { log('loop_2s 错误: ' + err); } }; const handleCourseCompletion = (completedCourse, toLearnList, isAutoLearning) => { const courseType = completedCourse.courseType || '单课程'; if (!isAutoLearning || courseType === '单课程') { log('🏁 单课程学习完成,返回个人中心'); location.assign('https://learn.baowugroup.com/#/userCenter'); return; } const remaining = toLearnList.filter(c => { if (String(c.courseGuid) === String(completedCourse.courseGuid)) return false; return !isCourseFinished(c.score); }); AppData.save({ toLearnList: remaining }); BWGUI.showToLearnTable(remaining, getCurrentCourseGuid()); if (remaining.length > 0) { const next = remaining[0]; log(`➡️ 继续学习下一课程: ${next.courseName}`); location.assign(`https://learn.baowugroup.com/#/courseStudy?centerCode=${next.centerCode}&guid=${next.courseGuid}`); } else { log('🎉 所有课程学习完成,返回个人中心'); AppData.save({ state: { isAutoLearning: false }, toLearnList: [] }); location.assign('https://learn.baowugroup.com/#/userCenter'); } }; let scoreUpdateLock = false; const loop_60s = async () => { if (scoreUpdateLock) return; const state = AppData.get('state'); if (!state.isVideoPage) return; const hash = window.location.hash; if (!hash) return; const q = hash.indexOf('?'); if (q === -1) return; const params = new URLSearchParams(hash.substring(q + 1)); const centerCode = params.get('centerCode'); const courseGuid = params.get('guid'); if (!centerCode || !courseGuid) return; scoreUpdateLock = true; try { let toLearnList = AppData.get('toLearnList') || []; let target = toLearnList.find(c => String(c.courseGuid) === String(courseGuid)); if (!target) return; const oldScore = target.score || {}; const newScore = await BWAPI.fetchCourseScoreInVideoPage(target.centerCode, target.courseNo, target.olClassNo); if (newScore) { const isSame = newScore.completedDuration === oldScore.completedDuration && newScore.learnScore === oldScore.learnScore && newScore.passScore === oldScore.passScore && newScore.requiredDuration === oldScore.requiredDuration; if (!isSame) { const updatedList = toLearnList.map(c => (c.courseGuid === courseGuid ? { ...c, score: newScore } : c)); AppData.save({ toLearnList: updatedList }); const fmt = (v) => (v != null ? Math.round(v) : '-'); log(`🔄 学习进度: 时长: ${fmt(newScore.completedDuration)}/${fmt(newScore.requiredDuration)}, 得分: ${fmt(newScore.learnScore)}/${fmt(newScore.passScore)}`); const req = Number(newScore.requiredDuration); const oldDone = Number(oldScore.requiredDuration) > 0 && Number(oldScore.completedDuration) > Number(oldScore.requiredDuration); const newDone = req > 0 && Number(newScore.completedDuration) > req; if (newDone && !oldDone) { const finalList = updatedList.map(c => c.courseGuid === courseGuid ? { ...c, learnStatus: '2', score: newScore } : c); AppData.save({ toLearnList: finalList }); log('✅ 当前课程学习完成'); handleCourseCompletion(target, finalList, state.isAutoLearning); return; } BWGUI.showToLearnTable(updatedList, getCurrentCourseGuid()); } } } catch (e) { log('获取成绩失败: ' + e); } finally { scoreUpdateLock = false; } }; const bootstrap = () => { AppData.init(); BWUTL.fswn(); BWGUI.init({ scriptName: GM_info.script.name, version: GM_info.script.version, buttonHandlers: { 'publicList': showPublicCoursesTable, 'refreshPublic': refreshPublicCourses, 'learnPublic': learnPublicCourses, 'zoneList': showZoneCoursesTable, 'refreshZone': refreshZoneCourses, 'learnZone': learnZoneCourses, } }); BWUTL.loop(loop_2s, true, 2000); BWUTL.loop(loop_60s, true, 60000); log(`🚀 ${GM_info.script.name} v${GM_info.script.version} 启动成功`); console.log(BWAPI.version); console.log(BWGUI.version); console.log(BWUTL.version); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', bootstrap); } else { bootstrap(); } })();