// ==UserScript== // @name cela-auto // @namespace https://github.com/Moker32/ // @version 4.2.1 // @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 // @downloadURL https://update.greasyfork.icu/scripts/563442/cela-auto.user.js // @updateURL https://update.greasyfork.icu/scripts/563442/cela-auto.meta.js // ==/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" }, FLOW_RESULTS: { WAITING_FOR_USER: "WAITING_FOR_USER" }, COMPLETION_THRESHOLD: 80, SKIP_COMPLETED_COURSES: true, API_ENDPOINTS: { GET_PLAY_TREND: "/inc/nc/course/play/getPlayTrend", PULSE_SAVE_RECORD: "/inc/nc/course/play/pulseSaveRecord", GET_STUDY_RECORD: "/inc/nc/course/getStudyRecord", GET_COURSEWARE_DETAIL: "/inc/nc/course/play/getCoursewareDetail", GET_PACK_BY_ID: "/inc/nc/pack/getById", GET_COURSE_LIST: "/api/course/list" }, SELECTORS: { PANEL: "#api-learner-panel", STATUS_DISPLAY: "#learner-status", PROGRESS_INNER: "#learner-progress-inner", TOGGLE_BTN: "#toggle-learning-btn", LOG_CONTAINER: "#api-learner-panel .log-container", STAT_TOTAL: "#stat-total", STAT_COMPLETED: "#stat-completed", STAT_LEARNED: "#stat-learned", STAT_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" ], VIDEO_SELECTORS: [ "video", "[api-base-url]", '[class*="video"]', 'iframe[src*="play"]' ], COOKIE_PATTERNS: { USER_ID: /userId=([^;]+)/, TOKEN: /token=([^;]+)/, P_PARAM: /_p=([^;]+)/ }, TIME_FORMATS: { DEFAULT_DURATION: 1800 }, 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" } } }, SELECTOR_STRATEGY: { PUDONG: { INDEX: { primary: ".dsf_nc_pd_special_item", secondary: ".dsjy_card", tertiary: ".dsf-many-schedule-course-list-row", fallback: '[class*="course"][data-id]', context: "#app", extractors: { courseId: [ "data-id", "id", "data-course-id" ], courseName: [ ".title", ".name", ".item_content", "h3", "h4" ], link: [ "data-url", "href", "[data-link]" ] } }, COLUMN: { specialdetail: { primary: ".dsf_nc_pd_special_item", apiEndpoint: "/inc/nc/course/pd/getPackById" }, specialcolumn: { primary: ".dsjy_card", secondary: ".dsf-many-schedule-course-list-row", apiEndpoint: "/inc/nc/course/pd/getPackById" }, channelDetail: { primary: ".dsf_nc_pd_special_item", apiEndpoint: "/inc/nc/channel/getCourseList" }, pdzq: { primary: ".dsf_nc_pd_special_item", apiEndpoint: "/inc/nc/course/pd/getPackById" }, xisijuan: { primary: ".dsf_nc_pd_special_item", apiEndpoint: "/inc/nc/course/pd/getPackById" } }, PLAYER: { container: "#coursePlayer", video: 'video[api-base-url], iframe[src*="play"]', controls: ".video-controls, .dsf-video-player" } }, GWYPX: { INDEX: { primary: ".course-item", fallback: '[class*="course"]', context: "#app", extractors: { courseId: [ "data-id", "id" ], courseName: [ ".title", "h3" ], link: [ "href" ] } }, PLAYER: { container: ".video-container", video: "video.vjs-tech", controls: ".vjs-control-bar" } }, CBEAD: { INDEX: { primary: ".activity-list .list-item", secondary: ".course-item", fallback: '[class*="course"]', context: "#app", extractors: { courseId: [ "data-id", "id" ], courseName: [ ".title", ".name" ], link: [ "data-url", "href" ] } }, COLUMN: { trainNew: { primary: ".activity-stage ul.list", courseItem: ".activity-stage ul.list li.clearfix", extractors: { titleElement: ".common-title.text-overflow", titleIdPattern: /D75itemDetail1-([a-f0-9-]+)/, studyBtn: ".study-btn", studyBtnIdPattern: /D75itemDetail2-([a-f0-9-]+)/, progressElement: ".completed-rate-bar .bar", progressAttr: "style", progressPattern: /width:\s*(\d+)%/ }, apiEndpoint: "/inc/nc/course/pd/getPackById" } }, PLAYER: { catalog: ".course-side-catalog", chapterList: ".chapter-list-box", extractors: { chapterTitle: ".chapter-item .text-overflow", durationElement: ".section-item .item:last-child", durationPattern: /(\d+):(\d+)/, completedText: ".item-completed", completedPattern: /完成率[::]\s*(\d+)%/ }, videoPlayer: "video.vjs-tech", videoId: "D249player_html5_api", playerType: "videojs" } }, FALLBACK: { INDEX: { primary: ".dsf_nc_pd_special_item", secondary: ".dsjy_card", tertiary: ".dsf-many-schedule-course-list-row", fallback: '[class*="course"][data-id]', context: "#app", extractors: { courseId: [ "data-id", "id", "data-course-id" ], courseName: [ ".title", ".name", ".item_content", "h3", "h4" ], link: [ "data-url", "href", "[data-link]" ] } }, COLUMN: { specialdetail: { primary: ".dsf_nc_pd_special_item", apiEndpoint: "/inc/nc/course/pd/getPackById" }, specialcolumn: { primary: ".dsjy_card", secondary: ".dsf-many-schedule-course-list-row", apiEndpoint: "/inc/nc/course/pd/getPackById" }, channelDetail: { primary: ".dsf_nc_pd_special_item", apiEndpoint: "/inc/nc/channel/getCourseList" }, pdzq: { primary: ".dsf_nc_pd_special_item", apiEndpoint: "/inc/nc/course/pd/getPackById" }, xisijuan: { primary: ".dsf_nc_pd_special_item", apiEndpoint: "/inc/nc/course/pd/getPackById" } }, PLAYER: { container: "#coursePlayer", video: 'video[api-base-url], iframe[src*="play"]', controls: ".video-controls, .dsf-video-player" } } }, PAGE_TYPES: { PORTAL_INDEX: { name: "门户首页", urlPattern: /^https?:\/\/www\.cela\.gov\.cn\/?(#.*)?$/, domPattern: [ "#branchCon", "#courseCon" ], features: [ "traditional", "jquery" ], actions: [ "navigate_to_branch" ] }, PUDONG_INDEX: { name: "浦东首页", urlPattern: /pagehome\/index/, domPattern: [ "#app" ], features: [ "vue", "spa" ], actions: [ "scan_courses" ] }, PUDONG_COLUMN_SPECIALDETAIL: { name: "浦东专题详情页", urlPattern: /specialdetail/, hashPattern: /[?&]id=([^&]+)/, domPattern: [ ".dsf_nc_pd_special_item" ], apiEndpoint: "/inc/nc/course/pd/getPackById", actions: [ "extract_from_api", "extract_from_dom" ] }, PUDONG_COLUMN_SPECIALCOLUMN: { name: "浦东专栏页", urlPattern: /specialcolumn/, domPattern: [ ".dsjy_card" ], apiEndpoint: "/inc/nc/course/pd/getPackById", actions: [ "extract_from_api", "extract_from_dom" ] }, PUDONG_COLUMN_CHANNELDETAIL: { name: "浦东频道详情页", urlPattern: /channelDetail/, domPattern: [ ".dsf_nc_pd_special_item" ], apiEndpoint: "/inc/nc/channel/getCourseList", actions: [ "extract_from_api", "extract_from_dom" ] }, PUDONG_COLUMN_PDZQ: { name: "浦东专区页", urlPattern: /pdzq/, domPattern: [ ".dsf_nc_pd_special_item" ], apiEndpoint: "/inc/nc/course/pd/getPackById", actions: [ "extract_from_api", "extract_from_dom" ] }, PUDONG_COLUMN_XISIJUAN: { name: "浦东西席卷页", urlPattern: /xisijuan/, domPattern: [ ".dsf_nc_pd_special_item" ], apiEndpoint: "/inc/nc/course/pd/getPackById", actions: [ "extract_from_api", "extract_from_dom" ] }, PUDONG_PLAYER: { name: "浦东播放页", urlPattern: /coursePlayer/, domPattern: [ "#coursePlayer", "video[api-base-url]" ], actions: [ "report_progress" ] }, GWYPX_INDEX: { name: "党校首页", urlPattern: /pcPage\/index/, domPattern: [ "#app" ], features: [ "vue", "spa" ], actions: [ "scan_courses" ] }, GWYPX_PLAYER: { name: "党校播放页", urlPattern: /pcPage\/commend\/coursedetail/, domPattern: [ "video.vjs-tech" ], actions: [ "report_progress" ] }, CBEAD_INDEX: { name: "企业首页", urlPattern: /class\/index/, domPattern: [ "#app", ".activity-list" ], features: [ "vue", "spa" ], actions: [ "scan_courses" ] }, CBEAD_COLUMN_TRAIN_NEW: { name: "企业专题详情页", urlPattern: /train-new\/class-detail/, hashPattern: /class-detail\/([a-f0-9-]+)/, domPattern: [ ".activity-stage", ".list" ], apiEndpoint: "/inc/nc/course/pd/getPackById", actions: [ "extract_from_api", "extract_from_dom" ] }, CBEAD_PLAYER: { name: "企业播放页", urlPattern: /study\/course\/detail/, domPattern: [ ".player-content", ".new-global-height", "video" ], actions: [ "report_progress" ] }, UNKNOWN: { name: "未知页面", actions: [ "detect_features" ] } }, API_CONFIG: { PORTAL: { baseUrl: "https://www.cela.gov.cn", version: "v1", endpoints: {}, supported: false }, PUDONG: { baseUrl: "auto", version: "v1", endpoints: { GET_PLAY_TREND: "/inc/nc/course/play/getPlayTrend", GET_STUDY_RECORD: "/inc/nc/course/study/getStudyRecord", GET_COURSE_LIST: "/inc/nc/course/getCourseList", GET_PACK_BY_ID: "/inc/nc/course/pd/getPackById", GET_CHANNEL_INFO: "/inc/nc/channel/getChannelInfo", PULSE_SAVE_RECORD: "/api/player/pulse/saveRecord", REPORT_PROGRESS: "/inc/nc/course/play/reportProgress", GET_COURSE_LIST_FROM_CHANNEL: "/inc/nc/channel/getCourseList" }, variants: { main: { host: "cela.e-celap.cn", endpoints: {} }, pudong: { host: "pudong.e-celap.cn", endpoints: {} } }, fallback: { GET_COURSE_LIST: [ "/inc/nc/course/pd/getPackById", "/inc/nc/channel/getCourseList", "/api/course/list" ], PULSE_SAVE_RECORD: [ "/api/player/pulse/saveRecord", "/inc/nc/course/play/pulseSaveRecord", "/api/player/pulse" ] }, requestConfig: { timeout: 1e4, retries: 3, retryDelay: 1e3 } }, GWYPX: { baseUrl: "https://cela.gwypx.com.cn", version: "v1", endpoints: { GET_COURSE_LIST: "/api/course/list", PULSE_SAVE_RECORD: "/api/player/pulse/saveRecord" }, supported: true, fallback: { PULSE_SAVE_RECORD: [ "/inc/nc/course/play/pulseSaveRecord" ] } }, CBEAD: { baseUrl: "https://cela.cbead.cn", version: "v1", endpoints: { GET_PLAY_TREND: "/inc/nc/course/play/getPlayTrend", GET_STUDY_RECORD: "/inc/nc/course/study/getStudyRecord", GET_COURSE_LIST: "/inc/nc/course/getCourseList", PULSE_SAVE_RECORD: "/inc/nc/course/play/pulseSaveRecord", GET_COURSEWARE_DETAIL: "/inc/nc/course/play/getCoursewareDetail", GET_PACK_BY_ID: "/inc/nc/course/pd/getPackById" }, supported: true, fallback: { PULSE_SAVE_RECORD: [ "/inc/nc/course/play/pulseSaveRecord", "/api/player/pulse/saveRecord" ] }, requestConfig: { timeout: 1e4, retries: 3, retryDelay: 1e3 } } }, PAGE_TYPE_WHITELIST: { PUDONG: [ "PLAYER", "COLUMN", "INDEX" ], CBEAD: [ "PLAYER", "COLUMN", "BRANCH_LIST" ], GWYPX: [ "PLAYER", "CENTER" ] } }; 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 = { 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; } }); var infraConfig = Object.freeze({ __proto__: null, CONFIG: CONFIG, Settings: Settings }); 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", ".vjs-play-control", ".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; const observer = new MutationObserver(mutations => { if (isClicking) return; let shouldCheck = false; for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { shouldCheck = true; break; } } if (shouldCheck) { isClicking = true; setTimeout(() => { const mask = detectMask(); if (mask.exists) { clickMaskButton(); } setTimeout(() => { isClicking = false; }, 1e3); }, 300); } }); observer.observe(document.body, { childList: true, subtree: true }); console.log("[DOMHelper] 🛡️ 全局遮罩监听器已启动"); return observer; } var infraDomHelper = 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) { if (urlPath.startsWith("#")) { const hashPath = urlPath.substring(1); if (hashPath === pattern || hashPath.startsWith(pattern + "/")) { return true; } const hashParts = hashPath.split("/").filter(Boolean); const patternParts = pattern.split("/").filter(Boolean); if (patternParts.length === 1 && !pattern.includes("/")) { if (hashParts.length >= 1) { const hashRouteName = hashParts[0]; if (hashRouteName === pattern || hashRouteName.startsWith(pattern + "?")) { return true; } } } else if (patternParts.length > 0 && hashParts.length >= patternParts.length) { const lastPart = hashParts[patternParts.length - 1]; const patternLastPart = patternParts[patternParts.length - 1]; if (lastPart === patternLastPart || lastPart.startsWith(patternLastPart + "?")) { return true; } } return false; } if (urlPath === pattern) { return true; } if (urlPath.endsWith(pattern)) { return true; } if (urlPath.startsWith(pattern + "/")) { return true; } const urlParts = urlPath.split("/").filter(Boolean); const patternParts = pattern.split("/").filter(Boolean); if (patternParts.length > 0 && urlParts.length >= patternParts.length) { const lastPart = urlParts[urlParts.length - 1]; const patternLastPart = patternParts[patternParts.length - 1]; if (lastPart === patternLastPart || lastPart.startsWith(patternLastPart + "?")) { return true; } } return false; } 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 (e) { 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 PUDONG_CONSTANTS = { PATH_PATTERNS: { INDEX: "/pc/nc/pagehome/index", COLUMN: [ "zgpdyxkc", "specialcolumn", "specialdetail", "channelDetail", "pdchanel/pdzq" ], PLAYER: "coursePlayer" }, PAGE_TYPES: { INDEX: "index", COLUMN: "column", PLAYER: "player", UNKNOWN: "unknown" }, SELECTORS: { COURSE_ITEMS: [ ".dsf_nc_zg_item", ".dsf_nc_pd_course_express_item", ".dsf-many-schedule-course-list-row", ".dsf_nc_pd_special_item", ".pd_course_item", ".dsjy_card" ], ENTER_BTN: ".course-enter-btn", PLAYER_CONTAINER: "#coursePlayer" } }; 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, 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 }); }, _extractToken() { if (this._cachedToken) 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._cachedToken = token; this._log(`找到认证token: ${token.substring(0, 20)}...`, "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; } }; 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$2(...args) {} const PUDONG_API_CONFIG = { baseUrl: null, endpoints: { GET_PLAY_TREND: "/inc/nc/course/play/getPlayTrend", GET_COURSE_LIST: "/inc/nc/course/list", PULSE_SAVE_RECORD: "/inc/nc/course/play/pulseSaveRecord", PULSE_SAVE_RECORD_ALT: "/api/player/pulse", GET_STUDY_RECORD: "/inc/nc/course/play/getStudyRecord", GET_COURSEWARE_LIST: "/inc/nc/course/play/getPlayTrend", GET_COURSEWARE_LIST_ALT: "/inc/nc/course/play/getPlayInfoById" }, 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$1(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); }, async getPlayInfo(courseId, coursewareId = null, courseDuration = null) { const {Utils: Utils} = await Promise.resolve().then(function() { return utils; }); try { debugLog$2(`[getPlayInfo] 获取课程 ${courseId} 的播放信息`); const response = await this.get(_buildUrl$1("GET_PLAY_TREND", { courseId: courseId })); if (response?.success && response?.data) { const data = response.data; let videoId = null; let duration = 0; let lastLearnedTime = 0; let foundCoursewareId = coursewareId; if (coursewareId && 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$2(`成功匹配到课件: ${target.title}`); } } if (!videoId && data.locationSite) { 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$2("无法获取真实 videoId,使用模拟ID"); } 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" }; } return null; } catch (error) { debugLog$2(`[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$1("PULSE_SAVE_RECORD"), payload, { headers: { "Content-Type": "application/x-www-form-urlencoded" } }); } catch (error) { return await this.post(_buildUrl$1("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$1("GET_PLAY_TREND", { courseId: courseId })); const config = ServiceLocator.get(ServiceNames.CONFIG); if (response?.success && response?.data) { const data = response.data; if (coursewareId && data.playTree?.children) { const target = data.playTree.children.find(c => String(c.id) === String(coursewareId)); if (target) { const finishedRate = parseInt(target.finishedRate || 0); return { isCompleted: finishedRate >= (config?.COMPLETION_THRESHOLD || 80), finishedRate: finishedRate, 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$2(`完成度检查失败: ${error.message}`); return { isCompleted: false, finishedRate: 0, method: "error" }; } }, async getCoursewareList(courseId) { try { debugLog$2(`正在获取课程包详细信息 (ID: ${courseId})...`); const endpoints = [ _buildUrl$1("GET_COURSEWARE_LIST", { courseId: courseId }), _buildUrl$1("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$2(`从 playTree 获取到 ${videos.length} 个课件`); return videos.map((v, index) => CourseAdapter.normalize({ id: courseId, courseId: courseId, dsUnitId: v.id, title: v.title || `${data.title || "课程"} - 视频${index + 1}`, duration: v.sumDurationLong || 0 }, "pudong_api_tree")); } } if (data.coursewareIdList && Array.isArray(data.coursewareIdList) && data.coursewareIdList.length > 0) { debugLog$2(`从 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$2(`从 API 子列表获取到 ${list.length} 个视频`); return list.map(item => CourseAdapter.normalize(item, "pudong_api_sublist")); } } } catch { continue; } } return []; } catch (error) { debugLog$2(`获取课件列表失败: ${error.message}`); return []; } }, async getCourseList() { try { debugLog$2("正在获取课程列表..."); const currentUrl = window.location.href; if (currentUrl.toLowerCase().includes("specialdetail") || currentUrl.toLowerCase().includes("channeldetail") || currentUrl.toLowerCase().includes("pdchanel")) { debugLog$2("检测到专题详情页,尝试从API获取课程列表..."); 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 (error) { debugLog$2(`解析频道ID失败: ${error.message}`); } if (channelId) { debugLog$2(`专题ID: ${channelId}`); return await this.getCourseListFromChannel(channelId); } } debugLog$2("正在扫描页面课程元素..."); for (let i = 0; i < 10; i++) { const hasContent = document.querySelectorAll('.dsf_nc_pd_special_item, .list_item, .pd_course_item, .dsjy_card, .el-card, [class*="course"]').length > 0; if (hasContent) break; await new Promise(r => setTimeout(r, 500)); } const courseList = []; let courseElements = []; const pudongItems = document.querySelectorAll(".dsf_nc_pd_special_item, .list_item, .pd_course_item, .dsjy_card"); if (pudongItems.length > 0) { debugLog$2(`找到浦东分院专用列表项: ${pudongItems.length}个`); courseElements = Array.from(pudongItems); } else { const selectors = [ ".dsf_nc_zg_item", ".dsf-many-schedule-course-list-row", ".dsf_nc_pd_course_express_item", ".el-card" ]; for (const selector of selectors) { const elements = document.querySelectorAll(selector); const validElements = Array.from(elements).filter(el => !el.closest("#api-learner-panel")); if (validElements.length > 0) { courseElements = validElements; debugLog$2(`使用选择器 "${selector}" 找到 ${validElements.length} 个课程元素`); break; } } } courseElements.forEach((el, index) => { const findId = element => { let current = element; let depth = 0; while (current && depth < 5) { const id = current.getAttribute("data-id") || current.getAttribute("data-course-id") || current.getAttribute("id") || current.getAttribute("data-courseid") || current.querySelector("[data-id]")?.getAttribute("data-id") || current.querySelector("[data-course-id]")?.getAttribute("data-course-id"); if (id && !id.includes("kapture") && !id.includes("course_") && id.length > 5) return id; current = current.parentElement; depth++; } const uuidMatch = (element.getAttribute("onclick") || element.parentElement?.innerHTML || "").match(/[a-f0-9]{32}/); return uuidMatch ? uuidMatch[0] : null; }; const courseId = findId(el); if (!courseId) return; const rawData = { courseId: courseId, dsUnitId: el.getAttribute("data-unit-id") || el.getAttribute("data-dsunit") || `unit_${index}`, courseName: el.querySelector(".title, .name, .course-title, .item_content, h3, h4")?.textContent?.trim() || el.getAttribute("title") || el.textContent?.trim()?.split("\n")[0]?.substring(0, 80) || `课程${index + 1}`, durationStr: el.querySelector(".duration, .time, .period")?.textContent?.trim() || "00:30:00", status: el.getAttribute("data-status") || "not_started" }; if (rawData.courseName && rawData.courseName.length > 2) { courseList.push(CourseAdapter.normalize(rawData, "pudong_dom")); } }); debugLog$2(`解析得到 ${courseList.length} 门课程`); return courseList; } catch (error) { debugLog$2(`获取课程列表失败: ${error.message}`); return []; } }, async getCourseListFromChannel(channelId) { try { debugLog$2(`正在从频道获取课程列表 (ID: ${channelId})...`); const apiEndpoints = [ `/inc/nc/pack/getById?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$2(`尝试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 (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$2(`从 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$2(`从 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$2(`从API获取到 ${courseList.length} 门课程`); return courseList; } } } catch (error) { debugLog$2(`API端点 ${endpoint} 失败: ${error.message}`); continue; } } debugLog$2("所有频道API端点都失败了"); return []; } catch (error) { debugLog$2(`从频道获取课程列表失败: ${error.message}`); return []; } } }; function debugLog$1(...args) {} const PudongScanner = { async scanCourses(selectors = PUDONG_CONSTANTS.SELECTORS) { try { const currentUrl = window.location.href; if (currentUrl.toLowerCase().includes("channeldetail") || currentUrl.toLowerCase().includes("specialdetail") || currentUrl.toLowerCase().includes("pdchanel")) { debugLog$1("检测到专题详情页,调用 API 获取课程列表..."); const apiCourses = await PudongApi.getCourseList(); if (apiCourses && apiCourses.length > 0) { debugLog$1(`API 返回 ${apiCourses.length} 门课程`); return apiCourses; } debugLog$1("API 未返回课程,降级到 DOM 扫描"); } await this._waitForCourseElements(); const courseElements = this._findCourseElements(selectors); if (courseElements.length === 0) { debugLog$1("未找到课程元素"); return []; } debugLog$1(`找到 ${courseElements.length} 个课程元素`); const courses = this._parseCourseElements(courseElements); const uniqueCourses = this._uniqueCourses(courses); debugLog$1(`解析得到 ${uniqueCourses.length} 门有效课程`); return uniqueCourses; } catch (error) { console.error(`[PudongScanner] 扫描失败: ${error.message}`); return []; } }, async _waitForCourseElements() { for (let i = 0; i < 10; i++) { const found = PUDONG_CONSTANTS.SELECTORS.COURSE_ITEMS.some(s => document.querySelector(s)); if (found) break; await new Promise(r => setTimeout(r, 500)); } }, _findCourseElements(selectors) { const pudongItems = document.querySelectorAll(".dsf_nc_pd_special_item, .list_item, .pd_course_item, .dsjy_card"); if (pudongItems.length > 0) { return Array.from(pudongItems); } for (const selector of selectors.COURSE_ITEMS) { const elements = document.querySelectorAll(selector); const validElements = Array.from(elements).filter(el => !el.closest("#api-learner-panel")); if (validElements.length > 0) { return validElements; } } return []; }, _parseCourseElements(elements) { const courseList = []; elements.forEach((el, index) => { const courseId = this._extractCourseId(el); if (!courseId) return; const rawData = { courseId: courseId, dsUnitId: el.getAttribute("data-unit-id") || el.getAttribute("data-dsunit") || `unit_${index}`, courseName: this._extractCourseName(el), durationStr: this._extractDuration(el), status: el.getAttribute("data-status") || "not_started" }; if (rawData.courseName && rawData.courseName.length > 2) { courseList.push(CourseAdapter.normalize(rawData, "pudong_dom")); } }); return courseList; }, _extractCourseId(element) { let current = element; let depth = 0; while (current && depth < 5) { const id = current.getAttribute("data-id") || current.getAttribute("data-course-id") || current.getAttribute("id") || current.getAttribute("data-courseid") || current.querySelector("[data-id]")?.getAttribute("data-id") || current.querySelector("[data-course-id]")?.getAttribute("data-course-id"); if (id && !id.includes("kapture") && !id.includes("course_") && id.length > 5) { return id; } current = current.parentElement; depth++; } const uuidMatch = (element.getAttribute("onclick") || element.parentElement?.innerHTML || "").match(/[a-f0-9]{32}/); return uuidMatch ? uuidMatch[0] : null; }, _extractCourseName(element) { return element.querySelector(".title, .name, .course-title, .item_content, h3, h4")?.textContent?.trim() || element.getAttribute("title") || element.textContent?.trim()?.split("\n")[0]?.substring(0, 80) || `课程${Date.now()}`; }, _extractDuration(element) { return element.querySelector(".duration, .time, .period")?.textContent?.trim() || "00:30:00"; }, _uniqueCourses(courses) { return courses.filter((course, index, self) => index === self.findIndex(c => c.courseId === course.courseId)); }, getEnterButtonSelector() { return PUDONG_CONSTANTS.SELECTORS.ENTER_BTN; }, getPlayerContainerSelector() { return PUDONG_CONSTANTS.SELECTORS.PLAYER_CONTAINER; } }; 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, 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; }, async executeCourseLearning(course) { const {Utils: Utils} = await Promise.resolve().then(function() { return utils; }); const {LearningStrategies: LearningStrategies} = await Promise.resolve().then(function() { return bizStrategies; }); const courseId = course.id || course.courseId; const dsUnitId = course.dsUnitId; const playInfo = await PudongApi.getPlayInfo(courseId, dsUnitId, course.durationStr); if (!playInfo) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `无法获取课程 ${courseId} 的播放信息`, type: "error" }); return false; } const courseInfo = { ...course, ...playInfo, title: course.title || course.courseName, courseId: courseId }; const currentProgress = Math.floor(playInfo.lastLearnedTime / playInfo.duration * 100); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `[学习启动] 课程: ${courseInfo.title}`, type: "info" }); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `[当前进度] ${currentProgress}% (${Utils.formatTime(playInfo.lastLearnedTime)}/${Utils.formatTime(playInfo.duration)})`, type: "info" }); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "执行极速完成策略 - 直接冲刺 99%", type: "info" }); const success = await LearningStrategies.instant_finish({ playInfo: courseInfo, duration: playInfo.duration, currentTime: playInfo.lastLearnedTime }); if (success) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `课程处理完成: ${courseInfo.title}`, type: "success" }); } else { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `课程处理失败: ${courseInfo.title}`, type: "error" }); } return success; }, async checkCourseCompletion(courseId, coursewareId = null) { return await PudongApi.checkCompletion(courseId, coursewareId); } }; 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 PudongLearner$1 = { get _pageValidator() { return createPageValidator({ whitelist: CONSTANTS.PAGE_TYPE_WHITELIST.PUDONG, pageTypes: PudongHandler.PAGE_TYPES, customMessages: { default: "⚠️ 当前页面不支持自动学习。请进入课程播放页或列表页。" } }); }, _validatePageType(pageType) { return this._pageValidator.validate(pageType); }, async selectAndExecute() { if (!CONFIG.PUDONG_MODE) { return null; } const pageType = PudongHandler.identifyPage(); if (!this._validatePageType(pageType)) { return false; } if (pageType === PudongHandler.PAGE_TYPES.PLAYER) { return await this._handlePlayerPage(); } if (pageType === PudongHandler.PAGE_TYPES.COLUMN || pageType === PudongHandler.PAGE_TYPES.INDEX) { return await this._handleColumnPage(); } return null; }, async _handlePlayerPage() { return await this.startPlayerFlow(); }, async _handleColumnPage() { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "📋 检测到浦东分院专栏页", type: "info" }); const courses = await PudongHandler.scanCourses(); if (!courses || courses.length === 0) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "⚠️ 未扫描到课程列表", type: "warn" }); return false; } EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `✅ 扫描到 ${courses.length} 门课程`, type: "success" }); const stats = { total: courses.length, completed: 0, learned: 0, failed: 0, skipped: 0 }; EventBus.publish(CONSTANTS.EVENTS.STATISTICS_UPDATE, stats); for (let i = 0; i < courses.length; i++) { const {Learner: Learner} = await Promise.resolve().then(function() { return bizLearner; }); if (Learner && Learner.stopRequested) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "\n🛑 用户停止学习", type: "warn" }); break; } const course = courses[i]; const courseId = course.id || course.courseId; const coursewareId = course.dsUnitId; EventBus.publish(CONSTANTS.EVENTS.COURSE_START, { course: course, index: i + 1, total: courses.length }); EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, `处理课程 ${i + 1}/${courses.length}`); try { const completionCheck = await PudongHandler.checkCompletion(courseId, coursewareId); if (completionCheck.isCompleted) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `⏭️ 跳过已完成课程: ${course.title} (${completionCheck.finishedRate}%)`, type: "info" }); stats.skipped++; } else { const success = await PudongPlayerFlow.executeCourseLearning(course); if (success) { stats.learned++; } else { stats.failed++; } } stats.completed = stats.skipped + stats.learned; EventBus.publish(CONSTANTS.EVENTS.STATISTICS_UPDATE, stats); EventBus.publish(CONSTANTS.EVENTS.PROGRESS_UPDATE, Math.floor((i + 1) / courses.length * 100)); if (stats.learned > 0 && i < courses.length - 1) { await this._coolingDown(i === courses.length - 1); } } catch (error) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `❌ 处理课程失败: ${course.title} - ${error.message}`, type: "error" }); stats.failed++; EventBus.publish(CONSTANTS.EVENTS.STATISTICS_UPDATE, stats); } } EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `\n🎉 所有课程处理完成!`, type: "success" }); EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, "学习完成"); this._resetToggleButton("学习完成"); return true; }, async startPlayerFlow() { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "🎬 检测到浦东分院播放页,开始处理", type: "info" }); const courses = await PudongPlayerFlow.startPlayerFlow(); if (!courses || courses.length === 0) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "❌ 未找到可学习的课件", type: "error" }); return false; } const successCount = []; const failCount = []; for (const course of courses) { try { const success = await PudongPlayerFlow.executeCourseLearning(course); if (success) { successCount.push(course); } else { failCount.push(course); } } catch (error) { console.error("[PudongLearner] 课件学习失败:", error); failCount.push(course); } } if (successCount.length > 0) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `✅ 成功完成 ${successCount.length} 个课件`, type: "success" }); } if (failCount.length > 0) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `⚠️ ${failCount.length} 个课件处理失败`, type: "warn" }); } this._resetToggleButton("学习完成"); return successCount.length > 0; }, async _coolingDown(isLast) { if (isLast) return; const minDelay = 5e3; const maxDelay = 1e4; const delay = Math.random() * (maxDelay - minDelay) + minDelay; const seconds = Math.round(delay / 1e3); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `冷却等待: ${seconds}秒`, type: "info" }); for (let i = seconds; i > 0; i--) { EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, `等待中 (${i}s)`); await new Promise(r => setTimeout(r, 1e3)); } }, _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); } }; const PudongHandler = { PAGE_TYPES: PUDONG_CONSTANTS.PAGE_TYPES, SELECTORS: PUDONG_CONSTANTS.SELECTORS, identifyPage: createPageDetector({ pathPatterns: PUDONG_CONSTANTS.PATH_PATTERNS, pageTypes: { ...PUDONG_CONSTANTS.PAGE_TYPES, UNKNOWN: "unknown" }, domSelectors: [ "video[api-base-url]", ".dsf_course_player", ".course-player", ".pd_course_pla" ], domMatchType: "PLAYER" }), init() { if (!this.isPudongMode()) return; console.log("浦东分院处理器已激活"); EventBus.subscribe("pudong:startLearning", async () => { console.log("[PudongHandler] 收到开始学习事件"); const pageType = this.identifyPage(); const url = window.location.href; if (pageType === this.PAGE_TYPES.PLAYER) { await PudongLearner$1._handlePlayerPage(); } else if (pageType === this.PAGE_TYPES.COLUMN) { if (url.includes("/specialcolumn")) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "⚠️ 当前页面为专题入口页,请进入具体的专题详情页后再开始学习", type: "warn" }); EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, "页面不支持"); this._resetToggleButton(); } else if (url.includes("/pdchanel/pdzq")) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "⚠️ 当前页面为干部履职通识课程专区,请进入具体的课程后再开始学习", type: "warn" }); EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, "页面不支持"); this._resetToggleButton(); } else { await PudongLearner$1._handleColumnPage(); } } else if (pageType === this.PAGE_TYPES.INDEX) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "⚠️ 首页暂不支持自动学习,请进入专栏或课程页面", type: "warn" }); this._resetToggleButton(); } else { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "⚠️ 当前页面不支持自动学习", type: "warn" }); EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, "页面不支持"); this._resetToggleButton(); } }); }, _resetToggleButton() { const toggleBtn = document.getElementById(CONSTANTS.SELECTORS.TOGGLE_BTN.replace("#", "")); if (toggleBtn) { toggleBtn.setAttribute("data-state", "stopped"); toggleBtn.textContent = "开始学习"; } }, isPudongMode() { return CONFIG.PUDONG_MODE === true; }, async scanCourses() { return await PudongScanner.scanCourses(); }, async startPlayerFlow() { return await PudongPlayerFlow.startPlayerFlow(); }, async executeCourseLearning(course) { return await PudongPlayerFlow.executeCourseLearning(course); }, async checkCompletion(courseId, coursewareId = null) { return await PudongPlayerFlow.checkCourseCompletion(courseId, coursewareId); }, async getPlayInfo(courseId, coursewareId = null, duration = null) { return await PudongApi.getPlayInfo(courseId, coursewareId, duration); }, async reportProgress(playInfo, currentTime) { return await PudongApi.reportProgress(playInfo, currentTime); }, getEnterButtonSelector() { return PudongScanner.getEnterButtonSelector(); }, getPlayerContainerSelector() { return PudongScanner.getPlayerContainerSelector(); } }; const CBEAD_CONSTANTS = { 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" }, PATH_PATTERNS: { PLAYER: "study/course/detail", COLUMN: "train-new/class-detail", BRANCH_LIST: "branch-list-v", INDEX: "class/index", HOME_V: "home-v" }, 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_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/audience/course/list", MY_COURSE_LIST: "/api/v1/audience/course/my-course-list", STUDY_PROGRESS: "/api/v1/audience/course/study-progress" }, VALIDATION: { PAGE_LOAD_TIMEOUT: 1e4, SKELETON_TIMEOUT: 5e3, CONTENT_TIMEOUT: 8e3, VUE_TIMEOUT: 6e3, MAX_RETRY_COUNT: 3 } }, COLUMN_PAGE: { ACTIVITY_STYLE: { CONTAINER_SELECTOR: ".activity-page", ITEM_SELECTOR: "li.clearfix", TITLE_SELECTOR: ".common-title", PROGRESS_BAR_SELECTOR: ".completed-rate-bar .bar", STUDY_BTN_SELECTOR: ".study-btn", COMPLETED_STATUS_TEXT: "已完成", IN_PROGRESS_STATUS_TEXT: "学习中", NOT_STARTED_STATUS_TEXT: "未开始" }, LAYOUT_STYLE: { CONTAINER_SELECTOR: ".class-layout", LIST_SELECTOR: ".item-content ul.clearfix", ITEM_SELECTOR: "li.pull-left", TITLE_SELECTOR: ".title-text", TITLE_ID_PREFIX: "D74itemDetail-", STATUS_SELECTOR: ".ms-train-state", STATUS_TEXT: { COMPLETED: "已完成", UNFINISHED: "未完成", IN_PROGRESS: "学习中" }, TAB_SWITCH: { CONTAINER_SELECTOR: ".item-switch-list", TAB_ITEM_SELECTOR: "ul.clearfix > li", ACTIVE_CLASS: "current", SWITCH_DELAY: 1500 } } }, HEARTBEAT: { INTERVAL: 1e4, ENABLED: true, MAX_RETRIES: 3, TIMEOUT: 5e3, API_PENDING: true } }; Object.defineProperty(CBEAD_CONSTANTS.COLUMN_PAGE, "CONTAINER_SELECTOR", { get() { return this.ACTIVITY_STYLE.CONTAINER_SELECTOR; }, enumerable: true, configurable: true }); Object.defineProperty(CBEAD_CONSTANTS.COLUMN_PAGE, "ITEM_SELECTOR", { get() { return this.ACTIVITY_STYLE.ITEM_SELECTOR; }, enumerable: true, configurable: true }); Object.defineProperty(CBEAD_CONSTANTS.COLUMN_PAGE, "PROGRESS_BAR_SELECTOR", { get() { return this.ACTIVITY_STYLE.PROGRESS_BAR_SELECTOR; }, enumerable: true, configurable: true }); const CourseStatusDetector = { STATUS: { COMPLETED: "completed", IN_PROGRESS: "in_progress", NOT_STARTED: "not_started" }, STATUS_TEXT_MAP: { "学习中": "in_progress", "已完成": "completed", "未开始": "not_started" }, detectCourseStatus(courseItem) { if (!courseItem || typeof courseItem.querySelector !== "function") { return this._createStatusResult(null, 0, "无效的课程元素"); } const statusFromVue = this._detectFromVueData(courseItem); const statusFromText = this._detectFromStatusElement(courseItem); const statusFromProgressBar = this._detectFromProgressBar(courseItem); const statusFromClass = this._detectFromClass(courseItem); const statusFromDataAttr = this._detectFromDataAttr(courseItem); const results = [ statusFromVue, statusFromText, statusFromProgressBar, statusFromClass, statusFromDataAttr ]; const validResults = results.filter(r => r !== null); const statusCounts = {}; validResults.forEach(r => { statusCounts[r] = (statusCounts[r] || 0) + 1; }); let finalStatus = null; let maxCount = 0; for (const [status, count] of Object.entries(statusCounts)) { if (count > maxCount) { maxCount = count; finalStatus = status; } } const confidence = this._calculateConfidence(statusFromVue, statusFromText, statusFromProgressBar, statusFromClass, statusFromDataAttr); const title = this._extractTitle(courseItem); const courseId = this._extractCourseId(courseItem); if (finalStatus) { console.log(`[CourseStatusDetector] ${title} - 状态: ${finalStatus}, 置信度: ${confidence}%, 来源: ${validResults.join(", ")}`); } return this._createStatusResult(finalStatus, confidence, { vue: statusFromVue, text: statusFromText, progressBar: statusFromProgressBar, class: statusFromClass, dataAttr: statusFromDataAttr }, { title: title, courseId: courseId }); }, detectBatch(courseItems) { if (!Array.isArray(courseItems)) { console.warn("[CourseStatusDetector] 批量检测收到非数组参数"); return []; } return courseItems.map((item, index) => { const statusInfo = this.detectCourseStatus(item); return { index: index, element: item, ...statusInfo }; }); }, _detectFromVueData(courseItem) { const config = CBEAD_CONSTANTS.BRANCH_LIST; const cardItem = courseItem.querySelector(config.CARD_SELECTOR); if (!cardItem || !cardItem.__vue__) { return null; } const vueInstance = cardItem.__vue__; const data = vueInstance.$data || vueInstance._data || {}; if (data.studyStatus !== undefined) { if (data.studyStatus === 1 || data.studyStatus === "studying") { return this.STATUS.IN_PROGRESS; } else if (data.studyStatus === 2 || data.studyStatus === "completed") { return this.STATUS.COMPLETED; } else if (data.studyStatus === 0 || data.studyStatus === "not_started") { return this.STATUS.NOT_STARTED; } } const progress = data.studyProgress ?? data.progress ?? data.percentage ?? data.studyPercentage; if (progress !== undefined) { if (progress >= 100) { return this.STATUS.COMPLETED; } else if (progress > 0) { return this.STATUS.IN_PROGRESS; } else { return this.STATUS.NOT_STARTED; } } if (data.isCompleted === true || data.isCompleted === "true") { return this.STATUS.COMPLETED; } if (data.completedTime || data.finishTime || data.studyEndTime) { return this.STATUS.COMPLETED; } return null; }, _detectFromStatusElement(courseItem) { const config = CBEAD_CONSTANTS.BRANCH_LIST; const statusEl = courseItem.querySelector(config.STATUS_SELECTOR); if (!statusEl) { return null; } const statusText = statusEl.textContent?.trim() || ""; if (statusText === "学习中") { return this.STATUS.IN_PROGRESS; } else if (statusText === "已完成") { return this.STATUS.COMPLETED; } else if (statusText === "未开始") { return this.STATUS.NOT_STARTED; } const hiddenStatus = statusEl.getAttribute("data-status") || statusEl.getAttribute("data-study-status"); if (hiddenStatus) { if (hiddenStatus === "studying" || hiddenStatus === "1") { return this.STATUS.IN_PROGRESS; } else if (hiddenStatus === "completed" || hiddenStatus === "2") { return this.STATUS.COMPLETED; } else if (hiddenStatus === "not_started" || hiddenStatus === "0") { return this.STATUS.NOT_STARTED; } } return null; }, _detectFromProgressBar(courseItem) { const progressBar = courseItem.querySelector('.progress-bar, [class*="progress"], .completed-rate-bar, .rate-bar'); if (!progressBar) { return null; } const style = progressBar.getAttribute("style") || ""; const progressMatch = style.match(/width:\s*(\d+)%/); if (progressMatch) { const progress = parseInt(progressMatch[1], 10); if (progress >= 100) { return this.STATUS.COMPLETED; } else if (progress > 0) { return this.STATUS.IN_PROGRESS; } } const progressChild = progressBar.querySelector('.progress, .bar, .completed, [class*="completed"]'); if (progressChild) { const childStyle = progressChild.getAttribute("style") || ""; const childProgressMatch = childStyle.match(/width:\s*(\d+)%/); if (childProgressMatch) { const progress = parseInt(childProgressMatch[1], 10); if (progress >= 100) { return this.STATUS.COMPLETED; } else if (progress > 0) { return this.STATUS.IN_PROGRESS; } } } const progressText = progressBar.textContent || ""; if (progressText.includes("100%") || progressText.includes("已完成")) { return this.STATUS.COMPLETED; } return null; }, _detectFromClass(courseItem) { const classList = courseItem.classList; if (classList.contains("is-completed") || classList.contains("completed") || classList.contains("study-completed") || classList.contains("finished")) { return this.STATUS.COMPLETED; } if (classList.contains("is-learning") || classList.contains("learning") || classList.contains("study-in-progress") || classList.contains("in-study")) { return this.STATUS.IN_PROGRESS; } if (classList.contains("is-not-started") || classList.contains("not-started") || classList.contains("unstarted")) { return this.STATUS.NOT_STARTED; } return null; }, _detectFromDataAttr(courseItem) { const dataStatus = courseItem.getAttribute("data-status") || courseItem.getAttribute("data-study-status") || courseItem.getAttribute("data-progress-status"); if (dataStatus) { if (dataStatus === "completed" || dataStatus === "2" || dataStatus === "true") { return this.STATUS.COMPLETED; } else if (dataStatus === "studying" || dataStatus === "learning" || dataStatus === "1") { return this.STATUS.IN_PROGRESS; } else if (dataStatus === "not_started" || dataStatus === "0" || dataStatus === "false") { return this.STATUS.NOT_STARTED; } } const dataProgress = courseItem.getAttribute("data-progress"); if (dataProgress !== null) { const progress = parseInt(dataProgress, 10); if (!isNaN(progress)) { if (progress >= 100) { return this.STATUS.COMPLETED; } else if (progress > 0) { return this.STATUS.IN_PROGRESS; } else { return this.STATUS.NOT_STARTED; } } } return null; }, _calculateConfidence(vue, text, progressBar, className, dataAttr) { let score = 0; let sources = 0; const weights = { vue: 35, text: 25, progressBar: 20, dataAttr: 15, class: 5 }; if (vue) { score += weights.vue; sources++; } if (text) { score += weights.text; sources++; } if (progressBar) { score += weights.progressBar; sources++; } if (dataAttr) { score += weights.dataAttr; sources++; } if (className) { score += weights.class; sources++; } if (sources >= 3) { score += 15; } else if (sources >= 2) { score += 8; } if (sources === 1) { score *= .8; } return Math.min(Math.round(score), 100); }, _createStatusResult(status, confidence, sources = {}, extra = {}) { return { status: status, confidence: confidence, sources: sources, isCompleted: status === this.STATUS.COMPLETED, isInProgress: status === this.STATUS.IN_PROGRESS, isNotStarted: status === this.STATUS.NOT_STARTED, ...extra }; }, _extractTitle(courseItem) { const config = CBEAD_CONSTANTS.BRANCH_LIST; const titleEl = courseItem.querySelector(config.TITLE_SELECTOR); return titleEl?.textContent?.trim() || "未知课程"; }, _extractCourseId(courseItem) { const linkEl = courseItem.querySelector('a[href*="/study/course/detail/"]'); if (linkEl) { const href = linkEl.getAttribute("href"); const match = href.match(/detail\/([^&\/]+)/); if (match) return match[1]; } const dataId = courseItem.getAttribute("data-id") || courseItem.getAttribute("data-course-id"); if (dataId) return dataId; return null; }, filterLearningNeeded(statusResults) { return statusResults.filter(result => { if (result.status === this.STATUS.COMPLETED && result.confidence >= 70) { return false; } return result.status === this.STATUS.IN_PROGRESS || result.status === this.STATUS.NOT_STARTED; }); }, getStatistics(statusResults) { const stats = { total: statusResults.length, completed: 0, inProgress: 0, notStarted: 0, unknown: 0, avgConfidence: 0 }; let totalConfidence = 0; let confidenceCount = 0; statusResults.forEach(result => { if (result.status === this.STATUS.COMPLETED) { stats.completed++; } else if (result.status === this.STATUS.IN_PROGRESS) { stats.inProgress++; } else if (result.status === this.STATUS.NOT_STARTED) { stats.notStarted++; } else { stats.unknown++; } if (result.confidence > 0) { totalConfidence += result.confidence; confidenceCount++; } }); stats.avgConfidence = confidenceCount > 0 ? Math.round(totalConfidence / confidenceCount) : 0; return stats; } }; const BranchListValidator = { async validateAndGetInfo() { const urlValidation = this._validateUrlFormat(); if (!urlValidation.isValid) { return { isValid: false, pageType: "unknown", pageNumber: 1, totalPages: 1, courseCount: 0, error: urlValidation.reason }; } const domValidation = this._validateDomElements(); if (!domValidation.isValid) { return { isValid: false, pageType: "unknown", pageNumber: 1, totalPages: 1, courseCount: 0, error: domValidation.reason }; } const pageReady = await this._waitForPageReady(); if (!pageReady) { console.warn(`[BranchListValidator] 页面加载超时,尝试提取基本信息`); } const pageInfo = await this.extractPageInfo(); return { isValid: true, pageType: "branch-list-v", organizationId: pageInfo.organizationId, pageNumber: pageInfo.currentPage, totalPages: pageInfo.totalPages, courseCount: pageInfo.courseCount, pageSize: pageInfo.pageSize, ...pageInfo }; }, async validatePage() { const urlValidation = this._validateUrlFormat(); if (!urlValidation.isValid) { console.warn(`[BranchListValidator] URL格式验证失败: ${urlValidation.reason}`); return { isValid: false, pageInfo: null, error: urlValidation.reason }; } const domValidation = this._validateDomElements(); if (!domValidation.isValid) { console.warn(`[BranchListValidator] DOM元素验证失败: ${domValidation.reason}`); return { isValid: false, pageInfo: null, error: domValidation.reason }; } const pageReady = await this._waitForPageReady(); if (!pageReady) { console.warn(`[BranchListValidator] 页面加载超时`); return { isValid: false, pageInfo: null, error: "页面加载超时" }; } const pageInfo = await this.extractPageInfo(); console.log(`[BranchListValidator] 页面验证通过`); console.log(`[BranchListValidator] 组织ID: ${pageInfo.organizationId.substring(0, 8)}...`); console.log(`[BranchListValidator] 总页数: ${pageInfo.totalPages}`); console.log(`[BranchListValidator] 当前页: ${pageInfo.currentPage}`); console.log(`[BranchListValidator] 课程数量: ${pageInfo.courseCount}`); return { isValid: true, pageInfo: pageInfo }; }, _validateUrlFormat() { const hash = window.location.hash || ""; if (!hash.includes("#/branch-list-v/")) { return { isValid: false, reason: "URL不包含 branch-list-v 路径" }; } const match = hash.match(/#\/branch-list-v\/([a-f0-9-]+)/i); if (!match) { return { isValid: false, reason: "无法提取组织ID" }; } const organizationId = match[1]; if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(organizationId)) { return { isValid: false, reason: "组织ID格式无效" }; } return { isValid: true, organizationId: organizationId }; }, _validateDomElements() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const container = document.querySelector(config.CONTAINER_SELECTOR); if (!container) { return { isValid: false, reason: `未找到课程列表容器: ${config.CONTAINER_SELECTOR}` }; } const pagination = document.querySelector(config.PAGINATION_SELECTOR); if (!pagination) { return { isValid: false, reason: `未找到分页组件: ${config.PAGINATION_SELECTOR}` }; } return { isValid: true, container: container, pagination: pagination }; }, async _waitForPageReady() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const validationConfig = config.VALIDATION || { PAGE_LOAD_TIMEOUT: 1e4 }; console.log(`[BranchListValidator] 等待页面加载...`); let pageLoaded = false; let attempts = 0; const maxAttempts = Math.ceil(validationConfig.PAGE_LOAD_TIMEOUT / 500); while (!pageLoaded && attempts < maxAttempts) { const container = document.querySelector(config.CONTAINER_SELECTOR); if (container) { const items = container.querySelectorAll(config.ITEM_SELECTOR); if (items && items.length > 0) { pageLoaded = true; console.log(`[BranchListValidator] ✅ 页面已就绪,检测到 ${items.length} 个课程项`); } } if (!pageLoaded) { await new Promise(resolve => setTimeout(resolve, 500)); attempts++; } } if (pageLoaded) { return true; } console.warn(`[BranchListValidator] 页面加载超时`); return false; }, async extractPageInfo() { const urlMatch = window.location.hash.match(/#\/branch-list-v\/([a-f0-9-]+)/i); const organizationId = urlMatch ? urlMatch[1] : null; return { organizationId: organizationId, currentPage: this._getCurrentPage(), totalPages: this._getTotalPages(), courseCount: this._getCourseCount(), pageSize: this._getPageSize(), startTime: Date.now() }; }, _getCurrentPage() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const activeItem = document.querySelector(config.ACTIVE_PAGE_SELECTOR); if (activeItem) { const pageNum = parseInt(activeItem.textContent?.trim(), 10); if (!isNaN(pageNum) && pageNum > 0) { return pageNum; } } const isActiveItem = document.querySelector(`${config.PAGINATION_SELECTOR} .is-active`); if (isActiveItem) { const pageNum = parseInt(isActiveItem.textContent?.trim(), 10); if (!isNaN(pageNum) && pageNum > 0) { return pageNum; } } const activeLi = document.querySelector(`${config.PAGINATION_SELECTOR} li.active`); if (activeLi) { const pageNum = parseInt(activeLi.textContent?.trim(), 10); if (!isNaN(pageNum) && pageNum > 0) { return pageNum; } } console.warn(`[BranchListValidator] 无法确定当前页码,默认为1`); return 1; }, _getTotalPages() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const paginationBox = document.querySelector(config.PAGINATION_BOX_SELECTOR); if (paginationBox) { const pageText = paginationBox.textContent || ""; const totalMatch = pageText.match(/共\s*(\d+)\s*页/); if (totalMatch) { const total = parseInt(totalMatch[1], 10); if (total > 0) { return total; } } const slashMatch = pageText.match(/(\d+)\s*\/\s*(\d+)/); if (slashMatch) { const total = parseInt(slashMatch[2], 10); if (total > 0) { return total; } } } const paginationEl = document.querySelector(config.PAGINATION_SELECTOR); if (paginationEl) { const pageItems = paginationEl.querySelectorAll(config.PAGE_ITEM_SELECTOR); if (pageItems.length > 0) { const lastItem = pageItems[pageItems.length - 1]; const lastPageNum = parseInt(lastItem.textContent?.trim(), 10); if (!isNaN(lastPageNum) && lastPageNum > 0) { return lastPageNum; } } } const lastPageBtn = document.querySelector(`${config.PAGINATION_SELECTOR} .zxy-pagination-item-last`); if (lastPageBtn) { const lastPage = lastPageBtn.getAttribute("data-page"); if (lastPage) { const total = parseInt(lastPage, 10); if (total > 0) { return total; } } } console.warn(`[BranchListValidator] 无法确定总页数,默认为1`); return 1; }, _getCourseCount() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const container = document.querySelector(config.CONTAINER_SELECTOR); if (!container) { return 0; } const courseItems = container.querySelectorAll(config.ITEM_SELECTOR); return courseItems.length; }, _getPageSize() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const paginationBox = document.querySelector(config.PAGINATION_BOX_SELECTOR); if (paginationBox) { const pageText = paginationBox.textContent || ""; const sizeMatch = pageText.match(/每页\s*(\d+)\s*条/); if (sizeMatch) { return parseInt(sizeMatch[1], 10); } } const courseCount = this._getCourseCount(); if (courseCount > 0 && courseCount <= 12) return 12; if (courseCount > 12 && courseCount <= 16) return 16; if (courseCount > 16 && courseCount <= 20) return 20; return 20; }, isLastPage() { const currentPage = this._getCurrentPage(); const totalPages = this._getTotalPages(); return currentPage >= totalPages; }, getPaginationSummary() { const currentPage = this._getCurrentPage(); const totalPages = this._getTotalPages(); const courseCount = this._getCourseCount(); return `第 ${currentPage}/${totalPages} 页,共 ${courseCount} 门课程`; } }; const CbeadScanner = { extractCourseInfo(element) { try { const titleSelectors = [ ".common-title", ".title", ".activity-stage-name", "h3", "h4" ]; let courseName = null; for (const selector of titleSelectors) { const titleEl = element.querySelector(selector); if (titleEl) { courseName = titleEl.textContent?.trim(); break; } } if (!courseName) return null; const linkEl = element.querySelector("a"); const courseId = linkEl?.getAttribute("data-id") || element?.getAttribute("data-id") || linkEl?.getAttribute("id"); const studyLink = linkEl?.getAttribute("href") || element.querySelector(".study-btn")?.getAttribute("data-url"); return { id: courseId, name: courseName, link: studyLink, element: element }; } catch (error) { console.warn("[CbeadScanner] 提取课程信息失败:", error); return null; } }, getCourses(selectors) { const courses = []; for (const selector of selectors.COURSE_ITEMS) { const elements = document.querySelectorAll(selector); for (const element of elements) { const courseInfo = this.extractCourseInfo(element); if (courseInfo) { courses.push(courseInfo); } } if (courses.length > 0) break; } return courses; }, 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); console.log(`[CbeadScanner] 课程分类结果:`); console.log(` - 📖 学习中: ${categorized.inProgress.length} 门`); console.log(` - 📝 未开始: ${categorized.notStarted.length} 门`); console.log(` - ✅ 已完成: ${categorized.completed.length} 门 (将跳过)`); console.log(` - 📚 需要学习: ${categorized.toLearn.length} 门`); return categorized; }, getSortedLearningList(courses) { const categorized = this.categorizeAndSortCourses(courses); const sortedList = [ ...categorized.inProgress, ...categorized.notStarted ]; console.log(`[CbeadScanner] 学习顺序:`); sortedList.forEach((course, index) => { let prefix = course.status === "in_progress" ? "📖" : "📝"; console.log(` ${index + 1}. ${prefix} ${course.title} (${course.progress}%)`); }); if (categorized.completed.length > 0) { console.log(`[CbeadScanner] 以下课程将跳过(已完成):`); categorized.completed.forEach(course => { console.log(` ✅ ${course.title} (${course.progress}%)`); }); } return sortedList; }, async scanCoursesFromBranchListPage() { const courses = []; const config = CBEAD_CONSTANTS.BRANCH_LIST; console.log(`[CbeadScanner] 开始扫描课程列表...`); console.log(`[CbeadScanner] 选择器配置:`); console.log(` - 容器选择器: ${config.CONTAINER_SELECTOR}`); console.log(` - 课程项选择器: ${config.ITEM_SELECTOR}`); console.log(` - 卡片容器选择器: ${config.CARD_BOX_SELECTOR}`); console.log(` - 标题选择器: ${config.TITLE_SELECTOR}`); console.log(` - 状态选择器: ${config.STATUS_SELECTOR}`); let container = document.querySelector(config.CONTAINER_SELECTOR); let attempts = 0; const maxAttempts = 20; while (!container && attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 500)); container = document.querySelector(config.CONTAINER_SELECTOR); attempts++; } if (attempts > 0) { console.log(`[CbeadScanner] 等待页面加载完成,尝试 ${attempts} 次`); } if (!container) { console.warn(`[CbeadScanner] ❌ 未找到课程列表容器: ${config.CONTAINER_SELECTOR}`); const altContainer = document.querySelector(".card-wrapper") || document.querySelector(".card-box"); if (altContainer) { console.log(`[CbeadScanner] ✅ 找到备选容器: ${altContainer.className}`); } return courses; } console.log(`[CbeadScanner] ✅ 找到课程列表容器`); const courseItems = container.querySelectorAll(config.ITEM_SELECTOR); console.log(`[CbeadScanner] 📚 使用 ${config.ITEM_SELECTOR} 找到 ${courseItems.length} 个课程项`); if (courseItems.length === 0) { console.log(`[CbeadScanner] 使用 ${config.ITEM_SELECTOR} 未找到课程项,尝试备选选择器...`); const altItems = container.querySelectorAll(config.CARD_BOX_SELECTOR); if (altItems.length > 0) { console.log(`[CbeadScanner] ✅ 使用备选选择器 ${config.CARD_BOX_SELECTOR} 找到 ${altItems.length} 个课程项`); } } courseItems.forEach((item, index) => { try { const courseInfo = this._extractCourseInfoFromBranchListItem(item, index); if (courseInfo) { courses.push(courseInfo); } } catch (error) { console.error(`[CbeadScanner] 处理课程项失败 (索引 ${index}):`, error); } }); if (courses.length > 0) { console.log(`[CbeadScanner] 从DOM成功提取 ${courses.length} 门课程`); return courses; } const vueCourses = await this._extractCoursesFromVueData(); if (vueCourses && vueCourses.length > 0) { console.log(`[CbeadScanner] 从Vue组件数据成功提取 ${vueCourses.length} 门课程`); return vueCourses; } const apiCourses = await this._fetchCoursesFromApi(); if (apiCourses && apiCourses.length > 0) { console.log(`[CbeadScanner] 从API成功提取 ${apiCourses.length} 门课程`); return apiCourses; } const deepVueCourses = await this._extractFromVueDeepScan(); if (deepVueCourses && deepVueCourses.length > 0) { console.log(`[CbeadScanner] 从Vue深度扫描成功提取 ${deepVueCourses.length} 门课程`); return deepVueCourses; } console.log(`[CbeadScanner] 成功扫描 ${courses.length} 门课程`); return courses; }, async _fetchCoursesFromApi() { const courses = []; try { const hash = window.location.hash || ""; const match = hash.match(/branch-list-v\/([a-f0-9-]+)/i); const organizationId = match ? match[1] : null; if (!organizationId) { console.debug("[CbeadScanner] 无法从URL提取组织ID"); return courses; } console.log(`[CbeadScanner] 尝试从API获取课程列表,组织ID: ${organizationId.substring(0, 8)}...`); const apiUrl = `/api/v1/audience/course/list`; const response = await fetch(apiUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ organizationId: organizationId, pageNum: 1, pageSize: 20 }) }); if (!response.ok) { console.debug(`[CbeadScanner] API请求失败: ${response.status}`); return courses; } const data = await response.json(); console.log(`[CbeadScanner] API返回数据:`, data); if (data.code === 200 || data.code === 0 || data.success) { const records = data.data?.records || data.records || data.list || data.data || []; if (Array.isArray(records) && records.length > 0) { for (const item of records) { const courseInfo = this._buildCourseInfoFromApiData(item); if (courseInfo && courseInfo.id) { courses.push(courseInfo); } } console.log(`[CbeadScanner] 从API解析到 ${courses.length} 门课程`); } } } catch (error) { console.debug(`[CbeadScanner] 从API获取课程失败:`, error.message); } return courses; }, _extractCoursesFromVueData() { const courses = []; const vueElements = document.querySelectorAll("[data-v-]"); for (const el of vueElements) { if (el.__vue__) { const vueInstance = el.__vue__; const courseData = this._parseVueInstanceData(vueInstance); if (courseData.length > 0) { courses.push(...courseData); } } } if (window.__VueApp__ || window.__vue__ || window.Vue) { try { const vueApp = window.__VueApp__ || window.__vue__ || window.Vue; if (vueApp && vueApp.componentInstances) { for (const instance of vueApp.componentInstances) { const courseData = this._parseVueInstanceData(instance); if (courseData.length > 0) { courses.push(...courseData); } } } } catch (e) { console.debug("[CbeadScanner] 无法访问全局Vue实例:", e); } } const cardItems = document.querySelectorAll(".card-item"); for (const card of cardItems) { const parentVue = this._findParentVueInstance(card); if (parentVue) { const courseData = this._parseVueInstanceData(parentVue); if (courseData.length > 0) { for (const course of courseData) { if (!courses.find(c => c.id === course.id)) { courses.push(course); } } } } } return courses; }, _findParentVueInstance(element) { let el = element; const maxDepth = 10; let depth = 0; while (el && depth < maxDepth) { if (el.__vue__) { return el.__vue__; } if (el._vueParent) { return el._vueParent; } el = el.parentElement; depth++; } return null; }, _parseVueInstanceData(vueInstance) { const courses = []; if (!vueInstance) return courses; const dataProps = [ "courseList", "list", "courses", "items", "data", "courseData", "tableData", "tableList", "$data", "data", "propsData" ]; for (const prop of dataProps) { try { let data = vueInstance[prop]; if (!data && vueInstance.$data) { data = vueInstance.$data[prop]; } if (data && Array.isArray(data) && data.length > 0) { const firstItem = data[0]; if (firstItem.id || firstItem.courseId || firstItem.dsUnitId || firstItem.courseName || firstItem.title) { for (const item of data) { const courseInfo = this._buildCourseInfoFromApiData(item); if (courseInfo && courseInfo.id) { courses.push(courseInfo); } } console.log(`[CbeadScanner] 从Vue数据属性 ${prop} 提取到 ${courses.length} 门课程`); return courses; } } } catch (e) {} } try { const keys = Object.keys(vueInstance); for (const key of keys) { if (key.startsWith("_") || key === "constructor") continue; try { const value = vueInstance[key]; if (value && typeof value === "object") { if (Array.isArray(value)) { const courseData = this._checkAndParseCourseArray(value); if (courseData.length > 0) { courses.push(...courseData); } } if (value.data && Array.isArray(value.data)) { const courseData = this._checkAndParseCourseArray(value.data); if (courseData.length > 0) { courses.push(...courseData); } } if (value.list && Array.isArray(value.list)) { const courseData = this._checkAndParseCourseArray(value.list); if (courseData.length > 0) { courses.push(...courseData); } } } } catch (e) {} } } catch (e) { console.debug("[CbeadScanner] 解析Vue实例数据失败:", e); } return courses; }, _checkAndParseCourseArray(arr) { const courses = []; if (!arr || arr.length === 0) return courses; const firstItem = arr[0]; const hasCourseFields = firstItem.id || firstItem.courseId || firstItem.dsUnitId || firstItem.courseName || firstItem.title || firstItem.name; if (hasCourseFields) { for (const item of arr) { const courseInfo = this._buildCourseInfoFromApiData(item); if (courseInfo && courseInfo.id) { courses.push(courseInfo); } } } return courses; }, _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; let status = "not_started"; if (data.studyProgress !== undefined) { progress = data.studyProgress; } else if (data.progress !== undefined) { progress = data.progress; } else if (data.percentage !== undefined) { progress = data.percentage; } if (progress >= CBEAD_CONSTANTS.THRESHOLDS.COMPLETED_PROGRESS) { status = "completed"; } else if (progress > 0) { status = "in_progress"; } if (data.studyStatus === 1 || data.studyStatus === "studying") { status = "in_progress"; progress = progress || 50; } else if (data.studyStatus === 2 || data.studyStatus === "completed") { status = "completed"; progress = 100; } else if (data.studyStatus === 0 || data.studyStatus === "not_started") { status = "not_started"; progress = 0; } 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 }; }, _extractCourseInfoFromBranchListItem(item, index) { const config = CBEAD_CONSTANTS.BRANCH_LIST; const titleEl = item.querySelector(config.TITLE_SELECTOR); const title = titleEl?.textContent?.trim() || null; if (!title) { console.warn(`[CbeadScanner] 无法提取课程标题 (索引 ${index})`); return null; } let courseId = null; let studyLink = null; courseId = item.getAttribute("data-id") || item.getAttribute("data-course-id") || item.getAttribute("data-key") || item.getAttribute("data-vid") || item.getAttribute("data-idx"); if (!courseId && item.id) { const idMatch = item.id.match(/([a-f0-9-]{36}|[a-f0-9]{32})/i); if (idMatch) courseId = idMatch[1]; } if (!courseId) { const dataIdEl = item.querySelector("[data-id], [data-course-id]"); if (dataIdEl) { courseId = dataIdEl.getAttribute("data-id") || dataIdEl.getAttribute("data-course-id"); } } if (!studyLink) { const linkEl = item.querySelector('a[href*="/study/course/detail/"]'); if (linkEl) { const href = linkEl.getAttribute("href"); if (href && href.includes("/study/course/detail/")) { studyLink = href; if (!courseId) { const linkMatch = href.match(/detail\/([^&\/]+)/); if (linkMatch) courseId = linkMatch[1]; } } } } if (!courseId || !studyLink) { const vueData = this._scanVueInstanceForCourseData(item, title); if (vueData) { if (!courseId && vueData.id) courseId = vueData.id; if (!studyLink && vueData.link) studyLink = vueData.link; } } if (!courseId) { const dynamicKey = item.getAttribute("data-dynamic-key") || item.querySelector("[data-dynamic-key]")?.getAttribute("data-dynamic-key"); if (dynamicKey && /^[a-f0-9-]{36}$/i.test(dynamicKey)) { courseId = dynamicKey; } } if (!courseId || !studyLink) { const clickEl = item.querySelector("[onclick], [data-link], [data-url]"); if (clickEl) { const onclick = clickEl.getAttribute("onclick") || clickEl.getAttribute("data-link") || clickEl.getAttribute("data-url"); if (onclick) { const linkMatch = onclick.match(/['"]?([^'"]*\/study\/course\/detail\/[^'"]*)['"]?/) || onclick.match(/detail\/([^&\/]+)/); if (linkMatch) { const link = linkMatch[1] || linkMatch[0]; studyLink = link.includes("#") ? link : `#/study/course/detail/${link}`; if (!courseId) { const idMatch = link.match(/detail\/([^&\/]+)/); if (idMatch) courseId = idMatch[1]; } } } } } if (courseId && !studyLink) { studyLink = `#/study/course/detail/${courseId}`; } const statusEl = item.querySelector(config.STATUS_SELECTOR); const statusText = statusEl?.textContent?.trim() || ""; let progress = 0; let status = "not_started"; if (statusText === "学习中") { status = "in_progress"; progress = 50; } else if (statusText === "已完成") { status = "completed"; progress = 100; } else if (statusText === "未开始") { status = "not_started"; progress = 0; } const progressBar = item.querySelector('.progress-bar, .completed-rate-bar, [class*="progress"]'); if (progressBar) { const style = progressBar.getAttribute("style") || ""; const progressMatch = style.match(/width:\s*(\d+)%/); if (progressMatch) { progress = parseInt(progressMatch[1], 10); if (progress >= CBEAD_CONSTANTS.THRESHOLDS.COMPLETED_PROGRESS) { status = "completed"; } else if (progress > 0) { status = "in_progress"; } } } const courseInfo = { id: courseId, courseId: courseId, dsUnitId: courseId, title: title, courseName: title, link: studyLink, element: item, progress: progress, isCompleted: progress >= CBEAD_CONSTANTS.THRESHOLDS.COMPLETED_PROGRESS, status: status, statusText: statusText, source: "cbead_branch_list_scan" }; const statusTextDisplay = { completed: "✅ 已完成", in_progress: "📖 学习中", not_started: "📝 未开始" }; const idDisplay = courseId ? `[ID:${courseId.substring(0, 8)}...]` : "[无ID]"; const linkDisplay = studyLink ? `[链接:${studyLink}]` : "[无链接]"; console.log(`[CbeadScanner] 课程 ${index + 1}: ${title} - ${statusTextDisplay[status]} (${progress}%) ${idDisplay} ${linkDisplay}`); return courseInfo; }, _scanVueInstanceForCourseData(element, title) { const vueInstances = []; let el = element; for (let depth = 0; depth < 15 && el; depth++) { if (el.__vue__) { vueInstances.push(el.__vue__); } if (el.__vue__) { const vm = el.__vue__; if (vm.$root && vm.$root !== vm && !vueInstances.includes(vm.$root)) { vueInstances.push(vm.$root); } if (vm.$parent && vm.$parent !== vm && !vueInstances.includes(vm.$parent)) { vueInstances.push(vm.$parent); } } el = el.parentElement; } for (const vm of vueInstances) { try { const result = this._scanVueForCourse(vm, title); if (result) return result; } catch (e) {} } const dataVElements = element.querySelectorAll("[data-v-]"); for (const el of dataVElements) { if (el.__vue__) { try { const result = this._scanVueForCourse(el.__vue__, title); if (result) return result; } catch (e) {} } } return null; }, _scanVueForCourse(vm, title) { if (!vm || typeof vm !== "object") return null; const keys = Object.keys(vm); for (const key of keys) { if (key.startsWith("_") || key === "constructor") continue; try { const val = vm[key]; if (!val || typeof val !== "object") continue; if (Array.isArray(val)) { for (const item of val) { if (item && typeof item === "object") { const matchTitle = item.title || item.courseName || item.name; if (matchTitle === title) { if (item.id || item.courseId || item.dsUnitId) { return { id: item.id || item.courseId || item.dsUnitId, link: item.link || item.url || item.href || item.studyLink }; } } } } } if (val.id && /^[a-f0-9-]{8}/i.test(String(val.id))) { const matchTitle = val.title || val.courseName || val.name; if (matchTitle === title) { return { id: val.id, link: val.link || val.url || val.href }; } } if ((val.link || val.url || val.href) && String(val.link || val.url || val.href).includes("/study/course/detail/")) { const matchTitle = val.title || val.courseName || val.name; if (matchTitle === title) { const link = val.link || val.url || val.href; const idMatch = link.match(/detail\/(?:[^/]*@@)?([a-f0-9-]{36})/i); return { id: idMatch ? idMatch[1] : null, link: link }; } } } catch (e) {} } if (vm.$data) { const data = vm.$data; const listKeys = [ "list", "courses", "courseList", "items", "data", "tableData" ]; for (const listKey of listKeys) { const list = data[listKey]; if (Array.isArray(list)) { for (const item of list) { if (item && typeof item === "object") { const matchTitle = item.title || item.courseName || item.name; if (matchTitle === title) { if (item.id || item.courseId) { return { id: item.id || item.courseId, link: item.link || item.url || item.href }; } } } } } } } return null; }, _getTotalPagesFromText() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const paginationBox = document.querySelector(config.PAGINATION_BOX_SELECTOR); if (paginationBox) { const pageText = paginationBox.textContent || ""; const totalMatch = pageText.match(/共\s*(\d+)\s*页/); if (totalMatch) { const total = parseInt(totalMatch[1], 10); console.log(`[CbeadScanner] 从文本匹配到总页数: ${total}`); return total; } const slashMatch = pageText.match(/(\d+)\s*\/\s*(\d+)/); if (slashMatch) { const total = parseInt(slashMatch[2], 10); console.log(`[CbeadScanner] 从 slash 格式匹配到总页数: ${total}`); return total; } } const lastPageBtn = document.querySelector(`${config.PAGINATION_SELECTOR} .zxy-pagination-item-last`); if (lastPageBtn) { const lastPage = lastPageBtn.getAttribute("data-page"); if (lastPage) { const total = parseInt(lastPage, 10); console.log(`[CbeadScanner] 从尾页按钮匹配到总页数: ${total}`); return total; } } console.warn(`[CbeadScanner] 无法从文本确定总页数`); return 0; }, _getTotalPages() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const paginationBox = document.querySelector(config.PAGINATION_BOX_SELECTOR); if (paginationBox) { const pageText = paginationBox.textContent || ""; const totalMatch = pageText.match(/共\s*(\d+)\s*页/); if (totalMatch) { const total = parseInt(totalMatch[1], 10); console.log(`[CbeadScanner] 从文本匹配到总页数: ${total}`); return total; } const slashMatch = pageText.match(/(\d+)\s*\/\s*(\d+)/); if (slashMatch) { const total = parseInt(slashMatch[2], 10); console.log(`[CbeadScanner] 从 slash 格式匹配到总页数: ${total}`); return total; } } const lastPageBtn = document.querySelector(`${config.PAGINATION_SELECTOR} .zxy-pagination-item-last`); if (lastPageBtn) { const lastPage = lastPageBtn.getAttribute("data-page"); if (lastPage) { const total = parseInt(lastPage, 10); console.log(`[CbeadScanner] 从尾页按钮匹配到总页数: ${total}`); return total; } } console.warn(`[CbeadScanner] 无法确定总页数,默认为 1`); return 1; }, _isPageLoaded() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const container = document.querySelector(config.CONTAINER_SELECTOR); const items = container?.querySelectorAll(config.ITEM_SELECTOR); return items && items.length > 0; }, async _waitForPageLoad(timeout) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { if (this._isPageLoaded()) { return true; } await new Promise(resolve => setTimeout(resolve, 500)); } return false; }, _isLastPage() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const nextBtn = document.querySelector(config.NEXT_BTN_SELECTOR); if (nextBtn) { const parentLi = nextBtn.closest("li"); if (parentLi && parentLi.classList.contains("zxy-pagination-disabled")) { console.log(`[CbeadScanner] 检测到父元素 li 包含禁用类(最后一页)`); return true; } if (nextBtn.classList.contains("is-disabled") || nextBtn.classList.contains("disabled") || nextBtn.classList.contains(config.DISABLED_PAGINATION_CLASS)) { console.log(`[CbeadScanner] 检测到下一页按钮包含禁用类(最后一页)`); return true; } if (nextBtn.hasAttribute("disabled") || nextBtn.getAttribute("aria-disabled") === "true") { console.log(`[CbeadScanner] 检测到下一页按钮已禁用(最后一页)`); return true; } const style = nextBtn.getAttribute("style") || ""; if (style.includes("display: none") || style.includes("visibility: hidden")) { console.log(`[CbeadScanner] 检测到下一页按钮隐藏(最后一页)`); return true; } } const disabledNext = document.querySelector(".zxy-pagination-next.is-disabled, .zxy-pagination-next.disabled, .zxy-pagination-next.zxy-pagination-disabled"); if (disabledNext) { console.log(`[CbeadScanner] 检测到禁用的下一页按钮(最后一页)`); return true; } const nextLi = document.querySelector(".zxy-pagination-next")?.closest("li"); if (nextLi && nextLi.classList.contains("zxy-pagination-disabled")) { console.log(`[CbeadScanner] 检测到下一页父元素 li.zxy-pagination-disabled(最后一页)`); return true; } const disabledItems = document.querySelectorAll(`${config.PAGINATION_SELECTOR} .is-disabled, ${config.PAGINATION_SELECTOR} .disabled`); for (const item of disabledItems) { if (item.classList.contains("zxy-pagination-next") || item.querySelector(".zxy-pagination-next")) { console.log(`[CbeadScanner] 检测到禁用的下一页分页项(最后一页)`); return true; } } const currentPage = this._getCurrentPage(); const totalPages = this._getTotalPagesFromText(); console.log(`[CbeadScanner] 当前页: ${currentPage}, 总页数: ${totalPages}`); if (currentPage >= totalPages && totalPages > 0) { console.log(`[CbeadScanner] 当前页码 >= 总页数(最后一页)`); return true; } return false; }, async _clickNextPage() { const config = CBEAD_CONSTANTS.BRANCH_LIST; if (this._isLastPage()) { console.log(`[CbeadScanner] 已到达最后一页`); return false; } const paginationContainer = document.querySelector(config.PAGINATION_BOX_SELECTOR); if (!paginationContainer) { console.warn(`[CbeadScanner] 未找到分页容器: ${config.PAGINATION_BOX_SELECTOR}`); return false; } const nextBtn = paginationContainer.querySelector(".zxy-pagination-next"); if (!nextBtn) { console.warn(`[CbeadScanner] 未找到下一页按钮`); return false; } const currentUrl = window.location.href; console.log(`[CbeadScanner] 点击前 URL: ${currentUrl}`); const parentLi = nextBtn.closest("li"); if (parentLi) { console.log(`[CbeadScanner] 找到下一页按钮,父元素类名: ${parentLi.className}`); } if (parentLi && parentLi.classList.contains("zxy-pagination-disabled")) { console.log(`[CbeadScanner] 下一页按钮父元素 li 已禁用(最后一页)`); return false; } if (nextBtn.classList.contains("is-disabled") || nextBtn.classList.contains("disabled") || nextBtn.classList.contains(config.DISABLED_PAGINATION_CLASS) || nextBtn.hasAttribute("disabled") || nextBtn.getAttribute("aria-disabled") === "true") { console.log(`[CbeadScanner] 下一页按钮已禁用(最后一页)`); return false; } const currentPage = this._getCurrentPage(); console.log(`[CbeadScanner] 正在从第 ${currentPage} 页点击下一页...`); nextBtn.click(); console.log(`[CbeadScanner] ✅ 已点击下一页按钮`); await new Promise(resolve => setTimeout(resolve, config.PAGINATION_DELAY)); console.log(`[CbeadScanner] 点击后 URL: ${window.location.href}`); if (window.location.pathname === "/" && !window.location.hash) { console.error(`[CbeadScanner] ⚠️ 检测到异常跳转到根路径!`); return false; } let newPage = this._getCurrentPage(); let attempts = 0; const maxAttempts = 5; while (newPage === currentPage && attempts < maxAttempts) { attempts++; console.log(`[CbeadScanner] 页面未变化(第${attempts}次尝试),继续等待...`); if (this._isLastPage()) { console.log(`[CbeadScanner] 检测到已到达最后一页`); return false; } await new Promise(resolve => setTimeout(resolve, 500)); newPage = this._getCurrentPage(); } console.log(`[CbeadScanner] 页面变化: ${currentPage} -> ${newPage}`); if (newPage === currentPage) { console.warn(`[CbeadScanner] 页面未变化,可能已到最后一页或加载失败`); if (this._isLastPage()) { return false; } console.warn(`[CbeadScanner] 页面加载异常,停止翻页`); return false; } const totalPages = this._getTotalPagesFromText(); if (totalPages > 0 && newPage > totalPages) { console.warn(`[CbeadScanner] 页码异常: 新页码 ${newPage} 超过总页数 ${totalPages}`); return false; } return true; }, _getCurrentPage() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const activeItem = document.querySelector(config.ACTIVE_PAGE_SELECTOR); if (activeItem) { const pageNum = parseInt(activeItem.textContent?.trim(), 10); if (!isNaN(pageNum)) { return pageNum; } } const isActiveItem = document.querySelector(`${config.PAGINATION_SELECTOR} .is-active`); if (isActiveItem) { const pageNum = parseInt(isActiveItem.textContent?.trim(), 10); if (!isNaN(pageNum)) { return pageNum; } } return 1; }, async scanAllCoursesWithPagination() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const allCourses = []; let currentPage = 1; console.log(`[CbeadScanner] 开始翻页扫描课程...`); while (true) { const pageCourses = await this.scanCoursesFromBranchListPage(); console.log(`[CbeadScanner] 第 ${currentPage} 页: 扫描到 ${pageCourses.length} 门课程`); allCourses.push(...pageCourses); const hasNext = await this._clickNextPage(); if (!hasNext) { console.log(`[CbeadScanner] 已到达最后一页,停止扫描`); break; } currentPage++; const loaded = await this._waitForPageLoad(config.PAGE_LOAD_TIMEOUT); if (!loaded) { console.warn(`[CbeadScanner] 页面 ${currentPage} 加载超时,停止扫描`); break; } } console.log(`[CbeadScanner] 翻页扫描完成,共 ${allCourses.length} 门课程`); console.log(`[CbeadScanner] 📤 返回 ${allCourses.length} 门课程`); return allCourses; }, _getTagList() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const tagContainer = document.querySelector(config.TAG_CONTAINER_SELECTOR); if (!tagContainer) { console.warn(`[CbeadScanner] 未找到标签容器: ${config.TAG_CONTAINER_SELECTOR}`); return []; } const tagBtns = tagContainer.querySelectorAll(config.TAG_BTN_SELECTOR); return Array.from(tagBtns); }, async clickTagFilter(tagBtn) { const config = CBEAD_CONSTANTS.BRANCH_LIST; if (!tagBtn) { return false; } if (tagBtn.classList.contains("is-active") || tagBtn.classList.contains("active")) { console.log(`[CbeadScanner] 标签已激活,跳过`); return false; } const tagText = tagBtn.textContent?.trim() || "未知标签"; console.log(`[CbeadScanner] 点击标签: ${tagText}`); tagBtn.click(); await new Promise(resolve => setTimeout(resolve, config.TAG_SWITCH_DELAY)); return true; }, async scanAllCoursesWithTagFilter(skipAllTag = true) { const allCourses = []; const seenLinks = new Set; console.log(`[CbeadScanner] 开始扫描所有标签下的课程...`); const tagList = this._getTagList(); if (tagList.length === 0) { console.warn(`[CbeadScanner] 未找到标签,尝试普通扫描`); return this.scanAllCoursesWithPagination(); } for (let i = 0; i < tagList.length; i++) { const tagBtn = tagList[i]; const tagText = tagBtn.textContent?.trim() || `标签${i + 1}`; if (skipAllTag && (tagText === "全部" || tagText === "All" || tagText === "全部标签")) { console.log(`[CbeadScanner] 跳过"全部"标签`); continue; } console.log(`[CbeadScanner] 处理标签: ${tagText}`); if (i > 0 || !skipAllTag) { const clicked = await this.clickTagFilter(tagBtn); if (!clicked) { console.log(`[CbeadScanner] 标签 ${tagText} 无需切换或切换失败`); } } const tagCourses = await this.scanAllCoursesWithPagination(); for (const course of tagCourses) { if (!seenLinks.has(course.link)) { seenLinks.add(course.link); allCourses.push(course); } } console.log(`[CbeadScanner] 标签 ${tagText}: ${tagCourses.length} 门课程(累计 ${allCourses.length} 门)`); } console.log(`[CbeadScanner] 标签扫描完成,共 ${allCourses.length} 门课程(去重后)`); return allCourses; }, async scanCoursesFromBranchList(options = {}) { const {useTagFilter: useTagFilter = false, skipAllTag: skipAllTag = true} = options; console.log(`[CbeadScanner] 开始扫描 branch-list-v 页面`); console.log(`[CbeadScanner] 选项: useTagFilter=${useTagFilter}, skipAllTag=${skipAllTag}`); console.log(`[CbeadScanner] 当前页面URL: ${window.location.href}`); const config = CBEAD_CONSTANTS.BRANCH_LIST; let container = document.querySelector(config.CONTAINER_SELECTOR); let attempts = 0; const maxAttempts = 20; while (!container && attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 500)); container = document.querySelector(config.CONTAINER_SELECTOR); attempts++; } if (attempts > 0) { console.log(`[CbeadScanner] 等待页面加载完成,尝试 ${attempts} 次`); } if (useTagFilter) { console.log(`[CbeadScanner] 使用标签筛选模式...`); return await this.scanAllCoursesWithTagFilter(skipAllTag); } else { console.log(`[CbeadScanner] 使用普通翻页模式...`); return await this.scanAllCoursesWithPagination(); } }, async scanCoursesFromColumnPage() { console.log(`[CbeadScanner] 开始扫描专题班课程...`); const pageStyle = this._detectColumnPageStyle(); console.log(`[CbeadScanner] 检测到页面样式: ${pageStyle}`); if (pageStyle === "activity") { return await this._scanFromActivityPageStyle(); } else if (pageStyle === "layout") { return await this._scanFromLayoutStyle(); } else { console.warn(`[CbeadScanner] 未识别的页面样式,尝试所有扫描方法`); let courses = await this._scanFromActivityPageStyle(); if (courses.length > 0) return courses; courses = await this._scanFromLayoutStyle(); return courses; } }, _detectColumnPageStyle() { const config = CBEAD_CONSTANTS.COLUMN_PAGE; const activityContainer = document.querySelector(config.ACTIVITY_STYLE.CONTAINER_SELECTOR); if (activityContainer) { if (activityContainer.querySelector("ul.list")) { console.log(`[CbeadScanner] 检测到活动清单样式 (.activity-page) - 通过 ul.list`); return "activity"; } const courseItems = activityContainer.querySelectorAll("li.clearfix"); if (courseItems.length > 0) { console.log(`[CbeadScanner] 检测到活动清单样式 (.activity-page) - 通过课程项`); return "activity"; } const allItems = activityContainer.querySelectorAll('[class*="activity-stage"]'); if (allItems.length > 0) { console.log(`[CbeadScanner] 检测到活动清单样式 (.activity-page) - 通过 stage 元素`); return "activity"; } console.log(`[CbeadScanner] 检测到活动清单样式 (.activity-page) - 容器存在`); return "activity"; } const layoutContainer = document.querySelector(config.LAYOUT_STYLE.CONTAINER_SELECTOR); if (layoutContainer) { const listElements = layoutContainer.querySelector(config.LAYOUT_STYLE.LIST_SELECTOR); if (listElements || layoutContainer.querySelector(config.LAYOUT_STYLE.ITEM_SELECTOR)) { console.log(`[CbeadScanner] 检测到layout样式 (.class-layout)`); return "layout"; } } console.log(`[CbeadScanner] 无法确定页面样式`); return "unknown"; }, async _scanFromActivityPageStyle() { const config = CBEAD_CONSTANTS.COLUMN_PAGE.ACTIVITY_STYLE; let container = document.querySelector(config.CONTAINER_SELECTOR); let attempts = 0; const maxAttempts = 20; while (!container && attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 500)); container = document.querySelector(config.CONTAINER_SELECTOR); attempts++; } if (attempts > 0) { console.log(`[CbeadScanner] 等待页面加载完成,尝试 ${attempts} 次`); } const courses = []; if (!container) { console.warn(`[CbeadScanner] 未找到专题班课程列表容器: ${config.CONTAINER_SELECTOR}`); return courses; } const allLists = container.querySelectorAll(".activity-stage ul.list"); console.log(`[CbeadScanner] 专题班页面找到 ${allLists.length} 个课程列表模块`); allLists.forEach((list, moduleIndex) => { const moduleTitleEl = list.parentElement?.querySelector(".activity-stage-name"); const moduleTitle = moduleTitleEl?.textContent?.trim() || `模块${moduleIndex + 1}`; const courseItems = list.querySelectorAll(config.ITEM_SELECTOR); console.log(`[CbeadScanner] ${moduleTitle}模块: 找到 ${courseItems.length} 个课程项`); courseItems.forEach((item, index) => { try { const titleEl = item.querySelector(config.TITLE_SELECTOR); if (!titleEl) { console.warn(`[CbeadScanner] ${moduleTitle} 课程 ${index + 1}: 未找到标题元素`); return; } const idMatch = titleEl.id?.match(/D75itemDetail1-([a-f0-9-]+)/); if (!idMatch) { const altIdMatch = item.id?.match(/([a-f0-9-]{36})/i); if (!altIdMatch) { console.warn(`[CbeadScanner] ${moduleTitle} 课程 ${index + 1}: 无法提取课程ID`); return; } var courseId = altIdMatch[1]; } else { courseId = idMatch[1]; } const title = titleEl.textContent?.trim() || `课程${courseId.substring(0, 8)}`; const progressBar = item.querySelector(config.PROGRESS_BAR_SELECTOR); const style = progressBar?.getAttribute("style") || ""; const progressMatch = style.match(/width:\s*(\d+)%/); const progress = progressMatch ? parseInt(progressMatch[1]) : 0; const studyLink = `#/study/course/detail/${courseId}`; let status = "not_started"; if (progress >= CBEAD_CONSTANTS.THRESHOLDS.COMPLETED_PROGRESS) { status = "completed"; } else if (progress > 0) { status = "in_progress"; } courses.push({ id: courseId, courseId: courseId, dsUnitId: courseId, title: title, courseName: title, link: studyLink, progress: progress, isCompleted: progress >= CBEAD_CONSTANTS.THRESHOLDS.COMPLETED_PROGRESS, status: status, element: item, source: "cbead_column_activity_style", module: moduleTitle }); const statusTextDisplay = { completed: "✅ 已完成", in_progress: "📖 学习中", not_started: "📝 未开始" }; console.log(`[CbeadScanner] ${moduleTitle} 课程 ${index + 1}: ${title} - ${statusTextDisplay[status]} (${progress}%)`); } catch (error) { console.error(`[CbeadScanner] ${moduleTitle} 处理课程项失败 (索引 ${index}):`, error); } }); }); console.log(`[CbeadScanner] 活动清单样式扫描完成,共 ${courses.length} 门课程`); return courses; }, async _scanFromLayoutStyle() { const config = CBEAD_CONSTANTS.COLUMN_PAGE.LAYOUT_STYLE; let container = document.querySelector(config.CONTAINER_SELECTOR); let attempts = 0; const maxAttempts = 20; while (!container && attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 500)); container = document.querySelector(config.CONTAINER_SELECTOR); attempts++; } if (attempts > 0) { console.log(`[CbeadScanner] 等待页面加载完成,尝试 ${attempts} 次`); } if (!container) { console.warn(`[CbeadScanner] 未找到layout样式课程列表容器: ${config.CONTAINER_SELECTOR}`); return []; } const tabContainer = document.querySelector(config.TAB_SWITCH.CONTAINER_SELECTOR); const hasTabs = tabContainer !== null; if (hasTabs) { console.log(`[CbeadScanner] 检测到标签切换,将遍历所有标签`); return await this._scanFromLayoutStyleWithTabs(container, tabContainer); } else { console.log(`[CbeadScanner] 未检测到标签切换,扫描单个列表`); return await this._scanFromLayoutStyleSingle(container); } }, async _scanFromLayoutStyleWithTabs(container, tabContainer) { const config = CBEAD_CONSTANTS.COLUMN_PAGE.LAYOUT_STYLE; const allCourses = []; const seenCourseIds = new Set; const tabItems = tabContainer.querySelectorAll(config.TAB_SWITCH.TAB_ITEM_SELECTOR); console.log(`[CbeadScanner] 找到 ${tabItems.length} 个标签`); for (let tabIndex = 0; tabIndex < tabItems.length; tabIndex++) { const tab = tabItems[tabIndex]; const tabName = tab.textContent?.trim() || `标签${tabIndex + 1}`; const isActive = tab.classList.contains(config.TAB_SWITCH.ACTIVE_CLASS); if (!isActive) { console.log(`[CbeadScanner] 切换到标签: ${tabName}`); tab.click(); await new Promise(resolve => setTimeout(resolve, config.TAB_SWITCH.SWITCH_DELAY)); } else { console.log(`[CbeadScanner] 当前已在标签: ${tabName}`); } const courseList = container.querySelector(config.LIST_SELECTOR); if (!courseList) { console.warn(`[CbeadScanner] 标签 "${tabName}" 未找到课程列表`); continue; } const courseItems = courseList.querySelectorAll(config.ITEM_SELECTOR); console.log(`[CbeadScanner] 标签 "${tabName}": 找到 ${courseItems.length} 个课程项`); for (let itemIndex = 0; itemIndex < courseItems.length; itemIndex++) { const item = courseItems[itemIndex]; try { const courseInfo = this._extractCourseFromLayoutItem(item, config, itemIndex, tabName); if (courseInfo && courseInfo.id && !seenCourseIds.has(courseInfo.id)) { seenCourseIds.add(courseInfo.id); allCourses.push(courseInfo); } else if (courseInfo && seenCourseIds.has(courseInfo.id)) { console.log(`[CbeadScanner] 标签 "${tabName}" 课程 ${itemIndex + 1}: ${courseInfo.title} - 已存在,跳过`); } } catch (error) { console.error(`[CbeadScanner] 标签 "${tabName}" 处理课程项失败 (索引 ${itemIndex}):`, error); } } } console.log(`[CbeadScanner] layout样式(带标签)扫描完成,共 ${allCourses.length} 门课程(去重后)`); return allCourses; }, async _scanFromLayoutStyleSingle(container) { const config = CBEAD_CONSTANTS.COLUMN_PAGE.LAYOUT_STYLE; const courses = []; const courseList = container.querySelector(config.LIST_SELECTOR); if (!courseList) { console.warn(`[CbeadScanner] 未找到课程列表: ${config.LIST_SELECTOR}`); return courses; } const courseItems = courseList.querySelectorAll(config.ITEM_SELECTOR); console.log(`[CbeadScanner] layout样式找到 ${courseItems.length} 个课程项`); courseItems.forEach((item, index) => { try { const courseInfo = this._extractCourseFromLayoutItem(item, config, index); if (courseInfo) { courses.push(courseInfo); } } catch (error) { console.error(`[CbeadScanner] layout样式 处理课程项失败 (索引 ${index}):`, error); } }); console.log(`[CbeadScanner] layout样式扫描完成,共 ${courses.length} 门课程`); return courses; }, _extractCourseFromLayoutItem(item, config, index, tabName = "") { const titleEl = item.querySelector(config.TITLE_SELECTOR); if (!titleEl) { console.warn(`[CbeadScanner] ${tabName ? `标签"${tabName}" ` : ""}课程 ${index + 1}: 未找到标题元素`); return null; } let courseId = null; const idMatch = titleEl.id?.match(/D74itemDetail-([a-f0-9-]+)/i); if (idMatch) { courseId = idMatch[1]; } else { courseId = item.getAttribute("data-activityid"); if (!courseId) { const altIdMatch = titleEl.id?.match(/([a-f0-9-]{36})/i); if (altIdMatch) { courseId = altIdMatch[1]; } else { console.warn(`[CbeadScanner] ${tabName ? `标签"${tabName}" ` : ""}课程 ${index + 1}: 无法提取课程ID`); return null; } } } const title = titleEl.textContent?.trim() || `课程${courseId.substring(0, 8)}`; const statusEl = item.querySelector(config.STATUS_SELECTOR); const statusText = statusEl?.textContent?.trim() || ""; let progress = 0; let status = "not_started"; if (statusText === config.STATUS_TEXT.COMPLETED) { status = "completed"; progress = 100; } else if (statusText === config.STATUS_TEXT.IN_PROGRESS) { status = "in_progress"; progress = 50; } else if (statusText === config.STATUS_TEXT.UNFINISHED) { status = "not_started"; progress = 0; } else { status = "not_started"; progress = 0; } const studyLink = `#/study/course/detail/${courseId}`; const statusTextDisplay = { completed: "✅ 已完成", in_progress: "📖 学习中", not_started: "📝 未开始" }; const tabPrefix = tabName ? `[${tabName}] ` : ""; console.log(`[CbeadScanner] ${tabPrefix}课程 ${index + 1}: ${title} - ${statusTextDisplay[status]} (${progress}%) [状态文本: "${statusText}"]`); 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: item, source: "cbead_column_layout_style", statusText: statusText, tabName: tabName || null }; }, detectPageType() { const hash = window.location.hash || ""; if (hash.includes(CBEAD_CONSTANTS.PATH_PATTERNS.BRANCH_LIST)) { return { pageType: "branch-list", pageName: "在线自学课程列表", scanMethod: () => this.scanCoursesFromBranchListPage() }; } if (hash.includes(CBEAD_CONSTANTS.PATH_PATTERNS.COLUMN)) { return { pageType: "column", pageName: "专题班详情", scanMethod: () => this.scanCoursesFromColumnPage() }; } const branchContainer = document.querySelector(CBEAD_CONSTANTS.BRANCH_LIST.CONTAINER_SELECTOR); if (branchContainer) { return { pageType: "branch-list", pageName: "在线自学课程列表", scanMethod: () => this.scanCoursesFromBranchListPage() }; } const columnContainerActivity = document.querySelector(CBEAD_CONSTANTS.COLUMN_PAGE.ACTIVITY_STYLE.CONTAINER_SELECTOR); const columnContainerLayout = document.querySelector(CBEAD_CONSTANTS.COLUMN_PAGE.LAYOUT_STYLE.CONTAINER_SELECTOR); if (columnContainerActivity || columnContainerLayout) { return { pageType: "column", pageName: "专题班详情", scanMethod: () => this.scanCoursesFromColumnPage() }; } return { pageType: "unknown", pageName: "未知页面", scanMethod: null }; }, async _extractFromVueDeepScan() { const courses = []; try { const vueInstances = []; const allElements = document.querySelectorAll("*"); for (const el of allElements) { if (el.__vue__) { vueInstances.push(el.__vue__); } } console.log(`[CbeadScanner] 找到 ${vueInstances.length} 个Vue实例进行深度扫描`); for (const instance of vueInstances) { const instanceCourses = this._deepScanVueInstance(instance); for (const course of instanceCourses) { if (!courses.find(c => c.id === course.id)) { courses.push(course); } } } } catch (error) { console.debug("[CbeadScanner] Vue深度扫描失败:", error.message); } return courses; }, _deepScanVueInstance(obj, visited = new Set, depth = 0) { const courses = []; const maxDepth = 15; const maxItems = 5e3; if (depth > maxDepth) return courses; if (visited.size > maxItems) return courses; if (!obj || typeof obj !== "object") return courses; visited.add(obj); try { if (Array.isArray(obj)) { for (const item of obj) { if (item && typeof item === "object") { const courseInfo = this._tryBuildCourseInfo(item); if (courseInfo) { courses.push(courseInfo); } else { courses.push(...this._deepScanVueInstance(item, visited, depth + 1)); } } } } else { const courseInfo = this._tryBuildCourseInfo(obj); if (courseInfo) { courses.push(courseInfo); } for (const key of Object.keys(obj)) { if (key.startsWith("_") || key === "constructor") continue; try { const value = obj[key]; if (value && typeof value === "object" && !visited.has(value)) { courses.push(...this._deepScanVueInstance(value, visited, depth + 1)); } } catch (e) {} } } } catch (error) {} return courses; }, _tryBuildCourseInfo(data) { if (!data || typeof data !== "object") return null; const id = data.id || data.courseId || data.dsUnitId || data.courseCode; const title = data.title || data.courseName || data.name || data.courseTitle; if (!id || !title) return null; if (!/^[a-f0-9]{8}-?[a-f0-9]{4}/i.test(String(id))) { return null; } const lowerTitle = String(title).toLowerCase(); const invalidKeywords = [ "分院", "主页", "专题班", "在线自学", "专栏", "个人中心", "AI教练", "最近活动", "往期回顾", "课程目录体系", "其他课程", "分享到问道", "默认配置", "配置", "设置", "导航", "菜单", "页脚", "皮肤", "标签", "学习任务", "数据归档", "密码安全", "第三方平台", "最近学习", "备案信息", "登录页", "协议", "logo", "勋章", "纷享", "打赏", "限流", "防刷", "飘窗", "学币", "分院统计", "语言配置", "播放器", "数据导出", "账户登录", "身份验证", "证书获取", "人脸", "重操作", "防录屏", "扫码下载", "扫码关注", "问吧", "知识付费", "APP状态栏", "移动端", "管理员", "默认首页", "详情页", "个人信息", "网页置灰", "分享", "重操作显示", "课程目录体系", "其他课程", "系统配置", "系统设置", "微信", "QQ空间", "新浪微搏", "微博", "安徽", "北京", "上海", "广东", "深圳", "浙江", "江苏", "政治理论", "党性教育", "时政热点", "宏观经济", "产业发展", "管理理论", "实践", "国企改革", "国企党建", "转型升级", "人文素养", "安全生产", "工会工作", "合规管理", "创新管理", "二十大", "三中全会", "现代企业制度", "理想信念", "创新思维", "战略管理", "八项规定", "创新转型", "ESG", "领导力", "科改", "技术发展", "调查研究", "行业发展", "主题教育", "职业素养", "人才培养", "人工智能", "数字化转型", "碳达峰", "碳中和", "党的宗旨", "党建生产", "深度融合", "世界一流", "新质生产力", "资本运营", "财务管理", "企业文化", "廉政教育", "全面从严治党", "市场化", "经营机制", "金融形势", "品牌建设", "党史", "国史", "科技创新", "好干部", "国企改革趋势", "总体要求", "国有企业简史", "特色课程", "乡村振兴", "转型发展", "领导力", "形势政策", "政治经济", "并购重组", "传统文化", "审计", "应急管理", "纪检监察", "产业形势", "思想政治", "职场胜任", "宏观形势", "行业领域", "资本市场", "生态文明", "采购管理", "治国理政", "危机领导力", "绿色发展", "数智化", "国际化", "涉外", "风险管理", "业财融合", "新发展", "政治能力", "创新发展", "激励与绩效", "金融风险", "支部建设", "理论学习", "国资监管", "风控体系", "条例", "变革领导力", "报表分析", "团队领导力", "混合所有制", "新闻宣传", "国际化经营", "国际形势", "梯队建设", "演讲与沟通", "三项制度", "外交政策", "绿色金融", "媒介素养", "廉洁从业", "宏观政策", "形势教育", "公共关系", "物流与供应链", "战略人力", "基本理论", "产业创新", "企业管理", "党建", "改革发展", "个人修养", "党性修养", "政治能力" ]; for (const keyword of invalidKeywords) { if (lowerTitle.includes(keyword.toLowerCase())) { return null; } } const invalidPrefixes = [ "系统", "后台", "配置", "设置", "菜单" ]; for (const prefix of invalidPrefixes) { if (lowerTitle.startsWith(prefix.toLowerCase())) { return null; } } if (/^[a-f0-9]{32}$/i.test(lowerTitle) || /^[a-f0-9]{8}-[a-f0-9]{4}/i.test(lowerTitle)) { return null; } const titleLength = String(title).length; if (titleLength < 4 || titleLength > 50) { return null; } return this._buildCourseInfoFromApiData(data); }, async detectCourseStatusBatch(courseItems) { if (!Array.isArray(courseItems) || courseItems.length === 0) { console.warn("[CbeadScanner] 检测状态收到空课程列表"); return []; } console.log(`[CbeadScanner] 开始批量检测课程状态,共 ${courseItems.length} 个课程项`); const hasDomElements = courseItems.some(item => item && typeof item.querySelector === "function"); let statusResults; if (hasDomElements) { statusResults = CourseStatusDetector.detectBatch(courseItems); } else { console.log(`[CbeadScanner] 输入为课程对象数组,跳过状态检测`); statusResults = courseItems.map((item, index) => { const status = item.status || "not_started"; const progress = item.progress || 0; return { index: index, element: item.element || null, status: status, confidence: 100, isCompleted: item.isCompleted || progress >= 100, isInProgress: status === "in_progress", isNotStarted: status === "not_started", title: item.title || item.courseName || "未知课程", courseId: item.id || item.courseId || null }; }); } const coursesWithStatus = statusResults.map((result, index) => { const item = courseItems[index]; const title = result.title || item && (item.title || item.courseName) || this._safeExtractTitle(item); const courseId = result.courseId || item && (item.id || item.courseId) || this._safeExtractId(item); let status = "not_started"; let progress = 0; if (result.status === CourseStatusDetector.STATUS.COMPLETED) { status = "completed"; progress = 100; } else if (result.status === CourseStatusDetector.STATUS.IN_PROGRESS) { status = "in_progress"; progress = 50; } return { id: courseId, courseId: courseId, dsUnitId: courseId, title: title, courseName: title, link: item?.link || (courseId ? `#/study/course/detail/${courseId}` : null), element: item?.element || item, progress: progress, isCompleted: result.isCompleted, status: status, statusConfidence: result.confidence, statusSources: result.sources, source: "cbead_status_detector" }; }); const stats = CourseStatusDetector.getStatistics(statusResults); console.log(`[CbeadScanner] 状态检测统计: 全部${stats.total}, 已完成${stats.completed}, 学习中${stats.inProgress}, 未开始${stats.notStarted}, 平均置信度${stats.avgConfidence}%`); return coursesWithStatus; }, _safeExtractTitle(element) { if (!element || typeof element.querySelector !== "function") { return "未知课程"; } const config = CBEAD_CONSTANTS.BRANCH_LIST; const titleEl = element.querySelector(config.TITLE_SELECTOR); return titleEl?.textContent?.trim() || "未知课程"; }, _safeExtractId(element) { if (!element || typeof element.querySelector !== "function") { return null; } const linkEl = element.querySelector('a[href*="/study/course/detail/"]'); if (linkEl) { const href = linkEl.getAttribute("href"); const match = href.match(/detail\/([^&\/]+)/); if (match) return match[1]; } return element.getAttribute("data-id") || null; }, _extractIdFromElement(element) { const linkEl = element.querySelector('a[href*="/study/course/detail/"]'); if (linkEl) { const href = linkEl.getAttribute("href"); const match = href.match(/detail\/([^&\/]+)/); if (match) return match[1]; } return element.getAttribute("data-id") || null; }, async getLearningQueueWithSmartFilter(courseItems) { console.log("[CbeadScanner] 使用智能过滤获取学习队列..."); const courses = await this.detectCourseStatusBatch(courseItems); const toLearn = CourseStatusDetector.filterLearningNeeded(courses.map(c => ({ status: c.status, confidence: c.statusConfidence, title: c.title }))); const courseIdsToLearn = new Set(toLearn.map(t => t.title)); const learningQueue = courses.filter(c => courseIdsToLearn.has(c.title)); const stats = { total: courses.length, toLearn: learningQueue.length, skip: courses.length - learningQueue.length, byStatus: { completed: courses.filter(c => c.status === "completed").length, inProgress: courses.filter(c => c.status === "in_progress").length, notStarted: courses.filter(c => c.status === "not_started").length } }; console.log(`[CbeadScanner] 智能过滤结果: 共${stats.total}门, 待学习${stats.toLearn}门, 跳过${stats.skip}门`); return { courses: learningQueue, statistics: stats }; }, async validateAndInitializePage() { console.log("[CbeadScanner] 验证并初始化页面..."); const pageInfo = await BranchListValidator.validateAndGetInfo(); if (!pageInfo.isValid) { console.error("[CbeadScanner] 页面验证失败:", pageInfo.error); return { success: false, pageInfo: pageInfo }; } console.log("[CbeadScanner] 页面验证成功:", { "页码": pageInfo.pageNumber, "总页数": pageInfo.totalPages, "课程数": pageInfo.courseCount }); return { success: true, pageInfo: pageInfo }; } }; 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 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; } 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; } clearInterval(id) { clearInterval(id); this.intervals.delete(id); } clearTimeout(id) { clearTimeout(id); this.timeouts.delete(id); } clearAll() { this.intervals.forEach(id => clearInterval(id)); this.intervals.clear(); this.timeouts.forEach(id => clearTimeout(id)); this.timeouts.clear(); console.log("[CbeadPlayer] ✅ 已清理所有定时器"); } getCounts() { return { intervals: this.intervals.size, timeouts: this.timeouts.size }; } } const CbeadPlayer = { DEBUG: false, _debugLog(...args) { if (this.DEBUG) { console.log(...args); } }, detectVideoPlayer() { const videoJsPlayer = document.querySelector("video.vjs-tech"); if (videoJsPlayer) { console.log("[CbeadPlayer] 检测到 Video.js 播放器"); 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 (e) { console.warn("[CbeadPlayer] Video.js实例静音失败:", e); } } if (muted) { videoJsPlayer.volume = 0; } }, getMuted: () => videoJsPlayer.muted }; } const genericVideo = document.querySelector("video"); if (genericVideo) { console.log("[CbeadPlayer] 使用通用 video 元素"); 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 }; } console.warn("[CbeadPlayer] 未找到视频播放器"); return null; }, parseTimeToSeconds(timeStr) { if (!timeStr) return 0; const match = timeStr.match(/(\d+):(\d+)/); if (!match) return 0; const minutes = parseInt(match[1]); const seconds = parseInt(match[2]); return minutes * 60 + seconds; }, extractChapterProgress(verbose = true) { const catalogSelectors = [ ".course-side-catalog", ".new-course-side-catalog", ".course-catalog", '[class*="side-catalog"]', '[class*="catalog"]' ]; let catalog = null; for (const selector of catalogSelectors) { catalog = document.querySelector(selector); if (catalog) { if (verbose) console.log(`[CbeadPlayer] 使用目录选择器: ${selector}`); break; } } if (!catalog) { const allElements = document.querySelectorAll("*"); for (const el of allElements) { if (el.className && typeof el.className === "string" && el.className.includes("catalog")) { catalog = el; if (verbose) console.log(`[CbeadPlayer] 通过类名找到目录: ${el.className}`); break; } } } if (!catalog) { if (verbose) console.warn("[CbeadPlayer] 未找到章节目录"); return null; } const boxSelectors = [ ".chapter-list-box", '[class*="chapter-list-box"]', ".chapter-list", '[class*="chapter-box"]' ]; let chapterBoxes = null; for (const selector of boxSelectors) { chapterBoxes = catalog.querySelectorAll(selector); if (chapterBoxes.length > 0) { if (verbose) console.log(`[CbeadPlayer] 使用章节框选择器: ${selector}`); break; } } if (!chapterBoxes || chapterBoxes.length === 0) { const jspPane = catalog.querySelector(".jspPane"); if (jspPane) { chapterBoxes = jspPane.querySelectorAll(".chapter-list-box"); if (chapterBoxes.length > 0) { if (verbose) console.log("[CbeadPlayer] 从 .jspPane 中找到章节框"); } } } if (!chapterBoxes || chapterBoxes.length === 0) { const allBoxes = catalog.querySelectorAll('[class*="chapter"]'); const validBoxes = Array.from(allBoxes).filter(box => box.querySelector(".item-completed") || box.querySelector(".item.sub.item-completed") || box.textContent?.includes("完成率")); if (validBoxes.length > 0) { chapterBoxes = validBoxes; if (verbose) console.log(`[CbeadPlayer] 通过 item-completed 筛选找到 ${chapterBoxes.length} 个章节`); } } if (!chapterBoxes || chapterBoxes.length === 0) { if (verbose) console.warn("[CbeadPlayer] 未找到章节元素"); return null; } const chapters = []; chapterBoxes.forEach((box, index) => { try { const titleEl = box.querySelector(".chapter-item .text-overflow"); const title = titleEl?.textContent?.trim() || `章节${index + 1}`; this._debugLog(`\n[调试] 处理章节 ${index + 1}: ${title}`); const allItems = box.querySelectorAll(".item"); this._debugLog(` [调试] 找到 ${allItems.length} 个 .item 元素:`); allItems.forEach((el, i) => { const cls = el.className || ""; const text = el.textContent?.trim() || ""; this._debugLog(` [调试] .item[${i}]: class="${cls}", text="${text}"`); }); const statusEls = box.querySelectorAll(".item.item22"); this._debugLog(` [调试] .item.item22 数量: ${statusEls.length}`); const statusEl = statusEls[statusEls.length - 1]; const statusText = statusEl?.textContent?.trim() || ""; this._debugLog(` [调试] statusText: "${statusText}"`); const completedEl = box.querySelector(".item-completed") || box.querySelector(".item.sub.item-completed") || box.querySelector('[class*="item-completed"]') || box.querySelector(".item.sub"); const completedText = completedEl?.textContent?.trim() || ""; const progressMatch = completedText.match(/完成率[::]\s*(\d+)%/); const progress = progressMatch ? parseInt(progressMatch[1]) : 0; this._debugLog(` [调试] completedEl: ${completedEl ? "找到" : "未找到"}`); this._debugLog(` [调试] completedText: "${completedText}"`); this._debugLog(` [调试] progress: ${progress}%`); let status = "unknown"; let isCompleted = false; const normalizedStatusText = statusText.trim(); if (normalizedStatusText === "已完成") { status = "completed"; isCompleted = true; } else if (normalizedStatusText === "学习中") { status = "in_progress"; isCompleted = false; } else if (normalizedStatusText === "未开始") { status = "not_started"; isCompleted = false; } else { if (progress > 0) { status = "in_progress"; isCompleted = false; } else { status = "not_started"; isCompleted = false; } } const statusInfo = { completed: { icon: "✅", text: "已完成" }, in_progress: { icon: "📖", text: "学习中" }, not_started: { icon: "📝", text: "未开始" }, unknown: { icon: "❓", text: "未知" } }; const statusDisplay = statusInfo[status] || statusInfo["unknown"]; chapters.push({ index: index + 1, title: title, status: status, statusText: statusText, progress: progress, isCompleted: isCompleted, element: box }); if (verbose) { console.log(` ${statusDisplay.icon} 章节 ${index + 1}: ${title} - ${statusDisplay.text} (${progress}%)`); } } catch (error) { if (verbose) console.error(`[CbeadPlayer] 处理章节失败 (索引 ${index}):`, error); } }); if (chapters.length === 0) { if (verbose) console.warn("[CbeadPlayer] 未找到章节"); return 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] 找到 ${chapters.length} 个章节:`); console.log(` - ✅ 已完成: ${stats.completed} 门`); console.log(` - 📖 学习中: ${stats.inProgress} 门`); console.log(` - 📝 未开始: ${stats.notStarted} 门`); if (firstIncomplete) { const statusInfo = { completed: { icon: "✅", text: "已完成" }, in_progress: { icon: "📖", text: "学习中" }, not_started: { icon: "📝", text: "未开始" } }; const statusDisplay = statusInfo[firstIncomplete.status] || { text: "未知" }; console.log(`[CbeadPlayer] 💡 当前章节: ${firstIncomplete.index} - ${statusDisplay.text} (${firstIncomplete.progress}%)`); } } return { total: chapters.length, completed: stats.completed, inProgress: stats.inProgress, notStarted: stats.notStarted, chapters: chapters, firstIncomplete: firstIncomplete ?? null, allCompleted: firstIncomplete == null }; }, isCourseReallyCompleted() { const chapterProgress = this.extractChapterProgress(); if (!chapterProgress) { console.warn("[CbeadPlayer] 无法判断章节进度,假设未完成"); return false; } const isCompleted = chapterProgress.allCompleted; if (isCompleted) { console.log(`[CbeadPlayer] ✅ 所有章节已完成 (${chapterProgress.completed}/${chapterProgress.total})`); } else { const first = chapterProgress.firstIncomplete; if (first) { console.log(`[CbeadPlayer] 📖 章节 ${first.index} 未完成 (${first.status}, ${first.progress}%)`); console.log(`[CbeadPlayer] 💡 判断标准:章节右侧的"已完成"标记,而非完成率百分比`); } else { console.warn(`[CbeadPlayer] ⚠️ 章节状态异常: firstIncomplete为null但allCompleted为false`); console.log(`[CbeadPlayer] 📊 统计信息: ${chapterProgress.completed}已完成/${chapterProgress.total}总计`); return chapterProgress.completed >= chapterProgress.total; } } return isCompleted; }, extractChapterList() { const catalog = document.querySelector(".course-side-catalog"); if (!catalog) { console.warn("[CbeadPlayer] 未找到章节目录"); return []; } const chapters = []; const chapterBoxes = catalog.querySelectorAll(".chapter-list-box"); console.log(`[CbeadPlayer] 找到 ${chapterBoxes.length} 个章节`); chapterBoxes.forEach((box, index) => { try { const titleEl = box.querySelector(".chapter-item .text-overflow"); const title = titleEl?.textContent?.trim() || `第${index + 1}章`; const sections = box.querySelectorAll(".section-item-wrapper"); const sectionList = []; sections.forEach(section => { const durationText = section.querySelector(".section-item .item:last-child")?.textContent?.trim(); const duration = this.parseTimeToSeconds(durationText); const completedEl = section.closest(".chapter-list-box")?.querySelector(".item-completed"); const completedText = completedEl?.textContent?.trim() || ""; const progressMatch = completedText.match(/完成率[::]\s*(\d+)%/); const progress = progressMatch ? parseInt(progressMatch[1]) : 0; sectionList.push({ title: `${title} - 节`, duration: duration, progress: progress, isCompleted: progress >= 100 }); }); chapters.push({ title: title, sections: sectionList }); console.log(`[CbeadPlayer] 章节 ${index + 1}: ${title} (${sectionList.length} 节)`); } catch (error) { console.error(`[CbeadPlayer] 处理章节失败 (索引 ${index}):`, error); } }); console.log(`[CbeadPlayer] 成功提取 ${chapters.length} 个章节`); return chapters; }, clickChapter(chapterTitle) { try { console.log(`[CbeadPlayer] 🔍 查找章节: ${chapterTitle}`); 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)) { console.log(`[CbeadPlayer] ✅ 找到目标章节: ${title}`); box.click(); const playBtn = box.querySelector(".section-item"); if (playBtn) { playBtn.click(); } return true; } } console.warn(`[CbeadPlayer] 未找到章节: ${chapterTitle}`); return false; } catch (error) { console.error(`[CbeadPlayer] 点击章节失败:`, error); return false; } }, async waitForPlayerReady(maxWaitTime = CBEAD_CONSTANTS.TIMING.PLAYER_INIT_TIMEOUT, checkInterval = CBEAD_CONSTANTS.TIMING.PLAYER_CHECK_INTERVAL) { const startTime = Date.now(); const manager = new IntervalManager; console.log("[CbeadPlayer] ⏳ 等待播放器准备就绪..."); console.log(`[CbeadPlayer] 📋 配置: 最大等待=${maxWaitTime}ms, 检查间隔=${checkInterval}ms`); return new Promise(resolve => { manager.setInterval(() => { try { const elapsed = Date.now() - startTime; const player = this.detectVideoPlayer(); if (player) { const duration = player.getDuration(); const currentTime = player.getCurrentTime(); const isMuted = player.getMuted(); console.log(`[CbeadPlayer] 🔍 检查 #${Math.floor(elapsed / checkInterval)}: 播放器=${player.type}, duration=${duration}s, currentTime=${currentTime}s, muted=${player.getMuted()}`); if (duration && duration > 0 && isFinite(duration)) { manager.clearAll(); console.log(`[CbeadPlayer] ✅ 播放器已就绪 (等待 ${elapsed}ms)`); console.log(`[CbeadPlayer] 📹 播放器详情: type=${player.type}, duration=${Math.round(duration)}s, currentTime=${Math.round(currentTime)}s`); resolve(player); return; } else { console.log(`[CbeadPlayer] ⏳ duration 未就绪: ${duration} (需要 > 0)`); } } else { console.log(`[CbeadPlayer] ⏳ 检查 #${Math.floor(elapsed / checkInterval)}: 未检测到播放器`); } if (elapsed >= maxWaitTime) { manager.clearAll(); console.warn(`[CbeadPlayer] ⚠️ 播放器等待超时 (${maxWaitTime}ms)`); resolve(null); return; } } catch (error) { console.error("[CbeadPlayer] 播放器检测异常:", error); manager.clearAll(); resolve(null); } }, checkInterval); manager.setTimeout(() => { if (manager.getCounts().intervals > 0) { console.warn("[CbeadPlayer] ⏰ 超时保护触发,强制清理定时器"); manager.clearAll(); resolve(null); } }, maxWaitTime + CBEAD_CONSTANTS.TIMING.PROTECTION_TIMEOUT); }); }, getCurrentChapterName() { try { const chapterProgress = this.extractChapterProgress(false); if (chapterProgress && chapterProgress.chapters && chapterProgress.chapters.length > 0) { return chapterProgress.chapters[0].title; } } catch (error) {} return null; }, createIntervalManager() { return new IntervalManager; }, 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 (e) { return null; } }, cleanupPlaybackResources(resources, reason = "未知原因") { const {wakeLock: wakeLock, handleVisibilityChange: handleVisibilityChange, timerManager: timerManager} = resources; console.log(`[CbeadPlayer] 🧹 清理播放资源 (${reason})`); if (wakeLock) { try { wakeLock.release(); console.log("[CbeadPlayer] 📱 已释放屏幕常亮锁"); } catch (err) { console.warn("[CbeadPlayer] ⚠️ 释放 Wake Lock 失败:", err); } } if (handleVisibilityChange) { try { document.removeEventListener("visibilitychange", handleVisibilityChange); console.log("[CbeadPlayer] 🧹 已移除可见性监听器"); } catch (err) { console.warn("[CbeadPlayer] ⚠️ 移除可见性监听器失败:", err); } } if (timerManager) { try { timerManager.clearAll(); } catch (err) { console.warn("[CbeadPlayer] ⚠️ 清理定时器失败:", err); } } console.log("[CbeadPlayer] ✅ 资源清理完成"); } }; 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" }); }); } }; function debugLog(...args) {} const CBEAD_API_CONFIG = { baseUrl: null, endpoints: { GET_PLAY_TREND: "/inc/nc/course/play/getPlayTrend", GET_COURSE_LIST: "/inc/nc/course/getCourseList", PULSE_SAVE_RECORD: "/inc/nc/course/play/pulseSaveRecord", GET_STUDY_RECORD: "/inc/nc/course/getStudyRecord", GET_COURSEWARE_DETAIL: "/inc/nc/course/play/getCoursewareDetail", GET_PACK_BY_ID: "/inc/nc/course/pd/getPackById" }, getBaseUrl() { const config = ServiceLocator.get(ServiceNames.CONFIG); this.baseUrl = config?.CBEAD_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 = CBEAD_API_CONFIG.getUrl(endpoint); const queryString = new URLSearchParams({ ...params, _t: Date.now() }).toString(); return `${url}?${queryString}`; } const CbeadApi = { ...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); }, 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 })); if (response?.success && response?.data) { const data = response.data; let videoId = null; let duration = 0; let lastLearnedTime = 0; let foundCoursewareId = coursewareId; if (coursewareId && 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}`); } } if (!videoId && data.locationSite) { 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("无法获取真实 videoId,使用模拟ID"); } 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" }; } return null; } 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) { debugLog(`[进度同步] 失败: ${error.message}`); throw error; } }, 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 (coursewareId && data.playTree?.children) { const target = data.playTree.children.find(c => String(c.id) === String(coursewareId)); if (target) { const finishedRate = parseInt(target.finishedRate || 0); return { isCompleted: finishedRate >= (config?.COMPLETION_THRESHOLD || 80), finishedRate: finishedRate, 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_PLAY_TREND", { courseId: courseId }), _buildUrl("GET_COURSEWARE_DETAIL", { courseId: 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) => CourseAdapter.normalize({ id: courseId, courseId: courseId, dsUnitId: v.id, title: v.title || `${data.title || "课程"} - 视频${index + 1}`, duration: v.sumDurationLong || 0 }, "cbead_api_tree")); } } 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 }, "cbead_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, "cbead_api_sublist")); } } } catch { continue; } } return []; } catch (error) { debugLog(`获取课件列表失败: ${error.message}`); return []; } }, async getPackById(packId) { try { debugLog(`获取课程包信息 (ID: ${packId})`); return await this.get(_buildUrl("GET_PACK_BY_ID", { id: packId })); } catch (error) { debugLog(`获取课程包信息失败: ${error.message}`); return null; } } }; const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, NONE: 4 }; const defaultConfig$1 = { level: LOG_LEVELS.INFO, showTimestamp: true, showModule: true, disableInProduction: true }; let config$1 = { ...defaultConfig$1 }; 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$1.showTimestamp) { parts.push(`[${getTimestamp()}]`); } parts.push(`[${level}]`); if (config$1.showModule && module) { parts.push(`[${module}]`); } return [ parts.join(" "), ...args ]; } const Logger = { setLevel(level) { if (typeof level === "string") { const upperLevel = level.toUpperCase(); config$1.level = LOG_LEVELS[upperLevel] !== undefined ? LOG_LEVELS[upperLevel] : LOG_LEVELS.INFO; } else if (typeof level === "number") { config$1.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$1.level === LOG_LEVELS.INFO) { config$1.level = LOG_LEVELS.DEBUG; } } }, getLevel() { return config$1.level; }, debug(...args) { if (config$1.level <= LOG_LEVELS.DEBUG) { console.debug(...formatMessage("DEBUG", null, args)); } }, debugM(module, ...args) { if (config$1.level <= LOG_LEVELS.DEBUG) { console.debug(...formatMessage("DEBUG", module, args)); } }, info(...args) { if (config$1.level <= LOG_LEVELS.INFO) { console.info(...formatMessage("INFO", null, args)); } }, infoM(module, ...args) { if (config$1.level <= LOG_LEVELS.INFO) { console.info(...formatMessage("INFO", module, args)); } }, warn(...args) { if (config$1.level <= LOG_LEVELS.WARN) { console.warn(...formatMessage("WARN", null, args)); } }, warnM(module, ...args) { if (config$1.level <= LOG_LEVELS.WARN) { console.warn(...formatMessage("WARN", module, args)); } }, error(...args) { if (config$1.level <= LOG_LEVELS.ERROR) { console.error(...formatMessage("ERROR", null, args)); } }, errorM(module, ...args) { if (config$1.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$1.level <= levelValue) { console.log(...formatMessage(upperLevel, null, args)); } }, group(label, collapsed = false) { if (config$1.level <= LOG_LEVELS.DEBUG) { if (collapsed) { console.groupCollapsed(label); } else { console.group(label); } } }, groupEnd() { if (config$1.level <= LOG_LEVELS.DEBUG) { console.groupEnd(); } }, time(label) { if (config$1.level <= LOG_LEVELS.DEBUG) { console.time(label); } }, timeEnd(label) { if (config$1.level <= LOG_LEVELS.DEBUG) { console.timeEnd(label); } }, table(label, data) { if (config$1.level <= LOG_LEVELS.DEBUG && Array.isArray(data)) { this.group(label); console.table(data); this.groupEnd(); } }, configure(newConfig) { config$1 = { ...config$1, ...newConfig }; }, reset() { config$1 = { ...defaultConfig$1 }; } }; 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 WAIT_LOGGER = createLogger("WaitHelper"); const defaultConfig = { elementTimeout: 1e4, stableTimeout: 2e3, randomMin: 500, randomMax: 2e3, observerInterval: 100, retryCount: 3, retryDelay: 1e3 }; let config = { ...defaultConfig }; const WaitHelper = { async forElement(selector, timeout = config.elementTimeout) { const existing = document.querySelector(selector); if (existing) { WAIT_LOGGER.debug(`元素已存在: ${selector}`); return existing; } WAIT_LOGGER.debug(`等待元素: ${selector} (超时: ${timeout}ms)`); return new Promise((resolve, _reject) => { const observer = new MutationObserver(() => { const el = document.querySelector(selector); if (el) { observer.disconnect(); WAIT_LOGGER.debug(`找到元素: ${selector}`); resolve(el); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); const el = document.querySelector(selector); if (el) { resolve(el); } else { WAIT_LOGGER.warn(`等待元素超时: ${selector}`); resolve(null); } }, timeout); }); }, async forElements(selector, minCount = 1, timeout = config.elementTimeout) { const checkElements = () => { const elements = document.querySelectorAll(selector); return elements.length >= minCount ? elements : null; }; const existing = checkElements(); if (existing) { WAIT_LOGGER.debug(`元素已满足条件: ${selector} (${existing.length} 个)`); return existing; } WAIT_LOGGER.debug(`等待至少 ${minCount} 个元素: ${selector}`); return new Promise(resolve => { const observer = new MutationObserver(() => { const elements = checkElements(); if (elements) { observer.disconnect(); WAIT_LOGGER.debug(`找到 ${elements.length} 个元素: ${selector}`); resolve(elements); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); const elements = checkElements(); if (elements) { resolve(elements); } else { WAIT_LOGGER.warn(`等待元素超时: ${selector}`); resolve(null); } }, timeout); }); }, async forStable(timeout = config.stableTimeout) { WAIT_LOGGER.debug(`等待页面稳定 (${timeout}ms)`); let stableTime = 0; let lastState = document.body?.innerHTML?.length || 0; return new Promise(resolve => { const observer = new MutationObserver(() => { stableTime = 0; lastState = document.body?.innerHTML?.length || 0; }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true }); } const interval = setInterval(() => { const currentState = document.body?.innerHTML?.length || 0; if (Math.abs(currentState - lastState) > 100) { stableTime = 0; lastState = currentState; } else { stableTime += config.observerInterval; } if (stableTime >= timeout) { clearInterval(interval); observer.disconnect(); WAIT_LOGGER.debug("页面已稳定"); resolve(true); } }, config.observerInterval); setTimeout(() => { clearInterval(interval); observer.disconnect(); if (stableTime >= timeout / 2) { WAIT_LOGGER.debug("页面接近稳定,继续执行"); resolve(true); } else { WAIT_LOGGER.warn("页面稳定超时"); resolve(false); } }, timeout + 1e3); }); }, async forVue(options = {}) { const {vueTimeout: vueTimeout = 12e3, skeletonTimeout: skeletonTimeout = 15e3, contentTimeout: contentTimeout = 15e3} = options; WAIT_LOGGER.debug("等待 Vue 组件就绪"); if (window.Vue || document.querySelector("[data-v-]")?.__vue__) { WAIT_LOGGER.debug("检测到 Vue 实例"); return true; } const skeletonSelectors = [ ".el-loading-mask", ".v-loading", ".ant-spin-mask", ".loading-mask", '[class*="loading"]' ]; const startTime = Date.now(); return new Promise(resolve => { const checkVueReady = () => { const elapsed = Date.now() - startTime; if (window.Vue || document.querySelector("[data-v-]")?.__vue__) { return { ready: true, reason: "Vue 实例检测到" }; } const contentSelectors = [ ".el-main", ".main-content", ".content-area", ".app-main" ]; for (const selector of contentSelectors) { const el = document.querySelector(selector); if (el && el.children.length > 0) { return { ready: true, reason: `内容区域 ${selector} 有内容` }; } } let hasSkeleton = false; for (const selector of skeletonSelectors) { if (document.querySelector(selector)) { hasSkeleton = true; break; } } if (!hasSkeleton && elapsed > skeletonTimeout) { return { ready: true, reason: "骨架屏超时但无 Vue" }; } if (elapsed > contentTimeout) { return { ready: true, reason: "内容加载超时,继续执行" }; } return { ready: false }; }; const interval = setInterval(() => { const result = checkVueReady(); if (result.ready) { clearInterval(interval); WAIT_LOGGER.debug(`Vue 就绪: ${result.reason}`); resolve(true); } }, 200); setTimeout(() => { clearInterval(interval); const result = checkVueReady(); if (result.ready) { resolve(true); } else { WAIT_LOGGER.warn("Vue 等待超时,继续执行"); resolve(true); } }, Math.max(vueTimeout, skeletonTimeout, contentTimeout)); }); }, async random(min = config.randomMin, max = config.randomMax) { const delay = Math.floor(Math.random() * (max - min + 1)) + min; WAIT_LOGGER.debug(`随机延时: ${delay}ms`); return new Promise(resolve => setTimeout(resolve, delay)); }, delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }, async retry(asyncFn, options = {}) { const {count: count = config.retryCount, delay: retryDelay = config.retryDelay, onRetry: onRetry = null} = options; let lastError; for (let i = 0; i < count; i++) { try { return await asyncFn(); } catch (error) { lastError = error; WAIT_LOGGER.warn(`重试 ${i + 1}/${count} 失败: ${error.message}`); if (onRetry) { await onRetry(error, i + 1); } if (i < count - 1) { await this.delay(retryDelay); } } } throw lastError; }, async forIframe(selector, timeout = config.elementTimeout) { const iframe = await this.forElement(selector, timeout); if (!iframe) { return null; } if (iframe.contentDocument?.readyState === "complete") { return iframe; } return new Promise(resolve => { const onLoad = () => { resolve(iframe); }; iframe.addEventListener("load", onLoad); setTimeout(() => { iframe.removeEventListener("load", onLoad); WAIT_LOGGER.warn(`iframe 加载超时: ${selector}`); resolve(iframe); }, timeout); }); }, configure(newConfig) { config = { ...config, ...newConfig }; }, reset() { config = { ...defaultConfig }; } }; 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(); let chapterName = course.title; let currentChapterIndex = null; try { console.log(`[CbeadPlayerFlow] ⏳ 等待章节目录加载...`); const catalogSelectors = [ ".course-side-catalog", ".new-course-side-catalog", ".course-catalog" ]; let catalogLoaded = false; for (const selector of catalogSelectors) { const catalog = await WaitHelper.forElement(selector, 5e3); if (catalog) { console.log(`[CbeadPlayerFlow] ✅ 章节目录已加载: ${selector}`); catalogLoaded = true; break; } } if (!catalogLoaded) { console.warn(`[CbeadPlayerFlow] ⚠️ 章节目录未加载,尝试继续提取`); } let chapterProgress = null; for (let i = 0; i < 2; i++) { chapterProgress = 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 = 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 }; } } if (initialChapterProgress && currentChapterIndex !== null) { const targetChapter = initialChapterProgress.chapters.find(ch => ch.index === currentChapterIndex); if (targetChapter) { console.log(`[CbeadPlayerFlow] 📖 目标章节: ${targetChapter.title}, 状态: ${targetChapter.status}`); const currentVideo = document.querySelector("video"); const currentPlayer = CbeadPlayer.detectVideoPlayer(); if (currentPlayer && currentVideo) { currentPlayer.getCurrentTime(); currentPlayer.getDuration(); 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)); } else { console.warn(`[CbeadPlayerFlow] ⚠️ 章节切换失败,可能已在正确章节`); } } else { console.log(`[CbeadPlayerFlow] ✅ 服务器进度 ${targetServerProgress}%,似乎在正确的章节`); } } } } try { console.log(`[CbeadPlayerFlow] ⏳ 步骤 1/7: 等待播放器初始化...`); const player = await CbeadPlayer.waitForPlayerReady(); if (!player) { console.error(`[CbeadPlayerFlow] ❌ 播放器初始化失败`); throw new Error("播放器初始化超时,无法获取视频信息"); } console.log(`[CbeadPlayerFlow] ✅ 步骤 1/7: 播放器初始化完成`); console.log(`[CbeadPlayerFlow] 📹 播放器类型: ${player.type}`); console.log(`[CbeadPlayerFlow] ⏳ 步骤 2/7: 获取视频信息...`); const duration = player.getDuration(); const currentTime = player.getCurrentTime(); const durationMinutes = Math.round(duration / 60); const serverProgress = CbeadPlayer.getServerProgress(currentChapterIndex); 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 infraDomHelper; }); 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", () => { console.log(`[CbeadPlayerFlow] ========== 播放完成 ==========`); console.log(`[CbeadPlayerFlow] ✅ 视频播放完成!`); if (heartbeatWorker) { CbeadHeartbeatWorker.stop(heartbeatWorker); heartbeatWorker = null; } if (wakeLock) { wakeLock.release(); wakeLock = null; } document.removeEventListener("visibilitychange", handleVisibilityChange); 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: chapterCompletedDetected ? "章节状态变更为已完成" : "视频播放完成" }); }, { 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); reject(new Error("视频播放出错")); }, { once: true }); timerManager.setInterval(() => { if (learningState.isFailed()) { console.warn(`[CbeadPlayerFlow] 🚨 检测到课程失败,立即停止播放`); if (heartbeatWorker) { CbeadHeartbeatWorker.stop(heartbeatWorker); heartbeatWorker = null; } timerManager.clearAll(); if (wakeLock) wakeLock.release(); document.removeEventListener("visibilitychange", handleVisibilityChange); currentPlayer.pause(); reject(new Error(`进度上报失败: ${learningState.getFailureReason()}`)); return; } const currentTime = currentPlayer.getCurrentTime(); const serverProgress = CbeadPlayer.getServerProgress(currentChapterIndex); console.log(`[CbeadPlayerFlow] 📊 已播放: ${Math.round(currentTime)}秒 / ${Math.round(duration)}秒 (服务器进度: ${serverProgress !== null ? serverProgress + "%" : "N/A"})`); 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(() => { if (learningState.isFailed()) { console.warn(`[CbeadPlayerFlow] 🚨 检测到课程失败,立即停止播放`); timerManager.clearAll(); if (wakeLock) wakeLock.release(); document.removeEventListener("visibilitychange", handleVisibilityChange); currentPlayer.pause(); reject(new Error(`进度上报失败: ${learningState.getFailureReason()}`)); return; } const serverProgress = CbeadPlayer.getServerProgress(currentChapterIndex); if (serverProgress !== null && serverProgress < CBEAD_CONSTANTS.THRESHOLDS.CHAPTER_CHECK_MIN_PROGRESS) { return; } console.log(`[CbeadPlayerFlow] 🔍 检查章节状态 (服务器进度: ${serverProgress}%)...`); const chapterProgress = CbeadPlayer.extractChapterProgress(false); if (chapterProgress && chapterProgress.completed === chapterProgress.total) { console.log(`[CbeadPlayerFlow] 🎉 检测到所有章节已完成!`); chapterCompletedDetected = true; if (heartbeatWorker) { CbeadHeartbeatWorker.stop(heartbeatWorker); heartbeatWorker = null; } timerManager.clearAll(); if (wakeLock) wakeLock.release(); document.removeEventListener("visibilitychange", handleVisibilityChange); currentPlayer.pause(); currentPlayer.setCurrentTime(duration); 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; } timerManager.clearAll(); if (wakeLock) wakeLock.release(); document.removeEventListener("visibilitychange", handleVisibilityChange); currentPlayer.pause(); currentPlayer.setCurrentTime(duration); 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) { timerManager.clearAll(); console.error(`[CbeadPlayerFlow] 学习失败: ${course.title}`, error); throw error; } } }; const CbeadHandler = { PAGE_TYPES: { PLAYER: "player", BRANCH_LIST: "branch_list", COLUMN: "column", HOME_V: "home_v", UNKNOWN: "unknown" }, 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: { PLAYER: "player", BRANCH_LIST: "branch_list", COLUMN: "column", HOME_V: "home_v", UNKNOWN: "unknown" } }), isCbeadMode() { return CONFIG.CBEAD_MODE === true; }, extractId() { const hash = window.location.hash; const match = hash.match(/class-detail\/([a-f0-9-]+)/); if (match) return match[1]; const courseMatch = hash.match(/course\/detail\/[\d&]+\/([a-f0-9-]+)/); if (courseMatch) return courseMatch[1]; return null; }, getCourses() { return CbeadScanner.getCourses(this.SELECTORS); }, scanCoursesFromColumnPage() { return CbeadScanner.scanCoursesFromColumnPage(); }, async scanCoursesFromBranchList(options = {}) { return await CbeadScanner.scanCoursesFromBranchList(options); }, async scanCoursesFromBranchListPage() { return await CbeadScanner.scanCoursesFromBranchListPage(); }, async clickNextPage() { return await CbeadScanner._clickNextPage(); }, getTotalPages() { return CbeadScanner._getTotalPages(); }, categorizeAndSortCourses(courses) { return CbeadScanner.categorizeAndSortCourses(courses); }, getSortedLearningList(courses) { return CbeadScanner.getSortedLearningList(courses); }, saveLearningProgress(learningList, currentIndex) { return CbeadProgressManager.saveLearningProgress(learningList, currentIndex); }, loadLearningProgress() { return CbeadProgressManager.loadLearningProgress(); }, clearLearningProgress() { return CbeadProgressManager.clearLearningProgress(); }, hasPendingLearningTask() { return CbeadProgressManager.hasPendingLearningTask(); }, saveLearningQueue(learningList, totalCourses, pageUrl) { return CbeadProgressManager.saveLearningQueue(learningList, totalCourses, pageUrl); }, loadLearningQueue() { return CbeadProgressManager.loadLearningQueue(); }, updateCurrentIndex(newIndex) { return CbeadProgressManager.updateCurrentIndex(newIndex); }, hasValidQueue(currentUrl) { return CbeadProgressManager.hasValidQueue(currentUrl); }, returnToList(returnUrl) { return CbeadProgressManager.returnToList(returnUrl); }, publishCompletionStats(totalCourses) { return CbeadProgressManager.publishCompletionStats(totalCourses); }, normalizeCourseId(rawId) { return CbeadProgressManager.normalizeCourseId(rawId); }, extractPlayerParams() { return CbeadProgressManager.extractPlayerParams(); }, detectVideoPlayer() { return CbeadPlayer.detectVideoPlayer(); }, extractChapterProgress(verbose) { return CbeadPlayer.extractChapterProgress(verbose); }, isCourseReallyCompleted() { return CbeadPlayer.isCourseReallyCompleted(); }, extractChapterList() { return CbeadPlayer.extractChapterList(); }, 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; }, parseTimeToSeconds(timeStr) { return CbeadPlayer.parseTimeToSeconds(timeStr); }, waitForPlayerReady(maxWaitTime, checkInterval) { return CbeadPlayer.waitForPlayerReady(maxWaitTime, checkInterval); }, clickChapter(chapterTitle) { return CbeadPlayer.clickChapter(chapterTitle); }, getServerProgress(currentChapterIndex = null) { return CbeadPlayer.getServerProgress(currentChapterIndex); }, cleanupPlaybackResources(resources, reason) { return CbeadPlayer.cleanupPlaybackResources(resources, reason); }, getCurrentChapterName() { return CbeadPlayer.getCurrentChapterName(); }, getLearningState() { return CbeadProgressManager.getLearningState(); }, createIntervalManager() { return CbeadPlayer.createIntervalManager(); }, async learnWithRealPlayback(course) { return await CbeadPlayerFlow.learnWithRealPlayback(course); }, init() { if (!this.isCbeadMode()) { console.log("[CbeadHandler] 非企业分院环境,跳过初始化"); return; } const pageType = this.identifyPage(); console.log(`[CbeadHandler] 检测到页面类型: ${pageType}`); EventBus.publish("cbead:pageDetected", { pageType: pageType, url: window.location.href, id: this.extractId() }); } }; const IEnvironment = { PAGE_TYPES: { INDEX: "index", COLUMN: "column", PLAYER: "player", UNKNOWN: "unknown" }, identifyPage: null, isEnvironmentMode: null, init: null, getCourses: null, scanCourses: null, startPlayerFlow: null, getPlayInfo: null, reportProgress: null, checkCompletion: null }; const ENVIRONMENT_IDS = { PUDONG: "pudong", CBEAD: "cbead", DX: "dx" }; const GwypxPlayer = { ...CbeadPlayer, SELECTORS: { ...CbeadPlayer.SELECTORS, ALI_PLAYER: ".prism-player", ALI_VIDEO: ".prism-player video", COMPLETED_TAG: ".el-tag--success", PROGRESS_TEXT: ".progress-text" }, extractChapterProgress(verbose = false) { const pageText = document.body.innerText; const progressMatch = pageText.match(/完成度[::]\s*(\d+)%/); if (progressMatch) { return parseInt(progressMatch[1]); } return CbeadPlayer.extractChapterProgress(verbose); }, isCourseReallyCompleted() { const tags = Array.from(document.querySelectorAll(".el-tag")); const hasCompletedTag = tags.some(tag => tag.innerText.includes("已完成")); if (hasCompletedTag) return true; const progress = this.extractChapterProgress(); if (progress >= 100) return true; return false; } }; const GwypxPlayerFlow = { ...CbeadPlayerFlow, _getPlayer() { return GwypxPlayer; } }; const GWYPX_PATH_PATTERNS = { INDEX: "/pcPage/index", CENTER: "/pcPage/personalCenter", PLAYER: "/pcPage/commend/coursedetail" }; const GWYPX_PAGE_TYPES = { INDEX: "index", PLAYER: "player", CENTER: "personal_center", UNKNOWN: "unknown" }; const GwypxHandler = { ...IEnvironment, PAGE_TYPES: GWYPX_PAGE_TYPES, identifyPage: createPageDetector({ pathPatterns: GWYPX_PATH_PATTERNS, pageTypes: GWYPX_PAGE_TYPES, domSelectors: [ "video.vjs-tech", ".prism-player", ".aliplayer-container" ], domMatchType: "PLAYER" }), isCourseReallyCompleted() { return GwypxPlayer.isCourseReallyCompleted(); }, async learnWithRealPlayback(course) { return await GwypxPlayerFlow.learnWithRealPlayback(course); }, extractPageCourseTitle() { const titleEl = document.querySelector(".course-name, .title, h1"); return titleEl ? titleEl.textContent.trim() : document.title; }, init() { if (!CONFIG.GWYPX_MODE) return; console.log("[GwypxHandler] 初始化党校分院处理器"); } }; let currentPlayerLearningId = null; const CbeadLearner = { get _pageValidator() { return createPageValidator({ whitelist: CONSTANTS.PAGE_TYPE_WHITELIST.CBEAD, pageTypes: CbeadHandler.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("train-new/class-detail")) { return await this._handleColumnPage(); } if (href.includes("branch-list-v")) { return await this._handleBranchListPage(); } return null; }, async _handlePlayerPage() { return await this.startPlayerFlow(); }, async _handleColumnPage() { const {findSignUpButton: findSignUpButton, getSignUpButtonText: getSignUpButtonText} = await Promise.resolve().then(function() { return infraDomHelper; }); 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 = sessionStorage.getItem("cbeadLearnSourceUrl"); if (!sourceUrl) { const currentUrl = window.location.href; sessionStorage.setItem("cbeadLearnSourceUrl", currentUrl); console.log(`[CbeadLearner] 📌 保存来源 URL: ${currentUrl}`); } await this.startBatchFlow(); return true; }, async _handleBranchListPage() { const oldSavedUrl = sessionStorage.getItem("cbeadBranchListUrl"); if (oldSavedUrl) { console.log(`[CbeadLearner] 🧹 清除旧的分支列表页URL保存`); sessionStorage.removeItem("cbeadBranchListUrl"); } const sourceUrl = sessionStorage.getItem("cbeadLearnSourceUrl"); if (!sourceUrl) { console.log(`[CbeadLearner] 💡 进入列表页,请点击"开始学习"按钮开始批量学习`); return CONSTANTS.FLOW_RESULTS.WAITING_FOR_USER; } console.log(`[CbeadLearner] 🔄 检测到批量学习流程,继续执行...`); await this.startBranchListFlow(); return true; }, async startPlayerFlow() { 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 = 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" }); this._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 = 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 bizLearner; }); await Learner.startLearning(); return true; } console.log("[CbeadLearner] ✅ 课程学习完成"); this.setCurrentPlayerLearningId(null); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "✅ 当前视频学习完成!", type: "success" }); const returnUrl = sessionStorage.getItem("cbeadLearnSourceUrl"); if (!returnUrl) { console.warn("[CbeadLearner] ⚠️ 未找到来源 URL,无法返回继续学习"); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "⚠️ 学习完成,但无法返回来源页面", type: "warn" }); this._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; } this._resetToggleButton("学习完成"); return false; }, async _handleVideoFailed() { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "❌ 视频学习失败", type: "error" }); this._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" }); await this._waitForPageReady(); const scanPage = this._loadScanPage(); let currentPage = scanPage; console.log(`[CbeadLearner] 📄 当前扫描页码: ${currentPage}`); const result = await this._scanAndLearnBranchList(currentPage); if (result && result.completed) { this._clearScanPage(); try { sessionStorage.removeItem("cbeadLearnSourceUrl"); console.log(`[CbeadLearner] 🧹 清除来源 URL`); } catch (e) { console.warn("[CbeadLearner] 清除来源 URL 失败:", e); } EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "✅ 所有课程已完成!", type: "success" }); this._resetToggleButton("学习完成"); } return true; }, async _scanAndLearnBranchList(startPage) { const pageCourses = await CbeadScanner.scanCoursesFromBranchListPage(); if (!pageCourses || pageCourses.length === 0) { console.log("[CbeadLearner] 当前页没有找到课程"); const hasNext = await CbeadScanner._clickNextPage(); if (hasNext) { return { continue: true, nextPage: startPage + 1 }; } else { return { completed: true }; } } const incompleteCourses = pageCourses.filter(c => c.progress < 100); if (incompleteCourses.length > 0) { const currentUrl = window.location.href; sessionStorage.setItem("cbeadLearnSourceUrl", currentUrl); console.log(`[CbeadLearner] 📌 保存来源 URL: ${currentUrl}`); await this._navigateToCourse(incompleteCourses[0]); return { continue: true, nextPage: startPage }; } const hasNext = await CbeadScanner._clickNextPage(); if (hasNext) { const loaded = await this._waitForPageLoad(); if (loaded) { return await this._scanAndLearnBranchList(startPage + 1); } return { continue: true, nextPage: startPage + 1 }; } else { return { completed: true }; } }, _loadScanPage() { try { const data = sessionStorage.getItem("cbeadScanPage"); if (data) { return parseInt(data, 10); } } catch (e) { console.warn("[CbeadLearner] 读取扫描页码失败:", e); } return 1; }, _saveScanPage(page) { try { sessionStorage.setItem("cbeadScanPage", page.toString()); } catch (e) { console.warn("[CbeadLearner] 保存扫描页码失败:", e); } }, _clearScanPage() { try { sessionStorage.removeItem("cbeadScanPage"); } catch (e) { console.warn("[CbeadLearner] 清除扫描页码失败:", e); } }, _clearBranchListUrl() { try { sessionStorage.removeItem("cbeadBranchListUrl"); } catch (e) { console.warn("[CbeadLearner] 清除分支列表页URL失败:", e); } }, async _waitForPageLoad() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const timeout = CBEAD_CONSTANTS.TIMEOUT?.PAGE_LOAD; console.log("[CbeadLearner] ⏳ 等待翻页加载完成..."); const startTime = Date.now(); while (Date.now() - startTime < timeout) { const container = document.querySelector(config.CONTAINER_SELECTOR); if (container) { const items = container.querySelectorAll(config.ITEM_SELECTOR); if (items.length > 0) { console.log(`[CbeadLearner] ✅ 翻页加载完成,找到 ${items.length} 个课程项`); return true; } } await new Promise(resolve => setTimeout(resolve, 500)); } console.warn("[CbeadLearner] ⚠️ 翻页加载超时,继续执行"); return false; }, async _waitForPageReady() { const config = CBEAD_CONSTANTS.BRANCH_LIST; const timeout = CBEAD_CONSTANTS.TIMEOUT?.PAGE_LOAD; console.log("[CbeadLearner] ⏳ 等待页面渲染完成..."); const startTime = Date.now(); while (Date.now() - startTime < timeout) { const container = document.querySelector(config.CONTAINER_SELECTOR); if (container) { const items = container.querySelectorAll(config.ITEM_SELECTOR); if (items.length > 0) { console.log(`[CbeadLearner] ✅ 页面渲染完成,找到 ${items.length} 个课程项`); return true; } } await new Promise(resolve => setTimeout(resolve, 500)); } console.warn("[CbeadLearner] ⚠️ 等待页面渲染超时,继续执行"); return false; }, 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) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `\n📖 开始学习: ${course.title} (${course.progress}%)`, type: "info" }); const currentUrl = window.location.href; const sourceUrl = `#${currentUrl.split("#")[1] || ""}`; sessionStorage.setItem("cbeadLearnSourceUrl", sourceUrl); console.log(`[CbeadLearner] 📌 保存来源 URL: ${sourceUrl}`); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `📌 保存来源页: ${sourceUrl}`, type: "info" }); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `🔄 正在跳转到播放页...`, type: "info" }); await new Promise(resolve => setTimeout(resolve, 1e3)); window.location.href = course.link; }, _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); } }; var learner = 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" }, 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 findDeep = (obj, target) => { if (!obj || typeof obj !== "object") return null; if (obj[target]) return obj[target]; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const res = findDeep(obj[key], target); if (res) return res; } } return null; }; if (window.userInfo?.idCardHash) { hash = window.userInfo.idCardHash; source = "window.userInfo"; } else if (window.traineeInfo?.idCardHash) { hash = window.traineeInfo.idCardHash; source = "window.traineeInfo"; } else if (window.subSiteConfig) { hash = window.subSiteConfig.idCardHash || window.subSiteConfig.data?.idCardHash || window.subSiteConfig.data?.data?.idCardHash; if (hash) { source = "window.subSiteConfig(nested)"; } else { hash = findDeep(window.subSiteConfig, "idCardHash"); if (hash) source = "window.subSiteConfig(deep)"; } } } if (!hash) { try { const app = document.querySelector("#app"); const vue = app?.__vue__ || app?.__vue_app__; const store = vue?.$store || vue?.store; if (store?.state) { const findInObj = (obj, targetKey) => { if (!obj || typeof obj !== "object") return null; if (obj[targetKey]) return obj[targetKey]; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const found = findInObj(obj[key], targetKey); if (found) return found; } } return null; }; const found = findInObj(store.state, "idCardHash"); if (found) { hash = found; source = "Vuex"; } } } 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; }, interceptIdCardHash(payload) { if (typeof payload === "string" && payload.includes("idCardHash")) { try { const data = JSON.parse(payload); if (data.idCardHash) { console.log("[GwypxApi] 拦截到 idCardHash:", data.idCardHash); localStorage.setItem("idCardHash", data.idCardHash); } } catch (e) {} } }, _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 = response.data.progress || 0; 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 queryRel(courseId) { Validator.validateCourseId(courseId, "queryRel"); const url = GWYPX_API_CONFIG.getUrl("COURSE_QUERY_REL"); const payload = { courseId: courseId, idCardHash: this._getIdCardHash() }; console.log(`[GwypxApi] 查询课程关联状态: ${courseId}`); return await this.post(url, JSON.stringify(payload)); }, async addRel(courseId) { Validator.validateCourseId(courseId, "addRel"); const url = GWYPX_API_CONFIG.getUrl("COURSE_ADD_REL"); const payload = { courseId: courseId, idCardHash: this._getIdCardHash() }; console.log(`[GwypxApi] 建立课程关联: ${courseId}`); return await this.post(url, JSON.stringify(payload)); }, 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 }; console.log(`[GwypxApi] 发送完成确认 (v2/end): ${playInfo.courseId}, tbtpId: ${payload.tbtpId}`); 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)); } }; 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 }; console.log("[GwypxStateManager] 🔓 全局状态已重置"); } } cleanup() { delete window[this.STORAGE_KEY]; console.log("[GwypxStateManager] 🗑️ 全局状态已清理"); } 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() { const state = this.getState(); return state.stopRequested; } isRunning() { const state = this.getState(); return state.isRunning; } incrementLoopCount() { const state = this.getState(); state.loopCount = (state.loopCount || 0) + 1; return state.loopCount; } getLoopCount() { const state = this.getState(); return state.loopCount || 0; } exceedsMaxLoops(maxLoops = LEARNER_CONSTANTS.MAX_SYNC_LOOPS) { return this.getLoopCount() >= maxLoops; } } const stateManager = new GlobalStateManager; const GwypxLearner = { get _pageValidator() { return createPageValidator({ whitelist: CONSTANTS.PAGE_TYPE_WHITELIST.GWYPX, pageTypes: GwypxHandler.PAGE_TYPES, customMessages: { index: "⚠️ 党校分院首页暂不支持自动学习。请进入课程播放页或个人中心。", default: "⚠️ 当前页面不支持自动学习。请进入课程播放页或个人中心。" } }); }, _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 this.startPlayerFlow(); } if (pageType === GwypxHandler.PAGE_TYPES.CENTER) { return await this.startCategoryBatchFlow(); } return null; }, async startPlayerFlow() { if (stateManager.isRunning()) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "⚠️ 学习流程已在运行中,跳过重复启动", 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: "❌ 无法识别课程ID", type: "error" }); return false; } EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `🎬 开始党校分院播放页流程 (ID: ${courseId})`, type: "info" }); const playInfo = await GwypxApi.getPlayInfo(courseId); if (!playInfo) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "❌ 获取播放信息失败,可能未识别学员身份", type: "error" }); return false; } playInfo.tbtpId = tbtpId; const domProgressAtStart = this._extractProgressFromDom(); if (playInfo.progress >= 100 || domProgressAtStart >= 100) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "✅ 该课程已完成 (100%)!正在执行最终确认...", type: "success" }); const endResult = await GwypxApi.reportEnd(playInfo); if (GwypxApi.isSuccessResponse(endResult)) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "🎊 课程已确认结课!", type: "success" }); } this._resetToggleButton("学习完成"); return true; } const {detectMask: detectMask} = await Promise.resolve().then(function() { return infraDomHelper; }); const mask = detectMask(); if (mask.exists) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "🛡️ 检测到系统弹窗,等待自动处理...", 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: "🖱️ 正在自动关联课程 (建立学习关系)...", 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: "🛰️ 已发送开始学习信号", type: "info" }); this._clickPlayButton(); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "🚀 启动持续进度同步机制...", type: "info" }); let cumulativeTime = (playInfo.studyTimes || 0) + LEARNER_CONSTANTS.INITIAL_TIME_OFFSET; while (true) { if (stateManager.shouldStop()) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "⏹️ 收到停止请求,终止进度同步", type: "warn" }); return false; } if (stateManager.exceedsMaxLoops()) { const elapsed = Math.floor((Date.now() - stateManager.getState().startTime) / 1e3); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `⚠️ 已达到最大同步次数限制 (${LEARNER_CONSTANTS.MAX_SYNC_LOOPS}次),已运行 ${elapsed} 秒。为防止内存溢出已停止。如果进度未满,请刷新页面继续。`, type: "error" }); this._resetToggleButton("同步超时"); return false; } stateManager.incrementLoopCount(); const result = await GwypxApi.reportProgress(playInfo, cumulativeTime, LEARNER_CONSTANTS.PROGRESS_INCREMENT); const domProgress = this._extractProgressFromDom(); const apiProgress = parseInt(result?.courseProgress?.percentage || result?.data?.percentage || 0); const currentProgress = Math.max(domProgress, apiProgress); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `📊 进度 ${currentProgress}% (页面${domProgress}%, API${apiProgress}%) - 循环 ${stateManager.getLoopCount()}/${LEARNER_CONSTANTS.MAX_SYNC_LOOPS}`, type: "info" }); EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, `学习中 ${currentProgress}%`); if (currentProgress >= 100) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "✅ 课程已达到 100% 完成!正在进行最终确认...", type: "success" }); const endResult = await GwypxApi.reportEnd(playInfo); if (GwypxApi.isSuccessResponse(endResult)) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "🎊 课程已成功结课!", type: "success" }); } this._resetToggleButton("学习完成"); return true; } 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("[GwypxLearner] startPlayerFlow error:", error); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `❌ 学习流程异常: ${error.message}`, type: "error" }); return false; } finally { stateManager.resetState(); } }, _extractProgressFromDom() { const progressEl = document.querySelector(".el-progress__text"); if (progressEl) { const text = progressEl.textContent || ""; const match = text.match(/(\d+)%/); if (match) return parseInt(match[1]); } return 0; }, _clickPlayButton() { console.log("[GwypxLearner] ===== 播放按钮检测开始 ====="); const video = document.querySelector("video"); if (video) { video.muted = true; console.log("[GwypxLearner] 🔇 已静音"); } const aliPlayer = document.querySelector(".prism-player"); const videoJsPlayer = document.querySelector(".video-js"); console.log(`[GwypxLearner] 🎬 播放器: ${videoJsPlayer ? "Video.js" : aliPlayer ? "Aliplayer" : "未知"}`); const bigPlayBtn = document.querySelector(".vjs-big-play-button"); console.log(`[GwypxLearner] 🔍 大播放按钮: ${bigPlayBtn ? "✓" : "✗"}`); let clicked = false; if (bigPlayBtn && bigPlayBtn.offsetParent !== null) { console.log("[GwypxLearner] 🎯 点击大播放按钮"); bigPlayBtn.click(); clicked = true; } if (!clicked && video) { console.log("[GwypxLearner] 🎯 直接 video.play()"); const playPromise = video.play(); if (playPromise !== undefined) { playPromise.then(() => { console.log("[GwypxLearner] ✅ 播放成功"); }).catch(error => { console.warn("[GwypxLearner] ⚠️ 播放失败:", error.message); }); } clicked = true; } if (!clicked) { console.warn("[GwypxLearner] ⚠️ 未找到播放按钮"); } console.log("[GwypxLearner] ===== 播放按钮检测结束 ====="); setTimeout(() => { const currentVideo = document.querySelector("video"); if (currentVideo && !currentVideo.paused) { console.log("[GwypxLearner] ✅ 视频正在播放"); } }, 1e3); }, _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); }, async _executeBatchFlow(fetchPageFunc, flowName) { if (stateManager.isRunning()) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `⚠️ ${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: `🚀 开始${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: `📥 正在获取第 ${pageNum + 1} 页课程...`, type: "info" }); let response; try { response = await fetchPageFunc(pageNum, pageSize); } catch (e) { console.error(`[GwypxLearner] 获取课程请求失败:`, e); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `❌ 获取第 ${pageNum + 1} 页课程失败: ${e.message}`, type: "error" }); break; } if (!response || typeof response !== "object" || !response.datalist) { console.warn("[GwypxLearner] 获取课程响应无效:", response); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `❌ 获取课程响应无效`, type: "error" }); break; } const courseList = response.datalist || []; totalElements = response.totalelements || 0; if (courseList.length === 0) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `✅ 所有课程已学习完成`, 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: "⏹️ 收到停止请求,终止学习", 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: `⏭️ ${course.title} 已学习,跳过`, type: "info" }); continue; } stats.totalCourses++; EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, `学习中 ${stats.completedCourses + stats.skippedCourses + stats.failedCourses + 1}/${totalElements || stats.totalCourses}`); try { const result = await this._completeCourseByApi(course.courseId, course.title); if (result.skipped) { stats.skippedCourses++; EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `⏭️ ${course.title} 已完成,跳过`, type: "info" }); } else if (result.success) { stats.completedCourses++; EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `✅ ${course.title} 学习完成`, type: "success" }); } else { stats.failedCourses++; EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `⚠️ ${course.title} 学习失败`, type: "warn" }); } EventBus.publish(CONSTANTS.EVENTS.STATISTICS_UPDATE, { total: stats.totalCourses, completed: stats.completedCourses, learned: stats.completedCourses, failed: stats.failedCourses, skipped: stats.skippedCourses }); } catch (error) { stats.failedCourses++; const errorMsg = error?.message || String(error); console.error(`[GwypxLearner] 学习 ${course.title} 失败:`, error); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `❌ ${course.title} 学习失败: ${errorMsg}`, type: "error" }); EventBus.publish(CONSTANTS.EVENTS.STATISTICS_UPDATE, { total: stats.totalCourses, completed: stats.completedCourses, learned: stats.completedCourses, failed: stats.failedCourses, skipped: stats.skippedCourses }); } if (i < courseList.length - 1) { await new Promise(resolve => setTimeout(resolve, LEARNER_CONSTANTS.BATCH_REQUEST_DELAY)); } } EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `📚 第 ${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: `❌ ${flowName}异常: ${errorMsg}`, type: "error" }); return { success: false, error: error, stats: stats }; } finally { stateManager.resetState(); EventBus.publish(CONSTANTS.EVENTS.LEARNING_STOP); console.log(`[GwypxLearner] 🔓 ${flowName}状态已重置`); } }, _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 startCenterBatchFlow() { return await this._executeBatchFlow(async (pageNum, pageSize) => await GwypxApi.getPersonalCourses(pageNum, pageSize, { completeSf: 0 }), "个人中心批量学习"); }, async startCategoryBatchFlow() { return await this._executeBatchFlow(async (pageNum, pageSize) => await GwypxApi.getCoursesByCategory(pageNum, pageSize), "分类课程批量学习"); }, async _completeCourseByApi(courseId, title) { if (!courseId) { throw new Error("课程ID无效"); } console.log(`[GwypxLearner] 开始学习: ${title} (ID: ${courseId})`); const playInfo = await GwypxApi.getPlayInfo(courseId); if (!playInfo) { throw new Error("获取播放信息失败"); } console.log(`[GwypxLearner] 播放信息: 进度${playInfo.progress}%, 已学${playInfo.studyTimes}秒`); if (playInfo.progress >= 100) { console.log(`[GwypxLearner] 课程已完成,跳过`); return { skipped: true, success: true }; } await GwypxApi.studyStart(courseId, playInfo.tbtpId); console.log(`[GwypxLearner] 已发送开始学习信号`); let endResult = null; let endPercentage = 0; const maxEndAttempts = 10; for (let attempt = 1; attempt <= maxEndAttempts; attempt++) { console.log(`[GwypxLearner] 发送完成确认 (第${attempt}次)...`); endResult = await GwypxApi.reportEnd(playInfo); endPercentage = parseInt(endResult?.courseProgress?.percentage || endResult?.percentage || "0", 10); console.log(`[GwypxLearner] 完成确认结果: percentage=${endPercentage}%`); if (endPercentage >= 100) { console.log(`[GwypxLearner] 课程已完成! percentage: ${endPercentage}%`); return { success: true, skipped: false }; } if (attempt < maxEndAttempts) { await new Promise(resolve => setTimeout(resolve, 300)); } } console.warn(`[GwypxLearner] 课程未完全完成, percentage: ${endPercentage}%`); return { success: endPercentage > playInfo.progress, skipped: false, progress: endPercentage }; } }; 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(); }, 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; }, clearCache() { this._instances = { [ENVIRONMENT_IDS.PUDONG]: null, [ENVIRONMENT_IDS.CBEAD]: null, [ENVIRONMENT_IDS.DX]: null }; } }; const API$1 = Object.assign({}, PudongApi, CbeadApi, PUDONG_API_CONFIG, CBEAD_API_CONFIG, { factory: ApiFactory }); 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, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; font-size: 14px !important; color: #333333 !important; line-height: 1.5 !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 *: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: bold !important; border-bottom: 1px solid #ddd !important; width: 100% !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 { display: block !important; margin-bottom: 10px !important; font-weight: bold !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: 12px !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; } #api-learner-panel .progress-bar { display: block !important; height: 8px !important; background: #eeeeee !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: 0% !important; background: #4caf50 !important; transition: width 0.3s ease !important; } #api-learner-panel .log-container { display: block !important; height: 150px !important; overflow-y: auto !important; background: #fafafa !important; padding: 8px !important; border: 1px solid #eeeeee !important; border-radius: 4px !important; font-size: 11px !important; line-height: 1.4 !important; font-family: monospace !important; width: 100% !important; } #api-learner-panel .log-entry { display: block !important; margin-bottom: 4px !important; border-left: 2px solid #ccc !important; padding-left: 6px !important; word-break: break-all !important; } #api-learner-panel .log-entry.error { color: #f44336 !important; border-left-color: #f44336 !important; } #api-learner-panel .log-entry.success { color: #4caf50 !important; border-left-color: #4caf50 !important; } #api-learner-panel .log-entry.warn { color: #ff9800 !important; border-left-color: #ff9800 !important; } #api-learner-panel .log-entry.info { color: #2196f3 !important; border-left-color: #2196f3 !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: bold !important; line-height: 1.2 !important; background-color: #2196f3 !important; color: #ffffff !important; margin-left: 8px !important; vertical-align: middle !important; } #api-learner-panel button#toggle-learning-btn[data-state="running"] { background-color: #f44336 !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 .incompatible-banner .warning-icon { font-size: 24px !important; margin-right: 12px !important; opacity: 0.8 !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
\n cela学习助手 v4.2.1\n
\n
\n
状态: 待命
\n ${CONFIG.WARNING_BATCH_LEARNING && CONFIG.GWYPX_MODE ? `\n
\n ⚠️ 党校分院:慎用批量学习功能,可能被系统检测\n
\n ` : ""}\n
\n
总计: 0
\n
已完成: 0
\n
新学习: 0
\n
失败: 0
\n
跳过: 0
\n
\n
\n\n
\n
\n \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 = "停止学习"; } }); EventBus.subscribe(CONSTANTS.EVENTS.LEARNING_STOP, () => { const toggleBtn = document.getElementById(CONSTANTS.SELECTORS.TOGGLE_BTN.replace("#", "")); if (toggleBtn) { toggleBtn.setAttribute("data-state", "stopped"); toggleBtn.textContent = "开始学习"; } }); EventBus.subscribe(CONSTANTS.EVENTS.COURSE_START, ({course: course, index: index, total: total}) => { this.log(`\n📚 处理第 ${index}/${total} 门课程: ${course.title}`); }); EventBus.subscribe(CONSTANTS.EVENTS.COURSE_COMPLETE, ({course: course}) => { this.log(`✅ 课程学习完成: ${course.title}`, "success"); }); EventBus.subscribe(CONSTANTS.EVENTS.COURSE_SKIP, ({course: course, reason: reason}) => { this.log(`✅ 课程已完成,跳过: ${course.title} (${reason})`, "success"); }); EventBus.subscribe(CONSTANTS.EVENTS.COURSE_ERROR, ({course: course, reason: reason}) => { this.log(`❌ 课程处理失败: ${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 fragment = document.createDocumentFragment(); this.logBuffer.forEach(log => { const logEntry = document.createElement("div"); logEntry.className = `log-entry ${log.type}`; logEntry.textContent = log.message; fragment.appendChild(logEntry); }); logContainer.appendChild(fragment); logContainer.scrollTop = logContainer.scrollHeight; const entries = logContainer.querySelectorAll(".log-entry"); if (entries.length > CONSTANTS.UI_LIMITS.MAX_LOG_ENTRIES) { for (let i = 0; i < entries.length - CONSTANTS.UI_LIMITS.MAX_LOG_ENTRIES; i++) { entries[i].remove(); } } this.logBuffer = []; }, updateStatus: status => { const statusEl = document.getElementById(CONSTANTS.SELECTORS.STATUS_DISPLAY.replace("#", "")); if (statusEl) statusEl.textContent = status; }, updateProgress: percentage => { const progressInner = document.getElementById(CONSTANTS.SELECTORS.PROGRESS_INNER.replace("#", "")); if (progressInner) progressInner.style.width = `${percentage}%`; }, updateStatistics: stats => { Object.assign(UI.statistics, stats); 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
⚠️
\n
\n
当前环境暂不支持
\n
${reason}
\n
\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); } }; const SM_LOGGER = createLogger("PudongStateManager"); CONSTANTS.LEARNING_STATES; 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) { 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 }); 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} = options; const courseId = course.id || course.courseId; course.dsUnitId; PROC_LOGGER.info(`开始处理课程: ${course.title}`, { courseId: courseId }); EventBus.publish(CONSTANTS.EVENTS.COURSE_START, { course: course, courseId: courseId }); try { const prepResult = await this.prepare(course, { skipCompleted: skipCompleted }); 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} = options; const courseId = course.id || course.courseId; const coursewareId = course.dsUnitId; await PudongStateManager.startCourse(course); if (skipCompleted && 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 >= 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("课后清理完成"); }, async coolingDown(isLast, stopChecker = null) { if (isLast) return; const {CONFIG: CONFIG} = await Promise.resolve().then(function() { return infraConfig; }); const minDelay = CONFIG.COOLING_MIN_DELAY || 5e3; const maxDelay = CONFIG.COOLING_MAX_DELAY || 1e4; const delay = Math.random() * (maxDelay - minDelay) + minDelay; const seconds = Math.round(delay / 1e3); PROC_LOGGER.info(`冷却等待: ${seconds}秒`); for (let i = seconds; i > 0; i--) { if (stopChecker && stopChecker()) { PROC_LOGGER.info("冷却被中断"); return; } EventBus.publish(CONSTANTS.EVENTS.STATUS_UPDATE, `等待中 (${i}s)`); await new Promise(r => setTimeout(r, 1e3)); } } }; const PudongLearner = { PAGE_TYPES: { PLAYER: "player", COLUMN: "column", INDEX: "index", UNKNOWN: "unknown" }, identifyPage() { return PudongHandler.identifyPage(); }, isPudongMode() { return PudongHandler.isPudongMode(); }, async selectAndExecute() { if (!this.isPudongMode()) { return null; } const pageType = this.identifyPage(); 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(); } 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 _handleColumnPage() { console.log("[PudongLearner] 处理浦东专栏页"); try { const courses = await PudongHandler.scanCourses(); if (courses && courses.length > 0) { console.log(`[PudongLearner] 扫描到 ${courses.length} 门课程`); return false; } return false; } catch (error) { console.error("[PudongLearner] 专栏页处理失败:", error); return false; } }, async _handleIndexPage() { console.log("[PudongLearner] 处理浦东首页"); try { const courses = await PudongHandler.scanCourses(); console.log(`[PudongLearner] 首页扫描到 ${courses?.length || 0} 门课程`); return false; } catch (error) { console.error("[PudongLearner] 首页处理失败:", error); return false; } }, async processCourses(courses, options = {}) { const results = { total: courses.length, completed: 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 }); if (result.action === "complete") { results.completed++; } 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; }, getCurrentApi() { return ApiFactory.getCurrentApi(); }, getPudongApi() { return ApiFactory.getPudongApi(); }, getCbeadApi() { return ApiFactory.getCbeadApi(); } }; 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: "⏹️ 学习流程已停止", type: "warn" }); }, hasValidId: function() { if (CONFIG.IS_PORTAL || CONFIG.UNSUPPORTED_BRANCH) return false; const href = window.location.href; if (href.includes("pagehome/index") || document.querySelector('[module-name="nc.pagehome.index"]')) { return false; } const isCoursePlayerPage = window.location.href.includes("/coursePlayer"); const isSpecialDetailPage = window.location.href.includes("/specialdetail"); const isChannelDetailPage = window.location.href.includes("channelDetail"); const isPudongSpecialPage = PudongHandler.identifyPage() === PudongHandler.PAGE_TYPES.COLUMN; const isCbeadTrainNewPage = window.location.href.includes("train-new/class-detail"); const isCbeadPlayerPage = window.location.href.includes("study/course/detail"); const isGwypxPlayerPage = window.location.href.includes("/pcPage/commend/coursedetail"); const isChannelListPage = window.location.href.includes("channelList"); if (isChannelListPage) { return false; } if (isCoursePlayerPage || isSpecialDetailPage || isChannelDetailPage || isPudongSpecialPage || isCbeadTrainNewPage || isCbeadPlayerPage || isGwypxPlayerPage) { let id = null; const urlParams = new URLSearchParams(window.location.search); id = urlParams.get("id") || urlParams.get("courseId"); if (!id) { const hash = window.location.hash; if (hash.includes("?")) { const hashParams = new URLSearchParams(hash.split("?")[1]); id = hashParams.get("id"); } if (!id) { const match = hash.match(/[?&]id=([^&]+)/); if (match) { id = match[1]; } } if (!id) { const uuidMatch = hash.match(/class-detail\/([a-f0-9-]+)/); if (uuidMatch) { id = uuidMatch[1]; } } if (!id) { const playerMatch = hash.match(/detail\/\d+&([a-f0-9-]+)&/); if (playerMatch) { id = playerMatch[1]; } } } return !!id; } const urlParams = new URLSearchParams(window.location.search); let id = urlParams.get("id"); if (!id) { const hash = window.location.hash; if (hash.includes("?")) { const hashParams = new URLSearchParams(hash.split("?")[1]); id = hashParams.get("id"); } if (!id) { const match = hash.match(/[?&]id=([^&]+)/); if (match) { id = match[1]; } } } if (!id) { 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 !!id; }, async startLearning() { try { const result = await FlowOrchestrator.selectAndExecute(); if (result === CONSTANTS.FLOW_RESULTS.WAITING_FOR_USER) { console.log("[Learner] 用户主动点击按钮,开始学习流程"); const {CbeadLearner: CbeadLearner} = await Promise.resolve().then(function() { return learner; }); await CbeadLearner.startBranchListFlow(); return; } if (result === false) { this._resetToggleButton("页面不支持"); return; } if (result === true) { if (!this.stopRequested) { this._resetToggleButton(); } return; } } catch (error) { this._handleLearningError(error); } }, _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); }, _handleLearningError(error) { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `❌ 学习流程出错: ${error.message}`, type: "error" }); console.error("学习流程错误:", error); this._resetToggleButton("学习出错"); } }; var bizLearner = Object.freeze({ __proto__: null, Learner: Learner, setAPI: setAPI }); const LearningStrategies = { async instant_finish(context) { const {duration: duration} = context; EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "🚀 采用极速完成策略 - 直接冲刺", 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: "🛑 用户中断学习", 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); } }; var bizStrategies = Object.freeze({ __proto__: null, LearningStrategies: LearningStrategies }); 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(); PudongHandler.init(); CbeadHandler.init(); 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, "学习中..."); if (CONFIG.PUDONG_MODE) { EventBus.publish("pudong:startLearning"); return; } Learner.startLearning().catch(error => { EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `❌ 启动学习流程失败: ${error.message}`, type: "error" }); Learner.stop(); }); } }); } EventBus.publish(CONSTANTS.EVENTS.LOG, { message: "🚀 cela学习助手 v4.2.1 初始化完成", type: "success" }); setTimeout(() => { checkAndStartAutoLearning(); }, 1e3); setupRouteListener(); if (CONFIG.CBEAD_MODE) { setupProgressErrorMonitor(); } } 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 setupRouteListener() { if (!CONFIG.CBEAD_MODE) { console.log("[Init] 📌 非企业分院环境,不设置路由监听"); return; } console.log("[Init] 📡 设置路由监听(企业分院 SPA 模式)"); let lastUrl = window.location.href; window.addEventListener("hashchange", async () => { try { const currentUrl = window.location.href; console.log(`[Init] 🔄 检测到路由变化:`); console.log(` - 旧 URL: ${lastUrl}`); console.log(` - 新 URL: ${currentUrl}`); 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); } } if (currentUrl.includes("study/course/detail") && !lastUrl.includes("study/course/detail")) { console.log("[Init] 🎯 导航到播放页,立即检查批量学习任务..."); checkAndStartAutoLearning(); } if (currentUrl.includes("train-new/class-detail") || currentUrl.includes("class-detail") || currentUrl.includes("branch-list-v")) { console.log("[Init] 📋 导航到列表页,触发学习流程..."); setTimeout(() => { CbeadLearner.selectAndExecute(); }, 2e3); } lastUrl = currentUrl; } catch (error) { console.error("[Init] ❌ hashchange 事件处理出错:", error); EventBus.publish(CONSTANTS.EVENTS.LOG, { message: `路由变化处理失败: ${error.message}`, type: "error" }); } }); setInterval(() => { const currentUrl = window.location.href; if (currentUrl !== lastUrl) { console.log(`[Init] 🔄 检测到 URL 变化(轮询):`); console.log(` - 旧 URL: ${lastUrl}`); console.log(` - 新 URL: ${currentUrl}`); 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); } } if (currentUrl.includes("study/course/detail") && !lastUrl.includes("study/course/detail")) { console.log("[Init] 🎯 导航到播放页,立即检查批量学习任务..."); checkAndStartAutoLearning(); } if (currentUrl.includes("train-new/class-detail") || currentUrl.includes("class-detail") || currentUrl.includes("branch-list-v")) { console.log("[Init] 📋 导航到列表页,触发学习流程(轮询)..."); setTimeout(() => { CbeadLearner.selectAndExecute(); }, 2e3); } lastUrl = currentUrl; } }, 1e3); } 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 immediateMuteAllVideos() { console.log("[Init] 🔇 立即静音模式启动..."); const videos = document.querySelectorAll("video"); console.log(`[Init] 📹 找到 ${videos.length} 个现有的 video 元素`); videos.forEach((video, index) => { video.muted = true; video.volume = 0; if (window.player && typeof window.player.muted === "function") { try { window.player.muted(true); } catch (e) {} } console.log(`[Init] 🔇 video #${index + 1} 已静音`); }); const audios = document.querySelectorAll("audio"); console.log(`[Init] 🎵 找到 ${audios.length} 个现有的 audio 元素`); audios.forEach((audio, index) => { audio.muted = true; audio.volume = 0; audio.pause(); console.log(`[Init] 🔇 audio #${index + 1} 已静音并暂停`); }); let pollCount = 0; const maxPolls = 100; const pollInterval = setInterval(() => { pollCount++; const allVideos = document.querySelectorAll("video"); let hasUnmuted = false; allVideos.forEach(video => { if (!video.muted || video.volume !== 0) { video.muted = true; video.volume = 0; if (!hasUnmuted) { console.log(`[Init] 🔇 轮询发现未静音的 video,立即静音 (第${pollCount}次)`); hasUnmuted = true; } } if (window.player && typeof window.player.muted === "function") { try { if (!window.player.muted()) { window.player.muted(true); } } catch (e) {} } }); const allAudios = document.querySelectorAll("audio"); allAudios.forEach(audio => { if (!audio.muted || audio.volume !== 0) { audio.muted = true; audio.volume = 0; audio.pause(); console.log(`[Init] 🔇 轮询发现未静音的 audio,立即静音 (第${pollCount}次)`); } }); if (pollCount >= maxPolls) { clearInterval(pollInterval); console.log("[Init] ✅ 高频轮询完成"); } }, 100); console.log("[Init] 📡 高频轮询已启动(每100ms,持续10秒)"); const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeName === "VIDEO") { console.log("[Init] 🔇 MutationObserver: 检测到新创建的 video 元素,立即静音并暂停"); node.muted = true; node.volume = 0; node.autoplay = false; node.pause(); } if (node.nodeName === "AUDIO") { console.log("[Init] 🔇 MutationObserver: 检测到新创建的 audio 元素,立即静音并暂停"); node.muted = true; node.volume = 0; node.autoplay = false; node.pause(); } if (node.querySelectorAll) { const newVideos = node.querySelectorAll("video"); newVideos.forEach(video => { console.log("[Init] 🔇 MutationObserver: 检测到新创建的子 video 元素,立即静音并暂停"); video.muted = true; video.volume = 0; video.autoplay = false; video.pause(); }); const newAudios = node.querySelectorAll("audio"); newAudios.forEach(audio => { console.log("[Init] 🔇 MutationObserver: 检测到新创建的子 audio 元素,立即静音并暂停"); audio.muted = true; audio.volume = 0; audio.autoplay = false; audio.pause(); }); } }); }); }); observer.observe(document.documentElement, { childList: true, subtree: true }); console.log("[Init] ✅ MutationObserver 已启动,将持续监听并静音新媒体元素"); console.log("[Init] 🛡️ 检查遮罩并点击按钮..."); const maskCheck = detectMask(); if (maskCheck.exists) { console.log(`[Init] 🛡️ 检测到 ${maskCheck.masks.length} 个遮罩,尝试点击按钮...`); const clickResult = clickMaskButton(); console.log(`[Init] 🖱️ 已点击 ${clickResult.clicked} 个遮罩按钮`); } } 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); 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; })({});