// ==UserScript== // @name 【效率版】2026年基础教育、职教高教暑期教师研修|国家中小学智慧教育平台·全自动半分钟完成 // @namespace https://card.wlxy.top/links/09A67934 // @version 1.3 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @description 国家中小学智慧教育平台(2026年基础教育/职教高教暑期教师研修)在线学习辅助,效率拉满,直接10学分瞬间拉满半分钟完成,安全保障!职业教育(职业本科、高职高专、中职中专、技工学校)与高等教育教师学习专题和基础教育教师学习专题自动适配支持! // @match https://basic.smartedu.cn/* // @match https://www.smartedu.cn/* // @match https://teacher.ykt.eduyun.cn/* // @match https://open.basic.smartedu.cn/* // @connect basic.smartedu.cn // @connect open.basic.smartedu.cn // @connect s-file-1.ykt.cbern.com.cn // @connect s-file-2.ykt.cbern.com.cn // @connect x-study-record-api.ykt.eduyun.cn // @connect x-user-profile.ykt.eduyun.cn // @connect elearning-train-gateway.ykt.eduyun.cn // @connect elearning-train-gateway-safe.ykt.eduyun.cn // @connect elearning-train-api.ykt.eduyun.cn // @connect uc-gateway.ykt.eduyun.cn // @connect oa11.ahzsksw.cn // @connect * // @grant GM_xmlhttpRequest // @grant GM_openInTab // @grant unsafeWindow // @run-at document-start // @license All Rights Reserved // ==/UserScript== (function () { "use strict"; const _w=(()=>{ const S=[ "jMXVz4jS0dKE39nby8me193T3NjSl8rO2s7J", "jMXVz4jS0dKE39nby8me193T3NjSl8rO3sw=", "jMXVz4jS0dKEz8HHyt7Fn9Db29De3w==", "jMXVz4jS0dKE3MzAytyc3NzA3NXS", "jMXVz4jS0dKEwMjP3NU=", "jMXVz4jEwMnOwt7LgMbUwNrSzA==", "y9DR1tSShoXEzZyfgdHZyMDfxsGZ29c=" ]; 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 "1.0.0"; })(); const SDP_APP_ID = "e5649925-441d-4a53-b525-51a2f1c4e0a8"; const PANEL_ID = "zxx-auto-panel"; const STORAGE_ENABLED = "zxx_auto_enabled"; const STORAGE_MODE = "zxx_study_mode"; const STORAGE_TRAIN = "zxx_train_key_v2"; const STORAGE_QUEUE = "zxx_course_queue"; const STORAGE_POS = "zxx_panel_pos_v1"; const STORAGE_COLLAPSED = "zxx_panel_collapsed_v1"; const STORAGE_SPEED = "zxx_play_speed"; const STORAGE_INTERVAL = "zxx_report_interval_sec"; const STORAGE_XPATH = "zxx_sdp_xpath_id_v1"; const STORAGE_DEVICE = "zxx_device_id_v1"; const STORAGE_WARMUP_OK_HIDE = "zxx_warmup_ok_hide_v1"; const TRAIN_OPTIONS = [ { key: "basic", id: "dc6d78f2-bad8-4d09-b8da-0d758803dbe4", apiKey: "2026jjsqpx", aliases: [ "2026jjsqpx", "dc6d78f2-bad8-4d09-b8da-0d758803dbe4", "basic", "\u57fa\u7840\u6559\u80b2", ], label: "\u57fa\u7840\u6559\u80b2", chip: "\u57fa\u7840\u6559\u80b2\u7814\u4fee", title: "2026\u5e74\u57fa\u7840\u6559\u80b2\u6691\u671f\u6559\u5e08\u7814\u4fee", }, { key: "he_vocational", id: "595a8948-d059-4972-bc30-bf10685f0061", apiKey: "2026zgjsqpx", aliases: [ "2026zgjsqpx", "595a8948-d059-4972-bc30-bf10685f0061", "he_vocational", "\u804c\u6559\u9ad8\u6559", "\u804c\u4e1a\u6559\u80b2\u548c\u9ad8\u7b49\u6559\u80b2", ], label: "\u804c\u6559\u9ad8\u6559", chip: "\u804c\u6559\u9ad8\u6559\u7814\u4fee", title: "2026\u5e74\u804c\u6559\u9ad8\u6559\u6691\u671f\u6559\u5e08\u7814\u4fee", }, ]; const DEFAULT_TRAIN_KEY = TRAIN_OPTIONS[0].apiKey; const DEFAULT_CLOUD_API_BASE = _w(6); const PRO_BUY_URL = "https://card.wlxy.live/details/8751041E"; const STORAGE_TOKEN = "zxx_cloud_token_v1"; const STORAGE_LEASE = "zxx_cloud_lease_v1"; const PANEL_NOTICE = "\u667a\u6167\u6559\u80b2\u770b\u8bfe\u52a9\u624b · \u8bf7\u5148\u767b\u5f55\u5e76\u5b8c\u6210\u811a\u672c\u9884\u70ed"; 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 state = { enabled: false, studyMode: "efficiency", trainShort: DEFAULT_TRAIN_KEY, trainOption: TRAIN_OPTIONS[0], trainMeta: null, courses: [], queue: [], chapterPreview: [], userId: "", appId: SDP_APP_ID, authReady: false, busy: false, stopFlag: false, logLines: [], panelHint: "", currentCourse: "", currentChapter: "", currentTask: "\u8bf7\u9009\u62e9\u8bfe\u7a0b\u540e\u5f00\u59cb", doneVideos: 0, totalVideos: 0, playSpeed: 2, reportIntervalSec: 3, autoplayTimer: null, xpathId: "", deviceId: "", periodRatio: 2700, // 官方 period_conversion_ratio:有效学习秒数 → 认定学时 xpathReadyAnnounced: false, xpathWatchTimer: null, cloudToken: "", cloudLease: "", cloudLeaseExp: 0, cloudTier: "unknown", cloudRevoked: false, freeChapterLimit: 3, freeUsedChapters: 0, proBuyUrl: PRO_BUY_URL, teacherProfile: null, }; function findTrainOption(raw) { const s = String(raw || "").trim(); if (!s) return TRAIN_OPTIONS[0]; const low = s.toLowerCase(); const hit = TRAIN_OPTIONS.find((t) => { if (t.apiKey === s || t.id === s || t.key === s || t.label === s) return true; const aliases = Array.isArray(t.aliases) ? t.aliases : []; return aliases.some((a) => String(a) === s || String(a).toLowerCase() === low); }); if (hit) return hit; if (/^[0-9a-fA-F-]{36}$/i.test(s)) { return { key: s, id: s, apiKey: s, label: "\u81ea\u5b9a\u4e49\u4e13\u9898", title: "\u7814\u4fee\u4e13\u9898 " + s.slice(0, 8) }; } return TRAIN_OPTIONS[0]; } function currentTrainTitle() { return (state.trainOption && state.trainOption.title) || TRAIN_OPTIONS[0].title; } function queueStorageKey(apiKey) { return `${STORAGE_QUEUE}_${apiKey || state.trainShort || DEFAULT_TRAIN_KEY}`; } function gmGet(key, def = "") { try { if (typeof GM_getValue === "function") { const v = GM_getValue(key, def); if (v != null && String(v) !== "") return String(v); } } catch (_) {} return def == null ? "" : String(def); } function gmSet(key, val) { try { if (typeof GM_setValue === "function") GM_setValue(key, String(val == null ? "" : val)); } catch (_) {} } function readShared(key) { const fromGm = gmGet(key, ""); if (fromGm) { try { if (localStorage.getItem(key) !== fromGm) localStorage.setItem(key, fromGm); } catch (_) {} return fromGm; } try { const loc = localStorage.getItem(key) || ""; if (loc) gmSet(key, loc); return loc; } catch (_) {} return ""; } function writeShared(key, val) { const v = String(val == null ? "" : val); try { localStorage.setItem(key, v); } catch (_) {} gmSet(key, v); } try { state.enabled = localStorage.getItem(STORAGE_ENABLED) === "1"; state.studyMode = "efficiency"; localStorage.setItem(STORAGE_MODE, "efficiency"); const savedTrain = localStorage.getItem(STORAGE_TRAIN) || DEFAULT_TRAIN_KEY; state.trainOption = findTrainOption(savedTrain); state.trainShort = state.trainOption.apiKey; state.queue = loadQueue(); state.playSpeed = Number(localStorage.getItem(STORAGE_SPEED) || 2) || 2; state.reportIntervalSec = Number(localStorage.getItem(STORAGE_INTERVAL) || 3) || 3; state.xpathId = readShared(STORAGE_XPATH) || ""; state.deviceId = readShared(STORAGE_DEVICE) || ""; state.cloudToken = localStorage.getItem(STORAGE_TOKEN) || ""; } catch (_) {} function periodToSeconds(period) { return Math.ceil(Number(period || 0) * (Number(state.periodRatio) || 2700)); } function secondsToPeriod(sec) { const r = Number(state.periodRatio) || 2700; return Math.round((Number(sec || 0) / r) * 100) / 100; } function rememberHeader(name, value) { if (!value) return; const v = String(value).trim(); if (!v) return; const low = String(name).toLowerCase(); if (low === "sdp-xpath-id") { if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(v)) return; const newly = !state.xpathId; if (state.xpathId === v) { writeShared(STORAGE_XPATH, v); return; } state.xpathId = v; writeShared(STORAGE_XPATH, v); forceWarmupReadyUI(newly); } else if (low === "device_id") { if (state.deviceId === v) { writeShared(STORAGE_DEVICE, v); return; } state.deviceId = v; writeShared(STORAGE_DEVICE, v); } } function hasXpathId() { if (state.xpathId) return true; try { const saved = readShared(STORAGE_XPATH) || ""; if (/^[a-f0-9-]{36}$/i.test(saved.trim())) { rememberHeader("sdp-xpath-id", saved.trim()); return !!state.xpathId; } } catch (_) {} scanStorageForIds(); return !!state.xpathId; } function forceWarmupReadyUI(isNew) { const box = document.getElementById("zxx-warmup-banner"); if (box) box.dataset.state = ""; if (isNew || !state.xpathReadyAnnounced) { onXpathReady(); } else { updateWarmupUI(); updatePanel(); } } function syncWarmupDetect() { ensureHeaderProbe(); if (!hasXpathId()) return false; forceWarmupReadyUI(!state.xpathReadyAnnounced); return true; } function onXpathReady() { stopXpathWatch(); if (!state.xpathReadyAnnounced) { state.xpathReadyAnnounced = true; log("\u811a\u672c\u9884\u70ed\u5b8c\u6210\uff0c\u53ef\u52fe\u9009\u8bfe\u7a0b\u540e\u70b9\u300c\u5f00\u59cb\u300d"); } setPanelHint("\u9884\u70ed\u5b8c\u6210\uff0c\u53ef\u52fe\u9009\u8bfe\u7a0b\u5f00\u59cb\u8fd0\u884c", "ok"); const box = document.getElementById("zxx-warmup-banner"); if (box) box.dataset.state = ""; updateWarmupUI(); updatePanel(); } function isTeacherPlayPage() { const href = location.href || ""; if (/teacherTraining\/(courseIndex|courseDetail)/i.test(href)) return true; if (/teacherTraining/i.test(href) && document.querySelector("video")) return true; return false; } function startXpathWatch() { if (state.xpathWatchTimer) return; state.xpathWatchTimer = setInterval(() => { tryWarmupPlayAssist(); if (syncWarmupDetect()) { stopXpathWatch(); return; } }, 500); } function stopXpathWatch() { if (state.xpathWatchTimer) { clearInterval(state.xpathWatchTimer); state.xpathWatchTimer = null; } } function tryWarmupPlayAssist() { if (state.xpathId) return; if (!isTeacherPlayPage()) return; try { const v = document.querySelector("video"); if (v) { v.muted = true; if (v.paused) v.play().catch(() => {}); if (!v.__zxxWarmupBound) { v.__zxxWarmupBound = true; const kick = () => { setTimeout(() => syncWarmupDetect(), 300); setTimeout(() => syncWarmupDetect(), 1200); }; v.addEventListener("play", kick); v.addEventListener("playing", kick); } } const btns = [...document.querySelectorAll("button, div, span, a, i")].filter((el) => { const t = (el.getAttribute("aria-label") || el.textContent || el.title || "").trim(); const cls = String(el.className || ""); return ( /^(\u64ad\u653e|\u70b9\u51fb\u64ad\u653e|\u5f00\u59cb\u5b66\u4e60)$/.test(t) || /play-btn|video-play|icon-play|xgplayer-start|play-icon/i.test(cls) ); }); for (const el of btns.slice(0, 3)) { try { el.click(); } catch (_) {} } } catch (_) {} } async function openWarmupPlayback(courseId) { let course = null; if (courseId) course = state.courses.find((c) => c.id === courseId) || null; if (!course) { course = state.courses.find((c) => !c.done) || state.courses[0] || null; } if (!course) { log("\u9884\u70ed\u5931\u8d25\uff1a\u8bf7\u5148\u52a0\u8f7d\u8bfe\u7a0b\u540e\u518d\u6253\u5f00\u89c6\u9891\u64ad\u653e\u9875"); setPanelHint("\u8bf7\u7b49\u5f85\u8bfe\u7a0b\u5217\u8868\u52a0\u8f7d\u5b8c\u6210\uff0c\u518d\u6253\u5f00\u89c6\u9891\u64ad\u653e\u9875\u9884\u70ed"); return; } if (!course.libraryId) { try { const { ctx } = await _pc(course); if (ctx.libraryId) course.libraryId = ctx.libraryId; } catch (e) { log("\u8bfb\u53d6\u8bfe\u7a0b\u4fe1\u606f\u5931\u8d25\uff0c\u4ecd\u5c1d\u8bd5\u6253\u5f00\u64ad\u653e\u9875\uff1a" + (e.message || e)); } } const url = playPageUrl(course.id, { libraryId: course.libraryId, tag: state.trainMeta?.train?.title || "", }); log(`\u9884\u70ed\u5f15\u5bfc\uff1a\u5df2\u6253\u5f00\u300c${course.title}\u300d\u89c6\u9891\u64ad\u653e\u9875\uff0c\u8bf7\u70b9\u51fb\u64ad\u653e\u5e76\u505c\u7559\u51e0\u79d2`); setPanelHint("\u8bf7\u5728\u6253\u5f00\u7684\u9875\u9762\u70b9\u51fb\u64ad\u653e\u89c6\u9891\uff0c\u505c\u7559\u51e0\u79d2\uff0c\u9762\u677f\u4f1a\u63d0\u793a\u9884\u70ed\u5b8c\u6210"); startXpathWatch(); const box = document.getElementById("zxx-warmup-banner"); if (box) box.dataset.state = ""; updateWarmupUI(); try { GM_openInTab(url, { active: true, insert: true, setParent: true }); } catch (_) { window.open(url, "_blank"); } } function isWarmupOkHidden() { try { return localStorage.getItem(STORAGE_WARMUP_OK_HIDE) === "1"; } catch (_) { return false; } } function setWarmupOkHidden(v) { try { if (v) localStorage.setItem(STORAGE_WARMUP_OK_HIDE, "1"); else localStorage.removeItem(STORAGE_WARMUP_OK_HIDE); } catch (_) {} } function updateWarmupUI() { const box = document.getElementById("zxx-warmup-banner"); if (!box) return; const sharedXp = String(readShared(STORAGE_XPATH) || "").trim(); const ready = !!state.xpathId || !!sharedXp; if (ready && isWarmupOkHidden() && sharedXp) { box.style.display = "none"; box.dataset.state = "ok-hidden"; box.innerHTML = ""; return; } const key = ready ? "ok" : "need-play"; if (box.dataset.state === key && box.innerHTML) { box.style.display = ""; return; } box.dataset.state = key; box.className = ready ? "zxx-warmup-banner zxx-warmup-ok" : "zxx-warmup-banner zxx-warmup-need"; box.style.display = ""; if (ready) { box.innerHTML = `` + `
\u811a\u672c\u9884\u70ed\u5b8c\u6210
` + `
\u53ef\u4ee5\u52fe\u9009\u8bfe\u7a0b\u540e\u70b9\u300c\u5f00\u59cb\u300d\u3002
`; document.getElementById("zxx-warmup-close")?.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); setWarmupOkHidden(true); updateWarmupUI(); }); } else { setWarmupOkHidden(false); box.innerHTML = `
\u5f00\u59cb\u524d\u9700\u8981\u811a\u672c\u9884\u70ed
` + `
\u8bf7\u6253\u5f00\u5e26\u89c6\u9891\u7684\u64ad\u653e\u9875\uff0c\u70b9\u51fb\u64ad\u653e\u5e76\u505c\u7559\u51e0\u79d2\u3002\u9884\u70ed\u5b8c\u6210\u540e\u4f1a\u81ea\u52a8\u540c\u6b65\u56de\u672c\u9875\u3002
` + `
` + `` + `` + `
`; document.getElementById("zxx-warmup-open")?.addEventListener("click", () => openWarmupPlayback()); document.getElementById("zxx-warmup-check")?.addEventListener("click", () => { if (syncWarmupDetect()) return; log("\u4ecd\u672a\u5b8c\u6210\u9884\u70ed\uff1a\u8bf7\u8fdb\u5165\u89c6\u9891\u64ad\u653e\u9875\u5e76\u70b9\u51fb\u64ad\u653e\uff0c\u505c\u7559\u51e0\u79d2\u540e\u518d\u68c0\u6d4b"); setPanelHint("\u8bf7\u5148\u6253\u5f00\u89c6\u9891\u64ad\u653e\u9875\u5e76\u70b9\u51fb\u64ad\u653e"); startXpathWatch(); const b = document.getElementById("zxx-warmup-banner"); if (b) b.dataset.state = ""; updateWarmupUI(); updatePanel(); }); } } function scanStorageForIds() { const tryStore = (store) => { if (!store) return; try { for (let i = 0; i < store.length; i++) { const k = store.key(i); const val = store.getItem(k) || ""; if (!state.deviceId && /device[_-]?id/i.test(k) && /^[a-f0-9]{16,64}$/i.test(val.trim())) { rememberHeader("device_id", val.trim()); } if (!state.xpathId && /xpath/i.test(k) && /[a-f0-9-]{36}/i.test(val)) { const m = val.match(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i); if (m) rememberHeader("sdp-xpath-id", m[0]); } if (/xpath|device/i.test(val) && val.length < 2000) { try { const j = JSON.parse(val); const walk = (o) => { if (!o || typeof o !== "object") return; for (const [kk, vv] of Object.entries(o)) { if (typeof vv === "string") { if (/xpath/i.test(kk)) rememberHeader("sdp-xpath-id", vv); if (/device/i.test(kk)) rememberHeader("device_id", vv); } else if (vv && typeof vv === "object") walk(vv); } }; walk(j); } catch (_) {} } } } catch (_) {} }; tryStore(localStorage); tryStore(sessionStorage); } function ensureDeviceId() { if (state.deviceId) return state.deviceId; scanStorageForIds(); if (state.deviceId) return state.deviceId; let id = ""; const chars = "0123456789abcdef"; for (let i = 0; i < 32; i++) id += chars[(Math.random() * 16) | 0]; rememberHeader("device_id", id); return state.deviceId; } function ensureHeaderProbe() { const root = typeof unsafeWindow !== "undefined" && unsafeWindow ? unsafeWindow : window; const pickHeaders = (h) => { if (!h) return; try { if (typeof h.forEach === "function") { h.forEach((v, k) => rememberHeader(k, v)); return; } } catch (_) {} if (Array.isArray(h)) { h.forEach((pair) => { if (Array.isArray(pair) && pair.length >= 2) rememberHeader(pair[0], pair[1]); }); return; } if (typeof h === "object") { Object.keys(h).forEach((k) => rememberHeader(k, h[k])); } }; try { const cur = root.fetch; if (typeof cur === "function" && !cur.__zxxHooked) { const ofetch = cur.bind(root); const hooked = function (input, init) { try { if (init && init.headers) pickHeaders(init.headers); if (input && typeof input === "object" && input.headers) pickHeaders(input.headers); } catch (_) {} return ofetch.apply(this, arguments); }; hooked.__zxxHooked = true; root.fetch = hooked; } } catch (_) {} try { const XO = root.XMLHttpRequest; if (!XO || !XO.prototype) return; if (XO.prototype.__zxxHooked) return; XO.prototype.__zxxHooked = true; const open = XO.prototype.open; const setRequestHeader = XO.prototype.setRequestHeader; const send = XO.prototype.send; XO.prototype.open = function () { this.__zxxHeaders = {}; return open.apply(this, arguments); }; XO.prototype.setRequestHeader = function (k, v) { try { this.__zxxHeaders[k] = v; rememberHeader(k, v); } catch (_) {} return setRequestHeader.apply(this, arguments); }; XO.prototype.send = function () { try { if (this.__zxxHeaders) pickHeaders(this.__zxxHeaders); } catch (_) {} return send.apply(this, arguments); }; } catch (_) {} } function installWarmupAutoDetect() { if (window.__zxxWarmupAuto) return; window.__zxxWarmupAuto = true; try { window.addEventListener("storage", (e) => { if (e && e.key === STORAGE_XPATH && e.newValue) { rememberHeader("sdp-xpath-id", e.newValue); } if (e && e.key === STORAGE_DEVICE && e.newValue) { rememberHeader("device_id", e.newValue); } }); } catch (_) {} try { if (typeof GM_addValueChangeListener === "function") { GM_addValueChangeListener(STORAGE_XPATH, (_n, _o, newV) => { if (newV) rememberHeader("sdp-xpath-id", newV); }); GM_addValueChangeListener(STORAGE_DEVICE, (_n, _o, newV) => { if (newV) rememberHeader("device_id", newV); }); } } catch (_) {} setInterval(() => { try { if (!state.xpathId) { const v = readShared(STORAGE_XPATH); if (v) rememberHeader("sdp-xpath-id", v); } if (!state.deviceId) { const d = readShared(STORAGE_DEVICE); if (d) rememberHeader("device_id", d); } } catch (_) {} }, 1500); try { document.addEventListener( "play", (e) => { if (!e || !e.target || String(e.target.tagName || "").toLowerCase() !== "video") return; setTimeout(() => syncWarmupDetect(), 200); setTimeout(() => syncWarmupDetect(), 1000); setTimeout(() => syncWarmupDetect(), 2500); }, true ); } catch (_) {} try { const mo = new MutationObserver(() => { if (state.xpathId) return; if (isTeacherPlayPage()) startXpathWatch(); }); mo.observe(document.documentElement, { childList: true, subtree: true }); } catch (_) {} } ensureHeaderProbe(); installWarmupAutoDetect(); scanStorageForIds(); ensureDeviceId(); function loadQueue(apiKey) { try { const key = queueStorageKey(apiKey); let raw = JSON.parse(localStorage.getItem(key) || "null"); if (!Array.isArray(raw) && (!apiKey || apiKey === TRAIN_OPTIONS[0].apiKey)) { raw = JSON.parse(localStorage.getItem(STORAGE_QUEUE) || "[]"); } return Array.isArray(raw) ? raw : []; } catch (_) { return []; } } function saveQueue(q) { state.queue = q; localStorage.setItem(queueStorageKey(), JSON.stringify(q)); } function logDebug(text) { try { console.log("[\u667a\u6167\u6559\u80b2]", text); } catch (_) {} } function log(text) { const msg = String(text || "").trim(); if (!msg) return; const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false }); state.logLines.unshift(`${ts} ${msg}`); if (state.logLines.length > 80) state.logLines.length = 80; renderLog(); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function getCloudApiBase() { return DEFAULT_CLOUD_API_BASE.replace(/\/$/, ""); } function getLearningUserId() { return String(state.userId || "").trim(); } async function _fp(force) { if (!force && state.teacherProfile && state.teacherProfile.learning_user_id) { return state.teacherProfile; } try { const data = await api("https://x-user-profile.ykt.eduyun.cn/v2/teacher/profile", "GET"); if (!data || typeof data !== "object") return null; const uid = String(data.user_id || "").trim(); if (!uid) return null; const school = data.school && typeof data.school === "object" ? data.school : {}; const verify = data.teacher_verify_info && typeof data.teacher_verify_info === "object" ? data.teacher_verify_info : {}; const verifySchool = verify.school && typeof verify.school === "object" ? verify.school : {}; const prof = { learning_user_id: uid, name: String(data.name || verify.name || "").trim(), shortName: String(data.name || verify.name || "").trim(), idcarNo: String(data.cert_no || "").trim(), userName: String(data.cert_no || "").trim(), school_name: String( school.school_name || verifySchool.school_name || "" ).trim(), schoolName: String( school.school_name || verifySchool.school_name || "" ).trim(), office: String(school.school_name || verifySchool.school_name || "").trim(), }; state.teacherProfile = prof; if (!state.userId) state.userId = uid; return prof; } catch (_) { return null; } } 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 bodyObj = payload; if (bodyObj && typeof bodyObj === "object") { bodyObj = Object.assign({}, bodyObj); if (path !== _w(4) && state.cloudLease && bodyObj.lease == null) { bodyObj.lease = state.cloudLease; } } const res = await gmFetch(url, { method: method || "POST", headers, body: bodyObj == null ? undefined : JSON.stringify(bodyObj), }); let data = null; try { data = await res.json(); } catch (_) { data = {}; } if (!res.ok) { throw new Error(String((data && (data.message || data.detail)) || `http_${res.status}`)); } return data || {}; } function _cu() { const tierEl = document.getElementById("zxx-cloud-tier"); const freeEl = document.getElementById("zxx-cloud-free"); const freeLabel = document.getElementById("zxx-cloud-free-label"); const tokenInput = document.getElementById("zxx-cloud-token"); const t = String(state.cloudTier || "").toLowerCase(); if (tierEl) { tierEl.textContent = state.cloudRevoked || t === "revoked" ? "\u5df2\u7981\u7528" : t === "pro" ? "Pro \u4f1a\u5458" : t === "free" ? "\u514d\u8d39\u4f53\u9a8c" : "\u672a\u6821\u9a8c"; } if (freeLabel && freeEl) { if (t === "pro") { freeLabel.textContent = "\u6388\u6743"; freeEl.textContent = "\u4e0d\u9650\u89c6\u9891"; } else { freeLabel.textContent = `\u514d\u8d39\u4f53\u9a8c\uff08${state.freeChapterLimit || 3}\u4e2a\u89c6\u9891\uff09`; freeEl.textContent = `${state.freeUsedChapters}/${state.freeChapterLimit}`; } } if (tokenInput && tokenInput !== document.activeElement) tokenInput.value = state.cloudToken || ""; } async function _el(force) { const now = Math.floor(Date.now() / 1000); if (!force && state.cloudLease && state.cloudLeaseExp - now > 60) return true; const luid = getLearningUserId(); if (!luid) throw new Error("\u672a\u767b\u5f55"); const leaseBody = { learning_user_id: luid }; const prof = await _fp(!!force); if (prof) { Object.assign(leaseBody, { learning_user_id: prof.learning_user_id || luid, name: prof.name, shortName: prof.shortName, idcarNo: prof.idcarNo, userName: prof.userName, school_name: prof.school_name, schoolName: prof.schoolName, office: prof.office, }); } const data = await _rq(_w(4), "POST", leaseBody); state.cloudLease = String(data.lease || ""); state.cloudLeaseExp = Number(data.exp || now + 600); state.cloudTier = String(data.tier || (state.cloudToken ? "pro" : "free")); state.cloudRevoked = false; state.freeChapterLimit = Number(data.free_chapter_limit ?? data.free_video_limit ?? 3) || 3; state.freeUsedChapters = Number(data.free_used_chapters ?? data.free_used_videos ?? 0) || 0; _cu(); return !!state.cloudLease; } async function _vt() { if (!state.cloudToken) return true; await _rq(_w(5), "GET", null); return true; } async function _cr() { try { if (!getLearningUserId()) { try { const t = _gt(); state.userId = String(t.token.user_id || ""); } catch (_) {} } if (state.cloudToken) { try { await _vt(); } catch (e) { const em = String(e.message || e); if (/revoked|invalid|expired/i.test(em)) { state.cloudRevoked = true; state.cloudTier = "revoked"; _cu(); log("Token \u65e0\u6548\u6216\u5df2\u7981\u7528\uff0c\u8bf7\u66f4\u6362\u540e\u91cd\u8bd5"); openProModal(); return false; } throw e; } } await _el(false); if (String(state.cloudTier).toLowerCase() === "pro") return true; if (Number(state.freeUsedChapters) >= Number(state.freeChapterLimit)) { log(`\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${state.freeUsedChapters}/${state.freeChapterLimit}\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`); openProModal(); return false; } return true; } catch (e) { logDebug("\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff1a" + (e.message || e)); log("\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"); return false; } } async function _es(context, cfg) { await _el(false); return _rq(_w(0), "POST", { lease: state.cloudLease, kind: "course", context: context || {}, config: cfg || {}, }); } async function _ep(sessionId, event, lastResult, contextPatch) { await _el(false); return _rq(_w(1), "POST", { lease: state.cloudLease, session_id: sessionId, event: event || "tick", last_result: lastResult || {}, context_patch: contextPatch || null, }); } async function _xc(cmd, courseCtx) { const type = String((cmd && cmd.type) || ""); if (type === "need_warmup") { if (!hasXpathId()) { await warmXpathId(courseCtx.course.id); if (!hasXpathId()) { openWarmupPlayback(courseCtx.course.id); for (let i = 0; i < 40 && !hasXpathId(); i++) await sleep(500); } } if (!hasXpathId()) throw new Error("\u8bf7\u5148\u5b8c\u6210\u811a\u672c\u9884\u70ed"); return { ok: true, xpath_ready: true }; } if (type === "relay") { return _rp(cmd); } if (type === "done") return { ok: true, done: true }; if (type === "failed") throw new Error(String((cmd && cmd.message) || "\u5b66\u4e60\u5931\u8d25")); if (type === "put_positions" || type === "report_progress") { throw new Error("\u4e91\u7aef\u7248\u672c\u8fc7\u65e7\uff0c\u8bf7\u66f4\u65b0 oa11 \u5f15\u64ce\u540e\u91cd\u8bd5"); } return { ok: true }; } async function _rp(cmd) { const reqs = Array.isArray(cmd.requests) ? cmd.requests : []; const concurrency = Math.max(1, Number(cmd.concurrency) || 1); const ui = cmd.ui && typeof cmd.ui === "object" ? cmd.ui : {}; if (ui.chapter) state.currentChapter = String(ui.chapter); if (ui.task) { const total = Number(ui.total || reqs.length || 0); state.currentTask = String(ui.task) + (cmd.phase === "put" && total ? ` 0/${total}` : ""); } updatePanel(); const results = {}; for (let i = 0; i < reqs.length; i += concurrency) { if (state.stopFlag) throw new Error("\u5df2\u505c\u6b62"); const batch = reqs.slice(i, i + concurrency); if (cmd.phase === "put" && ui.task) { state.currentTask = `${ui.task} ${Math.min(i + concurrency, reqs.length)}/${reqs.length}`; updatePanel(); } await Promise.all( batch.map(async (r) => { const id = String(r.id || Math.random()); try { await api(String(r.url || ""), String(r.method || "GET").toUpperCase(), r.body, { trainHeaders: !!r.train_headers, trainId: r.train_id || "", courseId: r.course_id || "", xpathId: state.xpathId, }); results[id] = { ok: true }; const rid = String(r.mark_resource_id || "").trim(); if (rid) { state.doneVideos += 1; const item = state.chapterPreview.find((x) => x.resourceId === rid); if (item) item.done = true; } } catch (e) { if (r.optional) { results[id] = { ok: false, error: String(e.message || e) }; return; } throw e; } }) ); renderChapterPreview(); updatePanel(); if (cmd.phase === "put") await sleep(50); } for (const rid of Array.isArray(cmd.mark_done) ? cmd.mark_done : []) { const id = String(rid || "").trim(); if (!id) continue; const item = state.chapterPreview.find((x) => x.resourceId === id); if (item) item.done = true; } renderChapterPreview(); const wait = Math.max(0, Number(cmd.sleep_ms) || 0); if (wait) await sleep(wait); return { ok: true, count: reqs.length, results }; } async function _ve(startRes, courseCtx) { let sessionId = String(startRes.session_id || ""); let cmd = startRes.command || {}; let logMsg = startRes.log || ""; if (logMsg) log(logMsg); if (startRes.free_used_chapters != null) state.freeUsedChapters = Number(startRes.free_used_chapters); if (startRes.free_chapter_limit != null) state.freeChapterLimit = Number(startRes.free_chapter_limit); _cu(); for (let guard = 0; guard < 500; guard++) { if (state.stopFlag) throw new Error("\u5df2\u505c\u6b62"); const type = String(cmd.type || ""); if (type === "done") { if (logMsg) log(logMsg); return { ok: true }; } if (type === "failed") throw new Error(String(cmd.message || "\u5b66\u4e60\u5931\u8d25")); let lastResult = {}; let event = "tick"; let patch = null; try { lastResult = await _xc(cmd, courseCtx); if (type === "need_warmup") { event = "warmup_ok"; patch = { xpath_ready: true }; } } catch (e) { if (String(e.message || e).includes("\u5df2\u505c\u6b62")) throw e; lastResult = { ok: false, error: String(e.message || e) }; } const step = await _ep(sessionId, event, lastResult, patch); sessionId = String(step.session_id || sessionId); cmd = step.command || {}; logMsg = step.log || ""; if (logMsg) log(logMsg); if (step.free_used_chapters != null) state.freeUsedChapters = Number(step.free_used_chapters); _cu(); } throw new Error("\u5b66\u4e60\u8d85\u65f6"); } function openProBuyPage() { const url = String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL; try { GM_openInTab(url, { active: true }); } catch (_) { window.open(url, "_blank"); } } function openProModal() { const vm = document.getElementById("zxx-pro-modal"); if (vm) vm.style.display = "flex"; } function closeProModal() { const vm = document.getElementById("zxx-pro-modal"); if (vm) vm.style.display = "none"; } function createProModal() { if (document.getElementById("zxx-pro-modal")) return; const modal = document.createElement("div"); modal.id = "zxx-pro-modal"; modal.innerHTML = `
\u5f00\u901a Pro
\u514d\u8d39\u7528\u6237\u53ef\u4f53\u9a8c ${state.freeChapterLimit || 3} \u4e2a\u89c6\u9891\uff0cPro \u4e0d\u9650\u89c6\u9891

