// ==UserScript== // @name 【效率版】泰州师说|泰州教师培训系统自动学习助手 // @namespace https://card.wlxy.live/details/EC44E97F // @version 1.0 // @description 泰州师说(www.tze.cn / jspx.tze.cn)一键学习辅助:课程分类勾选、章节预览、顺序学习;免费体验3个视频章节,Pro不限;Pro激活后30天有效。 // @antifeature payment 免费体验3个视频章节,升级Pro不限 // @antifeature membership 需云端授权 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @match http://jspx.tze.cn/* // @match https://jspx.tze.cn/* // @match http://jspx.tze.cn:9900/* // @match https://www.tze.cn/* // @match http://www.tze.cn/* // @noframes // @connect jspx.tze.cn // @connect www.tze.cn // @connect oa17.ahzsksw.cn // @connect card.wlxy.live // @connect huaweicloudobs.ahjxjy.cn // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_openInTab // @grant unsafeWindow // @run-at document-idle // @tag 泰州师说 // @tag 泰州教师培训 // @tag 学习辅助 // @license All Rights Reserved // ==/UserScript== (function () { "use strict"; try { if (window.top !== window.self) return; } catch (_) { return; } const BOOT_KEY = "__TZSS_WATCH_HELPER_BOOTED__"; try { const root = (typeof unsafeWindow !== "undefined" && unsafeWindow.top) || window.top || window; if (root[BOOT_KEY]) return; root[BOOT_KEY] = "v1.0"; } catch (_) { if (window[BOOT_KEY]) return; window[BOOT_KEY] = true; } const SCRIPT_VERSION = "1.0"; const API_BASE = "http://jspx.tze.cn:9900"; const PAGE_ORIGIN = "http://jspx.tze.cn"; const TOKEN_KEY = "tzss_token_v1"; const USER_KEY = "tzss_user_v1"; const PANEL_POS_KEY = "tzss_panel_pos_v1"; const PANEL_COLLAPSED_KEY = "tzss_panel_collapsed_v1"; const CLOUD_TOKEN_KEY = "tzss_cloud_token_v1"; const CLOUD_LEASE_KEY = "tzss_cloud_lease_v1"; const COMPLETE_THRESHOLD = 95; const REPORT_TIMES = 3; const INTERVAL_MS = 3 * 1000; const COURSE_GAP_MS = 65 * 1000; const LOCK_RETRY_MAX = 8; const LOCK_RETRY_WAIT_MS = 45 * 1000; const PANEL_LOGO_URL = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const PANEL_FEATURE_SLOGAN = "\u987a\u5e8f\u5b66\u4e60 · \u5206\u7c7b\u5237\u8bfe"; const CLOUD_API_BASE = "https://oa17.ahzsksw.cn"; const PRO_BUY_URL = "https://card.wlxy.live/details/EC44E97F"; const PANEL_NOTICE = "\u514d\u8d39\u4f53\u9a8c 3 \u4e2a\u89c6\u9891\u7ae0\u8282\uff1bPro \u4e0d\u9650\u3002Pro \u6fc0\u6d3b\u540e 30 \u5929\u6709\u6548\u3002\u8d2d\u4e70\uff1a" + PRO_BUY_URL; const LOG_LIMIT = 400; const LOG_PALETTE = [ "#0369a1", "#0f766e", "#b45309", "#a21caf", "#be123c", "#1d4ed8", "#047857", "#c2410c", "#7c3aed", "#0e7490", ]; const state = { token: "", userId: "", userName: "", activities: [], selectedAids: new Set(), activeAid: null, courses: [], selectedCatIds: new Set(), sectionIdByAid: {}, studyProcessByAid: {}, sectionOptions: [], subjectOptionsByAid: {}, categoryOptionsByAid: {}, onlyIncomplete: false, logLines: [], running: false, stopFlag: false, currentTask: "", lastLockedRid: "", cloudToken: "", cloudLease: "", cloudLeaseExp: 0, cloudTier: "free", cloudProExpireAt: 0, tokenBoundBlocked: false, freeVideoLimit: 3, freeUsedVideos: 0, proBuyUrl: PRO_BUY_URL, panelNotice: PANEL_NOTICE, }; function courseKey(aid, rid) { return `${aid}:${rid}`; } function parseCourseKey(key) { const i = String(key || "").indexOf(":"); if (i < 0) return { aid: "", rid: "" }; return { aid: key.slice(0, i), rid: key.slice(i + 1) }; } function findCourse(aid, rid) { return state.courses.find( (c) => String(c.activityId) === String(aid) && String(c.resourceId) === String(rid) ); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function escHtml(s) { return String(s == null ? "" : s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function truncate(s, n) { const t = String(s || ""); return t.length > n ? t.slice(0, n) + "…" : t; } function formatTime(sec) { const s = Math.max(0, Math.floor(Number(sec) || 0)); const m = Math.floor(s / 60); const r = s % 60; return `${m}:${String(r).padStart(2, "0")}`; } function formatDurationCn(sec) { const s = Math.max(0, Math.floor(Number(sec) || 0)); const m = Math.floor(s / 60); const r = s % 60; if (m >= 60) { const h = Math.floor(m / 60); const mm = m % 60; return r ? `${h}\u5c0f\u65f6${mm}\u5206${r}\u79d2` : `${h}\u5c0f\u65f6${mm}\u5206`; } return `${m}\u5206${r}\u79d2`; } function formatEta(ms) { const s = Math.max(0, Math.ceil(ms / 1000)); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); const r = s % 60; return r ? `${m}\u5206${r}\u79d2` : `${m}\u5206`; } function pctOf(studied, total) { const t = Number(total) || 0; if (t <= 0) return 0; return Math.min(100, Math.round(((Number(studied) || 0) / t) * 100)); } function getLocalStudiedSec(aid) { let n = 0; getScopedCourses(aid).forEach((c) => { const vd = Number(c.videoDuration) || 0; const sd = Math.min(vd, Number(c.studyDuration) || 0); if (c.done || c.completeFlag || (c.progress || 0) >= COMPLETE_THRESHOLD) n += vd; else n += sd; }); return n; } function _gtp(act) { if (!act) { return { pct: 0, requireMin: 0, studiedSec: 0, requireLabel: "—", studiedLabel: "—", }; } const sp = state.studyProcessByAid[act.id] || {}; const requireMin = Number( sp.requireDuration != null ? sp.requireDuration : act.requireDuration ) || 0; const platformSec = Number( sp.studyDuration != null ? sp.studyDuration : act.studyDuration ) || 0; const localSec = getLocalStudiedSec(act.id); const studiedSec = Math.max(platformSec, localSec); const requireSec = requireMin * 60; let pct = 0; if (requireSec > 0) pct = pctOf(studiedSec, requireSec); else if (Number(sp.progress) > 0) pct = Math.min(100, Math.round(Number(sp.progress))); else pct = Math.min(100, Math.round(Number(act.progress) || 0)); return { pct, requireMin, studiedSec, requireLabel: requireMin ? `${requireMin}\u5206\u949f` : "—", studiedLabel: formatDurationCn(studiedSec), }; } function _isle(err) { const msg = String((err && err.message) || err || ""); return /\u4e0d\u53ef\u540c\u65f6|\u6b63\u5728\u770b\u8bfe|\u540c\u65f6\u770b\u591a\u4e2a/.test(msg); } async function _wc(ms, label) { const total = Math.max(0, Number(ms) || 0); const due = Date.now() + total; while (!state.stopFlag && Date.now() < due) { const left = due - Date.now(); setCurrentTask(`${label || "\u7b49\u5f85"} · ${formatEta(left)}`); await sleep(Math.min(1000, Math.max(200, left))); } return !state.stopFlag; } function openUrl(url) { const u = String(url || "").trim(); if (!u) return; try { if (typeof GM_openInTab === "function") GM_openInTab(u, { active: true }); else window.open(u, "_blank"); } catch (_) { try { window.open(u, "_blank"); } catch (e) {} } } function _gct() { try { const t = String(GM_getValue(CLOUD_TOKEN_KEY, "") || state.cloudToken || "").trim(); state.cloudToken = t; return t; } catch (_) { return String(state.cloudToken || "").trim(); } } function _sct(token) { const t = String(token || "").trim(); state.cloudToken = t; try { GM_setValue(CLOUD_TOKEN_KEY, t); } catch (_) {} } function _ipu() { return String(state.cloudTier || "").toLowerCase() === "pro" && !state.tokenBoundBlocked; } function _fpe(ts) { const n = Number(ts) || 0; if (!n) return ""; const ms = n > 1e12 ? n : n * 1000; const d = new Date(ms); if (Number.isNaN(d.getTime())) return ""; const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } function _cgm(url, method, headers, body) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: method || "GET", url, headers: headers || {}, data: body == null ? undefined : body, timeout: 45000, onload(res) { resolve({ status: res.status, text: res.responseText || "" }); }, onerror: () => reject(new Error("\u4e91\u7aef\u7f51\u7edc\u5931\u8d25")), ontimeout: () => reject(new Error("\u4e91\u7aef\u8bf7\u6c42\u8d85\u65f6")), }); }); } async function _crq(path, method, payload) { const url = `${CLOUD_API_BASE}${path.startsWith("/") ? path : "/" + path}`; const headers = { Accept: "application/json", "Content-Type": "application/json", }; const luid = String(state.userId || "").trim(); if (luid) headers["x-learning-user-id"] = luid; const tk = _gct(); if (tk) headers.Authorization = `Bearer ${tk}`; let bodyObj = payload; if (bodyObj && typeof bodyObj === "object") { bodyObj = Object.assign({}, bodyObj); if (path !== "/api/tzss/lease" && state.cloudLease && bodyObj.lease == null) { bodyObj.lease = state.cloudLease; } } const body = bodyObj == null ? null : JSON.stringify(bodyObj); const res = await _cgm(url, method || "POST", headers, body); let data = {}; try { data = res.text ? JSON.parse(res.text) : {}; } catch (_) { data = {}; } if (res.status < 200 || res.status >= 300) { const detail = data.detail; if (detail && typeof detail === "object") { const err = new Error(String(detail.message || detail.code || "cloud_error")); err.code = String(detail.code || ""); err.detail = detail; throw err; } throw new Error(String(data.detail || data.message || data.code || `http_${res.status}`)); } return data; } function _ccl() { try { GM_setValue(CLOUD_LEASE_KEY, { lease: state.cloudLease, exp: state.cloudLeaseExp, tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, proExpireAt: state.cloudProExpireAt, }); } catch (_) {} } function _rcl() { try { return GM_getValue(CLOUD_LEASE_KEY) || null; } catch (_) { return null; } } function _alp(data) { if (!data || typeof data !== "object") return; if (data.lease) state.cloudLease = String(data.lease); if (data.lease_exp != null || data.exp != null) { state.cloudLeaseExp = Number(data.lease_exp ?? data.exp) || 0; } if (data.tier) state.cloudTier = String(data.tier); if (data.free_video_limit != null || data.freeVideoLimit != null) { state.freeVideoLimit = Number(data.free_video_limit ?? data.freeVideoLimit) || 3; } if (data.free_used_videos != null || data.freeUsedVideos != null) { state.freeUsedVideos = Number(data.free_used_videos ?? data.freeUsedVideos) || 0; } if (data.pro_expires_at != null || data.proExpireAt != null) { state.cloudProExpireAt = Number(data.pro_expires_at ?? data.proExpireAt) || 0; } if (data.token_bound_blocked != null) { state.tokenBoundBlocked = !!data.token_bound_blocked; } if (data.proBuyUrl) state.proBuyUrl = String(data.proBuyUrl).trim() || PRO_BUY_URL; if (data.panelNotice) state.panelNotice = String(data.panelNotice); _ccl(); _ucp(); _upn(); } async function _cl(force) { const now = Math.floor(Date.now() / 1000); const hasToken = !!_gct(); if (hasToken && String(state.cloudTier || "").toLowerCase() !== "pro") force = true; if (!force && state.cloudLease && state.cloudLeaseExp - now > 60) return true; if (!force) { const cached = _rcl(); if (cached && cached.exp - now > 60) { const cachedTier = String(cached.tier || "").toLowerCase(); if (!(hasToken && cachedTier !== "pro")) { state.cloudLease = cached.lease || ""; state.cloudLeaseExp = Number(cached.exp) || 0; state.cloudTier = String(cached.tier || state.cloudTier || "free"); if (cached.freeVideoLimit != null) state.freeVideoLimit = Number(cached.freeVideoLimit) || 3; if (cached.freeUsedVideos != null) state.freeUsedVideos = Number(cached.freeUsedVideos) || 0; if (cached.proExpireAt != null) state.cloudProExpireAt = Number(cached.proExpireAt) || 0; _ucp(); return !!state.cloudLease; } } } try { const data = await _crq("/api/tzss/lease", "POST", { token: _gct(), learning_user_id: String(state.userId || ""), userid: String(state.userId || ""), }); _alp(data); if (data.token_bound_other_user || data.tokenBoundBlocked) { state.tokenBoundBlocked = true; state.cloudTier = "free"; _plog("Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\uff0c\u672c\u8d26\u53f7\u6309\u514d\u8d39\u4f53\u9a8c"); } return !!state.cloudLease; } catch (e) { const msg = String(e.message || e); if (/free_quota|\u514d\u8d39/.test(msg)) throw e; if (!state.cloudLease) { state.cloudTier = _gct() ? state.cloudTier : "free"; state.cloudLease = "local-dev"; state.cloudLeaseExp = now + 600; } return true; } } async function _cc(resourceId) { await _cl(!!_gct()); if (_ipu()) return { tier: "pro" }; if (state.cloudLease === "local-dev") { if (state.freeUsedVideos >= state.freeVideoLimit) { const err = new Error(`\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff08${state.freeUsedVideos}/${state.freeVideoLimit}\uff09`); err.code = "free_quota_exhausted"; throw err; } state.freeUsedVideos += 1; _ucp(); return { tier: "free", used: state.freeUsedVideos }; } try { const data = await _crq("/api/tzss/video/consume", "POST", { cs_id: String(resourceId || ""), res_id: String(resourceId || ""), }); _alp(data); if (data.free_used_videos != null) state.freeUsedVideos = Number(data.free_used_videos) || 0; if (data.used != null) state.freeUsedVideos = Number(data.used) || state.freeUsedVideos; _ucp(); return data; } catch (e) { const msg = String(e.message || e); if (/free_quota/i.test(msg) && _gct()) { await _cl(true); if (_ipu()) { return _crq("/api/tzss/video/consume", "POST", { cs_id: String(resourceId || ""), res_id: String(resourceId || ""), }); } } throw e; } } function _upn() { const el = document.getElementById("tzss-ann-text"); if (el) el.textContent = String(state.panelNotice || PANEL_NOTICE); } function _ucp() { const tierEl = document.getElementById("tzss-cloud-tier"); const quotaEl = document.getElementById("tzss-cloud-quota"); const expireEl = document.getElementById("tzss-cloud-expire"); const buyBtn = document.getElementById("tzss-buy-pro"); const settingsTier = document.getElementById("tzss-settings-tier"); const settingsExpire = document.getElementById("tzss-settings-expire"); const settingsQuota = document.getElementById("tzss-settings-quota"); const isPro = _ipu(); const expireText = _fpe(state.cloudProExpireAt); if (tierEl) { tierEl.textContent = isPro ? "Pro" : "\u514d\u8d39\u7248"; tierEl.style.color = isPro ? "#166534" : "#9a3412"; tierEl.style.background = isPro ? "#f0fdf4" : "#fff7ed"; tierEl.style.borderColor = isPro ? "#86efac" : "#fdba74"; } if (quotaEl) { quotaEl.textContent = PANEL_FEATURE_SLOGAN; quotaEl.classList.add("tzss-sub-chip-em"); } if (expireEl) { if (isPro && expireText) { expireEl.textContent = `\u5230\u671f ${expireText}`; } else if (isPro) { expireEl.textContent = "Pro \u5df2\u5f00\u901a"; } else { expireEl.textContent = `\u4f53\u9a8c ${state.freeUsedVideos}/${state.freeVideoLimit} \u7ae0\u8282`; } } if (settingsTier) settingsTier.textContent = isPro ? "Pro" : "\u514d\u8d39\u7248"; if (settingsExpire) { settingsExpire.textContent = isPro && expireText ? expireText : isPro ? "\u5df2\u5f00\u901a" : "—"; } if (settingsQuota) { settingsQuota.textContent = isPro ? "\u4e0d\u9650" : `${state.freeUsedVideos}/${state.freeVideoLimit}`; } if (buyBtn) { buyBtn.style.display = ""; buyBtn.textContent = isPro ? "\u7eed\u8d39 / \u67e5\u770b Pro" : "\u5347\u7ea7 Pro"; } const inp = document.getElementById("tzss-cloud-token-input"); if (inp && _gct() && !inp.value) inp.value = _gct(); _upn(); } async function fetchPanelNoticeText() { const res = await _cgm( `${CLOUD_API_BASE}/api/tzss/panel-notice`, "GET", { Accept: "text/plain" }, null ); if (res.status >= 200 && res.status < 300 && res.text) { return String(res.text).trim(); } return ""; } async function _lcc() { try { const cfg = await _crq("/api/tzss/client-config", "GET", null); if (cfg.proBuyUrl) state.proBuyUrl = String(cfg.proBuyUrl).trim() || PRO_BUY_URL; if (cfg.freeVideoLimit != null) state.freeVideoLimit = Number(cfg.freeVideoLimit) || 3; if (cfg.panelNotice) state.panelNotice = String(cfg.panelNotice).trim() || PANEL_NOTICE; } catch (_) {} try { const notice = await fetchPanelNoticeText(); if (notice) state.panelNotice = notice; } catch (_) {} _ucp(); _upn(); } function persistToken(token, user) { const t = String(token || "").trim(); if (!t || t === "null") return false; state.token = t; try { GM_setValue(TOKEN_KEY, t); } catch (_) {} if (user && typeof user === "object") { state.userId = String(user.id || user.userId || state.userId || ""); state.userName = String(user.realName || user.userName || state.userName || ""); try { GM_setValue(USER_KEY, { id: state.userId, realName: state.userName, }); } catch (_) {} } return true; } function clearToken() { state.token = ""; state.userId = ""; state.userName = ""; try { GM_setValue(TOKEN_KEY, ""); GM_setValue(USER_KEY, null); } catch (_) {} } function getStoredToken() { try { return String(GM_getValue(TOKEN_KEY) || "").trim(); } catch (_) { return ""; } } function readCookieToken() { try { const m = document.cookie.match(/(?:^|;\s*)token=([^;]+)/); return m ? decodeURIComponent(m[1]).trim() : ""; } catch (_) { return ""; } } function readStorageToken() { const keys = ["token", "Token", "ACCESS_TOKEN", "access_token"]; for (const store of [localStorage, sessionStorage]) { try { for (const k of keys) { const v = store.getItem(k); if (v && v !== "null" && v.length >= 8) return String(v).trim(); } for (let i = 0; i < store.length; i++) { const k = store.key(i); if (!k) continue; if (/token/i.test(k)) { const v = store.getItem(k); if (v && v !== "null" && /^[a-f0-9]{16,}$/i.test(v.trim())) return v.trim(); } } } catch (_) {} } return ""; } function getLiveToken() { return readCookieToken() || readStorageToken() || state.token || getStoredToken(); } function getToken() { const live = getLiveToken(); if (live && live !== state.token) persistToken(live); return state.token || live || ""; } function setupTokenInterceptor() { const hook = (headers) => { try { if (!headers) return; const t = headers.get?.("token") || headers.token || (typeof headers === "object" ? headers["token"] : ""); if (t && String(t) !== "null") persistToken(t); } catch (_) {} }; try { const uw = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; const rawFetch = uw.fetch; if (typeof rawFetch === "function" && !rawFetch.__tzss_hooked) { const wrapped = function (input, init) { try { const h = init && init.headers; if (h) { if (typeof Headers !== "undefined" && h instanceof Headers) hook(h); else if (typeof h === "object") { const t = h.token || h.Token; if (t) persistToken(t); } } } catch (_) {} return rawFetch.apply(this, arguments); }; wrapped.__tzss_hooked = true; uw.fetch = wrapped; } } catch (_) {} try { const XHR = XMLHttpRequest.prototype; const open = XHR.open; const setRequestHeader = XHR.setRequestHeader; XHR.open = function () { this.__tzss_headers = {}; return open.apply(this, arguments); }; XHR.setRequestHeader = function (k, v) { try { if (String(k).toLowerCase() === "token" && v && v !== "null") { persistToken(v); } } catch (_) {} return setRequestHeader.apply(this, arguments); }; } catch (_) {} } function log(text, opts) { const line = String(text || ""); const now = new Date(); const ts = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`; const row = { ts, text: line, color: (opts && opts.color) || "#334155", done: !!(opts && opts.done), info: !!(opts && opts.info), }; state.logLines.unshift(row); if (state.logLines.length > LOG_LIMIT) state.logLines.length = LOG_LIMIT; const box = document.getElementById("tzss-run-log"); if (box) { const cls = row.done ? "tzss-log-done" : row.info ? "tzss-log-info" : "tzss-log-progress"; const el = document.createElement("div"); el.className = `tzss-log-row ${cls}`; el.style.setProperty("--log-c", row.color); el.innerHTML = `${escHtml(ts)} ${escHtml(line)}`; if (box.querySelector(".tzss-empty-state")) box.innerHTML = ""; box.insertBefore(el, box.firstChild); } try { console.log("[\u6cf0\u5dde\u5e08\u8bf4]", line); } catch (_) {} } function _plog(text, courseName) { let color = LOG_PALETTE[0]; if (courseName) { let h = 0; const s = String(courseName); for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; color = LOG_PALETTE[h % LOG_PALETTE.length]; } log(text, { color }); } function setCurrentTask(msg) { state.currentTask = String(msg || ""); const el = document.getElementById("tzss-current-task"); if (el) { el.textContent = state.currentTask || "\u5f85\u547d"; el.title = state.currentTask || ""; } } function syncRunButtons() { const startBtn = document.getElementById("tzss-start"); const stopBtn = document.getElementById("tzss-stop"); if (startBtn) { startBtn.disabled = !!state.running; startBtn.classList.toggle("tzss-btn-off", !!state.running); } if (stopBtn) { stopBtn.disabled = !state.running; stopBtn.classList.toggle("tzss-btn-off", !state.running); } } function setFooter(text) { const el = document.getElementById("tzss-footer-text"); if (el) el.textContent = text; } function apiRequest({ method = "GET", path, data, query }) { return new Promise((resolve, reject) => { const token = getToken(); if (!token) { reject(new Error("\u672a\u83b7\u53d6\u5230 token\uff0c\u8bf7\u5148\u6253\u5f00 jspx.tze.cn \u5e76\u767b\u5f55")); return; } const isGet = String(method || "GET").toUpperCase() === "GET"; let url = path.startsWith("http") ? path : API_BASE + path; const q = query || (isGet ? data : null); if (q && typeof q === "object") { const qs = Object.keys(q) .filter((k) => q[k] != null && q[k] !== "") .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(q[k])}`) .join("&"); if (qs) url += (url.includes("?") ? "&" : "?") + qs; } GM_xmlhttpRequest({ method: isGet ? "GET" : "POST", url, headers: { Accept: "application/json, text/plain, */*", ...(isGet ? {} : { "Content-Type": "application/json;charset=UTF-8" }), token: token, Origin: PAGE_ORIGIN, Referer: PAGE_ORIGIN + "/", }, data: isGet ? undefined : data != null ? JSON.stringify(data) : undefined, timeout: 60000, onload(res) { if (res.status < 200 || res.status >= 300) { reject(new Error(`HTTP ${res.status}`)); return; } try { const json = JSON.parse(res.responseText); if (Number(json.code) === 200) resolve(json); else if (Number(json.code) === 401) { clearToken(); reject(new Error("token \u5df2\u5931\u6548\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55\u5e73\u53f0")); } else reject(new Error(json.message || "\u8bf7\u6c42\u5931\u8d25")); } catch (e) { reject(new Error("\u89e3\u6790\u54cd\u5e94\u5931\u8d25")); } }, onerror: () => reject(new Error("\u7f51\u7edc\u8bf7\u6c42\u5931\u8d25")), ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6")), }); }); } async function _fga() { const res = await apiRequest({ path: "/myactivity/myActivity/listGoing", query: { status: 0, pageNum: 1, pageSize: 50 }, }); return Array.isArray(res.data?.list) ? res.data.list : []; } async function fetchUserSections(activityId) { const res = await apiRequest({ path: "/myactivity/userActivitySection/getUserSections", query: { activityId }, }); const d = res.data || {}; return { sectionId: String(d.sectionId || ""), userId: String(d.userId || ""), }; } async function fetchStudyProcess(activityId) { const res = await apiRequest({ path: "/myactivity/myActivity/detail/studyProcess", query: { activityId }, }); return res.data?.studyInfoResult || null; } async function fetchCoursePage({ activityId, sectionId, completeFlag, pageNum, pageSize }) { const res = await apiRequest({ path: "/myactivity/myCourse/list", query: { activityId, completeFlag: completeFlag == null ? "" : completeFlag, pageNum: pageNum || 1, pageSize: pageSize || 50, sectionId: sectionId || "", subjectId: "", courseCategoryId: "", }, }); return res.data || { list: [], totalPage: 1, total: 0 }; } async function fetchSectionDict() { const res = await apiRequest({ path: "/myactivity/dict/list", query: { category: 1, includeAll: true, createTime: "", delFlag: "", dictName: "", id: "", pid: "", refId: "", updateTime: "", }, }); const list = Array.isArray(res.data) ? res.data : []; return list .map((d) => ({ id: d.id == null || d.id === "" ? "" : String(d.id), name: String(d.dictName || "\u5168\u90e8"), })) .filter((d) => d.name); } async function fetchSubjectOptions(activityId) { const res = await apiRequest({ path: "/myactivity/myCourse/getCourseSubjectList", query: { includeAll: true, activityId }, }); const list = Array.isArray(res.data) ? res.data : []; return list .map((d) => ({ id: d.id == null || d.id === "" ? "" : String(d.id), name: String(d.dictName || "\u5168\u90e8"), })) .filter((d) => d.name); } async function fetchCategoryTree(activityId) { const res = await apiRequest({ path: "/train/trainCourseCategory/tree", query: { activeId: activityId }, }); const list = Array.isArray(res.data) ? res.data : []; return list.map((d) => ({ id: String(d.id || ""), name: String(d.name || ""), parentId: String(d.parentId || "0"), })); } async function _fac(activityId, sectionId) { const out = []; const seen = new Set(); for (const flag of [0, 1, ""]) { let page = 1; let totalPage = 1; do { const data = await fetchCoursePage({ activityId, sectionId, completeFlag: flag, pageNum: page, pageSize: 50, }); totalPage = Number(data.totalPage) || 1; const list = Array.isArray(data.list) ? data.list : []; for (const row of list) { const rid = String(row.resourceId || ""); if (!rid || seen.has(rid)) continue; seen.add(rid); out.push(mapCourse(row, activityId)); } page += 1; } while (page <= totalPage && page <= 40); if (flag === "" && out.length) break; } return out; } function mapCourse(row, activityId) { const videoDuration = Number(row.videoDuration) || 0; const studyDuration = Number(row.studyDuration) || 0; const completeFlag = Number(row.completeFlag) === 1; let progress = Number(row.progress); if (!Number.isFinite(progress) || progress < 0) { progress = completeFlag ? 100 : pctOf(studyDuration, videoDuration); } if (completeFlag) progress = 100; const sectionNames = String(row.sectionNames || "") .split(/[,\uff0c]/) .map((s) => s.trim()) .filter(Boolean); const subjectNames = String(row.subjectNames || "") .split(/[,\uff0c]/) .map((s) => s.trim()) .filter(Boolean); return { activityId: String(row.activityId || activityId), resourceId: String(row.resourceId || ""), name: String(row.resourceTitle || row.fileName || "\u672a\u547d\u540d\u8bfe\u7a0b"), categoryName: String(row.categoryName || ""), courseCategoryId: String(row.courseCategoryId || ""), sectionNames, subjectNames, videoDuration, studyDuration, completeFlag, progress: Math.min(100, Math.max(0, progress)), done: completeFlag || progress >= COMPLETE_THRESHOLD, }; } async function _usl(activityId, resourceId, studyDuration) { const res = await apiRequest({ method: "POST", path: "/myactivity/studyLog/_usl", data: { activityId: String(activityId), resourceId: String(resourceId), studyDuration: Math.floor(Number(studyDuration) || 0), }, }); return res.data || {}; } async function _usr(activityId, resourceId, studyDuration) { let lastErr = null; for (let attempt = 1; attempt <= LOCK_RETRY_MAX; attempt++) { if (state.stopFlag) throw new Error("\u5df2\u505c\u6b62"); try { const data = await _usl(activityId, resourceId, studyDuration); state.lastLockedRid = String(resourceId); return data; } catch (e) { lastErr = e; if (!_isle(e)) throw e; const waitMs = LOCK_RETRY_WAIT_MS; _plog("\u5df2\u5b66\u5b8c\u4e00\u4e2a\u89c6\u9891\u9700\u8981\u4f11\u606f1\u5206\u949f"); const ok = await _wc(waitMs, "\u4f11\u606f\u4e2d"); if (!ok) throw new Error("\u5df2\u505c\u6b62"); } } throw lastErr || new Error("\u5355\u8bfe\u9501\u91cd\u8bd5\u6b21\u6570\u7528\u5c3d"); } async function ensureSession() { const token = getToken(); if (!token) throw new Error("\u8bf7\u5148\u767b\u5f55 jspx.tze.cn\uff08\u4ece www.tze.cn \u8fdb\u5165\u57f9\u8bad\uff09"); if (!state.userId) { try { const u = GM_getValue(USER_KEY); if (u?.id) { state.userId = String(u.id); state.userName = String(u.realName || ""); } } catch (_) {} } } async function _la({ quiet } = {}) { await ensureSession(); const list = await _fga(); state.activities = list.map((a) => ({ id: String(a.id), name: String(a.activityName || a.id), classHours: a.classHours, requireDuration: Number(a.requireDuration) || 0, studyDuration: Number(a.studyDuration) || 0, progress: 0, finishTime: a.finishTime || "", })); try { state.sectionOptions = await fetchSectionDict(); } catch (e) { state.sectionOptions = [ { id: "", name: "\u5168\u90e8" }, { id: "1529641001463316480", name: "\u5c0f\u5b66" }, { id: "1529641058803646464", name: "\u521d\u4e2d" }, { id: "1529641122968109056", name: "\u9ad8\u4e2d" }, { id: "1529641171659784192", name: "\u5e7c\u513f\u56ed" }, { id: "1529641267826786304", name: "\u4e2d\u804c" }, ]; if (!quiet) log(`\u5b66\u6bb5\u5b57\u5178\u52a0\u8f7d\u5931\u8d25\uff0c\u5df2\u7528\u9ed8\u8ba4\uff1a${e.message || e}`, { info: true }); } for (const a of state.activities) { try { const sp = await fetchStudyProcess(a.id); if (sp) { state.studyProcessByAid[a.id] = sp; a.requireDuration = Number(sp.requireDuration) || a.requireDuration; a.studyDuration = Number(sp.studyDuration) || 0; a.progress = _gtp(a).pct; } } catch (_) {} try { const sec = await fetchUserSections(a.id); if (sec.sectionId) state.sectionIdByAid[a.id] = sec.sectionId; if (sec.userId && !state.userId) { persistToken(getToken(), { id: sec.userId, realName: state.userName }); } else if (sec.userId) { state.userId = sec.userId; try { GM_setValue(USER_KEY, { id: state.userId, realName: state.userName }); } catch (_) {} } } catch (_) {} try { state.subjectOptionsByAid[a.id] = await fetchSubjectOptions(a.id); } catch (_) { state.subjectOptionsByAid[a.id] = [{ id: "", name: "\u5168\u90e8" }]; } try { state.categoryOptionsByAid[a.id] = await fetchCategoryTree(a.id); } catch (_) { state.categoryOptionsByAid[a.id] = []; } } if (!state.selectedAids.size && state.activities.length) { state.selectedAids.add(state.activities[0].id); state.activeAid = state.activities[0].id; } if (!quiet) log(`\u5df2\u52a0\u8f7d\u8fdb\u884c\u4e2d\u57f9\u8bad ${state.activities.length} \u4e2a`, { info: true }); await _lc({ quiet: true }); updateAuthUi(); updateSummary(); } async function _lc({ quiet, keepSelection } = {}) { const prev = keepSelection ? new Set(state.selectedCatIds) : null; const aids = [...state.selectedAids]; const all = []; for (const aid of aids) { const sectionId = state.sectionIdByAid[aid] || ""; try { const list = await _fac(aid, sectionId); all.push(...list); } catch (e) { log(`\u52a0\u8f7d\u8bfe\u7a0b\u5931\u8d25\u300c${aid}\u300d\uff1a${e.message || e}`); } if (!state.subjectOptionsByAid[aid]) { try { state.subjectOptionsByAid[aid] = await fetchSubjectOptions(aid); } catch (_) { state.subjectOptionsByAid[aid] = [{ id: "", name: "\u5168\u90e8" }]; } } if (!state.categoryOptionsByAid[aid]) { try { state.categoryOptionsByAid[aid] = await fetchCategoryTree(aid); } catch (_) { state.categoryOptionsByAid[aid] = []; } } } state.courses = all; if (prev) { const valid = new Set(_gcg(state.activeAid).map((g) => g.id)); state.selectedCatIds = new Set([...prev].filter((id) => valid.has(id))); } else { state.selectedCatIds = new Set(); } if (!state.activeAid || !state.selectedAids.has(state.activeAid)) { state.activeAid = aids[0] || null; } if (!quiet) { const sec = sectionNameById(state.sectionIdByAid[state.activeAid] || "") || "\u5168\u90e8"; log(`\u5df2\u6309\u7528\u6237\u5b66\u6bb5\u300c${sec}\u300d\u52a0\u8f7d · \u5206\u7c7b ${_gcg().length} · \u5b50\u7ae0\u8282 ${state.courses.length}`, { info: true, }); } _rcb(); _rcl2(); _rcp(); updateSummary(); } function sectionNameById(id) { const hit = (state.sectionOptions || []).find((s) => String(s.id) === String(id)); return hit ? hit.name : ""; } function getActiveActivity() { return state.activities.find((a) => String(a.id) === String(state.activeAid)) || null; } function getUserSectionName(aid) { const sid = state.sectionIdByAid[aid || state.activeAid] || ""; return sectionNameById(sid) || (sid ? sid : "\u5168\u90e8"); } function getUserSubjectLabel(aid) { const list = state.subjectOptionsByAid[aid || state.activeAid] || []; const named = list.filter((s) => s.id && s.name && s.name !== "\u5168\u90e8"); if (!named.length) return "\u5168\u90e8"; return named.map((s) => s.name).join("\u3001"); } function getScopedCourses(scopeAid) { const aid = scopeAid != null ? scopeAid : state.activeAid; return state.courses.filter((c) => String(c.activityId) === String(aid)); } function _gcg(scopeAid) { const aid = scopeAid != null ? scopeAid : state.activeAid; const list = getScopedCourses(aid); const tree = state.categoryOptionsByAid[aid] || []; const byId = new Map(); tree.forEach((cat, idx) => { byId.set(String(cat.id), { id: String(cat.id), name: cat.name, order: idx, courses: [], }); }); const orphan = { id: "_other", name: "\u5176\u4ed6\u8bfe\u7a0b", order: 9999, courses: [] }; list.forEach((c) => { const cid = String(c.courseCategoryId || ""); const g = byId.get(cid); if (g) g.courses.push(c); else orphan.courses.push(c); }); const groups = [...byId.values()]; if (orphan.courses.length) groups.push(orphan); groups.sort((a, b) => a.order - b.order); return groups; } function _gsc() { const groups = _gcg(); const selected = groups.filter((g) => state.selectedCatIds.has(g.id)); let courses = []; selected.forEach((g) => { courses = courses.concat(g.courses); }); if (state.onlyIncomplete) { courses = courses.filter((c) => !c.done && (c.progress || 0) < COMPLETE_THRESHOLD); } return courses; } async function _soc(course) { if (course.done || course.completeFlag) return true; const full = Math.max(1, Number(course.videoDuration) || 0); let cur = Math.min(full, Math.max(0, Number(course.studyDuration) || 0)); if (cur >= full) { course.done = true; course.progress = 100; course.completeFlag = true; return true; } if ( state.lastLockedRid && String(state.lastLockedRid) !== String(course.resourceId) ) { _plog("\u5df2\u5b66\u5b8c\u4e00\u4e2a\u89c6\u9891\u9700\u8981\u4f11\u606f1\u5206\u949f"); const ok = await _wc(COURSE_GAP_MS, "\u4f11\u606f\u4e2d"); if (!ok) return false; } await _cc(course.resourceId); const times = REPORT_TIMES; const interval = INTERVAL_MS; const remain = full - cur; const chunk = Math.max(1, Math.ceil(remain / times)); _plog(`\u5f00\u59cb\u5b66\u4e60\u300c${truncate(course.name, 22)}\u300d`, course.name); let reportIdx = 0; while (!state.stopFlag && cur < full) { reportIdx += 1; const next = Math.min(full, cur + chunk); setCurrentTask( `${truncate(course.name, 12)} · ${reportIdx}/${times} · ${formatTime(next)}/${formatTime(full)}` ); const data = await _usr(course.activityId, course.resourceId, next); cur = Number(data.studyDuration != null ? data.studyDuration : next) || next; course.studyDuration = cur; course.progress = pctOf(cur, full); const doneFlag = Number(data.completeFlag) === 1 || cur >= full; course.completeFlag = doneFlag; course.done = doneFlag; if (doneFlag) course.progress = 100; _rcl2(); _rcp(); updateSummary(); _rcb(); if (doneFlag) { _plog(`\u300c${truncate(course.name, 22)}\u300d\u5b66\u4e60\u5b8c\u6210 100%`, course.name); return true; } const due = Date.now() + interval; while (!state.stopFlag && Date.now() < due) { const left = due - Date.now(); setCurrentTask(`${truncate(course.name, 12)} · \u4e0b\u6b21 ${formatEta(left)}`); await sleep(Math.min(1000, Math.max(200, left))); } } return !state.stopFlag && (course.done || course.progress >= COMPLETE_THRESHOLD); } async function _rs() { const list = _gsc().filter( (c) => !c.done && (c.progress || 0) < COMPLETE_THRESHOLD ); if (!list.length) { _plog("\u6ca1\u6709\u53ef\u5b66\u4e60\u7684\u5b50\u7ae0\u8282\uff08\u8bf7\u5148\u52fe\u9009\u5206\u7c7b\uff09"); return; } await _cl(false); _plog(`\u5f00\u59cb\u5b66\u4e60 · \u5171 ${list.length} \u95e8`); let okN = 0; let failN = 0; for (let i = 0; i < list.length; i++) { const c = list[i]; if (state.stopFlag) break; try { const ok = await _soc(c); if (ok) okN += 1; else if (!state.stopFlag) failN += 1; } catch (e) { failN += 1; const msg = String(e.message || e); if (/free_quota|\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c/i.test(msg) || e.code === "free_quota_exhausted") { _pup(msg); break; } if (_isle(e)) { _plog("\u5df2\u5b66\u5b8c\u4e00\u4e2a\u89c6\u9891\u9700\u8981\u4f11\u606f1\u5206\u949f"); const waited = await _wc(COURSE_GAP_MS, "\u4f11\u606f\u4e2d"); if (!waited) break; i -= 1; continue; } _plog(`\u300c${truncate(c.name, 18)}\u300d\u4e2d\u65ad`); } } if (!state.stopFlag) { _plog(`\u672c\u8f6e\u7ed3\u675f · \u5b8c\u6210 ${okN} · \u5171 ${list.length}`); } } async function _ss() { if (state.running) return; if (!state.activeAid) { _plog("\u6682\u65e0\u57f9\u8bad\u9879\u76ee"); return; } if (!state.selectedCatIds.size) { _plog("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b\u5206\u7c7b"); return; } state.running = true; state.stopFlag = false; syncRunButtons(); setCurrentTask("\u5b66\u4e60\u4e2d…"); showLogTab(); try { await ensureSession(); await _rs(); if (!state.stopFlag) { _plog("\u672c\u8f6e\u4efb\u52a1\u7ed3\u675f\uff08\u672a\u5b8c\u6210\u7684\u53ef\u7a0d\u540e\u518d\u70b9\u5f00\u59cb\u7eed\u5b66\uff09"); } } catch (e) { _plog(`\u770b\u8bfe\u4e2d\u65ad\uff1a${e.message || e}`); } finally { state.running = false; state.stopFlag = false; syncRunButtons(); setCurrentTask("\u5f85\u547d"); try { await _lc({ quiet: true, keepSelection: true }); if (state.activeAid) { try { const sp = await fetchStudyProcess(state.activeAid); if (sp) { state.studyProcessByAid[state.activeAid] = sp; const act = getActiveActivity(); if (act) { act.requireDuration = Number(sp.requireDuration) || act.requireDuration; act.studyDuration = Number(sp.studyDuration) || 0; act.progress = _gtp(act).pct; } _rcb(); } } catch (_) {} } } catch (_) {} } } function _st() { if (!state.running) return; state.stopFlag = true; setCurrentTask("\u6b63\u5728\u505c\u6b62…"); } function showLogTab() { const panel = document.getElementById("tzss-auto-panel"); if (!panel) return; panel.querySelectorAll(".tzss-tab-btn").forEach((b) => b.classList.remove("active")); panel.querySelectorAll(".tzss-pane").forEach((p) => p.classList.remove("active")); const btn = panel.querySelector('.tzss-tab-btn[data-tab="log"]'); const pane = panel.querySelector('.tzss-pane[data-pane="log"]'); if (btn) btn.classList.add("active"); if (pane) pane.classList.add("active"); } function injectStyles() { let st = document.getElementById("tzss-panel-style"); if (!st) { st = document.createElement("style"); st.id = "tzss-panel-style"; document.head.appendChild(st); } st.textContent = ` #tzss-auto-panel{position:fixed;right:20px;top:80px;z-index:999999;width:380px;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;} #tzss-auto-panel.tzss-panel-max{max-height:min(92vh,760px);} #tzss-auto-panel.tzss-panel-min #tzss-panel-body,#tzss-auto-panel.tzss-panel-min #tzss-panel-footer,#tzss-auto-panel.tzss-panel-min .tzss-footer-extra{display:none !important;} #tzss-auto-panel.tzss-panel-min #tzss-panel-header{border-bottom:none;} #tzss-panel-header{flex:0 0 auto;padding:7px 10px;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;} #tzss-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #tzss-panel-logo{width:30px;height:30px;border-radius:9px;display:block;object-fit:cover;background:#fff;border:1px solid rgba(148,163,184,.45);box-shadow:0 2px 9px rgba(15,23,42,.11);flex:0 0 auto;} #tzss-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;} .tzss-panel-title-text{flex:1 1 12em;min-width:0;} .tzss-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;} #tzss-panel-sub{display:flex;flex-wrap:wrap;gap:3px 5px;margin-top:3px;} .tzss-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;} .tzss-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);} .tzss-sub-chip-mute{color:#57534e;background:rgba(255,255,255,.55);border-color:rgba(120,113,108,.22);} #tzss-panel-controls{display:flex;gap:5px;} .tzss-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-weight:900;} #tzss-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:6px;} .tzss-card{background:#fff;border:1px solid #d9e2ee;border-radius:10px;padding:5px 7px;margin-bottom:6px;} .tzss-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;} .tzss-status-label{font-size:10px;color:#64748b;font-weight:700;} .tzss-status-badge{padding:1px 6px;border-radius:999px;font-weight:800;font-size:10px;background:#fff;border:1px solid #cbd5e1;color:#334155;} .tzss-status-main{margin-top:2px;} .tzss-status-metrics{display:flex;align-items:center;gap:5px;font-size:11px;color:#475569;min-width:0;flex:1;} .tzss-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .tzss-progress-pct{font-size:13px;font-weight:900;color:#0369a1;flex:0 0 auto;} .tzss-progress-bar{height:4px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:3px;} .tzss-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;} .tzss-tabbar{display:flex;gap:6px;margin-bottom:7px;} .tzss-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;} .tzss-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .tzss-pane{display:none;}.tzss-pane.active{display:block;} .tzss-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;gap:6px;} .tzss-list-head-actions{display:flex;align-items:center;gap:5px;flex:0 0 auto;} .tzss-mini-btn{border:1px solid #cbd5e1;background:#fff;color:#334155;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:700;cursor:pointer;line-height:1.4;white-space:nowrap;} .tzss-mini-btn:hover{background:#f8fafc;} .tzss-mini-btn.on{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;} .tzss-list-title{font-size:12px;color:#64748b;font-weight:700;} .tzss-list-tag{font-size:11px;color:#1e40af;background:#dbeafe;border:1px solid #93c5fd;border-radius:999px;padding:2px 7px;} #tzss-course-list,#tzss-chapter-preview,#tzss-run-log{max-height:210px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:10px;padding:4px;} .tzss-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;} #tzss-project-card{margin-bottom:6px;} .tzss-hero{position:relative;padding:10px 11px 9px;border-radius:12px;background:radial-gradient(100px 70px at 100% 0%,rgba(255,255,255,.5),transparent 70%),linear-gradient(135deg,#1d4ed8 0%,#2563eb 42%,#0ea5e9 100%);color:#fff;box-shadow:0 8px 20px rgba(37,99,235,.18);overflow:hidden;} .tzss-hero::before{content:"";position:absolute;left:-20px;bottom:-30px;width:110px;height:110px;border-radius:50%;background:rgba(255,255,255,.12);pointer-events:none;} .tzss-hero-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;position:relative;z-index:1;} .tzss-hero-badge{font-size:10px;font-weight:900;color:#1e3a8a;background:rgba(255,255,255,.92);padding:3px 9px;border-radius:999px;} .tzss-hero-pct{font-size:16px;font-weight:900;letter-spacing:-.02em;} .tzss-hero-title{font-size:13px;font-weight:900;line-height:1.35;position:relative;z-index:1;margin-top:2px;} .tzss-hero-meta{font-size:10px;font-weight:700;opacity:.88;margin-top:5px;position:relative;z-index:1;} .tzss-hero-bar{height:4px;border-radius:999px;background:rgba(255,255,255,.22);overflow:hidden;margin-top:7px;position:relative;z-index:1;} .tzss-hero-bar>i{display:block;height:100%;background:#bbf7d0;border-radius:999px;} .tzss-cat-row{display:flex;gap:7px;align-items:center;background:#fff;border:1px solid #e8eef6;border-radius:8px;padding:5px 7px;margin-bottom:4px;cursor:pointer;transition:border-color .12s,background .12s;} .tzss-cat-row:hover{border-color:#bfdbfe;background:#f8fbff;} .tzss-cat-row.active{border-color:#3b82f6;background:#eff6ff;} .tzss-cat-row.empty{opacity:.5;cursor:not-allowed;} .tzss-cat-row.empty:hover{border-color:#e8eef6;background:#fff;} .tzss-cat-row input{margin:0;flex:0 0 auto;transform:scale(.92);} .tzss-cat-main{flex:1;min-width:0;} .tzss-cat-top{display:flex;align-items:baseline;justify-content:space-between;gap:8px;} .tzss-cat-name{display:block;font-size:11px;font-weight:800;color:#0f172a;line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .tzss-cat-sub{flex:0 0 auto;font-size:10px;color:#64748b;font-weight:700;white-space:nowrap;} .tzss-bar{display:block;height:3px;background:#e2e8f0;border-radius:999px;margin-top:4px;overflow:hidden;} .tzss-bar>i{display:block;height:100%;background:linear-gradient(90deg,#3b82f6,#0ea5e9);border-radius:999px;} .tzss-bar>i.full{background:linear-gradient(90deg,#16a34a,#22c55e);} .tzss-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;overflow:hidden;} .tzss-chapter-title{padding:7px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:800;display:flex;justify-content:space-between;gap:8px;} .tzss-chapter-title .n{color:#64748b;font-weight:700;font-size:11px;} .tzss-chapter-item{padding:6px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;color:#334155;border-bottom:1px dashed #eef2f7;} .tzss-chapter-item:last-child{border-bottom:none;} .tzss-chapter-item.done{color:#64748b;} .tzss-chapter-item.learning{color:#0369a1;font-weight:600;} .tzss-dot{display:inline-block;width:1em;font-weight:900;} .tzss-dim{color:#94a3b8;font-size:11px;margin-left:4px;} .tzss-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;align-items:center;} .tzss-meta-label{color:#64748b;} .tzss-meta-value{font-weight:700;text-align:right;} .tzss-btn-row{display:flex;gap:7px;margin-top:6px;} .tzss-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;} .tzss-btn-primary{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);} .tzss-btn-start{background:#16a34a;} .tzss-btn-stop{background:#ef4444;} .tzss-btn-off,.tzss-btn:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;box-shadow:none;} .tzss-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;} .tzss-task-row{font-size:11px;color:#475569;margin-top:6px;line-height:1.35;} .tzss-task-row em{font-style:normal;font-weight:800;color:#0f172a;} .tzss-input{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 8px;font-size:12px;box-sizing:border-box;} .tzss-footer-extra{flex:0 0 auto;padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;} .tzss-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:7px 8px 7px 10px;} .tzss-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-radius:11px 0 0 11px;} #tzss-panel-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;} .tzss-log-row{padding:4px 7px 4px 8px;margin-bottom:2px;border-bottom:1px dashed #e2e8f0;border-left:3px solid #94a3b8;border-radius:0 7px 7px 0;font-size:12px;line-height:1.42;background:#fff;} .tzss-log-time{color:#94a3b8;font-weight:500;margin-right:2px;} .tzss-log-done{background:rgba(16,185,129,.1);} .tzss-log-info{background:#f8fafc;color:#64748b!important;border-left-color:#94a3b8!important;} .tzss-log-progress{background:color-mix(in srgb, var(--log-c) 7%, #fff);border-left-color:var(--log-c);} #tzss-dialog-mask,#tzss-token-modal{position:fixed;inset:0;background:rgba(15,23,42,.42);z-index:1000001;display:none;align-items:center;justify-content:center;padding:20px;} .tzss-dialog{width:min(380px,92vw);background:#f4f7fb;border:1px solid #e5dbc6;border-radius:16px;box-shadow:0 18px 46px rgba(15,23,42,.28);overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;} .tzss-dialog-head{padding:10px 12px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;align-items:center;gap:9px;} .tzss-dialog-logo{width:28px;height:28px;border-radius:8px;object-fit:cover;background:#fff;border:1px solid rgba(148,163,184,.45);flex:0 0 auto;} .tzss-dialog-title{font-size:14px;font-weight:900;color:#9a3412;line-height:1.3;flex:1;min-width:0;} .tzss-dialog-body{padding:14px 14px 8px;background:#fff;} .tzss-dialog-msg{font-size:13px;color:#334155;line-height:1.55;font-weight:600;word-break:break-word;white-space:pre-line;} .tzss-dialog-foot{padding:10px 14px 14px;background:#fff;display:flex;gap:8px;} .tzss-dialog-foot .tzss-btn{flex:1;padding:8px 10px;border-radius:10px;font-size:13px;} .tzss-token-card{width:min(420px,92vw);background:#f4f7fb;border:1px solid #e5dbc6;border-radius:16px;box-shadow:0 18px 46px rgba(15,23,42,.28);overflow:hidden;} .tzss-token-card .tzss-dialog-body{padding:12px 14px;} `; } function friendlyCloudMessage(raw) { const msg = String(raw || "").trim(); if (!msg) return "\u64cd\u4f5c\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"; if (/free_quota_exhausted|free_quota|\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c/i.test(msg)) { return `\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff08${state.freeUsedVideos}/${state.freeVideoLimit} \u7ae0\u8282\uff09\uff0c\u5347\u7ea7 Pro \u540e\u4e0d\u9650\u7ae0\u8282\u3002`; } if (/token.?bound|\u5df2\u7ed1\u5b9a\u5176\u4ed6/i.test(msg)) { return "\u8be5 Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\uff0c\u672c\u8d26\u53f7\u6309\u514d\u8d39\u4f53\u9a8c\u4f7f\u7528\u3002"; } if (/invalid.?lease|lease/i.test(msg) && !/[\u4e00-\u9fff]/.test(msg)) { return "\u4e91\u7aef\u6388\u6743\u5931\u6548\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u6216\u91cd\u65b0\u4fdd\u5b58 Pro Token\u3002"; } if (/^http_\d+$/i.test(msg) || /^[a-z0-9_]+$/i.test(msg)) { return `\u4e91\u7aef\u63d0\u793a\uff1a${msg}`; } return msg; } function ensureDialogMask() { let mask = document.getElementById("tzss-dialog-mask"); if (mask) return mask; mask = document.createElement("div"); mask.id = "tzss-dialog-mask"; mask.innerHTML = `