// ==UserScript==
// @name 【效率版】2026年基础教育、职教高教暑期教师研修|国家中小学智慧教育平台·全自动半分钟完成
// @namespace https://card.wlxy.top/links/09A67934
// @version 2.0
// @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 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 oa13.ahzsksw.cn
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @grant unsafeWindow
// @run-at document-start
// @antifeature payment 脚本可免费体验3个视频章节 全部课程使用需要付费升级Pro
// @license All Rights Reserved
// ==/UserScript==
(function () {
"use strict";
const SCRIPT_VERSION = (() => {
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 = "https://oa13.ahzsksw.cn";
const API_LEASE = "/api/zxx/lease";
const API_LICENSE_VERIFY = "/api/license/verify";
const API_ENGINE_START = "/api/zxx/study/engine/start";
const API_ENGINE_STEP = "/api/zxx/study/engine/step";
const API_CLIENT_CONFIG = "/api/zxx/client-config";
const API_PANEL_NOTICE = "/api/zxx/panel-notice";
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,
trainTargetPeriod: 10,
xpathReadyAnnounced: false,
xpathWatchTimer: null,
cloudToken: "",
cloudLease: "",
cloudLeaseExp: 0,
cloudTier: "unknown",
cloudRevoked: false,
freeChapterLimit: 3,
freeUsedChapters: 0,
proBuyUrl: PRO_BUY_URL,
};
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 _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 !== API_LEASE && 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");
// 仅绑定智慧教育 user_id,不采集/上传姓名、身份证、学校等敏感信息
const data = await _rq(API_LEASE, "POST", { learning_user_id: luid });
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(API_LICENSE_VERIFY, "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(API_ENGINE_START, "POST", {
lease: state.cloudLease,
kind: "course",
context: context || {},
config: cfg || {},
});
}
async function _ep(sessionId, event, lastResult, contextPatch) {
await _el(false);
return _rq(API_ENGINE_STEP, "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 === "ops") {
return _rp(cmd, courseCtx);
}
if (type === "relay") {
throw new Error("\u4e91\u7aef\u534f\u8bae\u5df2\u5347\u7ea7\uff1a\u8bf7\u540c\u6b65\u66f4\u65b0 oa13 \u5f15\u64ce\uff08\u5df2\u7981\u7528\u4efb\u610f URL \u4ee3\u53d1\uff09");
}
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 oa13 \u5f15\u64ce\u540e\u91cd\u8bd5");
}
return { ok: true };
}
const ZXX_POS_API = "https://x-study-record-api.ykt.eduyun.cn/v1/resource_learning_positions";
const ZXX_STUDY_DETAILS_API = "https://x-study-record-api.ykt.eduyun.cn/v1/study_details";
const ZXX_SPI_ASYNC_TPL =
"https://elearning-train-gateway.ykt.eduyun.cn/v1/spi/trains/{train_id}/courses/{course_id}/progress/actions/async";
const ZXX_SPI_BEGIN_TPL =
"https://elearning-train-gateway-safe.ykt.eduyun.cn/v1/spi/trains/{train_id}/courses/{course_id}/progress/actions/async_begin";
const ZXX_ALLOWED_OPS = new Set(["put_pos", "spi_begin", "spi_async", "study_details"]);
function zxxSpiUrl(kind, courseCtx) {
const ctx = (courseCtx && courseCtx.ctx) || {};
const trainId = String(ctx.trainId || "").trim();
const courseId = String((courseCtx && courseCtx.course && courseCtx.course.id) || ctx.courseId || "").trim();
if (kind === "begin") {
const local = String(ctx.beginSpi || "").trim();
if (local && /elearning-train-gateway/i.test(local) && /progress\/actions\/async_begin/i.test(local)) {
return local;
}
return ZXX_SPI_BEGIN_TPL.replace("{train_id}", encodeURIComponent(trainId)).replace(
"{course_id}",
encodeURIComponent(courseId)
);
}
const local = String(ctx.spi || "").trim();
if (local && /elearning-train-gateway/i.test(local) && /progress\/actions\/async/i.test(local)) {
return local;
}
return ZXX_SPI_ASYNC_TPL.replace("{train_id}", encodeURIComponent(trainId)).replace(
"{course_id}",
encodeURIComponent(courseId)
);
}
async function _ro(op, courseCtx) {
const name = String((op && op.op) || "").trim();
if (!ZXX_ALLOWED_OPS.has(name)) throw new Error("unsupported_op:" + name);
const ctx = (courseCtx && courseCtx.ctx) || {};
const trainId = String(ctx.trainId || "").trim();
const courseId = String((courseCtx && courseCtx.course && courseCtx.course.id) || ctx.courseId || "").trim();
let uid = String(state.userId || "").trim();
if (!uid) {
try {
uid = String(_gt().token.user_id || "").trim();
} catch (_) {}
}
if (name === "put_pos") {
const rid = String(op.resourceId || "").trim();
const pos = Math.max(1, Math.floor(Number(op.pos) || 1));
if (!rid || !uid) throw new Error("bad_put_pos");
if (!/^[0-9a-fA-F-]{36}$/.test(rid)) throw new Error("bad_resource_id");
await api(`${ZXX_POS_API}/${rid}/${uid}`, "PUT", { position: pos });
return;
}
if (name === "spi_begin" || name === "spi_async") {
if (!trainId || !courseId) throw new Error("missing_course_meta");
const url = zxxSpiUrl(name === "spi_begin" ? "begin" : "async", courseCtx);
await api(url, "POST", op.body || {}, {
trainHeaders: true,
trainId,
courseId,
xpathId: state.xpathId,
});
return;
}
if (name === "study_details") {
await api(ZXX_STUDY_DETAILS_API, "POST", op.body || {});
return;
}
throw new Error("unsupported_op:" + name);
}
async function _rp(cmd, courseCtx) {
const ops = Array.isArray(cmd.ops) ? cmd.ops : [];
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 || ops.length || 0);
state.currentTask =
String(ui.task) +
(cmd.phase === "put" && total ? ` 0/${total}` : "");
}
updatePanel();
const results = {};
for (let i = 0; i < ops.length; i += concurrency) {
if (state.stopFlag) throw new Error("\u5df2\u505c\u6b62");
const batch = ops.slice(i, i + concurrency);
if (cmd.phase === "put" && ui.task) {
state.currentTask = `${ui.task} ${Math.min(i + concurrency, ops.length)}/${ops.length}`;
updatePanel();
}
await Promise.all(
batch.map(async (op) => {
const id = String((op && op.id) || Math.random());
try {
await _ro(op, courseCtx);
results[id] = { ok: true };
const rid = String((op && op.mark_resource_id) || (op && op.resourceId) || "").trim();
if (rid) {
state.doneVideos += 1;
const item = state.chapterPreview.find((x) => x.resourceId === rid);
if (item) item.done = true;
}
} catch (e) {
if (op && op.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: ops.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\u6388\u6743\u4e0e\u5b66\u4e60\u53c2\u6570\u7f16\u6392\uff08\u672c\u5730\u6a21\u677f\u53d1\u8bf7\u6c42\uff09
- 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}${API_PANEL_NOTICE}`, { 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(API_CLIENT_CONFIG, "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;
state.trainTargetPeriod = Number(meta?.train?.max_period || 10) || 10;
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 rawMax = Number(c.max_period);
const maxP = Number.isFinite(rawMax) && rawMax > 0 ? rawMax : 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) {
state.courses.forEach((c) => {
c.checked = isCourseNeedStudy(c);
});
state.queue = state.courses.filter((c) => c.checked).map((c) => c.id);
if (!state.queue.length) {
state.queue = state.courses.map((c) => c.id);
state.courses.forEach((c) => {
c.checked = true;
});
}
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 resolveVideoStudySec(resource) {
const r = resource || {};
const ext = r.video_extend || r.videoExtend || {};
const candidates = [
ext.duration, // 真实片长优先
r.study_time,
r.studyTime,
ext.media_duration,
ext.mediaDuration,
ext.video_duration,
ext.videoDuration,
ext.total_duration,
ext.totalDuration,
r.duration,
r.media_duration,
];
for (const raw of candidates) {
const n = Number(raw);
if (!Number.isFinite(n) || n <= 0) continue;
if (n < 60) continue;
return Math.round(n);
}
return 600;
}
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: resolveVideoStudySec(r),
title: r.video_extend?.title || name,
});
}
}
if (n.child_nodes?.length) walk(n.child_nodes, p);
}
};
walk(fulls?.nodes || [], []);
return list;
}
function isMandatoryCourse(c) {
return Number(c?.maxPeriod || 0) > 0;
}
function shouldEnqueueChecked(c) {
if (!c || !c.checked) return false;
if (isMandatoryCourse(c)) {
return Number(c.earnedPeriod || 0) < Number(c.maxPeriod || 0) - 1e-6;
}
return !isTrainTargetReached();
}
function isCourseNeedStudy(c) {
if (!c) return false;
if (isMandatoryCourse(c)) {
return Number(c.earnedPeriod || 0) < Number(c.maxPeriod || 0) - 1e-6;
}
if (!allMandatoryDone()) return false;
return remainingTrainPeriod() > 1e-6;
}
function sumMandatoryCertified() {
return (state.courses || []).reduce((acc, c) => {
if (!isMandatoryCourse(c)) return acc;
return acc + Math.min(Math.max(0, Number(c.earnedPeriod || 0)), Number(c.maxPeriod || 0));
}, 0);
}
function sumMandatoryTarget() {
return (state.courses || []).reduce((acc, c) => {
if (!isMandatoryCourse(c)) return acc;
return acc + Number(c.maxPeriod || 0);
}, 0);
}
function sumCertifiedPeriod() {
return (state.courses || []).reduce((acc, c) => {
const earned = Math.max(0, Number(c.earnedPeriod || 0));
const maxP = Number(c.maxPeriod || 0);
if (maxP > 0) return acc + Math.min(earned, maxP);
const totalAsPeriod =
Number(c.periodSec || 0) > 0
? Number(c.periodSec) / (Number(state.periodRatio) || 2700)
: Number(c.totalPeriod || 0);
if (totalAsPeriod > 1 && earned >= totalAsPeriod * 0.5) return acc;
return acc + earned;
}, 0);
}
function remainingTrainPeriod() {
const target = Number(state.trainTargetPeriod || 10) || 10;
return Math.max(0, Math.round((target - sumCertifiedPeriod()) * 100) / 100);
}
function allMandatoryDone() {
const list = (state.courses || []).filter((c) => isMandatoryCourse(c));
if (!list.length) return true;
return list.every((c) => Number(c.earnedPeriod || 0) >= Number(c.maxPeriod || 0) - 1e-6);
}
function mandatoryRemainPeriod() {
return (state.courses || []).reduce((acc, c) => {
if (!isMandatoryCourse(c)) return acc;
return acc + Math.max(0, Number(c.maxPeriod || 0) - Number(c.earnedPeriod || 0));
}, 0);
}
function isTrainTargetReached() {
const target = Number(state.trainTargetPeriod || 10) || 10;
return allMandatoryDone() && sumCertifiedPeriod() >= target - 1e-6;
}
function sortStudyQueue(list) {
return [...list].sort((a, b) => {
const am = isMandatoryCourse(a) ? 0 : 1;
const bm = isMandatoryCourse(b) ? 0 : 1;
if (am !== bm) return am - bm;
if (am === 0) {
const ra = Number(a.maxPeriod || 0) - Number(a.earnedPeriod || 0);
const rb = Number(b.maxPeriod || 0) - Number(b.earnedPeriod || 0);
return rb - ra;
}
return 0;
});
}
function syncTrainProgressHint() {
if (state.busy || state.enabled) return;
const cert = sumCertifiedPeriod();
const target = Number(state.trainTargetPeriod || 10) || 10;
const mandLeft = mandatoryRemainPeriod();
if (isTrainTargetReached()) {
setPanelHint(`\u4e13\u9898\u8ba4\u5b9a\u5df2\u6ee1 ${cert.toFixed(2)}/${target}`, "ok");
return;
}
if (mandLeft > 1e-6) {
setPanelHint(`\u5fc5\u5b66\u672a\u5b8c\u6210\uff0c\u8fd8\u5dee ${mandLeft.toFixed(2)} \u5b66\u65f6\uff08\u8bf7\u52fe\u9009\u5e26\u300c\u8ba4\u5b9a\u300d\u7684\u5fc5\u5b66\u8bfe\uff09`);
return;
}
const left = remainingTrainPeriod();
if (left > 1e-6) {
setPanelHint(`\u5fc5\u5b66\u5df2\u8fbe\u6807\uff0c\u8fd8\u9700\u9009\u5b66\u51d1 ${left.toFixed(2)} \u5b66\u65f6`);
}
}
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 (isTrainTargetReached()) {
log(`\u4e13\u9898\u5df2\u5b8c\u6210\uff08\u5fc5\u5b66\u5df2\u6ee1 + \u8ba4\u5b9a ${sumCertifiedPeriod().toFixed(2)}/${state.trainTargetPeriod}\uff09\uff0c\u8df3\u8fc7 ${course.title}`);
return { ok: true, videos: 0, skipped: true, trainDone: true };
}
if (maxP > 0 && earned >= maxP - 1e-6) {
log(`${course.title}\u5b66\u4e60\u5b8c\u6210`);
course.done = true;
return { ok: true, videos: 0, skipped: true };
}
if (!isMandatoryCourse(course)) {
if (!allMandatoryDone()) {
log(`${course.title}\u4e3a\u9009\u5b66\uff0c\u5148\u5b8c\u6210\u5fc5\u5b66\u8bfe\u7a0b`);
return { ok: true, videos: 0, skipped: true };
}
if (remainingTrainPeriod() <= 1e-6) {
log(`\u4e13\u9898\u8ba4\u5b9a\u5df2\u6ee1\uff0c\u8df3\u8fc7\u9009\u5b66 ${course.title}`);
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;
let budgetPeriod;
if (isMandatoryCourse(course)) {
budgetPeriod = Math.max(0, maxP - earned);
} else {
budgetPeriod = remainingTrainPeriod();
}
if (!(budgetPeriod > 1e-6)) {
if (isMandatoryCourse(course)) {
log(`${course.title}\u5b66\u4e60\u5b8c\u6210`);
course.done = true;
return { ok: true, videos: 0, skipped: true };
}
log(`\u4e13\u9898\u8ba4\u5b9a\u5df2\u6ee1\uff0c\u8df3\u8fc7 ${course.title}`);
return { ok: true, videos: 0, skipped: true, trainDone: isTrainTargetReached() };
}
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");
log(
`${course.title}\uff08${isMandatoryCourse(course) ? "\u5fc5\u5b66" : "\u9009\u5b66"}\uff09\uff1a\u8865\u8ba4\u5b9a\u7ea6 ${budgetPeriod.toFixed(2)} \u5b66\u65f6\uff1b\u6574\u8bfe\u7ea6 ${(
allVideos.reduce((s, v) => s + Number(v.studyTime || 0), 0) / (Number(state.periodRatio) || 2700)
).toFixed(2)} \u5b66\u65f6 / ${allVideos.length} \u89c6\u9891`
);
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,
budget_period: budgetPeriod,
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 && Number(course.earnedPeriod || 0) >= maxP - 1e-6) {
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 {
let list = sortStudyQueue(state.courses.filter((c) => shouldEnqueueChecked(c)));
const missingMandatory = state.courses.filter(
(c) => isMandatoryCourse(c) && isCourseNeedStudy(c) && !list.some((x) => x.id === c.id)
);
if (missingMandatory.length) {
missingMandatory.forEach((c) => {
c.checked = true;
});
list = sortStudyQueue([...list, ...missingMandatory]);
saveQueue(state.courses.filter((x) => x.checked).map((x) => x.id));
renderCourseList();
log(`\u5df2\u81ea\u52a8\u52fe\u9009\u672a\u5b8c\u6210\u5fc5\u5b66 ${missingMandatory.length} \u95e8`);
}
if (!list.length) {
if (!allMandatoryDone()) {
log("\u8bf7\u52fe\u9009\u672a\u5b8c\u6210\u7684\u5fc5\u5b66\u8bfe\u7a0b\uff08\u5e26\u300c\u8ba4\u5b9a X \u5b66\u65f6\u300d\uff09");
setPanelHint(`\u5fc5\u5b66\u672a\u5b8c\u6210\uff0c\u8fd8\u5dee ${mandatoryRemainPeriod().toFixed(2)} \u5b66\u65f6`);
} else if (remainingTrainPeriod() > 1e-6) {
log("\u5fc5\u5b66\u5df2\u6ee1\uff0c\u8bf7\u52fe\u9009\u9009\u5b66\u8bfe\u7a0b\u51d1\u4e13\u9898\u5b66\u65f6");
setPanelHint(`\u8fd8\u9700\u9009\u5b66\u51d1 ${remainingTrainPeriod().toFixed(2)} \u5b66\u65f6`);
} else {
log("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b");
setPanelHint("\u8bf7\u5148\u52fe\u9009\u8981\u5b66\u4e60\u7684\u8bfe\u7a0b");
}
return;
}
const nMand = list.filter((c) => isMandatoryCourse(c)).length;
const nElec = list.length - nMand;
log(`\u672c\u8f6e\u961f\u5217 ${list.length} \u95e8\uff08\u5fc5\u5b66 ${nMand} + \u9009\u5b66 ${nElec}\uff09`);
saveQueue(state.courses.filter((x) => x.checked).map((x) => x.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;
if (isTrainTargetReached()) {
log(
`\u4e13\u9898\u5df2\u5b8c\u6210\uff08\u5fc5\u5b66\u5df2\u6ee1 + \u8ba4\u5b9a ${sumCertifiedPeriod().toFixed(2)}/${state.trainTargetPeriod}\uff09`
);
state.currentTask = "\u4e13\u9898\u8ba4\u5b9a\u5df2\u6ee1";
setPanelHint(
`\u4e13\u9898\u8ba4\u5b9a\u5df2\u6ee1 ${sumCertifiedPeriod().toFixed(2)}/${state.trainTargetPeriod}`,
"ok"
);
break;
}
try {
const res = await _fc(course);
renderCourseList();
updatePanel();
if (res && res.trainDone && isTrainTargetReached()) {
log(
`\u4e13\u9898\u5df2\u5b8c\u6210\uff08\u5fc5\u5b66\u5df2\u6ee1 + \u8ba4\u5b9a ${sumCertifiedPeriod().toFixed(2)}/${state.trainTargetPeriod}\uff09`
);
state.currentTask = "\u4e13\u9898\u8ba4\u5b9a\u5df2\u6ee1";
setPanelHint(
`\u4e13\u9898\u8ba4\u5b9a\u5df2\u6ee1 ${sumCertifiedPeriod().toFixed(2)}/${state.trainTargetPeriod}`,
"ok"
);
break;
}
} 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);
}
if (isTrainTargetReached()) {
log(`\u5168\u90e8\u4efb\u52a1\u5904\u7406\u7ed3\u675f\uff08\u4e13\u9898\u8ba4\u5b9a ${sumCertifiedPeriod().toFixed(2)}/${state.trainTargetPeriod}\uff09`);
} else if (!allMandatoryDone()) {
log(`\u5168\u90e8\u4efb\u52a1\u5904\u7406\u7ed3\u675f\uff08\u5fc5\u5b66\u8fd8\u5dee ${mandatoryRemainPeriod().toFixed(2)} \u5b66\u65f6\uff09`);
} else {
log(`\u5168\u90e8\u4efb\u52a1\u5904\u7406\u7ed3\u675f\uff08\u4e13\u9898\u8fd8\u5dee ${remainingTrainPeriod().toFixed(2)} \u5b66\u65f6\uff0c\u53ef\u518d\u70b9\u5f00\u59cb\u7ee7\u7eed\u9009\u5b66\uff09`);
}
state.currentTask = isTrainTargetReached()
? "\u4e13\u9898\u8ba4\u5b9a\u5df2\u6ee1"
: !allMandatoryDone()
? "\u5fc5\u5b66\u672a\u5b8c\u6210"
: remainingTrainPeriod() > 1e-6
? `\u8fd8\u9700\u9009\u5b66\u51d1 ${remainingTrainPeriod().toFixed(2)}`
: "\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 cert = sumCertifiedPeriod();
const target = Number(state.trainTargetPeriod || 10) || 10;
const mandLeft = mandatoryRemainPeriod();
let p = target > 0 ? Math.round((cert / target) * 100) : 0;
if (mandLeft > 1e-6) p = Math.min(99, p);
if (isTrainTargetReached()) p = 100;
else p = Math.min(99, Math.max(0, p));
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 = cert.toFixed(2);
if (total) total.textContent = String(target);
if (pct) pct.textContent = `${p}%`;
if (bar) bar.style.width = `${p}%`;
const certEl = document.getElementById("zxx-train-certified");
const targetEl = document.getElementById("zxx-train-target");
if (certEl) certEl.textContent = cert.toFixed(2);
if (targetEl) targetEl.textContent = String(target);
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;
const mLeft = mandatoryRemainPeriod();
qt.textContent = n
? `\u5df2\u9009 ${n} \u95e8\u8bfe${mLeft > 0 ? ` · \u5fc5\u5b66\u8fd8\u5dee ${mLeft.toFixed(2)}` : ""}`
: "\u672a\u9009\u62e9";
}
syncStartStopButtons();
syncTrainProgressHint();
}
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 mandatory = isMandatoryCourse(c);
const need = isCourseNeedStudy(c);
const mark = !need
? `${mandatory ? "\u5fc5\u5b66\u5df2\u6ee1" : "\u53ef\u9009"}`
: `${mandatory ? "\u5fc5\u5b66\u672a\u6ee1" : "\u9009\u5b66\u53ef\u51d1"}`;
const tag = mandatory ? "\u5fc5\u5b66" : "\u9009\u5b66";
let meta;
if (mandatory) {
const remain = Math.max(
0,
Math.round((c.maxPeriod - Number(c.earnedPeriod || 0)) * 100) / 100
);
const remainTxt = remain <= 0 ? " · \u8ba4\u5b9a\u5df2\u6ee1" : ` · \u8fd8\u9700 ${remain} \u5b66\u65f6`;
meta = `${tag} · \u8ba4\u5b9a ${Number(c.earnedPeriod || 0).toFixed(2)}/${c.maxPeriod} \u5b66\u65f6${remainTxt}`;
} else {
meta = `${tag} · \u5df2\u5b66 ${Number(c.earnedPeriod || 0).toFixed(2)} \u5b66\u65f6\uff08\u51d1\u4e13\u9898\u603b\u5b66\u65f6\uff09`;
}
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 = `
\u8fd0\u884c\u72b6\u6001
\u5df2\u505c\u6b62
\u4e13\u9898\u8ba4\u5b9a 0.00/10 \u5b66\u65f6
0%
\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;
}
state.courses.forEach((c) => {
c.checked = isCourseNeedStudy(c);
});
state.courses.forEach((c) => {
if (isMandatoryCourse(c) && Number(c.earnedPeriod || 0) < Number(c.maxPeriod || 0) - 1e-6) {
c.checked = true;
}
});
saveQueue(state.courses.filter((x) => x.checked).map((x) => x.id));
renderCourseList();
updatePanel();
log(`\u5df2\u52fe\u9009\u9700\u5b66\u4e60\u8bfe\u7a0b ${state.courses.filter((c) => c.checked).length} \u95e8\uff08\u5fc5\u5b66\u4f18\u5148\uff09`);
});
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();
}
})();