// ==UserScript== // @name 【效率版】河南专技培训平台学习助手(河南继续教育学会/南阳理工学院) // @namespace https://card.wlxy.live/details/C7467074 // @version 1.1 // @description 河南继续教育学会(jxjyedu.org.cn)、南阳理工学院(nypx.jxjyedu.org.cn)专业技术人员继续教育助手:已报名公需课、专业课效率快速完成;免费每站3章节,Pro本站激活绑定不限。 // @antifeature payment 免费体验3个视频章节,升级Pro不限 // @antifeature membership 需云端授权 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @match *://www.jxjyedu.org.cn/* // @match *://jxjyedu.org.cn/* // @match *://nypx.jxjyedu.org.cn/* // @match *://www.hnzjgl.gov.cn/* // @match *://*.hnzjgl.gov.cn/* // @match *://jxjy.henu.edu.cn/* // @match *://www.jxjy.henu.edu.cn/* // @match *://hnpihn.newzhihui.cn/* // @match *://www.hnpihn.newzhihui.cn/* // @match *://zdkj.v.zzu.edu.cn/* // @match *://hnzj.ghlearning.com/* // @match *://hnzj.user.ghlearning.com/* // @match *://zyjs.lypt.edu.cn/* // @match *://www.zyjs.lypt.edu.cn/* // @match *://www.ly.fnhzj.com/* // @match *://ly.fnhzj.com/* // @match *://www.huayuzj.com/* // @match *://huayuzj.com/* // @noframes // @connect www.jxjyedu.org.cn // @connect jxjyedu.org.cn // @connect nypx.jxjyedu.org.cn // @connect oa25.ahzsksw.cn // @connect card.wlxy.live // @connect huaweicloudobs.ahjxjy.cn // @connect cdn2.sxzjpx.cn // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_openInTab // @grant unsafeWindow // @run-at document-idle // @tag 河南继续教育 // @tag 南阳理工学院 // @tag 专技继续教育 // @tag 河南专技 // @license All Rights Reserved // ==/UserScript== (function () { "use strict"; const SCRIPT_VERSION = "1.3.6"; try { if (window.top !== window.self) return; } catch (_) { return; } const SITE_HOST = String(location.hostname || "") .replace(/^www\./i, "") .toLowerCase(); const JXJY_SUPPORTED_HOSTS = new Set(["jxjyedu.org.cn", "nypx.jxjyedu.org.cn"]); const LYLG_SUPPORTED_HOSTS = new Set([ "ly.fnhzj.com", "huayuzj.com", "testplugservice-v3.ylxue.net", "plugservice-v3.ylxue.net", ]); const REDIRECT_ONLY_HOSTS = new Set([ "hnzjgl.gov.cn", "jxjy.henu.edu.cn", "hnpihn.newzhihui.cn", "zdkj.v.zzu.edu.cn", "hnzj.ghlearning.com", "hnzj.user.ghlearning.com", "zyjs.lypt.edu.cn", ]); const HN_REDIRECT_BOOT_KEY = "__HN_ZJ_PORTAL_REDIRECT_V1__"; const HN_SCRIPT_JXJY_URL = "https://scriptcat.org/zh-CN/script-show-page/7693"; const HN_SCRIPT_LYLG_URL = "https://scriptcat.org/zh-CN/script-show-page/7691"; const HN_ADAPTED_PLATFORMS = [ { name: "\u6cb3\u5357\u7701\u7ee7\u7eed\u6559\u80b2\u5b66\u4f1a", url: "https://www.jxjyedu.org.cn/", modeLabel: "\u6548\u7387\u5feb\u901f\u5b8c\u6210", modeClass: "hn-zj-badge-fast", scriptUrl: HN_SCRIPT_JXJY_URL, scriptLabel: "\u5b89\u88c5\u5b66\u4e60\u52a9\u624b\uff08ScriptCat #7693\uff09", }, { name: "\u5357\u9633\u7406\u5de5\u5b66\u9662", url: "https://nypx.jxjyedu.org.cn/", modeLabel: "\u6548\u7387\u5feb\u901f\u5b8c\u6210", modeClass: "hn-zj-badge-fast", scriptUrl: HN_SCRIPT_JXJY_URL, scriptLabel: "\u5b89\u88c5\u5b66\u4e60\u52a9\u624b\uff08ScriptCat #7693\uff09", }, { name: "\u6d1b\u9633\u7406\u5de5\u5b66\u9662", url: "https://www.ly.fnhzj.com/", modeLabel: "1:1 \u591a\u8bfe\u540c\u5237", modeClass: "hn-zj-badge-multi", scriptUrl: HN_SCRIPT_LYLG_URL, scriptLabel: "\u5b89\u88c5\u5b66\u4e60\u52a9\u624b\uff08ScriptCat #7691\uff09", }, { name: "\u4e2d\u539f\u5de5\u5b66\u9662", url: "https://www.huayuzj.com/", modeLabel: "1:1 \u591a\u8bfe\u540c\u5237", modeClass: "hn-zj-badge-multi", scriptUrl: HN_SCRIPT_LYLG_URL, scriptLabel: "\u5b89\u88c5\u5b66\u4e60\u52a9\u624b\uff08ScriptCat #7691\uff09", }, ]; const REDIRECT_SOURCE_LABELS = { "hnzjgl.gov.cn": "\u6cb3\u5357\u4e13\u6280\u7ba1\u7406\u5e73\u53f0", "jxjy.henu.edu.cn": "\u6cb3\u5357\u5927\u5b66", "hnpihn.newzhihui.cn": "\u6cb3\u5357\u5de5\u4e1a\u804c\u4e1a\u6280\u672f\u5927\u5b66", "zdkj.v.zzu.edu.cn": "\u90d1\u5dde\u5927\u5b66", "hnzj.ghlearning.com": "\u6cb3\u5357\u9ad8\u8f89\u6559\u80b2\u79d1\u6280\u6709\u9650\u516c\u53f8", "hnzj.user.ghlearning.com": "\u6cb3\u5357\u9ad8\u8f89\u6559\u80b2\u79d1\u6280\u6709\u9650\u516c\u53f8", "zyjs.lypt.edu.cn": "\u6d1b\u9633\u804c\u4e1a\u6280\u672f\u5b66\u9662", }; function resolveHostRole(host) { if (JXJY_SUPPORTED_HOSTS.has(host)) return "jxjy"; if (LYLG_SUPPORTED_HOSTS.has(host)) return "lylg"; if (REDIRECT_ONLY_HOSTS.has(host)) return "redirect"; return "redirect"; } function injectHnRedirectStyles() { if (document.getElementById("hn-zj-redirect-style")) return; const st = document.createElement("style"); st.id = "hn-zj-redirect-style"; st.textContent = ` #hn-zj-redirect-panel{position:fixed;right:20px;top:80px;z-index:999999;width:390px;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;} #hn-zj-redirect-header{padding:10px 12px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;cursor:move;user-select:none;display:flex;align-items:center;gap:9px;} #hn-zj-redirect-logo{width:28px;height:28px;border-radius:8px;object-fit:cover;background:#fff;border:1px solid rgba(148,163,184,.45);} #hn-zj-redirect-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.3;flex:1;} #hn-zj-redirect-body{padding:10px 12px 12px;} .hn-zj-redirect-lead{font-size:12px;color:#475569;line-height:1.55;margin:0 0 8px;} .hn-zj-redirect-lead strong{color:#9a3412;} .hn-zj-redirect-list{display:flex;flex-direction:column;gap:7px;margin:0;padding:0;list-style:none;} .hn-zj-redirect-item{background:#fff;border:1px solid #dbe4f0;border-radius:11px;padding:8px 9px;} .hn-zj-redirect-item-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;} .hn-zj-redirect-link{font-size:13px;font-weight:800;color:#1d4ed8;text-decoration:none;} .hn-zj-redirect-link:hover{text-decoration:underline;} .hn-zj-redirect-item-foot{margin-top:5px;} .hn-zj-redirect-script{font-size:11px;font-weight:700;color:#7c2d12;text-decoration:none;} .hn-zj-redirect-script:hover{text-decoration:underline;color:#9a3412;} .hn-zj-badge{font-size:10px;font-weight:800;padding:2px 7px;border-radius:999px;white-space:nowrap;flex:0 0 auto;} .hn-zj-badge-fast{color:#0f766e;background:#ecfdf5;border:1px solid #99f6e4;} .hn-zj-badge-multi{color:#1d4ed8;background:#eff6ff;border:1px solid #bfdbfe;} .hn-zj-redirect-hint{margin:9px 0 0;font-size:11px;color:#64748b;line-height:1.45;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:10px;padding:7px 9px;} `; document.head.appendChild(st); } function bindHnRedirectDrag(panel) { const header = panel.querySelector("#hn-zj-redirect-header"); if (!header) return; let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false; header.addEventListener("mousedown", (e) => { dragging = true; sx = e.clientX; sy = e.clientY; const rect = panel.getBoundingClientRect(); ox = rect.left; oy = rect.top; e.preventDefault(); }); window.addEventListener("mousemove", (e) => { if (!dragging) return; panel.style.left = `${ox + e.clientX - sx}px`; panel.style.top = `${oy + e.clientY - sy}px`; panel.style.right = "auto"; }); window.addEventListener("mouseup", () => { dragging = false; }); } function _hnEsc(s) { return String(s || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function renderHnRedirectPlatformItems() { return HN_ADAPTED_PLATFORMS.map( (p) => `
  • ${_hnEsc(p.name)}${_hnEsc(p.modeLabel)}
    ${_hnEsc( p.scriptLabel )} →
  • ` ).join(""); } function runHnRedirectPanelOnly() { try { const root = (typeof unsafeWindow !== "undefined" && unsafeWindow.top) || window.top || window; if (root[HN_REDIRECT_BOOT_KEY]) return; root[HN_REDIRECT_BOOT_KEY] = SCRIPT_VERSION; } catch (_) { if (window[HN_REDIRECT_BOOT_KEY]) return; window[HN_REDIRECT_BOOT_KEY] = SCRIPT_VERSION; } injectHnRedirectStyles(); document.querySelectorAll("#hn-zj-redirect-panel").forEach((el) => el.remove()); const sourceLabel = REDIRECT_SOURCE_LABELS[SITE_HOST] || "\u5f53\u524d\u5e73\u53f0"; const panel = document.createElement("div"); panel.id = "hn-zj-redirect-panel"; panel.innerHTML = `
    \u6cb3\u5357\u4e13\u6280\u7ee7\u7eed\u6559\u80b2 · \u5df2\u9002\u914d\u5e73\u53f0\u6307\u5f15

    \u60a8\u5f53\u524d\u8bbf\u95ee\u7684\u662f ${_hnEsc(sourceLabel)}\uff0c\u6682\u672a\u63a5\u5165\u81ea\u52a8\u5b66\u4e60\u3002

    \u5efa\u8bae\u4f18\u5148\u524d\u5f80\u4ee5\u4e0b 4 \u4e2a\u5df2\u9002\u914d\u5e73\u53f0 \u5b66\u4e60\uff1a

    \u524d\u5f80\u5e73\u53f0\u524d\u8bf7\u5148\u5b89\u88c5\u5bf9\u5e94 ScriptCat \u811a\u672c\u3002\u6cb3\u5357\u7701\u7ee7\u7eed\u6559\u80b2\u5b66\u4f1a\u3001\u5357\u9633\u7406\u5de5\u5b66\u9662\u652f\u6301\u6548\u7387\u5feb\u901f\u5b8c\u6210\uff08#7693\uff09\uff1b\u6d1b\u9633\u7406\u5de5\u5b66\u9662\u3001\u4e2d\u539f\u5de5\u5b66\u9662\u652f\u6301 1:1 \u65f6\u957f\u591a\u8bfe\u540c\u65f6\u5b66\u4e60\uff08#7691\uff09\u3002

    `; document.body.appendChild(panel); bindHnRedirectDrag(panel); } const HOST_ROLE = resolveHostRole(SITE_HOST); if (HOST_ROLE === "lylg") return; if (HOST_ROLE === "redirect") { runHnRedirectPanelOnly(); return; } const BOOT_KEY = "__JXJY_PROGRESS_HELPER_BOOTED__"; try { const root = (typeof unsafeWindow !== "undefined" && unsafeWindow.top) || window.top || window; if (root[BOOT_KEY]) return; root[BOOT_KEY] = SCRIPT_VERSION; } catch (_) { if (window[BOOT_KEY]) return; window[BOOT_KEY] = SCRIPT_VERSION; } const SITE_HOST_NORM = SITE_HOST; const SITE_REGISTRY = { "jxjyedu.org.cn": { id: "jxjy", name: "\u6cb3\u5357\u7ee7\u7eed\u6559\u80b2\u5b66\u4f1a\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2\u52a9\u624b", webSite: "www.jxjyedu.org.cn", }, "nypx.jxjyedu.org.cn": { id: "nypx", name: "\u5357\u9633\u7406\u5de5\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2\u52a9\u624b", webSite: "nypx.jxjyedu.org.cn", }, }; const SITE_PREF_KEY = "jxjyedu_active_site_v1"; function resolveActiveSiteHost() { if (SITE_REGISTRY[SITE_HOST_NORM]) return SITE_HOST_NORM; try { const pref = GM_getValue(SITE_PREF_KEY); if (pref && SITE_REGISTRY[pref]) return pref; } catch (_) {} return "jxjyedu.org.cn"; } const ACTIVE_SITE_HOST = resolveActiveSiteHost(); const ACTIVE_SITE = SITE_REGISTRY[ACTIVE_SITE_HOST] || SITE_REGISTRY["jxjyedu.org.cn"]; const SITE_NAME = ACTIVE_SITE.name; const SITE_ID = ACTIVE_SITE.id; const WEB_SITE = ACTIVE_SITE.webSite; const CLOUD_API_PREFIX = `/api/${SITE_ID}/`; try { if (SITE_REGISTRY[SITE_HOST_NORM]) GM_setValue(SITE_PREF_KEY, SITE_HOST_NORM); } catch (_) {} const PANEL_LOGO_URL = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const PANEL_FEATURE_SLOGAN = "\u8fdb\u5ea6\u540c\u6b65"; const CLOUD_API_BASE = "https://oa25.ahzsksw.cn"; const PRO_BUY_URL = "https://card.wlxy.live/details/C7467074"; const CLOUD_TOKEN_KEY = `${SITE_ID}_cloud_token_v1`; const CLOUD_LEASE_KEY = `${SITE_ID}_cloud_lease_v1`; const PANEL_POS_KEY = `${SITE_ID}_panel_pos_v1`; const PANEL_COLLAPSED_KEY = `${SITE_ID}_panel_collapsed_v1`; const FINISH_TIP = "\u6240\u9009\u8bfe\u7a0b\u5df2\u5168\u90e8\u5b66\u5b8c\u3002\u82e5\u5e73\u53f0\u8fdb\u5ea6\u7a0d\u540e\u66f4\u65b0\u5c5e\u6b63\u5e38\u73b0\u8c61\uff0c\u53ef\u5237\u65b0\u9875\u9762\u67e5\u770b\u3002"; const PANEL_NOTICE = `\u514d\u8d39\u4f53\u9a8c 3 \u4e2a\u89c6\u9891\u7ae0\u8282\uff08${SITE_NAME} \u72ec\u7acb\u8ba1\u6570\uff09\uff1bPro \u672c\u7ad9\u6fc0\u6d3b\u540e\u7ed1\u5b9a\u672c\u7ad9\u3002\u8d2d\u4e70\uff1a` + PRO_BUY_URL; const QQ_GROUP_URL = ""; const QQ_GROUP_LABEL = "\u4ea4\u6d41\u7fa4"; const LOG_PALETTE = [ "#0369a1", "#0f766e", "#b45309", "#a21caf", "#be123c", "#1d4ed8", "#047857", "#c2410c", "#7c3aed", "#0e7490", "#4f46e5", "#ca8a04", ]; const LOG_LIMIT = 400; const COMPLETE_THRESHOLD = 95; const REPORT_INTERVAL = 30; const TURBO_MODE = true; const TURBO_STEP_SEC = 300; const TURBO_DELAY_MS = 600; const API_BASE = "/wx/mp"; const state = { student: null, years: [], selectedYear: null, courses: [], selectedCourseKeys: new Set(), chapterPreview: [], logLines: [], loading: false, running: false, stopFlag: false, currentTask: "", cloudToken: "", cloudLease: "", cloudLeaseExp: 0, cloudTier: "free", cloudProExpireAt: 0, tokenBoundBlocked: false, freeChapterLimit: 3, freeUsedChapters: 0, proBuyUrl: PRO_BUY_URL, finishTip: FINISH_TIP, panelNotice: PANEL_NOTICE, }; function courseKey(year, cid) { return `${Number(year)}:${Number(cid)}`; } function parseCourseKey(key) { const parts = String(key || "").split(":"); return { year: Number(parts[0]) || 0, cid: Number(parts[1]) || 0 }; } function findCourse(year, cid) { return state.courses.find( (c) => Number(c.id) === Number(cid) && Number(c.year) === Number(year) ); } function syncSelectedCourseKeys() { const year = state.selectedYear; if (!year) { state.selectedCourseKeys = new Set(); return; } const valid = new Set( state.courses .filter((c) => Number(c.year) === Number(year)) .map((c) => courseKey(c.year, c.id)) ); state.selectedCourseKeys = new Set( [...state.selectedCourseKeys].filter((k) => valid.has(k)) ); if (state.selectedCourseKeys.size) return; const incomplete = state.courses .filter( (c) => Number(c.year) === Number(year) && (Number(c.progress) || 0) < COMPLETE_THRESHOLD ) .map((c) => courseKey(c.year, c.id)); if (incomplete.length) { state.selectedCourseKeys = new Set(incomplete); } } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function openUrl(url) { const u = String(url || "").trim(); if (!u) return; try { if (typeof GM_openInTab === "function") GM_openInTab(u, { active: true }); else window.open(u, "_blank"); } catch (_) { try { window.open(u, "_blank"); } catch (e) {} } } function escHtml(s) { return String(s ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function truncate(s, n) { s = String(s || ""); return s.length > n ? s.slice(0, n) + "…" : s; } function formatTime(sec) { sec = Math.max(0, Math.floor(Number(sec) || 0)); const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = sec % 60; if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; } function toFormBody(params) { const parts = []; Object.keys(params || {}).forEach((k) => { const v = params[k]; if (v === undefined || v === null) return; parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`); }); return parts.join("&"); } function courseListProgress(c) { const cc = Number(c.chapter_count) || 0; const done = Number(c.completed_count) || 0; if (cc > 0) return Math.round((done / cc) * 100); return Math.min(100, Math.max(0, Number(c.progress) || 0)); } function computeChapterBlockProgress(chapters) { if (!chapters || !chapters.length) return 0; const avg = chapters.reduce((s, ch) => s + (Number(ch.progress) || 0), 0) / chapters.length; const doneCount = chapters.filter( (ch) => (Number(ch.progress) || 0) >= COMPLETE_THRESHOLD ).length; const byDone = Math.round((doneCount / chapters.length) * 100); return Math.round(Math.max(avg, byDone)); } function log(_msg) {} function logColorForKey(key) { const s = String(key || ""); let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return LOG_PALETTE[h % LOG_PALETTE.length]; } function inferLogKind(msg) { if (/\u5b8c\u6210$|\u5904\u7406\u5b8c\u6bd5|\u5168\u90e8/.test(msg)) return "done"; if (/\u6ca1\u6709|\u8bf7\u5148|\u4e2d\u65ad|\u5931\u8d25|\u9519\u8bef|\u672a\u5b8c\u6210|\u767b\u5f55/.test(msg)) return "info"; return "progress"; } function renderLogRow(entry) { if (typeof entry === "string") { return `
    ${escHtml(entry)}
    `; } const kind = entry.kind || "progress"; const color = kind === "info" ? "#64748b" : logColorForKey(entry.key || entry.msg); let body = escHtml(entry.msg); if (kind === "progress") { body = body .replace(/(\u5df2\u5b66[\d.]+%)/g, '$1') .replace(/(\d+\u5206\u949f\/\u603b\d+\u5206)/g, '$1'); } else if (kind === "done") { body = body.replace(/(\u5b8c\u6210)/g, '$1'); } return `
    [${escHtml(entry.time)}] ${body}
    `; } function _plog(msg, colorKey) { const entry = { time: new Date().toLocaleTimeString(), msg: String(msg), key: colorKey || String(msg).slice(0, 24), kind: inferLogKind(msg), }; state.logLines.unshift(entry); if (state.logLines.length > LOG_LIMIT) state.logLines.length = LOG_LIMIT; const box = document.getElementById("jxjy-run-log"); if (box) { box.innerHTML = state.logLines.map(renderLogRow).join(""); box.scrollTop = 0; } } function setCurrentTask(text) { state.currentTask = String(text || ""); const el = document.getElementById("jxjy-current-task"); if (el) el.textContent = state.currentTask || "\u5f85\u547d"; } function syncRunButtons() { const start = document.getElementById("jxjy-start"); const stop = document.getElementById("jxjy-stop"); if (start) { start.disabled = state.running; start.classList.toggle("jxjy-btn-off", state.running); } if (stop) { stop.disabled = !state.running; stop.classList.toggle("jxjy-btn-off", !state.running); } } function showLogTab() { const panel = document.getElementById("jxjy-auto-panel"); if (!panel) return; panel.querySelectorAll(".jxjy-tab-btn").forEach((b) => b.classList.remove("active")); panel.querySelectorAll(".jxjy-pane").forEach((p) => p.classList.remove("active")); panel.querySelector('.jxjy-tab-btn[data-tab="log"]')?.classList.add("active"); panel.querySelector('.jxjy-pane[data-pane="log"]')?.classList.add("active"); } async function apiGet(path, params) { const qs = params && Object.keys(params).length ? "?" + toFormBody(params) : ""; const url = path.startsWith("http") ? path + qs : API_BASE + path + qs; const res = await fetch(url, { method: "GET", credentials: "include", headers: { Accept: "application/json, text/plain, */*" }, }); const text = await res.text(); let data = {}; try { data = text ? JSON.parse(text) : {}; } catch (_) { throw new Error("\u54cd\u5e94\u975e JSON"); } if (Number(data.code) === -1) { throw new Error("\u767b\u5f55\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55"); } return data; } async function apiPostForm(path, params) { const url = path.startsWith("http") ? path : API_BASE + path; const res = await fetch(url, { method: "POST", credentials: "include", headers: { Accept: "application/json, text/plain, */*", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", }, body: toFormBody(params), }); const text = await res.text(); let data = {}; try { data = text ? JSON.parse(text) : {}; } catch (_) { throw new Error("\u54cd\u5e94\u975e JSON"); } if (Number(data.code) === -1) { throw new Error("\u767b\u5f55\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55"); } return data; } async function fetchStudentInfo() { const data = await apiGet("/StudentInfo"); if (Number(data.code) !== 0 || !data.student) { throw new Error(data.msg || "\u672a\u767b\u5f55\uff0c\u8bf7\u5148\u767b\u5f55\u5e73\u53f0"); } state.student = data.student; updateAuthUi(); return data.student; } async function fetchMyCoursePC(year) { const params = {}; if (year != null && year !== "") params.year = String(year); const data = await apiGet("/MyCoursePC", params); if (Number(data.code) !== 0) { throw new Error(data.msg || "\u52a0\u8f7d\u8bfe\u7a0b\u5931\u8d25"); } const years = Array.isArray(data.years) ? data.years.map((y) => (typeof y === "object" ? Number(y.year || y) : Number(y))).filter(Boolean) : []; const list = Array.isArray(data.userCourseList) ? data.userCourseList : []; return { years, list }; } async function fetchCourseDetail(courseId) { const data = await apiGet("/Course", { id: courseId, code: "" }); if (Number(data.code) !== 0) { throw new Error(data.msg || "\u52a0\u8f7d\u7ae0\u8282\u5931\u8d25"); } return data; } function mapCourseRow(row, year) { const progress = courseListProgress(row); return { id: Number(row.id) || 0, name: String(row.name || "\u672a\u547d\u540d\u8bfe\u7a0b"), credit: Number(row.credit) || 0, type: row.type, card_year: row.card_year, chapter_count: Number(row.chapter_count) || 0, completed_count: Number(row.completed_count) || 0, studylong: Number(row.studylong) || 0, status: row.status, status_study: row.status_study, begin: row.begin, year: Number(year) || Number(row.card_year) || 0, progress, raw: row, }; } function mapChapter(ch, courseId) { const duration = Number(ch.duration) || 0; const position = Number(ch.position) || 0; const progress = Number(ch.progress) || 0; const done = progress >= COMPLETE_THRESHOLD || (duration > 0 && position >= duration); return { id: Number(ch.id) || 0, serial: Number(ch.serial) || 0, name: String(ch.name || "\u7ae0\u8282"), duration, position, progress, url: ch.url, courseid: Number(ch.courseid || courseId) || courseId, done, status: done ? "completed" : progress > 0 ? "learning" : "notstarted", }; } async function reportCourseStudy(params) { const data = await apiPostForm("/CourseStudy", params); if (Number(data.code) > 0) { throw new Error(data.msg || "\u4e0a\u62a5\u5931\u8d25"); } return data; } function getCloudToken() { try { const t = String(GM_getValue(CLOUD_TOKEN_KEY, "") || state.cloudToken || "").trim(); state.cloudToken = t; return t; } catch (_) { return String(state.cloudToken || "").trim(); } } function setCloudToken(token) { const t = String(token || "").trim(); state.cloudToken = t; try { GM_setValue(CLOUD_TOKEN_KEY, t); } catch (_) {} } function getLearningUserId() { const s = state.student || {}; return String(s.id || s.userid || s.uuid || "").trim(); } function _gj(url, opts = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: opts.method || "GET", url, headers: opts.headers || {}, data: opts.data, timeout: opts.timeout || 60000, onload(res) { resolve({ status: res.status, text: String(res.responseText || ""), }); }, onerror: () => reject(new Error("\u4e91\u7aef\u7f51\u7edc\u5931\u8d25")), ontimeout: () => reject(new Error("\u4e91\u7aef\u8bf7\u6c42\u8d85\u65f6")), }); }); } function _pcd(data, status) { let detail = data && (data.detail != null ? data.detail : data.message || data.code); if (detail && typeof detail === "object") { if ( detail.code === "free_quota_exceeded" || /free_quota/i.test(String(detail.code || "")) || /\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c/.test(String(detail.message || "")) ) { if (detail.proBuyUrl) state.proBuyUrl = String(detail.proBuyUrl).trim() || PRO_BUY_URL; if (detail.free_video_limit != null || detail.free_chapter_limit != null) { state.freeChapterLimit = Number(detail.free_chapter_limit ?? detail.free_video_limit) || 3; } if (detail.free_used_videos != null || detail.free_used_chapters != null) { state.freeUsedChapters = Number(detail.free_used_chapters ?? detail.free_used_videos) || 0; } const err = new Error("free_quota_exceeded"); err.code = "free_quota_exceeded"; err.detail = detail; return err; } if (detail.code === "pro_required" || /pro_required/i.test(String(detail.code || ""))) { if (detail.proBuyUrl) state.proBuyUrl = String(detail.proBuyUrl).trim() || PRO_BUY_URL; const err = new Error("pro_required"); err.code = "pro_required"; err.detail = detail; return err; } const code = String(detail.code || detail.message || ""); if (/token_bound/i.test(code) || /\u5df2\u7ed1\u5b9a/i.test(String(detail.message || ""))) { const err = new Error(String(detail.message || "token_bound")); err.code = String(detail.code || "token_bound_other_user"); err.detail = detail; return err; } return new Error(String(detail.message || detail.code || JSON.stringify(detail))); } return new Error(String(detail || `\u4e91\u7aef HTTP ${status}`)); } async function _crq(path, method, payload) { const base = CLOUD_API_BASE.replace(/\/$/, ""); const url = base + (path.startsWith("/") ? path : "/" + path); const headers = { Accept: "application/json", "Content-Type": "application/json", }; const token = getCloudToken(); if (token && !state.tokenBoundBlocked) headers.Authorization = "Bearer " + token; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; let bodyObj = payload; if (bodyObj && typeof bodyObj === "object") { bodyObj = Object.assign({}, bodyObj); if (path !== `${CLOUD_API_PREFIX}lease` && state.cloudLease && bodyObj.lease == null) { bodyObj.lease = state.cloudLease; } if (path === `${CLOUD_API_PREFIX}lease` && state.tokenBoundBlocked) bodyObj.token = ""; } const res = await _gj(url, { method: method || "POST", headers, data: bodyObj == null ? null : JSON.stringify(bodyObj), }); let data = {}; const text = String(res.text || "").trim(); if (text) { try { data = JSON.parse(text); } catch (_) { data = { message: text.slice(0, 200) }; } } if (res.status < 200 || res.status >= 300) { throw _pcd(data, res.status); } return data; } function _alp(data) { if (!data || typeof data !== "object") return; if (data.lease) { state.cloudLease = String(data.lease || ""); state.cloudLeaseExp = Number(data.lease_expires_at || data.exp || 0) || Math.floor(Date.now() / 1000) + 600; try { GM_setValue(CLOUD_LEASE_KEY, { lease: state.cloudLease, exp: state.cloudLeaseExp, tier: String(data.tier || ""), freeChapterLimit: Number( data.free_chapter_limit ?? data.free_video_limit ?? data.freeChapterLimit ?? 3 ), freeUsedChapters: Number( data.free_used_chapters ?? data.free_used_videos ?? data.freeUsedChapters ?? 0 ), proExpireAt: Number(data.pro_expires_at || data.proExpireAt || 0) || 0, boundUid: String(data.bound_learning_user_id || getLearningUserId() || ""), proBuyUrl: String(data.proBuyUrl || state.proBuyUrl || PRO_BUY_URL), finishTip: String(data.finishTip || state.finishTip || FINISH_TIP), panelNotice: String(data.panelNotice || state.panelNotice || PANEL_NOTICE), }); } catch (_) {} } if (data.tier) state.cloudTier = String(data.tier); if (data.free_chapter_limit != null || data.free_video_limit != null || data.freeChapterLimit != null) { state.freeChapterLimit = Number(data.free_chapter_limit ?? data.free_video_limit ?? data.freeChapterLimit) || 3; } if (data.free_used_chapters != null || data.free_used_videos != null || data.freeUsedChapters != null) { state.freeUsedChapters = Number(data.free_used_chapters ?? data.free_used_videos ?? data.freeUsedChapters) || 0; } if (data.pro_expires_at != null || data.proExpireAt != null) { state.cloudProExpireAt = Number(data.pro_expires_at ?? data.proExpireAt) || 0; } if (data.proBuyUrl) state.proBuyUrl = String(data.proBuyUrl).trim() || PRO_BUY_URL; if (data.finishTip) state.finishTip = String(data.finishTip); if (data.panelNotice) state.panelNotice = String(data.panelNotice); _ucp(); _upn(); } function _lcl() { try { const cached = GM_getValue(CLOUD_LEASE_KEY, null); if (!cached || typeof cached !== "object") return null; const lease = String(cached.lease || "").trim(); const exp = Number(cached.exp || 0); if (!lease || !exp) return null; if (exp - Math.floor(Date.now() / 1000) <= 60) return null; const boundUid = String(cached.boundUid || "").trim(); const uid = getLearningUserId(); if (uid && boundUid && uid !== boundUid) return null; state.cloudLease = lease; state.cloudLeaseExp = exp; if (cached.tier) state.cloudTier = String(cached.tier); if (cached.freeChapterLimit != null) state.freeChapterLimit = Number(cached.freeChapterLimit) || 3; if (cached.freeUsedChapters != null) state.freeUsedChapters = Number(cached.freeUsedChapters) || 0; if (cached.proExpireAt != null) state.cloudProExpireAt = Number(cached.proExpireAt) || 0; if (cached.proBuyUrl) state.proBuyUrl = String(cached.proBuyUrl); if (cached.finishTip) state.finishTip = String(cached.finishTip); if (cached.panelNotice) state.panelNotice = String(cached.panelNotice); return cached; } catch (_) { return null; } } function _icc() { state.cloudLease = ""; state.cloudLeaseExp = 0; try { GM_setValue(CLOUD_LEASE_KEY, null); } catch (_) {} } async function _cl(force) { const now = Math.floor(Date.now() / 1000); if (!force && state.cloudLease && state.cloudLeaseExp - now > 60) return true; if (!force && _lcl()) { _ucp(); return true; } const postLease = async (tokenOverride) => _crq(`${CLOUD_API_PREFIX}lease`, "POST", { learning_user_id: getLearningUserId(), userid: getLearningUserId(), site_id: SITE_ID, token: tokenOverride != null ? tokenOverride : state.tokenBoundBlocked ? "" : getCloudToken(), }); let data; try { data = await postLease(); state.tokenBoundBlocked = false; } catch (e) { if (String(e && e.code ? e.code : e.message || e).includes("token_bound")) { state.tokenBoundBlocked = true; _icc(); state.cloudTier = "free"; state.cloudProExpireAt = 0; const tip = (e && e.detail && e.detail.message) || e.message || "Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\u6216\u7ad9\u70b9"; _plog(String(tip)); data = await postLease(""); } else { throw e; } } _alp(data); if (!state.cloudLeaseExp) state.cloudLeaseExp = now + 600; return !!state.cloudLease; } async function _es(context) { await _cl(false); const data = await _crq(`${CLOUD_API_PREFIX}study/engine/start`, "POST", { lease: state.cloudLease, kind: "vod", context: context || {}, config: { report_interval_sec: REPORT_INTERVAL, complete_threshold: COMPLETE_THRESHOLD, }, }); if (data.free_used_chapters != null || data.free_used_videos != null) { state.freeUsedChapters = Number(data.free_used_chapters ?? data.free_used_videos) || 0; } if (data.free_chapter_limit != null || data.free_video_limit != null) { state.freeChapterLimit = Number(data.free_chapter_limit ?? data.free_video_limit) || 3; } if (data.tier) state.cloudTier = String(data.tier); if (data.proBuyUrl) state.proBuyUrl = String(data.proBuyUrl).trim() || PRO_BUY_URL; _ucp(); return data; } async function _estep(sessionId, event, lastResult) { await _cl(false); return _crq(`${CLOUD_API_PREFIX}study/engine/step`, "POST", { lease: state.cloudLease, session_id: String(sessionId || ""), event: String(event || "chapter_done"), last_result: lastResult == null ? null : lastResult, }); } function _ifqe(e) { return String(e && e.code ? e.code : e.message || e).includes("free_quota"); } function _ifqx() { return state.cloudTier !== "pro" && state.freeUsedChapters >= state.freeChapterLimit; } async function _hfq() { const url = String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL; _plog(`\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff08${state.freeUsedChapters}/${state.freeChapterLimit}\uff09`); _ucp(); const ok = await _spc("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u662f\u5426\u6253\u5f00 Pro \u8d2d\u4e70\u9875\uff1f", { title: "\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c", confirmText: "\u6253\u5f00\u8d2d\u4e70\u9875", cancelText: "\u7a0d\u540e\u518d\u8bf4", }); if (ok) openUrl(url); } function formatProExpireAt(ts) { const n = Number(ts) || 0; if (!n) return ""; const ms = n > 1e12 ? n : n * 1000; const d = new Date(ms); if (Number.isNaN(d.getTime())) return ""; const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } function _ucp() { const tierEl = document.getElementById("jxjy-cloud-tier"); const quotaEl = document.getElementById("jxjy-cloud-quota"); const expireEl = document.getElementById("jxjy-cloud-expire"); const buyBtn = document.getElementById("jxjy-buy-pro"); const settingsTier = document.getElementById("jxjy-settings-tier"); const settingsExpire = document.getElementById("jxjy-settings-expire"); const isPro = state.cloudTier === "pro"; const expireText = formatProExpireAt(state.cloudProExpireAt); if (tierEl) { tierEl.textContent = isPro ? "Pro" : "\u514d\u8d39\u7248"; tierEl.style.color = isPro ? "#166534" : "#9a3412"; tierEl.style.background = isPro ? "#f0fdf4" : "#fff7ed"; tierEl.style.borderColor = isPro ? "#86efac" : "#fdba74"; } if (quotaEl) { quotaEl.textContent = PANEL_FEATURE_SLOGAN; quotaEl.title = ""; quotaEl.classList.add("jxjy-sub-chip-em"); } if (expireEl) { if (isPro && expireText) { expireEl.textContent = `\u5230\u671f ${expireText}`; expireEl.title = ""; } else if (isPro) { expireEl.textContent = "Pro \u5df2\u5f00\u901a"; expireEl.title = ""; } else { expireEl.textContent = `\u4f53\u9a8c ${state.freeUsedChapters}/${state.freeChapterLimit} \u7ae0\u8282`; expireEl.title = ""; } } if (settingsTier) { settingsTier.textContent = isPro ? "Pro" : "\u514d\u8d39\u7248"; } if (settingsExpire) { settingsExpire.textContent = isPro && expireText ? expireText : isPro ? "\u5df2\u5f00\u901a" : "—"; } if (buyBtn) { buyBtn.style.display = ""; buyBtn.textContent = isPro ? "\u7eed\u8d39 / \u67e5\u770b Pro" : "\u5347\u7ea7 Pro"; } const inp = document.getElementById("jxjy-cloud-token-input"); if (inp && getCloudToken() && !inp.value) inp.value = getCloudToken(); } function _upn() { const el = document.getElementById("jxjy-ann-text"); if (el) { el.textContent = String(state.panelNotice || PANEL_NOTICE); } } async function _lcc() { try { const cfg = await _crq(`${CLOUD_API_PREFIX}client-config`, "GET", null); if (cfg.proBuyUrl) state.proBuyUrl = String(cfg.proBuyUrl).trim() || PRO_BUY_URL; if (cfg.freeChapterLimit != null || cfg.freeVideoLimit != null) { state.freeChapterLimit = Number(cfg.freeChapterLimit ?? cfg.freeVideoLimit) || 3; } if (cfg.finishTip) state.finishTip = String(cfg.finishTip); if (cfg.panelNotice) state.panelNotice = String(cfg.panelNotice); _ucp(); _upn(); } catch (_) {} try { const res = await _gj(CLOUD_API_BASE + `${CLOUD_API_PREFIX}panel-notice`, { method: "GET", headers: { Accept: "text/plain, */*" }, }); if (res.status >= 200 && res.status < 300) { const text = String(res.text || "").trim(); if (text) { state.panelNotice = text; _upn(); } } } catch (_) {} } function updateAuthUi() { const badge = document.getElementById("jxjy-auth-badge"); const ok = !!(state.student && (state.student.id || state.student.userid)); if (badge) { badge.textContent = ok ? "\u5df2\u767b\u5f55" : state.loading ? "\u52a0\u8f7d\u4e2d" : "\u672a\u767b\u5f55"; badge.title = ok ? "\u5e73\u53f0\u767b\u5f55\u6709\u6548" : "\u8bf7\u5148\u767b\u5f55\u5e73\u53f0"; badge.style.color = ok ? "#166534" : "#92400e"; badge.style.borderColor = ok ? "#86efac" : "#fcd34d"; badge.style.background = ok ? "#f0fdf4" : "#fffbeb"; } _ucp(); } async function loadYearsAndCourses(opts = {}) { const quiet = !!opts.quiet; if (!quiet) state.loading = true; updateAuthUi(); try { await fetchStudentInfo(); const first = await fetchMyCoursePC(""); const yearSet = new Set(first.years); if (!yearSet.size && first.list.length) { first.list.forEach((c) => { const y = Number(c.card_year) || new Date().getFullYear(); yearSet.add(y); }); } state.years = [...yearSet] .sort((a, b) => b - a) .map((y) => ({ year: y, progress: 0 })); if (state.selectedYear == null && state.years.length) { state.selectedYear = state.years[0].year; } if ( state.selectedYear != null && !state.years.some((y) => Number(y.year) === Number(state.selectedYear)) ) { state.selectedYear = state.years[0]?.year ?? null; } const allCourses = []; if (state.selectedYear != null) { const { list } = await fetchMyCoursePC(state.selectedYear); list.forEach((row) => { allCourses.push(mapCourseRow(row, state.selectedYear)); }); } state.courses = allCourses; state.years.forEach((y) => recomputeYearProgress(y.year)); syncSelectedCourseKeys(); renderYearSelect(); renderCourseList(); if (state.selectedCourseKeys.size) { try { await refreshChapterPreview({ quiet: true }); } catch (_) {} } else { updateSummary(); } if (!quiet) { _plog( state.selectedYear ? `\u5df2\u52a0\u8f7d ${state.selectedYear} \u5e74 · ${state.courses.length} \u95e8\u8bfe\u7a0b` : `\u5df2\u52a0\u8f7d ${state.years.length} \u4e2a\u5e74\u5ea6` ); } } catch (e) { if (!quiet) _plog(e.message || String(e)); if (!state.student) { renderYearSelect(); renderCourseList(); } } finally { state.loading = false; updateAuthUi(); } } function recomputeYearProgress(year) { const courses = state.courses.filter((c) => Number(c.year) === Number(year)); if (!courses.length) return 0; const avg = courses.reduce((s, c) => s + (Number(c.progress) || 0), 0) / courses.length; const y = state.years.find((x) => Number(x.year) === Number(year)); if (y) y.progress = Math.round(avg); return Math.round(avg); } async function refreshChapterPreview(opts = {}) { const quiet = !!opts.quiet; const keys = [...state.selectedCourseKeys]; if (!keys.length) { state.chapterPreview = []; renderChapterPreview(); updateSummary(); return; } if (!quiet) { const box = document.getElementById("jxjy-chapter-preview"); if (box) box.innerHTML = `
    \u52a0\u8f7d\u7ae0\u8282…
    `; } const blocks = []; for (const key of keys) { const { year, cid } = parseCourseKey(key); const course = findCourse(year, cid); if (!course) continue; try { const detail = await fetchCourseDetail(cid); const chapters = (detail.courseChapter || []).map((ch) => mapChapter(ch, cid)); const computed = computeChapterBlockProgress(chapters); course.progress = computed; recomputeYearProgress(year); blocks.push({ year, courseId: cid, courseTitle: course.name, yearLabel: `${year}\u5e74`, courseProgress: computed, computedProgress: computed, chapters, }); } catch (e) { blocks.push({ year, courseId: cid, courseTitle: course.name, yearLabel: `${year}\u5e74`, error: e.message || String(e), chapters: [], }); } } state.chapterPreview = blocks; renderChapterPreview(); renderCourseList(); renderYearSelect(); updateSummary(); } function applyLiveProgress(course, chapter) { const pct = Math.min(100, Math.max(0, Number(chapter.progress) || 0)); const year = Number(course.year); const c = findCourse(year, course.id); let block = state.chapterPreview.find( (x) => Number(x.courseId) === Number(course.id) && Number(x.year) === year ); if (!block) { block = { year, courseId: course.id, courseTitle: course.name, yearLabel: `${year}\u5e74`, courseProgress: c ? c.progress : 0, computedProgress: 0, chapters: [], }; state.chapterPreview.push(block); } const chs = block.chapters || (block.chapters = []); const idx = chs.findIndex((v) => Number(v.id) === Number(chapter.id)); const snap = { ...(idx >= 0 ? chs[idx] : {}), id: chapter.id, serial: chapter.serial, name: chapter.name, duration: chapter.duration, position: chapter.position, progress: pct, done: chapter.done || pct >= COMPLETE_THRESHOLD, status: chapter.done || pct >= COMPLETE_THRESHOLD ? "completed" : pct > 0 ? "learning" : "notstarted", }; if (idx >= 0) chs[idx] = snap; else chs.push(snap); const computed = computeChapterBlockProgress(chs); block.computedProgress = computed; block.courseProgress = computed; block.courseTitle = course.name; if (c) c.progress = computed; recomputeYearProgress(year); renderCourseList(); renderChapterPreview(); updateYearProgressLabels(); updateSummary(); } function formatProgressLine(course, chapter) { const pct = Math.min(100, Number(chapter.progress) || 0); return `${course.name}${chapter.name} \u5df2\u5b66${pct}%`; } async function _soc(chapter, course) { if (chapter.done || (Number(chapter.progress) || 0) >= COMPLETE_THRESHOLD) { return true; } setCurrentTask(`${truncate(course.name, 8)} / ${chapter.name}`); applyLiveProgress(course, chapter); let sessionId = "local"; const startRes = await _es({ courseId: course.id, chapterId: chapter.id, chapterSerial: chapter.serial, year: course.year, learning_user_id: getLearningUserId(), site_id: SITE_ID, duration: chapter.duration || 60, position: chapter.position || 0, progress: chapter.progress || 0, courseName: course.name || "", chapterName: chapter.name || "", }); sessionId = startRes.session_id || "local"; const duration = Math.max(1, Number(chapter.duration) || 60); let position = Math.max(0, Number(chapter.position) || 0); if (duration - position <= 60) position = 0; let progress = Number(chapter.progress) || 0; let lastResult = { ok: false }; while (!state.stopFlag && progress < COMPLETE_THRESHOLD && position < duration) { const stepSec = TURBO_MODE ? TURBO_STEP_SEC : REPORT_INTERVAL; const studylong = Math.min(stepSec, duration - position); if (studylong <= 0) break; await sleep(TURBO_MODE ? TURBO_DELAY_MS : studylong * 1000); if (state.stopFlag) break; position += studylong; if (position > duration) position = duration; try { const res = await reportCourseStudy({ courseid: course.id, chapter: chapter.serial, duration, studylong, position, }); progress = Number(res.progress) || Math.floor((position / duration) * 100); chapter.position = position; chapter.progress = progress; chapter.done = progress >= COMPLETE_THRESHOLD || position >= duration; chapter.status = chapter.done ? "completed" : "learning"; _plog(formatProgressLine(course, chapter), course.name); applyLiveProgress(course, chapter); lastResult = { ok: true, progress, position, studylong }; } catch (e) { lastResult = { ok: false, message: String(e.message || e) }; throw e; } } if (state.stopFlag) { try { await _estep(sessionId, "stopped", lastResult); } catch (_) {} return false; } chapter.done = progress >= COMPLETE_THRESHOLD || position >= duration; chapter.progress = Math.max(progress, chapter.done ? COMPLETE_THRESHOLD : progress); if (chapter.done) chapter.status = "completed"; applyLiveProgress(course, chapter); try { await _estep(sessionId, "chapter_done", lastResult); } catch (e) { if (chapter.done) { _plog(`${course.name}${chapter.name}\u5b8c\u6210`, course.name); return true; } throw e; } if (chapter.done) { _plog(`${course.name}${chapter.name}\u5b8c\u6210`, course.name); return true; } return false; } async function _scs(course) { let guard = 0; while (!state.stopFlag && guard++ < 80) { const detail = await fetchCourseDetail(course.id); const chapters = (detail.courseChapter || []) .map((ch) => mapChapter(ch, course.id)) .sort((a, b) => (a.serial || 0) - (b.serial || 0)); const pending = chapters.filter( (ch) => !ch.done && (Number(ch.progress) || 0) < COMPLETE_THRESHOLD ); if (!pending.length) { _plog(`${course.name}\u5b8c\u6210`, course.name); course.progress = 100; const block = state.chapterPreview.find( (x) => Number(x.courseId) === Number(course.id) && Number(x.year) === Number(course.year) ); if (block) { block.courseProgress = 100; block.computedProgress = 100; (block.chapters || []).forEach((ch) => { ch.progress = 100; ch.done = true; ch.status = "completed"; }); } recomputeYearProgress(course.year); renderCourseList(); renderChapterPreview(); updateYearProgressLabels(); updateSummary(); return; } const chapter = pending[0]; const ok = await _soc(chapter, course); if (!ok && !state.stopFlag) { _plog(`\u300c${course.name}\u300d\u672a\u5b8c\u6210\uff0c\u5df2\u505c\u6b62`); return; } } } async function _rs() { const courses = [...state.selectedCourseKeys] .map((key) => { const { year, cid } = parseCourseKey(key); return findCourse(year, cid); }) .filter(Boolean) .filter((c) => (c.progress || 0) < COMPLETE_THRESHOLD); if (!courses.length) { _plog("\u6ca1\u6709\u53ef\u5b66\u4e60\u7684\u8bfe\u7a0b"); return "no_courses"; } try { await refreshChapterPreview({ quiet: true }); } catch (_) {} _plog(`\u5f00\u59cb\u5b66\u4e60 · ${courses.length} \u95e8\u8bfe`); for (const course of courses) { if (state.stopFlag) break; try { await _scs(course); } catch (e) { if (_ifqe(e)) return "quota_exceeded"; _plog(`\u8bfe\u7a0b\u4e2d\u65ad\u300c${course.name}\u300d\uff1a${e.message || e}`); return "error"; } } if (state.stopFlag) return "stopped"; return "completed"; } async function _ss() { if (state.running) return; if (!state.student) { _plog("\u8bf7\u5148\u767b\u5f55\u5e73\u53f0"); return; } if (state.selectedYear == null) { _plog("\u8bf7\u5148\u9009\u62e9\u57f9\u8bad\u5e74\u5ea6"); return; } if (!state.selectedCourseKeys.size) { _plog("\u8bf7\u5148\u9009\u62e9\u8981\u5b66\u4e60\u7684\u8bfe\u7a0b"); return; } state.running = true; state.stopFlag = false; syncRunButtons(); setCurrentTask("\u5b66\u4e60\u4e2d…"); showLogTab(); try { await _cl(true); if (_ifqx()) { await _hfq(); return; } const result = await _rs(); if (result === "completed" && !state.stopFlag) { _plog("\u5168\u90e8\u8bfe\u7a0b\u5df2\u5b66\u5b8c"); if (state.finishTip) { await _spa(String(state.finishTip), "\u5b66\u4e60\u5b8c\u6210"); } } else if (result === "quota_exceeded") { await _hfq(); } } catch (e) { if (_ifqe(e)) { await _hfq(); } else { _plog(`\u770b\u8bfe\u4e2d\u65ad\uff1a${e.message || e}`); } } finally { state.running = false; state.stopFlag = false; syncRunButtons(); setCurrentTask("\u5f85\u547d"); try { await loadYearsAndCourses({ quiet: true }); } catch (_) {} } } function _st() { if (!state.running) return; state.stopFlag = true; setCurrentTask("\u6b63\u5728\u505c\u6b62…"); } function updateSummary() { let total = 0; let done = 0; let learnedSec = 0; let totalSec = 0; state.chapterPreview.forEach((c) => { (c.chapters || []).forEach((ch) => { total += 1; if (ch.done) done += 1; totalSec += Number(ch.duration) || 0; learnedSec += ((Number(ch.duration) || 0) * (Number(ch.progress) || 0)) / 100; }); }); const pct = totalSec > 0 ? Math.round((learnedSec / totalSec) * 100) : total ? Math.round((done / total) * 100) : 0; const selCount = state.selectedCourseKeys.size; const doneEl = document.getElementById("jxjy-queue-done"); const totalEl = document.getElementById("jxjy-queue-total"); const pctEl = document.getElementById("jxjy-queue-percent"); const bar = document.getElementById("jxjy-queue-progress"); const selEl = document.getElementById("jxjy-selected-count"); const foot = document.getElementById("jxjy-footer-text"); if (selEl) selEl.textContent = String(selCount); if (doneEl) doneEl.textContent = String(done); if (totalEl) totalEl.textContent = String(total); if (pctEl) pctEl.textContent = `${pct}%`; if (bar) bar.style.width = `${pct}%`; if (foot) { foot.textContent = state.selectedYear ? `${state.selectedYear}\u5e74 · ${state.courses.length} \u95e8\u8bfe · \u5df2\u9009 ${selCount} \u95e8` : state.years.length ? "\u8bf7\u9009\u62e9\u57f9\u8bad\u5e74\u5ea6" : "\u8bf7\u767b\u5f55\u540e\u52a0\u8f7d\u8bfe\u7a0b"; } } function updateYearProgressLabels() { const sel = document.getElementById("jxjy-year-select"); if (!sel) return; [...sel.options].forEach((opt) => { const y = Number(opt.value); const meta = state.years.find((x) => Number(x.year) === y); const count = Number(y) === Number(state.selectedYear) ? state.courses.length : state.courses.filter((c) => Number(c.year) === y).length; if (meta) opt.textContent = `${y}\u5e74 · ${meta.progress || 0}% · ${count}\u8bfe`; }); } function renderYearSelect() { const wrap = document.getElementById("jxjy-year-select-wrap"); if (!wrap) return; if (!state.years.length) { wrap.innerHTML = `
    ${state.student ? "\u6682\u65e0\u57f9\u8bad\u5e74\u5ea6" : "\u767b\u5f55\u540e\u52a0\u8f7d\u57f9\u8bad"}
    `; return; } const cur = state.selectedYear ?? state.years[0].year; wrap.innerHTML = ``; wrap.querySelector("#jxjy-year-select").addEventListener("change", async (e) => { state.selectedYear = Number(e.target.value); state.selectedCourseKeys = new Set(); await loadYearsAndCourses({ quiet: true }); }); } function toggleIncompleteCourses() { const year = state.selectedYear; const incomplete = state.courses .filter( (c) => Number(c.year) === Number(year) && (Number(c.progress) || 0) < COMPLETE_THRESHOLD ) .map((c) => courseKey(c.year, c.id)); if (!incomplete.length) { _plog("\u5f53\u524d\u5e74\u5ea6\u6ca1\u6709\u672a\u5b8c\u6210\u8bfe\u7a0b"); return; } const allChecked = incomplete.every((k) => state.selectedCourseKeys.has(k)); if (allChecked) { incomplete.forEach((k) => state.selectedCourseKeys.delete(k)); } else { incomplete.forEach((k) => state.selectedCourseKeys.add(k)); } renderCourseList(); updateSummary(); refreshChapterPreview(); } function renderCourseList() { const box = document.getElementById("jxjy-course-list"); if (!box) return; if (state.selectedYear == null) { box.innerHTML = `
    \u8bf7\u5148\u9009\u62e9\u57f9\u8bad\u5e74\u5ea6
    `; return; } const year = state.selectedYear; const list = state.courses.filter((c) => Number(c.year) === Number(year)); if (!list.length) { box.innerHTML = `
    \u8be5\u5e74\u5ea6\u6682\u65e0\u8bfe\u7a0b
    `; return; } box.innerHTML = list .map((c) => { const pct = Math.min(100, Math.max(0, c.progress || 0)); const key = courseKey(c.year, c.id); const checked = state.selectedCourseKeys.has(key) ? "checked" : ""; const active = checked ? "active" : ""; const done = pct >= COMPLETE_THRESHOLD; const cc = Number(c.chapter_count) || 0; const doneCh = Number(c.completed_count) || 0; const sub = cc > 0 ? `${c.credit || 0} \u5b66\u65f6 · ${doneCh}/${cc}\u7ae0 · ${pct}%` : `${c.credit || 0} \u5b66\u65f6 · ${pct}%`; return ` `; }) .join(""); box.querySelectorAll("input[type=checkbox]").forEach((el) => { el.addEventListener("change", () => { const cur = []; box.querySelectorAll("input[type=checkbox]").forEach((cb) => { if (cb.checked) cur.push(cb.dataset.ckey); }); state.selectedCourseKeys = new Set(cur); renderCourseList(); updateSummary(); refreshChapterPreview(); }); }); } function renderChapterPreview() { const box = document.getElementById("jxjy-chapter-preview"); if (!box) return; if (!state.chapterPreview.length) { box.innerHTML = `
    \u8bf7\u9009\u62e9\u8bfe\u7a0b\u67e5\u770b\u7ae0\u8282
    `; return; } box.innerHTML = state.chapterPreview .map((course) => { if (course.error) { return `
    ${escHtml(course.yearLabel || "")} · ${escHtml(course.courseTitle)}
    ${escHtml(course.error)}
    `; } const items = (course.chapters || []) .map((ch) => { const cls = ch.done ? "done" : ch.status === "learning" ? "learning" : ""; const mark = ch.done ? "✓" : ch.status === "learning" ? "▶" : "•"; return `
    ${mark} ${escHtml(ch.serial ? `${ch.serial}. ` : "")}${escHtml(ch.name)} ${formatTime(ch.duration)}${Math.round(ch.progress)}%
    `; }) .join(""); return `
    ${escHtml(course.yearLabel || "")} · ${escHtml(course.courseTitle)} · \u8bfe ${course.courseProgress}% · \u7ae0\u8282\u5408\u8ba1 ${course.computedProgress ?? 0}%
    ${items || `
    \u65e0\u7ae0\u8282
    `}
    `; }) .join(""); } function injectStyles() { if (document.getElementById("jxjy-panel-style")) return; const st = document.createElement("style"); st.id = "jxjy-panel-style"; st.textContent = ` #jxjy-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;} #jxjy-auto-panel.jxjy-panel-max{max-height:min(92vh,780px);} #jxjy-auto-panel.jxjy-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);} #jxjy-auto-panel.jxjy-panel-min #jxjy-panel-body,#jxjy-auto-panel.jxjy-panel-min #jxjy-panel-footer,#jxjy-auto-panel.jxjy-panel-min .jxjy-footer-extra{display:none !important;} #jxjy-auto-panel.jxjy-panel-min #jxjy-panel-header{border-bottom:none;} #jxjy-panel-header{flex:0 0 auto;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;} #jxjy-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #jxjy-panel-logo{width:30px;height:30px;border-radius:9px;display:block;object-fit:cover;background:#fff;border:1px solid rgba(148,163,184,.45);box-shadow:0 2px 9px rgba(15,23,42,.11);flex:0 0 auto;} #jxjy-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;} .jxjy-panel-title-text{flex:1 1 12em;min-width:0;} .jxjy-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;} #jxjy-panel-sub{display:flex;flex-wrap:wrap;gap:3px 5px;margin-top:3px;} .jxjy-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;} .jxjy-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);} .jxjy-sub-chip-mute{color:#57534e;background:rgba(255,255,255,.55);border-color:rgba(120,113,108,.22);} #jxjy-panel-controls{display:flex;gap:5px;} .jxjy-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-weight:900;} #jxjy-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;} .jxjy-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .jxjy-card-status{padding:6px 8px;margin-bottom:6px;} .jxjy-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;} .jxjy-status-label{font-size:11px;color:#64748b;font-weight:700;} .jxjy-status-badge{padding:2px 7px;border-radius:999px;font-weight:800;font-size:11px;background:#fff;border:1px solid #cbd5e1;color:#334155;} .jxjy-status-main{margin-top:3px;} .jxjy-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;} .jxjy-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .jxjy-progress-pct{font-size:14px;font-weight:900;color:#0369a1;flex:0 0 auto;} .jxjy-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;} .jxjy-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;} .jxjy-tabbar{display:flex;gap:6px;margin-bottom:7px;} .jxjy-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;} .jxjy-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .jxjy-pane{display:none;}.jxjy-pane.active{display:block;} .jxjy-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;gap:6px;} .jxjy-list-head-actions{display:flex;align-items:center;gap:5px;flex:0 0 auto;} .jxjy-mini-btn{border:1px solid #cbd5e1;background:#fff;color:#334155;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:700;cursor:pointer;line-height:1.4;white-space:nowrap;} .jxjy-mini-btn:hover{background:#f8fafc;color:#0f172a;} .jxjy-mini-btn.on{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;} .jxjy-list-title{font-size:12px;color:#64748b;font-weight:700;} .jxjy-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;} .jxjy-year-select{width:100%;padding:6px 9px;border:1px solid #cbd5e1;border-radius:9px;font-size:12px;font-weight:700;color:#334155;background:#fff;margin-bottom:7px;box-sizing:border-box;} .jxjy-year-select:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 2px rgba(37,99,235,.15);} #jxjy-year-select-wrap{margin-bottom:4px;} #jxjy-course-list,#jxjy-chapter-preview,#jxjy-run-log{max-height:220px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} .jxjy-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;} .jxjy-course-item{display:flex;gap:7px;align-items:flex-start;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:7px;margin-bottom:6px;cursor:pointer;} .jxjy-course-item.done{opacity:.92;background:#f1f5f9;} .jxjy-course-item.active{border-color:#16a34a;background:#f0fdf4;} .jxjy-bar{display:block;height:4px;background:#e2e8f0;border-radius:2px;margin-top:5px;overflow:hidden;} .jxjy-bar>i{display:block;height:100%;background:#2563eb;border-radius:2px;} .jxjy-bar>i.full{background:#16a34a;} .jxjy-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;} .jxjy-chapter-title{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;} .jxjy-chapter-item{padding:5px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;color:#334155;} .jxjy-chapter-item.done{color:#64748b;} .jxjy-chapter-item.learning{color:#0369a1;font-weight:600;} .jxjy-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;align-items:center;} .jxjy-meta-label{color:#64748b;} .jxjy-meta-value{font-weight:700;text-align:right;} .jxjy-btn-row{display:flex;gap:7px;margin-top:6px;} .jxjy-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;} .jxjy-btn-primary{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);} .jxjy-btn-start{background:#16a34a;} .jxjy-btn-stop{background:#ef4444;} .jxjy-btn-off,.jxjy-btn:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;box-shadow:none;} .jxjy-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;} .jxjy-task-row{font-size:11px;color:#475569;margin-top:6px;line-height:1.35;} .jxjy-task-row em{font-style:normal;font-weight:800;color:#0f172a;} .jxjy-input{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 8px;font-size:12px;box-sizing:border-box;} .jxjy-footer-extra{flex:0 0 auto;padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;} .jxjy-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;} .jxjy-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-radius:11px 0 0 11px;} #jxjy-panel-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;} .jxjy-log-row{padding:4px 7px 4px 8px;margin-bottom:2px;border-bottom:1px dashed #e2e8f0;border-left:3px solid #94a3b8;border-radius:0 7px 7px 0;font-size:12px;line-height:1.42;background:#fff;} .jxjy-log-time{color:#94a3b8;font-weight:500;margin-right:2px;} .jxjy-log-pct{font-weight:700;text-decoration:underline;text-underline-offset:2px;} .jxjy-log-mins{opacity:.88;} .jxjy-log-done{background:rgba(16,185,129,.1);} .jxjy-log-done-mark{font-weight:700;color:#059669;} .jxjy-log-info{background:#f8fafc;color:#64748b!important;border-left-color:#94a3b8!important;} .jxjy-log-progress{background:color-mix(in srgb, var(--log-c) 7%, #fff);} #jxjy-dialog-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000002;display:none;align-items:center;justify-content:center;padding:20px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;} .jxjy-dialog-card{width:min(420px,92vw);background:#fff;border-radius:16px;border:1px solid #dbe4f0;box-shadow:0 18px 46px rgba(15,23,42,.25);overflow:hidden;} .jxjy-dialog-head{padding:12px 16px 10px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;} .jxjy-dialog-title{font-size:16px;font-weight:900;color:#9a3412;line-height:1.35;} .jxjy-dialog-body{padding:14px 16px 16px;font-size:13px;color:#475569;line-height:1.55;white-space:pre-wrap;word-break:break-word;} .jxjy-dialog-actions{display:flex;gap:8px;padding:0 16px 16px;} .jxjy-dialog-actions .jxjy-btn{flex:1;} `; document.head.appendChild(st); } let panelDialogResolve = null; function _cpd(result) { const modal = document.getElementById("jxjy-dialog-modal"); if (modal) modal.style.display = "none"; if (panelDialogResolve) { const done = panelDialogResolve; panelDialogResolve = null; done(result); } } function _epd() { injectStyles(); let modal = document.getElementById("jxjy-dialog-modal"); if (modal) return modal; modal = document.createElement("div"); modal.id = "jxjy-dialog-modal"; modal.innerHTML = `
    `; document.body.appendChild(modal); modal.addEventListener("click", (e) => { if (e.target === modal && modal.dataset.dismissible === "1") _cpd(true); }); return modal; } function _spa(message, title) { return new Promise((resolve) => { const modal = _epd(); modal.dataset.dismissible = "1"; modal.querySelector("#jxjy-dialog-title").textContent = title || "\u63d0\u793a"; modal.querySelector("#jxjy-dialog-body").textContent = String(message || ""); const actions = modal.querySelector("#jxjy-dialog-actions"); actions.innerHTML = ''; panelDialogResolve = () => resolve(true); actions.querySelector("#jxjy-dialog-ok").onclick = () => _cpd(true); modal.style.display = "flex"; }); } function _spc(message, opts) { const o = opts || {}; return new Promise((resolve) => { const modal = _epd(); modal.dataset.dismissible = "0"; modal.querySelector("#jxjy-dialog-title").textContent = o.title || "\u786e\u8ba4"; modal.querySelector("#jxjy-dialog-body").textContent = String(message || ""); const actions = modal.querySelector("#jxjy-dialog-actions"); actions.innerHTML = ` `; panelDialogResolve = resolve; actions.querySelector("#jxjy-dialog-cancel").onclick = () => _cpd(false); actions.querySelector("#jxjy-dialog-confirm").onclick = () => _cpd(true); modal.style.display = "flex"; }); } function bindDrag(panel) { const header = panel.querySelector("#jxjy-panel-header"); let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false; header.addEventListener("mousedown", (e) => { if (e.target.closest("#jxjy-panel-controls")) return; dragging = true; sx = e.clientX; sy = e.clientY; const rect = panel.getBoundingClientRect(); ox = rect.left; oy = rect.top; e.preventDefault(); }); window.addEventListener("mousemove", (e) => { if (!dragging) return; const left = Math.max(0, ox + e.clientX - sx); const top = Math.max(0, oy + e.clientY - sy); panel.style.left = left + "px"; panel.style.top = top + "px"; panel.style.right = "auto"; }); window.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; try { GM_setValue(PANEL_POS_KEY, { left: panel.style.left, top: panel.style.top }); } catch (_) {} }); } function createPanel() { document.querySelectorAll("#jxjy-auto-panel").forEach((el) => el.remove()); injectStyles(); const panel = document.createElement("div"); panel.id = "jxjy-auto-panel"; panel.className = "jxjy-panel-max"; panel.innerHTML = `
    ${escHtml(SITE_NAME)} v${SCRIPT_VERSION}
    \u514d\u8d39\u7248 ${PANEL_FEATURE_SLOGAN} \u4f53\u9a8c 0/3 \u7ae0\u8282
    \u5f53\u524d\u8bfe\u7a0b\u8fdb\u5ea6 \u672a\u767b\u5f55
    \u5df2\u9009 0 \u95e8 · \u7ae0\u8282 0/0
    0%
    \u9009\u62e9\u5e74\u5ea6\u4e0e\u8bfe\u7a0b
    \u767b\u5f55\u540e\u52a0\u8f7d\u57f9\u8bad
    \u767b\u5f55\u540e\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
    \u7ae0\u8282\u9884\u89c8\u7ae0\u8282\u8fdb\u5ea6
    \u8bf7\u9009\u62e9\u8bfe\u7a0b
    \u8fd0\u884c\u65e5\u5fd7
    \u6682\u65e0\u65e5\u5fd7
    \u4e91\u7aef Pro\u6388\u6743
    \u6863\u4f4d\u514d\u8d39\u7248
    \u5230\u671f
    ${ QQ_GROUP_URL ? `
    ` : "" }
    \u5f53\u524d\u4efb\u52a1\uff1a\u5f85\u547d
    `; document.body.appendChild(panel); try { const pos = GM_getValue(PANEL_POS_KEY); if (pos?.left && pos?.top) { panel.style.left = pos.left; panel.style.top = pos.top; panel.style.right = "auto"; } if (GM_getValue(PANEL_COLLAPSED_KEY)) { panel.classList.add("jxjy-panel-min"); panel.classList.remove("jxjy-panel-max"); panel.querySelector("#jxjy-btn-min").style.display = "none"; panel.querySelector("#jxjy-btn-max").style.display = ""; } } catch (_) {} bindDrag(panel); panel.querySelector("#jxjy-btn-min").onclick = () => { panel.classList.add("jxjy-panel-min"); panel.classList.remove("jxjy-panel-max"); panel.querySelector("#jxjy-btn-min").style.display = "none"; panel.querySelector("#jxjy-btn-max").style.display = ""; try { GM_setValue(PANEL_COLLAPSED_KEY, true); } catch (_) {} }; panel.querySelector("#jxjy-btn-max").onclick = () => { panel.classList.remove("jxjy-panel-min"); panel.classList.add("jxjy-panel-max"); panel.querySelector("#jxjy-btn-min").style.display = ""; panel.querySelector("#jxjy-btn-max").style.display = "none"; try { GM_setValue(PANEL_COLLAPSED_KEY, false); } catch (_) {} }; panel.querySelectorAll(".jxjy-tab-btn").forEach((btn) => { btn.addEventListener("click", () => { panel.querySelectorAll(".jxjy-tab-btn").forEach((b) => b.classList.remove("active")); panel.querySelectorAll(".jxjy-pane").forEach((p) => p.classList.remove("active")); btn.classList.add("active"); panel.querySelector(`.jxjy-pane[data-pane="${btn.dataset.tab}"]`)?.classList.add("active"); }); }); panel.querySelector("#jxjy-toggle-incomplete").onclick = () => toggleIncompleteCourses(); panel.querySelector("#jxjy-start").onclick = () => _ss(); panel.querySelector("#jxjy-stop").onclick = () => _st(); panel.querySelector("#jxjy-refresh-courses").onclick = () => loadYearsAndCourses(); panel.querySelector("#jxjy-buy-pro")?.addEventListener("click", () => openUrl(state.proBuyUrl || PRO_BUY_URL) ); panel.querySelector("#jxjy-qq-group")?.addEventListener("click", () => openUrl(QQ_GROUP_URL)); panel.querySelector("#jxjy-save-cloud-token")?.addEventListener("click", async () => { const inp = panel.querySelector("#jxjy-cloud-token-input"); setCloudToken(inp ? inp.value : ""); state.tokenBoundBlocked = false; _icc(); try { await _cl(true); _plog( state.cloudTier === "pro" ? "Pro Token \u9a8c\u8bc1\u6210\u529f" : `\u5df2\u7528 ${state.freeUsedChapters}/${state.freeChapterLimit} \u7ae0\u8282` ); } catch (e) { _plog(`\u4e91\u7aef\u9a8c\u8bc1\u5931\u8d25\uff1a${e.message || e}`); } _ucp(); }); panel.querySelector("#jxjy-clear-log").onclick = () => { state.logLines = []; const box = document.getElementById("jxjy-run-log"); if (box) box.innerHTML = `
    \u6682\u65e0\u65e5\u5fd7
    `; }; syncRunButtons(); setCurrentTask("\u5f85\u547d"); updateAuthUi(); _upn(); } function init() { document.querySelectorAll("#jxjy-auto-panel").forEach((el) => el.remove()); getCloudToken(); createPanel(); updateAuthUi(); _plog(`\u5df2\u9002\u914d ${SITE_NAME}\uff08${WEB_SITE}\uff09`); setTimeout(async () => { try { await _lcc(); await _cl(false); } catch (_) {} try { await loadYearsAndCourses({ quiet: true }); } catch (_) {} }, 600); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();