// ==UserScript== // @name 【效率版】法宣在线全国智慧普法依法治理云平台学习助手 // @namespace https://faxuanyun.com/ // @version 1.0 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @description 法宣在线全国智慧普法依法治理云平台(*.faxuanyun.com)学习助手:勾选课程自动学时 + Pro自动考试,高效率完成 // @antifeature payment 免费体验章节有限,升级Pro不限 // @antifeature membership 需云端授权 // @match *://*.faxuanyun.com/* // @match *://faxuanyun.com/* // @match *://*.faxuan.net/* // @match *://faxuan.net/* // @match *://*.ifaxuan.com/* // @match *://ifaxuan.com/* // @match *://*.ifaxuan.net/* // @connect *.faxuanyun.com // @connect faxuanyun.com // @connect *.faxuan.net // @connect faxuan.net // @connect *.ifaxuan.com // @connect ifaxuan.com // @connect *.ifaxuan.net // @connect *.ahzsksw.cn // @connect ahzsksw.cn // @connect fa.ahsxks.com // @connect card.wlxy.live // @connect huaweicloudobs.ahjxjy.cn // @grant GM_xmlhttpRequest // @grant GM_info // @grant GM_openInTab // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // @license All Rights Reserved // ==/UserScript== (function () { "use strict"; const SCRIPT_VERSION = (() => { try { return String(GM_info?.script?.version || "1.0"); } catch (_) { return "1.0"; } })(); if (window.__fxFaxuanHelper) return; window.__fxFaxuanHelper = true; const HOST = location.hostname; const ORIGIN = location.origin; const PANEL_POS_KEY = "fx_panel_pos_v1"; const PANEL_COLLAPSED_KEY = "fx_panel_collapsed_v1"; const LOG_KEY = "fx_run_log_v1"; const QUEUE_KEY = "fx_course_queue_v1"; const CLOUD_TOKEN_KEY = "fx_cloud_token_v1"; const CLOUD_LEASE_CACHE_KEY = "fx_cloud_lease_cache_v1"; const CLOUD_PRO_EXPIRE_CACHE_KEY = "fx_cloud_pro_expire_cache_v1"; const CLOUD_LAST_STATE_KEY = "fx_cloud_last_state_v1"; const PANEL_LOGO_URL = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const QQ_GROUP_NUMBER = "903117129"; const QQ_GROUP_LINK = "https://qun.qq.com/universal-share/share?ac=1&authKey=rxdL6YIJ0%2FxOEemjLqTGULvl5aAfJIVQcIvkvnwvmL%2FAmpFZnSafajYHgSXMUXvx&busi_data=eyJncm91cENvZGUiOiI5MDMxMTcxMjkiLCJ0b2tlbiI6IlB2dkFGSm5XRXBrSEhtQVFTUGQzdVNZakhNWDNMbW1kODA2enpoMi9obDh4SWp0YzBDODNFaGtwRU44Z0hyU0siLCJ1aW4iOiIxMjU0MzE1MTQifQ%3D%3D&data=9oyJixSPcigCQW-saV5eXlcMwV9C6J36XySx-rDHwVwNlofvRmd2ze5sLwFtHTbYbG4nAWIUrI0qftC6aTX9xg&svctype=4&tempid=h5_group_info"; const DEFAULT_CLOUD_API_BASE = "https://oa37.ahzsksw.cn"; const PRO_BUY_URL = "https://card.wlxy.live/details/D0D9BC01"; const PANEL_NOTICE_FALLBACK = "\u6cd5\u5ba3\u5728\u7ebf\u5b66\u4e60\u52a9\u624b"; const FREE_CHAPTER_LIMIT_DEFAULT = 3; const PRO_DAYS = 30; const TERMINAL = "PC"; let EXAM_FIXED_SERIES = 316; let LEARN_MAX_JUMP_SEC = 120; const PERIOD_PROGRESS_TTL_MS = 45000; let _periodProgCache = { courseId: "", at: 0, data: null }; function invalidatePeriodProgressCache(courseId) { if (!courseId || _periodProgCache.courseId === String(courseId)) { _periodProgCache = { courseId: "", at: 0, data: null }; } } const state = { user: null, courses: [], waresByCourse: {}, selectedCourseIds: loadQueue(), running: false, stopFlag: false, lastAction: "\u5c31\u7eea", currentCourseName: "-", currentTitle: "-", progressPct: 0, queueVideoTotal: 0, queueVideoDone: 0, logs: [], secretSk: "", secretSkEnc: "", cloudApiBase: DEFAULT_CLOUD_API_BASE, cloudToken: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(), cloudTier: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim() ? "unknown" : "free", cloudLease: "", cloudLeaseExp: 0, cloudLeaseLuid: "", cloudProExpireAt: 0, cloudRevoked: false, freeVideoLimit: FREE_CHAPTER_LIMIT_DEFAULT, freeUsedVideos: 0, panelNoticePath: "/api/fxzx/panel-notice", remotePanelNotice: "", proBuyUrl: PRO_BUY_URL, lastLoggedPctBucket: -1, }; try { state.logs = JSON.parse(localStorage.getItem(LOG_KEY) || "[]") || []; } catch (_) { state.logs = []; } const lastState = readCloudLastState(); if (lastState) { let curLuid = ""; try { const u = parseCookieJson("loginUser"); curLuid = String(u?.userAccount || "").trim(); } catch (_) {} if (lastState.luid && curLuid && lastState.luid === curLuid) { if (lastState.tier) state.cloudTier = lastState.tier; if (lastState.freeVideoLimit > 0) state.freeVideoLimit = lastState.freeVideoLimit; if (lastState.freeUsedVideos >= 0) state.freeUsedVideos = lastState.freeUsedVideos; state.cloudLeaseLuid = curLuid; state._authLuid = curLuid; } else if (lastState.luid && curLuid && lastState.luid !== curLuid) { try { localStorage.removeItem(CLOUD_LAST_STATE_KEY); localStorage.removeItem(CLOUD_LEASE_CACHE_KEY); } catch (_) {} } } function loadQueue() { try { const raw = JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]"); return Array.isArray(raw) ? raw.map(String) : []; } catch (_) { return []; } } function saveQueue(ids) { const list = Array.isArray(ids) ? ids.map(String) : []; state.selectedCourseIds = list; try { localStorage.setItem(QUEUE_KEY, JSON.stringify(list)); } catch (_) {} } function escapeHtml(s) { return String(s || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function log(msg) { const line = `[${new Date().toLocaleTimeString()}] ${msg}`; state.logs.unshift(line); if (state.logs.length > 50) state.logs.length = 50; try { localStorage.setItem(LOG_KEY, JSON.stringify(state.logs.slice(0, 30))); } catch (_) {} state.lastAction = msg; const el = document.getElementById("fx-run-log"); if (el) { el.innerHTML = state.logs .slice(0, 30) .map((s) => `
${escapeHtml(s)}
`) .join(""); } } function clearRunLog() { state.logs = []; try { localStorage.removeItem(LOG_KEY); } catch (_) {} const el = document.getElementById("fx-run-log"); if (el) el.innerHTML = ""; } function logProgress(title, pct) { const bucket = Math.floor(pct / 10) * 10; if (bucket === state.lastLoggedPctBucket && pct < 100) return; if (pct < 100 && bucket === 0 && state.lastLoggedPctBucket < 0) { state.lastLoggedPctBucket = 0; return; } if (pct < 100 && bucket <= state.lastLoggedPctBucket) return; state.lastLoggedPctBucket = bucket; log(`\u5b66\u4e60\u4e2d\uff1a${title} ${Math.floor(pct)}%`); } function shouldLogCloudLine(line) { const s = String(line || "").trim(); if (!s) return false; if (/\u6548\u7387|\u500d\u901f|\u6253\u6ee1|\u7533\u62a5|\u9876\u683c|burst|tick|\u7ed3\u8bfe/i.test(s)) return false; if (/^\u5b66\u4e60\u4e2d\uff1a\d+%$/.test(s)) return false; if (s === "\u5df2\u5b8c\u6210" || s === "\u8003\u8bd5\u5b8c\u6210") return false; if (/^\u8003\u8bd5\u5b8c\u6210/.test(s)) return false; return true; } function readRawCookie(name) { const parts = document.cookie.split("; "); for (const p of parts) { const i = p.indexOf("="); if (i < 0) continue; if (decodeURIComponent(p.slice(0, i)) === name) { return decodeURIComponent(p.slice(i + 1)); } } return ""; } function parseCookieJson(name) { const raw = readRawCookie(name); if (!raw) return null; try { return JSON.parse(raw); } catch (_) { try { return JSON.parse(decodeURIComponent(raw)); } catch (_) { return null; } } } function refreshUser() { const u = parseCookieJson("loginUser"); const next = u && u.userAccount && u.sid ? String(u.userAccount).trim() : ""; const prev = String(state.cloudLeaseLuid || state._authLuid || "").trim(); if (next && prev && next !== prev) { invalidateCloudAuthOnAccountSwitch(prev, next); } if (u && u.userAccount && u.sid) { state.user = u; state._authLuid = next; return u; } if (prev && !next) { invalidateCloudAuthOnAccountSwitch(prev, ""); } state.user = null; state._authLuid = ""; return null; } function _glu() { return String(state.user?.userAccount || refreshUser()?.userAccount || "").trim(); } function invalidateCloudAuthOnAccountSwitch(prevLuid, nextLuid) { state.cloudLease = ""; state.cloudLeaseExp = 0; state.cloudLeaseLuid = ""; state.cloudTier = state.cloudToken ? "unknown" : "free"; state.cloudProExpireAt = 0; state.cloudRevoked = false; state.freeUsedVideos = 0; state.freeVideoLimit = FREE_CHAPTER_LIMIT_DEFAULT; state.stopFlag = true; writeCloudLeaseCache("", 0); try { localStorage.removeItem(CLOUD_LAST_STATE_KEY); } catch (_) {} _ucu(); if (prevLuid || nextLuid) { log( nextLuid ? `\u8d26\u53f7\u5df2\u5207\u6362\uff0c\u6388\u6743\u5df2\u91cd\u7f6e` : `\u8d26\u53f7\u5df2\u9000\u51fa\uff0c\u6388\u6743\u5df2\u91cd\u7f6e` ); } } function _aca() { const luid = _glu(); if (!luid) throw new Error("\u8bf7\u5148\u767b\u5f55"); if (state.cloudLeaseLuid && state.cloudLeaseLuid !== luid) { invalidateCloudAuthOnAccountSwitch(state.cloudLeaseLuid, luid); throw new Error("\u8d26\u53f7\u5df2\u5207\u6362\uff0c\u8bf7\u91cd\u65b0\u5f00\u59cb"); } return luid; } async function _cas(op, texts) { const list = (texts || []).map((x) => (x == null ? "" : String(x))); if (!list.length) return []; await _el(false); const data = await _crq("/api/fxzx/crypto/seal", "POST", { op: op === "decrypt" ? "decrypt" : "encrypt", texts: list, }); if (!data || data.ok === false) { throw new Error(data?.message || data?.detail || "\u4e91\u7aef\u52a0\u89e3\u5bc6\u5931\u8d25"); } if (op === "decrypt") { const arr = data.plaintexts; if (Array.isArray(arr) && arr.length === list.length) return arr.map(String); if (data.plaintext != null && list.length === 1) return [String(data.plaintext)]; } else { const arr = data.ciphertexts; if (Array.isArray(arr) && arr.length === list.length) return arr.map(String); if (data.ciphertext != null && list.length === 1) return [String(data.ciphertext)]; } throw new Error("\u4e91\u7aef\u52a0\u89e3\u5bc6\u56de\u5305\u5f02\u5e38"); } async function _cpk(items) { const list = (items || []).filter((x) => x && typeof x === "object"); if (!list.length) return { fields: {}, ciphertexts: [], items: [] }; await _el(false); const data = await _crq("/api/fxzx/crypto/pack", "POST", { items: list }); if (!data || data.ok === false) { throw new Error(data?.message || data?.detail || "\u4e91\u7aef\u62fc\u88c5\u52a0\u5bc6\u5931\u8d25"); } return { fields: data.fields && typeof data.fields === "object" ? data.fields : {}, ciphertexts: Array.isArray(data.ciphertexts) ? data.ciphertexts : [], items: Array.isArray(data.items) ? data.items : [], }; } async function _pf(kind, params, name) { const packed = await _cpk([ Object.assign({ kind, name: name || kind }, params || {}), ]); const key = name || kind; const v = packed.fields[key] || packed.ciphertexts[0]; if (!v) throw new Error("\u4e91\u7aef pack \u65e0\u5bc6\u6587"); return String(v); } async function _ae(plain) { if (plain == null || plain === "") return plain; const [out] = await _cas("encrypt", [String(plain)]); return out; } async function _aem(plains) { const list = (plains || []).map((x) => (x == null ? "" : String(x))); if (!list.length) return []; const needIdx = []; const need = []; list.forEach((t, i) => { if (t !== "") { needIdx.push(i); need.push(t); } }); if (!need.length) return list; const sealed = await _cas("encrypt", need); const out = list.slice(); needIdx.forEach((i, j) => { out[i] = sealed[j]; }); return out; } async function _ad(b64) { if (b64 == null || b64 === "") return ""; const [out] = await _cas("decrypt", [String(b64)]); return out || ""; } async function _wsk() { if (!state.secretSk) return ""; if (state.secretSkEnc) return state.secretSkEnc; state.secretSkEnc = await _ae(state.secretSk); return state.secretSkEnc; } function _csk() { state.secretSk = ""; state.secretSkEnc = ""; } async function ensureCryptoJs() { return true; } function gmRequest(method, url, body, headers) { return new Promise((resolve, reject) => { const h = Object.assign( { Accept: "application/json, text/plain, */*" }, headers || {} ); const done = (status, text) => { if (status >= 200 && status < 300) resolve({ status, text }); else reject(new Error(`HTTP ${status}: ${(text || "").slice(0, 160)}`)); }; if (typeof GM_xmlhttpRequest === "function") { GM_xmlhttpRequest({ method, url, headers: h, data: body, withCredentials: true, anonymous: false, onload(res) { done(res.status, res.responseText); }, onerror() { reject(new Error("\u7f51\u7edc\u9519\u8bef")); }, }); return; } fetch(url, { method, headers: h, body, credentials: "include" }) .then(async (r) => { const text = await r.text(); done(r.status, text); }) .catch((e) => reject(e)); }); } function gmRequestWithStatus(url, method, headers, data) { return new Promise((resolve, reject) => { const req = { method: method || "GET", url, headers: headers || {}, timeout: 30000, withCredentials: false, responseType: "text", onload: (res) => resolve({ status: res.status, text: res.responseText || "" }), onerror: () => reject(new Error("\u7f51\u7edc\u9519\u8bef")), ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6")), }; if (data != null) req.data = data; if (typeof GM_xmlhttpRequest === "function") { GM_xmlhttpRequest(req); return; } const xhr = new XMLHttpRequest(); xhr.open(req.method, url, true); xhr.timeout = req.timeout; Object.keys(req.headers).forEach((k) => xhr.setRequestHeader(k, req.headers[k])); xhr.onload = () => resolve({ status: xhr.status, text: xhr.responseText || "" }); xhr.onerror = () => reject(new Error("\u7f51\u7edc\u9519\u8bef")); xhr.ontimeout = () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6")); xhr.send(data); }); } const FALLBACK_API_ORIGINS = [ "http://nmg.faxuanyun.com", "https://nmg.faxuanyun.com", "http://ah.faxuanyun.com", "https://ah.faxuanyun.com", "http://bj.faxuanyun.com", "https://bj.faxuanyun.com", "http://sx.faxuanyun.com", "https://sx.faxuanyun.com", "http://ha.faxuanyun.com", "https://ha.faxuanyun.com", "http://sc.faxuanyun.com", "https://sc.faxuanyun.com", "http://gd.faxuanyun.com", "https://gd.faxuanyun.com", ]; let apiOrigin = ORIGIN; function isLearningTenantHost(host) { const h = String(host || "").toLowerCase(); return ( /(^|\.)faxuanyun\.com$/.test(h) || /(^|\.)faxuan\.net$/.test(h) ) && !/(^|\.)(www\.)?ifaxuan\.(com|net)$/.test(h); } function regionalApiCandidates() { const out = []; const push = (o) => { const s = String(o || "").replace(/\/$/, ""); if (s && !out.includes(s)) out.push(s); }; if (isLearningTenantHost(HOST)) { push(ORIGIN); if (ORIGIN.startsWith("https://")) push("http://" + HOST); else if (ORIGIN.startsWith("http://")) push("https://" + HOST); } else { push(ORIGIN); } FALLBACK_API_ORIGINS.forEach(push); return out; } function apiUrl(path) { if (/^https?:\/\//i.test(path)) return path; return String(apiOrigin).replace(/\/$/, "") + path; } async function probeApiOrigin() { const candidates = regionalApiCandidates(); for (const origin of candidates) { try { const url = origin.replace(/\/$/, "") + "/bps/common/comm_resources.js"; const res = await gmRequest("GET", url, undefined, {}); if (res.text && res.text.indexOf("_rlp") >= 0) { apiOrigin = origin; if (origin !== ORIGIN) { log("\u5df2\u5207\u6362\u5b66\u4e60\u7ad9\u70b9\uff1a" + origin.replace(/^https?:\/\//, "")); } return origin; } } catch (_) {} } apiOrigin = isLearningTenantHost(HOST) ? ORIGIN : candidates[0] || ORIGIN; return apiOrigin; } async function apiForm(path, fields) { const body = new URLSearchParams(); Object.keys(fields || {}).forEach((k) => { if (fields[k] == null) return; body.append(k, String(fields[k])); }); const res = await gmRequest("POST", apiUrl(path), body.toString(), { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", }); const text = res.text || ""; const meta = { _http: res.status, _raw: text, _bodyLen: text.length, _path: String(path || ""), }; if (!text) { return Object.assign({ code: 200, msg: "", _empty: true }, meta); } try { const j = JSON.parse(text); if (j && typeof j === "object" && !Array.isArray(j)) { return Object.assign({}, j, meta); } return Object.assign({ code: 200, data: j }, meta); } catch (_) { return Object.assign({ code: 200, raw: text }, meta); } } async function apiGet(path) { const res = await gmRequest("GET", apiUrl(path), undefined, {}); return res.text || ""; } async function getSystemTime() { try { const j = await apiForm("/dsyun/service/getSystemTime", {}); let n = null; if (j == null) n = null; else if (typeof j === "number") n = j; else if (j.data != null) n = Number(j.data); else if (j.raw != null && String(j.raw).trim() !== "") n = Number(j.raw); if (n != null && Number.isFinite(n) && n > 0) { return n < 1e12 ? Math.floor(n * 1000) : Math.floor(n); } } catch (_) {} return Date.now(); } function getCloudApiBase() { return String(state.cloudApiBase || DEFAULT_CLOUD_API_BASE).replace(/\/$/, ""); } function resolveLeaseExpireSec(data) { const raw = data?.exp ?? data?.expire_at ?? data?.expires_at ?? data?.lease_exp ?? 0; const n = Number(raw || 0); if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000); return Number.isFinite(n) ? Math.floor(n) : 0; } function resolveProExpireSec(data) { const raw = data?.pro_expires_at ?? data?.proExpiresAt ?? data?.pro_expire_at ?? 0; const n = Number(raw || 0); if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000); return Number.isFinite(n) ? Math.floor(n) : 0; } function formatLeaseExpireText(expSec) { const ex = Number(expSec || 0); if (!Number.isFinite(ex) || ex <= 0) return "—"; const d = new Date(ex * 1000); if (Number.isNaN(d.getTime())) return "—"; return d.toLocaleString("zh-CN", { hour12: false }); } function readCloudLeaseCache() { try { const raw = localStorage.getItem(CLOUD_LEASE_CACHE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") return null; const lease = String(parsed.lease || "").trim(); const exp = Number(parsed.exp || 0); const luid = String(parsed.luid || parsed.learning_user_id || "").trim(); if (!lease || !Number.isFinite(exp) || exp <= 0) return null; return { lease, exp, luid, tier: String(parsed.tier || "").trim(), freeVideoLimit: Number( parsed.freeVideoLimit ?? parsed.free_video_limit ?? parsed.free_chapter_limit ?? FREE_CHAPTER_LIMIT_DEFAULT ), freeUsedVideos: Number( parsed.freeUsedVideos ?? parsed.free_used_videos ?? 0 ), proExpireAt: Number(parsed.proExpireAt ?? parsed.pro_expire_at ?? 0), }; } catch (_) { return null; } } function writeCloudLeaseCache(lease, exp, extra) { if (!lease || !exp) { localStorage.removeItem(CLOUD_LEASE_CACHE_KEY); return; } localStorage.setItem( CLOUD_LEASE_CACHE_KEY, JSON.stringify(Object.assign({ lease, exp }, extra || {})) ); } function readCloudLastState() { try { const raw = localStorage.getItem(CLOUD_LAST_STATE_KEY); if (!raw) return null; const p = JSON.parse(raw); if (!p || typeof p !== "object") return null; return { luid: String(p.luid || "").trim(), tier: String(p.tier || "").trim(), freeVideoLimit: Number(p.freeVideoLimit ?? p.free_video_limit ?? 1), freeUsedVideos: Number(p.freeUsedVideos ?? p.free_used_videos ?? 0), }; } catch (_) { return null; } } function writeCloudLastState(partial) { try { const prev = readCloudLastState() || {}; localStorage.setItem( CLOUD_LAST_STATE_KEY, JSON.stringify( Object.assign({}, prev, partial || {}, { ts: Date.now() }) ) ); } catch (_) {} } function writeCloudProExpireCache(token, exp) { try { const tk = String(token || "").trim(); if (!tk || !exp) { localStorage.removeItem(CLOUD_PRO_EXPIRE_CACHE_KEY); return; } localStorage.setItem( CLOUD_PRO_EXPIRE_CACHE_KEY, JSON.stringify({ token: tk, exp: Math.floor(exp) }) ); } catch (_) {} } async function _crq(path, method, payload) { const base = getCloudApiBase(); const url = `${base}${path.startsWith("/") ? path : `/${path}`}`; const headers = { Accept: "application/json", "Content-Type": "application/json", }; const luid = _glu(); if (luid) headers["x-learning-user-id"] = luid; if (state.cloudToken) headers.Authorization = `Bearer ${state.cloudToken}`; let reqPayload = payload; if (reqPayload && typeof reqPayload === "object") { reqPayload = Object.assign({}, reqPayload); if (path !== "/api/fxzx/lease" && state.cloudLease && reqPayload.lease == null) { reqPayload.lease = state.cloudLease; } } const body = reqPayload == null ? null : JSON.stringify(reqPayload); const res = await gmRequestWithStatus(url, method || "POST", headers, body); const text = String(res.text || "").trim(); let data = {}; if (text) { try { data = JSON.parse(text); } catch (_) { data = { raw: text }; } } if (res.status < 200 || res.status >= 300) { const msg = String( data.detail || data.message || data.code || `http_${res.status}` ); if ( (res.status === 401 || res.status === 403) && /lease_user_mismatch|learning_user_mismatch|token_bound/i.test(msg) ) { invalidateCloudAuthOnAccountSwitch( state.cloudLeaseLuid, _glu() ); } throw new Error(msg); } return data; } async function _el(forceRefresh) { const luid = _glu(); if (!luid) throw new Error("\u8bf7\u5148\u767b\u5f55"); const now = Math.floor(Date.now() / 1000); if ( !forceRefresh && state.cloudLease && state.cloudLeaseLuid === luid && state.cloudLeaseExp - now > 60 ) { return true; } if (!forceRefresh) { const cached = readCloudLeaseCache(); if ( cached && cached.luid === luid && cached.exp - now > 60 ) { state.cloudLease = cached.lease; state.cloudLeaseExp = cached.exp; state.cloudLeaseLuid = luid; state.cloudProExpireAt = Number(cached.proExpireAt || 0); if (cached.tier) state.cloudTier = cached.tier; if (cached.freeVideoLimit > 0) state.freeVideoLimit = cached.freeVideoLimit; if (cached.freeUsedVideos >= 0) { state.freeUsedVideos = Math.max( state.freeUsedVideos, cached.freeUsedVideos ); } _ucu(); return true; } if (cached && cached.luid && cached.luid !== luid) { writeCloudLeaseCache("", 0); } } let data; try { data = await _crq("/api/fxzx/lease", "POST", { learning_user_id: luid, }); state.cloudRevoked = false; } catch (e) { const em = String(e?.message || e); if (/(revoked|invalid token|expired|learning_user_mismatch|token_bound)/i.test(em)) { state.cloudRevoked = true; state.cloudTier = "revoked"; state.cloudLease = ""; state.cloudLeaseExp = 0; state.cloudLeaseLuid = ""; writeCloudLeaseCache("", 0); _ucu(); } throw e; } const lease = String(data.lease || ""); const exp = resolveLeaseExpireSec(data); let tier = String(data.tier || "").trim() || (state.cloudToken ? "pro" : "free"); const proExpireAt = resolveProExpireSec(data); state.cloudLease = lease; state.cloudLeaseExp = exp; state.cloudLeaseLuid = luid; state.cloudProExpireAt = proExpireAt > 0 ? proExpireAt : 0; state.cloudTier = tier; state.freeVideoLimit = Number( data.free_chapter_limit ?? data.free_video_limit ?? data.freeVideoLimit ?? state.freeVideoLimit ?? FREE_CHAPTER_LIMIT_DEFAULT ) || FREE_CHAPTER_LIMIT_DEFAULT; const serverUsed = Number( data.free_used_videos ?? data.free_used_chapters ?? data.freeUsedVideos ?? 0 ); state.freeUsedVideos = serverUsed; writeCloudLeaseCache(lease, exp, { luid, tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, proExpireAt: state.cloudProExpireAt, }); writeCloudLastState({ luid, tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, }); if (tier === "pro" && state.cloudProExpireAt > 0) { writeCloudProExpireCache(state.cloudToken, state.cloudProExpireAt); } _ucu(); return !!lease; } async function cloudVerifyToken() { if (!state.cloudToken) return true; const base = getCloudApiBase(); const url = `${base}/api/license/verify`; const headers = { Accept: "application/json", Authorization: `Bearer ${state.cloudToken}`, }; const luid = _glu(); if (luid) headers["x-learning-user-id"] = luid; const res = await gmRequestWithStatus(url, "GET", headers, null); const text = String(res.text || "").trim(); let data = {}; if (text) { try { data = JSON.parse(text); } catch (_) {} } if (res.status < 200 || res.status >= 300 || data.ok === false) { throw new Error(String(data.message || data.detail || "\u6388\u6743\u65e0\u6548\u8bf7\u68c0\u67e5 Token")); } return true; } async function _ecr() { refreshUser(); if (!state.user) { log("\u8bf7\u5148\u767b\u5f55"); return false; } try { if (state.cloudToken) await cloudVerifyToken(); await _el(false); } catch (err) { const msg = String(err?.message || err); if (/token|\u6388\u6743|invalid|expired|revoked/i.test(msg)) { log("\u6388\u6743\u65e0\u6548\u8bf7\u68c0\u67e5 Token"); } else { log("\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"); } console.debug("[\u6cd5\u5ba3\u52a9\u624b] cloud error", err); return false; } const tier = String(state.cloudTier || "").toLowerCase(); if (tier === "pro") return true; if (tier === "free" || tier === "unknown") { if (Number(state.freeUsedVideos) >= Number(state.freeVideoLimit)) { log("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"); switchPanelTab("settings"); openProModal(); return false; } return true; } log("\u6388\u6743\u65e0\u6548\u8bf7\u68c0\u67e5 Token"); return false; } function canTakeExam() { return String(state.cloudTier || "").toLowerCase() === "pro"; } function canStudyNextFreeVideo() { if (state.cloudRevoked) return false; const tier = String(state.cloudTier || "").toLowerCase(); if (tier === "pro") return true; return Number(state.freeUsedVideos) < Number(state.freeVideoLimit); } function markFreeVideoUsed() { const tier = String(state.cloudTier || "").toLowerCase(); if (tier === "pro") return; state.freeUsedVideos = Number(state.freeUsedVideos || 0) + 1; writeCloudLastState({ tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, }); _ucu(); } async function _fpn() { try { const origin = getCloudApiBase(); const path = String(state.panelNoticePath || "/api/fxzx/panel-notice"); const url = `${origin}${path.startsWith("/") ? path : `/${path}`}`; const res = await gmRequestWithStatus(url, "GET", {}, null); if (res.status >= 200 && res.status < 300) { state.remotePanelNotice = String(res.text || "").trim() || PANEL_NOTICE_FALLBACK; const el = document.querySelector("#fx-ann-text"); if (el) el.textContent = state.remotePanelNotice; } } catch (_) {} } async function _fcc() { try { const data = await gmRequestWithStatus( `${getCloudApiBase()}/api/fxzx/client-config`, "GET", { Accept: "application/json" }, null ); if (data.status >= 200 && data.status < 300 && data.text) { const j = JSON.parse(data.text); if (j?.panelNoticePath) state.panelNoticePath = String(j.panelNoticePath); if (j?.freeVideoLimit != null) state.freeVideoLimit = Number(j.freeVideoLimit) || FREE_CHAPTER_LIMIT_DEFAULT; if (j?.free_chapter_limit != null) state.freeVideoLimit = Number(j.free_chapter_limit) || FREE_CHAPTER_LIMIT_DEFAULT; if (j?.proDays != null) state.proDays = Number(j.proDays) || PRO_DAYS; if (j?.proBuyUrl) state.proBuyUrl = String(j.proBuyUrl).trim() || PRO_BUY_URL; if (j?.examFixedSeries != null) { const n = Number(j.examFixedSeries); if (Number.isFinite(n)) EXAM_FIXED_SERIES = Math.max(0, Math.min(499, Math.floor(n))); } if (j?.learnMaxJumpSec != null) { const n = Number(j.learnMaxJumpSec); if (Number.isFinite(n) && n > 0) LEARN_MAX_JUMP_SEC = Math.floor(n); } } } catch (_) {} await _fpn(); } function getProBuyUrl() { return String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL; } function openProBuyPage() { const url = getProBuyUrl(); try { GM_openInTab(url, { active: true, insert: true, setParent: true }); } catch (_) { window.open(url, "_blank", "noopener,noreferrer"); } } function openProModal() { const vm = document.getElementById("fx-pro-modal"); if (vm) vm.style.display = "flex"; } function closeProModal() { const vm = document.getElementById("fx-pro-modal"); if (vm) vm.style.display = "none"; } function createProModal() { const old = document.getElementById("fx-pro-modal"); if (old) old.remove(); const modal = document.createElement("div"); modal.id = "fx-pro-modal"; modal.innerHTML = `
\u5f00\u901a Pro
Pro \u6fc0\u6d3b\u540e ${PRO_DAYS} \u5929\u6709\u6548\uff0c\u8bfe\u7a0b\u4e0e\u8003\u8bd5\u4e0d\u9650

