// ==UserScript==
// @name 【效率版】白玉兰远程医学网学习助手(1分钟全部搞定)
// @namespace https://card.wlxy.live/details/6771FFF7
// @version 1.0
// @description 白玉兰远程医学网(https://byljxjy.com)学习助手:个人中心正在学项目自动看课与考试;快速一键完成课时和考试考核,所有课程1分钟学完,标准用户可体验部分章节,会员不限量30天。
// @author 柠檬真酸
// @match https://byljxjy.com/*
// @match https://www.byljxjy.com/*
// @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png
// @connect byljxjy.com
// @connect www.byljxjy.com
// @connect oa22.ahzsksw.cn
// @connect huaweicloudobs.ahjxjy.cn
// @connect card.wlxy.live
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-start
// @antifeature payment 标准用户可体验部分章节,开通会员不限量
// @antifeature membership 需授权校验
// @license All Rights Reserved
// ==/UserScript==
(function () {
"use strict";
const SCRIPT_VERSION = "0.2.3";
const PAGE = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
const PANEL_LOGO_URL =
"https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png";
const ORIGIN = "https://byljxjy.com";
const FACADE = "/health-edu-continuingeducationstudent-facade";
const BASICS = "/health-edu-student-basics";
const HOME = "/health-edu-student-home";
const PATH_LEARNING = `${BASICS}/v1/personalCenter/continueToTeachLearning`;
const PATH_COMPLETED = `${BASICS}/v1/personalCenter/continueToTeachCompleted`;
const PATH_YEARS = `${BASICS}/v1/personalCenter/continueProjectsYears`;
const PATH_FIND = `${FACADE}/v1/ce/project/student/findById/`;
const PATH_COURSEWARE = `${FACADE}/v1/ce/project/student/queryProjectCoursewareById/`;
const PATH_VIDEO_DONE = `${FACADE}/v1/ce/project/student/videoFinishedStatus/`;
const PATH_WATCH = `${FACADE}/v1/ce/project/student/apply/submitWatchVideoRecord`;
const PATH_EXAM = `${FACADE}/v1/ce/project/student/queryExamById/`;
const PATH_SUBMIT_EXAM = `${FACADE}/v1/ce/project/student/apply/submitStudentExam`;
const PATH_ACCOUNT = `${FACADE}/v1/ce/student/studycard/queryAccount`;
const PATH_USER = `${HOME}/findByMultiWay`;
const PATH_WS_URL = `${FACADE}/v1/ce/project/student/queryWebSocketUrl`;
const PINNED_CLOUD_HOST = "oa22.ahzsksw.cn";
const DEFAULT_CLOUD_API_BASE = "https://oa22.ahzsksw.cn";
const CLOUD_TOKEN_KEY = "byljx_cloud_token_v1";
const CLOUD_LEASE_CACHE_KEY = "byljx_cloud_lease_cache_v1";
const CLOUD_FREE_USED_KEY = "byljx_cloud_free_used_v1";
const CLOUD_API_BASE_KEY = "byljx_cloud_api_base_v1";
const DEFAULT_FREE_CHAPTER_LIMIT = 3;
const PRO_BUY_URL = "https://card.wlxy.live/details/6771FFF7";
const PRO_DAYS = 30;
const PANEL_NOTICE_FALLBACK = "\u767d\u7389\u5170\u8fdc\u7a0b\u533b\u5b66\u7f51\u5b66\u4e60\u52a9\u624b";
const DEFAULT_PANEL_NOTICE_PATH = "/api/byljx/panel-notice";
const QUEUE_KEY = "byljx_project_queue_v1";
const PANEL_POS_KEY = "byljx_panel_pos_v1";
const PANEL_COLLAPSED_KEY = "byljx_panel_collapsed_v1";
const AUTH_CACHE_KEY = "byljx_auth_cache_v1";
const YEAR_KEY = "byljx_year_v1";
const EFFICIENCY_JUMP_RATIO = 0.98;
const WATCH_GAP_MS = 1200;
const WATCH_MAX_ROUNDS = 8;
const ENGINE_TICK_MS = 1200;
const EXAM_PASS_RETRY = 3;
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: resolvePinnedCloudApiBase(),
panelNoticePath: DEFAULT_PANEL_NOTICE_PATH,
remotePanelNotice: "",
_lastProErr: "",
_ws: null,
_wsProjectId: "",
_bootLoaded: false,
_capturedLists: { learning: [], completed: [] },
};
function trace(msg) {
console.log(`[\u767d\u7389\u5170\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 log(msg) {
const line = `${new Date().toLocaleTimeString()} ${String(msg || "")}`;
state.runLogs.unshift(line);
if (state.runLogs.length > 200) state.runLogs.length = 200;
const el = document.getElementById("byljx-run-log");
if (el) {
el.innerHTML = state.runLogs.map((x) => `
${escHtml(x)}
`).join("") ||
`\u6682\u65e0\u65e5\u5fd7
`;
}
trace(msg);
}
function resolvePinnedCloudApiBase() {
const raw = String(pageStorage().getItem(CLOUD_API_BASE_KEY) || DEFAULT_CLOUD_API_BASE)
.trim()
.replace(/\/+$/, "");
if (!raw || !/oa22\.ahzsksw\.cn/i.test(raw)) {
try {
pageStorage().setItem(CLOUD_API_BASE_KEY, DEFAULT_CLOUD_API_BASE);
} catch (_) {}
return DEFAULT_CLOUD_API_BASE;
}
return raw;
}
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("byljx-ann-text");
if (el) el.textContent = getPanelNoticeText();
}
function decodeJwtPayload(token) {
try {
const part = String(token || "").split(".")[1];
if (!part) return null;
const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
const pad = b64 + "===".slice((b64.length + 3) % 4);
const bin = atob(pad);
let json = bin;
try {
json = decodeURIComponent(Array.from(bin, (c) => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")).join(""));
} catch (_) {}
return JSON.parse(json);
} catch (_) {
return null;
}
}
function looksLikeJwt(v) {
const s = String(v || "").trim().replace(/^"|"$/g, "");
return /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(s);
}
function findJwtDeep(v, depth) {
if (depth > 5 || v == null) return "";
if (typeof v === "string") {
const s = v.trim().replace(/^"|"$/g, "");
if (looksLikeJwt(s)) return s;
if ((s.startsWith("{") || s.startsWith("[")) && s.length < 200000) {
try {
return findJwtDeep(JSON.parse(s), depth + 1);
} catch (_) {}
}
return "";
}
if (typeof v === "object") {
const prefer = ["token", "accessToken", "access_token", "Authorization", "authorization", "data", "value", "jwt"];
for (const k of prefer) {
if (v[k] != null) {
const found = findJwtDeep(v[k], depth + 1);
if (found) return found;
}
}
for (const val of Object.values(v)) {
const found = findJwtDeep(val, depth + 1);
if (found) return found;
}
}
return "";
}
function pickTokenFromStorage() {
const ls = pageStorage();
const keys = [
"Authorization",
"authorization",
"token",
"Token",
"access_token",
"accessToken",
"Admin-Token",
"admin-token",
"BYL_TOKEN",
"byl_token",
"health-token",
"vue_admin_template_token",
"Health-Token",
"LOGIN_TOKEN",
];
for (const k of keys) {
try {
const found = findJwtDeep(ls.getItem(k), 0);
if (found) return found;
} catch (_) {}
}
try {
for (let i = 0; i < ls.length; i++) {
const k = ls.key(i);
if (!k) continue;
const found = findJwtDeep(ls.getItem(k), 0);
if (found) return found;
}
} catch (_) {}
try {
const ss = PAGE.sessionStorage;
for (let i = 0; i < ss.length; i++) {
const k = ss.key(i);
const found = findJwtDeep(ss.getItem(k), 0);
if (found) return found;
}
} catch (_) {}
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 rememberPlatformToken(token) {
const t = String(token || "").replace(/^Bearer\s+/i, "").trim();
if (!looksLikeJwt(t)) return;
if (PAGE.__byljxToken === t) return;
PAGE.__byljxToken = t;
const p = decodeJwtPayload(t) || {};
const patch = { token: t };
if (p.user_name) patch.loginId = String(p.user_name);
writeAuthCache(patch);
}
function getRuntimeAuth() {
const cache = readAuthCache();
const hooked = String(PAGE.__byljxToken || "").trim();
const token = hooked || (looksLikeJwt(cache.token) ? cache.token : "") || pickTokenFromStorage();
if (token && PAGE.__byljxToken !== token) PAGE.__byljxToken = token;
const payload = decodeJwtPayload(token) || {};
const loginId = String(
cache.loginId || payload.user_name || payload.username || payload.loginId || ""
).trim();
return { token: String(token || "").trim(), loginId, payload };
}
function ingestListPayload(url, json) {
try {
if (!json || String(json.errorCode || "OK").toUpperCase() !== "OK") return;
const content = json?.data?.content;
if (!Array.isArray(content)) return;
const isCompleted = /continueToTeachCompleted/i.test(url);
const isLearning = /continueToTeachLearning/i.test(url);
if (!isCompleted && !isLearning) return;
const source = isCompleted ? "completed" : "learning";
const mapped = content.map((row) => mapProjectRow(row, source));
const bag = state._capturedLists || { learning: [], completed: [] };
const byId = new Map((bag[source] || []).map((p) => [p.projectId, p]));
for (const p of mapped) byId.set(p.projectId, p);
bag[source] = Array.from(byId.values());
state._capturedLists = bag;
if (document.getElementById("byljx-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.__byljxNetHooked) return;
PAGE.__byljxNetHooked = 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.__byljxUrl = String(url || "");
this.__byljxMethod = String(method || "");
} catch (_) {}
return open.apply(this, arguments);
};
XHR.prototype.setRequestHeader = function (k, v) {
try {
if (/^authorization$/i.test(String(k || ""))) rememberPlatformToken(v);
} catch (_) {}
return setHeader.apply(this, arguments);
};
XHR.prototype.send = function () {
try {
this.addEventListener("load", function () {
try {
const url = String(this.__byljxUrl || "");
if (!/continueToTeach(Learning|Completed)|continueProjectsYears|findByMultiWay/i.test(url)) return;
const text = String(this.responseText || "");
if (!text || text.length > 5e6) return;
const json = JSON.parse(text);
if (/continueProjectsYears/i.test(url) && Array.isArray(json?.data)) {
state.yearOptions = json.data.map(Number).filter((n) => n > 2000);
}
if (/findByMultiWay/i.test(url) && json?.data) {
state.userProfile = json.data;
}
ingestListPayload(url, json);
} catch (_) {}
});
} catch (_) {}
return send.apply(this, arguments);
};
} catch (_) {}
try {
const rawFetch = PAGE.fetch;
if (typeof rawFetch === "function" && !rawFetch.__byljxPatched) {
const wrapped = function (input, init) {
try {
const h = init && init.headers;
let auth = "";
if (h && typeof h.get === "function") auth = h.get("Authorization") || h.get("authorization") || "";
else if (h && typeof h === "object") auth = h.Authorization || h.authorization || "";
if (auth) rememberPlatformToken(auth);
} catch (_) {}
const url = typeof input === "string" ? input : input && input.url;
return rawFetch.apply(PAGE, arguments).then((res) => {
try {
const u = String(url || "");
if (/continueToTeach(Learning|Completed)/i.test(u)) {
res
.clone()
.json()
.then((json) => ingestListPayload(u, json))
.catch(() => {});
}
} catch (_) {}
return res;
});
};
wrapped.__byljxPatched = true;
PAGE.fetch = wrapped;
}
} catch (_) {}
}
function apiUrl(path) {
if (/^https?:\/\//i.test(path)) return path;
return ORIGIN + (path.startsWith("/") ? path : `/${path}`);
}
async function _prq(method, path, body) {
const auth = getRuntimeAuth();
if (!auth.token) throw new Error("\u672a\u83b7\u53d6\u767b\u5f55 Token\uff0c\u8bf7\u5148\u767b\u5f55\u5b98\u7f51\u540e\u70b9\u300c\u5237\u65b0\u300d");
const url = apiUrl(path);
const headers = {
Accept: "application/json, text/plain, */*",
Authorization: auth.token,
};
let payload = undefined;
if (body !== undefined && body !== null && String(method).toUpperCase() !== "GET") {
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?.message || json?.msg || `HTTP ${status}`);
}
const code = String(json?.errorCode || "").toUpperCase();
if (code && code !== "OK" && code !== "0" && code !== "SUCCESS") {
throw new Error(json?.message || json?.msg || code);
}
return json;
};
try {
const pageFetch = PAGE.fetch || fetch;
const res = await pageFetch.call(PAGE, url, {
method,
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 (/\u672a\u83b7\u53d6\u767b\u5f55|HTTP |Unauth|invalid_token|Token/i.test(msg) && !/Failed to fetch|NetworkError|CORS/i.test(msg)) {
if (!/Failed to fetch|NetworkError/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,
url,
headers,
data: payload,
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 apiPost(path, body) {
return _prq("POST", path, body == null ? {} : body);
}
function getLearningUserId() {
return String(
state.userProfile?.id ||
state.userProfile?.loginId ||
getRuntimeAuth().loginId ||
readAuthCache().userId ||
readAuthCache().loginId ||
""
).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 readLocalFreeUsed() {
return Math.max(0, Number(pageStorage().getItem(CLOUD_FREE_USED_KEY) || 0) || 0);
}
function writeLocalFreeUsed(n) {
pageStorage().setItem(CLOUD_FREE_USED_KEY, String(Math.max(0, Number(n) || 0)));
}
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 || "",
freeChapterLimit: Number(p.freeChapterLimit ?? DEFAULT_FREE_CHAPTER_LIMIT),
freeUsedChapters: Number(p.freeUsedChapters ?? 0),
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 fetchPanelNotice() {
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 fetchClientConfig() {
try {
const data = await _crq("/api/byljx/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 fetchPanelNotice();
}
function formatCloudAuthError(err) {
const em = String(err?.message || err || "");
if (/\u672a\u90e8\u7f72|404|cloud_404/i.test(em)) {
return "\u4e91\u7aef\u767d\u7389\u5170\u670d\u52a1\u672a\u4e0a\u7ebf\uff0c\u8bf7\u5148\u90e8\u7f72 \u6388\u6743\u670d\u52a1 \u5e76\u53cd\u4ee3 /api/byljx/";
}
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;
writeLocalFreeUsed(state.freeUsedChapters);
}
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,
freeChapterLimit: state.freeChapterLimit,
freeUsedChapters: state.freeUsedChapters,
proExpireAt: state.cloudProExpireAt,
});
updateCloudPanelUI();
}
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.freeChapterLimit = cached.freeChapterLimit || state.freeChapterLimit;
state.freeUsedChapters = Number(cached.freeUsedChapters ?? 0);
state.cloudProExpireAt = cached.proExpireAt || state.cloudProExpireAt;
updateCloudPanelUI();
return true;
}
const luid = getLearningUserId();
if (!luid) {
state.cloudTier = state.cloudToken ? "pro" : "free";
state.freeUsedChapters = 0;
updateCloudPanelUI();
return false;
}
try {
const u = state.userProfile || {};
const data = await _crq("/api/byljx/lease", "POST", {
learning_user_id: luid,
lease: state.cloudLease || undefined,
site: "byljxjy",
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";
state.freeUsedChapters = Number(cached.freeUsedChapters ?? 0);
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/byljx/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;
}
}
function freeQuotaExhausted() {
if (isCloudProTier()) return false;
return Number(state.freeUsedChapters) >= Number(state.freeChapterLimit || DEFAULT_FREE_CHAPTER_LIMIT);
}
async function _acp() {
try {
await _ecl(true);
} catch (_) {
return false;
}
if (!state.cloudLease) return false;
if (isCloudProTier()) return true;
if (freeQuotaExhausted()) {
openProModal();
return false;
}
return true;
}
async function _rcq() {
try {
const data = await _crq("/api/byljx/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/i.test(em)) {
openProModal();
} else {
trace(`\u6263\u989d\u5931\u8d25\uff1a${em || "\u7f51\u7edc\u9519\u8bef"}`);
}
return false;
}
}
async function _ate() {
try {
await _ecl(true);
} catch (_) {
return false;
}
try {
const data = await _crq("/api/byljx/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() {
const url = state.proBuyUrl || PRO_BUY_URL;
try {
window.open(url, "_blank", "noopener");
} catch (_) {}
}
function closeProModal() {
document.getElementById("byljx-pro-modal")?.remove();
}
function openProModal() {
closeProModal();
const modal = document.createElement("div");
modal.id = "byljx-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.addEventListener("click", (e) => {
if (e.target === modal) closeProModal();
});
modal.querySelector("#byljx-pro-close")?.addEventListener("click", closeProModal);
modal.querySelector("#byljx-pro-buy-link")?.addEventListener("click", openProBuyPage);
}
function showToast(msg) {
let el = document.getElementById("byljx-toast");
if (!el) {
el = document.createElement("div");
el.id = "byljx-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 wareTotalSeconds(ware) {
const c = ware?.courseware || {};
const n = Number(c.videoLengthDouble || c.viedoLength || c.videoLength || ware.totalTime || 0);
return Math.max(1, Math.ceil(n));
}
function wareStudySeconds(ware) {
return Math.max(0, Number(ware.coursewareStudyTime || ware.videoStudyTime || 0) || 0);
}
function progressStandard(projectDetail) {
const n = Number(projectDetail?.studyProgressStandard ?? 80);
return Number.isFinite(n) && n > 0 ? Math.min(100, n) : 80;
}
function isWareDone(ware, projectDetail) {
if (!ware) return true;
if (ware.finished === true) return true;
if (Number(ware.coursewareStudyStatus) >= 2) return true;
const total = wareTotalSeconds(ware);
const need = Math.ceil((total * progressStandard(projectDetail)) / 100);
return wareStudySeconds(ware) >= need;
}
function wareDisplayStudiedSeconds(ware, projectDetail) {
const total = wareTotalSeconds(ware);
const raw = wareStudySeconds(ware);
if (isWareDone(ware, projectDetail)) {
return Math.max(raw, total);
}
return Math.min(raw, 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 isProjectFullyFinished(p) {
if (!p) return false;
if (p.listSource === "completed") return true;
if (p.projectFinishedStatus === true) return true;
if (Number(p.studyProgress) >= 100 && (!p.examRequired || p.examDone)) return true;
if (Number(p.studyProgress) >= 100 && p.examDone) return true;
return false;
}
function isProjectSelectable(p) {
return p && !isProjectFullyFinished(p);
}
function projectListBadge(p) {
if (isProjectFullyFinished(p)) {
return `\u5df2\u5b8c\u6210`;
}
return `\u6b63\u5728\u5b66`;
}
function syncProjectFinishedFlags(p) {
if (!p) return;
if (isProjectFullyFinished(p)) {
p.listSource = "completed";
p.sourceLabel = "\u5df2\u5b8c\u6210";
p.studyLabel = "\u5df2\u5b8c\u6210";
p.projectFinishedStatus = true;
}
}
function examPassed(detail) {
if (!detail) return false;
if (detail.projectFinishedStatus === true && detail.examId) {
}
const rec = detail.studentExamRecord;
if (!rec) return detail.projectFinishedStatus === true && !detail.examId;
const list = rec.studentExamList || rec.list || [];
if (!Array.isArray(list) || !list.length) {
const score = Number(rec.score || 0);
const need = Number(detail.projectAssessmentStandard || 80);
return score > 0 && score >= need;
}
const best = Math.max(...list.map((x) => Number(x.score || 0)));
const need = Number(detail.projectAssessmentStandard || 80);
return best >= need;
}
function bestExamScore(detail) {
const rec = detail?.studentExamRecord;
if (!rec) return null;
const list = rec.studentExamList || rec.list || [];
if (Array.isArray(list) && list.length) {
return Math.max(...list.map((x) => Number(x.score || 0)));
}
if (rec.score != null && rec.score !== "") return Number(rec.score);
return null;
}
function applyExamToProject(p, detail) {
if (!p || !detail) return p;
const examId = detail.examId || detail.exam?.id || null;
const examName = detail.examName || detail.exam?.examName || "";
const need = Number(detail.projectAssessmentStandard || 80);
const score = bestExamScore(detail);
p.examId = examId ? String(examId) : "";
p.examName = examName || "";
p.examRequired = !!examId;
p.examPassLine = need;
p.examScore = score;
p.examDone = examPassed(detail);
if (!p.examRequired) {
p.examLabel = "\u65e0\u8003\u8bd5";
} else if (p.examDone) {
p.examLabel = score != null ? `\u5df2\u901a\u8fc7 ${score}\u5206` : "\u5df2\u901a\u8fc7";
} else if (score != null) {
p.examLabel = `\u672a\u901a\u8fc7 ${score}/${need}`;
} else {
p.examLabel = `\u5f85\u8003\u8bd5\uff08≥${need}\u5206\uff09`;
}
return p;
}
function mapProjectRow(row, source) {
const progress = Number(row.studyProgress ?? 0);
const src = source === "completed" ? "completed" : "learning";
const finishedHint = src === "completed" || progress >= 100;
return {
projectId: String(row.projectId || row.id),
projectName: row.projectName || `\u9879\u76ee${row.projectId || row.id}`,
creditType: row.creditType || "",
projectCredit: row.projectCredit,
studyProgress: progress,
studyTime: Number(row.studyTime || 0),
projectImg: row.projectImg || "",
listSource: src,
sourceLabel: src === "completed" ? "\u5df2\u5b8c\u6210" : "\u6b63\u5728\u5b66",
studyLabel: finishedHint
? src === "completed"
? "\u5df2\u5b8c\u6210"
: "\u8fdb\u5ea6\u6ee1"
: progress > 0
? `\u5b66\u4e60\u4e2d ${progress}%`
: "\u672a\u5b66\u4e60",
examId: "",
examName: "",
examDone: false,
examRequired: true,
examScore: null,
examPassLine: 80,
examLabel: "\u8003\u8bd5\u52a0\u8f7d\u4e2d…",
videoFinishedStatus: finishedHint,
projectFinishedStatus: src === "completed",
raw: row,
};
}
async function _eup(force) {
if (!force && state.userProfile?.id) return state.userProfile;
const j = await apiGet(PATH_USER);
state.userProfile = j.data || null;
if (state.userProfile?.id || state.userProfile?.loginId) {
writeAuthCache({
userId: state.userProfile?.id ? String(state.userProfile.id) : undefined,
loginId: state.userProfile?.loginId ? String(state.userProfile.loginId) : undefined,
});
}
return state.userProfile;
}
async function fetchYears() {
try {
const j = await apiPost(PATH_YEARS, {});
const arr = Array.isArray(j.data) ? j.data : [];
state.yearOptions = arr.map(Number).filter((n) => n > 2000);
if (state.yearOptions.length && !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 fetchProjectListPage(path, pageNumber, pageSize) {
const body = {
pageNumber: pageNumber || 1,
pageSize: pageSize || 50,
year: state.studyYear || null,
};
const j = await apiPost(path, body);
return j.data || {};
}
async function fetchAllByPath(path, source) {
const all = [];
let page = 1;
let totalPage = 1;
do {
const data = await fetchProjectListPage(path, page, 50);
const content = Array.isArray(data.content) ? data.content : [];
all.push(...content.map((row) => mapProjectRow(row, source)));
totalPage = Math.max(1, Number(data.totalPage || 1));
page += 1;
} while (page <= totalPage && page <= 20);
return all;
}
async function _fap() {
const [learning, completed] = await Promise.all([
fetchAllByPath(PATH_LEARNING, "learning").catch((e) => {
trace(`\u6b63\u5728\u5b66\u5217\u8868\u5931\u8d25: ${e.message || e}`);
return state._capturedLists?.learning || [];
}),
fetchAllByPath(PATH_COMPLETED, "completed").catch((e) => {
trace(`\u5df2\u5b8c\u6210\u5217\u8868\u5931\u8d25: ${e.message || e}`);
return state._capturedLists?.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 fetchProjectDetail(projectId) {
const j = await apiGet(PATH_FIND + encodeURIComponent(String(projectId)));
return j.data || null;
}
async function fetchCoursewareList(projectId) {
const j = await apiGet(PATH_COURSEWARE + encodeURIComponent(String(projectId)));
return Array.isArray(j.data) ? j.data : [];
}
async function fetchVideoFinished(projectId) {
const j = await apiGet(PATH_VIDEO_DONE + encodeURIComponent(String(projectId)));
return !!(j.data && j.data.videoFinishedStatus);
}
async function submitWatch(coursewareId, projectId, studyTime, totalTime) {
const j = await apiPost(PATH_WATCH, {
coursewareId: Number(coursewareId),
projectId: String(projectId),
studyTime: Math.max(1, Math.floor(Number(studyTime) || 1)),
totalTime: Math.max(1, Math.floor(Number(totalTime) || 1)),
});
return j.data || {};
}
async function fetchExam(examId) {
const j = await apiGet(PATH_EXAM + encodeURIComponent(String(examId)));
return j.data || null;
}
function buildExamAnswers(exam) {
const questions = Array.isArray(exam?.questions) ? exam.questions : [];
return questions.map((q) => {
let sel = Array.isArray(q.correctAnswers) ? q.correctAnswers.slice() : [];
if (!sel.length && Array.isArray(q.options)) {
const marked1 = q.options.filter((o) => Number(o.isCorrect) === 1).map((o) => o.selection);
const marked0 = q.options.filter((o) => Number(o.isCorrect) === 0).map((o) => o.selection);
if (Number(q.questionType) === 0 || Number(q.questionType) === 2) {
if (marked1.length === 1) sel = marked1;
else if (marked0.length === 1) sel = marked0;
else sel = marked1.length ? [marked1[0]] : marked0.slice(0, 1);
} else {
sel = marked1.length && marked1.length <= marked0.length ? marked1 : marked0;
}
}
if (!sel.length) sel = ["A"];
return {
id: String(q.id),
questionType: String(q.questionType ?? 0),
userSelection: sel.map(String),
};
});
}
async function submitExam(projectId, exam) {
const questionList = buildExamAnswers(exam);
const j = await apiPost(PATH_SUBMIT_EXAM, {
examId: Number(exam.id),
projectId: Number(projectId),
examName: exam.examName || "",
questionList,
});
return j;
}
async function closeProjectWs() {
try {
if (state._ws) {
state._ws.close();
}
} catch (_) {}
state._ws = null;
state._wsProjectId = "";
}
async function ensureProjectWs(projectId) {
const auth = getRuntimeAuth();
const loginId = auth.loginId || state.userProfile?.loginId;
if (!loginId || !projectId) return;
if (state._ws && state._wsProjectId === String(projectId) && state._ws.readyState <= 1) return;
await closeProjectWs();
try {
let base = "wss://byljxjy.com";
try {
const j = await apiPost(PATH_WS_URL, {});
if (j.data) base = String(j.data).replace(/\/+$/, "");
} catch (_) {}
const url = `${base}${FACADE}/ws/connectWebSocket/${encodeURIComponent(loginId)}/${encodeURIComponent(projectId)}`;
const ws = new WebSocket(url);
state._ws = ws;
state._wsProjectId = String(projectId);
ws.onerror = () => trace("websocket error");
ws.onclose = () => {
if (state._ws === ws) {
state._ws = null;
state._wsProjectId = "";
}
};
} catch (e) {
trace(`ws: ${e.message || e}`);
}
}
async function _sw(projectId, ware, projectDetail) {
const coursewareId = ware.coursewareId || ware.courseware?.id;
const name = ware.coursewareName || ware.courseware?.coursewareName || coursewareId;
const total = wareTotalSeconds(ware);
const standard = progressStandard(projectDetail);
const target = Math.min(total, Math.ceil((total * Math.max(standard, EFFICIENCY_JUMP_RATIO * 100)) / 100));
let cur = wareStudySeconds(ware);
state.lastUserAction = name;
updatePanel();
for (let round = 0; round < WATCH_MAX_ROUNDS && state.enabled; round++) {
if (cur >= target) break;
const next = Math.max(cur + 1, target);
try {
const ret = await submitWatch(coursewareId, projectId, next, total);
cur = next;
if (ret.complete) break;
} catch (e) {
trace(`\u4e0a\u62a5\u5931\u8d25\uff1a${e.message || e}`);
await sleep(2000);
}
await sleep(WATCH_GAP_MS);
}
try {
const list = await fetchCoursewareList(projectId);
const fresh = list.find((x) => String(x.coursewareId) === String(coursewareId));
if (fresh && !isWareDone(fresh, projectDetail)) {
const need = Math.ceil((wareTotalSeconds(fresh) * standard) / 100);
await submitWatch(coursewareId, projectId, Math.max(need, wareTotalSeconds(fresh)), wareTotalSeconds(fresh));
}
} catch (_) {}
log(`\u8bfe\u4ef6\u300c${name}\u300d\u5df2\u5b8c\u6210`);
}
async function _sp(project) {
const projectId = project.projectId;
state.lastUserAction = project.projectName;
updatePanel();
log(`\u5f00\u59cb\uff1a${project.projectName}`);
await ensureProjectWs(projectId);
let detail = await fetchProjectDetail(projectId);
if (!detail) throw new Error("\u65e0\u6cd5\u83b7\u53d6\u9879\u76ee\u8be6\u60c5");
project.videoFinishedStatus = !!detail.videoFinishedStatus;
project.projectFinishedStatus = !!detail.projectFinishedStatus;
applyExamToProject(project, detail);
let wares = Array.isArray(detail.projectCoursewareList) && detail.projectCoursewareList.length
? detail.projectCoursewareList
: await fetchCoursewareList(projectId);
const pending = wares.filter((w) => !isWareDone(w, detail));
if (pending.length) {
if (!(await _acp())) {
state.enabled = false;
return;
}
for (const ware of pending) {
if (!state.enabled) return;
if (!(await _acp())) {
state.enabled = false;
return;
}
if (!(await _rcq())) {
state.enabled = false;
return;
}
await _sw(projectId, ware, detail);
updateCloudPanelUI();
}
}
try {
project.videoFinishedStatus = await fetchVideoFinished(projectId);
} catch (_) {}
detail = await fetchProjectDetail(projectId);
project.videoFinishedStatus = !!(detail?.videoFinishedStatus || project.videoFinishedStatus);
project.projectFinishedStatus = !!detail?.projectFinishedStatus;
if (detail) applyExamToProject(project, detail);
project.studyProgress = Number(detail?.studyProgress ?? project.studyProgress);
if (detail?.examId && !examPassed(detail) && state.enabled) {
if (await _ate()) {
state.lastUserAction = project.projectName;
updatePanel();
for (let i = 0; i < EXAM_PASS_RETRY && state.enabled; i++) {
try {
const exam = await fetchExam(detail.examId);
if (!exam?.questions?.length) break;
await submitExam(projectId, exam);
await sleep(1500);
detail = await fetchProjectDetail(projectId);
if (detail) applyExamToProject(project, detail);
if (examPassed(detail)) {
log(`${project.projectName}\u8003\u8bd5\u5df2\u5b8c\u6210`);
break;
}
} catch (e) {
trace(`\u8003\u8bd5\u5931\u8d25\uff1a${e.message || e}`);
await sleep(2000);
}
}
}
}
project.projectFinishedStatus = !!(detail && detail.projectFinishedStatus);
if (detail) applyExamToProject(project, detail);
syncProjectFinishedFlags(project);
await _rcp();
updatePanel();
}
async function courseHasPendingWork(project) {
try {
const detail = await fetchProjectDetail(project.projectId);
if (!detail) return true;
if (detail.projectFinishedStatus) return false;
const wares = detail.projectCoursewareList || (await fetchCoursewareList(project.projectId));
if (wares.some((w) => !isWareDone(w, detail))) return true;
if (detail.examId && !examPassed(detail)) return true;
return !detail.videoFinishedStatus;
} 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;
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;
await closeProjectWs();
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("#byljx-panel-body");
const footer = panel.querySelector("#byljx-panel-footer");
const extra = panel.querySelector(".byljx-footer-extra");
const btnMin = panel.querySelector("#byljx-btn-min");
const btnMax = panel.querySelector("#byljx-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("#byljx-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("#byljx-auto-panel .byljx-tab-btn").forEach((b) => {
b.classList.toggle("active", b.dataset.tab === tab);
});
document.querySelectorAll("#byljx-auto-panel .byljx-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(`\u8bfe\u4ef6\u9884\u89c8\u5931\u8d25\uff1a${e.message || e}`))
.finally(() => {
state.chapterPreviewLoading = false;
updatePanel();
});
}
}
function updateCloudPanelUI() {
const tierEl = document.getElementById("byljx-cloud-tier");
const freeEl = document.getElementById("byljx-cloud-free");
const freeLabelEl = document.getElementById("byljx-cloud-free-label");
const freeRow = document.getElementById("byljx-cloud-free-row");
const tokenInput = document.getElementById("byljx-cloud-token");
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("byljx-auto-status");
if (status) {
status.textContent = state.enabled ? "\u8fd0\u884c\u4e2d" : "\u5df2\u505c\u6b62";
status.style.color = state.enabled ? "#15803d" : "#64748b";
}
const stats = recomputeQueueStats();
const doneEl = document.getElementById("byljx-queue-done");
const totalEl = document.getElementById("byljx-queue-total");
const examEl = document.getElementById("byljx-queue-exam");
const pctEl = document.getElementById("byljx-queue-percent");
const bar = document.getElementById("byljx-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("byljx-current-user");
if (userEl) {
const u = state.userProfile;
const name = String(u?.name || "").trim();
userEl.textContent = name || "—";
}
const courseEl = document.getElementById("byljx-current-course");
const chapterEl = document.getElementById("byljx-current-chapter");
const taskEl = document.getElementById("byljx-current-task");
if (taskEl) taskEl.textContent = state.lastUserAction || "\u52fe\u9009\u9879\u76ee\u540e\u70b9\u300c\u5f00\u59cb\u300d";
if (courseEl && !state.enabled) courseEl.textContent = "\u65e0";
if (chapterEl && !state.enabled) chapterEl.textContent = "\u65e0";
const startBtn = document.getElementById("byljx-start");
const stopBtn = document.getElementById("byljx-stop");
if (startBtn) startBtn.disabled = !!state.enabled;
if (stopBtn) {
stopBtn.disabled = !state.enabled;
stopBtn.classList.toggle("byljx-btn-off", !state.enabled);
}
const qText = document.getElementById("byljx-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("byljx-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 hasToken = !!getRuntimeAuth().token;
box.innerHTML = `${
hasToken
? "\u6682\u65e0\u9879\u76ee\u3002\u8bf7\u786e\u8ba4\u5e74\u5ea6\u7b5b\u9009\uff0c\u6216\u5230\u5b98\u7f51\u300c\u6b63\u5728\u5b66 / \u5df2\u5b8c\u6210\u300d\u70b9\u4e00\u4e0b\u540e\u518d\u5237\u65b0\u3002"
: "\u8bf7\u5148\u767b\u5f55\u5b98\u7f51\uff0c\u6253\u5f00\u300c\u4e2a\u4eba\u4e2d\u5fc3\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(`\u8bfe\u4ef6\u9884\u89c8\u5931\u8d25\uff1a${e.message || e}`))
.finally(() => {
state.chapterPreviewLoading = false;
updatePanel();
});
});
});
}
function renderChapterPreview() {
const box = document.getElementById("byljx-chapter-preview");
if (!box) return;
const q = loadQueue().map(String).filter(Boolean);
if (!q.length) {
box.innerHTML = `\u8bf7\u52fe\u9009\u9879\u76ee
`;
return;
}
if (state.chapterPreviewLoading && !state.chapterPreview.length) {
box.innerHTML = `\u6b63\u5728\u52a0\u8f7d\u8bfe\u4ef6\u4e0e\u8003\u8bd5\u4fe1\u606f…
`;
return;
}
if (!state.chapterPreview.length) {
box.innerHTML = `\u5df2\u52fe\u9009 ${q.length} \u9879\uff0c\u4f46\u672a\u8bfb\u5230\u8bfe\u4ef6\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
? p.examRequired
? `${p.examName ? escHtml(p.examName) + " · " : ""}${escHtml(p.examLabel || "\u5f85\u67e5")}`
: "\u65e0\u8003\u8bd5"
: "\u8003\u8bd5\u4fe1\u606f\u52a0\u8f7d\u4e2d…";
const wareDone = items.filter((x) => x.done).length;
html.push(`
${escHtml(title)}
\u8003\u8bd5\uff1a${examLine}
\u8bfe\u4ef6 ${wareDone}/${items.length}
${items
.map((c) => {
if (c.kind === "exam") return "";
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\u8bfe\u4ef6
`;
}
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\u9879\u76ee\uff0c\u8bf7\u5148\u5237\u65b0\u9879\u76ee\u5217\u8868\uff09",
studied: 0,
total: 0,
done: false,
kind: "ware",
});
continue;
}
try {
const detail = await fetchProjectDetail(id);
if (!detail) throw new Error("\u8be6\u60c5\u4e3a\u7a7a");
applyExamToProject(p, detail);
p.videoFinishedStatus = !!detail.videoFinishedStatus;
p.projectFinishedStatus = !!detail.projectFinishedStatus;
p.studyProgress = Number(detail.studyProgress ?? p.studyProgress);
if (p.listSource !== "completed" && !p.projectFinishedStatus) {
p.studyLabel =
p.studyProgress >= 100
? "\u8fdb\u5ea6\u6ee1"
: p.studyProgress > 0
? `\u5b66\u4e60\u4e2d ${p.studyProgress}%`
: "\u672a\u5b66\u4e60";
}
syncProjectFinishedFlags(p);
let wares = Array.isArray(detail.projectCoursewareList) ? detail.projectCoursewareList : [];
if (!wares.length) {
try {
wares = await fetchCoursewareList(id);
} catch (_) {}
}
if (!wares.length) {
rows.push({
projectId: id,
projectName: p.projectName,
name: "\u672a\u83b7\u53d6\u5230\u8bfe\u4ef6\u5217\u8868",
studied: 0,
total: 0,
done: false,
kind: "ware",
});
} else {
for (const w of wares) {
const done = isWareDone(w, detail);
const total = wareTotalSeconds(w);
rows.push({
projectId: id,
projectName: p.projectName,
name: w.coursewareName || w.courseware?.coursewareName || String(w.coursewareId),
studied: wareDisplayStudiedSeconds(w, detail),
total,
done,
kind: "ware",
});
}
}
} 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",
});
p.examLabel = "\u8003\u8bd5\u8bfb\u53d6\u5931\u8d25";
}
}
state.chapterPreview = rows;
}
async function enrichExamStatusForProjects(list, limit) {
const targets = (list || []).slice(0, Math.max(1, limit || 12));
for (const p of targets) {
if (!p || p._examEnriched) continue;
try {
const detail = await fetchProjectDetail(p.projectId);
if (detail) {
applyExamToProject(p, detail);
p.videoFinishedStatus = !!detail.videoFinishedStatus;
p.projectFinishedStatus = !!detail.projectFinishedStatus;
if (detail.studyProgress != null) {
p.studyProgress = Number(detail.studyProgress);
if (p.listSource !== "completed" && !p.projectFinishedStatus) {
p.studyLabel =
p.studyProgress >= 100
? "\u8fdb\u5ea6\u6ee1"
: p.studyProgress > 0
? `\u5b66\u4e60\u4e2d ${p.studyProgress}%`
: "\u672a\u5b66\u4e60";
}
}
syncProjectFinishedFlags(p);
} else {
p.examLabel = "\u65e0\u8be6\u60c5";
}
} catch (_) {
p.examLabel = "\u8bfb\u53d6\u5931\u8d25";
}
p._examEnriched = true;
updatePanel();
await sleep(120);
}
}
async function _rpp(opt) {
const silent = !!opt?.silent;
state.panelRefreshing = true;
updatePanel();
try {
for (let i = 0; i < 8 && !getRuntimeAuth().token; i++) {
await sleep(400);
}
if (!getRuntimeAuth().token) {
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\u767b\u5f55 Token\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55\u540e\u70b9\u5237\u65b0");
}
try {
await _eup(false);
} catch (e) {
trace(`\u7528\u6237\u4fe1\u606f: ${e.message || e}`);
}
await fetchYears();
state.panelProjects = await _fap();
if (!state.panelProjects.length) {
mergeCapturedIntoPanel(true);
}
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(`\u8bfe\u4ef6\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 = "byljx-panel-style-v2";
if (document.getElementById(id)) return;
document.getElementById("byljx-panel-style-v1")?.remove();
const style = document.createElement("style");
style.id = id;
style.textContent = `
#byljx-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);}
#byljx-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;}
#byljx-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;}
#byljx-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:cover;display:block;flex:0 0 auto;background:#e2e8f0;}
#byljx-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;}
#byljx-panel-sub{margin-top:3px;font-size:11px;color:#7c2d12;font-weight:700;}
#byljx-panel-controls{display:flex;gap:5px;}
.byljx-panel-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;font-size:15px;font-weight:900;}
#byljx-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;}
#byljx-panel-footer{padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;}
.byljx-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;}
.byljx-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;}
.byljx-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;}
.byljx-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;}
.byljx-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;}
.byljx-status-metrics{font-size:12px;color:#475569;}
.byljx-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;}
.byljx-progress-pct{font-size:14px;font-weight:900;color:#0369a1;}
.byljx-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;}
.byljx-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;}
.byljx-tabbar{display:flex;gap:6px;margin-bottom:7px;}
.byljx-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;}
.byljx-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;}
.byljx-pane{display:none;}.byljx-pane.active{display:block;}
.byljx-list-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;gap:6px;flex-wrap:wrap;}
.byljx-list-title{font-size:12px;color:#64748b;font-weight:700;}
.byljx-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;}
.byljx-list-head-actions{display:flex;gap:6px;align-items:center;flex-wrap:nowrap;justify-content:flex-start;flex:1 1 100%;}
.byljx-list-head-actions .byljx-btn{flex:0 0 auto !important;width:auto;min-width:0;white-space:nowrap;padding:4px 8px;font-size:11px;line-height:1.2;}
.byljx-btn{border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;}
.byljx-btn-ghost{background:#fff !important;color:#0f172a !important;border:1px solid #cbd5e1;}
.byljx-btn-start{flex:1;background:#16a34a;}
.byljx-btn-stop{flex:1;background:#ef4444;}
.byljx-btn-off,.byljx-btn:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;}
.byljx-btn-row{display:flex;gap:7px;}
.byljx-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;}
#byljx-course-list,#byljx-chapter-preview,#byljx-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;}
.byljx-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:700;}
.byljx-course-item,.byljx-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;}
.byljx-course-name{font-size:12px;line-height:1.38;font-weight:700;display:flex;align-items:flex-start;gap:6px;flex-wrap:wrap;}
.byljx-course-meta{font-size:11px;color:#64748b;margin-top:2px;line-height:1.4;}
.byljx-badge{font-size:10px;font-weight:800;padding:2px 6px;border-radius:999px;border:1px solid;line-height:1.3;}
.byljx-badge-learn{color:#1d4ed8;background:#eff6ff;border-color:#93c5fd;}
.byljx-badge-done{color:#047857;background:#ecfdf5;border-color:#6ee7b7;}
.byljx-preview-group{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;padding:0;}
.byljx-preview-hd{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;}
.byljx-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;}
.byljx-preview-group .byljx-chapter-item{margin:0 6px 6px;background:#f8fafc;}
.byljx-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:11px;margin-bottom:4px;}
.byljx-meta-label{color:#64748b;}.byljx-meta-value{font-weight:700;text-align:right;max-width:68%;word-break:break-all;}
.byljx-log-line{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;}
#byljx-year-select{border:1px solid #cbd5e1;border-radius:8px;padding:3px 6px;font-size:12px;margin-left:4px;}
#byljx-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;}
.byljx-pro-card{width:min(420px,94vw);background:#fff;border-radius:14px;padding:16px;box-shadow:0 20px 50px rgba(0,0,0,.25);}
.byljx-pro-title{font-size:18px;font-weight:900;color:#9a3412;}
.byljx-pro-sub{margin-top:6px;font-size:12px;color:#78716c;line-height:1.45;}
.byljx-pro-sec{margin-top:12px;}
.byljx-pro-sec h4{margin:0 0 6px;font-size:13px;color:#0f172a;}
.byljx-pro-sec ul{margin:0;padding-left:18px;font-size:12px;color:#334155;line-height:1.55;}
.byljx-pro-sec p{margin:0;font-size:12px;color:#475569;line-height:1.5;}
.byljx-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%;}
.byljx-pro-actions{margin-top:14px;display:flex;justify-content:flex-end;}
.byljx-pro-close{border:1px solid #cbd5e1;background:#f8fafc;border-radius:8px;padding:7px 12px;cursor:pointer;font-weight:700;}
#byljx-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;}
#byljx-toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
`;
(document.head || document.documentElement).appendChild(style);
}
function _cp() {
if (document.getElementById("byljx-auto-panel")) return;
injectPanelStyles();
const panel = document.createElement("div");
panel.id = "byljx-auto-panel";
panel.innerHTML = `
\u8fd0\u884c\u72b6\u6001
\u5df2\u505c\u6b62
\u8bfe\u4ef6 0/0 · \u5168\u5b8c\u6210 —
0%
\u9879\u76ee\u5e74\u4efd
\u540c\u65f6\u8bfb\u53d6\u5b98\u7f51\u300c\u6b63\u5728\u5b66\u300d\u4e0e\u300c\u5df2\u5b8c\u6210\u300d\u3002\u82e5\u5217\u8868\u4e3a\u7a7a\uff1a\u5148\u6253\u5f00\u4e2a\u4eba\u4e2d\u5fc3\u5bf9\u5e94\u9875\u7b7e\uff0c\u518d\u70b9\u5237\u65b0\u3002
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d
\u7ae0\u8282\u9884\u89c8\u8bfe\u4ef6
\u8bf7\u52fe\u9009\u9879\u76ee
\u8fd0\u884c\u65e5\u5fd7\u8fdb\u5ea6
\u4f1a\u5458\u8bbe\u7f6e
\u4f1a\u5458\u72b6\u6001—
\u6709\u6548\u671f—
\u6388\u6743\u7801
\u5f53\u524d\u7528\u6237—
\u5f53\u524d\u4efb\u52a1\u52fe\u9009\u9879\u76ee\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("#byljx-btn-min")?.addEventListener("click", (e) => {
e.stopPropagation();
writePanelCollapsed(true);
applyPanelCollapsed(panel, true);
});
panel.querySelector("#byljx-btn-max")?.addEventListener("click", (e) => {
e.stopPropagation();
writePanelCollapsed(false);
applyPanelCollapsed(panel, false);
});
panel.querySelectorAll(".byljx-tab-btn").forEach((btn) => {
btn.addEventListener("click", () => switchPanelTab(btn.dataset.tab));
});
const yearSel = panel.querySelector("#byljx-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("byljx-refresh")?.addEventListener("click", () => {
void _rpp({ reason: "manual" }).then(() => panel.__fillYears?.());
});
document.getElementById("byljx-select-unfinished")?.addEventListener("click", () => selectUnfinishedProjects());
document.getElementById("byljx-clear-select")?.addEventListener("click", () => clearProjectSelection());
document.getElementById("byljx-cloud-save")?.addEventListener("click", () => {
const input = document.getElementById("byljx-cloud-token");
void _sct(input?.value || "").then((ok) => {
updateCloudPanelUI();
showToast(ok ? "\u5df2\u4fdd\u5b58" : "\u4fdd\u5b58\u5931\u8d25");
});
});
document.getElementById("byljx-open-pro")?.addEventListener("click", () => openProModal());
document.getElementById("byljx-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\u9879\u76ee");
showToast("\u8bf7\u5148\u52fe\u9009\u9879\u76ee");
updatePanel();
return;
}
void (async () => {
try {
await _ecl(true);
} catch (_) {}
if (!(await _acp())) {
updatePanel();
return;
}
state.enabled = true;
state.lastUserAction = "";
updatePanel();
void _rse();
})();
});
document.getElementById("byljx-stop")?.addEventListener("click", () => {
state.enabled = false;
state.lastUserAction = "";
updatePanel();
});
updatePanel();
void fetchClientConfig();
}
async function _bst() {
if (!/byljxjy\.com$/i.test(location.hostname) && !/\.byljxjy\.com$/i.test(location.hostname)) return;
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 (getRuntimeAuth().token) {
await _rpp({ reason: "retry", silent: true });
break;
}
}
}
const panel = document.getElementById("byljx-auto-panel");
panel?.__fillYears?.();
state._bootLoaded = true;
if (!getRuntimeAuth().token) {
trace("\u672a\u767b\u5f55\uff0c\u8bf7\u6253\u5f00\u4e2a\u4eba\u4e2d\u5fc3\u540e\u5237\u65b0");
} else if (!state.panelProjects.length) {
trace("\u5217\u8868\u4e3a\u7a7a\uff0c\u8bf7\u5207\u6362\u6b63\u5728\u5b66/\u5df2\u5b8c\u6210\u9875\u7b7e\u540e\u5237\u65b0");
}
}
_bst().catch((e) => trace(e));
})();