// ==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 = `
\u548c\u7530\u4e13\u4e1a\u6280\u672f\u7ee7\u7eed\u6559\u80b2\u8fdb\u5ea6\u52a9\u624b v${SCRIPT_VERSION}
\u514d\u8d39\u7248 \u4f53\u9a8c 0/3 \u8bfe\u65f6 \u672a\u5f00\u901a Pro
\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
\u6682\u65e0\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(); } })();