Pro \u6743\u76ca

  • \u89e3\u9664\u514d\u8d39\u89c6\u9891\u9650\u5236\uff0c\u53ef\u5b66\u4e60\u5168\u90e8\u52fe\u9009\u8bfe\u7a0b
  • \u4e91\u7aef\u7f16\u6392\u5b66\u4e60\u8fdb\u5ea6\uff0c\u65e5\u5fd7\u7b80\u6d01\u6e05\u6670
  • Pro \u7ed1\u5b9a\u5f53\u524d\u667a\u6167\u6559\u80b2\u8d26\u53f7\uff08\u8bf7\u52ff\u5171\u7528 Token\uff09

\u5f00\u901a\u65b9\u5f0f

\u524d\u5f80\u8d2d\u4e70\u9875\u83b7\u53d6 Token\uff0c\u8d2d\u5f97\u540e\u5728\u8bbe\u7f6e\u9875\u7c98\u8d34\u5e76\u4fdd\u5b58\u6821\u9a8c\u3002

`; document.body.appendChild(modal); modal.addEventListener("click", (e) => { if (e.target === modal) closeProModal(); }); modal.querySelector("#zxx-pro-close")?.addEventListener("click", closeProModal); modal.querySelector("#zxx-pro-buy-link")?.addEventListener("click", openProBuyPage); } async function _pn() { try { const base = getCloudApiBase(); const res = await gmFetch(`${base}${_w(3)}`, { method: "GET", headers: { Accept: "text/plain" } }); const t = (await res.text()).trim(); if (t) { const el = document.getElementById("zxx-ann-text"); if (el) el.textContent = t; } } catch (_) {} try { const cfg = await _rq(_w(2), "GET", null); if (cfg.freeChapterLimit != null) state.freeChapterLimit = Number(cfg.freeChapterLimit) || 3; if (cfg.proBuyUrl) state.proBuyUrl = String(cfg.proBuyUrl).trim() || PRO_BUY_URL; _cu(); } catch (_) {} } function gmFetch(url, opts = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url, method: opts.method || "GET", headers: opts.headers || {}, data: opts.body, responseType: opts.responseType || "text", onload(res) { resolve({ ok: res.status >= 200 && res.status < 300, status: res.status, text: () => Promise.resolve(res.responseText || ""), json: async () => { const t = res.responseText || ""; if (!t) return null; return JSON.parse(t); }, raw: res, }); }, onerror(err) { reject(err || new Error("network error")); }, }); }); } function _gt() { const pattern = /^ND_UC_AUTH-([0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})&ncet-xedu&token$/; for (const key of Object.keys(localStorage)) { const m = key.match(pattern); if (!m) continue; try { const wrapper = JSON.parse(localStorage.getItem(key)); const token = JSON.parse(wrapper.value); return { key, appId: m[1], token }; } catch (_) {} } throw new Error("\u672a\u627e\u5230\u767b\u5f55\u51ed\u8bc1\uff0c\u8bf7\u5148\u767b\u5f55 basic.smartedu.cn"); } function randomNonce(n) { const chars = "0123456789ABCDEFGHIJKLMNOPQRTUVWXZYS".split(""); let s = ""; for (let i = 0; i < n; i++) s += chars[Math.ceil(35 * Math.random())]; return s; } async function _hm(keyStr, dataStr) { const enc = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", enc.encode(keyStr), { name: "HMAC", hash: "SHA-256" }, false, ["sign"] ); const sig = await crypto.subtle.sign("HMAC", key, enc.encode(dataStr)); const bytes = new Uint8Array(sig); let bin = ""; for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); return btoa(bin); } async function _bm(url, method = "GET") { const { token, appId } = _gt(); const diff = parseInt(token.diff || "0", 10) || 0; const nonce = `${Date.now() + diff}:${randomNonce(8)}`; const u = new URL(url); const relative = u.pathname; const authority = u.hostname; const raw = `${nonce}\n${String(method).toUpperCase()}\n${relative}\n${authority}\n`; const mac = await _hm(token.mac_key, raw); state.userId = String(token.user_id || ""); state.appId = appId || SDP_APP_ID; state.authReady = true; return { authorization: `MAC id="${token.access_token}",nonce="${nonce}",mac="${mac}"`, appId: state.appId, userId: state.userId, token, }; } async function api(url, method = "GET", body, extra = {}) { const auth = await _bm(url, method); const headers = { Accept: "application/json, text/plain, */*", "sdp-app-id": auth.appId, Authorization: auth.authorization, Origin: "https://basic.smartedu.cn", Referer: "https://basic.smartedu.cn/", }; const isTrainSpi = /elearning-train-gateway/i.test(url); if (isTrainSpi || extra.trainHeaders) { const deviceId = ensureDeviceId(); const xpathId = state.xpathId || extra.xpathId || ""; if (deviceId) headers["device_id"] = deviceId; if (xpathId) headers["sdp-xpath-id"] = xpathId; if (extra.trainId) headers["x-train-id"] = extra.trainId; if (extra.courseId) headers["x-course-id"] = extra.courseId; } if (body != null) headers["Content-Type"] = "application/json"; let status = 0; let text = ""; try { const pageFetch = typeof unsafeWindow !== "undefined" && unsafeWindow.fetch ? unsafeWindow.fetch.bind(unsafeWindow) : null; if (pageFetch && /smartedu\.cn$/i.test(location.hostname)) { const res = await pageFetch(url, { method, headers, body: body != null ? JSON.stringify(body) : undefined, mode: "cors", credentials: "omit", }); status = res.status; text = await res.text(); } else { throw new Error("no page fetch"); } } catch (_) { const res = await gmFetch(url, { method, headers, body: body != null ? JSON.stringify(body) : undefined, }); status = res.status; text = await res.text(); } let data = null; try { data = text ? JSON.parse(text) : null; } catch (_) { data = null; } if (status < 200 || status >= 300) { const code = data && (data.code || data.error_code || data.message || data.msg); const hint = String(code || text || "") .replace(/\s+/g, " ") .slice(0, 220); throw new Error(`HTTP ${status}${hint ? " · " + hint : ""}`); } return data; } async function warmXpathId(courseId, timeoutMs = 15000) { scanStorageForIds(); if (state.xpathId) return state.xpathId; log("\u7f3a\u5c11\u9884\u70ed\u4fe1\u606f\uff0c\u6b63\u5728\u6253\u5f00\u89c6\u9891\u64ad\u653e\u9875…"); let iframe = null; return new Promise((resolve) => { let settled = false; const done = () => { if (settled) return; settled = true; clearInterval(poll); clearTimeout(timer); try { if (iframe && iframe.parentNode) iframe.parentNode.removeChild(iframe); } catch (_) {} resolve(state.xpathId || ""); }; const timer = setTimeout(() => { if (!state.xpathId) { log("\u9884\u70ed\u8d85\u65f6\uff1a\u8bf7\u624b\u52a8\u6253\u5f00\u89c6\u9891\u64ad\u653e\u9875\u5e76\u70b9\u51fb\u64ad\u653e\u540e\u518d\u8bd5"); } done(); }, timeoutMs); const poll = setInterval(() => { try { const saved = readShared(STORAGE_XPATH); if (saved && saved !== state.xpathId) rememberHeader("sdp-xpath-id", saved); const dev = readShared(STORAGE_DEVICE); if (dev && dev !== state.deviceId) rememberHeader("device_id", dev); } catch (_) {} if (state.xpathId) { log("\u9884\u70ed\u5b8c\u6210"); done(); } }, 400); try { GM_openInTab(coursePageUrl(courseId), { active: false, insert: true, setParent: true }); } catch (_) { try { iframe = document.createElement("iframe"); iframe.style.cssText = "position:fixed;width:1px;height:1px;left:-100px;top:-100px;opacity:0;pointer-events:none;border:0;"; iframe.src = coursePageUrl(courseId); (document.documentElement || document.body).appendChild(iframe); } catch (__) { window.open(coursePageUrl(courseId), "_blank"); } } }); } async function apiPublic(url) { const res = await gmFetch(url, { headers: { Accept: "application/json, text/plain, */*", Origin: "https://basic.smartedu.cn", Referer: "https://basic.smartedu.cn/", }, }); if (!res.ok) throw new Error(`\u62c9\u53d6\u5931\u8d25 ${res.status}`); return res.json(); } function detectTrainKeyFromUrl() { const path = String(location.pathname || ""); const m = path.match(/\/(?:jiaoshi\/)?training\/([0-9a-fA-F-]{36}|[A-Za-z0-9_-]+)/i) || path.match(/\/training\/([^/?#]+)/i); if (m) { const token = decodeURIComponent(m[1]); const opt = findTrainOption(token); if ( opt && (opt.apiKey === token || opt.id === token || (opt.aliases || []).some((a) => String(a) === token)) ) { return opt.apiKey; } if (/^[0-9a-fA-F-]{36}$/i.test(token) || /^[A-Za-z0-9_-]+$/.test(token)) return token; } const q = new URLSearchParams(location.search); const fromQ = q.get("train") || q.get("trainId") || ""; if (fromQ) return findTrainOption(fromQ).apiKey || fromQ; return ""; } function detectTrainKey() { return detectTrainKeyFromUrl() || state.trainShort || DEFAULT_TRAIN_KEY; } function syncTrainSelectUI() { const sel = document.getElementById("zxx-train-select"); if (sel) sel.value = state.trainOption?.apiKey || state.trainShort || DEFAULT_TRAIN_KEY; const titleEl = document.getElementById("zxx-train-title"); if (titleEl) titleEl.textContent = currentTrainTitle(); const chipEl = document.getElementById("zxx-sub-chip-train"); if (chipEl) { const opt = state.trainOption || findTrainOption(state.trainShort); chipEl.textContent = opt?.chip || `${opt?.label || "\u6559\u5e08"}\u7814\u4fee`; } } async function syncTrainFromLocation(opts = {}) { const silent = !!opts.silent; const fromUrl = detectTrainKeyFromUrl(); if (!fromUrl) return false; const opt = findTrainOption(fromUrl); const nextKey = opt.apiKey || fromUrl; if (nextKey === state.trainShort && state.trainMeta?.train?.id) { syncTrainSelectUI(); return false; } if (state.busy) { if (!silent) log(`\u6b63\u5728\u8fd0\u884c\uff0c\u7a0d\u540e\u5c06\u6309\u9875\u9762\u4e13\u9898\u5207\u6362\uff1a${opt.label || nextKey}`); state._pendingTrainKey = nextKey; syncTrainSelectUI(); return false; } const ok = await switchTrain(nextKey, { silent: true }); if (ok && !silent) log(`\u5df2\u6309\u5f53\u524d\u9875\u9762\u81ea\u52a8\u5207\u6362\u4e13\u9898\uff1a${opt.label || nextKey}`); return ok; } function installTrainRouteWatch() { if (state._trainRouteWatchInstalled) return; state._trainRouteWatchInstalled = true; let lastHref = String(location.href || ""); const tick = () => { const href = String(location.href || ""); if (href === lastHref) return; lastHref = href; syncTrainFromLocation({ silent: false }).catch(() => {}); }; window.addEventListener("popstate", tick); window.addEventListener("hashchange", tick); try { const wrap = (type) => { const raw = history[type]; if (typeof raw !== "function") return; history[type] = function () { const ret = raw.apply(this, arguments); try { window.dispatchEvent(new Event("zxx-locationchange")); } catch (_) {} return ret; }; }; wrap("pushState"); wrap("replaceState"); window.addEventListener("zxx-locationchange", tick); } catch (_) {} setInterval(tick, 1200); } function playPageUrl(courseId, extra = {}) { const params = new URLSearchParams(); params.set("courseId", courseId); if (extra.libraryId) params.set("libraryId", String(extra.libraryId)); const tag = extra.tag || state.trainMeta?.train?.title || currentTrainTitle() || ""; if (tag) { params.set("breadcrumb", tag); params.set("tag", tag); } return `https://basic.smartedu.cn/teacherTraining/courseIndex?${params.toString()}`; } function coursePageUrl(courseId, extra) { const course = state.courses.find((c) => c.id === courseId); return playPageUrl(courseId, { libraryId: (extra && extra.libraryId) || course?.libraryId || "", tag: (extra && extra.tag) || state.trainMeta?.train?.title || currentTrainTitle() || "", }); } async function loadTrain(apiKey) { const opt = findTrainOption(apiKey || detectTrainKey()); const short = opt.apiKey || apiKey || detectTrainKey(); state.trainOption = findTrainOption(short); state.trainShort = state.trainOption.apiKey || short; localStorage.setItem(STORAGE_TRAIN, state.trainShort); state.queue = loadQueue(state.trainShort); const metaUrl = `https://s-file-1.ykt.cbern.com.cn/teach/api_static/trains/${state.trainShort}.json`; const coursesUrl = `https://s-file-1.ykt.cbern.com.cn/teach/api_static/trains/${state.trainShort}/train_courses.json`; const [meta, courses] = await Promise.all([ apiPublic(metaUrl), apiPublic(coursesUrl), ]); state.trainMeta = meta; state.periodRatio = Number(meta?.train?.period_conversion_ratio || 2700) || 2700; const trainId = meta?.train?.id || state.trainOption.id; let periodMap = {}; try { const auth = _gt(); const uid = auth.token.user_id; state.userId = String(uid); if (trainId && uid) { periodMap = (await api( `https://elearning-train-api.ykt.eduyun.cn/v1/users/${uid}/trains/${trainId}/courses_period/actions/list` )) || {}; } } catch (e) { log("\u5b66\u65f6\u8fdb\u5ea6\u6682\u4e0d\u53ef\u7528\uff1a" + (e.message || e)); } state.courses = (courses || []).map((c) => { const id = c.course_id; const earned = Number(periodMap[id] || 0); const maxP = Number(c.max_period || 0); const statusKey = `${id}-status`; const done = Number(periodMap[statusKey] || 0) >= 1 || (maxP > 0 && earned >= maxP - 1e-6); return { id, title: c.title || id, cover: c.front_cover_url || "", maxPeriod: maxP, earnedPeriod: earned, totalPeriod: Number(c.total_period || 0), periodSec: Number(c.period || 0), done, checked: state.queue.includes(id), }; }); if (!state.queue.length) { const undone = state.courses.filter((c) => !c.done).map((c) => c.id); state.queue = undone.length ? undone : state.courses.map((c) => c.id); state.courses.forEach((c) => { c.checked = state.queue.includes(c.id); }); saveQueue(state.queue); } else { state.courses.forEach((c) => { c.checked = state.queue.includes(c.id); }); } syncTrainSelectUI(); logDebug(`\u5b66\u65f6\u6362\u7b97 ratio=${state.periodRatio}`); return state.courses; } async function switchTrain(apiKey, { silent } = {}) { if (state.busy) { log("\u8fd0\u884c\u4e2d\u8bf7\u5148\u505c\u6b62\uff0c\u518d\u5207\u6362\u4e13\u9898"); syncTrainSelectUI(); return false; } const opt = findTrainOption(apiKey); try { state.chapterPreview = []; await loadTrain(opt.apiKey); renderCourseList(); renderChapterPreview(); updatePanel(); if (!silent) log(`\u5df2\u5207\u6362\u4e13\u9898\uff1a${opt.label}\uff08${state.courses.length} \u95e8\u8bfe\uff09`); return true; } catch (e) { log("\u5207\u6362\u4e13\u9898\u5931\u8d25"); logDebug(String(e.message || e)); syncTrainSelectUI(); return false; } } async function refreshCourseEarned(course) { try { const trainId = state.trainMeta?.train?.id || state.trainOption?.id; const uid = state.userId || _gt().token.user_id; if (!trainId || !uid || !course?.id) return course; const periodMap = (await api( `https://elearning-train-api.ykt.eduyun.cn/v1/users/${uid}/trains/${trainId}/courses_period/actions/list` )) || {}; const earned = Number(periodMap[course.id] || 0); course.earnedPeriod = earned; if (course.maxPeriod > 0 && earned >= course.maxPeriod - 1e-6) course.done = true; } catch (_) {} return course; } async function loadCourseRelative(courseId) { return apiPublic( `https://s-file-1.ykt.cbern.com.cn/teach/s_course/v2/business_courses/${courseId}/course_relative_infos/zh-CN.json` ); } async function loadFulls(activitySetId) { return apiPublic( `https://s-file-1.ykt.cbern.com.cn/teach/s_course/v2/activity_sets/${activitySetId}/fulls.json` ); } function flattenVideos(fulls) { const list = []; const walk = (nodes, path) => { for (const n of nodes || []) { const name = n.node_name || "\u672a\u547d\u540d"; const p = path.concat(name); if (n.node_type === "activity") { const act = n.relations?.activity || {}; const resources = act.activity_resources || []; for (const r of resources) { if (String(r.resource_type || "").toLowerCase() !== "video") continue; list.push({ activityId: n.node_id || act.id, activityTitle: name, path: p.join(" / "), resourceId: r.resource_id, studyTime: Math.max(1, Number(r.study_time || 0) || 1), title: r.video_extend?.title || name, }); } } if (n.child_nodes?.length) walk(n.child_nodes, p); } }; walk(fulls?.nodes || [], []); return list; } async function getStudyDetails(courseId) { const uid = state.userId || _gt().token.user_id; try { return await api( `https://x-study-record-api.ykt.eduyun.cn/v1/study_details/${courseId}/${uid}` ); } catch (_) { return null; } } function parseExtInfo(raw) { if (!raw) return {}; if (typeof raw === "object") return raw; try { return JSON.parse(raw) || {}; } catch (_) { return {}; } } function extractLibraryId(relative, fulls) { const tags = relative?.library_tags; if (Array.isArray(tags) && tags[0]) { if (tags[0].library_id) return String(tags[0].library_id); if (tags[0].id) return String(tags[0].id); } const walk = (nodes) => { for (const n of nodes || []) { const c = n.relations?.activity?.context_id || ""; const m = String(c).match(/library:([0-9a-fA-F-]+)/); if (m) return m[1]; const hit = walk(n.child_nodes); if (hit) return hit; } return ""; }; return walk(fulls?.nodes) || ""; } function extractTags(relative, trainMeta) { const tagTitle = relative?.library_tags?.[0]?.tags?.[0]?.title; const trainTitle = trainMeta?.train?.title || ""; return { tags: tagTitle || trainTitle, origin: trainTitle.replace(/\uff08[^\uff09]*\uff09\s*$/, "") || trainTitle, }; } async function _pc(course) { const relative = await loadCourseRelative(course.id); const detail = relative.course_detail || {}; const ext = detail.ext_info || {}; const pc = ext.progress_config || {}; const activitySetId = detail.activity_set_id; if (!activitySetId) throw new Error("\u65e0 activity_set_id"); const fulls = await loadFulls(activitySetId); const videos = flattenVideos(fulls); const lib = extractLibraryId(relative, fulls); const tagInfo = extractTags(relative, state.trainMeta); const existing = await getStudyDetails(course.id); const baseExt = parseExtInfo(existing?.ext_info); const ctx = { trainId: state.trainMeta?.train?.id || state.trainOption?.id, courseId: course.id, courseName: detail.name || course.title, tags: tagInfo.tags, origin: tagInfo.origin, cover: course.cover || "", libraryId: lib, spi: pc.spi, beginSpi: pc["begin-spi"] || pc.begin_spi, maxPeriod: Number(pc.max_period || course.maxPeriod || 0), }; return { ctx, videos, baseExt, relative, fulls }; } async function previewCourseChapters(course) { state.currentCourse = course.title; state.currentTask = "\u52a0\u8f7d\u7ae0\u8282…"; updatePanel(); const { videos } = await _pc(course); state.chapterPreview = videos.map((v) => ({ courseId: course.id, courseTitle: course.title, title: v.title, studyTime: v.studyTime, resourceId: v.resourceId, done: false, })); renderChapterPreview(); switchPanelTab("chapter"); state.currentTask = `\u5df2\u52a0\u8f7d ${videos.length} \u4e2a\u89c6\u9891`; updatePanel(); log(`${course.title}\uff1a\u7ae0\u8282 ${videos.length} \u4e2a\u89c6\u9891`); return videos; } async function _fc(course) { state.currentCourse = course.title; await refreshCourseEarned(course); let maxP = Number(course.maxPeriod || 0); let earned = Number(course.earnedPeriod || 0); if (maxP > 0 && earned >= maxP - 1e-6) { log(`${course.title}\u5b66\u4e60\u5b8c\u6210`); course.done = true; return { ok: true, videos: 0, skipped: true }; } const { ctx, videos: allVideos, baseExt } = await _pc(course); if (ctx.maxPeriod > 0) { course.maxPeriod = ctx.maxPeriod; maxP = ctx.maxPeriod; } if (!allVideos.length) { log(`${course.title}\u5b66\u4e60\u5b8c\u6210`); return { ok: true, videos: 0 }; } if (!ctx.libraryId) throw new Error("\u8bfe\u7a0b\u4fe1\u606f\u4e0d\u5b8c\u6574"); if (!ctx.trainId) throw new Error("\u8bfe\u7a0b\u4fe1\u606f\u4e0d\u5b8c\u6574"); course.libraryId = ctx.libraryId; const videosPayload = allVideos.map((v) => ({ resourceId: v.resourceId, activityId: v.activityId, activityTitle: v.activityTitle, title: v.title, studyTime: v.studyTime, })); state.totalVideos += allVideos.length; state.chapterPreview = allVideos.map((v) => ({ courseId: course.id, courseTitle: course.title, title: v.title, studyTime: v.studyTime, resourceId: v.resourceId, done: false, })); renderChapterPreview(); switchPanelTab("chapter"); const startRes = await _es( { user_id: String(state.userId || _gt().token.user_id || ""), course_id: course.id, course_title: course.title, course_name: ctx.courseName || course.title, max_period: maxP, earned_period: earned, period_ratio: state.periodRatio, train_id: ctx.trainId, library_id: ctx.libraryId, tags: ctx.tags || "", origin: ctx.origin || "", cover: ctx.cover || "", spi: ctx.spi || "", begin_spi: ctx.beginSpi || "", xpath_ready: !!state.xpathId, base_ext: baseExt || {}, videos: videosPayload, }, { study_mode: state.studyMode || "efficiency", report_interval_sec: state.reportIntervalSec } ); if (String(startRes.message || "").includes("free_quota")) { log(`\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${state.freeUsedChapters}/${state.freeChapterLimit}\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`); openProModal(); throw new Error("free_quota_exhausted"); } await _ve(startRes, { course, ctx, baseExt }); await refreshCourseEarned(course); if (maxP > 0) { course.earnedPeriod = Math.max(Number(course.earnedPeriod) || 0, maxP); course.done = true; } else { course.done = true; } state.currentTask = "\u672c\u8bfe\u5b8c\u6210"; updatePanel(); return { ok: true }; } function findVideo() { return document.querySelector("video"); } function clickNextActivity() { const candidates = [ ...document.querySelectorAll("button, a, div, span, li"), ].filter((el) => { const t = (el.textContent || "").trim(); return /\u4e0b\u4e00|\u4e0b\u4e00\u4e2a|\u4e0b\u4e00\u9879|\u7ee7\u7eed\u5b66\u4e60|\u4e0b\u4e00\u4e2a\u6d3b\u52a8/.test(t); }); for (const el of candidates) { try { el.click(); return true; } catch (_) {} } const undone = document.querySelector( ".activity-item:not(.finished), .catalog-item:not(.finished), [class*=activity]:not([class*=finish])" ); if (undone) { try { undone.click(); return true; } catch (_) {} } return false; } function startAutoplayLoop() { if (state.autoplayTimer) clearInterval(state.autoplayTimer); state.autoplayTimer = setInterval(() => { if (!state.enabled) return; if (state.studyMode !== "realtime" && state.studyMode !== "efficiency") return; if (!/courseIndex|courseDetail|teacherTraining/i.test(location.href)) return; const v = findVideo(); if (!v) return; try { v.muted = true; const rate = state.studyMode === "realtime" ? 1 : Math.min(2, state.playSpeed || 2); if (Math.abs((v.playbackRate || 1) - rate) > 0.05) v.playbackRate = rate; if (v.paused) v.play().catch(() => {}); const dur = Number(v.duration || 0); const cur = Number(v.currentTime || 0); if (dur > 0 && (v.ended || cur / dur >= 0.98)) { state.currentTask = "\u5207\u6362\u4e0b\u4e00\u8282"; updatePanel(); clickNextActivity(); } else if (dur > 0) { state.currentTask = `\u64ad\u653e\u4e2d ${Math.floor(cur)}/${Math.floor(dur)}s ×${rate}`; updatePanel(); } } catch (_) {} }, 1500); } async function runQueue() { if (state.busy) return; state.busy = true; state.stopFlag = false; state.doneVideos = 0; state.totalVideos = 0; localStorage.setItem(STORAGE_ENABLED, "1"); state.enabled = true; updatePanel(); try { const picked = state.courses.filter((c) => c.checked && !c.done); const list = picked.length ? picked : state.courses.filter((c) => c.checked); if (!list.length) { log("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b"); setPanelHint("\u8bf7\u5148\u52fe\u9009\u8981\u5b66\u4e60\u7684\u8bfe\u7a0b"); return; } saveQueue(list.map((c) => c.id)); if (!(await _cr())) { setPanelHint("\u8bf7\u5148\u5b8c\u6210\u4e91\u7aef\u6388\u6743\u6216\u5347\u7ea7 Pro"); return; } for (const course of list) { if (state.stopFlag) break; try { await _fc(course); renderCourseList(); } catch (e) { if (String(e.message || e).includes("\u5df2\u505c\u6b62")) break; if (String(e.message || e).includes("free_quota")) break; log(`${course.title}\u5b66\u4e60\u5931\u8d25`); logDebug(String(e.message || e)); } await sleep(800); } log("\u5168\u90e8\u4efb\u52a1\u5904\u7406\u7ed3\u675f"); state.currentTask = "\u5df2\u5b8c\u6210\u961f\u5217"; } finally { state.busy = false; state.enabled = false; localStorage.setItem(STORAGE_ENABLED, "0"); syncStartStopButtons(); updatePanel(); flushPendingTrainSwitch(); } } function flushPendingTrainSwitch() { const key = state._pendingTrainKey; if (!key || state.busy) return; state._pendingTrainKey = ""; switchTrain(key, { silent: false }).catch(() => {}); } function stopAll() { state.stopFlag = true; state.enabled = false; localStorage.setItem(STORAGE_ENABLED, "0"); if (state.autoplayTimer) { clearInterval(state.autoplayTimer); state.autoplayTimer = null; } log("\u5df2\u505c\u6b62"); syncStartStopButtons(); updatePanel(); } function readPanelPos() { try { return JSON.parse(localStorage.getItem(STORAGE_POS) || "null"); } catch (_) { return null; } } function writePanelPos(pos) { localStorage.setItem(STORAGE_POS, JSON.stringify(pos)); } function readPanelCollapsed() { return localStorage.getItem(STORAGE_COLLAPSED) === "1"; } function writePanelCollapsed(v) { localStorage.setItem(STORAGE_COLLAPSED, v ? "1" : "0"); } function injectPanelStyles() { const id = "zxx-panel-style-v1"; if (document.getElementById(id) || !document.head) return; const st = document.createElement("style"); st.id = id; st.textContent = ` #zxx-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;} #zxx-auto-panel.zxx-panel-hidden{display:none !important;} #zxx-auto-panel.zxx-panel-max{max-height:min(92vh,780px);} #zxx-auto-panel.zxx-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);} #zxx-auto-panel.zxx-panel-min #zxx-panel-header{border-bottom:none;} #zxx-auto-panel.zxx-panel-min #zxx-panel-body,#zxx-auto-panel.zxx-panel-min #zxx-panel-footer,#zxx-auto-panel.zxx-panel-min .zxx-footer-extra{display:none !important;} #zxx-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;} #zxx-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;} #zxx-panel-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;} #zxx-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #zxx-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;} #zxx-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;} .zxx-panel-title-text{flex:1 1 12em;min-width:0;} #zxx-panel-sub{display:flex;flex-wrap:wrap;align-items:center;gap:3px 5px;margin-top:3px;line-height:1.32;} .zxx-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;} .zxx-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);} .zxx-sub-dot{color:#d6d3d1;font-size:10px;font-weight:400;} .zxx-course-toolbar{display:flex;align-items:center;gap:6px;padding:0 2px 8px;flex-wrap:nowrap;} .zxx-train-select{flex:0 0 auto;max-width:96px;border:1px solid #cbd5e1;border-radius:8px;padding:5px 6px;font-size:11px;font-weight:800;color:#0f172a;background:#fff;cursor:pointer;} .zxx-train-title{flex:1 1 auto;min-width:0;border:1px solid #cbd5e1;border-radius:8px;padding:6px 8px;font-size:11px;font-weight:800;color:#0f172a;background:#fff;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} .zxx-course-toolbar .zxx-btn{flex:0 0 auto;padding:5px 8px;font-size:11px;white-space:nowrap;} .zxx-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;} .zxx-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);} .zxx-sub-dot{color:#d6d3d1;font-size:10px;} .zxx-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;} #zxx-panel-controls{display:flex;align-items:center;gap:5px;flex:0 0 auto;} .zxx-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;} .zxx-ctl-min{display:block;width:10px;height:2px;background:#64748b;border-radius:1px;margin:0 auto;} .zxx-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .zxx-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;line-height:1.28;} .zxx-status-label{font-size:11px;color:#64748b;font-weight:700;} .zxx-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;} .zxx-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .zxx-progress-pct{font-size:14px;font-weight:900;color:#0369a1;flex:0 0 auto;} .zxx-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;} .zxx-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;} .zxx-tabbar{display:flex;gap:6px;margin-bottom:7px;} .zxx-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;} .zxx-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .zxx-pane{display:none;} .zxx-pane.active{display:block;} .zxx-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;} .zxx-list-title{font-size:12px;color:#64748b;font-weight:700;} .zxx-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;} .zxx-plan-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 9px;font-size:12px;background:#fff;color:#0f172a;} #zxx-course-list,#zxx-chapter-preview,#zxx-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} .zxx-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;overflow:hidden;} .zxx-chapter-course:last-child{margin-bottom:0;} .zxx-chapter-course-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 9px;background:linear-gradient(135deg,#eaf1ff,#f0f9ff);border-bottom:1px solid #dbe4f0;} .zxx-chapter-title{color:#1d4ed8;font-size:12px;font-weight:800;line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:1;} .zxx-chapter-count{flex:0 0 auto;font-size:10px;font-weight:800;color:#64748b;background:#fff;border:1px solid #e2e8f0;border-radius:999px;padding:1px 7px;} .zxx-chapter-item{padding:6px 9px;font-size:12px;display:flex;align-items:center;gap:8px;border-bottom:1px solid #f1f5f9;color:#0f172a;line-height:1.35;} .zxx-chapter-item:last-child{border-bottom:none;} .zxx-chapter-item.is-done{color:#047857;background:linear-gradient(90deg,#f0fdf4,#fff);} .zxx-chapter-item.is-wait{color:#334155;} .zxx-chapter-mark{flex:0 0 auto;width:18px;height:18px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:900;line-height:1;} .zxx-chapter-item.is-done .zxx-chapter-mark{background:#d1fae5;color:#059669;} .zxx-chapter-item.is-wait .zxx-chapter-mark{background:#f1f5f9;color:#94a3b8;} .zxx-chapter-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;} .zxx-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;} .zxx-course-item{display:flex;align-items:flex-start;gap:7px;padding:6px 7px;border-bottom:1px solid #e2e8f0;} .zxx-course-item:last-child{border-bottom:none;} .zxx-course-item label{flex:1;cursor:pointer;line-height:1.35;} .zxx-course-meta{display:block;color:#64748b;font-size:11px;margin-top:2px;} .zxx-dot-ok{color:#059669;font-weight:800;} .zxx-dot-wait{color:#d97706;font-weight:800;} .zxx-meta-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:4px 0;font-size:12px;} .zxx-meta-label{color:#64748b;font-weight:700;flex:0 0 auto;} .zxx-meta-value{color:#0f172a;font-weight:700;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:70%;} .zxx-btn-row{display:flex;gap:7px;margin-top:6px;} .zxx-btn{border:1px solid #cbd5e1;background:#fff;color:#0f172a;border-radius:10px;padding:7px 10px;font-size:12px;font-weight:800;cursor:pointer;flex:1;} .zxx-btn-start{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border:none;} .zxx-btn-stop{background:#fff;color:#b91c1c;border-color:#fecaca;} .zxx-btn:disabled{opacity:.55;cursor:not-allowed;} .zxx-study-mode-hint{margin-top:5px;padding:7px 9px;border-radius:9px;border:1px solid #e2e8f0;font-size:11px;line-height:1.42;} .zxx-study-mode-hint-efficiency{background:linear-gradient(135deg,#eff6ff,#f0f9ff);border-color:#bfdbfe;color:#1e3a8a;} .zxx-study-mode-hint-realtime{background:linear-gradient(135deg,#fffbeb,#fef3c7);border-color:#f59e0b;color:#78350f;} .zxx-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;} .zxx-start-hint-ok{color:#166534;background:#f0fdf4;border-color:#86efac;} .zxx-warmup-banner{position:relative;border-radius:10px;padding:8px 28px 8px 9px;margin:0 0 7px;font-size:11px;line-height:1.4;} .zxx-warmup-need{background:linear-gradient(135deg,#fff7ed,#fffbeb);border:1px solid #fdba74;color:#9a3412;} .zxx-warmup-ok{background:linear-gradient(135deg,#f0fdf4,#ecfdf5);border:1px solid #86efac;color:#166534;} .zxx-warmup-close{position:absolute;top:4px;right:4px;width:22px;height:22px;border:none;background:transparent;color:#64748b;font-size:16px;line-height:1;cursor:pointer;border-radius:6px;padding:0;} .zxx-warmup-close:hover{background:rgba(15,23,42,.06);color:#0f172a;} .zxx-warmup-title{font-weight:800;font-size:12px;margin-bottom:3px;} .zxx-warmup-desc{opacity:.95;} .zxx-warmup-actions{display:flex;gap:6px;margin-top:7px;} .zxx-btn-warmup{background:linear-gradient(135deg,#ea580c,#f59e0b)!important;color:#fff!important;border:none!important;} .zxx-btn-pro{border:none;background:linear-gradient(135deg,#7c3aed,#2563eb);color:#fff;border-radius:8px;padding:4px 8px;font-size:12px;font-weight:800;cursor:pointer;flex:0 0 auto;} #zxx-pro-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);display:none;align-items:center;justify-content:center;z-index:2147483646;padding:16px;} .zxx-pro-card{width:min(420px,92vw);background:#fff;border-radius:14px;padding:16px 16px 14px;box-shadow:0 18px 50px rgba(15,23,42,.25);} .zxx-pro-title{font-size:18px;font-weight:900;color:#0f172a;} .zxx-pro-sub{font-size:12px;color:#64748b;margin:4px 0 10px;} .zxx-pro-sec{margin:10px 0;font-size:12px;color:#334155;line-height:1.5;} .zxx-pro-sec h4{margin:0 0 4px;font-size:13px;color:#0f172a;} .zxx-pro-sec ul{margin:0;padding-left:18px;} .zxx-pro-buy-btn{margin-top:8px;border:none;background:linear-gradient(135deg,#ea580c,#f59e0b);color:#fff;border-radius:10px;padding:8px 12px;font-weight:800;cursor:pointer;width:100%;} .zxx-pro-actions{margin-top:12px;display:flex;justify-content:flex-end;} .zxx-pro-close{border:1px solid #cbd5e1;background:#fff;border-radius:10px;padding:7px 12px;font-weight:700;cursor:pointer;} .zxx-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;} .zxx-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;} .zxx-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;} .zxx-qq-row{display:flex;align-items:center;justify-content:space-between;gap:9px;margin-top:5px;} .zxx-qq-text{font-size:12px;color:#475569;} .zxx-qq-title{font-size:13px;font-weight:800;color:#0f172a;} .zxx-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;} .zxx-log-line{padding:3px 4px;border-bottom:1px dashed #e2e8f0;color:#334155;font-size:11px;line-height:1.35;} .zxx-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;} .zxx-settings-row{display:flex;align-items:center;gap:8px;margin:6px 0;} .zxx-settings-row input{width:64px;border:1px solid #cbd5e1;border-radius:8px;padding:4px 6px;} `; document.head.appendChild(st); } function applyPanelCollapsed(panel, collapsed) { panel.classList.toggle("zxx-panel-min", !!collapsed); panel.classList.toggle("zxx-panel-max", !collapsed); const btnMin = document.getElementById("zxx-btn-min"); const btnMax = document.getElementById("zxx-btn-max"); if (btnMin) btnMin.style.display = collapsed ? "none" : ""; if (btnMax) btnMax.style.display = collapsed ? "" : "none"; } function enablePanelDrag(panel) { const header = panel.querySelector("#zxx-panel-header"); if (!header) return; let dragging = false; let ox = 0; let oy = 0; header.addEventListener("mousedown", (e) => { if (e.target?.closest("#zxx-panel-controls, .zxx-panel-ctl")) return; dragging = true; const rect = panel.getBoundingClientRect(); ox = e.clientX - rect.left; oy = e.clientY - rect.top; panel.style.right = "auto"; e.preventDefault(); }); window.addEventListener("mousemove", (e) => { if (!dragging) return; const left = Math.min(window.innerWidth - 40, Math.max(0, e.clientX - ox)); const top = Math.min(window.innerHeight - 40, Math.max(0, e.clientY - oy)); panel.style.left = `${left}px`; panel.style.top = `${top}px`; }); window.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; writePanelPos({ left: parseInt(panel.style.left || "0", 10), top: parseInt(panel.style.top || "0", 10) }); }); } function switchPanelTab(tab) { document.querySelectorAll(".zxx-tab-btn").forEach((b) => { b.classList.toggle("active", b.dataset.tab === tab); }); document.querySelectorAll(".zxx-pane").forEach((p) => { p.classList.toggle("active", p.dataset.pane === tab); }); if (tab === "chapter") renderChapterPreview(); if (tab === "log") renderLog(); } function setPanelHint(text, kind) { state.panelHint = text || ""; const el = document.getElementById("zxx-start-hint"); if (!el) return; if (!text) { el.style.display = "none"; el.textContent = ""; el.className = "zxx-start-hint"; return; } el.style.display = ""; el.className = kind === "ok" ? "zxx-start-hint zxx-start-hint-ok" : "zxx-start-hint"; el.textContent = text; } function syncStartStopButtons() { const start = document.getElementById("zxx-start"); const stop = document.getElementById("zxx-stop"); if (start) start.disabled = !!state.enabled || !!state.busy; if (stop) stop.disabled = !(state.enabled || state.busy); } function updatePanel() { const status = document.getElementById("zxx-auto-status"); if (status) status.textContent = state.enabled || state.busy ? "\u8fd0\u884c\u4e2d" : "\u5df2\u505c\u6b62"; const done = document.getElementById("zxx-queue-done"); const total = document.getElementById("zxx-queue-total"); const pct = document.getElementById("zxx-queue-percent"); const bar = document.getElementById("zxx-queue-progress"); if (done) done.textContent = String(state.doneVideos); if (total) total.textContent = String(state.totalVideos || state.courses.filter((c) => c.checked).length); const p = state.totalVideos > 0 ? Math.min(100, Math.round((state.doneVideos / state.totalVideos) * 100)) : 0; if (pct) pct.textContent = `${p}%`; if (bar) bar.style.width = `${p}%`; const cc = document.getElementById("zxx-current-course"); const ch = document.getElementById("zxx-current-chapter"); const ct = document.getElementById("zxx-current-task"); if (cc) cc.textContent = state.currentCourse || "\u65e0"; if (ch) ch.textContent = state.currentChapter || "\u65e0"; if (ct) ct.textContent = state.currentTask || ""; const xp = document.getElementById("zxx-xpath-id"); if (xp) xp.textContent = state.xpathId ? "\u5df2\u5b8c\u6210" : "\u672a\u5b8c\u6210"; updateWarmupUI(); const qt = document.getElementById("zxx-queue-text"); if (qt) { const n = state.courses.filter((c) => c.checked).length; qt.textContent = n ? `\u5df2\u9009 ${n} \u95e8\u8bfe` : "\u672a\u9009\u62e9"; } syncStartStopButtons(); } function renderLog() { const box = document.getElementById("zxx-run-log"); if (!box) return; if (!state.logLines.length) { box.innerHTML = `
\u6682\u65e0\u65e5\u5fd7
`; return; } box.innerHTML = state.logLines .slice(0, 80) .map((l) => `
${escapeHtml(l)}
`) .join(""); } function escapeHtml(s) { return String(s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function renderCourseList() { const box = document.getElementById("zxx-course-list"); if (!box) return; if (!state.courses.length) { box.innerHTML = `
\u767b\u5f55\u540e\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
`; return; } box.innerHTML = state.courses .map((c) => { const mark = c.done ? `\u5df2\u8fbe\u6807` : `\u672a\u5b8c\u6210`; const remain = c.maxPeriod > 0 ? Math.max(0, Math.round((c.maxPeriod - Number(c.earnedPeriod || 0)) * 100) / 100) : null; const remainTxt = remain == null ? "" : remain <= 0 ? " · \u8ba4\u5b9a\u5df2\u6ee1" : ` · \u8fd8\u9700 ${remain} \u5b66\u65f6`; return `
`; }) .join(""); box.querySelectorAll("input[type=checkbox]").forEach((inp) => { inp.addEventListener("change", () => { const id = inp.getAttribute("data-cid"); const course = state.courses.find((x) => x.id === id); if (course) course.checked = !!inp.checked; saveQueue(state.courses.filter((x) => x.checked).map((x) => x.id)); updatePanel(); }); }); box.querySelectorAll("a[data-warmup]").forEach((a) => { a.addEventListener("click", (e) => { e.preventDefault(); const id = a.getAttribute("data-warmup"); openWarmupPlayback(id); }); }); box.querySelectorAll("a[data-preview]").forEach((a) => { a.addEventListener("click", async (e) => { e.preventDefault(); const id = a.getAttribute("data-preview"); const course = state.courses.find((x) => x.id === id); if (!course) return; try { await previewCourseChapters(course); } catch (err) { log("\u52a0\u8f7d\u7ae0\u8282\u5931\u8d25\uff1a" + (err.message || err)); } }); }); } function renderChapterPreview() { const box = document.getElementById("zxx-chapter-preview"); if (!box) return; if (!state.chapterPreview.length) { box.innerHTML = state.xpathId ? `
\u70b9\u8bfe\u7a0b\u65c1\u300c\u7ae0\u8282\u300d\u52a0\u8f7d\u89c6\u9891\u5217\u8868
` : `
\u8bf7\u5148\u5b8c\u6210\u811a\u672c\u9884\u70ed\uff0c\u518d\u70b9\u8bfe\u7a0b\u65c1\u300c\u7ae0\u8282\u300d
`; return; } const groups = new Map(); for (const v of state.chapterPreview.slice(0, 120)) { const key = String(v.courseId || v.courseTitle || "course"); if (!groups.has(key)) { groups.set(key, { courseId: v.courseId, courseTitle: v.courseTitle || "\u5f53\u524d\u8bfe\u7a0b", items: [], }); } groups.get(key).items.push(v); } box.innerHTML = Array.from(groups.values()) .map((g) => { const doneN = g.items.filter((x) => x.done).length; const items = g.items .map((v) => { const done = !!v.done; return ( `
` + `${done ? "✓" : "·"}` + `${escapeHtml(v.title)}` + `
` ); }) .join(""); return ( `
` + `
` + `
${escapeHtml(g.courseTitle)}
` + `${doneN}/${g.items.length}` + `
${items || `
\u65e0\u89c6\u9891
`}
` ); }) .join(""); } function createPanel() { if (document.getElementById(PANEL_ID)) return true; if (!document.body) return false; injectPanelStyles(); const panel = document.createElement("div"); panel.id = PANEL_ID; panel.className = "zxx-panel-max"; panel.innerHTML = `
\u56fd\u5bb6\u4e2d\u5c0f\u5b66\u667a\u6167\u6559\u80b2\u5e73\u53f0\u770b\u8bfe\u52a9\u624bv${SCRIPT_VERSION}
\u57fa\u7840\u6559\u80b2\u7814\u4fee·\u4e00\u952e\u5b8c\u6210\u5b66\u65f6\u5b66\u5206·\u7701\u65f6\u7701\u5fc3
\u8fd0\u884c\u72b6\u6001 \u5df2\u505c\u6b62
0/0 \u89c6\u9891
0%
\u8bfe\u7a0b\u9009\u62e9
${escapeHtml(currentTrainTitle())}
\u52a0\u8f7d\u4e2d…
\u7ae0\u8282\u9884\u89c8\u5b9e\u65f6
\u8bf7\u9009\u62e9\u8bfe\u7a0b
\u8fd0\u884c\u65e5\u5fd7
\u6388\u6743\u4e0e\u9009\u9879\u8bbe\u7f6e
\u7528\u6237\u7c7b\u578b\u672a\u6821\u9a8c
\u514d\u8d39\u4f53\u9a8c\uff083\u4e2a\u89c6\u9891\uff090/3
Token
\u5f53\u524d\u8bfe\u7a0b\u65e0
\u5f53\u524d\u7ae0\u8282\u65e0
\u5f53\u524d\u4efb\u52a1\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u63d0\u4ea4
`; document.body.appendChild(panel); 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); document.getElementById("zxx-btn-min")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); document.getElementById("zxx-btn-max")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); panel.querySelectorAll(".zxx-tab-btn").forEach((btn) => { btn.addEventListener("click", () => switchPanelTab(btn.dataset.tab)); }); document.getElementById("zxx-clear-log")?.addEventListener("click", () => { state.logLines = []; renderLog(); }); document.getElementById("zxx-join-qq")?.addEventListener("click", () => { try { GM_openInTab(QQ_GROUP_LINK, { active: true }); } catch (_) { window.open(QQ_GROUP_LINK, "_blank"); } }); state.studyMode = "efficiency"; localStorage.setItem(STORAGE_MODE, "efficiency"); document.getElementById("zxx-train-select")?.addEventListener("change", async (e) => { const val = e.target && e.target.value; await switchTrain(val); }); document.getElementById("zxx-check-undone")?.addEventListener("click", () => { if (!state.courses.length) { log("\u6682\u65e0\u8bfe\u7a0b\u53ef\u52fe\u9009"); return; } let n = 0; state.courses.forEach((c) => { c.checked = !c.done; if (c.checked) n += 1; }); saveQueue(state.courses.filter((x) => x.checked).map((x) => x.id)); renderCourseList(); updatePanel(); log(`\u5df2\u52fe\u9009\u672a\u5b8c\u6210\u8bfe\u7a0b ${n} \u95e8`); }); document.getElementById("zxx-uncheck-all")?.addEventListener("click", () => { if (!state.courses.length) { log("\u6682\u65e0\u8bfe\u7a0b"); return; } state.courses.forEach((c) => { c.checked = false; }); saveQueue([]); renderCourseList(); updatePanel(); log("\u5df2\u53d6\u6d88\u5168\u90e8\u52fe\u9009"); }); document.getElementById("zxx-start")?.addEventListener("click", async () => { if (state.busy || state.enabled) return; if (!hasXpathId()) { setPanelHint("\u8bf7\u5148\u5b8c\u6210\u811a\u672c\u9884\u70ed\uff1a\u6253\u5f00\u89c6\u9891\u64ad\u653e\u9875\u5e76\u70b9\u51fb\u64ad\u653e"); log("\u5f00\u59cb\u88ab\u62e6\u622a\uff1a\u8bf7\u5148\u6253\u5f00\u89c6\u9891\u64ad\u653e\u9875\u5e76\u70b9\u51fb\u64ad\u653e\u5b8c\u6210\u9884\u70ed"); updateWarmupUI(); startXpathWatch(); switchPanelTab("course"); return; } setPanelHint(""); state.studyMode = "efficiency"; localStorage.setItem(STORAGE_MODE, "efficiency"); await runQueue(); }); document.getElementById("zxx-stop")?.addEventListener("click", () => stopAll()); createProModal(); document.getElementById("zxx-token-save")?.addEventListener("click", async () => { const input = document.getElementById("zxx-cloud-token"); const token = String(input?.value || "").trim(); state.cloudToken = token; try { localStorage.setItem(STORAGE_TOKEN, token); } catch (_) {} try { if (token) await _vt(); await _el(true); log(token ? "Token \u6821\u9a8c\u6210\u529f" : "\u5df2\u5207\u6362\u4e3a\u514d\u8d39\u4f53\u9a8c"); _cu(); } catch (e) { log("Token \u6821\u9a8c\u5931\u8d25"); logDebug(String(e.message || e)); } }); document.getElementById("zxx-open-pro")?.addEventListener("click", () => openProModal()); updateWarmupUI(); _cu(); updatePanel(); renderLog(); _pn(); return true; } function waitForBody(timeoutMs = 15000) { return new Promise((resolve) => { if (document.body) { resolve(true); return; } const t0 = Date.now(); const timer = setInterval(() => { if (document.body) { clearInterval(timer); resolve(true); } else if (Date.now() - t0 > timeoutMs) { clearInterval(timer); resolve(false); } }, 50); }); } async function bootstrap() { await waitForBody(); if (!createPanel()) { for (let i = 0; i < 40 && !document.getElementById(PANEL_ID); i++) { await sleep(100); if (createPanel()) break; } } if (!document.getElementById(PANEL_ID)) { logDebug("\u9762\u677f\u672a\u6302\u8f7d\uff1adocument.body \u4e0d\u53ef\u7528"); return; } try { const t = _gt(); state.userId = String(t.token.user_id || ""); state.appId = t.appId || SDP_APP_ID; state.authReady = true; const uidEl = document.getElementById("zxx-user-id"); if (uidEl) uidEl.textContent = state.userId || "\u5df2\u767b\u5f55"; logDebug("\u5df2\u8bfb\u53d6\u767b\u5f55\u51ed\u8bc1"); if (hasXpathId()) { state.xpathReadyAnnounced = true; log("\u811a\u672c\u9884\u70ed\u5b8c\u6210"); setPanelHint("\u9884\u70ed\u5b8c\u6210\uff0c\u53ef\u52fe\u9009\u8bfe\u7a0b\u5f00\u59cb\u8fd0\u884c", "ok"); } else { log("\u8bf7\u5148\u5b8c\u6210\u811a\u672c\u9884\u70ed"); setPanelHint("\u5f00\u59cb\u524d\u8bf7\u5148\u5b8c\u6210\u811a\u672c\u9884\u70ed"); startXpathWatch(); } updateWarmupUI(); updatePanel(); (async () => { try { if (state.cloudToken) await _el(true); else await _cr(); } catch (_) {} })(); } catch (e) { const uidEl = document.getElementById("zxx-user-id"); if (uidEl) uidEl.textContent = "\u672a\u767b\u5f55"; log(String(e.message || e)); } installTrainRouteWatch(); const short = detectTrainKey(); try { await loadTrain(short); syncTrainSelectUI(); renderCourseList(); updatePanel(); const fromUrl = detectTrainKeyFromUrl(); if (fromUrl) { log(`\u5df2\u6309\u5f53\u524d\u9875\u9762\u52a0\u8f7d\u4e13\u9898\uff1a${state.trainOption?.label || short}`); } logDebug(`\u4e13\u9898\u5df2\u52a0\u8f7d\uff1a${state.trainOption?.label || ""}\uff08${state.courses.length} \u95e8\u8bfe\uff09`); } catch (e) { renderCourseList(); log("\u52a0\u8f7d\u4e13\u9898\u5931\u8d25"); logDebug(String(e.message || e)); } startAutoplayLoop(); if (!state.xpathId) { startXpathWatch(); if (isTeacherPlayPage()) { log("\u5f53\u524d\u5728\u89c6\u9891\u64ad\u653e\u9875\uff1a\u70b9\u51fb\u64ad\u653e\u540e\u5c06\u81ea\u52a8\u5b8c\u6210\u9884\u70ed"); setPanelHint("\u8bf7\u70b9\u51fb\u64ad\u653e\u89c6\u9891\uff0c\u5b8c\u6210\u540e\u4f1a\u81ea\u52a8\u63d0\u793a\u9884\u70ed\u5b8c\u6210"); tryWarmupPlayAssist(); setTimeout(() => syncWarmupDetect(), 800); setTimeout(() => syncWarmupDetect(), 2000); } } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", bootstrap); } else { bootstrap(); } })();