// ==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);
})();