// ==UserScript== // @name 中国保密在线网-自动学习工具 // @namespace http://tampermonkey.net/ // @version 2.9 // @description 自动完成中国保密在线网的视频学习,智能处理不同时长视频,支持跳过已学完课程,支持暂停/继续,优化UI响应速度 // @author 卖炸弹的小女孩 // @match https://www.baomi.org.cn/* // @icon https://file.baomi.org.cn/spc/upload/pagemanagement/2021-10-29/106KXG4GFKMA83255419.png // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; // 从当前URL获取所有参数 function getAllUrlParams() { const urlParams = new URLSearchParams(window.location.search); const params = {}; for (let [key, value] of urlParams) { params[key] = value; } return params; } // 配置信息 let CONFIG = { coursePacketId: null, delayBetweenRequests: 1500, debugMode: true, maxRetries: 3, urlParams: {}, longVideoThreshold: 600, segmentSize: 300, minSegmentSize: 60 }; // 全局变量 let allCourses = []; let filteredCourses = []; let isRunning = false; let isPaused = false; let currentIndex = 0; let currentProcess = null; let courseDetail = null; let sessionStats = { success: 0, failed: 0, skipped: 0 }; // SVG图标 const SVG_ICONS = { play: '', pause: '', stop: '', check: '', error: '', folder: '', clock: '', settings: '', book: '', time: '', filter: '', skip: '', loading: '' }; // 工具函数:将时间字符串转换为秒数 function timeToSeconds(timeStr) { try { const parts = timeStr.split(':'); if (parts.length === 3) { return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]); } return 300; } catch (e) { return 300; } } // 工具函数:获取token function getToken() { const cookieMatch = document.cookie.match(/token=([^;]+)/); if (cookieMatch) return cookieMatch[1]; if (localStorage.getItem('token')) return localStorage.getItem('token'); if (window.authToken) return window.authToken; return null; } // 获取headers function getHeaders() { return { 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'application/json', 'token': getToken(), 'siteId': '95', 'authToken': '3613146b9c1a486b8666e86a78298a1a' }; } async function interruptibleWait(ms) { const checkInterval = 100; let elapsed = 0; while (elapsed < ms) { if (!isRunning) return; await new Promise(resolve => setTimeout(resolve, checkInterval)); elapsed += checkInterval; } } async function checkCourseStatus(course) { const token = getToken(); const resourceDirectoryId = course.SYS_UUID; const url = `https://www.baomi.org.cn/portal/main-api/v2/coursePacket/getResourceUserStatistic?coursePacketId=${CONFIG.coursePacketId}&token=${token}&resourceDirectoryId=${resourceDirectoryId}`; if (CONFIG.debugMode) { console.log(`正在检测课程状态: ${course.name}`, url); } try { const response = await fetch(url, { method: 'GET', headers: getHeaders() }); if (!response.ok) return false; const result = await response.json(); if (result.status === 0 && result.data) { const data = result.data; const isFinished = data.progressRate >= 1.0 || (data.studyLength > 0 && data.studyLength >= data.resourceLength); if (CONFIG.debugMode) { console.log(`状态检测结果 [${course.name}]: 进度 ${(data.progressRate * 100).toFixed(0)}%, 已学 ${data.studyLength}s`); } return isFinished; } return false; } catch (error) { console.warn(`检测课程状态失败 [${course.name}],将尝试默认提交逻辑:`, error); return false; } } // 获取课程详情 async function getCourseDetail() { const { pubId, siteId } = CONFIG.urlParams; if (!pubId || !siteId) return null; const url = `https://www.baomi.org.cn/portal/main-api/resource/simpleDetail.do?pubId=${pubId}&siteId=${siteId}`; try { const response = await fetch(url, { headers: getHeaders() }); const data = await response.json(); return (data.successResult && data.data) ? data.data : null; } catch (error) { return null; } } // 获取目录列表 async function getDirectoryList() { const url = `https://www.baomi.org.cn/portal/main-api/v2/coursePacket/getCourseDirectoryList?scale=1&coursePacketId=${CONFIG.coursePacketId}`; try { const response = await fetch(url, { headers: getHeaders() }); const data = await response.json(); return (data.status === 0 && data.data) ? data.data : []; } catch (error) { return []; } } // 递归获取所有目录 function getAllDirectories(directoryList) { const directories = []; function traverse(dirs) { if (!dirs || !Array.isArray(dirs)) return; dirs.forEach(dir => { directories.push({ SYS_UUID: dir.SYS_UUID, name: dir.name, directoryCascadeID: dir.directoryCascadeID }); if (dir.subDirectory && dir.subDirectory.length > 0) traverse(dir.subDirectory); }); } traverse(directoryList); return directories; } // 获取单个目录下的课程列表 async function getCoursesByDirectory(directoryId, directoryName, retryCount = 0) { const url = `https://www.baomi.org.cn/portal/main-api/v2/coursePacket/getCourseResourceList?coursePacketId=${CONFIG.coursePacketId}&directoryId=${directoryId}&token=${getToken()}`; try { const response = await fetch(url, { headers: getHeaders() }); const data = await response.json(); if (data.status === 0 && data.data && data.data.listdata) { return data.data.listdata.map(course => ({ ...course, directoryName: directoryName, directoryId: directoryId, totalSeconds: timeToSeconds(course.timeLength) })); } return []; } catch (error) { if (retryCount < CONFIG.maxRetries) { await new Promise(resolve => setTimeout(resolve, 1000)); return getCoursesByDirectory(directoryId, directoryName, retryCount + 1); } return []; } } // 获取所有课程 async function getAllCourses() { const directoryList = await getDirectoryList(); if (directoryList.length === 0) return []; const allDirectories = getAllDirectories(directoryList); let allCourses = []; for (let i = 0; i < allDirectories.length; i++) { const directory = allDirectories[i]; const courses = await getCoursesByDirectory(directory.SYS_UUID, directory.name); allCourses = allCourses.concat(courses); if (i < allDirectories.length - 1) await new Promise(resolve => setTimeout(resolve, 300)); } return allCourses; } // 提交单个课程的学习进度 - 智能分段提交 async function submitCourseProgress(course, index, total) { const totalSeconds = course.totalSeconds || timeToSeconds(course.timeLength); const resourceDirectoryId = course.SYS_UUID; const encodedResourceName = encodeURIComponent(course.name); const isLongVideo = totalSeconds > CONFIG.longVideoThreshold; if (isLongVideo) { return await submitLongVideoProgress(course, totalSeconds, resourceDirectoryId, encodedResourceName); } else { return await submitShortVideoProgress(course, totalSeconds, resourceDirectoryId, encodedResourceName); } } // 提交短视频学习进度 async function submitShortVideoProgress(course, totalSeconds, resourceDirectoryId, encodedResourceName) { const params = new URLSearchParams({ courseId: CONFIG.coursePacketId, resourceId: course.resourceID, resourceDirectoryId: resourceDirectoryId, resourceLength: totalSeconds, studyLength: totalSeconds, studyTime: totalSeconds, startTime: Date.now() - (totalSeconds * 1000), resourceName: encodedResourceName, resourceType: '1', resourceLibId: '3', studyResourceId: course.SYS_DOCUMENTID, token: getToken() }); const url = `https://www.baomi.org.cn/portal/main-api/v2/studyTime/saveCoursePackage.do?${params}`; try { const response = await fetch(url, { headers: getHeaders() }); const result = await response.json(); if (result.status === 0) return { success: true, course: course.name, directory: course.directoryName }; return { success: false, course: course.name, error: result.message, directory: course.directoryName }; } catch (error) { return { success: false, course: course.name, error: error.message, directory: course.directoryName }; } } // 提交长视频学习进度 async function submitLongVideoProgress(course, totalSeconds, resourceDirectoryId, encodedResourceName) { const segments = Math.ceil(totalSeconds / CONFIG.segmentSize); let remainingSeconds = totalSeconds; let submittedLength = 0; for (let segment = 1; segment <= segments; segment++) { if (!isRunning) break; const isLastSegment = segment === segments; const segmentStudyTime = isLastSegment ? Math.max(remainingSeconds, CONFIG.minSegmentSize) : CONFIG.segmentSize; submittedLength += segmentStudyTime; const currentStudyLength = Math.min(submittedLength, totalSeconds); const params = new URLSearchParams({ courseId: CONFIG.coursePacketId, resourceId: course.resourceID, resourceDirectoryId: resourceDirectoryId, resourceLength: totalSeconds, studyLength: currentStudyLength, studyTime: segmentStudyTime, startTime: Date.now() - (totalSeconds * 1000) + ((segment - 1) * CONFIG.segmentSize * 1000), resourceName: encodedResourceName, resourceType: '1', resourceLibId: '3', studyResourceId: course.SYS_DOCUMENTID, token: getToken() }); const url = `https://www.baomi.org.cn/portal/main-api/v2/studyTime/saveCoursePackage.do?${params}`; try { await fetch(url, { headers: getHeaders() }); remainingSeconds -= segmentStudyTime; if (!isLastSegment) await interruptibleWait(1000); } catch (error) { console.error(`分段提交失败`, error); } } if (!isRunning) return { success: false, course: course.name, paused: true }; return { success: true, course: course.name, directory: course.directoryName }; } function updateProgress(current, total, message) { const progressElement = document.getElementById('progressBar'); const progressText = document.getElementById('progressText'); const statusElement = document.getElementById('status'); const percent = total > 0 ? Math.round((current / total) * 100) : 0; if (progressElement && progressText) { progressElement.style.width = percent + '%'; progressText.textContent = `${current}/${total} (${percent}%)`; } if (statusElement) statusElement.textContent = message; } function showFinalResults() { console.log('--- 批量处理完成 ---'); console.log(`成功提交: ${sessionStats.success}`); console.log(`检测跳过: ${sessionStats.skipped}`); console.log(`提交失败: ${sessionStats.failed}`); updateProgress(filteredCourses.length, filteredCourses.length, '全部处理完成!'); updateStatus(`完成: ${sessionStats.success}提交 / ${sessionStats.skipped}跳过 / ${sessionStats.failed}失败`); } async function submitAllCourses() { if (filteredCourses.length === 0) { updateStatus('正在获取目录列表...'); allCourses = await getAllCourses(); if (allCourses.length === 0) { updateStatus('获取课程失败,请刷新重试'); isRunning = false; updateUIState('stopped'); return; } filteredCourses = allCourses; } if (!isRunning) { isRunning = true; updateUIState('running'); } const total = filteredCourses.length; for (let i = currentIndex; i < total; i++) { if (!isRunning) { break; } currentIndex = i; const course = filteredCourses[i]; updateProgress(i, total, `[${i + 1}/${total}] 检测: ${course.name.substring(0, 10)}...`); const isAlreadyFinished = await checkCourseStatus(course); if (isAlreadyFinished) { console.log(`[跳过] 已完成课程: ${course.name}`); sessionStats.skipped++; await interruptibleWait(500); continue; } updateProgress(i, total, `[${i + 1}/${total}] 学习: ${course.name.substring(0, 10)}...`); const result = await submitCourseProgress(course, i, total); if (result.paused) { break; } if (result.success) sessionStats.success++; else sessionStats.failed++; if (i < total - 1 && isRunning) { await interruptibleWait(CONFIG.delayBetweenRequests); } } if (!isRunning) { if (isPaused) { updateStatus(`已暂停 (进度: ${currentIndex + 1}/${total})`); updateUIState('paused'); } else { updateStatus('已停止'); updateUIState('stopped'); } } else { showFinalResults(); isRunning = false; currentIndex = 0; filteredCourses = []; updateUIState('finished'); } } function updateStatus(message) { const statusElement = document.getElementById('status'); if (statusElement) statusElement.textContent = message; } function updateUIState(state) { const startBtn = document.getElementById('startAutoStudy'); const pauseBtn = document.getElementById('pauseAutoStudy'); const stopBtn = document.getElementById('stopAutoStudy'); const startText = startBtn.querySelector('span'); pauseBtn.disabled = false; pauseBtn.style.opacity = '1'; pauseBtn.style.cursor = 'pointer'; pauseBtn.innerHTML = `${SVG_ICONS.pause}暂停`; stopBtn.disabled = false; stopBtn.style.opacity = '1'; stopBtn.style.cursor = 'pointer'; stopBtn.innerHTML = `${SVG_ICONS.stop}停止`; startBtn.style.display = 'none'; pauseBtn.style.display = 'none'; stopBtn.style.display = 'none'; if (state === 'running') { pauseBtn.style.display = 'flex'; stopBtn.style.display = 'flex'; } else if (state === 'paused') { startBtn.style.display = 'flex'; stopBtn.style.display = 'flex'; startText.textContent = '继续学习'; // startIcon 保持 play } else if (state === 'stopped' || state === 'finished') { startBtn.style.display = 'flex'; startText.textContent = '开始智能学习'; } else { // initial startBtn.style.display = 'flex'; } } function addUI() { const existingUI = document.getElementById('auto-study-ui'); if (existingUI) existingUI.remove(); const ui = document.createElement('div'); ui.id = 'auto-study-ui'; ui.innerHTML = `
${SVG_ICONS.settings}自动学习工具 v3.0
${SVG_ICONS.book}加载中...
${SVG_ICONS.filter}支持暂停/继续,跳过已完成
进度:0/0 (0%)
准备就绪,点击开始
`; document.body.appendChild(ui); const style = document.createElement('style'); style.id = 'auto-study-style'; style.textContent = ` .auto-study-container { position: fixed; top: 20px; right: 20px; background: #fff; border: 1px solid #e1e5e9; border-radius: 12px; padding: 16px; z-index: 9999; box-shadow: 0 8px 24px rgba(0,0,0,0.12); width: 320px; font-family: sans-serif; } .auto-study-header { margin-bottom: 12px; border-bottom: 1px solid #f0f0f0; padding-bottom: 8px; } .auto-study-title { display: flex; align-items: center; gap: 8px; font-weight: 600; margin-bottom: 8px; } .auto-study-course-name { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 500; margin-bottom: 4px; } .auto-study-detail-item { font-size: 11px; color: #666; display: flex; align-items: center; gap: 4px; } .auto-study-controls { display: flex; gap: 8px; margin-bottom: 12px; } .auto-study-btn { flex: 1; padding: 8px; border: none; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; color: white; font-size: 13px; transition: all 0.2s; } .auto-study-btn-primary { background: #10b981; } .auto-study-btn-primary:hover { background: #059669; } .auto-study-btn-warning { background: #f59e0b; } .auto-study-btn-warning:hover { background: #d97706; } .auto-study-btn-secondary { background: #ef4444; } .auto-study-btn-secondary:hover { background: #dc2626; } .auto-study-progress-bar { height: 6px; background: #f0f0f0; border-radius: 3px; overflow: hidden; margin-top: 6px; } .auto-study-progress-fill { height: 100%; background: #10b981; width: 0%; transition: width 0.3s; } .auto-study-progress-info { display: flex; justify-content: space-between; font-size: 12px; color: #666; } .auto-study-status { font-size: 12px; color: #888; margin-top: 8px; } .spin { animation: spin 1s linear infinite; } @keyframes spin { 100% { transform: rotate(360deg); } } `; if (!document.getElementById('auto-study-style')) document.head.appendChild(style); document.getElementById('startAutoStudy').addEventListener('click', async function () { isRunning = true; updateUIState('running'); if (isPaused) { isPaused = false; } else { currentIndex = 0; sessionStats = { success: 0, failed: 0, skipped: 0 }; if (!isPaused) filteredCourses = []; } await submitAllCourses(); }); // 暂停按钮 document.getElementById('pauseAutoStudy').addEventListener('click', function () { isRunning = false; isPaused = true; const btn = this; btn.innerHTML = `${SVG_ICONS.loading}正在暂停...`; btn.style.opacity = '0.7'; btn.style.cursor = 'not-allowed'; btn.disabled = true; updateStatus('正在暂停,请稍候...'); }); // 停止按钮 document.getElementById('stopAutoStudy').addEventListener('click', function () { isRunning = false; isPaused = false; currentIndex = 0; const btn = this; btn.innerHTML = `${SVG_ICONS.loading}正在停止...`; btn.style.opacity = '0.7'; btn.style.cursor = 'not-allowed'; btn.disabled = true; updateStatus('正在停止,请稍候...'); }); } async function main() { const currentUrlParams = getAllUrlParams(); CONFIG.urlParams = currentUrlParams; CONFIG.coursePacketId = currentUrlParams.id; if (!CONFIG.coursePacketId) return; if (!getToken()) return; addUI(); updateStatus('正在获取课程信息...'); courseDetail = await getCourseDetail(); if (courseDetail) { const nameEl = document.getElementById('courseName'); if(nameEl) nameEl.textContent = courseDetail.docName || courseDetail.name; updateStatus('课程信息加载完成'); } } function startUrlMonitor() { setInterval(() => { const isDetailPage = location.href.indexOf('bmCourseDetail') > -1; const uiExists = document.getElementById('auto-study-ui'); if (isDetailPage && !uiExists) main(); }, 1000); } startUrlMonitor(); })();