// ==UserScript==
// @name 【效率版】河南专技培训平台学习助手(洛阳理工/中原工学院)
// @namespace https://card.wlxy.live/details/AF7F66EC
// @version 1.2
// @description 洛阳理工学院河南专技培训平台(ly.fnhzj.com)和中原工学院河南专技培训平台(www.huayuzj.com)学习辅助:已报名公需课、专业课、可勾选所有课程章节同步进行,无需排队单刷缩短看课时间,免费每站3章节视频,Pro首次激活绑定本站;公需课无考试,专业课考试仅Pro。
// @antifeature payment 免费每站3视频,升级Pro不限制
// @antifeature membership 需云端授权
// @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png
// @author 柠檬真酸
// @match https://www.ly.fnhzj.com/*
// @match https://ly.fnhzj.com/*
// @match https://www.huayuzj.com/*
// @match https://huayuzj.com/*
// @match https://testplugservice-v3.ylxue.net/*
// @match https://plugservice-v3.ylxue.net/*
// @noframes
// @connect oa24.ahzsksw.cn
// @connect newapi.ylxue.net
// @connect learningapi.ylxue.net
// @connect learn.ylxue.net
// @connect examinationapi.ylxue.net
// @connect testplugservice-v3.ylxue.net
// @connect plugservice-v3.ylxue.net
// @connect ylxue.net
// @connect www.ly.fnhzj.com
// @connect ly.fnhzj.com
// @connect www.huayuzj.com
// @connect huayuzj.com
// @connect huaweicloudobs.ahjxjy.cn
// @connect card.wlxy.live
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_openInTab
// @grant unsafeWindow
// @run-at document-idle
// @tag 洛阳理工
// @tag 中原工学院
// @tag 河南专技
// @tag 继续教育
// @tag ylxue
// @license All Rights Reserved
// ==/UserScript==
(function () {
"use strict";
try {
if (window.top !== window.self) return;
} catch (_) {
return;
}
const SCRIPT_VERSION = "1.2";
const SITE_HOST = String(location.hostname || "")
.replace(/^www\./i, "")
.toLowerCase();
const JXJY_SUPPORTED_HOSTS = new Set(["jxjyedu.org.cn", "nypx.jxjyedu.org.cn"]);
const LYLG_SUPPORTED_HOSTS = new Set([
"ly.fnhzj.com",
"huayuzj.com",
"testplugservice-v3.ylxue.net",
"plugservice-v3.ylxue.net",
]);
const REDIRECT_ONLY_HOSTS = new Set([
"hnzjgl.gov.cn",
"jxjy.henu.edu.cn",
"hnpihn.newzhihui.cn",
"zdkj.v.zzu.edu.cn",
"hnzj.ghlearning.com",
"hnzj.user.ghlearning.com",
"zyjs.lypt.edu.cn",
]);
const HN_REDIRECT_BOOT_KEY = "__HN_ZJ_PORTAL_REDIRECT_V1__";
const HN_SCRIPT_JXJY_URL = "https://scriptcat.org/zh-CN/script-show-page/7693";
const HN_SCRIPT_LYLG_URL = "https://scriptcat.org/zh-CN/script-show-page/7691";
const HN_ADAPTED_PLATFORMS = [
{
name: "\u6cb3\u5357\u7701\u7ee7\u7eed\u6559\u80b2\u5b66\u4f1a",
url: "https://www.jxjyedu.org.cn/",
modeLabel: "\u6548\u7387\u5feb\u901f\u5b8c\u6210",
modeClass: "hn-zj-badge-fast",
scriptUrl: HN_SCRIPT_JXJY_URL,
scriptLabel: "\u5b89\u88c5\u5b66\u4e60\u52a9\u624b\uff08ScriptCat #7693\uff09",
},
{
name: "\u5357\u9633\u7406\u5de5\u5b66\u9662",
url: "https://nypx.jxjyedu.org.cn/",
modeLabel: "\u6548\u7387\u5feb\u901f\u5b8c\u6210",
modeClass: "hn-zj-badge-fast",
scriptUrl: HN_SCRIPT_JXJY_URL,
scriptLabel: "\u5b89\u88c5\u5b66\u4e60\u52a9\u624b\uff08ScriptCat #7693\uff09",
},
{
name: "\u6d1b\u9633\u7406\u5de5\u5b66\u9662",
url: "https://www.ly.fnhzj.com/",
modeLabel: "1:1 \u591a\u8bfe\u540c\u5237",
modeClass: "hn-zj-badge-multi",
scriptUrl: HN_SCRIPT_LYLG_URL,
scriptLabel: "\u5b89\u88c5\u5b66\u4e60\u52a9\u624b\uff08ScriptCat #7691\uff09",
},
{
name: "\u4e2d\u539f\u5de5\u5b66\u9662",
url: "https://www.huayuzj.com/",
modeLabel: "1:1 \u591a\u8bfe\u540c\u5237",
modeClass: "hn-zj-badge-multi",
scriptUrl: HN_SCRIPT_LYLG_URL,
scriptLabel: "\u5b89\u88c5\u5b66\u4e60\u52a9\u624b\uff08ScriptCat #7691\uff09",
},
];
const REDIRECT_SOURCE_LABELS = {
"hnzjgl.gov.cn": "\u6cb3\u5357\u4e13\u6280\u7ba1\u7406\u5e73\u53f0",
"jxjy.henu.edu.cn": "\u6cb3\u5357\u5927\u5b66",
"hnpihn.newzhihui.cn": "\u6cb3\u5357\u5de5\u4e1a\u804c\u4e1a\u6280\u672f\u5927\u5b66",
"zdkj.v.zzu.edu.cn": "\u90d1\u5dde\u5927\u5b66",
"hnzj.ghlearning.com": "\u6cb3\u5357\u9ad8\u8f89\u6559\u80b2\u79d1\u6280\u6709\u9650\u516c\u53f8",
"hnzj.user.ghlearning.com": "\u6cb3\u5357\u9ad8\u8f89\u6559\u80b2\u79d1\u6280\u6709\u9650\u516c\u53f8",
"zyjs.lypt.edu.cn": "\u6d1b\u9633\u804c\u4e1a\u6280\u672f\u5b66\u9662",
};
function resolveHostRole(host) {
if (JXJY_SUPPORTED_HOSTS.has(host)) return "jxjy";
if (LYLG_SUPPORTED_HOSTS.has(host)) return "lylg";
if (REDIRECT_ONLY_HOSTS.has(host)) return "redirect";
return "redirect";
}
function _hnEsc(s) {
return String(s || "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function injectHnRedirectStyles() {
if (document.getElementById("hn-zj-redirect-style")) return;
const st = document.createElement("style");
st.id = "hn-zj-redirect-style";
st.textContent = `
#hn-zj-redirect-panel{position:fixed;right:20px;top:80px;z-index:999999;width:390px;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#0f172a;overflow:hidden;}
#hn-zj-redirect-header{padding:10px 12px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;cursor:move;user-select:none;display:flex;align-items:center;gap:9px;}
#hn-zj-redirect-logo{width:28px;height:28px;border-radius:8px;object-fit:cover;background:#fff;border:1px solid rgba(148,163,184,.45);}
#hn-zj-redirect-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.3;flex:1;}
#hn-zj-redirect-body{padding:10px 12px 12px;}
.hn-zj-redirect-lead{font-size:12px;color:#475569;line-height:1.55;margin:0 0 8px;}
.hn-zj-redirect-lead strong{color:#9a3412;}
.hn-zj-redirect-list{display:flex;flex-direction:column;gap:7px;margin:0;padding:0;list-style:none;}
.hn-zj-redirect-item{background:#fff;border:1px solid #dbe4f0;border-radius:11px;padding:8px 9px;}
.hn-zj-redirect-item-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;}
.hn-zj-redirect-link{font-size:13px;font-weight:800;color:#1d4ed8;text-decoration:none;}
.hn-zj-redirect-link:hover{text-decoration:underline;}
.hn-zj-redirect-item-foot{margin-top:5px;}
.hn-zj-redirect-script{font-size:11px;font-weight:700;color:#7c2d12;text-decoration:none;}
.hn-zj-redirect-script:hover{text-decoration:underline;color:#9a3412;}
.hn-zj-badge{font-size:10px;font-weight:800;padding:2px 7px;border-radius:999px;white-space:nowrap;flex:0 0 auto;}
.hn-zj-badge-fast{color:#0f766e;background:#ecfdf5;border:1px solid #99f6e4;}
.hn-zj-badge-multi{color:#1d4ed8;background:#eff6ff;border:1px solid #bfdbfe;}
.hn-zj-redirect-hint{margin:9px 0 0;font-size:11px;color:#64748b;line-height:1.45;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:10px;padding:7px 9px;}
`;
document.head.appendChild(st);
}
function bindHnRedirectDrag(panel) {
const header = panel.querySelector("#hn-zj-redirect-header");
if (!header) return;
let sx = 0,
sy = 0,
ox = 0,
oy = 0,
dragging = false;
header.addEventListener("mousedown", (e) => {
dragging = true;
sx = e.clientX;
sy = e.clientY;
const rect = panel.getBoundingClientRect();
ox = rect.left;
oy = rect.top;
e.preventDefault();
});
window.addEventListener("mousemove", (e) => {
if (!dragging) return;
panel.style.left = `${ox + e.clientX - sx}px`;
panel.style.top = `${oy + e.clientY - sy}px`;
panel.style.right = "auto";
});
window.addEventListener("mouseup", () => {
dragging = false;
});
}
function renderHnRedirectPlatformItems() {
return HN_ADAPTED_PLATFORMS.map(
(p) =>
`
`
).join("");
}
function runHnRedirectPanelOnly() {
try {
const root =
(typeof unsafeWindow !== "undefined" && unsafeWindow.top) ||
window.top ||
window;
if (root[HN_REDIRECT_BOOT_KEY]) return;
root[HN_REDIRECT_BOOT_KEY] = SCRIPT_VERSION;
} catch (_) {
if (window[HN_REDIRECT_BOOT_KEY]) return;
window[HN_REDIRECT_BOOT_KEY] = SCRIPT_VERSION;
}
injectHnRedirectStyles();
document.querySelectorAll("#hn-zj-redirect-panel").forEach((el) => el.remove());
const sourceLabel = REDIRECT_SOURCE_LABELS[SITE_HOST] || "\u5f53\u524d\u5e73\u53f0";
const panel = document.createElement("div");
panel.id = "hn-zj-redirect-panel";
panel.innerHTML = `
\u60a8\u5f53\u524d\u8bbf\u95ee\u7684\u662f ${_hnEsc(sourceLabel)}\uff0c\u6682\u672a\u63a5\u5165\u81ea\u52a8\u5b66\u4e60\u3002
\u5efa\u8bae\u4f18\u5148\u524d\u5f80\u4ee5\u4e0b 4 \u4e2a\u5df2\u9002\u914d\u5e73\u53f0 \u5b66\u4e60\uff1a
${renderHnRedirectPlatformItems()}
\u524d\u5f80\u5e73\u53f0\u524d\u8bf7\u5148\u5b89\u88c5\u5bf9\u5e94 ScriptCat \u811a\u672c\u3002\u6cb3\u5357\u7701\u7ee7\u7eed\u6559\u80b2\u5b66\u4f1a\u3001\u5357\u9633\u7406\u5de5\u5b66\u9662\u652f\u6301\u6548\u7387\u5feb\u901f\u5b8c\u6210\uff08#7693\uff09\uff1b\u6d1b\u9633\u7406\u5de5\u5b66\u9662\u3001\u4e2d\u539f\u5de5\u5b66\u9662\u652f\u6301 1:1 \u65f6\u957f\u591a\u8bfe\u540c\u65f6\u5b66\u4e60\uff08#7691\uff09\u3002
`;
document.body.appendChild(panel);
bindHnRedirectDrag(panel);
}
const HOST_ROLE = resolveHostRole(SITE_HOST);
if (HOST_ROLE === "jxjy") return;
if (HOST_ROLE === "redirect") {
runHnRedirectPanelOnly();
return;
}
const BOOT_KEY = "__LYLG_PROGRESS_HELPER_BOOTED__";
try {
const root =
(typeof unsafeWindow !== "undefined" && unsafeWindow.top) ||
window.top ||
window;
if (root[BOOT_KEY]) return;
root[BOOT_KEY] = _bootTok();
} catch (_) {
if (window[BOOT_KEY]) return;
window[BOOT_KEY] = true;
}
function _bootTok() {
return "v1.3.9";
}
const PANEL_LOGO_URL =
"https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png";
const PANEL_FEATURE_SLOGAN = "\u591a\u8bfe\u540c\u5237 · \u81ea\u52a8\u8003\u8bd5";
const CLOUD_API_BASE = "https://oa24.ahzsksw.cn";
const PRO_BUY_URL = "https://card.wlxy.live/details/AF7F66EC";
const FINISH_TIP =
"\u6240\u9009\u8bfe\u7a0b\u5df2\u5168\u90e8\u5b66\u5b8c\u3002\u82e5\u5e73\u53f0\u8fdb\u5ea6\u7a0d\u540e\u66f4\u65b0\u5c5e\u6b63\u5e38\uff1b\u8003\u8bd5\u9700 Pro\uff08\u516c\u9700\u8bfe\u65e0\u8003\u8bd5\uff09\u3002";
const LOG_PALETTE = [
"#0369a1",
"#0f766e",
"#b45309",
"#a21caf",
"#be123c",
"#1d4ed8",
"#047857",
"#c2410c",
"#7c3aed",
"#0e7490",
"#4f46e5",
"#ca8a04",
];
const LOG_LIMIT = 400;
const PLUG_ORIGIN = "https://testplugservice-v3.ylxue.net";
const COMPLETE_THRESHOLD = 95;
const TRAIN_COURSE_COMPLETE_PCT = 100;
const STEP_SECONDS = 60;
const REPORT_INTERVAL = 60;
const CROSS_WAIT_SECONDS = 60;
const CROSS_MAX_RETRY = 12;
const JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
const SITE_HOST_NORM = SITE_HOST;
const SITE_REGISTRY = {
"ly.fnhzj.com": {
id: "lylg",
name: "\u6d1b\u9633\u7406\u5de5\u5b66\u9662\u6cb3\u5357\u4e13\u6280\u57f9\u8bad\u5e73\u53f0",
customerNo: "luoyangligong",
webSite: "www.ly.fnhzj.com",
trainNamePrefix: "\u6d1b\u9633\u7406\u5de5\u5b66\u9662\u6cb3\u5357\u4e13\u6280\u57f9\u8bad\u5e73\u53f0",
},
"huayuzj.com": {
id: "huayu",
name: "\u4e2d\u539f\u5de5\u5b66\u9662\u6cb3\u5357\u4e13\u6280\u57f9\u8bad\u5e73\u53f0",
customerNo: "huayuzj2021",
webSite: "www.huayuzj.com",
trainNamePrefix: "\u4e2d\u539f\u5de5\u5b66\u9662\u6cb3\u5357\u4e13\u6280\u57f9\u8bad\u5e73\u53f0",
},
};
const CUSTOMER_NO_TO_HOST = Object.fromEntries(
Object.entries(SITE_REGISTRY).map(([host, cfg]) => [cfg.customerNo, host])
);
const SITE_PREF_KEY = "ylxue_active_site_v1";
function normalizeToken(val) {
if (!val || typeof val !== "string") return null;
let clean = val.trim();
if (/^bearer\s+/i.test(clean)) clean = clean.replace(/^bearer\s+/i, "");
clean = clean.replace(/^["']|["']$/g, "").trim();
const m = clean.match(JWT_RE);
if (m) clean = m[0];
return clean.split(".").length === 3 ? clean : null;
}
function b64UrlToJson(part) {
const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
const binary = atob(padded);
try {
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return JSON.parse(new TextDecoder("utf-8").decode(bytes));
} catch (e) {
return JSON.parse(binary);
}
}
function parseJwtPayload(token) {
try {
const clean = normalizeToken(token);
if (!clean) return null;
return b64UrlToJson(clean.split(".")[1]);
} catch (e) {
return null;
}
}
function jwtDataBags(token) {
const payload = parseJwtPayload(token);
if (!payload) return [];
let data = payload.data;
if (typeof data === "string") {
try {
data = JSON.parse(data);
} catch (e) {
data = null;
}
}
const bags = [payload];
if (data && typeof data === "object") bags.push(data);
return bags;
}
function getCustomerNoFromToken(token) {
for (const bag of jwtDataBags(token)) {
for (const k of ["CustomerNo", "customerNo", "s_customerNo", "S_customerNo"]) {
const v = bag[k];
if (v != null && String(v).trim()) return String(v).trim();
}
}
return "";
}
function getUidFromToken(token) {
for (const bag of jwtDataBags(token)) {
for (const k of ["UserId", "userId", "uid", "Uid", "i_userId"]) {
const n = parseInt(bag[k], 10);
if (Number.isFinite(n) && n > 0) return n;
}
}
return null;
}
function acceptUserToken(token) {
const clean = normalizeToken(token);
if (!clean) return null;
return getUidFromToken(clean) ? clean : null;
}
function getJwtTime(token) {
const p = parseJwtPayload(token);
if (!p) return 0;
return Number(p.iat || 0) || Number(p.exp || 0) || 0;
}
function pickNewestToken(candidates) {
let best = null;
let bestT = -1;
const seen = new Set();
for (const raw of candidates) {
const t = acceptUserToken(raw);
if (!t || seen.has(t)) continue;
seen.add(t);
const ts = getJwtTime(t);
if (!best || ts >= bestT) {
best = t;
bestT = ts;
}
}
return best;
}
function collectStorageJwts(store) {
const out = [];
try {
for (let i = 0; i < store.length; i++) {
const key = store.key(i);
if (!key) continue;
const t = acceptUserToken(store.getItem(key));
if (t) out.push(t);
}
} catch (e) {}
return out;
}
function collectPageTokens() {
const out = [];
try {
const win = unsafeWindow || window;
for (const k of ["token", "accessToken", "Authorization", "userToken", "jwt", "ylxueToken"]) {
try {
if (win[k]) out.push(win[k]);
} catch (e) {}
}
const ax = win.axios;
if (ax?.defaults?.headers) {
const h = ax.defaults.headers;
out.push(
h.common?.Authorization,
h.common?.authorization,
h.Authorization,
h.authorization
);
}
} catch (e) {}
try {
out.push(...collectStorageJwts(localStorage));
out.push(...collectStorageJwts(sessionStorage));
} catch (e) {}
return out.filter(Boolean);
}
function getLivePageToken() {
return pickNewestToken(collectPageTokens());
}
function resolveActiveSiteHost() {
if (SITE_REGISTRY[SITE_HOST_NORM]) return SITE_HOST_NORM;
try {
const ref = document.referrer;
if (ref) {
const rh = new URL(ref).hostname.replace(/^www\./i, "").toLowerCase();
if (SITE_REGISTRY[rh]) return rh;
}
} catch (e) {}
const newest = getLivePageToken();
if (newest) {
const cn = getCustomerNoFromToken(newest);
if (cn && CUSTOMER_NO_TO_HOST[cn]) return CUSTOMER_NO_TO_HOST[cn];
}
try {
const pref = GM_getValue(SITE_PREF_KEY);
if (pref && SITE_REGISTRY[pref]) return pref;
} catch (e) {}
return "ly.fnhzj.com";
}
const ACTIVE_SITE_HOST = resolveActiveSiteHost();
const ACTIVE_SITE = SITE_REGISTRY[ACTIVE_SITE_HOST] || SITE_REGISTRY["ly.fnhzj.com"];
const SITE_NAME = ACTIVE_SITE.name;
const CUSTOMER_NO = ACTIVE_SITE.customerNo;
const WEB_SITE = ACTIVE_SITE.webSite;
const CLOUD_API_PREFIX = `/api/${ACTIVE_SITE.id}/`;
const TRAIN_NAME_PREFIX = ACTIVE_SITE.trainNamePrefix || "";
const PANEL_NOTICE =
`\u514d\u8d39\u4f53\u9a8c 3 \u4e2a\u89c6\u9891\uff08${SITE_NAME} \u72ec\u7acb\u8ba1\u6570\uff09\uff1bPro \u9996\u6b21\u5728\u672c\u7ad9\u6fc0\u6d3b\u540e\u7ed1\u5b9a\u672c\u7ad9\uff0c\u4e0d\u53ef\u6362\u7ad9\u3002\u516c\u9700\u8bfe\u65e0\u8003\u8bd5\u3002\u8d2d\u4e70\uff1a` +
PRO_BUY_URL;
const CLOUD_TOKEN_KEY = `${ACTIVE_SITE.id}_cloud_token_v1`;
const CLOUD_LEASE_KEY = `${ACTIVE_SITE.id}_cloud_lease_v1`;
const TOKEN_KEY = `${ACTIVE_SITE.id}_cached_token_v1`;
const PANEL_POS_KEY = `${ACTIVE_SITE.id}_panel_pos_v1`;
const PANEL_COLLAPSED_KEY = `${ACTIVE_SITE.id}_panel_collapsed_v1`;
const BATCH_JOBS_KEY = `${ACTIVE_SITE.id}_batch_jobs_v1`;
function persistActiveSitePref() {
if (!SITE_REGISTRY[SITE_HOST_NORM]) return;
try {
GM_setValue(SITE_PREF_KEY, SITE_HOST_NORM);
} catch (e) {}
}
const state = {
token: "",
uid: 0,
trains: [],
selectedTids: new Set(),
courses: [],
selectedCourseKeys: new Set(),
activeTrainTabTid: null,
chapterPreview: [],
logLines: [],
loading: false,
liveToken: null,
running: false,
stopFlag: false,
currentTask: "",
switchingUser: false,
cloudToken: "",
cloudLease: "",
cloudLeaseExp: 0,
cloudTier: "free",
cloudProExpireAt: 0,
cloudSiteBindOnActivate: false,
cloudServerVersion: "",
tokenBoundBlocked: false,
tokenSiteId: "",
freeVideoLimit: 3,
freeUsedVideos: 0,
proBuyUrl: PRO_BUY_URL,
finishTip: FINISH_TIP,
panelNotice: PANEL_NOTICE,
examRows: [],
};
function courseKey(tid, cid) {
return `${Number(tid)}:${Number(cid)}`;
}
function parseCourseKey(key) {
const parts = String(key || "").split(":");
return { tid: Number(parts[0]) || 0, cid: Number(parts[1]) || 0 };
}
function findCourse(tid, cid) {
return state.courses.find(
(c) => Number(c.id) === Number(cid) && Number(c.tid) === Number(tid)
);
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
function openUrl(url) {
const u = String(url || "").trim();
if (!u) return;
try {
if (typeof GM_openInTab === "function") GM_openInTab(u, { active: true });
else window.open(u, "_blank");
} catch (_) {
try {
window.open(u, "_blank");
} catch (e) {}
}
}
function getCloudToken() {
try {
const t = String(GM_getValue(CLOUD_TOKEN_KEY, "") || state.cloudToken || "").trim();
state.cloudToken = t;
return t;
} catch (_) {
return String(state.cloudToken || "").trim();
}
}
function setCloudToken(token) {
const t = String(token || "").trim();
state.cloudToken = t;
try {
GM_setValue(CLOUD_TOKEN_KEY, t);
} catch (_) {}
}
function _gj(url, opts = {}) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: opts.method || "GET",
url,
headers: opts.headers || {},
data: opts.data,
timeout: opts.timeout || 60000,
onload(res) {
resolve({
status: res.status,
text: String(res.responseText || ""),
});
},
onerror: () => reject(new Error("\u4e91\u7aef\u7f51\u7edc\u5931\u8d25")),
ontimeout: () => reject(new Error("\u4e91\u7aef\u8bf7\u6c42\u8d85\u65f6")),
});
});
}
function _pcd(data, status) {
let detail = data && (data.detail != null ? data.detail : data.message || data.code);
if (detail && typeof detail === "object") {
if (
detail.code === "free_quota_exceeded" ||
/free_quota/i.test(String(detail.code || "")) ||
/\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c/.test(String(detail.message || ""))
) {
if (detail.proBuyUrl) state.proBuyUrl = String(detail.proBuyUrl).trim() || PRO_BUY_URL;
if (detail.free_video_limit != null)
state.freeVideoLimit = Number(detail.free_video_limit) || 3;
if (detail.free_used_videos != null)
state.freeUsedVideos = Number(detail.free_used_videos) || 0;
const err = new Error("free_quota_exceeded");
err.code = "free_quota_exceeded";
err.detail = detail;
return err;
}
if (detail.code === "pro_required" || /pro_required/i.test(String(detail.code || ""))) {
if (detail.proBuyUrl) state.proBuyUrl = String(detail.proBuyUrl).trim() || PRO_BUY_URL;
const err = new Error("pro_required");
err.code = "pro_required";
err.detail = detail;
return err;
}
const code = String(detail.code || detail.message || "");
if (/token_bound/i.test(code)) {
const err = new Error("token_bound_other_user");
err.code = "token_bound_other_user";
err.detail = detail;
return err;
}
if (detail.code === "token_wrong_site" || /token_wrong_site/i.test(code)) {
const err = new Error(
String(detail.message || "\u6b64 Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u7ad9\u70b9\uff0c\u4e0d\u80fd\u5728\u672c\u7ad9\u4f7f\u7528")
);
err.code = "token_wrong_site";
err.detail = detail;
return err;
}
return new Error(String(detail.message || detail.code || JSON.stringify(detail)));
}
return new Error(String(detail || `\u4e91\u7aef HTTP ${status}`));
}
function cloudAuthError(message, code) {
const err = new Error(message || "\u4e91\u7aef\u9274\u6743\u5931\u8d25");
err.code = code || "cloud_auth_required";
return err;
}
function isCloudMissingError(e) {
const msg = String((e && e.message) || e || "").toLowerCase();
const status = Number(e && e.status);
return (
status === 404 ||
/not found|404|\u4e91\u7aef http 404/.test(msg) ||
/\/api\/(lylg|huayu)\//.test(msg)
);
}
async function _crq(path, method, payload) {
const base = CLOUD_API_BASE.replace(/\/$/, "");
const url = base + (path.startsWith("/") ? path : "/" + path);
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
};
const token = getCloudToken();
if (token && !state.tokenBoundBlocked) headers.Authorization = "Bearer " + token;
if (state.uid) headers["x-learning-user-id"] = String(state.uid);
let bodyObj = payload;
if (bodyObj && typeof bodyObj === "object") {
bodyObj = Object.assign({}, bodyObj);
if (path !== `${CLOUD_API_PREFIX}lease` && state.cloudLease && bodyObj.lease == null) {
bodyObj.lease = state.cloudLease;
}
if (path === `${CLOUD_API_PREFIX}lease` && state.tokenBoundBlocked) bodyObj.token = "";
}
const res = await _gj(url, {
method: method || "POST",
headers,
data: bodyObj == null ? null : JSON.stringify(bodyObj),
});
let data = {};
const text = String(res.text || "").trim();
if (text) {
try {
data = JSON.parse(text);
} catch (_) {
data = { message: text.slice(0, 200) };
}
}
if (res.status < 200 || res.status >= 300) {
const err = _pcd(data, res.status);
err.status = res.status;
throw err;
}
return data;
}
function _alp(data) {
if (!data || typeof data !== "object") return;
if (data.lease) {
state.cloudLease = String(data.lease || "");
state.cloudLeaseExp =
Number(data.lease_expires_at || data.exp || 0) ||
Math.floor(Date.now() / 1000) + 600;
try {
GM_setValue(CLOUD_LEASE_KEY, {
lease: state.cloudLease,
exp: state.cloudLeaseExp,
tier: String(data.tier || ""),
freeVideoLimit: Number(data.free_video_limit ?? data.freeVideoLimit ?? 3),
freeUsedVideos: Number(data.free_used_videos ?? data.freeUsedVideos ?? 0),
proExpireAt: Number(data.pro_expires_at || data.proExpireAt || 0) || 0,
boundUid: String(data.bound_learning_user_id || state.uid || ""),
proBuyUrl: String(data.proBuyUrl || state.proBuyUrl || PRO_BUY_URL),
finishTip: String(data.finishTip || state.finishTip || FINISH_TIP),
panelNotice: String(data.panelNotice || state.panelNotice || PANEL_NOTICE),
});
} catch (_) {}
}
if (data.tier) state.cloudTier = String(data.tier);
if (data.free_video_limit != null || data.freeVideoLimit != null) {
state.freeVideoLimit = Number(data.free_video_limit ?? data.freeVideoLimit) || 3;
}
if (data.free_used_videos != null || data.freeUsedVideos != null) {
state.freeUsedVideos = Number(data.free_used_videos ?? data.freeUsedVideos) || 0;
}
if (data.pro_expires_at != null || data.proExpireAt != null) {
state.cloudProExpireAt = Number(data.pro_expires_at ?? data.proExpireAt) || 0;
}
if (data.proBuyUrl) state.proBuyUrl = String(data.proBuyUrl).trim() || PRO_BUY_URL;
if (data.token_site_id != null) {
state.tokenSiteId = String(data.token_site_id || "").trim();
} else if (data.tokenSiteId != null) {
state.tokenSiteId = String(data.tokenSiteId || "").trim();
}
if (data.finishTip) state.finishTip = String(data.finishTip);
if (data.panelNotice) state.panelNotice = String(data.panelNotice);
if (data.siteBindOnActivate != null) {
state.cloudSiteBindOnActivate = !!data.siteBindOnActivate;
}
if (data.serverVersion) state.cloudServerVersion = String(data.serverVersion);
_ucp();
_upn();
}
function _lcl() {
try {
const cached = GM_getValue(CLOUD_LEASE_KEY, null);
if (!cached || typeof cached !== "object") return null;
const lease = String(cached.lease || "").trim();
const exp = Number(cached.exp || 0);
if (!lease || !exp) return null;
if (exp - Math.floor(Date.now() / 1000) <= 60) return null;
const boundUid = String(cached.boundUid || "").trim();
if (state.uid && boundUid && String(state.uid) !== boundUid) return null;
state.cloudLease = lease;
state.cloudLeaseExp = exp;
if (cached.tier) state.cloudTier = String(cached.tier);
if (cached.freeVideoLimit != null) state.freeVideoLimit = Number(cached.freeVideoLimit) || 3;
if (cached.freeUsedVideos != null) state.freeUsedVideos = Number(cached.freeUsedVideos) || 0;
if (cached.proExpireAt != null) state.cloudProExpireAt = Number(cached.proExpireAt) || 0;
if (cached.proBuyUrl) state.proBuyUrl = String(cached.proBuyUrl);
if (cached.finishTip) state.finishTip = String(cached.finishTip);
if (cached.panelNotice) state.panelNotice = String(cached.panelNotice);
return cached;
} catch (_) {
return null;
}
}
function _icc() {
state.cloudLease = "";
state.cloudLeaseExp = 0;
try {
GM_setValue(CLOUD_LEASE_KEY, null);
} catch (_) {}
}
async function _cl(force, opts = {}) {
const strict = !!(opts && opts.strict);
const now = Math.floor(Date.now() / 1000);
if (!force && state.cloudLease && state.cloudLeaseExp - now > 60) return true;
if (!force && _lcl()) {
_ucp();
return true;
}
const postLease = async (tokenOverride) =>
_crq(`${CLOUD_API_PREFIX}lease`, "POST", {
site_id: ACTIVE_SITE.id,
learning_user_id: state.uid ? String(state.uid) : "",
userid: state.uid ? String(state.uid) : "",
token: tokenOverride != null ? tokenOverride : state.tokenBoundBlocked ? "" : getCloudToken(),
});
let data;
const tokenBefore = getCloudToken();
try {
data = await postLease();
state.tokenBoundBlocked = false;
} catch (e) {
if (String(e && e.code ? e.code : e.message || e).includes("token_wrong_site")) {
setCloudToken("");
_icc();
state.cloudTier = "free";
state.cloudProExpireAt = 0;
const msg =
(e.detail && e.detail.message) ||
`\u6b64 Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u7ad9\u70b9\uff0c\u4e0d\u80fd\u5728\u672c\u7ad9\uff08${SITE_NAME}\uff09\u4f7f\u7528`;
_plog(msg);
if (strict) {
e.userMessage = msg;
throw e;
}
data = await postLease("");
} else if (String(e && e.code ? e.code : e.message || e).includes("token_bound")) {
state.tokenBoundBlocked = true;
_icc();
state.cloudTier = "free";
state.cloudProExpireAt = 0;
_plog("Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\uff0c\u672c\u8d26\u53f7\u6309\u514d\u8d39\u4f53\u9a8c");
data = await postLease("");
} else if (isCloudMissingError(e)) {
throw cloudAuthError("\u4e91\u7aef\u670d\u52a1\u672a\u5f00\u901a\u6216\u4e0d\u53ef\u7528\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5", "cloud_unavailable");
} else {
throw e;
}
}
_alp(data);
if (strict && tokenBefore && state.cloudTier !== "pro") {
throw cloudAuthError("Pro Token \u672a\u751f\u6548\uff08\u53ef\u80fd\u5df2\u7ed1\u5b9a\u5176\u4ed6\u7ad9\u70b9\u6216\u8d26\u53f7\uff09", "token_not_pro");
}
if (!state.cloudLease) {
throw cloudAuthError("\u4e91\u7aef lease \u83b7\u53d6\u5931\u8d25", "cloud_lease_failed");
}
if (!state.cloudLeaseExp) state.cloudLeaseExp = now + 600;
return true;
}
async function _es(context) {
const leased = await _cl(false);
if (!leased || !state.cloudLease) {
throw cloudAuthError("\u4e91\u7aef\u9274\u6743\u5931\u8d25\uff0c\u65e0\u6cd5\u5f00\u59cb\u5237\u8bfe");
}
const data = await _crq(`${CLOUD_API_PREFIX}study/engine/start`, "POST", {
lease: state.cloudLease,
kind: "vod",
context: context || {},
config: {
step_seconds: STEP_SECONDS,
report_interval_ms: REPORT_INTERVAL * 1000,
cross_wait_ms: CROSS_WAIT_SECONDS * 1000,
cross_max_retry: CROSS_MAX_RETRY,
},
});
if (data.free_used_videos != null) state.freeUsedVideos = Number(data.free_used_videos) || 0;
if (data.free_video_limit != null) state.freeVideoLimit = Number(data.free_video_limit) || 3;
if (data.tier) state.cloudTier = String(data.tier);
if (data.proBuyUrl) state.proBuyUrl = String(data.proBuyUrl).trim() || PRO_BUY_URL;
_ucp();
return data;
}
async function _estep(sessionId, event, lastResult) {
const leased = await _cl(false);
if (!leased || !state.cloudLease) {
throw cloudAuthError("\u4e91\u7aef lease \u5df2\u5931\u6548\uff0c\u8bf7\u91cd\u65b0\u5f00\u59cb");
}
return _crq(`${CLOUD_API_PREFIX}study/engine/step`, "POST", {
lease: state.cloudLease,
session_id: String(sessionId || ""),
event: String(event || "tick"),
last_result: lastResult == null ? null : lastResult,
});
}
function _hfq() {
const url = String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL;
_plog(
`\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff08${state.freeUsedVideos}/${state.freeVideoLimit}\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`
);
_ucp();
showPanelConfirm("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u662f\u5426\u6253\u5f00 Pro \u8d2d\u4e70\u9875\uff1f", {
title: "\u5347\u7ea7 Pro",
confirmText: "\u53bb\u8d2d\u4e70",
cancelText: "\u6682\u4e0d",
}).then((ok) => {
if (ok) openUrl(url);
});
}
function formatProExpireAt(ts) {
const n = Number(ts) || 0;
if (!n) return "";
const ms = n > 1e12 ? n : n * 1000;
const d = new Date(ms);
if (Number.isNaN(d.getTime())) return "";
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
function _ucp() {
const tierEl = document.getElementById("lylg-cloud-tier");
const quotaEl = document.getElementById("lylg-cloud-quota");
const expireEl = document.getElementById("lylg-cloud-expire");
const buyBtn = document.getElementById("lylg-buy-pro");
const settingsTier = document.getElementById("lylg-settings-tier");
const settingsExpire = document.getElementById("lylg-settings-expire");
const isPro = state.cloudTier === "pro";
const expireText = formatProExpireAt(state.cloudProExpireAt);
if (tierEl) {
tierEl.textContent = isPro ? "Pro" : "\u514d\u8d39\u7248";
tierEl.style.color = isPro ? "#166534" : "#9a3412";
tierEl.style.background = isPro ? "#f0fdf4" : "#fff7ed";
tierEl.style.borderColor = isPro ? "#86efac" : "#fdba74";
}
if (quotaEl) {
quotaEl.textContent = PANEL_FEATURE_SLOGAN;
quotaEl.title = "\u672c\u811a\u672c\u7279\u8272\uff1a\u52fe\u9009\u591a\u95e8\u8bfe\u540c\u65f6\u5b66\uff0c\u5b66\u5b8c\u540e\u53ef\u4e00\u952e\u8003\u8bd5";
quotaEl.classList.add("lylg-sub-chip-em");
}
if (expireEl) {
if (isPro && expireText) {
expireEl.textContent = `\u5230\u671f ${expireText}`;
expireEl.title = `Pro \u6709\u6548\u671f\u81f3 ${expireText}`;
} else if (isPro) {
expireEl.textContent = "Pro \u5df2\u5f00\u901a";
expireEl.title = "";
} else {
expireEl.textContent = `\u4f53\u9a8c ${state.freeUsedVideos}/${state.freeVideoLimit} \u89c6\u9891`;
expireEl.title = "\u514d\u8d39\u4f53\u9a8c\u89c6\u9891\u8fdb\u5ea6";
}
}
if (settingsTier) settingsTier.textContent = isPro ? "Pro" : "\u514d\u8d39\u7248";
if (settingsExpire) {
settingsExpire.textContent =
isPro && expireText ? expireText : isPro ? "\u5df2\u5f00\u901a" : "—";
}
if (buyBtn) {
buyBtn.style.display = "";
buyBtn.textContent = isPro ? "\u7eed\u8d39 / \u67e5\u770b Pro" : "\u5347\u7ea7 Pro";
}
const inp = document.getElementById("lylg-cloud-token-input");
if (inp && getCloudToken() && !inp.value) inp.value = getCloudToken();
syncFreeSelectTag();
if (document.getElementById("lylg-course-list")) renderCourseList();
}
function syncFreeSelectTag() {
const tag = document.getElementById("lylg-free-select-tag");
if (!tag) return;
if (isProTier()) {
tag.style.display = "none";
return;
}
const budget = freeSelectBudget();
const pending = countPendingVideosForKeys(state.selectedCourseKeys);
tag.style.display = "";
tag.textContent =
budget > 0 ? `\u514d\u8d39\u52fe\u9009 ${pending}/${budget} \u89c6\u9891` : "\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c";
tag.title =
budget > 0
? `\u514d\u8d39\u7248\u6700\u591a\u52fe\u9009 ${budget} \u4e2a\u5f85\u5b66\u89c6\u9891`
: "\u8bf7\u5347\u7ea7 Pro \u540e\u7ee7\u7eed\u52fe\u9009";
}
function _upn() {
const el = document.getElementById("lylg-ann-text");
if (el) el.textContent = String(state.panelNotice || PANEL_NOTICE);
}
async function _lcc() {
try {
const cfg = await _crq(`${CLOUD_API_PREFIX}client-config`, "GET", null);
if (cfg.proBuyUrl) state.proBuyUrl = String(cfg.proBuyUrl).trim() || PRO_BUY_URL;
if (cfg.freeVideoLimit != null) state.freeVideoLimit = Number(cfg.freeVideoLimit) || 3;
if (cfg.finishTip) state.finishTip = String(cfg.finishTip);
if (cfg.panelNotice) state.panelNotice = String(cfg.panelNotice);
if (cfg.siteBindOnActivate != null) {
state.cloudSiteBindOnActivate = !!cfg.siteBindOnActivate;
}
if (cfg.serverVersion) state.cloudServerVersion = String(cfg.serverVersion);
if (cfg.siteBindOnActivate == null && !cfg.serverVersion) {
log("\u4e91\u7aef\u4ecd\u4e3a\u65e7\u7248\uff1aToken \u6682\u672a\u5206\u7ad9\u70b9\u7ed1\u5b9a\uff0c\u8bf7\u66f4\u65b0 oa24 \u4e0a\u7684 \u6388\u6743\u670d\u52a1 \u5e76\u91cd\u542f");
}
_ucp();
_upn();
} catch (_) {}
}
async function _rel() {
const box = document.getElementById("lylg-exam-list");
if (box) box.innerHTML = `\u52a0\u8f7d\u8003\u8bd5\u5217\u8868…
`;
try {
await _cl(false);
if (state.cloudTier !== "pro") {
if (box) {
box.innerHTML = `\u8003\u8bd5\u4ec5 Pro \u53ef\u7528
`;
box.querySelector("#lylg-exam-buy")?.addEventListener("click", () =>
openUrl(state.proBuyUrl || PRO_BUY_URL)
);
}
return;
}
try {
await _crq(`${CLOUD_API_PREFIX}exam/list`, "POST", {
lease: state.cloudLease,
kind: "exam",
context: { uid: state.uid, tid: state.activeTrainTabTid || 0 },
config: {},
});
} catch (e) {
if (String(e && e.code ? e.code : e.message || e).includes("pro_required")) {
throw e;
}
}
if (!state.trains.length) await loadTrains();
const rows = await loadPlatformExamRows();
state.examRows = rows;
if (!rows.length) {
if (box)
box.innerHTML = `\u6682\u65e0\u53ef\u8003\u8bd5\u57f9\u8bad
\u516c\u9700\u8bfe\u65e0\u8003\u8bd5\uff1b\u4e13\u4e1a\u8bfe\u9700\u5b66\u5b8c\u4e14\u5e73\u53f0\u5df2\u5f00\u542f\u8003\u8bd5
`;
return;
}
if (box) {
box.innerHTML = rows
.map((r, idx) => {
const st = r.qualified
? "\u5df2\u5408\u683c"
: !r.completed
? "\u8bfe\u7a0b\u672a\u5b8c\u6210"
: r.ready
? "\u53ef\u8003\u8bd5"
: r.reason || "\u4e0d\u53ef\u8003";
const score =
r.score != null && r.score !== "" ? ` · \u6210\u7ee9 ${r.score}` : "";
const left =
r.settings && r.settings.i_surplusCount != null
? ` · \u5269\u4f59${r.settings.i_surplusCount}\u6b21`
: "";
return `
${escHtml(
(r.year ? r.year + "\u5e74 · " : "") + (r.trainName || "")
)}
${escHtml(
r.childName || ""
)} · ${escHtml(st)}${escHtml(score)}${escHtml(left)}
${
r.ready
? `
`
: ""
}
`;
})
.join("");
box.querySelectorAll(".lylg-exam-start").forEach((btn) => {
btn.addEventListener("click", () => {
const idx = Number(btn.dataset.examIdx);
startExamByIndex(idx);
});
});
}
} catch (e) {
if (String(e && e.code ? e.code : e.message || e).includes("pro_required")) {
if (box) {
box.innerHTML = `\u8003\u8bd5\u4ec5 Pro \u53ef\u7528
`;
box.querySelector("#lylg-exam-buy2")?.addEventListener("click", () =>
openUrl(state.proBuyUrl || PRO_BUY_URL)
);
}
return;
}
if (box)
box.innerHTML = `${escHtml(e.message || e)}
`;
}
}
async function startExamByIndex(idx) {
const row = (state.examRows || [])[idx];
if (!row || !row.ready) return;
if (state.running) {
_plog("\u8bf7\u5148\u505c\u6b62\u770b\u8bfe\u518d\u8003\u8bd5");
return;
}
state.running = true;
state.stopFlag = false;
syncRunButtons();
setCurrentTask(`\u8003\u8bd5 · ${row.childName || row.trainName}`);
showLogTab();
try {
await _cl(false);
if (state.cloudTier !== "pro") {
_hfq();
_plog("\u8003\u8bd5\u4ec5 Pro \u53ef\u7528");
return;
}
_plog(`\u5f00\u8003 · ${row.year ? row.year + "\u5e74 · " : ""}${row.childName || row.trainName}`);
const result = await runOneExam({
tid: row.tid,
uid: state.uid,
majorId: row.majorId,
courseCode: row.courseCode,
trainName: row.trainName,
});
if (!result.qualified) {
_plog(`\u672a\u5408\u683c · ${result.score}\u5206\uff08\u9700${result.passScore}\u5206\uff09\uff0c\u53ef\u518d\u8003`);
}
await _rel();
} catch (e) {
_plog(`\u8003\u8bd5\u672a\u5b8c\u6210\uff1a${e.message || e}`);
} finally {
state.running = false;
state.stopFlag = false;
syncRunButtons();
setCurrentTask("\u5f85\u547d");
}
}
function log(_msg) {
}
function logColorForKey(key) {
const s = String(key || "");
let h = 2166136261;
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return LOG_PALETTE[h % LOG_PALETTE.length];
}
function inferLogKind(msg) {
if (/\u5b8c\u6210$|\u5904\u7406\u5b8c\u6bd5|\u5408\u683c$/.test(msg)) return "done";
if (/\u6ca1\u6709|\u8bf7\u5148|\u4e2d\u65ad|\u5931\u8d25|\u9519\u8bef|\u672a\u5b8c\u6210|\u672a\u5408\u683c/.test(msg)) return "info";
return "progress";
}
function renderLogRow(entry) {
if (typeof entry === "string") {
return `${escHtml(entry)}
`;
}
const kind = entry.kind || "progress";
const color =
kind === "info" ? "#64748b" : logColorForKey(entry.key || entry.msg);
let body = escHtml(entry.msg);
if (kind === "progress") {
body = body
.replace(/(\u5df2\u5b66[\d.]+%)/g, '$1')
.replace(
/(\d+\u5206\u949f\/\u603b\d+\u5206)/g,
'$1'
);
} else if (kind === "done") {
body = body.replace(/(\u5b8c\u6210)/g, '$1');
}
return `
[${escHtml(entry.time)}] ${body}
`;
}
function _plog(msg, colorKey) {
const entry = {
time: new Date().toLocaleTimeString(),
msg: String(msg),
key: colorKey || String(msg).slice(0, 24),
kind: inferLogKind(msg),
};
state.logLines.unshift(entry);
if (state.logLines.length > LOG_LIMIT) state.logLines.length = LOG_LIMIT;
const box = document.getElementById("lylg-run-log");
if (box) {
box.innerHTML = state.logLines.map(renderLogRow).join("");
box.scrollTop = 0;
}
}
function formatStudyMins(lastMs, durationSec) {
const learned = Math.max(0, Math.floor(Number(lastMs) / 60000));
const total = Math.max(1, Math.ceil((Number(durationSec) || 0) / 60));
return { learned, total };
}
function formatProgressPct(lastMs, durationSec) {
if (!durationSec) return 0;
return Math.min(100, Math.round((lastMs / 1000 / durationSec) * 1000) / 10);
}
function progressLine(course, video, lastMs) {
const duration = video.duration || 60;
const pct = formatProgressPct(lastMs, duration);
const { learned, total } = formatStudyMins(lastMs, duration);
return `${course.name}${video.title} \u5df2\u5b66${pct}% ${learned}\u5206\u949f/\u603b${total}\u5206`;
}
function _alp2(course, video) {
const pct = Math.min(100, Math.max(0, Number(video.progress) || 0));
const tid = Number(course.tid);
const c = findCourse(tid, course.id);
let block = state.chapterPreview.find(
(x) => Number(x.courseId) === Number(course.id) && Number(x.tid) === tid
);
if (!block) {
block = {
tid,
courseId: course.id,
courseTitle: course.name,
trainLabel: course.trainLabel || "",
courseProgress: c ? c.progress : 0,
computedProgress: 0,
videos: [],
};
state.chapterPreview.push(block);
}
const vids = block.videos || (block.videos = []);
const idx = vids.findIndex((v) => Number(v.id) === Number(video.id));
const snap = {
...(idx >= 0 ? vids[idx] : {}),
id: video.id,
title: video.title,
number: video.number,
duration: video.duration,
lasttime: video.lasttime,
clrId: video.clrId,
isfirst: video.isfirst,
progress: pct,
done: video.done || video.isfirst === 1 || pct >= 100,
status:
video.done || video.isfirst === 1 || pct >= 100
? "completed"
: pct > 0
? "learning"
: "notstarted",
};
if (idx >= 0) vids[idx] = snap;
else vids.push(snap);
const learnedSec = vids.reduce(
(s, v) => s + ((Number(v.duration) || 0) * (Number(v.progress) || 0)) / 100,
0
);
const totalSec = vids.reduce((s, v) => s + (Number(v.duration) || 0), 0);
const computed = totalSec > 0 ? Math.round((learnedSec / totalSec) * 100) : pct;
const allVideosDone =
vids.length > 0 &&
vids.every(
(v) => v.done || v.isfirst === 1 || (Number(v.progress) || 0) >= TRAIN_COURSE_COMPLETE_PCT
);
const coursePct = allVideosDone ? TRAIN_COURSE_COMPLETE_PCT : computed;
block.computedProgress = coursePct;
block.courseProgress = coursePct;
block.courseTitle = course.name;
if (c) c.progress = coursePct;
_rtp(tid);
renderCourseList();
renderChapterPreview();
_utp();
updateSummary();
}
function getTrainCourses(tid) {
return state.courses.filter((c) => Number(c.tid) === Number(tid));
}
function courseProgressPct(course) {
const raw = course?.raw || {};
const v = Number(raw.i_learningProgress ?? course?.progress);
return Number.isFinite(v) ? Math.round(v) : 0;
}
function isCourseFullyComplete(course) {
return courseProgressPct(course) >= TRAIN_COURSE_COMPLETE_PCT;
}
function _cpc(t) {
if (!t) return { pct: 0, completed: 0, total: 0 };
const courses = getTrainCourses(t.id);
const total = courses.length;
if (!total) {
const platformPct = Math.round(
Number(t.raw?.f_Learning_Progress ?? t.platformProgress ?? t.progress) || 0
);
return { pct: platformPct, completed: 0, total: 0 };
}
const completed = courses.filter(isCourseFullyComplete).length;
const pct = Math.min(100, Math.round((completed / total) * 100));
return { pct, completed, total };
}
function applyTrainCourseProgress(t) {
const platformPct = Math.round(
Number(t.raw?.f_Learning_Progress ?? t.platformProgress ?? t.progress) || 0
);
t.platformProgress = platformPct;
const { pct, completed, total } = _cpc(t);
t.completedCourseCount = completed;
t.totalCourseCount = total;
if (total > 0) {
t.progress = pct;
} else {
t.progress = platformPct;
}
return t.progress;
}
function _stpa(t) {
if (!t) return 0;
return applyTrainCourseProgress(t);
}
function _rtp(tid) {
const t = state.trains.find((x) => Number(x.id) === Number(tid));
if (!t) return 0;
if (!state.running) {
return _stpa(t);
}
const { pct, completed, total } = _cpc(t);
if (!total) {
return _stpa(t);
}
t.completedCourseCount = completed;
t.totalCourseCount = total;
const baseline = Number(t.studyBaselineProgress ?? t.platformProgress) || 0;
t.progress = Math.max(baseline, pct);
return t.progress;
}
function _ftc(t) {
const count = Number(t.totalCourseCount) || getTrainCourses(t.id).length || 0;
const completed = Number(t.completedCourseCount);
const done =
Number.isFinite(completed) && completed >= 0
? completed
: getTrainCourses(t.id).filter(isCourseFullyComplete).length;
return { done, count };
}
function _utp() {
state.trains.forEach((t) => {
const pct = `${Number(t.progress) || 0}%`;
const { done, count } = _ftc(t);
const countText = count > 0 ? `\u5df2\u5b8c\u6210 ${done}/${count}\u95e8` : "";
document
.querySelectorAll(`#lylg-train-chips input[data-tid="${t.id}"]`)
.forEach((inp) => {
const chip = inp.closest(".lylg-train-chip");
const el = chip && chip.querySelector(".pct");
if (el) el.textContent = pct;
if (chip && countText) chip.title = `${t.name || "\u57f9\u8bad"} · ${countText}`;
});
});
}
function showLogTab() {
const panel = document.getElementById("lylg-panel");
if (!panel) return;
panel.querySelectorAll(".lylg-tab-btn").forEach((b) => b.classList.remove("active"));
panel.querySelectorAll(".lylg-pane").forEach((p) => p.classList.remove("active"));
panel.querySelector('.lylg-tab-btn[data-tab="log"]')?.classList.add("active");
panel.querySelector('.lylg-pane[data-pane="log"]')?.classList.add("active");
}
function escHtml(s) {
return String(s ?? "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function truncate(s, n) {
s = String(s || "");
return s.length > n ? s.slice(0, n) + "…" : s;
}
function formatTime(sec) {
sec = Math.max(0, Math.floor(Number(sec) || 0));
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = sec % 60;
if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
}
function resetUserData(reason) {
state.stopFlag = true;
state.running = false;
state.trains = [];
state.courses = [];
state.chapterPreview = [];
state.selectedTids = new Set();
state.selectedCourseKeys = new Set();
state.activeTrainTabTid = null;
renderTrainSelect();
renderCourseList();
renderChapterPreview();
updateSummary();
syncRunButtons();
setCurrentTask("\u5f85\u547d");
if (reason) log(reason);
}
function persistToken(token, opts = {}) {
const clean = acceptUserToken(token);
if (!clean) return null;
const prevUid = state.uid || 0;
const nextUid = getUidFromToken(clean) || 0;
const tokenChanged = clean !== state.token;
state.token = clean;
state.liveToken = clean;
state.uid = nextUid;
try {
GM_setValue(TOKEN_KEY, clean);
} catch (e) {}
if (prevUid && nextUid && prevUid !== nextUid) {
handleUserSwitch(prevUid, nextUid, opts.from || "token");
} else if (tokenChanged) {
updateAuthUi();
}
return clean;
}
function handleUserSwitch(prevUid, nextUid, from) {
if (state.switchingUser) return;
state.switchingUser = true;
resetUserData(`\u68c0\u6d4b\u5230\u6362\u53f7 ${prevUid} → ${nextUid}\uff08\u6765\u81ea ${from}\uff09\uff0c\u5df2\u6e05\u7a7a\u4e0a\u4e00\u7528\u6237\u6570\u636e`);
updateAuthUi();
setTimeout(async () => {
try {
await loadTrains();
} catch (e) {
log(`\u6362\u53f7\u540e\u91cd\u8f7d\u5931\u8d25: ${e.message || e}`);
} finally {
state.switchingUser = false;
}
}, 50);
}
function clearToken() {
state.token = "";
state.liveToken = null;
state.uid = 0;
try {
GM_setValue(TOKEN_KEY, null);
} catch (e) {}
}
function getToken() {
const live = getLivePageToken();
if (live) return persistToken(live, { from: "\u9875\u9762" });
try {
const cached = acceptUserToken(GM_getValue(TOKEN_KEY));
if (cached) {
const pageAgain = getLivePageToken();
if (pageAgain) {
const pageUid = getUidFromToken(pageAgain);
const cacheUid = getUidFromToken(cached);
if (pageUid && cacheUid && pageUid !== cacheUid) {
return persistToken(pageAgain, { from: "\u9875\u9762(\u8986\u76d6\u7f13\u5b58)" });
}
return persistToken(pageAgain, { from: "\u9875\u9762" });
}
state.token = cached;
state.uid = getUidFromToken(cached);
return cached;
}
} catch (e) {}
return acceptUserToken(state.token);
}
function setupTokenInterceptor() {
try {
const win = unsafeWindow || window;
const capture = (auth, src) => {
const t = acceptUserToken(auth);
if (!t) return;
const oldUid = state.uid || 0;
const newUid = getUidFromToken(t) || 0;
if (t === state.token && oldUid === newUid) return;
persistToken(t, { from: src });
if (oldUid === newUid) {
log(`\u5df2\u4ece\u9875\u9762${src}\u66f4\u65b0 Token · uid=${state.uid}`);
updateAuthUi();
}
};
if (win.fetch && !win.fetch.__lylgPatched) {
const raw = win.fetch.bind(win);
win.fetch = function (...args) {
try {
const headers = (args[1] && args[1].headers) || {};
const auth =
typeof headers.get === "function"
? headers.get("Authorization") || headers.get("authorization")
: headers.Authorization || headers.authorization;
capture(auth, "fetch");
} catch (e) {}
return raw(...args);
};
win.fetch.__lylgPatched = true;
}
const XHR = win.XMLHttpRequest;
if (XHR?.prototype && !XHR.prototype.__lylgPatched) {
const rawSet = XHR.prototype.setRequestHeader;
XHR.prototype.setRequestHeader = function (name, value) {
try {
if (name && String(name).toLowerCase() === "authorization") {
capture(value, "XHR");
}
} catch (e) {}
return rawSet.apply(this, arguments);
};
XHR.prototype.__lylgPatched = true;
}
if (!win.__lylgTokenWatch) {
win.__lylgTokenWatch = setInterval(() => {
try {
if (state.running || state.switchingUser) return;
const live = getLivePageToken();
if (!live) return;
const liveUid = getUidFromToken(live);
if (liveUid && state.uid && liveUid !== state.uid) {
persistToken(live, { from: "\u5b9a\u65f6\u6821\u5bf9" });
} else if (live !== state.token) {
persistToken(live, { from: "\u5b9a\u65f6\u6821\u5bf9" });
}
} catch (e) {}
}, 4000);
}
} catch (e) {}
}
function apiRequest({ method = "POST", url, data, origin, referer }) {
return new Promise((resolve, reject) => {
const token = getToken();
if (!token) {
reject(new Error("\u672a\u83b7\u53d6\u5230 Token\uff0c\u8bf7\u5148\u767b\u5f55\u6216\u5728\u8bbe\u7f6e\u4e2d\u7c98\u8d34 JWT"));
return;
}
const isGet = String(method || "POST").toUpperCase() === "GET";
let finalUrl = url;
if (isGet && data && typeof data === "object") {
const q = Object.keys(data)
.filter((k) => data[k] != null && data[k] !== "")
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(data[k])}`)
.join("&");
if (q) finalUrl += (url.includes("?") ? "&" : "?") + q;
}
GM_xmlhttpRequest({
method: isGet ? "GET" : "POST",
url: finalUrl,
headers: {
Accept: "application/json, text/plain, */*",
...(isGet ? {} : { "Content-Type": "application/json" }),
Authorization: `Bearer ${token}`,
Origin: origin || PLUG_ORIGIN,
Referer: referer || PLUG_ORIGIN + "/",
},
data: isGet ? undefined : data ? JSON.stringify(data) : undefined,
timeout: 60000,
onload(res) {
if (res.status < 200 || res.status >= 300) {
reject(new Error(`HTTP ${res.status}`));
return;
}
try {
const json = JSON.parse(res.responseText);
const ok =
json.StatusCode === 0 ||
json.code === 0 ||
json.Code === 0 ||
json.Code === "0" ||
json.Msg === "sucess" ||
json.Msg === "success";
const detail = String(json.Detail || json.detail || "").trim();
if (ok && detail && !/^(sucess|success)$/i.test(detail)) {
reject(new Error(detail));
return;
}
if (ok) resolve(json);
else if (json.StatusCode === 401 || json.code === 401) {
clearToken();
reject(new Error("Token \u5df2\u8fc7\u671f"));
} else reject(new Error(detail || json.Info || json.message || json.Msg || "\u8bf7\u6c42\u5931\u8d25"));
} catch (e) {
const text = String(res.responseText || "").trim();
if (text && text[0] !== "{" && text[0] !== "[") {
resolve({ Data: text, StatusCode: 0 });
} else reject(new Error("\u89e3\u6790\u54cd\u5e94\u5931\u8d25"));
}
},
onerror: () => reject(new Error("\u7f51\u7edc\u8bf7\u6c42\u5931\u8d25")),
ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6")),
});
});
}
function fetchTrains(uid) {
return apiRequest({
url: "https://newapi.ylxue.net/api/Trainclass/GetUsersTrainClassByToken",
data: {
allMajors: 0,
peixunRW: 2,
uid,
webSite: WEB_SITE,
token: state.token || "",
isTrainOther: 0,
platformId: 1,
},
});
}
function fetchCourses(tid, uid) {
return apiRequest({
url: "https://newapi.ylxue.net/api/UserTrainCourse/GetUserLearnCourseByToken",
data: {
pageindex: 1,
pagesize: 1000,
tid,
uid,
token: state.token || "",
platformId: 1,
},
});
}
function fetchVideos(cid, tid, uid) {
return apiRequest({
url: "https://learningapi.ylxue.net/Course/GetCourseClassVideo",
data: {
cid,
tid,
uid,
token: state.token || "",
platformId: 1,
},
});
}
function md5(str) {
function cmn(q, a, b, x, s, t) {
a = (a + q + x + t) | 0;
return (((a << s) | (a >>> (32 - s))) + b) | 0;
}
function ff(a, b, c, d, x, s, t) {
return cmn((b & c) | (~b & d), a, b, x, s, t);
}
function gg(a, b, c, d, x, s, t) {
return cmn((b & d) | (c & ~d), a, b, x, s, t);
}
function hh(a, b, c, d, x, s, t) {
return cmn(b ^ c ^ d, a, b, x, s, t);
}
function ii(a, b, c, d, x, s, t) {
return cmn(c ^ (b | ~d), a, b, x, s, t);
}
function toUtf8Bytes(input) {
const s = unescape(encodeURIComponent(String(input)));
const out = [];
for (let i = 0; i < s.length; i++) out.push(s.charCodeAt(i) & 0xff);
return out;
}
const msg = toUtf8Bytes(str);
const origLenBits = msg.length * 8;
msg.push(0x80);
while (msg.length % 64 !== 56) msg.push(0);
for (let i = 0; i < 4; i++) msg.push((origLenBits >>> (i * 8)) & 0xff);
const hi = Math.floor(origLenBits / 0x100000000);
for (let i = 0; i < 4; i++) msg.push((hi >>> (i * 8)) & 0xff);
let a = 1732584193;
let b = -271733879;
let c = -1732584194;
let d = 271733878;
for (let i = 0; i < msg.length; i += 64) {
const w = new Array(16);
for (let j = 0; j < 16; j++) {
const k = i + j * 4;
w[j] =
msg[k] |
(msg[k + 1] << 8) |
(msg[k + 2] << 16) |
(msg[k + 3] << 24);
}
const aa = a,
bb = b,
cc = c,
dd = d;
a = ff(a, b, c, d, w[0], 7, -680876936);
d = ff(d, a, b, c, w[1], 12, -389564586);
c = ff(c, d, a, b, w[2], 17, 606105819);
b = ff(b, c, d, a, w[3], 22, -1044525330);
a = ff(a, b, c, d, w[4], 7, -176418897);
d = ff(d, a, b, c, w[5], 12, 1200080426);
c = ff(c, d, a, b, w[6], 17, -1473231341);
b = ff(b, c, d, a, w[7], 22, -45705983);
a = ff(a, b, c, d, w[8], 7, 1770035416);
d = ff(d, a, b, c, w[9], 12, -1958414417);
c = ff(c, d, a, b, w[10], 17, -42063);
b = ff(b, c, d, a, w[11], 22, -1990404162);
a = ff(a, b, c, d, w[12], 7, 1804603682);
d = ff(d, a, b, c, w[13], 12, -40341101);
c = ff(c, d, a, b, w[14], 17, -1502002290);
b = ff(b, c, d, a, w[15], 22, 1236535329);
a = gg(a, b, c, d, w[1], 5, -165796510);
d = gg(d, a, b, c, w[6], 9, -1069501632);
c = gg(c, d, a, b, w[11], 14, 643717713);
b = gg(b, c, d, a, w[0], 20, -373897302);
a = gg(a, b, c, d, w[5], 5, -701558691);
d = gg(d, a, b, c, w[10], 9, 38016083);
c = gg(c, d, a, b, w[15], 14, -660478335);
b = gg(b, c, d, a, w[4], 20, -405537848);
a = gg(a, b, c, d, w[9], 5, 568446438);
d = gg(d, a, b, c, w[14], 9, -1019803690);
c = gg(c, d, a, b, w[3], 14, -187363961);
b = gg(b, c, d, a, w[8], 20, 1163531501);
a = gg(a, b, c, d, w[13], 5, -1444681467);
d = gg(d, a, b, c, w[2], 9, -51403784);
c = gg(c, d, a, b, w[7], 14, 1735328473);
b = gg(b, c, d, a, w[12], 20, -1926607734);
a = hh(a, b, c, d, w[5], 4, -378558);
d = hh(d, a, b, c, w[8], 11, -2022574463);
c = hh(c, d, a, b, w[11], 16, 1839030562);
b = hh(b, c, d, a, w[14], 23, -35309556);
a = hh(a, b, c, d, w[1], 4, -1530992060);
d = hh(d, a, b, c, w[4], 11, 1272893353);
c = hh(c, d, a, b, w[7], 16, -155497632);
b = hh(b, c, d, a, w[10], 23, -1094730640);
a = hh(a, b, c, d, w[13], 4, 681279174);
d = hh(d, a, b, c, w[0], 11, -358537222);
c = hh(c, d, a, b, w[3], 16, -722521979);
b = hh(b, c, d, a, w[6], 23, 76029189);
a = hh(a, b, c, d, w[9], 4, -640364487);
d = hh(d, a, b, c, w[12], 11, -421815835);
c = hh(c, d, a, b, w[15], 16, 530742520);
b = hh(b, c, d, a, w[2], 23, -995338651);
a = ii(a, b, c, d, w[0], 6, -198630844);
d = ii(d, a, b, c, w[7], 10, 1126891415);
c = ii(c, d, a, b, w[14], 15, -1416354905);
b = ii(b, c, d, a, w[5], 21, -57434055);
a = ii(a, b, c, d, w[12], 6, 1700485571);
d = ii(d, a, b, c, w[3], 10, -1894986606);
c = ii(c, d, a, b, w[10], 15, -1051523);
b = ii(b, c, d, a, w[1], 21, -2054922799);
a = ii(a, b, c, d, w[8], 6, 1873313359);
d = ii(d, a, b, c, w[15], 10, -30611744);
c = ii(c, d, a, b, w[6], 15, -1560198380);
b = ii(b, c, d, a, w[13], 21, 1309151649);
a = ii(a, b, c, d, w[4], 6, -145523070);
d = ii(d, a, b, c, w[11], 10, -1120210379);
c = ii(c, d, a, b, w[2], 15, 718787259);
b = ii(b, c, d, a, w[9], 21, -343485551);
a = (a + aa) | 0;
b = (b + bb) | 0;
c = (c + cc) | 0;
d = (d + dd) | 0;
}
function rhex(n) {
let s = "";
for (let j = 0; j < 4; j++) s += ((n >>> (j * 8)) & 0xff).toString(16).padStart(2, "0");
return s;
}
return rhex(a) + rhex(b) + rhex(c) + rhex(d);
}
function examSign(tid, examScore, uid) {
const raw = String(Number(tid) * Number(examScore) + Number(uid) + Number(tid) + "");
return md5(md5(raw));
}
function pickField(obj, keys, fallback) {
if (!obj || typeof obj !== "object") return fallback;
for (const k of keys) {
if (obj[k] != null && obj[k] !== "") return obj[k];
}
return fallback;
}
function pickNum(obj, keys, fallback = 0) {
const v = pickField(obj, keys, null);
if (v == null) return fallback;
const n = Number(v);
return Number.isFinite(n) ? n : fallback;
}
function questionType(q) {
return pickNum(q, ["type", "Type", "i_type", "i_Type", "questionType"], 0);
}
function normalizeExamQuestion(q) {
const items = pickField(q, ["answerItems", "AnswerItems", "options", "Options"], []) || [];
return {
raw: q,
type: questionType(q),
answer: String(pickField(q, ["answer", "Answer", "s_answer"], "") || ""),
randomCode: String(pickField(q, ["randomCode", "RandomCode", "s_randomCode"], "") || ""),
code: pickField(q, ["code", "Code", "s_code"], ""),
answerItems: Array.isArray(items) ? items : [],
};
}
function optionCodes(q) {
const items = q.answerItems || [];
return items
.map((it) =>
String(
pickField(it, ["answersCode", "AnswersCode", "code", "Code", "s_code"], "") || ""
).trim()
)
.filter(Boolean);
}
function optionTexts(q) {
const items = q.answerItems || [];
return items
.map((it) =>
String(pickField(it, ["answers", "Answers", "content", "Content"], "") || "").trim()
)
.filter(Boolean);
}
function multiCombos(codes) {
const out = [];
const n = codes.length;
if (n > 12) return out; // 防御:选项过多不爆算
const total = 1 << n;
for (let mask = 1; mask < total; mask++) {
const pick = [];
for (let i = 0; i < n; i++) if (mask & (1 << i)) pick.push(codes[i]);
pick.sort((a, b) => a.localeCompare(b));
out.push(pick.join(""));
}
return out;
}
function solveQuestion(rawQ) {
const q = rawQ.answer != null || rawQ.Answer != null ? normalizeExamQuestion(rawQ) : rawQ;
const target = String(q.answer || "").toLowerCase();
const rnd = String(q.randomCode || "");
const type = Number(q.type) || 0;
const codes = optionCodes(q);
const texts = optionTexts(q);
const tryOne = (ans) => md5(String(ans) + rnd).toLowerCase() === target;
if (!target) return { ok: false, answer: "", reason: "\u65e0\u6821\u9a8c\u54c8\u5e0c", type };
if (type === 4) return { ok: false, answer: "", reason: "\u586b\u7a7a\u9898\u65e0\u6cd5\u7a77\u4e3e", type };
const candidates = [];
for (const c of codes) candidates.push(c);
if (type === 2 || type === 0 || codes.length >= 2) {
for (const c of multiCombos(codes)) candidates.push(c);
}
for (const t of texts) candidates.push(t);
for (const ans of ["A", "B", "\u5bf9", "\u9519", "\u6b63\u786e", "\u9519\u8bef", "Y", "N", "T", "F", "true", "false"]) {
candidates.push(ans);
}
const seen = new Set();
for (const ans of candidates) {
if (!ans || seen.has(ans)) continue;
seen.add(ans);
if (tryOne(ans)) return { ok: true, answer: ans, type };
}
return {
ok: false,
answer: "",
reason: codes.length ? "\u7a77\u4e3e\u672a\u547d\u4e2d" : "\u65e0\u9009\u9879",
type,
};
}
function examScoreOfType(settings, type) {
const single = pickNum(settings, [
"f_single_choice_question_score",
"F_single_choice_question_score",
"f_SingleChoiceQuestionScore",
]);
const multi = pickNum(settings, [
"f_check_question_score",
"F_check_question_score",
"f_CheckQuestionScore",
]);
const judge = pickNum(settings, [
"f_judge_question_score",
"F_judge_question_score",
"f_JudgeQuestionScore",
]);
const caseScore = pickNum(settings, ["f_case_score", "F_case_score", "f_CaseScore"]);
if (type === 1) return single;
if (type === 2) return multi;
if (type === 3) return judge;
if (type === 5) return caseScore;
return 0;
}
function fetchExamSettings(tid, uid) {
return apiRequest({
url: "https://examinationapi.ylxue.net/api/TrainClass/GetExamSettingByAPP",
data: { tid, uid, platformId: 1 },
});
}
function fetchExamMajors(tid, uid) {
return apiRequest({
url: "https://examinationapi.ylxue.net/api/TrainingPlatform/GetChildMajorType",
data: { tid, uid, platformId: 1 },
});
}
function fetchExamQuestions(payload) {
return apiRequest({
url: "https://examinationapi.ylxue.net/api/TrainingPlatform/GetQuestions",
data: { ...payload, platformId: 1 },
});
}
async function fetchExamRequestNo() {
const res = await apiRequest({
method: "GET",
url: "https://learn.ylxue.net/api/Examination/Token",
data: { platformId: 1 },
});
if (res == null) return "";
if (typeof res === "string") return res;
if (res.Data != null && typeof res.Data !== "object") return String(res.Data);
if (res.requestNo) return String(res.requestNo);
if (res.Data && res.Data.requestNo) return String(res.Data.requestNo);
return String(res.Data || res.token || "");
}
function submitExamPaper(payload) {
return apiRequest({
url: "https://learn.ylxue.net/api/Examination/SaveAndGetExaminationId",
data: { ...payload, platformId: 1 },
});
}
async function runOneExam({ tid, uid, majorId, courseCode, trainName }) {
const setRes = await fetchExamSettings(tid, uid);
const settings = setRes.Data || setRes.data || {};
if (Number(settings.i_surplusCount) === 0 && Number(settings.i_isBuyExams) === 1) {
throw new Error("\u8003\u8bd5\u6b21\u6570\u5df2\u7528\u5b8c\uff0c\u8bf7\u5148\u5728\u5e73\u53f0\u8d2d\u4e70\u6b21\u6570");
}
const qBody = {
checkQuestionNum: settings.i_check_question_num,
isExameCourse: settings.i_isExamCourse,
judgequestionNum: settings.i_judge_question_num,
singleChoiceQuestionNum: settings.i_single_choice_question_num,
specialtyId: majorId,
customerNo: settings.i_isQuestionBank ? settings.s_customerNo || "" : "",
tid,
uid,
isMockExamination: settings.i_mockExam || 0,
isRandomCode: 1,
courseCode: courseCode || "",
caseNum: settings.i_case_num,
language: "zh",
};
_plog("\u6b63\u5728\u7b54\u9898…");
const qRes = await fetchExamQuestions(qBody);
let questions = Array.isArray(qRes.Data)
? qRes.Data
: Array.isArray(qRes.data)
? qRes.data
: [];
if (!questions.length && qRes.Data && typeof qRes.Data === "object") {
const d = qRes.Data;
questions = d.list || d.List || d.questions || d.Questions || [];
}
if (!Array.isArray(questions) || !questions.length) {
throw new Error("\u672a\u83b7\u53d6\u5230\u8bd5\u5377\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5");
}
let score = 0;
let right = 0;
let wrong = 0;
const answers = [];
const typeCount = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 0: 0 };
for (const raw of questions) {
if (state.stopFlag) throw new Error("\u5df2\u505c\u6b62");
const q = normalizeExamQuestion(raw);
typeCount[q.type] = (typeCount[q.type] || 0) + 1;
const solved = solveQuestion(q);
const qCode = q.code || raw.code || raw.Code || "?";
if (solved.ok) {
right += 1;
let add = examScoreOfType(settings, q.type);
if (!add && q.type === 0) add = examScoreOfType(settings, 1);
score += add;
answers.push({ answers: solved.answer, code: qCode, isCorrect: 1 });
} else {
wrong += 1;
answers.push({
answers: solved.answer || "\u672a\u9009\u9898",
code: qCode,
isCorrect: 0,
});
}
}
const fullMarks = pickNum(
settings,
["f_examfullmarks", "F_examfullmarks", "f_ExamFullMarks"],
0
);
const passScore = pickNum(
settings,
["f_qualifiedScore", "F_qualifiedScore", "f_QualifiedScore"],
0
);
if (score === 0 && right > 0) {
const unit = fullMarks > 0 ? fullMarks / questions.length : 100 / questions.length;
score = Math.round(right * unit * 100) / 100;
}
if (wrong > 0 && score < passScore) {
throw new Error(`\u8fd8\u6709 ${wrong} \u9898\u672a\u7b54\u51fa\uff0c\u5df2\u53d6\u6d88\u4ea4\u5377\uff08\u907f\u514d\u6d6a\u8d39\u6b21\u6570\uff09`);
}
const payload = {
answers,
examScore: score,
isMockExamination: settings.i_mockExam || settings.I_mockExam || 0,
isQualified: score >= passScore ? 1 : 0,
operateDevice: "web-plug",
specialtyId: majorId,
customerNo:
settings.i_isQuestionBank || settings.I_isQuestionBank
? settings.s_customerNo || settings.S_customerNo || ""
: "",
tid,
uid,
courseCode: courseCode || "",
};
payload.requestNo = await fetchExamRequestNo();
payload.sign = examSign(tid, score, uid);
const sub = await submitExamPaper(payload);
const examId = sub.Data != null ? sub.Data : sub.data;
const shortName = trainName
? String(trainName)
.replace(new RegExp(`^${TRAIN_NAME_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`), "")
.replace(/\u57f9\u8bad\u73ed$/, "") || trainName
: "\u672c\u6b21";
_plog(
score >= passScore
? `${shortName} · ${score}\u5206 · \u5408\u683c`
: `${shortName} · ${score}\u5206 · \u672a\u5408\u683c`
);
return {
ok: score >= passScore,
score,
passScore,
right,
wrong,
examId,
qualified: score >= passScore,
};
}
function trainHasExam(t) {
if (!t) return false;
const name = String(t.name || t.raw?.s_name || t.raw?.s_trainClassName || "");
if (/\u516c\u9700/.test(name)) return false;
if (Number(t.examSettings) !== 1 && Number(t.raw?.i_examSettings) !== 1) return false;
return true;
}
async function loadPlatformExamRows() {
const uid = state.uid || (await ensureSession());
const rows = [];
for (const t of state.trains) {
if (!trainHasExam(t)) continue;
if (Number(t.progress) < 100 && Number(t.raw?.f_Learning_Progress) < 100) continue;
try {
const majorRes = await fetchExamMajors(t.id, uid);
const majors = Array.isArray(majorRes.Data) ? majorRes.Data : [];
let settings = {};
try {
const sRes = await fetchExamSettings(t.id, uid);
settings = sRes.Data || {};
} catch (_) {}
if (!majors.length) {
rows.push({
tid: t.id,
trainName: t.name,
year: t.year,
majorId: 0,
childName: "\u65e0\u8003\u8bd5\u4e13\u4e1a",
ready: false,
reason: "\u65e0\u4e13\u4e1a\u5206\u7c7b",
settings,
});
continue;
}
for (const m of majors) {
const majorId = Number(m.ChildMajorId || m.childMajorId || m.MajorId || m.Id || 0);
const completed =
m.IsCourseExamCompleted == null ||
m.isCourseExamCompleted == null ||
Number(m.IsCourseExamCompleted ?? m.isCourseExamCompleted) === 1;
const qualified = Number(m.IsQualified ?? m.isQualified) === 1;
rows.push({
tid: t.id,
trainName: t.name,
year: t.year,
majorId,
childName: m.ChildName || m.childName || "\u8003\u8bd5",
courseCode: m.CourseCode || m.courseCode || "",
ready: !!majorId && completed && !qualified,
qualified,
completed,
score: m.ExamScore ?? m.examScore,
settings,
});
}
} catch (e) {
rows.push({
tid: t.id,
trainName: t.name,
year: t.year,
majorId: 0,
childName: "\u52a0\u8f7d\u5931\u8d25",
ready: false,
reason: e.message || String(e),
});
}
}
return rows;
}
function nowCreateTime() {
return Math.floor(Date.now() / 1000);
}
function formatCreateTime(sec) {
try {
const d = new Date(Number(sec) * 1000);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
} catch (_) {
return String(sec);
}
}
function formatEta(ms) {
const s = Math.max(0, Math.ceil(Number(ms) / 1000));
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
if (h > 0) return `${h}\u5c0f\u65f6${m}\u5206${sec}\u79d2`;
if (m > 0) return `${m}\u5206${sec}\u79d2`;
return `${sec}\u79d2`;
}
function _rvp({
courseId,
lessonId,
tid,
uid,
clrId,
lasttime,
isfirst,
createTime,
}) {
const lt = Math.floor(lasttime);
const ct = createTime != null ? Math.floor(createTime) : nowCreateTime();
const clr = Number(clrId) > 0 ? Number(clrId) : 0;
return apiRequest({
url: "https://learn.ylxue.net/api/ClassesLearningRecord/SaveLearningRecordByToken",
data: {
cid: courseId,
classId: lessonId,
clrId: clr,
isfirst: isfirst ?? 0,
lasttime: lt,
tid,
uid: uid || state.uid,
createTime: ct,
token: state.token || "",
platformId: 1,
},
origin: PLUG_ORIGIN,
referer: PLUG_ORIGIN + "/",
}).then((res) => {
res.__createTime = ct;
res.__lasttime = lt;
return res;
});
}
function _ecl(res) {
const d = res && res.Data;
if (d == null || d === "") return 0;
if (typeof d === "number" && d > 0) return Math.floor(d);
if (typeof d === "string" && /^\d+$/.test(d.trim())) return Number(d.trim());
if (typeof d === "object") {
const n = Number(d.i_clrId ?? d.clrId ?? d.ClrId ?? d.id ?? 0);
return n > 0 ? n : 0;
}
return 0;
}
function summarizeApiRes(res) {
if (!res || typeof res !== "object") return String(res);
const data = res.Data;
let dataBrief = "";
if (data == null) dataBrief = "null";
else if (typeof data === "object") dataBrief = JSON.stringify(data).slice(0, 120);
else dataBrief = String(data);
return `Code=${res.Code ?? res.code} Msg=${res.Msg || res.message || ""} Data=${dataBrief}`;
}
function mapVideo(v) {
const durationSec = Number(v.i_timelength ?? v.timelength ?? 0) || 0;
const lastRaw = Number(v.i_lasttime ?? v.lasttime ?? 0) || 0;
const isfirst = Number(v.i_isfirst ?? v.isfirst);
const clrId = Number(v.i_clrId ?? v.clrId ?? 0) || 0;
let lastMs = 0;
if (lastRaw > 0) {
const maxMs = durationSec > 0 ? durationSec * 1000 * 1.5 : lastRaw;
lastMs = Math.min(lastRaw, maxMs);
}
let progress = 0;
let status = "notstarted";
if (isfirst === 1) {
progress = 100;
status = "completed";
} else if (isfirst === 0) {
if (durationSec > 0 && lastMs > 0) {
progress = Math.min(99.9, (lastMs / 1000 / durationSec) * 100);
}
status = progress > 0 || lastMs > 0 || clrId > 0 ? "learning" : "notstarted";
} else {
progress = 0;
status = "notstarted";
}
return {
id: v.i_id,
title: v.s_className || v.s_name || `\u8bfe\u65f6${v.i_numbers || ""}`,
number: v.i_numbers || 0,
duration: durationSec,
lasttime: lastMs,
clrId,
isfirst: Number.isFinite(isfirst) ? isfirst : -1,
progress: Math.round(progress * 10) / 10,
done: isfirst === 1,
status,
raw: v,
};
}
async function ensureSession() {
let token = getToken();
if (!token) {
token = await waitForLiveToken(6000);
}
if (!token) throw new Error("\u672a\u83b7\u53d6\u5230 Token");
persistToken(token);
if (!state.uid) throw new Error("Token \u4e2d\u65e0 UserId");
return state.uid;
}
async function waitForLiveToken(timeoutMs) {
const hit = Array.from(document.querySelectorAll("a,button,span,div,li")).find(
(el) => (el.textContent || "").trim() === "\u5df2\u62a5\u540d\u57f9\u8bad"
);
try {
hit?.click();
} catch (e) {}
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const t = getLivePageToken();
if (t) return t;
await new Promise((r) => setTimeout(r, 300));
}
return null;
}
async function loadTrains() {
if (state.loading) return;
state.loading = true;
updateAuthUi();
try {
const uid = await ensureSession();
log(`\u52a0\u8f7d\u5df2\u62a5\u540d\u57f9\u8bad · uid=${uid}`);
const res = await fetchTrains(uid);
const list = Array.isArray(res.Data) ? res.Data : [];
state.trains = list
.filter((c) => c.i_state === 1)
.map((c) => ({
id: c.i_id,
name: c.s_name || "\u672a\u547d\u540d\u57f9\u8bad",
year: c.i_trainYears || 0,
progress: Math.round(Number(c.f_Learning_Progress) || 0),
platformProgress: Math.round(Number(c.f_Learning_Progress) || 0),
requiredHours: Number(
c.d_classHours ??
c.i_classHours ??
c.d_SelectClassHour ??
c.i_trainClassHour ??
c.d_trainClassHour ??
0
) || 0,
selectedCourseCount: Number(
c.i_selClassNum ?? c.d_selClassNum ?? c.i_SelectClassNum ?? 0
) || 0,
learnedHours: Number(
c.d_learningClassHour ?? c.i_LearningClassNum ?? c.f_LearningClassHour ?? 0
) || 0,
examSettings: Number(c.i_examSettings) || 0,
raw: c,
}));
log(`\u5df2\u62a5\u540d\u57f9\u8bad ${state.trains.length} \u4e2a\uff08\u63a5\u53e3 ${list.length} \u6761\uff09`);
const validIds = new Set(state.trains.map((t) => t.id));
state.selectedTids = new Set([...state.selectedTids].filter((id) => validIds.has(id)));
if (!state.selectedTids.size) {
state.trains.forEach((t) => state.selectedTids.add(t.id));
}
syncActiveTrainTab();
renderTrainSelect();
await loadCourses();
} catch (e) {
log(`\u52a0\u8f7d\u57f9\u8bad\u5931\u8d25: ${e.message}`);
state.trains = [];
state.courses = [];
state.chapterPreview = [];
renderTrainSelect();
renderCourseList();
renderChapterPreview();
updateSummary();
} finally {
state.loading = false;
updateAuthUi();
}
}
function trainLabelOf(tid) {
const t = state.trains.find((x) => Number(x.id) === Number(tid));
if (!t) return `\u57f9\u8bad${tid}`;
return `${t.year ? t.year + "\u5e74 · " : ""}${truncate(t.name, 18)}`;
}
async function loadCourses(opts = {}) {
const quiet = !!opts.quiet;
const keepSelection = !!opts.keepSelection || quiet;
const tids = [...state.selectedTids];
const box = document.getElementById("lylg-course-list");
if (!tids.length) {
state.courses = [];
renderCourseList();
state.chapterPreview = [];
renderChapterPreview();
updateSummary();
return;
}
if (box) box.innerHTML = `\u52a0\u8f7d\u8bfe\u7a0b\u4e2d…
`;
try {
const uid = state.uid || (await ensureSession());
const all = [];
for (const tid of tids) {
const res = await fetchCourses(tid, uid);
const list = Array.isArray(res.Data) ? res.Data : [];
const label = trainLabelOf(tid);
list.forEach((c) => {
all.push({
id: c.i_id,
tid,
trainLabel: label,
name: c.s_courseName || "\u672a\u547d\u540d\u8bfe\u7a0b",
progress: courseProgressPct({ progress: 0, raw: c }),
classHours: c.d_classHours || 0,
length: c.i_courseLength || 0,
raw: c,
});
});
}
state.courses = all;
if (!quiet) log(`\u5df2\u9009\u57f9\u8bad\u8bfe\u7a0b\u5408\u8ba1 ${state.courses.length} \u95e8`);
if (!keepSelection) {
const incomplete = state.courses.filter((c) => c.progress < COMPLETE_THRESHOLD);
const allKeys = incomplete.map((c) => courseKey(c.tid, c.id));
if (isProTier()) {
state.selectedCourseKeys = new Set(allKeys);
} else {
state.selectedCourseKeys = new Set(trimKeysToFreeBudget(allKeys));
}
if (!state.selectedCourseKeys.size && state.courses[0]) {
const c0 = state.courses[0];
if (isProTier() || freeSelectBudget() > 0) {
state.selectedCourseKeys.add(courseKey(c0.tid, c0.id));
}
}
} else {
const valid = new Set(state.courses.map((c) => courseKey(c.tid, c.id)));
state.selectedCourseKeys = new Set(
[...state.selectedCourseKeys].filter((k) => valid.has(k))
);
}
syncActiveTrainTab();
state.trains
.filter((t) => tids.includes(Number(t.id)))
.forEach((t) => _stpa(t));
renderTrainSelect();
renderCourseList();
_utp();
await refreshChapterPreview({ quiet });
} catch (e) {
log(`\u52a0\u8f7d\u8bfe\u7a0b\u5931\u8d25: ${e.message}`);
state.courses = [];
renderCourseList();
}
}
async function refreshChapterPreview(opts = {}) {
const quiet = !!opts.quiet;
const keys = [...state.selectedCourseKeys];
const previewBox = document.getElementById("lylg-chapter-preview");
if (!keys.length) {
state.chapterPreview = [];
renderChapterPreview();
updateSummary();
return;
}
if (previewBox) previewBox.innerHTML = `\u52a0\u8f7d\u7ae0\u8282\u8fdb\u5ea6…
`;
const uid = state.uid;
const list = [];
for (const key of keys) {
const { tid, cid } = parseCourseKey(key);
const course = findCourse(tid, cid);
if (!course) continue;
try {
const res = await fetchVideos(cid, tid, uid);
const ok = res.Code === "0" || res.Code === 0 || res.StatusCode === 0;
const videos = ok && Array.isArray(res.Data) ? res.Data.map((v) => mapVideo(v)) : [];
const learnedSec = videos.reduce((sum, v) => sum + (v.duration * v.progress) / 100, 0);
const totalSec = videos.reduce((sum, v) => sum + v.duration, 0);
const computedProgress = totalSec > 0 ? Math.round((learnedSec / totalSec) * 100) : 0;
list.push({
tid,
courseId: cid,
courseTitle: course.name,
trainLabel: course.trainLabel || trainLabelOf(tid),
courseProgress: course.progress,
computedProgress,
videos,
});
} catch (e) {
list.push({
tid,
courseId: cid,
courseTitle: course.name,
trainLabel: course.trainLabel || trainLabelOf(tid),
courseProgress: course.progress,
videos: [],
error: e.message,
});
}
}
state.chapterPreview = list;
if (!isProTier()) {
const trimmed = trimKeysToFreeBudget([...state.selectedCourseKeys]);
if (trimmed.length < state.selectedCourseKeys.size) {
state.selectedCourseKeys = new Set(trimmed);
renderCourseList();
updateSummary();
_plog(`\u514d\u8d39\u7248\u6700\u591a ${freeSelectBudget()} \u4e2a\u5f85\u5b66\u89c6\u9891\uff0c\u5df2\u81ea\u52a8\u8c03\u6574\u52fe\u9009`);
}
}
const tids = new Set(list.map((b) => Number(b.tid)).filter(Boolean));
tids.forEach((tid) => _rtp(tid));
renderChapterPreview();
_utp();
updateSummary();
if (!quiet) {
const done = list.reduce((s, c) => s + c.videos.filter((v) => v.done).length, 0);
const total = list.reduce((s, c) => s + c.videos.length, 0);
log(`\u7ae0\u8282\u9884\u89c8\uff1a${done}/${total} \u8bfe\u65f6\u5df2\u5b8c\u6210`);
}
}
function setCurrentTask(msg) {
state.currentTask = String(msg || "");
const el = document.getElementById("lylg-current-task");
if (el) {
el.textContent = state.currentTask || "\u5f85\u547d";
el.title = state.currentTask || "";
}
}
function syncRunButtons() {
const startBtn = document.getElementById("lylg-start");
const stopBtn = document.getElementById("lylg-stop");
if (startBtn) {
startBtn.disabled = !!state.running;
startBtn.classList.toggle("lylg-btn-off", !!state.running);
}
if (stopBtn) {
stopBtn.disabled = !state.running;
stopBtn.classList.toggle("lylg-btn-off", !state.running);
}
}
async function _vlc(courseId, tid, lessonId) {
try {
const res = await fetchVideos(courseId, tid, state.uid);
const ok = res.Code === "0" || res.Code === 0 || res.StatusCode === 0;
const list = ok && Array.isArray(res.Data) ? res.Data.map((v) => mapVideo(v)) : [];
const want = Number(lessonId);
const hit = list.find((v) => Number(v.id) === want);
return hit || null;
} catch (_) {
return null;
}
}
async function waitForLessonClrId(courseId, tid, lessonId, { rounds = 5, gapMs = 1000 } = {}) {
let hit = null;
for (let i = 0; i < rounds; i++) {
if (i > 0) await sleep(gapMs);
hit = await _vlc(courseId, tid, lessonId);
if (hit && hit.clrId) return hit;
}
return hit;
}
function loadAllBatchJobs() {
try {
const all = GM_getValue(BATCH_JOBS_KEY);
return Array.isArray(all) ? all : [];
} catch (_) {
return [];
}
}
function saveAllBatchJobs(all) {
try {
GM_setValue(BATCH_JOBS_KEY, all);
} catch (_) {}
}
function loadBatchJobs() {
return loadAllBatchJobs().filter((j) => j && j.uid === state.uid);
}
function upsertBatchJob(job) {
const all = loadAllBatchJobs().filter(
(j) =>
!(
j.uid === job.uid &&
j.tid === job.tid &&
j.courseId === job.courseId &&
j.lessonId === job.lessonId
)
);
all.push(job);
saveAllBatchJobs(all);
}
function removeBatchJob(job) {
const all = loadAllBatchJobs().filter(
(j) =>
!(
j.uid === job.uid &&
j.tid === job.tid &&
j.courseId === job.courseId &&
j.lessonId === job.lessonId
)
);
saveAllBatchJobs(all);
}
async function sleepUntilDue(dueAt, label) {
while (!state.stopFlag && Date.now() < dueAt) {
const left = dueAt - Date.now();
setCurrentTask(`${label} · \u5269\u4f59 ${formatEta(left)}`);
await sleep(Math.min(15000, Math.max(1000, left)));
}
return !state.stopFlag;
}
async function loadCourseVideos(courseId, tid) {
const res = await fetchVideos(courseId, tid, state.uid);
const ok = res.Code === "0" || res.Code === 0 || res.StatusCode === 0;
return ok && Array.isArray(res.Data) ? res.Data.map((v) => mapVideo(v)) : [];
}
function _fct(lastMs) {
return Math.floor(Date.now() / 1000) - Math.floor(Math.max(0, lastMs) / 1000);
}
async function _wld(courseId, tid, lessonId, { rounds = 8, gapMs = 1000 } = {}) {
let hit = null;
for (let i = 0; i < rounds; i++) {
if (i > 0) await sleep(gapMs);
hit = await _vlc(courseId, tid, lessonId);
if (hit && hit.isfirst === 1) return hit;
}
return hit;
}
async function _sov(video, course, tid) {
if (video.done || video.isfirst === 1) return true;
setCurrentTask(`${truncate(course.name, 8)} / ${video.title}`);
_alp2(course, video);
let startRes;
try {
startRes = await _es({
cid: course.id,
courseId: course.id,
classId: video.id,
lessonId: video.id,
tid,
uid: state.uid,
duration: video.duration || 60,
lasttime: video.lasttime || 0,
clrId: video.clrId || 0,
isfirst: video.isfirst,
done: !!video.done,
courseName: course.name || "",
videoTitle: video.title || "",
learning_user_id: String(state.uid || ""),
});
} catch (e) {
if (String(e && e.code ? e.code : e.message || e).includes("free_quota")) {
_hfq();
}
throw e;
}
const sessionId = startRes.session_id;
let cmd = startRes.command;
if (!cmd) return false;
if (String(cmd.type || "") === "done") {
video.done = true;
video.progress = 100;
video.status = "completed";
video.isfirst = 1;
_plog(`${course.name}${video.title}\u5b8c\u6210`, course.name);
_alp2(course, video);
return true;
}
if (String(cmd.type || "") === "failed") {
throw new Error(cmd.message || "\u4e91\u7aef\u5b66\u4e60\u5931\u8d25");
}
if (String(cmd.type || "") === "run_lesson") {
const result = await _rll(cmd, video, course, tid);
let step;
try {
step = await _estep(sessionId, cmd.event || "lesson_result", result);
} catch (e) {
throw cloudAuthError(
`\u4e91\u7aef\u786e\u8ba4\u5931\u8d25\uff1a${e.message || e}\uff08\u672c\u89c6\u9891\u8fdb\u5ea6\u672a\u8ba1\u5165\uff09`,
"cloud_step_failed"
);
}
const endCmd = step && step.command;
if (endCmd && String(endCmd.type) === "failed") {
throw new Error(endCmd.message || "\u4e91\u7aef\u786e\u8ba4\u5931\u8d25");
}
if (result.ok || (endCmd && String(endCmd.type) === "done")) {
video.done = true;
video.progress = 100;
video.status = "completed";
video.isfirst = 1;
if (result.clrId) video.clrId = result.clrId;
if (result.lasttime) video.lasttime = result.lasttime;
_plog(`${course.name}${video.title}\u5b8c\u6210`, course.name);
_alp2(course, video);
return true;
}
return false;
}
return await _leg(sessionId, cmd, video, course, tid);
}
async function _rll(plan, video, course, tid) {
const fullMs = Number(plan.fullMs) || (video.duration || 60) * 1000;
const stepSec = Number(plan.stepSec) || STEP_SECONDS;
const intervalMs = Number(plan.intervalMs) || REPORT_INTERVAL * 1000;
const crossWaitMs = Number(plan.crossWaitMs) || CROSS_WAIT_SECONDS * 1000;
const crossMax = Number(plan.crossMaxRetry) || CROSS_MAX_RETRY;
let clrId = Number(plan.clrId) || video.clrId || 0;
let lastMs = Math.min(fullMs, Math.max(0, Number(plan.startMs) || video.lasttime || 0));
let crossRetry = 0;
while (!state.stopFlag && lastMs < fullMs) {
const nextMs = Math.min(fullMs, lastMs + stepSec * 1000);
const ct = plan.forge_create_time !== false ? _fct(nextMs) : nowCreateTime();
try {
const res = await _rvp({
courseId: plan.cid || course.id,
lessonId: plan.classId || video.id,
tid: plan.tid || tid,
uid: plan.uid || state.uid,
clrId,
lasttime: nextMs,
isfirst: 0,
createTime: ct,
});
const got = _ecl(res);
if (got) clrId = got;
if (!clrId) {
const hit = await _vlc(course.id, tid, video.id);
if (hit && hit.clrId) clrId = hit.clrId;
}
lastMs = nextMs;
crossRetry = 0;
video.lasttime = lastMs;
video.clrId = clrId;
video.isfirst = 0;
video.progress = formatProgressPct(lastMs, video.duration || 60);
video.done = false;
video.status = "learning";
_plog(progressLine(course, video, lastMs), course.name);
_alp2(course, video);
if (lastMs < fullMs) await sleep(intervalMs);
} catch (e) {
const msg = String(e.message || e);
if (msg.includes("\u8de8\u8fdb\u5ea6") || msg.includes("\u987a\u5e8f")) {
crossRetry += 1;
if (crossRetry > crossMax) {
return { ok: false, message: `\u8de8\u8fdb\u5ea6/\u987a\u5e8f\u91cd\u8bd5\u8d85\u8fc7 ${crossMax} \u6b21`, clrId, lasttime: lastMs };
}
await sleep(crossWaitMs);
continue;
}
return { ok: false, message: msg, clrId, lasttime: lastMs };
}
}
if (state.stopFlag) {
return { ok: false, message: "\u5df2\u505c\u6b62", clrId, lasttime: lastMs, stopped: true };
}
const ctFull = _fct(fullMs);
let finishSubmitted = false;
for (const isfirst of [0, 1]) {
let okFinish = false;
let finishRetry = 0;
while (!state.stopFlag && !okFinish) {
try {
await _rvp({
courseId: plan.cid || course.id,
lessonId: plan.classId || video.id,
tid: plan.tid || tid,
uid: plan.uid || state.uid,
clrId,
lasttime: fullMs,
isfirst,
createTime: ctFull,
});
okFinish = true;
if (isfirst === 1) finishSubmitted = true;
} catch (e) {
const msg = String(e.message || e);
if (msg.includes("\u8de8\u8fdb\u5ea6") || msg.includes("\u987a\u5e8f")) {
finishRetry += 1;
if (finishRetry > crossMax) {
return { ok: false, message: msg, clrId, lasttime: lastMs };
}
await sleep(crossWaitMs);
continue;
}
return { ok: false, message: msg, clrId, lasttime: lastMs };
}
}
await sleep(500);
}
let doneHit = await _wld(course.id, tid, video.id, { rounds: 8, gapMs: 1000 });
if ((!doneHit || doneHit.isfirst !== 1) && finishSubmitted && !state.stopFlag) {
try {
await _rvp({
courseId: plan.cid || course.id,
lessonId: plan.classId || video.id,
tid: plan.tid || tid,
uid: plan.uid || state.uid,
clrId,
lasttime: fullMs,
isfirst: 1,
createTime: _fct(fullMs),
});
} catch (_) {}
doneHit = await _wld(course.id, tid, video.id, { rounds: 6, gapMs: 1200 });
}
if (doneHit && doneHit.isfirst === 1) {
return {
ok: true,
clrId: doneHit.clrId || clrId,
lasttime: doneHit.lasttime || fullMs,
isfirst: 1,
};
}
if (finishSubmitted) {
return { ok: true, clrId, lasttime: fullMs, isfirst: 1, assumed: true };
}
return { ok: false, message: "\u7ed3\u8bfe\u56de\u67e5\u672a\u786e\u8ba4", clrId, lasttime: lastMs };
}
async function _leg(sessionId, cmd, video, course, tid) {
let guard = 0;
while (!state.stopFlag && cmd && guard++ < 800) {
const type = String(cmd.type || "");
if (type === "done") {
video.done = true;
video.progress = 100;
video.status = "completed";
video.isfirst = 1;
_plog(`${course.name}${video.title}\u5b8c\u6210`, course.name);
_alp2(course, video);
return true;
}
if (type === "failed") throw new Error(cmd.message || "\u4e91\u7aef\u5b66\u4e60\u5931\u8d25");
if (type === "wait") {
await sleep(Number(cmd.ms) || 1000);
if (state.stopFlag) return false;
const step = await _estep(sessionId, cmd.event || "tick", { ok: true });
cmd = step.command;
continue;
}
if (type === "save_learning_record") {
const waitMs = Number(cmd.wait_ms) || 0;
if (waitMs > 0) await sleep(waitMs);
if (state.stopFlag) return false;
const lastMs = Number(cmd.lasttime) || 0;
const ct = cmd.forge_create_time !== false ? _fct(lastMs) : nowCreateTime();
let lastResult;
try {
const res = await _rvp({
courseId: cmd.cid || course.id,
lessonId: cmd.classId || video.id,
tid: cmd.tid || tid,
uid: cmd.uid || state.uid,
clrId: cmd.clrId || video.clrId || 0,
lasttime: lastMs,
isfirst: Number(cmd.isfirst) || 0,
createTime: ct,
});
const got = _ecl(res);
if (got) video.clrId = got;
video.lasttime = lastMs;
video.isfirst = Number(cmd.isfirst) || 0;
video.progress = formatProgressPct(lastMs, video.duration || 60);
video.status = video.isfirst === 1 ? "completed" : "learning";
video.done = video.isfirst === 1;
if (video.isfirst !== 1) _plog(progressLine(course, video, lastMs), course.name);
_alp2(course, video);
lastResult = { ok: true, clrId: video.clrId || 0, lasttime: lastMs, isfirst: video.isfirst };
} catch (e) {
lastResult = { ok: false, message: String(e.message || e) };
}
const step = await _estep(sessionId, cmd.event || "save_result", lastResult);
cmd = step.command;
continue;
}
if (type === "verify_lesson") {
const waitMs = Number(cmd.wait_ms) || 0;
if (waitMs > 0) await sleep(waitMs);
const hit = await _vlc(cmd.cid || course.id, cmd.tid || tid, cmd.classId || video.id);
const lastResult = hit
? { ok: true, isfirst: hit.isfirst, lasttime: hit.lasttime, clrId: hit.clrId || 0 }
: { ok: false, isfirst: -1, lasttime: 0, clrId: 0 };
const step = await _estep(sessionId, cmd.event || "verify_result", lastResult);
cmd = step.command;
continue;
}
if (type === "run_lesson") {
const result = await _rll(cmd, video, course, tid);
const step = await _estep(sessionId, cmd.event || "lesson_result", result);
cmd = step.command;
continue;
}
throw new Error("\u672a\u77e5\u4e91\u7aef\u6307\u4ee4: " + type);
}
return false;
}
async function _scs(course, tid) {
let guard = 0;
while (!state.stopFlag && guard++ < 80) {
const videos = await loadCourseVideos(course.id, tid);
const pending = videos
.filter((v) => v.isfirst !== 1)
.sort((a, b) => (a.number || 0) - (b.number || 0));
if (!pending.length) {
_plog(`${course.name}\u5b8c\u6210`, course.name);
const c = findCourse(course.tid, course.id);
if (c) c.progress = 100;
const block = state.chapterPreview.find(
(x) =>
Number(x.courseId) === Number(course.id) &&
Number(x.tid) === Number(course.tid)
);
if (block) {
block.courseProgress = 100;
block.computedProgress = 100;
(block.videos || []).forEach((v) => {
v.progress = 100;
v.done = true;
v.isfirst = 1;
v.status = "completed";
});
}
_rtp(course.tid);
renderCourseList();
renderChapterPreview();
_utp();
updateSummary();
return;
}
const video = pending[0];
const ok = await _sov(video, course, tid);
if (!ok && !state.stopFlag) {
await sleep(1500);
const again = await _vlc(course.id, tid, video.id);
if (again && again.isfirst === 1) {
log(`\u4e8c\u6b21\u786e\u8ba4\u300c${video.title}\u300d\u5df2\u5b8c\u6210\uff0c\u7ee7\u7eed\u540e\u7eed\u8bfe\u65f6`);
_plog(`${course.name}${video.title}\u5b8c\u6210`, course.name);
video.done = true;
video.progress = 100;
video.isfirst = 1;
video.clrId = again.clrId || video.clrId;
_alp2(course, video);
continue;
}
log(`\u300c${course.name}\u300d\u5f53\u524d\u8282\u672a\u5b8c\u6210\uff0c\u505c\u6b62\u8be5\u8bfe\u540e\u7eed`);
return;
}
}
}
async function _rpp() {
const courses = [...state.selectedCourseKeys]
.map((key) => {
const { tid, cid } = parseCourseKey(key);
return findCourse(tid, cid);
})
.filter(Boolean)
.filter((c) => (c.progress || 0) < COMPLETE_THRESHOLD);
if (!courses.length) {
_plog("\u6ca1\u6709\u53ef\u5b66\u4e60\u7684\u52fe\u9009\u8bfe\u7a0b");
return;
}
try {
await refreshChapterPreview({ quiet: true });
} catch (e) {}
const tidCount = new Set(courses.map((c) => c.tid)).size;
log(`\u5f00\u59cb\u5b66\u4e60 · ${courses.length} \u95e8\u8bfe · ${tidCount} \u4e2a\u57f9\u8bad`);
await Promise.all(
courses.map(async (course) => {
try {
await _scs(course, course.tid);
} catch (e) {
log(`\u8bfe\u7a0b\u4e2d\u65ad\u300c${course.name}\u300d\uff1a${e.message || e}`);
}
})
);
}
async function startStudy() {
if (state.running) return;
if (!state.selectedTids.size) {
_plog("\u8bf7\u5148\u52fe\u9009\u57f9\u8bad");
return;
}
if (![...state.selectedCourseKeys].length) {
_plog("\u8bf7\u5148\u52fe\u9009\u8981\u5b66\u4e60\u7684\u8bfe\u7a0b");
return;
}
state.running = true;
state.stopFlag = false;
state.trains.forEach((t) => {
if (state.selectedTids.has(t.id)) {
t.studyBaselineProgress = Number(t.progress ?? t.platformProgress) || 0;
}
});
syncRunButtons();
setCurrentTask("\u591a\u8bfe\u5b66\u4e60\u4e2d…");
try {
await ensureSession();
await _cl(true);
if (!state.cloudLease) {
throw cloudAuthError("\u8bf7\u5148\u5b8c\u6210\u4e91\u7aef\u9274\u6743\u540e\u518d\u5f00\u59cb\u5237\u8bfe");
}
await _rpp();
if (!state.stopFlag) {
_plog("\u5168\u90e8\u52fe\u9009\u8bfe\u7a0b\u5904\u7406\u5b8c\u6bd5");
if (state.finishTip) {
await showPanelAlert(String(state.finishTip), "\u5b66\u4e60\u5b8c\u6210");
}
}
} catch (e) {
if (String(e && e.code ? e.code : e.message || e).includes("free_quota")) {
_hfq();
} else {
_plog(`\u770b\u8bfe\u4e2d\u65ad\uff1a${e.message}`);
}
log(`\u770b\u8bfe\u4e2d\u65ad\uff1a${e.message}`);
} finally {
state.running = false;
state.stopFlag = false;
state.trains.forEach((t) => {
delete t.studyBaselineProgress;
_stpa(t);
});
_utp();
syncRunButtons();
setCurrentTask("\u5f85\u547d");
showLogTab();
try {
await loadCourses({ quiet: true, keepSelection: true });
} catch (e) {}
}
}
function stopStudy() {
if (!state.running) return;
state.stopFlag = true;
setCurrentTask("\u6b63\u5728\u505c\u6b62…");
}
function injectStyles() {
if (document.getElementById("lylg-panel-style")) return;
const st = document.createElement("style");
st.id = "lylg-panel-style";
st.textContent = `
#lylg-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;}
#lylg-auto-panel.lylg-panel-max{max-height:min(92vh,780px);}
#lylg-auto-panel.lylg-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);}
#lylg-auto-panel.lylg-panel-min #lylg-panel-body,#lylg-auto-panel.lylg-panel-min #lylg-panel-footer,#lylg-auto-panel.lylg-panel-min .lylg-footer-extra{display:none !important;}
#lylg-auto-panel.lylg-panel-min #lylg-panel-header{border-bottom:none;}
#lylg-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;}
#lylg-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;}
#lylg-panel-logo{width:30px;height:30px;border-radius:9px;display:block;object-fit:cover;background:#fff;border:1px solid rgba(148,163,184,.45);box-shadow:0 2px 9px rgba(15,23,42,.11);flex:0 0 auto;}
#lylg-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;}
.lylg-panel-title-text{flex:1 1 12em;min-width:0;}
.lylg-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;}
#lylg-panel-sub{display:flex;flex-wrap:wrap;gap:3px 5px;margin-top:3px;}
.lylg-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;}
.lylg-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);}
.lylg-sub-chip-mute{color:#57534e;background:rgba(255,255,255,.55);border-color:rgba(120,113,108,.22);}
#lylg-panel-controls{display:flex;gap:5px;}
.lylg-panel-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1);font-weight:900;}
#lylg-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;}
.lylg-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;}
.lylg-card-status{padding:6px 8px;margin-bottom:6px;}
.lylg-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;}
.lylg-status-label{font-size:11px;color:#64748b;font-weight:700;}
.lylg-status-badge{padding:2px 7px;border-radius:999px;font-weight:800;font-size:11px;background:#fff;border:1px solid #cbd5e1;color:#334155;}
.lylg-status-main{margin-top:3px;}
.lylg-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;}
.lylg-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;}
.lylg-progress-pct{font-size:14px;font-weight:900;color:#0369a1;flex:0 0 auto;}
.lylg-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;}
.lylg-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;}
.lylg-tabbar{display:flex;gap:6px;margin-bottom:7px;}
.lylg-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;}
.lylg-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;}
.lylg-pane{display:none;}.lylg-pane.active{display:block;}
.lylg-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;gap:6px;}
.lylg-list-head-actions{display:flex;align-items:center;gap:5px;flex:0 0 auto;}
.lylg-mini-btn{border:1px solid #cbd5e1;background:#fff;color:#334155;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:700;cursor:pointer;line-height:1.4;white-space:nowrap;}
.lylg-mini-btn:hover{background:#f8fafc;color:#0f172a;}
.lylg-mini-btn.on{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;}
.lylg-list-title{font-size:12px;color:#64748b;font-weight:700;}
.lylg-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;}
.lylg-plan-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 9px;font-size:12px;background:#fff;color:#0f172a;margin-bottom:6px;}
.lylg-train-chips{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:7px;}
.lylg-train-chip{display:inline-flex;align-items:center;gap:5px;border:1px solid #cbd5e1;background:#fff;border-radius:999px;padding:4px 9px 4px 7px;cursor:pointer;font-size:11px;font-weight:700;color:#334155;max-width:100%;}
.lylg-train-chip.active{border-color:#2563eb;background:#eff6ff;color:#1d4ed8;}
.lylg-train-chip input{margin:0;}
.lylg-train-chip .pct{color:#64748b;font-weight:700;}
.lylg-train-chip.active .pct{color:#2563eb;}
.lylg-train-chip.viewing{box-shadow:0 0 0 2px #2563eb;}
#lylg-course-list,#lylg-chapter-preview,#lylg-run-log{max-height:220px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;}
#lylg-train-list,#lylg-train-group{display:none;}
.lylg-train-item{display:none;}
.lylg-train-group-title{display:none;}
.lylg-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;}
.lylg-course-item{display:flex;gap:7px;align-items:flex-start;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:7px;margin-bottom:6px;cursor:pointer;}
.lylg-course-item.done{opacity:.92;background:#f1f5f9;}
.lylg-course-item.active{border-color:#16a34a;background:#f0fdf4;}
.lylg-course-item.lylg-course-limit{opacity:.58;cursor:not-allowed;}
.lylg-course-item.lylg-course-limit input{cursor:not-allowed;}
.lylg-bar{display:block;height:4px;background:#e2e8f0;border-radius:2px;margin-top:5px;overflow:hidden;}
.lylg-bar>i{display:block;height:100%;background:#2563eb;border-radius:2px;}
.lylg-bar>i.full{background:#16a34a;}
.lylg-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;}
.lylg-chapter-title{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;}
.lylg-chapter-item{padding:5px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;color:#334155;}
.lylg-chapter-item.done{color:#64748b;}
.lylg-chapter-item.learning{color:#0369a1;font-weight:600;}
.lylg-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;align-items:center;}
.lylg-meta-label{color:#64748b;}
.lylg-meta-value{font-weight:700;text-align:right;}
.lylg-btn-row{display:flex;gap:7px;margin-top:6px;}
.lylg-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;}
.lylg-btn-refresh{background:#64748b;}
.lylg-btn-primary{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);}
.lylg-btn-start{background:#16a34a;}
.lylg-btn-stop{background:#ef4444;}
.lylg-btn-off,.lylg-btn:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;box-shadow:none;}
.lylg-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;}
.lylg-task-row{font-size:11px;color:#475569;margin-top:6px;line-height:1.35;}
.lylg-task-row em{font-style:normal;font-weight:800;color:#0f172a;}
.lylg-input{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 8px;font-size:12px;box-sizing:border-box;}
.lylg-textarea{width:100%;min-height:72px;border:1px solid #cbd5e1;border-radius:8px;padding:6px 8px;font-size:11px;font-family:ui-monospace,Consolas,monospace;box-sizing:border-box;resize:vertical;}
.lylg-footer-extra{flex:0 0 auto;padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;}
.lylg-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;}
.lylg-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;}
#lylg-panel-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;}
.lylg-log-row{padding:4px 7px 4px 8px;margin-bottom:2px;border-bottom:1px dashed #e2e8f0;border-left:3px solid #94a3b8;border-radius:0 7px 7px 0;font-size:12px;line-height:1.42;background:#fff;}
.lylg-log-time{color:#94a3b8;font-weight:500;margin-right:2px;}
.lylg-log-pct{font-weight:700;text-decoration:underline;text-underline-offset:2px;}
.lylg-log-mins{opacity:.88;}
.lylg-log-done{background:rgba(16,185,129,.1);}
.lylg-log-done-mark{font-weight:700;color:#059669;}
.lylg-log-info{background:#f8fafc;color:#64748b!important;border-left-color:#94a3b8!important;}
.lylg-log-progress{background:color-mix(in srgb, var(--log-c) 7%, #fff);}
#lylg-token-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000001;display:none;align-items:center;justify-content:center;padding:20px;}
#lylg-dialog-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000002;display:none;align-items:center;justify-content:center;padding:20px;}
.lylg-token-card{width:min(560px,92vw);background:#fff;border-radius:16px;border:1px solid #dbe4f0;box-shadow:0 18px 46px rgba(15,23,42,.25);padding:16px;}
.lylg-dialog-title{font-size:17px;font-weight:900;color:#0f172a;margin-bottom:8px;line-height:1.35;}
.lylg-dialog-body{font-size:13px;color:#475569;line-height:1.55;white-space:pre-wrap;word-break:break-word;}
`;
document.head.appendChild(st);
}
function updateAuthUi() {
const badge = document.getElementById("lylg-auth-badge");
const ok = !!(state.token && state.uid);
if (badge) {
badge.textContent = ok
? `uid ${state.uid}`
: state.loading
? "\u52a0\u8f7d\u4e2d"
: "\u672a\u767b\u5f55";
badge.title = ok ? `\u5f53\u524d\u7ed1\u5b9a\u7528\u6237 UserId=${state.uid}` : "\u672a\u7ed1\u5b9a\u767b\u5f55 Token";
badge.style.color = ok ? "#166534" : "#92400e";
badge.style.borderColor = ok ? "#86efac" : "#fcd34d";
badge.style.background = ok ? "#f0fdf4" : "#fffbeb";
}
_ucp();
}
function updateSummary() {
let videoTotal = 0;
let videoDone = 0;
let learnedSec = 0;
let totalSec = 0;
state.chapterPreview.forEach((c) => {
(c.videos || []).forEach((v) => {
videoTotal += 1;
if (v.done) videoDone += 1;
totalSec += Number(v.duration) || 0;
learnedSec += ((Number(v.duration) || 0) * (Number(v.progress) || 0)) / 100;
});
});
const selectedVideoPct =
totalSec > 0
? Math.round((learnedSec / totalSec) * 100)
: videoTotal
? Math.round((videoDone / videoTotal) * 100)
: 0;
const activeTrains = state.trains.filter((t) => state.selectedTids.has(t.id));
let displayPct = selectedVideoPct;
let metricDone = videoDone;
let metricTotal = videoTotal;
let progressHint = "";
if (activeTrains.length === 1) {
const tr = activeTrains[0];
const completed = Number(tr.completedCourseCount) || 0;
const totalCourses =
Number(tr.totalCourseCount) || getTrainCourses(tr.id).length || 0;
const platformPct = Number(tr.platformProgress) || 0;
displayPct = Number(tr.progress) || 0;
metricDone = completed;
metricTotal = totalCourses;
progressHint = state.running
? `\u5df2\u5b8c\u6210 ${completed}/${totalCourses} \u95e8 · \u52fe\u9009\u89c6\u9891 ${videoDone}/${videoTotal}`
: `\u5df2\u5b8c\u6210 ${completed}/${totalCourses} \u95e8\uff08\u4ec5\u6574\u95e8\u8bfe 100% \u8ba1\u6570\uff09· \u5e73\u53f0 ${platformPct}%`;
} else if (activeTrains.length > 1) {
const avg =
activeTrains.reduce((s, t) => s + (Number(t.progress) || 0), 0) / activeTrains.length;
displayPct = Math.round(avg);
progressHint = `\u5df2\u9009 ${activeTrains.length} \u4e2a\u57f9\u8bad`;
}
const selCount = state.selectedCourseKeys.size;
const doneEl = document.getElementById("lylg-queue-done");
const totalEl = document.getElementById("lylg-queue-total");
const pctEl = document.getElementById("lylg-queue-percent");
const bar = document.getElementById("lylg-queue-progress");
const selEl = document.getElementById("lylg-selected-count");
const foot = document.getElementById("lylg-footer-text");
if (selEl) selEl.textContent = String(selCount);
if (doneEl) doneEl.textContent = String(metricDone);
if (totalEl) totalEl.textContent = String(metricTotal);
if (pctEl) pctEl.textContent = `${displayPct}%`;
if (bar) bar.style.width = `${displayPct}%`;
const labelEl = document.getElementById("lylg-progress-label");
if (labelEl) {
labelEl.textContent =
activeTrains.length === 1 ? "\u57f9\u8bad\u603b\u8fdb\u5ea6" : "\u52fe\u9009\u8bfe\u7a0b\u8fdb\u5ea6";
labelEl.title = progressHint || "";
}
if (foot) {
if (state.trains.length) {
const selPending = countPendingVideosForKeys(state.selectedCourseKeys);
const base = `\u5df2\u9009\u57f9\u8bad ${state.selectedTids.size}/${state.trains.length} · \u8bfe\u8868 ${state.courses.length} \u95e8 · \u52fe\u9009 ${selCount} \u95e8`;
foot.textContent = isProTier()
? base
: `${base} · \u5f85\u5b66\u89c6\u9891 ${selPending}/${freeSelectBudget() || 0}`;
} else {
foot.textContent = "\u8bf7\u52fe\u9009\u5df2\u62a5\u540d\u57f9\u8bad";
}
}
}
function syncActiveTrainTab() {
const tids = [...state.selectedTids];
if (!tids.length) {
state.activeTrainTabTid = null;
return;
}
if (!tids.includes(state.activeTrainTabTid)) {
state.activeTrainTabTid = tids[0];
}
}
function shortTrainTabLabel(t) {
if (!t) return "\u57f9\u8bad";
const name = String(t.name || "").trim();
if (name) return truncate(name, 16);
if (t.year) return `${t.year}\u5e74`;
return "\u57f9\u8bad";
}
function renderTrainSelect() {
const chips = document.getElementById("lylg-train-chips");
if (!chips) return;
if (!state.trains.length) {
chips.innerHTML = `\u6682\u65e0\u5df2\u62a5\u540d\u57f9\u8bad
`;
return;
}
syncActiveTrainTab();
chips.innerHTML = state.trains
.map((t) => {
const on = state.selectedTids.has(t.id);
const viewing = Number(state.activeTrainTabTid) === Number(t.id) ? "viewing" : "";
const { done, count } = _ftc(t);
const tip =
count > 0
? `${t.name || "\u57f9\u8bad"} · ${t.progress}% · \u5df2\u5b8c\u6210 ${done}/${count}\u95e8`
: `${t.name || "\u57f9\u8bad"} · ${t.progress}%`;
return ``;
})
.join("");
chips.querySelectorAll("input[type=checkbox]").forEach((el) => {
el.addEventListener("change", async () => {
state.selectedTids = new Set();
chips.querySelectorAll("input[type=checkbox]").forEach((cb) => {
if (cb.checked) state.selectedTids.add(Number(cb.dataset.tid));
});
state.selectedCourseKeys = new Set();
syncActiveTrainTab();
renderTrainSelect();
await loadCourses();
});
});
chips.querySelectorAll(".lylg-train-chip").forEach((chip) => {
chip.addEventListener("click", (e) => {
if (e.target.type === "checkbox") return;
const tid = Number(chip.querySelector("input")?.dataset.tid);
if (!state.selectedTids.has(tid)) return;
state.activeTrainTabTid = tid;
renderTrainSelect();
renderCourseList();
});
});
}
function isProTier() {
return state.cloudTier === "pro";
}
function freeSelectBudget() {
if (isProTier()) return Infinity;
return Math.max(
0,
(Number(state.freeVideoLimit) || 3) - (Number(state.freeUsedVideos) || 0)
);
}
function countPendingVideosForKeys(keys) {
const keyList = Array.isArray(keys) ? keys : [...keys];
let total = 0;
for (const key of keyList) {
const { tid, cid } = parseCourseKey(key);
const block = state.chapterPreview.find(
(x) => Number(x.tid) === tid && Number(x.courseId) === cid
);
if (block && Array.isArray(block.videos) && block.videos.length) {
total += block.videos.filter((v) => !v.done && v.isfirst !== 1).length;
} else {
const course = findCourse(tid, cid);
if (course && (course.progress || 0) < COMPLETE_THRESHOLD) total += 1;
}
}
return total;
}
function trimKeysToFreeBudget(keys) {
if (isProTier()) return [...keys];
const budget = freeSelectBudget();
if (budget <= 0) return [];
const kept = [];
for (const key of keys) {
if (countPendingVideosForKeys([...kept, key]) <= budget) kept.push(key);
else break;
}
return kept;
}
function notifyFreeSelectLimit(budget, pending) {
if (budget <= 0) {
_plog("\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro");
showPanelAlert("\u514d\u8d39\u4f53\u9a8c\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro \u540e\u7ee7\u7eed\u52fe\u9009\u3002", "\u5347\u7ea7 Pro");
return;
}
_plog(`\u514d\u8d39\u7248\u6700\u591a\u52fe\u9009 ${budget} \u4e2a\u5f85\u5b66\u89c6\u9891\uff08\u5f53\u524d ${pending} \u4e2a\uff09`);
showPanelAlert(
`\u514d\u8d39\u7248\u6700\u591a\u52fe\u9009 ${budget} \u4e2a\u5f85\u5b66\u89c6\u9891\u3002\nPro \u4e0d\u9650\u52fe\u9009\u4e0e\u5237\u8bfe\u3002`,
"\u52fe\u9009\u9650\u5236"
);
}
function getIncompleteCourseKeys(scopeTid) {
return state.courses
.filter((c) => (c.progress || 0) < COMPLETE_THRESHOLD)
.filter((c) => (scopeTid == null ? true : Number(c.tid) === Number(scopeTid)))
.map((c) => courseKey(c.tid, c.id));
}
function toggleIncompleteCourses() {
const tid = state.activeTrainTabTid;
const incomplete = getIncompleteCourseKeys(tid);
if (!incomplete.length) {
_plog("\u5f53\u524d\u57f9\u8bad\u6ca1\u6709\u672a\u5b8c\u6210\u8bfe\u7a0b");
return;
}
const allChecked = incomplete.every((k) => state.selectedCourseKeys.has(k));
if (allChecked) {
incomplete.forEach((k) => state.selectedCourseKeys.delete(k));
} else {
const other = [...state.selectedCourseKeys].filter((k) => {
const { tid: t } = parseCourseKey(k);
return Number(t) !== Number(tid);
});
const merged = [...other, ...incomplete];
if (!isProTier()) {
const budget = freeSelectBudget();
const pending = countPendingVideosForKeys(merged);
if (pending > budget) {
const trimmed = trimKeysToFreeBudget(merged);
state.selectedCourseKeys = new Set(trimmed);
notifyFreeSelectLimit(budget, pending);
} else {
state.selectedCourseKeys = new Set(merged);
}
} else {
incomplete.forEach((k) => state.selectedCourseKeys.add(k));
}
}
renderCourseList();
syncToggleIncompleteBtn();
updateSummary();
refreshChapterPreview();
}
function syncToggleIncompleteBtn() {
const btn = document.getElementById("lylg-toggle-incomplete");
if (!btn) return;
const incomplete = getIncompleteCourseKeys(state.activeTrainTabTid);
btn.textContent = "\u52fe\u9009/\u53d6\u6d88";
if (!incomplete.length) {
btn.classList.remove("on");
btn.disabled = true;
return;
}
btn.disabled = false;
const allChecked = incomplete.every((k) => state.selectedCourseKeys.has(k));
btn.classList.toggle("on", allChecked);
if (!isProTier()) {
const budget = freeSelectBudget();
btn.title =
budget > 0
? `\u514d\u8d39\u7248\u6700\u591a\u52fe\u9009 ${budget} \u4e2a\u5f85\u5b66\u89c6\u9891`
: "\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro";
} else {
btn.title = "\u52fe\u9009/\u53d6\u6d88\u5f53\u524d Tab \u672a\u5b8c\u6210\u8bfe\u7a0b";
}
}
function renderCourseList() {
const box = document.getElementById("lylg-course-list");
if (!box) return;
if (!state.selectedTids.size) {
box.innerHTML = `\u8bf7\u5148\u52fe\u9009\u57f9\u8bad
`;
syncToggleIncompleteBtn();
return;
}
syncActiveTrainTab();
const tid = state.activeTrainTabTid;
const list = state.courses.filter((c) => Number(c.tid) === Number(tid));
if (!list.length) {
box.innerHTML = `\u8be5\u57f9\u8bad\u6682\u65e0\u8bfe\u7a0b
`;
syncToggleIncompleteBtn();
return;
}
const budget = freeSelectBudget();
const selectedPending = countPendingVideosForKeys(state.selectedCourseKeys);
const atFreeLimit = !isProTier() && selectedPending >= budget;
box.innerHTML = list
.map((c) => {
const pct = Math.min(100, Math.max(0, c.progress || 0));
const key = courseKey(c.tid, c.id);
const checked = state.selectedCourseKeys.has(key) ? "checked" : "";
const done = pct >= TRAIN_COURSE_COMPLETE_PCT;
const disableCheck = !isProTier() && !checked && !done && atFreeLimit;
return `
`;
})
.join("");
box.querySelectorAll("input[type=checkbox]").forEach((el) => {
el.addEventListener("change", () => {
const other = [...state.selectedCourseKeys].filter((k) => {
const { tid: t } = parseCourseKey(k);
return Number(t) !== Number(tid);
});
const cur = [];
box.querySelectorAll("input[type=checkbox]").forEach((cb) => {
if (cb.checked) cur.push(cb.dataset.ckey);
});
const proposed = [...other, ...cur];
if (!isProTier()) {
const max = freeSelectBudget();
const pending = countPendingVideosForKeys(proposed);
if (pending > max) {
el.checked = false;
notifyFreeSelectLimit(max, pending);
return;
}
}
state.selectedCourseKeys = new Set(proposed);
renderCourseList();
updateSummary();
refreshChapterPreview();
});
});
syncToggleIncompleteBtn();
syncFreeSelectTag();
updateSummary();
}
function renderChapterPreview() {
const box = document.getElementById("lylg-chapter-preview");
if (!box) return;
if (!state.chapterPreview.length) {
box.innerHTML = `\u8bf7\u52fe\u9009\u8bfe\u7a0b\u540e\u67e5\u770b\u7ae0\u8282\u8fdb\u5ea6
`;
return;
}
box.innerHTML = state.chapterPreview
.map((course) => {
if (course.error) {
return `${escHtml(course.trainLabel || "")} · ${escHtml(course.courseTitle)}
${escHtml(course.error)}
`;
}
const items = (course.videos || [])
.map((v) => {
const cls = v.done ? "done" : v.status === "learning" ? "learning" : "";
const mark = v.done ? "✓" : v.status === "learning" ? "▶" : "•";
return `${mark} ${escHtml(v.number ? `${v.number}. ` : "")}${escHtml(v.title)} ${formatTime(v.duration)}${Math.round(v.progress)}%
`;
})
.join("");
return `${escHtml(course.trainLabel || "")} · ${escHtml(course.courseTitle)} · \u8bfe ${course.courseProgress}% · \u8bfe\u65f6\u5408\u8ba1 ${course.computedProgress ?? 0}%
${items || `
\u65e0\u8bfe\u65f6
`}
`;
})
.join("");
}
function bindDrag(panel) {
const header = panel.querySelector("#lylg-panel-header");
let sx = 0,
sy = 0,
ox = 0,
oy = 0,
dragging = false;
header.addEventListener("mousedown", (e) => {
if (e.target.closest("#lylg-panel-controls")) return;
dragging = true;
sx = e.clientX;
sy = e.clientY;
const rect = panel.getBoundingClientRect();
ox = rect.left;
oy = rect.top;
e.preventDefault();
});
window.addEventListener("mousemove", (e) => {
if (!dragging) return;
const left = Math.max(0, ox + e.clientX - sx);
const top = Math.max(0, oy + e.clientY - sy);
panel.style.left = left + "px";
panel.style.top = top + "px";
panel.style.right = "auto";
});
window.addEventListener("mouseup", () => {
if (!dragging) return;
dragging = false;
try {
GM_setValue(PANEL_POS_KEY, { left: panel.style.left, top: panel.style.top });
} catch (e) {}
});
}
let panelDialogResolve = null;
function closePanelDialog(result) {
const modal = document.getElementById("lylg-dialog-modal");
if (modal) modal.style.display = "none";
if (panelDialogResolve) {
const done = panelDialogResolve;
panelDialogResolve = null;
done(result);
}
}
function ensurePanelDialog() {
let modal = document.getElementById("lylg-dialog-modal");
if (modal) return modal;
modal = document.createElement("div");
modal.id = "lylg-dialog-modal";
modal.innerHTML = `
`;
document.body.appendChild(modal);
modal.addEventListener("click", (e) => {
if (e.target === modal && modal.dataset.dismissible === "1") closePanelDialog(true);
});
return modal;
}
function showPanelAlert(message, title) {
return new Promise((resolve) => {
const modal = ensurePanelDialog();
modal.dataset.dismissible = "1";
modal.querySelector("#lylg-dialog-title").textContent = title || "\u63d0\u793a";
modal.querySelector("#lylg-dialog-body").textContent = String(message || "");
const actions = modal.querySelector("#lylg-dialog-actions");
actions.innerHTML =
'';
panelDialogResolve = () => resolve(true);
actions.querySelector("#lylg-dialog-ok").onclick = () => closePanelDialog(true);
modal.style.display = "flex";
});
}
function showPanelConfirm(message, opts) {
const o = opts || {};
return new Promise((resolve) => {
const modal = ensurePanelDialog();
modal.dataset.dismissible = "0";
modal.querySelector("#lylg-dialog-title").textContent = o.title || "\u786e\u8ba4";
modal.querySelector("#lylg-dialog-body").textContent = String(message || "");
const actions = modal.querySelector("#lylg-dialog-actions");
actions.innerHTML = `
`;
panelDialogResolve = resolve;
actions.querySelector("#lylg-dialog-cancel").onclick = () => closePanelDialog(false);
actions.querySelector("#lylg-dialog-confirm").onclick = () => closePanelDialog(true);
modal.style.display = "flex";
});
}
function showTokenModal() {
let modal = document.getElementById("lylg-token-modal");
if (!modal) {
modal = document.createElement("div");
modal.id = "lylg-token-modal";
modal.innerHTML = `
\u8bbe\u7f6e\u767b\u5f55 Token
F12 → Network → \u8bf7\u6c42\u5934 Authorization: Bearer \u540e\u7684 JWT
\u6293\u5305\u6837\u4f8b\u7ad9\u70b9\uff1anewapi.ylxue.net · payload \u542b data.UserId / CustomerNo=${CUSTOMER_NO}
`;
document.body.appendChild(modal);
modal.addEventListener("click", (e) => {
if (e.target === modal) modal.style.display = "none";
});
modal.querySelector("#lylg-token-cancel").onclick = () => {
modal.style.display = "none";
};
modal.querySelector("#lylg-token-ok").onclick = async () => {
const raw = modal.querySelector("#lylg-token-input").value;
const msg = modal.querySelector("#lylg-token-msg");
const ok = persistToken(raw);
if (!ok) {
msg.textContent = normalizeToken(raw)
? "JWT \u4e2d\u65e0 UserId\uff0c\u8bf7\u7c98\u8d34\u767b\u5f55\u7528\u6237 Token"
: "Token \u683c\u5f0f\u65e0\u6548";
return;
}
msg.textContent = `\u5df2\u7ed1\u5b9a uid=${state.uid}`;
msg.style.color = "#166534";
modal.style.display = "none";
updateAuthUi();
await loadTrains();
};
}
modal.querySelector("#lylg-token-input").value = state.token || "";
modal.querySelector("#lylg-token-msg").textContent = "";
modal.style.display = "flex";
}
function createPanel() {
document.querySelectorAll("#lylg-auto-panel, .lylg-auto-panel").forEach((el) => el.remove());
injectStyles();
const panel = document.createElement("div");
panel.id = "lylg-auto-panel";
panel.className = "lylg-panel-max";
panel.innerHTML = `
\u57f9\u8bad\u603b\u8fdb\u5ea6
\u672a\u767b\u5f55
\u5df2\u9009 0 \u95e8
·
\u5df2\u5b8c\u6210 0/0 \u95e8
0%
\u57f9\u8bad\u591a\u9009 · \u70b9\u82af\u7247\u5207\u8bfe\u7a0b
\u514d\u8d39\u52fe\u9009
\u591a\u8bfe\u540c\u5237
\u767b\u5f55\u540e\u52a0\u8f7d\u57f9\u8bad
\u767b\u5f55\u540e\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
\u7ae0\u8282\u9884\u89c8\u8bfe\u65f6\u8fdb\u5ea6
\u8bf7\u52fe\u9009\u8bfe\u7a0b
\u8003\u8bd5\u5217\u8868
\u4ec5 Pro
\u5b66\u5b8c\u57f9\u8bad\u540e\u53ef\u8003\u8bd5\uff08\u4ec5 Pro\uff09
\u8fd0\u884c\u65e5\u5fd7
\u4e91\u7aef Pro\u6388\u6743
\u6863\u4f4d\u514d\u8d39\u7248
\u5230\u671f—
\u5f53\u524d\u4efb\u52a1\uff1a\u5f85\u547d
`;
document.body.appendChild(panel);
try {
const pos = GM_getValue(PANEL_POS_KEY);
if (pos?.left && pos?.top) {
panel.style.left = pos.left;
panel.style.top = pos.top;
panel.style.right = "auto";
}
if (GM_getValue(PANEL_COLLAPSED_KEY)) {
panel.classList.add("lylg-panel-min");
panel.classList.remove("lylg-panel-max");
panel.querySelector("#lylg-btn-min").style.display = "none";
panel.querySelector("#lylg-btn-max").style.display = "";
}
} catch (e) {}
bindDrag(panel);
panel.querySelector("#lylg-btn-min").onclick = () => {
panel.classList.add("lylg-panel-min");
panel.classList.remove("lylg-panel-max");
panel.querySelector("#lylg-btn-min").style.display = "none";
panel.querySelector("#lylg-btn-max").style.display = "";
try {
GM_setValue(PANEL_COLLAPSED_KEY, true);
} catch (e) {}
};
panel.querySelector("#lylg-btn-max").onclick = () => {
panel.classList.remove("lylg-panel-min");
panel.classList.add("lylg-panel-max");
panel.querySelector("#lylg-btn-min").style.display = "";
panel.querySelector("#lylg-btn-max").style.display = "none";
try {
GM_setValue(PANEL_COLLAPSED_KEY, false);
} catch (e) {}
};
panel.querySelectorAll(".lylg-tab-btn").forEach((btn) => {
btn.addEventListener("click", () => {
panel.querySelectorAll(".lylg-tab-btn").forEach((b) => b.classList.remove("active"));
panel.querySelectorAll(".lylg-pane").forEach((p) => p.classList.remove("active"));
btn.classList.add("active");
panel.querySelector(`.lylg-pane[data-pane="${btn.dataset.tab}"]`)?.classList.add("active");
if (btn.dataset.tab === "exam") _rel();
});
});
panel.querySelector("#lylg-toggle-incomplete").onclick = () => toggleIncompleteCourses();
panel.querySelector("#lylg-start").onclick = () => startStudy();
panel.querySelector("#lylg-stop").onclick = () => stopStudy();
panel.querySelector("#lylg-refresh-exam")?.addEventListener("click", () => _rel());
panel.querySelector("#lylg-buy-pro")?.addEventListener("click", () =>
openUrl(state.proBuyUrl || PRO_BUY_URL)
);
panel.querySelector("#lylg-save-cloud-token")?.addEventListener("click", async () => {
const inp = panel.querySelector("#lylg-cloud-token-input");
setCloudToken(inp ? inp.value : "");
state.tokenBoundBlocked = false;
_icc();
try {
await _cl(true, { strict: true });
const siteTip = state.cloudSiteBindOnActivate
? "\uff08\u9996\u6b21\u6fc0\u6d3b\u5df2\u7ed1\u5b9a\u672c\u7ad9\uff09"
: "";
_plog(
state.cloudTier === "pro"
? `Pro Token \u9a8c\u8bc1\u6210\u529f${siteTip}`
: `\u5f53\u524d\u514d\u8d39\u6863 · \u5df2\u7528 ${state.freeUsedVideos}/${state.freeVideoLimit} \u89c6\u9891`
);
} catch (e) {
_plog(`\u4e91\u7aef\u9a8c\u8bc1\u5931\u8d25\uff1a${e.userMessage || e.message || e}`);
}
_ucp();
});
syncRunButtons();
setCurrentTask("\u5f85\u547d");
panel.querySelector("#lylg-clear-token").onclick = () => {
clearToken();
resetUserData("\u5df2\u6e05\u9664 Token \u7f13\u5b58\u4e0e\u8bfe\u7a0b\u6570\u636e");
updateAuthUi();
};
panel.querySelector("#lylg-clear-log").onclick = () => {
state.logLines = [];
const box = document.getElementById("lylg-run-log");
if (box) box.innerHTML = `\u6682\u65e0\u65e5\u5fd7
`;
};
updateAuthUi();
_upn();
}
function init() {
persistActiveSitePref();
document.querySelectorAll("#lylg-auto-panel").forEach((el) => el.remove());
setupTokenInterceptor();
getCloudToken();
createPanel();
updateAuthUi();
_plog(`\u5df2\u9002\u914d ${SITE_NAME}\uff08${WEB_SITE}\uff09`, "site");
setTimeout(async () => {
try {
await _lcc();
await _cl(false);
} catch (_) {}
let token = getLivePageToken();
if (!token) token = await waitForLiveToken(5000);
if (!token) token = getToken();
else persistToken(token, { from: "\u542f\u52a8" });
updateAuthUi();
if (state.uid) {
try {
await _cl(true);
} catch (_) {}
await loadTrains();
}
}, 600);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();