// ==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 `${escapeHtml(
y.label || v || "\u5168\u90e8"
)} `;
})
.join("");
sel.innerHTML =
`${state.years.length ? "\u5168\u90e8\u5e74\u5ea6" : "\u70b9\u5237\u65b0\u52a0\u8f7d\u5e74\u5ea6"} ` +
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
\u4e0d\u9650\u7ae0\u8282\u5b66\u4e60\u4e0e\u8003\u8bd5\uff08\u514d\u8d39\u4f53\u9a8c ${FREE_CHAPTER_LIMIT_DEFAULT} \u4e2a\u7ae0\u8282\uff0cPro \u6fc0\u6d3b\u540e ${PRO_DAYS} \u5929\uff09
\u6fc0\u6d3b\u540e ${PRO_DAYS} \u5929\u6709\u6548\uff0c\u7ed1\u5b9a\u5f53\u524d\u767b\u5f55\u8d26\u53f7
\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
\u524d\u5f80\u8d2d\u4e70\u9875\u5f00\u901a Pro
\u6211\u77e5\u9053\u4e86
`;
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 = `
\u8fd0\u884c\u72b6\u6001
\u5df2\u505c\u6b62
0 /0 \u8bfe\u7a0b\u5df2\u5b66\u4e60
0%
\u8bfe\u7a0b\u5217\u8868
\u7ae0\u8282\u9884\u89c8
\u8fd0\u884c\u65e5\u5fd7
\u8bbe\u7f6e
\u8bfe\u7a0b\u9009\u62e9
\u52fe\u9009\u672a\u5b8c\u6210
\u5237\u65b0
\u767b\u5f55\u540e\u70b9\u5237\u65b0\u52a0\u8f7d\u5e74\u5ea6
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
\u7ae0\u8282\u9884\u89c8
\u5b9e\u65f6\u8fdb\u5ea6
\u5237\u65b0
\u8bf7\u52fe\u9009\u8bfe\u7a0b
\u8fd0\u884c\u65e5\u5fd7
\u6e05\u7a7a
\u6388\u6743\u4e0e\u9009\u9879
\u8bbe\u7f6e
\u7528\u6237\u7c7b\u578b
\u672a\u6821\u9a8c
\u4f53\u9a8c\u8fdb\u5ea6
0/${FREE_CHAPTER_LIMIT_DEFAULT}
Token
\u4fdd\u5b58\u5e76\u6821\u9a8c
\u5347\u7ea7Pro
\u5f53\u524d\u8bfe\u7a0b -
\u5f53\u524d\u8bfe\u4ef6 -
\u8bfe\u4ef6\u8fdb\u5ea6 0%
\u5b66\u4e60\u6a21\u5f0f
${Object.values(SPEED_MODES)
.map(
(m) =>
`${escapeHtml(m.label)} `
)
.join("")}
\u5f00\u59cb\u5b66\u4e60
\u505c\u6b62
`;
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();
}
})();