// ==UserScript== // @name 【效率版】菏泽专技继续教育-菏泽市人力资源发展促进会学习助手 // @namespace https://card.wlxy.live/details/B0AA3D86 // @version 1.0 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @description 菏泽市人力资源发展促进会学习平台(jsxy.tyjrpx.com):课程学习与考试;免费体验3章节,Pro不限且可考试(30天)。 // @antifeature payment 免费体验3个章节,升级Pro不限且可考试(30天) // @antifeature membership 需云端授权 // @match *://jsxy.tyjrpx.com/* // @match *://*.tyjrpx.com/* // @connect jsxy.tyjrpx.com // @connect *.tyjrpx.com // @connect oa49.ahzsksw.cn // @connect card.wlxy.live // @connect huaweicloudobs.ahjxjy.cn // @grant GM_xmlhttpRequest // @grant GM_info // @grant GM_openInTab // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // @tag 菏泽专技 // @tag 继续教育 // @tag 学习辅助 // @license All Rights Reserved // ==/UserScript== (function () { "use strict"; try { if (window.top !== window.self) return; } catch (_) { return; } const SCRIPT_VERSION = (() => { try { return String(GM_info?.script?.version || "1.0"); } catch (_) { return "1.0"; } })(); const DURATION_PLACEHOLDER = 1000000000; const DURATION_MIN_TRUST = 120; const DURATION_MAX = 7200; if (window.__hzcjhHelper) return; window.__hzcjhHelper = true; const LOCAL_DEBUG = false; const PRODUCT_NAME = "\u83cf\u6cfd\u4e13\u6280\u7ee7\u7eed\u6559\u80b2\u5b66\u4e60\u52a9\u624b"; const PRO_DAYS = 30; const PANEL_POS_KEY = "hzcjh_panel_pos_v1"; const PANEL_COLLAPSED_KEY = "hzcjh_panel_collapsed_v1"; const LOG_KEY = "hzcjh_run_log_v1"; const QUEUE_KEY = "hzcjh_lesson_queue_v1"; const CLOUD_TOKEN_KEY = "hzcjh_cloud_token_v1"; const CLOUD_LEASE_CACHE_KEY = "hzcjh_cloud_lease_cache_v1"; const CLOUD_LAST_STATE_KEY = "hzcjh_cloud_last_state_v1"; const SPEED_KEY = "hzcjh_speed_mode_v1"; const YEAR_KEY = "hzcjh_course_year_v1"; const FREE_STUDIED_KEY = "hzcjh_free_studied_lessons_v1"; const PANEL_LOGO_URL = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const QQ_GROUP_NUMBER = "903117129"; const QQ_GROUP_LINK = "https://qun.qq.com/universal-share/share?ac=1&authKey=rxdL6YIJ0%2FxOEemjLqTGULvl5aAfJIVQcIvkvnwvmL%2FAmpFZnSafajYHgSXMUXvx&busi_data=eyJncm91cENvZGUiOiI5MDMxMTcxMjkiLCJ0b2tlbiI6IlB2dkFGSm5XRXBrSEhtQVFTUGQzdVNZakhNWDNMbW1kODA2enpoMi9obDh4SWp0YzBDODNFaGtwRU44Z0hyU0siLCJ1aW4iOiIxMjU0MzE1MTQifQ%3D%3D&data=9oyJixSPcigCQW-saV5eXlcMwV9C6J36XySx-rDHwVwNlofvRmd2ze5sLwFtHTbYbG4nAWIUrI0qftC6aTX9xg&svctype=4&tempid=h5_group_info"; const DEFAULT_CLOUD_API_BASE = "https://oa49.ahzsksw.cn"; const PRO_BUY_URL = "https://card.wlxy.live/details/B0AA3D86"; const PANEL_NOTICE_FALLBACK = "\u83cf\u6cfd\u4e13\u6280\u7ee7\u7eed\u6559\u80b2\u5b66\u4e60\u52a9\u624b\uff1a\u52fe\u9009\u8bfe\u7a0b\u540e\u5f00\u59cb\u5b66\u4e60\uff1bPro \u53ef\u53c2\u52a0\u8003\u8bd5"; const FREE_CHAPTER_LIMIT_DEFAULT = 3; const API_BASE = () => `${location.origin}/apt/exam/api-v3`; const SPEED_MODES = { sec: { id: "sec", label: "\u8d85\u901f\u6a21\u5f0f(\u79d2)" }, eff: { id: "eff", label: "\u6548\u7387\u6a21\u5f0f" }, oneone: { id: "oneone", label: "1:1\u5e38\u901f\u6a21\u5f0f" }, }; const UPPLAY_MIN_GAP_MS = 10000; let _lastUpPlayAt = 0; let _upPlayCooldownUntil = 0; const _playInfoCache = Object.create(null); const state = { user: null, years: [], courseYear: String(localStorage.getItem(YEAR_KEY) || "").trim(), lessons: [], exams: [], selectedIds: loadQueue(), running: false, stopFlag: false, lastAction: "\u5c31\u7eea", currentLessonName: "-", currentTitle: "-", progressPct: 0, queueDone: 0, queueTotal: 0, logs: [], token: "", uid: "", cloudApiBase: DEFAULT_CLOUD_API_BASE, cloudToken: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(), cloudTier: LOCAL_DEBUG ? "debug" : String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim() ? "unknown" : "free", cloudLease: "", cloudLeaseExp: 0, cloudLeaseLuid: "", cloudProExpireAt: 0, cloudRevoked: false, freeVideoLimit: LOCAL_DEBUG ? 9999 : FREE_CHAPTER_LIMIT_DEFAULT, freeUsedVideos: 0, remotePanelNotice: "", proBuyUrl: PRO_BUY_URL, speedMode: (() => { const raw = String( localStorage.getItem(SPEED_KEY) || (LOCAL_DEBUG ? "eff" : "sec") ).trim(); if (raw === "normal" || raw === "fine" || raw === "platform" || raw === "oneone") return "oneone"; if (raw === "soft" || raw === "eff") return "eff"; if (raw === "sec" || raw === "turbo" || raw === "hyper" || raw === "burst") return "sec"; return SPEED_MODES[raw] ? raw : LOCAL_DEBUG ? "eff" : "sec"; })(), previewByLesson: {}, _previewLoadSeq: 0, _doneLogKeys: Object.create(null), cloudOnline: false, }; try { state.logs = JSON.parse(localStorage.getItem(LOG_KEY) || "[]") || []; } catch (_) { state.logs = []; } if (!LOCAL_DEBUG) { const lastState = readCloudLastState(); if (lastState) { const t = String(lastState.tier || "").toLowerCase(); if (t && t !== "offline" && t !== "unknown") state.cloudTier = lastState.tier; if (t !== "offline" && lastState.freeUsedVideos >= 0) { state.freeUsedVideos = Number(lastState.freeUsedVideos) || 0; } } if (String(state.cloudTier || "").toLowerCase() === "offline") { state.cloudTier = "free"; } } function loadQueue() { try { const arr = JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]"); return Array.isArray(arr) ? arr.map(String) : []; } catch (_) { return []; } } function saveQueue(ids) { state.selectedIds = [...new Set((ids || []).map(String))]; try { localStorage.setItem(QUEUE_KEY, JSON.stringify(state.selectedIds)); } catch (_) {} } function escapeHtml(s) { return String(s == null ? "" : s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function log(msg) { const line = `[${new Date().toLocaleTimeString()}] ${msg}`; state.logs.unshift(line); if (state.logs.length > 120) state.logs.length = 120; try { localStorage.setItem(LOG_KEY, JSON.stringify(state.logs.slice(0, 80))); } catch (_) {} state.lastAction = msg; const el = document.getElementById("hzcjh-run-log"); if (el) { el.innerHTML = state.logs .slice(0, 60) .map((s) => `
${escapeHtml(s)}
`) .join(""); } } function isEmptyPlatformRes(res) { if (res == null) return true; if (typeof res !== "object") return true; if (res._empty) return true; if (res.code == null && res.message == null && res.data == null && !res.rows) { return true; } return false; } function clearRunLog() { state.logs = []; try { localStorage.removeItem(LOG_KEY); } catch (_) {} const el = document.getElementById("hzcjh-run-log"); if (el) el.innerHTML = ""; } function logSessionDoneOnce(title) { const key = String(title || "").trim(); if (!key) return; if (state._doneLogKeys[key]) return; state._doneLogKeys[key] = 1; log(`\u5df2\u5b8c\u6210\uff1a${key}`); } function currentSpeedMode() { return SPEED_MODES[state.speedMode] || SPEED_MODES.sec; } function sessionGapMs() { const id = currentSpeedMode().id; if (id === "sec") return 3500; if (id === "eff") return 4000; return 6000; } async function _rcl() { const ok = await _ecl(false); if (!ok || !state.cloudLease) { throw new Error("\u8bf7\u5148\u8fde\u63a5\u6388\u6743\u670d\u52a1"); } if (state.cloudRevoked || String(state.cloudTier || "") === "revoked") { const err = new Error("\u6388\u6743\u5df2\u5931\u6548"); err.code = "QUOTA"; throw err; } return true; } function readCloudLastState() { try { return JSON.parse(localStorage.getItem(CLOUD_LAST_STATE_KEY) || "null"); } catch (_) { return null; } } function writeCloudLastState(partial) { try { const prev = readCloudLastState() || {}; localStorage.setItem( CLOUD_LAST_STATE_KEY, JSON.stringify(Object.assign({}, prev, partial || {}, { at: Date.now() })) ); } catch (_) {} } function writeCloudLeaseCache(lease, exp, extra) { try { if (!lease) { localStorage.removeItem(CLOUD_LEASE_CACHE_KEY); return; } localStorage.setItem( CLOUD_LEASE_CACHE_KEY, JSON.stringify(Object.assign({ lease, exp }, extra || {})) ); } catch (_) {} } function readCloudLeaseCache() { try { return JSON.parse(localStorage.getItem(CLOUD_LEASE_CACHE_KEY) || "null"); } catch (_) { return null; } } function readFreeStudied() { try { const arr = JSON.parse(localStorage.getItem(FREE_STUDIED_KEY) || "[]"); return Array.isArray(arr) ? arr.map(String) : []; } catch (_) { return []; } } function rememberFreeStudiedLesson(id) { if (LOCAL_DEBUG) return; const sid = String(id || ""); if (!sid) return; const arr = readFreeStudied(); if (!arr.includes(sid)) arr.push(sid); try { localStorage.setItem(FREE_STUDIED_KEY, JSON.stringify(arr)); } catch (_) {} if (!state.cloudOnline) { state.freeUsedVideos = arr.length; _ucp(); } } function readUserinfoRaw() { try { const raw = localStorage.getItem("userinfo"); if (!raw) return null; const j = JSON.parse(raw); return j && typeof j === "object" ? j : null; } catch (_) { return null; } } function syncAuthFromStorage() { const info = readUserinfoRaw(); if (!info) return false; const token = String(info.token || info.Token || "").trim(); const uid = String( info.uid || info.userId || info.userid || info.userinfo?.uid || "" ).trim(); if (!token || !uid) return false; state.token = token; state.uid = uid; const name = String( info.realName || info.userName || info.name || info.userinfo?.realName || info.userinfo?.name || "" ).trim(); state.user = { id: uid, name: name || uid, raw: info }; return true; } function getLearningUserId() { if (!state.uid) syncAuthFromStorage(); const luid = String(state.uid || "").trim(); if (luid && state.cloudLeaseLuid && state.cloudLeaseLuid !== luid) { state.cloudLease = ""; state.cloudLeaseExp = 0; state.cloudLeaseLuid = ""; writeCloudLeaseCache("", 0); } return luid; } function gmRequest(method, url, body, headers) { return new Promise((resolve, reject) => { const g = typeof GM_xmlhttpRequest === "function" ? GM_xmlhttpRequest : typeof GM !== "undefined" && GM.xmlHttpRequest ? GM.xmlHttpRequest.bind(GM) : null; if (!g) { reject(new Error("GM_xmlhttpRequest \u4e0d\u53ef\u7528")); return; } g({ method: method || "GET", url, headers: headers || {}, data: body == null ? undefined : body, anonymous: false, onload: (res) => { resolve({ status: Number(res.status) || 0, text: String(res.responseText || ""), }); }, onerror: () => reject(new Error("network")), ontimeout: () => reject(new Error("timeout")), }); }); } async function httpJson(method, url, bodyObj, headers) { const hdrs = Object.assign( { Accept: "application/json", "Content-Type": "application/json" }, headers || {} ); const body = bodyObj == null ? null : JSON.stringify(bodyObj); try { const res = await fetch(url, { method: method || "GET", headers: hdrs, body: method === "GET" || method === "HEAD" ? undefined : body, credentials: "include", }); const text = await res.text(); return { status: res.status, text }; } catch (_) { return gmRequest(method, url, body, hdrs); } } async function platformPost(serviceCode, param, page) { if (!syncAuthFromStorage() && (!state.token || !state.uid)) { throw new Error("\u8bf7\u5148\u767b\u5f55\u5e73\u53f0"); } const payload = { serviceCode: String(serviceCode), channel: "1", version: "1.0.0", token: state.token, uid: state.uid, }; if (param !== undefined) payload.param = param; if (page) payload.page = page; const url = `${API_BASE()}/${serviceCode}`; const res = await httpJson("POST", url, payload); const rawText = String(res.text || ""); const trimmed = rawText.trim(); let data = {}; if (!trimmed) { data = { _empty: true, _httpStatus: res.status, _rawPreview: "(empty body)", }; return data; } try { data = JSON.parse(trimmed); } catch (_) { data = { _empty: true, _httpStatus: res.status, _rawPreview: trimmed.slice(0, 120), raw: trimmed.slice(0, 500), }; return data; } if (data && typeof data === "object") { data._httpStatus = res.status; if (trimmed.length < 80) data._rawPreview = trimmed; } if (res.status < 200 || res.status >= 300) { throw new Error(String(data.message || data.code || `http_${res.status}`)); } const code = String(data.code ?? ""); if (code && code !== "200" && code !== "0") { throw new Error(String(data.message || `\u4e1a\u52a1\u9519\u8bef ${code}`)); } if ( data.code == null && data.message == null && data.data == null && !data.rows && !data.courses ) { data._empty = true; data._rawPreview = trimmed.slice(0, 120); } return data; } function getCloudApiBase() { return String(state.cloudApiBase || DEFAULT_CLOUD_API_BASE).replace(/\/$/, ""); } function formatLeaseExpireText(expSec) { const n = Number(expSec) || 0; if (!n) return "-"; try { return new Date(n * 1000).toLocaleString(); } catch (_) { return String(n); } } function formatCloudTierText(tier) { if (LOCAL_DEBUG) return "\u672c\u5730\u8c03\u8bd5"; if (state.cloudRevoked) return "\u5df2\u7981\u7528"; const t = String(tier || "").toLowerCase(); if (t === "pro") return "Pro \u4f1a\u5458"; if (t === "free") return "\u514d\u8d39\u4f53\u9a8c"; if (t === "offline") return "\u672a\u8fde\u4e91\u7aef"; if (t === "revoked") return "\u5df2\u7981\u7528"; if (t === "unknown") return "\u672a\u6821\u9a8c"; if (t === "debug") return "\u672c\u5730\u8c03\u8bd5"; return tier ? String(tier) : "\u672a\u6821\u9a8c"; } function _ucp() { const tierEl = document.querySelector("#hzcjh-cloud-tier"); const freeEl = document.querySelector("#hzcjh-cloud-free"); const freeLabelEl = document.querySelector("#hzcjh-cloud-free-label"); const tokenInput = document.querySelector("#hzcjh-cloud-token"); const saveBtn = document.querySelector("#hzcjh-cloud-save"); const proBtn = document.querySelector("#hzcjh-open-pro"); if (tierEl) { tierEl.textContent = formatCloudTierText(state.cloudTier); tierEl.style.color = LOCAL_DEBUG ? "#b45309" : state.cloudRevoked || String(state.cloudTier || "").toLowerCase() === "revoked" ? "#dc2626" : "#0f172a"; } if (LOCAL_DEBUG) { if (freeLabelEl) freeLabelEl.textContent = "\u8c03\u8bd5\u989d\u5ea6"; if (freeEl) freeEl.textContent = "\u4e0d\u9650"; if (tokenInput) { tokenInput.disabled = true; tokenInput.placeholder = "\u672c\u5730\u8c03\u8bd5\u4e2d\uff0c\u65e0\u9700 Token"; } if (saveBtn) saveBtn.disabled = true; if (proBtn) proBtn.style.display = "none"; return; } if (state.cloudRevoked) { if (freeLabelEl) freeLabelEl.textContent = "\u6388\u6743\u72b6\u6001"; if (freeEl) freeEl.textContent = "Token \u5df2\u7981\u7528"; } else if (String(state.cloudTier || "").toLowerCase() === "pro") { if (freeLabelEl) freeLabelEl.textContent = "Pro \u5230\u671f"; if (freeEl) freeEl.textContent = formatLeaseExpireText( state.cloudProExpireAt || state.cloudLeaseExp ); } else { if (freeLabelEl) freeLabelEl.textContent = "\u4f53\u9a8c\u8fdb\u5ea6"; const used = state.cloudOnline ? state.freeUsedVideos : Math.max(state.freeUsedVideos, readFreeStudied().length); if (freeEl) freeEl.textContent = `${used}/${state.freeVideoLimit}`; } if (tokenInput && tokenInput !== document.activeElement) { tokenInput.value = state.cloudToken || ""; } } async function _crq(path, method, payload) { const base = getCloudApiBase(); const url = `${base}${path.startsWith("/") ? path : `/${path}`}`; const headers = { Accept: "application/json", "Content-Type": "application/json", }; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; if (state.cloudToken) headers.Authorization = `Bearer ${state.cloudToken}`; let reqPayload = payload; if (reqPayload && typeof reqPayload === "object") { reqPayload = Object.assign({}, reqPayload); if (path !== "/api/hzcjh/lease" && state.cloudLease && reqPayload.lease == null) { reqPayload.lease = state.cloudLease; } } const body = reqPayload == null ? null : JSON.stringify(reqPayload); const res = await gmRequest(method || "POST", url, body, headers); const text = String(res.text || "").trim(); let data = {}; if (text) { try { data = JSON.parse(text); } catch (_) { data = { raw: text }; } } if (res.status < 200 || res.status >= 300) { const err = new Error( String(data.detail || data.message || data.code || `http_${res.status}`) ); err.status = res.status; err.data = data; throw err; } return data; } async function _ecl(forceRefresh) { if (LOCAL_DEBUG) { state.cloudOnline = false; state.cloudTier = "debug"; state.cloudRevoked = false; state.freeVideoLimit = 9999; _ucp(); return true; } const luid = getLearningUserId(); if (!luid) throw new Error("\u8bf7\u5148\u767b\u5f55"); const now = Math.floor(Date.now() / 1000); if ( !forceRefresh && state.cloudLease && state.cloudLeaseLuid === luid && state.cloudLeaseExp - now > 60 ) { state.cloudOnline = true; return true; } try { const data = await _crq("/api/hzcjh/lease", "POST", { learning_user_id: luid, name: state.user?.name || "", }); state.cloudRevoked = false; state.cloudOnline = true; const lease = String(data.lease || ""); const exp = Number(data.lease_expires_at || data.exp || now + 3600); const tier = String(data.tier || "").trim() || (state.cloudToken ? "pro" : "free"); const proExpireAt = Number(data.pro_expires_at || data.proExpireAt || 0); state.cloudLease = lease; state.cloudLeaseExp = exp; state.cloudLeaseLuid = luid; state.cloudProExpireAt = proExpireAt; state.cloudTier = tier; state.freeVideoLimit = Number( data.free_chapter_limit ?? data.free_video_limit ?? data.freeVideoLimit ?? FREE_CHAPTER_LIMIT_DEFAULT ) || FREE_CHAPTER_LIMIT_DEFAULT; state.freeUsedVideos = Number( data.free_used_chapters ?? data.free_used_videos ?? data.freeUsedVideos ?? 0 ); writeCloudLeaseCache(lease, exp, { luid, tier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, proExpireAt, }); writeCloudLastState({ luid, tier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, }); _ucp(); return !!lease; } catch (e) { state.cloudOnline = false; const em = String(e?.message || e); if (/(revoked|invalid token|expired)/i.test(em)) { state.cloudRevoked = true; state.cloudTier = "revoked"; _ucp(); throw e; } state.cloudLease = ""; state.cloudLeaseExp = 0; state.cloudOnline = false; _ucp(); return false; } } async function _cvt() { if (LOCAL_DEBUG) return true; if (!state.cloudToken) return true; const url = `${getCloudApiBase()}/api/license/verify`; const headers = { Accept: "application/json", Authorization: `Bearer ${state.cloudToken}`, }; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; const res = await gmRequest("GET", url, null, headers); const text = String(res.text || "").trim(); let data = {}; if (text) { try { data = JSON.parse(text); } catch (_) {} } if (res.status < 200 || res.status >= 300 || data.ok === false) { throw new Error(String(data.message || data.detail || "\u6388\u6743\u65e0\u6548\u8bf7\u68c0\u67e5 Token")); } return true; } async function _fpn() { const el = document.getElementById("hzcjh-ann-text"); try { const url = `${getCloudApiBase()}/api/hzcjh/panel-notice`; const res = await gmRequest("GET", url, null, { Accept: "text/plain,*/*" }); const text = String(res.text || "").trim(); if (res.status >= 200 && res.status < 300 && text) { state.remotePanelNotice = text; if (el) el.textContent = text; return; } } catch (_) {} if (el) el.textContent = state.remotePanelNotice || PANEL_NOTICE_FALLBACK; } function canStudyNextFreeLesson(lessonId) { if (String(state.cloudTier || "").toLowerCase() === "pro" && state.cloudLease) { return true; } if (!state.cloudOnline || !state.cloudLease) return false; const sid = String(lessonId || ""); const studied = new Set(readFreeStudied()); if (sid && studied.has(sid)) return true; return Number(state.freeUsedVideos || 0) < Number(state.freeVideoLimit || 0); } async function _cfl(lesson) { if (!state.cloudLease) return { quota: true, msg: "\u8bf7\u5148\u8fde\u63a5\u6388\u6743\u670d\u52a1" }; if (String(state.cloudTier || "").toLowerCase() === "pro") { return { ok: true }; } const id = String(lesson?.id || ""); if (!id) return { ok: true }; if (readFreeStudied().includes(id)) return { ok: true }; try { const data = await _crq("/api/hzcjh/lesson/consume", "POST", { learning_user_id: getLearningUserId(), lesson_id: id, lesson_name: lesson?.name || "", }); if (data.quota === true || data.ok === false) { return { quota: true, msg: data.message || "\u4f53\u9a8c\u6b21\u6570\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro" }; } state.freeUsedVideos = Number( data.free_used_chapters ?? data.free_used_videos ?? state.freeUsedVideos + 1 ); rememberFreeStudiedLesson(id); _ucp(); return { ok: true }; } catch (e) { if (/quota|\u989d\u5ea6|\u7528\u5b8c/i.test(String(e?.message || e))) { return { quota: true, msg: String(e.message || e) }; } return { quota: true, msg: "\u6388\u6743\u6821\u9a8c\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5" }; } } function notifyQuotaExhausted(msg) { log(String(msg || "\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro")); try { switchPanelTab("settings"); } catch (_) {} _opm(); } function lessonKey(row) { return String(row.courseWrapperId || "") + "|" + String(row.orderItemId || ""); } function formatLessonStatus(row) { if (row?.kind === "exam") { if (Number(row.inQualified) === 1 || Number(row.allProgress) >= 100) return "\u5df2\u901a\u8fc7"; return "\u5f85\u8003\u8bd5"; } const prog = Number(row.allProgress || 0); const st = Number(row.allStatus); if (prog >= 100 || st === 3) return "\u5df2\u5b8c\u6210"; if (prog > 0 || st === 2) return `\u5b66\u4e60\u4e2d ${prog}%`; return "\u672a\u5b66"; } function isLessonFinished(les) { if (les?.kind === "exam") { return Number(les.inQualified) === 1 || Number(les.allProgress) >= 100; } const prog = Number(les?.allProgress || 0); const st = Number(les?.allStatus); return prog >= 100 || st === 3; } function isLessonPending(les) { return !isLessonFinished(les); } function examListId(examId) { return `exam|${String(examId || "")}`; } function unwrapPlatform(raw) { if (!raw || typeof raw !== "object") return raw; const d = raw.data; if (d && typeof d === "object" && !Array.isArray(d)) { return Object.assign({}, raw, d); } return raw; } function normalizeCourseInfo(raw) { if (!raw || typeof raw !== "object") return raw; const merged = unwrapPlatform(raw); if (!merged.courses && raw.data && typeof raw.data === "object") { return Object.assign({}, raw, raw.data); } return merged; } function upPlayAccepted(res) { if (!res || typeof res !== "object") return false; if (res.data && typeof res.data === "object" && !Array.isArray(res.data)) { return ( res.data.currentProgress != null || res.data.chapterId != null || res.data.allProgress != null ); } return res.currentProgress != null; } function isRealDuration(sec) { const n = Number(sec) || 0; return n >= 5 && n < DURATION_PLACEHOLDER / 10; } function isTrustedDuration(sec) { return isRealDuration(sec) && Number(sec) >= DURATION_MIN_TRUST; } function adoptTotal(prev, next) { const p = Number(prev) || 0; let n = Number(next) || 0; if (!isRealDuration(n)) return p; n = Math.min(n, DURATION_MAX); if (!isRealDuration(p)) return n; return Math.min(DURATION_MAX, Math.max(p, n)); } function implyTotalOnce(curSec, pct, baseTotal) { const cur = Math.max(1, Math.floor(Number(curSec) || 0)); const p = Math.max(1, Math.min(99, Number(pct) || 0)); const raw = Math.ceil((cur * 100) / p); const base = isTrustedDuration(baseTotal) ? Number(baseTotal) : DURATION_MIN_TRUST; const cap = Math.min(DURATION_MAX, Math.max(base, Math.floor(base * 1.5), 1800)); return Math.min(cap, Math.max(base, raw)); } function flattenChapters(courseInfo) { const out = []; const info = normalizeCourseInfo(courseInfo) || {}; const courses = info.courses || []; for (const c of courses) { const courseId = String(c.courseId || ""); const courseName = String(c.courseName || ""); const zhangList = c.zhangVos || c.chapterVos || []; for (const z of zhangList) { const zhangName = String(z.chapterName || ""); const jies = z.jieVos || z.children || []; if (!jies.length && z.videoId && String(z.chapterType || "") !== "\u7ae0") { out.push({ chapterId: String(z.chapterId || ""), courseId: String(z.courseId || courseId), videoId: String(z.videoId || ""), chapterName: String(z.chapterName || ""), zhangName: "", courseName, chapterSort: Number(z.chapterSort || 0), currentProgress: Number(z.currentProgress || 0), finished: Number(z.currentProgress || 0) >= 100, }); continue; } for (const jie of jies) { out.push({ chapterId: String(jie.chapterId || ""), courseId: String(jie.courseId || courseId), videoId: String(jie.videoId || ""), chapterName: String(jie.chapterName || ""), zhangName, courseName, chapterSort: Number(jie.chapterSort || 0), currentProgress: Number(jie.currentProgress || 0), finished: Number(jie.currentProgress || 0) >= 100, }); } } } out.sort((a, b) => a.chapterSort - b.chapterSort); return out; } async function loadYears() { const data = await platformPost("getCourseScreen", { dictCode: "courseYear" }); const rows = data.rows || []; state.years = rows .map((r) => ({ label: String(r.itemName || r.itemValue || ""), value: String(r.itemValue ?? ""), })) .filter((y) => y.value !== "" || y.label.includes("\u5168\u90e8")); if (!state.courseYear) { const hit = rows.find((r) => String(r.isDefault || "").toUpperCase() === "Y" && r.itemValue) || rows.find((r) => r.itemValue); if (hit) state.courseYear = String(hit.itemValue); } syncYearSelect(); } function syncYearSelect() { const sel = document.getElementById("hzcjh-year-select"); if (!sel) return; const cur = state.courseYear || ""; const opts = (state.years || []) .map((y) => { const v = String(y.value || ""); return ``; }) .join(""); sel.innerHTML = `` + opts; sel.value = cur; } async function fetchCoursesByYear(year) { const rows = []; let page = 1; let totalPage = 1; while (page <= totalPage && page <= 30) { const data = await platformPost( "getUserCoursePageList", { courseType: "", courseYear: year == null ? "" : String(year) }, { currentPage: page, showCount: 20 } ); const chunk = data.rows || []; rows.push(...chunk); totalPage = Number(data.page?.totalPage || 1) || 1; page += 1; if (!chunk.length) break; } return rows; } async function _lls() { if (!syncAuthFromStorage()) throw new Error("\u672a\u767b\u5f55\uff1a\u8bf7\u5148\u767b\u5f55\u5e73\u53f0\u540e\u5237\u65b0"); if (!state.years.length) { try { await loadYears(); } catch (_) {} } const year = state.courseYear; let rows = []; if (year) { rows = await fetchCoursesByYear(year); } else { const ys = (state.years || []).map((y) => y.value).filter(Boolean); if (!ys.length) rows = await fetchCoursesByYear(""); else { const map = new Map(); for (const y of ys) { const part = await fetchCoursesByYear(y); for (const r of part) map.set(lessonKey(r), r); } rows = [...map.values()]; } } const courses = rows.map((r) => ({ kind: "course", id: lessonKey(r), courseWrapperId: String(r.courseWrapperId || ""), orderItemId: String(r.orderItemId || ""), name: String(r.courseName || "\u672a\u547d\u540d\u8bfe\u7a0b"), major: String(r.major || ""), hours: Number(r.hours || 0), allProgress: Number(r.allProgress || 0), allStatus: Number(r.allStatus || 0), courseYear: r.courseYear, raw: r, })); let exams = []; try { exams = await _lel(year); } catch (_) { exams = state.exams || []; } const examItems = (exams || []).map((e) => ({ kind: "exam", id: examListId(e.id), examId: e.id, name: e.name, major: "\u8003\u8bd5", hours: 0, allProgress: Number(e.inQualified) === 1 ? 100 : 0, allStatus: Number(e.inQualified) === 1 ? 3 : 0, inQualified: Number(e.inQualified || 0), courseYear: e.year, questionNumber: e.questionNumber, passScore: e.passScore, raw: e.raw || e, })); state.lessons = courses.concat(examItems); const valid = new Set(state.lessons.map((l) => l.id)); const next = state.selectedIds.filter((id) => valid.has(id)); if (next.length !== state.selectedIds.length) saveQueue(next); renderLessonList(); updateQueueSummary(); log( `\u5df2\u52a0\u8f7d ${courses.length} \u95e8\u8bfe\u7a0b` + (examItems.length ? `\u3001${examItems.length} \u573a\u8003\u8bd5` : "") ); } function courseInfoHasCatalog(info) { return !!(info && Array.isArray(info.courses) && info.courses.length); } async function getCourseInfo(course, extraParam) { const base = { courseWrapperId: course.courseWrapperId, orderItemId: course.orderItemId, }; let info2 = null; let info3 = null; try { const raw2 = await platformPost( "getCourseInfo2", Object.assign({}, base, extraParam || {}) ); info2 = normalizeCourseInfo(raw2); } catch (_) {} try { const raw3 = await platformPost( "getCourseInfoV3", Object.assign({ tabId: "" }, base, extraParam || {}) ); info3 = normalizeCourseInfo(raw3); } catch (_) {} if (courseInfoHasCatalog(info2)) { if (info3 && isRealDuration(info3.currentTotalDuration)) { info2.currentTotalDuration = info3.currentTotalDuration; if (info3.chapterId) info2.chapterId = info3.chapterId; if (info3.currentProgress != null) info2.currentProgress = info3.currentProgress; if (info3.mediaUrl) info2.mediaUrl = info3.mediaUrl; } return info2; } if (courseInfoHasCatalog(info3)) return info3; return info2 || info3 || {}; } async function throttleUpPlay() { const now = Date.now(); const waitCd = Math.max(0, _upPlayCooldownUntil - now); const waitGap = Math.max(0, UPPLAY_MIN_GAP_MS - (now - _lastUpPlayAt)); const wait = Math.max(waitCd, waitGap); if (wait > 0) await sleep(wait); } async function upPlayChapter(course, chapter, totalSec, curSec) { const total = Math.max(1, Math.floor(Number(totalSec) || 1)); let cur = Math.max(0, Math.floor(Number(curSec) || 0)); if (cur > total * 2) cur = total; const sentCur = Math.min(cur, total); const param = { courseWrapperId: course.courseWrapperId, courseId: chapter.courseId, chapterId: chapter.chapterId, currentTotalDuration: total, currentDuration: sentCur, orderItemId: course.orderItemId, }; await throttleUpPlay(); _lastUpPlayAt = Date.now(); const res = await platformPost("upPlayChapter", param); if (isEmptyPlatformRes(res)) { _upPlayCooldownUntil = Date.now() + 10000; } return res; } async function getChapterPlayInfo(course, chapter, opt) { const noCache = !!(opt && opt.noCache); const cacheKey = `${course.courseWrapperId}|${chapter.chapterId}`; const hit = _playInfoCache[cacheKey]; if (!noCache && hit && Date.now() - hit.at < 12000) { return hit.res; } const param = { courseWrapperId: course.courseWrapperId, orderItemId: course.orderItemId, courseId: chapter.courseId, chapterId: chapter.chapterId, }; let lastErr = null; try { const raw = await platformPost("getCourseCustomPlayId", param); const res = unwrapPlatform(raw); if (res && (res.mediaUrl || res.polyvId || res.currentTotalDuration || res.currentProgress != null)) { _playInfoCache[cacheKey] = { at: Date.now(), res }; return res; } } catch (e) { lastErr = e; } try { const raw = await platformPost("getVideoMediaUrl", param); const res = unwrapPlatform(raw); if (res && (res.mediaUrl || res.currentTotalDuration || res.currentProgress != null)) { _playInfoCache[cacheKey] = { at: Date.now(), res }; return res; } } catch (e) { lastErr = e; } if (lastErr) throw lastErr; return null; } function findPrevChapter(chapters, chapter) { const same = chapters .filter((c) => (c.zhangName || "") === (chapter.zhangName || "")) .sort((a, b) => a.chapterSort - b.chapterSort); const idx = same.findIndex((c) => c.chapterId === chapter.chapterId); if (idx <= 0) { const all = chapters.slice().sort((a, b) => a.chapterSort - b.chapterSort); const gi = all.findIndex((c) => c.chapterId === chapter.chapterId); return gi > 0 ? all[gi - 1] : null; } return same[idx - 1]; } function probeMediaDuration(url) { return new Promise((resolve, reject) => { const src = String(url || "").trim(); if (!src) { reject(new Error("\u65e0\u5a92\u4f53\u5730\u5740")); return; } const v = document.createElement("video"); v.preload = "metadata"; v.muted = true; v.playsInline = true; let done = false; const finish = (fn, arg) => { if (done) return; done = true; clearTimeout(timer); try { v.removeAttribute("src"); v.load(); } catch (_) {} fn(arg); }; const timer = setTimeout(() => finish(reject, new Error("\u8bfb\u53d6\u7247\u957f\u8d85\u65f6")), 12000); v.addEventListener("loadedmetadata", () => { const d = Math.floor(Number(v.duration) || 0); if (d >= 5 && isFinite(d)) finish(resolve, d); else finish(reject, new Error("\u7247\u957f\u65e0\u6548")); }); v.addEventListener("error", () => finish(reject, new Error("\u5a92\u4f53\u52a0\u8f7d\u5931\u8d25"))); v.src = src; }); } async function refreshCoursePlayState(course, chapter) { try { await getChapterPlayInfo(course, chapter, { silent: true }).catch(() => null); const info = await getCourseInfo(course); if (!info) return null; if (String(info.chapterId || "") !== String(chapter.chapterId)) { const hit = flattenChapters(info).find( (c) => c.chapterId === String(chapter.chapterId) ); return { info, progress: hit ? Number(hit.currentProgress || 0) : null, total: 0, mediaUrl: "", }; } return { info, progress: info.currentProgress != null ? Math.max(0, Math.min(100, Number(info.currentProgress) || 0)) : null, total: isRealDuration(info.currentTotalDuration) ? Math.floor(Number(info.currentTotalDuration)) : 0, mediaUrl: String(info.mediaUrl || "").trim(), }; } catch (_) { return null; } } async function resolvePlayMeta(course, chapter) { let progress = Math.max(0, Math.min(100, Number(chapter.currentProgress || 0))); let total = 0; let mediaUrl = ""; try { const play = await getChapterPlayInfo(course, chapter); if (play) { if (play.currentProgress != null && play.currentProgress !== "") { progress = Math.max(0, Math.min(100, Number(play.currentProgress) || 0)); } if (isTrustedDuration(play.currentTotalDuration)) { total = Math.floor(Number(play.currentTotalDuration)); } else if (isRealDuration(play.currentTotalDuration)) { total = adoptTotal(total, play.currentTotalDuration); } mediaUrl = String(play.mediaUrl || play.nowUrl || "").trim(); } } catch (_) {} const state2 = await refreshCoursePlayState(course, chapter); if (state2) { if (state2.progress != null) progress = state2.progress; if (isTrustedDuration(state2.total)) total = adoptTotal(total, state2.total); else if (isRealDuration(state2.total)) total = adoptTotal(total, state2.total); if (state2.mediaUrl) mediaUrl = state2.mediaUrl; } if ((!isTrustedDuration(total) || total < DURATION_MIN_TRUST) && mediaUrl) { try { const probed = await probeMediaDuration(mediaUrl); total = adoptTotal(total, probed); } catch (_) {} } let mediaTotal = isTrustedDuration(total) ? total : 0; if ((!isTrustedDuration(total) || total < DURATION_MIN_TRUST) && progress > 0 && progress < 100) { const guessCur = isRealDuration(total) ? Math.max(1, Math.floor((total * progress) / 100)) : Math.max(DURATION_MIN_TRUST, progress); total = implyTotalOnce(guessCur, progress, mediaTotal || total || 600); } total = Math.min(DURATION_MAX, Math.max(0, Number(total) || 0)); if (!isRealDuration(total)) { throw new Error(`\u65e0\u6cd5\u83b7\u53d6\u7ae0\u8282\u65f6\u957f\uff1a${chapter.chapterName}`); } const current = Math.min(total, Math.floor((total * progress) / 100)); return { info: state2?.info || null, total, current, progress, mediaUrl, mediaTotal: mediaTotal || 0, }; } function readPlatformProgress(res) { if (!res || typeof res !== "object") return null; const raw = res?.data?.currentProgress ?? res?.currentProgress ?? res?.data?.progress ?? null; if (raw == null || raw === "") return null; const n = Number(raw); return Number.isFinite(n) ? Math.max(0, Math.min(100, n)) : null; } function pickNextChapter(chapters, skippedIds) { const ordered = chapters .slice() .sort((a, b) => a.chapterSort - b.chapterSort || String(a.chapterId).localeCompare(String(b.chapterId))); const cont = ordered.filter((c) => { if (c.finished || skippedIds.has(c.chapterId)) return false; if (Number(c.currentProgress || 0) <= 0) return false; const prev = findPrevChapter(ordered, c); return !prev || prev.finished || skippedIds.has(prev.chapterId); }); if (cont.length) { cont.sort((a, b) => Number(b.currentProgress) - Number(a.currentProgress)); return cont[0]; } for (let i = 0; i < ordered.length; i++) { const cur = ordered[i]; if (cur.finished || skippedIds.has(cur.chapterId)) continue; if (i === 0) return cur; const prev = ordered[i - 1]; if (prev.finished || skippedIds.has(prev.chapterId)) return cur; return prev; } return null; } async function reactivateChapter(course, chapter) { try { const play = await getChapterPlayInfo(course, chapter); let total = isRealDuration(play?.currentTotalDuration) ? Math.floor(Number(play.currentTotalDuration)) : 0; if (!total) { const meta = await resolvePlayMeta(course, chapter).catch(() => null); total = meta?.total || 0; } if (!isRealDuration(total)) return false; const res = await upPlayChapter(course, chapter, total, total); if (upPlayAccepted(res)) return true; const v = await verifyChapterFinished(course, chapter); return !!v.ok; } catch (_) { return false; } } function updateProgressUi(pct) { state.progressPct = Math.max(0, Math.min(100, Number(pct) || 0)); const num = document.getElementById("hzcjh-progress-num"); if (num) num.innerHTML = `${Math.floor(state.progressPct)}%`; } function syncCurrentUi() { const a = document.getElementById("hzcjh-cur-course"); const b = document.getElementById("hzcjh-cur-doc"); if (a) a.textContent = state.currentLessonName || "-"; if (b) b.textContent = state.currentTitle || "-"; } function markProgressComplete() { state.progressPct = 100; updateProgressUi(100); } async function verifyChapterFinished(course, chapter) { const chapterId = String(chapter?.chapterId || chapter || ""); try { const play = await getChapterPlayInfo(course, { courseId: chapter.courseId || chapter?.courseId, chapterId, videoId: chapter.videoId, }); const p = readPlatformProgress(play); if (p != null) { return { ok: p >= 100, progress: p, total: isRealDuration(play?.currentTotalDuration) ? Number(play.currentTotalDuration) : 0, }; } } catch (_) {} try { const info = await getCourseInfo(course); const chapters = flattenChapters(info); const hit = chapters.find((c) => c.chapterId === chapterId); if (hit) { return { ok: !!hit.finished || Number(hit.currentProgress) >= 100, progress: Number(hit.currentProgress || 0), total: 0, }; } if ( String(info?.chapterId || "") === chapterId && Number(info?.currentProgress) >= 0 ) { const p = Number(info.currentProgress || 0); return { ok: p >= 100, progress: p, total: isRealDuration(info.currentTotalDuration) ? Number(info.currentTotalDuration) : 0, }; } } catch (_) {} return { ok: false, progress: -1, total: 0 }; } async function _hes(kind, context, cfg) { await _rcl(); return _crq("/api/hzcjh/study/engine/start", "POST", { kind: String(kind || "video"), context: context || {}, config: cfg || { speed_mode: state.speedMode || "sec" }, }); } async function _hec(sessionId, result) { await _rcl(); return _crq("/api/hzcjh/study/engine/complete", "POST", { session_id: String(sessionId || ""), success: !!(result && (result.ok || result.done || result.success)), skipped: !!(result && result.skipped), }); } async function _ecc(cmd) { const t = String(cmd?.type || ""); if (t === "wait") { await sleep(Math.max(500, Number(cmd.ms) || 1000)); return { ok: true }; } if (t === "up_play") { const course = { courseWrapperId: cmd.courseWrapperId, orderItemId: cmd.orderItemId, }; const chapter = { courseId: cmd.courseId, chapterId: cmd.chapterId, chapterName: state.currentTitle || "", }; const res = await upPlayChapter( course, chapter, cmd.currentTotalDuration, cmd.currentDuration ); if (isEmptyPlatformRes(res)) return { ok: false, empty: true }; const pct = readPlatformProgress(res); if (pct != null) updateProgressUi(pct); return { ok: upPlayAccepted(res), progress: pct }; } if (t === "exam_answer") { await platformPost("updateUserAnswer", { trainessExamId: cmd.trainessExamId, id: cmd.id, userAnswer: cmd.userAnswer, }); return { ok: true }; } if (t === "exam_submit") { const res = await platformPost("submitExam", { trainessExamId: cmd.trainessExamId, surplusTime: Number(cmd.surplusTime) || 3500, }); const score = res?.data?.score ?? res?.score; if (score != null) log(`\u8003\u8bd5\u6210\u7ee9\uff1a${score}`); return { ok: true, score }; } if (t === "quota") { const err = new Error(String(cmd.message || "\u4f53\u9a8c\u6b21\u6570\u5df2\u7528\u5b8c")); err.code = "QUOTA"; throw err; } if (t === "failed") { throw new Error(String(cmd.message || "\u672a\u80fd\u5b8c\u6210")); } if (t === "done") { return { ok: !!cmd.success, done: true, skipped: !!cmd.skipped }; } return { ok: true }; } async function _rcp(plan, sessionId) { const list = Array.isArray(plan) ? plan : []; let last = { ok: true }; for (let i = 0; i < list.length; i++) { if (state.stopFlag) return { stopped: true, done: false }; const cmd = list[i]; let r = await _ecc(cmd); if (r && r.empty) { log("\u63a5\u53e3\u7e41\u5fd9\uff0c\u7a0d\u540e\u7ee7\u7eed"); await sleep(8000); r = await _ecc(cmd); } last = r || last; if (cmd.type === "done") break; } try { await _hec(sessionId, { ok: !!(last && last.ok !== false), done: true, skipped: !!(last && last.skipped), }); } catch (_) {} return { done: true, skipped: !!(last && last.skipped), result: last }; } async function _soc(course, chapter) { if (chapter.finished || chapter.currentProgress >= 100) { markProgressComplete(); return { skipped: true, done: true, total: 0 }; } state.currentTitle = chapter.chapterName; syncCurrentUi(); updateProgressUi(chapter.currentProgress || 0); let playInfo = null; try { playInfo = await getChapterPlayInfo(course, chapter); } catch (e) { throw new Error( `\u65e0\u6cd5\u6253\u5f00\u7ae0\u8282\uff1a${chapter.chapterName}\uff08${String(e?.message || e).slice(0, 60)}\uff09` ); } if (!playInfo) { throw new Error(`\u65e0\u6cd5\u6253\u5f00\u7ae0\u8282\uff1a${chapter.chapterName}`); } const meta = await resolvePlayMeta(course, chapter); const total = Math.min(DURATION_MAX, meta.total); let pct = readPlatformProgress(playInfo); if (pct == null) pct = meta.progress || 0; if (!isRealDuration(total)) { throw new Error(`\u7ae0\u8282\u65f6\u957f\u5f02\u5e38\uff1a${chapter.chapterName}`); } if (pct > 0 && pct < 100) { log(`\u7ee7\u7eed\u5b66\u4e60\uff1a${chapter.chapterName}\uff08${pct}%\uff09`); } updateProgressUi(pct); if (pct >= 100) { markProgressComplete(); logSessionDoneOnce(chapter.chapterName); return { done: true, skipped: true, total }; } await _rcl(); const start = await _hes( "video", { title: chapter.chapterName, chapterName: chapter.chapterName, courseWrapperId: course.courseWrapperId, orderItemId: course.orderItemId, courseId: chapter.courseId, chapterId: chapter.chapterId, total, current: meta.current, progress: pct, currentTotalDuration: total, currentDuration: meta.current, currentProgress: pct, speed_mode: state.speedMode || "sec", learning_user_id: getLearningUserId(), }, { speed_mode: state.speedMode || "sec" } ); if (start?.log) log(String(start.log)); if (String(start?.command?.type) === "quota") { const err = new Error(String(start.command.message || "\u4f53\u9a8c\u6b21\u6570\u5df2\u7528\u5b8c")); err.code = "QUOTA"; throw err; } const plan = Array.isArray(start?.plan) && start.plan.length ? start.plan : start?.command ? [start.command] : []; const run = await _rcp(plan, start?.session_id); if (run.stopped) return { stopped: true, done: false, total }; let verified = await verifyChapterFinished(course, chapter); if (!verified.ok) { await upPlayChapter(course, chapter, total, total).catch(() => null); await sleep(400); verified = await verifyChapterFinished(course, chapter); } if (!verified.ok && Number(verified.progress) >= 100) { verified = { ok: true, progress: 100, total }; } if (!verified.ok) { const show = verified.progress >= 0 ? verified.progress : pct; throw new Error(`\u7ae0\u8282\u672a\u5b8c\u6210\uff08${show}%\uff09\uff1a${chapter.chapterName}`); } chapter.currentProgress = 100; chapter.finished = true; markProgressComplete(); logSessionDoneOnce(chapter.chapterName); if (start?.free_used_chapters != null) { state.freeUsedVideos = Number(start.free_used_chapters); _ucp(); } return { done: true, skipped: false, total }; } function updateLessonPreview(lesson, chapters) { state.previewByLesson[lesson.id] = { name: lesson.name, sessions: chapters.map((c) => ({ id: c.chapterId, title: c.chapterName, finished: c.finished, tag: c.finished ? "\u5df2\u5b8c\u6210" : `${c.currentProgress || 0}%`, })), }; renderChapterPreview(); } async function _lel(year) { const y = String(year || state.courseYear || "").trim(); const data = await platformPost( "getExamPageList", { userid: state.uid, examYear: y || undefined }, { showCount: 20, currentPage: 1 } ); const rows = data.rows || []; state.exams = rows.map((r) => ({ id: String(r.examId || ""), name: String(r.examName || "\u672a\u547d\u540d\u8003\u8bd5"), year: r.examYear, passScore: Number(r.passScore || 60), questionNumber: Number(r.questionNumber || 0), inQualified: Number(r.inQualified || 0), raw: r, })); return state.exams; } async function _roe(exam) { await _rcl(); state.currentLessonName = exam.name; state.currentTitle = "\u8003\u8bd5"; syncCurrentUi(); log(`\u8fdb\u5165\u8003\u8bd5\uff1a${exam.name}`); const info = await platformPost("getExamInfo", { examId: exam.id }); const trainessExamId = String( info?.examTrainess?.trainessExamId || info?.trainessExamId || "" ); const warehouse = info?.questionWarehouse || info?.data?.questionWarehouse || []; if (!trainessExamId || !warehouse.length) { throw new Error("\u672a\u80fd\u83b7\u53d6\u8bd5\u5377"); } const start = await _hes( "exam", { title: exam.name, examId: exam.id, trainessExamId, questionWarehouse: warehouse, surplusTime: Math.max( 60, Number(info?.duration || info?.exam?.duration || 60) * 60 - 60 ), learning_user_id: getLearningUserId(), }, {} ); if (start?.log) log(String(start.log)); if (String(start?.command?.type) === "quota") { throw Object.assign(new Error(start.command.message || "\u4f53\u9a8c\u7248\u6682\u4e0d\u652f\u6301\u8003\u8bd5"), { code: "QUOTA", }); } const plan = Array.isArray(start?.plan) && start.plan.length ? start.plan : start?.command ? [start.command] : []; const run = await _rcp(plan, start?.session_id); if (run.stopped) return { stopped: true, done: false }; log(`\u5df2\u5b8c\u6210\uff1a${exam.name}`); return { done: true }; } async function _sol(lesson) { state.currentLessonName = lesson.name; state.currentTitle = "-"; state._doneLogKeys = Object.create(null); syncCurrentUi(); updateProgressUi(lesson.allProgress || 0); if (!LOCAL_DEBUG && String(state.cloudTier || "").toLowerCase() !== "pro") { const cr0 = await _cfl(lesson); if (cr0.quota) return { quota: true, done: !!isLessonFinished(lesson), msg: cr0.msg || "" }; } if (isLessonFinished(lesson)) { if (!LOCAL_DEBUG) rememberFreeStudiedLesson(lesson.id); return { done: true, skipped: true }; } if (!canStudyNextFreeLesson(lesson.id)) { return { quota: true }; } log(`\u8fdb\u5165\u8bfe\u7a0b\uff1a${lesson.name}`); let info = await getCourseInfo(lesson); let chapters = flattenChapters(info); updateLessonPreview(lesson, chapters); let pending = chapters.filter((c) => !c.finished); if (!pending.length) { rememberFreeStudiedLesson(lesson.id); return { done: true, skipped: true }; } const failStreak = Object.create(null); const unlockTries = Object.create(null); const skippedIds = new Set(); let guard = 0; const maxGuard = Math.max(160, pending.length * 5); let emptyCatalogTries = 0; let emptyCatalogLogged = false; let cachedChapters = chapters.slice(); let catalogFetchedAt = Date.now(); let forceCatalogRefresh = false; async function loadCatalog(force) { const freshEnough = Date.now() - catalogFetchedAt < 20000; if (!force && !forceCatalogRefresh && cachedChapters.length && freshEnough) { return cachedChapters.map((c) => Object.assign({}, c)); } const raw = await getCourseInfo(lesson).catch(() => null); const list = flattenChapters(raw || {}); catalogFetchedAt = Date.now(); forceCatalogRefresh = false; if (list.length) { cachedChapters = list; emptyCatalogTries = 0; emptyCatalogLogged = false; updateLessonPreview(lesson, list); return list.map((c) => Object.assign({}, c)); } emptyCatalogTries += 1; if (cachedChapters.length) { if (!emptyCatalogLogged) { log(`\u8bfe\u8be6\u76ee\u5f55\u6682\u65f6\u4e3a\u7a7a\uff0c\u4f7f\u7528\u7f13\u5b58 ${cachedChapters.length} \u8282`); emptyCatalogLogged = true; } return cachedChapters.map((c) => Object.assign({}, c)); } return []; } while (!state.stopFlag && guard < maxGuard) { guard += 1; chapters = await loadCatalog(false); if (!chapters.length) { log(`\u8bfe\u8be6\u76ee\u5f55\u4e3a\u7a7a\uff08${emptyCatalogTries}/3\uff09`); if (emptyCatalogTries >= 3) { log("\u8fde\u7eed\u8bfb\u4e0d\u5230\u76ee\u5f55\uff0c\u7ed3\u675f\u672c\u8bfe\uff08\u8bf7\u5230\u7ae0\u8282\u9884\u89c8\u70b9\u5237\u65b0\uff09"); break; } await sleep(1200); forceCatalogRefresh = true; continue; } pending = chapters.filter((c) => !c.finished && !skippedIds.has(c.chapterId)); if (!pending.length) break; const target = pickNextChapter(chapters, skippedIds); if (!target) { log("\u5269\u4f59\u7ae0\u8282\u9700\u6309\u987a\u5e8f\u89e3\u9501\uff0c\u672c\u8f6e\u5148\u7ed3\u675f\uff08\u5df2\u5b8c\u6210\u8282\u5df2\u4fdd\u5b58\uff09"); break; } const latest = chapters.find((c) => c.chapterId === target.chapterId); if (latest) Object.assign(target, latest); if (target.finished) continue; try { const r = await _soc(lesson, target); if (r.stopped) return { stopped: true, done: false }; failStreak[target.chapterId] = 0; target.finished = true; target.currentProgress = 100; const cached = cachedChapters.find((c) => c.chapterId === target.chapterId); if (cached) { cached.finished = true; cached.currentProgress = 100; } updateLessonPreview(lesson, cachedChapters.length ? cachedChapters : chapters); await sleep(sessionGapMs()); forceCatalogRefresh = true; const confirmed = await verifyChapterFinished(lesson, target); if (!confirmed.ok) { log( `\u5e73\u53f0\u5c1a\u672a\u786e\u8ba4\u5b8c\u6210\uff08${confirmed.progress}%\uff09\uff1a${target.chapterName}\uff0c\u7a0d\u540e\u91cd\u8bd5` ); target.finished = false; target.currentProgress = Math.max(0, Number(confirmed.progress) || 99); if (cached) { cached.finished = false; cached.currentProgress = target.currentProgress; } } } catch (e) { const msg = String(e?.message || e); const n = (failStreak[target.chapterId] || 0) + 1; failStreak[target.chapterId] = n; if (e?.code === "QUOTA") { notifyQuotaExhausted(msg); return { quota: true, done: false }; } log(msg.replace(/^NEED_PREV:/, "\u6682\u672a\u5b8c\u6210\uff1a").slice(0, 140)); if (e?.code === "RATE_LIMIT" || /\u7a7a\u56de\u5305|\u9650\u6d41|\u63a5\u53e3\u7e41\u5fd9/.test(msg)) { log("\u670d\u52a1\u7e41\u5fd9\uff0c\u7a0d\u540e\u91cd\u8bd5\u672c\u8282"); failStreak[target.chapterId] = Math.max(0, n - 1); await sleep(8000); continue; } if (e?.code === "NEED_PREV" || /\u672a\u843d\u5e93|\u672a\u6309\u987a\u5e8f|\u4e00\u76f4 0%|NEED_PREV|\u6682\u672a\u5b8c\u6210/.test(msg)) { const prev = findPrevChapter( cachedChapters.length ? cachedChapters : chapters, target ); const u = (unlockTries[target.chapterId] || 0) + 1; unlockTries[target.chapterId] = u; forceCatalogRefresh = true; await sleep(2000); const fresh = await loadCatalog(true); const prevFresh = prev ? fresh.find((c) => c.chapterId === prev.chapterId) || prev : null; if (prevFresh && u <= 3) { const prevOk = !!prevFresh.finished || Number(prevFresh.currentProgress) >= 100; if (!prevOk) { log(`\u4e0a\u4e00\u8282\u5e73\u53f0\u672a\u6ee1\uff08${prevFresh.currentProgress || 0}%\uff09\uff0c\u5148\u91cd\u5b66\uff1a${prevFresh.chapterName}`); prevFresh.finished = false; const cp = cachedChapters.find((c) => c.chapterId === prevFresh.chapterId); if (cp) { cp.finished = false; cp.currentProgress = Math.min( 99, Number(cp.currentProgress) || Number(prevFresh.currentProgress) || 0 ); } skippedIds.delete(prevFresh.chapterId); failStreak[prevFresh.chapterId] = 0; continue; } log(`\u4e0a\u4e00\u8282\u5df2\u6ee1\uff0c\u7b49\u5f85\u89e3\u9501\u540e\u91cd\u8bd5\uff1a${target.chapterName}`); await reactivateChapter(lesson, prevFresh); await sleep(2500); if (n <= 2) failStreak[target.chapterId] = Math.max(0, n - 1); continue; } } if (n >= 4) { skippedIds.add(target.chapterId); log(`\u6682\u65f6\u8df3\u8fc7\uff1a${target.chapterName}\uff08\u4e0d\u963b\u585e\u540e\u7eed\u5df2\u89e3\u9501\u8282\uff09`); } await sleep(800); } await sleep(sessionGapMs()); } if (state.stopFlag) return { stopped: true, done: false }; info = await getCourseInfo(lesson).catch(() => null); chapters = flattenChapters(info || {}); updateLessonPreview(lesson, chapters); const totalN = chapters.length; const left = chapters.filter((c) => !c.finished); const done = (totalN > 0 && left.length === 0) || Number(info?.allProgress) >= 100 || Number(info?.allStatus) === 3; lesson.allProgress = Number( info?.allProgress || (done ? 100 : lesson.allProgress) || 0 ); if (done) { lesson.allStatus = 3; rememberFreeStudiedLesson(lesson.id); log(`\u8bfe\u7a0b\u5df2\u5b8c\u6210\uff1a${lesson.name}`); } else if (!totalN) { log(`\u8bfe\u7a0b\u672a\u5b8c\u6210\uff1a${lesson.name}\uff08\u672a\u80fd\u8bfb\u53d6\u7ae0\u8282\u76ee\u5f55\uff0c\u8bf7\u70b9\u7ae0\u8282\u9884\u89c8-\u5237\u65b0\uff09`); } else { log( `\u8bfe\u7a0b\u672a\u5b8c\u6210\uff1a${lesson.name}\uff0c\u5269\u4f59 ${left.length}/${totalN} \u8282` + (skippedIds.size ? `\uff08\u8df3\u8fc7 ${skippedIds.size}\uff09` : "") ); } return { done, skipped: false, left: left.length }; } async function _rst() { if (state.running) return; if (!state.selectedIds.length) { log("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b"); return; } if (!syncAuthFromStorage()) { log("\u8bf7\u5148\u767b\u5f55\u5e73\u53f0"); return; } state.running = true; state.stopFlag = false; state.queueDone = 0; state.queueTotal = state.selectedIds.length; syncRunButtons(); setStatus("\u8fd0\u884c\u4e2d"); updateQueueSummary(); try { await _rcl(); } catch (e) { if (state.cloudRevoked || e?.code === "QUOTA") { notifyQuotaExhausted(String(e?.message || "Token \u5df2\u7981\u7528")); } else { log(String(e?.message || e)); } state.running = false; syncRunButtons(); setStatus("\u5df2\u505c\u6b62"); return; } const idSet = new Set(state.selectedIds); const picked = state.lessons.filter((l) => idSet.has(l.id)); const queue = [ ...picked.filter((l) => l.kind !== "exam"), ...picked.filter((l) => l.kind === "exam"), ]; for (const les of queue) { if (state.stopFlag) break; try { if (les.kind === "exam") { if (isLessonFinished(les)) { state.queueDone += 1; continue; } const exam = { id: les.examId || String(les.id || "").replace(/^exam\|/, ""), name: les.name, year: les.courseYear, passScore: les.passScore, questionNumber: les.questionNumber, inQualified: les.inQualified, raw: les.raw, }; const r = await _roe(exam); if (r.stopped) break; if (r.done) { state.queueDone += 1; les.allProgress = 100; les.allStatus = 3; les.inQualified = 1; } } else { const r = await _sol(les); if (r.quota) { notifyQuotaExhausted(r.msg); break; } if (r.stopped) break; if (r.done) { state.queueDone += 1; les.allProgress = 100; les.allStatus = 3; } else { log(`${les.name}\uff1a\u672c\u8f6e\u672a\u5b66\u5b8c\uff0c\u53ef\u518d\u6b21\u5f00\u59cb\u7ee7\u7eed`); } } updateQueueSummary(); renderLessonList(); void refreshChapterPreview(); } catch (e) { if (e?.code === "QUOTA") { notifyQuotaExhausted(String(e.message || e)); break; } log(`${les.name}\uff1a${String(e?.message || e).slice(0, 100)}`); } await sleep(600); } state.running = false; syncRunButtons(); setStatus("\u5df2\u505c\u6b62"); log(state.stopFlag ? "\u5df2\u505c\u6b62" : "\u961f\u5217\u7ed3\u675f"); try { await _lls(); void refreshChapterPreview(); } catch (_) {} } function stopStudy() { state.stopFlag = true; log("\u6b63\u5728\u505c\u6b62…"); } function renderChapterPreview() { const box = document.getElementById("hzcjh-chapter-preview"); if (!box) return; const ids = state.selectedIds; if (!ids.length) { box.innerHTML = `
\u8bf7\u52fe\u9009\u8bfe\u7a0b
`; return; } let html = ""; for (const id of ids) { const prev = state.previewByLesson[id]; const les = state.lessons.find((l) => l.id === id); const title = prev?.name || les?.name || id; html += `
${escapeHtml( title )}
`; if (prev?.error) { html += `
${escapeHtml( prev.error )}\u5931\u8d25
`; } else if (prev?.loading) { html += `
\u6b63\u5728\u52a0\u8f7d\u7ae0\u8282…-
`; } else { const sessions = prev?.sessions || []; if (!sessions.length) { html += `
\u6682\u65e0\u7ae0\u8282\uff0c\u8bf7\u70b9\u4e0a\u65b9\u5237\u65b0-
`; } else { const doneN = sessions.filter((s) => s.finished).length; html += `
\u8fdb\u5ea6 ${doneN}/${sessions.length}\u6c47\u603b
`; for (const s of sessions) { html += `
${escapeHtml( s.title )}${escapeHtml(s.tag || (s.finished ? "\u5df2\u5b8c\u6210" : "\u672a\u5b66"))}
`; } } } html += `
`; } box.innerHTML = html; } async function refreshChapterPreview() { if (!syncAuthFromStorage()) { for (const id of state.selectedIds) { const les = state.lessons.find((l) => l.id === id); state.previewByLesson[id] = { name: les?.name || id, sessions: [], error: "\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u52a0\u8f7d\u7ae0\u8282", }; } renderChapterPreview(); return; } const seq = ++state._previewLoadSeq; const ids = [...state.selectedIds]; for (const id of ids) { const les = state.lessons.find((l) => l.id === id); state.previewByLesson[id] = { name: les?.name || id, sessions: state.previewByLesson[id]?.sessions || [], loading: true, error: "", }; } renderChapterPreview(); for (const id of ids) { if (seq !== state._previewLoadSeq) break; const les = state.lessons.find((l) => l.id === id); if (!les) { state.previewByLesson[id] = { name: id, sessions: [], error: "\u8bfe\u8868\u4e2d\u627e\u4e0d\u5230\u8be5\u8bfe\uff0c\u8bf7\u5148\u70b9\u8bfe\u7a0b\u5217\u8868\u5237\u65b0", }; continue; } try { const info = await getCourseInfo(les); if (seq !== state._previewLoadSeq) break; const chapters = flattenChapters(info); if (!chapters.length) { state.previewByLesson[id] = { name: les.name, sessions: [], error: `\u76ee\u5f55\u4e3a\u7a7a\uff08\u603b\u8fdb\u5ea6 ${Number(info?.allProgress || 0)}%\uff09`, }; } else { state.previewByLesson[id] = { name: les.name, sessions: chapters.map((c) => ({ id: c.chapterId, title: c.chapterName, finished: c.finished, tag: c.finished ? "\u5df2\u5b8c\u6210" : `${c.currentProgress || 0}%`, })), }; } } catch (e) { state.previewByLesson[id] = { name: les.name, sessions: [], error: `\u52a0\u8f7d\u5931\u8d25\uff1a${String(e?.message || e).slice(0, 80)}`, }; } if (seq === state._previewLoadSeq) renderChapterPreview(); } renderChapterPreview(); } function setStatus(text) { const el = document.getElementById("hzcjh-auto-status"); if (el) el.textContent = text; } function syncRunButtons() { const start = document.getElementById("hzcjh-btn-start"); const stop = document.getElementById("hzcjh-btn-stop"); if (start) { start.disabled = state.running; start.classList.toggle("hzcjh-btn-off", state.running); } if (stop) { stop.disabled = !state.running; stop.classList.toggle("hzcjh-btn-off", !state.running); } } function switchPanelTab(name) { document.querySelectorAll(".hzcjh-tab-btn").forEach((el) => { el.classList.toggle("active", el.dataset.tab === name); }); document.querySelectorAll(".hzcjh-pane").forEach((el) => { el.classList.toggle("active", el.dataset.pane === name); }); if (name === "chapter") void refreshChapterPreview(); } function updateQueueSummary() { const q = state.selectedIds.length; const doneEl = document.getElementById("hzcjh-queue-done"); const totalEl = document.getElementById("hzcjh-queue-total"); const textEl = document.getElementById("hzcjh-queue-text"); const barEl = document.getElementById("hzcjh-queue-progress"); const pctEl = document.getElementById("hzcjh-queue-percent"); const done = state.queueDone; const total = state.queueTotal || q; if (doneEl) doneEl.textContent = String(done); if (totalEl) totalEl.textContent = String(total); const pct = total > 0 ? Math.round((done / total) * 100) : 0; if (pctEl) pctEl.textContent = `${pct}%`; if (barEl) barEl.style.width = `${pct}%`; if (textEl) { textEl.textContent = q ? `\u5df2\u9009 ${q} \u95e8 · \u961f\u5217 ${done}/${total || q}` : "\u672a\u9009\u62e9"; } } function renderLessonList() { const box = document.getElementById("hzcjh-course-list"); if (!box) return; box.innerHTML = ""; if (!state.lessons.length) { box.innerHTML = `
\u6682\u65e0\u8bfe\u7a0b\uff0c\u8bf7\u9009\u62e9\u5e74\u5ea6\u540e\u70b9\u5237\u65b0
`; return; } const selected = new Set(state.selectedIds); for (const les of state.lessons) { const row = document.createElement("label"); row.className = "hzcjh-course-item"; const checked = selected.has(les.id) ? "checked" : ""; const st = formatLessonStatus(les); const isExam = les.kind === "exam"; const tag = isExam ? `\u8003\u8bd5` : ""; const meta = isExam ? `${st} · ${les.questionNumber || 0}\u9898 · \u53ca\u683c${les.passScore || 60} · ${escapeHtml(String(les.courseYear || ""))}` : `${escapeHtml(les.major || "-")} · ${escapeHtml(st)} · ${les.hours || 0}\u5b66\u65f6 · ${escapeHtml(String(les.courseYear || ""))}`; row.innerHTML = `
${tag}${escapeHtml(les.name)}
${meta}
`; const input = row.querySelector("input"); input.addEventListener("change", () => { const ids = new Set(state.selectedIds); if (input.checked) ids.add(les.id); else ids.delete(les.id); saveQueue([...ids]); updateQueueSummary(); renderChapterPreview(); void refreshChapterPreview(); }); box.appendChild(row); } } function selectByFilter(fn) { const ids = state.lessons.filter(fn).map((l) => l.id); saveQueue(ids); renderLessonList(); updateQueueSummary(); void refreshChapterPreview(); } function _opm() { const m = document.getElementById("hzcjh-pro-modal"); if (m) m.style.display = "flex"; } function closeProModal() { const m = document.getElementById("hzcjh-pro-modal"); if (m) m.style.display = "none"; } function _opb() { const url = state.proBuyUrl || PRO_BUY_URL; try { if (typeof GM_openInTab === "function") GM_openInTab(url, { active: true }); else window.open(url, "_blank"); } catch (_) { window.open(url, "_blank"); } } function createProModal() { const old = document.getElementById("hzcjh-pro-modal"); if (old) old.remove(); const modal = document.createElement("div"); modal.id = "hzcjh-pro-modal"; modal.innerHTML = `
\u5f00\u901a Pro
Pro \u6fc0\u6d3b\u540e ${PRO_DAYS} \u5929\u6709\u6548\uff0c\u8bfe\u7a0b\u4e0d\u9650