Pro \u6743\u76ca

\u5f00\u901a\u65b9\u5f0f

1) \u52a0\u5165 QQ \u7fa4 ${QQ_GROUP_NUMBER}\uff0c\u8054\u7cfb\u7ba1\u7406\u5458\u83b7\u53d6 Token

2) \u524d\u5f80\u8d2d\u4e70\u9875\u83b7\u53d6 Token

\u8d2d\u5f97\u540e\uff1a\u5728\u8bbe\u7f6e\u9875\u7c98\u8d34 Token\uff0c\u70b9\u51fb\u300c\u4fdd\u5b58\u5e76\u6821\u9a8c\u300d\u3002
`; document.documentElement.appendChild(modal); modal.addEventListener("click", (e) => { if (e.target === modal) closeProModal(); }); modal.querySelector("#fx-pro-close")?.addEventListener("click", closeProModal); modal.querySelector("#fx-pro-buy-link")?.addEventListener("click", openProBuyPage); } function formatCloudTierText(tier) { if (state.cloudRevoked) return "\u5df2\u7981\u7528"; const t = String(tier || "").trim().toLowerCase(); if (t === "pro") return "Pro \u4f1a\u5458"; if (t === "free") return "\u514d\u8d39\u4f53\u9a8c"; if (t === "revoked") return "\u5df2\u7981\u7528"; if (t === "unknown") return "\u672a\u6821\u9a8c"; return tier ? String(tier) : "\u672a\u6821\u9a8c"; } function _ucu() { const tierEl = document.querySelector("#fx-cloud-tier"); const freeEl = document.querySelector("#fx-cloud-free"); const freeLabelEl = document.querySelector("#fx-cloud-free-label"); const tokenInput = document.querySelector("#fx-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 (freeLabelEl) freeLabelEl.textContent = "\u6388\u6743\u72b6\u6001"; if (freeEl) freeEl.textContent = "Token \u5df2\u7981\u7528"; } else if (String(state.cloudTier || "").toLowerCase() === "pro") { if (freeLabelEl) freeLabelEl.textContent = "Pro \u5230\u671f"; if (freeEl) freeEl.textContent = formatLeaseExpireText( state.cloudProExpireAt || state.cloudLeaseExp ); } else { if (freeLabelEl) freeLabelEl.textContent = "\u514d\u8d39\u4f53\u9a8c\u7ae0\u8282"; if (freeEl) freeEl.textContent = `${state.freeUsedVideos}/${state.freeVideoLimit}`; } if (tokenInput && tokenInput !== document.activeElement) { tokenInput.value = state.cloudToken || ""; } } function domainGrade(domainCode) { const s = String(domainCode || ""); let g = 1; for (let v = 1; v <= 5; v++) { const slice = s.substr(0, v * 3); if (slice && !/^0+$/.test(s.substr(v * 3))) g = v; } let n = 0; for (let i = 0; i < s.length; i += 3) { const part = s.substr(i, 3); if (part && part !== "000") n = i / 3 + 1; } return Math.max(1, n || g); } function padDomain(prefix, totalLen) { let s = String(prefix || ""); while (s.length < totalLen) s += "0"; return s.slice(0, totalLen); } function buildCourseFields(user, courseClassify) { const e = Number(courseClassify) || 12; const w = user.rankId || ""; const x = user.politicsCode || ""; const k = String(user.domainCode || ""); const b = domainGrade(k); let a = ""; for (let v = 1; v <= b; v++) { const u = padDomain(k.substr(0, v * 3), 15); a += `STUDY:COURSE:STR.${e}_${u}_${w},`; if (x) a += `STUDY:COURSE:STR.${e}_${u}_${x},`; } return a.replace(/,+$/, ""); } async function getIndustryCodes(domainCode) { try { const res = await gmRequest( "POST", apiUrl("/bss/service/getIndustryCodes"), new URLSearchParams({ domainCode: domainCode || "" }).toString(), { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" } ); const text = (res.text || "").trim(); if (!text) return "00000000"; try { const j = JSON.parse(text); if (typeof j === "string") return j; if (j?.data != null) return String(j.data); } catch (_) { return text.replace(/^"|"$/g, ""); } return text; } catch (_) {} return "00000000"; } function parseCourseListPayload(text) { const out = []; const raw = String(text || "").trim(); if (!raw || raw === "null" || raw === "[]" || raw === "{}") return out; const pushCourse = (item) => { if (!item) return; if (typeof item === "string") { const s = item.trim(); if (!s || s === "{}" || s === "null") return; try { pushCourse(JSON.parse(s)); } catch (_) {} return; } if (Array.isArray(item)) { item.forEach(pushCourse); return; } if (item.courseId != null || item.courseName) { out.push({ courseId: String(item.courseId), courseName: item.courseName || String(item.courseId), courseWareCount: Number(item.courseWareCount || 0), courseWarePeriod: item.courseWarePeriod || "", courseClassify: Number(item.courseClassify || 0) || 0, courseType: item.courseType, raw: item, }); return; } Object.keys(item).forEach((k) => pushCourse(item[k])); }; try { if (raw.indexOf('{"') === 0) { const obj = JSON.parse(raw); Object.keys(obj).forEach((k) => pushCourse(obj[k])); } else if (raw.indexOf("[") === 0) { const arr = JSON.parse(raw); arr.forEach((row) => { if (!row) return; if (row.courseId != null) { pushCourse(row); return; } Object.keys(row).forEach((k) => pushCourse(row[k])); }); } else { pushCourse(JSON.parse(raw)); } } catch (e) { console.debug("[\u6cd5\u5ba3\u52a9\u624b] \u8bfe\u7a0b\u89e3\u6790\u5931\u8d25", e); } const seen = new Set(); return out.filter((c) => { if (!c.courseId || seen.has(c.courseId)) return false; seen.add(c.courseId); return true; }); } function courseKindLabel(course) { const c = Number(course?.courseClassify || course?.raw?.courseClassify || 0); if (c === 12) return "\u5fc5\u4fee\u8bfe"; if (c === 11) return "\u9009\u4fee\u8bfe"; if (c === 14 || c === 16 || c === 19) return "\u884c\u653f\u6267\u6cd5"; if (c === 18) return "\u5b66\u6cd5\u521b\u4e1a"; if (c === 24) return "\u516b\u4e94\u666e\u6cd5"; if (c === 25) return "\u601d\u653f\u5b66\u521b"; return ""; } function formatCoursePeriod(period) { const n = Number(period); if (!Number.isFinite(n) || n <= 0) return ""; const s = Number.isInteger(n) ? String(n) : String(n); return `${s} \u8bfe\u65f6`; } function collectCoursesFromPage() { const out = []; const seen = new Set(); const push = (c, classifyHint) => { if (!c || c.courseId == null) return; const id = String(c.courseId); if (seen.has(id)) return; seen.add(id); out.push({ courseId: id, courseName: c.courseName || id, courseWareCount: Number(c.courseWareCount || 0), courseWarePeriod: c.courseWarePeriod || "", courseClassify: Number(c.courseClassify || classifyHint || 0) || Number(classifyHint || 0) || 0, courseType: c.courseType, raw: c, }); }; try { if (typeof bxArr !== "undefined" && Array.isArray(bxArr)) bxArr.forEach((c) => push(c, 12)); if (typeof xxArr !== "undefined" && Array.isArray(xxArr)) xxArr.forEach((c) => push(c, 11)); if (typeof xzzfArr !== "undefined" && Array.isArray(xzzfArr)) xzzfArr.forEach((c) => push(c, 14)); if (typeof xjxcyArr !== "undefined" && Array.isArray(xjxcyArr)) xjxcyArr.forEach((c) => push(c, 18)); if (typeof ssxcyArr !== "undefined" && Array.isArray(ssxcyArr)) ssxcyArr.forEach((c) => push(c, 25)); if (typeof eightArr !== "undefined" && Array.isArray(eightArr)) eightArr.forEach((c) => push(c, 24)); } catch (_) {} return out; } async function fetchCoursesByClassify(user, industry, courseClassify) { const fields = buildCourseFields(user, courseClassify); const q = new URLSearchParams({ fileds: fields, industryCodes: industry || "00000000", languageType: "4", time: String(new Date()), }); const res = await gmRequest( "POST", apiUrl("/sss/service/getstudycoursebyindusrtycodenew?" + q.toString()), "", { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" } ); const list = parseCourseListPayload(res.text); const hint = Number(courseClassify) || 0; if (!hint) return list; return list.map((c) => ({ ...c, courseClassify: Number(c.courseClassify || hint) || hint, })); } async function loadCourses() { const user = refreshUser(); if (!user) throw new Error("\u8bf7\u5148\u767b\u5f55"); await probeApiOrigin(); let industry = await getIndustryCodes(user.domainCode); industry = String(industry || "00000000").trim(); if (!industry.includes("00000000")) { industry = industry.replace(/;?$/, "") + ";00000000"; } let courses = []; for (const classify of [12, 11]) { try { const list = await fetchCoursesByClassify(user, industry, classify); courses = courses.concat(list); } catch (e) { console.debug("[\u6cd5\u5ba3\u52a9\u624b] classify fail", classify, e); } } if (!courses.length) { try { courses = await fetchCoursesByClassify(user, "00000000", 12); } catch (_) {} } if (!courses.length) { const pageCourses = collectCoursesFromPage(); if (pageCourses.length) courses = pageCourses; } const seen = new Set(); state.courses = courses.filter((c) => { if (seen.has(c.courseId)) return false; seen.add(c.courseId); return true; }); const valid = new Set(state.courses.map((c) => c.courseId)); const q = loadQueue().filter((id) => valid.has(id)); saveQueue(q); log(`\u5df2\u52a0\u8f7d\u8bfe\u7a0b ${state.courses.length} \u95e8`); renderCourseList(); await refreshChapterPreview(); updateQueueSummary(); } function parseWareList(text) { const lines = String(text || "") .split(/\r?\n/) .map((l) => l.trim()) .filter(Boolean); const out = []; for (const line of lines) { if (/^[*$]/.test(line)) continue; const parts = line.split("_"); if (parts.length < 5) continue; const id = parts[0]; if (!/^\d+$/.test(id)) continue; const dateOrLecturer = parts[parts.length - 1]; const hasDate = /^\d{4}-\d{2}-\d{2}/.test(dateOrLecturer); const tail = hasDate ? 4 : 3; if (parts.length < tail + 1) continue; const type = Number(parts[parts.length - tail]) || 0; const period = parts[parts.length - (tail - 1)] || ""; const title = parts.slice(1, parts.length - tail).join("_") || id; out.push({ id: String(id), title, type, period, progress: 0, finished: false, }); } return out; } async function loadWareProgress(courseId, force) { const user = state.user; if (!user) return {}; const cid = String(courseId || ""); const now = Date.now(); if ( !force && _periodProgCache.data && _periodProgCache.courseId === cid && now - _periodProgCache.at < PERIOD_PROGRESS_TTL_MS ) { return _periodProgCache.data; } const skey = await _pf("period_skey", { time: Date.now(), userAccount: user.userAccount, sid: user.sid, courseId, terminal: TERMINAL, }); const j = await apiForm( "/sss/service/coursewareService!getLearnPeriod.do", { userAccount: user.userAccount, terminalCode: TERMINAL, skey, } ); const data = j?.data || {}; _periodProgCache = { courseId: cid, at: Date.now(), data }; return data; } async function getLearnedSeconds(courseId, coursewareId) { const user = state.user; const skey = await _pf("progress_skey", { userAccount: user.userAccount, sid: user.sid, courseId, coursewareId, time: Date.now(), terminal: TERMINAL, }); const j = await apiForm( "/sss/service/coursewareService!doGetLearnProgress.do", { userAccount: user.userAccount, terminalCode: TERMINAL, skey, } ); if (j?.code != null && j.code !== 200) return null; const sec = Number(j?.data); return Number.isFinite(sec) ? Math.max(0, sec) : null; } async function _gsp(courseId, coursewareId, duration, opts) { const needSec = !!(opts && opts.needSec); const forcePeriod = !!(opts && opts.force); let pctFromPeriod = null; try { const prog = await loadWareProgress(courseId, forcePeriod); if (prog && prog[`${coursewareId}_progress`] != null) { pctFromPeriod = Math.max( 0, Math.min(100, Number(prog[`${coursewareId}_progress`]) || 0) ); } } catch (_) {} let pctFromSec = null; let learnedSec = null; if (needSec) { try { learnedSec = await getLearnedSeconds(courseId, coursewareId); const dur = Number(duration) || 0; if (learnedSec != null && dur > 0) { pctFromSec = Math.max(0, Math.min(100, (learnedSec / dur) * 100)); } } catch (_) {} } if (pctFromPeriod == null && pctFromSec == null && learnedSec == null) { return null; } const pct = Math.max(pctFromPeriod || 0, pctFromSec || 0); return { pct, learnedSec, pctFromPeriod, pctFromSec }; } function progressPctOf(sp) { if (sp == null) return null; if (typeof sp === "number") return sp; if (typeof sp === "object" && typeof sp.pct === "number") return sp.pct; return null; } async function loadUserCourseMeta(courseId) { const user = state.user; const finished = new Set(); const studied = new Set(); try { const q = new URLSearchParams({ userAccount: user.userAccount, ssid: user.sid, time: String(new Date()), }); const res = await gmRequest( "POST", apiUrl("/sss/service/getusercourseinfo?" + q.toString()), "", { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" } ); const arr = JSON.parse(res.text); if (Array.isArray(arr)) { for (let i = 0; i + 1 < arr.length; i += 2) { const k = String(arr[i]); const v = String(arr[i + 1] || ""); if (k === `${courseId}_finished` && v) { v.split(",").forEach((id) => id && finished.add(String(id).trim())); } if (k === String(courseId) && v) { v.split(",").forEach((id) => id && studied.add(String(id).trim())); } } } } catch (e) { console.debug("[\u6cd5\u5ba3\u52a9\u624b] getusercourseinfo", e); } return { finished, studied }; } async function loadWares(courseId) { const text = await apiGet( `/sss/service/getcourseware?courseId=${encodeURIComponent(courseId)}&time=${encodeURIComponent(String(new Date()))}` ); const wares = parseWareList(text); let prog = {}; try { prog = await loadWareProgress(courseId); } catch (e) { console.debug("[\u6cd5\u5ba3\u52a9\u624b] progress", e); } const meta = await loadUserCourseMeta(courseId); for (const w of wares) { const p = prog[`${w.id}_progress`]; if (p != null) w.progress = Number(p) || 0; const finFlag = prog[`${w.id}_finished`]; if ( meta.finished.has(w.id) || w.progress >= 100 || finFlag == 1 || finFlag === "1" || finFlag === true ) { w.finished = true; w.progress = Math.max(w.progress, 100); } } state.waresByCourse[courseId] = wares; return wares; } async function getWareDetail(coursewareId) { const text = await apiGet( `/sss/service/getcoursecontent?coursewareId=${encodeURIComponent(coursewareId)}&time=${encodeURIComponent(String(new Date()))}` ); const m = text.match(/\{[\s\S]*\}/); if (!m) return null; try { return JSON.parse(m[0]); } catch (_) { return null; } } async function doComeStudy(courseId, coursewareId) { const user = state.user; const skey = await _pf("begin_skey", { time: Date.now(), userAccount: user.userAccount, sid: user.sid, courseId, coursewareId, terminal: TERMINAL, }); return apiForm("/sss/service/coursewareService!doBeginLeaningCheck.do", { userAccount: user.userAccount, terminalCode: TERMINAL, skey, time: Date.parse(new Date()), }); } async function _rsl(stuckCourseId, stuckWareId) { const prevSk = state.secretSk; const prevSkEnc = state.secretSkEnc; try { log(`\u9000\u51fa\u672a\u5b8c\u6210\u8bfe\u4ef6\u540e\u7ee7\u7eed`); await doComeStudy(stuckCourseId, stuckWareId); try { await initStudyExam(stuckCourseId, stuckWareId); } catch (_) {} let end = await endStudy(stuckCourseId, stuckWareId, 0); if (Number(end?.code) !== 200) { await sleep(400); end = await endStudy(stuckCourseId, stuckWareId, 0); } if (Number(end?.code) !== 200) { await sleep(400); end = await endStudy(stuckCourseId, stuckWareId, 1); } return Number(end?.code) === 200; } finally { state.secretSk = prevSk || ""; state.secretSkEnc = prevSkEnc || ""; } } async function initStudyExam(courseId, coursewareId) { const user = state.user; const t = await getSystemTime(); const secondaryKey = await _pf("init_study_secondary", { time: t, userAccount: user.userAccount, sid: user.sid, courseId, coursewareId, terminal: TERMINAL, }); const j = await apiForm("/sss/service/studyExamService!doInitStudyExam.do", { userAccount: user.userAccount, terminalCode: TERMINAL, type: 2, secondaryKey, time: t, }); if (j?.code !== 200 || !j?.data?.secretKey) { throw new Error(j?.msg || "\u521d\u59cb\u5316\u5b66\u4e60\u5931\u8d25"); } state.secretSk = j.data.secretKey; state.secretSkEnc = ""; return j.data.secretKey; } async function _qls(courseId, coursewareId) { const user = state.user; const skey = await _pf("progress_skey", { userAccount: user.userAccount, sid: user.sid, courseId, coursewareId, time: Date.now(), terminal: TERMINAL, }); const j = await apiForm( "/sss/service/coursewareService!doGetLearnProgress.do", { userAccount: user.userAccount, terminalCode: TERMINAL, skey, } ); let sec = null; if (j?.code == null || j.code === 200) { const n = Number(j?.data); if (Number.isFinite(n)) sec = Math.max(0, n); } return { sec, raw: j }; } async function _rlp(courseId, coursewareId, learnedSec, isFinished) { const user = state.user; const sec = Math.max(0, Math.floor(Number(learnedSec) || 0)); const packed = await _cpk([ { kind: "learn_key", name: "learnKey", time: Date.now(), sid: user.sid, userAccount: user.userAccount, terminal: TERMINAL, courseId, coursewareId, sec, }, ]); const j = await apiForm("/sss/service/coursewareService!doResetLearnProgress.do", { userAccount: user.userAccount, terminalCode: TERMINAL, secretKey: await _wsk(), learnKey: packed.fields.learnKey, isFinished: isFinished ? 1 : 0, source: 1, }); invalidatePeriodProgressCache(courseId); return j; } async function _rlt(courseId, coursewareId, targetSec, duration, wantFinish) { const dur = Math.max(30, Math.floor(Number(duration) || 30)); const target = Math.max(0, Math.min(dur, Math.floor(Number(targetSec) || 0))); let q0 = await _qls(courseId, coursewareId); let cur = q0.sec != null ? q0.sec : 0; let lastReset = null; let lastQuery = q0; let jumps = 0; while (cur < target - 1 && jumps < 80) { const next = Math.min(target, cur + LEARN_MAX_JUMP_SEC); const fin = !!(wantFinish && next >= target - 1); lastReset = await _rlp(courseId, coursewareId, next, fin ? 1 : 0); jumps += 1; await sleep(400); lastQuery = await _qls(courseId, coursewareId); const got = lastQuery.sec != null ? lastQuery.sec : cur; if (got <= cur + 1 && next > cur + 5) { const small = Math.min(target, cur + 30); lastReset = await _rlp(courseId, coursewareId, small, 0); await sleep(500); lastQuery = await _qls(courseId, coursewareId); const got2 = lastQuery.sec != null ? lastQuery.sec : cur; if (got2 <= cur + 1) { return { ok: false, before: q0.sec, after: got2, target, jumps, lastReset, lastQuery, }; } cur = got2; } else { cur = Math.max(got, cur); } if (state.stopFlag) break; } const ok = cur >= target - 3 || cur >= dur - 3; return { ok, before: q0.sec, after: cur, target, jumps, lastReset, lastQuery, }; } async function endStudy(courseId, coursewareId, isFinished) { const user = state.user; const t = await getSystemTime(); const packed = await _cpk([ { kind: "end_secondary", name: "secondaryKey", time: t, userAccount: user.userAccount, terminal: TERMINAL, domainCode: user.domainCode, sid: user.sid, courseId, coursewareId, isFinished: isFinished ? 1 : 0, }, { kind: "end_rkey", name: "rkey", coursewareId, isFinished: isFinished ? 1 : 0, }, ]); return apiForm("/sss/service/coursewareService!endStudy.do", { userAccount: user.userAccount, terminalCode: TERMINAL, secretKey: await _wsk(), secondaryKey: packed.fields.secondaryKey, rkey: packed.fields.rkey, timestamp: Date.parse(new Date()), }); } function parseJsonLoose(text) { const raw = String(text || "").trim(); if (!raw) return null; try { return JSON.parse(raw); } catch (_) {} try { return JSON.parse("[" + raw + "]"); } catch (_) {} return null; } async function _lme() { const user = state.user; if (!user) return []; let industry = "00000000"; try { industry = await getIndustryCodes(user.domainCode); } catch (_) {} const yearTypes = [1, 0]; const seen = new Set(); const all = []; for (const yearType of yearTypes) { try { const j = await apiForm("/ess/service/myexam/myExamAo!doGetPcExamList.do", { domainCode: user.domainCode, rank: user.rankId, userAccount: user.userAccount, yearType, industryCodes: industry || "00000000", politicsCode: user.politicsCode || "", }); const arr = Array.isArray(j) ? j : Array.isArray(j?.data) ? j.data : []; for (const ex of arr) { if (!ex) continue; const id = String(ex.id || ex.examId || ""); if (!id || seen.has(id)) continue; seen.add(id); all.push(ex); } } catch (_) {} } let now = 0; try { const sample = all.find((e) => Number(e?.sysTime) > 1e11); if (sample) now = Number(sample.sysTime); } catch (_) {} if (!now) { try { const withSy = all.find((e) => e?.syTime); const t = parseExamTimeMs(withSy?.syTime); if (t) now = t; } catch (_) {} } if (!now) { try { const st = await getSystemTime(); if (st && Number(st) > 0) now = Number(st); } catch (_) {} } if (!now) now = Date.now(); const withRemain = all.filter((ex) => examHasRemainAttempts(ex)); const out = withRemain .filter((ex) => isExamInTimeWindow(ex, now)) .sort((a, b) => { const ea = parseExamTimeMs(a.examEndTime) || 0; const eb = parseExamTimeMs(b.examEndTime) || 0; if (eb !== ea) return eb - ea; return Number(b.id || 0) - Number(a.id || 0); }); state._examListMeta = { total: all.length, remain: withRemain.length, open: out.length, picked: out.length, now: new Date(now).toISOString(), }; if (!out.length && withRemain.length) { state._examListHint = "\u6709\u6b21\u6570\u4f46\u90fd\u4e0d\u5728\u5f00\u653e\u65f6\u95f4\u5185"; } else if (!out.length && all.length) { state._examListHint = "\u8003\u8bd5\u6b21\u6570\u5df2\u7528\u5b8c\u6216\u5df2\u53ca\u683c"; } else if (!out.length) { state._examListHint = "\u63a5\u53e3\u672a\u8fd4\u56de\u8003\u8bd5"; } else { state._examListHint = ""; } console.debug("[\u6cd5\u5ba3\u52a9\u624b] exams", state._examListMeta, out.slice(0, 5).map((e) => ({ id: e.id, name: e.examName, join: e.examJoinNum, commit: e.examCommitNum, begin: e.examBeginTime, end: e.examEndTime, isPass: e.isPass, score: e.examResultScore, }))); return out; } function examHasRemainAttempts(ex) { try { const score = Number(ex?.examResultScore); const pass = Number(ex?.examPassScore); const passLine = Number.isFinite(pass) && pass > 0 ? pass : 60; if (Number.isFinite(score) && score >= passLine) return false; } catch (_) {} try { const join = Number(ex?.examJoinNum || 0); const commit = Number(ex?.examCommitNum || 0); if (commit > 0 && join >= commit) return false; } catch (_) {} return true; } function isExamInTimeWindow(ex, nowMs) { const now = Number(nowMs) || Date.now(); const begin = parseExamTimeMs(ex?.examBeginTime); const end = parseExamTimeMs(ex?.examEndTime); if (end != null && now > end) return false; if (begin != null && now < begin) return false; if (begin == null && end == null) return true; return true; } function parseExamTimeMs(raw) { const s = String(raw || "").trim(); if (!s) return null; if (/^\d{13}$/.test(s)) return Number(s); if (/^\d{10}$/.test(s)) return Number(s) * 1000; const norm = s .replace(/T/, " ") .replace(/-/g, "/") .replace(/\.\d+/, "") .replace(/Z$/, ""); let t = Date.parse(norm); if (Number.isFinite(t)) return t; t = Date.parse(s); return Number.isFinite(t) ? t : null; } async function getMyPoint() { return "5.00"; } function _nea(q, raw) { let ans = raw == null ? "" : String(raw).trim(); if (!ans) return ""; const t = String(q?.type || ""); if (t === "3") { if (ans === "1" || ans === "\u5bf9" || /^true$/i.test(ans) || /^A$/i.test(ans)) return "1"; if (ans === "0" || ans === "\u9519" || /^false$/i.test(ans) || /^B$/i.test(ans)) return "0"; return ans; } if (t === "2") { return ans.replace(/[,;/\s]/g, "").toUpperCase(); } return ans.replace(/;/g, ""); } async function fetchPaperAnswers(paperId, series, timeHints) { const user = state.user; const hints = (timeHints || []).map((x) => String(x ?? "").trim()).filter(Boolean); hints.push(String(Date.now()), String(await getSystemTime())); const tried = new Set(); for (const timeStr of hints) { if (tried.has(timeStr)) continue; tried.add(timeStr); try { const secretKey = await _pf("paper_answer_secret", { userAccount: user.userAccount, time: timeStr, paperId, series, terminal: TERMINAL, }); const j = await apiForm( "/sss/service/coursewareService!getPaperAnswer.do", { secretKey, userAccount: user.userAccount, terminalCode: TERMINAL, time: Date.parse(new Date()), } ); const enc = j?.data?.paperAnswer || j?.paperAnswer; if (!enc || j?.code !== 200) continue; const plain = await _ad(enc); if (!plain) continue; const bracket = plain.lastIndexOf("["); const jsonStr = bracket >= 0 ? plain.slice(bracket) : plain; let arr = []; try { arr = JSON.parse(jsonStr); } catch (_) { continue; } if (!Array.isArray(arr) || !arr.length) continue; const parts = plain.split(";"); let n0 = 0; let n1 = 0; if (parts.length >= 3) { n0 = Number(parts[parts.length - 3]) || 0; n1 = Number(parts[parts.length - 2]) || 0; } const map = {}; for (const row of arr) { if (!row) continue; const qid = Number(row.questionId) + n1 - n0; if (!Number.isFinite(qid)) continue; map[String(qid)] = row.answerNo; map[String(row.questionId)] = row.answerNo; } if (Object.keys(map).length) return map; } catch (_) {} } return {}; } async function loadPaperSkeleton(paperId, series) { const text = await apiGet( `/ess/service/getpapernew?paperId=${encodeURIComponent( paperId )}&series=${encodeURIComponent(series)}&dc=${Date.now()}` ); const arr = parseJsonLoose(text); return Array.isArray(arr) ? arr : []; } async function loadExamQuestions(questionIds, paperId, series) { const user = state.user; const ids = (questionIds || []).map(String).filter(Boolean); if (!ids.length) return []; const out = []; const chunk = 40; for (let i = 0; i < ids.length; i += chunk) { const part = ids.slice(i, i + chunk); const qs = [ `questionId=${encodeURIComponent(part.join(","))}`, `userAccount=${encodeURIComponent(user.userAccount)}`, `ssid=${encodeURIComponent(user.sid)}`, `paperID=${encodeURIComponent(paperId)}`, `series=${encodeURIComponent(series)}`, `timestamp=${encodeURIComponent(String(new Date()))}`, ].join("&"); const text = await apiGet(`/ess/service/getexerinfo?${qs}`); const parsed = parseJsonLoose(text); if (Array.isArray(parsed)) { for (const item of parsed) { if (typeof item === "string") { try { out.push(JSON.parse(item)); } catch (_) {} } else if (item && typeof item === "object") { out.push(item); } } } } return out; } async function initExamSession(examId, paperId, series) { const user = state.user; const t = await getSystemTime(); const token = `${examId}_${paperId}_${series}`; const secondaryKey = await _pf("exam_init_secondary", { time: t, userAccount: user.userAccount, sid: user.sid, token, examId, paperId, series, terminal: TERMINAL, }); const j = await apiForm("/sss/service/studyExamService!doInitStudyExam.do", { userAccount: user.userAccount, terminalCode: TERMINAL, type: 1, secondaryKey, time: t, }); if (j?.code !== 200 || !j?.data?.secretKey) { throw new Error(j?.msg || j?.message || "\u521d\u59cb\u5316\u8003\u8bd5\u5931\u8d25"); } const cipher = String(j.data.secretKey); let flagA = 0; let flagB = 0; try { const plain = await _ad(cipher); const parts = String(plain || "").split(";"); flagA = Number(parts[3]); flagB = Number(parts[5]); } catch (_) {} return { cipher, flagA, flagB, time: t, token }; } function resolveExamSeries(exam) { const paperType = Number(exam?.examPaperType || 0); if (paperType === 1) { const n = Number(EXAM_FIXED_SERIES); return Number.isFinite(n) ? Math.max(0, Math.min(499, Math.floor(n))) : 316; } return 0; } function extractStdAnswersFromCommit(commit) { const map = {}; const data = commit?.data; if (!Array.isArray(data)) return map; for (const row of data) { if (!row || typeof row !== "object") continue; const qid = String(row.questionID || row.questionId || ""); const ans = row.answer != null ? String(row.answer).trim() : ""; if (qid && ans !== "") map[qid] = ans; } return map; } function scoreFromCommit(commit) { if (!commit) return ""; if (commit?.data?.score != null) return commit.data.score; if (commit?.data?.examScore != null) return commit.data.examScore; if (commit?.score != null) return commit.score; const msg = String(commit?.message || commit?.msg || ""); const m = msg.match(/(\d+(?:\.\d+)?)\s*$/); if (m) return m[1]; return msg || ""; } async function enterExam(examId) { const user = state.user; return apiForm( `/ess/service/myexam/myExamAo!doComeExam.do?userAccount_before=${encodeURIComponent( user.userAccount )}`, { examId, userAccount: user.userAccount, myPoint: await getMyPoint(), } ); } async function _cep(examId, paperId, series, userAnswer) { const user = state.user; const init = await initExamSession(examId, paperId, series); const t = Date.now(); const packed = await _cpk([ { kind: "wrap", name: "secretKey", text: init.cipher }, { kind: "exam_commit_secondary", name: "secondaryKey", time: t, sid: user.sid, userAccount: user.userAccount, terminal: TERMINAL, examId, paperId, series, }, { kind: "wrap", name: "userAnswer", text: JSON.stringify(userAnswer) }, { kind: "vcode_null", name: "vCodes" }, ]); return apiForm("/ess/service/myexam/myExamAo!doCommitExamNew.do", { userAccount: user.userAccount, terminalCode: TERMINAL, secretKey: packed.fields.secretKey, secondaryKey: packed.fields.secondaryKey, userAnswer: packed.fields.userAnswer, vCodes: packed.fields.vCodes, timestamp: Date.parse(new Date()), }); } async function _cem(examId, paperId, series, skeleton, answerMap) { const user = state.user; const init = await initExamSession(examId, paperId, series); const t = Date.now(); const packed = await _cpk([ { kind: "wrap", name: "secretKey", text: init.cipher }, { kind: "exam_commit_secondary", name: "secondaryKey", time: t, sid: user.sid, userAccount: user.userAccount, terminal: TERMINAL, examId, paperId, series, }, { kind: "exam_user_answer", name: "userAnswer", skeleton: skeleton || [], answers: answerMap || {}, }, { kind: "vcode_null", name: "vCodes" }, ]); return apiForm("/ess/service/myexam/myExamAo!doCommitExamNew.do", { userAccount: user.userAccount, terminalCode: TERMINAL, secretKey: packed.fields.secretKey, secondaryKey: packed.fields.secondaryKey, userAnswer: packed.fields.userAnswer, vCodes: packed.fields.vCodes, timestamp: Date.parse(new Date()), }); } function buildUserAnswerList(skeleton, answerMap) { return (skeleton || []).map((q) => { const qid = String(q.questionId); const raw = answerMap ? answerMap[qid] : ""; return { questionId: Number(q.questionId) || q.questionId, answerNo: _nea(q, raw), }; }); } async function _toe(exam) { const user = state.user; const examId = String(exam?.id || exam?.examId || ""); const paperId = String(exam?.examPaperId || ""); const examName = String(exam?.examName || examId); if (!examId || !paperId) return { ok: false, msg: "\u8003\u8bd5\u4fe1\u606f\u4e0d\u5b8c\u6574" }; const series = resolveExamSeries(exam); const join = Number(exam?.examJoinNum || 0); const commitLimit = Number(exam?.examCommitNum || 0); const remain = commitLimit > 0 ? Math.max(0, commitLimit - join) : 99; if (remain <= 0) { return { ok: false, msg: "\u8003\u8bd5\u6b21\u6570\u5df2\u7528\u5b8c", examId, examName }; } try { let come = await enterExam(examId); if (Number(come?.code) !== 200) { return { ok: false, msg: come?.message || come?.msg || "\u65e0\u6cd5\u8fdb\u5165\u8003\u8bd5", examId, examName }; } const skeleton = await loadPaperSkeleton(paperId, series); if (!skeleton.length) return { ok: false, msg: "\u8bd5\u5377\u4e3a\u7a7a", examId, examName }; const qids = skeleton.map((q) => String(q.questionId)).filter(Boolean); try { await loadExamQuestions(qids, paperId, series); } catch (_) {} let answerMap = await fetchPaperAnswers(paperId, series, [ exam?.sysTime, Date.now(), ]); let filled = Object.keys(answerMap || {}).length; if (filled < Math.ceil(skeleton.length * 0.5)) { if (remain < 2) { return { ok: false, msg: "\u5269\u4f59\u8003\u8bd5\u6b21\u6570\u4e0d\u8db3\u4e24\u8f6e\uff0c\u65e0\u6cd5\u63a2\u7b54\u6848\u540e\u91cd\u4ea4", examId, examName, }; } const probeCommit = await _cem( examId, paperId, series, skeleton, {} ); if (Number(probeCommit?.code) !== 200) { return { ok: false, msg: probeCommit?.message || probeCommit?.msg || "\u63a2\u5377\u4ea4\u5377\u5931\u8d25", examId, examName, }; } answerMap = extractStdAnswersFromCommit(probeCommit); filled = Object.keys(answerMap).length; if (!filled) { return { ok: false, msg: "\u4ea4\u5377\u7ed3\u679c\u672a\u8fd4\u56de\u6807\u51c6\u7b54\u6848", examId, examName, }; } await sleep(800); come = await enterExam(examId); if (Number(come?.code) !== 200) { return { ok: false, msg: come?.message || come?.msg || "\u4e8c\u6b21\u8fdb\u5165\u8003\u8bd5\u5931\u8d25", examId, examName, probed: true, }; } const skeleton2 = await loadPaperSkeleton(paperId, series); if (!skeleton2.length) { return { ok: false, msg: "\u4e8c\u6b21\u62c9\u5377\u5931\u8d25", examId, examName, probed: true }; } try { await loadExamQuestions( skeleton2.map((q) => String(q.questionId)).filter(Boolean), paperId, series ); } catch (_) {} const commit = await _cem( examId, paperId, series, skeleton2, answerMap ); const ok = Number(commit?.code) === 200; return { ok, score: scoreFromCommit(commit), msg: commit?.message || commit?.msg || (ok ? "\u8003\u8bd5\u5b8c\u6210" : "\u4ea4\u5377\u5931\u8d25"), examId, examName, series, pass: 2, }; } const commit = await _cem( examId, paperId, series, skeleton, answerMap ); const ok = Number(commit?.code) === 200; return { ok, score: scoreFromCommit(commit), msg: commit?.message || commit?.msg || (ok ? "\u8003\u8bd5\u5b8c\u6210" : "\u4ea4\u5377\u5931\u8d25"), examId, examName, series, pass: 1, }; } catch (e) { return { ok: false, msg: e?.message || "\u8003\u8bd5\u5931\u8d25", examId, examName }; } } function syncCloudQuotaFromResponse(data) { if (!data || typeof data !== "object") return; if (data.tier) state.cloudTier = String(data.tier); const lim = data.free_chapter_limit ?? data.free_video_limit ?? data.freeVideoLimit ?? data.limit; const used = data.free_used_chapters ?? data.free_used_videos ?? data.freeUsedVideos ?? data.used; if (lim != null) state.freeVideoLimit = Number(lim) || FREE_CHAPTER_LIMIT_DEFAULT; if (used != null) state.freeUsedVideos = Number(used); writeCloudLastState({ tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, }); _ucu(); } async function _fes(context, cfg, kind) { _aca(); await _el(false); const data = await _crq("/api/fxzx/study/engine/start", "POST", { kind: String(kind || "video"), context: context || {}, config: cfg || {}, }); syncCloudQuotaFromResponse(data); return data; } async function _fep(sessionId, event, lastResult, contextPatch) { _aca(); await _el(false); const data = await _crq("/api/fxzx/study/engine/step", "POST", { session_id: String(sessionId || ""), event: String(event || "tick"), last_result: lastResult == null ? null : lastResult, context_patch: contextPatch == null ? null : contextPatch, }); syncCloudQuotaFromResponse(data); return data; } async function _xfc(cmd, ctx) { const c = cmd || {}; const t = String(c.type || ""); const courseId = ctx.courseId; const ware = ctx.ware; const duration = Math.max(30, Number(ctx.duration) || Number(c.duration) || 600); if (t === "wait") { await sleep(Math.max(300, Number(c.ms || 1000))); return { event: "tick", lastResult: null }; } if (t === "report_progress" || t === "fx_report") { const sec = Math.max( 0, Math.floor(Number(c.learned_sec ?? c.learnedSec ?? 0)) ); const fin = !!(c.is_finished ?? c.isFinished); const rep = await _rlt( courseId, ware.id, sec, duration, fin ); const afterSec = Number(rep.after) || 0; const serverPct = Math.min(100, (afterSec / duration) * 100); const claimPct = Math.min(100, (sec / duration) * 100); if (!rep.ok) { log(`\u8fdb\u5ea6\u4e0a\u62a5\u672a\u751f\u6548\uff0c\u5df2\u8df3\u8fc7\u672c\u8bfe\u4ef6`); } const showPct = Math.max(claimPct, serverPct); ware.progress = Math.floor(showPct); updateProgressUi(showPct); renderChapterPreview(); logProgress(ware.title, claimPct); return { event: "report_result", lastResult: { learned_sec: afterSec, pct: serverPct, claimed_sec: sec, duration, report_ok: !!rep.ok, }, }; } if (t === "end_study" || t === "fx_finish") { const endSec = Math.max( Math.floor(Number(c.learned_sec ?? c.learnedSec ?? duration - 1)), Math.floor(duration) - 1, 1 ); const rep = await _rlt( courseId, ware.id, Math.floor(duration), duration, true ); await sleep(200); let end = await endStudy(courseId, ware.id, 1); if ( end?.code === 200 && !(end.isFinished == 1 || end.isFinished === "1") ) { await _rlp(courseId, ware.id, Math.floor(duration), 1); await sleep(400); end = await endStudy(courseId, ware.id, 1); } let done = end?.code === 200 && (end.isFinished == 1 || end.isFinished === "1"); if (!done && end?.code === 200) { try { const meta = await loadUserCourseMeta(courseId); if (meta.finished.has(String(ware.id))) done = true; } catch (_) {} try { const prog = await loadWareProgress(courseId); if ( prog && (prog[`${ware.id}_finished`] == 1 || prog[`${ware.id}_finished`] === "1" || Number(prog[`${ware.id}_progress`]) >= 99) ) { done = true; } } catch (_) {} } if (!done) { const em = String(end?.msg || "\u9000\u51fa\u5b66\u4e60\u5931\u8d25") .replace(//gi, " ") .replace(/<[^>]+>/g, ""); log(em || "\u9000\u51fa\u5b66\u4e60\u5931\u8d25"); } return { terminal: true, ok: done, done, skipped: false, msg: done ? end?.msg || "\u5df2\u5b8c\u6210" : "", lastResult: { code: end?.code, isFinished: end?.isFinished, done, msg: end?.msg || "", }, }; } if (t === "done") { return { terminal: true, ok: !!c.success, skipped: !!c.skipped, done: !!c.success, msg: c.message || "", }; } if (t === "failed" || t === "quota") { return { terminal: true, ok: false, quota: t === "quota", msg: c.message || "failed", }; } if (t === "exam_list") { const exams = await _lme(); const sanitized = (exams || []).map((ex) => { const o = { ...(ex || {}) }; try { const score = Number(o.examResultScore); const pass = Number(o.examPassScore); const line = Number.isFinite(pass) && pass > 0 ? pass : 60; if (Number.isFinite(score) && score < line) o.isPass = 0; } catch (_) {} return o; }); if (!sanitized.length) { log(state._examListHint || "\u6682\u65e0\u5f00\u653e\u4e2d\u7684\u8003\u8bd5"); const meta = state._examListMeta; if (meta && meta.total > 0) { log( `\u5217\u8868 ${meta.total} \u573a\uff0c\u6709\u6b21\u6570 ${meta.remain} \u573a\uff0c\u5f00\u653e\u4e2d ${meta.open} \u573a` ); } } else { log(`\u5f85\u8003 ${sanitized.length} \u573a`); } return { event: "exam_list_result", lastResult: { exams: sanitized } }; } if (t === "exam_take") { const exam = c.exam || {}; const name = String(exam.examName || exam.id || ""); const r = await _toe(exam); if (r?.ok) { const score = r.score != null && String(r.score) !== "" ? ` ${r.score}` : ""; const pass = r.pass === 2 ? "\uff08\u4e24\u8f6e\uff09" : ""; log(`\u8003\u8bd5\u5b8c\u6210\uff1a${name || ""}${score}${pass}`.trim()); } else if (r?.msg) { const key = String(r.msg); state._examFailLog = state._examFailLog || {}; if (!state._examFailLog[key]) { state._examFailLog[key] = 1; log(key); } else { state._examFailLog[key] += 1; } } return { event: "exam_take_result", lastResult: r }; } await sleep(400); return { event: "tick", lastResult: null }; } async function _fec(sessionId, result) { _aca(); await _el(false); const data = await _crq("/api/fxzx/study/engine/complete", "POST", { session_id: String(sessionId || ""), success: !!(result && (result.ok || result.done || result.success)), skipped: !!(result && result.skipped), }); syncCloudQuotaFromResponse(data); return data; } async function _rcp(sessionId, plan, logLine, ctx, reportComplete) { if (shouldLogCloudLine(logLine)) log(String(logLine).trim()); const cmds = (plan || []).filter((c) => c && typeof c === "object"); for (let i = 0; i < cmds.length && !state.stopFlag; i++) { _aca(); const out = await _xfc(cmds[i], ctx); if (out.terminal !== undefined) { if ( reportComplete && (out.ok || out.done) && !out.skipped && !out.quota && !out.stopped ) { try { await _fec(sessionId, out); } catch (_) {} } return out; } if (i === cmds.length - 1 && out.event) { const next = await _fep( sessionId, out.event, out.lastResult, null ); if (shouldLogCloudLine(next.log)) log(String(next.log).trim()); const nextPlan = Array.isArray(next.plan) ? next.plan.filter((c) => c && typeof c === "object") : []; if (nextPlan.length) { return _rcp( String(next.session_id || sessionId), nextPlan, null, ctx, reportComplete ); } if (next.command) { return _rcp( String(next.session_id || sessionId), [next.command], null, ctx, reportComplete ); } } } if (state.stopFlag) return { terminal: true, ok: false, stopped: true }; return { terminal: true, ok: false, msg: "\u5b66\u4e60\u672a\u5b8c\u6210" }; } async function _rfl(startRes, ctx) { const sessionId = String(startRes.session_id || ""); const plan = Array.isArray(startRes.plan) ? startRes.plan.filter((c) => c && typeof c === "object") : []; const reportComplete = String(ctx?.kind || "") !== "exam"; if (plan.length) { return _rcp( sessionId, plan, startRes.log, ctx, reportComplete ); } let cmd = startRes.command; if (shouldLogCloudLine(startRes.log)) log(String(startRes.log).trim()); const maxSteps = 80; for (let i = 0; i < maxSteps && cmd && !state.stopFlag; i++) { const out = await _xfc(cmd, ctx); if (out.terminal !== undefined) { if ( reportComplete && (out.ok || out.done) && !out.skipped && !out.quota ) { try { await _fec(sessionId, out); } catch (_) {} } return out; } const next = await _fep( sessionId, out.event, out.lastResult, null ); if (shouldLogCloudLine(next.log)) log(String(next.log).trim()); const nextPlan = Array.isArray(next.plan) ? next.plan.filter((c) => c && typeof c === "object") : []; if (nextPlan.length) { return _rcp( String(next.session_id || sessionId), nextPlan, null, ctx, reportComplete ); } cmd = next.command; } if (state.stopFlag) return { terminal: true, ok: false, stopped: true }; return { terminal: true, ok: false, msg: "\u5b66\u4e60\u672a\u5b8c\u6210" }; } async function _swc(courseId, ware, duration, learnedSec) { const startRes = await _fes( { course_id: courseId, courseware_id: ware.id, title: ware.title, duration, learned_sec: learnedSec, progress: Math.min(100, (learnedSec / duration) * 100), learning_user_id: _glu(), allow_exam: canTakeExam(), }, { allow_exam: canTakeExam(), }, "video" ); const startCmd = startRes.command || {}; if (String(startCmd.type || "") === "done" && startCmd.skipped) { return { skipped: true, done: true }; } if (String(startCmd.type || "") === "quota") { return { skipped: false, done: false, quota: true, msg: startCmd.message }; } const loop = await _rfl(startRes, { courseId, ware, duration }); if (loop.stopped) return { skipped: false, done: false, stopped: true }; if (loop.quota) return { skipped: false, done: false, quota: true, msg: loop.msg }; if (loop.skipped) return { skipped: true, done: true }; if (loop.ok || loop.done) return { skipped: false, done: true }; if (loop.lastResult && loop.lastResult.done) return { skipped: false, done: true }; return { skipped: false, done: !!loop.ok, msg: loop.msg }; } async function _rec() { if (!canTakeExam()) return { skipped: true }; const startRes = await _fes( { learning_user_id: _glu(), allow_exam: true, }, { allow_exam: true, pro_days: PRO_DAYS }, "exam" ); const startCmd = startRes.command || {}; if (String(startCmd.type || "") === "quota") { log(startCmd.message || "\u514d\u8d39\u7528\u6237\u4e0d\u53ef\u8003\u8bd5"); return { skipped: true, quota: true }; } if (String(startCmd.type || "") === "done" && startCmd.skipped) { if (startCmd.message) log(String(startCmd.message)); return { skipped: true, done: true }; } return _rfl(startRes, { kind: "exam" }); } async function studyOneWare(courseId, ware) { const title = ware.title; state.currentTitle = title; state.lastLoggedPctBucket = -1; syncCurrentUi(); let serverPct = Math.max(0, Number(ware.progress) || 0); let learnedSec = 0; if (serverPct >= 100 || ware.finished) { ware.finished = true; ware.progress = 100; log(`\u8df3\u8fc7\u5df2\u5b8c\u6210\uff1a${title}`); renderChapterPreview(); updateQueueSummary(); return { skipped: true, done: true }; } try { const sp = await _gsp(courseId, ware.id, null, { needSec: false, }); const p = progressPctOf(sp); if (typeof p === "number") serverPct = Math.max(serverPct, p); } catch (_) {} if (serverPct >= 100) { ware.finished = true; ware.progress = 100; log(`\u8df3\u8fc7\u5df2\u5b8c\u6210\uff1a${title}`); renderChapterPreview(); updateQueueSummary(); return { skipped: true, done: true }; } if (!canStudyNextFreeVideo()) { log("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"); switchPanelTab("settings"); openProModal(); return { skipped: false, done: false, quota: true }; } const come = await doComeStudy(courseId, ware.id); if (come?.code === 301) { const stuckId = String(come?.data?.coursewareId || ""); const stuckCourse = String(come?.data?.courseId || courseId); if (stuckId && stuckId !== String(ware.id)) { const okLeave = await _rsl(stuckCourse, stuckId); const again = await doComeStudy(courseId, ware.id); if (again?.code === 301) { const s2 = String(again?.data?.coursewareId || ""); if (s2 && s2 !== String(ware.id)) { throw new Error( okLeave ? "\u65e7\u8bfe\u4ef6\u4ecd\u5360\u7528\uff0c\u8bf7\u624b\u52a8\u9000\u51fa\u540e\u518d\u5b66" : "\u65e0\u6cd5\u9000\u51fa\u65e7\u8bfe\u4ef6\uff0c\u8bf7\u624b\u52a8\u9000\u51fa\u540e\u518d\u5b66" ); } } else if (again?.code && again.code !== 200) { throw new Error( String(again.msg || "") .replace(//gi, " ") .replace(/<[^>]+>/g, "") || "\u5f00\u59cb\u5b66\u4e60\u6821\u9a8c\u5931\u8d25" ); } } } else if (come?.code && come.code !== 200) { throw new Error( String(come.msg || "") .replace(//gi, " ") .replace(/<[^>]+>/g, "") || "\u5f00\u59cb\u5b66\u4e60\u6821\u9a8c\u5931\u8d25" ); } await initStudyExam(courseId, ware.id); let detail = null; try { detail = await getWareDetail(ware.id); } catch (_) {} const duration = Math.max(30, Number(detail?.courseDuration || 600)); try { const sec = await getLearnedSeconds(courseId, ware.id); if (typeof sec === "number") learnedSec = sec; } catch (_) {} if (learnedSec <= 0 && serverPct > 0) { learnedSec = Math.floor((serverPct / 100) * duration); } const cloudOut = await _swc( courseId, ware, duration, learnedSec ); if (cloudOut.quota) { log(cloudOut.msg || "\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"); switchPanelTab("settings"); openProModal(); return { skipped: false, done: false, quota: true }; } if (cloudOut.stopped) return { skipped: false, done: false, stopped: true }; if (cloudOut.skipped) { ware.finished = true; ware.progress = 100; log(`\u8df3\u8fc7\u5df2\u5b8c\u6210\uff1a${title}`); renderChapterPreview(); updateQueueSummary(); return cloudOut; } if (cloudOut.done) { ware.finished = true; ware.progress = 100; log(`\u5df2\u5b8c\u6210\uff1a${title}`); markFreeVideoUsed(); renderChapterPreview(); updateQueueSummary(); return cloudOut; } if (cloudOut.msg) log(cloudOut.msg); else log("\u4e91\u7aef\u7f16\u6392\u672a\u5b8c\u6210"); renderChapterPreview(); updateQueueSummary(); return { skipped: false, done: false, msg: cloudOut.msg }; } async function runStudy() { if (state.running) return; state.running = true; state.stopFlag = false; syncRunButtons(); setStatus("\u5b66\u4e60\u4e2d"); try { await ensureCryptoJs(); refreshUser(); if (!state.user) { log("\u8bf7\u5148\u767b\u5f55"); return; } if (!(await _ecr())) return; if (!state.courses.length) await loadCourses(); const queue = loadQueue(); if (!queue.length) { log("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b"); return; } log("\u5f00\u59cb\u5b66\u4e60"); for (const courseId of queue) { if (state.stopFlag) break; const course = state.courses.find((c) => c.courseId === courseId); state.currentCourseName = course?.courseName || courseId; syncCurrentUi(); const wares = await loadWares(courseId); renderChapterPreview(); updateQueueSummary(); const pending = wares.filter( (w) => !w.finished && Number(w.progress) < 100 ); for (const w of pending) { if (state.stopFlag) break; if (!canStudyNextFreeVideo()) { log("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"); switchPanelTab("settings"); openProModal(); state.stopFlag = true; break; } const result = await studyOneWare(courseId, w); updateQueueSummary(); if (result?.quota || result?.stopped) break; } } if (!state.stopFlag && canTakeExam()) { await _rec(); } if (state.stopFlag) log("\u5df2\u505c\u6b62"); else log("\u5168\u90e8\u5b8c\u6210"); } catch (e) { log("\u8fd0\u884c\u51fa\u9519"); console.debug("[\u6cd5\u5ba3\u52a9\u624b] runStudy", e); } finally { state.running = false; setStatus("\u5df2\u505c\u6b62"); syncRunButtons(); } } function stopStudy() { state.stopFlag = true; log("\u5df2\u505c\u6b62"); setStatus("\u6b63\u5728\u505c\u6b62…"); syncRunButtons(); } function setStatus(text) { const el = document.getElementById("fx-auto-status"); if (el) el.textContent = text; } function updateProgressUi(pct) { state.progressPct = Math.max(0, Math.min(100, Number(pct) || 0)); const num = document.getElementById("fx-progress-num"); if (num) num.innerHTML = `${Math.floor(state.progressPct)}%`; } function syncCurrentUi() { const a = document.getElementById("fx-cur-course"); const b = document.getElementById("fx-cur-doc"); if (a) a.textContent = state.currentCourseName || "-"; if (b) b.textContent = state.currentTitle || "-"; } function syncRunButtons() { const start = document.getElementById("fx-btn-start"); const stop = document.getElementById("fx-btn-stop"); if (start) { start.disabled = state.running; start.classList.toggle("fx-btn-off", state.running); } if (stop) { stop.disabled = !state.running; stop.classList.toggle("fx-btn-off", !state.running); } } function switchPanelTab(name) { document.querySelectorAll(".fx-tab-btn").forEach((el) => { el.classList.toggle("active", el.dataset.tab === name); }); document.querySelectorAll(".fx-pane").forEach((el) => { el.classList.toggle("active", el.dataset.pane === name); }); } function updateQueueSummary() { let total = 0; let done = 0; const q = loadQueue(); for (const id of q) { const wares = state.waresByCourse[id]; if (!wares) continue; total += wares.length; done += wares.filter((w) => w.finished || w.progress >= 100).length; } state.queueVideoTotal = total; state.queueVideoDone = done; const doneEl = document.getElementById("fx-queue-done"); const totalEl = document.getElementById("fx-queue-total"); const pctEl = document.getElementById("fx-queue-percent"); const barEl = document.getElementById("fx-queue-progress"); const textEl = document.getElementById("fx-queue-text"); if (doneEl) doneEl.textContent = String(done); if (totalEl) totalEl.textContent = String(total); const pct = total > 0 ? Math.round((done / total) * 100) : 0; if (pctEl) pctEl.textContent = `${pct}%`; if (barEl) barEl.style.width = `${pct}%`; if (textEl) { if (!q.length) textEl.textContent = "\u672a\u9009\u62e9"; else if (total > 0) textEl.textContent = `\u89c6\u9891\u5df2\u5b66\u4e60 ${done}/${total}\uff08\u5df2\u9009 ${q.length} \u95e8\uff09`; else textEl.textContent = `\u5df2\u9009 ${q.length} \u95e8`; } } function courseHasUnfinished(courseId) { const wares = state.waresByCourse[courseId]; if (!wares || !wares.length) return true; return wares.some((w) => !w.finished && Number(w.progress) < 100); } async function selectUnfinishedCourses() { refreshUser(); if (!state.user) { log("\u8bf7\u5148\u767b\u5f55"); return; } if (!state.courses.length) await loadCourses(); log("\u6b63\u5728\u7b5b\u9009\u672a\u5b8c\u6210\u8bfe\u7a0b…"); const ids = []; for (const c of state.courses) { if (state.stopFlag) break; try { if (!state.waresByCourse[c.courseId]) await loadWares(c.courseId); } catch (e) { console.debug(e); continue; } if (courseHasUnfinished(c.courseId)) ids.push(c.courseId); } saveQueue(ids); renderCourseList(); await refreshChapterPreview(); updateQueueSummary(); log(ids.length ? `\u5df2\u52fe\u9009\u672a\u5b8c\u6210 ${ids.length} \u95e8` : "\u6ca1\u6709\u672a\u5b8c\u6210\u8bfe\u7a0b"); } function renderCourseList() { const box = document.getElementById("fx-course-list"); if (!box) return; const selected = new Set(loadQueue()); if (!state.courses.length) { box.innerHTML = `
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
`; return; } box.innerHTML = state.courses .map((c) => { const checked = selected.has(c.courseId) ? "checked" : ""; const wares = state.waresByCourse[c.courseId]; let pct = 0; if (wares && wares.length) { const d = wares.filter((w) => w.finished || w.progress >= 100).length; pct = Math.round((d / wares.length) * 100); } const kind = courseKindLabel(c); const period = formatCoursePeriod(c.courseWarePeriod); const tags = []; if (kind) { const kindClass = kind === "\u5fc5\u4fee\u8bfe" ? "fx-tag-required" : kind === "\u9009\u4fee\u8bfe" ? "fx-tag-elective" : "fx-tag-other"; tags.push( `${escapeHtml(kind)}` ); } if (period) { tags.push( `${escapeHtml(period)}` ); } const metaBits = [`${c.courseWareCount || "?"} \u8bfe\u4ef6`]; if (wares) metaBits.push(`${pct}%`); return ` `; }) .join(""); box.querySelectorAll("input[type=checkbox]").forEach((el) => { el.addEventListener("change", () => { const ids = []; box.querySelectorAll("input[type=checkbox]").forEach((cb) => { if (cb.checked && cb.dataset.cid) ids.push(cb.dataset.cid); }); saveQueue(ids); void refreshChapterPreview(); updateQueueSummary(); }); }); } function renderChapterPreview() { const box = document.getElementById("fx-chapter-preview"); if (!box) return; const q = loadQueue(); if (!q.length) { box.innerHTML = `
\u8bf7\u52fe\u9009\u8bfe\u7a0b
`; return; } const parts = []; for (const id of q) { const course = state.courses.find((c) => c.courseId === id); const wares = state.waresByCourse[id] || []; const kind = courseKindLabel(course); const period = formatCoursePeriod(course?.courseWarePeriod); const headBits = []; if (kind) headBits.push(kind); if (period) headBits.push(period); const headExtra = headBits.length ? ` · ${escapeHtml(headBits.join(" · "))}` : ""; parts.push( `
${escapeHtml( course?.courseName || id )}${headExtra}
` ); if (!wares.length) { parts.push(`
\u52a0\u8f7d\u4e2d…
`); } else { for (const w of wares) { const tag = w.finished || w.progress >= 100 ? "\u5b8c\u6210" : `${Math.floor(w.progress || 0)}%`; parts.push( `
${escapeHtml( w.title )}${escapeHtml(tag)}
` ); } } parts.push(`
`); } box.innerHTML = parts.join(""); } async function refreshChapterPreview() { const q = loadQueue(); for (const id of q) { if (state.stopFlag && state.running) break; try { if (!state.waresByCourse[id]) await loadWares(id); } catch (e) { console.debug("[\u6cd5\u5ba3\u52a9\u624b] loadWares", id, e); } } renderChapterPreview(); updateQueueSummary(); renderCourseList(); } function injectPanelStyles() { if (document.getElementById("fx-panel-style")) return; const st = document.createElement("style"); st.id = "fx-panel-style"; st.textContent = ` #fx-auto-panel{position:fixed;right:20px;top:80px;z-index:999999;width:412px;display:flex;flex-direction:column;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#0f172a;overflow:hidden;max-height:min(92vh,780px);} #fx-auto-panel.fx-min #fx-panel-body,#fx-auto-panel.fx-min #fx-panel-footer,#fx-auto-panel.fx-min .fx-footer-extra{display:none !important;} #fx-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;} #fx-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #fx-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:cover;border:1px solid rgba(148,163,184,.45);background:#fff;} #fx-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;display:flex;align-items:center;gap:5px;flex-wrap:wrap;} .fx-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;} #fx-panel-sub{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px;} .fx-sub-chip{font-size:11px;color:#7c2d12;background:rgba(255,255,255,.62);padding:2px 7px;border-radius:999px;border:1px solid rgba(180,83,9,.18);font-weight:700;} .fx-sub-chip-em{color:#9a3412;border-color:rgba(154,52,18,.28);background:rgba(255,247,237,.9);} .fx-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1);font-weight:900;} #fx-panel-body{flex:1;overflow-y:auto;padding:8px;min-height:0;} .fx-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .fx-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;} .fx-status-label{font-size:11px;color:#64748b;font-weight:700;} #fx-auto-status{padding:2px 7px;border-radius:999px;font-weight:900;font-size:11px;background:#fff;border:1px solid #cbd5e1;} .fx-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;flex:1;margin-top:4px;} .fx-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .fx-progress-pct{font-size:14px;font-weight:900;color:#0369a1;} .fx-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;} .fx-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s;} .fx-tabbar{display:flex;gap:6px;margin-bottom:7px;} .fx-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;} .fx-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .fx-pane{display:none;}.fx-pane.active{display:block;} .fx-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;gap:6px;} .fx-list-head-actions{display:flex;align-items:center;gap:5px;flex-shrink:0;} .fx-list-title{font-size:12px;color:#64748b;font-weight:700;} .fx-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;} .fx-log-clear-btn{border:1px solid #cbd5e1;background:#fff;color:#64748b;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;cursor:pointer;} #fx-course-list,#fx-chapter-preview,#fx-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} .fx-course-item{display:flex;gap:7px;align-items:flex-start;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:7px;margin-bottom:6px;cursor:pointer;} .fx-course-name{display:block;font-size:12px;color:#0f172a;line-height:1.38;} .fx-course-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px;} .fx-course-tag{font-style:normal;font-size:10px;font-weight:800;padding:1px 6px;border-radius:999px;border:1px solid transparent;line-height:1.5;} .fx-tag-required{color:#9a3412;background:#ffedd5;border-color:#fdba74;} .fx-tag-elective{color:#1d4ed8;background:#dbeafe;border-color:#93c5fd;} .fx-tag-other{color:#334155;background:#e2e8f0;border-color:#cbd5e1;} .fx-tag-period{color:#065f46;background:#d1fae5;border-color:#6ee7b7;} .fx-course-meta{display:block;font-size:11px;color:#64748b;margin-top:2px;} .fx-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;} .fx-chapter-title{padding:5px 8px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:11px;font-weight:700;} .fx-chapter-item{padding:4px 8px;font-size:11px;line-height:1.35;display:flex;justify-content:space-between;gap:6px;} .fx-chapter-item span:first-child{flex:1;min-width:0;word-break:break-all;} .fx-chapter-item em{font-style:normal;color:#0369a1;font-weight:800;font-size:10px;flex-shrink:0;} .fx-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;} .fx-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;} .fx-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;align-items:center;} .fx-meta-label{color:#64748b;}.fx-meta-value{font-weight:700;text-align:right;word-break:break-all;} .fx-auth-badge{padding:2px 7px;border-radius:999px;border:1px solid #cbd5e1;font-size:11px;font-weight:900;} .fx-btn-row{display:flex;gap:7px;} .fx-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;} .fx-btn-start{background:#16a34a;}.fx-btn-stop{background:#ef4444;}.fx-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;flex:0 0 auto;} .fx-btn-off,.fx-btn:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;} .fx-btn-pro{flex:0 0 auto;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;border:none;padding:4px 8px;font-size:12px;border-radius:10px;cursor:pointer;font-weight:800;} .fx-hidden{display:none !important;} .fx-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;} .fx-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;} .fx-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-radius:11px 0 0 11px;} .fx-qq-row{display:flex;align-items:center;justify-content:space-between;gap:9px;margin-top:5px;} .fx-qq-text{font-size:12px;color:#475569;} .fx-qq-title{font-size:13px;font-weight:800;color:#0f172a;} .fx-qq-btn{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:5px 12px;border-radius:999px;font-size:12px;font-weight:800;cursor:pointer;} #fx-panel-footer{padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;color:#64748b;} #fx-pro-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000001;display:none;align-items:center;justify-content:center;padding:20px;} .fx-pro-card{width:min(560px,92vw);max-height:88vh;overflow:auto;background:#fff;border-radius:18px;border:1px solid #dbe4f0;box-shadow:0 18px 46px rgba(15,23,42,.25);padding:16px;} .fx-pro-title{font-size:24px;font-weight:900;}.fx-pro-sub{font-size:13px;color:#64748b;margin-top:4px;} .fx-pro-sec{margin-top:12px;border:1px solid #dbe4f0;border-radius:12px;padding:12px;background:#f8fafc;} .fx-pro-sec h4{margin:0 0 8px;font-size:15px;}.fx-pro-sec p,.fx-pro-sec li{font-size:13px;color:#334155;line-height:1.5;} .fx-pro-buy{display:flex;flex-direction:column;gap:10px;margin-top:8px;} .fx-pro-muted{color:#64748b;font-size:12px;} .fx-pro-buy-btn{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 16px;border-radius:12px;font-size:14px;font-weight:800;border:none;cursor:pointer;align-self:flex-start;} .fx-pro-actions{margin-top:14px;display:flex;justify-content:flex-end;} .fx-pro-close{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 18px;border-radius:12px;font-size:14px;font-weight:800;cursor:pointer;} .fx-progress-num{font-size:18px;font-weight:900;} .fx-progress-num small{font-size:12px;color:#64748b;} `; (document.head || document.documentElement).appendChild(st); } function writePanelPos(left, top) { try { localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left, top })); } catch (_) {} } function readPanelPos() { try { return JSON.parse(localStorage.getItem(PANEL_POS_KEY) || "null"); } catch (_) { return null; } } function _cpn() { if (document.getElementById("fx-auto-panel")) return; injectPanelStyles(); const panel = document.createElement("div"); panel.id = "fx-auto-panel"; if (localStorage.getItem(PANEL_COLLAPSED_KEY) === "1") panel.classList.add("fx-min"); const pos = readPanelPos(); if (pos && Number.isFinite(pos.left) && Number.isFinite(pos.top)) { panel.style.left = pos.left + "px"; panel.style.top = pos.top + "px"; panel.style.right = "auto"; } panel.innerHTML = `
\u6cd5\u5ba3\u5728\u7ebf\u5b66\u4e60\u52a9\u624b v${escapeHtml(SCRIPT_VERSION)}
\u4e00\u952e\u5b8c\u6210\u89c6\u9891\u5b66\u65f6 \u5b66\u5b8c\u81ea\u52a8\u7ee7\u7eed \u7701\u65f6\u7701\u5fc3
\u8fd0\u884c\u72b6\u6001 \u5df2\u505c\u6b62
0/0 \u89c6\u9891\u5df2\u5b66\u4e60
0%
\u8bfe\u7a0b\u9009\u62e9
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
\u7ae0\u8282\u9884\u89c8 \u5b9e\u65f6\u8fdb\u5ea6
\u8bf7\u52fe\u9009\u8bfe\u7a0b
\u8fd0\u884c\u65e5\u5fd7
\u6388\u6743\u4e0e\u9009\u9879 \u8bbe\u7f6e
\u7528\u6237\u7c7b\u578b \u672a\u6821\u9a8c
\u514d\u8d39\u4f53\u9a8c\u7ae0\u8282 0/3
Token
\u4e91\u7aef\u670d\u52a1 ${DEFAULT_CLOUD_API_BASE}
\u5f53\u524d\u8bfe\u7a0b-
\u5f53\u524d\u8bfe\u4ef6-
\u8bfe\u4ef6\u8fdb\u5ea60%
`; document.documentElement.appendChild(panel); createProModal(); const header = panel.querySelector("#fx-panel-header"); let dragging = false, sx = 0, sy = 0, ol = 0, ot = 0; header.addEventListener("mousedown", (e) => { if (e.target.closest("button")) return; dragging = true; const rect = panel.getBoundingClientRect(); sx = e.clientX; sy = e.clientY; ol = rect.left; ot = rect.top; e.preventDefault(); }); window.addEventListener("mousemove", (e) => { if (!dragging) return; panel.style.left = Math.max(0, ol + e.clientX - sx) + "px"; panel.style.top = Math.max(0, ot + e.clientY - sy) + "px"; panel.style.right = "auto"; }); window.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; const rect = panel.getBoundingClientRect(); writePanelPos(rect.left, rect.top); }); document.getElementById("fx-btn-min").onclick = () => { panel.classList.toggle("fx-min"); localStorage.setItem( PANEL_COLLAPSED_KEY, panel.classList.contains("fx-min") ? "1" : "0" ); }; panel.querySelectorAll(".fx-tab-btn").forEach((btn) => { btn.addEventListener("click", () => switchPanelTab(btn.dataset.tab)); }); document.getElementById("fx-clear-log")?.addEventListener("click", clearRunLog); document.getElementById("fx-btn-start").onclick = () => runStudy(); document.getElementById("fx-btn-stop").onclick = () => stopStudy(); document.getElementById("fx-btn-refresh").onclick = async () => { try { await ensureCryptoJs(); refreshUser(); await loadCourses(); } catch (e) { log("\u5237\u65b0\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"); console.debug(e); } }; document.getElementById("fx-btn-select-pending").onclick = () => { selectUnfinishedCourses().catch((e) => { log("\u52fe\u9009\u5931\u8d25"); console.debug(e); }); }; document.getElementById("fx-join-qq")?.addEventListener("click", () => { try { GM_openInTab(QQ_GROUP_LINK, { active: true, insert: true, setParent: true }); } catch (_) { window.open(QQ_GROUP_LINK, "_blank"); } }); document.getElementById("fx-open-pro")?.addEventListener("click", openProModal); document.getElementById("fx-cloud-save")?.addEventListener("click", async () => { const input = document.getElementById("fx-cloud-token"); state.cloudToken = String((input && input.value) || "").trim(); state.cloudRevoked = false; localStorage.setItem(CLOUD_TOKEN_KEY, state.cloudToken); writeCloudLeaseCache("", 0); state.cloudLease = ""; state.cloudLeaseExp = 0; try { refreshUser(); if (!state.user) { log("\u8bf7\u5148\u767b\u5f55"); return; } if (state.cloudToken) await cloudVerifyToken(); await _el(true); log("\u6388\u6743\u5df2\u66f4\u65b0"); } catch (err) { log("\u6388\u6743\u65e0\u6548\u8bf7\u68c0\u67e5 Token"); console.debug(err); } _ucu(); }); const logBox = document.getElementById("fx-run-log"); if (logBox) { logBox.innerHTML = state.logs .slice(0, 40) .map((s) => `
${escapeHtml(s)}
`) .join(""); } const tokenInput = document.getElementById("fx-cloud-token"); if (tokenInput) tokenInput.value = state.cloudToken; _ucu(); syncRunButtons(); updateQueueSummary(); void _fcc(); } async function boot() { try { localStorage.removeItem("fx_speed_mode_v1"); } catch (_) {} _cpn(); refreshUser(); await ensureCryptoJs(); if (state.user) { loadCourses().catch((e) => { console.debug("[\u6cd5\u5ba3\u52a9\u624b] auto load", e); }); _el(false).catch(() => {}); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } })();