// ==UserScript==
// @name 云学堂视频学习监控(带控制面板)
// @namespace http://tampermonkey.net/
// @license MIT
// @version 3.0
// @description 云学堂学习监控脚本,带可拖拽控制面板,支持课程提取、学习监控、进度显示和日志记录
// @icon https://picobd.yunxuetang.cn/sys/asiainfo/others/202305/efc8749aa9474334a99be88b3c1131e5.ico
// @author You
// @match https://asiainfo.yunxuetang.cn/*
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @ts-nocheck
// ==/UserScript==
/*
* 云学堂学习监控脚本使用说明:
*
* 1. 进入个人任务中心:https://asiainfo.yunxuetang.cn/sty/index.htm
* 2. 点击任意学习任务,进入任务详情页面
* 3. 脚本会自动显示可拖拽的控制面板
* 4. 在任务详情页面点击"提取课程"按钮提取所有课程链接
* 5. 在学习页面点击"开始监控"按钮开始自动学习监控
*
* 控制面板功能:
* - 课程提取:自动提取任务中的所有课程链接
* - 学习监控:监控学习进度和剩余时间
* - 总体进度:显示当前课程在所有课程中的位置
* - 课程列表:查看和跳转到指定课程
* - 运行日志:实时显示脚本运行状态
* - 可拖拽移动:面板可自由拖拽到任意位置
*/
(function () {
'use strict';
let notificationShown = false;
let pageLoaded = false;
let autoScrollInterval = null;
// 控制面板相关变量
let controlPanel = null;
let isExtracting = false;
let extractProgress = { current: 0, total: 0 };
let learningMonitorInterval = null;
// 添加样式
// @ts-ignore
GM_addStyle(`
#yunxuetang-monitor-panel {
position: fixed !important;
top: 20px !important;
right: 20px !important;
width: 320px !important;
background: #fff !important;
border: 2px solid #007bff !important;
border-radius: 8px !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
z-index: 999999 !important;
font-family: Arial, sans-serif !important;
font-size: 12px !important;
box-sizing: border-box !important;
}
#yunxuetang-monitor-panel * {
box-sizing: border-box !important;
}
#yunxuetang-monitor-panel .panel-header {
background: #007bff !important;
color: white !important;
padding: 12px !important;
font-weight: bold !important;
border-radius: 6px 6px 0 0 !important;
cursor: move !important;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
min-height: 40px !important;
}
#yunxuetang-monitor-panel .panel-body {
padding: 15px !important;
}
#yunxuetang-monitor-panel .section {
margin-bottom: 15px !important;
padding-bottom: 10px !important;
border-bottom: 1px solid #eee !important;
}
#yunxuetang-monitor-panel .section:last-child {
border-bottom: none !important;
margin-bottom: 0 !important;
}
#yunxuetang-monitor-panel .section-title {
font-weight: bold !important;
margin-bottom: 8px !important;
color: #333 !important;
font-size: 13px !important;
}
#yunxuetang-monitor-panel button {
width: 100% !important;
padding: 8px 12px !important;
margin: 3px 0 !important;
border: none !important;
border-radius: 4px !important;
cursor: pointer !important;
font-size: 11px !important;
font-weight: bold !important;
min-height: 32px !important;
line-height: 1.2 !important;
text-align: center !important;
display: inline-block !important;
vertical-align: middle !important;
}
#yunxuetang-monitor-panel .btn-primary {
background: #007bff !important;
color: white !important;
}
#yunxuetang-monitor-panel .btn-success {
background: #28a745 !important;
color: white !important;
}
#yunxuetang-monitor-panel .btn-danger {
background: #dc3545 !important;
color: white !important;
}
#yunxuetang-monitor-panel .btn-secondary {
background: #6c757d !important;
color: white !important;
}
#yunxuetang-monitor-panel .btn:disabled {
opacity: 0.6 !important;
cursor: not-allowed !important;
}
#yunxuetang-monitor-panel .progress {
background: #f0f0f0 !important;
border-radius: 4px !important;
height: 20px !important;
margin: 8px 0 !important;
overflow: hidden !important;
position: relative !important;
}
#yunxuetang-monitor-panel .progress-bar {
background: #28a745 !important;
height: 100% !important;
transition: width 0.3s !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
color: white !important;
font-size: 10px !important;
font-weight: bold !important;
min-height: 20px !important;
line-height: 20px !important;
margin-top: 0;
}
#yunxuetang-monitor-panel .status {
font-size: 11px !important;
color: #666 !important;
margin: 5px 0 !important;
padding: 4px 8px !important;
background: #f8f9fa !important;
border-radius: 3px !important;
}
#yunxuetang-monitor-panel .log {
max-height: 120px !important;
overflow-y: auto !important;
background: #f8f9fa !important;
border: 1px solid #dee2e6 !important;
border-radius: 4px !important;
padding: 8px !important;
font-size: 10px !important;
line-height: 1.3 !important;
}
#yunxuetang-monitor-panel .log-entry {
margin: 2px 0 !important;
word-wrap: break-word !important;
}
#yunxuetang-monitor-panel .log-info { color: #333 !important; }
#yunxuetang-monitor-panel .log-success { color: #28a745 !important; }
#yunxuetang-monitor-panel .log-error { color: #dc3545 !important; }
#yunxuetang-monitor-panel .log-warning { color: #ffc107 !important; }
#yunxuetang-monitor-panel .close-btn {
background: none !important;
border: none !important;
color: white !important;
font-size: 18px !important;
cursor: pointer !important;
padding: 0 !important;
width: auto !important;
margin: 0 !important;
min-height: auto !important;
}
#yunxuetang-monitor-panel .info-row {
display: flex !important;
justify-content: space-between !important;
margin: 3px 0 !important;
font-size: 11px !important;
}
#yunxuetang-monitor-panel .info-label {
color: #666 !important;
}
#yunxuetang-monitor-panel .info-value {
font-weight: bold !important;
color: #333 !important;
}
#yunxuetang-monitor-panel .course-links {
max-height: 150px !important;
overflow-y: auto !important;
background: #f8f9fa !important;
border: 1px solid #dee2e6 !important;
border-radius: 4px !important;
padding: 8px !important;
margin-top: 8px !important;
}
#yunxuetang-monitor-panel .course-link-item {
padding: 4px 8px !important;
margin: 2px 0 !important;
background: white !important;
border: 1px solid #ddd !important;
border-radius: 3px !important;
font-size: 10px !important;
cursor: pointer !important;
transition: background-color 0.2s !important;
}
#yunxuetang-monitor-panel .course-link-item:hover {
background: #e9ecef !important;
}
#yunxuetang-monitor-panel .course-link-title {
font-weight: bold !important;
color: #007bff !important;
margin-bottom: 2px !important;
}
#yunxuetang-monitor-panel .course-link-url {
color: #666 !important;
font-size: 9px !important;
word-break: break-all !important;
}
`);
// 日志记录函数
function addLog(message, type = 'info') {
console.log(message);
if (controlPanel) {
const logContainer = controlPanel.querySelector('.log');
if (logContainer) {
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${type}`;
logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
}
}
// 更新进度条
function updateProgress(current, total, text = '') {
extractProgress.current = current;
extractProgress.total = total;
if (controlPanel) {
const progressBar = controlPanel.querySelector('.progress-bar');
const statusDiv = controlPanel.querySelector('.extract-status');
if (progressBar && total > 0) {
const percentage = Math.round((current / total) * 100);
progressBar.style.width = `${percentage}%`;
progressBar.textContent = `${percentage}%`;
}
if (statusDiv) {
statusDiv.textContent = text || `进度: ${current}/${total}`;
}
}
}
// 更新学习状态
function updateLearningStatus(currentCourse, progress, status) {
if (controlPanel) {
const currentCourseDiv = controlPanel.querySelector('.current-course');
const learningProgressDiv = controlPanel.querySelector('.learning-progress');
const monitorStatusDiv = controlPanel.querySelector('.monitor-status');
const overallProgressDiv = controlPanel.querySelector('.overall-progress');
if (currentCourseDiv) currentCourseDiv.textContent = currentCourse || '无';
if (learningProgressDiv) learningProgressDiv.textContent = progress || '0%';
// 获取剩余时间并显示
const leaveTimeElement = document.getElementById('spanLeavTimes');
let timeLeft = '未知';
if (leaveTimeElement && leaveTimeElement.textContent) {
timeLeft = leaveTimeElement.textContent.trim();
}
if (monitorStatusDiv) {
if (status === '监控中' || status === '监控中...') {
monitorStatusDiv.textContent = `监控中 (剩余: ${timeLeft})`;
} else {
monitorStatusDiv.textContent = status || '未开始';
}
}
// 更新总体进度
if (overallProgressDiv && autoLearningState) {
const currentIndex = autoLearningState.currentCourseIndex || 0;
const totalCourses = autoLearningState.courseLinks ? autoLearningState.courseLinks.length : 0;
overallProgressDiv.textContent = `${currentIndex + 1}/${totalCourses}`;
}
}
}
// 使面板可拖拽
function makeDraggable(element) {
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
const header = element.querySelector('.panel-header');
header.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
function dragStart(e) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === header || header.contains(e.target)) {
isDragging = true;
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
element.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
}
}
function dragEnd() {
initialX = currentX;
initialY = currentY;
isDragging = false;
}
}
// 创建控制面板
function createControlPanel() {
if (controlPanel) {
controlPanel.remove();
}
controlPanel = document.createElement('div');
controlPanel.id = 'yunxuetang-monitor-panel';
const isLearningPage = checkAutoLearningPage();
controlPanel.innerHTML = `
<div class="panel-header">
<span>云学堂监控面板</span>
<button class="close-btn" onclick="this.parentElement.parentElement.style.display='none'">×</button>
</div>
<div class="panel-body">
${!isLearningPage ? `
<div class="section">
<div class="section-title">课程提取</div>
<button id="extract-btn" class="btn-primary">提取并开始学习</button>
<button id="pause-extract-btn" class="btn-danger" style="display: none;">暂停提取</button>
<div class="progress">
<div class="progress-bar" style="width: 0%">0%</div>
</div>
<div class="status extract-status">等待提取</div>
<div class="info-row">
<span class="info-label">发现课程:</span>
<span class="info-value course-count">0</span>
</div>
</div>
` : ''}
${isLearningPage ? `
<div class="section">
<div class="section-title">学习监控</div>
<div class="info-row">
<span class="info-label">总体进度:</span>
<span class="info-value overall-progress">0/0</span>
</div>
<div class="info-row">
<span class="info-label">当前课程:</span>
<span class="info-value current-course">无</span>
</div>
<div class="info-row">
<span class="info-label">学习进度:</span>
<span class="info-value learning-progress">0%</span>
</div>
<div class="info-row">
<span class="info-label">监控状态:</span>
<span class="info-value monitor-status">未开始</span>
</div>
<button id="start-monitor-btn" class="btn-success">开始监控</button>
<button id="stop-monitor-btn" class="btn-danger">停止监控</button>
</div>
` : ''}
<div class="section">
<div class="section-title">控制操作</div>
<button id="clear-log-btn" class="btn-secondary">清空日志</button>
${isLearningPage ? `<button id="show-courses-btn" class="btn-secondary">查看课程列表</button>` : ''}
</div>
${isLearningPage ? `
<div class="section" id="course-links-section" style="display: none;">
<div class="section-title">课程列表</div>
<div class="course-links"></div>
</div>
` : ''}
<div class="section">
<div class="section-title">运行日志</div>
<div class="log"></div>
</div>
</div>
`;
document.body.appendChild(controlPanel);
makeDraggable(controlPanel);
// 绑定事件
const extractBtn = controlPanel.querySelector('#extract-btn');
const pauseExtractBtn = controlPanel.querySelector('#pause-extract-btn');
const startMonitorBtn = controlPanel.querySelector('#start-monitor-btn');
const stopMonitorBtn = controlPanel.querySelector('#stop-monitor-btn');
const clearLogBtn = controlPanel.querySelector('#clear-log-btn');
const showCoursesBtn = controlPanel.querySelector('#show-courses-btn');
if (extractBtn) {
extractBtn.addEventListener('click', () => {
if (!isExtracting) {
extractCourseLinks();
}
});
}
if (pauseExtractBtn) {
pauseExtractBtn.addEventListener('click', () => {
pauseExtraction();
});
}
if (startMonitorBtn) {
startMonitorBtn.addEventListener('click', () => {
startLearningMonitor();
});
}
if (stopMonitorBtn) {
stopMonitorBtn.addEventListener('click', () => {
if (learningMonitorInterval) {
clearInterval(learningMonitorInterval);
learningMonitorInterval = null;
addLog('学习监控已停止', 'warning');
}
});
}
if (clearLogBtn) {
clearLogBtn.addEventListener('click', () => {
const logContainer = controlPanel.querySelector('.log');
if (logContainer) {
logContainer.innerHTML = '';
}
});
}
if (showCoursesBtn) {
showCoursesBtn.addEventListener('click', () => {
toggleCourseLinks();
});
}
addLog('控制面板已初始化', 'success');
}
// 切换课程链接显示
function toggleCourseLinks() {
const courseLinksSection = controlPanel.querySelector('#course-links-section');
const courseLinksContainer = controlPanel.querySelector('.course-links');
const showCoursesBtn = controlPanel.querySelector('#show-courses-btn');
if (courseLinksSection.style.display === 'none') {
// 显示课程列表
if (autoLearningState && autoLearningState.courseLinks) {
courseLinksContainer.innerHTML = '';
autoLearningState.courseLinks.forEach((course, index) => {
if (course && course.title && course.url) {
const courseItem = document.createElement('div');
courseItem.className = 'course-link-item';
courseItem.innerHTML = `
<div class="course-link-title">${index + 1}. ${course.title}</div>
<div class="course-link-url">${course.url}</div>
`;
courseItem.addEventListener('click', () => {
window.open(course.url, '_blank');
});
courseLinksContainer.appendChild(courseItem);
}
});
courseLinksSection.style.display = 'block';
showCoursesBtn.textContent = '隐藏课程列表';
addLog(`显示课程列表,共 ${autoLearningState.courseLinks.length} 门课程`, 'info');
} else {
addLog('暂无课程数据', 'warning');
}
} else {
// 隐藏课程列表
courseLinksSection.style.display = 'none';
showCoursesBtn.textContent = '查看课程列表';
}
}
// 暂停提取函数
function pauseExtraction() {
if (isExtracting) {
isExtracting = false;
addLog('用户暂停了课程提取', 'warning');
updateProgress(0, 0, '提取已暂停');
// 重置按钮状态
resetExtractButtons();
}
}
// 重置提取按钮状态
function resetExtractButtons() {
const extractBtn = controlPanel.querySelector('#extract-btn');
const pauseExtractBtn = controlPanel.querySelector('#pause-extract-btn');
if (extractBtn) {
extractBtn.style.display = 'block';
extractBtn.textContent = '提取并开始学习';
}
if (pauseExtractBtn) {
pauseExtractBtn.style.display = 'none';
}
}
// 提取具体课程链接的函数
function extractCourseLinks() {
if (isExtracting) {
addLog('提取正在进行中,请等待...', 'warning');
return;
}
isExtracting = true;
addLog('开始提取具体课程链接...', 'info');
updateProgress(0, 1, '正在扫描页面...');
// 更新按钮状态
const extractBtn = controlPanel.querySelector('#extract-btn');
const pauseExtractBtn = controlPanel.querySelector('#pause-extract-btn');
if (extractBtn) {
extractBtn.style.display = 'none';
}
if (pauseExtractBtn) {
pauseExtractBtn.style.display = 'block';
}
// 获取当前页面的所有课程链接
const packageLinks = [];
const directCourseLinks = [];
const rows = document.querySelectorAll('tr[onclick*="StudyRowClick"]');
addLog(`发现 ${rows.length} 个课程行`, 'info');
updateProgress(1, 3, '正在分析课程链接...');
rows.forEach((row, index) => {
const onclickAttr = row.getAttribute('onclick');
if (onclickAttr && onclickAttr.includes('StudyRowClick')) {
const match = onclickAttr.match(/StudyRowClick\('([^']+)'/);
if (match && match[1]) {
const originalPath = match[1];
// 检查是否为直接课程链接(新格式)
if (originalPath.includes('/plan/video/') || originalPath.includes('/plan/document/')) {
const fullUrl = 'https://asiainfo.yunxuetang.cn/kng' + originalPath;
// 获取课程标题
const titleElement = row.querySelector('.st');
const title = titleElement ? (titleElement.textContent || titleElement['innerText'] || titleElement.getAttribute('title') || `课程${index + 1}`).trim() : `课程${index + 1}`;
// 确定课程类型
const courseType = originalPath.includes('/plan/video/') ? 'video' : 'document';
directCourseLinks.push({
title: title,
url: fullUrl,
originalPath: originalPath,
type: courseType,
packageTitle: '直接课程'
});
}
// 检查是否为课程包链接(原有格式和新格式)
else if (originalPath.includes('/package/') || originalPath.includes('/plan/package/')) {
const fullUrl = 'https://asiainfo.yunxuetang.cn/kng' + originalPath;
// 获取课程标题
const titleElement = row.querySelector('.ellipsis a, .ellipsis, .st');
const title = titleElement ? (titleElement.textContent || titleElement['innerText'] || titleElement.getAttribute('title') || `课程${index + 1}`).trim() : `课程${index + 1}`;
packageLinks.push({
title: title,
url: fullUrl,
originalPath: originalPath
});
}
}
}
});
// 统一处理所有类型的链接
// 更新课程数量显示
const totalCourses = directCourseLinks.length + packageLinks.length;
if (controlPanel) {
const courseCountDiv = controlPanel.querySelector('.course-count');
if (courseCountDiv) {
courseCountDiv.textContent = totalCourses.toString();
}
}
updateProgress(2, 3, '正在处理课程链接...');
if (directCourseLinks.length > 0 && packageLinks.length > 0) {
// 既有直接课程链接又有课程包链接,需要合并处理
addLog(`找到 ${directCourseLinks.length} 个直接课程链接和 ${packageLinks.length} 个课程包`, 'success');
addLog('提取到的直接课程链接: ' + directCourseLinks.map(c => c.title).join(', '), 'info');
// 先处理课程包获取具体课程链接,然后与直接课程链接合并
processCoursePackagesWithDirectLinks(packageLinks, directCourseLinks);
} else if (directCourseLinks.length > 0) {
// 只有直接课程链接
addLog(`找到 ${directCourseLinks.length} 个直接课程链接,开始自动学习循环`, 'success');
addLog('提取到的直接课程链接: ' + directCourseLinks.map(c => c.title).join(', '), 'info');
updateProgress(3, 3, '开始学习循环...');
startAutoLearningLoop(directCourseLinks);
isExtracting = false;
resetExtractButtons();
} else if (packageLinks.length > 0) {
// 只有课程包链接
addLog(`找到 ${packageLinks.length} 个课程包,开始获取具体课程链接...`, 'info');
processCoursePackages(packageLinks);
} else {
addLog('❌ 未找到课程包链接或直接课程链接!', 'error');
updateProgress(0, 1, '未找到课程');
isExtracting = false;
resetExtractButtons();
}
}
// 处理既有直接课程链接又有课程包链接的情况
async function processCoursePackagesWithDirectLinks(packageLinks, directCourseLinks) {
const allCourseLinks = [...directCourseLinks]; // 先添加直接课程链接
addLog(`开始处理 ${packageLinks.length} 个课程包...`, 'info');
for (let i = 0; i < packageLinks.length; i++) {
// 检查是否被用户暂停
if (!isExtracting) {
addLog('提取已被用户暂停', 'warning');
return;
}
const packageInfo = packageLinks[i];
addLog(`正在处理第 ${i + 1}/${packageLinks.length} 个课程包: ${packageInfo.title}`, 'info');
updateProgress(i, packageLinks.length, `处理课程包: ${packageInfo.title}`);
try {
const courseLinks = await fetchCourseLinksFromPackage(packageInfo);
allCourseLinks.push(...courseLinks);
addLog(`课程包 ${packageInfo.title} 包含 ${courseLinks.length} 个课程`, 'success');
// 添加延迟避免请求过快
if (i < packageLinks.length - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (error) {
addLog(`处理课程包 ${packageInfo.title} 时出错: ${error.message}`, 'error');
}
}
// 输出结果并开始自动学习循环
if (allCourseLinks.length > 0) {
addLog(`成功提取 ${allCourseLinks.length} 个课程链接(包含 ${directCourseLinks.length} 个直接链接)`, 'success');
updateProgress(packageLinks.length, packageLinks.length, '开始学习循环...');
// 开始自动学习循环
startAutoLearningLoop(allCourseLinks);
} else {
addLog('未找到任何课程链接!', 'error');
}
isExtracting = false;
resetExtractButtons();
}
// 处理课程包的异步函数
async function processCoursePackages(packageLinks) {
const allCourseLinks = [];
addLog(`开始处理 ${packageLinks.length} 个课程包...`, 'info');
for (let i = 0; i < packageLinks.length; i++) {
// 检查是否被用户暂停
if (!isExtracting) {
addLog('提取已被用户暂停', 'warning');
return;
}
const packageInfo = packageLinks[i];
addLog(`正在处理第 ${i + 1}/${packageLinks.length} 个课程包: ${packageInfo.title}`, 'info');
updateProgress(i, packageLinks.length, `处理课程包: ${packageInfo.title}`);
try {
const courseLinks = await fetchCourseLinksFromPackage(packageInfo);
allCourseLinks.push(...courseLinks);
addLog(`课程包 ${packageInfo.title} 包含 ${courseLinks.length} 个课程`, 'success');
// 添加延迟避免请求过快
if (i < packageLinks.length - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (error) {
addLog(`处理课程包 ${packageInfo.title} 时出错: ${error.message}`, 'error');
}
}
// 输出结果并开始自动学习循环
if (allCourseLinks.length > 0) {
addLog(`成功提取 ${allCourseLinks.length} 个具体课程链接`, 'success');
updateProgress(packageLinks.length, packageLinks.length, '开始学习循环...');
// 开始自动学习循环
startAutoLearningLoop(allCourseLinks);
} else {
addLog('未找到任何具体课程链接!', 'error');
}
isExtracting = false;
resetExtractButtons();
}
// 从课程包页面获取具体课程链接
async function fetchCourseLinksFromPackage(packageInfo) {
return new Promise((resolve, reject) => {
// 使用GM_xmlhttpRequest发送请求
if (typeof window['GM_xmlhttpRequest'] === 'function') {
window['GM_xmlhttpRequest']({
method: 'GET',
url: packageInfo.url,
headers: {
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
'Cache-Control': 'no-cache'
},
onload: function (response) {
if (response.status === 200) {
const courseLinks = parseCourseLinksFromHTML(response.responseText, packageInfo.title);
addLog(`成功获取课程包 "${packageInfo.title}" 中的 ${courseLinks.length} 个课程`, 'success');
resolve(courseLinks);
} else {
const errorMsg = `获取课程包失败: ${packageInfo.title}, 状态码: ${response.status}`;
console.error(errorMsg);
addLog(errorMsg, 'error');
resolve([]);
}
},
onerror: function (error) {
const errorMsg = `网络错误,无法获取课程包: ${packageInfo.title}`;
console.error(errorMsg, error);
addLog(errorMsg, 'error');
resolve([]);
},
ontimeout: function () {
const errorMsg = `请求超时,无法获取课程包: ${packageInfo.title}`;
console.error(errorMsg);
addLog(errorMsg, 'error');
resolve([]);
},
timeout: 10000
});
} else {
// 如果GM_xmlhttpRequest不可用,使用fetch作为备选
fetch(packageInfo.url)
.then(response => response.text())
.then(html => {
const courseLinks = parseCourseLinksFromHTML(html, packageInfo.title);
addLog(`成功获取课程包 "${packageInfo.title}" 中的 ${courseLinks.length} 个课程`, 'success');
resolve(courseLinks);
})
.catch(error => {
const errorMsg = `获取课程包失败: ${packageInfo.title} - ${error.message}`;
console.error(errorMsg, error);
addLog(errorMsg, 'error');
resolve([]);
});
}
});
}
// 解析HTML内容,提取课程链接
function parseCourseLinksFromHTML(html, packageTitle) {
const courseLinks = [];
// 创建临时DOM来解析HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// 查找包含StudyRowClick的链接,匹配/package/video/、/package/document/、/plan/video/或/plan/document/
const linkPatterns = [
/StudyRowClick\('(\/package\/(video|document)\/[^']+\.html[^']*)'[^)]*\)/g,
/StudyRowClick\('(\/plan\/(video|document)\/[^']+\.html[^']*)'[^)]*\)/g
];
linkPatterns.forEach(linkPattern => {
let match;
while ((match = linkPattern.exec(html)) !== null) {
const originalPath = match[1];
const courseType = match[2]; // video 或 document
// 构建最终的课程URL
const finalUrl = 'https://asiainfo.yunxuetang.cn/kng/course' + originalPath;
// 尝试从HTML中提取课程标题
let courseTitle = `${packageTitle} - ${courseType === 'video' ? '视频' : '文档'}课程`;
// 尝试从StudyRowClick附近的HTML中提取更精确的标题
const titleMatch = html.match(new RegExp(`StudyRowClick\\('${originalPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^']*'[^>]*>([^<]+)<`, 'i'));
if (titleMatch && titleMatch[1]) {
courseTitle = titleMatch[1].trim();
}
courseLinks.push({
title: courseTitle,
url: finalUrl,
originalPath: originalPath,
packageTitle: packageTitle,
type: courseType
});
}
});
addLog(`从 ${packageTitle} 中提取到 ${courseLinks.length} 个具体课程链接`, 'success');
if (courseLinks.length > 0) {
addLog('提取到的课程链接: ' + courseLinks.map(link => `${link.title} (${link.type})`).join(', '), 'info');
}
return courseLinks;
}
// 自动学习循环系统
let autoLearningState = {
courseLinks: [],
currentIndex: 0,
isRunning: false,
completedCount: 0,
skippedCount: 0
};
// 从localStorage恢复自动学习状态
function loadAutoLearningState() {
try {
const savedState = localStorage.getItem('yunxuetang_auto_learning_state');
if (savedState) {
const parsed = JSON.parse(savedState);
autoLearningState = {
courseLinks: parsed.courseLinks || [],
currentIndex: parsed.currentIndex || 0,
isRunning: parsed.isRunning || false,
completedCount: parsed.completedCount || 0,
skippedCount: parsed.skippedCount || 0
};
addLog('已恢复自动学习状态', 'success');
}
} catch (e) {
addLog('恢复自动学习状态失败: ' + e.message, 'error');
}
}
// 保存自动学习状态到localStorage
function saveAutoLearningState() {
try {
localStorage.setItem('yunxuetang_auto_learning_state', JSON.stringify(autoLearningState));
} catch (e) {
addLog('保存自动学习状态失败: ' + e.message, 'error');
}
}
// 清除自动学习状态
function clearAutoLearningState() {
try {
localStorage.removeItem('yunxuetang_auto_learning_state');
autoLearningState = {
courseLinks: [],
currentIndex: 0,
isRunning: false,
completedCount: 0,
skippedCount: 0
};
} catch (e) {
addLog('清除自动学习状态失败: ' + e.message, 'error');
}
}
// 开始自动学习循环
function startAutoLearningLoop(courseLinks) {
autoLearningState = {
courseLinks: courseLinks,
currentIndex: 0,
isRunning: true,
completedCount: 0,
skippedCount: 0
};
// 保存状态到localStorage
saveAutoLearningState();
addLog(`开始自动学习循环,共 ${courseLinks.length} 个课程`, 'info');
processNextCourse();
}
// 处理下一个课程
function processNextCourse() {
if (!autoLearningState.isRunning || autoLearningState.currentIndex >= autoLearningState.courseLinks.length) {
// 学习循环完成
const totalCourses = autoLearningState.courseLinks.length;
const completed = autoLearningState.completedCount;
const skipped = autoLearningState.skippedCount;
addLog(`🎉 自动学习循环完成!总课程: ${totalCourses}, 新完成: ${completed}, 已跳过: ${skipped}`, 'success');
notifyUser(`自动学习完成!共 ${totalCourses} 门课程,新完成 ${completed} 门`);
// 清除自动学习状态
clearAutoLearningState();
return;
}
const currentCourse = autoLearningState.courseLinks[autoLearningState.currentIndex];
const progress = `[${autoLearningState.currentIndex + 1}/${autoLearningState.courseLinks.length}]`;
addLog(`${progress} 准备访问课程: ${currentCourse.title}`, 'info');
addLog(`${progress} 课程链接: ${currentCourse.url}`, 'info');
// 保存当前状态
saveAutoLearningState();
// 重置页面加载时间计时器和调试标志
window.lastPageLoadTime = null;
window.courseCompletionCheckStarted = false;
// 跳转到课程页面
window.location.href = currentCourse.url;
}
// 根据当前URL查找对应的课程索引
function findCourseIndexByUrl(currentUrl) {
if (!autoLearningState || !autoLearningState.courseLinks) {
return -1;
}
// 提取当前URL的关键部分用于匹配
for (let i = 0; i < autoLearningState.courseLinks.length; i++) {
const course = autoLearningState.courseLinks[i];
if (course.url && currentUrl.includes(course.url)) {
return i;
}
// 如果直接URL匹配失败,尝试提取课程ID进行匹配
const currentUrlMatch = currentUrl.match(/courseId=(\d+)/);
const courseUrlMatch = course.url.match(/courseId=(\d+)/);
if (currentUrlMatch && courseUrlMatch && currentUrlMatch[1] === courseUrlMatch[1]) {
return i;
}
}
return -1;
}
// 检查当前页面是否为自动学习模式下的课程页面
function checkAutoLearningPage() {
if (!autoLearningState.isRunning) {
return false;
}
const currentUrl = window.location.href;
// 根据当前URL查找正确的课程索引
const actualCourseIndex = findCourseIndexByUrl(currentUrl);
if (actualCourseIndex !== -1 && actualCourseIndex !== autoLearningState.currentIndex) {
addLog(`检测到课程索引不匹配,从索引 ${autoLearningState.currentIndex} 更新为 ${actualCourseIndex}`, 'warning');
autoLearningState.currentIndex = actualCourseIndex;
saveAutoLearningState();
}
const currentCourse = autoLearningState.courseLinks[autoLearningState.currentIndex];
// 检查当前URL是否匹配预期的课程URL
if (currentUrl.includes('/kng/course/package/') || currentUrl.includes('/kng/plan/')) {
const progress = `[${autoLearningState.currentIndex + 1}/${autoLearningState.courseLinks.length}]`;
addLog(`${progress} 已进入课程页面: ${currentCourse ? currentCourse.title : '未知课程'}`, 'info');
// 检查课程是否已完成
if (checkCourseCompletion()) {
addLog(`${progress} 课程已完成,跳转到下一个`, 'success');
autoLearningState.skippedCount++;
autoLearningState.currentIndex++;
// 保存状态
saveAutoLearningState();
// 延迟跳转到下一个课程
setTimeout(() => {
processNextCourse();
}, 2000);
return true;
} else {
addLog(`${progress} 课程未完成,开始学习监控`, 'info');
// 开始监控学习进度
startLearningMonitor();
return true;
}
}
return false;
}
// 检查课程完成状态
function checkCourseCompletion() {
// 首先检查页面是否已完全加载(避免页面加载初期的误判)
const videoElement = document.querySelector('video');
const scheduleElement = document.querySelector('#ScheduleText');
const countdownElement = document.querySelector('#spanLeavTimes');
// 多重检查确保页面真正加载完成
const pageFullyLoaded = document.readyState === 'complete' &&
(videoElement || scheduleElement || countdownElement) &&
document.body &&
document.body.children.length > 0;
// 如果页面未完全加载,直接返回false
if (!pageFullyLoaded) {
// 添加调试日志
const readyState = document.readyState;
const hasVideo = !!videoElement;
const hasSchedule = !!scheduleElement;
const hasCountdown = !!countdownElement;
const hasBody = !!document.body;
addLog(`页面未完全加载 - readyState: ${readyState}, video: ${hasVideo}, schedule: ${hasSchedule}, countdown: ${hasCountdown}, body: ${hasBody}`, 'debug');
return false;
}
// 额外等待确保DOM元素稳定(避免元素刚出现但内容未更新的情况)
const currentTime = Date.now();
if (!window.lastPageLoadTime) {
window.lastPageLoadTime = currentTime;
addLog('页面加载完成,开始1秒等待期确保DOM稳定', 'debug');
return false; // 第一次检测时先等待
}
// 页面加载后至少等待1秒再进行检测
const waitTime = currentTime - window.lastPageLoadTime;
if (waitTime < 1000) {
addLog(`DOM稳定等待中... (${waitTime}/1000ms)`, 'debug');
return false;
}
// 避免重复输出相同的调试信息
if (!window.courseCompletionCheckStarted) {
addLog('页面加载检测通过,开始课程完成状态检测', 'debug');
window.courseCompletionCheckStarted = true;
}
// 检查进度是否达到100%
if (scheduleElement) {
const scheduleText = scheduleElement.textContent || scheduleElement['innerText'] || '';
const progressMatch = scheduleText.match(/(\d+(?:\.\d+)?)%/);
if (progressMatch) {
const progress = parseFloat(progressMatch[1]);
if (progress >= 100) {
addLog('检测到课程进度已达到 ' + progress + '%', 'success');
return true;
}
}
}
// 检查是否有完成提示
const completedArea = document.querySelector('#divCompletedArea');
if (completedArea && completedArea['style'].display !== 'none') {
const completedText = completedArea.textContent || completedArea['innerText'] || '';
if (completedText.includes('恭喜您,您已完成本视频的学习')) {
addLog('检测到课程完成提示', 'success');
return true;
}
}
// 检查倒计时是否为0
if (countdownElement) {
const countdownText = countdownElement.textContent || countdownElement['innerText'] || '';
// 倒计时为0时判断为完成
if (countdownText.includes('0分钟0秒') || countdownText.includes('00:00')) {
// 额外检查:确保不是页面刚加载时的初始状态
if (scheduleElement) {
const scheduleText = scheduleElement.textContent || scheduleElement['innerText'] || '';
const progressMatch = scheduleText.match(/(\d+(?:\.\d+)?)%/);
if (progressMatch) {
const progress = parseFloat(progressMatch[1]);
// 只有进度大于0%时,倒计时为0才认为是真正完成
if (progress > 0) {
addLog(`检测到倒计时已结束且学习进度为 ${progress}%`, 'warning');
return true;
}
}
}
}
}
return false;
}
// 开始学习监控(复用现有逻辑)
function startLearningMonitor() {
// 确保当前索引是正确的
const currentUrl = window.location.href;
const actualCourseIndex = findCourseIndexByUrl(currentUrl);
if (actualCourseIndex !== -1 && actualCourseIndex !== autoLearningState.currentIndex) {
addLog(`监控开始前检测到课程索引不匹配,从索引 ${autoLearningState.currentIndex} 更新为 ${actualCourseIndex}`, 'warning');
autoLearningState.currentIndex = actualCourseIndex;
saveAutoLearningState();
}
const courseInfo = autoLearningState ? (autoLearningState.courseLinks[autoLearningState.currentIndex] && autoLearningState.courseLinks[autoLearningState.currentIndex].title) || '当前课程' : '当前课程';
addLog(`🎯 开始监控课程学习进度: ${courseInfo}`, 'info');
addLog('⏰ 监控间隔: 5秒,超时时间: 180分钟', 'info');
// 更新控制面板状态
if (autoLearningState) {
const currentCourse = autoLearningState.courseLinks[autoLearningState.currentIndex];
const courseTitle = currentCourse && currentCourse.title ? currentCourse.title : '未知课程';
updateLearningStatus(courseTitle, '0%', '监控中');
}
// 设置监控间隔
learningMonitorInterval = setInterval(() => {
// 检查当前页面的课程索引是否正确
if (autoLearningState) {
const currentUrl = window.location.href;
const actualCourseIndex = findCourseIndexByUrl(currentUrl);
if (actualCourseIndex !== -1 && actualCourseIndex !== autoLearningState.currentIndex) {
addLog(`监控中检测到课程索引不匹配,从索引 ${autoLearningState.currentIndex} 更新为 ${actualCourseIndex}`, 'warning');
autoLearningState.currentIndex = actualCourseIndex;
saveAutoLearningState();
}
}
const currentTime = new Date().toLocaleTimeString();
const progress = autoLearningState ? `[${autoLearningState.currentIndex + 1}/${autoLearningState.courseLinks.length}]` : '[单独监控]';
// 更新学习状态到控制面板
if (autoLearningState) {
const currentCourse = autoLearningState.courseLinks[autoLearningState.currentIndex];
const progressPercent = Math.round(((autoLearningState.currentIndex + 1) / autoLearningState.courseLinks.length) * 100);
updateLearningStatus(currentCourse ? (currentCourse.title || '未知课程') : '未知课程', `${progressPercent}%`, '监控中...');
}
// 检查并处理"继续学习"弹窗
checkAndClickContinueButton();
if (checkCourseCompletion()) {
clearInterval(learningMonitorInterval);
const progress = `[${autoLearningState.currentIndex + 1}/${autoLearningState.courseLinks.length}]`;
const currentCourse = autoLearningState.courseLinks[autoLearningState.currentIndex];
addLog(`${progress} 课程学习完成: ${currentCourse.title}`, 'success');
autoLearningState.completedCount++;
autoLearningState.currentIndex++;
// 保存状态
saveAutoLearningState();
// 延迟跳转到下一个课程
setTimeout(() => {
processNextCourse();
}, 3000);
}
}, 5000); // 每5秒检查一次
// 设置超时保护(180分钟)
setTimeout(() => {
clearInterval(learningMonitorInterval);
addLog('课程学习监控超时,跳转到下一个课程', 'warning');
autoLearningState.currentIndex++;
processNextCourse();
}, 180 * 60 * 1000);
}
// 等待页面完全加载
function waitForPageLoad() {
return new Promise((resolve) => {
if (document.readyState === 'complete') {
setTimeout(resolve, 2000); // 额外等待2秒确保动态内容加载
} else {
window.addEventListener('load', () => {
setTimeout(resolve, 2000);
});
}
});
}
// 自动滚动功能
function performAutoScroll() {
addLog('🔄 开始执行自动滚动,保持页面活跃状态 - 时间: ' + new Date().toLocaleTimeString(), 'info');
// 检查并处理"继续学习"弹窗
checkAndClickContinueButton();
// 记录当前滚动位置
const currentScrollY = window.scrollY;
addLog('📍 当前滚动位置: ' + currentScrollY, 'info');
// 第一步:平滑滚动到底部
addLog('⬇️ 开始滚动到页面底部', 'info');
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
// 等待2秒后滚动到顶部
setTimeout(() => {
addLog('⬆️ 开始滚动到页面顶部', 'info');
window.scrollTo({
top: 0,
behavior: 'smooth'
});
// 再等待1.5秒后回到原位置
setTimeout(() => {
addLog('🎯 回到原始位置: ' + currentScrollY, 'info');
window.scrollTo({
top: currentScrollY,
behavior: 'smooth'
});
addLog('✅ 自动滚动完成,下次执行时间: ' + new Date(Date.now() + 3 * 60 * 1000).toLocaleTimeString(), 'success');
addLog('滚动已恢复到原位置', 'info');
}, 1500);
}, 2000);
}
// 连续检测计数器
let continueButtonClickCount = 0;
let lastContinueButtonDetectTime = 0;
// 检查并自动点击"继续学习"按钮
function checkAndClickContinueButton() {
const continueButton = document.querySelector('#reStartStudy');
const warningView = document.querySelector('#dvWarningView');
if (continueButton && warningView) {
// 检查弹窗是否可见
const style = window.getComputedStyle(warningView);
if (style.display !== 'none' && style.visibility !== 'hidden') {
const currentTime = Date.now();
// 如果距离上次检测时间超过30秒,重置计数器
if (currentTime - lastContinueButtonDetectTime > 30000) {
continueButtonClickCount = 0;
}
continueButtonClickCount++;
lastContinueButtonDetectTime = currentTime;
addLog(`检测到"继续学习"弹窗,第${continueButtonClickCount}次尝试点击继续学习按钮`, 'warning');
continueButton['click']();
// 如果连续检测到10次且点击无效,尝试刷新页面
if (continueButtonClickCount >= 10) {
addLog('连续检测到"继续学习"弹窗10次,点击可能无效,尝试刷新页面解决问题', 'error');
continueButtonClickCount = 0; // 重置计数器
setTimeout(() => {
addLog('正在刷新页面以解决"继续学习"按钮无响应问题...', 'warning');
window.location.reload();
}, 2000); // 延迟2秒后刷新,给最后一次点击一些时间
}
return true;
}
}
// 如果没有检测到弹窗,重置计数器
if (Date.now() - lastContinueButtonDetectTime > 60000) {
continueButtonClickCount = 0;
}
return false;
}
// 启动自动滚动定时器
function startAutoScroll() {
// 每3分钟执行一次自动滚动
autoScrollInterval = setInterval(performAutoScroll, 3 * 60 * 1000);
addLog('自动滚动功能已启动,每3分钟执行一次,下次执行时间: ' + new Date(Date.now() + 3 * 60 * 1000).toLocaleTimeString(), 'info');
// 立即执行一次自动滚动
setTimeout(() => {
addLog('执行首次自动滚动', 'info');
performAutoScroll();
}, 5000); // 5秒后执行首次滚动
}
// 停止自动滚动
function stopAutoScroll() {
if (autoScrollInterval) {
clearInterval(autoScrollInterval);
autoScrollInterval = null;
addLog('自动滚动功能已停止', 'info');
}
}
// 检测是否为学习页面
function isLearningPage() {
// 检查URL是否包含课程相关路径
const url = window.location.href;
return url.startsWith('https://asiainfo.yunxuetang.cn/kng/course/package/video') || url.startsWith('https://asiainfo.yunxuetang.cn/kng/course/package/document')
}
// 获取当前页面的课程标题
function getCurrentCourseTitle() {
// 直接使用页面标题
const pageTitle = document.title;
if (pageTitle && pageTitle !== '云学堂') {
return pageTitle;
}
return '当前课程';
}
// 简化通知函数 - 仅使用浏览器原生通知
function notifyUser(message, courseTitle = null) {
// 获取课程标题
const currentCourseTitle = courseTitle || getCurrentCourseTitle();
const fullMessage = `课程:${currentCourseTitle}\n${message}`;
// 控制台输出
addLog('🎉 云学堂监控通知: ' + fullMessage, 'success');
// 浏览器原生通知
if ('Notification' in window) {
if (Notification.permission === 'granted') {
const browserNotification = new Notification('🎓 云学堂课程完成', {
body: fullMessage,
icon: 'https://picobd.yunxuetang.cn/sys/asiainfo/others/202305/efc8749aa9474334a99be88b3c1131e5.ico',
requireInteraction: false,
silent: false,
tag: 'yunxuetang-course-complete'
});
// 点击通知时聚焦到当前标签页
browserNotification.onclick = function () {
window.focus();
browserNotification.close();
};
// 8秒后自动关闭浏览器通知
setTimeout(() => {
browserNotification.close();
}, 8000);
} else if (Notification.permission === 'default') {
// 请求通知权限
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
setTimeout(() => notifyUser(message, courseTitle), 100);
}
});
}
}
}
// 请求通知权限
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
// 监控状态
let lastLeaveTime = '';
let lastProgress = '';
let lastLogTime = 0; // 用于控制日志输出频率
// 监控函数
function checkConditions() {
if (notificationShown || !pageLoaded) return;
// 检查倒计时元素
const leaveTimeElement = document.getElementById('spanLeavTimes');
if (leaveTimeElement && leaveTimeElement.textContent) {
const timeText = leaveTimeElement.textContent.trim();
if (timeText !== lastLeaveTime) {
lastLeaveTime = timeText;
// 只在每30秒输出一次倒计时日志,减少日志频率
const currentTime = Date.now();
if (currentTime - lastLogTime > 30000) {
addLog('当前倒计时: ' + timeText, 'info');
lastLogTime = currentTime;
}
}
// 只在倒计时真正结束时通知(0分钟0秒或类似格式)
if (timeText.match(/^0[分:]0[0秒]?$/) || timeText === '0分钟0秒' || timeText === '00:00' || timeText === '0分钟') {
addLog('倒计时已结束!', 'success');
notifyUser('倒计时已结束!');
notificationShown = true;
return;
}
}
// 检查进度元素
const progressElement = document.getElementById('ScheduleText');
if (progressElement && progressElement.textContent) {
const progressText = progressElement.textContent.trim();
if (progressText !== lastProgress) {
lastProgress = progressText;
addLog('当前进度: ' + progressText, 'info');
}
// 检查是否显示 "100%" 或接近100%
const progressMatch = progressText.match(/(\d+(?:\.\d+)?)%/);
if (progressMatch) {
const progressValue = parseFloat(progressMatch[1]);
if (progressValue >= 99.5) {
addLog('学习进度已达到100%!', 'success');
notifyUser('学习进度已完成!当前进度: ' + progressText);
notificationShown = true;
return;
}
}
}
// 检查完成提示元素
const finishElement = document.getElementById('spanFinishContent');
const completedArea = document.getElementById('divCompletedArea');
// 检查完成区域是否显示
if (completedArea && completedArea.style.display !== 'none') {
if (finishElement && finishElement.textContent) {
const finishText = finishElement.textContent.trim();
addLog('完成状态: ' + finishText, 'info');
// 检查是否包含 "恭喜您已完成本视频的学习"
if (finishText.includes('恭喜您已完成本视频的学习')) {
addLog('视频学习已完成!', 'success');
notifyUser('视频学习完成!' + finishText);
notificationShown = true;
return;
}
}
}
}
// 启动监控
addLog('云学堂监控脚本已启动,等待页面加载完成...', 'info');
// 等待页面加载完成后再开始监控
waitForPageLoad().then(() => {
pageLoaded = true;
addLog('页面加载完成,开始监控学习状态', 'success');
// 恢复自动学习状态
loadAutoLearningState();
// 检查是否为自动学习模式下的课程页面
if (checkAutoLearningPage()) {
addLog('检测到自动学习模式,已处理当前页面', 'info');
}
// 使用 MutationObserver 监控DOM变化
const observer = new MutationObserver(checkConditions);
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
// 定期检查作为备用方案
setInterval(checkConditions, 10000); // 每10秒检查一次
// 只在学习页面启动自动滚动功能
if (isLearningPage()) {
startAutoScroll();
addLog('检测到学习页面,已启动自动滚动功能', 'info');
} else {
addLog('非学习页面,跳过自动滚动功能', 'info');
}
// 立即执行一次检查
checkConditions();
// 初始化控制面板
setTimeout(() => {
createControlPanel();
// 控制面板创建后,立即更新显示状态
if (autoLearningState && autoLearningState.courseLinks && autoLearningState.courseLinks.length > 0) {
// 更新课程数量显示
if (controlPanel) {
const courseCountDiv = controlPanel.querySelector('.course-count');
if (courseCountDiv) {
courseCountDiv.textContent = autoLearningState.courseLinks.length.toString();
}
}
// 如果是学习页面,更新学习状态显示
if (checkAutoLearningPage()) {
const currentCourse = autoLearningState.courseLinks[autoLearningState.currentIndex];
if (currentCourse) {
updateLearningStatus(currentCourse.title || '未知课程', '0%', '已恢复状态');
}
}
addLog(`已恢复课程数据,共 ${autoLearningState.courseLinks.length} 门课程`, 'success');
}
}, 1000);
// 页面卸载时清理
window.addEventListener('beforeunload', function () {
observer.disconnect();
stopAutoScroll();
});
});
addLog('云学堂视频学习监控脚本已加载', 'success');
})();