// ==UserScript==
// @name 安徽专业技术人员继续教育在线学习助手
// @namespace https://oa.wlxy.top/
// @version 2.0
// @author 柠檬真酸
// @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png
// @description 支持安徽专业技术人员继续教育在线,自动完成未完成课程和考试,一键完成当前培训计划内学时,支持快刷和1:1时长慢刷模式自由切换,安全稳定
// @antifeature payment 免费体验章节有限,升级Pro不限
// @antifeature membership 需云端授权
// @connect oa2.ahzsksw.cn
// @connect juheapi.zjzx.ah.cn
// @connect huaweicloudobs.ahjxjy.cn
// @connect fa.ahsxks.com
// @match https://www.zjzx.ah.cn/*
// @grant GM_xmlhttpRequest
// @grant GM_info
// @grant GM_openInTab
// @run-at document-start
// @license All Rights Reserved
// ==/UserScript==
(function () {
"use strict";
const STORAGE_KEY = "zjzx_auto_study_enabled";
const MULTIPLIER_KEY = "zjzx_study_time_multiplier";
const ADVANCE_KEY = "zjzx_advance_multiplier";
const SAVE_INTERVAL_KEY = "zjzx_save_interval_sec";
const HEARTBEAT_BASE = 300;
const MODE_KEY = "zjzx_run_mode";
const STUDY_MODE_KEY = "zjzx_study_mode";
const BG_STATE_KEY = "zjzx_bg_learn_state";
const QUEUE_KEY = "zjzx_course_queue";
const QUEUE_TOUCHED_KEY = "zjzx_queue_touched";
const PLAN_TP_KEY = "zjzx_selected_tp_id";
const AUTO_EXAM_KEY = "zjzx_auto_exam";
const SCRIPT_VERSION = (() => {
try {
if (typeof GM_info !== "undefined" && GM_info?.script?.version) {
return String(GM_info.script.version);
}
} catch (_) {}
return "unknown";
})();
const PRO_BUY_URL = "https://fa.ahsxks.com/ahzj/";
const PRO_BUY_OPEN_MS = new Date("2026-06-06T00:00:00+08:00").getTime();
const CLOUD_API_BASE_KEY = "zjzx_cloud_api_base";
const CLOUD_TOKEN_KEY = "zjzx_cloud_token_v1";
const CLOUD_LEASE_CACHE_KEY = "zjzx_cloud_lease_cache_v1";
const CLOUD_PRO_EXPIRE_CACHE_KEY = "zjzx_cloud_pro_expire_cache_v1";
const CLOUD_LAST_STATE_KEY = "zjzx_cloud_last_state_v1";
const PANEL_POS_KEY = "zjzx_panel_pos_v1";
const PANEL_COLLAPSED_KEY = "zjzx_auto_panel_collapsed_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://oa2.ahzsksw.cn";
const PANEL_NOTICE_FALLBACK = "\u5b89\u5fbd\u4e13\u6280\u5728\u7ebf\u52a9\u624b";
const NAV_GUARD_KEY = "zjzx_auto_nav_guard";
const TICK_MS = 3000;
const NAV_COOLDOWN_MS = 60000;
const CHAPTER_CLICK_COOLDOWN_MS = 20000;
const VIDEO_WAIT_MS = 45000;
const SITE_ID = "c95b90cef9a640b1aed870c0b8c3a935";
const API_BASE = "https://juheapi.zjzx.ah.cn";
const MAX_ADVANCE_SEC = 3600;
function injectPageKeepAlive() {
const code = function () {
if (window.__zjzxKeepAlivePatched) return;
window.__zjzxKeepAlivePatched = true;
window.requestKeepAlive = function () {
const xhr = new XMLHttpRequest();
xhr.open("GET", "/keepAlive.html", true);
xhr.withCredentials = true;
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) setTimeout(window.requestKeepAlive, 30000);
};
xhr.send();
};
};
const el = document.createElement("script");
el.textContent = "(" + code.toString() + ")();";
(document.documentElement || document.head).appendChild(el);
el.remove();
}
injectPageKeepAlive();
let keepAliveTimer = null;
function syncKeepAlive() {
if (keepAliveTimer) {
clearInterval(keepAliveTimer);
keepAliveTimer = null;
}
if (!state.enabled || getMode() !== "background") return;
keepAliveTimer = setInterval(() => {
try {
const xhr = new XMLHttpRequest();
xhr.open("GET", location.origin + "/keepAlive.html", true);
xhr.withCredentials = true;
xhr.send();
} catch (_) {}
}, 30000);
}
const state = {
enabled: false,
studyMode: "",
lastAction: "",
lastTick: 0,
bgTickBusy: false,
faceWaiting: false,
courseplayWaitSince: 0,
trainApiTriedAt: 0,
panelCourses: [],
teachPlans: [],
selectedTpId: "",
chapterPreview: [],
queueVideoTotal: 0,
queueVideoDone: 0,
queueExamTotal: 0,
queueExamDone: 0,
coursesDoneSession: 0,
panelRefreshing: false,
previewRefreshTimer: null,
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,
cloudProExpireAt: 0,
cloudRevoked: false,
freeVideoLimit: 1,
freeUsedVideos: 0,
panelNoticePath: "/api/zjzx/panel-notice",
remotePanelNotice: "",
proBuyUrl: PRO_BUY_URL,
logLines: [],
panelHint: "",
platformHeartbeatSec: 0,
studyModeHintTimer: null,
};
function loadQueue() {
try {
const raw = JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]");
return Array.isArray(raw) ? raw : [];
} catch (_) {
return [];
}
}
function saveQueue(csIds) {
localStorage.setItem(QUEUE_KEY, JSON.stringify(csIds));
}
function loadSelectedTpId() {
return String(localStorage.getItem(PLAN_TP_KEY) || "").trim();
}
function saveSelectedTpId(tpId) {
const id = String(tpId || "").trim();
localStorage.setItem(PLAN_TP_KEY, id);
state.selectedTpId = id;
}
function getAutoExam() {
return localStorage.getItem(AUTO_EXAM_KEY) !== "0";
}
function setAutoExam(v) {
localStorage.setItem(AUTO_EXAM_KEY, v ? "1" : "0");
return v;
}
function isTruthyFlag(v) {
return v === true || v === 1 || v === "1";
}
function isFalsyFlag(v) {
return v === false || v === 0 || v === "0";
}
function escHtml(s) {
return String(s || "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function getMultiplier() {
const v = parseFloat(localStorage.getItem(MULTIPLIER_KEY) || "2");
if (!Number.isFinite(v)) return 2;
return Math.min(5, Math.max(1, v));
}
function setMultiplier(v) {
const n = Math.min(5, Math.max(1, parseFloat(v) || 1));
localStorage.setItem(MULTIPLIER_KEY, String(n));
return n;
}
function getAdvanceMult() {
const v = parseFloat(localStorage.getItem(ADVANCE_KEY) || "6");
if (!Number.isFinite(v)) return 6;
return Math.min(18, Math.max(1, v));
}
function setAdvanceMult(v) {
const n = Math.min(18, Math.max(1, parseFloat(v) || 1));
localStorage.setItem(ADVANCE_KEY, String(n));
return n;
}
function getSaveInterval() {
if (isRealtimeStudyMode()) return getRealtimeChunkSec();
const v = parseInt(localStorage.getItem(SAVE_INTERVAL_KEY) || "5", 10);
if (!Number.isFinite(v)) return 300;
return Math.min(300, Math.max(3, v));
}
function setSaveInterval(v) {
const n = Math.min(300, Math.max(3, parseInt(v, 10) || 300));
localStorage.setItem(SAVE_INTERVAL_KEY, String(n));
return n;
}
function getAdvanceChunkSec() {
return Math.min(Math.round(HEARTBEAT_BASE * getAdvanceMult()), MAX_ADVANCE_SEC);
}
function readStudyModeSelect() {
const sel = document.getElementById("zjzx-study-mode");
const v = String(sel?.value || "").toLowerCase();
return v === "realtime" || v === "efficiency" ? v : "";
}
function hasStudyModeSelected() {
const ui = readStudyModeSelect();
if (ui) {
state.studyMode = ui;
return true;
}
if (state.enabled && (state.studyMode === "realtime" || state.studyMode === "efficiency")) {
return true;
}
state.studyMode = "";
return false;
}
function isRealtimeStudyMode() {
return getStudyMode() === "realtime";
}
function getStudyMode() {
const ui = readStudyModeSelect();
if (ui) return ui;
const m = String(state.studyMode || "");
if (m === "realtime" || m === "efficiency") return m;
return "";
}
function setStudyMode(mode) {
const raw = String(mode || "").toLowerCase();
if (raw !== "realtime" && raw !== "efficiency") {
state.studyMode = "";
syncStudyModeUi("");
return "";
}
state.studyMode = raw;
syncStudyModeUi(raw);
return raw;
}
function clearStudyModeSelection() {
state.studyMode = "";
hideStudyModeHint();
if (state.studyModeHintTimer) {
clearTimeout(state.studyModeHintTimer);
state.studyModeHintTimer = null;
}
syncStudyModeUi("");
}
function resetSessionOnPageLoad() {
state.enabled = false;
clearStudyModeSelection();
localStorage.setItem(STORAGE_KEY, "0");
localStorage.removeItem(STUDY_MODE_KEY);
}
function formatIntervalCn(sec) {
const s = Math.max(1, Math.round(Number(sec) || HEARTBEAT_BASE));
if (s >= 60 && s % 60 === 0) return `${s / 60}\u5206\u949f`;
return `${s}\u79d2`;
}
function formatDurationMinutesCn(sec) {
const s = Math.max(0, Math.round(Number(sec) || 0));
if (s >= 60 && s % 60 === 0) return `${s / 60}\u5206\u949f`;
if (s >= 60) {
const m = (s / 60).toFixed(1).replace(/\.0$/, "");
return `${m}\u5206\u949f`;
}
return `${s}\u79d2`;
}
async function ensurePlatformHeartbeatSec(force) {
if (!force && state.platformHeartbeatSec > 0) return state.platformHeartbeatSec;
const sec = await fetchHeartbeatSec();
state.platformHeartbeatSec = Math.min(300, Math.max(30, Math.round(sec) || HEARTBEAT_BASE));
syncStudyModeHintContent();
return state.platformHeartbeatSec;
}
function getRealtimeChunkSec() {
return state.platformHeartbeatSec > 0 ? state.platformHeartbeatSec : HEARTBEAT_BASE;
}
function formatRealtimeEngineLog(text) {
const s = String(text || "").trim();
if (!s.includes("[1:1]")) return s;
const submitRaw = s.match(/📤\s*\[1:1\]\s*(\d+)s→(\d+)s\s*\(([\d.]+)%\)(?:\s*\[[^\]]+\])?/);
if (submitRaw) {
const pct = submitRaw[3];
return `📤 [1:1] \u5df2\u5b66\u4e60${formatDurationMinutesCn(submitRaw[2])}\uff08${pct}%\uff09`;
}
const submitOld = s.match(/📤\s*\[1:1\]\s*\u4e0a\u62a5\u5b66\u4e60\u8fdb\u5ea6\s*(\d+)s→(\d+)s[\uff08(]([\d.]+)%[\uff09)]/);
if (submitOld) {
return `📤 [1:1] \u5df2\u5b66\u4e60${formatDurationMinutesCn(submitOld[2])}\uff08${submitOld[3]}%\uff09`;
}
const firstWait = s.match(/⏳\s*\[1:1\]\s*\u9996\u6b21\u8fdb\u5165\u7b49\u5f85(\d+)s\u540e\u66f4\u65b0\u8fdb\u5ea6(?:\s*\((\d+)\/(\d+)s\))?/);
if (firstWait) {
return `⏳ [1:1] \u9996\u6b21\u8fdb\u5165\u7b49\u5f85${formatIntervalCn(firstWait[1])}\u540e\u66f4\u65b0\u8fdb\u5ea6`;
}
const waitRaw = s.match(/⏳\s*\[1:1\]\s*\u7b49\u5f85(\d+)s\u540e\u7ee7\u7eed\s*\((\d+)\/(\d+)s\s*[·•]\s*([\d.]+)%\)/);
if (waitRaw) {
const [, waitSec, pos, total, pct] = waitRaw;
return `⏳ [1:1] ${formatIntervalCn(waitSec)}\u66f4\u65b0\u4e00\u6b21\u8fdb\u5ea6 · \u5df2\u5b8c\u6210 ${formatDurationMinutesCn(pos)}/${formatDurationMinutesCn(total)}\uff08${pct}%\uff09`;
}
const waitPartial = s.match(
/⏳\s*\[1:1\]\s*(?:\u7b49\u5f85)?(\d+\u5206\u949f|\d+\u79d2)?\u66f4\u65b0\u4e00\u6b21\u8fdb\u5ea6\s*·\s*\u5df2\u5b8c\u6210\s*(\d+)\/(\d+)s[\uff08(]([\d.]+)%[\uff09)]/
);
if (waitPartial) {
const waitLabel = waitPartial[1] || formatIntervalCn(getRealtimeChunkSec());
return `⏳ [1:1] ${waitLabel}\u66f4\u65b0\u4e00\u6b21\u8fdb\u5ea6 · \u5df2\u5b8c\u6210 ${formatDurationMinutesCn(waitPartial[2])}/${formatDurationMinutesCn(waitPartial[3])}\uff08${waitPartial[4]}%\uff09`;
}
if (/\u5df2\u5b66\u4e60\d|\u5df2\u5b8c\u6210\s*\d+\u5206\u949f\/\d+\u5206\u949f/.test(s)) return s;
return s
.replace(/\u7b49\u5f85(\d+)s\u540e\u7ee7\u7eed/g, (_, n) => `${formatIntervalCn(n)}\u66f4\u65b0\u4e00\u6b21\u8fdb\u5ea6`)
.replace(/\u6bcf(\d+)s\u4e0a\u62a5/g, (_, n) => `\u6bcf${formatIntervalCn(n)}\u66f4\u65b0\u8fdb\u5ea6`);
}
function studyModeLabel(mode) {
const m = mode !== undefined ? String(mode) : getStudyMode();
if (m === "realtime") return "1:1\u65f6\u957f\u6162\u5237\u6a21\u5f0f";
if (m === "efficiency") return "\u6548\u7387\u6a21\u5f0f";
return "\u672a\u9009\u62e9";
}
function getMode() {
const m = localStorage.getItem(MODE_KEY) || "background";
return ["background", "autoplay", "manual"].includes(m) ? m : "background";
}
function setMode(m) {
localStorage.setItem(MODE_KEY, m);
return m;
}
function applyLockedPanelDefaults() {
setMode("background");
if (getStudyMode() === "efficiency") {
setSaveInterval(60);
setMultiplier(2);
}
setAutoExam(true);
}
function shouldPatchPageLearnSave() {
return getMode() === "manual" || getMode() === "autoplay";
}
function loadBgState() {
try {
return JSON.parse(localStorage.getItem(BG_STATE_KEY) || "null") || {};
} catch (_) {
return {};
}
}
function saveBgState(bg) {
localStorage.setItem(BG_STATE_KEY, JSON.stringify(bg));
}
function flattenChapters(nodes, out) {
out = out || [];
for (const n of nodes || []) {
if (n.cptId) out.push(n);
if (n.childs?.length) flattenChapters(n.childs, out);
}
return out;
}
function patchLearnSaveBody(body) {
if (!shouldPatchPageLearnSave()) return body;
const mult = getMultiplier();
if (mult <= 1 || body == null) return body;
const raw = typeof body === "string" ? body : body instanceof URLSearchParams ? body.toString() : null;
if (!raw || raw[0] !== "{") return body;
try {
const data = JSON.parse(raw);
const p = data.payload;
if (!p || !p.studyTime || p.studyTime < 1) return body;
const start = Number(p.startTime) || 0;
const end = Number(p.endTime);
if (!Number.isFinite(end) || end <= start) return body;
const origStudy = p.studyTime;
const span = end - start;
const newStudy = Math.min(Math.round(origStudy * mult), Math.round(span * mult), 900);
if (newStudy <= origStudy) return body;
p.studyTime = newStudy;
console.log(
`[\u4e13\u6280\u770b\u8bfe] learnSave \u5b66\u65f6 ${origStudy}s → ${newStudy}s\uff08${mult}x\uff0c\u4ecd\u7ea65\u5206\u949f\u89e6\u53d1\u4e00\u6b21\uff09`
);
return JSON.stringify(data);
} catch (_) {
return body;
}
}
function isLearnSaveUrl(url) {
return url && String(url).includes("stdCustLearnRecord/learnSave/v1.lyai");
}
const xhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
this._zjzxUrl = url;
return xhrOpen.apply(this, arguments);
};
const xhrSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (body) {
if (body && isLearnSaveUrl(this._zjzxUrl)) {
body = patchLearnSaveBody(body);
}
return xhrSend.call(this, body);
};
const nativeFetch = window.fetch;
if (nativeFetch) {
window.fetch = function (input, init) {
const url = typeof input === "string" ? input : input?.url || "";
if (isLearnSaveUrl(url) && init?.body) {
init = Object.assign({}, init, { body: patchLearnSaveBody(init.body) });
}
return nativeFetch.call(this, input, init);
};
}
function log(msg) {
let text = String(msg || "");
if (isRealtimeStudyMode() && text.includes("[1:1]")) {
text = formatRealtimeEngineLog(text);
}
const line = `[\u4e13\u6280\u770b\u8bfe] ${text}`;
console.log(line);
state.lastAction = text;
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
state.logLines.push(`${ts} ${text}`);
if (state.logLines.length > 80) state.logLines.shift();
appendLogLine(`${ts} ${text}`);
updatePanel();
}
function courseDisplayName(nameOrCourse) {
const raw =
typeof nameOrCourse === "string"
? nameOrCourse
: nameOrCourse?.csName || nameOrCourse?.csId || "\u8bfe\u7a0b";
return String(raw).trim() || "\u8bfe\u7a0b";
}
function setPanelHint(msg) {
state.panelHint = String(msg || "").trim();
updatePanel();
}
function clearPanelHint() {
if (!state.panelHint) return;
state.panelHint = "";
updatePanel();
}
function appendLogLine(text) {
const box = document.getElementById("zjzx-run-log");
if (!box) return;
const div = document.createElement("div");
div.className = "zjzx-log-row";
div.textContent = text;
box.appendChild(div);
box.scrollTop = box.scrollHeight;
}
function clearRunLog() {
state.logLines = [];
const box = document.getElementById("zjzx-run-log");
if (box) box.innerHTML = "";
}
function getCloudApiBase() {
return DEFAULT_CLOUD_API_BASE;
}
function setCloudApiBase() {
state.cloudApiBase = DEFAULT_CLOUD_API_BASE;
}
function getLearningUserId() {
return String(getUserData()?.custId || "").trim();
}
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 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 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);
if (!lease || !Number.isFinite(exp) || exp <= 0) return null;
return {
lease,
exp,
tier: String(parsed.tier || "").trim(),
freeVideoLimit: Number(parsed.freeVideoLimit ?? parsed.free_video_limit ?? parsed.freeChapterLimit ?? 1),
freeUsedVideos: Number(parsed.freeUsedVideos ?? parsed.free_used_videos ?? parsed.freeUsedChapters ?? 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 {
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 (_) {}
}
function readCloudProExpireCache() {
try {
const raw = localStorage.getItem(CLOUD_PRO_EXPIRE_CACHE_KEY);
if (!raw) return null;
const p = JSON.parse(raw);
if (!p?.token || !p?.exp) return null;
return { token: String(p.token), exp: Number(p.exp) };
} catch (_) {
return null;
}
}
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 " + url)),
ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6 " + url)),
};
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 " + url));
xhr.ontimeout = () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6 " + url));
xhr.send(data);
});
}
async function _rq(path, method, payload) {
const base = getCloudApiBase();
const url = `${base}${path.startsWith("/") ? path : `/${path}`}`;
const headers = { Accept: "application/json", "Content-Type": "application/json" };
const luid = getLearningUserId();
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/zjzx/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) data = JSON.parse(text);
if (res.status < 200 || res.status >= 300) {
const errCode = String(data.detail || data.message || data.code || `http_${res.status}`);
throw new Error(errCode);
}
return data;
}
async function _el(forceRefresh) {
const now = Math.floor(Date.now() / 1000);
if (!forceRefresh && state.cloudLease && state.cloudLeaseExp - now > 60) return true;
if (!forceRefresh) {
const cached = readCloudLeaseCache();
if (cached && cached.exp - now > 60) {
state.cloudLease = cached.lease;
state.cloudLeaseExp = cached.exp;
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 = cached.freeUsedVideos;
updateCloudPanelUI();
return true;
}
}
const luid = getLearningUserId();
if (!luid) throw new Error("\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u83b7\u53d6\u4e91\u7aef\u6388\u6743");
let data;
try {
const leaseBody = { learning_user_id: luid };
data = await _rq("/api/zjzx/lease", "POST", leaseBody);
state.cloudRevoked = false;
} catch (e) {
const em = String(e?.message || e);
if (/(revoked|invalid token|expired)/i.test(em)) {
state.cloudRevoked = true;
state.cloudTier = "revoked";
state.cloudLease = "";
state.cloudLeaseExp = 0;
writeCloudLeaseCache("", 0);
updateCloudPanelUI();
}
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.cloudProExpireAt = proExpireAt > 0 ? proExpireAt : 0;
state.cloudTier = tier;
state.freeVideoLimit = Number(
data.free_video_limit ?? data.free_chapter_limit ?? data.freeVideoLimit ?? state.freeVideoLimit ?? 1
);
state.freeUsedVideos = Number(
data.free_used_videos ?? data.free_used_chapters ?? data.freeUsedVideos ?? 0
);
writeCloudLeaseCache(lease, exp, {
tier: state.cloudTier,
freeVideoLimit: state.freeVideoLimit,
freeUsedVideos: state.freeUsedVideos,
proExpireAt: state.cloudProExpireAt,
});
writeCloudLastState({
tier: state.cloudTier,
freeVideoLimit: state.freeVideoLimit,
freeUsedVideos: state.freeUsedVideos,
});
if (tier === "pro" && state.cloudProExpireAt > 0) {
writeCloudProExpireCache(state.cloudToken, state.cloudProExpireAt);
}
updateCloudPanelUI();
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 = getLearningUserId();
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) data = JSON.parse(text);
if (res.status < 200 || res.status >= 300 || !data.ok) {
throw new Error(String(data.message || data.detail || "Token \u6821\u9a8c\u5931\u8d25"));
}
return true;
}
function syncCloudQuotaFromResponse(data) {
if (!data || typeof data !== "object") return;
if (data.tier) state.cloudTier = String(data.tier);
const lim = data.free_video_limit ?? data.free_chapter_limit ?? data.limit;
const used = data.free_used_videos ?? data.free_used_chapters ?? data.used;
if (lim != null) state.freeVideoLimit = Number(lim);
if (used != null) state.freeUsedVideos = Number(used);
writeCloudLastState({
tier: state.cloudTier,
freeVideoLimit: state.freeVideoLimit,
freeUsedVideos: state.freeUsedVideos,
});
updateCloudPanelUI();
}
async function zjzxEngineStart(kind, context, cfg) {
await _el(false);
const data = await _rq("/api/zjzx/study/engine/start", "POST", {
kind: String(kind || ""),
context: context || {},
config: cfg || {},
});
syncCloudQuotaFromResponse(data);
return data;
}
async function zjzxEngineStep(sessionId, event, lastResult, contextPatch) {
await _el(false);
const data = await _rq("/api/zjzx/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 executeZjzxCommand(cmd) {
const c = cmd || {};
const t = String(c.type || "");
if (t === "platform_post") {
const res = await apiPost(c.path, c.payload || {});
return { event: "submit_result", lastResult: { data: res, http_status: 200 } };
}
if (t === "wait") {
await sleep(Math.max(500, Number(c.ms || 1000)));
return { event: "tick", lastResult: null };
}
if (t === "done") {
return {
terminal: true,
ok: !!c.success,
skipped: !!c.skipped,
score: c.score,
msg: c.message || "\u5b8c\u6210",
};
}
if (t === "failed") {
return { terminal: true, ok: false, msg: c.message || "failed" };
}
return { event: "tick", lastResult: null };
}
function shouldLogEngineLine(logText, options) {
const s = String(logText || "");
if (!s) return false;
if (options && options.realtime) return /\[1:1\]|learnSave \u672a\u6210\u529f|\u9996\u6b21\u8fdb\u5165\u7b49\u5f85/.test(s);
return true;
}
async function runZjzxEngineLoop(startRes, options) {
const quiet = !!(options && options.quiet);
const realtime = !!(options && options.realtime);
let sessionId = String(startRes.session_id || "");
let cmd = startRes.command;
if (!quiet && shouldLogEngineLine(startRes.log, options)) {
log(realtime ? formatRealtimeEngineLog(startRes.log) : startRes.log);
}
const maxSteps = Number(options && options.maxSteps) || (realtime ? 320 : 24);
for (let i = 0; i < maxSteps && cmd; i++) {
const out = await executeZjzxCommand(cmd);
if (out.terminal !== undefined) return out;
const next = await zjzxEngineStep(sessionId, out.event, out.lastResult, null);
sessionId = String(next.session_id || sessionId);
if (!quiet && shouldLogEngineLine(next.log, options)) {
log(realtime ? formatRealtimeEngineLog(next.log) : next.log);
}
cmd = next.command;
}
return { terminal: true, ok: false, msg: "\u4e91\u7aef\u5f15\u64ce\u6b65\u6570\u8d85\u9650" };
}
async function runZjzxExamLoop(startRes, courseName) {
let sessionId = String(startRes.session_id || "");
let cmd = startRes.command;
let questionCount = 0;
let submitAnnounced = false;
const maxSteps = 24;
for (let i = 0; i < maxSteps && cmd; i++) {
const out = await executeZjzxCommand(cmd);
if (out.terminal !== undefined) {
if (out.ok) {
const score = out.score != null && out.score !== "" ? out.score : "—";
log(`\u8bfe\u7a0b${courseName}\u8003\u8bd5\u5b8c\u6210\uff0c\u5f97\u5206\uff1a${score}\u5206`);
}
return out;
}
const next = await zjzxEngineStep(sessionId, out.event, out.lastResult, null);
sessionId = String(next.session_id || sessionId);
const slog = String(next.log || "");
const batchM = slog.match(/\u6279\u91cf\u4fdd\u5b58\s*(\d+)/);
if (batchM) {
questionCount = Number(batchM[1]) || questionCount;
if (!submitAnnounced && questionCount > 0) {
submitAnnounced = true;
log(`\u8bfe\u7a0b${courseName}\u8003\u8bd5\u5f00\u59cb\uff0c\u5171${questionCount}\u9053\u9898\uff0c\u5df2\u63d0\u4ea4\u7b54\u6848`);
}
}
if (/\u8003\u8bd5\u4f5c\u7b54\u4e2d/.test(slog)) {
log(`\u8bfe\u7a0b${courseName}\uff1a${slog}`);
}
if (/^\u4ea4\u5377/.test(slog)) {
log(`\u8bfe\u7a0b${courseName}\u5f00\u59cb\u4ea4\u5377`);
}
cmd = next.command;
}
return { terminal: true, ok: false, msg: "\u4e91\u7aef\u5f15\u64ce\u6b65\u6570\u8d85\u9650" };
}
async function ensureCloudReady() {
if (!isLoggedIn()) {
log("\u8bf7\u5148\u767b\u5f55\u4e13\u6280\u5e73\u53f0");
return false;
}
try {
if (state.cloudToken) await cloudVerifyToken();
await _el(false);
} catch (err) {
log("\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff1a" + (err.message || err));
return false;
}
const tier = String(state.cloudTier || "").toLowerCase();
if (tier === "pro") return true;
if (tier === "free") {
if (Number(state.freeUsedVideos) >= Number(state.freeVideoLimit)) {
log(`\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${state.freeUsedVideos}/${state.freeVideoLimit}\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`);
switchPanelTab("settings");
openProModal();
return false;
}
return true;
}
log("\u4e91\u7aef\u6388\u6743\u72b6\u6001\u5f02\u5e38\uff0c\u8bf7\u68c0\u67e5 Token \u6216\u4e91\u7aef\u5730\u5740");
return false;
}
function cloudServiceOrigin() {
try {
return new URL(getCloudApiBase()).origin;
} catch (_) {
return "";
}
}
async function fetchPanelNotice() {
const origin = cloudServiceOrigin();
if (!origin) return;
const path = String(state.panelNoticePath || "/api/zjzx/panel-notice");
const url = `${origin}${path.startsWith("/") ? path : `/${path}`}`;
try {
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("#zjzx-ann-text");
if (el) el.textContent = state.remotePanelNotice;
}
} catch (_) {}
}
async function fetchClientConfig() {
try {
const data = await gmRequestWithStatus(
`${getCloudApiBase()}/api/zjzx/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) || 1;
if (j?.proBuyUrl) state.proBuyUrl = String(j.proBuyUrl).trim() || PRO_BUY_URL;
}
} catch (_) {}
await fetchPanelNotice();
}
function getProBuyUrl() {
return String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL;
}
function isProBuyOpen() {
return Date.now() >= PRO_BUY_OPEN_MS;
}
function syncProBuyButtonUi() {
const btn = document.getElementById("zjzx-pro-buy-link");
if (!btn) return;
const open = isProBuyOpen();
btn.disabled = !open;
btn.textContent = open ? "\u524d\u5f80\u8d2d\u4e70\u9875\u5f00\u901a Pro" : "6\u67086\u65e50\u70b9\u5f00\u653e\u8d2d\u4e70";
btn.title = open ? "\u6253\u5f00\u8d2d\u4e70\u9875\u83b7\u53d6 Token" : "6\u67085\u65e5\u516c\u6d4b\u8bf7\u4eceQQ\u7fa4\u83b7\u53d6Token";
}
function openProBuyPage() {
if (!isProBuyOpen()) {
log("\u8d2d\u4e70\u9875\u5c06\u4e8e6\u67086\u65e50\u70b9\u5f00\u653e\uff0c\u516c\u6d4b\u8bf7\u52a0\u5165QQ\u7fa4\u83b7\u53d6Token");
return;
}
const url = getProBuyUrl();
try {
GM_openInTab(url, { active: true, insert: true, setParent: true });
} catch (_) {
window.open(url, "_blank", "noopener,noreferrer");
}
}
function openProModal() {
syncProBuyButtonUi();
const vm = document.getElementById("zjzx-pro-modal");
if (vm) vm.style.display = "flex";
}
function closeProModal() {
const vm = document.getElementById("zjzx-pro-modal");
if (vm) vm.style.display = "none";
}
function createProModal() {
const old = document.getElementById("zjzx-pro-modal");
if (old) old.remove();
const modal = document.createElement("div");
modal.id = "zjzx-pro-modal";
modal.innerHTML = `
\u5f00\u901a Pro
Pro \u7528\u6237\u53ef\u65e0\u9650\u5236\u89c6\u9891\u5b66\u4e60\uff0c\u5e76\u81ea\u52a8\u5b8c\u6210\u8bfe\u7a0b\u8003\u8bd5
Pro \u6743\u76ca
- \u89e3\u9664\u514d\u8d39\u4f53\u9a8c\u89c6\u9891\u6b21\u6570\u9650\u5236\uff0c\u5df2\u9009\u8bfe\u7a0b\u53ef\u6301\u7eed\u5b66\u4e60
- \u652f\u6301\u5b66\u5b8c\u540e\u81ea\u52a8\u8003\u8bd5\u4ea4\u5377
- Pro \u6709\u6548\u671f 10 \u5929\uff08\u7ed1\u5b9a\u4e13\u6280\u8d26\u53f7\uff0c\u5207\u6362\u8d26\u53f7\u5931\u6548\uff09
\u63d0\u793a\uff1aPro \u6743\u9650\u7ed1\u5b9a\u5f53\u524d\u767b\u5f55\u7684\u4e13\u6280\u8d26\u53f7\uff0c\u8bf7\u52ff\u4e0e\u4ed6\u4eba\u5171\u7528 Token\u3002
\u5f00\u901a\u65b9\u5f0f
1) \u52a0\u5165 QQ \u7fa4 ${QQ_GROUP_NUMBER}\uff0c\u8054\u7cfb\u7ba1\u7406\u5458\u83b7\u53d6 Token\uff08\u516c\u6d4b\u671f\u95f4\u622a\u6b626\u67085\u65e5\uff0c\u53cd\u9988Bug\u53ef\u4f18\u5148\u4f53\u9a8c\uff09
2) \u524d\u5f80\u8d2d\u4e70\u9875\u83b7\u53d6 Token\uff0c24 \u5c0f\u65f6\u81ea\u52a9\u53d1\u8d27
\u8d2d\u5f97 Token \u540e\uff1a\u5728\u8bbe\u7f6e\u9875 Token \u8f93\u5165\u6846\u7c98\u8d34\uff0c\u70b9\u51fb\u300c\u4fdd\u5b58\u5e76\u6821\u9a8c\u300d\u3002
\u91cd\u8981\uff1a\u8bf7\u52ff\u968f\u610f\u6cc4\u9732 Token\uff0c\u907f\u514d\u8d26\u53f7\u88ab\u591a\u4eba\u5171\u7528\u5bfc\u81f4\u5931\u6548\u3002
`;
document.body.appendChild(modal);
modal.addEventListener("click", (e) => {
if (e.target === modal) closeProModal();
});
modal.querySelector("#zjzx-pro-close")?.addEventListener("click", closeProModal);
modal.querySelector("#zjzx-pro-buy-link")?.addEventListener("click", openProBuyPage);
syncProBuyButtonUi();
}
function switchPanelTab(name) {
document.querySelectorAll(".zjzx-tab-btn").forEach((el) => {
el.classList.toggle("active", el.dataset.tab === name);
});
document.querySelectorAll(".zjzx-pane").forEach((el) => {
el.classList.toggle("active", el.dataset.pane === name);
});
}
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 updateCloudPanelUI() {
const tierEl = document.querySelector("#zjzx-cloud-tier");
const freeEl = document.querySelector("#zjzx-cloud-free");
const freeLabelEl = document.querySelector("#zjzx-cloud-free-label");
const tokenInput = document.querySelector("#zjzx-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\u89c6\u9891";
if (freeEl) freeEl.textContent = `${state.freeUsedVideos}/${state.freeVideoLimit}`;
}
if (tokenInput && tokenInput !== document.activeElement) tokenInput.value = state.cloudToken || "";
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getCookie(name) {
const m = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)"));
return m ? decodeURIComponent(m[1]) : "";
}
function getUserData() {
try {
const raw = getCookie("userData_lenye");
if (raw) return JSON.parse(raw);
} catch (_) {}
try {
const raw = localStorage.getItem("userData_lenye");
if (raw) return JSON.parse(raw);
} catch (_) {}
return null;
}
function isLoggedIn() {
return !!(getCookie("SESSION_TOKEN") || getUserData()?.SESSION_TOKEN);
}
function pageType() {
const path = location.pathname;
const qs = new URLSearchParams(location.search);
if (path.includes("courseplay")) return "courseplay";
if (path.includes("personcenter") && qs.get("type") === "train") return "train";
if (path.includes("personcenter")) return "personcenter";
if (path === "/" || path === "/index.html") return "home";
return "other";
}
function getCourseParams() {
const qs = new URLSearchParams(location.search);
return { csId: qs.get("id") || "", tpId: qs.get("tpId") || "" };
}
function courseplayPath(csId, tpId) {
return `/courseplay?id=${csId}&tpId=${tpId}`;
}
function readNavGuard() {
try {
return JSON.parse(sessionStorage.getItem(NAV_GUARD_KEY) || "{}");
} catch (_) {
return {};
}
}
function writeNavGuard(url) {
sessionStorage.setItem(
NAV_GUARD_KEY,
JSON.stringify({ url, time: Date.now() })
);
}
function isSameCoursePage(csId) {
return pageType() === "courseplay" && getCourseParams().csId === csId;
}
function canNavigate(url) {
const target = new URL(url, location.origin).pathname + new URL(url, location.origin).search;
const current = location.pathname + location.search;
if (target === current) return false;
const guard = readNavGuard();
if (guard.url === target && Date.now() - (guard.time || 0) < NAV_COOLDOWN_MS) {
return false;
}
return true;
}
function safeNavigate(url) {
if (!canNavigate(url)) return false;
writeNavGuard(url);
log("\u8df3\u8f6c\uff1a" + url);
location.href = url;
return true;
}
function canClickChapter(chapterId) {
if (!chapterId) return false;
const guard = readNavGuard();
if (guard.chapterId === chapterId && Date.now() - (guard.chapterClickAt || 0) < CHAPTER_CLICK_COOLDOWN_MS) {
return false;
}
return true;
}
function markChapterClick(chapterId) {
const guard = readNavGuard();
guard.chapterId = chapterId;
guard.chapterClickAt = Date.now();
sessionStorage.setItem(NAV_GUARD_KEY, JSON.stringify(guard));
}
function visible(el) {
if (!el) return false;
const st = getComputedStyle(el);
if (st.display === "none" || st.visibility === "hidden" || st.opacity === "0") return false;
return el.offsetWidth > 0 || el.offsetHeight > 0;
}
function clickEl(el) {
if (!el) return false;
el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
return true;
}
function findVisibleButton(root, pattern) {
return Array.from(root.querySelectorAll("button, .el-button, a")).find((el) =>
pattern.test((el.textContent || "").replace(/\s+/g, ""))
);
}
function dismissDialogs() {
const onCoursePlay = pageType() === "courseplay";
document.querySelectorAll(".el-message-box__wrapper, .el-dialog__wrapper").forEach((wrapper) => {
if (!visible(wrapper)) return;
const btn = onCoursePlay
? findVisibleButton(wrapper, /^(\u786e\u5b9a|\u77e5\u9053\u4e86|\u7ee7\u7eed|\u786e\u8ba4)$/) ||
findVisibleButton(wrapper, /(\u786e\u5b9a|\u77e5\u9053\u4e86)/)
: findVisibleButton(wrapper, /^(\u53bb\u5b66\u4e60|\u786e\u5b9a|\u77e5\u9053\u4e86|\u7ee7\u7eed|\u786e\u8ba4)$/) ||
findVisibleButton(wrapper, /(\u53bb\u5b66\u4e60|\u786e\u5b9a)/);
if (btn) clickEl(btn);
});
}
function isFaceDialogVisible() {
const qr = document.getElementById("qrCode");
if (qr && visible(qr.closest(".el-dialog__wrapper") || qr)) return true;
return Array.from(document.querySelectorAll(".el-dialog__wrapper")).some((w) => {
if (!visible(w)) return false;
const t = w.textContent || "";
return /\u4eba\u8138|\u8bc6\u522b|\u626b\u7801|\u4e0a\u4f20\u5934\u50cf/.test(t);
});
}
function getVideoEl() {
const box = document.getElementById("player_video");
if (box) {
const v = box.querySelector("video");
if (v) return v;
}
return document.querySelector("#player_video video, .playboxin video, video");
}
function getPlayButton() {
return (
document.querySelector(".e3cplay-play, .vjs-play-control, .xgplayer-play, .play-btn") ||
document.querySelector("#player_video .vjs-big-play-button")
);
}
async function ensurePlaying() {
if (isFaceDialogVisible()) {
if (!state.faceWaiting) {
state.faceWaiting = true;
log("\u68c0\u6d4b\u5230\u4eba\u8138\u8bc6\u522b\uff0c\u8bf7\u624b\u673a\u626b\u7801\u540e\u7ee7\u7eed");
}
return;
}
state.faceWaiting = false;
const video = getVideoEl();
if (!video) return;
if (video.ended) return;
if (video.paused) {
try {
video.muted = true;
await video.play();
log("\u5df2\u81ea\u52a8\u64ad\u653e\u89c6\u9891");
return;
} catch (_) {
const btn = getPlayButton();
if (btn) {
clickEl(btn);
log("\u5df2\u70b9\u51fb\u64ad\u653e\u6309\u94ae");
}
}
}
}
function chapterDone(link) {
const img = link.querySelector("img.course_play_percent");
if (!img || !img.src) return false;
return /play100|100/.test(img.src) || /play100/.test(img.getAttribute("src") || "");
}
function getChapterLinks() {
return Array.from(document.querySelectorAll("a.cptresult_a"));
}
function getCurrentChapterId() {
const active = document.querySelector("li.active a.cptresult_a");
return active?.dataset?.name || active?.id || "";
}
function findNextIncompleteChapter(skipCurrent) {
const links = getChapterLinks();
if (!links.length) return null;
const currentId = getCurrentChapterId();
let passedCurrent = !currentId || !skipCurrent;
for (const link of links) {
const id = link.dataset.name || link.id;
if (!passedCurrent) {
if (id === currentId) passedCurrent = true;
continue;
}
if (!chapterDone(link)) return link;
}
if (skipCurrent) return null;
return links.find((link) => !chapterDone(link)) || null;
}
function ensureInStudyTab() {
const inStudyTab = Array.from(document.querySelectorAll(".tab-ul li, .zaixue-course-tit li")).find((li) =>
/\u5728\u5b66/.test(li.textContent || "")
);
if (inStudyTab && !inStudyTab.classList.contains("on") && !inStudyTab.classList.contains("active")) {
clickEl(inStudyTab.querySelector("a") || inStudyTab);
return true;
}
return false;
}
function buildApiBody(payload) {
const user = getUserData();
const sessionToken = getCookie("SESSION_TOKEN") || user?.SESSION_TOKEN || "";
let uuid = getCookie("UUID") || user?.UUID || "";
if (!uuid) {
try {
const headerRaw = getCookie("header_name_lenye");
if (headerRaw) {
const h = JSON.parse(headerRaw);
uuid = h.UUID || uuid;
}
} catch (_) {}
}
if (!uuid && crypto.randomUUID) uuid = crypto.randomUUID();
return JSON.stringify({
header: {
SESSION_TOKEN: sessionToken,
CLIENT_OS: "O",
UUID: uuid,
REQ_TIME: String(Date.now()),
SITE_ID: SITE_ID,
},
payload,
});
}
function apiPost(path, payload) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", API_BASE + path, true);
xhr.withCredentials = false;
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.onload = function () {
try {
resolve(JSON.parse(xhr.responseText || "{}"));
} catch (err) {
reject(new Error("\u54cd\u5e94\u89e3\u6790\u5931\u8d25"));
}
};
xhr.onerror = function () {
reject(new Error("\u7f51\u7edc\u8bf7\u6c42\u5931\u8d25"));
};
xhr.send(buildApiBody(payload));
});
}
async function fetchHeartbeatSec() {
try {
const res = await apiPost("/resource-service/stdCustLearnRecord/getHeartBeatInterval/v1.lyai", {});
if (res.success === "1" && res.data?.interval) {
return Number(res.data.interval) || 300;
}
} catch (_) {}
return 300;
}
function isCourseUnfinished(c) {
if (!c) return false;
if (isFalsyFlag(c.lcsFinished)) return true;
if (isFalsyFlag(c.lcsStudyFinished)) return true;
if (isFalsyFlag(c.lcsExameFinished) && (c.courseTestList || []).length) return true;
const p = parseFloat(c.lcsProcess);
return !Number.isNaN(p) && p < 100;
}
function isCourseFullyFinished(c) {
if (!c) return false;
if (isTruthyFlag(c.lcsFinished)) return true;
const hasExam = (c.courseTestList || []).length > 0;
if (hasExam) return isStudyFinished(c) && isTruthyFlag(c.lcsExameFinished);
return isStudyFinished(c);
}
function isStudyFinished(course) {
if (!course) return false;
if (isTruthyFlag(course.lcsStudyFinished)) return true;
return parseFloat(course.lcsProcess || 0) >= 100;
}
function courseNeedsExam(course) {
if (!getAutoExam() || !course) return false;
if (isTruthyFlag(course.lcsExameFinished)) return false;
const test = (course.courseTestList || [])[0];
if (!test?.tsId) return false;
return isStudyFinished(course);
}
async function fetchCourseFromPlans(csId) {
const user = getUserData();
if (!user?.custId || !csId) return null;
const plans = await listTeachPlans(user.custId);
for (const plan of plans) {
const courses = await listPlanCourses(plan.tpId, user.custId);
const course = courses.find((c) => c.csId === csId);
if (course) return { ...course, tpId: plan.tpId, tpName: plan.tpName || plan.tpTitle || "" };
}
return null;
}
async function listTeachPlans(studentUserId) {
const res = await apiPost("/trainpronew-service/TPClazzStudentRel/queryStudentTeachPlanPage/v1.lyai", {
page: 1,
rows: "50",
sorts: "singupDate=0",
studentUserId,
tpName: "",
year: "",
});
if (res.success !== "1" || !res.data?.length) return [];
return res.data;
}
async function listPlanCourses(tpId, custId) {
const res = await apiPost("/trainpronew-service/login/cgUserPlanClazzCourse/queryUserPlanCoursePage/v1.lyai", {
tpId,
page: 1,
rows: 999,
custId,
sorts: "lcsFinished=1&updateDate=0",
needTeacherInfo: 0,
});
if (res.success !== "1" || !res.data?.length) return [];
return res.data;
}
async function listAllCoursesForPlan(plan) {
const user = getUserData();
if (!plan?.tpId || !user?.custId) return [];
const courses = await listPlanCourses(plan.tpId, user.custId);
const tpName = plan.tpName || plan.tpTitle || "";
const planYear = plan.year || "";
return courses.map((c) => ({ ...c, tpId: plan.tpId, tpName, planYear }));
}
async function listAllUnfinishedCourses() {
const user = getUserData();
if (!user?.custId) {
log("\u672a\u8bfb\u53d6\u5230 custId\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55");
return [];
}
const plans = await listTeachPlans(user.custId);
if (!plans.length) {
log("\u672a\u627e\u5230\u57f9\u8bad\u8ba1\u5212\uff0c\u8bf7\u5148\u5728\u5b66\u4e60\u4e2d\u5fc3\u9009\u8bfe");
return [];
}
const out = [];
for (const plan of plans) {
const tpId = plan.tpId;
if (!tpId) continue;
const courses = await listPlanCourses(tpId, user.custId);
for (const course of courses) {
if (!isCourseUnfinished(course)) continue;
out.push({ ...course, tpId, tpName: plan.tpName || plan.tpTitle || "" });
}
}
out.sort((a, b) => parseFloat(b.lcsProcess || 0) - parseFloat(a.lcsProcess || 0));
return out;
}
async function findUnfinishedCourse() {
const list = await listAllUnfinishedCourses();
if (!list.length) {
log("\u5df2\u68c0\u67e5\u5168\u90e8\u8ba1\u5212\uff0c\u8bfe\u7a0b\u5747\u663e\u793a\u5df2\u5b8c\u6210");
return null;
}
return list[0];
}
async function findNextWorkInQueue(options) {
const silent = !!(options && options.silent);
const queue = loadQueue();
if (!queue.length) {
if (!silent) log("\u8bf7\u52fe\u9009\u8981\u5b66\u4e60\u7684\u8bfe\u7a0b\u540e\u518d\u70b9\u5f00\u59cb");
return null;
}
const allCourses = await listAllUnfinishedCourses();
const courseMap = new Map(allCourses.map((c) => [c.csId, c]));
for (const csId of queue) {
let course = courseMap.get(csId);
if (!course) course = await fetchCourseFromPlans(csId);
if (!course) continue;
const allVideos = await loadCourseVideos(csId);
const videos = allVideos.filter(isVideoUnfinished);
if (videos.length) {
const done = allVideos.length - videos.length;
return {
phase: "study",
course,
videos,
videoTotal: allVideos.length,
videoDone: done,
};
}
if (courseNeedsExam(course)) {
const test = course.courseTestList[0];
return { phase: "exam", course, test };
}
}
return null;
}
function collectCorrectAnswers(paper) {
const answers = [];
for (const cat of paper.questionCats || []) {
const itemType = cat.itemType;
for (const q of cat.questions || []) {
const correct = (q.options || []).filter((o) => isTruthyFlag(o.answer));
if (!correct.length) continue;
let answerIdValue;
if (itemType === "ms") {
answerIdValue = correct.map((o) => o.itemId).join(",");
} else {
answerIdValue = correct[0].itemId;
}
answers.push({
questionId: q.questionId,
answerIdValue,
answerContentValue: "",
});
}
}
return answers;
}
async function submitCourseExam(course, testInfo) {
const user = getUserData();
if (!user?.custId) return { ok: false, msg: "\u672a\u767b\u5f55" };
const tsId = testInfo.tsId;
const spcId = testInfo.tsSpcId || testInfo.spcId;
if (!tsId || !spcId) return { ok: false, msg: "\u7f3a\u5c11 tsId/spcId" };
try {
const startRes = await zjzxEngineStart("exam", {
study_user_id: user.custId,
test: { tsId, tsSpcId: spcId, spcId },
course: { csId: course.csId, csName: course.csName },
}, {});
const loop = await runZjzxExamLoop(startRes, courseDisplayName(course));
if (loop.ok) {
return { ok: true, score: loop.score ?? "", msg: loop.msg || "\u4ea4\u5377\u6210\u529f" };
}
return { ok: false, msg: loop.msg || "\u4e91\u7aef\u8003\u8bd5\u5931\u8d25" };
} catch (err) {
return { ok: false, msg: err.message || String(err) };
}
}
function applyBgProgressEstimate(bg) {
const total = Number(bg.videoTotal) || 0;
if (!total) return;
const completed = Number(bg.videoDone || 0) + Number(bg.videoIndex || 0);
const est = (completed / total) * 100;
bg.lcsProcess = Math.max(Number(bg.lcsProcess || 0), est);
}
async function afterVideoLearnSuccess(bg, idx, waitSec) {
bg.videoIndex = idx + 1;
bg.lastSaveAt = Date.now();
bg.saveCount = (bg.saveCount || 0) + 1;
const prog = await fetchCourseProgress(bg.csId);
if (prog) {
bg.lcsProcess = prog.lcsProcess;
bg.csName = prog.csName || bg.csName;
}
applyBgProgressEstimate(bg);
const allVideosDone = bg.videoIndex >= bg.videos.length;
saveBgState(bg);
updateCourseProgressInPanel(bg.csId, bg.lcsProcess);
updatePanel();
void refreshChapterPreview();
if (allVideosDone) {
await finishCourseAndAdvance(bg);
} else {
const waitText = isRealtimeStudyMode() ? formatIntervalCn(waitSec) : `${waitSec}\u79d2`;
log(`\u89c6\u9891${idx + 1}\u5df2\u5b8c\u6210\uff0c\u7b49\u5f85${waitText}\u8fdb\u884c\u4e0b\u4e00\u4e2a\u5b66\u4e60`);
}
}
function stopAutoStudy(reason) {
if (!state.enabled) return;
state.enabled = false;
localStorage.setItem(STORAGE_KEY, "0");
clearStudyModeSelection();
syncStartStopButtons();
syncKeepAlive();
stopPreviewAutoRefresh();
log(reason || "\u6240\u6709\u5df2\u9009\u8bfe\u7a0b\u5747\u5df2\u5b8c\u6210\uff0c\u7a0b\u5e8f\u81ea\u52a8\u505c\u6b62");
updatePanel();
}
function buildBgStateFromWork(hit) {
const { course, phase } = hit;
const base = {
csId: course.csId,
tpId: course.tpId,
csName: course.csName,
lcsProcess: parseFloat(course.lcsProcess || 0),
phase,
lastSaveAt: 0,
saveCount: 0,
};
if (phase === "study") {
return {
...base,
videos: hit.videos,
videoIndex: 0,
videoTotal: hit.videoTotal ?? hit.videos.length,
videoDone: hit.videoDone ?? 0,
};
}
return { ...base, videos: [], videoIndex: 0, test: hit.test };
}
function isVideoUnfinished(v) {
if (!v) return false;
if (v.studyFinished === "1") return false;
if (Number(v.process) >= 100) return false;
return true;
}
async function loadCourseVideos(csId) {
const treeRes = await apiPost("/course-service/crsChapter/treeList/v1.lyai", { csId });
const chapters = flattenChapters(treeRes.data || []);
const cptIds = chapters.map((c) => c.cptId).filter(Boolean);
if (!cptIds.length) return [];
const relRes = await apiPost("/course-service/crsChapterResourceRel/queryListByCptIds/v1.lyai", {
cptId: cptIds.join(",") + ",",
csId,
sorts: "1",
});
if (relRes.success !== "1" || !relRes.data?.length) return [];
return relRes.data
.map((r) => ({
resId: r.resId,
cptId: r.cptId,
fileTimeLen: Number(r.fileTimeLen) || 0,
studyTime: Number(r.studyTime) || 0,
process: Number(r.process) || 0,
studyFinished: r.studyFinished,
resName: r.resName || "",
}))
.filter((v) => v.resId && v.fileTimeLen > 0);
}
async function fetchVideoBreakpoint(csId, resId) {
const user = getUserData();
if (!user?.custId || !resId) return 0;
let bp = 0;
const progRes = await apiPost("/resource-service/stdCustLearnResource/queryResources/v1.lyai", {
studyUserId: user.custId,
resinfo: "1",
resId,
needRepeatLearn: "1",
csId,
});
if (progRes.success === "1" && progRes.data?.[0]) {
const p0 = progRes.data[0];
bp = Number(p0.studyBreakpoint || p0.endTime || 0);
}
const procRes = await apiPost("/resource-service/stdCustLearnProcessRecord/queryPage/v1.lyai", {
studyUserId: user.custId,
resId,
siteId: SITE_ID,
source: "1",
});
if (procRes.success === "1" && procRes.data?.[0]?.endTime != null) {
bp = Math.max(bp, Number(procRes.data[0].endTime) || 0);
}
return Math.max(0, Math.floor(bp));
}
async function learnSaveFullVideo(csId, video, mult) {
const user = getUserData();
const breakpoint = await fetchVideoBreakpoint(csId, video.resId);
const realtime = isRealtimeStudyMode();
const chunkSec = realtime ? await ensurePlatformHeartbeatSec() : 0;
const cfg = {
study_mode: realtime ? "realtime" : "efficiency",
multiplier: realtime ? 1 : mult,
chunk_sec: realtime ? chunkSec : 0,
free_video_limit: state.freeVideoLimit,
};
const startRes = await zjzxEngineStart(
"video",
{
cs_id: csId,
study_user_id: user.custId,
video: {
resId: video.resId,
cptId: video.cptId,
fileTimeLen: video.fileTimeLen,
resName: video.resName || "",
},
multiplier: cfg.multiplier,
breakpoint,
},
cfg
);
const loop = await runZjzxEngineLoop(startRes, {
quiet: !realtime,
realtime,
maxSteps: realtime ? 320 : 24,
});
if (loop.skipped) return { success: "1", skipped: true };
if (loop.ok) return { success: "1" };
throw new Error(loop.msg || "\u4e91\u7aef learnSave \u5931\u8d25");
}
async function resolveResourceForCourse(csId, cptIdHint) {
const user = getUserData();
let cptId = cptIdHint || "";
const treeRes = await apiPost("/course-service/crsChapter/treeList/v1.lyai", { csId });
const chapters = flattenChapters(treeRes.data || []);
if (!chapters.length) return null;
if (!cptId) cptId = chapters[0].cptId;
const relRes = await apiPost("/course-service/crsChapterResourceRel/queryListByCptIds/v1.lyai", {
cptId,
csId,
sorts: "1",
});
const relList = relRes.data || [];
const rel = Array.isArray(relList) ? relList[0] : relList;
const resId = rel?.resId || rel?.resArr?.resId;
if (!resId) return null;
const detailRes = await apiPost("/resource-service/resResource/queryById/v1.lyai", { resId });
const detail = detailRes.data || {};
const duration = Number(detail.fileTimeLen || detail.resDuration || detail.playTime || 0);
let studyBreakpoint = 0;
const progRes = await apiPost("/resource-service/stdCustLearnResource/queryResources/v1.lyai", {
studyUserId: user.custId,
resinfo: "1",
resId,
needRepeatLearn: "1",
csId,
});
if (progRes.success === "1" && progRes.data?.[0]) {
const p0 = progRes.data[0];
studyBreakpoint = Number(p0.studyBreakpoint || p0.endTime || 0);
}
const procRes = await apiPost("/resource-service/stdCustLearnProcessRecord/queryPage/v1.lyai", {
studyUserId: user.custId,
resId,
siteId: SITE_ID,
source: "1",
});
if (procRes.success === "1" && procRes.data?.[0]?.endTime != null) {
studyBreakpoint = Math.max(studyBreakpoint, Number(procRes.data[0].endTime) || 0);
}
return {
csId,
cptId,
resId,
duration: duration > 0 ? duration : 7200,
studyBreakpoint,
csName: detail.resName || "",
};
}
async function initBackgroundTarget() {
const queue = loadQueue();
const saved = loadBgState();
if (queue.length && saved.csId && queue.includes(saved.csId)) {
if (saved.phase === "exam" && saved.test?.tsId) {
return { ...saved, lastSaveAt: 0 };
}
if (saved.videos?.length) {
const idx = saved.videoIndex || 0;
if (idx < saved.videos.length) {
const allVideos = await loadCourseVideos(saved.csId);
const pending = allVideos.filter(isVideoUnfinished);
if (pending.length) {
let videoIndex = idx;
const cur = saved.videos[idx];
if (cur?.resId) {
const at = pending.findIndex((v) => v.resId === cur.resId);
if (at >= 0) videoIndex = at;
else videoIndex = 0;
}
const prog = await fetchCourseProgress(saved.csId);
return {
...saved,
phase: "study",
videos: pending,
videoIndex,
lastSaveAt: 0,
videoTotal: allVideos.length,
videoDone: allVideos.length - pending.length,
lcsProcess: prog?.lcsProcess ?? saved.lcsProcess,
csName: prog?.csName || saved.csName,
};
}
}
}
} else if (saved.csId && queue.length && !queue.includes(saved.csId)) {
saveBgState({});
}
const hit = await findNextWorkInQueue({ silent: true });
if (!hit) return null;
const bg = buildBgStateFromWork(hit);
if (hit.phase === "study") {
const prog = await fetchCourseProgress(hit.course.csId);
if (prog) {
bg.lcsProcess = prog.lcsProcess;
bg.csName = prog.csName || bg.csName;
}
}
return bg;
}
async function finishCourseAndAdvance(bg) {
const course = (await fetchCourseFromPlans(bg.csId)) || { csId: bg.csId, csName: bg.csName, tpId: bg.tpId };
if (courseNeedsExam(course)) {
bg.phase = "exam";
bg.videos = [];
bg.videoIndex = 0;
bg.test = course.courseTestList[0];
bg.lastSaveAt = 0;
saveBgState(bg);
const proc = Number(bg.lcsProcess ?? 0).toFixed(1);
log(`\u8bfe\u7a0b${courseDisplayName(bg)}\u6240\u6709\u89c6\u9891\u5df2\u5b8c\u6210\u5b66\u4e60\uff0c\u5f53\u524d\u8fdb\u5ea6${proc}%\uff0c\u4e0b\u4e00\u6b65\u8fdb\u5165\u8003\u8bd5\u9636\u6bb5`);
renderCourseList();
void refreshChapterPreview();
return;
}
saveBgState({});
state.coursesDoneSession += 1;
log(`\u8bfe\u7a0b${courseDisplayName(bg)}\u5df2\u5b8c\u6210\uff08\u65e0\u8003\u8bd5\uff09`);
void refreshChapterPreview();
refreshPanelCourses();
}
async function fetchCourseProgress(csId) {
const user = getUserData();
if (!user?.custId || !csId) return null;
const res = await apiPost("/course-service/crsCustLearnCourse/queryUserCoursePage/v1.lyai", {
csId,
studyUserId: user.custId,
orderType: "1",
});
if (res.success !== "1" || !res.data?.[0]) return null;
const row = res.data[0];
return {
lcsProcess: parseFloat(row.lcsProcess || 0),
lcsStudyTime: row.lcsStudyTime,
csName: row.csName,
};
}
function formatBgPanelExtra(bg) {
if (!bg?.csId) return "";
const name = (bg.csName || "\u8bfe\u7a0b").slice(0, 12);
if (bg.phase === "exam") return `\u3010${name} · \u8003\u8bd5\u4e2d\u3011`;
const idx = (bg.videoIndex || 0) + 1;
const total = bg.videos?.length || 0;
const cur = bg.videos?.[bg.videoIndex || 0];
const proc = bg.lcsProcess != null ? ` · ${bg.lcsProcess}%` : "";
const vid = cur ? ` · ${cur.fileTimeLen}s` : "";
return `\u3010${name} \u89c6\u9891${idx}/${total}${vid}${proc}\u3011`;
}
function hasActiveBgTask(bg) {
if (!bg?.csId) return false;
if (bg.phase === "exam" && bg.test?.tsId) return true;
return !!(bg.videos && bg.videos.length);
}
async function backgroundLearnTick() {
if (state.bgTickBusy) return;
state.bgTickBusy = true;
try {
if (pageType() === "courseplay") {
log("\u540e\u53f0\u6a21\u5f0f\uff1a\u8bf7\u5173\u95ed\u64ad\u653e\u9875\uff0c\u907f\u514d\u4e0e\u9875\u9762\u81ea\u5e26\u4e0a\u62a5\u51b2\u7a81");
return;
}
let bg = loadBgState();
if (!hasActiveBgTask(bg)) {
bg = await initBackgroundTarget();
if (!bg) {
if (state.enabled) {
stopAutoStudy("\u6240\u6709\u5df2\u9009\u8bfe\u7a0b\u5747\u5df2\u5b8c\u6210\uff0c\u7a0b\u5e8f\u81ea\u52a8\u505c\u6b62");
}
return;
}
saveBgState(bg);
renderCourseList();
const cname = courseDisplayName(bg);
const proc = Number(bg.lcsProcess || 0).toFixed(1);
if (bg.phase === "exam") {
log(`\u8bfe\u7a0b${cname}\u6240\u6709\u89c6\u9891\u5df2\u5b8c\u6210\u5b66\u4e60\uff0c\u5f53\u524d\u8fdb\u5ea6${proc}%\uff0c\u4e0b\u4e00\u6b65\u8fdb\u5165\u8003\u8bd5\u9636\u6bb5`);
} else {
const total = bg.videoTotal ?? bg.videos?.length ?? 0;
const pending = bg.videos?.length ?? 0;
const done = bg.videoDone ?? Math.max(0, total - pending);
log(
`\u5f00\u59cb\u5b66\u4e60\u8bfe\u7a0b${cname} \u8fdb\u5ea6${proc}% \u89c6\u9891\u6570${total} \u5df2\u5b8c\u6210${done}\u4e2a\u89c6\u9891\uff0c\u5f85\u5b66\u4e60${pending}\u4e2a\u89c6\u9891`
);
}
}
const waitSec = getSaveInterval();
const elapsed = Date.now() - (bg.lastSaveAt || 0);
if (bg.lastSaveAt && elapsed < waitSec * 1000) {
const remain = Math.ceil((waitSec * 1000 - elapsed) / 1000);
state.lastAction = `\u7b49\u5f85 ${remain}s ${formatBgPanelExtra(bg)}`;
updatePanel();
return;
}
if (bg.phase === "exam") {
const course = (await fetchCourseFromPlans(bg.csId)) || {
csId: bg.csId,
csName: bg.csName,
tpId: bg.tpId,
courseTestList: [bg.test],
};
const examRes = await submitCourseExam(course, bg.test);
bg.lastSaveAt = Date.now();
if (examRes.ok) {
saveBgState({});
state.coursesDoneSession += 1;
refreshPanelCourses();
} else {
saveBgState(bg);
log(`\u8003\u8bd5\u5931\u8d25\uff1a${examRes.msg}`);
}
return;
}
const idx = bg.videoIndex || 0;
const video = bg.videos[idx];
if (!video) {
await finishCourseAndAdvance(bg);
return;
}
const mult = getMultiplier();
const realtime = isRealtimeStudyMode();
let modeTag = "\uff08\u6548\u7387\uff09";
if (realtime) {
const chunkSec = await ensurePlatformHeartbeatSec();
modeTag = `\uff081:1·\u6bcf${formatIntervalCn(chunkSec)}\u66f4\u65b0\u8fdb\u5ea6\uff09`;
}
log(`\u6b63\u5728\u5b66\u4e60\u89c6\u9891${idx + 1}${modeTag}`);
let res;
try {
res = await learnSaveFullVideo(bg.csId, video, mult);
} catch (err) {
const em = String(err.message || err);
log("\u4e91\u7aef\u89c6\u9891\u5f15\u64ce\uff1a" + em);
if (/free_quota|\u914d\u989d/.test(em)) {
state.enabled = false;
localStorage.setItem(STORAGE_KEY, "0");
clearStudyModeSelection();
syncStartStopButtons();
syncKeepAlive();
updatePanel();
}
return;
}
if (res.skipped || res.success === "1") {
await afterVideoLearnSuccess(bg, idx, waitSec);
return;
}
if (String(res.success).includes("drag")) {
saveBgState({});
log(`\u4e0a\u62a5\u88ab\u62d2\uff08${video.resName}\uff09\uff0c\u5df2\u91cd\u7f6e`);
return;
}
log("\u4e0a\u62a5\u5931\u8d25\uff1a" + (res.message || res.success) + " · " + (video.resName || ""));
} finally {
state.bgTickBusy = false;
}
}
async function openFirstUnfinishedCourseByApi() {
const course = await findUnfinishedCourse();
if (!course) return false;
if (isSameCoursePage(course.csId)) {
log("\u5df2\u5728\u5f53\u524d\u8bfe\u7a0b\u9875\uff0c\u7b49\u5f85\u64ad\u653e");
return false;
}
const target = courseplayPath(course.csId, course.tpId);
if (!canNavigate(target)) {
log("\u521a\u8fdb\u5165\u8bfe\u7a0b\uff0c\u7b49\u5f85\u9875\u9762\u7a33\u5b9a");
return false;
}
log(`\u8fdb\u5165\u8bfe\u7a0b\uff1a${course.csName || course.csId}\uff08${course.lcsProcess || 0}%\uff09`);
safeNavigate(target);
return true;
}
async function handleTrainPage() {
dismissDialogs();
if (ensureInStudyTab()) {
log("\u5df2\u5207\u6362\u5230\u300c\u5728\u5b66\u8bfe\u7a0b\u300d");
return;
}
const now = Date.now();
if (now - state.trainApiTriedAt < NAV_COOLDOWN_MS) {
log("\u5b66\u4e60\u4e2d\u5fc3\u7b49\u5f85\u4e2d\uff0c\u907f\u514d\u91cd\u590d\u6253\u5f00\u8bfe\u7a0b");
return;
}
state.trainApiTriedAt = now;
await openFirstUnfinishedCourseByApi();
}
async function handleCoursePlayPage() {
dismissDialogs();
const video = getVideoEl();
const links = getChapterLinks();
const activeId = getCurrentChapterId();
if (!video) {
if (activeId) {
if (!state.courseplayWaitSince) state.courseplayWaitSince = Date.now();
if (Date.now() - state.courseplayWaitSince < VIDEO_WAIT_MS) {
log("\u7b49\u5f85\u64ad\u653e\u5668\u52a0\u8f7d...");
return;
}
state.courseplayWaitSince = 0;
}
const next = findNextIncompleteChapter(true);
if (next) {
const chapterId = next.dataset.name || next.id;
if (!canClickChapter(chapterId)) {
log("\u7ae0\u8282\u5207\u6362\u51b7\u5374\u4e2d");
return;
}
markChapterClick(chapterId);
clickEl(next);
log("\u5df2\u5207\u6362\u7ae0\u8282\uff1a" + (next.title || next.textContent || "").trim());
state.courseplayWaitSince = Date.now();
return;
}
if (links.length && !links.some((link) => !chapterDone(link))) {
log("\u5f53\u524d\u8bfe\u7a0b\u7ae0\u8282\u5df2\u5168\u90e8\u5b8c\u6210\uff0c\u8fd4\u56de\u5b66\u4e60\u4e2d\u5fc3");
safeNavigate("/personcenter?type=train");
}
return;
}
state.courseplayWaitSince = 0;
if (video.ended) {
const next = findNextIncompleteChapter(true);
if (!next) {
log("\u5f53\u524d\u8bfe\u7a0b\u5df2\u5168\u90e8\u5b8c\u6210\uff0c\u8fd4\u56de\u5b66\u4e60\u4e2d\u5fc3");
safeNavigate("/personcenter?type=train");
}
return;
}
await ensurePlaying();
}
async function handleHomePage() {
if (!isLoggedIn()) {
log("\u8bf7\u5148\u767b\u5f55");
return;
}
if (location.pathname.includes("personcenter")) return;
safeNavigate("/personcenter?type=train");
}
async function tick() {
if (!state.enabled) return;
if (state.bgTickBusy) return;
const now = Date.now();
if (now - state.lastTick < TICK_MS) return;
state.lastTick = now;
if (!isLoggedIn()) {
log("\u672a\u767b\u5f55\uff0c\u8bf7\u5148\u767b\u5f55\u8d26\u53f7");
return;
}
if (!(await ensureCloudReady())) {
state.enabled = false;
localStorage.setItem(STORAGE_KEY, "0");
clearStudyModeSelection();
syncStartStopButtons();
syncKeepAlive();
updatePanel();
return;
}
dismissDialogs();
const mode = getMode();
const type = pageType();
try {
if (mode === "background") {
await backgroundLearnTick();
return;
}
if (mode === "manual") {
if (type === "courseplay") {
dismissDialogs();
return;
}
log("\u624b\u52a8\u6a21\u5f0f\uff1a\u8bf7\u81ea\u884c\u6253\u5f00\u64ad\u653e\u9875\u770b\u8bfe");
return;
}
if (type === "home" || type === "personcenter") {
await handleHomePage();
} else if (type === "train") {
await handleTrainPage();
} else if (type === "courseplay") {
await handleCoursePlayPage();
}
} catch (err) {
log("\u8fd0\u884c\u5f02\u5e38\uff1a" + (err.message || err));
}
}
async function fetchPanelCourses() {
const user = getUserData();
if (!user?.custId) {
state.panelCourses = [];
state.teachPlans = [];
return [];
}
const plans = await listTeachPlans(user.custId);
state.teachPlans = plans;
let tpId = loadSelectedTpId();
if (!tpId || !plans.some((p) => p.tpId === tpId)) {
tpId = plans[0]?.tpId || "";
if (tpId) saveSelectedTpId(tpId);
} else {
state.selectedTpId = tpId;
}
const plan = plans.find((p) => p.tpId === tpId);
const list = plan ? await listAllCoursesForPlan(plan) : [];
state.panelCourses = list.map((c) => ({
csId: c.csId,
tpId: c.tpId,
csName: c.csName || c.courseName || "\u672a\u547d\u540d\u8bfe\u7a0b",
lcsProcess: parseFloat(c.lcsProcess || 0),
tpName: c.tpName || "",
planYear: c.planYear || "",
lcsStudyFinished: c.lcsStudyFinished,
lcsExameFinished: c.lcsExameFinished,
lcsFinished: c.lcsFinished,
courseTestList: c.courseTestList || [],
}));
state.panelCourses.sort((a, b) => {
const au = isCourseUnfinished(a) ? 1 : 0;
const bu = isCourseUnfinished(b) ? 1 : 0;
if (au !== bu) return bu - au;
return b.lcsProcess - a.lcsProcess;
});
let q = loadQueue();
const touched = localStorage.getItem(QUEUE_TOUCHED_KEY) === "1";
if (!touched && !q.length && state.panelCourses.length) {
q = state.panelCourses.filter(isCourseUnfinished).map((c) => c.csId);
}
saveQueue(q);
return state.panelCourses;
}
async function refreshPanelCourses() {
if (!isLoggedIn()) return;
state.panelRefreshing = true;
updatePanel();
try {
await fetchPanelCourses();
renderPlanSelect();
renderCourseList();
await refreshChapterPreview();
} catch (err) {
log("\u5237\u65b0\u8bfe\u7a0b\u5931\u8d25\uff1a" + (err.message || err));
} finally {
state.panelRefreshing = false;
updatePanel();
}
}
function updateCourseProgressInPanel(csId, lcsProcess) {
const c = state.panelCourses.find((x) => x.csId === csId);
if (c && lcsProcess != null) c.lcsProcess = lcsProcess;
renderCourseList();
void refreshChapterPreview();
}
function startPreviewAutoRefresh() {
stopPreviewAutoRefresh();
state.previewRefreshTimer = setInterval(() => {
if (!state.enabled) return;
refreshChapterPreview().catch(() => {});
}, 12000);
}
function stopPreviewAutoRefresh() {
if (state.previewRefreshTimer) {
clearInterval(state.previewRefreshTimer);
state.previewRefreshTimer = null;
}
}
function courseExamLabel(c) {
if (isCourseFullyFinished(c)) return " · \u5df2\u5b8c\u6210";
if (!(c.courseTestList || []).length) return "";
if (isTruthyFlag(c.lcsExameFinished)) return " · \u8003✓";
if (isStudyFinished(c)) return " · \u5f85\u8003\u8bd5";
return " · \u542b\u8003\u8bd5";
}
function renderPlanSelect() {
const sel = document.getElementById("zjzx-plan-select");
if (!sel) return;
const plans = state.teachPlans || [];
if (!plans.length) {
sel.innerHTML = "";
sel.disabled = true;
return;
}
sel.disabled = false;
sel.innerHTML = plans
.map((p) => {
const label = p.tpName || (p.year ? `${p.year}\u5e74\u5ea6` : p.tpId);
const selected = p.tpId === state.selectedTpId ? " selected" : "";
return ``;
})
.join("");
}
function renderCourseList() {
const box = document.getElementById("zjzx-course-list");
if (!box) return;
const bg = loadBgState();
const activeCsId = bg.csId || "";
const selectedSet = new Set(loadQueue());
if (!state.panelCourses.length) {
box.innerHTML =
"\u5f53\u524d\u5e74\u5ea6\u8ba1\u5212\u6682\u65e0\u8bfe\u7a0b
\u53ef\u5207\u6362\u4e0a\u65b9\u57f9\u8bad\u8ba1\u5212\u67e5\u770b\u5176\u4ed6\u5e74\u5ea6
";
return;
}
box.innerHTML = state.panelCourses
.map((c) => {
const pct = Math.min(100, Math.max(0, c.lcsProcess || 0));
const isActive = c.csId === activeCsId;
const finished = isCourseFullyFinished(c);
const checked = selectedSet.has(c.csId) ? "checked" : "";
const border = isActive ? "#16a34a" : finished ? "#e2e8f0" : "#e2e8f0";
const bgColor = isActive ? "#f0fdf4" : finished ? "#f1f5f9" : "#f8fafc";
const titleColor = finished ? "#64748b" : "#0f172a";
return `
`;
})
.join("");
box.querySelectorAll("input[type='checkbox']").forEach((el) => {
el.addEventListener("change", () => {
const selected = [];
box.querySelectorAll("label").forEach((label) => {
const cb = label.querySelector("input[type='checkbox']");
if (cb?.checked && cb.dataset.csid) selected.push(cb.dataset.csid);
});
localStorage.setItem(QUEUE_TOUCHED_KEY, "1");
saveQueue(selected);
if (selected.length) clearPanelHint();
void refreshChapterPreview();
const bg = loadBgState();
if (bg.csId && selected.length && !selected.includes(bg.csId)) {
saveBgState({});
log("\u5f53\u524d\u8fdb\u884c\u4e2d\u7684\u8bfe\u7a0b\u5df2\u53d6\u6d88\u52fe\u9009\uff0c\u5df2\u91cd\u7f6e\u540e\u53f0\u4efb\u52a1");
}
});
});
}
async function resolveQueueCourse(csId) {
let course = state.panelCourses.find((c) => c.csId === csId);
if (course) return course;
try {
course = await fetchCourseFromPlans(csId);
} catch (_) {}
return course || null;
}
async function refreshChapterPreview() {
const selected = loadQueue();
if (!selected.length) {
state.chapterPreview = [];
state.queueVideoTotal = 0;
state.queueVideoDone = 0;
state.queueExamTotal = 0;
state.queueExamDone = 0;
renderChapterPreview();
updateQueueSummary();
return;
}
const list = [];
let examTotal = 0;
let examDone = 0;
for (const csId of selected) {
const course = await resolveQueueCourse(csId);
if (!course) continue;
if ((course.courseTestList || []).length) {
examTotal += 1;
if (isTruthyFlag(course.lcsExameFinished)) examDone += 1;
}
try {
const videos = await loadCourseVideos(csId);
list.push({
csId,
courseTitle: course.csName || "\u672a\u547d\u540d\u8bfe\u7a0b",
videos: videos.map((v) => ({
resId: v.resId,
title: v.resName || v.resId,
process: Number(v.process) || 0,
studyFinished: v.studyFinished,
active: false,
})),
});
} catch (_) {}
}
const bg = loadBgState();
const activeResId =
bg.csId && bg.videos?.length ? String(bg.videos[bg.videoIndex || 0]?.resId || "") : "";
list.forEach((c) => {
if (c.csId === bg.csId) {
c.videos.forEach((v) => {
if (String(v.resId) === activeResId) v.active = true;
});
}
});
state.chapterPreview = list;
state.queueVideoTotal = list.reduce((s, c) => s + (c.videos?.length || 0), 0);
state.queueVideoDone = list.reduce((s, c) => {
const arr = c.videos || [];
return s + arr.filter((v) => v.studyFinished === "1" || Number(v.process) >= 100).length;
}, 0);
state.queueExamTotal = examTotal;
state.queueExamDone = examDone;
renderChapterPreview();
updateQueueSummary();
}
function renderChapterPreview() {
const box = document.getElementById("zjzx-chapter-preview");
if (!box) return;
if (!state.chapterPreview.length) {
box.innerHTML = "\u8bf7\u9009\u62e9\u8bfe\u7a0b
";
return;
}
box.innerHTML = state.chapterPreview
.map((course) => {
const items = (course.videos || [])
.map((v) => {
const done = v.studyFinished === "1" || Number(v.process) >= 100;
const color = v.active ? "#16a34a" : done ? "#64748b" : "#334155";
const mark = v.active ? "▶" : done ? "✓" : "•";
const pct = Math.round(Number(v.process) || 0);
return `${mark} ${escHtml(v.title)}${pct}%
`;
})
.join("");
return `${escHtml(course.courseTitle)}
${items || "
\u65e0\u89c6\u9891
"}
`;
})
.join("");
}
function updateQueueSummary() {
const doneEl = document.getElementById("zjzx-queue-done");
const totalEl = document.getElementById("zjzx-queue-total");
const pctEl = document.getElementById("zjzx-queue-percent");
const barEl = document.getElementById("zjzx-queue-progress");
const examEl = document.getElementById("zjzx-queue-exam");
const textEl = document.getElementById("zjzx-queue-text");
const q = loadQueue();
const total = Math.max(0, Number(state.queueVideoTotal || 0));
const done = Math.max(0, Math.min(total, Number(state.queueVideoDone || 0)));
const examTotal = Math.max(0, Number(state.queueExamTotal || 0));
const examDone = Math.max(0, Math.min(examTotal, Number(state.queueExamDone || 0)));
if (doneEl) doneEl.textContent = String(done);
if (totalEl) totalEl.textContent = String(total);
const pct = total > 0 ? Math.max(0, Math.min(100, Math.round((done / total) * 100))) : 0;
if (pctEl) pctEl.textContent = `${pct}%`;
if (barEl) barEl.style.width = `${pct}%`;
if (examEl) {
examEl.textContent = examTotal > 0 ? `${examDone} / ${examTotal}` : "—";
}
if (textEl) {
if (!q.length) {
textEl.textContent = "\u672a\u9009\u62e9";
} else if (total > 0) {
const examPart = examTotal > 0 ? ` · \u8003\u8bd5 ${examDone}/${examTotal}` : "";
textEl.textContent = `\u89c6\u9891\u5df2\u5b66\u4e60 ${done}/${total}${examPart}\uff08\u5df2\u9009 ${q.length} \u95e8\uff09`;
} else {
textEl.textContent = `\u5df2\u9009 ${q.length} \u95e8\uff0c\u6b63\u5728\u7edf\u8ba1…`;
}
}
}
function updateCurrentMetaPanel() {
const courseEl = document.getElementById("zjzx-current-course");
const chapterEl = document.getElementById("zjzx-current-chapter");
const taskEl = document.getElementById("zjzx-current-task");
const bg = loadBgState();
if (courseEl) {
const courseText = bg.csName || "\u65e0";
courseEl.textContent = courseText;
courseEl.title = courseText;
}
if (chapterEl) {
let chapterText = "\u65e0";
if (bg.phase === "exam") chapterText = "\u8003\u8bd5\u4e2d";
else if (bg.videos?.length) {
const cur = bg.videos[bg.videoIndex || 0];
chapterText = cur?.resName || cur?.resId || `\u89c6\u9891 ${(bg.videoIndex || 0) + 1}/${bg.videos.length}`;
}
chapterEl.textContent = chapterText;
chapterEl.title = chapterText;
}
if (taskEl) {
if (!state.enabled) {
const taskText = state.panelHint || "\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u63d0\u4ea4";
taskEl.textContent = taskText;
taskEl.title = taskText;
} else if (bg.phase === "exam") {
taskEl.textContent = "\u6b63\u5728\u4ea4\u5377";
taskEl.title = "\u6b63\u5728\u4ea4\u5377";
} else if (bg.csId && bg.videos?.length) {
const cur = Math.min(
Number(bg.videoTotal || bg.videos.length),
Number(bg.videoDone || 0) + Number(bg.videoIndex || 0) + 1
);
const total = Number(bg.videoTotal || bg.videos.length);
const pct = Number(bg.lcsProcess ?? 0).toFixed(1);
const taskText = `\u89c6\u9891 ${cur}/${total} · \u8bfe\u7a0b ${pct}%`;
taskEl.textContent = taskText;
taskEl.title = taskText;
} else {
const taskText = state.lastAction || "\u6b63\u5728\u5206\u914d\u4e0b\u4e00\u95e8\u8bfe…";
taskEl.textContent = taskText;
taskEl.title = taskText;
}
}
}
function readPanelCollapsed() {
const v = localStorage.getItem(PANEL_COLLAPSED_KEY);
return v == null ? true : v === "1";
}
function writePanelCollapsed(collapsed) {
localStorage.setItem(PANEL_COLLAPSED_KEY, collapsed ? "1" : "0");
}
function readPanelPos() {
try {
return JSON.parse(localStorage.getItem(PANEL_POS_KEY) || "{}");
} catch (_) {
return null;
}
}
function writePanelPos(left, top) {
localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left, top }));
}
function clampPanelInViewport(panel) {
if (!panel) return;
const rect = panel.getBoundingClientRect();
const maxLeft = Math.max(0, window.innerWidth - panel.offsetWidth);
const maxTop = Math.max(0, window.innerHeight - panel.offsetHeight);
const left = Math.max(0, Math.min(maxLeft, rect.left));
const top = Math.max(0, Math.min(maxTop, rect.top));
panel.style.right = "auto";
panel.style.bottom = "auto";
panel.style.left = `${left}px`;
panel.style.top = `${top}px`;
writePanelPos(left, top);
}
function applyPanelCollapsed(panel, collapsed) {
const btnMin = panel.querySelector("#zjzx-btn-min");
const btnMax = panel.querySelector("#zjzx-btn-max");
panel.classList.toggle("zjzx-panel-min", !!collapsed);
panel.classList.toggle("zjzx-panel-max", !collapsed);
if (btnMin) btnMin.style.display = collapsed ? "none" : "";
if (btnMax) btnMax.style.display = collapsed ? "" : "none";
const footerExtra = panel.querySelector(".zjzx-footer-extra");
if (footerExtra) footerExtra.style.display = collapsed ? "none" : "";
clampPanelInViewport(panel);
}
function enablePanelDrag(panel) {
const header = panel.querySelector("#zjzx-panel-header");
if (!header) return;
let dragging = false;
let startX = 0;
let startY = 0;
let startLeft = 0;
let startTop = 0;
header.addEventListener("mousedown", (e) => {
if (e.target?.closest("#zjzx-panel-controls, .zjzx-panel-ctl")) return;
dragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = panel.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
panel.style.right = "auto";
panel.style.bottom = "auto";
e.preventDefault();
});
document.addEventListener("mousemove", (e) => {
if (!dragging) return;
const left = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, startLeft + (e.clientX - startX)));
const top = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, startTop + (e.clientY - startY)));
panel.style.left = `${left}px`;
panel.style.top = `${top}px`;
});
document.addEventListener("mouseup", () => {
if (!dragging) return;
dragging = false;
const rect = panel.getBoundingClientRect();
writePanelPos(rect.left, rect.top);
});
}
function injectPanelStyles() {
const id = "zjzx-panel-style-v4";
document.getElementById("zjzx-panel-style-v2")?.remove();
document.getElementById("zjzx-panel-style-v3")?.remove();
if (document.getElementById(id) || !document.head) return;
const st = document.createElement("style");
st.id = id;
st.textContent = `
#zjzx-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;transition:max-height .22s ease,box-shadow .22s ease;}
#zjzx-auto-panel.zjzx-panel-hidden{display:none !important;}
#zjzx-auto-panel.zjzx-panel-max{max-height:min(92vh,780px);}
#zjzx-auto-panel.zjzx-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);}
#zjzx-auto-panel.zjzx-panel-min #zjzx-panel-header{border-bottom:none;}
#zjzx-auto-panel.zjzx-panel-min #zjzx-panel-body,#zjzx-auto-panel.zjzx-panel-min #zjzx-panel-footer,#zjzx-auto-panel.zjzx-panel-min .zjzx-footer-extra{display:none !important;}
#zjzx-panel-header{flex:0 0 auto;}
#zjzx-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;}
.zjzx-footer-extra{flex:0 0 auto;}
#zjzx-panel-footer{flex:0 0 auto;}
#zjzx-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;}
#zjzx-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;}
#zjzx-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:cover;box-shadow:0 2px 9px rgba(15,23,42,.11);border:1px solid rgba(148,163,184,.45);background:#fff;flex:0 0 auto;}
#zjzx-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;display:flex;align-items:flex-start;gap:5px;flex-wrap:wrap;}
.zjzx-panel-title-text{flex:1 1 12em;min-width:0;letter-spacing:-0.01em;}
#zjzx-panel-sub{display:flex;flex-wrap:wrap;align-items:center;gap:3px 5px;margin-top:3px;line-height:1.32;}
.zjzx-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;}
.zjzx-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);}
.zjzx-sub-dot{color:#d6d3d1;font-size:10px;font-weight:400;}
.zjzx-panel-ctl .zjzx-ctl-ico{display:block;box-sizing:border-box;margin:0 auto;}
.zjzx-ctl-min{width:10px;height:2px;background:#64748b;border-radius:1px;}
.zjzx-ctl-plus{font-size:16px;line-height:1;font-weight:700;color:#64748b;}
.zjzx-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;}
#zjzx-panel-controls{display:flex;align-items:center;gap:5px;flex:0 0 auto;}
.zjzx-panel-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-size:15px;line-height:1;font-weight:900;padding:0;}
.zjzx-panel-ctl:hover{filter:brightness(1.03);}
.zjzx-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;}
.zjzx-card-status{padding:6px 8px;margin-bottom:6px;}
.zjzx-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;line-height:1.28;}
.zjzx-status-label{font-size:11px;color:#64748b;font-weight:700;}
.zjzx-status-badge{padding:2px 7px;border-radius:999px;font-weight:800;font-size:11px;background:#fff;border:1px solid #cbd5e1;color:#334155;}
.zjzx-status-main{margin-top:3px;}
.zjzx-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;}
.zjzx-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;}
.zjzx-metric-div{color:#cbd5e1;}
.zjzx-progress-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;font-size:11px;color:#334155;}
.zjzx-progress-num{font-size:22px;font-weight:900;color:#0f172a;}
.zjzx-progress-num small{font-size:13px;color:#64748b;}
.zjzx-card-status .zjzx-progress-pct{font-size:14px;font-weight:900;color:#0369a1;flex:0 0 auto;}
.zjzx-progress-pct{font-size:20px;font-weight:900;color:#0369a1;}
.zjzx-card-status .zjzx-progress-bar{height:5px;margin-top:4px;margin-bottom:0;}
.zjzx-progress-bar{height:9px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:5px;}
.zjzx-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;}
.zjzx-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;}
.zjzx-list-head-actions{display:flex;align-items:center;gap:5px;}
.zjzx-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;line-height:1.4;}
.zjzx-log-clear-btn:hover{background:#f8fafc;color:#0f172a;}
.zjzx-plan-row{padding:0 2px 5px;}
.zjzx-plan-row .zjzx-plan-select{padding:4px 8px;font-size:11px;line-height:1.25;border-radius:7px;}
.zjzx-plan-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 9px;font-size:12px;background:#fff;color:#0f172a;}
.zjzx-list-title{font-size:12px;color:#64748b;font-weight:700;}
.zjzx-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;}
.zjzx-settings-group{background:#f8fafc;border:1px solid #dbe4f0;border-radius:12px;padding:8px;margin-bottom:8px;}
.zjzx-settings-title{font-size:12px;color:#64748b;font-weight:900;margin-bottom:7px;}
.zjzx-hidden{display:none !important;}
.zjzx-btn-row-nowrap{flex-wrap:nowrap;}
.zjzx-btn-action{min-width:0 !important;flex:1 1 0 !important;}
.zjzx-btn-ico{font-size:13px;line-height:1;display:inline-flex;align-items:center;margin-right:5px;}
.zjzx-btn-label{white-space:nowrap;}
.zjzx-toggle-on{background:linear-gradient(135deg,#1d4ed8,#0ea5e9) !important;border-color:transparent !important;color:#fff !important;}
.zjzx-card-actions .zjzx-btn-row{margin-bottom:0;}
.zjzx-study-mode-block{margin-bottom:5px;}
.zjzx-study-mode-block .zjzx-study-mode-row{display:flex;align-items:center;gap:5px;margin-bottom:0;}
.zjzx-study-mode-block .zjzx-meta-label{flex:0 0 auto;font-size:10px;white-space:nowrap;line-height:1.2;}
.zjzx-study-mode-block .zjzx-plan-select{flex:1 1 auto;min-width:0;width:auto;padding:3px 22px 3px 7px;font-size:11px;line-height:1.25;border-radius:7px;}
.zjzx-study-mode-block .zjzx-study-mode-hint{margin-top:0;}
.zjzx-card-actions{padding:6px 8px;}
.zjzx-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;}
#zjzx-course-list,#zjzx-chapter-preview,#zjzx-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;}
#zjzx-settings-pane{padding:2px 0;}
.zjzx-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;}
.zjzx-chapter-title{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;}
.zjzx-chapter-item{padding:5px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;}
.zjzx-dot{font-weight:700;}
.zjzx-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;opacity:.95;}
.zjzx-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;margin-bottom:0;}
.zjzx-ann-title{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:900;color:#9a3412;margin-bottom:3px;}
.zjzx-ann-text{display:block;word-break:break-word;}
.zjzx-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-top-left-radius:11px;border-bottom-left-radius:11px;}
.zjzx-qq-row{display:flex;align-items:center;justify-content:space-between;gap:9px;margin-top:5px;}
.zjzx-qq-text{font-size:12px;color:#475569;}
.zjzx-qq-title{font-size:13px;font-weight:800;color:#0f172a;}
.zjzx-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;}
.zjzx-exam-hint{font-size:11px;color:#64748b;margin-top:5px;line-height:1.42;}
.zjzx-study-mode-hint{margin-top:5px;padding:7px 9px;border-radius:9px;border:1px solid #e2e8f0;font-size:11px;line-height:1.42;}
.zjzx-study-mode-hint-title{display:block;font-weight:800;margin-bottom:3px;}
.zjzx-study-mode-hint-body{margin:0;color:#475569;}
.zjzx-study-mode-hint-efficiency{background:linear-gradient(135deg,#eff6ff,#f0f9ff);border-color:#bfdbfe;color:#1e3a8a;}
.zjzx-study-mode-hint-efficiency .zjzx-study-mode-hint-body{color:#334155;}
.zjzx-study-mode-hint-realtime{background:linear-gradient(135deg,#fffbeb,#fef3c7);border-color:#f59e0b;color:#78350f;}
.zjzx-study-mode-hint-realtime .zjzx-study-mode-hint-body{color:#92400e;}
.zjzx-start-hint{font-size:11px;color:#b45309;background:#fffbeb;border:1px solid #fcd34d;border-radius:7px;padding:5px 7px;margin-bottom:5px;line-height:1.32;text-align:center;font-weight:600;}
.zjzx-btn-pro{flex:0 0 auto;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;border:none;box-shadow:0 3px 10px rgba(239,68,68,.22);padding:4px 8px;font-size:12px;border-radius:10px;cursor:pointer;font-weight:800;}
.zjzx-btn-pro:hover{filter:brightness(1.06);}
#zjzx-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;}
.zjzx-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;}
.zjzx-pro-title{font-size:24px;font-weight:900;color:#0f172a;}
.zjzx-pro-sub{font-size:13px;color:#64748b;margin-top:4px;line-height:1.45;}
.zjzx-pro-sec{margin-top:12px;border:1px solid #dbe4f0;border-radius:12px;padding:12px;background:#f8fafc;}
.zjzx-pro-sec h4{margin:0 0 8px;font-size:15px;color:#0f172a;}
.zjzx-pro-sec p{margin:4px 0;font-size:13px;color:#334155;line-height:1.5;}
.zjzx-pro-sec ul{margin:6px 0 0 18px;padding:0;}
.zjzx-pro-sec li{margin:4px 0;font-size:13px;color:#334155;line-height:1.45;}
.zjzx-pro-buy{display:flex;flex-direction:column;gap:10px;margin-top:8px;}
.zjzx-pro-muted{color:#64748b;font-size:12px;line-height:1.5;}
.zjzx-pro-buy-btn{display:inline-block;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;}
.zjzx-pro-buy-btn:hover{filter:brightness(1.05);}
.zjzx-pro-buy-btn:disabled{background:#94a3b8;cursor:not-allowed;box-shadow:none;opacity:.92;}
.zjzx-pro-buy-btn:disabled:hover{filter:none;}
.zjzx-pro-tip{margin-top:8px;background:#fef3c7;border:1px solid #fcd34d;border-radius:10px;padding:8px 10px;font-size:13px;color:#92400e;font-weight:700;line-height:1.45;}
.zjzx-pro-tip-info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:600;}
.zjzx-pro-actions{margin-top:14px;display:flex;justify-content:flex-end;}
.zjzx-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;}
.zjzx-tabbar{display:flex;gap:6px;margin-bottom:7px;}
.zjzx-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px 4px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;}
.zjzx-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;}
.zjzx-pane{display:none;}.zjzx-pane.active{display:block;}
.zjzx-btn-row{display:flex;gap:7px;margin-bottom:6px;flex-wrap:wrap;}
.zjzx-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;min-width:68px;}
.zjzx-btn-start{background:#16a34a;}.zjzx-btn-stop{background:#ef4444;}.zjzx-btn-refresh{background:#64748b;}
.zjzx-btn-start.zjzx-btn-off,.zjzx-btn-start:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;box-shadow:none;pointer-events:none;}
.zjzx-btn-stop.zjzx-btn-off,.zjzx-btn-stop:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;box-shadow:none;pointer-events:none;}
.zjzx-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;flex:0 0 auto;}
.zjzx-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;}
.zjzx-meta-label{color:#64748b;}.zjzx-meta-value{font-weight:700;text-align:right;}
.zjzx-card-current{padding:5px 8px;margin-bottom:6px;}
.zjzx-card-current .zjzx-meta-row{font-size:11px;margin-bottom:2px;gap:5px;line-height:1.28;align-items:flex-start;}
.zjzx-card-current .zjzx-meta-row:last-child{margin-bottom:0;}
.zjzx-card-current .zjzx-meta-label{flex:0 0 auto;font-size:10px;white-space:nowrap;}
.zjzx-card-current .zjzx-meta-value{flex:1 1 auto;min-width:0;font-size:11px;font-weight:600;text-align:right;word-break:break-all;line-height:1.28;}
.zjzx-auth-badge{padding:2px 7px;border-radius:999px;border:1px solid #cbd5e1;font-size:11px;font-weight:900;}
#zjzx-auto-status{padding:3px 9px;border-radius:999px;font-weight:900;font-size:12px;background:#fff;border:1px solid #cbd5e1;}
.zjzx-card-status #zjzx-auto-status{padding:2px 7px;font-size:11px;}
#zjzx-panel-footer{padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;}
.zjzx-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;}
`;
document.head.appendChild(st);
}
function hideStudyModeHint() {
const box = document.getElementById("zjzx-study-mode-hint");
if (box) box.style.display = "none";
}
function showStudyModeHintTemporary() {
const box = document.getElementById("zjzx-study-mode-hint");
if (!box) return;
syncStudyModeHintContent();
box.style.display = "";
if (state.studyModeHintTimer) clearTimeout(state.studyModeHintTimer);
state.studyModeHintTimer = setTimeout(() => {
state.studyModeHintTimer = null;
hideStudyModeHint();
}, 5000);
}
function syncStudyModeHintContent() {
const m = getStudyMode();
if (!m) return;
const box = document.getElementById("zjzx-study-mode-hint");
if (!box) return;
const title = box.querySelector(".zjzx-study-mode-hint-title");
const body = box.querySelector(".zjzx-study-mode-hint-body");
const rt = m === "realtime";
box.classList.toggle("zjzx-study-mode-hint-efficiency", !rt);
box.classList.toggle("zjzx-study-mode-hint-realtime", rt);
if (title) title.textContent = rt ? "1:1 \u65f6\u957f\u6162\u5237\u6a21\u5f0f" : "\u6548\u7387\u6a21\u5f0f";
if (body) {
const chunk = getRealtimeChunkSec();
body.textContent = rt
? `\u8bfb\u53d6\u5e73\u53f0\u89c4\u5219\uff08\u5f53\u524d ${formatIntervalCn(chunk)}\uff09\uff1a\u6bcf ${formatIntervalCn(chunk)} \u66f4\u65b0\u4e00\u6b21\u8fdb\u5ea6\uff0c\u5faa\u73af\u81f3\u5b66\u5b8c\uff1b\u6bd4\u6548\u7387\u6a21\u5f0f\u66f4\u6162\u3001\u66f4\u5b89\u5168\u3002`
: "\u5feb\u8bfb\u5b8c\u6210\u89c6\u9891\u8fdb\u5ea6\uff0c\u8bfe\u4e0e\u8bfe\u95f4\u9694\u7ea6 60 \u79d2\uff0c\u901f\u5ea6\u6700\u5feb\u3002";
}
}
function syncStudyModeUi(mode) {
const sel = document.getElementById("zjzx-study-mode");
const m = mode !== undefined && mode !== null ? String(mode) : getStudyMode();
if (sel) sel.value = m || "";
if (hasStudyModeSelected()) syncStudyModeHintContent();
}
function createPanel() {
const old = document.getElementById("zjzx-auto-panel") || document.getElementById("zjzx-auto-study-panel");
if (old) old.remove();
injectPanelStyles();
const panel = document.createElement("div");
panel.id = "zjzx-auto-panel";
panel.innerHTML = `
\u8fd0\u884c\u72b6\u6001
\u5df2\u505c\u6b62
0/0 \u89c6\u9891\u5df2\u5b66\u4e60
·
\u8003\u8bd5 —
0%
\u8bfe\u7a0b\u9009\u62e9\u8bfe\u7a0b\u5217\u8868
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
\u7ae0\u8282\u9884\u89c8\u5b9e\u65f6\u8fdb\u5ea6
\u8bf7\u9009\u62e9\u8bfe\u7a0b
\u8fd0\u884c\u65e5\u5fd7
\u5b9e\u65f6\u65e5\u5fd7
\u6388\u6743\u4e0e\u9009\u9879\u8bbe\u7f6e
\u7528\u6237\u7c7b\u578b\u672a\u6821\u9a8c
\u514d\u8d39\u4f53\u9a8c\uff081\u4e2a\u89c6\u9891\uff090/1
Token
\u4e91\u7aef\u670d\u52a1${DEFAULT_CLOUD_API_BASE}
\u8fd0\u884c\u6a21\u5f0f
\u4e0a\u62a5\u95f4\u9694 \u79d2
\u5b66\u65f6\u500d\u7387 \u500d
\u5b66\u5b8c\u540e\u81ea\u52a8\u8003\u8bd5\uff08\u5df2\u9ed8\u8ba4\u5f00\u542f\uff09
\u5f53\u524d\u8bfe\u7a0b\u65e0
\u5f53\u524d\u7ae0\u8282\u65e0
\u5f53\u524d\u4efb\u52a1\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u63d0\u4ea4
\u6548\u7387\u6a21\u5f0f
\u5feb\u8bfb\u5b8c\u6210\u5bf9\u5e94\u8bfe\u65f6\uff0c\u8bfe\u4e0e\u8bfe\u95f4\u9694\u7ea6 60 \u79d2\uff0c\u901f\u5ea6\u6700\u5feb\u3002
`;
document.body.appendChild(panel);
createProModal();
const savedPos = readPanelPos();
if (savedPos && savedPos.left != null && savedPos.top != null) {
panel.style.right = "auto";
panel.style.left = `${savedPos.left}px`;
panel.style.top = `${savedPos.top}px`;
}
applyPanelCollapsed(panel, readPanelCollapsed());
enablePanelDrag(panel);
const btnMin = document.getElementById("zjzx-btn-min");
const btnMax = document.getElementById("zjzx-btn-max");
if (btnMin) {
btnMin.addEventListener("click", (e) => {
e.stopPropagation();
writePanelCollapsed(true);
applyPanelCollapsed(panel, true);
});
}
if (btnMax) {
btnMax.addEventListener("click", (e) => {
e.stopPropagation();
writePanelCollapsed(false);
applyPanelCollapsed(panel, false);
});
}
panel.querySelectorAll(".zjzx-tab-btn").forEach((btn) => {
btn.addEventListener("click", () => switchPanelTab(btn.dataset.tab));
});
document.getElementById("zjzx-clear-log")?.addEventListener("click", () => {
clearRunLog();
});
const planSel = document.getElementById("zjzx-plan-select");
if (planSel) {
planSel.addEventListener("change", async () => {
saveSelectedTpId(planSel.value);
state.panelRefreshing = true;
updatePanel();
try {
await fetchPanelCourses();
renderPlanSelect();
renderCourseList();
await refreshChapterPreview();
} catch (err) {
log("\u5207\u6362\u57f9\u8bad\u8ba1\u5212\u5931\u8d25\uff1a" + (err.message || err));
} finally {
state.panelRefreshing = false;
updatePanel();
}
});
}
document.getElementById("zjzx-start").addEventListener("click", async () => {
if (state.enabled) return;
const picked = readStudyModeSelect();
if (!picked) {
state.studyMode = "";
log("\u8bf7\u5148\u9009\u62e9\u5b66\u4e60\u6a21\u5f0f");
setPanelHint("\u8bf7\u5148\u9009\u62e9\u5b66\u4e60\u6a21\u5f0f\u518d\u70b9\u5f00\u59cb");
syncStartStopButtons();
updatePanel();
return;
}
state.studyMode = picked;
if (!(await ensureCloudReady())) {
updatePanel();
return;
}
const q = loadQueue();
if (!q.length) {
log("\u8bf7\u5148\u52fe\u9009\u81f3\u5c11\u4e00\u95e8\u8bfe\u7a0b");
setPanelHint("\u8bf7\u5148\u52fe\u9009\u81f3\u5c11\u4e00\u95e8\u8bfe\u7a0b\u518d\u70b9\u5f00\u59cb");
updatePanel();
return;
}
clearPanelHint();
const bg = loadBgState();
if (bg.csId && !q.includes(bg.csId)) saveBgState({});
state.enabled = true;
localStorage.setItem(STORAGE_KEY, "1");
state.lastTick = 0;
syncStartStopButtons();
syncKeepAlive();
if (isRealtimeStudyMode()) await ensurePlatformHeartbeatSec(true);
log(
isRealtimeStudyMode()
? `\u81ea\u52a8\u770b\u8bfe\u5df2\u5f00\u542f\uff081:1\u65f6\u957f\u6162\u5237\u6a21\u5f0f·\u6bcf${formatIntervalCn(getRealtimeChunkSec())}\u66f4\u65b0\u8fdb\u5ea6\uff09`
: `\u81ea\u52a8\u770b\u8bfe\u5df2\u5f00\u542f\uff08${studyModeLabel(getStudyMode())}\uff09`
);
startPreviewAutoRefresh();
updatePanel();
tick();
});
document.getElementById("zjzx-stop").addEventListener("click", () => {
if (!state.enabled) return;
state.enabled = false;
localStorage.setItem(STORAGE_KEY, "0");
clearStudyModeSelection();
syncStartStopButtons();
syncKeepAlive();
stopPreviewAutoRefresh();
log("\u81ea\u52a8\u770b\u8bfe\u5df2\u6682\u505c");
updatePanel();
});
document.getElementById("zjzx-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("zjzx-open-pro")?.addEventListener("click", () => {
openProModal();
});
document.getElementById("zjzx-cloud-save").addEventListener("click", async () => {
const input = document.getElementById("zjzx-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 {
if (state.cloudToken) await cloudVerifyToken();
await _el(true);
log("\u4e91\u7aef\u6388\u6743\u5df2\u66f4\u65b0");
} catch (err) {
log("\u4e91\u7aef\u6821\u9a8c\u5931\u8d25\uff1a" + (err.message || err));
}
updateCloudPanelUI();
});
applyLockedPanelDefaults();
syncStudyModeUi();
hideStudyModeHint();
const studyModeSel = document.getElementById("zjzx-study-mode");
if (studyModeSel) {
studyModeSel.addEventListener("change", () => {
if (state.enabled) {
log("\u8bf7\u5148\u505c\u6b62\u540e\u518d\u5207\u6362\u5b66\u4e60\u6a21\u5f0f");
studyModeSel.value = getStudyMode() || "";
return;
}
const picked = String(studyModeSel.value || "");
if (!picked) {
clearStudyModeSelection();
syncStartStopButtons();
updatePanel();
return;
}
setStudyMode(picked);
clearPanelHint();
applyLockedPanelDefaults();
if (isRealtimeStudyMode()) void ensurePlatformHeartbeatSec(true);
showStudyModeHintTemporary();
syncStartStopButtons();
const chunk = getRealtimeChunkSec();
log(
isRealtimeStudyMode()
? `\u5df2\u9009\u62e91:1\u65f6\u957f\u6162\u5237\u6a21\u5f0f\uff08\u6bcf${formatIntervalCn(chunk)}\u66f4\u65b0\u8fdb\u5ea6\uff09`
: `\u5df2\u9009\u62e9${studyModeLabel(getStudyMode())}`
);
updatePanel();
});
}
setCloudApiBase();
state.cloudToken = String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim();
const tokenInput = document.getElementById("zjzx-cloud-token");
if (tokenInput) tokenInput.value = state.cloudToken;
updateCloudPanelUI();
updatePanel();
renderCourseList();
updateQueueSummary();
void fetchClientConfig();
void fetchPanelNotice();
}
function shouldHidePanelOnPage() {
return pageType() === "courseplay";
}
function syncPanelVisibility() {
const hide = shouldHidePanelOnPage();
const panel = document.getElementById("zjzx-auto-panel");
if (panel) panel.classList.toggle("zjzx-panel-hidden", hide);
if (hide) closeProModal();
}
function syncStartStopButtons() {
const running = !!state.enabled;
const needMode = !hasStudyModeSelected();
const startBtn = document.getElementById("zjzx-start");
const stopBtn = document.getElementById("zjzx-stop");
if (startBtn) {
const startOff = running || needMode;
startBtn.disabled = startOff;
startBtn.classList.toggle("zjzx-btn-off", startOff);
startBtn.setAttribute("aria-disabled", startOff ? "true" : "false");
}
if (stopBtn) {
stopBtn.disabled = !running;
stopBtn.classList.toggle("zjzx-btn-off", !running);
stopBtn.setAttribute("aria-disabled", !running ? "true" : "false");
}
}
function updatePanel() {
syncPanelVisibility();
syncProBuyButtonUi();
syncStartStopButtons();
const statusEl = document.getElementById("zjzx-auto-status");
if (!statusEl) return;
if (shouldHidePanelOnPage()) return;
if (state.panelRefreshing) {
statusEl.textContent = "\u5237\u65b0\u4e2d…";
} else if (!state.enabled) {
statusEl.textContent = "\u5df2\u505c\u6b62";
} else {
const bg = loadBgState();
if (bg.phase === "exam") statusEl.textContent = "\u8003\u8bd5\u4e2d";
else if (bg.csId && bg.videos?.length) {
statusEl.textContent = `\u8fd0\u884c ${(bg.videoIndex || 0) + 1}/${bg.videos.length}`;
} else statusEl.textContent = "\u8fd0\u884c\u4e2d";
}
updateCloudPanelUI();
updateCurrentMetaPanel();
updateQueueSummary();
const studyModeSel = document.getElementById("zjzx-study-mode");
if (studyModeSel) studyModeSel.disabled = !!state.enabled;
const hintEl = document.getElementById("zjzx-start-hint");
if (hintEl && !state.enabled) {
if (state.panelHint) {
hintEl.textContent = state.panelHint;
hintEl.style.display = "block";
} else if (!hasStudyModeSelected()) {
hintEl.textContent = "\u8bf7\u5148\u9009\u62e9\u5b66\u4e60\u6a21\u5f0f\u518d\u70b9\u5f00\u59cb";
hintEl.style.display = "block";
} else {
hintEl.textContent = "";
hintEl.style.display = "none";
}
} else if (hintEl) {
hintEl.textContent = "";
hintEl.style.display = "none";
}
}
async function initPanelData() {
if (!isLoggedIn()) return;
try {
await ensurePlatformHeartbeatSec(true);
} catch (_) {}
await refreshPanelCourses();
try {
await _el(false);
} catch (_) {}
}
function init() {
resetSessionOnPageLoad();
createPanel();
syncPanelVisibility();
syncKeepAlive();
initPanelData();
setInterval(tick, TICK_MS);
setInterval(updatePanel, 1000);
setInterval(() => {
if (isLoggedIn()) _el(false).catch(() => {});
}, 10 * 60 * 1000);
window.addEventListener("hashchange", () => {
state.lastTick = 0;
syncPanelVisibility();
});
window.addEventListener("popstate", () => {
syncPanelVisibility();
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();