您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
[API版] cela自动学习脚本,支持浦东分院课程列表页面,支持专栏详情页面课程获取,修复进度上报API端点,基于真实API分析优化。
// ==UserScript== // @name cela-自动学习脚本API版 // @namespace https://github.com/Moker32/ // @version 3.37.4 // @description [API版] cela自动学习脚本,支持浦东分院课程列表页面,支持专栏详情页面课程获取,修复进度上报API端点,基于真实API分析优化。 // @author Moker32 // @license GPL-3.0-or-later // @changelog v3.37.4: 移除UI配置功能,使用固定默认配置:智能学习模式、跳过已完成课程、启用学习记录、关闭调试模式 // @grant GM_getValue // @grant GM_setValue // @match *://cela.e-celap.cn/* // @match *://pudong.e-celap.cn/* // @match *://pd.cela.cn/* // @match *://*.e-celap.cn/* // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect cela.e-celap.cn // @connect pudong.e-celap.cn // @connect pd.cela.cn // @connect zpyapi.shsets.com // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // --- 常量集中管理 --- const CONSTANTS = { API_ENDPOINTS: { GET_PLAY_TREND: '/inc/nc/course/play/getPlayTrend', PULSE_SAVE_RECORD: '/inc/nc/course/play/pulseSaveRecord', REPORT_PROGRESS: '/inc/nc/course/play/reportProgress', UPDATE_PROGRESS: '/inc/nc/course/play/updateProgress', START_STUDY: '/inc/nc/course/play/startStudy', FINISH_STUDY: '/inc/nc/course/play/finishStudy', COMPLETE_COURSE: '/inc/nc/course/completeCourse', FINISH_LEARNING: '/inc/nc/course/finishLearning', UPDATE_STATUS: '/inc/nc/course/updateStatus', SUBMIT_LEARNING_RECORD: '/inc/nc/course/submitLearningRecord', GRADUATE: '/inc/nc/course/graduate', GET_STUDY_RECORD: '/inc/nc/course/getStudyRecord', CREATE_SESSION: '/inc/nc/course/createSession', INIT_STUDY_RECORD: '/inc/nc/course/initStudyRecord', COMPLETE_LEARNING: '/inc/nc/course/completeLearning', SAVE_COMPLETION_STATUS: '/inc/nc/course/saveCompletionStatus', SAVE_STUDY_RECORD: '/inc/nc/course/saveStudyRecord', GET_COURSEWARE_DETAIL: '/inc/nc/course/play/getCoursewareDetail', GET_PACK_BY_ID: '/inc/nc/pack/getById', GET_COURSE_LIST: '/api/course/list', PUSH_COURSE: '/dsfa/nc/cela/api/pushCourse', GET_COURSE_BY_USER: '/dsfa/nc/cela/api/getCourseByUserAndCourse', VIDEO_PROGRESS: '/api/player/progress' }, SELECTORS: { PANEL: '#api-learner-panel', STATUS_DISPLAY: '#learner-status', PROGRESS_INNER: '#learner-progress-inner', TOGGLE_BTN: '#toggle-learning-btn', LOG_CONTAINER: '#api-learner-panel .log-container', STAT_TOTAL: '#stat-total', STAT_COMPLETED: '#stat-completed', STAT_LEARNED: '#stat-learned', STAT_SKIPPED: '#stat-skipped', APP: '#app' }, STORAGE_KEYS: { TOKEN: 'token', AUTH_TOKEN: 'authToken', ACCESS_TOKEN: 'access_token', USER_ID: 'userId', USER_ID_ALT: 'user_id' }, COURSE_SELECTORS: [ '[class*="course"]', '[data-course]', '.course-item', '.lesson-item', '.el-card', '.el-card__body', '.course-card', '.course-box', '.nc-course-item', '.study-item', '.learn-item', '[class*="item"]', '[class*="card"]', '[data-id]', '.pudong-course', '.pd-course', '.dsf-course' ], FALLBACK_SELECTORS: [ 'div[class*="list"] > div', 'ul > li', '.content div', '#app div[class]', '[class*="container"] > div' ], VIDEO_SELECTORS: [ 'video', '[api-base-url]', '[class*="video"]', 'iframe[src*="play"]' ], COOKIE_PATTERNS: { USER_ID: /userId=([^;]+)/, TOKEN: /token=([^;]+)/, P_PARAM: /_p=([^;]+)/ }, TIME_FORMATS: { DEFAULT_DURATION: 1800, // 30分钟 MOCK_DURATION: 1800 }, UI_LIMITS: { MAX_LOG_ENTRIES: 50, LOG_FLUSH_DELAY: 100 } }; // --- 事件驱动机制 (v2.0优化) --- const EventBus = { events: {}, subscribe(event, listener) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(listener); return () => { // 返回取消订阅函数 const index = this.events[event].indexOf(listener); if (index > -1) { this.events[event].splice(index, 1); } }; }, publish(event, data) { if (!this.events[event]) return; this.events[event].forEach(listener => { try { listener(data); } catch (error) { console.error(`EventBus error in ${event}:`, error); } }); }, // 一次性事件监听 once(event, listener) { const unsubscribe = this.subscribe(event, (data) => { unsubscribe(); listener(data); }); return unsubscribe; } }; // --- 配置管理模块 (v2.0优化) --- const Settings = { defaultConfig: { // === 固定配置 (不可修改) === LEARNING_STRATEGY: 'smart', // 智能学习模式 SKIP_COMPLETED_COURSES: true, // 跳过已完成课程 STUDY_RECORD_ENABLED: true, // 启用学习记录 FALLBACK_MODE: true, // 启用兜底模式 DEBUG_MODE: false, // 关闭调试模式 HEARTBEAT_INTERVAL: 10, // 进度上报间隔(秒) COMPLETION_THRESHOLD: 95, // 完成度阈值(%) // === 技术配置 (高级选项) === MAX_RETRY_ATTEMPTS: 10, // 最大重试次数 RETRY_DELAY: 3000, // 重试延迟(毫秒) COURSE_COMPLETION_DELAY: 5, // 课程完成延迟(秒) // === 自动配置 (系统检测) === PUDONG_MODE: false, // 浦东分院模式(自动检测) PUDONG_API_BASE: '', // 浦东API基础URL(自动设置) // === 向后兼容配置 (将被迁移) === FAST_LEARNING_MODE: true, // 兼容旧版本 ULTRA_FAST_MODE: true, // 兼容旧版本 SMART_LEARNING_MODE: true // 兼容旧版本 }, config: {}, load() { // 使用固定配置,不再从存储加载 this.config = { ...this.defaultConfig }; EventBus.publish('log', { message: '✅ 使用固定配置:智能学习模式', type: 'success' }); }, get(key) { return this.config[key]; } }; // --- 学习策略枚举 (v3.37.3优化) --- const LEARNING_STRATEGIES = { SLOW: 'slow', // 慢启动 - 模拟真实学习 NORMAL: 'normal', // 正常速度 - 平衡效率与安全 FAST: 'fast', // 快速学习 - 提高学习效率 ULTRA: 'ultra', // 超快速 - 直接完成 SMART: 'smart' // 智能自适应 - 根据环境调整 }; // --- 配置区域 (v3.37.4简化) --- const CONFIG = new Proxy({}, { get(target, prop) { // 优先从Settings获取配置,如果不存在则使用默认值 return Settings.get(prop) ?? target[prop]; }, set(target, prop, value) { // 固定配置模式,直接设置到target target[prop] = value; return true; } }); // 自动检测当前环境 const detectEnvironment = () => { const hostname = window.location.hostname; const href = window.location.href; // 检测是否为浦东分院 if (hostname.includes('pudong') || hostname.includes('pd.') || href.includes('浦东分院') || href.includes('pudong') || document.title.includes('浦东')) { CONFIG.PUDONG_MODE = true; console.log('🏢 检测到浦东分院环境'); } // 设置API基础URL if (CONFIG.PUDONG_MODE) { if (hostname.includes('pudong.e-celap.cn')) { CONFIG.PUDONG_API_BASE = `https://${hostname}`; } else if (hostname.includes('pd.cela.cn')) { CONFIG.PUDONG_API_BASE = `https://${hostname}`; } else { // 默认使用主站API CONFIG.PUDONG_API_BASE = 'https://cela.e-celap.cn'; } } console.log(`🌐 当前环境: ${CONFIG.PUDONG_MODE ? '浦东分院' : '主站'} (${hostname})`); console.log(`🔗 API基础URL: ${CONFIG.PUDONG_API_BASE || 'https://cela.e-celap.cn'}`); }; // --- UI和日志(优化版) --- const UI = { logs: [], logBuffer: [], // 日志缓冲区 logUpdateTimeout: null, statistics: { total: 0, completed: 0, learned: 0, failed: 0, skipped: 0 }, createPanel: () => { const panel = document.createElement('div'); panel.id = 'api-learner-panel'; panel.innerHTML = ` <div class="header"> API学习助手 v3.37.4 </div> <div class="content"> <div class="status">状态: <span id="learner-status">待命</span></div> <div class="statistics"> <div class="stat-item">总计: <span id="stat-total">0</span></div> <div class="stat-item">已完成: <span id="stat-completed">0</span></div> <div class="stat-item">新学习: <span id="stat-learned">0</span></div> <div class="stat-item">跳过: <span id="stat-skipped">0</span></div> </div> <div class="progress-bar"><div id="learner-progress-inner"></div></div> <div class="log-container"></div> </div> <div class="footer"> <button id="toggle-learning-btn" data-state="stopped">开始学习</button> <div class="feature-note">✨ 智能学习模式 + 自动记录</div> </div> `; document.body.appendChild(panel); UI.addStyles(); UI.initEventListeners(); }, // 优化后的日志函数 - 使用批量更新策略 log: function(message, type = 'info') { const timestamp = new Date().toLocaleTimeString(); const logMessage = `[${timestamp}] ${message}`; // 添加到缓冲区 this.logBuffer.push({ message: logMessage, type }); // 使用防抖处理,批量更新DOM if (this.logUpdateTimeout) clearTimeout(this.logUpdateTimeout); this.logUpdateTimeout = setTimeout(() => this.flushLogBuffer(), CONSTANTS.UI_LIMITS.LOG_FLUSH_DELAY); if (CONFIG.DEBUG_MODE) { const debugMessage = `[API Learner Debug] ${logMessage}`; console.log(debugMessage); this.logs.push(debugMessage); } }, // 初始化事件监听器 (v2.0新增) initEventListeners: function() { // 订阅事件 EventBus.subscribe('log', ({ message, type }) => this.log(message, type)); EventBus.subscribe('statusUpdate', status => this.updateStatus(status)); EventBus.subscribe('progressUpdate', progress => this.updateProgress(progress)); EventBus.subscribe('statisticsUpdate', stats => this.updateStatistics(stats)); }, // 批量刷新日志缓冲区 flushLogBuffer: function() { const logContainer = document.querySelector(CONSTANTS.SELECTORS.LOG_CONTAINER); if (!logContainer || this.logBuffer.length === 0) return; const fragment = document.createDocumentFragment(); this.logBuffer.forEach(log => { const logEntry = document.createElement('div'); logEntry.className = `log-entry ${log.type}`; logEntry.textContent = log.message; fragment.appendChild(logEntry); }); logContainer.appendChild(fragment); logContainer.scrollTop = logContainer.scrollHeight; // 限制日志条数,避免占用过多内存 const entries = logContainer.querySelectorAll('.log-entry'); if (entries.length > CONSTANTS.UI_LIMITS.MAX_LOG_ENTRIES) { for (let i = 0; i < entries.length - CONSTANTS.UI_LIMITS.MAX_LOG_ENTRIES; i++) { entries[i].remove(); } } this.logBuffer = []; // 清空缓冲区 }, updateStatus: (status) => { const statusEl = document.getElementById(CONSTANTS.SELECTORS.STATUS_DISPLAY.replace('#', '')); if (statusEl) statusEl.textContent = status; }, updateProgress: (percentage) => { const progressInner = document.getElementById(CONSTANTS.SELECTORS.PROGRESS_INNER.replace('#', '')); if (progressInner) progressInner.style.width = `${percentage}%`; }, updateStatistics: (stats) => { Object.assign(UI.statistics, stats); document.getElementById(CONSTANTS.SELECTORS.STAT_TOTAL.replace('#', '')).textContent = UI.statistics.total; document.getElementById(CONSTANTS.SELECTORS.STAT_COMPLETED.replace('#', '')).textContent = UI.statistics.completed; document.getElementById(CONSTANTS.SELECTORS.STAT_LEARNED.replace('#', '')).textContent = UI.statistics.learned; document.getElementById(CONSTANTS.SELECTORS.STAT_SKIPPED.replace('#', '')).textContent = UI.statistics.skipped; }, addStyles: () => { const styles = ` #api-learner-panel { position: fixed; bottom: 20px; right: 20px; width: 400px; background: #fff; border: 1px solid #ddd; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 99999; font-family: sans-serif; font-size: 14px; color: #333; } #api-learner-panel .header { background: #f7f7f7; padding: 10px 15px; font-weight: bold; border-bottom: 1px solid #ddd; border-radius: 8px 8px 0 0; } #api-learner-panel .content { padding: 15px; } #api-learner-panel .status { margin-bottom: 10px; } #api-learner-panel .statistics { display: flex; justify-content: space-between; margin-bottom: 10px; padding: 8px; background: #f9f9f9; border-radius: 4px; font-size: 12px; } #api-learner-panel .stat-item { text-align: center; } #api-learner-panel .progress-bar { height: 8px; background: #eee; border-radius: 4px; overflow: hidden; margin-bottom: 10px; } #api-learner-panel #learner-progress-inner { height: 100%; width: 0%; background: #4caf50; transition: width 0.3s ease; } #api-learner-panel .log-container { max-height: 120px; overflow-y: auto; background: #fafafa; padding: 8px; border: 1px solid #eee; border-radius: 4px; font-size: 11px; line-height: 1.4; } #api-learner-panel .log-entry.error { color: #f44336; } #api-learner-panel .log-entry.success { color: #4caf50; } #api-learner-panel .log-entry.warn { color: #ff9800; } #api-learner-panel .footer { padding: 10px 15px; border-top: 1px solid #ddd; text-align: right; } #api-learner-panel button { padding: 6px 10px; border: none; border-radius: 4px; cursor: pointer; margin-left: 8px; font-size: 12px; } #api-learner-panel button#toggle-learning-btn { background: #2196f3; color: white; transition: background-color 0.3s ease; } #api-learner-panel button#toggle-learning-btn[data-state="running"] { background: #f44336; } #api-learner-panel button:disabled { background: #ccc; } #api-learner-panel .feature-note { font-size: 11px; color: #666; margin-top: 8px; text-align: center; } `; const styleSheet = document.createElement("style"); styleSheet.type = "text/css"; styleSheet.innerText = styles; document.head.appendChild(styleSheet); }, exportLogs: () => { if (UI.logs.length === 0) { alert('没有可导出的调试日志。'); return; } const blob = new Blob([UI.logs.join('\r\n')], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `api_learner_debug_log_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } }; // --- 学习策略模式 (v2.0优化) --- const LearningStrategies = { // 策略执行的辅助函数,封装重复逻辑 async _executeSteps(context, steps, delay) { for (const targetTime of steps) { // 检查停止请求 if (Learner.stopRequested) { EventBus.publish('log', { message: '⏹️ 收到停止请求,中断策略执行', type: 'warn' }); return false; } await new Promise(resolve => setTimeout(resolve, delay)); const success = await API.reportProgress(context.playInfo, targetTime); if (!success) return false; context.currentTime = targetTime; const progress = Math.floor((context.currentTime / context.duration) * 100); EventBus.publish('log', { message: `[策略执行] 进度: ${progress}%`, type: 'info' }); } return true; }, // 慢启动策略:分3步到50%,然后快速完成 async slow_start(context) { const { duration } = context; EventBus.publish('log', { message: '[慢启动策略] 开始执行', type: 'info' }); const steps = [0.2, 0.35, 0.5].map(p => Math.floor(duration * p)); const success = await this._executeSteps(context, steps, 8000); if (success && !Learner.stopRequested) { // 最后冲刺到完成 await new Promise(resolve => setTimeout(resolve, 5000)); context.currentTime = duration - 10; return await API.reportProgress(context.playInfo, context.currentTime); } return success; }, // 渐进式策略:分5步完成 async progressive(context) { const { duration, currentTime } = context; EventBus.publish('log', { message: '[渐进式策略] 开始执行', type: 'info' }); const remaining = duration - currentTime; const stepSize = Math.floor(remaining / 5); const steps = []; for (let i = 1; i <= 5; i++) { const nextTime = Math.min(currentTime + (stepSize * i), duration - 10); steps.push(nextTime); } return await this._executeSteps(context, steps, 6000); }, // 快速完成策略:直接跳到95%然后完成 async fast_finish(context) { const { duration } = context; EventBus.publish('log', { message: '[快速完成策略] 开始执行', type: 'info' }); if (Learner.stopRequested) return false; const target95 = Math.floor(duration * 0.95); await new Promise(resolve => setTimeout(resolve, 3000)); let success = await API.reportProgress(context.playInfo, target95); if (success && !Learner.stopRequested) { await new Promise(resolve => setTimeout(resolve, 5000)); context.currentTime = duration - 10; success = await API.reportProgress(context.playInfo, context.currentTime); } return success; }, // 最后冲刺策略:直接完成 async final_push(context) { const { duration } = context; EventBus.publish('log', { message: '[最后冲刺策略] 开始执行', type: 'info' }); if (Learner.stopRequested) return false; await new Promise(resolve => setTimeout(resolve, 3000)); context.currentTime = duration - 10; return await API.reportProgress(context.playInfo, context.currentTime); }, // 超快速策略:直接跳到结束 async ultra_fast(context) { const { duration } = context; EventBus.publish('log', { message: '[超快速策略] 开始执行', type: 'info' }); if (Learner.stopRequested) return false; await new Promise(resolve => setTimeout(resolve, 2000)); context.currentTime = duration - 5; return await API.reportProgress(context.playInfo, context.currentTime); }, // 策略选择器 selectStrategy(currentProgress) { if (Settings.get('ULTRA_FAST_MODE')) { return 'ultra_fast'; } else if (currentProgress < 10) { return 'slow_start'; } else if (currentProgress < 50) { return 'progressive'; } else if (currentProgress < 80) { return 'fast_finish'; } else { return 'final_push'; } } }; // --- 工具函数 --- const Utils = { formatTime: function(seconds) { if (!seconds || seconds < 0) return '00:00:00'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } }; // --- API 核心(优化版) --- const API = { _baseUrl: 'https://cela.e-celap.cn', _videoApiBaseUrl: 'https://zpyapi.shsets.com', abortController: null, // AbortController 支持 // 动态获取基础URL getBaseUrl: function() { if (CONFIG.PUDONG_MODE && CONFIG.PUDONG_API_BASE) { return CONFIG.PUDONG_API_BASE; } return this._baseUrl; }, // 通用API端点尝试策略 (根据审查报告建议优化) async _tryApiEndpoints(apiCalls, successMessage, failureMessage) { for (let i = 0; i < apiCalls.length; i++) { // 检查是否被中止 if (this.abortController && this.abortController.signal.aborted) { throw new DOMException('Aborted', 'AbortError'); } try { const result = await apiCalls[i](); if (this._isSuccessResponse(result)) { EventBus.publish('log', { message: `${successMessage} (方法${i+1})`, type: 'success' }); return result; } EventBus.publish('log', { message: `[API Strategy] 方法${i+1}失败: ${result?.message || 'unknown error'}`, type: 'debug' }); } catch (error) { EventBus.publish('log', { message: `[API Strategy] 方法${i+1}异常: ${error.message}`, type: 'debug' }); if (error.name === 'AbortError') { throw error; // 重新抛出中止错误 } } } EventBus.publish('log', { message: failureMessage, type: 'warn' }); return null; }, // 统一的成功响应判断逻辑 _isSuccessResponse(result) { return result && ( result.success === true || result.code === 200 || result.state === 20000 || result.status === 'success' || (result.code >= 200 && result.code < 300) ); }, _request: async function(options) { return new Promise((resolve, reject) => { // 检查是否被中止 if (this.abortController && this.abortController.signal.aborted) { return reject(new DOMException('Aborted', 'AbortError')); } // 提取Cookie和其他认证信息 const cookies = document.cookie; const token = this._extractToken(); // 构建请求头 - 根据数据类型设置Content-Type const headers = { 'Accept': 'application/json, text/plain, */*', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'X-Requested-With': 'XMLHttpRequest', 'Referer': window.location.href, 'Origin': this.getBaseUrl(), 'Cookie': cookies, ...options.headers }; // 根据数据类型设置Content-Type if (options.data instanceof FormData) { // FormData会自动设置Content-Type,包括boundary // 不要手动设置Content-Type } else if (typeof options.data === 'string' && options.data.includes('=')) { // URL编码的表单数据 headers['Content-Type'] = 'application/x-www-form-urlencoded'; } else { // JSON数据 headers['Content-Type'] = 'application/json'; } // 如果有token,添加到请求头 if (token) { headers['Authorization'] = `Bearer ${token}`; headers['X-Auth-Token'] = token; } // 精简日志输出 if (CONFIG.DEBUG_MODE) { UI.log(`[API] ${options.method || 'GET'} ${options.url}`); } const req = GM_xmlhttpRequest({ method: options.method || 'GET', url: options.url, headers: headers, data: options.data, timeout: 30000, onload: function(response) { if (CONFIG.DEBUG_MODE) { UI.log(`[API] ${response.status} ${response.responseText?.substring(0, 100)}...`); } try { if (response.responseText && response.responseText.trim()) { const data = JSON.parse(response.responseText); resolve(data); } else { resolve({ status: response.status, message: 'Empty response' }); } } catch (parseError) { UI.log(`❌ JSON解析失败: ${parseError.message}`, 'error'); resolve({ error: 'JSON解析失败', raw: response.responseText }); } }, onerror: function(error) { UI.log(`❌ 请求失败: ${error.message}`, 'error'); reject(error); }, ontimeout: function() { UI.log(`❌ 请求超时`, 'error'); reject(new Error('请求超时')); } }); // 支持AbortController if (this.abortController) { this.abortController.signal.addEventListener('abort', () => { if (req.abort) { req.abort(); } reject(new DOMException('Aborted', 'AbortError')); }); } }); }, _extractToken: function() { // 尝试从多个位置提取认证token const sources = [ () => localStorage.getItem(CONSTANTS.STORAGE_KEYS.TOKEN), () => localStorage.getItem(CONSTANTS.STORAGE_KEYS.AUTH_TOKEN), () => localStorage.getItem(CONSTANTS.STORAGE_KEYS.ACCESS_TOKEN), () => sessionStorage.getItem(CONSTANTS.STORAGE_KEYS.TOKEN), () => sessionStorage.getItem(CONSTANTS.STORAGE_KEYS.AUTH_TOKEN), () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'), () => window.token, () => window.authToken, () => { const match = document.cookie.match(CONSTANTS.COOKIE_PATTERNS.TOKEN); return match ? match[1] : null; } ]; for (const source of sources) { try { const token = source(); if (token && token.length > 10) { UI.log(`[Token] 找到认证token: ${token.substring(0, 20)}...`, 'debug'); return token; } } catch (e) { // 忽略提取错误 } } UI.log('[Token] 未找到认证token', 'debug'); return null; }, /** * 进度上报 - 增强版,根据深度分析报告优化 * 支持真实API优先,智能降级到模拟模式 */ reportProgress: async function(playInfo, currentTime) { try { EventBus.publish('log', { message: `[进度上报] 课程: ${playInfo.courseId}, 当前时间: ${currentTime}秒`, type: 'debug' }); const isMockData = playInfo.videoId && playInfo.videoId.startsWith('mock_'); const progressPercent = Math.round((currentTime / playInfo.duration) * 100); if (isMockData) { EventBus.publish('log', { message: `[进度上报] 检测到模拟数据,将尝试真实API后再模拟`, type: 'debug' }); } // 构建真实API调用方法(即使是模拟数据也要尝试) const reportMethods = [ // 方法1: 脉冲式进度上报(最接近真实用户行为) () => this.pulseSaveRecord(playInfo, currentTime), // 方法2: 课程播放进度上报API () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.REPORT_PROGRESS}`, data: JSON.stringify({ courseId: playInfo.courseId, locationSiteId: playInfo.coursewareId, videoId: isMockData ? playInfo.courseId : playInfo.videoId, // 模拟数据时使用courseId currentTime: currentTime, watchPoint: this.secondsToTimeString(currentTime), duration: playInfo.duration, progress: progressPercent, _t: Date.now() }), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/plain, */*' } }), // 方法3: 视频播放进度更新API () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.UPDATE_PROGRESS}`, data: JSON.stringify({ courseId: playInfo.courseId, coursewareId: playInfo.coursewareId, progress: progressPercent, currentTime: currentTime, lastWatchPoint: this.secondsToTimeString(currentTime), _t: Date.now() }), headers: { 'Content-Type': 'application/json' } }), // 方法4: 学习记录保存API(适合模拟数据) () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.SAVE_STUDY_RECORD}`, data: JSON.stringify({ courseId: playInfo.courseId, videoId: isMockData ? playInfo.courseId : playInfo.videoId, studyTime: currentTime, totalTime: playInfo.duration, finishRate: progressPercent, watchPoint: this.secondsToTimeString(currentTime), _t: Date.now() }), headers: { 'Content-Type': 'application/json' } }) ]; // 如果不是模拟数据,添加视频服务API if (!isMockData) { reportMethods.push( () => this._request({ method: 'POST', url: `${this._videoApiBaseUrl}${CONSTANTS.API_ENDPOINTS.VIDEO_PROGRESS}`, data: JSON.stringify({ videoId: playInfo.videoId, currentTime: currentTime, duration: playInfo.duration, progress: progressPercent, _t: Date.now() }), headers: { 'Content-Type': 'application/json' } }) ); } // 尝试真实API调用 const result = await this._tryApiEndpoints( reportMethods, `[进度上报] 成功 (${progressPercent}%)`, `[进度上报] 所有API方法失败` ); if (result) { // 真实API成功 if (isMockData) { EventBus.publish('log', { message: `[进度上报] 模拟数据成功调用真实API`, type: 'success' }); } return result; } // 所有真实API都失败,使用兜底策略 if (CONFIG.FALLBACK_MODE) { if (isMockData) { EventBus.publish('log', { message: `[模拟上报] 模拟进度上报: ${currentTime}秒 (${progressPercent}%)`, type: 'info' }); } else { EventBus.publish('log', { message: `[进度上报] 启用兜底模式: ${currentTime}秒 (${progressPercent}%)`, type: 'warn' }); } // 模拟网络延迟 await new Promise(resolve => setTimeout(resolve, 500 + Math.random() * 1000)); return { code: 200, success: true, msg: isMockData ? 'mock success' : 'fallback success', data: { currentTime, progress: progressPercent } }; } else { throw new Error('所有进度上报方法失败且兜底模式已禁用'); } } catch (error) { if (error.name === 'AbortError') { EventBus.publish('log', { message: `[进度上报] 已中止`, type: 'warn' }); throw error; } EventBus.publish('log', { message: `[进度上报] 发生错误: ${error.message}`, type: 'error' }); // 最终兜底 if (CONFIG.FALLBACK_MODE) { return { code: 200, success: true, msg: 'error fallback success' }; } else { throw error; } } }, // 使用优化后的API策略重构学习记录创建 _createStudyRecord: async function(courseId) { try { UI.log(`[学习记录] 创建课程 ${courseId} 的学习记录`, 'debug'); const createMethods = [ // 方法1: 开始学习记录 () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.START_STUDY}`, data: new URLSearchParams({ courseId: courseId, startTime: new Date().toISOString(), _t: Date.now() }).toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }), // 方法2: 创建学习会话 () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.CREATE_SESSION}`, data: JSON.stringify({ courseId: courseId, sessionStart: new Date().toISOString(), _t: Date.now() }), headers: { 'Content-Type': 'application/json' } }), // 方法3: 学习记录初始化 () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.INIT_STUDY_RECORD}`, data: JSON.stringify({ courseId: courseId, initTime: new Date().toISOString(), _t: Date.now() }), headers: { 'Content-Type': 'application/json' } }) ]; const result = await this._tryApiEndpoints( createMethods, `[学习记录] 创建成功`, `[学习记录] 所有创建方法失败,将跳过记录创建` ); return !!result; } catch (error) { if (error.name === 'AbortError') { throw error; } UI.log(`[学习记录] 创建异常: ${error.message}`, 'warn'); return false; } }, // 使用优化后的API策略重构学习记录完成 finishStudyRecord: async function(playInfo, finalTime) { try { UI.log(`[学习记录] 完成课程 ${playInfo.courseId} 的学习记录`, 'debug'); const finishMethods = [ // 方法1: 完成学习API () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.FINISH_STUDY}`, data: new URLSearchParams({ courseId: playInfo.courseId, coursewareId: playInfo.coursewareId, studyTime: finalTime, finishTime: new Date().toISOString(), isFinished: true, _t: Date.now() }).toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }), // 方法2: 完成课程学习 () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.COMPLETE_LEARNING}`, data: JSON.stringify({ courseId: playInfo.courseId, finalTime: finalTime, duration: playInfo.duration, completionRate: 100, endTime: new Date().toISOString(), _t: Date.now() }), headers: { 'Content-Type': 'application/json' } }), // 方法3: 保存完成状态 () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.SAVE_COMPLETION_STATUS}`, data: JSON.stringify({ courseId: playInfo.courseId, status: 'completed', completedAt: new Date().toISOString(), totalTime: finalTime, _t: Date.now() }), headers: { 'Content-Type': 'application/json' } }) ]; const result = await this._tryApiEndpoints( finishMethods, `[学习记录] 完成成功`, `[学习记录] 所有完成方法失败,学习记录可能未正确保存` ); return !!result; } catch (error) { if (error.name === 'AbortError') { throw error; } UI.log(`[学习记录] 完成异常: ${error.message}`, 'warn'); return false; } }, // 使用优化后的API策略重构课程完成 completeCourse: async function(courseInfo) { try { const courseId = courseInfo.id || courseInfo.courseId; UI.log(`[课程完成] 开始完成课程: ${courseId}`, 'debug'); const completionMethods = [ // 方法1: 课程学习完成API () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.COMPLETE_COURSE}`, data: JSON.stringify({ courseId: courseId, completionTime: new Date().toISOString(), studyDuration: courseInfo.durationStr || '00:30:00', _t: Date.now() }), headers: { 'Content-Type': 'application/json' } }), // 方法2: 学习进度完成API () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.FINISH_LEARNING}`, data: JSON.stringify({ courseId: courseId, status: 'completed', progress: 100, _t: Date.now() }), headers: { 'Content-Type': 'application/json' } }), // 方法3: 课程状态更新API () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.UPDATE_STATUS}`, data: JSON.stringify({ courseId: courseId, status: 'finished', finishTime: new Date().toISOString(), _t: Date.now() }), headers: { 'Content-Type': 'application/json' } }), // 方法4: 学习记录提交API () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.SUBMIT_LEARNING_RECORD}`, data: JSON.stringify({ courseId: courseId, learningTime: courseInfo.durationStr || '00:30:00', isCompleted: true, submitTime: new Date().toISOString(), _t: Date.now() }), headers: { 'Content-Type': 'application/json' } }), // 方法5: 课程结业API () => this._request({ method: 'POST', url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.GRADUATE}`, data: JSON.stringify({ courseId: courseId, graduateTime: new Date().toISOString(), _t: Date.now() }), headers: { 'Content-Type': 'application/json' } }) ]; const result = await this._tryApiEndpoints( completionMethods, `[课程完成] 完成成功`, `[课程完成] 所有方法失败,课程可能已通过其他方式完成` ); return !!result; } catch (error) { if (error.name === 'AbortError') { throw error; } UI.log(`[课程完成] 发生错误: ${error.message}`, 'error'); return false; } }, // 剩余的API方法(使用常量优化) getCourseListFromSpecialDetail: async () => { try { UI.log('检测到专栏详情页面,尝试获取课程列表...', 'info'); const urlParams = new URLSearchParams(window.location.hash.split('?')[1]); const channelId = urlParams.get('id'); if (!channelId) { UI.log('未找到专栏ID', 'error'); return []; } UI.log(`专栏ID: ${channelId}`, 'debug'); const url = `${CONSTANTS.API_ENDPOINTS.GET_PACK_BY_ID}?id=${channelId}&_t=${Date.now()}`; const response = await API._request({ method: 'GET', url: `${API.getBaseUrl()}${url}` }); if (!response.success || !response.data) { UI.log('专栏API返回数据异常', 'error'); return []; } const channelData = response.data; UI.log(`专栏标题: ${channelData.title}`, 'info'); const courseList = []; if (channelData.pdChannelUnitList) { for (const unit of channelData.pdChannelUnitList) { UI.log(`单元: ${unit.unitName} (${unit.totalPeriod}学时)`, 'debug'); if (unit.subList) { for (const course of unit.subList) { if (course.typeValue === 'course') { courseList.push({ id: course.businessId, courseId: course.businessId, dsUnitId: `unit_${unit.order}_${course.order}`, title: course.title, courseName: course.title, teacher: course.teacher, teacherJob: course.teacherjob, durationStr: course.duration, period: course.period, unit: unit.unitName, category: course.categoryText, publishTime: course.publishTime, order: course.order, unitOrder: unit.order, status: 'not_started' }); } } } } } UI.log(`[专栏课程] 成功获取 ${courseList.length} 门课程`, 'info'); return courseList; } catch (error) { UI.log(`获取专栏课程列表失败: ${error.message}`, 'error'); return []; } }, getCourseListFromChannel: async function(channelId) { try { UI.log(`正在从频道API获取课程列表 (ID: ${channelId})...`, 'info'); const apiEndpoints = [ `${CONSTANTS.API_ENDPOINTS.GET_PACK_BY_ID}?id=${channelId}&_t=${Date.now()}`, `/api/nc/channel/detail?id=${channelId}&_t=${Date.now()}`, `/inc/nc/course/list?channelId=${channelId}&_t=${Date.now()}`, `${CONSTANTS.API_ENDPOINTS.GET_COURSE_LIST}?channelId=${channelId}&_t=${Date.now()}` ]; for (const endpoint of apiEndpoints) { try { UI.log(`尝试API端点: ${endpoint}`, 'debug'); const response = await API._request({ method: 'GET', url: `${API.getBaseUrl()}${endpoint}` }); if (response && response.success && response.data) { const courseList = []; const data = response.data; if (data.pdChannelUnitList) { for (const unit of data.pdChannelUnitList) { UI.log(`单元: ${unit.unitName} (${unit.totalPeriod}学时)`, 'debug'); if (unit.subList) { for (const course of unit.subList) { if (course.typeValue === 'course') { courseList.push({ id: course.businessId, courseId: course.businessId, dsUnitId: `unit_${unit.order}_${course.order}`, title: course.title, courseName: course.title, teacher: course.teacher, teacherJob: course.teacherjob, durationStr: course.duration, period: course.period, unit: unit.unitName, category: course.categoryText, publishTime: course.publishTime, order: course.order, unitOrder: unit.order, status: 'not_started' }); } } } } } else { let courses = []; if (data.courseList) { courses = data.courseList; } else if (data.courses) { courses = data.courses; } else if (data.list) { courses = data.list; } else if (Array.isArray(data)) { courses = data; } courses.forEach(course => { courseList.push({ courseId: course.id || course.courseId, courseName: course.name || course.title || course.courseName, durationStr: course.duration || course.durationStr || '00:30:00', status: course.status || 'not_started' }); }); } if (courseList.length > 0) { UI.log(`✅ 从频道API获取到 ${courseList.length} 门课程`, 'info'); return courseList; } } } catch (error) { UI.log(`API端点 ${endpoint} 失败: ${error.message}`, 'debug'); } } UI.log('❌ 所有频道API端点都失败了', 'warn'); return []; } catch (error) { UI.log(`❌ 获取频道课程列表失败: ${error.message}`, 'error'); return []; } }, getCourseList: async () => { try { UI.log('正在获取课程列表...', 'info'); const waitForVueApp = async () => { let attempts = 0; const maxAttempts = 15; while (attempts < maxAttempts) { const app = document.querySelector(CONSTANTS.SELECTORS.APP); if (app && (window.Vue || app.__vue__ || app._vnode)) { UI.log('✅ 检测到Vue.js应用已加载', 'debug'); return true; } const hasContent = document.querySelectorAll('.el-card, [class*="course"], [class*="item"], [class*="card"]').length > 0; if (hasContent) { UI.log('✅ 检测到动态内容已加载', 'debug'); return true; } UI.log(`⏳ 等待Vue.js应用加载... (${attempts + 1}/${maxAttempts})`, 'debug'); await new Promise(resolve => setTimeout(resolve, 1000)); attempts++; } UI.log('⚠️ Vue.js应用加载超时,继续尝试获取课程', 'warn'); return false; }; await waitForVueApp(); const currentUrl = window.location.href; UI.log(`当前页面URL: ${currentUrl}`, 'debug'); if (currentUrl.includes('/specialdetail')) { UI.log('检测到专栏详情页面,尝试从API获取课程列表...', 'info'); return await API.getCourseListFromSpecialDetail(); } if (currentUrl.includes('channelDetail')) { UI.log('检测到频道详情页面,尝试从API获取课程列表...', 'info'); let channelId = null; try { const hash = window.location.hash; if (hash.includes('?')) { const urlParams = new URLSearchParams(hash.split('?')[1]); channelId = urlParams.get('id'); } if (!channelId && currentUrl.includes('?')) { const urlParams = new URLSearchParams(currentUrl.split('?')[1]); channelId = urlParams.get('id'); } if (!channelId) { const match = currentUrl.match(/[?&]id=([^&]+)/); if (match) { channelId = match[1]; } } } catch (error) { UI.log(`解析频道ID失败: ${error.message}`, 'debug'); } if (channelId) { UI.log(`频道ID: ${channelId}`, 'debug'); return await API.getCourseListFromChannel(channelId); } else { UI.log('❌ 无法从URL中提取频道ID', 'warn'); } } let courseList = []; let courseElements = []; await new Promise(resolve => setTimeout(resolve, 3000)); UI.log(`🔍 页面调试信息:`, 'debug'); UI.log(`- 页面标题: ${document.title}`, 'debug'); UI.log(`- ${CONSTANTS.SELECTORS.APP}元素: ${document.querySelector(CONSTANTS.SELECTORS.APP) ? '存在' : '不存在'}`, 'debug'); UI.log(`- Vue实例: ${window.Vue ? '存在' : '不存在'}`, 'debug'); UI.log(`- 所有元素数量: ${document.querySelectorAll('*').length}`, 'debug'); for (const selector of CONSTANTS.COURSE_SELECTORS) { const elements = document.querySelectorAll(selector); UI.log(`🔍 选择器 "${selector}": ${elements.length} 个元素`, 'debug'); if (elements.length > 0) { courseElements = elements; UI.log(`📋 使用选择器 "${selector}" 找到 ${elements.length} 个课程元素`, 'info'); break; } } if (courseElements.length === 0) { for (const selector of CONSTANTS.FALLBACK_SELECTORS) { const elements = document.querySelectorAll(selector); if (elements.length > 0) { courseElements = elements; UI.log(`📋 使用备用选择器 "${selector}" 找到 ${elements.length} 个元素`, 'info'); break; } } } UI.log(`[DOM解析] 找到 ${courseElements.length} 个课程元素`, 'debug'); courseElements.forEach((el, index) => { const courseData = { courseId: el.getAttribute('data-course-id') || el.getAttribute('data-id') || el.querySelector('[data-course-id]')?.getAttribute('data-course-id') || `course_${index}`, dsUnitId: el.getAttribute('data-unit-id') || el.getAttribute('data-dsunit') || el.querySelector('[data-unit-id]')?.getAttribute('data-unit-id') || `unit_${index}`, courseName: el.getAttribute('title') || el.querySelector('.course-title, .lesson-title, h3, h4')?.textContent?.trim() || el.textContent?.trim()?.split('\n')[0] || `课程${index + 1}`, durationStr: el.querySelector('.duration, .time, [class*="time"]')?.textContent?.trim() || '00:30:00', status: el.getAttribute('data-status') || 'not_started' }; if (courseData.courseName && courseData.courseName.length > 5) { courseList.push(courseData); } }); if (courseList.length === 0) { try { const apiUrl = `${API.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.GET_COURSE_LIST}`; const apiResponse = await API._request({ method: 'GET', url: apiUrl + '?' + new URLSearchParams({ _t: Date.now(), page: 1, size: 50 }).toString() }); if (apiResponse.success && apiResponse.data) { const apiCourses = Array.isArray(apiResponse.data) ? apiResponse.data : apiResponse.data.list || apiResponse.data.records || []; courseList = apiCourses.map((course, index) => ({ courseId: course.id || course.courseId || `api_course_${index}`, dsUnitId: course.dsUnitId || course.unitId || `api_unit_${index}`, courseName: course.name || course.title || course.courseName || `API课程${index + 1}`, durationStr: course.duration || course.timeLength || '00:30:00', status: course.status || 'not_started' })); } } catch (apiError) { UI.log(`[API获取课程] 失败: ${apiError.message}`, 'debug'); } } if (courseList.length === 0) { const videoElements = document.querySelectorAll(CONSTANTS.VIDEO_SELECTORS.join(', ')); UI.log(`[视频元素分析] 找到 ${videoElements.length} 个视频元素`, 'debug'); videoElements.forEach((el, index) => { const courseData = { courseId: el.getAttribute('data-course-id') || `video_course_${index}`, dsUnitId: el.getAttribute('data-unit-id') || `video_unit_${index}`, courseName: document.title || `视频课程${index + 1}`, durationStr: el.getAttribute('duration') || '00:30:00', status: 'not_started', videoElement: el }; courseList.push(courseData); }); } const uniqueCourses = courseList.filter((course, index, self) => index === self.findIndex(c => c.courseId === course.courseId) ); UI.log(`[课程列表] 获取到 ${uniqueCourses.length} 门课程`, 'info'); uniqueCourses.forEach(course => { UI.log(`- ${course.courseName} (${course.courseId})`, 'debug'); }); return uniqueCourses; } catch (error) { UI.log(`获取课程列表失败: ${error.message}`, 'error'); return []; } }, /** * 获取课程播放信息 - 增强版,支持多种数据源和智能降级 * 根据深度分析报告优化:处理API空数据、增强videoId提取、智能时长解析 */ getPlayInfo: async (courseId, dsUnitId, courseDuration) => { try { EventBus.publish('log', { message: `[getPlayInfo] 开始获取课程 ${courseId} 的播放信息`, type: 'debug' }); // 策略1: 尝试从播放趋势API获取数据 let playTrendData = null; try { const playTrendParams = new URLSearchParams({ courseId: courseId, _t: Date.now() }); playTrendData = await API._request({ method: 'GET', url: `${API.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.GET_PLAY_TREND}?${playTrendParams.toString()}` }); EventBus.publish('log', { message: `[getPlayTrend] 响应数据: ${JSON.stringify(playTrendData)}`, type: 'debug' }); } catch (error) { EventBus.publish('log', { message: `[getPlayTrend] API调用失败: ${error.message}`, type: 'warn' }); } let duration = 0; let lastLearnedTime = 0; let videoId = null; let coursewareId = null; let dataSource = 'unknown'; // 检查API是否返回有效数据(根据深度分析报告的建议) if (playTrendData?.success && playTrendData?.data && Object.keys(playTrendData.data).length > 0 && playTrendData.data.locationSite) { dataSource = 'api'; const trendData = playTrendData.data; const locationSite = trendData.locationSite; // 提取时长信息 duration = locationSite.sumDurationLong || locationSite.duration || 0; // 提取学习进度 if (locationSite.lastWatchPoint) { lastLearnedTime = API.parseTimeToSeconds(locationSite.lastWatchPoint); } else if (locationSite.lastLearnedTime) { lastLearnedTime = locationSite.lastLearnedTime; } coursewareId = locationSite.id; // 增强的videoId提取逻辑(根据学习记录API修复报告) const videoIdSources = [ // 方法1: 从fileUrl中提取 () => { if (locationSite.fileUrl) { try { const fileUrlData = JSON.parse(locationSite.fileUrl); if (fileUrlData.file?.[0]?.id) { return fileUrlData.file[0].id; } if (fileUrlData.videoId) { return fileUrlData.videoId; } } catch (e) { EventBus.publish('log', { message: `[videoId提取] fileUrl解析失败: ${e.message}`, type: 'debug' }); } } return null; }, // 方法2: 从fileAdditionUrl中提取 () => { if (locationSite.fileAdditionUrl) { try { const additionData = JSON.parse(locationSite.fileAdditionUrl); if (additionData.videoId) { return additionData.videoId; } } catch (e) { EventBus.publish('log', { message: `[videoId提取] fileAdditionUrl解析失败: ${e.message}`, type: 'debug' }); } } return null; }, // 方法3: 从playTree中查找 () => { if (trendData.playTree?.children) { for (const child of trendData.playTree.children) { if (child.rTypeValue === 'video' && child.id) { coursewareId = child.id; return child.videoId || child.id; } } } return null; } ]; // 尝试各种方法提取videoId for (const source of videoIdSources) { try { const extractedId = source(); if (extractedId) { videoId = extractedId; EventBus.publish('log', { message: `[videoId提取] 成功提取: ${videoId}`, type: 'debug' }); break; } } catch (e) { EventBus.publish('log', { message: `[videoId提取] 方法失败: ${e.message}`, type: 'debug' }); } } EventBus.publish('log', { message: `[getPlayInfo] 使用API数据源`, type: 'info' }); } else { // 策略2: API返回空数据,使用课程列表数据(根据深度分析报告) dataSource = 'courselist'; EventBus.publish('log', { message: `[getPlayInfo] API返回空数据,使用课程列表数据源`, type: 'warn' }); } // 时长处理:优先使用API数据,否则解析课程列表时长 if (duration === 0 && courseDuration) { duration = API.parseDuration(courseDuration); EventBus.publish('log', { message: `[getPlayInfo] 从课程列表解析时长: ${courseDuration} → ${duration}秒`, type: 'debug' }); } // 兜底时长 if (duration === 0) { duration = CONSTANTS.TIME_FORMATS.DEFAULT_DURATION; EventBus.publish('log', { message: `[getPlayInfo] 使用默认时长: ${duration}秒`, type: 'debug' }); } // 尝试通过课件详情API获取videoId if (!videoId && coursewareId) { try { const coursewareDetail = await API._request({ method: 'GET', url: `${API.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.GET_COURSEWARE_DETAIL}?coursewareId=${coursewareId}&_t=${Date.now()}` }); if (coursewareDetail?.success && coursewareDetail?.data?.videoId) { videoId = coursewareDetail.data.videoId; EventBus.publish('log', { message: `[getPlayInfo] 从课件详情获取videoId: ${videoId}`, type: 'debug' }); } } catch (e) { EventBus.publish('log', { message: `[getPlayInfo] 查询课件详情失败: ${e.message}`, type: 'debug' }); } } // 生成模拟videoId(但会尝试真实API) if (!videoId) { videoId = `mock_video_${courseId}`; EventBus.publish('log', { message: `[getPlayInfo] 生成模拟videoId: ${videoId}`, type: 'debug' }); } const playInfo = { courseId: courseId, coursewareId: coursewareId || dsUnitId, videoId: videoId, duration: duration, lastLearnedTime: lastLearnedTime, playURL: videoId.startsWith('mock_') ? 'mock://play.url' : `https://zpyapi.shsets.com/player/get?videoId=${videoId}`, dataSource: dataSource // 添加数据源标识 }; EventBus.publish('log', { message: `[getPlayInfo] 最终播放信息 (${dataSource}): ${JSON.stringify(playInfo)}`, type: 'debug' }); return playInfo; } catch (error) { EventBus.publish('log', { message: `[getPlayInfo] 错误: ${error.message}`, type: 'error' }); // 完全兜底方案 const duration = courseDuration ? API.parseDuration(courseDuration) : CONSTANTS.TIME_FORMATS.DEFAULT_DURATION; return { courseId: courseId, coursewareId: dsUnitId, videoId: `mock_video_${courseId}`, duration: duration, lastLearnedTime: 0, playURL: 'mock://play.url', dataSource: 'fallback' }; } }, parseTimeToSeconds: (timeStr) => { try { const parts = timeStr.split(':').map(part => parseInt(part, 10)); if (parts.length === 3) { return parts[0] * 3600 + parts[1] * 60 + parts[2]; } return 0; } catch (e) { return 0; } }, parseDuration: (durationStr) => { if (!durationStr || typeof durationStr !== 'string') return CONSTANTS.TIME_FORMATS.DEFAULT_DURATION; const timeParts = durationStr.split(':'); if (timeParts.length === 3) { const hours = parseInt(timeParts[0]) || 0; const minutes = parseInt(timeParts[1]) || 0; const seconds = parseInt(timeParts[2]) || 0; return hours * 3600 + minutes * 60 + seconds; } return CONSTANTS.TIME_FORMATS.DEFAULT_DURATION; }, pulseSaveRecord: async (playInfo, currentTime) => { const watchPoint = API.secondsToTimeString(currentTime); const payload = new URLSearchParams({ courseId: playInfo.courseId, coursewareId: playInfo.coursewareId || playInfo.videoId, watchPoint: watchPoint, pulseTime: 10, pulseRate: 1 }).toString(); UI.log(`[脉冲上报] ${watchPoint} (${Math.round((currentTime / playInfo.duration) * 100)}%)`, 'info'); return await API._request({ method: 'POST', url: `${API.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.PULSE_SAVE_RECORD}`, data: payload, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); }, secondsToTimeString: (seconds) => { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }, antiCheatCheck: async (courseId, userId) => { try { UI.log(`[防刷课检查] 课程ID: ${courseId}`, 'debug'); const pushUrl = `${CONSTANTS.API_ENDPOINTS.PUSH_COURSE}?user_id=${userId}&course_id=${courseId}&_t=${Date.now()}`; const pushResponse = await API._request({ method: 'GET', url: `${API.getBaseUrl()}${pushUrl}` }); const checkUrl = `${CONSTANTS.API_ENDPOINTS.GET_COURSE_BY_USER}?user_id=${userId}&course_id=${courseId}&_t=${Date.now()}`; const checkResponse = await API._request({ method: 'GET', url: `${API.getBaseUrl()}${checkUrl}` }); UI.log(`[防刷课检查结果] Push: ${pushResponse?.message || '未知'}, Check: ${checkResponse?.message || '未知'}`, 'debug'); return { pushOk: pushResponse?.success === true, checkOk: checkResponse?.success === true }; } catch (error) { UI.log(`[防刷课检查失败] ${error.message}`, 'error'); return { pushOk: false, checkOk: false }; } }, extractUserId: () => { try { const cookieMatch = document.cookie.match(CONSTANTS.COOKIE_PATTERNS.USER_ID); if (cookieMatch) { UI.log(`[用户ID提取] 从Cookie获取: ${cookieMatch[1]}`, 'debug'); return cookieMatch[1]; } const userIdElement = document.querySelector('[data-user-id]'); if (userIdElement) { const userId = userIdElement.getAttribute('data-user-id'); UI.log(`[用户ID提取] 从DOM获取: ${userId}`, 'debug'); return userId; } const urlParams = new URLSearchParams(window.location.search); const userId = urlParams.get('user_id'); if (userId) { UI.log(`[用户ID提取] 从URL获取: ${userId}`, 'debug'); return userId; } const storedUserId = localStorage.getItem(CONSTANTS.STORAGE_KEYS.USER_ID) || localStorage.getItem(CONSTANTS.STORAGE_KEYS.USER_ID_ALT); if (storedUserId) { UI.log(`[用户ID提取] 从localStorage获取: ${storedUserId}`, 'debug'); return storedUserId; } const pMatch = document.cookie.match(CONSTANTS.COOKIE_PATTERNS.P_PARAM); if (pMatch) { UI.log(`[用户ID提取] 从_p参数获取: ${pMatch[1]}`, 'debug'); return pMatch[1]; } UI.log('[用户ID提取] 未找到用户ID', 'warn'); return null; } catch (error) { UI.log(`[用户ID提取失败] ${error.message}`, 'error'); return null; } }, async checkCourseCompletion(courseId) { try { UI.log(`[完成度检查] 检查课程 ${courseId} 的完成状态`); const playTrend = await this._request({ url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.GET_PLAY_TREND}?courseId=${courseId}&_t=${Date.now()}`, method: 'GET' }); if (playTrend && playTrend.success && playTrend.data) { const { locationSite } = playTrend.data; if (locationSite && locationSite.finishedRate !== undefined) { const finishedRate = parseInt(locationSite.finishedRate); UI.log(`[完成度检查] 课程完成度: ${finishedRate}%`); if (finishedRate >= CONFIG.COMPLETION_THRESHOLD) { UI.log(`[完成度检查] 课程已完成 (${finishedRate}% >= ${CONFIG.COMPLETION_THRESHOLD}%)`, 'success'); return { isCompleted: true, finishedRate, method: 'playTrend' }; } return { isCompleted: false, finishedRate, method: 'playTrend' }; } } try { const studyRecord = await this._request({ url: `${this.getBaseUrl()}${CONSTANTS.API_ENDPOINTS.GET_STUDY_RECORD}?courseId=${courseId}&_t=${Date.now()}`, method: 'GET' }); if (studyRecord && studyRecord.success && studyRecord.data) { const isFinished = studyRecord.data.isFinished === true || studyRecord.data.status === 'completed'; if (isFinished) { UI.log(`[完成度检查] 学习记录显示课程已完成`, 'success'); return { isCompleted: true, finishedRate: 100, method: 'studyRecord' }; } } } catch (error) { UI.log(`[完成度检查] 学习记录API检查失败: ${error.message}`, 'warn'); } return { isCompleted: false, finishedRate: 0, method: 'default' }; } catch (error) { UI.log(`[完成度检查] 检查失败: ${error.message}`, 'error'); return { isCompleted: false, finishedRate: 0, method: 'error' }; } }, /** * 智能学习课程 - 根据当前进度自动选择最佳学习策略 * * @param {Object} courseInfo - 课程信息对象 * @param {string} courseInfo.courseId - 课程ID * @param {string} courseInfo.coursewareId - 课件ID * @param {string} courseInfo.videoId - 视频ID * @param {number} courseInfo.duration - 课程总时长(秒) * @param {number} courseInfo.lastLearnedTime - 上次学习时间点(秒) * @param {string} courseInfo.title - 课程标题 * @returns {Promise<boolean>} 学习是否成功 */ async smartLearnCourse(courseInfo) { const { courseId, coursewareId, videoId, duration, lastLearnedTime } = courseInfo; const currentProgress = Math.floor((lastLearnedTime / duration) * 100); EventBus.publish('log', { message: `[智能学习] 课程: ${courseInfo.title || courseId}`, type: 'info' }); EventBus.publish('log', { message: `[智能学习] 当前进度: ${currentProgress}% (${Utils.formatTime(lastLearnedTime)}/${Utils.formatTime(duration)})`, type: 'info' }); let strategy = 'normal'; if (currentProgress < 10) { strategy = 'slow_start'; UI.log(`[智能学习] 策略: 慢启动 - 从头开始学习`); } else if (currentProgress < 50) { strategy = 'progressive'; UI.log(`[智能学习] 策略: 渐进式 - 从${currentProgress}%继续`); } else if (currentProgress < 80) { strategy = 'fast_finish'; UI.log(`[智能学习] 策略: 快速完成 - 直接跳跃到结束`); } else { strategy = 'final_push'; UI.log(`[智能学习] 策略: 最后冲刺 - 完成剩余部分`); } await this._createStudyRecord(courseId); let currentTime = lastLearnedTime; let success = false; switch (strategy) { case 'slow_start': const step1 = Math.floor(duration * 0.2); const step2 = Math.floor(duration * 0.35); const step3 = Math.floor(duration * 0.5); for (const targetTime of [step1, step2, step3]) { if (Learner.stopRequested) { UI.log('⏹️ 收到停止请求,中断智能学习', 'warn'); return false; } await new Promise(resolve => setTimeout(resolve, 8000)); success = await this.reportProgress({ courseId, coursewareId, videoId, duration }, targetTime); if (!success) break; currentTime = targetTime; UI.log(`[慢启动] 进度: ${Math.floor((currentTime/duration)*100)}%`); } if (success && !Learner.stopRequested) { await new Promise(resolve => setTimeout(resolve, 5000)); currentTime = duration - 10; success = await this.reportProgress({ courseId, coursewareId, videoId, duration }, currentTime); } break; case 'progressive': const remaining = duration - currentTime; const stepSize = Math.floor(remaining / 5); for (let i = 1; i <= 5; i++) { if (Learner.stopRequested) { UI.log('⏹️ 收到停止请求,中断智能学习', 'warn'); return false; } await new Promise(resolve => setTimeout(resolve, 6000)); const nextTime = Math.min(currentTime + (stepSize * i), duration - 10); success = await this.reportProgress({ courseId, coursewareId, videoId, duration }, nextTime); if (!success) break; currentTime = nextTime; UI.log(`[渐进式] 步骤 ${i}/5: ${Math.floor((currentTime/duration)*100)}%`); } break; case 'fast_finish': if (Learner.stopRequested) { UI.log('⏹️ 收到停止请求,中断智能学习', 'warn'); return false; } const target95 = Math.floor(duration * 0.95); await new Promise(resolve => setTimeout(resolve, 3000)); success = await this.reportProgress({ courseId, coursewareId, videoId, duration }, target95); if (success && !Learner.stopRequested) { await new Promise(resolve => setTimeout(resolve, 5000)); currentTime = duration - 10; success = await this.reportProgress({ courseId, coursewareId, videoId, duration }, currentTime); } break; case 'final_push': if (Learner.stopRequested) { UI.log('⏹️ 收到停止请求,中断智能学习', 'warn'); return false; } await new Promise(resolve => setTimeout(resolve, 3000)); currentTime = duration - 10; success = await this.reportProgress({ courseId, coursewareId, videoId, duration }, currentTime); break; } if (success) { UI.log(`✅ 智能学习完成: ${courseInfo.title || courseId}`, 'success'); try { await this.finishStudyRecord(courseInfo, currentTime); await this.completeCourse(courseInfo); UI.log(`✅ 学习记录已保存`, 'success'); } catch (error) { UI.log(`⚠️ 学习记录保存失败: ${error.message}`, 'warn'); } return true; } else { UI.log(`❌ 智能学习失败`, 'error'); return false; } } }; // --- 主控制逻辑(增强版) --- const Learner = { isRunning: false, stopRequested: false, stop: function() { this.isRunning = false; this.stopRequested = true; // 使用AbortController真正中止所有正在进行的请求 if (API.abortController) { API.abortController.abort(); UI.log('🛑 正在中止所有网络请求...', 'info'); } const toggleBtn = document.getElementById(CONSTANTS.SELECTORS.TOGGLE_BTN.replace('#', '')); if (toggleBtn) { toggleBtn.setAttribute('data-state', 'stopped'); toggleBtn.textContent = '开始学习'; } UI.updateStatus('已停止'); UI.log('⏹️ 学习流程已停止', 'warn'); }, // 跳过已完成课程的独立功能 async skipCompletedCourses() { try { UI.log('🔍 开始检查并跳过已完成的课程...'); UI.updateStatus('检查已完成课程'); // 获取课程列表 const courses = await API.getCourseList(); if (!courses || courses.length === 0) { UI.log('❌ 未找到课程列表', 'error'); return; } let completedCount = 0; for (let i = 0; i < courses.length; i++) { const course = courses[i]; const courseId = course.id || course.courseId; UI.log(`检查第 ${i + 1}/${courses.length} 门课程: ${course.title}`); try { const completionCheck = await API.checkCourseCompletion(courseId); if (completionCheck.isCompleted) { UI.log(`✅ 已完成: ${course.title} (${completionCheck.finishedRate}%)`, 'success'); completedCount++; } else { UI.log(`📖 未完成: ${course.title} (${completionCheck.finishedRate}%)`); } } catch (error) { UI.log(`❌ 检查失败: ${course.title} - ${error.message}`, 'error'); } // 更新进度 const progress = Math.floor(((i + 1) / courses.length) * 100); UI.updateProgress(progress); } UI.log(`\n📊 检查完成: ${completedCount}/${courses.length} 门课程已完成`, 'success'); UI.updateStatus(`检查完成 - ${completedCount}/${courses.length} 已完成`); } catch (error) { UI.log(`❌ 检查过程出错: ${error.message}`, 'error'); UI.updateStatus('检查失败'); } }, // 更新主学习流程 - 增强已完成课程检查和统计信息 async processCourses(courses) { UI.log(`发现 ${courses.length} 门课程,开始处理...`); UI.updateStatus('处理课程列表'); // 初始化统计信息 const stats = { total: courses.length, completed: 0, learned: 0, failed: 0, skipped: 0 }; UI.updateStatistics(stats); for (let i = 0; i < courses.length; i++) { // 检查是否收到停止请求 if (this.stopRequested) { UI.log('⏹️ 收到停止请求,中断学习流程', 'warn'); break; } const course = courses[i]; UI.log(`\n📚 处理第 ${i + 1}/${courses.length} 门课程: ${course.title}`); UI.updateStatus(`学习课程 ${i + 1}/${courses.length}`); try { const courseId = course.id || course.courseId; // 首先检查课程是否已完成 if (CONFIG.SKIP_COMPLETED_COURSES) { const completionCheck = await API.checkCourseCompletion(courseId); if (completionCheck.isCompleted) { UI.log(`✅ 课程已完成,跳过: ${course.title} (${completionCheck.finishedRate}%)`, 'success'); stats.completed++; UI.updateStatistics(stats); continue; } } // 获取课程播放信息 const playInfo = await API.getPlayInfo(courseId, course.dsUnitId, course.durationStr); if (!playInfo) { UI.log(`❌ 无法获取课程播放信息,跳过: ${course.title}`, 'error'); stats.failed++; UI.updateStatistics(stats); continue; } // 双重检查:通过播放信息再次确认完成状态 const progressPercent = Math.floor((playInfo.lastLearnedTime / playInfo.duration) * 100); if (progressPercent >= CONFIG.COMPLETION_THRESHOLD) { UI.log(`✅ 播放信息确认课程已完成,跳过: ${course.title} (${progressPercent}%)`, 'success'); stats.completed++; UI.updateStatistics(stats); continue; } // 开始学习课程 const courseInfoWithPlayInfo = { ...course, ...playInfo, title: course.title || course.courseName, courseId: courseId }; // 使用智能学习策略 const success = await API.smartLearnCourse(courseInfoWithPlayInfo); if (success) { UI.log(`✅ 课程学习完成: ${course.title}`, 'success'); stats.learned++; } else { UI.log(`❌ 课程学习失败: ${course.title}`, 'error'); stats.failed++; } UI.updateStatistics(stats); // 更新总体进度 const overallProgress = Math.floor(((i + 1) / courses.length) * 100); UI.updateProgress(overallProgress); // 课程间隔等待(分段检查停止请求) if (i < courses.length - 1) { const delay = Math.random() * 8000 + 12000; // 12-20秒随机间隔 UI.log(`⏳ 等待 ${Math.round(delay/1000)} 秒后处理下一门课程...`); // 分段等待,每秒检查一次停止请求 const delaySeconds = Math.round(delay / 1000); for (let j = 0; j < delaySeconds; j++) { if (this.stopRequested) { UI.log('⏹️ 等待期间收到停止请求,中断学习流程', 'warn'); return; } await new Promise(resolve => setTimeout(resolve, 1000)); } } } catch (error) { UI.log(`❌ 处理课程 ${course.title} 时出错: ${error.message}`, 'error'); stats.failed++; UI.updateStatistics(stats); continue; } } // 显示学习统计 UI.log(`\n🎉 所有课程处理完成!`, 'success'); UI.log(`📊 学习统计:`); UI.log(` ✅ 已完成课程: ${stats.completed} 门`); UI.log(` 📚 新学完课程: ${stats.learned} 门`); UI.log(` ❌ 失败课程: ${stats.failed} 门`); UI.log(` 📖 总课程数: ${stats.total} 门`); UI.updateStatus(`完成 - ${stats.completed + stats.learned}/${stats.total} 门课程`); UI.updateProgress(100); }, // 主流程(增强版) async startLearning() { try { // 重置停止标志并创建新的AbortController this.stopRequested = false; API.abortController = new AbortController(); UI.log('开始学习流程...'); // 获取课程列表 const courses = await API.getCourseList(); if (!courses || courses.length === 0) { UI.log('❌ 未找到课程列表', 'error'); return; } // 使用新的课程处理方法 await this.processCourses(courses); // 学习完成后重置按钮状态 if (!this.stopRequested) { const toggleBtn = document.getElementById(CONSTANTS.SELECTORS.TOGGLE_BTN.replace('#', '')); toggleBtn.setAttribute('data-state', 'stopped'); toggleBtn.textContent = '开始学习'; UI.updateStatus('学习完成'); } } catch (error) { UI.log(`❌ 学习流程出错: ${error.message}`, 'error'); console.error('学习流程错误:', error); // 出错时也要重置按钮状态 const toggleBtn = document.getElementById(CONSTANTS.SELECTORS.TOGGLE_BTN.replace('#', '')); toggleBtn.setAttribute('data-state', 'stopped'); toggleBtn.textContent = '开始学习'; UI.updateStatus('学习出错'); } } }; // --- 初始化 (v2.0优化) --- function init() { // 1. 加载用户配置 Settings.load(); // 2. 创建UI面板(会自动初始化事件监听器) UI.createPanel(); // 3. 注册菜单命令 GM_registerMenuCommand('导出调试日志', UI.exportLogs, 'e'); // 4. 绑定主要控制按钮 const toggleBtn = document.getElementById(CONSTANTS.SELECTORS.TOGGLE_BTN.replace('#', '')); toggleBtn.addEventListener('click', () => { const isRunning = toggleBtn.getAttribute('data-state') === 'running'; if (isRunning) { Learner.stop(); } else { // 更新按钮状态 toggleBtn.setAttribute('data-state', 'running'); toggleBtn.textContent = '停止学习'; // 使用事件驱动更新状态 EventBus.publish('statusUpdate', '学习中...'); // 启动学习流程 Learner.startLearning().catch(error => { EventBus.publish('log', { message: `❌ 启动学习流程失败: ${error.message}`, type: 'error' }); Learner.stop(); }); } }); // 5. 发布初始化完成事件 EventBus.publish('log', { message: '🚀 API学习助手 v3.37.0 初始化完成', type: 'success' }); EventBus.publish('log', { message: '✨ 新特性: 事件驱动架构 + 用户配置界面 + 策略模式', type: 'info' }); } // 初始化环境检测和脚本 function initScript() { detectEnvironment(); init(); } setTimeout(initScript, 2000); })();