// ==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

\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 = `
\u767d\u7389\u5170\u8fdc\u7a0b\u533b\u5b66\u7f51\u5b66\u4e60\u52a9\u624b v${SCRIPT_VERSION}
\u81ea\u52a8\u770b\u8bfe\u4e0e\u8003\u8bd5
\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
\u6682\u65e0\u65e5\u5fd7
\u4f1a\u5458\u8bbe\u7f6e
\u4f1a\u5458\u72b6\u6001
\u6388\u6743\u7801
\u5f53\u524d\u7528\u6237
\u5f53\u524d\u4efb\u52a1\u52fe\u9009\u9879\u76ee\u540e\u70b9\u300c\u5f00\u59cb\u300d
`; 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)); })();