// ==UserScript==
// @name 【效率版】和田专业技术人员继续教育学习助手
// @namespace https://card.wlxy.live/links/09A67934
// @version 1.1
// @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png
// @description 和田地区专业技术人员继续教育(htzj.ylxue.net)学习辅助:已报名培训课程全部勾选、多章节课时同步进行、章节预览;免费体验3课时,Pro不限;学完后考试自动交卷(仅Pro)。
// @antifeature payment 免费体验3课时,升级Pro不限制
// @antifeature membership 需云端授权
// @author 柠檬真酸
// @match https://*.ylxue.net/*
// @match https://plugservice-v3.ylxue.net/*
// @noframes
// @connect oa16.ahzsksw.cn
// @connect newapi.ylxue.net
// @connect learningapi.ylxue.net
// @connect learn.ylxue.net
// @connect examinationapi.ylxue.net
// @connect plugservice-v3.ylxue.net
// @connect ylxue.net
// @connect htzj.ylxue.net
// @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 全自动看课考试
// @license All Rights Reserved
// ==/UserScript==
(function () {
"use strict";
try {
if (window.top !== window.self) return;
} catch (_) {
return;
}
const BOOT_KEY = "__HTZJ_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 "v14.1.4";
}
const SCRIPT_VERSION = "14.1.4";
const PANEL_LOGO_URL =
"https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png";
const CLOUD_API_BASE = "https://oa16.ahzsksw.cn";
const PRO_BUY_URL = "https://card.wlxy.live/details/CB710114";
const CLOUD_TOKEN_KEY = "htzj_cloud_token_v1";
const CLOUD_LEASE_KEY = "htzj_cloud_lease_v1";
const FINISH_TIP =
"\u6240\u9009\u8bfe\u7a0b\u5df2\u5168\u90e8\u5b66\u5b8c\u3002\u82e5\u5e73\u53f0\u8fdb\u5ea6\u7a0d\u540e\u66f4\u65b0\u5c5e\u6b63\u5e38\uff1b\u8003\u8bd5\u5217\u8868\u9700 Pro \u540e\u53ef\u7528\u3002";
const PANEL_NOTICE =
"\u514d\u8d39\u4f53\u9a8c 3 \u4e2a\u8bfe\u65f6\uff1bPro \u4e0d\u9650\u3002\u8003\u8bd5\u5217\u8868\u4ec5 Pro\u3002\u8d2d\u4e70\uff1a" + PRO_BUY_URL;
const LOG_PALETTE = [
"#0369a1",
"#0f766e",
"#b45309",
"#a21caf",
"#be123c",
"#1d4ed8",
"#047857",
"#c2410c",
"#7c3aed",
"#0e7490",
"#4f46e5",
"#ca8a04",
];
const LOG_LIMIT = 400;
const CUSTOMER_NO = "htrsj20200908";
const WEB_SITE = "htzj.ylxue.net";
const TOKEN_KEY = "htzj_cached_token_v1";
const PANEL_POS_KEY = "htzj_panel_pos_v1";
const PANEL_COLLAPSED_KEY = "htzj_panel_collapsed_v1";
const BATCH_JOBS_KEY = "htzj_batch_jobs_v1";
const COMPLETE_THRESHOLD = 95;
const STEP_SECONDS = 60;
const REPORT_INTERVAL = 5;
const CROSS_WAIT_SECONDS = 10;
const CROSS_MAX_RETRY = 40;
const JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
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,
tokenBoundBlocked: false,
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;
}
return new Error(String(detail.message || detail.code || JSON.stringify(detail)));
}
return new Error(String(detail || `\u4e91\u7aef HTTP ${status}`));
}
async function _crq(path, method, payload) {
const base = CLOUD_API_BASE.replace(/\/$/, "");
const url = base + (path.startsWith("/") ? path : "/" + path);
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
};
const token = getCloudToken();
if (token && !state.tokenBoundBlocked) headers.Authorization = "Bearer " + token;
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 !== "/api/htzj/lease" && state.cloudLease && bodyObj.lease == null) {
bodyObj.lease = state.cloudLease;
}
if (path === "/api/htzj/lease" && state.tokenBoundBlocked) bodyObj.token = "";
}
const res = await _gj(url, {
method: method || "POST",
headers,
data: bodyObj == null ? null : JSON.stringify(bodyObj),
});
let data = {};
const text = String(res.text || "").trim();
if (text) {
try {
data = JSON.parse(text);
} catch (_) {
data = { message: text.slice(0, 200) };
}
}
if (res.status < 200 || res.status >= 300) {
throw _pcd(data, res.status);
}
return data;
}
function _alp(data) {
if (!data || typeof data !== "object") return;
if (data.lease) {
state.cloudLease = String(data.lease || "");
state.cloudLeaseExp =
Number(data.lease_expires_at || data.exp || 0) ||
Math.floor(Date.now() / 1000) + 600;
try {
GM_setValue(CLOUD_LEASE_KEY, {
lease: state.cloudLease,
exp: state.cloudLeaseExp,
tier: String(data.tier || ""),
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.finishTip) state.finishTip = String(data.finishTip);
if (data.panelNotice) state.panelNotice = String(data.panelNotice);
_ucp();
_upn();
}
function _lcl() {
try {
const cached = GM_getValue(CLOUD_LEASE_KEY, null);
if (!cached || typeof cached !== "object") return null;
const lease = String(cached.lease || "").trim();
const exp = Number(cached.exp || 0);
if (!lease || !exp) return null;
if (exp - Math.floor(Date.now() / 1000) <= 60) return null;
const boundUid = String(cached.boundUid || "").trim();
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) {
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("/api/htzj/lease", "POST", {
learning_user_id: state.uid ? String(state.uid) : "",
userid: state.uid ? String(state.uid) : "",
token: tokenOverride != null ? tokenOverride : state.tokenBoundBlocked ? "" : getCloudToken(),
});
let data;
try {
data = await postLease();
state.tokenBoundBlocked = false;
} catch (e) {
if (String(e && e.code ? e.code : e.message || e).includes("token_bound")) {
state.tokenBoundBlocked = true;
_icc();
state.cloudTier = "free";
state.cloudProExpireAt = 0;
_plog("Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\uff0c\u672c\u8d26\u53f7\u6309\u514d\u8d39\u4f53\u9a8c");
data = await postLease("");
} else {
throw e;
}
}
_alp(data);
if (!state.cloudLeaseExp) state.cloudLeaseExp = now + 600;
return !!state.cloudLease;
}
async function _es(context) {
await _cl(false);
const data = await _crq("/api/htzj/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) {
await _cl(false);
return _crq("/api/htzj/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();
try {
if (confirm("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u662f\u5426\u6253\u5f00 Pro \u8d2d\u4e70\u9875\uff1f")) openUrl(url);
} catch (_) {
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("htzj-cloud-tier");
const quotaEl = document.getElementById("htzj-cloud-quota");
const expireEl = document.getElementById("htzj-cloud-expire");
const buyBtn = document.getElementById("htzj-buy-pro");
const settingsTier = document.getElementById("htzj-settings-tier");
const settingsExpire = document.getElementById("htzj-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 = isPro
? "\u8bfe\u65f6\u4e0d\u9650"
: `\u4f53\u9a8c ${state.freeUsedVideos}/${state.freeVideoLimit} \u8bfe\u65f6`;
quotaEl.classList.toggle("htzj-sub-chip-em", isPro);
}
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 = "\u672a\u5f00\u901a Pro";
expireEl.title = "";
}
}
if (settingsTier) settingsTier.textContent = isPro ? "Pro" : "\u514d\u8d39\u7248";
if (settingsExpire) {
settingsExpire.textContent =
isPro && expireText ? expireText : isPro ? "\u5df2\u5f00\u901a" : "—";
}
if (buyBtn) {
buyBtn.style.display = "";
buyBtn.textContent = isPro ? "\u7eed\u8d39 / \u67e5\u770b Pro" : "\u5347\u7ea7 Pro";
}
const inp = document.getElementById("htzj-cloud-token-input");
if (inp && getCloudToken() && !inp.value) inp.value = getCloudToken();
}
function _upn() {
const el = document.getElementById("htzj-ann-text");
if (el) el.textContent = String(state.panelNotice || PANEL_NOTICE);
}
async function _lcc() {
try {
const cfg = await _crq("/api/htzj/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);
_ucp();
_upn();
} catch (_) {}
}
async function _rel() {
const box = document.getElementById("htzj-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("#htzj-exam-buy")?.addEventListener("click", () =>
openUrl(state.proBuyUrl || PRO_BUY_URL)
);
}
return;
}
try {
await _crq("/api/htzj/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\u5df2\u5b66\u5b8c\u4e14\u5f00\u542f\u8003\u8bd5\u7684\u57f9\u8bad
\u9700 f_Learning_Progress=100 \u4e14 i_examSettings=1
`;
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(".htzj-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("#htzj-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("htzj-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;
block.computedProgress = computed;
block.courseProgress = computed;
block.courseTitle = course.name;
if (c) c.progress = computed;
_rtp(tid);
renderCourseList();
renderChapterPreview();
_utp();
updateSummary();
}
function _rtp(tid) {
const t = state.trains.find((x) => Number(x.id) === Number(tid));
if (!t) return 0;
const courses = state.courses.filter((c) => Number(c.tid) === Number(tid));
let learned = 0;
let courseHoursSum = 0;
courses.forEach((c) => {
const w = Math.max(0, Number(c.classHours) || Number(c.length) || 0);
courseHoursSum += w;
learned += (w * (Number(c.progress) || 0)) / 100;
});
if (!courses.length) {
const blocks = state.chapterPreview.filter((b) => Number(b.tid) === Number(tid));
let learnedSec = 0;
let totalSec = 0;
blocks.forEach((b) => {
(b.videos || []).forEach((v) => {
const dur = Number(v.duration) || 0;
totalSec += dur;
learnedSec += (dur * (Number(v.progress) || 0)) / 100;
});
});
if (totalSec > 0) {
t.progress = Math.min(100, Math.round((learnedSec / totalSec) * 100));
}
return t.progress;
}
const selected =
Number(t.selectedHours) ||
courseHoursSum ||
60;
t.learnedHoursLocal = Math.round(learned * 10) / 10;
t.progress = selected > 0 ? Math.min(100, Math.round((learned / selected) * 100)) : 0;
return t.progress;
}
function _utp() {
state.trains.forEach((t) => {
const pct = `${Number(t.progress) || 0}%`;
document
.querySelectorAll(`#htzj-train-chips input[data-tid="${t.id}"]`)
.forEach((inp) => {
const chip = inp.closest(".htzj-train-chip");
const el = chip && chip.querySelector(".pct");
if (el) el.textContent = pct;
});
document.querySelectorAll(`#htzj-train-tabs .htzj-train-tab[data-tid="${t.id}"]`).forEach((btn) => {
const subs = btn.querySelectorAll(".sub");
if (subs[0]) subs[0].textContent = pct;
});
});
}
function showLogTab() {
const panel = document.getElementById("htzj-panel");
if (!panel) return;
panel.querySelectorAll(".htzj-tab-btn").forEach((b) => b.classList.remove("active"));
panel.querySelectorAll(".htzj-pane").forEach((p) => p.classList.remove("active"));
panel.querySelector('.htzj-tab-btn[data-tab="log"]')?.classList.add("active");
panel.querySelector('.htzj-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 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 getUidFromToken(token) {
const payload = parseJwtPayload(token);
if (!payload) return null;
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);
for (const bag of bags) {
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 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.__htzjPatched) {
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.__htzjPatched = true;
}
const XHR = win.XMLHttpRequest;
if (XHR?.prototype && !XHR.prototype.__htzjPatched) {
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.__htzjPatched = true;
}
if (!win.__htzjTokenWatch) {
win.__htzjTokenWatch = 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 || "https://plugservice-v3.ylxue.net",
Referer: referer || "https://plugservice-v3.ylxue.net/",
},
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";
if (ok) resolve(json);
else if (json.StatusCode === 401 || json.code === 401) {
clearToken();
reject(new Error("Token \u5df2\u8fc7\u671f"));
} else reject(new Error(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; // \u9632\u5fa1\uff1a\u9009\u9879\u8fc7\u591a\u4e0d\u7206\u7b97
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(/^\u548c\u7530\u5730\u533a\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2/, "").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,
};
}
async function loadPlatformExamRows() {
const uid = state.uid || (await ensureSession());
const rows = [];
for (const t of state.trains) {
if (Number(t.examSettings) !== 1 && Number(t.raw?.i_examSettings) !== 1) 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: "https://plugservice-v3.ylxue.net",
referer: "https://plugservice-v3.ylxue.net/",
}).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),
selectedHours: Number(
c.i_selClassNum ??
c.d_selClassNum ??
c.i_SelectClassNum ??
c.d_SelectClassHour ??
c.i_classHours ??
c.d_classHours ??
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("htzj-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: Math.round(Number(c.i_learningProgress) || 0),
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) {
state.selectedCourseKeys = new Set(
state.courses
.filter((c) => c.progress < COMPLETE_THRESHOLD)
.map((c) => courseKey(c.tid, c.id))
);
if (!state.selectedCourseKeys.size && state.courses[0]) {
const c0 = state.courses[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();
renderTrainSelect();
renderCourseList();
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("htzj-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;
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("htzj-current-task");
if (el) {
el.textContent = state.currentTask || "\u5f85\u547d";
el.title = state.currentTask || "";
}
}
function syncRunButtons() {
const startBtn = document.getElementById("htzj-start");
const stopBtn = document.getElementById("htzj-stop");
if (startBtn) {
startBtn.disabled = !!state.running;
startBtn.classList.toggle("htzj-btn-off", !!state.running);
}
if (stopBtn) {
stopBtn.disabled = !state.running;
stopBtn.classList.toggle("htzj-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;
}
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) {
if (result && result.ok) {
video.done = true;
video.progress = 100;
video.isfirst = 1;
_plog(`${course.name}${video.title}\u5b8c\u6210`, course.name);
_alp2(course, video);
return true;
}
throw e;
}
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;
syncRunButtons();
setCurrentTask("\u591a\u8bfe\u5b66\u4e60\u4e2d…");
try {
await ensureSession();
await _cl(true);
await _rpp();
if (!state.stopFlag) {
_plog("\u5168\u90e8\u52fe\u9009\u8bfe\u7a0b\u5904\u7406\u5b8c\u6bd5");
if (state.finishTip) {
try {
alert(String(state.finishTip));
} catch (_) {}
}
}
} 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;
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("htzj-panel-style")) return;
const st = document.createElement("style");
st.id = "htzj-panel-style";
st.textContent = `
#htzj-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;}
#htzj-auto-panel.htzj-panel-max{max-height:min(92vh,780px);}
#htzj-auto-panel.htzj-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);}
#htzj-auto-panel.htzj-panel-min #htzj-panel-body,#htzj-auto-panel.htzj-panel-min #htzj-panel-footer,#htzj-auto-panel.htzj-panel-min .htzj-footer-extra{display:none !important;}
#htzj-auto-panel.htzj-panel-min #htzj-panel-header{border-bottom:none;}
#htzj-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;}
#htzj-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;}
#htzj-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;}
#htzj-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;}
.htzj-panel-title-text{flex:1 1 12em;min-width:0;}
.htzj-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;}
#htzj-panel-sub{display:flex;flex-wrap:wrap;gap:3px 5px;margin-top:3px;}
.htzj-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;}
.htzj-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);}
.htzj-sub-chip-mute{color:#57534e;background:rgba(255,255,255,.55);border-color:rgba(120,113,108,.22);}
#htzj-panel-controls{display:flex;gap:5px;}
.htzj-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;}
#htzj-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;}
.htzj-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;}
.htzj-card-status{padding:6px 8px;margin-bottom:6px;}
.htzj-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;}
.htzj-status-label{font-size:11px;color:#64748b;font-weight:700;}
.htzj-status-badge{padding:2px 7px;border-radius:999px;font-weight:800;font-size:11px;background:#fff;border:1px solid #cbd5e1;color:#334155;}
.htzj-status-main{margin-top:3px;}
.htzj-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;}
.htzj-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;}
.htzj-progress-pct{font-size:14px;font-weight:900;color:#0369a1;flex:0 0 auto;}
.htzj-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;}
.htzj-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;}
.htzj-tabbar{display:flex;gap:6px;margin-bottom:7px;}
.htzj-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;}
.htzj-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;}
.htzj-pane{display:none;}.htzj-pane.active{display:block;}
.htzj-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;gap:6px;}
.htzj-list-head-actions{display:flex;align-items:center;gap:5px;flex:0 0 auto;}
.htzj-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;}
.htzj-mini-btn:hover{background:#f8fafc;color:#0f172a;}
.htzj-mini-btn.on{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;}
.htzj-list-title{font-size:12px;color:#64748b;font-weight:700;}
.htzj-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;}
.htzj-plan-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 9px;font-size:12px;background:#fff;color:#0f172a;margin-bottom:6px;}
.htzj-train-chips{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:7px;}
.htzj-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%;}
.htzj-train-chip.active{border-color:#2563eb;background:#eff6ff;color:#1d4ed8;}
.htzj-train-chip input{margin:0;}
.htzj-train-chip .pct{color:#64748b;font-weight:700;}
.htzj-train-chip.active .pct{color:#2563eb;}
.htzj-train-tabs{display:flex;gap:5px;margin-bottom:7px;overflow-x:auto;padding-bottom:1px;}
.htzj-train-tab{flex:0 0 auto;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:5px 10px;border-radius:9px;cursor:pointer;font-weight:800;font-size:11px;white-space:nowrap;}
.htzj-train-tab.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;}
.htzj-train-tab .sub{opacity:.85;font-weight:700;margin-left:4px;}
#htzj-course-list,#htzj-chapter-preview,#htzj-run-log{max-height:220px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;}
#htzj-train-list,#htzj-train-group{display:none;}
.htzj-train-item{display:none;}
.htzj-train-group-title{display:none;}
.htzj-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;}
.htzj-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;}
.htzj-course-item.done{opacity:.92;background:#f1f5f9;}
.htzj-course-item.active{border-color:#16a34a;background:#f0fdf4;}
.htzj-bar{display:block;height:4px;background:#e2e8f0;border-radius:2px;margin-top:5px;overflow:hidden;}
.htzj-bar>i{display:block;height:100%;background:#2563eb;border-radius:2px;}
.htzj-bar>i.full{background:#16a34a;}
.htzj-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;}
.htzj-chapter-title{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;}
.htzj-chapter-item{padding:5px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;color:#334155;}
.htzj-chapter-item.done{color:#64748b;}
.htzj-chapter-item.learning{color:#0369a1;font-weight:600;}
.htzj-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;align-items:center;}
.htzj-meta-label{color:#64748b;}
.htzj-meta-value{font-weight:700;text-align:right;}
.htzj-btn-row{display:flex;gap:7px;margin-top:6px;}
.htzj-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;}
.htzj-btn-refresh{background:#64748b;}
.htzj-btn-primary{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);}
.htzj-btn-start{background:#16a34a;}
.htzj-btn-stop{background:#ef4444;}
.htzj-btn-off,.htzj-btn:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;box-shadow:none;}
.htzj-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;}
.htzj-task-row{font-size:11px;color:#475569;margin-top:6px;line-height:1.35;}
.htzj-task-row em{font-style:normal;font-weight:800;color:#0f172a;}
.htzj-input{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 8px;font-size:12px;box-sizing:border-box;}
.htzj-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;}
.htzj-footer-extra{flex:0 0 auto;padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;}
.htzj-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;}
.htzj-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;}
#htzj-panel-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;}
.htzj-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;}
.htzj-log-time{color:#94a3b8;font-weight:500;margin-right:2px;}
.htzj-log-pct{font-weight:700;text-decoration:underline;text-underline-offset:2px;}
.htzj-log-mins{opacity:.88;}
.htzj-log-done{background:rgba(16,185,129,.1);}
.htzj-log-done-mark{font-weight:700;color:#059669;}
.htzj-log-info{background:#f8fafc;color:#64748b!important;border-left-color:#94a3b8!important;}
.htzj-log-progress{background:color-mix(in srgb, var(--log-c) 7%, #fff);}
#htzj-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;}
.htzj-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;}
`;
document.head.appendChild(st);
}
function updateAuthUi() {
const badge = document.getElementById("htzj-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 total = 0;
let done = 0;
let learnedSec = 0;
let totalSec = 0;
state.chapterPreview.forEach((c) => {
(c.videos || []).forEach((v) => {
total += 1;
if (v.done) done += 1;
totalSec += Number(v.duration) || 0;
learnedSec += ((Number(v.duration) || 0) * (Number(v.progress) || 0)) / 100;
});
});
const pct = totalSec > 0
? Math.round((learnedSec / totalSec) * 100)
: total
? Math.round((done / total) * 100)
: 0;
const selCount = state.selectedCourseKeys.size;
const doneEl = document.getElementById("htzj-queue-done");
const totalEl = document.getElementById("htzj-queue-total");
const pctEl = document.getElementById("htzj-queue-percent");
const bar = document.getElementById("htzj-queue-progress");
const selEl = document.getElementById("htzj-selected-count");
const foot = document.getElementById("htzj-footer-text");
if (selEl) selEl.textContent = String(selCount);
if (doneEl) doneEl.textContent = String(done);
if (totalEl) totalEl.textContent = String(total);
if (pctEl) pctEl.textContent = `${pct}%`;
if (bar) bar.style.width = `${pct}%`;
if (foot) {
foot.textContent = state.trains.length
? `\u5df2\u9009\u57f9\u8bad ${state.selectedTids.size}/${state.trains.length} · \u8bfe\u7a0b ${state.courses.length} · \u52fe\u9009 ${selCount}`
: "\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";
if (t.year) return `${t.year}\u5e74`;
return truncate(t.name, 8);
}
function renderTrainSelect() {
const chips = document.getElementById("htzj-train-chips");
const tabs = document.getElementById("htzj-train-tabs");
if (!chips || !tabs) return;
if (!state.trains.length) {
chips.innerHTML = `\u6682\u65e0\u5df2\u62a5\u540d\u57f9\u8bad
`;
tabs.innerHTML = "";
return;
}
chips.innerHTML = state.trains
.map((t) => {
const on = state.selectedTids.has(t.id);
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();
});
});
const selectedTrains = state.trains.filter((t) => state.selectedTids.has(t.id));
if (!selectedTrains.length) {
tabs.innerHTML = `\u8bf7\u52fe\u9009\u57f9\u8bad
`;
return;
}
syncActiveTrainTab();
tabs.innerHTML = selectedTrains
.map((t) => {
const active = Number(state.activeTrainTabTid) === Number(t.id) ? "active" : "";
const count = state.courses.filter((c) => Number(c.tid) === Number(t.id)).length;
return ``;
})
.join("");
tabs.querySelectorAll(".htzj-train-tab").forEach((btn) => {
btn.addEventListener("click", () => {
state.activeTrainTabTid = Number(btn.dataset.tid);
renderTrainSelect();
renderCourseList();
});
});
}
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 {
incomplete.forEach((k) => state.selectedCourseKeys.add(k));
}
renderCourseList();
syncToggleIncompleteBtn();
updateSummary();
refreshChapterPreview();
}
function syncToggleIncompleteBtn() {
const btn = document.getElementById("htzj-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);
}
function renderCourseList() {
const box = document.getElementById("htzj-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;
}
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 >= COMPLETE_THRESHOLD;
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);
});
state.selectedCourseKeys = new Set([...other, ...cur]);
renderCourseList();
updateSummary();
refreshChapterPreview();
});
});
syncToggleIncompleteBtn();
}
function renderChapterPreview() {
const box = document.getElementById("htzj-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("#htzj-panel-header");
let sx = 0,
sy = 0,
ox = 0,
oy = 0,
dragging = false;
header.addEventListener("mousedown", (e) => {
if (e.target.closest("#htzj-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) {}
});
}
function showTokenModal() {
let modal = document.getElementById("htzj-token-modal");
if (!modal) {
modal = document.createElement("div");
modal.id = "htzj-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("#htzj-token-cancel").onclick = () => {
modal.style.display = "none";
};
modal.querySelector("#htzj-token-ok").onclick = async () => {
const raw = modal.querySelector("#htzj-token-input").value;
const msg = modal.querySelector("#htzj-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("#htzj-token-input").value = state.token || "";
modal.querySelector("#htzj-token-msg").textContent = "";
modal.style.display = "flex";
}
function createPanel() {
document.querySelectorAll("#htzj-auto-panel, .htzj-auto-panel").forEach((el) => el.remove());
injectStyles();
const panel = document.createElement("div");
panel.id = "htzj-auto-panel";
panel.className = "htzj-panel-max";
panel.innerHTML = `
\u52fe\u9009\u8bfe\u7a0b\u8fdb\u5ea6
\u672a\u767b\u5f55
\u5df2\u9009 0 \u95e8
·
\u8bfe\u65f6 0/0
0%
\u57f9\u8bad\u591a\u9009 · Tab \u5207\u8bfe\u7a0b
\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("htzj-panel-min");
panel.classList.remove("htzj-panel-max");
panel.querySelector("#htzj-btn-min").style.display = "none";
panel.querySelector("#htzj-btn-max").style.display = "";
}
} catch (e) {}
bindDrag(panel);
panel.querySelector("#htzj-btn-min").onclick = () => {
panel.classList.add("htzj-panel-min");
panel.classList.remove("htzj-panel-max");
panel.querySelector("#htzj-btn-min").style.display = "none";
panel.querySelector("#htzj-btn-max").style.display = "";
try {
GM_setValue(PANEL_COLLAPSED_KEY, true);
} catch (e) {}
};
panel.querySelector("#htzj-btn-max").onclick = () => {
panel.classList.remove("htzj-panel-min");
panel.classList.add("htzj-panel-max");
panel.querySelector("#htzj-btn-min").style.display = "";
panel.querySelector("#htzj-btn-max").style.display = "none";
try {
GM_setValue(PANEL_COLLAPSED_KEY, false);
} catch (e) {}
};
panel.querySelectorAll(".htzj-tab-btn").forEach((btn) => {
btn.addEventListener("click", () => {
panel.querySelectorAll(".htzj-tab-btn").forEach((b) => b.classList.remove("active"));
panel.querySelectorAll(".htzj-pane").forEach((p) => p.classList.remove("active"));
btn.classList.add("active");
panel.querySelector(`.htzj-pane[data-pane="${btn.dataset.tab}"]`)?.classList.add("active");
if (btn.dataset.tab === "exam") _rel();
});
});
panel.querySelector("#htzj-toggle-incomplete").onclick = () => toggleIncompleteCourses();
panel.querySelector("#htzj-start").onclick = () => startStudy();
panel.querySelector("#htzj-stop").onclick = () => stopStudy();
panel.querySelector("#htzj-refresh-exam")?.addEventListener("click", () => _rel());
panel.querySelector("#htzj-buy-pro")?.addEventListener("click", () =>
openUrl(state.proBuyUrl || PRO_BUY_URL)
);
panel.querySelector("#htzj-save-cloud-token")?.addEventListener("click", async () => {
const inp = panel.querySelector("#htzj-cloud-token-input");
setCloudToken(inp ? inp.value : "");
state.tokenBoundBlocked = false;
_icc();
try {
await _cl(true);
_plog(
state.cloudTier === "pro"
? "Pro Token \u9a8c\u8bc1\u6210\u529f"
: `\u5f53\u524d\u514d\u8d39\u6863 · \u5df2\u7528 ${state.freeUsedVideos}/${state.freeVideoLimit} \u8bfe\u65f6`
);
} catch (e) {
_plog(`\u4e91\u7aef\u9a8c\u8bc1\u5931\u8d25\uff1a${e.message || e}`);
}
_ucp();
});
syncRunButtons();
setCurrentTask("\u5f85\u547d");
panel.querySelector("#htzj-clear-token").onclick = () => {
clearToken();
resetUserData("\u5df2\u6e05\u9664 Token \u7f13\u5b58\u4e0e\u8bfe\u7a0b\u6570\u636e");
updateAuthUi();
};
panel.querySelector("#htzj-clear-log").onclick = () => {
state.logLines = [];
const box = document.getElementById("htzj-run-log");
if (box) box.innerHTML = `\u6682\u65e0\u65e5\u5fd7
`;
};
updateAuthUi();
_upn();
}
function init() {
document.querySelectorAll("#htzj-auto-panel").forEach((el) => el.remove());
setupTokenInterceptor();
getCloudToken();
createPanel();
updateAuthUi();
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();
}
})();