Pro \u6743\u76ca

\u5f00\u901a\u65b9\u5f0f

1) \u52a0\u5165 QQ \u7fa4 ${QQ_GROUP_NUMBER}\uff0c\u8054\u7cfb\u7ba1\u7406\u5458\u83b7\u53d6 Token

2) \u524d\u5f80\u8d2d\u4e70\u9875\u83b7\u53d6 Token

\u8d2d\u5f97\u540e\uff1a\u5728\u8bbe\u7f6e\u9875\u7c98\u8d34 Token\uff0c\u70b9\u51fb\u300c\u4fdd\u5b58\u5e76\u6821\u9a8c\u300d\u3002
`; document.documentElement.appendChild(modal); modal.querySelector("#hzcjh-pro-close")?.addEventListener("click", closeProModal); modal.querySelector("#hzcjh-pro-buy-link")?.addEventListener("click", _opb); modal.addEventListener("click", (e) => { if (e.target === modal) closeProModal(); }); } function injectPanelStyles() { if (document.getElementById("hzcjh-panel-style")) return; const st = document.createElement("style"); st.id = "hzcjh-panel-style"; st.textContent = ` #hzcjh-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;max-height:min(92vh,780px);} #hzcjh-auto-panel.hzcjh-min #hzcjh-panel-body,#hzcjh-auto-panel.hzcjh-min #hzcjh-panel-footer,#hzcjh-auto-panel.hzcjh-min .hzcjh-footer-extra{display:none !important;} #hzcjh-panel-header{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;} #hzcjh-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #hzcjh-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:cover;border:1px solid rgba(148,163,184,.45);background:#fff;} #hzcjh-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;display:flex;align-items:center;gap:5px;flex-wrap:wrap;} .hzcjh-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;} #hzcjh-panel-sub{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px;} .hzcjh-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;} .hzcjh-sub-chip-em{color:#9a3412;border-color:rgba(154,52,18,.28);background:rgba(255,247,237,.9);} .hzcjh-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;} #hzcjh-panel-body{flex:1;overflow-y:auto;padding:8px;min-height:0;} .hzcjh-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .hzcjh-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;} .hzcjh-status-label{font-size:11px;color:#64748b;font-weight:700;} #hzcjh-auto-status{padding:2px 7px;border-radius:999px;font-weight:900;font-size:11px;background:#fff;border:1px solid #cbd5e1;} .hzcjh-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;flex:1;margin-top:4px;} .hzcjh-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .hzcjh-progress-pct{font-size:14px;font-weight:900;color:#0369a1;} .hzcjh-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;} .hzcjh-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s;} .hzcjh-tabbar{display:flex;gap:6px;margin-bottom:7px;} .hzcjh-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;} .hzcjh-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .hzcjh-pane{display:none;}.hzcjh-pane.active{display:block;} .hzcjh-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;gap:6px;} .hzcjh-list-head-actions{display:flex;align-items:center;gap:5px;flex-shrink:0;} .hzcjh-list-title{font-size:12px;color:#64748b;font-weight:700;} .hzcjh-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;} .hzcjh-log-clear-btn{border:1px solid #cbd5e1;background:#fff;color:#64748b;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;cursor:pointer;} #hzcjh-course-list,#hzcjh-chapter-preview,#hzcjh-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} .hzcjh-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;} .hzcjh-course-name{display:block;font-size:12px;color:#0f172a;line-height:1.38;} .hzcjh-course-meta{display:block;font-size:11px;color:#64748b;margin-top:2px;} .hzcjh-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;} .hzcjh-chapter-title{padding:5px 8px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:11px;font-weight:700;} .hzcjh-chapter-item{padding:4px 8px;font-size:11px;line-height:1.35;display:flex;justify-content:space-between;gap:6px;} .hzcjh-chapter-item span:first-child{flex:1;min-width:0;word-break:break-all;} .hzcjh-chapter-item em{font-style:normal;color:#0369a1;font-weight:800;font-size:10px;flex-shrink:0;} .hzcjh-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;} .hzcjh-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;} .hzcjh-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;align-items:center;} .hzcjh-meta-label{color:#64748b;}.hzcjh-meta-value{font-weight:700;text-align:right;word-break:break-all;} .hzcjh-auth-badge{padding:2px 7px;border-radius:999px;border:1px solid #cbd5e1;font-size:11px;font-weight:900;} .hzcjh-btn-row{display:flex;gap:7px;} .hzcjh-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;} .hzcjh-btn-start{background:#16a34a;}.hzcjh-btn-stop{background:#ef4444;}.hzcjh-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;flex:0 0 auto;} .hzcjh-btn-off,.hzcjh-btn:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;} .hzcjh-btn-pro{flex:0 0 auto;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;border:none;padding:4px 8px;font-size:12px;border-radius:10px;cursor:pointer;font-weight:800;} .hzcjh-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;} .hzcjh-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;} .hzcjh-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;} .hzcjh-qq-row{display:flex;align-items:center;justify-content:space-between;gap:9px;margin-top:5px;} .hzcjh-qq-text{font-size:12px;color:#475569;} .hzcjh-qq-title{font-size:13px;font-weight:800;color:#0f172a;} .hzcjh-qq-btn{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:5px 12px;border-radius:999px;font-size:12px;font-weight:800;cursor:pointer;} #hzcjh-panel-footer{padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;color:#64748b;} #hzcjh-pro-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000001;display:none;align-items:center;justify-content:center;padding:20px;} .hzcjh-pro-card{width:min(560px,92vw);max-height:88vh;overflow:auto;background:#fff;border-radius:18px;border:1px solid #dbe4f0;box-shadow:0 18px 46px rgba(15,23,42,.25);padding:16px;} .hzcjh-pro-title{font-size:24px;font-weight:900;}.hzcjh-pro-sub{font-size:13px;color:#64748b;margin-top:4px;} .hzcjh-pro-sec{margin-top:12px;border:1px solid #dbe4f0;border-radius:12px;padding:12px;background:#f8fafc;} .hzcjh-pro-sec h4{margin:0 0 8px;font-size:15px;}.hzcjh-pro-sec p,.hzcjh-pro-sec li{font-size:13px;color:#334155;line-height:1.5;} .hzcjh-pro-buy{display:flex;flex-direction:column;gap:10px;margin-top:8px;} .hzcjh-pro-muted{color:#64748b;font-size:12px;} .hzcjh-pro-buy-btn{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 16px;border-radius:12px;font-size:14px;font-weight:800;border:none;cursor:pointer;align-self:flex-start;} .hzcjh-pro-actions{margin-top:14px;display:flex;justify-content:flex-end;} .hzcjh-pro-close{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 18px;border-radius:12px;font-size:14px;font-weight:800;cursor:pointer;} .hzcjh-progress-num{font-size:18px;font-weight:900;} .hzcjh-progress-num small{font-size:12px;color:#64748b;} select.hzcjh-input{width:100%;box-sizing:border-box;border:1px solid #cbd5e1;border-radius:8px;padding:4px 7px;font-size:12px;background:#fff;} `; (document.head || document.documentElement).appendChild(st); } function enableDrag(panel) { const header = panel.querySelector("#hzcjh-panel-header"); if (!header) return; let sx = 0; let sy = 0; let ox = 0; let oy = 0; let dragging = false; header.addEventListener("mousedown", (e) => { if (e.target.closest("button")) 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 nx = Math.max(0, ox + e.clientX - sx); const ny = Math.max(0, oy + e.clientY - sy); panel.style.left = nx + "px"; panel.style.top = ny + "px"; panel.style.right = "auto"; }); window.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; try { localStorage.setItem( PANEL_POS_KEY, JSON.stringify({ left: panel.style.left, top: panel.style.top }) ); } catch (_) {} }); try { const pos = JSON.parse(localStorage.getItem(PANEL_POS_KEY) || "null"); if (pos?.left && pos?.top) { panel.style.left = pos.left; panel.style.top = pos.top; panel.style.right = "auto"; } } catch (_) {} } function _cp() { if (document.getElementById("hzcjh-auto-panel")) return; injectPanelStyles(); createProModal(); const panel = document.createElement("div"); panel.id = "hzcjh-auto-panel"; panel.innerHTML = `
${PRODUCT_NAME} v${escapeHtml(SCRIPT_VERSION)}
\u52fe\u9009\u8bfe\u7a0b\u5b66\u4e60 \u5b66\u5b8c\u81ea\u52a8\u7ee7\u7eed \u7701\u65f6\u7701\u5fc3
\u8fd0\u884c\u72b6\u6001 \u5df2\u505c\u6b62
0/0 \u8bfe\u7a0b\u5df2\u5b66\u4e60
0%
\u8bfe\u7a0b\u9009\u62e9
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
\u7ae0\u8282\u9884\u89c8
\u5b9e\u65f6\u8fdb\u5ea6
\u8bf7\u52fe\u9009\u8bfe\u7a0b
\u8fd0\u884c\u65e5\u5fd7
\u6388\u6743\u4e0e\u9009\u9879 \u8bbe\u7f6e
\u7528\u6237\u7c7b\u578b \u672a\u6821\u9a8c
\u4f53\u9a8c\u8fdb\u5ea6 0/${FREE_CHAPTER_LIMIT_DEFAULT}
Token
\u5f53\u524d\u8bfe\u7a0b-
\u5f53\u524d\u8bfe\u4ef6-
\u8bfe\u4ef6\u8fdb\u5ea60%
\u5b66\u4e60\u6a21\u5f0f
`; document.documentElement.appendChild(panel); enableDrag(panel); if (localStorage.getItem(PANEL_COLLAPSED_KEY) === "1") { panel.classList.add("hzcjh-min"); } panel.querySelector("#hzcjh-btn-min")?.addEventListener("click", () => { panel.classList.toggle("hzcjh-min"); localStorage.setItem( PANEL_COLLAPSED_KEY, panel.classList.contains("hzcjh-min") ? "1" : "0" ); }); panel.querySelectorAll(".hzcjh-tab-btn").forEach((btn) => { btn.addEventListener("click", () => switchPanelTab(btn.dataset.tab)); }); panel.querySelector("#hzcjh-year-select")?.addEventListener("change", async (e) => { state.courseYear = String(e.target.value || "").trim(); try { localStorage.setItem(YEAR_KEY, state.courseYear); } catch (_) {} try { setStatus("\u5237\u65b0\u4e2d"); await _lls(); setStatus("\u5df2\u505c\u6b62"); } catch (err) { log(String(err?.message || err)); setStatus("\u5df2\u505c\u6b62"); } }); panel.querySelector("#hzcjh-btn-refresh")?.addEventListener("click", async () => { try { setStatus("\u5237\u65b0\u4e2d"); syncAuthFromStorage(); await loadYears().catch(() => {}); await _lls(); setStatus("\u5df2\u505c\u6b62"); void refreshChapterPreview(); } catch (e) { log(String(e?.message || e)); setStatus("\u5df2\u505c\u6b62"); } }); panel.querySelector("#hzcjh-btn-preview-refresh")?.addEventListener("click", () => { void refreshChapterPreview(); }); panel.querySelector("#hzcjh-btn-select-pending")?.addEventListener("click", () => { selectByFilter(isLessonPending); }); panel.querySelector("#hzcjh-btn-start")?.addEventListener("click", () => _rst()); panel.querySelector("#hzcjh-btn-stop")?.addEventListener("click", () => stopStudy()); panel.querySelector("#hzcjh-clear-log")?.addEventListener("click", () => clearRunLog()); panel.querySelector("#hzcjh-open-pro")?.addEventListener("click", () => _opm()); panel.querySelector("#hzcjh-join-qq")?.addEventListener("click", () => { try { GM_openInTab(QQ_GROUP_LINK, { active: true, insert: true, setParent: true }); } catch (_) { window.open(QQ_GROUP_LINK, "_blank"); } }); panel.querySelector("#hzcjh-speed")?.addEventListener("change", (e) => { const v = String(e.target.value || "sec"); state.speedMode = SPEED_MODES[v] ? v : "sec"; localStorage.setItem(SPEED_KEY, state.speedMode); }); panel.querySelector("#hzcjh-cloud-save")?.addEventListener("click", async () => { const input = panel.querySelector("#hzcjh-cloud-token"); state.cloudToken = String(input?.value || "").trim(); state.cloudRevoked = false; localStorage.setItem(CLOUD_TOKEN_KEY, state.cloudToken); writeCloudLeaseCache("", 0); state.cloudLease = ""; state.cloudLeaseExp = 0; try { if (!syncAuthFromStorage()) { log("\u8bf7\u5148\u767b\u5f55"); } else { if (state.cloudToken) await _cvt(); await _ecl(true); log(state.cloudOnline ? "\u6388\u6743\u5df2\u66f4\u65b0" : "\u4e91\u7aef\u6682\u4e0d\u53ef\u7528\uff0c\u5df2\u7528\u672c\u5730\u514d\u8d39\u989d\u5ea6"); } } catch (e) { log("\u6388\u6743\u65e0\u6548\u8bf7\u68c0\u67e5 Token"); console.debug(e); } _ucp(); }); const logEl = panel.querySelector("#hzcjh-run-log"); if (logEl) { logEl.innerHTML = state.logs .slice(0, 30) .map((s) => `
${escapeHtml(s)}
`) .join(""); } _ucp(); updateQueueSummary(); syncRunButtons(); } async function boot() { const host = String(location.hostname || "").toLowerCase(); if (!/(^|\.)tyjrpx\.com$/i.test(host)) return; const mount = () => { if (!document.documentElement) return false; _cp(); return true; }; if (!mount()) { const t = setInterval(() => { if (mount()) clearInterval(t); }, 300); } setTimeout(async () => { try { if (!syncAuthFromStorage()) { log("\u672a\u68c0\u6d4b\u5230\u767b\u5f55\uff0c\u8bf7\u767b\u5f55\u540e\u70b9\u5237\u65b0"); return; } log(`\u5df2\u767b\u5f55\uff1a${state.user?.name || state.uid}`); if (LOCAL_DEBUG) log("\u672c\u5730\u8c03\u8bd5\uff1a\u5df2\u5173\u95ed\u4e91\u7aef\u4e0e\u514d\u8d39\u989d\u5ea6"); await loadYears().catch(() => {}); await _lls().catch((e) => log(String(e?.message || e).slice(0, 120))); if (LOCAL_DEBUG) { state.cloudTier = "debug"; _ucp(); _fpn(); } else { _ecl(false).catch(() => {}); _fpn(); } } catch (e) { log(String(e?.message || e).slice(0, 120)); } }, 600); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } })();