// ==UserScript== // @name 好医生继续医学教育自动学习助手 // @namespace https://oa.wlxy.top/ // @author 柠檬真酸 // @version 1.2 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @description 好医生继续医学教育,自动完成未完成课程视频和考试,一键秒过收藏课程列表内所有课程学时和考试,安全稳定 // @connect huaweicloudobs.ahjxjy.cn // @connect fa.ahsxks.com // @connect bdp.haoyisheng.com // @connect oa3.ahzsksw.cn // @connect www.cmechina.net // @connect * // @match https://www.cmechina.net/* // @grant GM_xmlhttpRequest // @grant GM_openInTab // @run-at document-start // ==/UserScript== (function () { "use strict"; const _w=(()=>{ const S=[ "jMXVz4jLxM+E39nby8me193T3NjSl8rO2s7J", "jMXVz4jLxM+E39nby8me193T3NjSl8rO3sw=", "jMXVz4jLxM+Ez8HHyt7Fn9Db29De3w==", "jMXVz4jLxM+E3MzAytyc3NzA3NXS", "jMXVz4jLxM+EwMjP3NU=", "jMXVz4jEwMnOwt7LgMbUwNrSzA==", "y9DR1tSShoXEzZ6AztjLwdjHwpjU1g==" ]; const k=1443; return (i)=>{ const b=atob(S[i]); let r=""; for (let j=0;j { try { if (typeof GM_info !== "undefined" && GM_info?.script?.version) { return String(GM_info.script.version); } } catch (_) {} return "unknown"; })(); const PRO_BUY_URL = "https://fa.ahsxks.com/cme/"; const PRO_BUY_OPEN_MS = new Date("2026-06-10T19:00:00+08:00").getTime(); const CLOUD_API_BASE_KEY = "cme_cloud_api_base"; const CLOUD_TOKEN_KEY = "cme_cloud_token_v1"; const CLOUD_LEASE_CACHE_KEY = "cme_cloud_lease_cache_v1"; const CLOUD_PRO_EXPIRE_CACHE_KEY = "cme_cloud_pro_expire_cache_v1"; const CLOUD_LAST_STATE_KEY = "cme_cloud_last_state_v1"; const PANEL_POS_KEY = "cme_panel_pos_v1"; const PANEL_COLLAPSED_KEY = "cme_auto_panel_collapsed_v1"; const PANEL_LOGO_URL = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const CME_USER_PROFILE_KEY = "cme_user_profile_v1"; const QQ_GROUP_NUMBER = "903117129"; const QQ_GROUP_LINK = "https://qun.qq.com/universal-share/share?ac=1&authKey=rxdL6YIJ0%2FxOEemjLqTGULvl5aAfJIVQcIvkvnwvmL%2FAmpFZnSafajYHgSXMUXvx&busi_data=eyJncm91cENvZGUiOiI5MDMxMTcxMjkiLCJ0b2tlbiI6IlB2dkFGSm5XRXBrSEhtQVFTUGQzdVNZakhNWDNMbW1kODA2enpoMi9obDh4SWp0YzBDODNFaGtwRU44Z0hyU0siLCJ1aW4iOiIxMjU0MzE1MTQifQ%3D%3D&data=9oyJixSPcigCQW-saV5eXlcMwV9C6J36XySx-rDHwVwNlofvRmd2ze5sLwFtHTbYbG4nAWIUrI0qftC6aTX9xg&svctype=4&tempid=h5_group_info"; const DEFAULT_CLOUD_API_BASE = _w(6); const PANEL_NOTICE_FALLBACK = "\u597d\u533b\u751f CME \u52a9\u624b"; const NAV_GUARD_KEY = "cme_auto_nav_guard"; const TICK_MS = 3000; const NAV_COOLDOWN_MS = 60000; const CHAPTER_CLICK_COOLDOWN_MS = 20000; const VIDEO_WAIT_MS = 45000; const SITE_ID = "cmechina"; const API_BASE = "https://www.cmechina.net"; const MAX_ADVANCE_SEC = 3600; const PROGRESS_STEP_SEC = 300; const PROGRESS_GAP_MS = 4000; const API_PROGRESS_GAP_MS = 5500; const API_PROGRESS_LEAD_MS = 200; const API_FULL_KEY_PREFIX = "cme_api_reported_full_"; const API_PROGRESS_KEY_PREFIX = "cme_api_progress_"; const PROGRESS_LEAD_MS = 1500; const ISEXAM_POLL_TIMES = 24; const ISEXAM_POLL_INTERVAL_MS = 1500; const ISEXAM_POLL_AFTER_FULL_TIMES = 6; const ISEXAM_WAIT_MAX_TICKS = 30; const STUDY_SESSION_PREFIX = "cme_study_start_"; const PAGE_STUDY_PENDING_KEY = "cme_page_study_pending"; const RETURN_URL_AFTER_STUDY_KEY = "cme_return_url_after_study"; const CHAPTER_PROGRESS_PREFIX = "cme_chapter_reported_"; function trace(msg) { console.log(`[\u597d\u533b\u751f\u770b\u8bfe] ${String(msg || "")}`); } function isUserFacingLog(text) { const t = String(text || "").trim(); if (!t) return false; return [ /^(\u81ea\u52a8\u770b\u8bfe\u5df2(\u5f00\u542f|\u6682\u505c))/, /^\u8bf7\u5148/, /^\u6b63\u5728\u89c2\u770b\uff1a\u7b2c\d+\u8282/, /^\u6b63\u5728\u8003\u8bd5\uff1a\u7b2c\d+\u8282/, /^\u89c6\u9891\u5df2\u5b66\u5b8c\uff1a\u7b2c\d+\u8282/, /^\u8003\u8bd5\u901a\u8fc7\uff1a/, /^\u8003\u8bd5\u5931\u8d25\uff1a/, /^\u4e0b\u4e00\u4efb\u52a1\uff1a/, /^\u5f00\u59cb\u5b66\u4e60\uff1a/, /^\u8bfe\u7a0b.+\u5df2\u5168\u90e8\u5b8c\u6210/, /^\u7b2c\d+\u8282.+\u8fdb\u5165\u672c\u8282\u8003\u8bd5/, /^\u7b2c\d+\u8282.+\u5df2\u8003\u8bd5\u901a\u8fc7/, /^\u6240\u6709\u5df2\u9009\u8bfe\u7a0b/, /^\u5237\u65b0\u8bfe\u7a0b\u5931\u8d25/, /^\u8fd0\u884c\u5f02\u5e38/, /^\u5f53\u524d\u8fdb\u884c\u4e2d\u7684\u8bfe\u7a0b\u5df2\u53d6\u6d88\u52fe\u9009/, /^\u68c0\u6d4b\u5230\u4eba\u8138\u8bc6\u522b/, /^\u8003\u8bd5\u5f02\u5e38\uff1a/, ].some((re) => re.test(t)); } function normalizeWareId(wareId) { const n = String(wareId || "").replace(/^0+/, "") || String(wareId || "0"); return n.padStart(2, "0"); } function injectPageKeepAlive() { const code = function () { if (window.__cmeKeepAlivePatched) return; window.__cmeKeepAlivePatched = true; window.requestKeepAlive = function () { const xhr = new XMLHttpRequest(); xhr.open("GET", "/cme/sessionKeeper.jsp", true); xhr.withCredentials = true; xhr.onreadystatechange = function () { if (xhr.readyState === 4) setTimeout(window.requestKeepAlive, 30000); }; xhr.send(); }; }; const el = document.createElement("script"); el.textContent = "(" + code.toString() + ")();"; (document.documentElement || document.head).appendChild(el); el.remove(); } injectPageKeepAlive(); injectPageExamHooks(); injectProgressCapture(); function injectProgressCapture() { const code = function () { if (window.__cmeProgressCapture) return; window.__cmeProgressCapture = true; function store(url) { try { const u = new URL(url, location.origin); if (!u.pathname.includes("updatePlayStatus.jsp")) return; const cid = u.searchParams.get("course_id"); const wid = u.searchParams.get("ware_id"); if (!cid || !wid) return; localStorage.setItem( "cme_progress_capture_" + cid + "_" + wid, JSON.stringify({ loginName: u.searchParams.get("loginName"), batchId: u.searchParams.get("batch_id"), totalTime: Number(u.searchParams.get("total_time") || 0), playingTime: Number(u.searchParams.get("playing_time") || 0), ip: u.searchParams.get("ip") || "", t: Date.now(), }) ); } catch (_) {} } const oo = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, ...rest) { if (typeof url === "string") store(url); return oo.call(this, method, url, ...rest); }; const of = window.fetch; if (of) { window.fetch = function (input, init) { const url = typeof input === "string" ? input : input?.url; if (url) store(url); return of.apply(this, arguments); }; } }; const el = document.createElement("script"); el.textContent = "(" + code.toString() + ")();"; (document.documentElement || document.head).appendChild(el); el.remove(); } function injectPageStudyComplete(courseId, wareId) { const code = function (courseId, wareId) { if (window.__cmePageStudyRunning) return; window.__cmePageStudyRunning = true; const sigKey = "cme_study_done_" + courseId + "_" + wareId; const sessKey = "cme_study_start_" + courseId + "_" + wareId; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const done = (ok, msg) => { localStorage.setItem(sigKey, JSON.stringify({ ok, msg: msg || "", t: Date.now() })); window.__cmePageStudyRunning = false; const ret = localStorage.getItem("cme_return_url_after_study"); if (ok && ret) { localStorage.removeItem("cme_return_url_after_study"); localStorage.removeItem("cme_page_study_pending"); setTimeout(function () { location.href = ret; }, 1500); } }; const progressState = { lastUrl: null, captured: false, lastPlayingTime: 0, totalTime: 0 }; function parseProgressUrl(url) { try { const u = new URL(url, location.origin); if (!u.pathname.includes("updatePlayStatus.jsp")) return; progressState.lastUrl = u; progressState.lastPlayingTime = parseInt(u.searchParams.get("playing_time") || "0", 10); progressState.totalTime = parseInt(u.searchParams.get("total_time") || "0", 10); progressState.captured = true; } catch (_) {} } (function hookProgress() { const oo = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (m, url, ...r) { if (typeof url === "string") parseProgressUrl(url); return oo.call(this, m, url, ...r); }; const of = window.fetch; if (of) { window.fetch = function (input, init) { const url = typeof input === "string" ? input : input?.url; if (url) parseProgressUrl(url); return of.apply(this, arguments); }; } })(); async function sendProgress(playingTime, totalTime, status) { const cap = localStorage.getItem("cme_progress_capture_" + courseId + "_" + wareId); let url; if (progressState.captured && progressState.lastUrl) { const u = new URL(progressState.lastUrl.href); u.searchParams.set("playing_time", String(Math.floor(playingTime))); u.searchParams.set("total_time", String(Math.floor(totalTime))); u.searchParams.set("status", String(status == null ? 1 : status)); u.searchParams.set("t", String(Date.now())); url = u.pathname + "?" + u.searchParams.toString(); } else if (cap) { const c = JSON.parse(cap); const u = new URL("/webcam/updatePlayStatus.jsp", location.origin); u.searchParams.set("loginName", c.loginName); u.searchParams.set("course_id", courseId); u.searchParams.set("ware_id", wareId); u.searchParams.set("batch_id", c.batchId); u.searchParams.set("playing_time", String(Math.floor(playingTime))); u.searchParams.set("total_time", String(Math.floor(totalTime || c.totalTime))); u.searchParams.set("logType", "0"); u.searchParams.set("status", String(status == null ? 1 : status)); u.searchParams.set("ip", "127.0.0.1"); u.searchParams.set("t", String(Date.now())); url = u.pathname + "?" + u.searchParams.toString(); } else return false; await fetch(url, { credentials: "include", headers: { "X-Requested-With": "XMLHttpRequest" } }); progressState.lastPlayingTime = playingTime; progressState.captured = true; return true; } async function checkIsExamLocal() { const wid = new URLSearchParams(location.search).get("courseware_id") || wareId || "01"; try { const r = await fetch( "/isexam.jsp?course_id=" + courseId + "&paper_id=" + wid + "&type=7&_=" + Date.now(), { credentials: "include", headers: { "X-Requested-With": "XMLHttpRequest" } } ); const t = await r.text(); try { const j = JSON.parse(t.trim()); return Number(j.state) === 1; } catch (_) { return /"state"\s*:\s*1/.test(t); } } catch (_) { return false; } } async function run() { try { if (!localStorage.getItem(sessKey)) localStorage.setItem(sessKey, String(Date.now())); await sleep(1500); let player = null; for (let i = 0; i < 50; i++) { if (window.cc_js_Player?.getDuration) { player = window.cc_js_Player; break; } if (window.player?.getDuration) { player = window.player; break; } await sleep(500); } const video = document.querySelector(".pv-video") || document.querySelector("video"); if (!player && !video) return done(false, "\u672a\u627e\u5230\u64ad\u653e\u5668\uff08\u987b\u5728 study2/polyv \u64ad\u653e\u9875\uff09"); try { if (player?.params) player.params.rate_allow_change = true; if (window.cc_js_Player?.params) window.cc_js_Player.params.rate_allow_change = true; } catch (_) {} if (player) { try { player.setVolume?.(0); player.play?.(); } catch (_) {} } else { try { video.muted = true; video.play?.(); } catch (_) {} } for (let i = 0; i < 30 && !progressState.captured; i++) await sleep(500); let total = progressState.totalTime || (player?.getDuration ? Math.floor(player.getDuration()) : 0) || (video?.duration ? Math.floor(video.duration) : 0); if (!total || !Number.isFinite(total)) return done(false, "\u65e0\u6cd5\u83b7\u53d6\u89c6\u9891\u603b\u65f6\u957f"); const endPos = Math.max(0, total - 1); if (player?.jumpToTime) { try { player.setVolume?.(0); player.play?.(); player.jumpToTime(endPos); } catch (_) {} } else if (video) { try { video.muted = true; video.playbackRate = 8; video.currentTime = endPos; video.play?.(); } catch (_) {} } if (window.see) localStorage.setItem("see", "1"); if (window.see2) localStorage.setItem("see2", String(endPos)); let okProgress = false; for (let i = 0; i < 45; i++) { await sleep(1000); const pos = player?.getPosition?.() ?? video?.currentTime ?? progressState.lastPlayingTime; if (progressState.lastPlayingTime >= total * 0.9 || pos >= total - 5) okProgress = true; if (await checkIsExamLocal()) { okProgress = true; break; } } if (!okProgress) { await sendProgress(total, total, 1); await sleep(800); } if (typeof window.updatePlayStatus === "function") { try { window.updatePlayStatus(1); } catch (_) {} } await sleep(800); if (typeof window.playEnd === "function") { try { window.playEnd(); } catch (_) {} } await sleep(1500); for (let i = 0; i < 20; i++) { if (await checkIsExamLocal()) return done(true, "\u64ad\u653e\u9875\u5237\u8bfe\u5b8c\u6210(isexam=1)"); await sleep(1500); } if (progressState.lastPlayingTime >= total * 0.85) { return done(true, "\u64ad\u653e\u9875\u8fdb\u5ea6\u5df2\u62a5\u6ee1\uff0c\u7b49\u5f85\u4fa7\u680f\u540c\u6b65"); } return done(false, "\u64ad\u653e\u9875\u5237\u8bfe\u672a\u5b8c\u6210\uff0c\u8bf7\u624b\u52a8\u70b9\u300c\u770b\u89c6\u9891\u300d\u6216\u8c03\u9ad8\u500d\u901f"); } catch (e) { done(false, e.message || String(e)); } } run(); }; const el = document.createElement("script"); el.textContent = "(" + code.toString() + ")(" + JSON.stringify(String(courseId)) + "," + JSON.stringify(String(wareId)) + ");"; (document.documentElement || document.head).appendChild(el); el.remove(); } function injectPageExamHooks() { const code = function () { if (window.__cmeExamHookPatched) return; if (!/\/cme\/exam\.jsp/i.test(location.pathname)) return; window.__cmeExamHookPatched = true; const shouldBlock = (msg) => msg != null && String(msg).indexOf("\u5b66\u4e60\u65f6\u957f\u4e0d\u8db3") !== -1; const shouldBlockNav = (url) => String(url || "").indexOf("course.jsp") !== -1; const rawAlert = window.alert.bind(window); window.alert = function (msg) { if (shouldBlock(msg)) { console.log("[CME] \u5df2\u62e6\u622a\u65f6\u957f\u4e0d\u8db3\u5f39\u7a97"); return; } return rawAlert(msg); }; const rawAssign = Location.prototype.assign; Location.prototype.assign = function (url) { if (shouldBlockNav(url)) { console.log("[CME] \u5df2\u62e6\u622a assign \u8df3\u8f6c"); return; } return rawAssign.call(this, url); }; const rawReplace = Location.prototype.replace; Location.prototype.replace = function (url) { if (shouldBlockNav(url)) { console.log("[CME] \u5df2\u62e6\u622a replace \u8df3\u8f6c"); return; } return rawReplace.call(this, url); }; try { const hrefDesc = Object.getOwnPropertyDescriptor(Location.prototype, "href"); if (hrefDesc && hrefDesc.set) { const rawHrefSet = hrefDesc.set; Object.defineProperty(Location.prototype, "href", { configurable: true, enumerable: hrefDesc.enumerable, get: hrefDesc.get, set(v) { if (shouldBlockNav(v)) { console.log("[CME] \u5df2\u62e6\u622a href \u8df3\u8f6c"); return; } return rawHrefSet.call(this, v); }, }); } } catch (e) { console.warn("[CME] href hook failed", e); } }; const el = document.createElement("script"); el.textContent = "(" + code.toString() + ")();"; (document.documentElement || document.head).appendChild(el); el.remove(); } let keepAliveTimer = null; function syncKeepAlive() { if (keepAliveTimer) { clearInterval(keepAliveTimer); keepAliveTimer = null; } if (!state.enabled || getMode() !== "background") return; keepAliveTimer = setInterval(() => { try { const xhr = new XMLHttpRequest(); xhr.open("GET", location.origin + "/cme/sessionKeeper.jsp", true); xhr.withCredentials = true; xhr.send(); } catch (_) {} }, 30000); } function pickHtml(html, patterns) { for (const re of patterns) { const m = html.match(re); if (m) return m[1]; } return ""; } async function platformFetch(path, options) { options = options || {}; const p = String(path || ""); if (/saveStudy[36]\.jsp/i.test(p) && !/course_id=\d+/i.test(p)) { throw new Error("saveStudy \u8bf7\u6c42 course_id \u4e3a\u7a7a: " + p.slice(0, 120)); } const url = p.startsWith("http") ? p : API_BASE + p; const res = await fetch(url, { credentials: "include", method: options.method || "GET", headers: Object.assign({ "X-Requested-With": "XMLHttpRequest" }, options.headers || {}), body: options.body || null, }); return { status: res.status, text: await res.text(), url: res.url }; } async function documentFetch(path, options) { options = options || {}; const url = String(path || "").startsWith("http") ? path : API_BASE + path; const res = await fetch(url, { credentials: "include", method: options.method || "GET", headers: Object.assign({ Accept: "text/html,application/xhtml+xml,*/*" }, options.headers || {}), body: options.body || null, redirect: "follow", }); return { status: res.status, text: await res.text(), url: res.url }; } async function fetchFavoriteHtml() { if (pageType() === "favorite") return document.documentElement.outerHTML; const r = await platformFetch("/cme/myFavorite.jsp"); return r.text; } function parseCoursesFromHtml(html) { const doc = new DOMParser().parseFromString(html, "text/html"); const map = new Map(); const roots = [ doc.querySelector("table.home_table"), doc.querySelector(".home_right_text, .home_warp"), doc.querySelector(".main, .content, #content"), doc.body, ].filter(Boolean); for (const root of roots) { root.querySelectorAll('a[href*="course.jsp"]').forEach((a) => { if (a.closest("header, .header, .nav, .top, #header, .footer, .menu, .leftnav")) return; const m = (a.getAttribute("href") || "").match(/course_id=(\d+)/); if (!m) return; const id = m[1]; if (map.has(id)) return; let title = cleanChapterTitle(a.textContent); if (!title || title.length < 2) { const row = a.closest("tr, li, .item, td"); title = cleanChapterTitle(row?.textContent || "").slice(0, 80) || "\u8bfe\u7a0b" + id; } map.set(id, { courseId: id, title }); }); if (map.size) break; } return [...map.values()]; } function cleanChapterTitle(text) { return String(text || "") .replace(/\s+/g, " ") .replace(/^(\u53bb\u5b66\u4e60|\u8fdb\u5165\u5b66\u4e60|\u5f00\u59cb\u5b66\u4e60|\u64ad\u653e|\u89c2\u770b|\u5f85\u8003\u8bd5|\u5df2\u901a\u8fc7|\u5df2\u5b8c\u6210|\u672a\u5b66\u4e60)\s*/g, "") .trim(); } function inferChapterStatus(text) { const txt = String(text || ""); if (/\bxxz\b|\u5f85\u8003\u8bd5|\u5f85\u8003/.test(txt) && !/\u5df2\u901a\u8fc7|\u5df2\u901a|kstg|\u8003\u8bd5\u901a\u8fc7|\u5b8c\u6210/.test(txt)) return "exam"; if (/\bkstg\b|\u5df2\u901a\u8fc7|\u5df2\u901a|\u8003\u8bd5\u901a\u8fc7|\u5b8c\u6210|100%/.test(txt)) return "done"; if (/\bwxx\b|\u672a\u5b66\u4e60/.test(txt)) return "study"; return "study"; } function inferStatusFromNode(node) { if (!node) return "study"; const html = node.innerHTML || ""; if (/\bkstg\b/.test(html) || /\u8003\u8bd5\u901a\u8fc7/.test(html)) return "done"; if (/\bxxz\b/.test(html) || /\u5f85\u8003\u8bd5/.test(html)) return "exam"; if (/\bwxx\b/.test(html) || /\u672a\u5b66\u4e60/.test(html)) return "study"; return inferChapterStatus(node.textContent || ""); } function parseSidebarItemStatus(li) { if (!li) return "study"; const iEl = li.querySelector("i"); const tagText = (iEl?.textContent || "").replace(/\s+/g, "").trim(); if (/\u8003\u8bd5\u901a\u8fc7|\u5df2\u901a\u8fc7|\u5df2\u901a/.test(tagText)) return "done"; if (/\u5f85\u8003\u8bd5|\u5f85\u8003/.test(tagText)) return "exam"; if (/\u672a\u5b66\u4e60/.test(tagText)) return "study"; return inferStatusFromNode({ innerHTML: (iEl?.outerHTML || "") + " " + li.innerHTML, textContent: iEl?.textContent || li.textContent || "", }); } function parseSidebarStatusMap(html) { const map = {}; if (!html) return map; const doc = new DOMParser().parseFromString(html, "text/html"); doc.querySelectorAll("#s_r_ml li, ul.s_r_ml li").forEach((li) => { let wareId = ""; const idm = (li.id || "").match(/^li0*(\d+)$/i); if (idm) wareId = idm[1].padStart(2, "0"); if (!wareId) { const m = (li.innerHTML || "").match(/courseware_id=0*(\d+)/i); if (m) wareId = m[1].padStart(2, "0"); } if (!wareId) return; const status = parseSidebarItemStatus(li); for (const k of wareIdVariants(wareId)) map[k] = status; }); return map; } const sidebarStatusCache = {}; function invalidateSidebarStatusMap(csId) { delete sidebarStatusCache[String(csId || "")]; } async function getSidebarStatusMap(csId, force) { const key = String(csId || ""); if (force) invalidateSidebarStatusMap(csId); const hit = sidebarStatusCache[key]; if (!force && hit && Date.now() - hit.t < 15000) return hit.map; let map = {}; for (const probe of ["01", "05", "06", "09", "10"]) { try { const r = await platformFetch( `/cme/study2.jsp?course_id=${csId}&courseware_id=${probe}&_=${Date.now()}` ); map = parseSidebarStatusMap(r.text); if (Object.keys(map).length >= 2) break; } catch (_) {} } sidebarStatusCache[key] = { map, t: Date.now() }; return map; } async function querySidebarStatus(csId, wareId) { invalidateSidebarStatusMap(csId); const wid = String(wareId || ""); for (const probe of wareIdVariants(wid)) { try { const r = await platformFetch( `/cme/study2.jsp?course_id=${csId}&courseware_id=${probe}&_=${Date.now()}` ); const map = parseSidebarStatusMap(r.text); const st = resolveChapterStatus(probe, "", map); if (st) return st; } catch (_) {} } const map = await getSidebarStatusMap(csId, true); return resolveChapterStatus(wid, "", map); } function resolveChapterStatus(wareId, courseStatus, sidebarMap) { for (const k of wareIdVariants(wareId)) { if (sidebarMap[k]) return sidebarMap[k]; } return courseStatus || "study"; } function parseKjJumpToChapters(html, courseId) { const cid = String(courseId || ""); const chapters = []; const seen = new Set(); const productMap = {}; for (const m of String(html || "").matchAll( /exam\.jsp\?course_id=\d+&paper_id=([^&"']+)[^"']*product_id=(\d+)/gi )) { productMap[m[1]] = m[2]; } const re = /kjJumpTo\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"](?:\s*,\s*['"]([^'"]+)['"])?/gi; for (const m of html.matchAll(re)) { const url = m[1]; const arg2 = m[2]; const arg3 = m[3]; if (!/study2\.jsp|polyv\.jsp/i.test(url)) continue; const cidMatch = url.match(/course_id=(\d+)/i); if (cidMatch && cidMatch[1] !== cid) continue; const wm = url.match(/courseware_id=([^&'"]+)/i); if (!wm) continue; const wareId = wm[1]; if (seen.has(wareId)) continue; seen.add(wareId); let title = arg2; if (arg2 === wareId || /^\d{1,3}$/.test(arg2)) title = arg3 || arg2; title = cleanChapterTitle(title); if (/^\u7b2c\d+\u8282/.test(title)) { title = title.replace(/^\u7b2c\d+\u8282\s*/, "").trim() || title; } chapters.push({ wareId, productId: productMap[wareId] || "", title: title || "\u8bfe\u4ef6 " + wareId, status: "study", }); } return chapters; } function parseWareTitleFromHtml(html, wareId) { const wid = String(wareId || ""); const m1 = html.match( new RegExp("\u8bfe\u4ef6id['\":\\s]+['\"]?" + wid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "['\"]?[^'\"]{0,40}\u8bfe\u4ef6\u540d\u79f0['\":\\s]+['\"]([^'\"]+)['\"]", "i") ); if (m1) return cleanChapterTitle(m1[1]); const m2 = html.match( new RegExp("courseware_id[=:]['\"]?" + wid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "['\"]?[^'\"]{0,80}['\"]([^'\"]{2,60})['\"]", "i") ); if (m2) return cleanChapterTitle(m2[1]); return ""; } function parseChaptersFromCourseHtml(html, courseId) { const cid = String(courseId || ""); const doc = new DOMParser().parseFromString(html, "text/html"); const chapters = parseKjJumpToChapters(html, courseId); const byWare = new Map(chapters.map((c) => [c.wareId, c])); doc.querySelectorAll("li.course_list, #s_r_ml li, .course_box li").forEach((li) => { const onclick = li.getAttribute("onclick") || li.innerHTML || ""; const m = onclick.match(/kjJumpTo\s*\(\s*['"]([^'"]+)['"]/i) || li.innerHTML.match(/kjJumpTo\s*\(\s*['"]([^'"]+)['"]/i); if (!m) return; const url = m[1]; if (!/study2|polyv/i.test(url)) return; const cidMatch = url.match(/course_id=(\d+)/i); if (cidMatch && cidMatch[1] !== cid) return; const wm = url.match(/courseware_id=([^&'"]+)/i); if (!wm) return; const wareId = wm[1]; const status = inferStatusFromNode(li); const a = li.querySelector("a"); let title = cleanChapterTitle(a?.textContent || ""); title = title.replace(/^\u7b2c\d+\u8282\s*/, "").trim(); const existing = byWare.get(wareId); if (existing) { if (title && title.length > 1 && title.length < 80) existing.title = title; existing.status = status; } else { byWare.set(wareId, { wareId, productId: "", title: title || "\u8bfe\u4ef6 " + wareId, status, }); } }); const out = [...byWare.values()].sort((a, b) => String(a.wareId).localeCompare(String(b.wareId), undefined, { numeric: true }) ); for (const ch of out) { if (!ch.title || ch.title.startsWith("\u8bfe\u4ef6 ")) { const t = parseWareTitleFromHtml(html, ch.wareId); if (t) ch.title = t; } } return out; } async function fetchCourseHtml(courseId) { if (pageType() === "course" && getCourseParams().csId === String(courseId)) { return document.documentElement.outerHTML; } const r = await platformFetch(`/cme/course.jsp?course_id=${courseId}&_=${Date.now()}`); return r.text; } async function probeWareFromStudy2(courseId, wareId) { const r = await platformFetch( `/cme/study2.jsp?course_id=${courseId}&courseware_id=${wareId}&_=${Date.now()}` ); const html = r.text; if (/\u8bfe\u7a0b\u4e0d\u5b58\u5728|\u672a\u627e\u5230|\u9519\u8bef\u9875\u9762|\u8bf7\u5148\u767b\u5f55/i.test(html)) return null; const sidebarMap = parseSidebarStatusMap(html); const sidebarStatus = sidebarMap[wareId] || sidebarMap[String(parseInt(wareId, 10))]; if (!/total_time\s*[=:]\s*\d+/i.test(html) && !/updatePlayStatus/i.test(html)) return null; const productId = pickHtml(html, [/product_id=(\d+)/i]) || ""; const title = parseWareTitleFromHtml(html, wareId) || "\u8bfe\u4ef6 " + wareId; let status = sidebarStatus || "study"; if (!sidebarStatus) { if (/\u5f85\u8003\u8bd5/.test(html) && !/\u5df2\u901a\u8fc7|\u5df2\u901a/.test(html)) status = "exam"; else if (/\u5df2\u901a\u8fc7|\u5df2\u901a|kstg|play100/.test(html)) status = "done"; } return { html, wareId: String(wareId), productId, title, status }; } async function fetchCourseChapters(courseId, depth) { depth = depth || 0; if (!courseId || depth > 2) return []; const html = await fetchCourseHtml(courseId); let chapters = parseChaptersFromCourseHtml(html, courseId); if (!chapters.length) { for (const wid of ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10"]) { try { const probe = await probeWareFromStudy2(courseId, wid); if (!probe) continue; const fromStudy2 = parseChaptersFromCourseHtml(probe.html, courseId); if (fromStudy2.length) { chapters = fromStudy2; break; } if (!chapters.some((c) => c.wareId === wid)) { chapters.push({ wareId: wid, productId: probe.productId, title: probe.title, status: probe.status, }); } } catch (_) {} } chapters.sort((a, b) => String(a.wareId).localeCompare(String(b.wareId), undefined, { numeric: true })); } return chapters; } function parseStudyMetaFromHtml(html, courseId, wareId) { const cid = String(courseId || ""); const wid = String(wareId || "").padStart(2, "0"); const loginName = pickHtml(html, [ /var\s+loginName\s*=\s*['"]([^'"]+)['"]/i, /loginName\s*=\s*['"]([^'"]+)['"]/i, /loginName=([^&"'\s]+)/i, /login_name=([^&"'\s]+)/i, /updatePlayStatus\.jsp\?loginName=([^&]+)/i, ]); const batchId = pickHtml(html, [ /batchid\s*:\s*['"]([a-f0-9-]{36})['"]/i, /batch_id=([a-f0-9-]{36})/i, /batch_id\s*[=:]\s*['"]([a-f0-9-]{36})['"]/i, /batchid\s*[=:]\s*['"]([a-f0-9-]{36})['"]/i, ]); const pageWareId = pickHtml(html, [/var\s+courseware_id\s*=\s*['"](\d+)['"]/i, /courseware_id\s*=\s*['"](\d+)['"]/i]); const resolvedWare = pageWareId ? String(pageWareId).padStart(2, "0") : wid; const totalTime = parseInt( pickHtml(html, [ /var\s+total_time\s*=\s*['"](\d+)['"]/i, /total_time\s*=\s*['"](\d+)['"]/i, /totalTime\s*:\s*['"](\d+)['"]/i, /total_time\s*[=:]\s*['"]?(\d+)/i, /totalTime\s*[=:]\s*['"]?(\d+)/i, ]) || "3600", 10 ); const userid = pickHtml(html, [ /var\s+user_id\s*=\s*['"]([a-f0-9]+)['"]/i, /var\s+user_id\s*=\s*['"]([a-f0-9]+)/i, /userid\s*[=:]\s*['"]([a-f0-9]+)['"]/i, /userid=([a-f0-9]+)/i, ]); const timeStamp = pickHtml(html, [ /var\s+timeStamp\s*=\s*['"](\d+)['"]/i, /timeStamp\s*[=:]\s*['"]?(\d+)/i, ]); const sign = pickHtml(html, [ /var\s+sign\s*=\s*['"]([a-f0-9]{32})['"]/i, /sign\s*[=:]\s*['"]([a-f0-9]{32})['"]/i, ]); const clientIp = pickHtml(html, [/updatePlayStatus\.jsp[^"']*?ip=([\d.]+)/i, /[?&]ip=([\d.]+)&/i]); let saveStartUrl = (html.match(/(\/qypx\/bj\/saveStudy6\.jsp\?course_id=\d+[^"'\s<>]*)/i) || [])[1] || (html.match(/(https?:\/\/[^"'\s<>]*saveStudy6\.jsp\?course_id=\d+[^"'\s<>]*)/i) || [])[1] || ""; let saveEndUrl = (html.match(/(\/qypx\/bj\/saveStudy3\.jsp\?course_id=\d+[^"'\s<>]*)/i) || [])[1] || (html.match(/(https?:\/\/[^"'\s<>]*saveStudy3\.jsp\?course_id=\d+[^"'\s<>]*)/i) || [])[1] || ""; if (!saveStartUrl && userid && sign && timeStamp && cid && resolvedWare) { saveStartUrl = `/qypx/bj/saveStudy6.jsp?course_id=${cid}&cware_id=${resolvedWare}&userid=${userid}&timeStamp=${timeStamp}&sign=${sign}`; saveEndUrl = `/qypx/bj/saveStudy3.jsp?course_id=${cid}&cware_id=${resolvedWare}&userid=${userid}&timeStamp=${timeStamp}&sign=${sign}`; } const productId = pickHtml(html, [ new RegExp( "exam\\.jsp\\?course_id=" + cid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "&paper_id=" + resolvedWare.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "[^\"']*product_id=(\\d+)", "i" ), /product_id=(\d+)/i, ]); return { html, loginName, batchId, totalTime: Number.isFinite(totalTime) && totalTime > 0 ? totalTime : 3600, productId, userid, timeStamp, sign, clientIp, saveStartUrl, saveEndUrl, courseId: cid, wareId: resolvedWare, }; } function syncMetaToCapture(meta, opts) { opts = opts || {}; if (!meta?.loginName || !meta?.batchId) return; const existing = readProgressCapture(meta.courseId, meta.wareId); const apiSt = loadApiProgressState(meta.courseId, meta.wareId); const lockedBatch = apiSt?.batchId || (existing?.sign && existing?.batchId && Number(existing.playingTime) >= 0 ? existing.batchId : null); if (lockedBatch && lockedBatch !== meta.batchId && !opts.force) return; const playingTime = existing?.batchId === meta.batchId && Number(existing.playingTime) > 0 ? Number(existing.playingTime) : lockedBatch === meta.batchId && Number(existing?.playingTime) > 0 ? Number(existing.playingTime) : 0; const patch = { courseId: String(meta.courseId || existing?.courseId || ""), loginName: meta.loginName, batchId: meta.batchId, totalTime: meta.totalTime, playingTime, ip: meta.clientIp || existing?.ip || "", }; if (existing?.sign && existing.batchId === meta.batchId) { patch.userid = existing.userid; patch.timeStamp = existing.timeStamp; patch.sign = existing.sign; } else if (meta.userid && meta.sign && meta.timeStamp) { patch.userid = meta.userid; patch.timeStamp = meta.timeStamp; patch.sign = meta.sign; } saveProgressCapture(meta.courseId, meta.wareId, patch); } async function fetchStudyMeta(courseId, wareId, opts) { opts = opts || {}; const wid = String(wareId || ""); const tryIds = [...new Set([wid, wid.padStart(2, "0"), wid.replace(/^0+/, "") || wid])]; const referer = API_BASE + `/cme/course.jsp?course_id=${courseId}`; let last = null; for (const id of tryIds) { const r = await documentFetch(`/cme/study2.jsp?course_id=${courseId}&courseware_id=${id}&_=${Date.now()}`, { headers: { Referer: referer }, }); const html = r.text; if (/\u8bf7\u5148\u767b\u5f55|\u767b\u5f55\u8d85\u65f6|\u672a\u767b\u5f55/i.test(html)) throw new Error("\u672a\u767b\u5f55\u6216\u4f1a\u8bdd\u5931\u6548"); const meta = parseStudyMetaFromHtml(html, courseId, id); last = meta; if (meta.loginName && meta.batchId) { if (!opts.skipCaptureSync) syncMetaToCapture(meta); return meta; } } if (last?.loginName && !last.batchId) { log(`study2 \u5df2\u89e3\u6790 loginName=${last.loginName}\uff0c\u4f46\u672a\u627e\u5230 batch_id`); } return last || parseStudyMetaFromHtml("", courseId, wid); } function study2Referer(meta) { const cid = meta.courseId || ""; const wid = normalizeWareId(meta.wareId || "01"); return API_BASE + `/cme/study2.jsp?course_id=${cid}&courseware_id=${wid}`; } async function reportProgress(meta, playingTime, status, opts) { opts = opts || {}; if (!meta.loginName || !meta.batchId) throw new Error("\u7f3a\u5c11 loginName/batch_id"); const u = new URL("/webcam/updatePlayStatus.jsp", location.origin); u.searchParams.set("loginName", meta.loginName); u.searchParams.set("course_id", meta.courseId); u.searchParams.set("ware_id", normalizeWareId(meta.wareId)); u.searchParams.set("batch_id", meta.batchId); u.searchParams.set("playing_time", String(Math.floor(playingTime))); u.searchParams.set("total_time", String(Math.floor(meta.totalTime))); u.searchParams.set("logType", "0"); u.searchParams.set("status", String(status == null ? 1 : status)); u.searchParams.set("ip", opts.ip || meta.clientIp || "127.0.0.1"); u.searchParams.set("t", String(Date.now())); const r = await platformFetch(u.pathname + "?" + u.searchParams.toString(), { headers: { Referer: study2Referer(meta) }, }); return r.text; } function parseApiState(text) { const raw = String(text || "").trim(); try { const j = JSON.parse(raw); if (j.state != null) return Number(j.state); if (j.status != null) return Number(j.status); } catch (_) {} const m = raw.match(/"state"\s*:\s*(\d+)/); return m ? Number(m[1]) : NaN; } async function buildLockedStudyMeta(csId, wareId, cap0, apiSt0) { const wid = normalizeWareId(wareId); if (apiSt0?.batchId && cap0?.sign && cap0.batchId === apiSt0.batchId) { return studyMetaFromCapture(cap0, csId, wid); } if (cap0?.sign && cap0?.batchId && Number(cap0.playingTime) > 0) { return studyMetaFromCapture(cap0, csId, wid); } const fetched = await fetchStudyMeta(csId, wareId, { skipCaptureSync: !!(apiSt0?.batchId || cap0?.sign) }); let locked = mergeStudyCredentials(fetched, cap0); if (apiSt0?.batchId) locked.batchId = apiSt0.batchId; else if (cap0?.batchId && cap0?.sign) locked.batchId = cap0.batchId; return locked; } const apiStudyRunGates = new Map(); async function withApiStudyRunLock(csId, wareId, fn) { const key = String(csId) + "_" + normalizeWareId(wareId); while (apiStudyRunGates.has(key)) { await apiStudyRunGates.get(key); } let release; const gate = new Promise((resolve) => { release = resolve; }); apiStudyRunGates.set(key, gate); try { return await fn(); } finally { apiStudyRunGates.delete(key); release(); } } async function pollIsExamReady(csId, wareId, opts) { opts = opts || {}; const times = opts.times || ISEXAM_POLL_TIMES; const interval = opts.intervalMs || ISEXAM_POLL_INTERVAL_MS; clearIsExamCache(csId, wareId); for (let i = 0; i < times; i++) { if (await checkIsExam(csId, wareId)) return true; if (i < times - 1) await sleep(interval); } return false; } function isValidSaveStudyUrl(url, jspName) { if (!url || typeof url !== "string") return false; if (/\{|\}/.test(url)) return false; if (!url.includes(jspName)) return false; if (!/course_id=\d+/i.test(url)) return false; if (!/sign=[a-f0-9]{32}/i.test(url)) return false; if (!/userid=[a-f0-9]+/i.test(url)) return false; return true; } function normalizeSaveStudyPath(url) { let p = String(url || "").replace(/^https?:\/\/[^/]+/i, ""); if (!p.startsWith("/")) p = "/" + p; return p.split("&_=")[0]; } function buildSaveStudyPath(meta, jsp) { const cid = String(meta.courseId || "").trim(); const wid = normalizeWareId(meta.wareId || "01"); const uid = String(meta.userid || "").trim(); const ts = String(meta.timeStamp || "").trim(); const sign = String(meta.sign || "").trim(); if (!/^\d+$/.test(cid)) return ""; if (!uid || !ts || !/^[a-f0-9]{32}$/i.test(sign)) return ""; return `/qypx/bj/${jsp}?course_id=${cid}&cware_id=${wid}&userid=${uid}&timeStamp=${ts}&sign=${sign}&_=${Date.now()}`; } function ensureStudyMeta(meta, csId, wareId) { const m = Object.assign({}, meta || {}); const cid = String(csId || m.courseId || m.csId || "").trim(); if (/^\d+$/.test(cid)) m.courseId = cid; if (wareId != null && wareId !== "") m.wareId = normalizeWareId(wareId); delete m.saveEndUrl; delete m.saveStartUrl; return m; } function resolveSaveStudyPath(meta, jsp) { return buildSaveStudyPath(meta, jsp); } function mergeStudyCredentials(meta, capture) { if (!capture) return meta; const m = Object.assign({}, meta); if (capture.batchId) m.batchId = capture.batchId; if (capture.loginName) m.loginName = capture.loginName; if (capture.userid) m.userid = capture.userid; if (capture.timeStamp) m.timeStamp = capture.timeStamp; if (capture.sign && /^[a-f0-9]{32}$/i.test(String(capture.sign))) m.sign = capture.sign; if (capture.totalTime) m.totalTime = capture.totalTime; if (capture.ip) m.clientIp = capture.ip; if (!m.courseId && capture.courseId && /^\d+$/.test(String(capture.courseId))) { m.courseId = String(capture.courseId); } delete m.saveEndUrl; delete m.saveStartUrl; return m; } function studyMetaFromCapture(cap, csId, wareId) { return mergeStudyCredentials( { courseId: String(csId), wareId: normalizeWareId(wareId), totalTime: Number(cap?.totalTime) || 3600, }, cap ); } async function resolveStudyMetaForStep(csId, wareId, opts) { const wid = String(wareId || ""); const st = loadApiProgressState(csId, wid); const capture = readProgressCapture(csId, wid); if (opts?.lockedMeta) { const m = Object.assign({}, opts.lockedMeta); if (st?.batchId) m.batchId = st.batchId; else if (capture?.batchId) m.batchId = capture.batchId; m.wareId = st?.wareId || m.wareId || wid; m.courseId = String(csId); return mergeStudyCredentials(m, capture); } const meta = await fetchStudyMeta(csId, wareId, { skipCaptureSync: !!(st?.batchId || capture?.sign), }); let merged = meta; if (st?.batchId) merged = Object.assign({}, meta, { batchId: st.batchId }); else if (capture?.batchId) merged = Object.assign({}, meta, { batchId: capture.batchId }); return mergeStudyCredentials(merged, capture); } function progressCaptureKey(csId, wareId) { return "cme_progress_capture_" + csId + "_" + normalizeWareId(wareId); } function readProgressCapture(csId, wareId) { for (const w of [...new Set(wareIdVariants(wareId).map(normalizeWareId))]) { const raw = localStorage.getItem("cme_progress_capture_" + csId + "_" + w); if (!raw) continue; try { const c = JSON.parse(raw); if (c && c.batchId) return c; } catch (_) {} } return null; } function saveProgressCapture(csId, wareId, data) { const prev = readProgressCapture(csId, wareId) || {}; localStorage.setItem( progressCaptureKey(csId, wareId), JSON.stringify(Object.assign({}, prev, data, { t: Date.now() })) ); } function markApiReportedFull(csId, wareId, cap) { localStorage.setItem( API_FULL_KEY_PREFIX + csId + "_" + normalizeWareId(wareId), JSON.stringify({ batchId: cap?.batchId, totalTime: cap?.totalTime, loginName: cap?.loginName, t: Date.now(), }) ); } function isApiReportedFull(csId, wareId) { return !!localStorage.getItem(API_FULL_KEY_PREFIX + csId + "_" + normalizeWareId(wareId)); } function clearApiReportedFull(csId, wareId) { for (const w of [...new Set(wareIdVariants(wareId).map(normalizeWareId))]) { localStorage.removeItem(API_FULL_KEY_PREFIX + csId + "_" + w); } } function shouldOnlyWaitIsExam(csId, wareId) { return isApiReportedFull(csId, wareId) || isChapterProgressFull(csId, wareId); } function isChapterProgressFull(csId, wareId) { const cap = readProgressCapture(csId, wareId); if (!cap?.batchId || !Number(cap.totalTime)) return false; return Number(cap.playingTime) >= Number(cap.totalTime) * 0.99; } async function softBoostIsExamAfterFull(csId, wareId) { const cap = readProgressCapture(csId, wareId); if (!cap?.batchId || !cap?.sign) return false; const meta = studyMetaFromCapture(cap, csId, wareId); if (!meta.loginName) return false; const ip = cap.ip || meta.clientIp || "127.0.0.1"; meta.clientIp = ip; await reportProgress(meta, cap.totalTime, 5, { ip }); await sleep(600); await pingCcState(meta, 2); await callSaveStudyEnd(meta, csId, wareId); clearIsExamCache(csId, wareId); invalidateSidebarStatusMap(csId); return pollIsExamReady(csId, wareId, { times: 12, intervalMs: 1500 }); } function loadApiProgressState(csId, wareId) { for (const w of wareIdVariants(wareId)) { const raw = localStorage.getItem(API_PROGRESS_KEY_PREFIX + csId + "_" + w); if (!raw) continue; try { return JSON.parse(raw); } catch (_) {} } return null; } function saveApiProgressState(st) { localStorage.setItem( API_PROGRESS_KEY_PREFIX + st.csId + "_" + normalizeWareId(st.wareId), JSON.stringify({ ...st, wareId: normalizeWareId(st.wareId) }) ); } function clearApiProgressState(csId, wareId) { for (const w of wareIdVariants(wareId)) { localStorage.removeItem(API_PROGRESS_KEY_PREFIX + csId + "_" + w); } } function studyDoneSignalKey(courseId, wareId) { return `cme_study_done_${courseId}_${wareId}`; } function chapterProgressKey(courseId, wareId) { return `${CHAPTER_PROGRESS_PREFIX}${courseId}_${wareId}`; } function markChapterProgressReported(courseId, wareId) { for (const w of wareIdVariants(wareId)) { localStorage.setItem(chapterProgressKey(courseId, w), String(Date.now())); } } function isChapterProgressReported(courseId, wareId) { for (const w of wareIdVariants(wareId)) { if (localStorage.getItem(chapterProgressKey(courseId, w))) return true; } return false; } function clearChapterProgressReported(courseId, wareId) { for (const w of wareIdVariants(wareId)) { localStorage.removeItem(chapterProgressKey(courseId, w)); } } function wareIdsMatch(a, b) { if (!a || !b) return false; const av = wareIdVariants(a); const bv = wareIdVariants(b); return av.some((x) => bv.includes(x)); } async function waitStudyDoneSignal(courseId, wareId, timeoutMs) { const deadline = Date.now() + (timeoutMs || 900000); while (Date.now() < deadline) { for (const w of wareIdVariants(wareId)) { const raw = localStorage.getItem(studyDoneSignalKey(courseId, w)); if (!raw) continue; try { const j = JSON.parse(raw); if (j && j.t && Date.now() - j.t < 900000) return j; } catch (_) {} } await sleep(2000); } return null; } function shouldRunPageStudyOnThisTab() { const { csId, wareId } = getCourseParams(); if (!csId) return null; const wid = wareId || "01"; const pending = (() => { try { return JSON.parse(localStorage.getItem(PAGE_STUDY_PENDING_KEY) || "null"); } catch (_) { return null; } })(); if (pending && pending.csId === csId && wareIdsMatch(pending.wareId, wid)) { return { csId, wareId: wid, reason: "pending" }; } if (localStorage.getItem(STORAGE_KEY) !== "1") return null; const bg = loadBgState(); if (bg.phase !== "study" || bg.csId !== csId) return null; const cur = bg.videos?.[bg.videoIndex || 0]; if (cur?.resId && wareIdsMatch(cur.resId, wid)) { return { csId, wareId: wid, reason: "bg" }; } return null; } function initStudyPageWorker() { if (pageType() !== "courseplay") return; const hit = shouldRunPageStudyOnThisTab(); if (!hit) return; for (const w of wareIdVariants(hit.wareId)) { const sig = localStorage.getItem(studyDoneSignalKey(hit.csId, w)); if (sig) { try { const j = JSON.parse(sig); if (j?.ok && j.t && Date.now() - j.t < 120000) return; } catch (_) {} } } const start = () => { log(`\u7b2c${formatWareNo(hit.wareId)}\u8282 \u64ad\u653e\u9875\u5f15\u64ce\u542f\u52a8\uff08OpenScript \u8df3\u81f3\u7247\u5c3e\uff09…`); injectPageStudyComplete(hit.csId, hit.wareId); }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => setTimeout(start, 1500)); } else { setTimeout(start, 1500); } } function readRecentStudyDone(csId, wareId) { for (const w of wareIdVariants(wareId)) { const raw = localStorage.getItem(studyDoneSignalKey(csId, w)); if (!raw) continue; try { const j = JSON.parse(raw); if (j?.ok && j.t && Date.now() - j.t < 600000) return j; } catch (_) {} } return null; } async function navigateToPageStudy(csId, video) { const wid = String(video.cptId || video.resId); localStorage.setItem(PAGE_STUDY_PENDING_KEY, JSON.stringify({ csId, wareId: wid, t: Date.now() })); localStorage.setItem(RETURN_URL_AFTER_STUDY_KEY, location.href); for (const w of wareIdVariants(wid)) localStorage.removeItem(studyDoneSignalKey(csId, w)); markStudySessionStart(csId, wid); const url = API_BASE + courseplayPath(csId, "", wid, video.productId); log(`\u7b2c${formatWareNo(wid)}\u8282 \u8df3\u8f6c\u64ad\u653e\u9875\u5237\u8bfe\uff08\u5e73\u53f0\u987b\u5728 study2 \u64ad\u653e\u9875\u4e0a\u62a5\uff0c\u5b8c\u6210\u540e\u56de\u6536\u85cf\u9875\uff09…`); location.href = url; await new Promise(() => {}); } async function tryDelegatedPageStudy(csId, video) { const wid = String(video.cptId || video.resId); if (pageType() === "courseplay" && getCourseParams().csId === csId && wareIdsMatch(getCourseParams().wareId, wid)) { localStorage.removeItem(studyDoneSignalKey(csId, wid)); injectPageStudyComplete(csId, wid); log(`\u7b2c${formatWareNo(wid)}\u8282 \u5f53\u524d\u9875\u6267\u884c v1.4.1 \u5237\u8bfe…`); const sig = await waitStudyDoneSignal(csId, wid, 900000); if (sig?.ok) { markChapterProgressReported(csId, wid); return true; } if (sig) log(`\u7b2c${formatWareNo(wid)}\u8282 \u9875\u9762\u5237\u8bfe\u5931\u8d25\uff1a${sig.msg || "\u672a\u77e5"}`); return false; } const url = API_BASE + courseplayPath(csId, "", wid, video.productId); localStorage.setItem(PAGE_STUDY_PENDING_KEY, JSON.stringify({ csId, wareId: wid, t: Date.now() })); for (const w of wareIdVariants(wid)) localStorage.removeItem(studyDoneSignalKey(csId, w)); try { GM_openInTab(url, { active: true, insert: true, setParent: true }); } catch (_) { window.open(url, "_blank"); } log(`\u7b2c${formatWareNo(wid)}\u8282 \u5df2\u6253\u5f00\u64ad\u653e\u9875\uff08\u8bf7\u4fdd\u6301\u8be5\u6807\u7b7e\u9875\u5728\u524d\u53f0\u76f4\u81f3\u5237\u5b8c\uff09…`); const sig = await waitStudyDoneSignal(csId, wid, 900000); localStorage.removeItem(PAGE_STUDY_PENDING_KEY); if (sig?.ok) { markChapterProgressReported(csId, wid); return true; } if (sig) log(`\u7b2c${formatWareNo(wid)}\u8282 \u9875\u9762\u5237\u8bfe\u5931\u8d25\uff1a${sig.msg || "\u672a\u77e5"}\uff0c\u6539 API \u5237\u8bfe`); return false; } function studySessionKey(courseId, wareId) { return `${STUDY_SESSION_PREFIX}${courseId}_${wareId}`; } function markStudySessionStart(courseId, wareId) { const key = studySessionKey(courseId, wareId); if (!localStorage.getItem(key)) localStorage.setItem(key, String(Date.now())); return parseInt(localStorage.getItem(key) || String(Date.now()), 10); } function getStudyWaitInfo(courseId, wareId, totalSec) { const key = studySessionKey(courseId, wareId); const start = parseInt(localStorage.getItem(key) || String(Date.now()), 10); const elapsed = Math.floor((Date.now() - start) / 1000); const need = Number(totalSec) || 0; const remain = Math.max(0, need - elapsed); return { start, elapsed, need, remain, timeReady: remain <= 0 }; } async function checkChapterExamReady(courseId, wareId, totalSec) { let videoDone = false; for (const pid of wareIdVariants(wareId)) { if (await checkIsExam(courseId, pid)) { videoDone = true; break; } } const wait = getStudyWaitInfo(courseId, wareId, totalSec); return { videoDone, timeReady: wait.timeReady, canEnterExam: videoDone, remain: wait.remain, need: wait.need, elapsed: wait.elapsed, }; } function isWrongExamH3(el) { if (!el) return false; return el.classList.contains("cuo") || /\bcuo\b/.test(el.className); } function isExamPassResponse(text, url) { if (!text) return false; if (text.includes("\u5b66\u4e60\u65f6\u957f\u4e0d\u8db3")) return false; if (String(url || "").includes("examQuizFail")) return false; if (text.includes("answer_list") && text.includes("cuo")) return false; if (text.includes("show_exam_btns") || text.includes("\u8003\u8bd5\u901a\u8fc7") || text.includes("\u606d\u559c")) return true; if (text.includes("examSuccess") || text.includes("exam_pass")) return true; return false; } async function fixAnswersFromFail(text, url, answers) { let failText = text; let failUrl = url; if (failUrl.includes("examQuizFail") && !failText.includes("answer_list")) { const r = await documentFetch(failUrl); failText = r.text; failUrl = r.url; } if (!failText.includes("answer_list") && !failText.includes("cuo")) { const linkMatch = failText.match(/examQuizFail\.jsp[^"'\s<>]*/); if (linkMatch) { const failPath = linkMatch[0].startsWith("/") ? linkMatch[0] : `/cme/${linkMatch[0]}`; const r = await documentFetch(failPath); failText = r.text; failUrl = r.url; } } const next = [...answers]; const ansMatch = failUrl.match(/ansList=([^&]+)/) || failText.match(/ansList=([^&'"#]+)/); if (ansMatch) { ansMatch[1].split(",").forEach((a, i) => { if (a) next[i] = a; }); } const doc = new DOMParser().parseFromString(failText, "text/html"); const lists = doc.querySelectorAll(".answer_list"); const wrongIdx = []; lists.forEach((list, i) => { const h3 = list.querySelector("h3"); if (isWrongExamH3(h3)) { wrongIdx.push(i); if (next[i]?.length === 1) next[i] = nextExamChoice(next[i]); } }); return { answers: next, fixed: wrongIdx.length > 0, wrongIdx, failUrl }; } async function pingCcState(meta, times) { if (!meta?.loginName) return; const n = times || 1; const path = `/webcam/ccstate.jsp?loginName=${encodeURIComponent(meta.loginName)}` + `&course_id=${meta.courseId}&ware_id=${normalizeWareId(meta.wareId)}&expiresTime=10&t=${Date.now()}`; for (let i = 0; i < n; i++) { try { await platformFetch(path); } catch (_) {} if (i < n - 1) await sleep(300); } } async function apiStudyProgressStep(csId, wareId, opts) { opts = opts || {}; const meta = await resolveStudyMetaForStep(csId, wareId, opts); if (!meta.loginName || !meta.batchId) { throw new Error(`study2.jsp \u672a\u89e3\u6790\u5230 loginName/batch_id\uff08${meta.loginName || "\u65e0loginName"}\uff09`); } const wid = String(meta.wareId || wareId); const capture = readProgressCapture(csId, wid); const clientIp = capture?.ip || meta.clientIp || "127.0.0.1"; const st0 = loadApiProgressState(csId, wid); if (!opts.quiet && !st0) { trace( `study2 API\u53c2\u6570\uff1aloginName=${meta.loginName} ware=${wid} batch=${meta.batchId.slice(0, 8)}… total=${meta.totalTime}s ip=${clientIp}` ); } meta.clientIp = clientIp; const total = meta.totalTime; const step = opts.step || PROGRESS_STEP_SEC; const gapMs = opts.gapMs != null ? opts.gapMs : API_PROGRESS_GAP_MS; let st = loadApiProgressState(csId, wid); const apiStResume = st; if (!st || st.batchId !== meta.batchId) { let startCurrent = 0; const sameBatch = capture && capture.batchId === meta.batchId; if (sameBatch) startCurrent = Number(capture.playingTime) || 0; const resumeProgress = apiStResume && apiStResume.batchId === meta.batchId ? Number(apiStResume.current) : 0; if (resumeProgress > startCurrent) startCurrent = resumeProgress; const reuseBatch = sameBatch && (startCurrent > 0 || capture.needSlowRetry || capture.slowWalkStarted || resumeProgress > 0); if (reuseBatch) { if (capture.needSlowRetry && !capture.slowWalkStarted) { trace( `\u7b2c${formatWareNo(wid)}\u8282 \u91cd\u8bd5\u7eed\u62a5\uff085.5s/\u6863\uff0cbatch ${meta.batchId.slice(0, 8)}…\uff09` ); saveProgressCapture(csId, wid, { slowWalkStarted: true }); } else { trace( `\u7b2c${formatWareNo(wid)}\u8282 \u7eed\u62a5 batch=${meta.batchId.slice(0, 8)}… \u4ece ${startCurrent}/${total} \u79d2` ); } } else { markStudySessionStart(meta.courseId, wid); await callSaveStudyStart(meta, csId, wid); await reportProgress(meta, 0, 1, { ip: clientIp }); await pingCcState(meta, 2); saveProgressCapture(csId, wid, { courseId: String(csId), loginName: meta.loginName, batchId: meta.batchId, totalTime: total, playingTime: 0, ip: clientIp, userid: meta.userid, timeStamp: meta.timeStamp, sign: meta.sign, }); trace(`\u7b2c${formatWareNo(wid)}\u8282 API\u5237\u8bfe\u5f00\u59cb loginName=${meta.loginName} batch=${meta.batchId.slice(0, 8)}…`); } st = { csId: String(csId), wareId: wid, batchId: meta.batchId, loginName: meta.loginName, current: startCurrent, total, nextAt: 0, clientIp, }; } if (Date.now() < st.nextAt) { const remain = Math.ceil((st.nextAt - Date.now()) / 1000); return { waiting: true, done: false, remain, current: st.current, total: st.total, isReady: false, wid, }; } const next = Math.min(st.current + step, st.total); await sleep(opts.leadMs != null ? opts.leadMs : API_PROGRESS_LEAD_MS); await reportProgress(meta, next, next >= st.total ? 5 : 1, { ip: st.clientIp }); await pingCcState(meta, 2); st.current = next; saveProgressCapture(csId, wid, { courseId: String(csId), loginName: meta.loginName, batchId: meta.batchId, totalTime: st.total, playingTime: next, ip: st.clientIp, userid: meta.userid || capture?.userid, timeStamp: meta.timeStamp || capture?.timeStamp, sign: meta.sign || capture?.sign, }); if (opts.logEach) { trace( `\u7b2c${formatWareNo(wid)}\u8282 API\u4e0a\u62a5 ${next}/${st.total} \u79d2\uff08${Math.round((next / st.total) * 100)}%\uff09` ); } if (next >= st.total) { await sleep(500); await pingCcState(meta, 3); const metaEnd = ensureStudyMeta( mergeStudyCredentials(meta, readProgressCapture(csId, wid)), csId, wid ); let saved = await callSaveStudyEnd(metaEnd, csId, wid); if (!saved) { await sleep(1000); saved = await callSaveStudyEnd(metaEnd, csId, wid); } trace(`\u7b2c${formatWareNo(wid)}\u8282 saveStudy3 \u672a\u8fd4\u56de state=1\uff08sign=${String(metaEnd.sign || "").slice(0, 8)}…\uff09`); clearApiProgressState(csId, wid); clearIsExamCache(csId, wid); invalidateSidebarStatusMap(csId); const capDone = { courseId: String(csId), loginName: meta.loginName, batchId: meta.batchId, totalTime: st.total, playingTime: st.total, ip: st.clientIp, userid: metaEnd.userid, timeStamp: metaEnd.timeStamp, sign: metaEnd.sign, completed: true, }; saveProgressCapture(csId, wid, capDone); markApiReportedFull(csId, wid, capDone); const isReady = await pollIsExamReady(csId, wid, { times: ISEXAM_POLL_AFTER_FULL_TIMES, intervalMs: ISEXAM_POLL_INTERVAL_MS, }); const sidebarSt = await querySidebarStatus(csId, wid); if (!isReady) { if (!saved) { saveProgressCapture(csId, wid, { needSaveStudy3Retry: true, needSlowRetry: false, slowWalkStarted: false, playingTime: st.total, courseId: String(csId), userid: metaEnd.userid, timeStamp: metaEnd.timeStamp, sign: metaEnd.sign, }); clearApiProgressState(csId, wid); trace(`\u7b2c${formatWareNo(wid)}\u8282 saveStudy3 \u5931\u8d25\uff0c\u5c06\u7528\u9501\u5b9a sign \u91cd\u8bd5\uff08\u4e0d\u91cd\u62a5\u8fdb\u5ea6\uff09`); } else { trace(`\u7b2c${formatWareNo(wid)}\u8282 \u5df2\u62a5\u6ee1 ${st.total}s\uff0cisexam \u672a\u540c\u6b65\uff0c\u7a0d\u540e\u8f7b\u63a8\uff08batch \u9501\u5b9a ${meta.batchId.slice(0, 8)}…\uff09`); } } else { clearApiReportedFull(csId, wid); saveProgressCapture(csId, wid, { needSlowRetry: false }); } return { waiting: false, done: true, current: next, total: st.total, isReady, sidebarSt, wid, }; } if (opts.burst) { st.nextAt = 0; } else { st.nextAt = Date.now() + gapMs; } saveApiProgressState(st); const remain = opts.burst ? Math.ceil(gapMs / 1000) : Math.ceil((st.nextAt - Date.now()) / 1000); trace(`\u7b2c${formatWareNo(wid)}\u8282 \u5df2\u62a5 ${next}/${st.total}\uff0c${remain}s \u540e\u62a5\u4e0b\u4e00\u6863`); return { waiting: true, done: false, remain, current: next, total: st.total, isReady: false, wid, }; } function clearApiProgressWait(csId, wareId) { const st = loadApiProgressState(csId, wareId); if (st && st.nextAt > Date.now()) { st.nextAt = 0; saveApiProgressState(st); } } async function waitIsExamOnlyTick(csId, wareId) { const wid = normalizeWareId(wareId); const cap = readProgressCapture(csId, wid); if (await checkIsExam(csId, wid)) { return { waiting: false, done: true, isReady: true, wid, current: Number(cap?.playingTime) || cap?.totalTime, total: Number(cap?.totalTime) || 3600, }; } const lastBoost = Number(cap?.lastBoostAt) || 0; let ready = false; if (Date.now() - lastBoost > 12000) { ready = await softBoostIsExamAfterFull(csId, wid); saveProgressCapture(csId, wid, Object.assign({}, cap || {}, { lastBoostAt: Date.now() })); } else { ready = await pollIsExamReady(csId, wid, { times: 3, intervalMs: 1500 }); } const sidebarSt = await querySidebarStatus(csId, wid); return { waiting: false, done: true, isReady: ready, wid, current: Number(cap?.playingTime) || cap?.totalTime, total: Number(cap?.totalTime) || 3600, sidebarSt, }; } async function tryOneShotStudyReport(csId, wareId, opts) { opts = opts || {}; const wid = normalizeWareId(wareId); if (opts.skipOneShot || shouldOnlyWaitIsExam(csId, wid)) return null; if (isChapterProgressFull(csId, wid)) return null; const capture = readProgressCapture(csId, wid); const apiSt = loadApiProgressState(csId, wid); if (Number(apiSt?.current) > 0) return null; if (Number(capture?.playingTime) > 0) return null; const meta = ensureStudyMeta( Object.assign({}, opts.lockedMeta || (await fetchStudyMeta(csId, wareId)), { courseId: String(csId), wareId: wid, }), csId, wid ); if (!meta.loginName || !meta.batchId) return null; const clientIp = capture?.ip || meta.clientIp || "127.0.0.1"; meta.clientIp = clientIp; const total = meta.totalTime; markStudySessionStart(meta.courseId, wid); await callSaveStudyStart(meta, csId, wid); await reportProgress(meta, 0, 1, { ip: clientIp }); await pingCcState(meta, 2); trace(`\u7b2c${formatWareNo(wid)}\u8282 \u5c1d\u8bd5\u4e00\u6b21\u6027\u4e0a\u62a5 ${total}s…`); await sleep(API_PROGRESS_LEAD_MS); await reportProgress(meta, total, 5, { ip: clientIp }); await sleep(500); await pingCcState(meta, 3); const capPatch = { courseId: String(csId), loginName: meta.loginName, batchId: meta.batchId, totalTime: total, playingTime: total, ip: clientIp, userid: meta.userid, timeStamp: meta.timeStamp, sign: meta.sign, }; saveProgressCapture(csId, wid, capPatch); clearIsExamCache(csId, wid); invalidateSidebarStatusMap(csId); const saved = await callSaveStudyEnd(meta, csId, wid); if (!saved) { trace(`\u7b2c${formatWareNo(wid)}\u8282 \u4e00\u6b21\u6027 saveStudy3 \u672a\u6210\u529f\uff0c\u6539\u5206\u6863\u8fde\u62a5…`); saveApiProgressState({ csId: String(csId), wareId: wid, batchId: meta.batchId, loginName: meta.loginName, current: 0, total, nextAt: 0, clientIp, }); saveProgressCapture(csId, wid, Object.assign({}, capPatch, { playingTime: 0 })); return null; } markApiReportedFull(csId, wid, capPatch); const isReady = await pollIsExamReady(csId, wid, { times: ISEXAM_POLL_AFTER_FULL_TIMES, intervalMs: ISEXAM_POLL_INTERVAL_MS, }); const sidebarSt = await querySidebarStatus(csId, wid); clearApiProgressState(csId, wid); if (isReady) { clearApiReportedFull(csId, wid); trace(`\u7b2c${formatWareNo(wid)}\u8282 \u4e00\u6b21\u6027\u4e0a\u62a5\u6210\u529f\uff0cisexam=1`); } else { trace(`\u7b2c${formatWareNo(wid)}\u8282 \u4e00\u6b21\u6027\u5df2\u62a5\u6ee1\uff0cisexam \u672a\u540c\u6b65\uff0c\u7a0d\u540e\u8f7b\u63a8…`); } return { waiting: false, done: true, current: total, total, isReady, sidebarSt, wid, }; } async function retrySaveStudy3Only(csId, wareId) { const wid = normalizeWareId(wareId); const cap = readProgressCapture(csId, wid); if (!cap?.sign || !cap?.batchId) return null; const meta = studyMetaFromCapture(cap, csId, wid); trace(`\u7b2c${formatWareNo(wid)}\u8282 \u91cd\u8bd5 saveStudy3\uff08\u9501\u5b9a sign=${String(meta.sign).slice(0, 8)}…\uff09`); let saved = await callSaveStudyEnd(meta, csId, wid); if (!saved) { await sleep(800); await reportProgress(meta, cap.totalTime, 5, { ip: cap.ip || meta.clientIp }); await sleep(500); await pingCcState(meta, 2); saved = await callSaveStudyEnd(meta, csId, wid); } clearIsExamCache(csId, wid); invalidateSidebarStatusMap(csId); if (saved) { saveProgressCapture(csId, wid, { needSaveStudy3Retry: false, needSlowRetry: false }); const isReady = await pollIsExamReady(csId, wid, { times: ISEXAM_POLL_AFTER_FULL_TIMES, intervalMs: ISEXAM_POLL_INTERVAL_MS, }); return { waiting: false, done: true, isReady, wid, current: Number(cap.totalTime), total: Number(cap.totalTime), }; } return null; } async function runApiStudyProgress(_csId, _wareId, _opts) { throw new Error("\u672c\u5730\u5f15\u64ce\u5df2\u5173\u95ed\uff0c\u8bf7\u4f7f\u7528\u4e91\u7aef\u6388\u6743"); } function isApiOnlyStudy() { if (isRealtimeStudyMode()) return false; const v = localStorage.getItem(API_ONLY_STUDY_KEY); if (v === "0") return false; return v === "1" || v === null || v === ""; } async function callSaveStudyStart(meta, csId, wareId) { meta = ensureStudyMeta(meta, csId, wareId); const hdr = { Referer: study2Referer(meta), Accept: "application/json, text/javascript, */*; q=0.01", }; const path = resolveSaveStudyPath(meta, "saveStudy6.jsp"); if (!path) return; const r = await platformFetch(path, { headers: hdr }); parseApiState(r.text); } async function callSaveStudyEnd(meta, csId, wareId) { meta = ensureStudyMeta(meta, csId, wareId); const hdr = { Referer: study2Referer(meta), Accept: "application/json, text/javascript, */*; q=0.01", }; const path = resolveSaveStudyPath(meta, "saveStudy3.jsp"); if (!path) return false; const r = await platformFetch(path, { headers: hdr }); const st = parseApiState(r.text); return Number(st) === 1; } const nextExamChoice = (s) => { const c = s.charCodeAt(0) + 1; return c > 69 ? "A" : String.fromCharCode(c); }; const isExamCache = new Map(); function buildTestFromVideo(v) { const pid = v?.productId || ""; const wid = String(v?.resId || v?.cptId || ""); return { tsId: wid, tsSpcId: pid, spcId: pid, productId: pid, resName: v?.resName || "", }; } function clearIsExamCache(courseId, wareId) { const cid = String(courseId || ""); for (const pid of wareIdVariants(wareId)) { for (const k of [...isExamCache.keys()]) { if (k.startsWith(`${cid}:${pid}:`)) isExamCache.delete(k); } } } async function fetchIsExamState(courseId, paperId, type) { type = type || 7; const cid = String(courseId || ""); const pid = String(paperId || ""); const r = await platformFetch( `/isexam.jsp?course_id=${cid}&paper_id=${pid}&type=${type}&_=${Date.now()}` ); const text = String(r.text || "").trim(); let state = NaN; try { state = Number(JSON.parse(text).state); } catch (_) { const m = text.match(/"state"\s*:\s*(\d+)/); if (m) state = Number(m[1]); } return { ok: Number(state) === 1, state, text, paperId: pid }; } async function checkIsExam(courseId, paperId, type) { type = type || 7; const cid = String(courseId || ""); for (const pid of wareIdVariants(paperId)) { const key = `${cid}:${pid}:${type}`; const hit = isExamCache.get(key); if (hit && Date.now() - hit.t < 30000) { if (hit.v) return true; continue; } try { const res = await fetchIsExamState(cid, pid, type); isExamCache.set(key, { v: res.ok, t: Date.now() }); if (res.ok) return true; } catch (_) {} } return false; } function isChapterNeedStudy(v) { return !!(v && v.chapterStatus === "study"); } function isChapterNeedExam(v) { return !!(v && v.chapterStatus === "exam"); } function isChapterDone(v) { return !!(v && v.chapterStatus === "done"); } function compareWareId(a, b) { return String(a || "").localeCompare(String(b || ""), undefined, { numeric: true }); } function formatWareNo(wareId) { const n = parseInt(String(wareId || "").replace(/^0+/, ""), 10); return Number.isFinite(n) ? String(n) : String(wareId || ""); } function sortVideosByWare(videos) { return [...(videos || [])].sort((a, b) => compareWareId(a.resId, b.resId)); } async function canTakeChapterExam(csId, wareId) { for (const pid of wareIdVariants(wareId)) { if (await checkIsExam(csId, pid)) return pid; } return ""; } function isHoldingSameChapterExam(bg, wareId) { if (!bg) return false; if (bg.phase !== "exam") return false; const hold = bg.pendingExamWareId || bg.videos?.[bg.videoIndex || 0]?.resId || bg.test?.tsId || ""; if (!hold) return !!bg.sameChapterExam; if (!wareId) return true; const a = wareIdVariants(hold); const b = wareIdVariants(wareId); return a.some((x) => b.includes(x)); } async function tryBeginSameChapterExam(bg, video) { if (!getAutoExam() || !video) return false; clearIsExamCache(bg.csId, video.resId); let readyPaper = await canTakeChapterExam(bg.csId, video.resId); if (!readyPaper) { for (let i = 0; i < 4; i++) { await sleep(2000); readyPaper = await canTakeChapterExam(bg.csId, video.resId); if (readyPaper) break; } } if (!readyPaper) { if (isChapterProgressFull(bg.csId, video.resId)) { trace(`\u7b2c${formatWareNo(video.resId)}\u8282 \u5df2\u62a5\u6ee1\uff0c\u7b49\u5f85\u8003\u8bd5\u5c31\u7eea`); } bg.lastSaveAt = 0; saveBgState(bg); return false; } const chapter = { ...video, resId: readyPaper }; bg.phase = "exam"; bg.videos = [chapter]; bg.videoIndex = 0; bg.test = buildTestFromVideo(chapter); bg.lastSaveAt = 0; bg.sameChapterExam = true; bg.pendingExamWareId = chapter.resId; bg.apiBoostCount = 0; saveBgState(bg); const label = chapter.resName || `\u8bfe\u4ef6${formatWareNo(chapter.resId)}`; log(`\u7b2c${formatWareNo(chapter.resId)}\u8282 \u8fdb\u5165\u672c\u8282\u8003\u8bd5\uff1a${label}`); return true; } async function partitionCourseWork(csId, opts) { const holdExamWare = opts?.holdExamWare ? String(opts.holdExamWare) : ""; const all = sortVideosByWare(await loadCourseVideos(csId, true)); const study = []; const exam = []; for (const v of all) { if (isChapterDone(v)) continue; if (holdExamWare && wareIdVariants(v.resId).some((k) => wareIdVariants(holdExamWare).includes(k))) { exam.push(v); continue; } if (isChapterNeedExam(v)) { exam.push(v); continue; } if (isChapterNeedStudy(v)) { if (await checkIsExam(csId, v.resId)) exam.push(v); else if (isChapterProgressReported(csId, v.resId) && (await querySidebarStatus(csId, v.resId)) === "exam") { exam.push(v); } else study.push(v); continue; } study.push(v); } return { all, study, exam }; } function wareIdVariants(wareId) { const w = String(wareId || ""); const out = [w]; const n = w.replace(/^0+/, "") || w; const p2 = n.padStart(2, "0"); if (!out.includes(n)) out.push(n); if (!out.includes(p2)) out.push(p2); return out; } async function resolveExamContext(courseId, wareId, productId) { const cid = String(courseId || ""); let wid = String(wareId || ""); let pid = String(productId || ""); let resName = ""; try { const meta = await fetchStudyMeta(cid, wid); if (meta.productId) pid = meta.productId; const t = parseWareTitleFromHtml(meta.html, wid); if (t) resName = t; if (!pid) { const m = meta.html.match( new RegExp( "exam\\.jsp\\?course_id=" + cid + "&paper_id=" + wid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "[^\"']*product_id=(\\d+)", "i" ) ); if (m) pid = m[1]; } } catch (_) {} if (!pid) { try { const html = await fetchCourseHtml(cid); const m = html.match( new RegExp("paper_id=" + wid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "[^\"']*product_id=(\\d+)", "i") ); if (m) pid = m[1]; } catch (_) {} } return { courseId: cid, wareId: wid, productId: pid, resName }; } function parseExamDocument(html) { const doc = new DOMParser().parseFromString(html, "text/html"); const form = doc.querySelector('form[name="form1"]') || doc.querySelector("form#form1") || doc.querySelector('form[action*="examDo"]'); const items = doc.querySelectorAll(".exam_list li, .exam-list li, .exam_list .exam_item, ul.exam_list > li"); let qCount = items.length; if (!qCount && form) { const names = new Set(); form.querySelectorAll('input[type="radio"]').forEach((r) => r.name && names.add(r.name)); qCount = names.size; } return { doc, form, qCount, items }; } async function fetchExamPage(courseId, wareId, productId) { const ctx = await resolveExamContext(courseId, wareId, productId); const referer = API_BASE + `/cme/study2.jsp?course_id=${ctx.courseId}&courseware_id=${ctx.wareId}`; const paperIds = wareIdVariants(ctx.wareId); let lastHtml = ""; for (const pid of paperIds) { let url = `/cme/exam.jsp?course_id=${ctx.courseId}&paper_id=${pid}&type=7`; if (ctx.productId) url += `&product_id=${ctx.productId}`; url += `&_=${Date.now()}`; const exam = await documentFetch(url, { headers: { Referer: referer }, }); lastHtml = exam.text; if (/\u5df2\u901a\u8fc7|\u5df2\u8003\u8bd5|\u4e0d\u8981\u91cd\u590d|\u5df2\u5b8c\u6210\u8003\u8bd5|\u8003\u8bd5\u901a\u8fc7|show_exam_btns/.test(exam.text)) { return { ok: false, msg: "ALREADY_PASSED", ctx }; } const parsed = parseExamDocument(exam.text); if (parsed.form && parsed.qCount > 0) { return { ok: true, html: exam.text, parsed, examUrl: url, referer, ctx, forceExam: /\u5b66\u4e60\u65f6\u957f\u4e0d\u8db3/.test(exam.text) }; } if (/\u770b\u5b8c\u8bfe\u4ef6\u624d\u80fd|\u5b66\u4e60\u65f6\u957f\u4e0d\u8db3|\u65f6\u95f4\u672a\u5230|\u4e0d\u80fd\u53c2\u52a0|\u4e0d\u80fd\u7b54\u5377|\u8bf7\u5148\u5b66\u4e60|alert\s*\(\s*['"]\u770b\u5b8c/.test(exam.text)) { return { ok: false, msg: "\u5b66\u65f6\u672a\u6ee1\uff0c\u4e0d\u80fd\u7b54\u5377", ctx }; } } const hint = ctx.productId ? `paper=${ctx.wareId}` : "\u7f3a\u5c11 product_id"; return { ok: false, msg: `\u672a\u83b7\u53d6\u8bd5\u5377\uff08${hint}\uff09`, html: lastHtml, ctx }; } function buildExamBodyFromParsed(parsed, answers) { const form = parsed?.form; if (!form) return null; const body = new URLSearchParams(); form.querySelectorAll('input[type="hidden"]').forEach((inp) => { if (inp.name) body.append(inp.name, inp.value); }); const items = parsed.items && parsed.items.length ? parsed.items : parsed.doc.querySelectorAll(".exam_list li, .exam-list li, .exam_list .exam_item, ul.exam_list > li"); items.forEach((li, i) => { const ans = answers[i] || "A"; const radios = li.querySelectorAll('input[type="radio"]'); if (radios.length) { const pick = [...radios].find((r) => r.value === ans) || radios[0]; body.append(pick.name, pick.value); } }); const action = form.getAttribute("action") || "examDo.jsp"; const path = action.startsWith("/") ? action : "/cme/" + action.replace(/^\//, ""); return { path, body: body.toString() }; } async function postExamFromPage(page, answers) { const built = buildExamBodyFromParsed(page.parsed, answers); if (!built) throw new Error("\u672a\u83b7\u53d6\u8bd5\u5377"); const referer = page.referer || API_BASE + `/cme/study2.jsp?course_id=${page.ctx.courseId}&courseware_id=${page.ctx.wareId}`; return documentFetch(built.path, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Referer: referer, }, body: built.body, }); } async function runClientCourseExam(courseId, wareId, productId) { const page = await fetchExamPage(courseId, wareId, productId); if (!page.ok) { if (page.msg === "ALREADY_PASSED") return { ok: true, skipped: true, msg: "\u5df2\u901a\u8fc7" }; return { ok: false, msg: page.msg || "\u672a\u83b7\u53d6\u8bd5\u5377" }; } let answers = new Array(page.parsed.qCount).fill("A"); let rotate = 0; const maxTries = page.parsed.qCount * 5 + 5; for (let t = 0; t < maxTries; t++) { const fresh = await fetchExamPage(courseId, wareId, productId); if (!fresh.ok) { if (fresh.msg === "ALREADY_PASSED") return { ok: true, skipped: true, msg: "\u5df2\u901a\u8fc7" }; return { ok: false, msg: fresh.msg || "\u8bd5\u5377\u8fc7\u671f" }; } const post = await postExamFromPage(fresh, answers); if (isExamPassResponse(post.text, post.url)) { const scoreM = post.text.match(/(\d+)\s*\u5206/); return { ok: true, score: scoreM ? scoreM[1] : "", msg: "\u8003\u8bd5\u901a\u8fc7" }; } if (/\u5b66\u4e60\u65f6\u957f\u4e0d\u8db3|\u4e0d\u80fd\u7b54\u5377|\u8bf7\u5148\u5b66\u4e60/.test(post.text)) { return { ok: false, msg: "\u5b66\u65f6\u672a\u6ee1\uff0c\u4e0d\u80fd\u7b54\u5377" }; } const fixed = await fixAnswersFromFail(post.text, post.url, answers); if (fixed.fixed) { answers = fixed.answers; } else { answers[rotate % answers.length] = nextExamChoice(answers[rotate % answers.length]); rotate++; } await sleep(600); } return { ok: false, msg: "\u66b4\u529b\u7b54\u9898\u5931\u8d25" }; } async function promoteToNextTask(bg, course, opts) { const excludeWare = opts?.excludeWareId ? String(opts.excludeWareId) : ""; invalidateSidebarStatusMap(bg.csId); saveBgState({}); await refreshPanelCourses(); let rest = await partitionCourseWork(bg.csId); const dropWare = (v) => excludeWare && wareIdsMatch(v.resId, excludeWare); rest.study = rest.study.filter((v) => !dropWare(v)); rest.exam = rest.exam.filter((v) => !dropWare(v)); const prunedExam = []; for (const v of rest.exam) { if ((await querySidebarStatus(bg.csId, v.resId)) === "done") continue; prunedExam.push(v); } rest.exam = prunedExam; if (!rest.study.length && !rest.exam.length) { state.coursesDoneSession += 1; return false; } const pendingHold = bg?.pendingExamWareId && !(excludeWare && wareIdsMatch(bg.pendingExamWareId, excludeWare)) && rest.exam.some((v) => wareIdsMatch(v.resId, bg.pendingExamWareId)); const preferExam = rest.exam.length > 0 && (rest.study.length === 0 || pendingHold || (rest.study[0] && rest.exam[0] && (wareIdsMatch(rest.study[0].resId, rest.exam[0].resId) || isChapterProgressReported(bg.csId, rest.study[0].resId)))); const nb = buildBgStateFromWork({ course, phase: preferExam ? "exam" : "study", videos: preferExam ? [rest.exam[0]] : [rest.study[0]], test: preferExam ? buildTestFromVideo(rest.exam[0]) : undefined, videoTotal: rest.all.length, videoDone: rest.all.length - rest.study.length - rest.exam.length, }); saveBgState(nb); if (preferExam) { log(`\u4e0b\u4e00\u4efb\u52a1\uff1a\u8003\u8bd5\u7b2c${formatWareNo(rest.exam[0].resId)}\u8282 ${rest.exam[0].resName || ""}`); } else if (rest.study.length) { log(`\u4e0b\u4e00\u4efb\u52a1\uff1a\u89c6\u9891\u7b2c${formatWareNo(rest.study[0].resId)}\u8282 ${rest.study[0].resName || ""}`); } else { log(`\u4e0b\u4e00\u4efb\u52a1\uff1a\u8003\u8bd5\u7b2c${formatWareNo(rest.exam[0].resId)}\u8282 ${rest.exam[0].resName || ""}`); } return true; } async function skipChapterIfAlreadyPassed(bg, course, wareId, label) { const st = await querySidebarStatus(bg.csId, wareId); if (st !== "done") return false; trace(`\u7b2c${formatWareNo(wareId)}\u8282 ${label || ""} \u5df2\u901a\u8fc7\uff0c\u8df3\u8fc7`); clearIsExamCache(bg.csId, wareId); bg.sameChapterExam = false; bg.pendingExamWareId = ""; await promoteToNextTask(bg, course, { excludeWareId: wareId }); return true; } async function localExamSubmit(courseId, wareId, productId) { return runClientCourseExam(courseId, wareId, productId); } async function localLearnChapter(_csId, _video, _mult) { throw new Error("\u672c\u5730\u5f15\u64ce\u5df2\u5173\u95ed\uff0c\u8bf7\u4f7f\u7528\u4e91\u7aef\u6388\u6743"); } async function apiBoostStudyForExam(csId, wareId, label) { const elabel = label || ""; try { let meta = null; try { meta = await fetchStudyMeta(csId, wareId); } catch (_) {} const video = { resId: wareId, cptId: wareId, fileTimeLen: meta?.totalTime || 3600, resName: elabel, productId: meta?.productId || "", }; const res = await learnSaveFullVideo(csId, video, getMultiplier()); if (!(res.skipped || res.success === "1")) { trace(`\u7b2c${formatWareNo(wareId)}\u8282 ${elabel} \u4e91\u7aef\u7eed\u62a5\u672a\u5b8c\u6210`); return false; } const totalSec = meta?.totalTime || 3600; const examReady = await checkChapterExamReady(csId, wareId, totalSec); if (examReady.videoDone) { markChapterProgressReported(csId, wareId); trace(`\u7b2c${formatWareNo(wareId)}\u8282 ${elabel} \u4e91\u7aef\u7eed\u62a5\u5b8c\u6210\uff0c\u53ef\u8003\u8bd5`); return true; } trace(`\u7b2c${formatWareNo(wareId)}\u8282 ${elabel} \u4e91\u7aef\u7eed\u62a5\u540e\u7b49\u5f85 isexam \u540c\u6b65`); return false; } catch (e) { trace(`\u7b2c${formatWareNo(wareId)}\u8282 \u7eed\u62a5\u5931\u8d25\uff1a${e.message || e}`); return false; } } async function listTeachPlans() { return [{ tpId: "favorite", tpName: "\u6211\u7684\u6536\u85cf", year: "" }]; } async function listAllCoursesForPlan() { const html = await fetchFavoriteHtml(); return parseCoursesFromHtml(html).map((c) => ({ csId: c.courseId, tpId: "favorite", csName: c.title, courseName: c.title, lcsProcess: 0, tpName: "\u6211\u7684\u6536\u85cf", lcsStudyFinished: "0", lcsExameFinished: "0", lcsFinished: "0", courseTestList: [], })); } const state = { enabled: false, studyMode: "", lastAction: "", lastTick: 0, bgTickBusy: false, faceWaiting: false, courseplayWaitSince: 0, trainApiTriedAt: 0, panelCourses: [], teachPlans: [], selectedTpId: "", chapterPreview: [], queueVideoTotal: 0, queueVideoDone: 0, queueExamTotal: 0, queueExamDone: 0, coursesDoneSession: 0, panelRefreshing: false, previewRefreshTimer: null, cloudApiBase: DEFAULT_CLOUD_API_BASE, cloudToken: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(), cloudTier: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim() ? "unknown" : "free", cloudLease: "", cloudLeaseExp: 0, cloudProExpireAt: 0, cloudRevoked: false, freeVideoLimit: 1, freeUsedVideos: 0, panelNoticePath: _w(3), remotePanelNotice: "", proBuyUrl: PRO_BUY_URL, logLines: [], panelHint: "", platformHeartbeatSec: 0, studyModeHintTimer: null, cmeUserProfile: null, }; function loadQueue() { try { const raw = JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]"); return Array.isArray(raw) ? raw : []; } catch (_) { return []; } } function saveQueue(csIds) { localStorage.setItem(QUEUE_KEY, JSON.stringify(csIds)); } function loadSelectedTpId() { return String(localStorage.getItem(PLAN_TP_KEY) || "").trim(); } function saveSelectedTpId(tpId) { const id = String(tpId || "").trim(); localStorage.setItem(PLAN_TP_KEY, id); state.selectedTpId = id; } function getAutoExam() { return localStorage.getItem(AUTO_EXAM_KEY) !== "0"; } function setAutoExam(v) { localStorage.setItem(AUTO_EXAM_KEY, v ? "1" : "0"); return v; } function isTruthyFlag(v) { return v === true || v === 1 || v === "1"; } function isFalsyFlag(v) { return v === false || v === 0 || v === "0"; } function escHtml(s) { return String(s || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function getMultiplier() { const v = parseFloat(localStorage.getItem(MULTIPLIER_KEY) || "2"); if (!Number.isFinite(v)) return 2; return Math.min(5, Math.max(1, v)); } function setMultiplier(v) { const n = Math.min(5, Math.max(1, parseFloat(v) || 1)); localStorage.setItem(MULTIPLIER_KEY, String(n)); return n; } function getAdvanceMult() { const v = parseFloat(localStorage.getItem(ADVANCE_KEY) || "6"); if (!Number.isFinite(v)) return 6; return Math.min(18, Math.max(1, v)); } function setAdvanceMult(v) { const n = Math.min(18, Math.max(1, parseFloat(v) || 1)); localStorage.setItem(ADVANCE_KEY, String(n)); return n; } function getSaveInterval() { if (isRealtimeStudyMode()) return getRealtimeChunkSec(); const v = parseInt(localStorage.getItem(SAVE_INTERVAL_KEY) || "5", 10); if (!Number.isFinite(v)) return 300; return Math.min(300, Math.max(3, v)); } function setSaveInterval(v) { const n = Math.min(300, Math.max(3, parseInt(v, 10) || 300)); localStorage.setItem(SAVE_INTERVAL_KEY, String(n)); return n; } function getAdvanceChunkSec() { return Math.min(Math.round(HEARTBEAT_BASE * getAdvanceMult()), MAX_ADVANCE_SEC); } function readStudyModeSelect() { return "efficiency"; } function hasStudyModeSelected() { return true; } function isRealtimeStudyMode() { return false; } function getStudyMode() { return "efficiency"; } function setStudyMode(mode) { state.studyMode = "efficiency"; localStorage.setItem(STUDY_MODE_KEY, "efficiency"); return "efficiency"; } function ensureEfficiencyMode() { setStudyMode("efficiency"); } function clearStudyModeSelection() { ensureEfficiencyMode(); } function resetSessionOnPageLoad() { state.enabled = false; ensureEfficiencyMode(); localStorage.setItem(STORAGE_KEY, "0"); } function formatIntervalCn(sec) { const s = Math.max(1, Math.round(Number(sec) || HEARTBEAT_BASE)); if (s >= 60 && s % 60 === 0) return `${s / 60}\u5206\u949f`; return `${s}\u79d2`; } function formatDurationMinutesCn(sec) { const s = Math.max(0, Math.round(Number(sec) || 0)); if (s >= 60 && s % 60 === 0) return `${s / 60}\u5206\u949f`; if (s >= 60) { const m = (s / 60).toFixed(1).replace(/\.0$/, ""); return `${m}\u5206\u949f`; } return `${s}\u79d2`; } async function ensurePlatformHeartbeatSec(force) { if (!force && state.platformHeartbeatSec > 0) return state.platformHeartbeatSec; const sec = await fetchHeartbeatSec(); state.platformHeartbeatSec = Math.min(300, Math.max(30, Math.round(sec) || HEARTBEAT_BASE)); return state.platformHeartbeatSec; } function getRealtimeChunkSec() { return state.platformHeartbeatSec > 0 ? state.platformHeartbeatSec : HEARTBEAT_BASE; } function getMode() { const m = localStorage.getItem(MODE_KEY) || "background"; return ["background", "autoplay", "manual"].includes(m) ? m : "background"; } function setMode(m) { localStorage.setItem(MODE_KEY, m); return m; } function applyLockedPanelDefaults() { setMode("background"); if (getStudyMode() === "efficiency") { setSaveInterval(10); setMultiplier(2); localStorage.setItem(API_ONLY_STUDY_KEY, "1"); } setAutoExam(true); } function shouldPatchPageLearnSave() { return getMode() === "manual" || getMode() === "autoplay"; } function loadBgState() { try { return JSON.parse(localStorage.getItem(BG_STATE_KEY) || "null") || {}; } catch (_) { return {}; } } function saveBgState(bg) { localStorage.setItem(BG_STATE_KEY, JSON.stringify(bg)); } function flattenChapters(nodes, out) { out = out || []; for (const n of nodes || []) { if (n.cptId) out.push(n); if (n.childs?.length) flattenChapters(n.childs, out); } return out; } function isProgressUrl(url) { return url && String(url).includes("updatePlayStatus.jsp"); } function patchProgressUrl(url) { if (!shouldPatchPageLearnSave()) return url; const mult = getMultiplier(); if (mult <= 1) return url; try { const u = new URL(url, location.origin); const playing = Number(u.searchParams.get("playing_time") || 0); const total = Number(u.searchParams.get("total_time") || 0); if (playing > 0) { const boosted = Math.min(Math.round(playing * mult), total || playing * mult); if (boosted > playing) { u.searchParams.set("playing_time", String(boosted)); console.log(`[\u597d\u533b\u751f\u770b\u8bfe] \u8fdb\u5ea6\u4e0a\u62a5 ${playing}s → ${boosted}s\uff08${mult}x\uff09`); return u.pathname + "?" + u.searchParams.toString(); } } } catch (_) {} return url; } const xhrOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url) { let u = url; if (isProgressUrl(u)) u = patchProgressUrl(u); this._cmeUrl = u; const args = Array.from(arguments); args[1] = u; return xhrOpen.apply(this, args); }; const xhrSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function (body) { return xhrSend.call(this, body); }; const nativeFetch = window.fetch; if (nativeFetch) { window.fetch = function (input, init) { let url = typeof input === "string" ? input : input?.url || ""; if (isProgressUrl(url)) { const patched = patchProgressUrl(url); if (patched !== url) { if (typeof input === "string") input = patched; else input = new Request(patched, input); } } return nativeFetch.call(this, input, init); }; } function log(msg) { const text = String(msg || "").trim(); if (!text) return; if (!isUserFacingLog(text)) { trace(text); return; } state.lastUserAction = text; const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false }); state.logLines.push(`${ts} ${text}`); if (state.logLines.length > 50) state.logLines.shift(); appendLogLine(`${ts} ${text}`); updatePanel(); } function courseDisplayName(nameOrCourse) { const raw = typeof nameOrCourse === "string" ? nameOrCourse : nameOrCourse?.csName || nameOrCourse?.csId || "\u8bfe\u7a0b"; return String(raw).trim() || "\u8bfe\u7a0b"; } function setPanelHint(msg) { state.panelHint = String(msg || "").trim(); updatePanel(); } function clearPanelHint() { if (!state.panelHint) return; state.panelHint = ""; updatePanel(); } let panelToastTimer = null; function showPanelToast(msg, type) { const text = String(msg || "").trim(); if (!text) return; const panel = document.getElementById("cme-auto-panel"); if (!panel) return; let el = document.getElementById("cme-panel-toast"); if (!el) { el = document.createElement("div"); el.id = "cme-panel-toast"; panel.appendChild(el); } el.className = "cme-panel-toast cme-panel-toast-" + (type || "info"); el.textContent = text; el.style.display = "block"; clearTimeout(panelToastTimer); panelToastTimer = setTimeout(() => { if (el) el.style.display = "none"; }, 3500); } function formatCloudTokenError(err) { const em = String(err?.message || err || "").trim(); if (/invalid token/i.test(em)) return "Token \u65e0\u6548\uff0c\u8bf7\u68c0\u67e5\u662f\u5426\u590d\u5236\u5b8c\u6574"; if (/revoked/i.test(em)) return "Token \u5df2\u88ab\u7981\u7528"; if (/expired/i.test(em)) return "Token \u5df2\u8fc7\u671f"; if (em) return "\u4e91\u7aef\u6821\u9a8c\u5931\u8d25\uff1a" + em; return "\u4e91\u7aef\u6821\u9a8c\u5931\u8d25"; } function appendLogLine(text) { const box = document.getElementById("cme-run-log"); if (!box) return; const div = document.createElement("div"); div.className = "cme-log-row"; div.textContent = text; box.appendChild(div); box.scrollTop = box.scrollHeight; } function clearRunLog() { state.logLines = []; const box = document.getElementById("cme-run-log"); if (box) box.innerHTML = ""; } function getCloudApiBase() { return DEFAULT_CLOUD_API_BASE; } function setCloudApiBase() { state.cloudApiBase = DEFAULT_CLOUD_API_BASE; } function formatLeaseExpireText(expSec) { const ex = Number(expSec || 0); if (!Number.isFinite(ex) || ex <= 0) return "—"; const d = new Date(ex * 1000); if (Number.isNaN(d.getTime())) return "—"; return d.toLocaleString("zh-CN", { hour12: false }); } function resolveLeaseExpireSec(data) { const raw = data?.exp ?? data?.expire_at ?? data?.expires_at ?? data?.lease_exp ?? 0; const n = Number(raw || 0); if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000); return Number.isFinite(n) ? Math.floor(n) : 0; } function resolveProExpireSec(data) { const raw = data?.pro_expires_at ?? data?.proExpiresAt ?? data?.pro_expire_at ?? 0; const n = Number(raw || 0); if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000); return Number.isFinite(n) ? Math.floor(n) : 0; } function readCloudLeaseCache() { try { const raw = localStorage.getItem(CLOUD_LEASE_CACHE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") return null; const lease = String(parsed.lease || "").trim(); const exp = Number(parsed.exp || 0); if (!lease || !Number.isFinite(exp) || exp <= 0) return null; return { lease, exp, tier: String(parsed.tier || "").trim(), freeVideoLimit: Number(parsed.freeVideoLimit ?? parsed.free_video_limit ?? parsed.freeChapterLimit ?? 1), freeUsedVideos: Number(parsed.freeUsedVideos ?? parsed.free_used_videos ?? parsed.freeUsedChapters ?? 0), proExpireAt: Number(parsed.proExpireAt ?? parsed.pro_expire_at ?? 0), }; } catch (_) { return null; } } function writeCloudLeaseCache(lease, exp, extra) { if (!lease || !exp) { localStorage.removeItem(CLOUD_LEASE_CACHE_KEY); return; } localStorage.setItem(CLOUD_LEASE_CACHE_KEY, JSON.stringify(Object.assign({ lease, exp }, extra || {}))); } function readCloudLastState() { try { const raw = localStorage.getItem(CLOUD_LAST_STATE_KEY); if (!raw) return null; const p = JSON.parse(raw); if (!p || typeof p !== "object") return null; return { tier: String(p.tier || "").trim(), freeVideoLimit: Number(p.freeVideoLimit ?? p.free_video_limit ?? 1), freeUsedVideos: Number(p.freeUsedVideos ?? p.free_used_videos ?? 0), }; } catch (_) { return null; } } function writeCloudLastState(partial) { try { const prev = readCloudLastState() || {}; localStorage.setItem(CLOUD_LAST_STATE_KEY, JSON.stringify(Object.assign({}, prev, partial || {}, { ts: Date.now() }))); } catch (_) {} } function writeCloudProExpireCache(token, exp) { try { const tk = String(token || "").trim(); if (!tk || !exp) { localStorage.removeItem(CLOUD_PRO_EXPIRE_CACHE_KEY); return; } localStorage.setItem(CLOUD_PRO_EXPIRE_CACHE_KEY, JSON.stringify({ token: tk, exp: Math.floor(exp) })); } catch (_) {} } function readCloudProExpireCache() { try { const raw = localStorage.getItem(CLOUD_PRO_EXPIRE_CACHE_KEY); if (!raw) return null; const p = JSON.parse(raw); if (!p?.token || !p?.exp) return null; return { token: String(p.token), exp: Number(p.exp) }; } catch (_) { return null; } } function _gm(url, method, headers, data) { return new Promise((resolve, reject) => { const req = { method: method || "GET", url, headers: headers || {}, timeout: 30000, withCredentials: false, responseType: "text", onload: (res) => resolve({ status: res.status, text: res.responseText || "" }), onerror: () => reject(new Error("\u7f51\u7edc\u9519\u8bef " + url)), ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6 " + url)), }; if (data != null) req.data = data; if (typeof GM_xmlhttpRequest === "function") { GM_xmlhttpRequest(req); return; } const xhr = new XMLHttpRequest(); xhr.open(req.method, url, true); xhr.timeout = req.timeout; Object.keys(req.headers).forEach((k) => xhr.setRequestHeader(k, req.headers[k])); xhr.onload = () => resolve({ status: xhr.status, text: xhr.responseText || "" }); xhr.onerror = () => reject(new Error("\u7f51\u7edc\u9519\u8bef " + url)); xhr.ontimeout = () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6 " + url)); xhr.send(data); }); } async function _rq(path, method, payload) { const base = getCloudApiBase(); const url = `${base}${path.startsWith("/") ? path : `/${path}`}`; const headers = { Accept: "application/json", "Content-Type": "application/json" }; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; if (state.cloudToken) headers.Authorization = `Bearer ${state.cloudToken}`; let reqPayload = payload; if (reqPayload && typeof reqPayload === "object") { reqPayload = Object.assign({}, reqPayload); if (path !== _w(4) && state.cloudLease && reqPayload.lease == null) { reqPayload.lease = state.cloudLease; } } const body = reqPayload == null ? null : JSON.stringify(reqPayload); const res = await _gm(url, method || "POST", headers, body); const text = String(res.text || "").trim(); let data = {}; if (text) data = JSON.parse(text); if (res.status < 200 || res.status >= 300) { const errCode = String(data.detail || data.message || data.code || `http_${res.status}`); throw new Error(errCode); } return data; } function readCachedCmeProfile() { try { const raw = sessionStorage.getItem(CME_USER_PROFILE_KEY); if (!raw) return null; const p = JSON.parse(raw); if (!p || typeof p !== "object") return null; if (!isProfileCacheValid(p)) return null; if (!p.userAccount && !p.bdpUserId) return null; return p; } catch (_) { return null; } } function writeCachedCmeProfile(prof) { try { if (!prof) { sessionStorage.removeItem(CME_USER_PROFILE_KEY); return; } sessionStorage.setItem(CME_USER_PROFILE_KEY, JSON.stringify(prof)); } catch (_) {} } function clearCmeUserProfileCache() { state.cmeUserProfile = null; writeCachedCmeProfile(null); } function mergeCmeProfiles() { const out = {}; for (let i = 0; i < arguments.length; i++) { const p = arguments[i]; if (!p || typeof p !== "object") continue; for (const k of Object.keys(p)) { const v = p[k]; if (v != null && String(v).trim() !== "") out[k] = v; } } if (!out.userAccount && !out.bdpUserId) return null; return out; } function discoverBdpProfileUrl(html) { const text = String(html || ""); const m = text.match(/https?:\/\/bdp\.haoyisheng\.com\/bdp\/register\/toUserInfo\?[^"'\s<>]+/i) || text.match(/\/\/bdp\.haoyisheng\.com\/bdp\/register\/toUserInfo\?[^"'\s<>]+/i) || text.match(/bdp\.haoyisheng\.com\/bdp\/register\/toUserInfo\?[^"'\s<>]+/i); if (!m) return ""; let u = m[0]; if (!/^https?:/i.test(u)) u = "https://" + u.replace(/^\/\//, ""); return u; } function discoverHysSignAuthorization(html) { const text = String(html || ""); const m = text.match(/hysSign\/\?authorization=([^"'\s<>]+)/i) || text.match(/authorization=(eyJ[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+)/); return m ? decodeURIComponent(m[1]) : ""; } function parseBdpProfileHtml(html) { const text = String(html || ""); if (!text || !/userInfoForm|userAccount|realName/i.test(text)) return null; const userAccount = pickHtml(text, [ /name=["']userAccount["'][^>]*value=["']([^"']+)["']/i, /name=["']userAccount["'][^>]*value=([^>\s"']+)/i, ]); const realName = pickHtml(text, [ /id=["']realName["'][^>]*value=["']([^"']+)["']/i, /name=["']realName["'][^>]*value=["']([^"']+)["']/i, ]); const bdpUserId = pickHtml(text, [/name=["']userId["'][^>]*value=["'](\d+)["']/i]); const certificateNo = pickHtml(text, [ /id=["']certificateNo["'][^>]*value=["']([^"']+)["']/i, /name=["']certificateNo["'][^>]*value=["']([^"']+)["']/i, ]); const mobileNumber = pickHtml(text, [ /id=["']mobileNumber["'][^>]*value=["']([^"']+)["']/i, /name=["']mobileNumber["'][^>]*value=["']([^"']+)["']/i, ]); const hospital = pickHtml(text, [ /id=["']hospital["'][^>]*value=["']([^"']+)["']/i, /name=["']hospital["'][^>]*value=["']([^"']+)["']/i, ]); const department = pickHtml(text, [/name=["']deptName["'][^>]*value=["']([^"']+)["']/i]); if (!userAccount && !bdpUserId) return null; return { userAccount: String(userAccount || "").trim(), bdpUserId: String(bdpUserId || "").trim(), realName: String(realName || "").trim(), certificateNo: String(certificateNo || "").trim(), mobileNumber: String(mobileNumber || "").trim(), hospital: String(hospital || "").trim(), department: String(department || "").trim(), }; } async function fetchBdpProfileByUrl(url) { if (!url) return null; try { const res = await _gm( url, "GET", { Accept: "text/html,application/xhtml+xml", Referer: API_BASE + "/cme/index.jsp" }, null ); if (res.status < 200 || res.status >= 300) return null; return parseBdpProfileHtml(res.text); } catch (_) { return null; } } async function bootstrapCmeSessionCookies(htmlSources) { for (const html of htmlSources || []) { const auth = discoverHysSignAuthorization(html); if (!auth) continue; try { await platformFetch("/sso/hysSign/?authorization=" + encodeURIComponent(auth)); } catch (_) {} } } async function resolveUserAccountFromStudy2(favHtml) { const html = String(favHtml || ""); if (!html || /\u8bf7\u5148\u767b\u5f55|\u767b\u5f55\u8d85\u65f6|\u672a\u767b\u5f55/i.test(html)) return null; const courses = parseCoursesFromHtml(html); const csId = courses[0]?.courseId; if (!csId) return null; try { const r = await platformFetch( `/cme/study2.jsp?course_id=${csId}&courseware_id=01&_=${Date.now()}` ); if (/\u8bf7\u5148\u767b\u5f55|\u767b\u5f55\u8d85\u65f6|\u672a\u767b\u5f55/i.test(r.text)) return null; const meta = parseStudyMetaFromHtml(r.text, csId, "01"); const loginName = meta?.loginName || ""; if (!loginName) return null; trace("\u4ece study2 \u89e3\u6790 userAccount\uff1a" + loginName); return { userAccount: loginName }; } catch (_) { return null; } } function parseCmeIndexProfile(html) { const text = String(html || ""); if (!text) return null; if (/\u8bf7\u5148\u767b\u5f55|\u767b\u5f55\u8d85\u65f6|\u672a\u767b\u5f55/i.test(text) && !/\u6b22\u8fce\u60a8/.test(text)) return null; if (!/\u6b22\u8fce\u60a8|IC\s*\u5361|user_id\s*=/.test(text)) return null; const bdpUserId = pickHtml(text, [ /user_id\s*=\s*['"](\d+)['"]/i, /var\s+user_id\s*=\s*['"](\d+)['"]/i, ]); const realName = pickHtml(text, [ /

\s*\u6b22\u8fce\u60a8[\uff0c,]\s*([^<]+?)\s*<\/h4>/i, /\u6b22\u8fce\u60a8[\uff0c,]\s*([^\s<\uff0c,]+)/i, ]); const userAccount = pickHtml(text, [ /IC\s*\u5361\s*[:\uff1a]\s*([A-Za-z0-9]+)/i, /linkJumpTo\s*\(\s*['"]([A-Za-z0-9]+)['"]/i, /name=["']userAccount["'][^>]*value=["']([^"']+)["']/i, /userAccount\s*[=:]\s*['"]([A-Za-z0-9]+)['"]/i, /var\s+loginName\s*=\s*['"]([^'"]+)['"]/i, /loginName\s*[=:]\s*['"]([A-Za-z0-9]+)['"]/i, ]); const department = pickHtml(text, [/\u79d1\u5ba4[\uff1a:]\s*([^<\n]+)/i]); const organization = pickHtml(text, [/\u673a\u6784[\uff1a:]\s*([^<\n]+)/i]); const hospital = pickHtml(text, [/\u5355\u4f4d[\uff1a:]\s*([^<\n]+)/i]); const clean = (s) => String(s || "") .replace(/<[^>]+>/g, "") .replace(/\s+/g, " ") .trim(); const ic = String(userAccount || "").trim(); const uid = String(bdpUserId || "").trim(); if (!ic && !uid) return null; return { userAccount: ic, bdpUserId: uid, realName: clean(realName), department: clean(department), organization: clean(organization), hospital: clean(hospital), }; } async function _fip(forceRefresh) { if (!hasCmeSessionSync()) { const online = await probeCmeSessionOnline(); if (!online) { clearCmeUserProfileCache(); return null; } } if (!forceRefresh) { const cached = state.cmeUserProfile || readCachedCmeProfile(); if (cached && isProfileCacheValid(cached)) { state.cmeUserProfile = cached; return cached; } } const htmlSources = []; const loadPage = async (path, useDom) => { if (useDom) return document.documentElement.outerHTML; try { const r = await platformFetch(path + (path.includes("?") ? "&" : "?") + "_=" + Date.now()); return r.text; } catch (_) { return ""; } }; const loginScriptHtml = await loadPage("/loginScript.jsp", false); if (loginScriptHtml) htmlSources.push(loginScriptHtml); const indexHtml = await loadPage("/cme/index.jsp", pageType() === "home"); if (indexHtml) htmlSources.push(indexHtml); const favHtml = await loadPage("/cme/myFavorite.jsp", pageType() === "favorite"); if (favHtml) htmlSources.push(favHtml); await bootstrapCmeSessionCookies(htmlSources); let merged = null; for (const html of htmlSources) { merged = mergeCmeProfiles(merged, parseCmeIndexProfile(html)); merged = mergeCmeProfiles(merged, parseBdpProfileHtml(html)); } let bdpUrl = ""; for (const html of htmlSources) { bdpUrl = discoverBdpProfileUrl(html); if (bdpUrl) break; } if (bdpUrl) { const bdp = await fetchBdpProfileByUrl(bdpUrl); merged = mergeCmeProfiles(merged, bdp); } if (!merged?.userAccount && favHtml) { const fromStudy = await resolveUserAccountFromStudy2(favHtml); merged = mergeCmeProfiles(merged, fromStudy); } if (!merged?.userAccount && !merged?.bdpUserId) return null; const prof = Object.assign({ sessionFp: getSessionFingerprint() || "online", ts: Date.now() }, merged); state.cmeUserProfile = prof; writeCachedCmeProfile(prof); if (prof.userAccount) trace("\u8d26\u53f7\u7ed1\u5b9a ID\uff1a" + prof.userAccount); return prof; } async function _fup() { const prof = await _fip(false); if (!prof) return null; const bindId = String(prof.userAccount || prof.bdpUserId || "").trim(); if (!bindId) return null; const office = [prof.hospital, prof.department].filter(Boolean).join(" · "); return { learning_user_id: bindId, shortName: prof.realName || bindId, name: prof.realName || bindId, idcarNo: prof.certificateNo || "", userName: prof.certificateNo || "", phone: prof.mobileNumber || "", phoneNum: prof.mobileNumber || "", phone_num: prof.mobileNumber || "", office: office || prof.organization || "", school_name: prof.hospital || prof.organization || "", schoolName: prof.hospital || prof.organization || "", }; } async function _el(forceRefresh) { const now = Math.floor(Date.now() / 1000); if (!forceRefresh && state.cloudLease && state.cloudLeaseExp - now > 60) return true; if (!forceRefresh) { const cached = readCloudLeaseCache(); if (cached && cached.exp - now > 60) { state.cloudLease = cached.lease; state.cloudLeaseExp = cached.exp; state.cloudProExpireAt = Number(cached.proExpireAt || 0); if (cached.tier) state.cloudTier = cached.tier; if (cached.freeVideoLimit > 0) state.freeVideoLimit = cached.freeVideoLimit; if (cached.freeUsedVideos >= 0) state.freeUsedVideos = cached.freeUsedVideos; updateCloudPanelUI(); return true; } } const luid = getLearningUserId(); if (!luid) throw new Error("\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u83b7\u53d6\u4e91\u7aef\u6388\u6743"); let data; try { const leaseBody = { learning_user_id: luid }; const prof = await _fup(); if (prof) Object.assign(leaseBody, prof); data = await _rq(_w(4), "POST", leaseBody); state.cloudRevoked = false; } catch (e) { const em = String(e?.message || e); if (/(revoked|invalid token|expired)/i.test(em)) { state.cloudRevoked = true; state.cloudTier = "revoked"; state.cloudLease = ""; state.cloudLeaseExp = 0; writeCloudLeaseCache("", 0); updateCloudPanelUI(); } throw e; } const lease = String(data.lease || ""); const exp = resolveLeaseExpireSec(data); let tier = String(data.tier || "").trim() || (state.cloudToken ? "pro" : "free"); const proExpireAt = resolveProExpireSec(data); state.cloudLease = lease; state.cloudLeaseExp = exp; state.cloudProExpireAt = proExpireAt > 0 ? proExpireAt : 0; state.cloudTier = tier; state.freeVideoLimit = Number( data.free_video_limit ?? data.free_chapter_limit ?? data.freeVideoLimit ?? state.freeVideoLimit ?? 1 ); state.freeUsedVideos = Number( data.free_used_videos ?? data.free_used_chapters ?? data.freeUsedVideos ?? 0 ); writeCloudLeaseCache(lease, exp, { tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, proExpireAt: state.cloudProExpireAt, }); writeCloudLastState({ tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, }); if (tier === "pro" && state.cloudProExpireAt > 0) { writeCloudProExpireCache(state.cloudToken, state.cloudProExpireAt); } updateCloudPanelUI(); return !!lease; } async function _vt() { if (!state.cloudToken) return true; const base = getCloudApiBase(); const url = `${base}${_w(5)}`; const headers = { Accept: "application/json", Authorization: `Bearer ${state.cloudToken}` }; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; const res = await _gm(url, "GET", headers, null); const text = String(res.text || "").trim(); let data = {}; if (text) data = JSON.parse(text); if (res.status < 200 || res.status >= 300 || !data.ok) { throw new Error(String(data.message || data.detail || "Token \u6821\u9a8c\u5931\u8d25")); } return true; } function _sq(data) { if (!data || typeof data !== "object") return; if (data.tier) state.cloudTier = String(data.tier); const lim = data.free_video_limit ?? data.free_chapter_limit ?? data.limit; const used = data.free_used_videos ?? data.free_used_chapters ?? data.used; if (lim != null) state.freeVideoLimit = Number(lim); if (used != null) state.freeUsedVideos = Number(used); writeCloudLastState({ tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, }); updateCloudPanelUI(); } async function _es(kind, context, cfg) { await _el(false); const data = await _rq(_w(0), "POST", { kind: String(kind || ""), context: context || {}, config: cfg || {}, }); _sq(data); return data; } async function _ep(sessionId, event, lastResult, contextPatch) { await _el(false); const data = await _rq(_w(1), "POST", { session_id: String(sessionId || ""), event: String(event || "tick"), last_result: lastResult == null ? null : lastResult, context_patch: contextPatch == null ? null : contextPatch, }); _sq(data); return data; } async function _xc(cmd) { const c = cmd || {}; const t = String(c.type || ""); if (t === "document_fetch") { const r = await documentFetch(c.path || c.url, { method: "GET", headers: c.headers || {}, }); return { event: c.event || "submit_result", lastResult: { text: r.text, status: r.status, url: r.url }, }; } if (t === "platform_fetch" || t === "platform_get") { const r = await platformFetch(c.path || c.url, { method: "GET", headers: c.headers || {}, }); return { event: c.event || "submit_result", lastResult: { text: r.text, status: r.status, url: r.url }, }; } if (t === "platform_post") { const headers = Object.assign({}, c.headers || {}); if (!headers["Content-Type"] && !headers["content-type"]) { headers["Content-Type"] = "application/x-www-form-urlencoded"; } const body = c.body != null ? c.body : typeof c.payload === "string" ? c.payload : new URLSearchParams(c.payload || {}).toString(); const r = await platformFetch(c.path, { method: "POST", headers, body }); let data; try { data = JSON.parse(r.text); } catch { data = { text: r.text }; } return { event: "submit_result", lastResult: { data, http_status: r.status, text: r.text, url: r.url }, }; } if (t === "wait") { await sleep(Math.max(500, Number(c.ms || 1000))); return { event: "tick", lastResult: null }; } if (t === "done") { return { terminal: true, ok: !!c.success, skipped: !!c.skipped, score: c.score, msg: c.message || "\u5b8c\u6210", }; } if (t === "failed") { return { terminal: true, ok: false, msg: c.message || "failed" }; } return { event: "tick", lastResult: null }; } function shouldLogEngineLine(logText, options) { const s = String(logText || ""); if (!s) return false; if (options && options.realtime) return /\[1:1\]|learnSave \u672a\u6210\u529f|\u9996\u6b21\u8fdb\u5165\u7b49\u5f85/.test(s); return true; } async function _ve(startRes, options) { const quiet = !!(options && options.quiet); const realtime = !!(options && options.realtime); let sessionId = String(startRes.session_id || ""); let cmd = startRes.command; if (!quiet && shouldLogEngineLine(startRes.log, options)) { trace(startRes.log); } const maxSteps = Number(options && options.maxSteps) || (realtime ? 320 : 96); for (let i = 0; i < maxSteps && cmd; i++) { const out = await _xc(cmd); if (out.terminal !== undefined) return out; const next = await _ep(sessionId, out.event, out.lastResult, null); sessionId = String(next.session_id || sessionId); if (!quiet && shouldLogEngineLine(next.log, options)) { trace(next.log); } cmd = next.command; } return { terminal: true, ok: false, msg: "\u4e91\u7aef\u5f15\u64ce\u6b65\u6570\u8d85\u9650" }; } async function _xe(startRes, courseName) { let sessionId = String(startRes.session_id || ""); let cmd = startRes.command; let questionCount = 0; let submitAnnounced = false; const maxSteps = 96; for (let i = 0; i < maxSteps && cmd; i++) { const out = await _xc(cmd); if (out.terminal !== undefined) { if (out.ok) { const score = out.score != null && out.score !== "" ? out.score : "—"; log(`\u8bfe\u7a0b${courseName}\u8003\u8bd5\u5b8c\u6210\uff0c\u5f97\u5206\uff1a${score}\u5206`); } return out; } const next = await _ep(sessionId, out.event, out.lastResult, null); sessionId = String(next.session_id || sessionId); const slog = String(next.log || ""); const batchM = slog.match(/\u6279\u91cf\u4fdd\u5b58\s*(\d+)/); if (batchM) { questionCount = Number(batchM[1]) || questionCount; if (!submitAnnounced && questionCount > 0) { submitAnnounced = true; log(`\u8bfe\u7a0b${courseName}\u8003\u8bd5\u5f00\u59cb\uff0c\u5171${questionCount}\u9053\u9898\uff0c\u5df2\u63d0\u4ea4\u7b54\u6848`); } } if (/\u63d0\u4ea4\u7b54\u5377/.test(slog) && !submitAnnounced) { const qm = slog.match(/(\d+)\s*\u9898/); if (qm) { submitAnnounced = true; log(`\u8bfe\u7a0b${courseName}\u8003\u8bd5\u5f00\u59cb\uff0c\u5171${qm[1]}\u9053\u9898\uff0c\u5df2\u63d0\u4ea4\u7b54\u6848`); } } if (/\u8003\u8bd5\u4f5c\u7b54\u4e2d/.test(slog)) { log(`\u8bfe\u7a0b${courseName}\uff1a${slog}`); } if (/^\u4ea4\u5377/.test(slog)) { log(`\u8bfe\u7a0b${courseName}\u5f00\u59cb\u4ea4\u5377`); } cmd = next.command; } if (!cmd) { trace("\u4e91\u7aef\u8003\u8bd5\uff1a\u5f15\u64ce\u672a\u8fd4\u56de\u4e0b\u4e00\u6b65\u6307\u4ee4"); return { terminal: true, ok: false, msg: "\u4e91\u7aef\u5f15\u64ce\u65e0\u4e0b\u4e00\u6b65\u6307\u4ee4" }; } trace(`\u4e91\u7aef\u8003\u8bd5\uff1a\u6b65\u6570\u8fbe\u5230\u4e0a\u9650 ${maxSteps}`); return { terminal: true, ok: false, msg: "\u4e91\u7aef\u5f15\u64ce\u6b65\u6570\u8d85\u9650" }; } async function _cr() { if (!(await ensureCmeSession())) { clearCmeUserProfileCache(); log("\u8bf7\u5148\u767b\u5f55\u597d\u533b\u751f\u5e73\u53f0"); return false; } const prof = await _fip(true); if (!prof?.userAccount && !prof?.bdpUserId) { log("\u65e0\u6cd5\u8bfb\u53d6\u8d26\u53f7\u8d44\u6599\uff0c\u8bf7\u5237\u65b0\u6536\u85cf\u9875\u6216\u6253\u5f00\u9996\u9875\u540e\u91cd\u8bd5"); return false; } if (!prof?.userAccount) { trace("\u672a\u89e3\u6790\u5230 userAccount\uff0c\u6682\u7528 BDP \u7528\u6237 ID\uff1a" + prof.bdpUserId); } try { if (state.cloudToken) await _vt(); await _el(false); } catch (err) { log("\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff1a" + (err.message || err)); return false; } const tier = String(state.cloudTier || "").toLowerCase(); if (tier === "pro") return true; if (tier === "free") { if (Number(state.freeUsedVideos) >= Number(state.freeVideoLimit)) { log(`\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${state.freeUsedVideos}/${state.freeVideoLimit}\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`); switchPanelTab("settings"); openProModal(); return false; } return true; } log("\u4e91\u7aef\u6388\u6743\u72b6\u6001\u5f02\u5e38\uff0c\u8bf7\u68c0\u67e5 Token \u6216\u4e91\u7aef\u5730\u5740"); return false; } function _so() { try { return new URL(getCloudApiBase()).origin; } catch (_) { return ""; } } async function _pn() { const origin = _so(); if (!origin) return; const path = String(state.panelNoticePath || _w(3)); const url = `${origin}${path.startsWith("/") ? path : `/${path}`}`; try { const res = await _gm(url, "GET", {}, null); if (res.status >= 200 && res.status < 300) { state.remotePanelNotice = String(res.text || "").trim() || PANEL_NOTICE_FALLBACK; const el = document.querySelector("#cme-ann-text"); if (el) el.textContent = state.remotePanelNotice; } } catch (_) {} } async function _cf() { try { const data = await _gm( `${getCloudApiBase()}${_w(2)}`, "GET", { Accept: "application/json" }, null ); if (data.status >= 200 && data.status < 300 && data.text) { const j = JSON.parse(data.text); if (j?.panelNoticePath) state.panelNoticePath = String(j.panelNoticePath); if (j?.freeVideoLimit != null) state.freeVideoLimit = Number(j.freeVideoLimit) || 1; if (j?.proBuyUrl) state.proBuyUrl = String(j.proBuyUrl).trim() || PRO_BUY_URL; } } catch (_) {} await _pn(); } function getProBuyUrl() { return String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL; } function isProBuyOpen() { return Date.now() >= PRO_BUY_OPEN_MS; } function syncProBuyButtonUi() { const btn = document.getElementById("cme-pro-buy-link"); if (!btn) return; const open = isProBuyOpen(); btn.disabled = !open; btn.textContent = open ? "\u524d\u5f80\u8d2d\u4e70\u9875\u5f00\u901a Pro" : "6\u670810\u65e519\u70b9\u5f00\u653e\u8d2d\u4e70"; btn.title = open ? "\u6253\u5f00\u8d2d\u4e70\u9875\u83b7\u53d6 Token" : "6\u670810\u65e5\u516c\u6d4b\u8bf7\u4eceQQ\u7fa4\u83b7\u53d6Token"; } function openProBuyPage() { if (!isProBuyOpen()) { log("\u8d2d\u4e70\u9875\u5c06\u4e8e6\u670810\u65e519\u70b9\u5f00\u653e\uff0c\u516c\u6d4b\u8bf7\u52a0\u5165QQ\u7fa4\u83b7\u53d6Token"); return; } const url = getProBuyUrl(); try { GM_openInTab(url, { active: true, insert: true, setParent: true }); } catch (_) { window.open(url, "_blank", "noopener,noreferrer"); } } function openProModal() { syncProBuyButtonUi(); const vm = document.getElementById("cme-pro-modal"); if (vm) vm.style.display = "flex"; } function closeProModal() { const vm = document.getElementById("cme-pro-modal"); if (vm) vm.style.display = "none"; } function createProModal() { const old = document.getElementById("cme-pro-modal"); if (old) old.remove(); const modal = document.createElement("div"); modal.id = "cme-pro-modal"; modal.innerHTML = `
\u5f00\u901a Pro
Pro \u7528\u6237\u53ef\u65e0\u9650\u5236\u89c6\u9891\u5b66\u4e60\uff0c\u5e76\u81ea\u52a8\u5b8c\u6210\u8bfe\u7a0b\u8003\u8bd5

Pro \u6743\u76ca

  • \u89e3\u9664\u514d\u8d39\u4f53\u9a8c\u89c6\u9891\u6b21\u6570\u9650\u5236\uff0c\u5df2\u9009\u8bfe\u7a0b\u53ef\u6301\u7eed\u5b66\u4e60
  • \u652f\u6301\u5b66\u5b8c\u540e\u81ea\u52a8\u8003\u8bd5\u4ea4\u5377
  • Pro \u6709\u6548\u671f 10 \u5929\uff08\u7ed1\u5b9a\u597d\u533b\u751f\u8d26\u53f7\uff0c\u5207\u6362\u8d26\u53f7\u5931\u6548\uff09
\u63d0\u793a\uff1aPro \u6743\u9650\u7ed1\u5b9a\u5f53\u524d\u767b\u5f55\u7684\u597d\u533b\u751f\u8d26\u53f7\uff0c\u8bf7\u52ff\u4e0e\u4ed6\u4eba\u5171\u7528 Token\u3002

\u5f00\u901a\u65b9\u5f0f

1) \u52a0\u5165 QQ \u7fa4 ${QQ_GROUP_NUMBER}\uff0c\u8054\u7cfb\u7ba1\u7406\u5458\u83b7\u53d6 Token\uff08\u516c\u6d4b\u671f\u95f4\u622a\u6b626\u670810\u65e5\uff0c\u53cd\u9988Bug\u53ef\u4f18\u5148\u4f53\u9a8c\uff09

2) \u524d\u5f80\u8d2d\u4e70\u9875\u83b7\u53d6 Token\uff0c24 \u5c0f\u65f6\u81ea\u52a9\u53d1\u8d27

\u8d2d\u5f97 Token \u540e\uff1a\u5728\u8bbe\u7f6e\u9875 Token \u8f93\u5165\u6846\u7c98\u8d34\uff0c\u70b9\u51fb\u300c\u4fdd\u5b58\u5e76\u6821\u9a8c\u300d\u3002
\u91cd\u8981\uff1a\u8bf7\u52ff\u968f\u610f\u6cc4\u9732 Token\uff0c\u907f\u514d\u8d26\u53f7\u88ab\u591a\u4eba\u5171\u7528\u5bfc\u81f4\u5931\u6548\u3002
`; document.body.appendChild(modal); modal.addEventListener("click", (e) => { if (e.target === modal) closeProModal(); }); modal.querySelector("#cme-pro-close")?.addEventListener("click", closeProModal); modal.querySelector("#cme-pro-buy-link")?.addEventListener("click", openProBuyPage); syncProBuyButtonUi(); } function switchPanelTab(name) { document.querySelectorAll(".cme-tab-btn").forEach((el) => { el.classList.toggle("active", el.dataset.tab === name); }); document.querySelectorAll(".cme-pane").forEach((el) => { el.classList.toggle("active", el.dataset.pane === name); }); } function formatCloudTierText(tier) { if (state.cloudRevoked) return "\u5df2\u7981\u7528"; const t = String(tier || "").trim().toLowerCase(); if (t === "pro") return "Pro \u4f1a\u5458"; if (t === "free") return "\u514d\u8d39\u4f53\u9a8c"; if (t === "debug") return "\u672c\u5730\u8c03\u8bd5"; if (t === "revoked") return "\u5df2\u7981\u7528"; if (t === "unknown") return "\u672a\u6821\u9a8c"; return tier ? String(tier) : "\u672a\u6821\u9a8c"; } function updateCloudPanelUI() { const tierEl = document.querySelector("#cme-cloud-tier"); const freeEl = document.querySelector("#cme-cloud-free"); const freeLabelEl = document.querySelector("#cme-cloud-free-label"); const tokenInput = document.querySelector("#cme-cloud-token"); if (tierEl) { tierEl.textContent = formatCloudTierText(state.cloudTier); tierEl.style.color = state.cloudRevoked || String(state.cloudTier || "").toLowerCase() === "revoked" ? "#dc2626" : "#0f172a"; } if (state.cloudRevoked) { if (freeLabelEl) freeLabelEl.textContent = "\u6388\u6743\u72b6\u6001"; if (freeEl) freeEl.textContent = "Token \u5df2\u7981\u7528"; } else if (String(state.cloudTier || "").toLowerCase() === "debug") { if (freeLabelEl) freeLabelEl.textContent = "\u8fd0\u884c\u6a21\u5f0f"; if (freeEl) freeEl.textContent = "\u672c\u5730\u5f15\u64ce\uff08\u65e0\u4e91\u7aef\uff09"; } else if (String(state.cloudTier || "").toLowerCase() === "pro") { if (freeLabelEl) freeLabelEl.textContent = "Pro \u5230\u671f"; if (freeEl) freeEl.textContent = formatLeaseExpireText(state.cloudProExpireAt || state.cloudLeaseExp); } else { if (freeLabelEl) freeLabelEl.textContent = "\u514d\u8d39\u4f53\u9a8c\u89c6\u9891"; if (freeEl) freeEl.textContent = `${state.freeUsedVideos}/${state.freeVideoLimit}`; } if (tokenInput && tokenInput !== document.activeElement) tokenInput.value = state.cloudToken || ""; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function getCookie(name) { const m = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)")); return m ? decodeURIComponent(m[1]) : ""; } function getSessionFingerprint() { return String(getCookie("client_id") || "").trim(); } function isProfileCacheValid(prof) { if (!prof) return false; const fp = getSessionFingerprint(); const cachedFp = String(prof.sessionFp || prof.jsid || ""); if (fp && cachedFp) return fp === cachedFp; return state.cmeUserProfile === prof; } function isLoggedInByDom() { if (pageType() !== "favorite" && pageType() !== "home") return false; const html = document.documentElement.outerHTML; if (/\u8bf7\u5148\u767b\u5f55|\u767b\u5f55\u8d85\u65f6|\u672a\u767b\u5f55|cmsLogin/i.test(html)) return false; return /course\.jsp\?course_id=|\u6b22\u8fce\u60a8|IC\s*\u5361|home_table/i.test(html); } function hasCmeSessionSync() { if (getSessionFingerprint()) return true; if (getLearningUserId()) return true; return isLoggedInByDom(); } async function probeCmeSessionOnline() { try { const r = await platformFetch("/cme/myFavorite.jsp?_=" + Date.now()); if (/\u8bf7\u5148\u767b\u5f55|\u767b\u5f55\u8d85\u65f6|\u672a\u767b\u5f55|cmsLogin/i.test(r.text)) return false; return /course\.jsp\?course_id=|home_table|myFavorite/i.test(r.text); } catch (_) { return false; } } async function ensureCmeSession() { if (hasCmeSessionSync()) return true; return probeCmeSessionOnline(); } function getUserData() { const prof = state.cmeUserProfile || readCachedCmeProfile(); const uid = getLearningUserId(); if (!uid) return null; return { custId: uid, loginName: prof?.userAccount || uid, realName: prof?.realName || "", }; } function syncCmeProfileFromDom() { if (pageType() !== "home") return null; const parsed = parseCmeIndexProfile(document.documentElement.outerHTML); if (!parsed?.userAccount && !parsed?.bdpUserId) return null; if (!hasCmeSessionSync()) return null; const prof = Object.assign({ sessionFp: getSessionFingerprint() || "dom", ts: Date.now() }, parsed); state.cmeUserProfile = prof; writeCachedCmeProfile(prof); return prof; } function isLoggedIn() { if (hasCmeSessionSync()) return true; return isLoggedInByDom(); } function getLearningUserId() { const prof = state.cmeUserProfile || readCachedCmeProfile(); if (prof?.userAccount) return String(prof.userAccount).trim(); if (prof?.bdpUserId) return String(prof.bdpUserId).trim(); return ""; } function pageType() { const path = location.pathname; if (path.includes("study2") || path.includes("polyv")) return "courseplay"; if (path.includes("myFavorite")) return "favorite"; if (path.includes("course.jsp")) return "course"; if (path.includes("/cme/index.jsp") || path.endsWith("index.jsp")) return "home"; return "other"; } function getCourseParams() { const qs = new URLSearchParams(location.search); return { csId: qs.get("course_id") || qs.get("id") || "", wareId: qs.get("courseware_id") || "", }; } function courseplayPath(csId, tpId, wareId, productId) { if (wareId) { let p = `/cme/study2.jsp?course_id=${csId}&courseware_id=${wareId}`; if (productId) p += `&product_id=${productId}`; return p; } return `/cme/course.jsp?course_id=${csId}`; } function readNavGuard() { try { return JSON.parse(sessionStorage.getItem(NAV_GUARD_KEY) || "{}"); } catch (_) { return {}; } } function writeNavGuard(url) { sessionStorage.setItem( NAV_GUARD_KEY, JSON.stringify({ url, time: Date.now() }) ); } function isSameCoursePage(csId) { return (pageType() === "courseplay" || pageType() === "course") && getCourseParams().csId === csId; } function canNavigate(url) { const target = new URL(url, location.origin).pathname + new URL(url, location.origin).search; const current = location.pathname + location.search; if (target === current) return false; const guard = readNavGuard(); if (guard.url === target && Date.now() - (guard.time || 0) < NAV_COOLDOWN_MS) { return false; } return true; } function safeNavigate(url) { if (!canNavigate(url)) return false; writeNavGuard(url); log("\u8df3\u8f6c\uff1a" + url); location.href = url; return true; } function canClickChapter(chapterId) { if (!chapterId) return false; const guard = readNavGuard(); if (guard.chapterId === chapterId && Date.now() - (guard.chapterClickAt || 0) < CHAPTER_CLICK_COOLDOWN_MS) { return false; } return true; } function markChapterClick(chapterId) { const guard = readNavGuard(); guard.chapterId = chapterId; guard.chapterClickAt = Date.now(); sessionStorage.setItem(NAV_GUARD_KEY, JSON.stringify(guard)); } function visible(el) { if (!el) return false; const st = getComputedStyle(el); if (st.display === "none" || st.visibility === "hidden" || st.opacity === "0") return false; return el.offsetWidth > 0 || el.offsetHeight > 0; } function clickEl(el) { if (!el) return false; el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window })); return true; } function findVisibleButton(root, pattern) { return Array.from(root.querySelectorAll("button, .el-button, a")).find((el) => pattern.test((el.textContent || "").replace(/\s+/g, "")) ); } function dismissDialogs() { const onCoursePlay = pageType() === "courseplay"; document.querySelectorAll(".el-message-box__wrapper, .el-dialog__wrapper").forEach((wrapper) => { if (!visible(wrapper)) return; const btn = onCoursePlay ? findVisibleButton(wrapper, /^(\u786e\u5b9a|\u77e5\u9053\u4e86|\u7ee7\u7eed|\u786e\u8ba4)$/) || findVisibleButton(wrapper, /(\u786e\u5b9a|\u77e5\u9053\u4e86)/) : findVisibleButton(wrapper, /^(\u53bb\u5b66\u4e60|\u786e\u5b9a|\u77e5\u9053\u4e86|\u7ee7\u7eed|\u786e\u8ba4)$/) || findVisibleButton(wrapper, /(\u53bb\u5b66\u4e60|\u786e\u5b9a)/); if (btn) clickEl(btn); }); } function isFaceDialogVisible() { const qr = document.getElementById("qrCode"); if (qr && visible(qr.closest(".el-dialog__wrapper") || qr)) return true; return Array.from(document.querySelectorAll(".el-dialog__wrapper")).some((w) => { if (!visible(w)) return false; const t = w.textContent || ""; return /\u4eba\u8138|\u8bc6\u522b|\u626b\u7801|\u4e0a\u4f20\u5934\u50cf/.test(t); }); } function getVideoEl() { const box = document.getElementById("player_video"); if (box) { const v = box.querySelector("video"); if (v) return v; } return document.querySelector("#player_video video, .playboxin video, video"); } function getPlayButton() { return ( document.querySelector(".e3cplay-play, .vjs-play-control, .xgplayer-play, .play-btn") || document.querySelector("#player_video .vjs-big-play-button") ); } async function ensurePlaying() { if (isFaceDialogVisible()) { if (!state.faceWaiting) { state.faceWaiting = true; log("\u68c0\u6d4b\u5230\u4eba\u8138\u8bc6\u522b\uff0c\u8bf7\u624b\u673a\u626b\u7801\u540e\u7ee7\u7eed"); } return; } state.faceWaiting = false; const video = getVideoEl(); if (!video) return; if (video.ended) return; if (video.paused) { try { video.muted = true; await video.play(); log("\u5df2\u81ea\u52a8\u64ad\u653e\u89c6\u9891"); return; } catch (_) { const btn = getPlayButton(); if (btn) { clickEl(btn); log("\u5df2\u70b9\u51fb\u64ad\u653e\u6309\u94ae"); } } } } function chapterDone(link) { const img = link.querySelector("img.course_play_percent"); if (!img || !img.src) return false; return /play100|100/.test(img.src) || /play100/.test(img.getAttribute("src") || ""); } function getChapterLinks() { return Array.from(document.querySelectorAll("a.cptresult_a")); } function getCurrentChapterId() { const active = document.querySelector("li.active a.cptresult_a"); return active?.dataset?.name || active?.id || ""; } function findNextIncompleteChapter(skipCurrent) { const links = getChapterLinks(); if (!links.length) return null; const currentId = getCurrentChapterId(); let passedCurrent = !currentId || !skipCurrent; for (const link of links) { const id = link.dataset.name || link.id; if (!passedCurrent) { if (id === currentId) passedCurrent = true; continue; } if (!chapterDone(link)) return link; } if (skipCurrent) return null; return links.find((link) => !chapterDone(link)) || null; } function ensureInStudyTab() { const inStudyTab = Array.from(document.querySelectorAll(".tab-ul li, .zaixue-course-tit li")).find((li) => /\u5728\u5b66/.test(li.textContent || "") ); if (inStudyTab && !inStudyTab.classList.contains("on") && !inStudyTab.classList.contains("active")) { clickEl(inStudyTab.querySelector("a") || inStudyTab); return true; } return false; } function apiPost(path, payload) { const body = payload == null ? null : typeof payload === "string" ? payload : new URLSearchParams(payload).toString(); return platformFetch(path, { method: "POST", headers: body ? { "Content-Type": "application/x-www-form-urlencoded" } : {}, body, }).then((r) => { try { return JSON.parse(r.text); } catch { return { success: r.status < 400 ? "1" : "0", text: r.text, status: r.status }; } }); } async function fetchHeartbeatSec() { return HEARTBEAT_BASE; } function isCourseUnfinished(c) { if (!c) return false; if (isFalsyFlag(c.lcsFinished)) return true; if (isFalsyFlag(c.lcsStudyFinished)) return true; if (isFalsyFlag(c.lcsExameFinished) && (c.courseTestList || []).length) return true; const p = parseFloat(c.lcsProcess); return !Number.isNaN(p) && p < 100; } function isCourseFullyFinished(c) { if (!c) return false; if (isTruthyFlag(c.lcsFinished)) return true; const hasExam = (c.courseTestList || []).length > 0; if (hasExam) return isStudyFinished(c) && isTruthyFlag(c.lcsExameFinished); return isStudyFinished(c); } function isStudyFinished(course) { if (!course) return false; if (isTruthyFlag(course.lcsStudyFinished)) return true; return parseFloat(course.lcsProcess || 0) >= 100; } function courseNeedsExam(course) { if (!getAutoExam() || !course) return false; if (isTruthyFlag(course.lcsExameFinished)) return false; const test = (course.courseTestList || [])[0]; if (!test?.tsId) return false; return isStudyFinished(course); } async function buildCourseFromChapters(csId, title) { const chapters = await fetchCourseChapters(csId); const total = chapters.length || 1; const done = chapters.filter((x) => x.status === "done").length; const examPending = chapters.filter((x) => x.status === "exam").length; const process = Math.round(((done + examPending) / total) * 100); return { csId, tpId: "favorite", csName: title || "\u8bfe\u7a0b" + csId, lcsProcess: process, tpName: "\u6211\u7684\u6536\u85cf", planYear: "", lcsStudyFinished: examPending > 0 || done >= total ? "1" : "0", lcsExameFinished: done >= total && examPending === 0 ? "1" : "0", lcsFinished: done >= total && examPending === 0 ? "1" : "0", courseTestList: chapters .filter((ch) => ch.status === "exam") .map((ch) => ({ tsId: ch.wareId, tsSpcId: ch.productId, spcId: ch.productId })), }; } async function fetchCourseFromPlans(csId) { const cached = state.panelCourses.find((c) => c.csId === csId); if (cached) return cached; try { const html = await fetchFavoriteHtml(); const found = parseCoursesFromHtml(html).find((c) => c.courseId === csId); return await buildCourseFromChapters(csId, found?.title); } catch (_) { return null; } } async function listAllUnfinishedCourses() { if (!state.panelCourses.length) await fetchPanelCourses(); return state.panelCourses.filter(isCourseUnfinished); } async function findUnfinishedCourse() { const list = await listAllUnfinishedCourses(); if (!list.length) { log("\u5df2\u68c0\u67e5\u6536\u85cf\u8bfe\u7a0b\uff0c\u5747\u663e\u793a\u5df2\u5b8c\u6210"); return null; } return list[0]; } async function findNextWorkInQueue(options) { const silent = !!(options && options.silent); const queue = loadQueue(); if (!queue.length) { if (!silent) log("\u8bf7\u52fe\u9009\u8981\u5b66\u4e60\u7684\u8bfe\u7a0b\u540e\u518d\u70b9\u5f00\u59cb"); return null; } const allCourses = await listAllUnfinishedCourses(); const courseMap = new Map(allCourses.map((c) => [c.csId, c])); for (const csId of queue) { let course = courseMap.get(csId); if (!course) course = await fetchCourseFromPlans(csId); if (!course) continue; const { all, study, exam } = await partitionCourseWork(csId); if (study.length) { const nextStudy = study[0]; return { phase: "study", course, videos: [nextStudy], videoTotal: all.length, videoDone: all.length - study.length - exam.length, }; } if (exam.length && getAutoExam()) { const nextExam = exam[0]; return { phase: "exam", course, videos: [nextExam], test: buildTestFromVideo(nextExam), }; } } return null; } function collectCorrectAnswers(paper) { const answers = []; for (const cat of paper.questionCats || []) { const itemType = cat.itemType; for (const q of cat.questions || []) { const correct = (q.options || []).filter((o) => isTruthyFlag(o.answer)); if (!correct.length) continue; let answerIdValue; if (itemType === "ms") { answerIdValue = correct.map((o) => o.itemId).join(","); } else { answerIdValue = correct[0].itemId; } answers.push({ questionId: q.questionId, answerIdValue, answerContentValue: "", }); } } return answers; } async function submitCourseExam(course, testInfo) { const user = getUserData(); if (!user?.custId) return { ok: false, msg: "\u672a\u767b\u5f55" }; const tsId = testInfo.tsId; const spcId = testInfo.tsSpcId || testInfo.spcId || testInfo.productId; if (!tsId) return { ok: false, msg: "\u7f3a\u5c11 paper_id" }; try { await _el(false); } catch (err) { return { ok: false, msg: "\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff1a" + (err.message || err) }; } try { return await runClientCourseExam(course.csId, tsId, spcId); } catch (err) { trace("\u8003\u8bd5\uff1a" + (err.message || err)); return { ok: false, msg: err.message || "\u8003\u8bd5\u5931\u8d25" }; } } function applyBgProgressEstimate(bg) { const total = Number(bg.videoTotal) || 0; if (!total) return; const completed = Number(bg.videoDone || 0) + Number(bg.videoIndex || 0); const est = (completed / total) * 100; bg.lcsProcess = Math.max(Number(bg.lcsProcess || 0), est); } async function afterVideoLearnSuccess(bg, idx, waitSec) { const video = bg.videos[idx]; bg.videoIndex = idx + 1; bg.lastSaveAt = Date.now(); bg.saveCount = (bg.saveCount || 0) + 1; const prog = await fetchCourseProgress(bg.csId); if (prog) { bg.lcsProcess = prog.lcsProcess; bg.csName = prog.csName || bg.csName; } applyBgProgressEstimate(bg); updateCourseProgressInPanel(bg.csId, bg.lcsProcess); updatePanel(); void refreshChapterPreview(); if (video) { const vlabel = video.resName || `\u8bfe\u4ef6${formatWareNo(video.resId || idx + 1)}`; log(`\u89c6\u9891\u5df2\u5b66\u5b8c\uff1a\u7b2c${formatWareNo(video.resId || idx + 1)}\u8282 ${vlabel}`); } if (video && (await tryBeginSameChapterExam(bg, video))) { return; } saveBgState(bg); const allVideosDone = bg.videoIndex >= bg.videos.length; if (allVideosDone) { await finishCourseAndAdvance(bg); } } function stopAutoStudy(reason) { if (!state.enabled) return; state.enabled = false; localStorage.setItem(STORAGE_KEY, "0"); clearStudyModeSelection(); syncStartStopButtons(); syncKeepAlive(); stopPreviewAutoRefresh(); log(reason || "\u6240\u6709\u5df2\u9009\u8bfe\u7a0b\u5747\u5df2\u5b8c\u6210\uff0c\u7a0b\u5e8f\u81ea\u52a8\u505c\u6b62"); updatePanel(); } function buildBgStateFromWork(hit) { const { course, phase } = hit; const base = { csId: course.csId, tpId: course.tpId, csName: course.csName, lcsProcess: parseFloat(course.lcsProcess || 0), phase, lastSaveAt: 0, saveCount: 0, }; if (phase === "study") { return { ...base, videos: hit.videos, videoIndex: 0, videoTotal: hit.videoTotal ?? hit.videos.length, videoDone: hit.videoDone ?? 0, }; } return { ...base, videos: hit.videos || [], videoIndex: 0, test: hit.test }; } function isVideoUnfinished(v) { if (!v) return false; return isChapterNeedStudy(v); } async function loadCourseVideos(csId, forceSidebar) { const chapters = await fetchCourseChapters(csId); const sidebarMap = await getSidebarStatusMap(csId, !!forceSidebar); const out = []; for (const ch of chapters) { if (/^\d{10,}$/.test(String(ch.wareId || ""))) continue; const status = resolveChapterStatus(ch.wareId, ch.status, sidebarMap); let total = 3600; let resName = ch.title; let productId = ch.productId || ""; try { const meta = await fetchStudyMeta(csId, ch.wareId); total = meta.totalTime || 3600; if (meta.productId) productId = meta.productId; const metaTitle = parseWareTitleFromHtml(meta.html, ch.wareId); if (metaTitle && (!resName || resName.startsWith("\u8bfe\u4ef6 ") || resName.length > 50)) { resName = metaTitle; } } catch (_) {} out.push({ resId: ch.wareId, cptId: ch.wareId, fileTimeLen: total, studyTime: status === "done" ? total : 0, process: status === "done" ? 100 : status === "exam" ? 100 : 0, studyFinished: status === "done" || status === "exam" ? "1" : "0", resName: resName || "\u8bfe\u4ef6 " + ch.wareId, productId, chapterStatus: status, }); } return out; } async function fetchVideoBreakpoint(csId, resId) { return 0; } async function learnSaveFullVideo(csId, video, mult) { const wid = String(video.cptId || video.resId); const recent = readRecentStudyDone(csId, wid); if (recent?.ok && (await checkIsExam(csId, wid))) { markChapterProgressReported(csId, wid); return { success: "1", skipped: true }; } const user = getUserData(); if (!user?.custId) throw new Error("\u672a\u767b\u5f55"); const cfg = { study_mode: "efficiency", multiplier: mult, free_video_limit: state.freeVideoLimit, }; await _el(false); const startRes = await _es( "video", { cs_id: csId, course_id: csId, study_user_id: user.custId, video: { resId: video.resId, cptId: video.cptId, ware_id: video.cptId, fileTimeLen: video.fileTimeLen, resName: video.resName || "", product_id: video.productId || "", }, multiplier: cfg.multiplier, breakpoint: 0, }, cfg ); const loop = await _ve(startRes, { quiet: true, maxSteps: 96 }); if (loop.skipped) return { success: "1", skipped: true }; if (loop.ok) return { success: "1" }; throw new Error(loop.msg || "\u4e91\u7aef\u5f15\u64ce\u5931\u8d25"); } async function initBackgroundTarget() { const queue = loadQueue(); const saved = loadBgState(); if (queue.length && saved.csId && queue.includes(saved.csId)) { if (saved.phase === "exam" && (saved.videos?.length || saved.test?.tsId)) { const wid = saved.videos?.[saved.videoIndex || 0]?.resId || saved.test?.tsId; const st = wid ? await querySidebarStatus(saved.csId, wid) : ""; if (st === "done") { trace(`\u7b2c${formatWareNo(wid)}\u8282\u5df2\u901a\u8fc7\uff0c\u91cd\u65b0\u626b\u63cf\u4efb\u52a1`); saveBgState({}); } else { clearIsExamCache(saved.csId, wid); const meta = await fetchStudyMeta(saved.csId, wid).catch(() => null); const ready = meta ? await checkChapterExamReady(saved.csId, wid, meta.totalTime || 3600) : { videoDone: false }; if (!ready.videoDone) { trace(`\u7b2c${formatWareNo(wid)}\u8282 \u6062\u590d\u540e\u8865\u62a5\u5b66\u4e60\u8fdb\u5ea6`); return { ...saved, lastSaveAt: 0, apiBoostCount: 0 }; } return { ...saved, lastSaveAt: 0 }; } } if (saved.phase === "study" && saved.videos?.length) { const idx = saved.videoIndex || 0; if (idx < saved.videos.length) { const { all, study } = await partitionCourseWork(saved.csId); if (study.length) { let videoIndex = idx; const cur = saved.videos[idx]; if (cur?.resId) { const at = study.findIndex((v) => v.resId === cur.resId); if (at >= 0) videoIndex = 0; else videoIndex = 0; } const prog = await fetchCourseProgress(saved.csId); return { ...saved, phase: "study", videos: [study[0]], videoIndex: 0, lastSaveAt: 0, videoTotal: all.length, videoDone: all.length - study.length, lcsProcess: prog?.lcsProcess ?? saved.lcsProcess, csName: prog?.csName || saved.csName, }; } } } } else if (saved.csId && queue.length && !queue.includes(saved.csId)) { saveBgState({}); } const hit = await findNextWorkInQueue({ silent: true }); if (!hit) return null; const bg = buildBgStateFromWork(hit); if (hit.phase === "study") { const prog = await fetchCourseProgress(hit.course.csId); if (prog) { bg.lcsProcess = prog.lcsProcess; bg.csName = prog.csName || bg.csName; } } return bg; } async function finishCourseAndAdvance(bg) { bg.sameChapterExam = false; const { all, study, exam } = await partitionCourseWork(bg.csId); if (study.length) { bg.phase = "study"; bg.videos = [study[0]]; bg.videoIndex = 0; bg.videoTotal = all.length; bg.videoDone = all.length - study.length - exam.length; bg.lastSaveAt = 0; saveBgState(bg); log(`\u4e0b\u4e00\u4efb\u52a1\uff1a\u89c6\u9891\u7b2c${formatWareNo(study[0].resId)}\u8282 ${study[0].resName || ""}`); renderCourseList(); void refreshChapterPreview(); return; } if (exam.length && getAutoExam()) { const next = exam[0]; bg.phase = "exam"; bg.videos = [next]; bg.videoIndex = 0; bg.test = buildTestFromVideo(next); bg.lastSaveAt = 0; saveBgState(bg); log(`\u4e0b\u4e00\u4efb\u52a1\uff1a\u8003\u8bd5\u7b2c${formatWareNo(next.resId)}\u8282 ${next.resName || ""}`); renderCourseList(); void refreshChapterPreview(); return; } saveBgState({}); state.coursesDoneSession += 1; log(`\u8bfe\u7a0b${courseDisplayName(bg)}\u5df2\u5168\u90e8\u5b8c\u6210`); void refreshChapterPreview(); refreshPanelCourses(); } async function fetchCourseProgress(csId) { if (!csId) return null; try { const c = state.panelCourses.find((x) => x.csId === csId); if (c) return { lcsProcess: c.lcsProcess, csName: c.csName }; const row = await buildCourseFromChapters(csId); return { lcsProcess: row.lcsProcess, csName: row.csName }; } catch (_) { return null; } } function formatBgPanelExtra(bg) { if (!bg?.csId) return ""; const name = (bg.csName || "\u8bfe\u7a0b").slice(0, 12); const idx = (bg.videoIndex || 0) + 1; const total = bg.videos?.length || 0; if (bg.phase === "exam") { const cur = bg.videos?.[bg.videoIndex || 0]; const label = cur?.resName || bg.test?.resName || bg.test?.tsId || ""; return `\u3010${name} · \u8003\u8bd5 ${idx}/${total}${label ? " · " + label.slice(0, 8) : ""}\u3011`; } const cur = bg.videos?.[bg.videoIndex || 0]; const proc = bg.lcsProcess != null ? ` · ${bg.lcsProcess}%` : ""; const vid = cur ? ` · ${cur.fileTimeLen}s` : ""; return `\u3010${name} \u89c6\u9891${idx}/${total}${vid}${proc}\u3011`; } function hasActiveBgTask(bg) { if (!bg?.csId) return false; if (bg.phase === "exam") return !!(bg.videos?.length || bg.test?.tsId); return bg.phase === "study" && !!(bg.videos && bg.videos.length); } async function backgroundLearnTick() { try { if (pageType() === "courseplay") { trace("\u540e\u53f0\u6a21\u5f0f\uff1a\u8bf7\u5173\u95ed study2 \u64ad\u653e\u9875"); return; } let bg = loadBgState(); if (!hasActiveBgTask(bg)) { bg = await initBackgroundTarget(); if (!bg) { if (state.enabled) { stopAutoStudy("\u6240\u6709\u5df2\u9009\u8bfe\u7a0b\u5747\u5df2\u5b8c\u6210\uff0c\u7a0b\u5e8f\u81ea\u52a8\u505c\u6b62"); } return; } saveBgState(bg); renderCourseList(); const cname = courseDisplayName(bg); const proc = Number(bg.lcsProcess || 0).toFixed(1); if (bg.phase === "exam") { log(`\u5f00\u59cb\u5b66\u4e60\uff1a${cname}\uff08\u8fdb\u5165\u8003\u8bd5\u9636\u6bb5\uff09`); } else { log(`\u5f00\u59cb\u5b66\u4e60\uff1a${cname}`); } } const waitSec = getSaveInterval(); if (bg.phase === "study" && isApiOnlyStudy()) { const sw = bg.videos?.[bg.videoIndex || 0]?.resId; const apiSt = sw ? loadApiProgressState(bg.csId, sw) : null; if (apiSt && apiSt.current < apiSt.total && Date.now() < apiSt.nextAt) { updatePanel(); return; } } const elapsed = Date.now() - (bg.lastSaveAt || 0); let skipWait = false; if (bg.phase === "exam") { const ew = bg.videos?.[bg.videoIndex || 0]?.resId || bg.test?.tsId; if (ew) { clearIsExamCache(bg.csId, ew); skipWait = !(await checkIsExam(bg.csId, ew)); } } else if (bg.phase === "study" && isApiOnlyStudy()) { const sw = bg.videos?.[bg.videoIndex || 0]?.resId; if (sw && isChapterProgressFull(bg.csId, sw)) skipWait = true; const apiSt = sw ? loadApiProgressState(bg.csId, sw) : null; if (apiSt && apiSt.current < apiSt.total && Date.now() >= apiSt.nextAt) { skipWait = true; } } if (bg.lastSaveAt && elapsed < waitSec * 1000 && !skipWait) { updatePanel(); return; } if (bg.phase === "exam") { const course = (await fetchCourseFromPlans(bg.csId)) || { csId: bg.csId, csName: bg.csName, tpId: bg.tpId, courseTestList: [bg.test], }; const eidx = bg.videoIndex || 0; const ev = bg.videos?.[eidx]; const testInfo = ev ? buildTestFromVideo(ev) : bg.test; if (!testInfo?.tsId) { trace("\u8003\u8bd5\u961f\u5217\u5f02\u5e38\uff0c\u91cd\u65b0\u626b\u63cf\u8bfe\u7a0b"); saveBgState({}); return; } const examCtx = await resolveExamContext(bg.csId, testInfo.tsId, testInfo.productId); const elabel = ev?.resName || examCtx.resName || testInfo.resName || `\u8bfe\u4ef6${testInfo.tsId}`; try { if (await skipChapterIfAlreadyPassed(bg, course, testInfo.tsId, elabel)) return; const sidebarSt = await querySidebarStatus(bg.csId, testInfo.tsId); const holdingSame = isHoldingSameChapterExam(bg, testInfo.tsId); let meta = null; try { meta = await fetchStudyMeta(bg.csId, testInfo.tsId); } catch (_) {} const totalSec = meta?.totalTime || 3600; const examReady = await checkChapterExamReady(bg.csId, testInfo.tsId, totalSec); if (!examReady.videoDone) { const progressFull = isChapterProgressFull(bg.csId, testInfo.tsId); bg.apiBoostCount = (bg.apiBoostCount || 0) + 1; const maxBoost = progressFull ? ISEXAM_WAIT_MAX_TICKS : 8; if (bg.apiBoostCount <= maxBoost) { const boosted = await apiBoostStudyForExam(bg.csId, testInfo.tsId, elabel); clearIsExamCache(bg.csId, testInfo.tsId); if (boosted) { bg.apiBoostCount = 0; bg.lastSaveAt = 0; saveBgState(bg); return; } } else if (!progressFull && bg.apiBoostCount === maxBoost + 1) { trace(`\u7b2c${formatWareNo(testInfo.tsId)}\u8282 \u8003\u8bd5\u672a\u5c31\u7eea\uff0c\u9000\u56de\u5b66\u4e60\u961f\u5217`); clearChapterProgressReported(bg.csId, testInfo.tsId); bg.phase = "study"; bg.apiBoostCount = 0; bg.sameChapterExam = false; bg.pendingExamWareId = ""; bg.lastSaveAt = 0; saveBgState(bg); return; } bg.lastSaveAt = progressFull ? 0 : Date.now(); saveBgState(bg); trace( progressFull ? `\u7b2c${formatWareNo(testInfo.tsId)}\u8282 \u5df2\u62a5\u6ee1\uff0c\u7b49\u5f85 isexam\uff08${bg.apiBoostCount}/${maxBoost}\uff09` : `\u7b2c${formatWareNo(testInfo.tsId)}\u8282 ${elabel} isexam≠1\uff0c${waitSec}s \u540e API \u8865\u62a5\uff08${bg.apiBoostCount}/${maxBoost}\uff09` ); return; } bg.apiBoostCount = 0; if (sidebarSt === "study") { trace(`\u7b2c${formatWareNo(testInfo.tsId)}\u8282 \u4fa7\u680f\u672a\u540c\u6b65\uff0cisexam\u5df2\u5c31\u7eea\uff0c\u5f00\u59cb\u8003\u8bd5`); } if (examCtx.productId && !testInfo.productId) testInfo.productId = examCtx.productId; log(`\u6b63\u5728\u8003\u8bd5\uff1a\u7b2c${formatWareNo(testInfo.tsId)}\u8282 ${elabel}`); const examRes = await submitCourseExam(course, testInfo); bg.lastSaveAt = Date.now(); if (examRes.ok) { if (examRes.skipped) { trace(`\u7b2c${formatWareNo(testInfo.tsId)}\u8282 ${elabel} \u5df2\u901a\u8fc7\uff0c\u8df3\u8fc7`); } else { log(`\u8003\u8bd5\u901a\u8fc7\uff1a\u7b2c${formatWareNo(testInfo.tsId)}\u8282 ${elabel}`); } clearIsExamCache(bg.csId, testInfo.tsId); invalidateSidebarStatusMap(bg.csId); bg.sameChapterExam = false; bg.pendingExamWareId = ""; await promoteToNextTask(bg, course, { excludeWareId: testInfo.tsId }); } else { if (/\u672a\u83b7\u53d6\u8bd5\u5377|ALREADY_PASSED/.test(examRes.msg || "")) { const st = await querySidebarStatus(bg.csId, testInfo.tsId); if (st === "done") { trace(`\u7b2c${formatWareNo(testInfo.tsId)}\u8282 \u5df2\u901a\u8fc7\uff0c\u8df3\u8fc7\u91cd\u590d\u7b54\u5377`); bg.pendingExamWareId = ""; await promoteToNextTask(bg, course, { excludeWareId: testInfo.tsId }); return; } } saveBgState(bg); log(`\u8003\u8bd5\u5931\u8d25\uff1a\u7b2c${formatWareNo(testInfo.tsId)}\u8282 ${examRes.msg}`); } } catch (err) { bg.lastSaveAt = Date.now(); saveBgState(bg); log(`\u8003\u8bd5\u5f02\u5e38\uff1a${err.message || err}`); } return; } const idx = bg.videoIndex || 0; const video = bg.videos[idx]; if (!video) { await finishCourseAndAdvance(bg); return; } const mult = getMultiplier(); const vlabel = video.resName || `\u8bfe\u4ef6${formatWareNo(video.resId || idx + 1)}`; const recentDone = readRecentStudyDone(bg.csId, video.resId); if (recentDone?.ok) { trace(`\u7b2c${formatWareNo(video.resId)}\u8282 \u64ad\u653e\u9875\u5df2\u5237\u5b8c\uff0c\u7ee7\u7eed\u6d41\u7a0b`); for (const w of wareIdVariants(video.resId)) localStorage.removeItem(studyDoneSignalKey(bg.csId, w)); markChapterProgressReported(bg.csId, video.resId); await afterVideoLearnSuccess(bg, idx, waitSec); return; } let res; try { log(`\u6b63\u5728\u89c2\u770b\uff1a\u7b2c${formatWareNo(video.resId || idx + 1)}\u8282 ${vlabel}`); res = await learnSaveFullVideo(bg.csId, video, mult); } catch (err) { const em = String(err.message || err); trace("\u4e91\u7aef\u89c6\u9891\u5f15\u64ce\uff1a" + em); if (/free_quota|\u914d\u989d/.test(em)) { state.enabled = false; localStorage.setItem(STORAGE_KEY, "0"); clearStudyModeSelection(); syncStartStopButtons(); syncKeepAlive(); updatePanel(); } return; } if (res.skipped || res.success === "1") { await afterVideoLearnSuccess(bg, idx, waitSec); return; } if (String(res.success).includes("drag")) { saveBgState({}); log(`\u4e0a\u62a5\u88ab\u62d2\uff08${video.resName}\uff09\uff0c\u5df2\u91cd\u7f6e`); return; } log("\u4e0a\u62a5\u5931\u8d25\uff1a" + (res.message || res.success) + " · " + (video.resName || "")); } catch (err) { log("\u540e\u53f0\u5237\u8bfe\u5f02\u5e38\uff1a" + (err.message || err)); } } async function openFirstUnfinishedCourseByApi() { const course = await findUnfinishedCourse(); if (!course) return false; if (isSameCoursePage(course.csId)) { log("\u5df2\u5728\u5f53\u524d\u8bfe\u7a0b\u9875\uff0c\u7b49\u5f85\u64ad\u653e"); return false; } const videos = await loadCourseVideos(course.csId); const pending = videos.filter(isVideoUnfinished); const ware = pending[0]; const target = ware ? courseplayPath(course.csId, course.tpId, ware.resId, ware.productId) : courseplayPath(course.csId, course.tpId); if (!canNavigate(target)) { log("\u521a\u8fdb\u5165\u8bfe\u7a0b\uff0c\u7b49\u5f85\u9875\u9762\u7a33\u5b9a"); return false; } log(`\u8fdb\u5165\u8bfe\u7a0b\uff1a${course.csName || course.csId}\uff08${course.lcsProcess || 0}%\uff09`); safeNavigate(target); return true; } async function handleTrainPage() { dismissDialogs(); const qs = new URLSearchParams(location.search); const csId = qs.get("course_id"); if (!csId) { if (pageType() !== "favorite") safeNavigate("/cme/myFavorite.jsp"); return; } const now = Date.now(); if (now - state.trainApiTriedAt < NAV_COOLDOWN_MS) return; state.trainApiTriedAt = now; const videos = (await loadCourseVideos(csId)).filter(isVideoUnfinished); if (!videos.length) { log("\u5f53\u524d\u8bfe\u7a0b\u89c6\u9891\u5747\u5df2\u5b66\u5b8c"); safeNavigate("/cme/myFavorite.jsp"); return; } const v = videos[0]; const target = courseplayPath(csId, "favorite", v.resId, v.productId); if (location.href.includes(`courseware_id=${v.resId}`)) return; if (!canNavigate(target)) return; log(`\u8fdb\u5165\u8bfe\u4ef6\uff1a${v.resName || v.resId}`); safeNavigate(target); } async function handleCoursePlayPage() { dismissDialogs(); const video = getVideoEl(); const links = getChapterLinks(); const activeId = getCurrentChapterId(); if (!video) { if (activeId) { if (!state.courseplayWaitSince) state.courseplayWaitSince = Date.now(); if (Date.now() - state.courseplayWaitSince < VIDEO_WAIT_MS) { log("\u7b49\u5f85\u64ad\u653e\u5668\u52a0\u8f7d..."); return; } state.courseplayWaitSince = 0; } const next = findNextIncompleteChapter(true); if (next) { const chapterId = next.dataset.name || next.id; if (!canClickChapter(chapterId)) { log("\u7ae0\u8282\u5207\u6362\u51b7\u5374\u4e2d"); return; } markChapterClick(chapterId); clickEl(next); log("\u5df2\u5207\u6362\u7ae0\u8282\uff1a" + (next.title || next.textContent || "").trim()); state.courseplayWaitSince = Date.now(); return; } if (links.length && !links.some((link) => !chapterDone(link))) { log("\u5f53\u524d\u8bfe\u7a0b\u7ae0\u8282\u5df2\u5168\u90e8\u5b8c\u6210\uff0c\u8fd4\u56de\u6536\u85cf\u9875"); safeNavigate("/cme/myFavorite.jsp"); } return; } state.courseplayWaitSince = 0; if (video.ended) { const next = findNextIncompleteChapter(true); if (!next) { log("\u5f53\u524d\u8bfe\u7a0b\u5df2\u5168\u90e8\u5b8c\u6210\uff0c\u8fd4\u56de\u6536\u85cf\u9875"); safeNavigate("/cme/myFavorite.jsp"); } return; } await ensurePlaying(); } async function handleHomePage() { if (!isLoggedIn()) { log("\u8bf7\u5148\u767b\u5f55"); return; } if (pageType() === "favorite") return; safeNavigate("/cme/myFavorite.jsp"); } async function tick() { if (!state.enabled) return; if (state.bgTickBusy) return; const now = Date.now(); if (now - state.lastTick < TICK_MS) return; state.lastTick = now; if (!isLoggedIn()) { log("\u672a\u767b\u5f55\uff0c\u8bf7\u5148\u767b\u5f55\u8d26\u53f7"); return; } if (!(await _cr())) { state.enabled = false; localStorage.setItem(STORAGE_KEY, "0"); clearStudyModeSelection(); syncStartStopButtons(); syncKeepAlive(); updatePanel(); return; } dismissDialogs(); const mode = getMode(); const type = pageType(); try { if (mode === "background") { if (state.bgTickBusy) return; state.bgTickBusy = true; try { await backgroundLearnTick(); } finally { state.bgTickBusy = false; } return; } if (mode === "manual") { if (type === "courseplay") { dismissDialogs(); return; } log("\u624b\u52a8\u6a21\u5f0f\uff1a\u8bf7\u81ea\u884c\u6253\u5f00\u64ad\u653e\u9875\u770b\u8bfe"); return; } if (type === "home" || type === "favorite") { await handleHomePage(); } else if (type === "course") { await handleTrainPage(); } else if (type === "courseplay") { await handleCoursePlayPage(); } } catch (err) { log("\u8fd0\u884c\u5f02\u5e38\uff1a" + (err.message || err)); } } async function fetchPanelCourses() { if (!(await ensureCmeSession())) { clearCmeUserProfileCache(); state.panelCourses = []; state.teachPlans = []; return []; } await _fip(false); const plans = await listTeachPlans(); state.teachPlans = plans; let tpId = loadSelectedTpId(); if (!tpId || !plans.some((p) => p.tpId === tpId)) { tpId = plans[0]?.tpId || "favorite"; saveSelectedTpId(tpId); } else { state.selectedTpId = tpId; } const raw = await listAllCoursesForPlan({ tpId }); const list = []; for (const c of raw) { const chapters = await fetchCourseChapters(c.csId); const total = chapters.length || 1; const studyDone = chapters.filter((x) => x.status === "exam" || x.status === "done").length; const examDone = chapters.filter((x) => x.status === "done").length; const examPending = chapters.filter((x) => x.status === "exam").length; const process = Math.round((studyDone / total) * 100); list.push({ csId: c.csId, tpId: c.tpId, csName: c.csName || c.courseName || "\u672a\u547d\u540d\u8bfe\u7a0b", lcsProcess: process, chapterTotal: total, studyDone, examDone, examPending, tpName: c.tpName || "\u6211\u7684\u6536\u85cf", planYear: "", lcsStudyFinished: examPending > 0 || studyDone >= total ? "1" : "0", lcsExameFinished: examDone >= total ? "1" : "0", lcsFinished: examDone >= total ? "1" : "0", courseTestList: chapters .filter((ch) => ch.status === "exam") .map((ch) => ({ tsId: ch.wareId, tsSpcId: ch.productId, spcId: ch.productId })), }); } state.panelCourses = list; state.panelCourses.sort((a, b) => { const au = isCourseUnfinished(a) ? 1 : 0; const bu = isCourseUnfinished(b) ? 1 : 0; if (au !== bu) return bu - au; return b.lcsProcess - a.lcsProcess; }); let q = loadQueue(); const touched = localStorage.getItem(QUEUE_TOUCHED_KEY) === "1"; if (!touched && !q.length && state.panelCourses.length) { q = state.panelCourses.filter(isCourseUnfinished).map((c) => c.csId); } saveQueue(q); return state.panelCourses; } async function refreshPanelCourses() { if (!(await ensureCmeSession())) return; state.panelRefreshing = true; updatePanel(); try { await fetchPanelCourses(); renderPlanSelect(); renderCourseList(); await refreshChapterPreview(); } catch (err) { log("\u5237\u65b0\u8bfe\u7a0b\u5931\u8d25\uff1a" + (err.message || err)); } finally { state.panelRefreshing = false; updatePanel(); } } function updateCourseProgressInPanel(csId, lcsProcess) { const c = state.panelCourses.find((x) => x.csId === csId); if (c && lcsProcess != null) c.lcsProcess = lcsProcess; renderCourseList(); void refreshChapterPreview(); } function startPreviewAutoRefresh() { stopPreviewAutoRefresh(); state.previewRefreshTimer = setInterval(() => { if (!state.enabled) return; refreshChapterPreview().catch(() => {}); }, 12000); } function stopPreviewAutoRefresh() { if (state.previewRefreshTimer) { clearInterval(state.previewRefreshTimer); state.previewRefreshTimer = null; } } function courseProgressLabel(c) { const total = Math.max(1, Number(c.chapterTotal) || 1); const study = Number(c.studyDone) || 0; const exam = Number(c.examDone) || 0; return `\u89c6\u9891 ${study}/${total} · \u8003\u8bd5 ${exam}/${total}`; } function formatChapterExamTag(status) { if (status === "done") return "\u5df2\u901a\u8fc7"; if (status === "exam") return "\u5f85\u8003\u8bd5"; return "\u672a\u8003\u8bd5"; } function renderPlanSelect() { const sel = document.getElementById("cme-plan-select"); if (!sel) return; const plans = state.teachPlans || []; if (!plans.length) { sel.innerHTML = ""; sel.disabled = true; return; } sel.disabled = false; sel.innerHTML = plans .map((p) => { const label = p.tpName || (p.year ? `${p.year}\u5e74\u5ea6` : p.tpId); const selected = p.tpId === state.selectedTpId ? " selected" : ""; return ``; }) .join(""); } function renderCourseList() { const box = document.getElementById("cme-course-list"); if (!box) return; const bg = loadBgState(); const activeCsId = bg.csId || ""; const selectedSet = new Set(loadQueue()); if (!state.panelCourses.length) { box.innerHTML = "
\u6536\u85cf\u5939\u6682\u65e0\u8bfe\u7a0b
\u8bf7\u5148\u5728\u7f51\u7ad9\u6536\u85cf\u8981\u5b66\u4e60\u7684\u8bfe\u7a0b
"; return; } box.innerHTML = state.panelCourses .map((c) => { const total = Math.max(1, Number(c.chapterTotal) || 1); const studyDone = Number(c.studyDone) || 0; const examDone = Number(c.examDone) || 0; const studyPct = Math.round((studyDone / total) * 100); const examPct = Math.round((examDone / total) * 100); const isActive = c.csId === activeCsId; const finished = isCourseFullyFinished(c); const checked = selectedSet.has(c.csId) ? "checked" : ""; const border = isActive ? "#16a34a" : finished ? "#e2e8f0" : "#e2e8f0"; const bgColor = isActive ? "#f0fdf4" : finished ? "#f1f5f9" : "#f8fafc"; const titleColor = finished ? "#64748b" : "#0f172a"; return ` `; }) .join(""); box.querySelectorAll("input[type='checkbox']").forEach((el) => { el.addEventListener("change", () => { const selected = []; box.querySelectorAll("label").forEach((label) => { const cb = label.querySelector("input[type='checkbox']"); if (cb?.checked && cb.dataset.csid) selected.push(cb.dataset.csid); }); localStorage.setItem(QUEUE_TOUCHED_KEY, "1"); saveQueue(selected); if (selected.length) clearPanelHint(); void refreshChapterPreview(); const bg = loadBgState(); if (bg.csId && selected.length && !selected.includes(bg.csId)) { saveBgState({}); log("\u5f53\u524d\u8fdb\u884c\u4e2d\u7684\u8bfe\u7a0b\u5df2\u53d6\u6d88\u52fe\u9009\uff0c\u5df2\u91cd\u7f6e\u540e\u53f0\u4efb\u52a1"); } }); }); } async function resolveQueueCourse(csId) { let course = state.panelCourses.find((c) => c.csId === csId); if (course) return course; try { course = await fetchCourseFromPlans(csId); } catch (_) {} return course || null; } async function refreshChapterPreview() { const selected = loadQueue(); if (!selected.length) { state.chapterPreview = []; state.queueVideoTotal = 0; state.queueVideoDone = 0; state.queueExamTotal = 0; state.queueExamDone = 0; renderChapterPreview(); updateQueueSummary(); return; } const list = []; let examTotal = 0; let examDone = 0; for (const csId of selected) { const course = await resolveQueueCourse(csId); if (!course) continue; try { const videos = await loadCourseVideos(csId); examTotal += videos.length; examDone += videos.filter((v) => v.chapterStatus === "done").length; list.push({ csId, courseTitle: course.csName || "\u672a\u547d\u540d\u8bfe\u7a0b", videos: videos.map((v) => ({ resId: v.resId, title: v.resName || v.resId, process: Number(v.process) || 0, studyFinished: v.studyFinished, chapterStatus: v.chapterStatus || "study", active: false, })), }); } catch (_) {} } const bg = loadBgState(); const activeResId = bg.csId && bg.videos?.length ? String(bg.videos[bg.videoIndex || 0]?.resId || "") : ""; list.forEach((c) => { if (c.csId === bg.csId) { c.videos.forEach((v) => { if (String(v.resId) === activeResId) v.active = true; }); } }); state.chapterPreview = list; state.queueVideoTotal = list.reduce((s, c) => s + (c.videos?.length || 0), 0); state.queueVideoDone = list.reduce((s, c) => { const arr = c.videos || []; return s + arr.filter((v) => v.studyFinished === "1" || Number(v.process) >= 100).length; }, 0); state.queueExamTotal = examTotal; state.queueExamDone = examDone; renderChapterPreview(); updateQueueSummary(); } function renderChapterPreview() { const box = document.getElementById("cme-chapter-preview"); if (!box) return; if (!state.chapterPreview.length) { box.innerHTML = "
\u8bf7\u9009\u62e9\u8bfe\u7a0b
"; return; } box.innerHTML = state.chapterPreview .map((course) => { const items = (course.videos || []) .map((v) => { const done = v.studyFinished === "1" || Number(v.process) >= 100; const color = v.active ? "#16a34a" : done ? "#64748b" : "#334155"; const mark = v.active ? "▶" : done ? "✓" : "•"; const pct = Math.round(Number(v.process) || 0); const videoTag = done ? "\u89c6\u9891\u5df2\u5b66\u5b8c" : `\u89c6\u9891 ${pct}%`; const examTag = formatChapterExamTag(v.chapterStatus || (done ? "exam" : "study")); return `
${mark} ${escHtml(v.title)}${escHtml(videoTag)} · ${escHtml(examTag)}
`; }) .join(""); return `
${escHtml(course.courseTitle)}
${items || "
\u672a\u89e3\u6790\u5230\u8bfe\u4ef6\uff0c\u8bf7\u70b9\u5237\u65b0\u6216\u6253\u5f00\u8be5\u8bfe\u7a0b\u9875\u540e\u518d\u8bd5
"}
`; }) .join(""); } function updateQueueSummary() { const doneEl = document.getElementById("cme-queue-done"); const totalEl = document.getElementById("cme-queue-total"); const pctEl = document.getElementById("cme-queue-percent"); const barEl = document.getElementById("cme-queue-progress"); const examEl = document.getElementById("cme-queue-exam"); const textEl = document.getElementById("cme-queue-text"); const q = loadQueue(); const total = Math.max(0, Number(state.queueVideoTotal || 0)); const done = Math.max(0, Math.min(total, Number(state.queueVideoDone || 0))); const examTotal = Math.max(0, Number(state.queueExamTotal || 0)); const examDone = Math.max(0, Math.min(examTotal, Number(state.queueExamDone || 0))); if (doneEl) doneEl.textContent = String(done); if (totalEl) totalEl.textContent = String(total); const pct = total > 0 ? Math.max(0, Math.min(100, Math.round((done / total) * 100))) : 0; if (pctEl) pctEl.textContent = `${pct}%`; if (barEl) barEl.style.width = `${pct}%`; if (examEl) { examEl.textContent = examTotal > 0 ? `${examDone} / ${examTotal}` : "—"; } if (textEl) { if (!q.length) { textEl.textContent = "\u672a\u9009\u62e9"; } else if (total > 0) { const examPart = examTotal > 0 ? ` · \u8003\u8bd5 ${examDone}/${examTotal}` : ""; textEl.textContent = `\u89c6\u9891 ${done}/${total}${examPart}\uff08\u5df2\u9009 ${q.length} \u95e8\uff09`; } else { textEl.textContent = `\u5df2\u9009 ${q.length} \u95e8\uff0c\u6b63\u5728\u7edf\u8ba1…`; } } } function formatCurrentChapterLabel(bg) { if (!bg?.videos?.length) return "\u65e0"; const cur = bg.videos[bg.videoIndex || 0]; if (!cur) return "\u65e0"; const no = formatWareNo(cur.resId || ""); const name = String(cur.resName || "").trim(); return name ? `\u7b2c${no}\u8282 ${name}` : `\u7b2c${no}\u8282`; } function deriveCurrentTaskText(bg) { if (!state.enabled) return state.panelHint || "\u70b9\u300c\u5f00\u59cb\u300d\u540e\u81ea\u52a8\u5b66\u4e60"; if (bg?.csId && bg.videos?.length) { const cur = bg.videos[bg.videoIndex || 0]; const label = String(cur?.resName || "").trim(); const no = formatWareNo(cur?.resId); if (bg.phase === "exam") return `\u6b63\u5728\u8003\u8bd5\uff1a\u7b2c${no}\u8282 ${label}`.trim(); return `\u6b63\u5728\u89c2\u770b\uff1a\u7b2c${no}\u8282 ${label}`.trim(); } if (state.lastUserAction) return state.lastUserAction; return "\u6b63\u5728\u5206\u914d\u4efb\u52a1…"; } function updateCurrentMetaPanel() { const courseEl = document.getElementById("cme-current-course"); const chapterEl = document.getElementById("cme-current-chapter"); const taskEl = document.getElementById("cme-current-task"); const bg = loadBgState(); let courseText = "\u65e0"; let chapterText = "\u65e0"; const taskText = deriveCurrentTaskText(bg); if (bg?.csId) { courseText = bg.csName || state.panelCourses.find((c) => c.csId === bg.csId)?.csName || bg.csId; if (bg.videos?.length) chapterText = formatCurrentChapterLabel(bg); } else if (state.enabled) { const q = loadQueue(); const picked = q[0] && state.panelCourses.find((c) => c.csId === q[0]); if (picked) courseText = picked.csName || picked.csId; } if (courseEl) { courseEl.textContent = courseText; courseEl.title = courseText; } if (chapterEl) { chapterEl.textContent = chapterText; chapterEl.title = chapterText; } if (taskEl) { taskEl.textContent = taskText; taskEl.title = taskText; } } function readPanelCollapsed() { const v = localStorage.getItem(PANEL_COLLAPSED_KEY); return v == null ? true : v === "1"; } function writePanelCollapsed(collapsed) { localStorage.setItem(PANEL_COLLAPSED_KEY, collapsed ? "1" : "0"); } function readPanelPos() { try { return JSON.parse(localStorage.getItem(PANEL_POS_KEY) || "{}"); } catch (_) { return null; } } function writePanelPos(left, top) { localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left, top })); } function clampPanelInViewport(panel) { if (!panel) return; const rect = panel.getBoundingClientRect(); const maxLeft = Math.max(0, window.innerWidth - panel.offsetWidth); const maxTop = Math.max(0, window.innerHeight - panel.offsetHeight); const left = Math.max(0, Math.min(maxLeft, rect.left)); const top = Math.max(0, Math.min(maxTop, rect.top)); panel.style.right = "auto"; panel.style.bottom = "auto"; panel.style.left = `${left}px`; panel.style.top = `${top}px`; writePanelPos(left, top); } function applyPanelCollapsed(panel, collapsed) { const btnMin = panel.querySelector("#cme-btn-min"); const btnMax = panel.querySelector("#cme-btn-max"); panel.classList.toggle("cme-panel-min", !!collapsed); panel.classList.toggle("cme-panel-max", !collapsed); if (btnMin) btnMin.style.display = collapsed ? "none" : ""; if (btnMax) btnMax.style.display = collapsed ? "" : "none"; const footerExtra = panel.querySelector(".cme-footer-extra"); if (footerExtra) footerExtra.style.display = collapsed ? "none" : ""; clampPanelInViewport(panel); } function enablePanelDrag(panel) { const header = panel.querySelector("#cme-panel-header"); if (!header) return; let dragging = false; let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; header.addEventListener("mousedown", (e) => { if (e.target?.closest("#cme-panel-controls, .cme-panel-ctl")) return; dragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; panel.style.right = "auto"; panel.style.bottom = "auto"; e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (!dragging) return; const left = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, startLeft + (e.clientX - startX))); const top = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, startTop + (e.clientY - startY))); panel.style.left = `${left}px`; panel.style.top = `${top}px`; }); document.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; const rect = panel.getBoundingClientRect(); writePanelPos(rect.left, rect.top); }); } function injectPanelStyles() { const id = "cme-panel-style-v4"; document.getElementById("cme-panel-style-v2")?.remove(); document.getElementById("cme-panel-style-v3")?.remove(); if (document.getElementById(id) || !document.head) return; const st = document.createElement("style"); st.id = id; st.textContent = ` #cme-auto-panel{position:fixed;right:20px;top:80px;z-index:999999;width:412px;display:flex;flex-direction:column;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#0f172a;overflow:hidden;transition:max-height .22s ease,box-shadow .22s ease;} .cme-panel-toast{display:none;position:absolute;left:50%;top:10px;transform:translateX(-50%);z-index:1000002;padding:8px 14px;border-radius:10px;font-size:12px;font-weight:700;box-shadow:0 6px 20px rgba(15,23,42,.18);max-width:90%;text-align:center;pointer-events:none;line-height:1.4;} .cme-panel-toast-ok{background:#ecfdf5;color:#047857;border:1px solid #6ee7b7;} .cme-panel-toast-err{background:#fef2f2;color:#b91c1c;border:1px solid #fca5a5;} .cme-panel-toast-info{background:#eff6ff;color:#1d4ed8;border:1px solid #93c5fd;} #cme-auto-panel.cme-panel-hidden{display:none !important;} #cme-auto-panel.cme-panel-max{max-height:min(92vh,780px);} #cme-auto-panel.cme-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);} #cme-auto-panel.cme-panel-min #cme-panel-header{border-bottom:none;} #cme-auto-panel.cme-panel-min #cme-panel-body,#cme-auto-panel.cme-panel-min #cme-panel-footer,#cme-auto-panel.cme-panel-min .cme-footer-extra{display:none !important;} #cme-panel-header{flex:0 0 auto;} #cme-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;} .cme-footer-extra{flex:0 0 auto;} #cme-panel-footer{flex:0 0 auto;} #cme-panel-header{padding:8px 11px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;} #cme-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #cme-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:cover;box-shadow:0 2px 9px rgba(15,23,42,.11);border:1px solid rgba(148,163,184,.45);background:#fff;flex:0 0 auto;} #cme-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;display:flex;align-items:flex-start;gap:5px;flex-wrap:wrap;} .cme-panel-title-text{flex:1 1 12em;min-width:0;letter-spacing:-0.01em;} #cme-panel-sub{display:flex;flex-wrap:wrap;align-items:center;gap:3px 5px;margin-top:3px;line-height:1.32;} .cme-sub-chip{font-size:11px;color:#7c2d12;background:rgba(255,255,255,.62);padding:2px 7px;border-radius:999px;border:1px solid rgba(180,83,9,.18);font-weight:700;} .cme-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);} .cme-sub-dot{color:#d6d3d1;font-size:10px;font-weight:400;} .cme-panel-ctl .cme-ctl-ico{display:block;box-sizing:border-box;margin:0 auto;} .cme-ctl-min{width:10px;height:2px;background:#64748b;border-radius:1px;} .cme-ctl-plus{font-size:16px;line-height:1;font-weight:700;color:#64748b;} .cme-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;} #cme-panel-controls{display:flex;align-items:center;gap:5px;flex:0 0 auto;} .cme-panel-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1);font-size:15px;line-height:1;font-weight:900;padding:0;} .cme-panel-ctl:hover{filter:brightness(1.03);} .cme-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .cme-card-status{padding:6px 8px;margin-bottom:6px;} .cme-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;line-height:1.28;} .cme-status-label{font-size:11px;color:#64748b;font-weight:700;} .cme-status-badge{padding:2px 7px;border-radius:999px;font-weight:800;font-size:11px;background:#fff;border:1px solid #cbd5e1;color:#334155;} .cme-status-main{margin-top:3px;} .cme-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;} .cme-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .cme-metric-div{color:#cbd5e1;} .cme-progress-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;font-size:11px;color:#334155;} .cme-progress-num{font-size:22px;font-weight:900;color:#0f172a;} .cme-progress-num small{font-size:13px;color:#64748b;} .cme-card-status .cme-progress-pct{font-size:14px;font-weight:900;color:#0369a1;flex:0 0 auto;} .cme-progress-pct{font-size:20px;font-weight:900;color:#0369a1;} .cme-card-status .cme-progress-bar{height:5px;margin-top:4px;margin-bottom:0;} .cme-progress-bar{height:9px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:5px;} .cme-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;} .cme-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;} .cme-list-head-actions{display:flex;align-items:center;gap:5px;} .cme-log-clear-btn{border:1px solid #cbd5e1;background:#fff;color:#64748b;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;cursor:pointer;line-height:1.4;} .cme-log-clear-btn:hover{background:#f8fafc;color:#0f172a;} .cme-plan-row{padding:0 2px 5px;} .cme-plan-row .cme-plan-select{padding:4px 8px;font-size:11px;line-height:1.25;border-radius:7px;} .cme-plan-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 9px;font-size:12px;background:#fff;color:#0f172a;} .cme-list-title{font-size:12px;color:#64748b;font-weight:700;} .cme-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;} .cme-settings-group{background:#f8fafc;border:1px solid #dbe4f0;border-radius:12px;padding:8px;margin-bottom:8px;} .cme-settings-title{font-size:12px;color:#64748b;font-weight:900;margin-bottom:7px;} .cme-hidden{display:none !important;} .cme-btn-row-nowrap{flex-wrap:nowrap;} .cme-btn-action{min-width:0 !important;flex:1 1 0 !important;} .cme-btn-ico{font-size:13px;line-height:1;display:inline-flex;align-items:center;margin-right:5px;} .cme-btn-label{white-space:nowrap;} .cme-toggle-on{background:linear-gradient(135deg,#1d4ed8,#0ea5e9) !important;border-color:transparent !important;color:#fff !important;} .cme-card-actions .cme-btn-row{margin-bottom:0;} .cme-card-actions{padding:6px 8px;} .cme-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;} #cme-course-list,#cme-chapter-preview,#cme-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} #cme-settings-pane{padding:2px 0;} .cme-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;} .cme-chapter-title{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;} .cme-chapter-item{padding:5px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;} .cme-dot{font-weight:700;} .cme-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;opacity:.95;} .cme-ann{position:relative;font-size:12px;color:#334155;line-height:1.42;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:11px;padding:8px 9px 8px 11px;margin-bottom:0;} .cme-ann-title{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:900;color:#9a3412;margin-bottom:3px;} .cme-ann-text{display:block;word-break:break-word;} .cme-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-top-left-radius:11px;border-bottom-left-radius:11px;} .cme-qq-row{display:flex;align-items:center;justify-content:space-between;gap:9px;margin-top:5px;} .cme-qq-text{font-size:12px;color:#475569;} .cme-qq-title{font-size:13px;font-weight:800;color:#0f172a;} .cme-qq-btn{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:5px 12px;border-radius:999px;font-size:12px;font-weight:800;cursor:pointer;} .cme-exam-hint{font-size:11px;color:#64748b;margin-top:5px;line-height:1.42;} .cme-start-hint{font-size:11px;color:#b45309;background:#fffbeb;border:1px solid #fcd34d;border-radius:7px;padding:5px 7px;margin-bottom:5px;line-height:1.32;text-align:center;font-weight:600;} .cme-btn-pro{flex:0 0 auto;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;border:none;box-shadow:0 3px 10px rgba(239,68,68,.22);padding:4px 8px;font-size:12px;border-radius:10px;cursor:pointer;font-weight:800;} .cme-btn-pro:hover{filter:brightness(1.06);} #cme-pro-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000001;display:none;align-items:center;justify-content:center;padding:20px;} .cme-pro-card{width:min(560px,92vw);max-height:88vh;overflow:auto;background:#fff;border-radius:18px;border:1px solid #dbe4f0;box-shadow:0 18px 46px rgba(15,23,42,.25);padding:16px;} .cme-pro-title{font-size:24px;font-weight:900;color:#0f172a;} .cme-pro-sub{font-size:13px;color:#64748b;margin-top:4px;line-height:1.45;} .cme-pro-sec{margin-top:12px;border:1px solid #dbe4f0;border-radius:12px;padding:12px;background:#f8fafc;} .cme-pro-sec h4{margin:0 0 8px;font-size:15px;color:#0f172a;} .cme-pro-sec p{margin:4px 0;font-size:13px;color:#334155;line-height:1.5;} .cme-pro-sec ul{margin:6px 0 0 18px;padding:0;} .cme-pro-sec li{margin:4px 0;font-size:13px;color:#334155;line-height:1.45;} .cme-pro-buy{display:flex;flex-direction:column;gap:10px;margin-top:8px;} .cme-pro-muted{color:#64748b;font-size:12px;line-height:1.5;} .cme-pro-buy-btn{display:inline-block;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 16px;border-radius:12px;font-size:14px;font-weight:800;border:none;cursor:pointer;align-self:flex-start;} .cme-pro-buy-btn:hover{filter:brightness(1.05);} .cme-pro-buy-btn:disabled{background:#94a3b8;cursor:not-allowed;box-shadow:none;opacity:.92;} .cme-pro-buy-btn:disabled:hover{filter:none;} .cme-pro-tip{margin-top:8px;background:#fef3c7;border:1px solid #fcd34d;border-radius:10px;padding:8px 10px;font-size:13px;color:#92400e;font-weight:700;line-height:1.45;} .cme-pro-tip-info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:600;} .cme-pro-actions{margin-top:14px;display:flex;justify-content:flex-end;} .cme-pro-close{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 18px;border-radius:12px;font-size:14px;font-weight:800;cursor:pointer;} .cme-tabbar{display:flex;gap:6px;margin-bottom:7px;} .cme-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px 4px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;} .cme-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .cme-pane{display:none;}.cme-pane.active{display:block;} .cme-btn-row{display:flex;gap:7px;margin-bottom:6px;flex-wrap:wrap;} .cme-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;min-width:68px;} .cme-btn-start{background:#16a34a;}.cme-btn-stop{background:#ef4444;}.cme-btn-refresh{background:#64748b;} .cme-btn-start.cme-btn-off,.cme-btn-start:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;box-shadow:none;pointer-events:none;} .cme-btn-stop.cme-btn-off,.cme-btn-stop:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;box-shadow:none;pointer-events:none;} .cme-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;flex:0 0 auto;} .cme-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;} .cme-meta-label{color:#64748b;}.cme-meta-value{font-weight:700;text-align:right;} .cme-card-current{padding:5px 8px;margin-bottom:6px;} .cme-card-current .cme-meta-row{font-size:11px;margin-bottom:2px;gap:5px;line-height:1.28;align-items:flex-start;} .cme-card-current .cme-meta-row:last-child{margin-bottom:0;} .cme-card-current .cme-meta-label{flex:0 0 auto;font-size:10px;white-space:nowrap;} .cme-card-current .cme-meta-value{flex:1 1 auto;min-width:0;font-size:11px;font-weight:600;text-align:right;word-break:break-all;line-height:1.28;} .cme-auth-badge{padding:2px 7px;border-radius:999px;border:1px solid #cbd5e1;font-size:11px;font-weight:900;} #cme-auto-status{padding:3px 9px;border-radius:999px;font-weight:900;font-size:12px;background:#fff;border:1px solid #cbd5e1;} .cme-card-status #cme-auto-status{padding:2px 7px;font-size:11px;} #cme-panel-footer{padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;} .cme-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;} `; document.head.appendChild(st); } function createPanel() { const old = document.getElementById("cme-auto-panel") || document.getElementById("cme-auto-study-panel"); if (old) old.remove(); injectPanelStyles(); const panel = document.createElement("div"); panel.id = "cme-auto-panel"; panel.innerHTML = `
\u597d\u533b\u751f\u7ee7\u7eed\u533b\u5b66\u6559\u80b2\u81ea\u52a8\u5b66\u4e60\u52a9\u624bv${SCRIPT_VERSION}
\u4e00\u952e\u5b8c\u6210\u89c6\u9891\u5b66\u65f6·\u5b66\u5b8c\u81ea\u52a8\u8003\u8bd5·\u7701\u65f6\u7701\u5fc3
\u8fd0\u884c\u72b6\u6001 \u5df2\u505c\u6b62
\u89c6\u9891 0/0 · \u8003\u8bd5
0%
\u8bfe\u7a0b\u9009\u62e9\u89c6\u9891·\u8003\u8bd5
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
\u7ae0\u8282\u9884\u89c8\u89c6\u9891·\u8003\u8bd5
\u8bf7\u9009\u62e9\u8bfe\u7a0b
\u8fd0\u884c\u65e5\u5fd7
\u8fdb\u5ea6
\u6388\u6743\u4e0e\u9009\u9879\u8bbe\u7f6e
\u7528\u6237\u7c7b\u578b\u672a\u6821\u9a8c
\u514d\u8d39\u4f53\u9a8c\uff081\u4e2a\u89c6\u9891\uff090/1
Token
\u4e91\u7aef\u670d\u52a1${DEFAULT_CLOUD_API_BASE}
\u8fd0\u884c\u6a21\u5f0f
\u4e0a\u62a5\u95f4\u9694 \u79d2
\u5b66\u65f6\u500d\u7387 \u500d
\u5b66\u5b8c\u540e\u81ea\u52a8\u8003\u8bd5\uff08\u5df2\u9ed8\u8ba4\u5f00\u542f\uff09
\u5f53\u524d\u8bfe\u7a0b\u65e0
\u5f53\u524d\u7ae0\u8282\u65e0
\u5f53\u524d\u4efb\u52a1\u70b9\u300c\u5f00\u59cb\u300d\u540e\u81ea\u52a8\u5b66\u4e60
`; document.body.appendChild(panel); createProModal(); const savedPos = readPanelPos(); if (savedPos && savedPos.left != null && savedPos.top != null) { panel.style.right = "auto"; panel.style.left = `${savedPos.left}px`; panel.style.top = `${savedPos.top}px`; } applyPanelCollapsed(panel, readPanelCollapsed()); enablePanelDrag(panel); const btnMin = document.getElementById("cme-btn-min"); const btnMax = document.getElementById("cme-btn-max"); if (btnMin) { btnMin.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); } if (btnMax) { btnMax.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); } panel.querySelectorAll(".cme-tab-btn").forEach((btn) => { btn.addEventListener("click", () => switchPanelTab(btn.dataset.tab)); }); document.getElementById("cme-clear-log")?.addEventListener("click", () => { clearRunLog(); }); const planSel = document.getElementById("cme-plan-select"); if (planSel) { planSel.addEventListener("change", async () => { saveSelectedTpId(planSel.value); state.panelRefreshing = true; updatePanel(); try { await fetchPanelCourses(); renderPlanSelect(); renderCourseList(); await refreshChapterPreview(); } catch (err) { log("\u5207\u6362\u8bfe\u7a0b\u6765\u6e90\u5931\u8d25\uff1a" + (err.message || err)); } finally { state.panelRefreshing = false; updatePanel(); } }); } document.getElementById("cme-start").addEventListener("click", async () => { if (state.enabled) return; ensureEfficiencyMode(); applyLockedPanelDefaults(); if (!(await _cr())) { updatePanel(); return; } const q = loadQueue(); if (!q.length) { log("\u8bf7\u5148\u52fe\u9009\u81f3\u5c11\u4e00\u95e8\u8bfe\u7a0b"); setPanelHint("\u8bf7\u5148\u52fe\u9009\u81f3\u5c11\u4e00\u95e8\u8bfe\u7a0b\u518d\u70b9\u5f00\u59cb"); updatePanel(); return; } clearPanelHint(); const bg = loadBgState(); if (bg.csId && !q.includes(bg.csId)) saveBgState({}); else if (bg?.csId) { bg.lastSaveAt = 0; saveBgState(bg); } state.enabled = true; localStorage.setItem(STORAGE_KEY, "1"); state.lastTick = 0; syncStartStopButtons(); syncKeepAlive(); log("\u81ea\u52a8\u770b\u8bfe\u5df2\u5f00\u542f"); startPreviewAutoRefresh(); updatePanel(); tick(); }); document.getElementById("cme-stop").addEventListener("click", () => { if (!state.enabled) return; state.enabled = false; localStorage.setItem(STORAGE_KEY, "0"); clearStudyModeSelection(); syncStartStopButtons(); syncKeepAlive(); stopPreviewAutoRefresh(); log("\u81ea\u52a8\u770b\u8bfe\u5df2\u6682\u505c"); updatePanel(); }); document.getElementById("cme-join-qq").addEventListener("click", () => { try { GM_openInTab(QQ_GROUP_LINK, { active: true, insert: true, setParent: true }); } catch (_) { window.open(QQ_GROUP_LINK, "_blank"); } }); document.getElementById("cme-open-pro")?.addEventListener("click", () => { openProModal(); }); document.getElementById("cme-cloud-save").addEventListener("click", async () => { const input = document.getElementById("cme-cloud-token"); const btn = document.getElementById("cme-cloud-save"); state.cloudToken = String((input && input.value) || "").trim(); state.cloudRevoked = false; localStorage.setItem(CLOUD_TOKEN_KEY, state.cloudToken); writeCloudLeaseCache("", 0); state.cloudLease = ""; state.cloudLeaseExp = 0; if (btn) { btn.disabled = true; btn.textContent = "\u6821\u9a8c\u4e2d…"; } try { if (!state.cloudToken) { await _el(true); showPanelToast("Token \u5df2\u6e05\u7a7a\uff0c\u5f53\u524d\u4e3a\u514d\u8d39\u4f53\u9a8c", "info"); } else { await _vt(); await _el(true); const tier = String(state.cloudTier || "").toLowerCase(); if (tier === "pro") { showPanelToast("Pro Token \u6821\u9a8c\u6210\u529f", "ok"); } else { showPanelToast("\u4e91\u7aef\u6388\u6743\u5df2\u66f4\u65b0", "ok"); } } } catch (err) { showPanelToast(formatCloudTokenError(err), "err"); } finally { if (btn) { btn.disabled = false; btn.textContent = "\u4fdd\u5b58\u5e76\u6821\u9a8c"; } } updateCloudPanelUI(); }); applyLockedPanelDefaults(); setCloudApiBase(); state.cloudToken = String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(); const tokenInput = document.getElementById("cme-cloud-token"); if (tokenInput) tokenInput.value = state.cloudToken; updateCloudPanelUI(); updatePanel(); renderCourseList(); updateQueueSummary(); void _cf(); void _pn(); void _fip(false).then(() => { updatePanel(); void refreshPanelCourses(); }); } function shouldHidePanelOnPage() { return pageType() === "courseplay"; } function syncPanelVisibility() { const hide = shouldHidePanelOnPage(); const panel = document.getElementById("cme-auto-panel"); if (panel) panel.classList.toggle("cme-panel-hidden", hide); if (hide) closeProModal(); } function syncStartStopButtons() { const running = !!state.enabled; const startBtn = document.getElementById("cme-start"); const stopBtn = document.getElementById("cme-stop"); if (startBtn) { startBtn.disabled = running; startBtn.classList.toggle("cme-btn-off", running); startBtn.setAttribute("aria-disabled", running ? "true" : "false"); } if (stopBtn) { stopBtn.disabled = !running; stopBtn.classList.toggle("cme-btn-off", !running); stopBtn.setAttribute("aria-disabled", !running ? "true" : "false"); } } function updatePanel() { syncPanelVisibility(); syncProBuyButtonUi(); syncStartStopButtons(); const statusEl = document.getElementById("cme-auto-status"); if (!statusEl) return; if (shouldHidePanelOnPage()) return; if (state.panelRefreshing) { statusEl.textContent = "\u5237\u65b0\u4e2d…"; } else if (!state.enabled) { statusEl.textContent = "\u5df2\u505c\u6b62"; } else { const bg = loadBgState(); if (bg.phase === "exam") statusEl.textContent = "\u8003\u8bd5\u4e2d"; else if (bg.csId && bg.videos?.length) { statusEl.textContent = `\u8fd0\u884c ${(bg.videoIndex || 0) + 1}/${bg.videos.length}`; } else statusEl.textContent = "\u8fd0\u884c\u4e2d"; } updateCloudPanelUI(); updateCurrentMetaPanel(); updateQueueSummary(); const hintEl = document.getElementById("cme-start-hint"); if (hintEl && !state.enabled) { if (state.panelHint) { hintEl.textContent = state.panelHint; hintEl.style.display = "block"; } else { hintEl.textContent = ""; hintEl.style.display = "none"; } } else if (hintEl) { hintEl.textContent = ""; hintEl.style.display = "none"; } } async function initPanelData() { if (!(await ensureCmeSession())) return; ensureEfficiencyMode(); try { await ensurePlatformHeartbeatSec(true); } catch (_) {} await _fip(false); await refreshPanelCourses(); try { await _el(false); } catch (_) {} } function init() { resetSessionOnPageLoad(); if (pageType() === "courseplay") initStudyPageWorker(); if (pageType() === "favorite" || pageType() === "course" || pageType() === "home") createPanel(); syncPanelVisibility(); syncKeepAlive(); initPanelData(); setInterval(tick, TICK_MS); setInterval(updatePanel, 1000); setInterval(() => { if (isLoggedIn()) _el(false).catch(() => {}); }, 10 * 60 * 1000); window.addEventListener("hashchange", () => { state.lastTick = 0; syncPanelVisibility(); }); window.addEventListener("popstate", () => { syncPanelVisibility(); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();