中国干部网络学院自动学习脚本,支持浦东分院、企业分院、党校分院,模块化架构
// ==UserScript==
// @name cela-auto
// @version 4.4.0
// @description 中国干部网络学院自动学习脚本,支持浦东分院、企业分院、党校分院,模块化架构
// @author Moker32
// @license GPL-3.0-or-later
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @match *://cela.e-celap.cn/*
// @match *://pudong.e-celap.cn/*
// @match *://pd.cela.cn/*
// @match *://*.e-celap.cn/*
// @match *://www.cela.gov.cn/*
// @match *://cela.gwypx.com.cn/*
// @match *://cela.cbead.cn/*
// @connect cela.e-celap.cn
// @connect pudong.e-celap.cn
// @connect pd.cela.cn
// @connect cela.gwypx.com.cn
// @connect cela.cbead.cn
// @connect www.cela.gov.cn
// @connect zpyapi.shsets.com
// @run-at document-idle
// @namespace https://github.com/Moker32/
// ==/UserScript==
(function(exports) {
"use strict";
const CONSTANTS = {
EVENTS: {
LOG: "log",
STATUS_UPDATE: "statusUpdate",
PROGRESS_UPDATE: "progressUpdate",
STATISTICS_UPDATE: "statisticsUpdate",
LEARNING_START: "learningStart",
LEARNING_STOP: "learningStop",
COURSE_START: "courseStart",
COURSE_COMPLETE: "courseComplete",
COURSE_SKIP: "courseSkip",
COURSE_ERROR: "courseError",
PROGRESS_REPORT: "progressReport",
PROGRESS_SUCCESS: "progressSuccess",
PROGRESS_ERROR: "progressError",
ERROR: "error",
FATAL_ERROR: "fatalError"
},
LEARNING_STATES: {
IDLE: "idle",
PREPARING: "preparing",
LEARNING: "learning",
COMPLETED: "completed",
FAILED: "failed",
SKIPPED: "skipped"
},
WAITING_FOR_USER: "WAITING_FOR_USER",
SELECTORS: {
PANEL: "#api-learner-panel",
STATUS_LABEL: "#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_FAILED: "#stat-failed",
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: [ ".dsf-many-schedule-course-list-row", ".dsf_nc_pd_special_item", '[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", ".dsjy_card", ".item_content", ".class-item-desc" ],
COOKIE_PATTERNS: {
USER_ID: /userId=([^;]+)/,
TOKEN: /token=([^;]+)/,
P_PARAM: /_p=([^;]+)/
},
TIME_FORMATS: {
DEFAULT_DURATION: 1800
},
UI_LIMITS: {
MAX_LOG_ENTRIES: 50,
LOG_FLUSH_DELAY: 100
},
ENVIRONMENTS: {
PORTAL: {
name: "中央门户",
hostnames: [ "www.cela.gov.cn" ],
pathPatterns: [ "/home" ],
features: {
type: "traditional",
framework: "jquery",
courseContainer: "#courseCon",
branchNavigation: "#branchCon"
},
supported: false,
reason: "门户网站仅用于信息展示,不支持自动学习"
},
PUDONG: {
name: "浦东分院",
hostnames: [ "pudong.e-celap.cn", "pd.cela.cn", "cela.e-celap.cn" ],
pathPatterns: [ "/pc/nc/page", "/page:pd" ],
features: {
type: "spa",
framework: "vue",
router: "hash",
apiBase: "auto",
ssoParam: "cela_sso_logged"
},
supported: true,
variants: {
MAIN: "cela.e-celap.cn",
SUBDOMAIN: "pudong.e-celap.cn",
ALIAS: "pd.cela.cn"
}
},
GWYPX: {
name: "党校分院",
hostnames: [ "cela.gwypx.com.cn" ],
pathPatterns: [ "/pcPage/index", "/pcPage/commend/coursedetail" ],
features: {
type: "spa",
framework: "vue",
router: "history",
apiBase: "auto",
siteMeta: "fosung"
},
supported: true,
variants: {
MAIN: "cela.gwypx.com.cn"
}
},
CBEAD: {
name: "企业分院",
hostnames: [ "cela.cbead.cn" ],
pathPatterns: [ "/home", "/study", "/train-new" ],
features: {
type: "spa",
framework: "vue",
router: "hash",
apiBase: "auto",
ssoParam: "cela_sso_logged"
},
supported: true,
variants: {
MAIN: "cela.cbead.cn"
}
}
},
PAGE_CONFIG: {
PUDONG: {
PLAYER: {
path: "coursePlayer",
type: "player",
whitelist: true,
dom: [ "#coursePlayer", ".course-player", ".pd_course_pla" ],
actions: [ "report_progress" ]
},
COLUMN: {
path: [ "zgpdyxkc", "zgpdyxkczl", "specialcolumn", "specialColumn", "specialdetail", "specialDetail", "channelDetail", "pdchanel", "pdchanel/specialdetail", "pdchanel/pdzq" ],
type: "column",
whitelist: true,
actions: [ "scan_courses" ]
},
INDEX: {
path: "/pc/nc/pagehome/index",
type: "index",
whitelist: true,
actions: [ "scan_courses" ]
},
COURSE_LIST: {
path: "pagecourse/courseList",
type: "course_list",
whitelist: true,
actions: [ "scan_courses", "batch_learn" ]
}
},
GWYPX: {
PLAYER: {
path: "/pcPage/commend/coursedetail",
type: "player",
whitelist: true,
dom: [ "video.vjs-tech", ".prism-player", ".aliplayer-container" ],
actions: [ "report_progress" ]
},
CENTER: {
path: "/pcPage/personalCenter",
type: "personal_center",
whitelist: true,
actions: [ "batch_learn", "scan_courses" ]
},
INDEX: {
path: "/pcPage/index",
type: "index",
whitelist: false,
actions: [ "scan_courses" ]
},
SUBJECT_COLUMN_DETAIL: {
path: "/pcPage/subjectColumnDetail",
type: "subject_column_detail",
whitelist: true,
dom: [ ".course-list" ],
actions: [ "scan_courses", "batch_learn" ]
},
SUBJECT_DETAIL: {
path: "/pcPage/subjectDetail",
type: "subject_detail",
whitelist: true,
dom: [ ".course-list" ],
actions: [ "scan_courses", "batch_learn" ]
},
COMMEND_INDEX: {
path: "/pcPage/commendIndex",
type: "commend_index",
whitelist: true,
actions: [ "scan_courses", "batch_learn" ]
}
},
CBEAD: {
PLAYER: {
path: "study/course/detail",
type: "player",
whitelist: true,
dom: [ ".player-content", ".new-global-height", "video" ],
actions: [ "report_progress" ]
},
BRANCH_LIST: {
path: "branch-list-v",
type: "branch_list",
whitelist: true,
actions: [ "scan_courses" ]
},
COLUMN: {
path: "branch-list-v",
type: "column",
whitelist: true,
actions: [ "scan_courses" ]
},
TRAIN_NEW: {
path: "train-new/class-detail",
type: "train_new",
whitelist: true,
actions: [ "scan_courses" ]
},
HOME_V: {
path: "home-v",
type: "home_v",
whitelist: false,
actions: [ "scan_courses" ]
},
CENTER: {
path: "center/my/course",
type: "center",
whitelist: false,
actions: [ "scan_courses" ]
}
}
},
STATUS_DISPLAY: Object.freeze({
completed: Object.freeze({
icon: "✅",
text: "已完成"
}),
in_progress: Object.freeze({
icon: "📖",
text: "学习中"
}),
not_started: Object.freeze({
icon: "📝",
text: "未开始"
})
})
};
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;
}
};
const Utils = {
publishStats(stats) {
EventBus.publish(CONSTANTS.EVENTS.STATISTICS_UPDATE, {
total: stats.total,
completed: stats.completed ?? stats.completedCourses,
learned: stats.learned ?? stats.completedCourses,
failed: stats.failed ?? stats.failedCourses,
skipped: stats.skipped ?? stats.skippedCourses
});
},
formatTime(seconds) {
if (!seconds || seconds < 0) return "00:00:00";
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")}`;
},
parseTimeToSeconds(timeStr) {
try {
if (!timeStr) return 0;
const parts = timeStr.split(":").map(part => parseInt(part, 10));
if (parts.length === 3 && !parts.some(isNaN)) {
return parts[0] * 3600 + parts[1] * 60 + parts[2];
}
return 0;
} catch {
return 0;
}
},
parseDuration(durationStr) {
if (!durationStr || typeof durationStr !== "string") {
return CONSTANTS.TIME_FORMATS.DEFAULT_DURATION;
}
return this.parseTimeToSeconds(durationStr) || CONSTANTS.TIME_FORMATS.DEFAULT_DURATION;
}
};
var utils = Object.freeze({
__proto__: null,
Utils: Utils
});
const ServiceLocator = {
services: {},
register: function(name, service) {
if (!name || typeof name !== "string") {
throw new Error("ServiceLocator.register: name must be a non-empty string");
}
if (!service || typeof service !== "object") {
throw new Error(`ServiceLocator.register: service must be an object (got: ${typeof service})`);
}
this.services[name] = service;
},
get: function(name) {
return this.services[name] || null;
},
has: function(name) {
return name in this.services;
}
};
const ServiceNames = Object.freeze({
API: "api",
LEARNER: "learner",
UI: "ui",
CONFIG: "config"
});
const LearningState = {
_failed: false,
_reason: null,
markFailed(reason) {
this._failed = true;
this._reason = reason;
console.error(`[LearningState] 🚨 课程已标记为失败: ${reason}`);
},
reset() {
this._failed = false;
this._reason = null;
console.log("[LearningState] 🔄 学习状态已重置");
},
isFailed() {
return this._failed;
},
getFailureReason() {
return this._reason;
},
getState() {
return {
failed: this._failed,
reason: this._reason
};
}
};
const Settings = {
defaultConfig: {
LEARNING_STRATEGY: "default",
SKIP_COMPLETED_COURSES: true,
STUDY_RECORD_ENABLED: true,
FALLBACK_MODE: false,
DEBUG_MODE: false,
HEARTBEAT_INTERVAL: 10,
COMPLETION_THRESHOLD: 95,
MAX_RETRY_ATTEMPTS: 10,
RETRY_DELAY: 3e3,
COURSE_COMPLETION_DELAY: 5,
PUDONG_MODE: false,
PUDONG_API_BASE: "",
GWYPX_MODE: false,
GWYPX_API_BASE: "",
CBEAD_MODE: false,
CBEAD_API_BASE: "",
IS_PORTAL: false,
UNSUPPORTED_BRANCH: "",
SUPER_FAST_MODE: true,
FAST_LEARNING_MODE: true,
WARNING_BATCH_LEARNING: true
},
config: {},
load() {
this.config = {
...this.defaultConfig
};
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "✅ 使用固定配置:默认学习模式",
type: "success"
});
},
get(key) {
return this.config[key];
},
set(key, value) {
this.config[key] = value;
}
};
const CONFIG = new Proxy(Settings.config, {
get(target, prop) {
return Settings.get(prop) ?? target[prop];
},
set(target, prop, value) {
Settings.set(prop, value);
target[prop] = value;
return true;
}
});
const RequestQueue = {
queue: [],
activeCount: 0,
maxConcurrent: 2,
requestGap: 1e3,
add(fn) {
return new Promise((resolve, reject) => {
this.queue.push({
fn: fn,
resolve: resolve,
reject: reject
});
this.process();
});
},
async process() {
if (this.activeCount >= this.maxConcurrent || this.queue.length === 0) return;
this.activeCount++;
const {fn: fn, resolve: resolve, reject: reject} = this.queue.shift();
try {
const delay = this.requestGap + Math.random() * 500;
if (delay > 0) await new Promise(r => setTimeout(r, delay));
const result = await fn();
resolve(result);
} catch (e) {
reject(e);
} finally {
this.activeCount--;
setTimeout(() => this.process(), 100);
}
}
};
const SIGN_UP_PATTERNS = [ "我要报名", "立即报名", "立即加入", "报名学习", "加入学习", "是", "确定", "确认" ];
function findSignUpButton() {
const xpathConditions = SIGN_UP_PATTERNS.map(pattern => `contains(., "${pattern}")`).join(" or ");
const xpath = `//button[${xpathConditions}] | //a[${xpathConditions}]`;
const result = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
if (result.snapshotLength > 0) {
console.log(`[DOMHelper] ✅ 通过 XPath 找到报名按钮,共 ${result.snapshotLength} 个`);
return result.snapshotItem(0);
}
const selectors = [ "button", "a", ".btn", '[class*="button"]', '[class*="btn"]', '[role="button"]', ".el-button", ".ant-btn" ];
for (const selector of selectors) {
const elements = document.querySelectorAll(selector);
for (const btn of elements) {
const text = btn.textContent?.trim() || "";
if (SIGN_UP_PATTERNS.some(pattern => text.includes(pattern))) {
console.log(`[DOMHelper] ✅ 通过选择器 "${selector}" 找到报名按钮`);
console.log(`[DOMHelper] 按钮文本: "${text}"`);
console.log(`[DOMHelper] 按钮标签: ${btn.tagName}, class: ${btn.className}`);
return btn;
}
}
}
console.log(`[DOMHelper] ⚠️ 未找到报名按钮,页面可能已报名`);
return null;
}
function getSignUpButtonText(button) {
return button?.textContent?.trim() || "报名按钮";
}
const MASK_SELECTORS = [ "#D339registerMask", '[id*="registerMask"]', '[class*="register-mask"]', ".el-message-box__wrapper", ".v-modal" ];
const MASK_BUTTON_SELECTORS = [ ".register-img", '[class*="register-img"]', ".vjs-big-play-button", ".el-message-box__btns .el-button--primary" ];
function clickMaskButton() {
let clickedCount = 0;
const clickDetails = [];
const messageBox = document.querySelector(".el-message-box");
if (messageBox) {
const dialogButtons = messageBox.querySelectorAll('button, .el-button, [role="button"]');
for (const btn of dialogButtons) {
if (btn.offsetParent !== null) {
const text = btn.textContent?.trim() || "";
if (text === "是" || text === "确定" || text === "确认") {
btn.click();
clickedCount++;
console.log(`[DOMHelper] 🎯 点击弹窗按钮: "${text}"`);
}
}
}
return {
clicked: clickedCount,
buttons: clickDetails
};
}
for (const selector of MASK_BUTTON_SELECTORS) {
const elements = document.querySelectorAll(selector);
for (const el of elements) {
if (el.offsetParent !== null) {
el.click();
clickedCount++;
console.log(`[DOMHelper] 鼠标模拟点击选择器匹配按钮: ${selector}`);
}
}
}
const allButtons = document.querySelectorAll('button, .el-button, [role="button"]');
for (const btn of allButtons) {
if (btn.offsetParent !== null) {
const text = btn.textContent?.trim() || "";
if (SIGN_UP_PATTERNS.some(pattern => text === pattern || text.includes(pattern))) {
if (text === "否" || text === "取消") continue;
btn.click();
clickedCount++;
console.log(`[DOMHelper] 鼠标模拟点击文本匹配按钮: "${text}"`);
}
}
}
return {
clicked: clickedCount,
buttons: clickDetails
};
}
function detectMask() {
const masks = [];
for (const selector of MASK_SELECTORS) {
const elements = document.querySelectorAll(selector);
for (const el of elements) {
if (el.offsetParent !== null || el.style.display !== "none") {
masks.push({
selector: selector,
tagName: el.tagName,
id: el.id,
className: el.className
});
}
}
}
const elMessages = document.querySelectorAll(".el-message-box__message");
for (const msg of elMessages) {
if (msg.textContent.includes("是否学习") || msg.textContent.includes("未学习")) {
masks.push({
selector: "text:是否学习",
tagName: "DIV"
});
}
}
return {
exists: masks.length > 0,
masks: masks
};
}
function startMaskObserver() {
let isClicking = false;
let stopped = false;
const observer = new MutationObserver(mutations => {
if (isClicking || stopped) return;
let shouldCheck = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
shouldCheck = true;
break;
}
}
if (shouldCheck) {
isClicking = true;
setTimeout(() => {
if (stopped) return;
const mask = detectMask();
if (mask.exists) {
clickMaskButton();
}
setTimeout(() => {
isClicking = false;
}, 1e3);
}, 300);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
console.log("[DOMHelper] 🛡️ 全局遮罩监听器已启动");
const cleanup = () => {
if (!stopped) {
stopped = true;
observer.disconnect();
console.log("[DOMHelper] 🛑 遮罩监听器已自动清理(页面卸载)");
}
};
window.addEventListener("beforeunload", cleanup);
window.addEventListener("pagehide", cleanup);
return {
stop() {
if (!stopped) {
stopped = true;
observer.disconnect();
window.removeEventListener("beforeunload", cleanup);
window.removeEventListener("pagehide", cleanup);
console.log("[DOMHelper] 🛑 遮罩监听器已手动停止");
}
},
isStopped() {
return stopped;
}
};
}
var domHelper = Object.freeze({
__proto__: null,
clickMaskButton: clickMaskButton,
detectMask: detectMask,
findSignUpButton: findSignUpButton,
getSignUpButtonText: getSignUpButtonText,
startMaskObserver: startMaskObserver
});
function getUI() {
return typeof window !== "undefined" && window.UI ? window.UI : null;
}
function detectEnvironment() {
CONFIG.PUDONG_MODE = false;
CONFIG.GWYPX_MODE = false;
CONFIG.CBEAD_MODE = false;
CONFIG.IS_PORTAL = false;
CONFIG.UNSUPPORTED_BRANCH = "";
const hostname = window.location.hostname;
const pathname = window.location.pathname;
const hash = window.location.hash;
const result = {
currentEnv: null,
hostname: hostname,
pathname: pathname,
hash: hash,
confidence: 0,
features: {}
};
for (const [envKey, envConfig] of Object.entries(CONSTANTS.ENVIRONMENTS)) {
if (envConfig.hostnames.some(host => hostname === host || hostname.endsWith("." + host))) {
result.currentEnv = envKey;
result.confidence = 50;
console.log(`🔍 域名匹配: ${envKey} (${envConfig.name})`);
break;
}
}
if (result.currentEnv) {
const envConfig = CONSTANTS.ENVIRONMENTS[result.currentEnv];
if (envConfig.pathPatterns) {
const fullPath = pathname + hash;
const matchesPath = envConfig.pathPatterns.some(pattern => fullPath.includes(pattern));
if (matchesPath) {
result.confidence += 30;
console.log(`✅ 路径验证通过 (+30分)`);
}
}
}
if (result.currentEnv) {
const envConfig = CONSTANTS.ENVIRONMENTS[result.currentEnv];
if (envConfig.features) {
if (envConfig.features.framework === "vue") {
if (document.querySelector("#app") || window.Vue) {
result.confidence += 15;
result.features.framework = "vue";
console.log(`✅ Vue框架特征检测 (+15分)`);
}
}
if (envConfig.features.framework === "jquery") {
if (window.jQuery && !document.querySelector("#app")) {
result.confidence += 15;
result.features.framework = "jquery";
console.log(`✅ jQuery框架特征检测 (+15分)`);
}
}
if (envConfig.features.courseContainer) {
const container = document.querySelector(envConfig.features.courseContainer);
if (container) {
result.confidence += 5;
console.log(`✅ 容器特征检测 (${envConfig.features.courseContainer}, +5分)`);
}
}
if (envConfig.features.siteMeta) {
const meta = document.querySelector(`meta[name="site"][content="${envConfig.features.siteMeta}"]`);
if (meta) {
result.confidence += 20;
console.log(`✅ Site Meta特征检测 (${envConfig.features.siteMeta}, +20分)`);
}
}
}
}
const envConfig = result.currentEnv ? CONSTANTS.ENVIRONMENTS[result.currentEnv] : null;
if (result.currentEnv === "PORTAL") {
CONFIG.IS_PORTAL = true;
console.log("🏠 检测到中国干部网络学院门户页面");
} else if (result.currentEnv === "PUDONG") {
CONFIG.PUDONG_MODE = true;
CONFIG.PUDONG_API_BASE = `https://${hostname}`;
console.log("🏢 检测到浦东分院环境");
} else if (result.currentEnv === "GWYPX") {
CONFIG.GWYPX_MODE = true;
CONFIG.GWYPX_API_BASE = `https://${hostname}`;
console.log("🏢 检测到党校分院环境");
} else if (result.currentEnv === "CBEAD") {
CONFIG.CBEAD_MODE = true;
CONFIG.CBEAD_API_BASE = `https://${hostname}`;
console.log("🏢 检测到企业分院环境");
}
console.log(`🌐 环境检测完成: ${result.currentEnv || "UNKNOWN"} (置信度: ${result.confidence}%)`);
console.log(` - 域名: ${hostname}`);
console.log(` - 路径: ${pathname}`);
console.log(` - 特征: ${JSON.stringify(result.features)}`);
EventBus.publish("environment:detected", {
...result,
pudongMode: CONFIG.PUDONG_MODE,
gwypxMode: CONFIG.GWYPX_MODE,
isPortal: CONFIG.IS_PORTAL,
unsupportedBranch: CONFIG.UNSUPPORTED_BRANCH
});
setTimeout(() => {
const UI = getUI();
if (!UI || !UI.setIncompatible) return;
if (CONFIG.UNSUPPORTED_BRANCH && envConfig) {
UI.setIncompatible(`⚠️ 当前检测到【${envConfig.name}】,${envConfig.reason}`);
} else if (CONFIG.IS_PORTAL && envConfig) {
UI.setIncompatible(`🏠 ${envConfig.reason}`);
} else if (!CONFIG.PUDONG_MODE && !CONFIG.IS_PORTAL && !CONFIG.CBEAD_MODE && !CONFIG.GWYPX_MODE) {
UI.setIncompatible("🔍 当前域名未被识别为受支持的学习环境,脚本已停止加载。");
}
}, 100);
return result;
}
function _matchesPath(urlPath, pattern) {
return urlPath.toLowerCase().includes(pattern.toLowerCase());
}
function detectPageType(options) {
const {url: url = window.location.href, pathPatterns: pathPatterns, pageTypes: pageTypes, domSelectors: domSelectors = null, domMatchType: domMatchType = "PLAYER"} = options;
let urlPath = "";
const hashIndex = url.indexOf("#");
if (hashIndex !== -1) {
urlPath = url.substring(hashIndex);
} else {
try {
urlPath = new URL(url).pathname;
} catch {
urlPath = url;
}
}
for (const [type, pattern] of Object.entries(pathPatterns)) {
if (Array.isArray(pattern)) {
for (const p of pattern) {
if (_matchesPath(urlPath, p)) {
return pageTypes[type];
}
}
} else if (_matchesPath(urlPath, pattern)) {
return pageTypes[type];
}
}
if (domSelectors && domSelectors.length > 0) {
for (const selector of domSelectors) {
if (document.querySelector(selector)) {
return pageTypes[domMatchType] || domMatchType.toLowerCase();
}
}
}
return pageTypes.UNKNOWN || "unknown";
}
function createPageDetector(config) {
const {pathPatterns: pathPatterns, pageTypes: pageTypes, domSelectors: domSelectors, domMatchType: domMatchType} = config;
return function identifyPage() {
return detectPageType({
url: window.location.href,
pathPatterns: pathPatterns,
pageTypes: pageTypes,
domSelectors: domSelectors,
domMatchType: domMatchType
});
};
}
const pudongPages = CONSTANTS.PAGE_CONFIG.PUDONG;
const PUDONG_CONSTANTS = {
PATH_PATTERNS: Object.fromEntries(Object.entries(pudongPages).map(([k, v]) => [ k, v.path ])),
PAGE_TYPES: {
...Object.fromEntries(Object.entries(pudongPages).map(([k, v]) => [ k, v.type ])),
UNKNOWN: "unknown"
},
PAGE_TYPE_WHITELIST: Object.keys(pudongPages).filter(k => pudongPages[k].whitelist),
COMPLETION_THRESHOLD: 80,
SKIP_COMPLETED_COURSES: true
};
const Compliance = {
enforce(url, data) {
if (!data) return data;
if (url.includes("/pulseSaveRecord") || url.includes("pulseSaveRecord")) {
return this._enforcePulseRecord(data);
}
return data;
},
_enforcePulseRecord(data) {
let params;
let isString = typeof data === "string";
if (isString) {
params = new URLSearchParams(data);
} else if (data instanceof FormData) {
return data;
} else {
params = new URLSearchParams(data);
}
params.set("pulseTime", "10");
params.set("pulseRate", "1");
if (params.has("progress")) {
const progress = parseInt(params.get("progress"));
if (progress > 98) {
console.warn("[Compliance] ⚠️ 拦截异常进度上报:", progress, "-> 强制修正为 98");
params.set("progress", "98");
}
}
return isString ? params.toString() : Object.fromEntries(params);
}
};
const ApiCore = {
_cachedToken: null,
_tokenExpiry: null,
_TOKEN_CACHE_DURATION: 5 * 60 * 1e3,
abortController: null,
getBaseUrl() {
const config = ServiceLocator.get(ServiceNames.CONFIG);
if (config?.CBEAD_MODE) {
return config?.CBEAD_API_BASE || `https://${window.location.hostname}`;
}
if (config?.PUDONG_MODE) {
return config?.PUDONG_API_BASE || `https://${window.location.hostname}`;
}
return config?.PUDONG_API_BASE || config?.CBEAD_API_BASE || `https://${window.location.hostname}`;
},
_prepareHeaders(customHeaders = {}, data = null) {
const token = this._extractToken();
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: document.cookie,
...customHeaders
};
if (!(data instanceof FormData)) {
const config = ServiceLocator.get(ServiceNames.CONFIG);
if (config?.GWYPX_MODE && data) {
headers["Content-Type"] = "application/json";
} else if (typeof data === "string" && data.includes("=") && !data.startsWith("{")) {
headers["Content-Type"] = "application/x-www-form-urlencoded";
} else if (data) {
headers["Content-Type"] = "application/json";
}
}
if (token) {
headers["Authorization"] = `Bearer ${token}`;
headers["X-Auth-Token"] = token;
}
return headers;
},
_handleResponse(response, resolve, _reject) {
if (response.status === 401) {
this._cachedToken = null;
this._log("Token 可能已过期 (401),清除缓存", "warn");
}
const config = ServiceLocator.get(ServiceNames.CONFIG);
if (config?.DEBUG_MODE) {
this._log(`${response.status} ${response.responseText?.substring(0, 100)}...`);
}
try {
if (response.responseText && response.responseText.trim()) {
return resolve(JSON.parse(response.responseText));
}
if (response.status >= 200 && response.status < 300) {
return resolve({
code: response.status,
success: true,
message: "Success"
});
}
resolve({
status: response.status,
message: "Empty response"
});
} catch {
const html = response.responseText || "";
if (html.trim().startsWith("<")) {
if (html.includes("login") || html.includes("登录")) {
this._log("登录已失效,请重新登录", "error");
alert("cela学习助手:登录已失效,请刷新页面重新登录!");
const learner = ServiceLocator.get(ServiceNames.LEARNER);
if (learner) learner.stop();
EventBus.publish(CONSTANTS.EVENTS.LEARNING_STOP);
} else if (html.includes("verification") || html.includes("验证码") || html.includes("人机")) {
this._log("触发人机验证,请手动完成验证", "error");
alert('cela学习助手:触发人机验证!请在页面上完成验证后点击"开始学习"继续。');
const learner = ServiceLocator.get(ServiceNames.LEARNER);
if (learner) learner.stop();
EventBus.publish(CONSTANTS.EVENTS.LEARNING_STOP);
}
return resolve({
error: "HTML response received",
status: response.status,
isHtml: true
});
}
resolve({
status: response.status,
message: html || "Empty response",
success: response.status >= 200 && response.status < 300
});
}
},
async request(options) {
return RequestQueue.add(() => new Promise((resolve, reject) => {
if (this.abortController && this.abortController.signal.aborted) {
return reject(new DOMException("Aborted", "AbortError"));
}
if (options.data) {
options.data = Compliance.enforce(options.url, options.data);
}
const headers = this._prepareHeaders(options.headers, options.data);
const config = ServiceLocator.get(ServiceNames.CONFIG);
if (config?.DEBUG_MODE) {
this._log(`${options.method || "GET"} ${options.url}`);
}
const req = GM_xmlhttpRequest({
method: options.method || "GET",
url: options.url,
headers: headers,
data: options.data,
timeout: options.timeout || 3e4,
onload: res => this._handleResponse(res, resolve, reject),
onerror: err => {
this._log(`请求失败: ${err.message}`, "error");
reject(err);
},
ontimeout: () => {
this._log("请求超时", "error");
reject(new Error("请求超时"));
}
});
if (this.abortController) {
this.abortController.signal.addEventListener("abort", () => {
if (req.abort) req.abort();
reject(new DOMException("Aborted", "AbortError"));
});
}
}));
},
async get(url, options = {}) {
return this.request({
...options,
method: "GET",
url: url
});
},
async post(url, data, options = {}) {
return this.request({
...options,
method: "POST",
url: url,
data: data
});
},
_setTokenCache(token) {
this._cachedToken = token;
this._tokenExpiry = Date.now() + this._TOKEN_CACHE_DURATION;
},
_isTokenCacheValid() {
if (!this._cachedToken || !this._tokenExpiry) {
return false;
}
return Date.now() < this._tokenExpiry;
},
_extractToken() {
if (this._isTokenCacheValid()) {
return this._cachedToken;
}
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) {
this._setTokenCache(token);
this._log(`找到认证token: ${token.substring(0, 20)}... (缓存5分钟)`, "debug");
return token;
}
} catch {}
}
this._log("未找到认证token", "debug");
return null;
},
_log(message, level = "info") {
const ui = ServiceLocator.get(ServiceNames.UI);
if (ui) {
ui.log(message, level);
} else {
console.log(`[ApiCore] ${message}`);
}
},
setAbortController(controller) {
this.abortController = controller;
},
clearTokenCache() {
this._cachedToken = null;
this._tokenExpiry = null;
}
};
const CourseAdapter = {
normalize(raw, source = "api") {
return {
id: raw.id || raw.businessId || raw.courseId,
courseId: raw.courseId || raw.id || raw.businessId,
dsUnitId: raw.dsUnitId || raw.unitId || (raw.unitOrder && raw.order ? `unit_${raw.unitOrder}_${raw.order}` : "unit_default"),
title: raw.name || raw.title || raw.courseName || "未命名课程",
courseName: raw.name || raw.title || raw.courseName || "未命名课程",
teacher: raw.teacher || "",
durationStr: raw.duration || raw.durationStr || raw.timeLength || "00:30:00",
period: raw.period || 0,
status: raw.status || "not_started",
source: source
};
}
};
function debugLog(...args) {}
const PUDONG_API_CONFIG = {
baseUrl: null,
endpoints: {
GET_PLAY_TREND: "/inc/nc/course/play/getPlayTrend",
GET_COURSE_LIST: "/inc/nc/class/course/list",
GET_COURSE_LIST_OLD: "/inc/nc/course/list",
PULSE_SAVE_RECORD: "/inc/nc/course/play/pulseSaveRecord",
PULSE_SAVE_RECORD_ALT: "/api/player/pulse",
GET_COURSEWARE_LIST: "/inc/nc/course/play/getPlayTrend",
GET_COURSEWARE_LIST_ALT: "/inc/nc/course/play/getPlayInfoById",
GET_PLAY_BASE: "/inc/nc/course/play/getPlayBase",
GET_PLAY_RECORD: "/inc/nc/course/play/getPlayRecord"
},
getBaseUrl() {
const config = ServiceLocator.get(ServiceNames.CONFIG);
this.baseUrl = config?.PUDONG_API_BASE || `https://${window.location.hostname}`;
return this.baseUrl;
},
getUrl(endpoint) {
const baseUrl = this.getBaseUrl();
const endpointPath = this.endpoints[endpoint] || endpoint;
return baseUrl + endpointPath;
}
};
function _buildUrl(endpoint, params = {}) {
let url = PUDONG_API_CONFIG.getUrl(endpoint);
const queryString = new URLSearchParams({
...params,
_t: Date.now()
}).toString();
return `${url}?${queryString}`;
}
const PudongApi = {
...ApiCore,
isSuccessResponse(result) {
return result && (result.success === true || result.code === 200 || result.code === 2e4 || result.state === 2e4 || result.status === "success" || result.status === "ok" || result.code >= 200 && result.code < 300 || result.result === "success" || result.success === 1);
},
_validateResponse(response, context = "getPlayInfo") {
if (!response) {
return {
valid: false,
error: `[${context}] API 响应为空`
};
}
if (!this.isSuccessResponse(response)) {
const errorCode = response.code || response.status || "unknown";
const errorMsg = response.message || response.msg || response.error || "未知错误";
return {
valid: false,
error: `[${context}] API 调用失败: code=${errorCode}, message=${errorMsg}`
};
}
if (!response.data) {
return {
valid: false,
error: `[${context}] 响应 data 字段为空`
};
}
return {
valid: true
};
},
async getPlayInfo(courseId, coursewareId = null, courseDuration = null) {
const {Utils: Utils} = await Promise.resolve().then(function() {
return utils;
});
try {
debugLog(`[getPlayInfo] 获取课程 ${courseId} 的播放信息`);
const response = await this.get(_buildUrl("GET_PLAY_TREND", {
courseId: courseId
}));
const validation = this._validateResponse(response, "getPlayInfo");
if (!validation.valid) {
debugLog(validation.error);
return null;
}
const data = response.data;
if (data.playTree !== undefined && data.playTree !== null) {
if (data.playTree.children !== undefined && data.playTree.children !== null) {
if (!Array.isArray(data.playTree.children)) {
debugLog("[getPlayInfo] playTree.children 不是数组");
}
}
}
if (data.locationSite !== undefined && data.locationSite !== null) {
if (typeof data.locationSite !== "object") {
debugLog("[getPlayInfo] locationSite 不是对象");
}
}
let videoId = null;
let duration = 0;
let lastLearnedTime = 0;
let foundCoursewareId = coursewareId;
if (coursewareId && data.playTree?.children && Array.isArray(data.playTree.children)) {
const target = data.playTree.children.find(c => String(c.id) === String(coursewareId));
if (target) {
videoId = target.id;
foundCoursewareId = target.id;
duration = target.sumDurationLong || 0;
lastLearnedTime = target.lastWatchPoint ? Utils.parseTimeToSeconds(target.lastWatchPoint) : 0;
debugLog(`成功匹配到课件: ${target.title}`);
} else {
debugLog(`[getPlayInfo] 未在 playTree 中找到课件: ${coursewareId}`);
}
} else if (coursewareId) {
debugLog(`[getPlayInfo] playTree.children 无效,跳过课件匹配: courseId=${courseId}, coursewareId=${coursewareId}`);
}
if (!videoId && data.locationSite && typeof data.locationSite === "object") {
videoId = data.locationSite.id;
foundCoursewareId = data.locationSite.id;
duration = data.locationSite.sumDurationLong || 0;
lastLearnedTime = data.locationSite.lastWatchPoint ? Utils.parseTimeToSeconds(data.locationSite.lastWatchPoint) : 0;
}
if (duration === 0 && courseDuration) {
duration = Utils.parseDuration(courseDuration);
}
if (duration === 0) {
duration = CONSTANTS.TIME_FORMATS.DEFAULT_DURATION;
}
if (!videoId) {
videoId = `mock_video_${courseId}`;
debugLog(`[getPlayInfo] 无法获取真实 videoId,使用模拟ID: courseId=${courseId}`);
}
return {
courseId: courseId,
coursewareId: foundCoursewareId,
videoId: videoId,
duration: duration,
lastLearnedTime: lastLearnedTime,
playURL: `https://zpyapi.shsets.com/player/get?videoId=${videoId}`,
dataSource: videoId.startsWith("mock_") ? "fallback" : "api"
};
} catch (error) {
debugLog(`[getPlayInfo] 出错: ${error.message}`);
return null;
}
},
async reportProgress(playInfo, currentTime) {
const {Utils: Utils} = await Promise.resolve().then(function() {
return utils;
});
const watchPoint = Utils.formatTime(currentTime);
const progress = Math.round(currentTime / playInfo.duration * 100);
const payload = new URLSearchParams({
courseId: playInfo.courseId,
coursewareId: playInfo.coursewareId || playInfo.videoId,
videoId: playInfo.videoId || "",
watchPoint: watchPoint,
currentTime: currentTime,
duration: playInfo.duration,
progress: progress,
pulseTime: 10,
pulseRate: 1,
_t: Date.now()
}).toString();
try {
return await this.post(_buildUrl("PULSE_SAVE_RECORD"), payload, {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
});
} catch (error) {
return await this.post(_buildUrl("PULSE_SAVE_RECORD_ALT"), payload, {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
});
}
},
async reportProgressWithDelay(playInfo, currentTime) {
const progressPercent = Math.round(currentTime / playInfo.duration * 100);
if (progressPercent > 90) {
await new Promise(resolve => setTimeout(resolve, 1e3 + Math.random() * 1e3));
}
return await this.reportProgress(playInfo, currentTime);
},
async checkCompletion(courseId, coursewareId = null) {
try {
const response = await this.get(_buildUrl("GET_PLAY_TREND", {
courseId: courseId
}));
const config = ServiceLocator.get(ServiceNames.CONFIG);
if (response?.success && response?.data) {
const data = response.data;
if (data.playTree?.children && Array.isArray(data.playTree.children) && data.playTree.children.length > 0) {
const allVideos = data.playTree.children.filter(c => c.rTypeValue === "video");
if (allVideos.length > 1) {
const allDone = allVideos.every(v => parseInt(v.finishedRate || 0) >= (config?.COMPLETION_THRESHOLD || 80));
const minRate = Math.min(...allVideos.map(v => parseInt(v.finishedRate || 0)));
return {
isCompleted: allDone,
finishedRate: minRate,
method: "multi_chapter_all"
};
}
if (coursewareId) {
const target = allVideos.find(c => String(c.id) === String(coursewareId));
if (target) {
const rate = parseInt(target.finishedRate || 0);
return {
isCompleted: rate >= (config?.COMPLETION_THRESHOLD || 80),
finishedRate: rate,
method: "playTree_match"
};
}
}
}
if (data.locationSite && data.locationSite.finishedRate !== undefined) {
const finishedRate = parseInt(data.locationSite.finishedRate);
return {
isCompleted: finishedRate >= (config?.COMPLETION_THRESHOLD || 80),
finishedRate: finishedRate,
method: "playTrend_total"
};
}
}
return {
isCompleted: false,
finishedRate: 0,
method: "default"
};
} catch (error) {
debugLog(`完成度检查失败: ${error.message}`);
return {
isCompleted: false,
finishedRate: 0,
method: "error"
};
}
},
async getCoursewareList(courseId) {
try {
debugLog(`正在获取课程包详细信息 (ID: ${courseId})...`);
const endpoints = [ _buildUrl("GET_COURSEWARE_LIST", {
courseId: courseId
}), _buildUrl("GET_COURSEWARE_LIST_ALT", {
id: courseId
}) ];
for (const endpoint of endpoints) {
try {
const response = await this.get(endpoint);
if (response && response.success && response.data) {
const data = response.data;
if (data.playTree && data.playTree.children && Array.isArray(data.playTree.children)) {
const videos = data.playTree.children.filter(c => c.rTypeValue === "video" || c.rTypeValue === "courseware");
if (videos.length > 0) {
debugLog(`从 playTree 获取到 ${videos.length} 个课件`);
return videos.map((v, index) => {
const norm = CourseAdapter.normalize({
id: courseId,
courseId: courseId,
dsUnitId: v.id,
title: v.title || `${data.title || "课程"} - 视频${index + 1}`,
duration: v.sumDurationLong || 0
}, "pudong_api_tree");
norm.finishedRate = parseInt(v.finishedRate || 0);
return norm;
});
}
}
if (data.coursewareIdList && Array.isArray(data.coursewareIdList) && data.coursewareIdList.length > 0) {
debugLog(`从 coursewareIdList 获取到 ${data.coursewareIdList.length} 个课件`);
return data.coursewareIdList.map((cw, index) => CourseAdapter.normalize({
id: courseId,
courseId: courseId,
dsUnitId: cw.id || cw.coursewareId,
title: cw.name || cw.title || `${data.title || "课程"} - 视频${index + 1}`,
duration: cw.duration || 0
}, "pudong_api_list"));
}
const list = data.subList || data.courseList || data.lessons;
if (list && Array.isArray(list) && list.length > 0) {
debugLog(`从 API 子列表获取到 ${list.length} 个视频`);
return list.map(item => CourseAdapter.normalize(item, "pudong_api_sublist"));
}
}
} catch {
continue;
}
}
return [];
} catch (error) {
debugLog(`获取课件列表失败: ${error.message}`);
return [];
}
},
async getCourseList() {
const currentUrl = window.location.href;
if (!currentUrl.toLowerCase().includes("specialdetail") && !currentUrl.toLowerCase().includes("specialDetail") && !currentUrl.toLowerCase().includes("channeldetail") && !currentUrl.toLowerCase().includes("pdchanel")) {
return [];
}
let channelId = null;
try {
const urlObj = new URL(currentUrl.replace("#", ""));
channelId = urlObj.searchParams.get("id");
if (!channelId) {
const hash = window.location.hash;
const match = hash.match(/[?&]id=([^&]+)/);
if (match) channelId = match[1];
}
} catch (e) {}
if (!channelId) return [];
return await this.getCourseListFromChannel(channelId);
},
async getCourseListFromChannel(channelId) {
try {
debugLog(`正在从频道获取课程列表 (ID: ${channelId})...`);
const isClassCourseList = window.location.href.toLowerCase().includes("specialdetail");
const apiEndpoints = isClassCourseList ? [ `/inc/nc/class/course/list?id=${channelId}&_t=${Date.now()}`, `/inc/nc/pack/getById?id=${channelId}&_t=${Date.now()}` ] : [ `/inc/nc/pack/getById?id=${channelId}&_t=${Date.now()}`, `/inc/nc/class/course/list?id=${channelId}&_t=${Date.now()}`, `/inc/nc/course/pd/getPackById?id=${channelId}&_t=${Date.now()}`, `/api/nc/channel/detail?id=${channelId}&_t=${Date.now()}`, `/inc/nc/course/getCourseList?channelId=${channelId}&_t=${Date.now()}` ];
for (const endpoint of apiEndpoints) {
try {
debugLog(`尝试API端点: ${endpoint}`);
const response = await this.get(PUDONG_API_CONFIG.getBaseUrl() + endpoint);
if (response && response.success && response.data) {
const data = response.data;
const courseList = [];
if (Array.isArray(data)) {
for (const unit of data) {
if (unit.subList && Array.isArray(unit.subList)) {
for (const course of unit.subList) {
courseList.push(CourseAdapter.normalize({
courseId: course.businessId || course.subId,
id: course.businessId || course.subId,
dsUnitId: course.subId || course.unitId,
courseName: course.name || course.title,
durationStr: course.duration || "00:30:00",
status: course.progress > 0 ? course.progress >= 100 ? "completed" : "in_progress" : "not_started",
teacher: course.teacher,
period: course.period,
categoryText: course.categoryText
}, "pudong_api_class_course"));
}
}
}
} else if (data.pdChannelUnitList) {
for (const unit of data.pdChannelUnitList) {
if (unit.subList) {
for (const course of unit.subList) {
if (course.typeValue === "course") {
courseList.push(CourseAdapter.normalize({
...course,
unitOrder: unit.order
}, "pudong_api_unit"));
}
}
}
}
} else if (data.subList && Array.isArray(data.subList)) {
debugLog(`从 subList 获取到 ${data.subList.length} 个课程`);
data.subList.forEach((item, index) => {
courseList.push(CourseAdapter.normalize({
courseId: item.id || item.courseId || item.dsId,
dsUnitId: item.dsUnitId || item.id,
courseName: item.courseName || item.title || item.name || `课程${index + 1}`,
durationStr: item.durationStr || item.duration || "00:30:00",
status: item.status || "not_started"
}, "pudong_api_sublist"));
});
} else if (data.courseList && Array.isArray(data.courseList)) {
debugLog(`从 courseList 获取到 ${data.courseList.length} 个课程`);
data.courseList.forEach((item, index) => {
courseList.push(CourseAdapter.normalize({
courseId: item.id || item.courseId || item.dsId,
dsUnitId: item.dsUnitId || item.id,
courseName: item.courseName || item.title || item.name || `课程${index + 1}`,
durationStr: item.durationStr || item.duration || "00:30:00",
status: item.status || "not_started"
}, "pudong_api_courselist"));
});
}
if (courseList.length > 0) {
debugLog(`从API获取到 ${courseList.length} 门课程`);
return courseList;
}
}
} catch (error) {
debugLog(`API端点 ${endpoint} 失败: ${error.message}`);
continue;
}
}
debugLog("所有频道API端点都失败了");
return [];
} catch (error) {
debugLog(`从频道获取课程列表失败: ${error.message}`);
return [];
}
},
async getMetaListData(pageNum = 1, pageSize = 1e3) {
try {
const query = encodeURIComponent(JSON.stringify({
searchValue: ""
}));
const url = PUDONG_API_CONFIG.getBaseUrl() + `/inc/meta/list/data?pageNum=${pageNum}&pageSize=${pageSize}&query=${query}&order=[]&filter=[]&namespace=nc.pagecourse&pageName=courseList&_t=${Date.now()}`;
const response = await this.get(url);
if (response && response.data && Array.isArray(response.data)) {
return response.data.map(item => ({
id: item._id,
courseId: item._id,
dsUnitId: item["nc_courses_page_listsource.ds_unit_id"] || item.ds_unit_id,
title: item["nc_courses_page_listsource._name"] || item["nc_courses_page_listsource.title"] || item._name || item.title,
duration: item["nc_courses_page_listsource.duration_long"] || item.duration_long || 0,
progress: 0,
link: `#/pc/nc/page/coursePlayer?id=${item._id}`
}));
}
return [];
} catch (error) {
debugLog(`获取课程列表失败: ${error.message}`);
return [];
}
}
};
const PudongScanner = {
async scanCourses() {
console.log("[PudongScanner] scanCourses called, URL:", window.location.href.substring(0, 80));
const currentUrl = window.location.href;
if (currentUrl.toLowerCase().includes("channeldetail") || currentUrl.toLowerCase().includes("specialdetail") || currentUrl.toLowerCase().includes("specialDetail") || currentUrl.toLowerCase().includes("pdchanel")) {
const apiCourses = await PudongApi.getCourseList();
if (apiCourses && apiCourses.length > 0) return apiCourses;
}
if (currentUrl.toLowerCase().includes("pagecourse/courselist")) {
const apiCourses = await PudongApi.getMetaListData();
if (apiCourses && apiCourses.length > 0) return apiCourses;
}
return [];
}
};
const PudongPlayerFlow = {
async startPlayerFlow() {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "检测到课程播放页面,正在检索所有视频课件...",
type: "info"
});
const courseId = this._extractCourseIdFromUrl();
if (!courseId) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "无法从页面URL中提取课程ID",
type: "error"
});
return null;
}
const apiCourses = await PudongApi.getCoursewareList(courseId);
if (apiCourses && apiCourses.length > 0) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `成功获取到 ${apiCourses.length} 个视频课件`,
type: "success"
});
return apiCourses;
}
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "无法通过API获取视频列表,处理当前单一视频",
type: "warn"
});
return [ {
id: courseId,
courseId: courseId,
dsUnitId: courseId,
title: document.title || `当前视频 ${courseId}`,
courseName: document.title || `当前视频 ${courseId}`,
durationStr: "00:30:00"
} ];
},
_extractCourseIdFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
let courseId = urlParams.get("id");
if (!courseId) {
const hash = window.location.hash;
if (hash.includes("?")) {
const hashParams = new URLSearchParams(hash.split("?")[1]);
courseId = hashParams.get("id");
}
if (!courseId) {
const match = hash.match(/[?&]id=([^&]+)/);
if (match) {
courseId = match[1];
}
}
}
return courseId;
}
};
const PudongHandler = {
PAGE_TYPES: PUDONG_CONSTANTS.PAGE_TYPES,
identifyPage: createPageDetector({
pathPatterns: PUDONG_CONSTANTS.PATH_PATTERNS,
pageTypes: PUDONG_CONSTANTS.PAGE_TYPES,
domSelectors: [ "#coursePlayer", ".course-player", ".pd_course_pla" ],
domMatchType: "PLAYER"
}),
init() {
if (!this.isPudongMode()) return;
console.log("[PudongHandler] 浦东分院处理器已激活");
},
isPudongMode() {
return CONFIG.PUDONG_MODE === true;
},
async scanCourses() {
return await PudongScanner.scanCourses();
},
async startPlayerFlow() {
return await PudongPlayerFlow.startPlayerFlow();
}
};
const cbeadPages = CONSTANTS.PAGE_CONFIG.CBEAD;
const CBEAD_CONSTANTS = {
PATH_PATTERNS: Object.fromEntries(Object.entries(cbeadPages).map(([k, v]) => [ k, v.path ])),
PAGE_TYPES: {
...Object.fromEntries(Object.entries(cbeadPages).map(([k, v]) => [ k, v.type ])),
UNKNOWN: "unknown"
},
PAGE_TYPE_WHITELIST: Object.keys(cbeadPages).filter(k => cbeadPages[k].whitelist),
TIMEOUT: {
NAVIGATION: 3e3,
PAGE_LOAD: 1e4,
PLAYER_LOAD: 15e3,
LEARNING_SESSION: 6e5,
POLLING_INTERVAL: 500
},
TIMING: {
NAVIGATION: 3e3,
PLAYER_INIT_TIMEOUT: 1e4,
PLAYER_CHECK_INTERVAL: 500,
PROGRESS_REPORT_INTERVAL: 3e4,
CHAPTER_CHECK_INTERVAL: 15e3,
MUTE_VERIFY_DELAY: 1e3,
NEXT_COURSE_DELAY: 2e3,
CHAPTER_SWITCH_DELAY: 2e3,
PROTECTION_TIMEOUT: 100
},
THRESHOLDS: {
COMPLETED_PROGRESS: 100,
SKIP_COURSE_PROGRESS: 100,
CHAPTER_CHECK_MIN_PROGRESS: 80,
VIDEO_START_THRESHOLD: 10,
STORAGE_EXPIRY: 864e5
},
STORAGE_KEYS: {
LEARNING_PROGRESS: "cbead_learning_progress"
},
SELECTORS: {
START_BUTTON: '.start-study-btn, [class*="start-learn"]',
PROGRESS_BAR: ".el-progress-bar",
VIDEO_PLAYER: 'video, [class*="video-player"], [class*="player-container"]',
LEARNING_COMPLETED: '[class*="completed"], [class*="finished"]'
},
BRANCH_LIST: {
CONTAINER_SELECTOR: ".card-wrapper",
ITEM_SELECTOR: ".card-item",
CARD_SELECTOR: ".card-item",
CARD_BOX_SELECTOR: ".card-box",
STATUS_SELECTOR: ".status",
TITLE_SELECTOR: ".title-row",
PAGINATION_BOX_SELECTOR: ".e-pagination-box",
PAGINATION_SELECTOR: ".zxy-pagination",
PAGE_ITEM_SELECTOR: ".zxy-pagination-item:not(.zxy-pagination-prev):not(.zxy-pagination-next):not(.zxy-pagination-dots)",
ACTIVE_PAGE_SELECTOR: ".zxy-pagination-item.active",
NEXT_BTN_SELECTOR: ".e-pagination-box .zxy-pagination .zxy-pagination-next",
PREV_BTN_SELECTOR: ".zxy-pagination-prev",
DISABLED_PAGINATION_CLASS: "zxy-pagination-disabled",
TAG_CONTAINER_SELECTOR: ".label-container .tag-list",
TAG_BTN_SELECTOR: ".label-btn",
CATEGORY_MENU_SELECTOR: ".menu-wrapper .catalog-menu",
CATEGORY_ITEM_SELECTOR: ".catalog-menu-item",
PAGE_LOAD_TIMEOUT: 5e3,
PAGINATION_DELAY: 2e3,
TAG_SWITCH_DELAY: 2e3,
API_ENDPOINTS: {
COURSE_LIST: "/api/v1/course-study/course-info/front/find-by-ids",
MY_COURSE_LIST: "/api/v1/course-study/course-study-progress/personCourse-list",
STUDY_PROGRESS: "/api/v1/course-study/course-study-progress/course-section-study-number"
},
VALIDATION: {
PAGE_LOAD_TIMEOUT: 1e4,
SKELETON_TIMEOUT: 5e3,
CONTENT_TIMEOUT: 8e3,
VUE_TIMEOUT: 6e3,
MAX_RETRY_COUNT: 3
}
},
HEARTBEAT: {
INTERVAL: 1e4,
ENABLED: true,
MAX_RETRIES: 3,
TIMEOUT: 5e3
},
SCANNER: {
ELEMENT_WAIT: {
MAX_ATTEMPTS: 20,
DELAY: 500
},
DEEP_SCAN: {
MAX_DEPTH_DEFAULT: 10,
MAX_DEPTH_VUE: 15,
MAX_ITEMS: 5e3
},
PAGINATION: {
ITEMS_PER_PAGE: 12,
MAX_PAGES: 100
},
PROGRESS: {
IN_PROGRESS_DEFAULT: 50,
COMPLETED: 100,
NOT_STARTED: 0
}
}
};
const scannerCore = {
categorizeAndSortCourses(courses) {
if (!Array.isArray(courses)) {
console.warn("[CbeadScanner] categorizeAndSortCourses 收到非数组参数:", typeof courses);
return {
inProgress: [],
notStarted: [],
completed: [],
toLearn: []
};
}
const categorized = {
inProgress: [],
notStarted: [],
completed: [],
toLearn: []
};
courses.forEach(course => {
if (course.status === "in_progress") {
categorized.inProgress.push(course);
categorized.toLearn.push(course);
} else if (course.status === "not_started") {
categorized.notStarted.push(course);
categorized.toLearn.push(course);
} else if (course.status === "completed") {
categorized.completed.push(course);
}
});
categorized.inProgress.sort((a, b) => b.progress - a.progress);
return categorized;
},
getSortedLearningList(courses) {
const categorized = this.categorizeAndSortCourses(courses);
const sortedList = [ ...categorized.inProgress, ...categorized.notStarted ];
return sortedList;
}
};
const CBEAD_API_CONFIG = {
baseUrl: null,
endpoints: {
SECTION_PROGRESS: "/api/v1/course-study/course-front/course-section-progress",
COURSE_LIST: "/api/v1/course-study/course-info/front/find-by-ids",
COURSE_PROGRESS: "/api/v1/course-study/course-front/course-info-progress",
COURSE_SECTION: "/api/v1/course-study/course-front/course-info-section",
CLASS_INFO: "/api/v1/training/student/class-info/front/find-by-ids",
COURSE_POOL: "/api/v1/course-study/resource-pool/course/front",
CHAPTER_ACTIVITY_ALL: "/api/v1/training/student/class-info/safe/chapter-activity-all",
STUDY_RATE_BY_ACTIVITY: "/api/v1/training/class-activity/study-rate-by-activityId",
COURSE_INFO: "/api/v1/course-study/course-front/info"
},
getBaseUrl() {
this.baseUrl = this.baseUrl || `https://${window.location.hostname}`;
return this.baseUrl;
},
getUrl(endpoint) {
const baseUrl = this.getBaseUrl();
const endpointPath = this.endpoints[endpoint] || endpoint;
return baseUrl + endpointPath;
}
};
const CbeadApi = {
...ApiCore,
_extractToken() {
const raw = ApiCore._extractToken.call(this);
if (!raw) return null;
try {
const parsed = JSON.parse(raw);
if (parsed.access_token && parsed.access_token.length > 10) {
return parsed.access_token;
}
} catch {}
return raw;
},
_prepareHeaders(customHeaders = {}, data = null) {
const headers = ApiCore._prepareHeaders.call(this, customHeaders, data);
const token = this._extractToken();
if (token) {
headers["Authorization"] = `Bearer__${token}`;
}
headers["Version"] = "12.4.0";
return headers;
},
async getCourseProgress(courseIds) {
if (!courseIds || courseIds.length === 0) return [];
const url = CBEAD_API_CONFIG.getUrl("COURSE_PROGRESS") + "?ids=" + courseIds.join(",") + "&_=" + Date.now();
const response = await this.get(url);
return Array.isArray(response) ? response : [];
},
async getCourseListByOrg(organizationId, pageNum = 1, pageSize = 20) {
const homeConfigId = this._extractHomeConfigId();
if (homeConfigId) {
try {
const url = CBEAD_API_CONFIG.getUrl("COURSE_POOL") + `?page=${pageNum}&pageSize=${pageSize}&homeConfigId=${homeConfigId}&viewRule=0&publishClient=1&type=1&order=2&orderBy=0&_=${Date.now()}`;
const response = await this.get(url);
const items = response?.items || response?.data?.items || response?.data?.list || [];
if (items.length > 0) return items;
} catch (e) {
console.warn("[CbeadApi] resource-pool API失败:", e.message);
}
} else {
console.warn("[CbeadApi] 未找到 homeConfigId");
}
return [];
},
_extractHomeConfigId() {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.includes("homeConfigId")) {
const match = key.match(/"homeConfigId":"([a-f0-9-]+)"/);
if (match) return match[1];
}
}
return "";
},
async getClassInfo(classIds) {
if (!classIds || classIds.length === 0) return [];
const url = CBEAD_API_CONFIG.getUrl("CLASS_INFO");
const response = await this.post(url, JSON.stringify({
ids: classIds
}));
return response?.data || [];
},
async getChapterActivities(classId) {
if (!classId) return [];
const url = CBEAD_API_CONFIG.getUrl("CHAPTER_ACTIVITY_ALL") + "?classId=" + classId + "&_=" + Date.now();
const response = await this.get(url);
return Array.isArray(response) ? response : [];
},
async getStudyRateByActivity(activityIds, classId) {
if (!activityIds || activityIds.length === 0 || !classId) return [];
const url = CBEAD_API_CONFIG.getUrl("STUDY_RATE_BY_ACTIVITY");
const data = "activityIds=" + activityIds.join(",") + "&classId=" + classId;
const response = await this.post(url, data);
return Array.isArray(response) ? response : [];
},
async getCourseInfo(courseId) {
if (!courseId) return null;
const url = CBEAD_API_CONFIG.getUrl("COURSE_INFO") + "/" + courseId + "?type=1&_=" + Date.now();
const response = await this.get(url);
return response || null;
},
async getSectionProgress(resourceIds, courseId) {
if (!resourceIds || resourceIds.length === 0 || !courseId) return [];
const url = CBEAD_API_CONFIG.getUrl("SECTION_PROGRESS");
const data = "resourceIds=" + resourceIds.join(",") + "&courseId=" + courseId;
const response = await this.post(url, data);
return Array.isArray(response) ? response : [];
}
};
const STATUS_TEXT_MAP = {
"学习中": "in_progress",
"已完成": "completed",
"未开始": "not_started"
};
function resolveCourseStatus({progress: progress = 0, statusText: statusText, studyStatus: studyStatus} = {}) {
let status = "not_started";
if (studyStatus !== undefined) {
if (studyStatus === 1 || studyStatus === "studying") {
status = "in_progress";
} else if (studyStatus === 2 || studyStatus === "completed") {
status = "completed";
} else if (studyStatus === 0 || studyStatus === "not_started") {
status = "not_started";
}
} else if (statusText && STATUS_TEXT_MAP[statusText]) {
status = STATUS_TEXT_MAP[statusText];
} else if (progress >= 100) {
status = "completed";
} else if (progress > 0) {
status = "in_progress";
}
return {
status: status,
isCompleted: progress >= 100
};
}
const branchListMethods = {
async scanCoursesFromBranchListPage(pageNum = 1) {
const orgId = this._getOrgIdFromUrl();
if (!orgId) return [];
try {
const apiCourses = await CbeadApi.getCourseListByOrg(orgId, pageNum);
if (!apiCourses || apiCourses.length === 0) return [];
const courseIds = apiCourses.map(c => c.id || c.courseId).filter(Boolean);
const progressList = await CbeadApi.getCourseProgress(courseIds);
const progressMap = {};
if (Array.isArray(progressList)) {
progressList.forEach(p => {
progressMap[p.courseId] = p;
});
}
const courses = [];
for (const item of apiCourses) {
const prog = progressMap[item.id || item.courseId] || {};
const merged = {
...item,
...prog
};
const course = this._buildCourseInfoFromApiData(merged);
if (course && course.id) {
course.source = "api";
courses.push(course);
}
}
if (courses.length > 0) {
console.log(`[CbeadScanner] 从API成功获取 ${courses.length} 门课程`);
return courses;
}
} catch (e) {
console.warn("[CbeadScanner] API获取课程失败:", e.message);
}
return [];
},
_buildCourseInfoFromApiData(data) {
const courseId = data.id || data.courseId || data.dsUnitId || null;
if (!courseId) return null;
const title = data.courseName || data.title || data.name || "未知课程";
const studyLink = `#/study/course/detail/${courseId}`;
let progress = 0;
if (data.studyProgress !== undefined) {
progress = data.studyProgress;
} else if (data.progress !== undefined) {
progress = data.progress;
} else if (data.percentage !== undefined) {
progress = data.percentage;
}
if (data.finishStatus === 2) {
progress = 100;
} else if (data.finishStatus === 1) {
progress = progress || 1;
} else if (data.finishStatus === 0) {
progress = 0;
}
if (data.studyStatus === 1 || data.studyStatus === "studying") {
progress = progress || 1;
} else if (data.studyStatus === 2 || data.studyStatus === "completed") {
progress = 100;
} else if (data.studyStatus === 0 || data.studyStatus === "not_started") {
progress = 0;
}
const {status: status} = resolveCourseStatus({
progress: progress,
studyStatus: data.finishStatus === 2 ? 2 : data.studyStatus
});
return {
id: courseId,
courseId: courseId,
dsUnitId: courseId,
title: title,
courseName: title,
link: studyLink,
progress: progress,
isCompleted: progress >= CBEAD_CONSTANTS.THRESHOLDS.COMPLETED_PROGRESS,
status: status,
element: null,
source: "cbead_vue_data_scan",
rawData: data
};
},
_getOrgIdFromUrl() {
const hash = window.location.hash || "";
const match = hash.match(/branch-list-v\/([a-f0-9-]+)/i);
return match ? match[1] : null;
},
async scanAllCoursesWithPagination() {
const allCourses = [];
let pageNum = 1;
console.log(`[CbeadScanner] 开始翻页扫描课程...`);
while (true) {
const pageCourses = await this.scanCoursesFromBranchListPage(pageNum);
if (!pageCourses || pageCourses.length === 0) {
console.log(`[CbeadScanner] 第 ${pageNum} 页无数据,停止扫描`);
break;
}
console.log(`[CbeadScanner] 第 ${pageNum} 页: 扫描到 ${pageCourses.length} 门课程`);
allCourses.push(...pageCourses);
pageNum++;
}
console.log(`[CbeadScanner] 翻页扫描完成,共 ${allCourses.length} 门课程`);
return allCourses;
}
};
const columnMethods = {
async scanCoursesFromColumnPage() {
const classId = this._extractClassIdFromUrl();
if (!classId) return [];
const apiCourses = await this._scanFromColumnPageApi(classId);
if (apiCourses && apiCourses.length > 0) {
console.log(`[CbeadScanner] 从API成功获取 ${apiCourses.length} 门课程`);
return apiCourses;
}
return [];
},
_extractClassIdFromUrl() {
const hash = window.location.hash || "";
const match = hash.match(/(?:train-new\/class-detail|center\/my\/course)\/(?:[^/]*?)([a-f0-9-]{36})/i);
return match ? match[1] : null;
},
async _scanFromColumnPageApi(classId) {
const activities = await CbeadApi.getChapterActivities(classId);
if (!activities || activities.length === 0) return [];
const allActivityIds = [];
activities.forEach(group => {
if (group.classActivitys && Array.isArray(group.classActivitys)) {
group.classActivitys.forEach(act => {
if (act.id) allActivityIds.push(act.id);
});
}
});
let rateMap = {};
try {
const rateList = await CbeadApi.getStudyRateByActivity(allActivityIds, classId);
if (Array.isArray(rateList)) {
rateList.forEach(r => {
rateMap[r.activityId] = r;
});
}
} catch (e) {
console.warn("[CbeadScanner] 获取完成率失败,使用默认值:", e.message);
}
const courses = [];
activities.forEach(group => {
if (!group.classActivitys || !Array.isArray(group.classActivitys)) return;
group.classActivitys.forEach(act => {
const course = this._buildCourseFromChapterActivity(act, rateMap[act.id]);
if (course) courses.push(course);
});
});
return courses;
},
_buildCourseFromChapterActivity(act, rate) {
const courseId = act.businessId;
if (!courseId) return null;
const title = act.businessName || "未知课程";
const studyLink = `#/study/course/detail/${courseId}`;
const progressMeta = act.classStudentActivityProgress || {};
let progress = 0;
let status = "not_started";
if (progressMeta.finishStatus === 2 || progressMeta.status === 2) {
progress = 100;
status = "completed";
} else if (progressMeta.finishStatus === 1 || progressMeta.status === 1) {
progress = rate && rate.completedRate != null ? Math.round(rate.completedRate * 100) : 1;
status = "in_progress";
} else {
progress = 0;
status = "not_started";
}
return {
id: courseId,
courseId: courseId,
dsUnitId: courseId,
title: title,
courseName: title,
link: studyLink,
progress: progress,
isCompleted: progress >= CBEAD_CONSTANTS.THRESHOLDS.COMPLETED_PROGRESS,
status: status,
element: null,
source: "cbead_column_api",
rawData: act
};
}
};
const CbeadScanner = {
...scannerCore,
...branchListMethods,
...columnMethods
};
const learningState = {
failed: false,
failureReason: null,
markFailed(reason) {
this.failed = true;
this.failureReason = reason;
console.error(`[CbeadProgressManager] 🚨 课程已标记为失败: ${reason}`);
},
reset() {
this.failed = false;
this.failureReason = null;
},
isFailed() {
return this.failed;
},
getFailureReason() {
return this.failureReason;
}
};
const CbeadProgressManager = {
getLearningState() {
return learningState;
},
clearLearningProgress() {
try {
localStorage.removeItem(CBEAD_CONSTANTS.STORAGE_KEYS.LEARNING_PROGRESS);
console.log("[CbeadProgressManager] 学习进度已清除");
} catch (error) {
console.error("[CbeadProgressManager] 清除学习进度失败:", error);
}
},
saveLearningQueue(learningList, totalCourses, pageUrl) {
try {
const data = {
learningList: learningList.map(c => ({
id: c.id,
title: c.title,
link: c.link,
progress: c.progress,
status: c.status
})),
totalCourses: totalCourses,
currentIndex: 0,
pageUrl: pageUrl,
timestamp: Date.now()
};
localStorage.setItem(CBEAD_CONSTANTS.STORAGE_KEYS.LEARNING_PROGRESS, JSON.stringify(data));
console.log(`[CbeadProgressManager] 学习队列已保存: ${learningList.length} 门课程`);
} catch (error) {
console.error("[CbeadProgressManager] 保存学习队列失败:", error);
}
},
loadLearningQueue() {
try {
const data = localStorage.getItem(CBEAD_CONSTANTS.STORAGE_KEYS.LEARNING_PROGRESS);
if (!data) {
console.log("[CbeadProgressManager] 未找到学习队列");
return null;
}
const parsed = JSON.parse(data);
const QUEUE_TIMEOUT = 24 * 60 * 60 * 1e3;
if (Date.now() - parsed.timestamp > QUEUE_TIMEOUT) {
console.log("[CbeadProgressManager] 学习队列已超时,清除");
this.clearLearningProgress();
return null;
}
console.log(`[CbeadProgressManager] 学习队列已加载: 当前索引 ${parsed.currentIndex}/${parsed.learningList.length}`);
return parsed;
} catch (error) {
console.error("[CbeadProgressManager] 加载学习队列失败:", error);
return null;
}
},
updateCurrentIndex(newIndex) {
try {
const data = this.loadLearningQueue();
if (data) {
data.currentIndex = newIndex;
data.timestamp = Date.now();
localStorage.setItem(CBEAD_CONSTANTS.STORAGE_KEYS.LEARNING_PROGRESS, JSON.stringify(data));
console.log(`[CbeadProgressManager] 当前索引已更新: ${newIndex}`);
}
} catch (error) {
console.error("[CbeadProgressManager] 更新索引失败:", error);
}
},
hasValidQueue(currentUrl) {
const data = this.loadLearningQueue();
if (!data) return false;
if (!data.learningList || !Array.isArray(data.learningList)) {
console.warn("[CbeadProgressManager] 队列数据结构无效: 缺少 learningList");
return false;
}
if (typeof data.currentIndex !== "number") {
console.warn("[CbeadProgressManager] 队列数据结构无效: 缺少 currentIndex");
return false;
}
if (data.learningList.length === 0) {
console.warn("[CbeadProgressManager] 学习队列为空");
return false;
}
if (data.currentIndex < 0 || data.currentIndex >= data.learningList.length) {
console.warn(`[CbeadProgressManager] currentIndex ${data.currentIndex} 超出范围 [0, ${data.learningList.length})`);
return false;
}
if (currentUrl && data.pageUrl) {
try {
const currentUrlObj = new URL(currentUrl, "https://dummy.com");
const savedUrlObj = new URL(data.pageUrl, "https://dummy.com");
if (currentUrlObj.origin !== savedUrlObj.origin) {
console.warn("[CbeadProgressManager] URL 域名不匹配");
return false;
}
const currentPath = currentUrlObj.pathname || "";
const savedPath = savedUrlObj.pathname || "";
if (currentPath !== savedPath) {
if (!savedPath.startsWith(currentPath) && !currentPath.startsWith(savedPath)) {
console.warn(`[CbeadProgressManager] URL 路径不匹配: ${currentPath} vs ${savedPath}`);
return false;
}
}
} catch (e) {
console.warn("[CbeadProgressManager] URL 解析失败,跳过 URL 匹配检查");
}
}
console.log("[CbeadProgressManager] 学习队列有效");
return true;
},
async returnToList(returnUrl) {
console.log("[CbeadProgressManager] 准备返回列表页...");
this.clearLearningProgress();
await new Promise(resolve => setTimeout(resolve, CBEAD_CONSTANTS.TIMING.NEXT_COURSE_DELAY));
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `🔄 返回列表页,扫描后自动继续...`,
type: "info"
});
if (returnUrl && !returnUrl.includes("study/course/detail")) {
console.log(`[CbeadProgressManager] 🚀 返回列表页: ${returnUrl}`);
window.location.href = returnUrl;
} else {
console.warn("[CbeadProgressManager] ⚠️ 没有有效的返回 URL,尝试使用浏览器后退");
window.history.back();
}
},
publishCompletionStats(totalCourses) {
EventBus.publish(CONSTANTS.EVENTS.STATISTICS_UPDATE, {
total: totalCourses,
completed: totalCourses,
learned: totalCourses,
failed: 0,
skipped: 0
});
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, "学习完成");
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `🎉 专题班全部课程学习完成!共 ${totalCourses} 门课程`,
type: "success"
});
},
normalizeCourseId(rawId) {
if (!rawId) return null;
const uuidPattern = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
if (uuidPattern.test(rawId)) {
return rawId;
}
if (/^\d+$/.test(rawId)) {
return rawId;
}
const match = rawId.match(/[a-f0-9-]{36}/);
return match ? match[0] : rawId;
},
extractPlayerParams() {
const hash = window.location.hash;
const newMatch = hash.match(/detail\/(?:[^/]*@@)?([a-f0-9-]{36})/i);
if (newMatch) {
console.log(`[CbeadProgressManager] ✅ 使用新 URL 格式提取参数`);
return {
uuid: newMatch[1],
courseId: newMatch[1],
coursewareId: newMatch[1]
};
}
const midMatch = hash.match(/detail\/(\d+)&([a-f0-9-]{36})/i);
if (midMatch) {
console.log(`[CbeadProgressManager] ✅ 使用中间 URL 格式提取参数`);
return {
courseId: midMatch[1],
uuid: midMatch[2],
coursewareId: midMatch[2]
};
}
const oldMatch = hash.match(/detail\/(\d+)&([a-f0-9-]+)&(\d+)\/(\d+)\/(\d+)/);
if (oldMatch) {
console.log(`[CbeadProgressManager] ⚠️ 使用旧 URL 格式提取参数(兼容模式)`);
return {
courseId: oldMatch[1],
uuid: oldMatch[2],
sectionIndex: oldMatch[3],
totalSections: oldMatch[4],
currentIndex: oldMatch[5]
};
}
console.warn(`[CbeadProgressManager] ⚠️ 无法从 URL 提取参数: ${hash}`);
return null;
}
};
class IntervalManager {
constructor() {
this.intervals = new Set;
this.timeouts = new Set;
this._unloadHandlerRegistered = false;
this._boundUnloadHandler = null;
}
setInterval(callback, delay) {
const id = setInterval(callback, delay);
this.intervals.add(id);
return id;
}
setTimeout(callback, delay) {
const id = setTimeout(callback, delay);
this.timeouts.add(id);
return id;
}
clearAll() {
this.intervals.forEach(id => {
try {
clearInterval(id);
} catch {}
});
this.intervals.clear();
this.timeouts.forEach(id => {
try {
clearTimeout(id);
} catch {}
});
this.timeouts.clear();
}
getCounts() {
return {
intervals: this.intervals.size,
timeouts: this.timeouts.size
};
}
registerUnloadHandler() {
if (this._unloadHandlerRegistered) return;
this._boundUnloadHandler = () => this.clearAll();
window.addEventListener("beforeunload", this._boundUnloadHandler);
window.addEventListener("pagehide", this._boundUnloadHandler);
window.addEventListener("unload", this._boundUnloadHandler);
this._unloadHandlerRegistered = true;
}
unregisterUnloadHandler() {
if (!this._unloadHandlerRegistered || !this._boundUnloadHandler) return;
window.removeEventListener("beforeunload", this._boundUnloadHandler);
window.removeEventListener("pagehide", this._boundUnloadHandler);
window.removeEventListener("unload", this._boundUnloadHandler);
this._unloadHandlerRegistered = false;
this._boundUnloadHandler = null;
}
}
const BasePlayer = {
DEBUG: false,
_debugLog(...args) {
if (this.DEBUG) {
console.log(...args);
}
},
detectVideoPlayer() {
const videoJsPlayer = document.querySelector("video.vjs-tech");
if (videoJsPlayer) {
const videoJsInstance = window.player || videoJsPlayer.player && videoJsPlayer.player;
return {
type: "videojs",
element: videoJsPlayer,
player: videoJsInstance,
getCurrentTime: () => videoJsPlayer.currentTime,
setCurrentTime: time => {
videoJsPlayer.currentTime = time;
},
getDuration: () => videoJsPlayer.duration,
play: () => videoJsPlayer.play(),
pause: () => videoJsPlayer.pause(),
setMuted: muted => {
videoJsPlayer.muted = muted;
if (videoJsInstance && typeof videoJsInstance.muted === "function") {
try {
videoJsInstance.muted(muted);
} catch {}
}
if (muted) videoJsPlayer.volume = 0;
},
getMuted: () => videoJsPlayer.muted
};
}
const genericVideo = document.querySelector("video");
if (genericVideo) {
return {
type: "generic",
element: genericVideo,
getCurrentTime: () => genericVideo.currentTime,
setCurrentTime: time => {
genericVideo.currentTime = time;
},
getDuration: () => genericVideo.duration,
play: () => genericVideo.play(),
pause: () => genericVideo.pause(),
setMuted: muted => {
genericVideo.muted = muted;
},
getMuted: () => genericVideo.muted
};
}
return null;
},
parseTimeToSeconds(timeStr) {
if (!timeStr) return 0;
const match = timeStr.match(/(\d+):(\d+)/);
if (!match) return 0;
return parseInt(match[1]) * 60 + parseInt(match[2]);
},
createIntervalManager() {
return new IntervalManager;
},
async waitForPlayerReady(maxWaitTime = 1e4, checkInterval = 500) {
const startTime = Date.now();
const manager = new IntervalManager;
return new Promise(resolve => {
manager.setInterval(() => {
try {
const elapsed = Date.now() - startTime;
const player = this.detectVideoPlayer();
if (player) {
const duration = player.getDuration();
if (duration && duration > 0 && isFinite(duration)) {
manager.clearAll();
resolve(player);
return;
}
}
if (elapsed >= maxWaitTime) {
manager.clearAll();
resolve(null);
}
} catch {
const elapsed = Date.now() - startTime;
if (elapsed >= maxWaitTime) {
manager.clearAll();
resolve(null);
}
}
}, checkInterval);
});
},
getServerProgress(currentChapterIndex = null) {
try {
const chapterProgress = this.extractChapterProgress(false);
if (!chapterProgress) return null;
if (currentChapterIndex !== null) {
const currentChapter = chapterProgress.chapters?.find(ch => ch.index === currentChapterIndex);
if (currentChapter) return currentChapter.progress;
}
if (chapterProgress.firstIncomplete) {
return chapterProgress.firstIncomplete.progress;
}
return null;
} catch {
return null;
}
},
cleanupPlaybackResources(resources, _reason = "未知原因") {
const {wakeLock: wakeLock, handleVisibilityChange: handleVisibilityChange, timerManager: timerManager} = resources;
if (wakeLock) {
try {
wakeLock.release();
} catch {}
}
if (handleVisibilityChange) {
try {
document.removeEventListener("visibilitychange", handleVisibilityChange);
} catch {}
}
if (timerManager) {
try {
timerManager.clearAll();
} catch {}
}
}
};
const CbeadPlayer = {
...BasePlayer,
_extractCourseIdFromUrl() {
const hash = window.location.hash || "";
const match = hash.match(/study\/course\/detail\/([a-f0-9-]{36})/i);
return match ? match[1] : null;
},
async _extractChapterProgressFromApi(verbose = true) {
const courseId = this._extractCourseIdFromUrl();
if (!courseId) {
if (verbose) console.warn("[CbeadPlayer] API: 未找到课程ID");
return null;
}
let courseInfo;
try {
courseInfo = await CbeadApi.getCourseInfo(courseId);
} catch (e) {
if (verbose) console.warn("[CbeadPlayer] API: 获取课程信息失败:", e.message);
return null;
}
if (!courseInfo || !courseInfo.courseChapters) {
if (verbose) console.warn("[CbeadPlayer] API: 课程信息无章节数据");
return null;
}
const sections = [];
const resourceIds = [];
courseInfo.courseChapters.forEach(chapter => {
if (chapter.courseChapterSections) {
chapter.courseChapterSections.forEach(sec => {
sections.push({
chapterId: chapter.id,
chapterName: chapter.name,
sectionId: sec.id,
sectionName: sec.name,
resourceId: sec.resourceId || sec.attachmentId,
totalTime: sec.totalTime || 0,
required: sec.required !== 0,
sequence: sec.sequence || 0
});
if (sec.resourceId) resourceIds.push(sec.resourceId);
});
}
});
if (sections.length === 0) return null;
let progressList = [];
try {
progressList = await CbeadApi.getSectionProgress(resourceIds, courseId);
} catch (e) {
if (verbose) console.warn("[CbeadPlayer] API: 获取节进度失败:", e.message);
}
const progressMap = {};
if (Array.isArray(progressList)) {
progressList.forEach(p => {
if (p.resourceId) progressMap[p.resourceId] = p;
});
}
const chapters = [];
sections.forEach((sec, index) => {
const prog = progressMap[sec.resourceId] || {};
const completedRate = prog.completedRate != null ? prog.completedRate : 0;
const finishStatus = prog.finishStatus != null ? prog.finishStatus : 0;
let status = "not_started";
let isCompleted = false;
let progress = 0;
if (finishStatus === 2 || completedRate >= 100) {
status = "completed";
isCompleted = true;
progress = 100;
} else if (finishStatus === 1 || completedRate > 0) {
status = "in_progress";
progress = completedRate;
} else {
status = "not_started";
progress = 0;
}
chapters.push({
index: index + 1,
title: sec.sectionName || sec.chapterName,
status: status,
statusText: status === "completed" ? "已完成" : status === "in_progress" ? "学习中" : "未开始",
progress: progress,
isCompleted: isCompleted,
element: null
});
});
const stats = {
completed: chapters.filter(ch => ch.status === "completed").length,
inProgress: chapters.filter(ch => ch.status === "in_progress").length,
notStarted: chapters.filter(ch => ch.status === "not_started").length
};
const firstIncomplete = chapters.find(ch => !ch.isCompleted);
if (verbose) {
console.log(`[CbeadPlayer] API: 找到 ${chapters.length} 个章节: ✅ ${stats.completed} 📖 ${stats.inProgress} 📝 ${stats.notStarted}`);
if (firstIncomplete) {
const sd = CONSTANTS.STATUS_DISPLAY[firstIncomplete.status] || {
text: "未知"
};
console.log(`[CbeadPlayer] API: 💡 当前章节: ${firstIncomplete.index} - ${sd.text} (${firstIncomplete.progress}%)`);
}
}
return {
total: chapters.length,
completed: stats.completed,
inProgress: stats.inProgress,
notStarted: stats.notStarted,
chapters: chapters,
firstIncomplete: firstIncomplete ?? null,
allCompleted: firstIncomplete == null
};
},
async extractChapterProgress(verbose = true) {
return await this._extractChapterProgressFromApi(verbose);
},
async getServerProgress(currentChapterIndex = null) {
try {
const chapterProgress = await this.extractChapterProgress(false);
if (!chapterProgress) return null;
if (currentChapterIndex !== null) {
const currentChapter = chapterProgress.chapters?.find(ch => ch.index === currentChapterIndex);
if (currentChapter) return currentChapter.progress;
}
if (chapterProgress.firstIncomplete) {
return chapterProgress.firstIncomplete.progress;
}
return null;
} catch {
return null;
}
},
async isCourseReallyCompleted() {
const chapterProgress = await this.extractChapterProgress();
if (!chapterProgress) {
console.warn("[CbeadPlayer] 无法判断章节进度,假设未完成");
return false;
}
if (chapterProgress.allCompleted) {
console.log(`[CbeadPlayer] ✅ 所有章节已完成 (${chapterProgress.completed}/${chapterProgress.total})`);
return true;
}
const first = chapterProgress.firstIncomplete;
if (first) {
console.log(`[CbeadPlayer] 📖 章节 ${first.index} 未完成 (${first.status}, ${first.progress}%)`);
} else {
return chapterProgress.completed >= chapterProgress.total;
}
return false;
},
clickChapter(chapterTitle) {
try {
const catalog = document.querySelector(".course-side-catalog");
if (!catalog) {
console.warn("[CbeadPlayer] 未找到章节目录");
return false;
}
const chapterBoxes = catalog.querySelectorAll(".chapter-list-box");
for (const box of chapterBoxes) {
const titleEl = box.querySelector(".chapter-item .text-overflow");
const title = titleEl?.textContent?.trim();
if (title === chapterTitle || title?.includes(chapterTitle)) {
box.click();
const playBtn = box.querySelector(".section-item");
if (playBtn) playBtn.click();
return true;
}
}
return false;
} catch (error) {
console.error(`[CbeadPlayer] 点击章节失败:`, error);
return false;
}
}
};
const WORKER_CODE = `\n let timer = null;\n let config = null;\n\n self.onmessage = async function(e) {\n const { action, data } = e.data;\n\n if (action === 'start') {\n config = data;\n console.log('[CbeadHeartbeatWorker] 🚀 Worker 启动,心跳间隔:', config?.interval || 10000, 'ms');\n\n timer = setInterval(async () => {\n try {\n // 【TODO】后续补充企业分院 API 调用\n // 目前仅发送心跳消息保活 Worker 框架\n // 浦东分院 API (/inc/nc/course/play/pulseSaveRecord) 在企业分院无效\n // 企业分院专用 API 待探索\n\n // 示例调用(待实现):\n // const result = await sendHeartbeat({\n // courseId: config.courseId,\n // chapterId: config.chapterId,\n // timestamp: Date.now()\n // });\n\n self.postMessage({\n type: 'heartbeat',\n timestamp: Date.now(),\n config: config\n });\n } catch (err) {\n self.postMessage({\n type: 'error',\n error: err.message,\n timestamp: Date.now()\n });\n }\n }, config?.interval || 10000);\n\n } else if (action === 'stop') {\n if (timer) {\n clearInterval(timer);\n timer = null;\n }\n config = null;\n console.log('[CbeadHeartbeatWorker] 🛑 Worker 已停止');\n\n } else if (action === 'ping') {\n self.postMessage({\n type: 'pong',\n timestamp: Date.now(),\n config: config\n });\n }\n };\n`;
const CbeadHeartbeatWorker = {
createWorker() {
const blob = new Blob([ WORKER_CODE ], {
type: "application/javascript"
});
const workerUrl = URL.createObjectURL(blob);
return new Worker(workerUrl);
},
start(courseInfo) {
const worker = this.createWorker();
worker.onmessage = e => {
const {type: type, timestamp: timestamp, config: config} = e.data;
switch (type) {
case "heartbeat":
console.log(`[CbeadHeartbeatWorker] 💓 心跳 ${timestamp} - 课程: ${config?.courseId || "N/A"}`);
break;
case "error":
console.error(`[CbeadHeartbeatWorker] ❌ 心跳错误: ${e.data.error}`);
break;
case "pong":
console.log(`[CbeadHeartbeatWorker] 🔵 Pong ${timestamp}`);
break;
default:
console.log(`[CbeadHeartbeatWorker] 📨 消息:`, e.data);
}
};
worker.onerror = error => {
console.error(`[CbeadHeartbeatWorker] 💥 Worker 错误:`, error);
};
worker.postMessage({
action: "start",
data: {
courseId: courseInfo.courseId,
chapterId: courseInfo.chapterId,
interval: courseInfo.interval || 1e4
}
});
console.log(`[CbeadHeartbeatWorker] ✅ Worker 已启动`);
return worker;
},
stop(worker) {
if (worker) {
worker.postMessage({
action: "stop"
});
worker.terminate();
console.log(`[CbeadHeartbeatWorker] 🛑 Worker 已终止`);
}
},
async ping(worker) {
if (!worker) return false;
return new Promise(resolve => {
const timeout = setTimeout(() => {
resolve(false);
}, 2e3);
worker.onmessage = e => {
if (e.data.type === "pong") {
clearTimeout(timeout);
resolve(true);
}
};
worker.postMessage({
action: "ping"
});
});
}
};
const CbeadPlayerFlow = {
async learnWithRealPlayback(course) {
console.log(`[CbeadPlayerFlow] ========== 开始真实播放学习 ==========`);
console.log(`[CbeadPlayerFlow] 📚 课程名称: ${course.title}`);
console.log(`[CbeadPlayerFlow] 🆔 课程ID: ${course.id || course.courseId || "N/A"}`);
console.log(`[CbeadPlayerFlow] 🔗 当前URL: ${window.location.href}`);
const learningState = CbeadProgressManager.getLearningState();
learningState.reset();
console.log(`[CbeadPlayerFlow] 🔄 学习状态已重置`);
const timerManager = CbeadPlayer.createIntervalManager();
timerManager.registerUnloadHandler();
const cleanup = () => {
timerManager.unregisterUnloadHandler();
timerManager.clearAll();
};
let chapterName = course.title;
let currentChapterIndex = null;
try {
let chapterProgress = null;
for (let i = 0; i < 2; i++) {
chapterProgress = await CbeadPlayer.extractChapterProgress(false);
if (chapterProgress && chapterProgress.firstIncomplete) {
break;
}
if (i < 1) {
console.log(`[CbeadPlayerFlow] ⏳ 第${i + 1}次提取未完成,等待2秒后重试...`);
await new Promise(resolve => setTimeout(resolve, 2e3));
}
}
if (chapterProgress && chapterProgress.firstIncomplete) {
chapterName = chapterProgress.firstIncomplete.title;
currentChapterIndex = chapterProgress.firstIncomplete.index;
console.log(`[CbeadPlayerFlow] 📖 章节名称: ${chapterName}`);
console.log(`[CbeadPlayerFlow] 📌 章节索引: ${currentChapterIndex}`);
if (currentChapterIndex > 1) {
console.log(`[CbeadPlayerFlow] ⏭️ 已跳过 ${currentChapterIndex - 1} 个已完成章节`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `⏭️ 已跳过 ${currentChapterIndex - 1} 个已完成章节,开始学习: ${chapterName}`,
type: "info"
});
}
} else {
console.warn(`[CbeadPlayerFlow] ⚠️ 未能提取到章节信息,使用课程标题`);
}
} catch (error) {
console.warn(`[CbeadPlayerFlow] 无法提取章节信息,使用课程名:`, error);
}
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `📖 开始学习: ${chapterName}`,
type: "info"
});
console.log(`[CbeadPlayerFlow] 🔍 立即检查章节状态...`);
const initialChapterProgress = await CbeadPlayer.extractChapterProgress(false);
if (initialChapterProgress && currentChapterIndex !== null) {
const targetChapter = initialChapterProgress.chapters.find(ch => ch.index === currentChapterIndex);
if (targetChapter && targetChapter.status === "completed") {
console.log(`[CbeadPlayerFlow] ✅ 章节 ${currentChapterIndex} 已完成,跳过播放`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `⏭️ 章节已完成,跳过: ${targetChapter.title}`,
type: "info"
});
return {
success: true,
method: "skip_completed",
duration: 0,
watched: 0,
chapterName: targetChapter.title,
chapterCompleted: true,
earlyTermination: true,
reason: "章节已是已完成状态",
savedPercent: 100
};
}
}
try {
console.log(`[CbeadPlayerFlow] ⏳ 步骤 1/7: 等待播放器初始化...`);
let player = await CbeadPlayer.waitForPlayerReady();
if (!player) {
console.error(`[CbeadPlayerFlow] ❌ 播放器初始化失败`);
throw new Error("播放器初始化超时,无法获取视频信息");
}
console.log(`[CbeadPlayerFlow] ✅ 步骤 1/7: 播放器初始化完成`);
console.log(`[CbeadPlayerFlow] 📹 播放器类型: ${player.type}`);
if (initialChapterProgress && currentChapterIndex !== null) {
const targetChapter = initialChapterProgress.chapters.find(ch => ch.index === currentChapterIndex);
if (targetChapter) {
console.log(`[CbeadPlayerFlow] 📖 目标章节: ${targetChapter.title}, 状态: ${targetChapter.status}`);
const targetServerProgress = targetChapter.progress || 0;
if (targetServerProgress > 80 || targetServerProgress < 5) {
console.log(`[CbeadPlayerFlow] 🔄 服务器进度 ${targetServerProgress}%,尝试切换到目标章节: ${targetChapter.title}`);
const clicked = CbeadPlayer.clickChapter(targetChapter.title);
if (clicked) {
console.log(`[CbeadPlayerFlow] ✅ 已点击切换到章节: ${targetChapter.title}`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `🔄 切换到章节: ${targetChapter.title}`,
type: "info"
});
await new Promise(resolve => setTimeout(resolve, CBEAD_CONSTANTS.TIMING.CHAPTER_SWITCH_DELAY));
const newPlayer = CbeadPlayer.detectVideoPlayer();
if (newPlayer) {
player = newPlayer;
console.log(`[CbeadPlayerFlow] 📹 切换后播放器已重新检测`);
} else {
console.warn(`[CbeadPlayerFlow] ⚠️ 切换章节后播放器未就绪,等待重新初始化...`);
const reloadedPlayer = await CbeadPlayer.waitForPlayerReady();
if (reloadedPlayer) player = reloadedPlayer;
}
} else {
console.warn(`[CbeadPlayerFlow] ⚠️ 章节切换失败,可能已在正确章节`);
}
} else {
console.log(`[CbeadPlayerFlow] ✅ 服务器进度 ${targetServerProgress}%,似乎在正确的章节`);
}
}
}
console.log(`[CbeadPlayerFlow] ⏳ 步骤 2/7: 获取视频信息...`);
const duration = player.getDuration();
const currentTime = player.getCurrentTime();
const durationMinutes = Math.round(duration / 60);
const serverProgress = await CbeadPlayer.getServerProgress(currentChapterIndex);
if (serverProgress !== null) {
EventBus.publish(CONSTANTS.EVENTS.PROGRESS_UPDATE, serverProgress);
}
console.log(`[CbeadPlayerFlow] ✅ 步骤 2/7: 视频信息获取完成`);
console.log(`[CbeadPlayerFlow] ⏱️ 总时长: ${Math.round(duration)}秒 (${durationMinutes}分钟)`);
console.log(`[CbeadPlayerFlow] ⏯️ 当前位置: ${Math.round(currentTime)}秒 (服务器进度: ${serverProgress !== null ? serverProgress + "%" : "N/A"})`);
console.log(`[CbeadPlayerFlow] ⏳ 步骤 3/7: 设置静音...`);
player.setMuted(true);
console.log(`[CbeadPlayerFlow] 🔇 已调用 setMuted(true)`);
const mutedAfter = player.getMuted();
console.log(`[CbeadPlayerFlow] 🔍 静音状态验证: ${mutedAfter ? "成功" : "失败"}`);
console.log(`[CbeadPlayerFlow] ✅ 步骤 3/7: 静音设置完成`);
console.log(`[CbeadPlayerFlow] ⏳ 步骤 4/7: 设置播放位置...`);
if (serverProgress > 0) {
const targetTime = serverProgress / 100 * duration;
console.log(`[CbeadPlayerFlow] 📍 策略: 恢复进度 (服务器 ${serverProgress}% → ${Math.round(targetTime)}s / ${Math.round(duration)}s)`);
player.setCurrentTime(targetTime);
} else if (currentTime < CBEAD_CONSTANTS.THRESHOLDS.VIDEO_START_THRESHOLD) {
console.log(`[CbeadPlayerFlow] 📍 策略: 从头播放`);
player.setCurrentTime(0);
} else {
console.log(`[CbeadPlayerFlow] 📍 策略: 断点续播 (当前位置: ${Math.round(currentTime)}s)`);
}
console.log(`[CbeadPlayerFlow] ✅ 步骤 4/7: 播放位置设置完成`);
console.log(`[CbeadPlayerFlow] ⏳ 步骤 5/7: 启动播放器...`);
const currentPlayer = CbeadPlayer.detectVideoPlayer();
if (!currentPlayer) {
console.error(`[CbeadPlayerFlow] ❌ 播放器检测失败,可能已被销毁`);
throw new Error("播放器在启动前被销毁");
}
const {clickMaskButton: clickMaskButton} = await Promise.resolve().then(function() {
return domHelper;
});
clickMaskButton();
const bigPlayButton = document.querySelector(".vjs-big-play-button");
if (bigPlayButton) {
console.log(`[CbeadPlayerFlow] 🎯 发现大播放按钮遮罩,准备点击...`);
bigPlayButton.click();
console.log(`[CbeadPlayerFlow] ✅ 已点击大播放按钮`);
await new Promise(resolve => setTimeout(resolve, 800));
}
if (currentPlayer.element.paused) {
const playControlBtn = document.querySelector(".vjs-play-control");
if (playControlBtn) {
console.log(`[CbeadPlayerFlow] 🎯 发现播放控制按钮,准备点击...`);
playControlBtn.click();
await new Promise(resolve => setTimeout(resolve, 500));
console.log(`[CbeadPlayerFlow] ✅ 已点击播放控制按钮`);
}
}
if (!currentPlayer.element.paused) {
console.log(`[CbeadPlayerFlow] ✅ 播放器成功启动`);
} else {
console.error(`[CbeadPlayerFlow] ❌ 播放器启动失败`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "⚠️ 自动播放失败,请手动点击播放按钮",
type: "warn"
});
}
console.log(`[CbeadPlayerFlow] ✅ 步骤 5/7: 播放器启动完成`);
console.log(`[CbeadPlayerFlow] ⏳ 步骤 6/7: 设置播放监听器...`);
let notificationPermission = "default";
if ("Notification" in window) {
try {
notificationPermission = await Notification.requestPermission();
} catch (err) {
console.warn(`[CbeadPlayerFlow] ⚠️ 请求通知权限失败:`, err);
}
}
let wakeLock = null;
if ("wakeLock" in navigator) {
try {
wakeLock = await navigator.wakeLock.request("screen");
console.log(`[CbeadPlayerFlow] 📱 已启用屏幕常亮锁`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "📱 已启用屏幕常亮,防止屏幕关闭",
type: "info"
});
} catch (err) {
console.warn(`[CbeadPlayerFlow] ⚠️ 无法启用屏幕常亮锁:`, err);
}
}
const handleVisibilityChange = () => {
if (document.hidden) {
console.warn(`[CbeadPlayerFlow] ⚠️ 页面已隐藏到后台!`);
if (CBEAD_CONSTANTS.HEARTBEAT.ENABLED) {
console.log(`[CbeadPlayerFlow] 📱 页面进入后台,启动 Worker 心跳`);
heartbeatWorker = CbeadHeartbeatWorker.start({
courseId: course.id || course.courseId,
chapterId: currentChapterIndex,
interval: CBEAD_CONSTANTS.HEARTBEAT.INTERVAL
});
}
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "⚠️ 页面已隐藏到后台,已启动Worker心跳保活",
type: "warn"
});
if ("Notification" in window && notificationPermission === "granted") {
try {
new Notification("学习提醒", {
body: "页面已隐藏,请返回前台继续学习",
icon: "📺",
requireInteraction: true
});
} catch (err) {
console.warn(`[CbeadPlayerFlow] 发送通知失败:`, err);
}
}
} else {
console.log(`[CbeadPlayerFlow] ✅ 页面已返回前台`);
if (heartbeatWorker) {
console.log(`[CbeadPlayerFlow] 📱 页面回到前台,停止 Worker 心跳`);
CbeadHeartbeatWorker.stop(heartbeatWorker);
heartbeatWorker = null;
}
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "✅ 页面已返回前台,继续学习",
type: "info"
});
}
};
document.removeEventListener("visibilitychange", handleVisibilityChange);
document.addEventListener("visibilitychange", handleVisibilityChange);
console.log(`[CbeadPlayerFlow] ✅ 步骤 7/7: 监听器设置完成`);
console.log(`[CbeadPlayerFlow] ========== 开始播放,等待完成 ==========`);
let heartbeatWorker = null;
return new Promise((resolve, reject) => {
let chapterCompletedDetected = false;
currentPlayer.element.addEventListener("ended", async () => {
console.log(`[CbeadPlayerFlow] ========== 播放完成 ==========`);
console.log(`[CbeadPlayerFlow] ✅ 视频播放完成!`);
if (heartbeatWorker) {
CbeadHeartbeatWorker.stop(heartbeatWorker);
heartbeatWorker = null;
}
if (wakeLock) {
wakeLock.release();
wakeLock = null;
}
document.removeEventListener("visibilitychange", handleVisibilityChange);
cleanup();
console.log(`[CbeadPlayerFlow] ⏳ 等待页面更新完成状态...`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "⏳ 等待服务端记录学习进度...",
type: "info"
});
let serverConfirmed = false;
const POLL_INTERVAL = 5e3;
const MAX_WAIT = 12e4;
const startTime = Date.now();
await new Promise(resolveWait => {
const pollTimer = setInterval(async () => {
if (Date.now() - startTime > MAX_WAIT) {
clearInterval(pollTimer);
console.warn(`[CbeadPlayerFlow] ⚠️ 等待服务端确认超时 (${MAX_WAIT}ms)`);
resolveWait();
return;
}
const chapterProgress = await CbeadPlayer.extractChapterProgress(false);
if (chapterProgress && chapterProgress.allCompleted) {
console.log(`[CbeadPlayerFlow] ✅ 所有章节已完成 (${chapterProgress.completed}/${chapterProgress.total})`);
serverConfirmed = true;
chapterCompletedDetected = true;
clearInterval(pollTimer);
resolveWait();
} else if (chapterProgress && currentChapterIndex !== null) {
const currentChapter = chapterProgress.chapters.find(ch => ch.index === currentChapterIndex);
if (currentChapter && currentChapter.status === "completed") {
console.log(`[CbeadPlayerFlow] ✅ 当前章节已完成 (${currentChapter.title})`);
serverConfirmed = true;
chapterCompletedDetected = true;
clearInterval(pollTimer);
resolveWait();
}
}
}, POLL_INTERVAL);
});
if (serverConfirmed) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "✅ 课程学习完成!",
type: "success"
});
} else {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "⚠️ 视频已播完,但服务端进度尚未更新",
type: "warn"
});
}
const finalWatched = Math.round(duration) - Math.round(currentTime);
resolve({
success: true,
method: "real_playback",
duration: Math.round(duration),
watched: finalWatched,
chapterName: chapterName,
chapterCompleted: chapterCompletedDetected,
earlyTermination: chapterCompletedDetected,
reason: serverConfirmed ? "服务端确认完成" : "视频播放完成(服务端未确认)"
});
}, {
once: true
});
currentPlayer.element.addEventListener("error", error => {
console.error(`[CbeadPlayerFlow] 播放错误:`, error);
if (heartbeatWorker) {
CbeadHeartbeatWorker.stop(heartbeatWorker);
heartbeatWorker = null;
}
if (wakeLock) wakeLock.release();
document.removeEventListener("visibilitychange", handleVisibilityChange);
cleanup();
reject(new Error("视频播放出错"));
}, {
once: true
});
timerManager.setInterval(async () => {
if (learningState.isFailed()) {
console.warn(`[CbeadPlayerFlow] 🚨 检测到课程失败,立即停止播放`);
if (heartbeatWorker) {
CbeadHeartbeatWorker.stop(heartbeatWorker);
heartbeatWorker = null;
}
if (wakeLock) wakeLock.release();
currentPlayer.pause();
cleanup();
reject(new Error(`进度上报失败: ${learningState.getFailureReason()}`));
return;
}
const currentTime = currentPlayer.getCurrentTime();
const serverProgress = await CbeadPlayer.getServerProgress(currentChapterIndex);
console.log(`[CbeadPlayerFlow] 📊 已播放: ${Math.round(currentTime)}秒 / ${Math.round(duration)}秒 (服务器进度: ${serverProgress !== null ? serverProgress + "%" : "N/A"})`);
if (serverProgress !== null) {
EventBus.publish(CONSTANTS.EVENTS.PROGRESS_UPDATE, serverProgress);
}
if (currentPlayer.element.paused) {
console.warn(`[CbeadPlayerFlow] ⚠️ 检测到播放暂停,尝试恢复播放...`);
const playPromise = currentPlayer.play();
if (playPromise !== undefined) {
playPromise.then(() => {
console.log(`[CbeadPlayerFlow] ✅ 播放已恢复`);
}).catch(error => {
console.warn(`[CbeadPlayerFlow] ⚠️ 恢复播放失败: ${error.message}`);
if (document.hidden && "Notification" in window && notificationPermission === "granted") {
try {
new Notification("学习暂停提醒", {
body: "⚠️ 视频已暂停!请返回前台继续学习。",
icon: "⚠️",
requireInteraction: true
});
} catch (err) {
console.warn(`[CbeadPlayerFlow] 发送通知失败:`, err);
}
}
});
}
}
}, CBEAD_CONSTANTS.TIMING.PROGRESS_REPORT_INTERVAL);
timerManager.setInterval(async () => {
if (learningState.isFailed()) {
console.warn(`[CbeadPlayerFlow] 🚨 检测到课程失败,立即停止播放`);
if (wakeLock) wakeLock.release();
currentPlayer.pause();
cleanup();
reject(new Error(`进度上报失败: ${learningState.getFailureReason()}`));
return;
}
const serverProgress = await CbeadPlayer.getServerProgress(currentChapterIndex);
if (serverProgress !== null && serverProgress < CBEAD_CONSTANTS.THRESHOLDS.CHAPTER_CHECK_MIN_PROGRESS) {
return;
}
console.log(`[CbeadPlayerFlow] 🔍 检查章节状态 (服务器进度: ${serverProgress}%)...`);
const chapterProgress = await CbeadPlayer.extractChapterProgress(false);
if (chapterProgress && chapterProgress.completed === chapterProgress.total) {
console.log(`[CbeadPlayerFlow] 🎉 检测到所有章节已完成!`);
chapterCompletedDetected = true;
if (heartbeatWorker) {
CbeadHeartbeatWorker.stop(heartbeatWorker);
heartbeatWorker = null;
}
if (wakeLock) wakeLock.release();
currentPlayer.pause();
currentPlayer.setCurrentTime(duration);
cleanup();
resolve({
success: true,
method: "real_playback",
duration: Math.round(duration),
watched: Math.round(currentTime),
chapterName: chapterName,
chapterCompleted: true,
earlyTermination: true,
reason: "所有章节已完成",
savedPercent: 100 - (serverProgress || 0)
});
}
if (chapterProgress && currentChapterIndex !== null) {
const currentChapter = chapterProgress.chapters.find(ch => ch.index === currentChapterIndex);
if (currentChapter && currentChapter.status === "completed") {
console.log(`[CbeadPlayerFlow] 🎉 检测到章节状态变化: 章节 ${currentChapterIndex} 已完成!`);
chapterCompletedDetected = true;
if (heartbeatWorker) {
CbeadHeartbeatWorker.stop(heartbeatWorker);
heartbeatWorker = null;
}
if (wakeLock) wakeLock.release();
currentPlayer.pause();
currentPlayer.setCurrentTime(duration);
cleanup();
resolve({
success: true,
method: "real_playback",
duration: Math.round(duration),
watched: Math.round(currentTime),
chapterName: chapterName,
chapterCompleted: true,
earlyTermination: true,
reason: "章节状态变更为已完成",
savedPercent: 100 - (serverProgress || 0)
});
}
}
}, CBEAD_CONSTANTS.TIMING.CHAPTER_CHECK_INTERVAL);
});
} catch (error) {
cleanup();
console.error(`[CbeadPlayerFlow] 学习失败: ${course.title}`, error);
throw error;
}
}
};
const CbeadHandler = {
PAGE_TYPES: CBEAD_CONSTANTS.PAGE_TYPES,
SELECTORS: {
COURSE_ITEMS: [ ".activity-stage .list li", ".list-item", ".activity-list .list-item" ],
ENTER_BTN: ".study-btn",
PLAYER_CONTAINER: ".player-content",
VIDEO_ELEMENT: "video",
CHAPTER_CONTAINER: ".course-side-catalog",
BRANCH_LIST: {
CONTAINER: ".activity-main-area .vertical",
ITEM: ".list-item",
PAGINATION: ".e-pagination-box .zxy-pagination",
NEXT_BTN: ".zxy-pagination-item-next",
TAG_CONTAINER: ".label .tag-list",
TAG_BTN: ".label-btn"
}
},
identifyPage: createPageDetector({
pathPatterns: CBEAD_CONSTANTS.PATH_PATTERNS,
pageTypes: CBEAD_CONSTANTS.PAGE_TYPES,
domSelectors: [ ".player-content", ".new-global-height", "video" ],
domMatchType: "PLAYER"
}),
scanCoursesFromColumnPage() {
return CbeadScanner.scanCoursesFromColumnPage();
},
getSortedLearningList(courses) {
return CbeadScanner.getSortedLearningList(courses);
},
extractPlayerParams() {
return CbeadProgressManager.extractPlayerParams();
},
async isCourseReallyCompleted() {
return await CbeadPlayer.isCourseReallyCompleted();
},
extractPageCourseTitle() {
const selectors = [ ".course-title", ".video-course-title", ".detail-title", ".study-detail-title", "h1.title", ".header-title", ".course-name", '[class*="title"]' ];
for (const selector of selectors) {
const el = document.querySelector(selector);
if (el && el.textContent?.trim()) {
const text = el.textContent.trim();
if (text.length > 5 && text.length < 200 && !text.includes("中国干部网络学院")) {
return text;
}
}
}
const pageText = document.body?.textContent || "";
const courseNameMatch = pageText.match(/《([^》]+)》/);
if (courseNameMatch && courseNameMatch[1]) {
return courseNameMatch[1];
}
return null;
},
async learnWithRealPlayback(course) {
return await CbeadPlayerFlow.learnWithRealPlayback(course);
},
returnToList(returnUrl) {
return CbeadProgressManager.returnToList(returnUrl);
},
isCbeadMode() {
return CONFIG.CBEAD_MODE === true;
},
init() {
if (!this.isCbeadMode()) return;
console.log(`[CbeadHandler] 检测到页面类型: ${this.identifyPage()}`);
}
};
const gwypxPages = CONSTANTS.PAGE_CONFIG.GWYPX;
const GWYPX_CONSTANTS = {
PATH_PATTERNS: Object.fromEntries(Object.entries(gwypxPages).map(([k, v]) => [ k, v.path ])),
PAGE_TYPES: {
...Object.fromEntries(Object.entries(gwypxPages).map(([k, v]) => [ k, v.type ])),
UNKNOWN: "unknown"
},
PAGE_TYPE_WHITELIST: Object.keys(gwypxPages).filter(k => gwypxPages[k].whitelist)
};
const GwypxHandler = {
PAGE_TYPES: GWYPX_CONSTANTS.PAGE_TYPES,
identifyPage: createPageDetector({
pathPatterns: GWYPX_CONSTANTS.PATH_PATTERNS,
pageTypes: GWYPX_CONSTANTS.PAGE_TYPES,
domSelectors: [ "video.vjs-tech", ".prism-player", ".aliplayer-container" ],
domMatchType: "PLAYER"
}),
init() {
if (!CONFIG.GWYPX_MODE) return;
console.log("[GwypxHandler] 初始化党校分院处理器");
}
};
const DEFAULT_MESSAGES = {
default: "⚠️ 当前页面不支持自动学习。请进入课程播放页或列表页。"
};
function createPageValidator(options) {
const {whitelist: whitelist, pageTypes: pageTypes, customMessages: customMessages = {}} = options;
function validate(pageType) {
const allowedTypes = whitelist.map(key => pageTypes[key]);
if (allowedTypes.includes(pageType)) {
return true;
}
const message = customMessages[pageType] || customMessages.default || DEFAULT_MESSAGES.default;
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: message,
type: "warn"
});
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, "页面不支持");
return false;
}
return {
validate: validate
};
}
const SessionHelper = {
get(key, def = null) {
try {
return sessionStorage.getItem(key) ?? def;
} catch {
return def;
}
},
set(key, value) {
try {
sessionStorage.setItem(key, value);
} catch {}
},
remove(key) {
try {
sessionStorage.removeItem(key);
} catch {}
}
};
const UI_CSS_CONTENT = '#api-learner-panel { all: initial !important; position: fixed !important; bottom: 20px !important; right: 20px !important; left: auto !important; top: auto !important; width: 400px !important; height: auto !important; min-height: 200px !important; margin: 0 !important; padding: 0 !important; transform: none !important; zoom: 1 !important; background: #ffffff !important; border: 1px solid #dddddd !important; border-radius: 8px !important; box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; z-index: 2147483647 !important; font-family: -apple-system, "SF Pro Text", "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif !important; font-size: 14px !important; color: #333333 !important; line-height: 1.6 !important; text-align: left !important; box-sizing: border-box !important; display: flex !important; flex-direction: column !important; overflow: hidden !important; } #api-learner-panel * { all: unset !important; box-sizing: border-box !important; font-family: inherit !important; background: transparent !important; margin: 0 !important; padding: 0 !important; border: none !important; } #api-learner-panel #learner-progress-inner .progress-shine { position: absolute !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; background: linear-gradient( 90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100% ) !important; animation: progress-shine 0.8s linear infinite !important; pointer-events: none !important; display: none !important; } #api-learner-panel *:before, #api-learner-panel *:after { content: none !important; display: none !important; } #api-learner-panel .header { display: block !important; background: #f7f7f7 !important; padding: 10px 15px !important; font-weight: 600 !important; border-bottom: 1px solid #dddddd !important; width: 100% !important; letter-spacing: 0.3px !important; } #api-learner-panel .content { display: block !important; padding: 15px !important; width: 100% !important; background: #ffffff !important; flex-grow: 1 !important; } #api-learner-panel .status-row { display: flex !important; align-items: center !important; gap: 8px !important; margin-bottom: 12px !important; font-weight: 600 !important; font-size: 15px !important; } #api-learner-panel #learner-status { flex: 1 !important; font-weight: 500 !important; letter-spacing: 0.2px !important; display: flex !important; align-items: center !important; gap: 4px !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } #api-learner-panel #learner-status .stat-num { font-family: "SF Mono", "Monaco", "Menlo", "Consolas", monospace !important; color: #666666 !important; } #api-learner-panel #learner-status .course-title { font-weight: 400 !important; color: #333333 !important; max-width: 180px !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } #api-learner-panel .status-indicator { width: 12px !important; height: 12px !important; border-radius: 50% !important; background: #9ca3af !important; position: relative !important; flex-shrink: 0 !important; } #api-learner-panel .status-indicator[data-state="idle"] { background: #9ca3af !important; } #api-learner-panel .status-indicator[data-state="running"] { background: #22c55e !important; animation: status-pulse 1.5s ease-in-out infinite !important; } #api-learner-panel .status-indicator[data-state="completed"] { background: transparent !important; width: 16px !important; height: 16px !important; border: 2px solid #22c55e !important; transform: rotate(45deg) !important; } #api-learner-panel .status-indicator[data-state="completed"]::before { content: \'\' !important; position: absolute !important; top: 8px !important; left: 4px !important; width: 4px !important; height: 6px !important; background: #22c55e !important; transform: rotate(-45deg) !important; } #api-learner-panel .status-indicator[data-state="completed"]::after { content: \'\' !important; position: absolute !important; top: 10px !important; left: 0 !important; width: 6px !important; height: 4px !important; background: #22c55e !important; transform: rotate(-45deg) !important; } #api-learner-panel .status-indicator[data-state="error"] { background: #ef4444 !important; animation: status-shake 0.5s ease-in-out !important; } @keyframes status-pulse { 0%, 100% { opacity: 1 !important; box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.6) !important; transform: scale(1) !important; } 50% { opacity: 0.3 !important; box-shadow: 0 0 0 8px rgba(34, 197, 94, 0) !important; transform: scale(0.7) !important; } } @keyframes status-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-3px); } 40% { transform: translateX(3px); } 60% { transform: translateX(-2px); } 80% { transform: translateX(2px); } } #api-learner-panel .status { display: block !important; margin-bottom: 10px !important; font-weight: 600 !important; font-size: 15px !important; } #api-learner-panel .warning-box { display: block !important; margin-bottom: 10px !important; padding: 8px 12px !important; background: #fff3cd !important; border: 1px solid #ffc107 !important; border-radius: 4px !important; color: #856404 !important; font-size: 13px !important; line-height: 1.4 !important; } #api-learner-panel .statistics { display: flex !important; justify-content: space-between !important; margin-bottom: 10px !important; padding: 8px !important; background: #f9f9f9 !important; border-radius: 4px !important; font-size: 12px !important; width: 100% !important; } #api-learner-panel .stat-item { display: block !important; text-align: center !important; flex: 1 !important; font-weight: 500 !important; } #api-learner-panel .progress-bar { display: block !important; height: 8px !important; background: #f0f0f0 !important; border: none !important; border-radius: 4px !important; overflow: hidden !important; margin-bottom: 10px !important; width: 100% !important; } #api-learner-panel #learner-progress-inner { display: block !important; height: 100% !important; width: auto; background: #4caf50 !important; border-radius: 4px !important; transition: width 0.3s ease !important; } #api-learner-panel #learner-progress-inner[data-animate="true"] { background: repeating-linear-gradient( -45deg, #4caf50, #4caf50 8px, #66bb6a 8px, #66bb6a 16px ) !important; background-size: 22.63px 100% !important; background-attachment: fixed !important; animation: progress-stripe 0.5s linear infinite !important; } @keyframes progress-stripe { from { background-position: 0 0; } to { background-position: 22.63px 0; } } #api-learner-panel #learner-progress-inner[data-state="completed"] { background: #22c55e !important; transition: width 0.5s ease, background 0.3s ease !important; } #api-learner-panel #learner-progress-inner[data-state="idle"] { background: #9ca3af !important; width: 0% !important; } @keyframes progress-stripes { 0% { background-position: 0 0; } 100% { background-position: 22.63px 0; } } #api-learner-panel .log-container { display: block !important; height: 150px !important; overflow-y: auto !important; background: #fafafa !important; padding: 8px 10px !important; border: 1px solid #eeeeee !important; border-radius: 4px !important; font-size: 11px !important; line-height: 1.6 !important; font-family: "SF Mono", "Monaco", "Menlo", "Consolas", monospace !important; width: 100% !important; } #api-learner-panel .log-entry { display: flex !important; align-items: baseline !important; margin-bottom: 4px !important; padding: 2px 0 !important; word-break: break-all !important; } #api-learner-panel .log-dot { display: inline-block !important; width: 5px !important; height: 5px !important; border-radius: 50% !important; margin-right: 8px !important; flex-shrink: 0 !important; } #api-learner-panel .log-content { flex: 1 !important; display: flex !important; flex-wrap: wrap !important; align-items: baseline !important; gap: 4px !important; } #api-learner-panel .log-time { color: #999999 !important; font-size: 10px !important; } #api-learner-panel .log-tag { display: inline-block !important; padding: 0 4px !important; border-radius: 3px !important; font-size: 10px !important; font-weight: 500 !important; font-family: "SF Mono", "Monaco", monospace !important; } #api-learner-panel .log-entry.info .log-dot, #api-learner-panel .log-entry.info .log-tag { background: #2196f3 !important; color: #2196f3 !important; } #api-learner-panel .log-entry.success .log-dot, #api-learner-panel .log-entry.success .log-tag { background: #4caf50 !important; color: #4caf50 !important; } #api-learner-panel .log-entry.error .log-dot, #api-learner-panel .log-entry.error .log-tag { background: #f44336 !important; color: #f44336 !important; } #api-learner-panel .log-entry.warn .log-dot, #api-learner-panel .log-entry.warn .log-tag { background: #ff9800 !important; color: #ff9800 !important; } #api-learner-panel .log-entry.info .log-tag { background: #e3f2fd !important; } #api-learner-panel .log-entry.success .log-tag { background: #e8f5e9 !important; } #api-learner-panel .log-entry.error .log-tag { background: #ffebee !important; } #api-learner-panel .log-entry.warn .log-tag { background: #fff3e0 !important; } #api-learner-panel .log-container::-webkit-scrollbar { width: 5px !important; } #api-learner-panel .log-container::-webkit-scrollbar-track { background: transparent !important; } #api-learner-panel .log-container::-webkit-scrollbar-thumb { background: #dddddd !important; border-radius: 3px !important; } #api-learner-panel .footer { color: #60a5fa !important; } #api-learner-panel .log-entry.success .log-tag { background: rgba(34, 197, 94, 0.15) !important; color: #4ade80 !important; } #api-learner-panel .log-entry.error .log-tag { background: rgba(239, 68, 68, 0.15) !important; color: #f87171 !important; } #api-learner-panel .log-entry.warn .log-tag { background: rgba(249, 115, 22, 0.15) !important; color: #fb923c !important; } #api-learner-panel .log-container::-webkit-scrollbar { width: 6px !important; } #api-learner-panel .log-container::-webkit-scrollbar-track { background: transparent !important; } #api-learner-panel .log-container::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15) !important; border-radius: 3px !important; } #api-learner-panel .log-container::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.25) !important; } #api-learner-panel .footer { display: block !important; padding: 10px 15px !important; border-top: 1px solid #dddddd !important; text-align: center !important; width: 100% !important; background: #ffffff !important; } #api-learner-panel button { display: inline-block !important; padding: 8px 16px !important; border-radius: 4px !important; cursor: pointer !important; font-size: 13px !important; font-weight: 600 !important; line-height: 1.2 !important; background-color: #2196f3 !important; color: #ffffff !important; margin-left: 8px !important; transition: all 0.2s ease !important; border: none !important; } #api-learner-panel button#toggle-learning-btn[data-state="running"] { background-color: #f44336 !important; } #api-learner-panel button:hover { opacity: 0.9 !important; } #api-learner-panel button:active { transform: translateY(1px) !important; } #api-learner-panel .incompatible-banner { display: flex !important; align-items: center !important; padding: 12px 15px !important; background: #fff9e6 !important; border-bottom: 1px solid #ffe082 !important; } #api-learner-panel .warning-box .warning-icon { display: inline-block !important; width: 14px !important; height: 14px !important; background: #d97706 !important; border-radius: 50% !important; margin-right: 6px !important; vertical-align: middle !important; position: relative !important; } #api-learner-panel .warning-box .warning-icon::after { content: \'!\' !important; position: absolute !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; color: white !important; font-size: 10px !important; font-weight: bold !important; line-height: 1 !important; } #api-learner-panel .incompatible-banner .warning-icon { display: inline-block !important; width: 16px !important; height: 16px !important; background: #f57c00 !important; border-radius: 50% !important; margin-right: 8px !important; position: relative !important; flex-shrink: 0 !important; } #api-learner-panel .incompatible-banner .warning-icon::after { content: \'!\' !important; position: absolute !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; color: white !important; font-size: 11px !important; font-weight: bold !important; line-height: 1 !important; } #api-learner-panel .incompatible-banner .warning-content { flex: 1 !important; } #api-learner-panel .incompatible-banner .warning-title { font-size: 14px !important; font-weight: bold !important; color: #f57c00 !important; margin-bottom: 3px !important; } #api-learner-panel .incompatible-banner .warning-message { font-size: 12px !important; color: #f57c00 !important; line-height: 1.4 !important; opacity: 0.85 !important; } #api-learner-panel.incompatible-mode { border: 2px solid #ffe082 !important; box-shadow: 0 2px 8px rgba(245, 124, 0, 0.15) !important; } #api-learner-panel.incompatible-mode .header { background: #fffde7 !important; color: #f57c00 !important; }';
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 = `\n <div class="header">\n cela学习助手\n </div>\n <div class="content">\n <div class="status-row">\n <div class="status-indicator" id="status-indicator" data-state="idle"></div>\n <span id="learner-status">就绪</span>\n </div>\n ${CONFIG.WARNING_BATCH_LEARNING && CONFIG.GWYPX_MODE ? `\n <div class="warning-box">\n <span class="warning-icon"></span>\n 党校分院:慎用批量学习功能,可能被系统检测\n </div>\n ` : ""}\n <div class="statistics">\n <div class="stat-item">总计: <span id="stat-total">0</span></div>\n <div class="stat-item">已完成: <span id="stat-completed">0</span></div>\n <div class="stat-item">新学习: <span id="stat-learned">0</span></div>\n <div class="stat-item">失败: <span id="stat-failed">0</span></div>\n <div class="stat-item">跳过: <span id="stat-skipped">0</span></div>\n </div>\n <div class="progress-bar"><div id="learner-progress-inner" data-state="idle" style="width: 0% !important;"></div></div>\n\n <div class="log-container"></div>\n </div>\n <div class="footer">\n <button id="toggle-learning-btn" data-state="stopped">开始学习</button>\n </div>\n `;
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: type
});
if (this.logUpdateTimeout) clearTimeout(this.logUpdateTimeout);
this.logUpdateTimeout = setTimeout(() => this.flushLogBuffer(), CONSTANTS.UI_LIMITS.LOG_FLUSH_DELAY);
if (typeof CONFIG !== "undefined" && CONFIG.DEBUG_MODE) {
const debugMessage = `[API Learner Debug] ${logMessage}`;
console.log(debugMessage);
this.logs.push(debugMessage);
}
},
initEventListeners: function() {
EventBus.subscribe(CONSTANTS.EVENTS.LOG, ({message: message, type: type}) => this.log(message, type));
EventBus.subscribe(CONSTANTS.EVENTS.STATUS_UPDATE, status => this.updateStatus(status));
EventBus.subscribe(CONSTANTS.EVENTS.PROGRESS_UPDATE, progress => this.updateProgress(progress));
EventBus.subscribe(CONSTANTS.EVENTS.STATISTICS_UPDATE, stats => this.updateStatistics(stats));
EventBus.subscribe(CONSTANTS.EVENTS.LEARNING_START, () => {
const toggleBtn = document.getElementById(CONSTANTS.SELECTORS.TOGGLE_BTN.replace("#", ""));
if (toggleBtn) {
toggleBtn.setAttribute("data-state", "running");
toggleBtn.textContent = "停止学习";
}
const progressInner = document.getElementById("learner-progress-inner");
if (progressInner) {
progressInner.setAttribute("data-state", "running");
progressInner.setAttribute("data-animate", "true");
}
});
EventBus.subscribe(CONSTANTS.EVENTS.LEARNING_STOP, () => {
const toggleBtn = document.getElementById(CONSTANTS.SELECTORS.TOGGLE_BTN.replace("#", ""));
if (toggleBtn) {
toggleBtn.setAttribute("data-state", "stopped");
toggleBtn.textContent = "开始学习";
}
UI.updateProgress(0);
const progressInner = document.getElementById("learner-progress-inner");
if (progressInner) {
progressInner.setAttribute("data-state", "idle");
progressInner.removeAttribute("data-animate");
}
});
EventBus.subscribe(CONSTANTS.EVENTS.COURSE_COMPLETE, () => {
const progressInner = document.getElementById("learner-progress-inner");
if (progressInner) {
progressInner.setAttribute("data-state", "completed");
progressInner.removeAttribute("data-animate");
}
});
EventBus.subscribe(CONSTANTS.EVENTS.COURSE_START, ({course: course, index: index, total: total}) => {
this.log(`[START] 处理第 ${index}/${total} 门课程: ${course.title}`);
});
EventBus.subscribe(CONSTANTS.EVENTS.COURSE_COMPLETE, ({course: course}) => {
this.log(`[OK] 课程学习完成: ${course.title}`, "success");
});
EventBus.subscribe(CONSTANTS.EVENTS.COURSE_SKIP, ({course: course, reason: reason}) => {
this.log(`[SKIP] 课程已完成,跳过: ${course.title} (${reason})`, "success");
});
EventBus.subscribe(CONSTANTS.EVENTS.COURSE_ERROR, ({course: course, reason: reason}) => {
this.log(`[ERR] 课程处理失败: ${course.title} - ${reason}`, "error");
});
EventBus.subscribe(CONSTANTS.EVENTS.PROGRESS_REPORT, _data => {});
EventBus.subscribe(CONSTANTS.EVENTS.PROGRESS_SUCCESS, ({message: message, _progress: _progress}) => {
this.log(message, "success");
});
EventBus.subscribe(CONSTANTS.EVENTS.PROGRESS_ERROR, ({message: message}) => {
this.log(message, "warn");
});
},
flushLogBuffer: function() {
const logContainer = document.querySelector(CONSTANTS.SELECTORS.LOG_CONTAINER);
if (!logContainer || this.logBuffer.length === 0) return;
const tagLabels = {
START: "INIT",
STOP: "STOP",
SKIP: "SKIP",
ERR: "ERR",
WARN: "WARN",
OK: "OK",
PROG: "PROG",
GET: "GET",
BATCH: "BATCH",
SPEED: "FAST",
SYNC: "SYNC",
SIGNAL: "SIG"
};
const fragment = document.createDocumentFragment();
this.logBuffer.forEach(log => {
const logEntry = document.createElement("div");
logEntry.className = `log-entry ${log.type}`;
const dot = document.createElement("span");
dot.className = "log-dot";
const content = document.createElement("span");
content.className = "log-content";
const tagMatch = log.message.match(/^\[([A-Z]+)\]\s*(.*)/);
if (tagMatch) {
const tagType = tagMatch[1];
const tagLabel = tagLabels[tagType] || tagType;
const messageText = tagMatch[2];
const time = (new Date).toLocaleTimeString("zh-CN", {
hour12: false
});
const tagSpan = document.createElement("span");
tagSpan.className = "log-tag";
tagSpan.textContent = tagLabel;
const msgText = document.createTextNode(messageText);
content.appendChild(document.createTextNode(`[${time}] `));
content.appendChild(tagSpan);
content.appendChild(msgText);
} else {
content.textContent = log.message;
}
logEntry.appendChild(dot);
logEntry.appendChild(content);
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_LABEL.replace("#", ""));
const indicator = document.getElementById("status-indicator");
if (statusEl) {
let html = status || "";
html = html.replace(/【([^【】]+)】/g, '<span class="course-title">$1</span>');
html = html.replace(/(\d+\s*\/\s*\d+)/g, '<span class="stat-num">$1</span>');
statusEl.innerHTML = html;
}
if (indicator) {
const statusLower = (status || "").toLowerCase();
if (statusLower.includes("学习中") || statusLower.includes("运行")) {
indicator.setAttribute("data-state", "running");
} else if (statusLower.includes("完成") || statusLower.includes("成功")) {
indicator.setAttribute("data-state", "completed");
} else if (statusLower.includes("停止") || statusLower.includes("暂停") || statusLower.includes("错误")) {
indicator.setAttribute("data-state", "error");
} else {
indicator.setAttribute("data-state", "idle");
}
}
},
updateProgress: percentage => {
const progressInner = document.getElementById(CONSTANTS.SELECTORS.PROGRESS_INNER.replace("#", ""));
if (!progressInner) {
console.log("[UI] 进度条元素未找到");
return;
}
let percent;
if (typeof percentage === "number") {
percent = percentage;
} else if (typeof percentage === "object" && percentage !== null) {
percent = percentage.percent || percentage.percentage || 0;
} else {
percent = 0;
}
percent = Math.max(0, Math.min(100, percent));
progressInner.style.setProperty("width", `${percent}%`, "important");
const computedStyle = window.getComputedStyle(progressInner);
console.log(`[UI] 更新进度条: ${percent}%, 实际宽度: ${computedStyle.width}, 显示: ${computedStyle.display}`);
},
updateStatistics: stats => {
Object.assign(UI.statistics, stats);
const totalEl = document.getElementById(CONSTANTS.SELECTORS.STAT_TOTAL.replace("#", ""));
const completedEl = document.getElementById(CONSTANTS.SELECTORS.STAT_COMPLETED.replace("#", ""));
const learnedEl = document.getElementById(CONSTANTS.SELECTORS.STAT_LEARNED.replace("#", ""));
const failedEl = document.getElementById(CONSTANTS.SELECTORS.STAT_FAILED.replace("#", ""));
const skippedEl = document.getElementById(CONSTANTS.SELECTORS.STAT_SKIPPED.replace("#", ""));
if (totalEl) totalEl.textContent = UI.statistics.total;
if (completedEl) completedEl.textContent = UI.statistics.completed;
if (learnedEl) learnedEl.textContent = UI.statistics.learned;
if (failedEl) failedEl.textContent = UI.statistics.failed;
if (skippedEl) skippedEl.textContent = UI.statistics.skipped;
},
addStyles: () => {
{
const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.textContent = UI_CSS_CONTENT;
document.head.appendChild(styleSheet);
}
},
setIncompatible: reason => {
UI.updateStatus("当前页面暂不兼容");
UI.log(`[兼容性检查] ${reason}`, "warn");
const panel = document.getElementById("api-learner-panel");
if (!panel) return;
panel.classList.add("incompatible-mode");
if (panel.querySelector(".incompatible-banner")) return;
const warningBanner = document.createElement("div");
warningBanner.className = "incompatible-banner";
warningBanner.innerHTML = `\n <div class="warning-icon"></div>\n <div class="warning-content">\n <div class="warning-title">当前环境暂不支持</div>\n <div class="warning-message">${reason}</div>\n </div>\n `;
const header = panel.querySelector(".header");
if (header) {
panel.insertBefore(warningBanner, header);
}
},
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);
},
resetToggleButton(statusText = "学习完成") {
const toggleBtn = document.getElementById(CONSTANTS.SELECTORS.TOGGLE_BTN.replace("#", ""));
if (toggleBtn) {
toggleBtn.setAttribute("data-state", "stopped");
toggleBtn.textContent = "开始学习";
}
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, statusText);
}
};
let currentPlayerLearningId = null;
const CbeadLearner = {
get _pageValidator() {
return createPageValidator({
whitelist: CBEAD_CONSTANTS.PAGE_TYPE_WHITELIST,
pageTypes: CBEAD_CONSTANTS.PAGE_TYPES,
customMessages: {
home_v: "⚠️ 当前是展示主页页面,没有可学习的课程列表。请进入课程详情页或列表页。",
default: "⚠️ 当前页面不支持自动学习。请进入专题详情页或课程列表页。"
}
});
},
getCurrentPlayerLearningId() {
return currentPlayerLearningId;
},
setCurrentPlayerLearningId(id) {
const oldId = currentPlayerLearningId;
currentPlayerLearningId = id;
console.log(`[CbeadLearner] 📍 更新播放页学习任务ID: ${oldId || "none"} → ${id || "none"}`);
},
_validatePageType(pageType) {
return this._pageValidator.validate(pageType);
},
async selectAndExecute() {
if (!CONFIG.CBEAD_MODE) {
return null;
}
const pageType = CbeadHandler.identifyPage();
if (!this._validatePageType(pageType)) {
return false;
}
const href = window.location.href;
if (href.includes("study/course/detail")) {
return await this._handlePlayerPage();
}
if (href.includes("branch-list-v")) {
return await this._handleBranchListPage();
}
if (href.includes("/center/my/course")) {
return await this._handleColumnPage();
}
if (href.includes("train-new/class-detail")) {
return await this._handleColumnPage();
}
return null;
},
async _handlePlayerPage() {
return await this.startPlayerFlow();
},
async _handleColumnPage() {
const {findSignUpButton: findSignUpButton, getSignUpButtonText: getSignUpButtonText} = await Promise.resolve().then(function() {
return domHelper;
});
const signUpButton = findSignUpButton();
if (signUpButton) {
const buttonText = getSignUpButtonText(signUpButton);
const errorMsg = `🚫 该专栏/专题班未报名,无法开始自动学习。请先点击"${buttonText}"按钮完成报名。`;
console.error(`[CbeadLearner] ${errorMsg}`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: errorMsg,
type: "error"
});
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, "未报名");
throw new Error("专栏/专题班未报名");
}
const sourceUrl = this._getSourceUrl();
if (!sourceUrl) {
this._saveSourceUrl();
console.log(`[CbeadLearner] 📌 保存来源 URL: ${window.location.href}`);
}
await this.startBatchFlow();
return true;
},
async _handleBranchListPage() {
this._clearBranchListUrl();
const sourceUrl = this._getSourceUrl();
if (!sourceUrl) {
console.log(`[CbeadLearner] 💡 进入列表页,请点击"开始学习"按钮开始批量学习`);
return CONSTANTS.WAITING_FOR_USER;
}
console.log(`[CbeadLearner] 🔄 检测到批量学习流程,继续执行...`);
await this.startBranchListFlow();
return true;
},
async startPlayerFlow() {
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, "学习中");
const playerParams = CbeadHandler.extractPlayerParams();
const currentPageCourseId = playerParams?.uuid;
if (currentPageCourseId && currentPlayerLearningId === currentPageCourseId) {
console.log(`[CbeadLearner] ⏭️ 播放页学习任务已在进行中,跳过重复执行: ${currentPageCourseId.substring(0, 8)}...`);
return false;
}
if (currentPageCourseId) {
this.setCurrentPlayerLearningId(currentPageCourseId);
}
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "🎬 检测到企业分院播放页,直接处理当前视频",
type: "info"
});
const isReallyCompleted = await CbeadHandler.isCourseReallyCompleted();
if (isReallyCompleted) {
this.setCurrentPlayerLearningId(null);
return await this._handleAllChaptersCompleted();
}
return await this._learnCurrentVideo();
},
async _handleAllChaptersCompleted() {
console.log("[CbeadLearner] ✅ 检测到所有章节已完成,跳过播放");
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "✅ 该课程所有章节已完成!",
type: "success"
});
UI.resetToggleButton("学习完成");
return false;
},
async _learnCurrentVideo() {
const playerParams = CbeadHandler.extractPlayerParams();
if (!playerParams || !playerParams.uuid) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "❌ 无法从URL提取视频UUID",
type: "error"
});
return false;
}
const courseTitle = CbeadHandler.extractPageCourseTitle() || document.title;
const currentCourse = {
id: playerParams.uuid,
courseId: playerParams.uuid,
dsUnitId: playerParams.uuid,
title: courseTitle,
courseName: courseTitle,
source: "cbead_current_video"
};
LearningState.reset();
const success = await this._executeVideoLearning(currentCourse);
if (success) {
return await this._handleVideoCompleted(currentCourse);
} else {
await this._handleVideoFailed();
return false;
}
},
async _executeVideoLearning(course) {
return await CbeadHandler.learnWithRealPlayback(course);
},
async _handleVideoCompleted(course) {
const isReallyCompleted = await CbeadHandler.isCourseReallyCompleted();
if (!isReallyCompleted) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "✅ 当前章节学习完成,继续下一章节...",
type: "info"
});
await new Promise(resolve => setTimeout(resolve, 1e3));
const {Learner: Learner} = await Promise.resolve().then(function() {
return learner;
});
await Learner.startLearning();
return true;
}
console.log("[CbeadLearner] ✅ 课程学习完成");
this.setCurrentPlayerLearningId(null);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "✅ 当前视频学习完成!",
type: "success"
});
const returnUrl = this._getSourceUrl();
if (!returnUrl) {
console.warn("[CbeadLearner] ⚠️ 未找到来源 URL,无法返回继续学习");
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "⚠️ 学习完成,但无法返回来源页面",
type: "warn"
});
UI.resetToggleButton("学习完成");
return false;
}
console.log(`[CbeadLearner] 🔄 返回来源页面: ${returnUrl}`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `🔄 返回列表页扫描继续...`,
type: "info"
});
await new Promise(resolve => setTimeout(resolve, 2e3));
if (returnUrl && returnUrl.startsWith("#")) {
const baseUrl = window.location.href.split("#")[0];
window.location.href = baseUrl + returnUrl;
} else {
window.location.href = returnUrl;
}
UI.resetToggleButton("学习完成");
return false;
},
async _handleVideoFailed() {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "❌ 视频学习失败",
type: "error"
});
UI.resetToggleButton("学习失败");
},
async startBatchFlow() {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "📋 检测到专题班/专栏,开始批量学习流程",
type: "info"
});
const allCourses = await CbeadHandler.scanCoursesFromColumnPage();
if (allCourses.length === 0) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "❌ 未找到课程",
type: "error"
});
return false;
}
const learningList = CbeadHandler.getSortedLearningList(allCourses);
if (learningList.length === 0) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "✅ 所有课程已完成!",
type: "success"
});
return false;
}
const totalCourses = allCourses.length;
const skippedCount = totalCourses - learningList.length;
EventBus.publish(CONSTANTS.EVENTS.STATISTICS_UPDATE, {
total: totalCourses,
completed: skippedCount,
learned: 0,
failed: 0,
skipped: skippedCount
});
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `📚 准备学习 ${learningList.length} 门课程`,
type: "info"
});
const firstCourse = learningList[0];
await this._navigateToCourse(firstCourse);
return true;
},
async startBranchListFlow() {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "📋 检测到分支列表页,开始边扫描边学习",
type: "info"
});
const scanPage = this._loadScanPage();
let currentPage = scanPage;
console.log(`[CbeadLearner] 📄 当前扫描页码: ${currentPage}`);
const result = await this._scanAndLearnBranchList(currentPage);
if (result && result.completed) {
this._clearScanPage();
this._clearSourceUrl();
console.log(`[CbeadLearner] 🧹 清除来源 URL`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "✅ 所有课程已完成!",
type: "success"
});
UI.resetToggleButton("学习完成");
} else if (result && result.continue) {
this._saveScanPage(result.nextPage);
}
return true;
},
async _scanAndLearnBranchList(startPage) {
const pageCourses = await CbeadScanner.scanCoursesFromBranchListPage(startPage);
if (!pageCourses || pageCourses.length === 0) {
console.log("[CbeadLearner] 当前页没有找到课程,停止扫描");
return {
completed: true
};
}
const incompleteCourses = pageCourses.filter(c => c.progress < 100);
if (incompleteCourses.length > 0) {
this._saveSourceUrl();
console.log(`[CbeadLearner] 📌 保存来源 URL: ${window.location.href}`);
await this._navigateToCourse(incompleteCourses[0]);
return {
continue: true,
nextPage: startPage
};
}
return await this._scanAndLearnBranchList(startPage + 1);
},
_loadScanPage() {
const data = SessionHelper.get("cbeadScanPage");
return data ? parseInt(data, 10) : 1;
},
_saveScanPage(page) {
SessionHelper.set("cbeadScanPage", page.toString());
},
_clearScanPage() {
SessionHelper.remove("cbeadScanPage");
},
_clearBranchListUrl() {
SessionHelper.remove("cbeadBranchListUrl");
},
_saveSourceUrl() {
const sourceUrl = `#${window.location.href.split("#")[1] || ""}`;
SessionHelper.set("cbeadLearnSourceUrl", sourceUrl);
return sourceUrl;
},
_getSourceUrl() {
return SessionHelper.get("cbeadLearnSourceUrl");
},
_clearSourceUrl() {
SessionHelper.remove("cbeadLearnSourceUrl");
},
async selectCourse(options = {}) {
const {scanMethod: scanMethod} = options;
if (!scanMethod) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "❌ 未提供扫描方法",
type: "error"
});
return false;
}
const allCourses = await scanMethod();
if (allCourses.length === 0) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "❌ 未找到课程",
type: "error"
});
return false;
}
const learningList = CbeadHandler.getSortedLearningList(allCourses);
if (learningList.length === 0) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "✅ 所有课程已完成!",
type: "success"
});
return false;
}
const totalCourses = allCourses.length;
const skippedCount = totalCourses - learningList.length;
EventBus.publish(CONSTANTS.EVENTS.STATISTICS_UPDATE, {
total: totalCourses,
completed: skippedCount,
learned: 0,
failed: 0,
skipped: skippedCount
});
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `📚 准备学习 ${learningList.length} 门课程`,
type: "info"
});
const firstCourse = learningList[0];
await this._navigateToCourse(firstCourse);
return true;
},
async _navigateToCourse(course) {
const courseProgress = course.progress || 0;
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, `学习【${course.title}】 ${courseProgress}%`);
EventBus.publish(CONSTANTS.EVENTS.PROGRESS_UPDATE, courseProgress);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `\n📖 开始学习: ${course.title} (${courseProgress}%)`,
type: "info"
});
this._saveSourceUrl();
const savedUrl = this._getSourceUrl() || "";
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `📌 保存来源页: ${savedUrl}`,
type: "info"
});
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `🔄 正在跳转到播放页...`,
type: "info"
});
await new Promise(resolve => setTimeout(resolve, 1e3));
window.location.href = course.link;
}
};
var learner$1 = Object.freeze({
__proto__: null,
CbeadLearner: CbeadLearner,
default: CbeadLearner
});
const Validator = {
validateCourseId(courseId, methodName = "Method") {
if (!courseId || typeof courseId !== "string" || courseId.trim() === "") {
throw new Error(`${methodName}: 课程ID无效 (courseId: "${courseId}")`);
}
},
validateNumber(value, paramName, methodName = "Method", min = 0, max = Infinity) {
const num = Number(value);
if (isNaN(num) || num < min || num > max) {
throw new Error(`${methodName}: ${paramName}无效 (值: ${value}, 范围: ${min}-${max})`);
}
return num;
}
};
const GWYPX_API_CONFIG = {
baseUrl: null,
endpoints: {
APP_INIT: "/pcApi/api/portal/app/init",
COURSE_GET: "/pcApi/api/course/web/get",
STUDY_START: "/pcApi/api/study/start",
STUDY_END: "/pcApi/api/study/v2/end",
STUDY_PROGRESS: "/pcApi/api/study/progress",
TRAINEE_INFO: "/pcApi/api/getTrainee",
COURSE_QUERY_REL: "/pcApi/api/courseuser/web/queryRel",
COURSE_ADD_REL: "/pcApi/api/courseuser/web/addRel",
PERSONAL_COURSES: "/pcApi/api/personal/queryDataUnenrolled",
COURSE_BY_CATEGORY: "/pcApi/api/portal/course/getPageByCategory",
COURSE_RECOMMEND: "/pcApi/api/portal/course/recommend",
GET_TOPIC: "/pcApi/api/portal/course/getTopic"
},
getBaseUrl() {
const config = ServiceLocator.get(ServiceNames.CONFIG);
this.baseUrl = config?.GWYPX_API_BASE || `https://${window.location.hostname}`;
return this.baseUrl;
},
getUrl(endpoint) {
const baseUrl = this.getBaseUrl();
const endpointPath = this.endpoints[endpoint] || endpoint;
return baseUrl + endpointPath;
}
};
const GwypxApi = {
...ApiCore,
isSuccessResponse(result) {
return result && (result.success === true || result.code === 200 || result.code === 2e4 || result.state === 2e4 || result.status === "success" || result.status === "ok");
},
_getIdCardHash() {
let hash = "";
let source = "";
const cookieMatch = document.cookie.match(/idCardHash=([^;]+)/);
if (cookieMatch) {
hash = decodeURIComponent(cookieMatch[1]);
source = "cookie";
}
if (!hash) {
const deepFind = (obj, k) => {
if (!obj || typeof obj !== "object") return null;
if (obj[k]) return obj[k];
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const res = deepFind(obj[key], k);
if (res) return res;
}
}
return null;
};
const candidates = [ {
obj: window.userInfo,
name: "window.userInfo"
}, {
obj: window.traineeInfo,
name: "window.traineeInfo"
}, {
obj: window.subSiteConfig,
name: "window.subSiteConfig"
} ];
for (const c of candidates) {
if (c.obj) {
hash = c.obj.idCardHash || deepFind(c.obj, "idCardHash");
if (hash) {
source = c.name;
break;
}
}
}
}
if (!hash) {
try {
const app = document.querySelector("#app");
const vue = app?.__vue__ || app?.__vue_app__;
const store = vue?.$store || vue?.store;
if (store?.state?.user?.info?.idCardHash) {
hash = store.state.user.info.idCardHash;
source = "Vuex.state.user.info";
}
if (!hash) {
const found = deepFind(store?.state, "idCardHash");
if (found) {
hash = found;
source = "Vuex.deepFind";
}
}
} catch (e) {}
}
if (!hash) {
hash = localStorage.getItem("idCardHash");
if (hash) source = "localStorage";
}
if (!hash) {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const val = localStorage.getItem(key);
if (val && val.includes("idCardHash")) {
try {
const parsed = JSON.parse(val);
const findHash = obj => {
if (!obj || typeof obj !== "object") return null;
if (obj.idCardHash) return obj.idCardHash;
for (const k in obj) {
const res = findHash(obj[k]);
if (res) return res;
}
return null;
};
const res = findHash(parsed);
if (res) {
hash = res;
source = `localStorage.${key}`;
break;
}
} catch (e) {}
}
}
}
const finalHash = hash || sessionStorage.getItem("idCardHash") || "";
if (finalHash) {
console.log(`[GwypxApi] 找到 idCardHash: ${finalHash.substring(0, 8)}... 来自 ${source || "sessionStorage"}`);
}
return finalHash;
},
_prepareHeaders(customHeaders = {}, data = null) {
const headers = ApiCore._prepareHeaders.call(this, customHeaders, data);
delete headers["Authorization"];
delete headers["X-Auth-Token"];
if (data) {
headers["Content-Type"] = "application/json";
}
headers["Referer"] = window.location.href + (window.location.href.includes("?") ? "&" : "?") + "_cela_t=" + Date.now();
return headers;
},
async request(options) {
const originalLog = this._log;
this._log = (msg, level) => {
if (msg.includes("未找到认证token")) return;
originalLog.call(this, msg, level);
};
try {
return await ApiCore.request.call(this, options);
} finally {
this._log = originalLog;
}
},
async getPlayInfo(courseId) {
try {
Validator.validateCourseId(courseId, "getPlayInfo");
const url = GWYPX_API_CONFIG.getUrl("COURSE_GET");
const payload = {
courseId: courseId,
idCardHash: this._getIdCardHash()
};
console.log("[GwypxApi] 正在获取播放信息, payload:", payload);
const response = await this.post(url, JSON.stringify(payload), {
headers: {
"Content-Type": "application/json"
}
});
console.log("[GwypxApi] 播放信息响应:", response);
if (response && response.data) {
const studyTimes = response.data.studyTimes || 0;
const progress = parseInt(response.data.percentage || "0", 10);
return {
courseId: courseId,
title: response.data.name,
duration: response.data.courseDuration || response.data.duration || 0,
studyTimes: studyTimes,
progress: progress,
idCardHash: payload.idCardHash
};
}
if (response && response.name && response.courseId) {
return {
courseId: courseId,
title: response.name,
duration: response.courseDuration || response.duration || 0,
idCardHash: payload.idCardHash
};
}
console.warn("[GwypxApi] getPlayInfo: 响应格式无法识别", response);
return null;
} catch (error) {
if (error.message.includes("课程ID无效")) {
throw error;
}
console.error("[GwypxApi] getPlayInfo failed:", error);
return null;
}
},
async studyStart(courseId, tbtpId = null) {
Validator.validateCourseId(courseId, "studyStart");
const url = GWYPX_API_CONFIG.getUrl("STUDY_START");
const payload = {
courseId: courseId,
idCardHash: this._getIdCardHash(),
studyType: "VIDEO",
tbtpId: tbtpId
};
console.log(`[GwypxApi] 发送 studyStart: ${courseId}, tbtpId: ${tbtpId}`);
return await this.post(url, JSON.stringify(payload));
},
async _relAction(courseId, endpointKey, actionName) {
Validator.validateCourseId(courseId, actionName);
const payload = {
courseId: courseId,
idCardHash: this._getIdCardHash()
};
console.log(`[GwypxApi] ${actionName}: ${courseId}`);
return await this.post(GWYPX_API_CONFIG.getUrl(endpointKey), JSON.stringify(payload));
},
async queryRel(courseId) {
return await this._relAction(courseId, "COURSE_QUERY_REL", "查询课程关联状态");
},
async addRel(courseId) {
return await this._relAction(courseId, "COURSE_ADD_REL", "建立课程关联");
},
async reportProgress(playInfo, studyTimes = 60, totalStudyTimes = 60) {
const url = GWYPX_API_CONFIG.getUrl("STUDY_PROGRESS");
const payload = {
courseId: playInfo.courseId,
idCardHash: playInfo.idCardHash || this._getIdCardHash(),
studyTimes: studyTimes,
totalStudyTimes: totalStudyTimes,
tbtpId: playInfo.tbtpId || null
};
console.log(`[GwypxApi] 发送进度同步 (studyTimes: ${studyTimes}): ${playInfo.courseId}, tbtpId: ${payload.tbtpId}`);
return await this.post(url, JSON.stringify(payload));
},
async reportEnd(playInfo) {
const url = GWYPX_API_CONFIG.getUrl("STUDY_END");
const payload = {
courseId: playInfo.courseId,
idCardHash: playInfo.idCardHash || this._getIdCardHash(),
tbtpId: playInfo.tbtpId || null,
studyTimes: playInfo.studyTimes || 60
};
console.log(`[GwypxApi] 发送完成确认 (v2/end): ${playInfo.courseId}, studyTimes: ${payload.studyTimes}`);
return await this.post(url, JSON.stringify(payload));
},
async getPersonalCourses(pageNum = 0, pageSize = 15, filters = {}) {
pageNum = Validator.validateNumber(pageNum, "pageNum", "getPersonalCourses", 0);
pageSize = Validator.validateNumber(pageSize, "pageSize", "getPersonalCourses", 1, 100);
const url = GWYPX_API_CONFIG.getUrl("PERSONAL_COURSES");
const payload = {
pagenum: pageNum,
pageSize: pageSize,
isComplete: filters.isComplete || null,
isExcellent: filters.isExcellent || null,
isFinished: filters.isFinished || null,
year: filters.year || null,
idCardHash: this._getIdCardHash(),
completeSf: filters.completeSf || 0
};
console.log(`[GwypxApi] 获取个人中心课程列表: 第${pageNum + 1}页, 每页${pageSize}`);
return await this.post(url, JSON.stringify(payload));
},
async getCoursesByCategory(pageNum = 0, pageSize = 15, categoryId = null) {
pageNum = Validator.validateNumber(pageNum, "pageNum", "getCoursesByCategory", 0);
pageSize = Validator.validateNumber(pageSize, "pageSize", "getCoursesByCategory", 1, 100);
const url = GWYPX_API_CONFIG.getUrl("COURSE_BY_CATEGORY");
const payload = {
pagenum: pageNum,
pagesize: pageSize,
name: "",
idCardHash: this._getIdCardHash()
};
if (categoryId) {
payload.categoryId = categoryId;
}
console.log(`[GwypxApi] 获取分类课程: categoryId=${categoryId || "默认"}, 第${pageNum + 1}页, 每页${pageSize}`);
return await this.post(url, JSON.stringify(payload));
},
async getRecommendCourses(pageNum = 0, pageSize = 15, recommendation = "new") {
const url = GWYPX_API_CONFIG.getUrl("COURSE_RECOMMEND");
const payload = {
pagenum: pageNum,
pagesize: pageSize,
recommendation: recommendation,
name: "",
idCardHash: this._getIdCardHash()
};
return await this.post(url, JSON.stringify(payload));
},
async getTopic(topicId) {
const url = GWYPX_API_CONFIG.getUrl("GET_TOPIC");
const payload = {
id: topicId,
idCardHash: this._getIdCardHash()
};
return await this.post(url, JSON.stringify(payload));
},
async getSubjectColumnCourses(code, pageNum = 0, pageSize = 1e6) {
const url = GWYPX_API_CONFIG.getUrl("COURSE_BY_CATEGORY");
const payload = {
code: code,
pagesize: pageSize,
pagenum: pageNum,
idCardHash: this._getIdCardHash()
};
console.log(`[GwypxApi] 获取专栏课程: code=${code}`);
return await this.post(url, JSON.stringify(payload));
}
};
const LEARNER_CONSTANTS = {
PROGRESS_INCREMENT: 60,
INITIAL_TIME_OFFSET: 60,
MAX_SYNC_LOOPS: 60,
SUPER_FAST_INTERVAL: 5e3,
NORMAL_INTERVAL: 1e4,
BATCH_PAGE_SIZE: 20,
BATCH_REQUEST_DELAY: 1e3,
MASK_WAIT_TIME: 2e3,
API_REL_DELAY: 1e3
};
class GlobalStateManager {
constructor() {
this.STORAGE_KEY = "__CELA_GWYPX_STATE__";
}
getState() {
if (!window[this.STORAGE_KEY]) {
window[this.STORAGE_KEY] = {
isRunning: false,
stopRequested: false,
startTime: null,
loopCount: 0
};
}
return window[this.STORAGE_KEY];
}
resetState() {
if (window[this.STORAGE_KEY]) {
window[this.STORAGE_KEY] = {
isRunning: false,
stopRequested: false,
startTime: null,
loopCount: 0
};
}
}
cleanup() {
delete window[this.STORAGE_KEY];
}
setRunning(isRunning) {
const state = this.getState();
state.isRunning = isRunning;
if (isRunning) {
state.startTime = Date.now();
state.loopCount = 0;
}
}
requestStop() {
const state = this.getState();
state.stopRequested = true;
}
shouldStop() {
return this.getState().stopRequested;
}
isRunning() {
return this.getState().isRunning;
}
incrementLoopCount() {
const state = this.getState();
state.loopCount = (state.loopCount || 0) + 1;
return state.loopCount;
}
getLoopCount() {
return this.getState().loopCount || 0;
}
exceedsMaxLoops(maxLoops = LEARNER_CONSTANTS.MAX_SYNC_LOOPS) {
return this.getLoopCount() >= maxLoops;
}
}
const stateManager = new GlobalStateManager;
const GwypxPlayerFlow = {
async startPlayerFlow() {
if (stateManager.isRunning()) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[WARN] 学习流程已在运行中,跳过重复启动",
type: "warn"
});
return false;
}
stateManager.setRunning(true);
try {
const urlParams = new URLSearchParams(window.location.search);
const courseId = urlParams.get("courseId") || urlParams.get("id");
const tbtpId = urlParams.get("tbtpId");
if (!courseId) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[ERR] 无法识别课程ID",
type: "error"
});
return false;
}
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[PLAY] 开始党校分院播放页流程 (ID: ${courseId})`,
type: "info"
});
const playInfo = await GwypxApi.getPlayInfo(courseId);
if (!playInfo) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[ERR] 获取播放信息失败,可能未识别学员身份",
type: "error"
});
return false;
}
playInfo.tbtpId = tbtpId;
if (playInfo.progress >= 100) {
return await this._confirmCompletion(playInfo, "该课程已完成 (100%)!正在执行最终确认...");
}
const {detectMask: detectMask} = await Promise.resolve().then(function() {
return domHelper;
});
const mask = detectMask();
if (mask.exists) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[PROTECT] 检测到系统弹窗,等待自动处理...",
type: "info"
});
await new Promise(resolve => setTimeout(resolve, LEARNER_CONSTANTS.MASK_WAIT_TIME));
} else {
const relStatus = await GwypxApi.queryRel(courseId);
if (!relStatus?.data) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[CLICK] 正在自动关联课程 (建立学习关系)...",
type: "info"
});
await GwypxApi.addRel(courseId);
await new Promise(resolve => setTimeout(resolve, LEARNER_CONSTANTS.API_REL_DELAY));
}
}
await GwypxApi.studyStart(courseId, playInfo.tbtpId);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[SIGNAL] 已发送开始学习信号",
type: "info"
});
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[SYNC] 启动持续进度同步机制...",
type: "info"
});
let cumulativeTime = (playInfo.studyTimes || 0) + LEARNER_CONSTANTS.INITIAL_TIME_OFFSET;
while (true) {
if (stateManager.shouldStop()) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[STOP] 收到停止请求,终止进度同步",
type: "warn"
});
return false;
}
if (stateManager.exceedsMaxLoops()) {
const elapsed = Math.floor((Date.now() - stateManager.getState().startTime) / 1e3);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[WARN] 已达到最大同步次数限制 (${LEARNER_CONSTANTS.MAX_SYNC_LOOPS}次),已运行 ${elapsed} 秒。为防止内存溢出已停止。如果进度未满,请刷新页面继续。`,
type: "error"
});
UI.resetToggleButton("同步超时");
return false;
}
stateManager.incrementLoopCount();
const result = await GwypxApi.reportProgress(playInfo, cumulativeTime, LEARNER_CONSTANTS.PROGRESS_INCREMENT);
const currentProgress = parseInt(result?.courseProgress?.percentage || result?.data?.percentage || 0);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[PROG] 进度 ${currentProgress}% - 循环 ${stateManager.getLoopCount()}/${LEARNER_CONSTANTS.MAX_SYNC_LOOPS}`,
type: "info"
});
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, `学习中 ${currentProgress}%`);
EventBus.publish(CONSTANTS.EVENTS.PROGRESS_UPDATE, currentProgress);
if (currentProgress >= 100) {
return await this._confirmCompletion(playInfo, "课程已达到 100% 完成!正在进行最终确认...");
}
cumulativeTime += LEARNER_CONSTANTS.PROGRESS_INCREMENT;
const waitTime = CONFIG.SUPER_FAST_MODE ? LEARNER_CONSTANTS.SUPER_FAST_INTERVAL : LEARNER_CONSTANTS.NORMAL_INTERVAL;
await new Promise(resolve => setTimeout(resolve, waitTime));
}
} catch (error) {
console.error("[GwypxPlayerFlow] startPlayerFlow error:", error);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[ERR] 学习流程异常: ${error.message}`,
type: "error"
});
return false;
} finally {
stateManager.resetState();
}
},
async _confirmCompletion(playInfo, logPrefix) {
console.log(`[GwypxPlayerFlow] [OK] ${logPrefix}`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[OK] ${logPrefix}`,
type: "success"
});
const endResult = await GwypxApi.reportEnd(playInfo);
if (GwypxApi.isSuccessResponse(endResult)) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[DONE] 课程已确认结课!",
type: "success"
});
}
UI.resetToggleButton("学习完成");
return true;
}
};
const GwypxProcessor = {
async completeCourseByApi(courseId, title) {
if (!courseId) {
throw new Error("课程ID无效");
}
console.log(`[GwypxProcessor] 开始学习: ${title} (ID: ${courseId})`);
const playInfo = await GwypxApi.getPlayInfo(courseId);
if (!playInfo) {
throw new Error("获取播放信息失败");
}
console.log(`[GwypxProcessor] 播放信息: 进度${playInfo.progress}%, 已学${playInfo.studyTimes}秒`);
if (playInfo.progress >= 100) {
console.log(`[GwypxProcessor] 课程已完成,跳过`);
EventBus.publish(CONSTANTS.EVENTS.COURSE_SKIP, {
course: {
id: courseId,
title: title
},
reason: "已完成"
});
return {
skipped: true,
success: true
};
}
await GwypxApi.studyStart(courseId, playInfo.tbtpId);
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, title);
console.log(`[GwypxProcessor] 已发送开始学习信号`);
const durationSeconds = (playInfo.duration || 0) * 60;
const neededIterations = durationSeconds > 0 ? Math.ceil(durationSeconds / 60) + 5 : 100;
const maxAttempts = Math.min(neededIterations, 100);
let cumulativeTime = (playInfo.studyTimes || 0) + 60;
let knownProgress = playInfo.progress || 0;
if (knownProgress > 0) {
EventBus.publish(CONSTANTS.EVENTS.PROGRESS_UPDATE, knownProgress);
}
for (let i = 0; i < maxAttempts; i++) {
if (stateManager.shouldStop()) {
console.warn(`[GwypxProcessor] 收到停止请求,终止学习`);
return {
success: false,
skipped: false,
progress: knownProgress,
stopped: true
};
}
playInfo.studyTimes = cumulativeTime;
console.log(`[GwypxProcessor] 发送 end (${i + 1}/${maxAttempts}), studyTimes=${cumulativeTime}`);
const endResp = await GwypxApi.reportEnd(playInfo);
cumulativeTime += 60;
const endProgress = parseInt(endResp?.data?.percentage || endResp?.data?.progress || -1);
if (endProgress >= 0) knownProgress = Math.max(knownProgress, endProgress);
if (endProgress >= 100) {
console.log(`[GwypxProcessor] ✅ 服务端返回进度 ${endProgress}%,提前结束`);
EventBus.publish(CONSTANTS.EVENTS.COURSE_COMPLETE, {
course: {
id: courseId,
title: title
}
});
EventBus.publish(CONSTANTS.EVENTS.PROGRESS_UPDATE, 100);
return {
success: true,
skipped: false
};
}
if (i > 0 && i % 3 === 0) {
const verifyInfo = await GwypxApi.getPlayInfo(courseId);
if (verifyInfo && verifyInfo.progress >= 0) knownProgress = Math.max(knownProgress, verifyInfo.progress);
if (verifyInfo && verifyInfo.progress >= 100) {
console.log(`[GwypxProcessor] ✅ 验证进度 ${verifyInfo.progress}%,提前结束`);
EventBus.publish(CONSTANTS.EVENTS.COURSE_COMPLETE, {
course: {
id: courseId,
title: title
}
});
EventBus.publish(CONSTANTS.EVENTS.PROGRESS_UPDATE, 100);
return {
success: true,
skipped: false
};
}
if (verifyInfo && verifyInfo.progress >= 0) {
EventBus.publish(CONSTANTS.EVENTS.PROGRESS_UPDATE, verifyInfo.progress);
}
}
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, `学习中 ${Math.min(Math.round((i + 1) / maxAttempts * 100), 99)}%`);
await new Promise(resolve => setTimeout(resolve, 500));
}
console.log(`[GwypxProcessor] end 循环完成,正在验证进度...`);
const verifyInfo = await GwypxApi.getPlayInfo(courseId);
if (verifyInfo && verifyInfo.progress >= 100) {
console.log(`[GwypxProcessor] ✅ 课程已完成! (验证进度: ${verifyInfo.progress}%)`);
EventBus.publish(CONSTANTS.EVENTS.COURSE_COMPLETE, {
course: {
id: courseId,
title: title
}
});
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, "学习中");
EventBus.publish(CONSTANTS.EVENTS.PROGRESS_UPDATE, 100);
return {
success: true,
skipped: false
};
}
console.warn(`[GwypxProcessor] ⚠️ 验证进度: ${verifyInfo?.progress || 0}%,未达 100%`);
return {
success: false,
skipped: false,
progress: verifyInfo?.progress || 0,
error: "verify_failed"
};
}
};
const GwypxLearner = {
_log(message, type = "info") {
console.log(`[GwypxLearner] ${message}`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: message,
type: type
});
},
get _pageValidator() {
return createPageValidator({
whitelist: GWYPX_CONSTANTS.PAGE_TYPE_WHITELIST,
pageTypes: GwypxHandler.PAGE_TYPES,
customMessages: {
index: "[WARN] 党校分院首页暂不支持自动学习。请进入课程播放页或个人中心。",
default: "[WARN] 当前页面不支持自动学习。请进入课程播放页或个人中心。"
}
});
},
_validatePageType(pageType) {
return this._pageValidator.validate(pageType);
},
async selectAndExecute() {
if (!CONFIG.GWYPX_MODE) return null;
const pageType = GwypxHandler.identifyPage();
if (!this._validatePageType(pageType)) {
return false;
}
if (pageType === GwypxHandler.PAGE_TYPES.PLAYER) {
return await GwypxPlayerFlow.startPlayerFlow();
}
if (pageType === GwypxHandler.PAGE_TYPES.CENTER) {
return await this.startCategoryBatchFlow();
}
if (pageType === GwypxHandler.PAGE_TYPES.COMMEND_INDEX) {
return await this._handleCommendIndex();
}
if (pageType === GwypxHandler.PAGE_TYPES.SUBJECT_COLUMN_DETAIL || pageType === GwypxHandler.PAGE_TYPES.SUBJECT_DETAIL) {
return await this._handleSubjectColumnDetail();
}
return null;
},
_publishStats(stats) {
Utils.publishStats(stats);
},
async _processCourses(courses, flowName) {
if (!courses || courses.length === 0) {
this._log("⚠️ 未获取到课程列表", "warn");
return false;
}
this._log(`✅ 获取到 ${courses.length} 门课程`);
let learned = 0, failed = 0, skipped = 0;
for (let i = 0; i < courses.length; i++) {
const {Learner: Learner} = await Promise.resolve().then(function() {
return learner;
});
if (Learner && Learner.stopRequested) {
this._log("🛑 用户停止学习", "warn");
break;
}
const c = courses[i];
const courseId = String(c.id);
const name = c.name || `课程${courseId}`;
if (this._isCourseCompleted(c)) {
this._log(`[SKIP] ${name} (已完成)`, "info");
skipped++;
continue;
}
this._log(`[学习] ${i + 1}/${courses.length}: ${name}`);
try {
const result = await GwypxProcessor.completeCourseByApi(courseId, name);
if (result.success) learned++; else failed++;
} catch (e) {
this._log(`❌ ${name}: ${e.message}`, "error");
failed++;
}
}
this._log(`🎉 ${flowName}完成: ${learned}学习 ${skipped}跳过 ${failed}失败`, "success");
return true;
},
async _handleCommendIndex() {
this._log("📋 检测到课程推荐页,开始扫描...");
try {
const resp = await GwypxApi.getRecommendCourses(0, 50, "new");
return await this._processCourses(resp?.datalist, "课程推荐");
} catch (e) {
this._log(`❌ 课程推荐页处理失败: ${e.message}`, "error");
return false;
}
},
async _handleSubjectColumnDetail() {
this._log("📋 检测到专栏详情页,开始扫描...");
try {
const topicId = new URLSearchParams(window.location.search).get("id");
if (!topicId) {
this._log("❌ 无法获取专栏ID", "error");
return false;
}
const topicResp = await GwypxApi.getTopic(topicId);
const code = topicResp?.data?.code;
if (!code) {
this._log("❌ 无法获取专栏编码", "error");
return false;
}
this._log(`📚 专栏: ${topicResp.data.name}`);
const courseResp = await GwypxApi.getSubjectColumnCourses(code);
return await this._processCourses(courseResp?.datalist, "专栏");
} catch (e) {
this._log(`❌ 专栏详情页处理失败: ${e.message}`, "error");
return false;
}
},
async _executeBatchFlow(fetchPageFunc, flowName) {
if (stateManager.isRunning()) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[WARN] ${flowName}已在运行中,跳过重复启动`,
type: "warn"
});
return {
success: false,
alreadyRunning: true
};
}
stateManager.setRunning(true);
EventBus.publish(CONSTANTS.EVENTS.LEARNING_START);
const stats = {
totalCourses: 0,
completedCourses: 0,
skippedCourses: 0,
failedCourses: 0
};
try {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[BATCH] 开始${flowName}...`,
type: "info"
});
let pageNum = 0;
const pageSize = LEARNER_CONSTANTS.BATCH_PAGE_SIZE;
let totalElements = 0;
while (!stateManager.shouldStop()) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[GET] 正在获取第 ${pageNum + 1} 页课程...`,
type: "info"
});
let response;
try {
response = await fetchPageFunc(pageNum, pageSize);
} catch (e) {
console.error(`[GwypxLearner] 获取课程请求失败:`, e);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[ERR] 获取第 ${pageNum + 1} 页课程失败: ${e.message}`,
type: "error"
});
break;
}
if (!response || typeof response !== "object" || !response.datalist) {
console.warn("[GwypxLearner] 获取课程响应无效:", response);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[ERR] 获取课程响应无效`,
type: "error"
});
break;
}
const courseList = response.datalist || [];
totalElements = response.totalelements || 0;
if (courseList.length === 0) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[OK] 所有课程已学习完成`,
type: "success"
});
break;
}
if (pageNum === 0 && courseList.length > 0) {
console.log("[GwypxLearner] 课程数据示例:", JSON.stringify(courseList[0], null, 2));
}
for (let i = 0; i < courseList.length; i++) {
if (stateManager.shouldStop()) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[STOP] 收到停止请求,终止学习",
type: "warn"
});
break;
}
const item = courseList[i];
const course = this._normalizeCourseItem(item);
if (this._isCourseCompleted(item)) {
stats.skippedCourses++;
console.log(`[GwypxLearner] 跳过已学习课程: ${course.title}`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[SKIP] ${course.title} 已学习,跳过`,
type: "info"
});
continue;
}
stats.totalCourses++;
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, course.title);
try {
const result = await GwypxProcessor.completeCourseByApi(course.courseId, course.title);
if (result.skipped) {
stats.skippedCourses++;
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[SKIP] ${course.title} 已完成,跳过`,
type: "info"
});
} else if (result.success) {
stats.completedCourses++;
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[OK] ${course.title} 学习完成`,
type: "success"
});
} else {
stats.failedCourses++;
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[WARN] ${course.title} 学习失败`,
type: "warn"
});
}
this._publishStats(stats);
} catch (error) {
stats.failedCourses++;
const errorMsg = error?.message || String(error);
console.error(`[GwypxLearner] 学习 ${course.title} 失败:`, error);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[ERR] ${course.title} 学习失败: ${errorMsg}`,
type: "error"
});
this._publishStats(stats);
}
if (i < courseList.length - 1) {
await new Promise(resolve => setTimeout(resolve, LEARNER_CONSTANTS.BATCH_REQUEST_DELAY));
}
}
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[BOOK] 第 ${pageNum + 1} 页学习完成: 完成 ${stats.completedCourses}, 跳过 ${stats.skippedCourses}, 失败 ${stats.failedCourses}`,
type: "info"
});
if (courseList.length < pageSize || stats.completedCourses + stats.skippedCourses >= totalElements) {
break;
}
pageNum++;
}
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `🎉 ${flowName}完成!总计: ${stats.totalCourses}, 完成: ${stats.completedCourses}, 跳过: ${stats.skippedCourses}, 失败: ${stats.failedCourses}`,
type: "success"
});
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, `学习完成 ${stats.completedCourses}/${stats.totalCourses}`);
return {
success: true,
stats: stats
};
} catch (error) {
console.error(`[GwypxLearner] ${flowName} error:`, error);
const errorMsg = error?.message || String(error);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `[ERR] ${flowName}异常: ${errorMsg}`,
type: "error"
});
return {
success: false,
error: error,
stats: stats
};
} finally {
stateManager.resetState();
EventBus.publish(CONSTANTS.EVENTS.LEARNING_STOP);
}
},
_normalizeCourseItem(item) {
return {
courseId: item.id || item.courseId || item.tbtpId,
title: item.name || item.title || item.courseName || item.tbtpName,
percentage: item.percentage,
studyStatus: item.studyStatus,
creditHour: item.creditHour
};
},
_isCourseCompleted(item) {
return item.showStatusMsg === "已学习" || item.studyStatus === "2" || item.percentage === "100%";
},
async startCategoryBatchFlow() {
return await this._executeBatchFlow(async (pageNum, pageSize) => await GwypxApi.getCoursesByCategory(pageNum, pageSize), "分类课程批量学习");
}
};
const ENVIRONMENT_IDS = {
PUDONG: "pudong",
CBEAD: "cbead",
DX: "dx"
};
const ApiFactory = {
_instances: {
[ENVIRONMENT_IDS.PUDONG]: null,
[ENVIRONMENT_IDS.CBEAD]: null,
[ENVIRONMENT_IDS.DX]: null
},
createApi(envId) {
if (this._instances[envId]) {
return this._instances[envId];
}
let apiInstance;
switch (envId) {
case ENVIRONMENT_IDS.PUDONG:
apiInstance = PudongApi;
break;
case ENVIRONMENT_IDS.CBEAD:
apiInstance = CbeadApi;
break;
case ENVIRONMENT_IDS.DX:
apiInstance = GwypxApi;
break;
default:
throw new Error(`未知的 API 类型: ${envId}`);
}
this._instances[envId] = apiInstance;
return apiInstance;
},
getPudongApi() {
return this.createApi(ENVIRONMENT_IDS.PUDONG);
},
getCbeadApi() {
return this.createApi(ENVIRONMENT_IDS.CBEAD);
},
getGwypxApi() {
return this.createApi(ENVIRONMENT_IDS.DX);
},
getCurrentApi() {
const config = ServiceLocator.get(ServiceNames.CONFIG);
if (config?.PUDONG_MODE) {
return this.getPudongApi();
}
if (config?.CBEAD_MODE) {
return this.getCbeadApi();
}
if (config?.GWYPX_MODE) {
return this.getGwypxApi();
}
return this.getPudongApi();
},
clearCache() {
Object.keys(this._instances).forEach(key => {
this._instances[key] = null;
});
},
getCurrentConfig() {
const config = ServiceLocator.get(ServiceNames.CONFIG);
if (config?.PUDONG_MODE) return PUDONG_API_CONFIG;
if (config?.CBEAD_MODE) return CBEAD_API_CONFIG;
if (config?.GWYPX_MODE) return GWYPX_API_CONFIG;
return PUDONG_API_CONFIG;
}
};
const API$1 = Object.assign({}, PudongApi, CbeadApi, {
factory: ApiFactory,
_getCurrentApi() {
const config = ServiceLocator.get(ServiceNames.CONFIG);
if (config?.PUDONG_MODE) return PudongApi;
if (config?.CBEAD_MODE) return CbeadApi;
if (config?.GWYPX_MODE) return GwypxApi;
return PudongApi;
},
async reportProgress(playInfo, currentTime) {
return this._getCurrentApi().reportProgress(playInfo, currentTime);
},
async reportProgressWithDelay(playInfo, currentTime) {
return this._getCurrentApi().reportProgressWithDelay(playInfo, currentTime);
}
});
const LOG_LEVELS = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
NONE: 4
};
const defaultConfig = {
level: LOG_LEVELS.INFO,
showTimestamp: true,
showModule: true,
disableInProduction: true
};
let config = {
...defaultConfig
};
function getTimestamp() {
const now = new Date;
return now.toISOString().split("T")[1].replace("Z", "").slice(0, -1);
}
function formatMessage(level, module, args) {
const parts = [];
if (config.showTimestamp) {
parts.push(`[${getTimestamp()}]`);
}
parts.push(`[${level}]`);
if (config.showModule && module) {
parts.push(`[${module}]`);
}
return [ parts.join(" "), ...args ];
}
const Logger = {
setLevel(level) {
if (typeof level === "string") {
const upperLevel = level.toUpperCase();
config.level = LOG_LEVELS[upperLevel] !== undefined ? LOG_LEVELS[upperLevel] : LOG_LEVELS.INFO;
} else if (typeof level === "number") {
config.level = level >= 0 && level <= 4 ? level : LOG_LEVELS.INFO;
}
if (typeof window !== "undefined") {
const isDev = window.location?.hostname?.includes("localhost") || window.location?.hostname?.includes("dev");
if (isDev && config.level === LOG_LEVELS.INFO) {
config.level = LOG_LEVELS.DEBUG;
}
}
},
getLevel() {
return config.level;
},
debug(...args) {
if (config.level <= LOG_LEVELS.DEBUG) {
console.debug(...formatMessage("DEBUG", null, args));
}
},
debugM(module, ...args) {
if (config.level <= LOG_LEVELS.DEBUG) {
console.debug(...formatMessage("DEBUG", module, args));
}
},
info(...args) {
if (config.level <= LOG_LEVELS.INFO) {
console.info(...formatMessage("INFO", null, args));
}
},
infoM(module, ...args) {
if (config.level <= LOG_LEVELS.INFO) {
console.info(...formatMessage("INFO", module, args));
}
},
warn(...args) {
if (config.level <= LOG_LEVELS.WARN) {
console.warn(...formatMessage("WARN", null, args));
}
},
warnM(module, ...args) {
if (config.level <= LOG_LEVELS.WARN) {
console.warn(...formatMessage("WARN", module, args));
}
},
error(...args) {
if (config.level <= LOG_LEVELS.ERROR) {
console.error(...formatMessage("ERROR", null, args));
}
},
errorM(module, ...args) {
if (config.level <= LOG_LEVELS.ERROR) {
console.error(...formatMessage("ERROR", module, args));
}
},
log(level, ...args) {
const upperLevel = level.toUpperCase();
const levelValue = LOG_LEVELS[upperLevel] !== undefined ? LOG_LEVELS[upperLevel] : LOG_LEVELS.INFO;
if (config.level <= levelValue) {
console.log(...formatMessage(upperLevel, null, args));
}
},
group(label, collapsed = false) {
if (config.level <= LOG_LEVELS.DEBUG) {
if (collapsed) {
console.groupCollapsed(label);
} else {
console.group(label);
}
}
},
groupEnd() {
if (config.level <= LOG_LEVELS.DEBUG) {
console.groupEnd();
}
},
time(label) {
if (config.level <= LOG_LEVELS.DEBUG) {
console.time(label);
}
},
timeEnd(label) {
if (config.level <= LOG_LEVELS.DEBUG) {
console.timeEnd(label);
}
},
table(label, data) {
if (config.level <= LOG_LEVELS.DEBUG && Array.isArray(data)) {
this.group(label);
console.table(data);
this.groupEnd();
}
},
configure(newConfig) {
config = {
...config,
...newConfig
};
},
reset() {
config = {
...defaultConfig
};
}
};
function createLogger(moduleName) {
return {
debug: (...args) => Logger.debugM(moduleName, ...args),
info: (...args) => Logger.infoM(moduleName, ...args),
warn: (...args) => Logger.warnM(moduleName, ...args),
error: (...args) => Logger.errorM(moduleName, ...args),
group: (label, collapsed) => Logger.group(`[${moduleName}] ${label}`, collapsed),
time: label => Logger.time(`[${moduleName}] ${label}`),
timeEnd: label => Logger.timeEnd(`[${moduleName}] ${label}`)
};
}
const SM_LOGGER = createLogger("PudongStateManager");
const PudongStateManager = {
status: CONSTANTS.LEARNING_STATES.IDLE,
currentCourse: null,
progress: {
currentPercent: 0,
watchedSeconds: 0,
totalSeconds: 0,
currentChapter: 0,
totalChapters: 0
},
failureReason: null,
async startCourse(course, {index: index, total: total} = {}) {
this.currentCourse = course;
this.status = CONSTANTS.LEARNING_STATES.PREPARING;
this.failureReason = null;
this.progress = {
currentPercent: 0,
watchedSeconds: 0,
totalSeconds: 0,
currentChapter: 0,
totalChapters: 0
};
EventBus.publish(CONSTANTS.EVENTS.COURSE_START, {
course: course,
status: this.status,
index: index,
total: total
});
SM_LOGGER.info(`开始课程: ${course.title}`);
},
async updateProgress(updates) {
this.progress = {
...this.progress,
...updates
};
EventBus.publish(CONSTANTS.EVENTS.PROGRESS_UPDATE, {
percent: this.progress.currentPercent,
watched: this.progress.watchedSeconds,
total: this.progress.totalSeconds
});
},
async setLearning() {
this.status = CONSTANTS.LEARNING_STATES.LEARNING;
SM_LOGGER.debug("进入学习状态");
},
async complete(result = {}) {
this.status = CONSTANTS.LEARNING_STATES.COMPLETED;
EventBus.publish(CONSTANTS.EVENTS.COURSE_COMPLETE, {
course: this.currentCourse,
result: result
});
SM_LOGGER.info(`课程完成: ${this.currentCourse?.title}`, result);
},
async skip(reason) {
this.status = CONSTANTS.LEARNING_STATES.SKIPPED;
EventBus.publish(CONSTANTS.EVENTS.COURSE_SKIP, {
course: this.currentCourse,
reason: reason
});
SM_LOGGER.info(`课程跳过: ${this.currentCourse?.title}, 原因: ${reason}`);
},
async fail(reason, error = null) {
this.status = CONSTANTS.LEARNING_STATES.FAILED;
this.failureReason = reason;
EventBus.publish(CONSTANTS.EVENTS.COURSE_ERROR, {
course: this.currentCourse,
reason: reason,
error: error
});
SM_LOGGER.error(`课程失败: ${this.currentCourse?.title}, 原因: ${reason}`, error);
},
async reset() {
this.status = CONSTANTS.LEARNING_STATES.IDLE;
this.currentCourse = null;
this.failureReason = null;
this.progress = {
currentPercent: 0,
watchedSeconds: 0,
totalSeconds: 0,
currentChapter: 0,
totalChapters: 0
};
SM_LOGGER.info("状态已重置");
},
isLearning() {
return this.status === CONSTANTS.LEARNING_STATES.LEARNING;
},
isCompleted() {
return this.status === CONSTANTS.LEARNING_STATES.COMPLETED;
},
getState() {
return {
status: this.status,
course: this.currentCourse,
progress: this.progress,
failureReason: this.failureReason
};
}
};
const PROC_LOGGER = createLogger("PudongProcessor");
const PudongProcessor = {
async processCourse(course, options = {}) {
const {skipCompleted: skipCompleted = true, index: index, total: total} = options;
const courseId = course.id || course.courseId;
course.dsUnitId;
PROC_LOGGER.info(`开始处理课程: ${course.title}`, {
courseId: courseId
});
try {
const prepResult = await this.prepare(course, {
skipCompleted: skipCompleted,
index: index,
total: total
});
if (prepResult.action === "skip") {
PROC_LOGGER.info(`课程跳过: ${course.title}, 原因: ${prepResult.reason}`);
return {
action: "skip",
course: course,
reason: prepResult.reason
};
}
if (prepResult.action === "fail") {
PROC_LOGGER.warn(`课程准备失败: ${course.title}`);
return {
action: "fail",
course: course,
reason: prepResult.reason
};
}
const execResult = await this.execute(course, prepResult.playInfo);
if (!execResult.success) {
await PudongStateManager.fail(execResult.reason || "学习执行失败");
return {
action: "fail",
course: course,
reason: execResult.reason
};
}
await this.cleanup();
await PudongStateManager.complete(execResult);
PROC_LOGGER.info(`课程完成: ${course.title}`, execResult);
return {
action: "complete",
course: course,
result: execResult
};
} catch (error) {
PROC_LOGGER.error(`课程处理异常: ${course.title}`, error);
await PudongStateManager.fail("unknown_error", error);
return {
action: "fail",
course: course,
reason: error.message
};
}
},
async prepare(course, options = {}) {
const {skipCompleted: skipCompleted = true, index: index, total: total} = options;
const courseId = course.id || course.courseId;
const coursewareId = course.dsUnitId;
await PudongStateManager.startCourse(course, {
index: index,
total: total
});
if (skipCompleted && PUDONG_CONSTANTS.SKIP_COMPLETED_COURSES) {
try {
const completionCheck = await PudongApi.checkCompletion(courseId, coursewareId);
if (completionCheck.isCompleted) {
await PudongStateManager.skip(`已完成 (${completionCheck.finishedRate}%)`);
return {
action: "skip",
reason: "课程已完成"
};
}
} catch (e) {
PROC_LOGGER.warn("完成度检查失败,继续学习", e);
}
}
let playInfo;
try {
playInfo = await PudongApi.getPlayInfo(courseId, course.dsUnitId, course.durationStr);
} catch (e) {
PROC_LOGGER.warn("获取播放信息失败,使用默认值", e);
playInfo = {
courseId: courseId,
coursewareId: coursewareId || courseId,
videoId: `mock_${courseId}`,
duration: CONSTANTS.TIME_FORMATS.DEFAULT_DURATION,
lastLearnedTime: 0
};
}
if (!playInfo) {
await PudongStateManager.fail("无法获取播放信息");
return {
action: "fail",
reason: "无法获取课程播放信息"
};
}
await PudongStateManager.updateProgress({
totalSeconds: playInfo.duration,
watchedSeconds: playInfo.lastLearnedTime
});
const progressPercent = Math.floor(playInfo.lastLearnedTime / playInfo.duration * 100);
if (progressPercent >= PUDONG_CONSTANTS.COMPLETION_THRESHOLD) {
await PudongStateManager.skip(`进度已达 ${progressPercent}%`);
return {
action: "skip",
reason: "播放信息确认已完成"
};
}
PROC_LOGGER.info(`课程准备完成: ${course.title}`, {
progress: `${progressPercent}%`,
duration: playInfo.duration
});
return {
action: "learn",
playInfo: playInfo
};
},
async execute(course, playInfo) {
await PudongStateManager.setLearning();
try {
const courseInfo = {
...course,
...playInfo,
title: course.title || course.courseName,
courseId: course.id || course.courseId
};
const success = await PudongApi.reportProgress(playInfo, playInfo.duration - 30);
if (success) {
return {
success: true,
reason: "策略执行成功"
};
} else {
return {
success: false,
reason: "策略执行失败"
};
}
} catch (error) {
return {
success: false,
reason: error.message
};
}
},
async cleanup() {
PROC_LOGGER.debug("课后清理完成");
}
};
const PudongLearner = {
PAGE_TYPES: PUDONG_CONSTANTS.PAGE_TYPES,
get _pageValidator() {
return createPageValidator({
whitelist: PUDONG_CONSTANTS.PAGE_TYPE_WHITELIST,
pageTypes: PUDONG_CONSTANTS.PAGE_TYPES,
customMessages: {
index: "⚠️ 首页暂不支持自动学习,请进入专栏或课程页面",
default: "⚠️ 当前页面不支持自动学习。请进入课程播放页或列表页。"
}
});
},
_validatePageType(pageType) {
return this._pageValidator.validate(pageType);
},
identifyPage() {
return PudongHandler.identifyPage();
},
isPudongMode() {
return PudongHandler.isPudongMode();
},
async selectAndExecute() {
if (!this.isPudongMode()) {
return null;
}
const pageType = this.identifyPage();
if (!this._validatePageType(pageType)) {
return false;
}
if (pageType === this.PAGE_TYPES.PLAYER) {
return await this._handlePlayerPage();
}
if (pageType === this.PAGE_TYPES.COLUMN) {
return await this._handleColumnPage();
}
if (pageType === this.PAGE_TYPES.INDEX) {
return await this._handleIndexPage();
}
if (pageType === this.PAGE_TYPES.COURSE_LIST) {
return await this._handleCourseListPage();
}
return null;
},
async _handlePlayerPage() {
console.log("[PudongLearner] 处理浦东播放页");
try {
const result = await PudongHandler.startPlayerFlow();
if (result) {
EventBus.publish(CONSTANTS.EVENTS.LEARNING_START, {
source: "pudong_player"
});
return true;
}
return false;
} catch (error) {
console.error("[PudongLearner] 播放页处理失败:", error);
return false;
}
},
async _batchLearn(courses, pageLabel) {
if (!courses || courses.length === 0) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "⚠️ 未扫描到课程",
type: "warn"
});
return false;
}
console.log(`[PudongLearner] 获取到 ${courses.length} 门课程`);
EventBus.publish(CONSTANTS.EVENTS.STATISTICS_UPDATE, {
total: courses.length,
completed: 0,
learned: 0,
failed: 0,
skipped: 0
});
const {Learner: Learner} = await Promise.resolve().then(function() {
return learner;
});
const results = await this.processCourses(courses, {
stopChecker: () => Learner && Learner.stopRequested
});
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `🎉 ${pageLabel}完成: ${results.completed}学习 ${results.skipped}跳过 ${results.failed}失败`,
type: "success"
});
return true;
},
async _handleColumnPage() {
console.log("[PudongLearner] 处理浦东专栏页");
try {
return await this._batchLearn(await PudongHandler.scanCourses(), "专栏");
} catch (error) {
console.error("[PudongLearner] 专栏页处理失败:", error);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `❌ 专栏页处理失败: ${error.message}`,
type: "error"
});
return false;
}
},
async _handleIndexPage() {
console.log("[PudongLearner] 处理浦东首页");
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "⚠️ 首页暂不支持自动学习,请进入专栏或课程页面",
type: "warn"
});
return false;
},
async _handleCourseListPage() {
console.log("[PudongLearner] 处理浦东课程列表页");
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "📋 检测到课程列表页,开始扫描...",
type: "info"
});
try {
return await this._batchLearn(await PudongHandler.scanCourses(), "课程列表");
} catch (error) {
console.error("[PudongLearner] 课程列表页处理失败:", error);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `❌ 课程列表页处理失败: ${error.message}`,
type: "error"
});
return false;
}
},
async processCourses(courses, options = {}) {
const results = {
total: courses.length,
completed: 0,
learned: 0,
skipped: 0,
failed: 0,
details: []
};
const {stopChecker: stopChecker = null} = options;
for (let i = 0; i < courses.length; i++) {
if (stopChecker && stopChecker()) {
console.log("[PudongLearner] 用户停止学习");
break;
}
const course = courses[i];
const isLast = i === courses.length - 1;
try {
const result = await PudongProcessor.processCourse(course, {
skipCompleted: true,
index: i + 1,
total: courses.length
});
if (result.action === "complete") {
results.completed++;
results.learned++;
} else if (result.action === "skip") {
results.skipped++;
} else {
results.failed++;
}
results.details.push({
courseId: course.id || course.courseId,
title: course.title || course.courseName,
action: result.action,
reason: result.reason
});
EventBus.publish(CONSTANTS.EVENTS.STATISTICS_UPDATE, results);
if (!isLast && result.action !== "fail") {
await PudongProcessor.coolingDown(isLast, stopChecker);
}
} catch (error) {
console.error(`[PudongLearner] 处理课程失败: ${course.title}`, error);
results.failed++;
results.details.push({
courseId: course.id || course.courseId,
title: course.title || course.courseName,
action: "error",
reason: error.message
});
}
}
console.log("[PudongLearner] 批量处理完成", results);
return results;
},
async getPlayerCoursewareList() {
const courseId = this._extractCourseIdFromUrl();
if (!courseId) {
console.warn("[PudongLearner] 无法提取课程ID");
return [];
}
return await PudongApi.getCoursewareList(courseId);
},
_extractCourseIdFromUrl() {
const url = new URL(window.location.href);
return url.searchParams.get("courseId") || url.searchParams.get("id");
},
async reset() {
await PudongStateManager.reset();
},
getState() {
return PudongStateManager.getState();
}
};
const FlowOrchestrator = {
async selectAndExecute() {
if (CONFIG.GWYPX_MODE) {
const gwypxResult = await GwypxLearner.selectAndExecute();
if (gwypxResult !== null) {
return gwypxResult;
}
}
if (CONFIG.CBEAD_MODE) {
const cbeadResult = await CbeadLearner.selectAndExecute();
if (cbeadResult !== null) {
return cbeadResult;
}
}
if (CONFIG.PUDONG_MODE) {
const pudongResult = await PudongLearner.selectAndExecute();
if (pudongResult !== null) {
return pudongResult;
}
}
return null;
}
};
function _buildPagePatterns() {
const patterns = {};
for (const [platform, pages] of Object.entries(CONSTANTS.PAGE_CONFIG)) {
for (const [key, config] of Object.entries(pages)) {
const paths = Array.isArray(config.path) ? config.path : [ config.path ];
patterns[`${platform}_${key}`] = paths.map(p => new RegExp(p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
}
}
return patterns;
}
const _patterns = _buildPagePatterns();
const UrlParser = {
PAGE_PATTERNS: _patterns,
extractIdFromUrl(url) {
if (!url) return null;
const urlObj = new URL(url, "http://localhost");
let id = urlObj.searchParams.get("id") || urlObj.searchParams.get("courseId");
if (id) return id;
const hash = urlObj.hash;
if (!hash) return null;
if (hash.includes("?")) {
const hashSearch = hash.split("?")[1];
const hashParams = new URLSearchParams(hashSearch);
id = hashParams.get("id");
if (id) return id;
}
let match = hash.match(/[?&]id=([^&]+)/);
if (match) return match[1];
match = hash.match(/branch-list-v\/([a-f0-9-]+)/);
if (match) return match[1];
match = hash.match(/detail\/\d+&([a-f0-9-]{36})/);
if (match) return match[1];
match = hash.match(/detail\/(?:[^/]*@@)?([a-f0-9-]{36})/i);
if (match) return match[1];
return null;
},
isIdRequiredPage(href) {
return Object.values(_patterns).some(patterns => patterns.some(re => re.test(href)));
},
isChannelListPage(href) {
return href.includes("channelList");
},
isPudongSpecialPage(PudongHandler) {
if (!PudongHandler) return false;
return PudongHandler.identifyPage() === PudongHandler.PAGE_TYPES.COLUMN;
},
isHomePage(href) {
for (const pages of Object.values(CONSTANTS.PAGE_CONFIG)) {
for (const [key, config] of Object.entries(pages)) {
if (key === "INDEX" || key === "HOME_V") {
const paths = Array.isArray(config.path) ? config.path : [ config.path ];
if (paths.some(p => href.includes(p))) return true;
}
}
}
if (href.includes("pagehome/index")) return true;
const homeElement = document?.querySelector('[module-name="nc.pagehome.index"]');
return !!homeElement;
}
};
let API = null;
function setAPI(api) {
API = api;
}
const Learner = {
state: CONSTANTS.LEARNING_STATES.IDLE,
isRunning: false,
stopRequested: false,
stop: function() {
this.isRunning = false;
this.stopRequested = true;
this.state = CONSTANTS.LEARNING_STATES.IDLE;
const gwypxState = window.__CELA_GWYPX_STATE__;
if (gwypxState) {
gwypxState.stopRequested = true;
}
if (API && API.abortController) {
API.abortController.abort();
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "正在中止所有网络请求...",
type: "info"
});
}
const toggleBtn = document.getElementById(CONSTANTS.SELECTORS.TOGGLE_BTN.replace("#", ""));
if (toggleBtn) {
toggleBtn.setAttribute("data-state", "stopped");
toggleBtn.textContent = "开始学习";
}
EventBus.publish(CONSTANTS.EVENTS.LEARNING_STOP);
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, "已停止");
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[STOP] 学习流程已停止",
type: "warn"
});
},
hasValidId: function() {
if (CONFIG.IS_PORTAL || CONFIG.UNSUPPORTED_BRANCH) return false;
const href = window.location.href;
if (UrlParser.isHomePage(href)) return false;
if (UrlParser.isChannelListPage(href)) return false;
const needsId = UrlParser.isIdRequiredPage(href);
const id = UrlParser.extractIdFromUrl(href);
if (id) return true;
if (needsId) return false;
const hasCourseElements = CONSTANTS.COURSE_SELECTORS.some(selector => document.querySelector(selector));
if (hasCourseElements) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[校验] 虽然URL没发现ID,但页面检测到课程元素,允许启动",
type: "info"
});
return true;
}
return false;
},
async startLearning() {
try {
const result = await FlowOrchestrator.selectAndExecute();
if (result === CONSTANTS.WAITING_FOR_USER) {
console.log("[Learner] 用户主动点击按钮,开始学习流程");
const {CbeadLearner: CbeadLearner} = await Promise.resolve().then(function() {
return learner$1;
});
await CbeadLearner.startBranchListFlow();
return;
}
if (result === null) {
UI.resetToggleButton("页面不支持");
return;
}
if (result === false) {
UI.resetToggleButton("页面不支持");
return;
}
if (result === true) {
if (!this.stopRequested) {
UI.resetToggleButton();
}
return;
}
} catch (error) {
this._handleLearningError(error);
}
},
_handleLearningError(error) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `学习流程出错: ${error.message}`,
type: "error"
});
console.error("学习流程错误:", error);
UI.resetToggleButton("学习出错");
}
};
var learner = Object.freeze({
__proto__: null,
Learner: Learner,
setAPI: setAPI
});
const LearningStrategies = {
async instant_finish(context) {
const {duration: duration} = context;
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[SPEED] 采用极速完成策略 - 直接冲刺",
type: "info"
});
const learner = ServiceLocator.get(ServiceNames.LEARNER);
const delay = Math.floor(Math.random() * 500 + 500);
const steps = 5;
const stepDelay = delay / steps;
for (let i = 0; i < steps; i++) {
if (learner && learner.stopRequested) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[STOP] 用户中断学习",
type: "warn"
});
return false;
}
await new Promise(resolve => setTimeout(resolve, stepDelay));
}
const finalTime = Math.max(0, duration - 30);
if (learner && learner.stopRequested) {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "🛑 用户中断学习",
type: "warn"
});
return false;
}
const api = ServiceLocator.get(ServiceNames.API);
return await api.reportProgressWithDelay(context.playInfo, finalTime);
}
};
function setupDependencyInjection() {
if (typeof window !== "undefined") {
window.UI = UI;
window.CbeadLearner = CbeadLearner;
}
ServiceLocator.register(ServiceNames.UI, UI);
ServiceLocator.register(ServiceNames.API, API$1);
ServiceLocator.register(ServiceNames.LEARNER, Learner);
ServiceLocator.register(ServiceNames.CONFIG, CONFIG);
setAPI(API$1);
}
function initScript() {
startMaskObserver();
Settings.load();
setupDependencyInjection();
UI.createPanel();
detectEnvironment();
if (CONFIG.PUDONG_MODE) {
PudongHandler.init();
}
if (CONFIG.CBEAD_MODE) {
CbeadHandler.init();
setupRouteListener();
setupProgressErrorMonitor();
}
if (CONFIG.GWYPX_MODE) {
GwypxHandler.init();
}
GM_registerMenuCommand("导出调试日志", UI.exportLogs, "e");
const toggleBtn = document.getElementById(CONSTANTS.SELECTORS.TOGGLE_BTN.replace("#", ""));
if (toggleBtn) {
toggleBtn.addEventListener("click", () => {
const isRunning = toggleBtn.getAttribute("data-state") === "running";
if (isRunning) {
Learner.stop();
} else {
Learner.stopRequested = false;
Learner.isRunning = true;
Learner.state = CONSTANTS.LEARNING_STATES.LEARNING;
EventBus.publish(CONSTANTS.EVENTS.LEARNING_START);
EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, "学习中...");
Learner.startLearning().catch(error => {
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `❌ 启动学习流程失败: ${error.message}`,
type: "error"
});
Learner.stop();
});
}
});
}
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "[START] cela学习助手 初始化完成",
type: "success"
});
setTimeout(() => {
checkAndStartAutoLearning();
}, 1e3);
}
let isAutoStarting = false;
function checkAndStartAutoLearning() {
const isCbeadPlayer = CONFIG.CBEAD_MODE && window.location.href.includes("study/course/detail");
const isGwypxPlayer = CONFIG.GWYPX_MODE && window.location.href.includes("/pcPage/commend/coursedetail");
if (isCbeadPlayer || isGwypxPlayer) {
console.log(`[Init] 🔍 检测到${isCbeadPlayer ? "企业" : "党校"}分院播放页,准备开始学习...`);
if (isAutoStarting) {
console.log("[Init] ⚠️ 学习任务已在进行中,跳过重复启动");
return;
}
isAutoStarting = true;
console.log("[Init] 🔒 设置防重复标志,开始学习...");
const toggleBtn = document.getElementById("toggle-learning-btn");
if (toggleBtn) {
toggleBtn.setAttribute("data-state", "learning");
toggleBtn.textContent = "停止学习";
}
Learner.startLearning();
setTimeout(() => {
isAutoStarting = false;
console.log("[Init] 🔓 重置防重复标志(超时重置)");
}, 6e4);
}
}
function handleErrorPageNavigation(currentUrl, lastUrl) {
if (currentUrl.includes("study/errors/") && lastUrl.includes("study/course/detail")) {
console.warn("[Init] 检测到跳转到错误页面!");
console.warn("[Init] 可能原因:课程不存在、无访问权限或已被删除");
const uuidMatch = currentUrl.match(/errors\/([a-f0-9-]+)/);
if (uuidMatch) {
const failedUuid = uuidMatch[1];
console.warn(`[Init] 失败课程 UUID: ${failedUuid}`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `❌ 课程加载失败(UUID: ${failedUuid.substring(0, 8)}...),可能无访问权限`,
type: "error"
});
LearningState.markFailed("课程加载失败(跳转到错误页面)");
setTimeout(() => {
if (typeof CbeadHandler !== "undefined" && CbeadHandler.returnToList) {
console.log("[Init] 返回列表页...");
const returnUrl = "#/branch-list-v";
CbeadHandler.returnToList(returnUrl);
}
}, 2e3);
}
return true;
}
return false;
}
function handlePageChange(currentUrl, lastUrl) {
try {
console.log(`[Init] 检测到路由变化:`);
console.log(` - 旧 URL: ${lastUrl}`);
console.log(` - 新 URL: ${currentUrl}`);
handleErrorPageNavigation(currentUrl, lastUrl);
if (currentUrl.includes("study/course/detail") && !lastUrl.includes("study/course/detail")) {
console.log("[Init] 导航到播放页,立即检查批量学习任务...");
checkAndStartAutoLearning();
}
if (currentUrl.includes("branch-list-v") || currentUrl.includes("center/my/course") || currentUrl.includes("class-detail")) {
console.log("[Init] 导航到列表页,触发学习流程...");
setTimeout(() => {
CbeadLearner.selectAndExecute();
}, 2e3);
}
return true;
} catch (error) {
console.error("[Init] 页面变化处理出错:", error);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `路由变化处理失败: ${error.message}`,
type: "error"
});
return false;
}
}
function setupRouteListener() {
if (!CONFIG.CBEAD_MODE) {
console.log("[Init] 非企业分院环境,不设置路由监听");
return;
}
console.log("[Init] 设置路由监听(企业分院 SPA 模式)");
let lastUrl = window.location.href;
window.addEventListener("hashchange", () => {
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
handlePageChange(currentUrl, lastUrl);
lastUrl = currentUrl;
}
});
const urlCheckInterval = setInterval(() => {
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
handlePageChange(currentUrl, lastUrl);
lastUrl = currentUrl;
}
}, 1e3);
window.__celaAutoUrlCheckInterval = urlCheckInterval;
console.log("[Init] 路由监听已启动(hashchange + 轮询)");
}
function setupProgressErrorMonitor() {
console.log("[Init] 📡 设置进度错误监听器(企业分院专用)...");
LearningState.reset();
const skipCurrentCourse = reason => {
if (LearningState.isFailed()) {
console.log("[ProgressError] ⚠️ 跳过已触发,忽略重复调用");
return;
}
LearningState.markFailed(reason);
console.warn(`[ProgressError] 🚨 检测到进度上报失败,标记当前课程为失败: ${reason}`);
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: `❌ 进度上报失败(${reason}),标记当前课程为失败`,
type: "error"
});
EventBus.publish(CONSTANTS.EVENTS.LOG, {
message: "💡 服务器无法记录学习进度,继续播放无意义",
type: "info"
});
EventBus.publish("course:failed", {
reason: `进度上报失败: ${reason}`
});
setTimeout(() => {
if (typeof Learner !== "undefined" && Learner.state === CONSTANTS.LEARNING_STATES.LEARNING) {
console.log("[ProgressError] 🛑 停止当前学习流程...");
Learner.stop();
}
console.log("[ProgressError] ✅ 已标记当前课程为失败");
}, 500);
};
window.celaAutoResetCourseFail = () => {
LearningState.reset();
};
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
this._method = method;
this._url = url;
return originalXHROpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
if (this._url && this._url.includes("update-progress")) {
this.addEventListener("load", function() {
if (this.status === 422) {
console.error("[ProgressError] POST 422:", this._url);
skipCurrentCourse("422 Unprocessable Content");
}
});
}
return originalXHRSend.apply(this, arguments);
};
const originalFetch = window.fetch;
window.fetch = function(url, options) {
return originalFetch.apply(this, arguments).then(response => {
if (typeof url === "string" && url.includes("update-progress") && response.status === 422) {
console.error("[ProgressError] POST 422:", url);
skipCurrentCourse("422 Unprocessable Content");
}
return response;
});
};
console.log("[Init] ✅ 进度错误监听器已启动(422 错误将自动跳过课程)");
}
function muteSingleMedia(media, type) {
media.muted = true;
media.volume = 0;
if (type === "video" && window.player && typeof window.player.muted === "function") {
try {
window.player.muted(true);
} catch (e) {}
}
console.log(`[Init] ${type} 已静音`);
}
function muteSingleVideo(video, index) {
muteSingleMedia(video, "video");
console.log(`[Init] video #${index + 1} 已静音`);
}
function muteSingleAudio(audio, index) {
audio.muted = true;
audio.volume = 0;
audio.pause();
console.log(`[Init] audio #${index + 1} 已静音并暂停`);
}
function muteExistingMedia(selector, type) {
const elements = document.querySelectorAll(selector);
console.log(`[Init] 找到 ${elements.length} 个现有的 ${type} 元素`);
elements.forEach((element, index) => muteSingleMedia(element, type));
}
function startPollingMute() {
let pollCount = 0;
const maxPolls = 100;
const pollIntervalMs = 100;
const pollInterval = setInterval(() => {
pollCount++;
const allVideos = document.querySelectorAll("video");
let hasUnmuted = false;
allVideos.forEach(video => {
if (!video.muted || video.volume !== 0) {
muteSingleVideo(video, 0);
if (!hasUnmuted) {
console.log(`[Init] 轮询发现未静音的 video,立即静音 (第${pollCount}次)`);
hasUnmuted = true;
}
}
});
const allAudios = document.querySelectorAll("audio");
allAudios.forEach(audio => {
if (!audio.muted || audio.volume !== 0) {
muteSingleAudio(audio, 0);
console.log(`[Init] 轮询发现未静音的 audio,立即静音 (第${pollCount}次)`);
}
});
if (pollCount >= maxPolls) {
clearInterval(pollInterval);
console.log("[Init] 高频轮询完成");
}
}, pollIntervalMs);
window.__celaAutoPollInterval = pollInterval;
console.log(`[Init] 高频轮询已启动(每${pollIntervalMs}ms,持续10秒)`);
return pollInterval;
}
function muteNewMediaElement(node) {
if (node.nodeName === "VIDEO") {
console.log("[Init] MutationObserver: 检测到新创建的 video 元素,立即静音并暂停");
muteSingleVideo(node, 0);
node.autoplay = false;
node.pause();
}
if (node.nodeName === "AUDIO") {
console.log("[Init] MutationObserver: 检测到新创建的 audio 元素,立即静音并暂停");
muteSingleAudio(node, 0);
node.autoplay = false;
}
if (node.querySelectorAll) {
node.querySelectorAll("video").forEach(video => {
console.log("[Init] MutationObserver: 检测到新创建的子 video 元素,立即静音并暂停");
muteSingleVideo(video, 0);
video.autoplay = false;
video.pause();
});
node.querySelectorAll("audio").forEach(audio => {
console.log("[Init] MutationObserver: 检测到新创建的子 audio 元素,立即静音并暂停");
muteSingleAudio(audio, 0);
audio.autoplay = false;
});
}
}
function startMuteMutationObserver() {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => muteNewMediaElement(node));
});
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
console.log("[Init] MutationObserver 已启动,将持续监听并静音新媒体元素");
return observer;
}
function clickMaskButtons() {
console.log("[Init] 检查遮罩并点击按钮...");
const maskCheck = detectMask();
if (maskCheck.exists) {
console.log(`[Init] 检测到 ${maskCheck.masks.length} 个遮罩,尝试点击按钮...`);
const clickResult = clickMaskButton();
console.log(`[Init] 已点击 ${clickResult.clicked} 个遮罩按钮`);
}
}
function immediateMuteAllVideos() {
console.log("[Init] 立即静音模式启动...");
muteExistingMedia("video", "video");
muteExistingMedia("audio", "audio");
startPollingMute();
startMuteMutationObserver();
clickMaskButtons();
}
const hasVideoElement = document.querySelector("video") !== null;
const isPlayerPage = window.location.href.includes("study/course/detail") || window.location.href.includes("/pcPage/commend/coursedetail");
if (isPlayerPage && hasVideoElement) {
immediateMuteAllVideos();
}
setTimeout(initScript, 1e3);
window.addEventListener("beforeunload", () => {
if (window.__celaAutoUrlCheckInterval) {
clearInterval(window.__celaAutoUrlCheckInterval);
}
if (window.__celaAutoPollInterval) {
clearInterval(window.__celaAutoPollInterval);
}
});
exports.API = API$1;
exports.ApiFactory = ApiFactory;
exports.CONFIG = CONFIG;
exports.CONSTANTS = CONSTANTS;
exports.CbeadHandler = CbeadHandler;
exports.CbeadLearner = CbeadLearner;
exports.CourseAdapter = CourseAdapter;
exports.EventBus = EventBus;
exports.Learner = Learner;
exports.LearningStrategies = LearningStrategies;
exports.PudongHandler = PudongHandler;
exports.RequestQueue = RequestQueue;
exports.Settings = Settings;
exports.UI = UI;
exports.Utils = Utils;
exports.detectEnvironment = detectEnvironment;
return exports;
})({});