// ==UserScript== // @name 湖北专业技术人员继续教育公需科目(hb12333.com)学习助手 // @namespace https://card.wlxy.live/details/HBZJ0001 // @version 1.2 // @description 湖北省专业技术人员继续教育公需科目hbzsgx.hb12333.com跳转hbzj-train.yxlearning.com平台学习助手:一键自动看课与考试;官方1:1全自动课时完成,标准用户可体验3个视频章节,会员30天不限量。 // @author 柠檬真酸 // @match https://hbzj-train.yxlearning.com/* // @match https://*.yxlearning.com/* // @match https://hbzsgx.hb12333.com/* // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @connect hbzj-train.yxlearning.com // @connect yxlearning.com // @connect oa23.ahzsksw.cn // @connect huaweicloudobs.ahjxjy.cn // @connect card.wlxy.live // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-start // @antifeature payment 标准用户可体验3个视频章节,开通会员不限量 // @antifeature membership 需授权校验 // @license All Rights Reserved // ==/UserScript== (function () { "use strict"; const SCRIPT_VERSION = "1.2"; const PAGE = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; const PANEL_LOGO_URL = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const FALLBACK_ORIGIN = "https://hbzj-train.yxlearning.com"; const API_PREFIX = "/train"; const PATH_ACCOUNT_INFO = `${API_PREFIX}/login/get-account-info.gson`; const PATH_STUDY_LIST = `${API_PREFIX}/cms/study/find-my-study-info.gson`; const PATH_STUDY_YEARS = `${API_PREFIX}/cms/study/find-my-study-info-yearlist.gson`; const PATH_CLASS_YEARS = `${API_PREFIX}/cms/study/get-class-year-info.gson`; const PATH_MY_CLASS = `${API_PREFIX}/class/my-class.gson`; const PATH_MY_CLASS_BOOKS = `${API_PREFIX}/class/my-class-books.gson`; const PATH_VIDEO_SV = `${API_PREFIX}/cms/my-video/sv.gson`; const PATH_VIDEO_CV = `${API_PREFIX}/cms/my-video/cv.gson`; const PATH_UPDATE_LAST_VIDEO = `${API_PREFIX}/cms/my-video/update-last-video.gson`; const PATH_EXAM_LIST = `${API_PREFIX}/cms/paper/find-paper-list.gson`; const PATH_EXAM_DETAIL = `${API_PREFIX}/cms/paper/get-paper-detail.gson`; const PATH_EXAM_CHECK_STATUS = `${API_PREFIX}/cms/paper/check-my-exam-status.gson`; const PATH_EXAM_SAVE_RECORD = `${API_PREFIX}/cms/paper/save-my-exam-record.gson`; const PATH_EXAM_START = `${API_PREFIX}/cms/paper/start-do-paper-or-test.gson`; const PATH_EXAM_SUBMIT = `${API_PREFIX}/cms/paper/submit-paper.gson`; const CMS_ACCOUNT_KEY = "cmsAccountInfo"; const PINNED_CLOUD_HOST = "oa23.ahzsksw.cn"; const DEFAULT_CLOUD_API_BASE = "https://oa23.ahzsksw.cn"; const CLOUD_TOKEN_KEY = "hbzj_cloud_token_v1"; const CLOUD_LEASE_CACHE_KEY = "hbzj_cloud_lease_cache_v1"; const CLOUD_API_BASE_KEY = "hbzj_cloud_api_base_v1"; const DEFAULT_FREE_CHAPTER_LIMIT = 3; const PRO_BUY_URL = "https://card.wlxy.live/details/3D736BD3"; const PRO_DAYS = 30; const PANEL_NOTICE_FALLBACK = "\u6e56\u5317\u4e13\u6280\u57f9\u8bad\u5b66\u4e60\u52a9\u624b"; const DEFAULT_PANEL_NOTICE_PATH = "/api/hbzj/panel-notice"; const QUEUE_KEY = "hbzj_project_queue_v1"; const PANEL_POS_KEY = "hbzj_panel_pos_v1"; const PANEL_COLLAPSED_KEY = "hbzj_panel_collapsed_v1"; const AUTH_CACHE_KEY = "hbzj_auth_cache_v1"; const YEAR_KEY = "hbzj_year_v1"; const EFFICIENCY_JUMP_RATIO = 0.99; const CV_INTERVAL_MS = 120000; const CV_STEP_SECONDS = 120; const SV_BEFORE_CV_MS = 5000; const WATCH_MAX_ROUNDS = 200; const ENGINE_TICK_MS = 1200; const EXAM_PASS_RETRY = 2; const CV_INTERVAL_MINUTES = CV_INTERVAL_MS / 60000; const state = { enabled: false, engineRunning: false, panelProjects: [], panelRefreshing: false, chapterPreview: [], chapterPreviewLoading: false, queueWareTotal: 0, queueWareDone: 0, userProfile: null, panelHint: "", lastUserAction: "", runLogs: [], studyYear: Number( (typeof unsafeWindow !== "undefined" ? unsafeWindow : window).localStorage.getItem(YEAR_KEY) || new Date().getFullYear() ) || null, yearOptions: [], cloudToken: String( (typeof unsafeWindow !== "undefined" ? unsafeWindow : window).localStorage.getItem(CLOUD_TOKEN_KEY) || "" ).trim(), cloudLease: "", cloudLeaseExp: 0, cloudTier: "unknown", cloudProExpireAt: 0, cloudRevoked: false, freeChapterLimit: DEFAULT_FREE_CHAPTER_LIMIT, freeUsedChapters: 0, proBuyUrl: PRO_BUY_URL, cloudApiBase: "", panelNoticePath: DEFAULT_PANEL_NOTICE_PATH, remotePanelNotice: "", _lastProErr: "", _bootLoaded: false, _capturedLists: { learning: [], completed: [] }, _accountId: "", }; state.cloudApiBase = resolvePinnedCloudApiBase(); function trace(msg) { console.log(`[\u6e56\u5317\u4e13\u6280\u52a9\u624b] ${String(msg || "")}`); } function escHtml(s) { return String(s ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function pageStorage() { try { return PAGE.localStorage; } catch (_) { return localStorage; } } function readJson(key, fallback) { try { const raw = pageStorage().getItem(key); return raw ? JSON.parse(raw) : fallback; } catch (_) { return fallback; } } function writeJson(key, val) { pageStorage().setItem(key, JSON.stringify(val)); } function detectLogKind(msg, kind) { if (kind) return String(kind); const s = String(msg || ""); if (/^\u5f00\u59cb\uff1a/.test(s)) return "start"; if (/^\u5b66\u4e60\uff1a/.test(s)) return "study"; if (/^\u5b8c\u6210\uff1a/.test(s)) return "done"; if (/^\u8003\u8bd5\uff1a/.test(s) || /\u8003\u8bd5.*\u5df2\u63d0\u4ea4/.test(s)) return "exam"; if (/^\u5df2\u505c\u6b62/.test(s)) return "stop"; if (/\u6bcf.*\u5206\u949f|\u8010\u5fc3\u7b49\u5f85|\u4ecd\u5728\u8fd0\u884c/.test(s)) return "hint"; if (/\u5931\u8d25|\u9519\u8bef|\u4e0a\u9650|\u8df3\u8fc7|\u65e0\u89c6\u9891\u6e90|\u7535\u5b50\u4e66|\u8bf7\u5148\u52fe\u9009/.test(s)) return "warn"; return "info"; } function _rrl() { const el = document.getElementById("hbzj-run-log"); if (!el) return; if (!state.runLogs.length) { el.innerHTML = `
\u6682\u65e0\u65e5\u5fd7
`; return; } el.innerHTML = state.runLogs .map((item) => { const row = typeof item === "string" ? { time: "", msg: item, kind: detectLogKind(item) } : item; const time = row.time ? `${escHtml(row.time)}` : ""; const kind = escHtml(row.kind || "info"); return `
${time}${escHtml(row.msg || "")}
`; }) .join(""); } function log(msg, kind) { const text = String(msg || ""); const row = { time: new Date().toLocaleTimeString(), msg: text, kind: detectLogKind(text, kind), }; state.runLogs.unshift(row); if (state.runLogs.length > 200) state.runLogs.length = 200; _rrl(); trace(text); } function _lwh() { if (state._studyWaitHintShown) return; state._studyWaitHintShown = true; log(`\u770b\u8bfe\u8fdb\u5ea6\u7ea6\u6bcf ${CV_INTERVAL_MINUTES} \u5206\u949f\u66f4\u65b0\u4e00\u6b21\uff0c\u7b49\u5f85\u671f\u95f4\u811a\u672c\u4ecd\u5728\u8fd0\u884c`, "hint"); } function resolvePinnedCloudApiBase() { try { const raw = String(pageStorage().getItem(CLOUD_API_BASE_KEY) || DEFAULT_CLOUD_API_BASE) .trim() .replace(/\/+$/, ""); if (!raw || !new RegExp(PINNED_CLOUD_HOST.replace(/\./g, "\\."), "i").test(raw)) { try { pageStorage().setItem(CLOUD_API_BASE_KEY, DEFAULT_CLOUD_API_BASE); } catch (_) {} return DEFAULT_CLOUD_API_BASE; } return raw; } catch (_) { return DEFAULT_CLOUD_API_BASE; } } function getCloudApiBase() { return String(state.cloudApiBase || DEFAULT_CLOUD_API_BASE).trim().replace(/\/+$/, ""); } function getPanelNoticeText() { return String(state.remotePanelNotice || PANEL_NOTICE_FALLBACK).trim() || PANEL_NOTICE_FALLBACK; } function renderPanelNotice() { const el = document.getElementById("hbzj-ann-text"); if (el) el.textContent = getPanelNoticeText(); } function getApiOrigin() { try { if (/yxlearning\.com$/i.test(location.hostname)) return location.origin; } catch (_) {} return FALLBACK_ORIGIN; } function uuidv4() { try { if (PAGE.crypto && typeof PAGE.crypto.randomUUID === "function") return PAGE.crypto.randomUUID(); } catch (_) {} const s = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === "x" ? r : (r & 0x3) | 0x8; return v.toString(16); }); return s; } function toQuery(params) { const parts = []; Object.keys(params || {}).forEach((k) => { const v = params[k]; if (v === undefined || v === null) return; parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`); }); return parts.join("&"); } function toFormBody(params) { return toQuery(params); } function readCmsAccountInfo() { try { const raw = pageStorage().getItem(CMS_ACCOUNT_KEY); if (!raw || raw === "null") return null; const obj = JSON.parse(raw); return obj && typeof obj === "object" ? obj : null; } catch (_) { return null; } } function rememberAccountId(id) { const aid = String(id || "").trim(); if (!aid || aid.length < 8) return; if (state._accountId === aid && PAGE.__hbzjAccountId === aid) return; state._accountId = aid; PAGE.__hbzjAccountId = aid; writeAuthCache({ accountId: aid }); } function getAccountId() { if (state._accountId) return state._accountId; if (PAGE.__hbzjAccountId) { state._accountId = String(PAGE.__hbzjAccountId); return state._accountId; } const cms = readCmsAccountInfo(); if (cms?.accountId) { rememberAccountId(cms.accountId); return state._accountId; } const cache = readAuthCache(); if (cache.accountId) { state._accountId = String(cache.accountId); return state._accountId; } return ""; } function readAuthCache() { return readJson(AUTH_CACHE_KEY, {}) || {}; } function writeAuthCache(partial) { const prev = readAuthCache(); try { pageStorage().setItem(AUTH_CACHE_KEY, JSON.stringify(Object.assign({}, prev, partial || {}))); } catch (_) {} } function hasPlatformAuth() { return !!getAccountId() || !!readCmsAccountInfo(); } function unwrapPlatformJson(json) { if (!json || typeof json !== "object") { return { code: -1, msg: "empty", data: null, raw: json }; } if (json.respCode != null && json.attribute !== undefined) { return { code: Number(json.respCode), msg: String(json.respDesc || json.msg || ""), data: json.attribute, raw: json, }; } return { code: Number(json.code != null ? json.code : json.respCode), msg: String(json.msg || json.respDesc || ""), data: json.data !== undefined ? json.data : json.attribute, raw: json, }; } function pickList(data) { if (Array.isArray(data)) return data; if (!data || typeof data !== "object") return []; if (Array.isArray(data.list)) return data.list; if (data.listPage && Array.isArray(data.listPage.list)) return data.listPage.list; if (data.data && Array.isArray(data.data.list)) return data.data.list; if (Array.isArray(data.myClassCourseRPList)) return data.myClassCourseRPList; if (Array.isArray(data.data)) return data.data; if (Array.isArray(data.records)) return data.records; if (Array.isArray(data.content)) return data.content; if (Array.isArray(data.rows)) return data.rows; if (Array.isArray(data.courseData)) return data.courseData; if (data.data && typeof data.data === "object") return pickList(data.data); return []; } function ingestStudyListPayload(url, json) { try { if (!/find-my-study-info\.gson/i.test(url)) return; const un = unwrapPlatformJson(json); if (Number(un.code) !== 200) return; const rows = pickList(un.data); if (!rows.length) return; const learning = []; const completed = []; for (const row of rows) { const p = mapProjectRow(row); if (p.listSource === "completed") completed.push(p); else learning.push(p); } const bag = state._capturedLists || { learning: [], completed: [] }; const merge = (src, arr) => { const byId = new Map((bag[src] || []).map((x) => [x.projectId, x])); for (const p of arr) byId.set(p.projectId, p); bag[src] = Array.from(byId.values()); }; merge("learning", learning); merge("completed", completed); state._capturedLists = bag; if (document.getElementById("hbzj-auto-panel") && !state.panelRefreshing) { mergeCapturedIntoPanel(false); } } catch (_) {} } function mergeCapturedIntoPanel(preferApi) { const bag = state._capturedLists || { learning: [], completed: [] }; if (!bag.learning.length && !bag.completed.length) return false; const byId = new Map(); for (const p of bag.learning || []) byId.set(p.projectId, p); for (const p of bag.completed || []) byId.set(p.projectId, p); const merged = Array.from(byId.values()); for (const p of merged) syncProjectFinishedFlags(p); if (!preferApi || !state.panelProjects.length) { state.panelProjects = merged; updatePanel(); } return state.panelProjects.length > 0; } function installAuthHooks() { if (PAGE.__hbzjNetHooked) return; PAGE.__hbzjNetHooked = true; try { const XHR = PAGE.XMLHttpRequest; const open = XHR.prototype.open; const send = XHR.prototype.send; const setHeader = XHR.prototype.setRequestHeader; XHR.prototype.open = function (method, url) { try { this.__hbzjUrl = String(url || ""); this.__hbzjMethod = String(method || ""); } catch (_) {} return open.apply(this, arguments); }; XHR.prototype.setRequestHeader = function (k, v) { try { if (/^accountid$/i.test(String(k || ""))) rememberAccountId(v); } catch (_) {} return setHeader.apply(this, arguments); }; XHR.prototype.send = function () { try { this.addEventListener("load", function () { try { const url = String(this.__hbzjUrl || ""); const text = String(this.responseText || ""); if (!text || text.length > 5e6) return; const json = JSON.parse(text); if (/get-account-info\.gson/i.test(url)) { const un = unwrapPlatformJson(json); const info = un.data?.data || un.data || null; if (info?.accountId) rememberAccountId(info.accountId); if (info) { state.userProfile = normalizeUserProfile(info); } } if (/yearlist|get-class-year-info/i.test(url)) { const years = extractYears(unwrapPlatformJson(json).data); if (years.length) state.yearOptions = years; } ingestStudyListPayload(url, json); } catch (_) {} }); } catch (_) {} return send.apply(this, arguments); }; } catch (_) {} try { const rawFetch = PAGE.fetch; if (typeof rawFetch === "function" && !rawFetch.__hbzjPatched) { const wrapped = function (input, init) { try { const h = init && init.headers; let aid = ""; if (h && typeof h.get === "function") aid = h.get("accountId") || h.get("AccountId") || ""; else if (h && typeof h === "object") aid = h.accountId || h.AccountId || ""; if (aid) rememberAccountId(aid); } catch (_) {} const url = typeof input === "string" ? input : input && input.url; return rawFetch.apply(PAGE, arguments).then((res) => { try { const u = String(url || ""); if (/find-my-study-info\.gson|get-account-info\.gson/i.test(u)) { res .clone() .json() .then((json) => { if (/get-account-info/i.test(u)) { const un = unwrapPlatformJson(json); const info = un.data?.data || un.data || null; if (info?.accountId) rememberAccountId(info.accountId); } ingestStudyListPayload(u, json); }) .catch(() => {}); } } catch (_) {} return res; }); }; wrapped.__hbzjPatched = true; PAGE.fetch = wrapped; } } catch (_) {} } function apiUrl(path) { if (/^https?:\/\//i.test(path)) return path; return getApiOrigin() + (path.startsWith("/") ? path : `/${path}`); } async function _prq(method, path, body, opt) { const accountId = getAccountId(); if (!accountId) throw new Error("\u672a\u83b7\u53d6 accountId\uff0c\u8bf7\u5148\u767b\u5f55\u5b98\u7f51\u540e\u70b9\u300c\u5237\u65b0\u300d"); const url = apiUrl(path); const headers = Object.assign( { Accept: "application/json, text/plain, */*", accountId: accountId, }, (opt && opt.headers) || {} ); let payload = undefined; const m = String(method || "GET").toUpperCase(); if (body !== undefined && body !== null && m !== "GET") { if (opt && opt.form) { headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8"; payload = typeof body === "string" ? body : toFormBody(body); } else { headers["Content-Type"] = "application/json;charset=UTF-8"; payload = typeof body === "string" ? body : JSON.stringify(body); } } const parseOk = (json, status) => { if (status < 200 || status >= 300) { throw new Error(json?.msg || json?.respDesc || json?.message || `HTTP ${status}`); } const un = unwrapPlatformJson(json); if (Number(un.code) !== 200) { throw new Error(un.msg || `code_${un.code}`); } return un; }; try { const pageFetch = PAGE.fetch || fetch; const res = await pageFetch.call(PAGE, url, { method: m, headers, body: payload, credentials: "include", }); const json = await res.json().catch(() => ({})); return parseOk(json, res.status); } catch (e) { const msg = String((e && e.message) || e || ""); if (!/Failed to fetch|NetworkError|CORS/i.test(msg) && /\u672a\u83b7\u53d6|HTTP |code_|accountId/i.test(msg)) { throw e; } trace(`\u9875\u9762 fetch \u5931\u8d25\uff0c\u6539 GM\uff1a${msg}`); return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== "function") { reject(e); return; } GM_xmlhttpRequest({ method: m, url, headers, data: payload, anonymous: false, onload(resp) { let json = {}; try { json = JSON.parse(resp.responseText || "{}"); } catch (_) {} try { resolve(parseOk(json, resp.status)); } catch (err) { reject(err); } }, onerror() { reject(new Error("GM \u7f51\u7edc\u9519\u8bef")); }, }); }); } } function apiGet(path) { return _prq("GET", path, null); } function apiPostForm(path, body) { return _prq("POST", path, body, { form: true }); } function apiPostJson(path, body) { return _prq("POST", path, body == null ? {} : body, { form: false }); } function normalizeUserProfile(info) { if (!info || typeof info !== "object") return null; return { accountId: String(info.accountId || ""), name: String(info.realName || info.name || info.nickName || info.userName || "").trim(), loginId: String(info.loginName || info.userName || info.account || info.mobile || "").trim(), phone: String(info.mobile || info.phone || "").trim(), raw: info, }; } function extractYears(data) { const arr = pickList(data); const years = []; for (const item of arr) { if (typeof item === "number" || /^\d{4}$/.test(String(item))) { years.push(Number(item)); } else if (item && typeof item === "object") { const y = Number(item.year || item.studyYear || item.classYear || item.value || 0); if (y > 2000) years.push(y); } } return Array.from(new Set(years.filter((n) => n > 2000))).sort((a, b) => b - a); } function getLearningUserId() { return String( getAccountId() || state.userProfile?.accountId || readAuthCache().accountId || "" ).trim(); } function formatCloudTierText(tier) { if (state.cloudRevoked) return "\u5df2\u505c\u7528"; const t = String(tier || "").toLowerCase(); if (t === "pro") return "\u5df2\u5f00\u901a"; if (t === "free") return "\u6807\u51c6\u7528\u6237"; if (t === "revoked") return "\u5df2\u505c\u7528"; if (t === "unknown") return "—"; return tier ? String(tier) : "—"; } function formatExpireText(ts) { const n = Number(ts || 0); if (!n) return "—"; const d = new Date(n * 1000); return Number.isNaN(d.getTime()) ? "—" : d.toLocaleString(); } function isCloudProTier() { return !state.cloudRevoked && String(state.cloudTier || "").toLowerCase() === "pro"; } function readCloudLeaseCache() { try { const raw = pageStorage().getItem(CLOUD_LEASE_CACHE_KEY); if (!raw) return null; const p = JSON.parse(raw); const lease = String(p.lease || "").trim(); const exp = Number(p.exp || 0); if (!lease || !Number.isFinite(exp) || exp <= 0) return null; return { lease, exp, tier: p.tier || "", proExpireAt: Number(p.proExpireAt || 0), }; } catch (_) { return null; } } function writeCloudLeaseCache(lease, exp, extra) { if (!lease || !exp) { pageStorage().removeItem(CLOUD_LEASE_CACHE_KEY); return; } pageStorage().setItem( CLOUD_LEASE_CACHE_KEY, JSON.stringify(Object.assign({ lease, exp }, extra || {})) ); } function _crq(path, method, body, headersExtra) { const url = `${getCloudApiBase()}${path}`; const headers = Object.assign( { Accept: "application/json", "Content-Type": "application/json;charset=UTF-8", }, headersExtra || {} ); if (state.cloudToken) headers.Authorization = `Bearer ${state.cloudToken}`; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; const payload = body == null ? null : JSON.stringify(body); return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== "function") { reject(new Error("\u4e91\u7aef\u8bf7\u6c42\u4e0d\u53ef\u7528")); return; } GM_xmlhttpRequest({ method: method || "POST", url, headers, data: payload, onload(resp) { let data = {}; try { data = JSON.parse(String(resp.responseText || "").trim() || "{}"); } catch (_) { data = {}; } const status = Number(resp.status || 0); if (status === 404) { reject(new Error("cloud_404")); return; } if (status < 200 || status >= 300) { reject(new Error(String(data.detail || data.message || data.msg || `http_${status}`))); return; } resolve(data); }, onerror() { reject(new Error("\u4e91\u7aef\u7f51\u7edc\u9519\u8bef")); }, ontimeout() { reject(new Error("\u4e91\u7aef\u8bf7\u6c42\u8d85\u65f6")); }, }); }); } async function _fpn() { try { const text = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `${getCloudApiBase()}${state.panelNoticePath || DEFAULT_PANEL_NOTICE_PATH}`, headers: { Accept: "text/plain, */*" }, onload(resp) { if (resp.status >= 200 && resp.status < 300) resolve(String(resp.responseText || "").trim()); else reject(new Error("notice_fail")); }, onerror() { reject(new Error("notice_net")); }, }); }); if (text) state.remotePanelNotice = text; renderPanelNotice(); } catch (_) {} } async function _fcc() { try { const data = await _crq("/api/hbzj/client-config", "GET", null, { Accept: "application/json", }); if (data?.panelNoticePath) { const p = String(data.panelNoticePath).trim(); if (p) state.panelNoticePath = p.startsWith("/") ? p : `/${p}`; } if (data?.freeChapterLimit != null) { state.freeChapterLimit = Number(data.freeChapterLimit) || DEFAULT_FREE_CHAPTER_LIMIT; } if (data?.proBuyUrl) { const buy = String(data.proBuyUrl).trim(); if (buy) state.proBuyUrl = buy; } state.cloudApiBase = resolvePinnedCloudApiBase(); } catch (_) {} await _fpn(); } function formatCloudAuthError(err) { const em = String(err?.message || err || ""); if (/\u672a\u90e8\u7f72|404|cloud_404/i.test(em)) return "\u4e91\u7aef\u670d\u52a1\u6682\u4e0d\u53ef\u7528"; if (/invalid token/i.test(em)) return "Token \u65e0\u6548"; if (/revoked/i.test(em)) return "Token \u5df2\u88ab\u7981\u7528"; if (/expired/i.test(em)) return "Token \u5df2\u8fc7\u671f"; return em || "\u6821\u9a8c\u5931\u8d25"; } function _acd(data) { if (!data || typeof data !== "object") return; if (data.lease) state.cloudLease = String(data.lease); const exp = Number(data.exp ?? data.expire_at ?? data.expires_at ?? data.lease_exp ?? 0); if (Number.isFinite(exp) && exp > 0) state.cloudLeaseExp = Math.floor(exp); if (data.tier) state.cloudTier = String(data.tier).trim().toLowerCase(); const lim = data.free_chapter_limit ?? data.freeChapterLimit ?? data.free_video_limit; const used = data.free_used_chapters ?? data.freeUsedChapters ?? data.free_used_videos; if (lim != null) state.freeChapterLimit = Number(lim) || DEFAULT_FREE_CHAPTER_LIMIT; if (used != null) state.freeUsedChapters = Number(used) || 0; const proExp = Number(data.pro_expires_at ?? data.proExpiresAt ?? data.pro_expire_at ?? 0); if (Number.isFinite(proExp) && proExp > 0) state.cloudProExpireAt = Math.floor(proExp); if (!state.cloudTier) state.cloudTier = state.cloudToken ? "pro" : "free"; writeCloudLeaseCache(state.cloudLease, state.cloudLeaseExp, { tier: state.cloudTier, proExpireAt: state.cloudProExpireAt, }); } async function _ecl(force) { const now = Math.floor(Date.now() / 1000); if (!force && state.cloudLease && state.cloudLeaseExp - now > 60) return true; const cached = readCloudLeaseCache(); if (!force && cached && cached.exp - now > 60) { state.cloudLease = cached.lease; state.cloudLeaseExp = cached.exp; state.cloudTier = cached.tier || state.cloudTier; state.cloudProExpireAt = cached.proExpireAt || state.cloudProExpireAt; updateCloudPanelUI(); return true; } const luid = getLearningUserId(); if (!luid) { state.cloudTier = state.cloudToken ? "pro" : "free"; updateCloudPanelUI(); return false; } try { const u = state.userProfile || {}; const data = await _crq("/api/hbzj/lease", "POST", { learning_user_id: luid, lease: state.cloudLease || undefined, site: "hbzj", script_version: SCRIPT_VERSION, name: String(u.name || "").trim(), realName: String(u.name || "").trim(), userName: String(u.loginId || "").trim(), phone: String(u.phone || "").trim(), }); state.cloudRevoked = false; _acd(data); return !!state.cloudLease; } catch (e) { const em = String(e?.message || e); if (/revoked|invalid token|expired/i.test(em)) { state.cloudRevoked = true; state.cloudTier = "revoked"; state.cloudLease = ""; writeCloudLeaseCache("", 0); updateCloudPanelUI(); throw e; } if (em === "cloud_404") { state.panelHint = ""; updateCloudPanelUI(); return false; } if (cached && cached.lease && cached.exp - now > -300 && isCloudProTier()) { state.cloudLease = cached.lease; state.cloudLeaseExp = cached.exp; state.cloudTier = cached.tier || "pro"; updateCloudPanelUI(); return true; } trace(`\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff1a${em || "\u7f51\u7edc\u9519\u8bef"}`); updateCloudPanelUI(); return false; } } async function _sct(token) { const tk = String(token || "").trim(); state.cloudToken = tk; if (tk) pageStorage().setItem(CLOUD_TOKEN_KEY, tk); else pageStorage().removeItem(CLOUD_TOKEN_KEY); if (!tk) { state.cloudTier = "free"; state.cloudProExpireAt = 0; state.cloudRevoked = false; try { await _ecl(true); } catch (_) {} updateCloudPanelUI(); trace("\u5df2\u6e05\u9664\u6388\u6743\u7801"); return true; } try { await _crq("/api/hbzj/license/verify", "GET", null); await _ecl(true); if (!isCloudProTier()) state.cloudTier = "pro"; state.cloudRevoked = false; updateCloudPanelUI(); trace("\u4f1a\u5458\u72b6\u6001\u5df2\u66f4\u65b0"); return true; } catch (e) { const tip = formatCloudAuthError(e); if (state._lastProErr !== tip) { state._lastProErr = tip; trace(`\u6388\u6743\u6821\u9a8c\u5931\u8d25\uff1a${tip}`); } updateCloudPanelUI(); return false; } } async function _acp() { try { await _ecl(true); } catch (_) { return false; } return !!state.cloudLease; } async function _rcq() { try { const data = await _crq("/api/hbzj/study/consume", "POST", { lease: state.cloudLease || undefined, learning_user_id: getLearningUserId(), kind: "chapter", }); if (data && data.ok === false) { throw new Error(String(data.message || "quota_denied")); } _acd(data); return true; } catch (e) { const em = String(e?.message || e); if (/free_quota_exhausted|quota|403|exam_requires_pro/i.test(em)) { openProModal(); } else { trace(`\u4e91\u7aef\uff1a${em || "\u7f51\u7edc\u9519\u8bef"}`); } return false; } } async function _ate() { try { await _ecl(true); } catch (_) { return false; } if (!state.cloudLease) return false; try { const data = await _crq("/api/hbzj/study/can-exam", "POST", { lease: state.cloudLease || undefined, learning_user_id: getLearningUserId(), }); if (data && data.ok !== false && data.success !== false) return true; } catch (e) { trace(`\u8003\u8bd5\u6821\u9a8c\uff1a${e?.message || e}`); } openProModal(); return false; } function openProBuyPage() { try { PAGE.open(state.proBuyUrl || PRO_BUY_URL, "_blank"); } catch (_) { location.href = state.proBuyUrl || PRO_BUY_URL; } } function closeProModal() { document.getElementById("hbzj-pro-modal")?.remove(); } function openProModal() { closeProModal(); const modal = document.createElement("div"); modal.id = "hbzj-pro-modal"; modal.innerHTML = `
\u5f00\u901a\u4f1a\u5458
\u5982\u9700\u7ee7\u7eed\u4f7f\u7528\u5168\u90e8\u5b66\u4e60\u4e0e\u8003\u8bd5\u529f\u80fd\uff0c\u8bf7\u5f00\u901a\u4f1a\u5458

\u4f1a\u5458\u6743\u76ca

\u5f00\u901a\u65b9\u5f0f

\u524d\u5f80\u8d2d\u4e70\u9875\u83b7\u53d6\u6388\u6743\u7801\uff0c\u5728\u300c\u8bbe\u7f6e\u300d\u4e2d\u7c98\u8d34\u5e76\u4fdd\u5b58\u3002

`; document.body.appendChild(modal); modal.querySelector("#hbzj-pro-buy")?.addEventListener("click", () => openProBuyPage()); modal.querySelector("#hbzj-pro-close")?.addEventListener("click", () => closeProModal()); modal.addEventListener("click", (e) => { if (e.target === modal) closeProModal(); }); } function showToast(msg) { let el = document.getElementById("hbzj-toast"); if (!el) { el = document.createElement("div"); el.id = "hbzj-toast"; document.body.appendChild(el); } el.textContent = String(msg || ""); el.classList.add("show"); clearTimeout(showToast._t); showToast._t = setTimeout(() => el.classList.remove("show"), 2600); } function loadQueue() { return readJson(QUEUE_KEY, []); } function saveQueue(list) { writeJson(QUEUE_KEY, Array.isArray(list) ? list : []); } function isEbookVideo(v) { return Number(v?.type) === 2; } function videoTotalSeconds(v) { const n = Number(v?.duration || v?.videoDuration || v?.totalDuration || 0); return Math.max(1, Math.ceil(n)); } function videoStudySeconds(v) { const total = videoTotalSeconds(v); const watched = Math.max(0, Number(v?.watchTimeLength || 0)); const rate = Number(v?.learnSpeed || 0); const fromRate = rate > 0 ? Math.floor((total * rate) / 100) : 0; return Math.max(watched, fromRate); } function studiedSecondsFromRate(total, rate) { const t = Math.max(1, Number(total) || 1); const r = Math.max(0, Math.min(100, Number(rate) || 0)); return Math.min(t, Math.floor((t * r) / 100)); } function _ppp(projectId, videoId, videoName, rate, total) { const pid = String(projectId || ""); const vid = String(videoId || ""); const studied = studiedSecondsFromRate(total, rate); const done = Number(rate) >= 100; let hit = false; for (const c of state.chapterPreview) { if (String(c.projectId) !== pid) continue; const same = (vid && String(c.videoId || "") === vid) || (!vid && videoName && String(c.name || "") === String(videoName)); if (!same) continue; c.studied = studied; c.total = Math.max(1, Number(total) || c.total || 1); c.done = done; hit = true; } if (!hit && pid && videoName) { const p = state.panelProjects.find((x) => String(x.projectId) === pid); state.chapterPreview.push({ projectId: pid, projectName: p?.projectName || pid, videoId: vid, name: videoName, studied, total: Math.max(1, Number(total) || 1), done, kind: "ware", }); } _rsp(pid); } function _rsp(projectId) { const pid = String(projectId || ""); const p = state.panelProjects.find((x) => String(x.projectId) === pid); if (!p) return; const items = state.chapterPreview.filter( (c) => String(c.projectId) === pid && c.kind !== "exam" && Math.max(0, Number(c.total) || 0) > 0 ); if (!items.length) return; let studied = 0; let total = 0; for (const c of items) { studied += Math.max(0, Number(c.studied) || 0); total += Math.max(0, Number(c.total) || 0); } if (total <= 0) return; const progress = Math.min(100, (studied / total) * 100); p.studyProgress = Number(progress.toFixed(1)); if (progress >= 100) { p.videoFinishedStatus = true; if (!isProjectFullyFinished(p)) p.studyLabel = "\u5df2\u5b66\u5b8c"; } else if (progress > 0) { p.studyLabel = `\u5b66\u4e60\u4e2d ${p.studyProgress}%`; } else { p.studyLabel = "\u672a\u5b66\u4e60"; } syncProjectFinishedFlags(p); } function isVideoDone(v) { if (!v) return true; if (isEbookVideo(v)) return true; const speed = Number(v.learnSpeed || 0); return speed >= 100; } function videoDisplayStudiedSeconds(v) { const total = videoTotalSeconds(v); const raw = videoStudySeconds(v); if (isVideoDone(v)) return Math.max(raw, total); return Math.min(raw, total); } function formatStudyProgressLog(name, rate, total) { const studiedSec = studiedSecondsFromRate(total, rate); return `\u5b66\u4e60\uff1a${name}\u5df2\u5b66${formatMinutes(studiedSec)}/${formatMinutes(total)}`; } function formatMinutes(sec) { const s = Math.max(0, Number(sec) || 0); const m = s / 60; if (m >= 100) return `${Math.round(m)} \u5206\u949f`; if (m >= 10) return `${m.toFixed(0)} \u5206\u949f`; return `${m.toFixed(1)} \u5206\u949f`; } function wareProgressPct(studied, total, done) { if (done) return 100; const t = Math.max(1, Number(total) || 1); return Math.min(100, Math.round((Math.max(0, Number(studied) || 0) / t) * 100)); } function videoNameOf(v) { const video = String(v?.videoName || v?.name || v?.title || "").trim(); const course = String(v?.courseName || v?.myClassCourseName || "").trim(); if (video && course && !video.includes(course)) return `${course} · ${video}`; return video || course || String(v?.myClassCourseVideoId || "\u89c6\u9891"); } function videoSourceOf(v) { return String(v?.baiJiaYunVideoSource || v?.ccVideoSource || v?.videoSource || "").trim(); } function flattenVideosFromBooks(booksData, myClassId) { let courseData = []; if (Array.isArray(booksData)) courseData = booksData; else if (Array.isArray(booksData?.myClassCourseRPList)) courseData = booksData.myClassCourseRPList; else if (Array.isArray(booksData?.courseData)) courseData = booksData.courseData; else if (Array.isArray(booksData?.data?.myClassCourseRPList)) courseData = booksData.data.myClassCourseRPList; else if (Array.isArray(booksData?.data?.courseData)) courseData = booksData.data.courseData; else if (Array.isArray(booksData?.data)) courseData = booksData.data; else courseData = pickList(booksData); const out = []; for (const course of courseData || []) { const courseName = String(course?.courseName || course?.name || course?.title || "").trim(); const vids = Array.isArray(course?.videoRPs) ? course.videoRPs : []; for (const v of vids) { out.push({ ...v, myClassId: myClassId || v.myClassId, myClassCourseId: v.myClassCourseId || course.myClassCourseId || course.id, courseType: course.type, courseName: courseName || v.courseName || "", }); } } return out; } function isVideoFinished(p) { if (!p) return false; if (p.videoFinishedStatus === true) return true; if (Number(p.studyStatus) === 2) return true; if (Number(p.studyProgress) >= 100) return true; return false; } function isProjectFullyFinished(p) { if (!p) return false; if (!isVideoFinished(p) && p.listSource !== "completed" && !p.projectFinishedStatus) return false; if (p.examRequired && !p.examDone) return false; if (String(p.examLabel || "").includes("\u5f85\u8003\u8bd5")) return false; if (p._examEnriched) { return !!(p.examDone || String(p.examLabel || "").includes("\u5df2\u901a\u8fc7") || p.examLabel === "\u65e0\u8bd5\u5377" || p.examLabel === "\u65e0\u5f85\u8003"); } if (isVideoFinished(p) || p.listSource === "completed") return false; return false; } function isProjectSelectable(p) { return p && !isProjectFullyFinished(p); } function projectListBadge(p) { if (isProjectFullyFinished(p)) { return `\u5df2\u5b8c\u6210`; } if ( isVideoFinished(p) && (p.examRequired || String(p.examLabel || "").includes("\u5f85\u8003\u8bd5")) ) { return `\u5f85\u8003\u8bd5`; } return `\u6b63\u5728\u5b66`; } function syncProjectFinishedFlags(p) { if (!p) return; if (isVideoFinished(p)) { p.videoFinishedStatus = true; if (!String(p.studyLabel || "").includes("\u5b66\u4e60\u4e2d") && p.studyLabel !== "\u672a\u5b66\u4e60") { p.studyLabel = "\u5df2\u5b66\u5b8c"; } else if (Number(p.studyProgress) >= 100) { p.studyLabel = "\u5df2\u5b66\u5b8c"; } } if (isProjectFullyFinished(p)) { p.listSource = "completed"; p.sourceLabel = "\u5df2\u5b8c\u6210"; p.studyLabel = "\u5df2\u5b8c\u6210"; p.projectFinishedStatus = true; } else if (isVideoFinished(p) && (p.examRequired || String(p.examLabel || "").includes("\u5f85\u8003\u8bd5"))) { p.projectFinishedStatus = false; p.sourceLabel = "\u5f85\u8003\u8bd5"; p.listSource = "learning"; } } function classifyStudyRow(row) { const status = Number(row?.studyStatus); const speed = Number( row?.studySpeed ?? row?.studyProgress ?? row?.learnSpeed ?? row?.percent ?? row?.classEntity?.studySpeed ?? 0 ); if (status === 2 || speed >= 100) return "completed"; return "learning"; } function mapProjectRow(row) { const entity = row?.classEntity && typeof row.classEntity === "object" ? row.classEntity : {}; const info = row?.classInfo && typeof row.classInfo === "object" ? row.classInfo : {}; const src = classifyStudyRow(row); const progress = Number( row.studySpeed ?? row.studyProgress ?? row.learnSpeed ?? row.percent ?? entity.studySpeed ?? info.studySpeed ?? 0 ); const myClassId = String(row.myClassId || row.id || entity.myClassId || info.myClassId || ""); const classId = String(row.classId || entity.classId || info.classId || myClassId); const finishedHint = src === "completed" || progress >= 100; const name = row.title || row.className || entity.className || info.className || row.myClassName || row.name || ""; return { projectId: myClassId, projectName: name || `\u73ed\u7ea7${myClassId}`, creditType: row.subjectName || row.classTypeName || row.creditType || entity.subjectName || info.subjectName || "", projectCredit: row.classHour ?? row.credit ?? row.projectCredit ?? row.period ?? entity.classHour ?? info.classHour, studyProgress: progress, studyStatus: Number(row.studyStatus || 0), studyTime: Number(row.studyTime || 0), classId, listSource: src, sourceLabel: src === "completed" ? "\u5df2\u5b8c\u6210" : "\u6b63\u5728\u5b66", studyLabel: finishedHint ? "\u5df2\u5b66\u5b8c" : progress > 0 ? `\u5b66\u4e60\u4e2d ${Number(progress.toFixed(1))}%` : "\u672a\u5b66\u4e60", examId: "", examName: "", examDone: false, examRequired: false, examScore: null, examPassLine: 60, examLabel: "\u8003\u8bd5\u5f85\u67e5", videoFinishedStatus: finishedHint, projectFinishedStatus: false, raw: row, }; } async function ensureUserProfile(force) { if (!force && state.userProfile?.accountId) return state.userProfile; const cms = readCmsAccountInfo(); if (cms?.accountId) { rememberAccountId(cms.accountId); state.userProfile = normalizeUserProfile(cms); if (!force) return state.userProfile; } try { const j = await apiGet(PATH_ACCOUNT_INFO); const info = j.data?.data || j.data || null; if (info?.accountId) rememberAccountId(info.accountId); state.userProfile = normalizeUserProfile(info || cms || {}); if (state.userProfile?.accountId) { writeAuthCache({ accountId: state.userProfile.accountId }); } } catch (e) { if (cms) state.userProfile = normalizeUserProfile(cms); else throw e; } return state.userProfile; } async function fetchYears() { try { let j; try { j = await apiGet(PATH_STUDY_YEARS); } catch (_) { j = await apiGet(PATH_CLASS_YEARS); } const years = extractYears(j.data); if (years.length) { state.yearOptions = years; if (!state.yearOptions.includes(Number(state.studyYear))) { state.studyYear = state.yearOptions[0]; pageStorage().setItem(YEAR_KEY, String(state.studyYear)); } } } catch (e) { trace(`years: ${e.message || e}`); } } async function fetchStudyListPage(pageNo, studyStatus) { const qs = toQuery({ searchYear: state.studyYear || "", studyStatus: studyStatus == null ? "" : studyStatus, mylearnTaskId: "", attribute: "", orderBy: 2, mathRandom: Math.random(), pageNo: pageNo || 1, subject: "", classType: 0, pageSize: 50, }); const j = await apiGet(`${PATH_STUDY_LIST}?${qs}`); return pickList(j.data); } async function fetchAllPanelProjects() { const allRows = []; try { const rows = await fetchStudyListPage(1, ""); allRows.push(...rows); } catch (e) { trace(`\u5b66\u4e60\u5217\u8868\u5931\u8d25: ${e.message || e}`); try { const a = await fetchStudyListPage(1, ""); allRows.push(...a); } catch (_) {} } if (!allRows.length) { try { const learning = await fetchStudyListPage(1, 1); const done = await fetchStudyListPage(1, 2); allRows.push(...learning, ...done); } catch (_) {} } const mapped = allRows.map((row) => mapProjectRow(row)); const learning = mapped.filter((p) => p.listSource === "learning"); const completed = mapped.filter((p) => p.listSource === "completed"); state._capturedLists = { learning, completed }; const byId = new Map(); for (const p of learning) byId.set(p.projectId, p); for (const p of completed) byId.set(p.projectId, p); const merged = Array.from(byId.values()); for (const p of merged) syncProjectFinishedFlags(p); return merged; } async function fetchClassDetail(myClassId) { const j = await apiGet(`${PATH_MY_CLASS}?${toQuery({ myClassId })}`); const raw = j.data?.data || j.data || null; if (!raw || typeof raw !== "object") return raw; if (raw.classInfo && typeof raw.classInfo === "object") { if (!raw.classId && raw.classInfo.classId) raw.classId = raw.classInfo.classId; if (!raw.className && raw.classInfo.className) raw.className = raw.classInfo.className; } return raw; } async function fetchClassBooks(myClassId) { const j = await apiGet(`${PATH_MY_CLASS}?${toQuery({ myClassId })}`); const raw = j.data?.data || j.data || null; if (!raw || typeof raw !== "object") return { courseData: [] }; if (Array.isArray(raw.myClassCourseRPList) && raw.myClassCourseRPList.length) { return raw; } try { const bj = await apiGet(`${PATH_MY_CLASS_BOOKS}?${toQuery({ myClassId })}`); const bro = bj.data?.data != null ? bj.data.data : bj.data; if (Array.isArray(bro)) return { courseData: bro }; if (bro && typeof bro === "object") return bro; } catch (_) {} return raw; } async function enrichProjectNames(projects, limit) { const list = (projects || []).slice(0, Math.max(1, limit || 20)); for (const p of list) { if (!p || !p.projectId) continue; if (p.projectName && !/^\u73ed\u7ea7[0-9a-f-]{30,}$/i.test(p.projectName)) continue; try { const detail = await fetchClassDetail(p.projectId); const name = detail?.classInfo?.className || detail?.className || detail?.classInfo?.title || ""; if (name) { p.projectName = name; if (detail?.classId) p.classId = String(detail.classId); } } catch (_) {} } } function buildWatchInfo(video, playduration, pid, timestamp) { const vid = videoSourceOf(video); return JSON.stringify({ vid, pid: pid || `BAIJIAYUN_${uuidv4()}`, playduration: Math.max(0, Math.floor(Number(playduration) || 0)), timestamp: Number(timestamp) > 0 ? Math.floor(Number(timestamp)) : Date.now(), }); } function formatVideoRate(rate) { const n = Number(rate); if (!Number.isFinite(n)) return "0"; if (n >= 100) return "100"; return n.toFixed(1); } function initialPlaySeconds(video, total) { const watched = Math.floor(Number(video?.watchTimeLength) || 0); if (watched > 0) return Math.min(watched, Math.max(0, total - 1)); const speed = Number(video?.learnSpeed || 0); if (speed > 0) return Math.min(Math.floor((total * speed) / 100), Math.max(0, total - 1)); return 1; } function videoInnerResult(un) { const attr = un?.data; if (!attr) return null; if (attr.data && typeof attr.data === "object") return attr.data; return attr; } async function _psw(myClassId, video, watchInfo) { return apiPostForm(PATH_VIDEO_SV, { myClassId, myClassCourseId: video.myClassCourseId, myClassCourseVideoId: video.myClassCourseVideoId, watchInfo, }); } async function _pcw(myClassId, video, watchInfo) { return apiPostForm(PATH_VIDEO_CV, { myClassId, myClassCourseId: video.myClassCourseId, myClassCourseVideoId: video.myClassCourseVideoId, watchInfo, isCalculateclassHourFlag: true, }); } async function postUpdateLastVideo(classId, myClassId, video) { try { await apiPostForm(PATH_UPDATE_LAST_VIDEO, { classId: classId || myClassId, myClassId, myClassCourseId: video.myClassCourseId, myClassCourseVideoId: video.myClassCourseVideoId, }); } catch (e) { trace(`update-last-video: ${e.message || e}`); } } function paperNeedsExam(row) { if (!row) return false; if (Number(row.isPass) === 1 || row.passFlag === true || row.pass === true) return false; const pass = Number(row.passScore ?? row.passMark ?? row.passLine ?? 60); const score = Number(row.score ?? row.myHighestScore ?? row.highestScore ?? NaN); if (Number.isFinite(score) && score >= pass) return false; const studySpeed = Number(row.studySpeed ?? 0); if (studySpeed < 100) return false; const statusName = String(row.statusName || ""); if (statusName.includes("\u672a\u5b8c\u6210")) return false; if (statusName.includes("\u5df2\u901a\u8fc7")) return false; return true; } function paperUnfinished(row) { return paperNeedsExam(row); } async function fetchPaperList(cmsType) { const qs = toQuery({ pageNo: 1, pageSize: 20, cmsType: cmsType == null ? 9 : cmsType }); const j = await apiGet(`${PATH_EXAM_LIST}?${qs}`); return pickList(j.data); } async function checkExamStatus(myExamId) { if (!myExamId) return; try { await apiGet(`${PATH_EXAM_CHECK_STATUS}?${toQuery({ myExamId })}`); } catch (e) { trace(`check-my-exam-status\uff1a${e.message || e}`); } } async function saveExamRecord(paperRow) { const qs = toQuery({ myExamId: paperRow.myExamId, isMakeUp: paperRow.isMakeUpStatus ?? paperRow.isMakeUp ?? 0, examType: paperRow.examType ?? paperRow.type ?? 0, paperId: paperRow.paperId, paperSource: paperRow.paperSource ?? 1, }); const j = await apiGet(`${PATH_EXAM_SAVE_RECORD}?${qs}`); const rid = j.data?.data ?? j.data; return rid ? String(rid) : ""; } async function startPaper(paperId, myExamRecordId) { const qs = toQuery({ paperId: paperId || "", myExamRecordId: myExamRecordId || "", }); const j = await apiGet(`${PATH_EXAM_START}?${qs}`); return j.data?.data || j.data || null; } function isStandardOption(opt) { if (!opt) return false; const v = opt.standardAnswer; return v === true || v === 1 || v === "1" || Number(opt.isStandardAnswer) === 1 || opt.isCorrect === true; } function buildExamAnswerList(paperDetail) { const stems = paperDetail?.questionStemRPS || paperDetail?.listQuestionStemRP || []; if (!Array.isArray(stems) || !stems.length) return null; const answerList = []; for (const stem of stems) { const questions = stem.listPaperQuestionRP || stem.paperQuestionRPS || []; if (!Array.isArray(questions) || !questions.length) return null; for (const q of questions) { const qid = q.paperQuestionId || q.questionId || q.id; if (!qid) return null; const opts = q.paperOptionRPS || q.paperOptionList || q.options || []; const std = (Array.isArray(opts) ? opts : []).filter(isStandardOption); if (!std.length) return null; answerList.push({ paperQuestionId: String(qid), answers: std.map((o) => ({ paperOptionId: String(o.paperOptionId || o.id) })), }); } } return answerList.length ? answerList : null; } async function submitPaper(payload) { const body = { myExamId: payload.myExamId, isMakeUp: payload.isMakeUp != null ? payload.isMakeUp : false, myExamRecordId: payload.myExamRecordId, answerList: typeof payload.answerList === "string" ? payload.answerList : JSON.stringify(payload.answerList || []), }; try { return await apiPostForm(PATH_EXAM_SUBMIT, body); } catch (_) { return await apiPostJson(PATH_EXAM_SUBMIT, body); } } async function _roe(paperRow) { const myExamId = paperRow.myExamId; const paperId = paperRow.paperId; const name = paperRow.paperName || paperRow.className || paperId; if (!myExamId || !paperId) throw new Error("\u7f3a\u5c11 myExamId \u6216 paperId"); await checkExamStatus(myExamId); const myExamRecordId = await saveExamRecord(paperRow); if (!myExamRecordId) throw new Error("save-my-exam-record \u672a\u8fd4\u56de recordId"); const detail = await startPaper(paperId, myExamRecordId); if (!detail) throw new Error("start-do-paper \u65e0\u9898\u76ee"); const answerList = buildExamAnswerList(detail); if (!answerList) throw new Error("\u9898\u76ee\u65e0 standardAnswer\uff0c\u5df2\u8df3\u8fc7"); await submitPaper({ myExamId, isMakeUp: paperRow.isMakeUpStatus ?? paperRow.isMakeUp ?? false, myExamRecordId, answerList, }); log(`\u8003\u8bd5\uff1a${name} \u5df2\u63d0\u4ea4`); return true; } async function _tea(project) { if (!(await _ate())) return false; let papers = []; try { papers = await fetchPaperList(9); } catch (e) { trace(`\u8bd5\u5377\u5217\u8868(cmsType=9)\uff1a${e.message || e}`); } if (!papers.length) { try { papers = await fetchPaperList(1); } catch (e) { trace(`\u8bd5\u5377\u5217\u8868(cmsType=1)\uff1a${e.message || e}`); } } let pending = papers.filter(paperNeedsExam); if (project?.projectId) { const mine = pending.filter((p) => String(p.myClassId || "") === String(project.projectId)); if (mine.length) pending = mine; } if (!pending.length) { if (project) { const st = examLabelForProject(project, papers); project.examRequired = st.examRequired; project.examDone = st.examDone; project.examLabel = st.examLabel; project.examScore = st.examScore; project._examEnriched = true; syncProjectFinishedFlags(project); } return false; } if (project) { project.examRequired = true; project.examLabel = `\u5f85\u8003\u8bd5 ${pending.length} \u4efd`; } let did = false; for (const paper of pending.slice(0, 3)) { if (!state.enabled) break; try { await _roe(paper); did = true; if (project) { project.examDone = true; project.examRequired = false; project.examLabel = "\u5df2\u63d0\u4ea4\uff0c\u5237\u65b0\u6210\u7ee9\u4e2d…"; project._examEnriched = false; } await sleep(1500); try { let papers2 = await fetchPaperList(9); if (!papers2.length) papers2 = await fetchPaperList(1); if (project) { const st = examLabelForProject(project, papers2); project.examRequired = st.examRequired; project.examDone = st.examDone; project.examLabel = st.examLabel; project.examScore = st.examScore; project._examEnriched = true; syncProjectFinishedFlags(project); updatePanel(); } } catch (_) { if (project) { project.examLabel = "\u5df2\u63d0\u4ea4"; project.examDone = true; project.examRequired = false; } } await sleep(800); } catch (e) { const msg = e.message || e; log(`\u8003\u8bd5\u300c${paper.paperName || paper.className || paper.paperId}\u300d\u5931\u8d25\uff1a${msg}`); if (project && String(msg).includes("standardAnswer")) { project.examLabel = "\u7f3a\u6807\u51c6\u7b54\u6848\uff0c\u5df2\u8df3\u8fc7"; } trace(`\u8003\u8bd5\u5931\u8d25\uff1a${msg}`); } } return did; } async function _sw(project, video) { const myClassId = project.projectId; const name = videoNameOf(video); const total = videoTotalSeconds(video); const goalSec = Math.min(total, Math.ceil(total * EFFICIENCY_JUMP_RATIO)); const vid = videoSourceOf(video); if (!vid) { log(`\u89c6\u9891\u300c${name}\u300d\u65e0\u89c6\u9891\u6e90\uff0c\u8df3\u8fc7`); return; } if (isEbookVideo(video)) { log(`\u300c${name}\u300d\u4e3a\u7535\u5b50\u4e66\uff0c\u8df3\u8fc7\u8ba1\u65f6`); return; } state.lastUserAction = name; updatePanel(); const pid = `BAIJIAYUN_${uuidv4()}`; let playduration = initialPlaySeconds(video, total); let rate = Number(video.learnSpeed || 0); let lastReportMs = Date.now(); await postUpdateLastVideo(project.classId || myClassId, myClassId, video); let svOk = false; try { const watchInfo0 = buildWatchInfo(video, playduration, pid, lastReportMs); const svRes = await _psw(myClassId, video, watchInfo0); const inner = videoInnerResult(svRes); if (inner && String(inner.respCode) !== "SUCCESS" && inner.respCode != null) { throw new Error(inner.respDesc || inner.respCode || "sv_fail"); } svOk = true; if (inner?.videoLearnRate != null) rate = Number(inner.videoLearnRate); video.learnSpeed = rate; _ppp(myClassId, video.myClassCourseVideoId, name, rate, total); updatePanel(); log(formatStudyProgressLog(name, rate, total)); _lwh(); } catch (e) { trace(`sv\uff1a${e.message || e}`); await sleep(2000); } if (!svOk) { log(`\u8df3\u8fc7\uff1a${name}`); return; } await sleep(SV_BEFORE_CV_MS); const needSteps = Math.ceil(Math.max(0, goalSec - playduration) / CV_STEP_SECONDS); const maxRounds = Math.min(WATCH_MAX_ROUNDS, needSteps + 2); for (let round = 0; round < maxRounds && state.enabled; round++) { if (rate >= 100) break; if (playduration >= goalSec) break; const now = Date.now(); const elapsedSec = Math.max(1, Math.floor((now - lastReportMs) / 1000)); const step = Math.min(CV_STEP_SECONDS, elapsedSec, Math.max(1, goalSec - playduration)); playduration = Math.min(goalSec, playduration + step); lastReportMs = now; const watchInfo = buildWatchInfo(video, playduration, pid, lastReportMs); let cvOk = false; try { const cvRes = await _pcw(myClassId, video, watchInfo); const inner = videoInnerResult(cvRes); if (cvRes.msg && String(cvRes.msg).includes("today class hour limit")) { log(`\u4eca\u65e5\u5b66\u65f6\u5df2\u8fbe\u4e0a\u9650`); break; } if (inner && String(inner.respCode) === "INVALID_PARAM") { trace(`cv\uff1a${inner.respDesc || inner.respCode}`); playduration = Math.max(1, playduration - step); await sleep(CV_INTERVAL_MS); continue; } if (inner && String(inner.respCode) === "SUCCESS") { if (inner.videoLearnRate != null) rate = Number(inner.videoLearnRate); video.learnSpeed = rate; _ppp(myClassId, video.myClassCourseVideoId, name, rate, total); updatePanel(); log(formatStudyProgressLog(name, rate, total)); if (rate >= 100 || playduration >= goalSec) break; } else if (inner && inner.respCode != null) { throw new Error(inner.respDesc || inner.respCode); } } catch (e) { trace(`cv\uff1a${e.message || e}`); playduration = Math.max(1, playduration - step); await sleep(CV_INTERVAL_MS); continue; } if (rate >= 100 || playduration >= goalSec) break; await sleep(CV_INTERVAL_MS); } video.learnSpeed = Math.max(Number(video.learnSpeed || 0), rate); if (rate >= 100 || playduration >= goalSec) video.learnSpeed = 100; _ppp(myClassId, video.myClassCourseVideoId, name, video.learnSpeed, total); log(formatStudyProgressLog(name, video.learnSpeed, total).replace(/^\u5b66\u4e60\uff1a/, "\u5b8c\u6210\uff1a")); } async function _sp(project) { const projectId = project.projectId; state.lastUserAction = project.projectName; updatePanel(); log(`\u5f00\u59cb\uff1a${project.projectName}`); let detail = null; try { detail = await fetchClassDetail(projectId); if (detail?.classId) project.classId = String(detail.classId); if (detail?.studyStatus != null) project.studyStatus = Number(detail.studyStatus); if (detail?.studySpeed != null) project.studyProgress = Number(detail.studySpeed); } catch (e) { trace(`\u73ed\u7ea7\u8be6\u60c5\uff1a${e.message || e}`); } let books = null; try { books = await fetchClassBooks(projectId); } catch (e) { throw new Error(`\u8bfe\u4ef6\u5217\u8868\u5931\u8d25\uff1a${e.message || e}`); } const videos = flattenVideosFromBooks(books, projectId).filter((v) => !isEbookVideo(v)); const pending = videos.filter((v) => !isVideoDone(v)); if (pending.length) { if (!(await _acp())) { state.enabled = false; return; } for (const video of pending) { if (!state.enabled) return; if (!(await _acp())) { state.enabled = false; return; } if (!(await _rcq())) { state.enabled = false; return; } await _sw(project, video); updateCloudPanelUI(); } } else { log(`${project.projectName} \u89c6\u9891\u5df2\u5168\u90e8\u8fbe\u6807`); } try { const books2 = await fetchClassBooks(projectId); const vids2 = flattenVideosFromBooks(books2, projectId).filter((v) => !isEbookVideo(v)); const allDone = vids2.length ? vids2.every(isVideoDone) : pending.length === 0; project.videoFinishedStatus = allDone; if (allDone) { project.studyProgress = Math.max(Number(project.studyProgress || 0), 100); } } catch (_) {} if (state.enabled) { try { for (let i = 0; i < EXAM_PASS_RETRY && state.enabled; i++) { const did = await _tea(project); if (!did) break; await sleep(800); } } catch (e) { trace(`\u8003\u8bd5\u6d41\u7a0b\uff1a${e.message || e}`); } } if (project.videoFinishedStatus && (!project.examRequired || project.examDone)) { project.projectFinishedStatus = true; project.listSource = "completed"; } syncProjectFinishedFlags(project); await _rcp(); updatePanel(); } async function courseHasPendingWork(project) { try { const books = await fetchClassBooks(project.projectId); const videos = flattenVideosFromBooks(books, project.projectId).filter((v) => !isEbookVideo(v)); if (videos.some((v) => !isVideoDone(v))) return true; try { const papers = await fetchPaperList(9); if (papers.some(paperUnfinished)) return true; } catch (_) {} return false; } catch (_) { return true; } } async function _rse() { if (state.engineRunning) return; state.engineRunning = true; try { while (state.enabled) { const queue = loadQueue().filter(Boolean); if (!queue.length) { state.enabled = false; break; } let progressed = false; for (const projectId of queue) { if (!state.enabled) break; const project = state.panelProjects.find((p) => p.projectId === projectId); if (!project) continue; if (isProjectFullyFinished(project)) continue; const hasWork = await courseHasPendingWork(project); if (!hasWork) { project.projectFinishedStatus = true; syncProjectFinishedFlags(project); continue; } if (!(await _acp())) { state.enabled = false; break; } progressed = true; try { await _sp(project); } catch (e) { trace(`${project.projectName} \u5f02\u5e38\uff1a${e.message || e}`); await sleep(3000); } } if (!progressed) { state.enabled = false; break; } await sleep(ENGINE_TICK_MS); } } finally { state.engineRunning = false; state.lastUserAction = ""; updatePanel(); if (!state.enabled) log("\u5df2\u505c\u6b62"); } } function readPanelPos() { return readJson(PANEL_POS_KEY, null); } function writePanelPos(pos) { writeJson(PANEL_POS_KEY, pos); } function readPanelCollapsed() { return pageStorage().getItem(PANEL_COLLAPSED_KEY) === "1"; } function writePanelCollapsed(v) { pageStorage().setItem(PANEL_COLLAPSED_KEY, v ? "1" : "0"); } function applyPanelCollapsed(panel, collapsed) { const body = panel.querySelector("#hbzj-panel-body"); const footer = panel.querySelector("#hbzj-panel-footer"); const extra = panel.querySelector(".hbzj-footer-extra"); const btnMin = panel.querySelector("#hbzj-btn-min"); const btnMax = panel.querySelector("#hbzj-btn-max"); if (collapsed) { if (body) body.style.display = "none"; if (footer) footer.style.display = "none"; if (extra) extra.style.display = "none"; if (btnMin) btnMin.style.display = "none"; if (btnMax) btnMax.style.display = ""; } else { if (body) body.style.display = ""; if (footer) footer.style.display = ""; if (extra) extra.style.display = ""; if (btnMin) btnMin.style.display = ""; if (btnMax) btnMax.style.display = "none"; } } function enablePanelDrag(panel) { const hd = panel.querySelector("#hbzj-panel-header"); if (!hd) return; let ox = 0; let oy = 0; let dragging = false; hd.addEventListener("mousedown", (e) => { if (e.target.closest("button")) return; dragging = true; const rect = panel.getBoundingClientRect(); ox = e.clientX - rect.left; oy = e.clientY - rect.top; e.preventDefault(); }); window.addEventListener("mousemove", (e) => { if (!dragging) return; const left = Math.max(0, e.clientX - ox); const top = Math.max(0, e.clientY - oy); panel.style.left = `${left}px`; panel.style.top = `${top}px`; panel.style.right = "auto"; }); window.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; writePanelPos({ left: parseInt(panel.style.left || "0", 10), top: parseInt(panel.style.top || "0", 10), }); }); } function switchPanelTab(tab) { document.querySelectorAll("#hbzj-auto-panel .hbzj-tab-btn").forEach((b) => { b.classList.toggle("active", b.dataset.tab === tab); }); document.querySelectorAll("#hbzj-auto-panel .hbzj-pane").forEach((p) => { p.classList.toggle("active", p.dataset.pane === tab); }); if (tab === "chapter" && loadQueue().length && !state.chapterPreview.length && !state.chapterPreviewLoading) { state.chapterPreviewLoading = true; renderChapterPreview(); void _rcp() .catch((e) => trace(`\u7ae0\u8282\u9884\u89c8\u5931\u8d25\uff1a${e.message || e}`)) .finally(() => { state.chapterPreviewLoading = false; updatePanel(); }); } } function updateCloudPanelUI() { const tierEl = document.getElementById("hbzj-cloud-tier"); const freeEl = document.getElementById("hbzj-cloud-free"); const freeLabelEl = document.getElementById("hbzj-cloud-free-label"); const freeRow = document.getElementById("hbzj-cloud-free-row"); const tokenInput = document.getElementById("hbzj-cloud-token"); if (tokenInput) tokenInput.disabled = false; if (tierEl) { tierEl.textContent = formatCloudTierText(state.cloudTier); tierEl.style.color = state.cloudRevoked || String(state.cloudTier).toLowerCase() === "revoked" ? "#dc2626" : "#0f172a"; } if (state.cloudRevoked) { if (freeRow) freeRow.style.display = "none"; } else if (isCloudProTier()) { if (freeRow) freeRow.style.display = ""; if (freeLabelEl) freeLabelEl.textContent = "\u6709\u6548\u671f"; if (freeEl) freeEl.textContent = formatExpireText(state.cloudProExpireAt || state.cloudLeaseExp); } else { if (freeRow) freeRow.style.display = "none"; } if (tokenInput && tokenInput !== document.activeElement) tokenInput.value = state.cloudToken || ""; } function recomputeQueueStats() { const q = loadQueue(); let total = 0; let done = 0; let allDone = 0; for (const id of q) { const p = state.panelProjects.find((x) => x.projectId === id); if (!p) continue; const preview = state.chapterPreview.filter((c) => c.projectId === id); if (preview.length) { total += preview.length; done += preview.filter((c) => c.done).length; } else { total += 1; if (isProjectFullyFinished(p)) done += 1; } if (isProjectFullyFinished(p)) allDone += 1; } state.queueWareTotal = total; state.queueWareDone = done; return { total, done, allDone, selected: q.length }; } function updatePanel() { const status = document.getElementById("hbzj-auto-status"); if (status) { status.textContent = state.enabled ? `\u8fd0\u884c\u4e2d · ${CV_INTERVAL_MINUTES}\u5206\u949f/\u6b21` : "\u5df2\u505c\u6b62"; status.style.color = state.enabled ? "#15803d" : "#64748b"; } const stats = recomputeQueueStats(); const doneEl = document.getElementById("hbzj-queue-done"); const totalEl = document.getElementById("hbzj-queue-total"); const examEl = document.getElementById("hbzj-queue-exam"); const pctEl = document.getElementById("hbzj-queue-percent"); const bar = document.getElementById("hbzj-queue-progress"); const pct = stats.total ? Math.round((stats.done / stats.total) * 100) : 0; if (doneEl) doneEl.textContent = String(stats.done); if (totalEl) totalEl.textContent = String(stats.total); if (examEl) examEl.textContent = stats.selected ? `${stats.allDone}/${stats.selected}` : "—"; if (pctEl) pctEl.textContent = `${pct}%`; if (bar) bar.style.width = `${pct}%`; const userEl = document.getElementById("hbzj-current-user"); if (userEl) { const u = state.userProfile; const name = String(u?.name || "").trim(); userEl.textContent = name || (getAccountId() ? "\u5df2\u767b\u5f55" : "—"); } const courseEl = document.getElementById("hbzj-current-course"); const chapterEl = document.getElementById("hbzj-current-chapter"); const taskEl = document.getElementById("hbzj-current-task"); if (taskEl) taskEl.textContent = state.lastUserAction || "\u52fe\u9009\u73ed\u7ea7\u540e\u70b9\u300c\u5f00\u59cb\u300d"; if (courseEl && !state.enabled) courseEl.textContent = "\u65e0"; if (chapterEl && !state.enabled) chapterEl.textContent = "\u65e0"; const startBtn = document.getElementById("hbzj-start"); const stopBtn = document.getElementById("hbzj-stop"); if (startBtn) startBtn.disabled = !!state.enabled; if (stopBtn) { stopBtn.disabled = !state.enabled; stopBtn.classList.toggle("hbzj-btn-off", !state.enabled); } const qText = document.getElementById("hbzj-queue-text"); if (qText) qText.textContent = stats.selected ? `\u5df2\u9009 ${stats.selected} \u9879` : "\u672a\u9009\u62e9"; renderCourseList(); renderChapterPreview(); updateCloudPanelUI(); renderPanelNotice(); } function renderCourseList() { const box = document.getElementById("hbzj-course-list"); if (!box) return; if (state.panelRefreshing) { box.innerHTML = `
\u52a0\u8f7d\u4e2d…
`; return; } const q = new Set(loadQueue().map(String)); if (!state.panelProjects.length) { const ok = hasPlatformAuth(); box.innerHTML = `
${ ok ? "\u6682\u65e0\u73ed\u7ea7\u3002\u8bf7\u786e\u8ba4\u5e74\u5ea6\u7b5b\u9009\uff0c\u6216\u5230\u5b98\u7f51\u300c\u6211\u7684\u5b66\u4e60\u300d\u70b9\u4e00\u4e0b\u540e\u518d\u5237\u65b0\u3002" : "\u8bf7\u5148\u767b\u5f55\u5b98\u7f51\uff0c\u6253\u5f00\u300c\u6211\u7684\u5b66\u4e60\u300d\uff0c\u518d\u70b9\u5237\u65b0\u3002" }
`; return; } box.innerHTML = state.panelProjects .map((p) => { const checked = q.has(String(p.projectId)) ? "checked" : ""; const disabled = isProjectFullyFinished(p) ? "disabled" : ""; const badge = projectListBadge(p); return ``; }) .join(""); box.querySelectorAll("input[type=checkbox]").forEach((inp) => { inp.addEventListener("change", () => { const id = String(inp.getAttribute("data-pid") || ""); let list = loadQueue().map(String); if (inp.checked) { if (!list.includes(id)) list.push(id); } else { list = list.filter((x) => x !== id); } saveQueue(list); state.chapterPreviewLoading = true; renderChapterPreview(); void _rcp() .catch((e) => trace(`\u7ae0\u8282\u9884\u89c8\u5931\u8d25\uff1a${e.message || e}`)) .finally(() => { state.chapterPreviewLoading = false; updatePanel(); }); }); }); } function renderChapterPreview() { const box = document.getElementById("hbzj-chapter-preview"); if (!box) return; const q = loadQueue().map(String).filter(Boolean); if (!q.length) { box.innerHTML = `
\u8bf7\u52fe\u9009\u73ed\u7ea7
`; return; } if (state.chapterPreviewLoading && !state.chapterPreview.length) { box.innerHTML = `
\u6b63\u5728\u52a0\u8f7d\u89c6\u9891\u7ae0\u8282…
`; return; } if (!state.chapterPreview.length) { box.innerHTML = `
\u5df2\u52fe\u9009 ${q.length} \u9879\uff0c\u4f46\u672a\u8bfb\u5230\u89c6\u9891\u3002\u8bf7\u70b9\u300c\u5237\u65b0\u300d\u540e\u91cd\u8bd5\u3002
`; return; } const groups = new Map(); for (const c of state.chapterPreview) { const pid = String(c.projectId); if (!groups.has(pid)) groups.set(pid, []); groups.get(pid).push(c); } const html = []; for (const [pid, items] of groups) { const p = state.panelProjects.find((x) => String(x.projectId) === pid); const title = p?.projectName || items[0]?.projectName || pid; const examLine = p ? escHtml(p.examLabel || "\u5f85\u67e5") : "\u8003\u8bd5\u4fe1\u606f\u52a0\u8f7d\u4e2d…"; const wareDone = items.filter((x) => x.done).length; html.push(`
${escHtml(title)}
\u8003\u8bd5\uff1a${examLine}
\u89c6\u9891 ${wareDone}/${items.length}
${items .map((c) => { const pct = wareProgressPct(c.studied, c.total, c.done); return `
${escHtml(c.name)}
${c.done ? "\u5df2\u8fbe\u6807" : "\u672a\u5b8c\u6210"} · ${formatMinutes(c.studied)} / ${formatMinutes(c.total)}\uff08${pct}%\uff09
`; }) .join("")}
`); } box.innerHTML = html.join("") || `
\u6682\u65e0\u89c6\u9891
`; } async function _rcp() { const q = loadQueue().map(String).filter(Boolean); const rows = []; if (!q.length) { state.chapterPreview = []; return; } for (const id of q) { const p = state.panelProjects.find((x) => String(x.projectId) === String(id)); if (!p) { rows.push({ projectId: id, projectName: id, name: "\uff08\u5217\u8868\u4e2d\u627e\u4e0d\u5230\u8be5\u73ed\u7ea7\uff0c\u8bf7\u5148\u5237\u65b0\uff09", studied: 0, total: 0, done: false, kind: "ware", }); continue; } try { const books = await fetchClassBooks(id); const videos = flattenVideosFromBooks(books, id).filter((v) => !isEbookVideo(v)); if (!videos.length) { rows.push({ projectId: id, projectName: p.projectName, name: "\u672a\u83b7\u53d6\u5230\u89c6\u9891\u5217\u8868", studied: 0, total: 0, done: false, kind: "ware", }); } else { for (const v of videos) { const done = isVideoDone(v); rows.push({ projectId: id, projectName: p.projectName, videoId: String(v.myClassCourseVideoId || ""), name: videoNameOf(v), studied: videoDisplayStudiedSeconds(v), total: videoTotalSeconds(v), done, kind: "ware", }); } const allDone = videos.every(isVideoDone); if (allDone) { p.videoFinishedStatus = true; p.studyProgress = Math.max(Number(p.studyProgress || 0), 100); } syncProjectFinishedFlags(p); } } catch (e) { trace(`preview ${id}: ${e.message || e}`); rows.push({ projectId: id, projectName: p.projectName, name: `\u52a0\u8f7d\u5931\u8d25\uff1a${e.message || e}`, studied: 0, total: 0, done: false, kind: "ware", }); } } state.chapterPreview = rows; } function formatExamScore(score) { const n = Number(score); if (!Number.isFinite(n)) return ""; return Number.isInteger(n) ? `${n}\u5206` : `${Number(n.toFixed(1))}\u5206`; } function examLabelForProject(project, papers) { const pid = String(project?.projectId || ""); const mine = (papers || []).filter((row) => String(row.myClassId || "") === pid); if (!mine.length) { return { examRequired: false, examDone: true, examLabel: "\u65e0\u8bd5\u5377", examScore: null }; } const pending = mine.filter(paperNeedsExam); if (pending.length) { const lastScore = Number(pending[0].score); const scoreHint = Number.isFinite(lastScore) && lastScore > 0 ? `\uff08\u4e0a\u6b21${formatExamScore(lastScore)}\uff09` : ""; return { examRequired: true, examDone: false, examScore: Number.isFinite(lastScore) ? lastScore : null, examLabel: (pending.length === 1 ? "\u5f85\u8003\u8bd5" : `\u5f85\u8003\u8bd5 ${pending.length} \u4efd`) + scoreHint, }; } const passed = mine.filter( (row) => Number(row.isPass) === 1 || String(row.statusName || "").includes("\u5df2\u901a\u8fc7") || (Number.isFinite(Number(row.score)) && Number(row.score) >= Number(row.passScore ?? 60)) ); if (passed.length) { const best = Math.max(...passed.map((r) => Number(r.score) || 0)); const scoreText = formatExamScore(best); return { examRequired: false, examDone: true, examScore: best, examLabel: scoreText ? `\u5df2\u901a\u8fc7 ${scoreText}` : "\u5df2\u901a\u8fc7", }; } const blocked = mine.filter( (row) => Number(row.studySpeed || 0) < 100 || String(row.statusName || "").includes("\u672a\u5b8c\u6210\u5b66\u4e60") ); if (blocked.length) { return { examRequired: false, examDone: false, examScore: null, examLabel: "\u9700\u5148\u5b66\u5b8c" }; } const anyScore = Math.max(...mine.map((r) => Number(r.score) || 0), 0); if (anyScore > 0) { return { examRequired: false, examDone: true, examScore: anyScore, examLabel: `\u6210\u7ee9 ${formatExamScore(anyScore)}`, }; } return { examRequired: false, examDone: true, examScore: null, examLabel: "\u65e0\u5f85\u8003" }; } async function enrichExamStatusForProjects(list, limit) { const targets = (list || []).slice(0, Math.max(1, limit || 12)); let papers = null; try { papers = await fetchPaperList(9); if (!papers.length) papers = await fetchPaperList(1); } catch (_) { papers = []; } for (const p of targets) { if (!p || p._examEnriched) continue; const st = examLabelForProject(p, papers); p.examRequired = st.examRequired; p.examDone = st.examDone; p.examLabel = st.examLabel; p.examScore = st.examScore; p._examEnriched = true; syncProjectFinishedFlags(p); updatePanel(); await sleep(60); } } async function _rpp(opt) { const silent = !!opt?.silent; state.panelRefreshing = true; updatePanel(); try { for (let i = 0; i < 8 && !getAccountId(); i++) { const cms = readCmsAccountInfo(); if (cms?.accountId) rememberAccountId(cms.accountId); await sleep(400); } if (!getAccountId()) { if (mergeCapturedIntoPanel(true)) { if (!silent) trace(`\u5df2\u7528\u9875\u9762\u7f13\u5b58\u5217\u8868\uff1a${state.panelProjects.length} \u9879`); return; } throw new Error("\u672a\u83b7\u53d6 accountId\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55\u540e\u70b9\u5237\u65b0"); } try { await ensureUserProfile(false); } catch (e) { trace(`\u7528\u6237\u4fe1\u606f: ${e.message || e}`); } await fetchYears(); state.panelProjects = await fetchAllPanelProjects(); await enrichProjectNames(state.panelProjects, 50); if (!state.panelProjects.length) { mergeCapturedIntoPanel(true); await enrichProjectNames(state.panelProjects, 50); } await _rcp(); void enrichExamStatusForProjects( [ ...state.panelProjects.filter((p) => loadQueue().map(String).includes(String(p.projectId))), ...state.panelProjects.filter((p) => p.listSource === "learning"), ...state.panelProjects.filter((p) => p.listSource === "completed"), ], 20 ); const nLearn = state.panelProjects.filter((p) => p.listSource === "learning").length; const nDone = state.panelProjects.filter((p) => p.listSource === "completed").length; if (!silent) trace(`\u5df2\u5237\u65b0\uff1a\u6b63\u5728\u5b66 ${nLearn} · \u5df2\u5b8c\u6210 ${nDone}`); } catch (e) { if (mergeCapturedIntoPanel(true)) { trace(`\u63a5\u53e3\u5f02\u5e38\uff0c\u5df2\u663e\u793a\u62e6\u622a\u5217\u8868\uff1a${e.message || e}`); } else { trace(`\u5237\u65b0\u5931\u8d25\uff1a${e.message || e}`); } } finally { state.panelRefreshing = false; updatePanel(); } } function selectUnfinishedProjects() { const list = state.panelProjects.filter(isProjectSelectable).map((p) => p.projectId); saveQueue(list); state.chapterPreviewLoading = true; renderChapterPreview(); void _rcp() .catch((e) => log(`\u7ae0\u8282\u9884\u89c8\u5931\u8d25\uff1a${e.message || e}`)) .finally(() => { state.chapterPreviewLoading = false; updatePanel(); }); trace(`\u5df2\u52fe\u9009\u672a\u5b8c\u6210 ${list.length} \u9879`); } function clearProjectSelection() { saveQueue([]); state.chapterPreview = []; updatePanel(); } function injectPanelStyles() { const id = "hbzj-panel-style-v2"; if (document.getElementById(id)) return; document.getElementById("hbzj-panel-style-v1")?.remove(); const style = document.createElement("style"); style.id = id; style.textContent = ` #hbzj-auto-panel{position:fixed;right:20px;top:80px;z-index:999999;width:412px;max-width:calc(100vw - 24px);display:flex;flex-direction:column;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#0f172a;overflow:hidden;max-height:min(92vh,780px);} #hbzj-panel-header{padding:8px 11px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;} #hbzj-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #hbzj-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:cover;display:block;flex:0 0 auto;background:#e2e8f0;} #hbzj-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;} #hbzj-panel-sub{margin-top:3px;font-size:11px;color:#7c2d12;font-weight:700;} #hbzj-panel-controls{display:flex;gap:5px;} .hbzj-panel-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;font-size:15px;font-weight:900;} #hbzj-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;} #hbzj-panel-footer{padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;} .hbzj-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;} .hbzj-ann{position:relative;font-size:12px;color:#334155;line-height:1.42;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:11px;padding:8px 9px 8px 11px;} .hbzj-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-top-left-radius:11px;border-bottom-left-radius:11px;} .hbzj-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .hbzj-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;} .hbzj-status-metrics{font-size:12px;color:#475569;} .hbzj-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .hbzj-progress-pct{font-size:14px;font-weight:900;color:#0369a1;} .hbzj-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;} .hbzj-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;} .hbzj-tabbar{display:flex;gap:6px;margin-bottom:7px;} .hbzj-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;} .hbzj-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .hbzj-pane{display:none;}.hbzj-pane.active{display:block;} .hbzj-list-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;gap:6px;flex-wrap:wrap;} .hbzj-list-title{font-size:12px;color:#64748b;font-weight:700;} .hbzj-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;} .hbzj-list-head-actions{display:flex;gap:6px;align-items:center;flex-wrap:nowrap;justify-content:flex-start;flex:1 1 100%;} .hbzj-list-head-actions .hbzj-btn{flex:0 0 auto !important;width:auto;min-width:0;white-space:nowrap;padding:4px 8px;font-size:11px;line-height:1.2;} .hbzj-btn{border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;} .hbzj-btn-ghost{background:#fff !important;color:#0f172a !important;border:1px solid #cbd5e1;} .hbzj-btn-start{flex:1;background:#16a34a;} .hbzj-btn-stop{flex:1;background:#ef4444;} .hbzj-btn-off,.hbzj-btn:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;} .hbzj-btn-row{display:flex;gap:7px;} .hbzj-btn-pro{border:none;background:linear-gradient(135deg,#f59e0b,#ea580c);color:#fff;border-radius:8px;padding:5px 10px;font-weight:800;cursor:pointer;font-size:12px;} #hbzj-course-list,#hbzj-chapter-preview,#hbzj-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} .hbzj-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:700;} .hbzj-course-item,.hbzj-chapter-item{display:flex;gap:7px;align-items:flex-start;border:1px solid #e2e8f0;border-radius:8px;padding:7px;margin-bottom:6px;cursor:pointer;background:#f8fafc;} .hbzj-course-name{font-size:12px;line-height:1.38;font-weight:700;display:flex;align-items:flex-start;gap:6px;flex-wrap:wrap;} .hbzj-course-meta{font-size:11px;color:#64748b;margin-top:2px;line-height:1.4;} .hbzj-badge{font-size:10px;font-weight:800;padding:2px 6px;border-radius:999px;border:1px solid;line-height:1.3;} .hbzj-badge-learn{color:#1d4ed8;background:#eff6ff;border-color:#93c5fd;} .hbzj-badge-exam{color:#c2410c;background:#fff7ed;border-color:#fdba74;} .hbzj-badge-done{color:#047857;background:#ecfdf5;border-color:#6ee7b7;} .hbzj-preview-group{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;padding:0;} .hbzj-preview-hd{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;} .hbzj-preview-exam{margin:6px 8px;font-size:11px;font-weight:700;color:#9a3412;background:#fff7ed;border:1px solid #fed7aa;border-radius:8px;padding:5px 7px;line-height:1.4;} .hbzj-preview-group .hbzj-chapter-item{margin:0 6px 6px;background:#f8fafc;} .hbzj-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:11px;margin-bottom:4px;} .hbzj-meta-label{color:#64748b;}.hbzj-meta-value{font-weight:700;text-align:right;max-width:68%;word-break:break-all;} .hbzj-log-line{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;display:flex;gap:6px;align-items:baseline;flex-wrap:wrap;} .hbzj-log-time{color:#94a3b8;font-size:11px;font-weight:700;flex:0 0 auto;} .hbzj-log-text{font-weight:700;word-break:break-word;} .hbzj-log-start .hbzj-log-text{color:#15803d;} .hbzj-log-study .hbzj-log-text{color:#1d4ed8;} .hbzj-log-done .hbzj-log-text{color:#047857;} .hbzj-log-exam .hbzj-log-text{color:#c2410c;} .hbzj-log-warn .hbzj-log-text{color:#b45309;} .hbzj-log-stop .hbzj-log-text{color:#64748b;} .hbzj-log-info .hbzj-log-text{color:#334155;} .hbzj-log-hint{background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;margin:2px 0;padding:5px 7px !important;border-bottom:none;} .hbzj-log-hint .hbzj-log-text{color:#0369a1;} .hbzj-log-tip{margin:0 0 6px;padding:6px 8px;border-radius:8px;background:#eff6ff;border:1px solid #bfdbfe;color:#0369a1;font-size:11px;font-weight:700;line-height:1.45;} #hbzj-year-select{border:1px solid #cbd5e1;border-radius:8px;padding:3px 6px;font-size:12px;margin-left:4px;} #hbzj-pro-modal{position:fixed;inset:0;z-index:1000000;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center;padding:16px;} .hbzj-pro-card{width:min(420px,94vw);background:#fff;border-radius:14px;padding:16px;box-shadow:0 20px 50px rgba(0,0,0,.25);} .hbzj-pro-title{font-size:18px;font-weight:900;color:#9a3412;} .hbzj-pro-sub{margin-top:6px;font-size:12px;color:#78716c;line-height:1.45;} .hbzj-pro-sec{margin-top:12px;} .hbzj-pro-sec h4{margin:0 0 6px;font-size:13px;color:#0f172a;} .hbzj-pro-sec ul{margin:0;padding-left:18px;font-size:12px;color:#334155;line-height:1.55;} .hbzj-pro-sec p{margin:0;font-size:12px;color:#475569;line-height:1.5;} .hbzj-pro-buy-btn{margin-top:8px;border:none;background:linear-gradient(135deg,#f59e0b,#ea580c);color:#fff;border-radius:8px;padding:8px 12px;font-weight:800;cursor:pointer;width:100%;} .hbzj-pro-actions{margin-top:14px;display:flex;justify-content:flex-end;} .hbzj-pro-close{border:1px solid #cbd5e1;background:#f8fafc;border-radius:8px;padding:7px 12px;cursor:pointer;font-weight:700;} #hbzj-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%) translateY(20px);z-index:1000001;background:rgba(15,23,42,.92);color:#fff;padding:10px 14px;border-radius:10px;font-size:12px;font-weight:700;opacity:0;pointer-events:none;transition:.2s ease;max-width:86vw;} #hbzj-toast.show{opacity:1;transform:translateX(-50%) translateY(0);} `; (document.head || document.documentElement).appendChild(style); } function _cp() { if (document.getElementById("hbzj-auto-panel")) return; injectPanelStyles(); const panel = document.createElement("div"); panel.id = "hbzj-auto-panel"; panel.innerHTML = `
\u6e56\u5317\u4e13\u6280\u57f9\u8bad\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%
\u5b66\u4e60\u5e74\u4efd
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d
\u7ae0\u8282\u9884\u89c8\u89c6\u9891
\u8bf7\u52fe\u9009\u73ed\u7ea7
\u8fd0\u884c\u65e5\u5fd7\u8fdb\u5ea6
⏱ \u770b\u8bfe\u8fdb\u5ea6\u7ea6\u6bcf ${CV_INTERVAL_MINUTES} \u5206\u949f\u66f4\u65b0\u4e00\u6b21\uff0c\u7b49\u5f85\u671f\u95f4\u811a\u672c\u4ecd\u5728\u8fd0\u884c
\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\u73ed\u7ea7\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("#hbzj-btn-min")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); panel.querySelector("#hbzj-btn-max")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); panel.querySelectorAll(".hbzj-tab-btn").forEach((btn) => { btn.addEventListener("click", () => switchPanelTab(btn.dataset.tab)); }); const yearSel = panel.querySelector("#hbzj-year-select"); const fillYears = () => { if (!yearSel) return; const opts = state.yearOptions.length ? state.yearOptions : [state.studyYear || new Date().getFullYear()]; yearSel.innerHTML = opts .map((y) => ``) .join(""); }; fillYears(); yearSel?.addEventListener("change", () => { state.studyYear = Number(yearSel.value) || null; pageStorage().setItem(YEAR_KEY, String(state.studyYear || "")); void _rpp({ reason: "year" }); }); panel.__fillYears = fillYears; document.getElementById("hbzj-refresh")?.addEventListener("click", () => { void _rpp({ reason: "manual" }).then(() => panel.__fillYears?.()); }); document.getElementById("hbzj-select-unfinished")?.addEventListener("click", () => selectUnfinishedProjects()); document.getElementById("hbzj-clear-select")?.addEventListener("click", () => clearProjectSelection()); document.getElementById("hbzj-cloud-save")?.addEventListener("click", () => { const input = document.getElementById("hbzj-cloud-token"); void _sct(input?.value || "").then((ok) => { updateCloudPanelUI(); showToast(ok ? "\u5df2\u4fdd\u5b58" : "\u4fdd\u5b58\u5931\u8d25"); }); }); document.getElementById("hbzj-open-pro")?.addEventListener("click", () => openProModal()); document.getElementById("hbzj-start")?.addEventListener("click", () => { const q = loadQueue().filter((id) => { const p = state.panelProjects.find((x) => x.projectId === id); return p && isProjectSelectable(p); }); saveQueue(q); if (!q.length) { log("\u8bf7\u5148\u52fe\u9009\u73ed\u7ea7"); showToast("\u8bf7\u5148\u52fe\u9009\u73ed\u7ea7"); updatePanel(); return; } void (async () => { try { await _ecl(true); } catch (_) {} if (!(await _acp())) { updatePanel(); return; } state.enabled = true; state.lastUserAction = ""; state._studyWaitHintShown = false; updatePanel(); void _rse(); })(); }); document.getElementById("hbzj-stop")?.addEventListener("click", () => { state.enabled = false; state.lastUserAction = ""; state._studyWaitHintShown = false; updatePanel(); }); updatePanel(); void _fcc(); } async function _bst() { const host = String(location.hostname || ""); if (!/yxlearning\.com$/i.test(host)) return; const cms = readCmsAccountInfo(); if (cms?.accountId) rememberAccountId(cms.accountId); installAuthHooks(); const waitBody = () => new Promise((r) => { if (document.body) r(); else document.addEventListener("DOMContentLoaded", r, { once: true }); }); await waitBody(); await sleep(600); _cp(); await _rpp({ reason: "boot", silent: true }); try { await _ecl(false); } catch (_) {} if (!state.panelProjects.length) { for (let i = 0; i < 6 && !state.panelProjects.length; i++) { await sleep(800); if (mergeCapturedIntoPanel(true)) break; if (getAccountId()) { await _rpp({ reason: "retry", silent: true }); break; } } } const panel = document.getElementById("hbzj-auto-panel"); panel?.__fillYears?.(); state._bootLoaded = true; if (!getAccountId()) { trace("\u672a\u767b\u5f55\uff0c\u8bf7\u6253\u5f00\u6211\u7684\u5b66\u4e60\u540e\u5237\u65b0"); } else if (!state.panelProjects.length) { trace("\u5217\u8868\u4e3a\u7a7a\uff0c\u8bf7\u6253\u5f00\u6211\u7684\u5b66\u4e60\u9875\u540e\u518d\u5237\u65b0"); } } _bst().catch((e) => trace(e)); })();