// ==UserScript==
// @name 湖北专业技术人员继续教育公需科目(hb12333.com)学习助手
// @namespace https://card.wlxy.live/details/HBZJ0001
// @version 1.1
// @description 湖北省专业技术人员继续教育公需科目hbzsgx.hb12333.com跳转hbzj-train.yxlearning.com平台学习助手:一键自动看课与考试;官方1:1全自动课时完成,标准用户可体验3个视频章节,会员30天不限量。
// @author 柠檬真酸
// @match https://hbzj-train.yxlearning.com/*
// @match https://*.yxlearning.com/*
// @match https://hbzsgx.hb12333.com/*
// @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png
// @connect hbzj-train.yxlearning.com
// @connect yxlearning.com
// @connect oa23.ahzsksw.cn
// @connect huaweicloudobs.ahjxjy.cn
// @connect card.wlxy.live
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-start
// @antifeature payment 标准用户可体验3个视频章节,开通会员不限量
// @antifeature membership 需授权校验
// @license All Rights Reserved
// ==/UserScript==
(function () {
"use strict";
const SCRIPT_VERSION = "0.3.4";
const PAGE = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
const PANEL_LOGO_URL =
"https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png";
const FALLBACK_ORIGIN = "https://hbzj-train.yxlearning.com";
const API_PREFIX = "/train";
const PATH_ACCOUNT_INFO = `${API_PREFIX}/login/get-account-info.gson`;
const PATH_STUDY_LIST = `${API_PREFIX}/cms/study/find-my-study-info.gson`;
const PATH_STUDY_YEARS = `${API_PREFIX}/cms/study/find-my-study-info-yearlist.gson`;
const PATH_CLASS_YEARS = `${API_PREFIX}/cms/study/get-class-year-info.gson`;
const PATH_MY_CLASS = `${API_PREFIX}/class/my-class.gson`;
const PATH_MY_CLASS_BOOKS = `${API_PREFIX}/class/my-class-books.gson`;
const PATH_VIDEO_SV = `${API_PREFIX}/cms/my-video/sv.gson`;
const PATH_VIDEO_CV = `${API_PREFIX}/cms/my-video/cv.gson`;
const PATH_UPDATE_LAST_VIDEO = `${API_PREFIX}/cms/my-video/update-last-video.gson`;
const PATH_EXAM_LIST = `${API_PREFIX}/cms/paper/find-paper-list.gson`;
const PATH_EXAM_DETAIL = `${API_PREFIX}/cms/paper/get-paper-detail.gson`;
const PATH_EXAM_CHECK_STATUS = `${API_PREFIX}/cms/paper/check-my-exam-status.gson`;
const PATH_EXAM_SAVE_RECORD = `${API_PREFIX}/cms/paper/save-my-exam-record.gson`;
const PATH_EXAM_START = `${API_PREFIX}/cms/paper/start-do-paper-or-test.gson`;
const PATH_EXAM_SUBMIT = `${API_PREFIX}/cms/paper/submit-paper.gson`;
const CMS_ACCOUNT_KEY = "cmsAccountInfo";
const PINNED_CLOUD_HOST = "oa23.ahzsksw.cn";
const DEFAULT_CLOUD_API_BASE = "https://oa23.ahzsksw.cn";
const CLOUD_TOKEN_KEY = "hbzj_cloud_token_v1";
const CLOUD_LEASE_CACHE_KEY = "hbzj_cloud_lease_cache_v1";
const CLOUD_API_BASE_KEY = "hbzj_cloud_api_base_v1";
const DEFAULT_FREE_CHAPTER_LIMIT = 3;
const PRO_BUY_URL = "https://card.wlxy.live/details/3D736BD3";
const PRO_DAYS = 30;
const PANEL_NOTICE_FALLBACK = "\u6e56\u5317\u4e13\u6280\u57f9\u8bad\u5b66\u4e60\u52a9\u624b";
const DEFAULT_PANEL_NOTICE_PATH = "/api/hbzj/panel-notice";
const QUEUE_KEY = "hbzj_project_queue_v1";
const PANEL_POS_KEY = "hbzj_panel_pos_v1";
const PANEL_COLLAPSED_KEY = "hbzj_panel_collapsed_v1";
const AUTH_CACHE_KEY = "hbzj_auth_cache_v1";
const YEAR_KEY = "hbzj_year_v1";
const EFFICIENCY_JUMP_RATIO = 0.99;
const CV_INTERVAL_MS = 120000;
const CV_STEP_SECONDS = 120;
const SV_BEFORE_CV_MS = 5000;
const WATCH_MAX_ROUNDS = 200;
const ENGINE_TICK_MS = 1200;
const EXAM_PASS_RETRY = 2;
const CV_INTERVAL_MINUTES = CV_INTERVAL_MS / 60000;
const state = {
enabled: false,
engineRunning: false,
panelProjects: [],
panelRefreshing: false,
chapterPreview: [],
chapterPreviewLoading: false,
queueWareTotal: 0,
queueWareDone: 0,
userProfile: null,
panelHint: "",
lastUserAction: "",
runLogs: [],
studyYear:
Number(
(typeof unsafeWindow !== "undefined" ? unsafeWindow : window).localStorage.getItem(YEAR_KEY) ||
new Date().getFullYear()
) || null,
yearOptions: [],
cloudToken: String(
(typeof unsafeWindow !== "undefined" ? unsafeWindow : window).localStorage.getItem(CLOUD_TOKEN_KEY) || ""
).trim(),
cloudLease: "",
cloudLeaseExp: 0,
cloudTier: "unknown",
cloudProExpireAt: 0,
cloudRevoked: false,
freeChapterLimit: DEFAULT_FREE_CHAPTER_LIMIT,
freeUsedChapters: 0,
proBuyUrl: PRO_BUY_URL,
cloudApiBase: "",
panelNoticePath: DEFAULT_PANEL_NOTICE_PATH,
remotePanelNotice: "",
_lastProErr: "",
_bootLoaded: false,
_capturedLists: { learning: [], completed: [] },
_accountId: "",
};
state.cloudApiBase = resolvePinnedCloudApiBase();
function trace(msg) {
console.log(`[\u6e56\u5317\u4e13\u6280\u52a9\u624b] ${String(msg || "")}`);
}
function escHtml(s) {
return String(s ?? "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
function pageStorage() {
try {
return PAGE.localStorage;
} catch (_) {
return localStorage;
}
}
function readJson(key, fallback) {
try {
const raw = pageStorage().getItem(key);
return raw ? JSON.parse(raw) : fallback;
} catch (_) {
return fallback;
}
}
function writeJson(key, val) {
pageStorage().setItem(key, JSON.stringify(val));
}
function detectLogKind(msg, kind) {
if (kind) return String(kind);
const s = String(msg || "");
if (/^\u5f00\u59cb\uff1a/.test(s)) return "start";
if (/^\u5b66\u4e60\uff1a/.test(s)) return "study";
if (/^\u5b8c\u6210\uff1a/.test(s)) return "done";
if (/^\u8003\u8bd5\uff1a/.test(s) || /\u8003\u8bd5.*\u5df2\u63d0\u4ea4/.test(s)) return "exam";
if (/^\u5df2\u505c\u6b62/.test(s)) return "stop";
if (/\u6bcf.*\u5206\u949f|\u8010\u5fc3\u7b49\u5f85|\u4ecd\u5728\u8fd0\u884c/.test(s)) return "hint";
if (/\u5931\u8d25|\u9519\u8bef|\u4e0a\u9650|\u8df3\u8fc7|\u65e0\u89c6\u9891\u6e90|\u7535\u5b50\u4e66|\u8bf7\u5148\u52fe\u9009/.test(s)) return "warn";
return "info";
}
function _rrl() {
const el = document.getElementById("hbzj-run-log");
if (!el) return;
if (!state.runLogs.length) {
el.innerHTML = `
\u6682\u65e0\u65e5\u5fd7
`;
return;
}
el.innerHTML = state.runLogs
.map((item) => {
const row = typeof item === "string" ? { time: "", msg: item, kind: detectLogKind(item) } : item;
const time = row.time ? `${escHtml(row.time)}` : "";
const kind = escHtml(row.kind || "info");
return `${time}${escHtml(row.msg || "")}
`;
})
.join("");
}
function log(msg, kind) {
const text = String(msg || "");
const row = {
time: new Date().toLocaleTimeString(),
msg: text,
kind: detectLogKind(text, kind),
};
state.runLogs.unshift(row);
if (state.runLogs.length > 200) state.runLogs.length = 200;
_rrl();
trace(text);
}
function _lwh() {
if (state._studyWaitHintShown) return;
state._studyWaitHintShown = true;
log(`\u770b\u8bfe\u8fdb\u5ea6\u7ea6\u6bcf ${CV_INTERVAL_MINUTES} \u5206\u949f\u66f4\u65b0\u4e00\u6b21\uff0c\u7b49\u5f85\u671f\u95f4\u811a\u672c\u4ecd\u5728\u8fd0\u884c`, "hint");
}
function resolvePinnedCloudApiBase() {
try {
const raw = String(pageStorage().getItem(CLOUD_API_BASE_KEY) || DEFAULT_CLOUD_API_BASE)
.trim()
.replace(/\/+$/, "");
if (!raw || !new RegExp(PINNED_CLOUD_HOST.replace(/\./g, "\\."), "i").test(raw)) {
try {
pageStorage().setItem(CLOUD_API_BASE_KEY, DEFAULT_CLOUD_API_BASE);
} catch (_) {}
return DEFAULT_CLOUD_API_BASE;
}
return raw;
} catch (_) {
return DEFAULT_CLOUD_API_BASE;
}
}
function getCloudApiBase() {
return String(state.cloudApiBase || DEFAULT_CLOUD_API_BASE).trim().replace(/\/+$/, "");
}
function getPanelNoticeText() {
return String(state.remotePanelNotice || PANEL_NOTICE_FALLBACK).trim() || PANEL_NOTICE_FALLBACK;
}
function renderPanelNotice() {
const el = document.getElementById("hbzj-ann-text");
if (el) el.textContent = getPanelNoticeText();
}
function getApiOrigin() {
try {
if (/yxlearning\.com$/i.test(location.hostname)) return location.origin;
} catch (_) {}
return FALLBACK_ORIGIN;
}
function uuidv4() {
try {
if (PAGE.crypto && typeof PAGE.crypto.randomUUID === "function") return PAGE.crypto.randomUUID();
} catch (_) {}
const s = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
return s;
}
function toQuery(params) {
const parts = [];
Object.keys(params || {}).forEach((k) => {
const v = params[k];
if (v === undefined || v === null) return;
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
});
return parts.join("&");
}
function toFormBody(params) {
return toQuery(params);
}
function readCmsAccountInfo() {
try {
const raw = pageStorage().getItem(CMS_ACCOUNT_KEY);
if (!raw || raw === "null") return null;
const obj = JSON.parse(raw);
return obj && typeof obj === "object" ? obj : null;
} catch (_) {
return null;
}
}
function rememberAccountId(id) {
const aid = String(id || "").trim();
if (!aid || aid.length < 8) return;
if (state._accountId === aid && PAGE.__hbzjAccountId === aid) return;
state._accountId = aid;
PAGE.__hbzjAccountId = aid;
writeAuthCache({ accountId: aid });
}
function getAccountId() {
if (state._accountId) return state._accountId;
if (PAGE.__hbzjAccountId) {
state._accountId = String(PAGE.__hbzjAccountId);
return state._accountId;
}
const cms = readCmsAccountInfo();
if (cms?.accountId) {
rememberAccountId(cms.accountId);
return state._accountId;
}
const cache = readAuthCache();
if (cache.accountId) {
state._accountId = String(cache.accountId);
return state._accountId;
}
return "";
}
function readAuthCache() {
return readJson(AUTH_CACHE_KEY, {}) || {};
}
function writeAuthCache(partial) {
const prev = readAuthCache();
try {
pageStorage().setItem(AUTH_CACHE_KEY, JSON.stringify(Object.assign({}, prev, partial || {})));
} catch (_) {}
}
function hasPlatformAuth() {
return !!getAccountId() || !!readCmsAccountInfo();
}
function unwrapPlatformJson(json) {
if (!json || typeof json !== "object") {
return { code: -1, msg: "empty", data: null, raw: json };
}
if (json.respCode != null && json.attribute !== undefined) {
return {
code: Number(json.respCode),
msg: String(json.respDesc || json.msg || ""),
data: json.attribute,
raw: json,
};
}
return {
code: Number(json.code != null ? json.code : json.respCode),
msg: String(json.msg || json.respDesc || ""),
data: json.data !== undefined ? json.data : json.attribute,
raw: json,
};
}
function pickList(data) {
if (Array.isArray(data)) return data;
if (!data || typeof data !== "object") return [];
if (Array.isArray(data.list)) return data.list;
if (data.listPage && Array.isArray(data.listPage.list)) return data.listPage.list;
if (data.data && Array.isArray(data.data.list)) return data.data.list;
if (Array.isArray(data.myClassCourseRPList)) return data.myClassCourseRPList;
if (Array.isArray(data.data)) return data.data;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.content)) return data.content;
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.courseData)) return data.courseData;
if (data.data && typeof data.data === "object") return pickList(data.data);
return [];
}
function ingestStudyListPayload(url, json) {
try {
if (!/find-my-study-info\.gson/i.test(url)) return;
const un = unwrapPlatformJson(json);
if (Number(un.code) !== 200) return;
const rows = pickList(un.data);
if (!rows.length) return;
const learning = [];
const completed = [];
for (const row of rows) {
const p = mapProjectRow(row);
if (p.listSource === "completed") completed.push(p);
else learning.push(p);
}
const bag = state._capturedLists || { learning: [], completed: [] };
const merge = (src, arr) => {
const byId = new Map((bag[src] || []).map((x) => [x.projectId, x]));
for (const p of arr) byId.set(p.projectId, p);
bag[src] = Array.from(byId.values());
};
merge("learning", learning);
merge("completed", completed);
state._capturedLists = bag;
if (document.getElementById("hbzj-auto-panel") && !state.panelRefreshing) {
mergeCapturedIntoPanel(false);
}
} catch (_) {}
}
function mergeCapturedIntoPanel(preferApi) {
const bag = state._capturedLists || { learning: [], completed: [] };
if (!bag.learning.length && !bag.completed.length) return false;
const byId = new Map();
for (const p of bag.learning || []) byId.set(p.projectId, p);
for (const p of bag.completed || []) byId.set(p.projectId, p);
const merged = Array.from(byId.values());
for (const p of merged) syncProjectFinishedFlags(p);
if (!preferApi || !state.panelProjects.length) {
state.panelProjects = merged;
updatePanel();
}
return state.panelProjects.length > 0;
}
function installAuthHooks() {
if (PAGE.__hbzjNetHooked) return;
PAGE.__hbzjNetHooked = true;
try {
const XHR = PAGE.XMLHttpRequest;
const open = XHR.prototype.open;
const send = XHR.prototype.send;
const setHeader = XHR.prototype.setRequestHeader;
XHR.prototype.open = function (method, url) {
try {
this.__hbzjUrl = String(url || "");
this.__hbzjMethod = String(method || "");
} catch (_) {}
return open.apply(this, arguments);
};
XHR.prototype.setRequestHeader = function (k, v) {
try {
if (/^accountid$/i.test(String(k || ""))) rememberAccountId(v);
} catch (_) {}
return setHeader.apply(this, arguments);
};
XHR.prototype.send = function () {
try {
this.addEventListener("load", function () {
try {
const url = String(this.__hbzjUrl || "");
const text = String(this.responseText || "");
if (!text || text.length > 5e6) return;
const json = JSON.parse(text);
if (/get-account-info\.gson/i.test(url)) {
const un = unwrapPlatformJson(json);
const info = un.data?.data || un.data || null;
if (info?.accountId) rememberAccountId(info.accountId);
if (info) {
state.userProfile = normalizeUserProfile(info);
}
}
if (/yearlist|get-class-year-info/i.test(url)) {
const years = extractYears(unwrapPlatformJson(json).data);
if (years.length) state.yearOptions = years;
}
ingestStudyListPayload(url, json);
} catch (_) {}
});
} catch (_) {}
return send.apply(this, arguments);
};
} catch (_) {}
try {
const rawFetch = PAGE.fetch;
if (typeof rawFetch === "function" && !rawFetch.__hbzjPatched) {
const wrapped = function (input, init) {
try {
const h = init && init.headers;
let aid = "";
if (h && typeof h.get === "function") aid = h.get("accountId") || h.get("AccountId") || "";
else if (h && typeof h === "object") aid = h.accountId || h.AccountId || "";
if (aid) rememberAccountId(aid);
} catch (_) {}
const url = typeof input === "string" ? input : input && input.url;
return rawFetch.apply(PAGE, arguments).then((res) => {
try {
const u = String(url || "");
if (/find-my-study-info\.gson|get-account-info\.gson/i.test(u)) {
res
.clone()
.json()
.then((json) => {
if (/get-account-info/i.test(u)) {
const un = unwrapPlatformJson(json);
const info = un.data?.data || un.data || null;
if (info?.accountId) rememberAccountId(info.accountId);
}
ingestStudyListPayload(u, json);
})
.catch(() => {});
}
} catch (_) {}
return res;
});
};
wrapped.__hbzjPatched = true;
PAGE.fetch = wrapped;
}
} catch (_) {}
}
function apiUrl(path) {
if (/^https?:\/\//i.test(path)) return path;
return getApiOrigin() + (path.startsWith("/") ? path : `/${path}`);
}
async function _prq(method, path, body, opt) {
const accountId = getAccountId();
if (!accountId) throw new Error("\u672a\u83b7\u53d6 accountId\uff0c\u8bf7\u5148\u767b\u5f55\u5b98\u7f51\u540e\u70b9\u300c\u5237\u65b0\u300d");
const url = apiUrl(path);
const headers = Object.assign(
{
Accept: "application/json, text/plain, */*",
accountId: accountId,
},
(opt && opt.headers) || {}
);
let payload = undefined;
const m = String(method || "GET").toUpperCase();
if (body !== undefined && body !== null && m !== "GET") {
if (opt && opt.form) {
headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8";
payload = typeof body === "string" ? body : toFormBody(body);
} else {
headers["Content-Type"] = "application/json;charset=UTF-8";
payload = typeof body === "string" ? body : JSON.stringify(body);
}
}
const parseOk = (json, status) => {
if (status < 200 || status >= 300) {
throw new Error(json?.msg || json?.respDesc || json?.message || `HTTP ${status}`);
}
const un = unwrapPlatformJson(json);
if (Number(un.code) !== 200) {
throw new Error(un.msg || `code_${un.code}`);
}
return un;
};
try {
const pageFetch = PAGE.fetch || fetch;
const res = await pageFetch.call(PAGE, url, {
method: m,
headers,
body: payload,
credentials: "include",
});
const json = await res.json().catch(() => ({}));
return parseOk(json, res.status);
} catch (e) {
const msg = String((e && e.message) || e || "");
if (!/Failed to fetch|NetworkError|CORS/i.test(msg) && /\u672a\u83b7\u53d6|HTTP |code_|accountId/i.test(msg)) {
throw e;
}
trace(`\u9875\u9762 fetch \u5931\u8d25\uff0c\u6539 GM\uff1a${msg}`);
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== "function") {
reject(e);
return;
}
GM_xmlhttpRequest({
method: m,
url,
headers,
data: payload,
anonymous: false,
onload(resp) {
let json = {};
try {
json = JSON.parse(resp.responseText || "{}");
} catch (_) {}
try {
resolve(parseOk(json, resp.status));
} catch (err) {
reject(err);
}
},
onerror() {
reject(new Error("GM \u7f51\u7edc\u9519\u8bef"));
},
});
});
}
}
function apiGet(path) {
return _prq("GET", path, null);
}
function apiPostForm(path, body) {
return _prq("POST", path, body, { form: true });
}
function apiPostJson(path, body) {
return _prq("POST", path, body == null ? {} : body, { form: false });
}
function normalizeUserProfile(info) {
if (!info || typeof info !== "object") return null;
return {
accountId: String(info.accountId || ""),
name: String(info.realName || info.name || info.nickName || info.userName || "").trim(),
loginId: String(info.loginName || info.userName || info.account || info.mobile || "").trim(),
phone: String(info.mobile || info.phone || "").trim(),
raw: info,
};
}
function extractYears(data) {
const arr = pickList(data);
const years = [];
for (const item of arr) {
if (typeof item === "number" || /^\d{4}$/.test(String(item))) {
years.push(Number(item));
} else if (item && typeof item === "object") {
const y = Number(item.year || item.studyYear || item.classYear || item.value || 0);
if (y > 2000) years.push(y);
}
}
return Array.from(new Set(years.filter((n) => n > 2000))).sort((a, b) => b - a);
}
function getLearningUserId() {
return String(
getAccountId() ||
state.userProfile?.accountId ||
readAuthCache().accountId ||
""
).trim();
}
function formatCloudTierText(tier) {
if (state.cloudRevoked) return "\u5df2\u505c\u7528";
const t = String(tier || "").toLowerCase();
if (t === "pro") return "\u5df2\u5f00\u901a";
if (t === "free") return "\u6807\u51c6\u7528\u6237";
if (t === "revoked") return "\u5df2\u505c\u7528";
if (t === "unknown") return "—";
return tier ? String(tier) : "—";
}
function formatExpireText(ts) {
const n = Number(ts || 0);
if (!n) return "—";
const d = new Date(n * 1000);
return Number.isNaN(d.getTime()) ? "—" : d.toLocaleString();
}
function isCloudProTier() {
return !state.cloudRevoked && String(state.cloudTier || "").toLowerCase() === "pro";
}
function readCloudLeaseCache() {
try {
const raw = pageStorage().getItem(CLOUD_LEASE_CACHE_KEY);
if (!raw) return null;
const p = JSON.parse(raw);
const lease = String(p.lease || "").trim();
const exp = Number(p.exp || 0);
if (!lease || !Number.isFinite(exp) || exp <= 0) return null;
return {
lease,
exp,
tier: p.tier || "",
proExpireAt: Number(p.proExpireAt || 0),
};
} catch (_) {
return null;
}
}
function writeCloudLeaseCache(lease, exp, extra) {
if (!lease || !exp) {
pageStorage().removeItem(CLOUD_LEASE_CACHE_KEY);
return;
}
pageStorage().setItem(
CLOUD_LEASE_CACHE_KEY,
JSON.stringify(Object.assign({ lease, exp }, extra || {}))
);
}
function _crq(path, method, body, headersExtra) {
const url = `${getCloudApiBase()}${path}`;
const headers = Object.assign(
{
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
},
headersExtra || {}
);
if (state.cloudToken) headers.Authorization = `Bearer ${state.cloudToken}`;
const luid = getLearningUserId();
if (luid) headers["x-learning-user-id"] = luid;
const payload = body == null ? null : JSON.stringify(body);
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== "function") {
reject(new Error("\u4e91\u7aef\u8bf7\u6c42\u4e0d\u53ef\u7528"));
return;
}
GM_xmlhttpRequest({
method: method || "POST",
url,
headers,
data: payload,
onload(resp) {
let data = {};
try {
data = JSON.parse(String(resp.responseText || "").trim() || "{}");
} catch (_) {
data = {};
}
const status = Number(resp.status || 0);
if (status === 404) {
reject(new Error("cloud_404"));
return;
}
if (status < 200 || status >= 300) {
reject(new Error(String(data.detail || data.message || data.msg || `http_${status}`)));
return;
}
resolve(data);
},
onerror() {
reject(new Error("\u4e91\u7aef\u7f51\u7edc\u9519\u8bef"));
},
ontimeout() {
reject(new Error("\u4e91\u7aef\u8bf7\u6c42\u8d85\u65f6"));
},
});
});
}
async function _fpn() {
try {
const text = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `${getCloudApiBase()}${state.panelNoticePath || DEFAULT_PANEL_NOTICE_PATH}`,
headers: { Accept: "text/plain, */*" },
onload(resp) {
if (resp.status >= 200 && resp.status < 300) resolve(String(resp.responseText || "").trim());
else reject(new Error("notice_fail"));
},
onerror() {
reject(new Error("notice_net"));
},
});
});
if (text) state.remotePanelNotice = text;
renderPanelNotice();
} catch (_) {}
}
async function _fcc() {
try {
const data = await _crq("/api/hbzj/client-config", "GET", null, {
Accept: "application/json",
});
if (data?.panelNoticePath) {
const p = String(data.panelNoticePath).trim();
if (p) state.panelNoticePath = p.startsWith("/") ? p : `/${p}`;
}
if (data?.freeChapterLimit != null) {
state.freeChapterLimit = Number(data.freeChapterLimit) || DEFAULT_FREE_CHAPTER_LIMIT;
}
if (data?.proBuyUrl) {
const buy = String(data.proBuyUrl).trim();
if (buy) state.proBuyUrl = buy;
}
state.cloudApiBase = resolvePinnedCloudApiBase();
} catch (_) {}
await _fpn();
}
function formatCloudAuthError(err) {
const em = String(err?.message || err || "");
if (/\u672a\u90e8\u7f72|404|cloud_404/i.test(em)) return "\u4e91\u7aef\u670d\u52a1\u6682\u4e0d\u53ef\u7528";
if (/invalid token/i.test(em)) return "Token \u65e0\u6548";
if (/revoked/i.test(em)) return "Token \u5df2\u88ab\u7981\u7528";
if (/expired/i.test(em)) return "Token \u5df2\u8fc7\u671f";
return em || "\u6821\u9a8c\u5931\u8d25";
}
function _acd(data) {
if (!data || typeof data !== "object") return;
if (data.lease) state.cloudLease = String(data.lease);
const exp = Number(data.exp ?? data.expire_at ?? data.expires_at ?? data.lease_exp ?? 0);
if (Number.isFinite(exp) && exp > 0) state.cloudLeaseExp = Math.floor(exp);
if (data.tier) state.cloudTier = String(data.tier).trim().toLowerCase();
const lim = data.free_chapter_limit ?? data.freeChapterLimit ?? data.free_video_limit;
const used = data.free_used_chapters ?? data.freeUsedChapters ?? data.free_used_videos;
if (lim != null) state.freeChapterLimit = Number(lim) || DEFAULT_FREE_CHAPTER_LIMIT;
if (used != null) state.freeUsedChapters = Number(used) || 0;
const proExp = Number(data.pro_expires_at ?? data.proExpiresAt ?? data.pro_expire_at ?? 0);
if (Number.isFinite(proExp) && proExp > 0) state.cloudProExpireAt = Math.floor(proExp);
if (!state.cloudTier) state.cloudTier = state.cloudToken ? "pro" : "free";
writeCloudLeaseCache(state.cloudLease, state.cloudLeaseExp, {
tier: state.cloudTier,
proExpireAt: state.cloudProExpireAt,
});
}
async function _ecl(force) {
const now = Math.floor(Date.now() / 1000);
if (!force && state.cloudLease && state.cloudLeaseExp - now > 60) return true;
const cached = readCloudLeaseCache();
if (!force && cached && cached.exp - now > 60) {
state.cloudLease = cached.lease;
state.cloudLeaseExp = cached.exp;
state.cloudTier = cached.tier || state.cloudTier;
state.cloudProExpireAt = cached.proExpireAt || state.cloudProExpireAt;
updateCloudPanelUI();
return true;
}
const luid = getLearningUserId();
if (!luid) {
state.cloudTier = state.cloudToken ? "pro" : "free";
updateCloudPanelUI();
return false;
}
try {
const u = state.userProfile || {};
const data = await _crq("/api/hbzj/lease", "POST", {
learning_user_id: luid,
lease: state.cloudLease || undefined,
site: "hbzj",
script_version: SCRIPT_VERSION,
name: String(u.name || "").trim(),
realName: String(u.name || "").trim(),
userName: String(u.loginId || "").trim(),
phone: String(u.phone || "").trim(),
});
state.cloudRevoked = false;
_acd(data);
return !!state.cloudLease;
} catch (e) {
const em = String(e?.message || e);
if (/revoked|invalid token|expired/i.test(em)) {
state.cloudRevoked = true;
state.cloudTier = "revoked";
state.cloudLease = "";
writeCloudLeaseCache("", 0);
updateCloudPanelUI();
throw e;
}
if (em === "cloud_404") {
state.panelHint = "";
updateCloudPanelUI();
return false;
}
if (cached && cached.lease && cached.exp - now > -300 && isCloudProTier()) {
state.cloudLease = cached.lease;
state.cloudLeaseExp = cached.exp;
state.cloudTier = cached.tier || "pro";
updateCloudPanelUI();
return true;
}
trace(`\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff1a${em || "\u7f51\u7edc\u9519\u8bef"}`);
updateCloudPanelUI();
return false;
}
}
async function _sct(token) {
const tk = String(token || "").trim();
state.cloudToken = tk;
if (tk) pageStorage().setItem(CLOUD_TOKEN_KEY, tk);
else pageStorage().removeItem(CLOUD_TOKEN_KEY);
if (!tk) {
state.cloudTier = "free";
state.cloudProExpireAt = 0;
state.cloudRevoked = false;
try {
await _ecl(true);
} catch (_) {}
updateCloudPanelUI();
trace("\u5df2\u6e05\u9664\u6388\u6743\u7801");
return true;
}
try {
await _crq("/api/hbzj/license/verify", "GET", null);
await _ecl(true);
if (!isCloudProTier()) state.cloudTier = "pro";
state.cloudRevoked = false;
updateCloudPanelUI();
trace("\u4f1a\u5458\u72b6\u6001\u5df2\u66f4\u65b0");
return true;
} catch (e) {
const tip = formatCloudAuthError(e);
if (state._lastProErr !== tip) {
state._lastProErr = tip;
trace(`\u6388\u6743\u6821\u9a8c\u5931\u8d25\uff1a${tip}`);
}
updateCloudPanelUI();
return false;
}
}
async function _acp() {
try {
await _ecl(true);
} catch (_) {
return false;
}
return !!state.cloudLease;
}
async function _rcq() {
try {
const data = await _crq("/api/hbzj/study/consume", "POST", {
lease: state.cloudLease || undefined,
learning_user_id: getLearningUserId(),
kind: "chapter",
});
if (data && data.ok === false) {
throw new Error(String(data.message || "quota_denied"));
}
_acd(data);
return true;
} catch (e) {
const em = String(e?.message || e);
if (/free_quota_exhausted|quota|403|exam_requires_pro/i.test(em)) {
openProModal();
} else {
trace(`\u4e91\u7aef\uff1a${em || "\u7f51\u7edc\u9519\u8bef"}`);
}
return false;
}
}
async function _ate() {
try {
await _ecl(true);
} catch (_) {
return false;
}
if (!state.cloudLease) return false;
try {
const data = await _crq("/api/hbzj/study/can-exam", "POST", {
lease: state.cloudLease || undefined,
learning_user_id: getLearningUserId(),
});
if (data && data.ok !== false && data.success !== false) return true;
} catch (e) {
trace(`\u8003\u8bd5\u6821\u9a8c\uff1a${e?.message || e}`);
}
openProModal();
return false;
}
function openProBuyPage() {
try {
PAGE.open(state.proBuyUrl || PRO_BUY_URL, "_blank");
} catch (_) {
location.href = state.proBuyUrl || PRO_BUY_URL;
}
}
function closeProModal() {
document.getElementById("hbzj-pro-modal")?.remove();
}
function openProModal() {
closeProModal();
const modal = document.createElement("div");
modal.id = "hbzj-pro-modal";
modal.innerHTML = `
\u5f00\u901a\u4f1a\u5458
\u5982\u9700\u7ee7\u7eed\u4f7f\u7528\u5168\u90e8\u5b66\u4e60\u4e0e\u8003\u8bd5\u529f\u80fd\uff0c\u8bf7\u5f00\u901a\u4f1a\u5458
\u4f1a\u5458\u6743\u76ca
- \u5b8c\u6574\u8bfe\u7a0b\u5b66\u4e60
- \u81ea\u52a8\u8003\u8bd5
- \u6709\u6548\u671f ${PRO_DAYS} \u5929
\u5f00\u901a\u65b9\u5f0f
\u524d\u5f80\u8d2d\u4e70\u9875\u83b7\u53d6\u6388\u6743\u7801\uff0c\u5728\u300c\u8bbe\u7f6e\u300d\u4e2d\u7c98\u8d34\u5e76\u4fdd\u5b58\u3002
`;
document.body.appendChild(modal);
modal.querySelector("#hbzj-pro-buy")?.addEventListener("click", () => openProBuyPage());
modal.querySelector("#hbzj-pro-close")?.addEventListener("click", () => closeProModal());
modal.addEventListener("click", (e) => {
if (e.target === modal) closeProModal();
});
}
function showToast(msg) {
let el = document.getElementById("hbzj-toast");
if (!el) {
el = document.createElement("div");
el.id = "hbzj-toast";
document.body.appendChild(el);
}
el.textContent = String(msg || "");
el.classList.add("show");
clearTimeout(showToast._t);
showToast._t = setTimeout(() => el.classList.remove("show"), 2600);
}
function loadQueue() {
return readJson(QUEUE_KEY, []);
}
function saveQueue(list) {
writeJson(QUEUE_KEY, Array.isArray(list) ? list : []);
}
function isEbookVideo(v) {
return Number(v?.type) === 2;
}
function videoTotalSeconds(v) {
const n = Number(v?.duration || v?.videoDuration || v?.totalDuration || 0);
return Math.max(1, Math.ceil(n));
}
function videoStudySeconds(v) {
const total = videoTotalSeconds(v);
const watched = Math.max(0, Number(v?.watchTimeLength || 0));
const rate = Number(v?.learnSpeed || 0);
const fromRate = rate > 0 ? Math.floor((total * rate) / 100) : 0;
return Math.max(watched, fromRate);
}
function studiedSecondsFromRate(total, rate) {
const t = Math.max(1, Number(total) || 1);
const r = Math.max(0, Math.min(100, Number(rate) || 0));
return Math.min(t, Math.floor((t * r) / 100));
}
function _ppp(projectId, videoId, videoName, rate, total) {
const pid = String(projectId || "");
const vid = String(videoId || "");
const studied = studiedSecondsFromRate(total, rate);
const done = Number(rate) >= 100;
let hit = false;
for (const c of state.chapterPreview) {
if (String(c.projectId) !== pid) continue;
const same =
(vid && String(c.videoId || "") === vid) ||
(!vid && videoName && String(c.name || "") === String(videoName));
if (!same) continue;
c.studied = studied;
c.total = Math.max(1, Number(total) || c.total || 1);
c.done = done;
hit = true;
}
if (!hit && pid && videoName) {
const p = state.panelProjects.find((x) => String(x.projectId) === pid);
state.chapterPreview.push({
projectId: pid,
projectName: p?.projectName || pid,
videoId: vid,
name: videoName,
studied,
total: Math.max(1, Number(total) || 1),
done,
kind: "ware",
});
}
_rsp(pid);
}
function _rsp(projectId) {
const pid = String(projectId || "");
const p = state.panelProjects.find((x) => String(x.projectId) === pid);
if (!p) return;
const items = state.chapterPreview.filter(
(c) => String(c.projectId) === pid && c.kind !== "exam" && Math.max(0, Number(c.total) || 0) > 0
);
if (!items.length) return;
let studied = 0;
let total = 0;
for (const c of items) {
studied += Math.max(0, Number(c.studied) || 0);
total += Math.max(0, Number(c.total) || 0);
}
if (total <= 0) return;
const progress = Math.min(100, (studied / total) * 100);
p.studyProgress = Number(progress.toFixed(1));
if (progress >= 100) {
p.videoFinishedStatus = true;
if (!isProjectFullyFinished(p)) p.studyLabel = "\u5df2\u5b66\u5b8c";
} else if (progress > 0) {
p.studyLabel = `\u5b66\u4e60\u4e2d ${p.studyProgress}%`;
} else {
p.studyLabel = "\u672a\u5b66\u4e60";
}
syncProjectFinishedFlags(p);
}
function isVideoDone(v) {
if (!v) return true;
if (isEbookVideo(v)) return true;
const speed = Number(v.learnSpeed || 0);
return speed >= 100;
}
function videoDisplayStudiedSeconds(v) {
const total = videoTotalSeconds(v);
const raw = videoStudySeconds(v);
if (isVideoDone(v)) return Math.max(raw, total);
return Math.min(raw, total);
}
function formatStudyProgressLog(name, rate, total) {
const studiedSec = studiedSecondsFromRate(total, rate);
return `\u5b66\u4e60\uff1a${name}\u5df2\u5b66${formatMinutes(studiedSec)}/${formatMinutes(total)}`;
}
function formatMinutes(sec) {
const s = Math.max(0, Number(sec) || 0);
const m = s / 60;
if (m >= 100) return `${Math.round(m)} \u5206\u949f`;
if (m >= 10) return `${m.toFixed(0)} \u5206\u949f`;
return `${m.toFixed(1)} \u5206\u949f`;
}
function wareProgressPct(studied, total, done) {
if (done) return 100;
const t = Math.max(1, Number(total) || 1);
return Math.min(100, Math.round((Math.max(0, Number(studied) || 0) / t) * 100));
}
function videoNameOf(v) {
const video = String(v?.videoName || v?.name || v?.title || "").trim();
const course = String(v?.courseName || v?.myClassCourseName || "").trim();
if (video && course && !video.includes(course)) return `${course} · ${video}`;
return video || course || String(v?.myClassCourseVideoId || "\u89c6\u9891");
}
function videoSourceOf(v) {
return String(v?.baiJiaYunVideoSource || v?.ccVideoSource || v?.videoSource || "").trim();
}
function flattenVideosFromBooks(booksData, myClassId) {
let courseData = [];
if (Array.isArray(booksData)) courseData = booksData;
else if (Array.isArray(booksData?.myClassCourseRPList)) courseData = booksData.myClassCourseRPList;
else if (Array.isArray(booksData?.courseData)) courseData = booksData.courseData;
else if (Array.isArray(booksData?.data?.myClassCourseRPList)) courseData = booksData.data.myClassCourseRPList;
else if (Array.isArray(booksData?.data?.courseData)) courseData = booksData.data.courseData;
else if (Array.isArray(booksData?.data)) courseData = booksData.data;
else courseData = pickList(booksData);
const out = [];
for (const course of courseData || []) {
const courseName = String(course?.courseName || course?.name || course?.title || "").trim();
const vids = Array.isArray(course?.videoRPs) ? course.videoRPs : [];
for (const v of vids) {
out.push({
...v,
myClassId: myClassId || v.myClassId,
myClassCourseId: v.myClassCourseId || course.myClassCourseId || course.id,
courseType: course.type,
courseName: courseName || v.courseName || "",
});
}
}
return out;
}
function isVideoFinished(p) {
if (!p) return false;
if (p.videoFinishedStatus === true) return true;
if (Number(p.studyStatus) === 2) return true;
if (Number(p.studyProgress) >= 100) return true;
return false;
}
function isProjectFullyFinished(p) {
if (!p) return false;
if (!isVideoFinished(p) && p.listSource !== "completed" && !p.projectFinishedStatus) return false;
if (p.examRequired && !p.examDone) return false;
if (String(p.examLabel || "").includes("\u5f85\u8003\u8bd5")) return false;
if (p._examEnriched) {
return !!(p.examDone || String(p.examLabel || "").includes("\u5df2\u901a\u8fc7") || p.examLabel === "\u65e0\u8bd5\u5377" || p.examLabel === "\u65e0\u5f85\u8003");
}
if (isVideoFinished(p) || p.listSource === "completed") return false;
return false;
}
function isProjectSelectable(p) {
return p && !isProjectFullyFinished(p);
}
function projectListBadge(p) {
if (isProjectFullyFinished(p)) {
return `\u5df2\u5b8c\u6210`;
}
if (
isVideoFinished(p) &&
(p.examRequired || String(p.examLabel || "").includes("\u5f85\u8003\u8bd5"))
) {
return `\u5f85\u8003\u8bd5`;
}
return `\u6b63\u5728\u5b66`;
}
function syncProjectFinishedFlags(p) {
if (!p) return;
if (isVideoFinished(p)) {
p.videoFinishedStatus = true;
if (!String(p.studyLabel || "").includes("\u5b66\u4e60\u4e2d") && p.studyLabel !== "\u672a\u5b66\u4e60") {
p.studyLabel = "\u5df2\u5b66\u5b8c";
} else if (Number(p.studyProgress) >= 100) {
p.studyLabel = "\u5df2\u5b66\u5b8c";
}
}
if (isProjectFullyFinished(p)) {
p.listSource = "completed";
p.sourceLabel = "\u5df2\u5b8c\u6210";
p.studyLabel = "\u5df2\u5b8c\u6210";
p.projectFinishedStatus = true;
} else if (isVideoFinished(p) && (p.examRequired || String(p.examLabel || "").includes("\u5f85\u8003\u8bd5"))) {
p.projectFinishedStatus = false;
p.sourceLabel = "\u5f85\u8003\u8bd5";
p.listSource = "learning";
}
}
function classifyStudyRow(row) {
const status = Number(row?.studyStatus);
const speed = Number(
row?.studySpeed ?? row?.studyProgress ?? row?.learnSpeed ?? row?.percent ?? row?.classEntity?.studySpeed ?? 0
);
if (status === 2 || speed >= 100) return "completed";
return "learning";
}
function mapProjectRow(row) {
const entity = row?.classEntity && typeof row.classEntity === "object" ? row.classEntity : {};
const info = row?.classInfo && typeof row.classInfo === "object" ? row.classInfo : {};
const src = classifyStudyRow(row);
const progress = Number(
row.studySpeed ?? row.studyProgress ?? row.learnSpeed ?? row.percent ?? entity.studySpeed ?? info.studySpeed ?? 0
);
const myClassId = String(row.myClassId || row.id || entity.myClassId || info.myClassId || "");
const classId = String(row.classId || entity.classId || info.classId || myClassId);
const finishedHint = src === "completed" || progress >= 100;
const name =
row.title ||
row.className ||
entity.className ||
info.className ||
row.myClassName ||
row.name ||
"";
return {
projectId: myClassId,
projectName: name || `\u73ed\u7ea7${myClassId}`,
creditType:
row.subjectName ||
row.classTypeName ||
row.creditType ||
entity.subjectName ||
info.subjectName ||
"",
projectCredit:
row.classHour ?? row.credit ?? row.projectCredit ?? row.period ?? entity.classHour ?? info.classHour,
studyProgress: progress,
studyStatus: Number(row.studyStatus || 0),
studyTime: Number(row.studyTime || 0),
classId,
listSource: src,
sourceLabel: src === "completed" ? "\u5df2\u5b8c\u6210" : "\u6b63\u5728\u5b66",
studyLabel: finishedHint
? "\u5df2\u5b66\u5b8c"
: progress > 0
? `\u5b66\u4e60\u4e2d ${Number(progress.toFixed(1))}%`
: "\u672a\u5b66\u4e60",
examId: "",
examName: "",
examDone: false,
examRequired: false,
examScore: null,
examPassLine: 60,
examLabel: "\u8003\u8bd5\u5f85\u67e5",
videoFinishedStatus: finishedHint,
projectFinishedStatus: false,
raw: row,
};
}
async function ensureUserProfile(force) {
if (!force && state.userProfile?.accountId) return state.userProfile;
const cms = readCmsAccountInfo();
if (cms?.accountId) {
rememberAccountId(cms.accountId);
state.userProfile = normalizeUserProfile(cms);
if (!force) return state.userProfile;
}
try {
const j = await apiGet(PATH_ACCOUNT_INFO);
const info = j.data?.data || j.data || null;
if (info?.accountId) rememberAccountId(info.accountId);
state.userProfile = normalizeUserProfile(info || cms || {});
if (state.userProfile?.accountId) {
writeAuthCache({ accountId: state.userProfile.accountId });
}
} catch (e) {
if (cms) state.userProfile = normalizeUserProfile(cms);
else throw e;
}
return state.userProfile;
}
async function fetchYears() {
try {
let j;
try {
j = await apiGet(PATH_STUDY_YEARS);
} catch (_) {
j = await apiGet(PATH_CLASS_YEARS);
}
const years = extractYears(j.data);
if (years.length) {
state.yearOptions = years;
if (!state.yearOptions.includes(Number(state.studyYear))) {
state.studyYear = state.yearOptions[0];
pageStorage().setItem(YEAR_KEY, String(state.studyYear));
}
}
} catch (e) {
trace(`years: ${e.message || e}`);
}
}
async function fetchStudyListPage(pageNo, studyStatus) {
const qs = toQuery({
searchYear: state.studyYear || "",
studyStatus: studyStatus == null ? "" : studyStatus,
mylearnTaskId: "",
attribute: "",
orderBy: 2,
mathRandom: Math.random(),
pageNo: pageNo || 1,
subject: "",
classType: 0,
pageSize: 50,
});
const j = await apiGet(`${PATH_STUDY_LIST}?${qs}`);
return pickList(j.data);
}
async function fetchAllPanelProjects() {
const allRows = [];
try {
const rows = await fetchStudyListPage(1, "");
allRows.push(...rows);
} catch (e) {
trace(`\u5b66\u4e60\u5217\u8868\u5931\u8d25: ${e.message || e}`);
try {
const a = await fetchStudyListPage(1, "");
allRows.push(...a);
} catch (_) {}
}
if (!allRows.length) {
try {
const learning = await fetchStudyListPage(1, 1);
const done = await fetchStudyListPage(1, 2);
allRows.push(...learning, ...done);
} catch (_) {}
}
const mapped = allRows.map((row) => mapProjectRow(row));
const learning = mapped.filter((p) => p.listSource === "learning");
const completed = mapped.filter((p) => p.listSource === "completed");
state._capturedLists = { learning, completed };
const byId = new Map();
for (const p of learning) byId.set(p.projectId, p);
for (const p of completed) byId.set(p.projectId, p);
const merged = Array.from(byId.values());
for (const p of merged) syncProjectFinishedFlags(p);
return merged;
}
async function fetchClassDetail(myClassId) {
const j = await apiGet(`${PATH_MY_CLASS}?${toQuery({ myClassId })}`);
const raw = j.data?.data || j.data || null;
if (!raw || typeof raw !== "object") return raw;
if (raw.classInfo && typeof raw.classInfo === "object") {
if (!raw.classId && raw.classInfo.classId) raw.classId = raw.classInfo.classId;
if (!raw.className && raw.classInfo.className) raw.className = raw.classInfo.className;
}
return raw;
}
async function fetchClassBooks(myClassId) {
const j = await apiGet(`${PATH_MY_CLASS}?${toQuery({ myClassId })}`);
const raw = j.data?.data || j.data || null;
if (!raw || typeof raw !== "object") return { courseData: [] };
if (Array.isArray(raw.myClassCourseRPList) && raw.myClassCourseRPList.length) {
return raw;
}
try {
const bj = await apiGet(`${PATH_MY_CLASS_BOOKS}?${toQuery({ myClassId })}`);
const bro = bj.data?.data != null ? bj.data.data : bj.data;
if (Array.isArray(bro)) return { courseData: bro };
if (bro && typeof bro === "object") return bro;
} catch (_) {}
return raw;
}
async function enrichProjectNames(projects, limit) {
const list = (projects || []).slice(0, Math.max(1, limit || 20));
for (const p of list) {
if (!p || !p.projectId) continue;
if (p.projectName && !/^\u73ed\u7ea7[0-9a-f-]{30,}$/i.test(p.projectName)) continue;
try {
const detail = await fetchClassDetail(p.projectId);
const name =
detail?.classInfo?.className ||
detail?.className ||
detail?.classInfo?.title ||
"";
if (name) {
p.projectName = name;
if (detail?.classId) p.classId = String(detail.classId);
}
} catch (_) {}
}
}
function buildWatchInfo(video, playduration, pid, timestamp) {
const vid = videoSourceOf(video);
return JSON.stringify({
vid,
pid: pid || `BAIJIAYUN_${uuidv4()}`,
playduration: Math.max(0, Math.floor(Number(playduration) || 0)),
timestamp: Number(timestamp) > 0 ? Math.floor(Number(timestamp)) : Date.now(),
});
}
function formatVideoRate(rate) {
const n = Number(rate);
if (!Number.isFinite(n)) return "0";
if (n >= 100) return "100";
return n.toFixed(1);
}
function initialPlaySeconds(video, total) {
const watched = Math.floor(Number(video?.watchTimeLength) || 0);
if (watched > 0) return Math.min(watched, Math.max(0, total - 1));
const speed = Number(video?.learnSpeed || 0);
if (speed > 0) return Math.min(Math.floor((total * speed) / 100), Math.max(0, total - 1));
return 1;
}
function videoInnerResult(un) {
const attr = un?.data;
if (!attr) return null;
if (attr.data && typeof attr.data === "object") return attr.data;
return attr;
}
async function _psw(myClassId, video, watchInfo) {
return apiPostForm(PATH_VIDEO_SV, {
myClassId,
myClassCourseId: video.myClassCourseId,
myClassCourseVideoId: video.myClassCourseVideoId,
watchInfo,
});
}
async function _pcw(myClassId, video, watchInfo) {
return apiPostForm(PATH_VIDEO_CV, {
myClassId,
myClassCourseId: video.myClassCourseId,
myClassCourseVideoId: video.myClassCourseVideoId,
watchInfo,
isCalculateclassHourFlag: true,
});
}
async function postUpdateLastVideo(classId, myClassId, video) {
try {
await apiPostForm(PATH_UPDATE_LAST_VIDEO, {
classId: classId || myClassId,
myClassId,
myClassCourseId: video.myClassCourseId,
myClassCourseVideoId: video.myClassCourseVideoId,
});
} catch (e) {
trace(`update-last-video: ${e.message || e}`);
}
}
function paperNeedsExam(row) {
if (!row) return false;
if (Number(row.isPass) === 1 || row.passFlag === true || row.pass === true) return false;
const pass = Number(row.passScore ?? row.passMark ?? row.passLine ?? 60);
const score = Number(row.score ?? row.myHighestScore ?? row.highestScore ?? NaN);
if (Number.isFinite(score) && score >= pass) return false;
const studySpeed = Number(row.studySpeed ?? 0);
if (studySpeed < 100) return false;
const statusName = String(row.statusName || "");
if (statusName.includes("\u672a\u5b8c\u6210")) return false;
if (statusName.includes("\u5df2\u901a\u8fc7")) return false;
return true;
}
function paperUnfinished(row) {
return paperNeedsExam(row);
}
async function fetchPaperList(cmsType) {
const qs = toQuery({ pageNo: 1, pageSize: 20, cmsType: cmsType == null ? 9 : cmsType });
const j = await apiGet(`${PATH_EXAM_LIST}?${qs}`);
return pickList(j.data);
}
async function checkExamStatus(myExamId) {
if (!myExamId) return;
try {
await apiGet(`${PATH_EXAM_CHECK_STATUS}?${toQuery({ myExamId })}`);
} catch (e) {
trace(`check-my-exam-status\uff1a${e.message || e}`);
}
}
async function saveExamRecord(paperRow) {
const qs = toQuery({
myExamId: paperRow.myExamId,
isMakeUp: paperRow.isMakeUpStatus ?? paperRow.isMakeUp ?? 0,
examType: paperRow.examType ?? paperRow.type ?? 0,
paperId: paperRow.paperId,
paperSource: paperRow.paperSource ?? 1,
});
const j = await apiGet(`${PATH_EXAM_SAVE_RECORD}?${qs}`);
const rid = j.data?.data ?? j.data;
return rid ? String(rid) : "";
}
async function startPaper(paperId, myExamRecordId) {
const qs = toQuery({
paperId: paperId || "",
myExamRecordId: myExamRecordId || "",
});
const j = await apiGet(`${PATH_EXAM_START}?${qs}`);
return j.data?.data || j.data || null;
}
function isStandardOption(opt) {
if (!opt) return false;
const v = opt.standardAnswer;
return v === true || v === 1 || v === "1" || Number(opt.isStandardAnswer) === 1 || opt.isCorrect === true;
}
function buildExamAnswerList(paperDetail) {
const stems = paperDetail?.questionStemRPS || paperDetail?.listQuestionStemRP || [];
if (!Array.isArray(stems) || !stems.length) return null;
const answerList = [];
for (const stem of stems) {
const questions = stem.listPaperQuestionRP || stem.paperQuestionRPS || [];
if (!Array.isArray(questions) || !questions.length) return null;
for (const q of questions) {
const qid = q.paperQuestionId || q.questionId || q.id;
if (!qid) return null;
const opts = q.paperOptionRPS || q.paperOptionList || q.options || [];
const std = (Array.isArray(opts) ? opts : []).filter(isStandardOption);
if (!std.length) return null;
answerList.push({
paperQuestionId: String(qid),
answers: std.map((o) => ({ paperOptionId: String(o.paperOptionId || o.id) })),
});
}
}
return answerList.length ? answerList : null;
}
async function submitPaper(payload) {
const body = {
myExamId: payload.myExamId,
isMakeUp: payload.isMakeUp != null ? payload.isMakeUp : false,
myExamRecordId: payload.myExamRecordId,
answerList:
typeof payload.answerList === "string"
? payload.answerList
: JSON.stringify(payload.answerList || []),
};
try {
return await apiPostForm(PATH_EXAM_SUBMIT, body);
} catch (_) {
return await apiPostJson(PATH_EXAM_SUBMIT, body);
}
}
async function _roe(paperRow) {
const myExamId = paperRow.myExamId;
const paperId = paperRow.paperId;
const name = paperRow.paperName || paperRow.className || paperId;
if (!myExamId || !paperId) throw new Error("\u7f3a\u5c11 myExamId \u6216 paperId");
await checkExamStatus(myExamId);
const myExamRecordId = await saveExamRecord(paperRow);
if (!myExamRecordId) throw new Error("save-my-exam-record \u672a\u8fd4\u56de recordId");
const detail = await startPaper(paperId, myExamRecordId);
if (!detail) throw new Error("start-do-paper \u65e0\u9898\u76ee");
const answerList = buildExamAnswerList(detail);
if (!answerList) throw new Error("\u9898\u76ee\u65e0 standardAnswer\uff0c\u5df2\u8df3\u8fc7");
await submitPaper({
myExamId,
isMakeUp: paperRow.isMakeUpStatus ?? paperRow.isMakeUp ?? false,
myExamRecordId,
answerList,
});
log(`\u8003\u8bd5\uff1a${name} \u5df2\u63d0\u4ea4`);
return true;
}
async function _tea(project) {
if (!(await _ate())) return false;
let papers = [];
try {
papers = await fetchPaperList(9);
} catch (e) {
trace(`\u8bd5\u5377\u5217\u8868(cmsType=9)\uff1a${e.message || e}`);
}
if (!papers.length) {
try {
papers = await fetchPaperList(1);
} catch (e) {
trace(`\u8bd5\u5377\u5217\u8868(cmsType=1)\uff1a${e.message || e}`);
}
}
let pending = papers.filter(paperNeedsExam);
if (project?.projectId) {
const mine = pending.filter((p) => String(p.myClassId || "") === String(project.projectId));
if (mine.length) pending = mine;
}
if (!pending.length) {
if (project) {
const st = examLabelForProject(project, papers);
project.examRequired = st.examRequired;
project.examDone = st.examDone;
project.examLabel = st.examLabel;
project.examScore = st.examScore;
project._examEnriched = true;
syncProjectFinishedFlags(project);
}
return false;
}
if (project) {
project.examRequired = true;
project.examLabel = `\u5f85\u8003\u8bd5 ${pending.length} \u4efd`;
}
let did = false;
for (const paper of pending.slice(0, 3)) {
if (!state.enabled) break;
try {
await _roe(paper);
did = true;
if (project) {
project.examDone = true;
project.examRequired = false;
project.examLabel = "\u5df2\u63d0\u4ea4\uff0c\u5237\u65b0\u6210\u7ee9\u4e2d…";
project._examEnriched = false;
}
await sleep(1500);
try {
let papers2 = await fetchPaperList(9);
if (!papers2.length) papers2 = await fetchPaperList(1);
if (project) {
const st = examLabelForProject(project, papers2);
project.examRequired = st.examRequired;
project.examDone = st.examDone;
project.examLabel = st.examLabel;
project.examScore = st.examScore;
project._examEnriched = true;
syncProjectFinishedFlags(project);
updatePanel();
}
} catch (_) {
if (project) {
project.examLabel = "\u5df2\u63d0\u4ea4";
project.examDone = true;
project.examRequired = false;
}
}
await sleep(800);
} catch (e) {
const msg = e.message || e;
log(`\u8003\u8bd5\u300c${paper.paperName || paper.className || paper.paperId}\u300d\u5931\u8d25\uff1a${msg}`);
if (project && String(msg).includes("standardAnswer")) {
project.examLabel = "\u7f3a\u6807\u51c6\u7b54\u6848\uff0c\u5df2\u8df3\u8fc7";
}
trace(`\u8003\u8bd5\u5931\u8d25\uff1a${msg}`);
}
}
return did;
}
async function _sw(project, video) {
const myClassId = project.projectId;
const name = videoNameOf(video);
const total = videoTotalSeconds(video);
const goalSec = Math.min(total, Math.ceil(total * EFFICIENCY_JUMP_RATIO));
const vid = videoSourceOf(video);
if (!vid) {
log(`\u89c6\u9891\u300c${name}\u300d\u65e0\u89c6\u9891\u6e90\uff0c\u8df3\u8fc7`);
return;
}
if (isEbookVideo(video)) {
log(`\u300c${name}\u300d\u4e3a\u7535\u5b50\u4e66\uff0c\u8df3\u8fc7\u8ba1\u65f6`);
return;
}
state.lastUserAction = name;
updatePanel();
const pid = `BAIJIAYUN_${uuidv4()}`;
let playduration = initialPlaySeconds(video, total);
let rate = Number(video.learnSpeed || 0);
let lastReportMs = Date.now();
await postUpdateLastVideo(project.classId || myClassId, myClassId, video);
let svOk = false;
try {
const watchInfo0 = buildWatchInfo(video, playduration, pid, lastReportMs);
const svRes = await _psw(myClassId, video, watchInfo0);
const inner = videoInnerResult(svRes);
if (inner && String(inner.respCode) !== "SUCCESS" && inner.respCode != null) {
throw new Error(inner.respDesc || inner.respCode || "sv_fail");
}
svOk = true;
if (inner?.videoLearnRate != null) rate = Number(inner.videoLearnRate);
video.learnSpeed = rate;
_ppp(myClassId, video.myClassCourseVideoId, name, rate, total);
updatePanel();
log(formatStudyProgressLog(name, rate, total));
_lwh();
} catch (e) {
trace(`sv\uff1a${e.message || e}`);
await sleep(2000);
}
if (!svOk) {
log(`\u8df3\u8fc7\uff1a${name}`);
return;
}
await sleep(SV_BEFORE_CV_MS);
const needSteps = Math.ceil(Math.max(0, goalSec - playduration) / CV_STEP_SECONDS);
const maxRounds = Math.min(WATCH_MAX_ROUNDS, needSteps + 2);
for (let round = 0; round < maxRounds && state.enabled; round++) {
if (rate >= 100) break;
if (playduration >= goalSec) break;
const now = Date.now();
const elapsedSec = Math.max(1, Math.floor((now - lastReportMs) / 1000));
const step = Math.min(CV_STEP_SECONDS, elapsedSec, Math.max(1, goalSec - playduration));
playduration = Math.min(goalSec, playduration + step);
lastReportMs = now;
const watchInfo = buildWatchInfo(video, playduration, pid, lastReportMs);
let cvOk = false;
try {
const cvRes = await _pcw(myClassId, video, watchInfo);
const inner = videoInnerResult(cvRes);
if (cvRes.msg && String(cvRes.msg).includes("today class hour limit")) {
log(`\u4eca\u65e5\u5b66\u65f6\u5df2\u8fbe\u4e0a\u9650`);
break;
}
if (inner && String(inner.respCode) === "INVALID_PARAM") {
trace(`cv\uff1a${inner.respDesc || inner.respCode}`);
playduration = Math.max(1, playduration - step);
await sleep(CV_INTERVAL_MS);
continue;
}
if (inner && String(inner.respCode) === "SUCCESS") {
if (inner.videoLearnRate != null) rate = Number(inner.videoLearnRate);
video.learnSpeed = rate;
_ppp(myClassId, video.myClassCourseVideoId, name, rate, total);
updatePanel();
log(formatStudyProgressLog(name, rate, total));
if (rate >= 100 || playduration >= goalSec) break;
} else if (inner && inner.respCode != null) {
throw new Error(inner.respDesc || inner.respCode);
}
} catch (e) {
trace(`cv\uff1a${e.message || e}`);
playduration = Math.max(1, playduration - step);
await sleep(CV_INTERVAL_MS);
continue;
}
if (rate >= 100 || playduration >= goalSec) break;
await sleep(CV_INTERVAL_MS);
}
video.learnSpeed = Math.max(Number(video.learnSpeed || 0), rate);
if (rate >= 100 || playduration >= goalSec) video.learnSpeed = 100;
_ppp(myClassId, video.myClassCourseVideoId, name, video.learnSpeed, total);
log(formatStudyProgressLog(name, video.learnSpeed, total).replace(/^\u5b66\u4e60\uff1a/, "\u5b8c\u6210\uff1a"));
}
async function _sp(project) {
const projectId = project.projectId;
state.lastUserAction = project.projectName;
updatePanel();
log(`\u5f00\u59cb\uff1a${project.projectName}`);
let detail = null;
try {
detail = await fetchClassDetail(projectId);
if (detail?.classId) project.classId = String(detail.classId);
if (detail?.studyStatus != null) project.studyStatus = Number(detail.studyStatus);
if (detail?.studySpeed != null) project.studyProgress = Number(detail.studySpeed);
} catch (e) {
trace(`\u73ed\u7ea7\u8be6\u60c5\uff1a${e.message || e}`);
}
let books = null;
try {
books = await fetchClassBooks(projectId);
} catch (e) {
throw new Error(`\u8bfe\u4ef6\u5217\u8868\u5931\u8d25\uff1a${e.message || e}`);
}
const videos = flattenVideosFromBooks(books, projectId).filter((v) => !isEbookVideo(v));
const pending = videos.filter((v) => !isVideoDone(v));
if (pending.length) {
if (!(await _acp())) {
state.enabled = false;
return;
}
for (const video of pending) {
if (!state.enabled) return;
if (!(await _acp())) {
state.enabled = false;
return;
}
if (!(await _rcq())) {
state.enabled = false;
return;
}
await _sw(project, video);
updateCloudPanelUI();
}
} else {
log(`${project.projectName} \u89c6\u9891\u5df2\u5168\u90e8\u8fbe\u6807`);
}
try {
const books2 = await fetchClassBooks(projectId);
const vids2 = flattenVideosFromBooks(books2, projectId).filter((v) => !isEbookVideo(v));
const allDone = vids2.length ? vids2.every(isVideoDone) : pending.length === 0;
project.videoFinishedStatus = allDone;
if (allDone) {
project.studyProgress = Math.max(Number(project.studyProgress || 0), 100);
}
} catch (_) {}
if (state.enabled) {
try {
for (let i = 0; i < EXAM_PASS_RETRY && state.enabled; i++) {
const did = await _tea(project);
if (!did) break;
await sleep(800);
}
} catch (e) {
trace(`\u8003\u8bd5\u6d41\u7a0b\uff1a${e.message || e}`);
}
}
if (project.videoFinishedStatus && (!project.examRequired || project.examDone)) {
project.projectFinishedStatus = true;
project.listSource = "completed";
}
syncProjectFinishedFlags(project);
await _rcp();
updatePanel();
}
async function courseHasPendingWork(project) {
try {
const books = await fetchClassBooks(project.projectId);
const videos = flattenVideosFromBooks(books, project.projectId).filter((v) => !isEbookVideo(v));
if (videos.some((v) => !isVideoDone(v))) return true;
try {
const papers = await fetchPaperList(9);
if (papers.some(paperUnfinished)) return true;
} catch (_) {}
return false;
} catch (_) {
return true;
}
}
async function _rse() {
if (state.engineRunning) return;
state.engineRunning = true;
try {
while (state.enabled) {
const queue = loadQueue().filter(Boolean);
if (!queue.length) {
state.enabled = false;
break;
}
let progressed = false;
for (const projectId of queue) {
if (!state.enabled) break;
const project = state.panelProjects.find((p) => p.projectId === projectId);
if (!project) continue;
if (isProjectFullyFinished(project)) continue;
const hasWork = await courseHasPendingWork(project);
if (!hasWork) {
project.projectFinishedStatus = true;
syncProjectFinishedFlags(project);
continue;
}
if (!(await _acp())) {
state.enabled = false;
break;
}
progressed = true;
try {
await _sp(project);
} catch (e) {
trace(`${project.projectName} \u5f02\u5e38\uff1a${e.message || e}`);
await sleep(3000);
}
}
if (!progressed) {
state.enabled = false;
break;
}
await sleep(ENGINE_TICK_MS);
}
} finally {
state.engineRunning = false;
state.lastUserAction = "";
updatePanel();
if (!state.enabled) log("\u5df2\u505c\u6b62");
}
}
function readPanelPos() {
return readJson(PANEL_POS_KEY, null);
}
function writePanelPos(pos) {
writeJson(PANEL_POS_KEY, pos);
}
function readPanelCollapsed() {
return pageStorage().getItem(PANEL_COLLAPSED_KEY) === "1";
}
function writePanelCollapsed(v) {
pageStorage().setItem(PANEL_COLLAPSED_KEY, v ? "1" : "0");
}
function applyPanelCollapsed(panel, collapsed) {
const body = panel.querySelector("#hbzj-panel-body");
const footer = panel.querySelector("#hbzj-panel-footer");
const extra = panel.querySelector(".hbzj-footer-extra");
const btnMin = panel.querySelector("#hbzj-btn-min");
const btnMax = panel.querySelector("#hbzj-btn-max");
if (collapsed) {
if (body) body.style.display = "none";
if (footer) footer.style.display = "none";
if (extra) extra.style.display = "none";
if (btnMin) btnMin.style.display = "none";
if (btnMax) btnMax.style.display = "";
} else {
if (body) body.style.display = "";
if (footer) footer.style.display = "";
if (extra) extra.style.display = "";
if (btnMin) btnMin.style.display = "";
if (btnMax) btnMax.style.display = "none";
}
}
function enablePanelDrag(panel) {
const hd = panel.querySelector("#hbzj-panel-header");
if (!hd) return;
let ox = 0;
let oy = 0;
let dragging = false;
hd.addEventListener("mousedown", (e) => {
if (e.target.closest("button")) return;
dragging = true;
const rect = panel.getBoundingClientRect();
ox = e.clientX - rect.left;
oy = e.clientY - rect.top;
e.preventDefault();
});
window.addEventListener("mousemove", (e) => {
if (!dragging) return;
const left = Math.max(0, e.clientX - ox);
const top = Math.max(0, e.clientY - oy);
panel.style.left = `${left}px`;
panel.style.top = `${top}px`;
panel.style.right = "auto";
});
window.addEventListener("mouseup", () => {
if (!dragging) return;
dragging = false;
writePanelPos({
left: parseInt(panel.style.left || "0", 10),
top: parseInt(panel.style.top || "0", 10),
});
});
}
function switchPanelTab(tab) {
document.querySelectorAll("#hbzj-auto-panel .hbzj-tab-btn").forEach((b) => {
b.classList.toggle("active", b.dataset.tab === tab);
});
document.querySelectorAll("#hbzj-auto-panel .hbzj-pane").forEach((p) => {
p.classList.toggle("active", p.dataset.pane === tab);
});
if (tab === "chapter" && loadQueue().length && !state.chapterPreview.length && !state.chapterPreviewLoading) {
state.chapterPreviewLoading = true;
renderChapterPreview();
void _rcp()
.catch((e) => trace(`\u7ae0\u8282\u9884\u89c8\u5931\u8d25\uff1a${e.message || e}`))
.finally(() => {
state.chapterPreviewLoading = false;
updatePanel();
});
}
}
function updateCloudPanelUI() {
const tierEl = document.getElementById("hbzj-cloud-tier");
const freeEl = document.getElementById("hbzj-cloud-free");
const freeLabelEl = document.getElementById("hbzj-cloud-free-label");
const freeRow = document.getElementById("hbzj-cloud-free-row");
const tokenInput = document.getElementById("hbzj-cloud-token");
if (tokenInput) tokenInput.disabled = false;
if (tierEl) {
tierEl.textContent = formatCloudTierText(state.cloudTier);
tierEl.style.color =
state.cloudRevoked || String(state.cloudTier).toLowerCase() === "revoked" ? "#dc2626" : "#0f172a";
}
if (state.cloudRevoked) {
if (freeRow) freeRow.style.display = "none";
} else if (isCloudProTier()) {
if (freeRow) freeRow.style.display = "";
if (freeLabelEl) freeLabelEl.textContent = "\u6709\u6548\u671f";
if (freeEl) freeEl.textContent = formatExpireText(state.cloudProExpireAt || state.cloudLeaseExp);
} else {
if (freeRow) freeRow.style.display = "none";
}
if (tokenInput && tokenInput !== document.activeElement) tokenInput.value = state.cloudToken || "";
}
function recomputeQueueStats() {
const q = loadQueue();
let total = 0;
let done = 0;
let allDone = 0;
for (const id of q) {
const p = state.panelProjects.find((x) => x.projectId === id);
if (!p) continue;
const preview = state.chapterPreview.filter((c) => c.projectId === id);
if (preview.length) {
total += preview.length;
done += preview.filter((c) => c.done).length;
} else {
total += 1;
if (isProjectFullyFinished(p)) done += 1;
}
if (isProjectFullyFinished(p)) allDone += 1;
}
state.queueWareTotal = total;
state.queueWareDone = done;
return { total, done, allDone, selected: q.length };
}
function updatePanel() {
const status = document.getElementById("hbzj-auto-status");
if (status) {
status.textContent = state.enabled ? `\u8fd0\u884c\u4e2d · ${CV_INTERVAL_MINUTES}\u5206\u949f/\u6b21` : "\u5df2\u505c\u6b62";
status.style.color = state.enabled ? "#15803d" : "#64748b";
}
const stats = recomputeQueueStats();
const doneEl = document.getElementById("hbzj-queue-done");
const totalEl = document.getElementById("hbzj-queue-total");
const examEl = document.getElementById("hbzj-queue-exam");
const pctEl = document.getElementById("hbzj-queue-percent");
const bar = document.getElementById("hbzj-queue-progress");
const pct = stats.total ? Math.round((stats.done / stats.total) * 100) : 0;
if (doneEl) doneEl.textContent = String(stats.done);
if (totalEl) totalEl.textContent = String(stats.total);
if (examEl) examEl.textContent = stats.selected ? `${stats.allDone}/${stats.selected}` : "—";
if (pctEl) pctEl.textContent = `${pct}%`;
if (bar) bar.style.width = `${pct}%`;
const userEl = document.getElementById("hbzj-current-user");
if (userEl) {
const u = state.userProfile;
const name = String(u?.name || "").trim();
userEl.textContent = name || (getAccountId() ? "\u5df2\u767b\u5f55" : "—");
}
const courseEl = document.getElementById("hbzj-current-course");
const chapterEl = document.getElementById("hbzj-current-chapter");
const taskEl = document.getElementById("hbzj-current-task");
if (taskEl) taskEl.textContent = state.lastUserAction || "\u52fe\u9009\u73ed\u7ea7\u540e\u70b9\u300c\u5f00\u59cb\u300d";
if (courseEl && !state.enabled) courseEl.textContent = "\u65e0";
if (chapterEl && !state.enabled) chapterEl.textContent = "\u65e0";
const startBtn = document.getElementById("hbzj-start");
const stopBtn = document.getElementById("hbzj-stop");
if (startBtn) startBtn.disabled = !!state.enabled;
if (stopBtn) {
stopBtn.disabled = !state.enabled;
stopBtn.classList.toggle("hbzj-btn-off", !state.enabled);
}
const qText = document.getElementById("hbzj-queue-text");
if (qText) qText.textContent = stats.selected ? `\u5df2\u9009 ${stats.selected} \u9879` : "\u672a\u9009\u62e9";
renderCourseList();
renderChapterPreview();
updateCloudPanelUI();
renderPanelNotice();
}
function renderCourseList() {
const box = document.getElementById("hbzj-course-list");
if (!box) return;
if (state.panelRefreshing) {
box.innerHTML = `\u52a0\u8f7d\u4e2d…
`;
return;
}
const q = new Set(loadQueue().map(String));
if (!state.panelProjects.length) {
const ok = hasPlatformAuth();
box.innerHTML = `${
ok
? "\u6682\u65e0\u73ed\u7ea7\u3002\u8bf7\u786e\u8ba4\u5e74\u5ea6\u7b5b\u9009\uff0c\u6216\u5230\u5b98\u7f51\u300c\u6211\u7684\u5b66\u4e60\u300d\u70b9\u4e00\u4e0b\u540e\u518d\u5237\u65b0\u3002"
: "\u8bf7\u5148\u767b\u5f55\u5b98\u7f51\uff0c\u6253\u5f00\u300c\u6211\u7684\u5b66\u4e60\u300d\uff0c\u518d\u70b9\u5237\u65b0\u3002"
}
`;
return;
}
box.innerHTML = state.panelProjects
.map((p) => {
const checked = q.has(String(p.projectId)) ? "checked" : "";
const disabled = isProjectFullyFinished(p) ? "disabled" : "";
const badge = projectListBadge(p);
return ``;
})
.join("");
box.querySelectorAll("input[type=checkbox]").forEach((inp) => {
inp.addEventListener("change", () => {
const id = String(inp.getAttribute("data-pid") || "");
let list = loadQueue().map(String);
if (inp.checked) {
if (!list.includes(id)) list.push(id);
} else {
list = list.filter((x) => x !== id);
}
saveQueue(list);
state.chapterPreviewLoading = true;
renderChapterPreview();
void _rcp()
.catch((e) => trace(`\u7ae0\u8282\u9884\u89c8\u5931\u8d25\uff1a${e.message || e}`))
.finally(() => {
state.chapterPreviewLoading = false;
updatePanel();
});
});
});
}
function renderChapterPreview() {
const box = document.getElementById("hbzj-chapter-preview");
if (!box) return;
const q = loadQueue().map(String).filter(Boolean);
if (!q.length) {
box.innerHTML = `\u8bf7\u52fe\u9009\u73ed\u7ea7
`;
return;
}
if (state.chapterPreviewLoading && !state.chapterPreview.length) {
box.innerHTML = `\u6b63\u5728\u52a0\u8f7d\u89c6\u9891\u7ae0\u8282…
`;
return;
}
if (!state.chapterPreview.length) {
box.innerHTML = `\u5df2\u52fe\u9009 ${q.length} \u9879\uff0c\u4f46\u672a\u8bfb\u5230\u89c6\u9891\u3002\u8bf7\u70b9\u300c\u5237\u65b0\u300d\u540e\u91cd\u8bd5\u3002
`;
return;
}
const groups = new Map();
for (const c of state.chapterPreview) {
const pid = String(c.projectId);
if (!groups.has(pid)) groups.set(pid, []);
groups.get(pid).push(c);
}
const html = [];
for (const [pid, items] of groups) {
const p = state.panelProjects.find((x) => String(x.projectId) === pid);
const title = p?.projectName || items[0]?.projectName || pid;
const examLine = p ? escHtml(p.examLabel || "\u5f85\u67e5") : "\u8003\u8bd5\u4fe1\u606f\u52a0\u8f7d\u4e2d…";
const wareDone = items.filter((x) => x.done).length;
html.push(`
${escHtml(title)}
\u8003\u8bd5\uff1a${examLine}
\u89c6\u9891 ${wareDone}/${items.length}
${items
.map((c) => {
const pct = wareProgressPct(c.studied, c.total, c.done);
return `
${escHtml(c.name)}
${c.done ? "\u5df2\u8fbe\u6807" : "\u672a\u5b8c\u6210"} · ${formatMinutes(c.studied)} / ${formatMinutes(c.total)}\uff08${pct}%\uff09
`;
})
.join("")}
`);
}
box.innerHTML = html.join("") || `\u6682\u65e0\u89c6\u9891
`;
}
async function _rcp() {
const q = loadQueue().map(String).filter(Boolean);
const rows = [];
if (!q.length) {
state.chapterPreview = [];
return;
}
for (const id of q) {
const p = state.panelProjects.find((x) => String(x.projectId) === String(id));
if (!p) {
rows.push({
projectId: id,
projectName: id,
name: "\uff08\u5217\u8868\u4e2d\u627e\u4e0d\u5230\u8be5\u73ed\u7ea7\uff0c\u8bf7\u5148\u5237\u65b0\uff09",
studied: 0,
total: 0,
done: false,
kind: "ware",
});
continue;
}
try {
const books = await fetchClassBooks(id);
const videos = flattenVideosFromBooks(books, id).filter((v) => !isEbookVideo(v));
if (!videos.length) {
rows.push({
projectId: id,
projectName: p.projectName,
name: "\u672a\u83b7\u53d6\u5230\u89c6\u9891\u5217\u8868",
studied: 0,
total: 0,
done: false,
kind: "ware",
});
} else {
for (const v of videos) {
const done = isVideoDone(v);
rows.push({
projectId: id,
projectName: p.projectName,
videoId: String(v.myClassCourseVideoId || ""),
name: videoNameOf(v),
studied: videoDisplayStudiedSeconds(v),
total: videoTotalSeconds(v),
done,
kind: "ware",
});
}
const allDone = videos.every(isVideoDone);
if (allDone) {
p.videoFinishedStatus = true;
p.studyProgress = Math.max(Number(p.studyProgress || 0), 100);
}
syncProjectFinishedFlags(p);
}
} catch (e) {
trace(`preview ${id}: ${e.message || e}`);
rows.push({
projectId: id,
projectName: p.projectName,
name: `\u52a0\u8f7d\u5931\u8d25\uff1a${e.message || e}`,
studied: 0,
total: 0,
done: false,
kind: "ware",
});
}
}
state.chapterPreview = rows;
}
function formatExamScore(score) {
const n = Number(score);
if (!Number.isFinite(n)) return "";
return Number.isInteger(n) ? `${n}\u5206` : `${Number(n.toFixed(1))}\u5206`;
}
function examLabelForProject(project, papers) {
const pid = String(project?.projectId || "");
const mine = (papers || []).filter((row) => String(row.myClassId || "") === pid);
if (!mine.length) {
return { examRequired: false, examDone: true, examLabel: "\u65e0\u8bd5\u5377", examScore: null };
}
const pending = mine.filter(paperNeedsExam);
if (pending.length) {
const lastScore = Number(pending[0].score);
const scoreHint =
Number.isFinite(lastScore) && lastScore > 0
? `\uff08\u4e0a\u6b21${formatExamScore(lastScore)}\uff09`
: "";
return {
examRequired: true,
examDone: false,
examScore: Number.isFinite(lastScore) ? lastScore : null,
examLabel:
(pending.length === 1 ? "\u5f85\u8003\u8bd5" : `\u5f85\u8003\u8bd5 ${pending.length} \u4efd`) + scoreHint,
};
}
const passed = mine.filter(
(row) =>
Number(row.isPass) === 1 ||
String(row.statusName || "").includes("\u5df2\u901a\u8fc7") ||
(Number.isFinite(Number(row.score)) &&
Number(row.score) >= Number(row.passScore ?? 60))
);
if (passed.length) {
const best = Math.max(...passed.map((r) => Number(r.score) || 0));
const scoreText = formatExamScore(best);
return {
examRequired: false,
examDone: true,
examScore: best,
examLabel: scoreText ? `\u5df2\u901a\u8fc7 ${scoreText}` : "\u5df2\u901a\u8fc7",
};
}
const blocked = mine.filter(
(row) =>
Number(row.studySpeed || 0) < 100 || String(row.statusName || "").includes("\u672a\u5b8c\u6210\u5b66\u4e60")
);
if (blocked.length) {
return { examRequired: false, examDone: false, examScore: null, examLabel: "\u9700\u5148\u5b66\u5b8c" };
}
const anyScore = Math.max(...mine.map((r) => Number(r.score) || 0), 0);
if (anyScore > 0) {
return {
examRequired: false,
examDone: true,
examScore: anyScore,
examLabel: `\u6210\u7ee9 ${formatExamScore(anyScore)}`,
};
}
return { examRequired: false, examDone: true, examScore: null, examLabel: "\u65e0\u5f85\u8003" };
}
async function enrichExamStatusForProjects(list, limit) {
const targets = (list || []).slice(0, Math.max(1, limit || 12));
let papers = null;
try {
papers = await fetchPaperList(9);
if (!papers.length) papers = await fetchPaperList(1);
} catch (_) {
papers = [];
}
for (const p of targets) {
if (!p || p._examEnriched) continue;
const st = examLabelForProject(p, papers);
p.examRequired = st.examRequired;
p.examDone = st.examDone;
p.examLabel = st.examLabel;
p.examScore = st.examScore;
p._examEnriched = true;
syncProjectFinishedFlags(p);
updatePanel();
await sleep(60);
}
}
async function _rpp(opt) {
const silent = !!opt?.silent;
state.panelRefreshing = true;
updatePanel();
try {
for (let i = 0; i < 8 && !getAccountId(); i++) {
const cms = readCmsAccountInfo();
if (cms?.accountId) rememberAccountId(cms.accountId);
await sleep(400);
}
if (!getAccountId()) {
if (mergeCapturedIntoPanel(true)) {
if (!silent) trace(`\u5df2\u7528\u9875\u9762\u7f13\u5b58\u5217\u8868\uff1a${state.panelProjects.length} \u9879`);
return;
}
throw new Error("\u672a\u83b7\u53d6 accountId\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55\u540e\u70b9\u5237\u65b0");
}
try {
await ensureUserProfile(false);
} catch (e) {
trace(`\u7528\u6237\u4fe1\u606f: ${e.message || e}`);
}
await fetchYears();
state.panelProjects = await fetchAllPanelProjects();
await enrichProjectNames(state.panelProjects, 50);
if (!state.panelProjects.length) {
mergeCapturedIntoPanel(true);
await enrichProjectNames(state.panelProjects, 50);
}
await _rcp();
void enrichExamStatusForProjects(
[
...state.panelProjects.filter((p) => loadQueue().map(String).includes(String(p.projectId))),
...state.panelProjects.filter((p) => p.listSource === "learning"),
...state.panelProjects.filter((p) => p.listSource === "completed"),
],
20
);
const nLearn = state.panelProjects.filter((p) => p.listSource === "learning").length;
const nDone = state.panelProjects.filter((p) => p.listSource === "completed").length;
if (!silent) trace(`\u5df2\u5237\u65b0\uff1a\u6b63\u5728\u5b66 ${nLearn} · \u5df2\u5b8c\u6210 ${nDone}`);
} catch (e) {
if (mergeCapturedIntoPanel(true)) {
trace(`\u63a5\u53e3\u5f02\u5e38\uff0c\u5df2\u663e\u793a\u62e6\u622a\u5217\u8868\uff1a${e.message || e}`);
} else {
trace(`\u5237\u65b0\u5931\u8d25\uff1a${e.message || e}`);
}
} finally {
state.panelRefreshing = false;
updatePanel();
}
}
function selectUnfinishedProjects() {
const list = state.panelProjects.filter(isProjectSelectable).map((p) => p.projectId);
saveQueue(list);
state.chapterPreviewLoading = true;
renderChapterPreview();
void _rcp()
.catch((e) => log(`\u7ae0\u8282\u9884\u89c8\u5931\u8d25\uff1a${e.message || e}`))
.finally(() => {
state.chapterPreviewLoading = false;
updatePanel();
});
trace(`\u5df2\u52fe\u9009\u672a\u5b8c\u6210 ${list.length} \u9879`);
}
function clearProjectSelection() {
saveQueue([]);
state.chapterPreview = [];
updatePanel();
}
function injectPanelStyles() {
const id = "hbzj-panel-style-v2";
if (document.getElementById(id)) return;
document.getElementById("hbzj-panel-style-v1")?.remove();
const style = document.createElement("style");
style.id = id;
style.textContent = `
#hbzj-auto-panel{position:fixed;right:20px;top:80px;z-index:999999;width:412px;max-width:calc(100vw - 24px);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);}
#hbzj-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;}
#hbzj-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;}
#hbzj-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:cover;display:block;flex:0 0 auto;background:#e2e8f0;}
#hbzj-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;}
#hbzj-panel-sub{margin-top:3px;font-size:11px;color:#7c2d12;font-weight:700;}
#hbzj-panel-controls{display:flex;gap:5px;}
.hbzj-panel-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;font-size:15px;font-weight:900;}
#hbzj-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;}
#hbzj-panel-footer{padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;}
.hbzj-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;}
.hbzj-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;}
.hbzj-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-top-left-radius:11px;border-bottom-left-radius:11px;}
.hbzj-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;}
.hbzj-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;}
.hbzj-status-metrics{font-size:12px;color:#475569;}
.hbzj-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;}
.hbzj-progress-pct{font-size:14px;font-weight:900;color:#0369a1;}
.hbzj-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;}
.hbzj-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;}
.hbzj-tabbar{display:flex;gap:6px;margin-bottom:7px;}
.hbzj-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;}
.hbzj-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;}
.hbzj-pane{display:none;}.hbzj-pane.active{display:block;}
.hbzj-list-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;gap:6px;flex-wrap:wrap;}
.hbzj-list-title{font-size:12px;color:#64748b;font-weight:700;}
.hbzj-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;}
.hbzj-list-head-actions{display:flex;gap:6px;align-items:center;flex-wrap:nowrap;justify-content:flex-start;flex:1 1 100%;}
.hbzj-list-head-actions .hbzj-btn{flex:0 0 auto !important;width:auto;min-width:0;white-space:nowrap;padding:4px 8px;font-size:11px;line-height:1.2;}
.hbzj-btn{border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;}
.hbzj-btn-ghost{background:#fff !important;color:#0f172a !important;border:1px solid #cbd5e1;}
.hbzj-btn-start{flex:1;background:#16a34a;}
.hbzj-btn-stop{flex:1;background:#ef4444;}
.hbzj-btn-off,.hbzj-btn:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;}
.hbzj-btn-row{display:flex;gap:7px;}
.hbzj-btn-pro{border:none;background:linear-gradient(135deg,#f59e0b,#ea580c);color:#fff;border-radius:8px;padding:5px 10px;font-weight:800;cursor:pointer;font-size:12px;}
#hbzj-course-list,#hbzj-chapter-preview,#hbzj-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;}
.hbzj-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:700;}
.hbzj-course-item,.hbzj-chapter-item{display:flex;gap:7px;align-items:flex-start;border:1px solid #e2e8f0;border-radius:8px;padding:7px;margin-bottom:6px;cursor:pointer;background:#f8fafc;}
.hbzj-course-name{font-size:12px;line-height:1.38;font-weight:700;display:flex;align-items:flex-start;gap:6px;flex-wrap:wrap;}
.hbzj-course-meta{font-size:11px;color:#64748b;margin-top:2px;line-height:1.4;}
.hbzj-badge{font-size:10px;font-weight:800;padding:2px 6px;border-radius:999px;border:1px solid;line-height:1.3;}
.hbzj-badge-learn{color:#1d4ed8;background:#eff6ff;border-color:#93c5fd;}
.hbzj-badge-exam{color:#c2410c;background:#fff7ed;border-color:#fdba74;}
.hbzj-badge-done{color:#047857;background:#ecfdf5;border-color:#6ee7b7;}
.hbzj-preview-group{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;padding:0;}
.hbzj-preview-hd{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;}
.hbzj-preview-exam{margin:6px 8px;font-size:11px;font-weight:700;color:#9a3412;background:#fff7ed;border:1px solid #fed7aa;border-radius:8px;padding:5px 7px;line-height:1.4;}
.hbzj-preview-group .hbzj-chapter-item{margin:0 6px 6px;background:#f8fafc;}
.hbzj-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:11px;margin-bottom:4px;}
.hbzj-meta-label{color:#64748b;}.hbzj-meta-value{font-weight:700;text-align:right;max-width:68%;word-break:break-all;}
.hbzj-log-line{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;display:flex;gap:6px;align-items:baseline;flex-wrap:wrap;}
.hbzj-log-time{color:#94a3b8;font-size:11px;font-weight:700;flex:0 0 auto;}
.hbzj-log-text{font-weight:700;word-break:break-word;}
.hbzj-log-start .hbzj-log-text{color:#15803d;}
.hbzj-log-study .hbzj-log-text{color:#1d4ed8;}
.hbzj-log-done .hbzj-log-text{color:#047857;}
.hbzj-log-exam .hbzj-log-text{color:#c2410c;}
.hbzj-log-warn .hbzj-log-text{color:#b45309;}
.hbzj-log-stop .hbzj-log-text{color:#64748b;}
.hbzj-log-info .hbzj-log-text{color:#334155;}
.hbzj-log-hint{background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;margin:2px 0;padding:5px 7px !important;border-bottom:none;}
.hbzj-log-hint .hbzj-log-text{color:#0369a1;}
.hbzj-log-tip{margin:0 0 6px;padding:6px 8px;border-radius:8px;background:#eff6ff;border:1px solid #bfdbfe;color:#0369a1;font-size:11px;font-weight:700;line-height:1.45;}
#hbzj-year-select{border:1px solid #cbd5e1;border-radius:8px;padding:3px 6px;font-size:12px;margin-left:4px;}
#hbzj-pro-modal{position:fixed;inset:0;z-index:1000000;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center;padding:16px;}
.hbzj-pro-card{width:min(420px,94vw);background:#fff;border-radius:14px;padding:16px;box-shadow:0 20px 50px rgba(0,0,0,.25);}
.hbzj-pro-title{font-size:18px;font-weight:900;color:#9a3412;}
.hbzj-pro-sub{margin-top:6px;font-size:12px;color:#78716c;line-height:1.45;}
.hbzj-pro-sec{margin-top:12px;}
.hbzj-pro-sec h4{margin:0 0 6px;font-size:13px;color:#0f172a;}
.hbzj-pro-sec ul{margin:0;padding-left:18px;font-size:12px;color:#334155;line-height:1.55;}
.hbzj-pro-sec p{margin:0;font-size:12px;color:#475569;line-height:1.5;}
.hbzj-pro-buy-btn{margin-top:8px;border:none;background:linear-gradient(135deg,#f59e0b,#ea580c);color:#fff;border-radius:8px;padding:8px 12px;font-weight:800;cursor:pointer;width:100%;}
.hbzj-pro-actions{margin-top:14px;display:flex;justify-content:flex-end;}
.hbzj-pro-close{border:1px solid #cbd5e1;background:#f8fafc;border-radius:8px;padding:7px 12px;cursor:pointer;font-weight:700;}
#hbzj-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%) translateY(20px);z-index:1000001;background:rgba(15,23,42,.92);color:#fff;padding:10px 14px;border-radius:10px;font-size:12px;font-weight:700;opacity:0;pointer-events:none;transition:.2s ease;max-width:86vw;}
#hbzj-toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
`;
(document.head || document.documentElement).appendChild(style);
}
function _cp() {
if (document.getElementById("hbzj-auto-panel")) return;
injectPanelStyles();
const panel = document.createElement("div");
panel.id = "hbzj-auto-panel";
panel.innerHTML = `
\u8fd0\u884c\u72b6\u6001
\u5df2\u505c\u6b62
\u8bfe\u4ef6 0/0 · \u5168\u5b8c\u6210 —
0%
\u5b66\u4e60\u5e74\u4efd
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d
\u7ae0\u8282\u9884\u89c8\u89c6\u9891
\u8bf7\u52fe\u9009\u73ed\u7ea7
\u8fd0\u884c\u65e5\u5fd7\u8fdb\u5ea6
⏱ \u770b\u8bfe\u8fdb\u5ea6\u7ea6\u6bcf ${CV_INTERVAL_MINUTES} \u5206\u949f\u66f4\u65b0\u4e00\u6b21\uff0c\u7b49\u5f85\u671f\u95f4\u811a\u672c\u4ecd\u5728\u8fd0\u884c
\u4f1a\u5458\u8bbe\u7f6e
\u4f1a\u5458\u72b6\u6001—
\u6709\u6548\u671f—
\u6388\u6743\u7801
\u5f53\u524d\u7528\u6237—
\u5f53\u524d\u4efb\u52a1\u52fe\u9009\u73ed\u7ea7\u540e\u70b9\u300c\u5f00\u59cb\u300d
\u5f53\u524d\u8bfe\u7a0b\u65e0
\u5f53\u524d\u8bfe\u4ef6\u65e0
`;
document.body.appendChild(panel);
const savedPos = readPanelPos();
if (savedPos?.left != null && savedPos?.top != null) {
panel.style.right = "auto";
panel.style.left = `${savedPos.left}px`;
panel.style.top = `${savedPos.top}px`;
}
applyPanelCollapsed(panel, readPanelCollapsed());
enablePanelDrag(panel);
panel.querySelector("#hbzj-btn-min")?.addEventListener("click", (e) => {
e.stopPropagation();
writePanelCollapsed(true);
applyPanelCollapsed(panel, true);
});
panel.querySelector("#hbzj-btn-max")?.addEventListener("click", (e) => {
e.stopPropagation();
writePanelCollapsed(false);
applyPanelCollapsed(panel, false);
});
panel.querySelectorAll(".hbzj-tab-btn").forEach((btn) => {
btn.addEventListener("click", () => switchPanelTab(btn.dataset.tab));
});
const yearSel = panel.querySelector("#hbzj-year-select");
const fillYears = () => {
if (!yearSel) return;
const opts = state.yearOptions.length ? state.yearOptions : [state.studyYear || new Date().getFullYear()];
yearSel.innerHTML = opts
.map((y) => ``)
.join("");
};
fillYears();
yearSel?.addEventListener("change", () => {
state.studyYear = Number(yearSel.value) || null;
pageStorage().setItem(YEAR_KEY, String(state.studyYear || ""));
void _rpp({ reason: "year" });
});
panel.__fillYears = fillYears;
document.getElementById("hbzj-refresh")?.addEventListener("click", () => {
void _rpp({ reason: "manual" }).then(() => panel.__fillYears?.());
});
document.getElementById("hbzj-select-unfinished")?.addEventListener("click", () => selectUnfinishedProjects());
document.getElementById("hbzj-clear-select")?.addEventListener("click", () => clearProjectSelection());
document.getElementById("hbzj-cloud-save")?.addEventListener("click", () => {
const input = document.getElementById("hbzj-cloud-token");
void _sct(input?.value || "").then((ok) => {
updateCloudPanelUI();
showToast(ok ? "\u5df2\u4fdd\u5b58" : "\u4fdd\u5b58\u5931\u8d25");
});
});
document.getElementById("hbzj-open-pro")?.addEventListener("click", () => openProModal());
document.getElementById("hbzj-start")?.addEventListener("click", () => {
const q = loadQueue().filter((id) => {
const p = state.panelProjects.find((x) => x.projectId === id);
return p && isProjectSelectable(p);
});
saveQueue(q);
if (!q.length) {
log("\u8bf7\u5148\u52fe\u9009\u73ed\u7ea7");
showToast("\u8bf7\u5148\u52fe\u9009\u73ed\u7ea7");
updatePanel();
return;
}
void (async () => {
try {
await _ecl(true);
} catch (_) {}
if (!(await _acp())) {
updatePanel();
return;
}
state.enabled = true;
state.lastUserAction = "";
state._studyWaitHintShown = false;
updatePanel();
void _rse();
})();
});
document.getElementById("hbzj-stop")?.addEventListener("click", () => {
state.enabled = false;
state.lastUserAction = "";
state._studyWaitHintShown = false;
updatePanel();
});
updatePanel();
void _fcc();
}
async function _bst() {
const host = String(location.hostname || "");
if (!/yxlearning\.com$/i.test(host)) return;
const cms = readCmsAccountInfo();
if (cms?.accountId) rememberAccountId(cms.accountId);
installAuthHooks();
const waitBody = () =>
new Promise((r) => {
if (document.body) r();
else document.addEventListener("DOMContentLoaded", r, { once: true });
});
await waitBody();
await sleep(600);
_cp();
await _rpp({ reason: "boot", silent: true });
try {
await _ecl(false);
} catch (_) {}
if (!state.panelProjects.length) {
for (let i = 0; i < 6 && !state.panelProjects.length; i++) {
await sleep(800);
if (mergeCapturedIntoPanel(true)) break;
if (getAccountId()) {
await _rpp({ reason: "retry", silent: true });
break;
}
}
}
const panel = document.getElementById("hbzj-auto-panel");
panel?.__fillYears?.();
state._bootLoaded = true;
if (!getAccountId()) {
trace("\u672a\u767b\u5f55\uff0c\u8bf7\u6253\u5f00\u6211\u7684\u5b66\u4e60\u540e\u5237\u65b0");
} else if (!state.panelProjects.length) {
trace("\u5217\u8868\u4e3a\u7a7a\uff0c\u8bf7\u6253\u5f00\u6211\u7684\u5b66\u4e60\u9875\u540e\u518d\u5237\u65b0");
}
}
_bst().catch((e) => trace(e));
})();