// ==UserScript== // @name 【效率版】Chinamde专业技术人员继续教育网络平台学习助手 // @namespace https://card.wlxy.live/details/EE248436 // @version 1.0 // @description chinamde.cn各省专技继续教育(*.chinamde.cn)学习辅助:扬州专技、湖南事业培训、内蒙古专技、江苏专技、吉林专技、甘肃专技、区块链工程培训、北京会计继教、四川专技、云南专技、天水博通、辽宁专技、天津专技、北京专技、山东专技、河北专技、山西专技、宁夏专技、新疆专技、河南专技、陕西专技、青海专技、上海专技、安徽专技、湖北专技、重庆专技、西藏专技、浙江专技、福建专技、江西专技、贵州专技、广东专技、广西专技、海南专技、杭州专技、陇南专技;已购公需/专业课勾选、章节顺序学习;免费体验3个视频章节,Pro不限。 // @antifeature payment 免费体验3个视频章节,升级Pro不限 // @antifeature membership 需云端授权 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @match *://*.chinamde.cn/* // @noframes // @connect chinamde.cn // @connect *.chinamde.cn // @connect vod.chinamde.cn // @connect oa18.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 chinamde.cn各省专技继续教育 // @tag chinamde.cn // @tag 学习辅助 // @license All Rights Reserved // ==/UserScript== (function () { "use strict"; try { if (window.top !== window.self) return; } catch (_) { return; } const BOOT_KEY = "__SCZJ_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 SITE_HOST = String(location.hostname || "").replace(/^www\./, "").toLowerCase(); const API_BASE = location.origin; const PAGE_ORIGIN = location.origin; const SITE_NAME_MAP = { "yzzj.chinamde.cn": "\u626c\u5dde\u5e02\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "hunan.chinamde.cn": "\u6e56\u5357\u5730\u533a\u4e8b\u4e1a\u5355\u4f4d\u57f9\u8bad", "nmg.chinamde.cn": "\u5185\u8499\u53e4\u81ea\u6cbb\u533a\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "jszj.chinamde.cn": "\u6c5f\u82cf\u7701\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "jilin.chinamde.cn": "\u5409\u6797\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "gs.chinamde.cn": "\u7518\u8083\u7701\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "szgxrc.chinamde.cn": "\u533a\u5757\u94fe\u5de5\u7a0b\u6280\u672f\u4eba\u5458\u57f9\u8bad", "bjkj.chinamde.cn": "\u5317\u4eac\u5e02\u4f1a\u8ba1\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "sczj.chinamde.cn": "\u56db\u5ddd\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "yunnan.chinamde.cn": "\u4e91\u5357\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "tsbt.chinamde.cn": "\u7518\u8083\u7701\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "lnzj.chinamde.cn": "\u8fbd\u5b81\u7701\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "tianjin.chinamde.cn": "\u5929\u6d25\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "beijing.chinamde.cn": "\u5317\u4eac\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "shandong.chinamde.cn": "\u5c71\u4e1c\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "hebei.chinamde.cn": "\u6cb3\u5317\u7701\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "shanxi.chinamde.cn": "\u5c71\u897f\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "ningxia.chinamde.cn": "\u5b81\u590f\u56de\u65cf\u81ea\u6cbb\u533a\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "xinjiang.chinamde.cn": "\u65b0\u7586\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "yrcti.chinamde.cn": "\u6cb3\u5357\u7701\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "sx.chinamde.cn": "\u9655\u897f\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "gansu.chinamde.cn": "\u7518\u8083\u7701\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "qinghai.chinamde.cn": "\u9752\u6d77\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "shanghai.chinamde.cn": "\u4e0a\u6d77\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "ahptc.chinamde.cn": "\u5b89\u5fbd\u7701\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "hubei.chinamde.cn": "\u6e56\u5317\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "chongqing.chinamde.cn": "\u91cd\u5e86\u5e02\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "cqut.chinamde.cn": "\u91cd\u5e86\u5e02\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "xizang.chinamde.cn": "\u897f\u85cf\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "zjzj.chinamde.cn": "\u6d59\u6c5f\u7701\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "fujian.chinamde.cn": "\u798f\u5efa\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "jiangxi.chinamde.cn": "\u6c5f\u897f\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "guizhou.chinamde.cn": "\u8d35\u5dde\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "gdzj.chinamde.cn": "\u5e7f\u4e1c\u7701\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "ggzj.chinamde.cn": "\u5e7f\u897f\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "hnzj.chinamde.cn": "\u6d77\u5357\u7701\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "xbsd.chinamde.cn": "\u7518\u8083\u7701\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "sxu.chinamde.cn": "\u5c71\u897f\u7701\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "hzzj.chinamde.cn": "\u676d\u5dde\u5e02\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "bgd.chinamde.cn": "\u5317\u4eac\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "btln.chinamde.cn": "\u9647\u5357\u5e02\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "jiangsu.chinamde.cn": "\u6c5f\u82cf\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", "liaoning.chinamde.cn": "\u8fbd\u5b81\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2", }; function detectSiteName() { if (SITE_NAME_MAP[SITE_HOST]) return SITE_NAME_MAP[SITE_HOST]; try { const seo = readCookie("SEO") || ""; const m = String(seo).match(/([\u4e00-\u9fff]{2,24}\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2)/); if (m) return m[1].replace(/\u7f51\u7edc\u5e73\u53f0$/, "").replace(/\u5e73\u53f0$/, ""); if (/\u4e13\u6280|\u7ee7\u7eed\u6559\u80b2/.test(seo)) { return String(seo).replace(/[-—|].*$/, "").trim().replace(/\u7f51\u7edc\u5e73\u53f0$/, "").replace(/\u5e73\u53f0$/, "").slice(0, 24) || "\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2"; } } catch (_) {} const title = String(document.title || "").replace(/[-—_|].*$/, "").trim(); if (title) return title.replace(/\u7f51\u7edc\u5e73\u53f0$/, "").replace(/\u5e73\u53f0$/, "").slice(0, 24); return "\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2"; } const SIGN_KEY = "1ut189dff111d4fb6hangchengf7b6"; const APP_KEY = "hangcheng.websites"; const TOKEN_KEY = `mde_token_${SITE_HOST}_v1`; const USER_KEY = `mde_user_${SITE_HOST}_v1`; const PANEL_POS_KEY = `mde_panel_pos_${SITE_HOST}_v1`; const PANEL_COLLAPSED_KEY = `mde_panel_collapsed_${SITE_HOST}_v1`; const CLOUD_TOKEN_KEY = `mde_cloud_token_${SITE_HOST}_v1`; const CLOUD_LEASE_KEY = `mde_cloud_lease_${SITE_HOST}_v1`; const LOCAL_FREE_USED_KEY = `mde_local_free_used_${SITE_HOST}_v1`; const COMPLETE_THRESHOLD = 95; const REPORT_TIMES = 3; const INTERVAL_MS = 3 * 1000; const CLOUD_QUOTA_ENABLED = true; const PANEL_LOGO_URL = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const PANEL_FEATURE_SLOGAN = "\u987a\u5e8f\u5b66\u4e60 · \u7ae0\u8282\u5237\u8bfe"; const CLOUD_API_BASE = "https://oa18.ahzsksw.cn"; const PRO_BUY_URL = "https://card.wlxy.live/details/EE248436"; let SITE_NAME = SITE_NAME_MAP[SITE_HOST] || "\u4e13\u4e1a\u6280\u672f\u4eba\u5458\u7ee7\u7eed\u6559\u80b2"; function getPanelNotice() { return `\u514d\u8d39\u4f53\u9a8c 3 \u4e2a\u89c6\u9891\u7ae0\u8282\uff1bPro \u4e0d\u9650\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: "", year: String(new Date().getFullYear()), courses: [], selectedCourseIds: new Set(), activeCourseId: null, hours: [], onlyIncomplete: true, logLines: [], running: false, stopFlag: false, currentTask: "", lastHourId: "", cloudToken: "", cloudLease: "", cloudLeaseExp: 0, cloudTier: "free", cloudProExpireAt: 0, tokenBoundBlocked: false, freeVideoLimit: 3, freeUsedVideos: 0, proBuyUrl: PRO_BUY_URL, panelNotice: getPanelNotice(), lastError: "", loadSource: "", }; 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 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 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 _djp(token) { try { const part = String(token || "").replace(/^Bearer\s+/i, "").split(".")[1]; if (!part) return null; const json = atob(part.replace(/-/g, "+").replace(/_/g, "/")); const bytes = Uint8Array.from(json, (c) => c.charCodeAt(0)); const text = new TextDecoder("utf-8").decode(bytes); const safe = text .replace(/"(user_id|userId)"\s*:\s*(\d{16,})/g, '"$1":"$2"') .replace(/"(user_id|userId)"\s*:\s*"(\d{16,})"/g, '"$1":"$2"'); return JSON.parse(safe); } catch (_) { return null; } } function _rbu() { try { const raw = readCookie("BaseUserInfo") || ""; if (!raw) return null; const text = raw.startsWith("%") ? decodeURIComponent(raw) : raw; const m = String(text).match(/"userId"\s*:\s*"(\d+)"/); const uid = m ? m[1] : ""; let obj = null; try { const safe = String(text).replace(/"(userId|userZoneId)"\s*:\s*(\d{16,})/g, '"$1":"$2"'); obj = JSON.parse(safe); } catch (_) {} return { userId: uid || String((obj && obj.userId) || ""), idCard: String((obj && obj.idCard) || ""), telephone: String((obj && obj.telephone) || ""), userZoneId: String((obj && obj.userZoneId) || ""), }; } catch (_) { return null; } } function _ppu(preferred) { const base = _rbu(); if (base && /^\d{10,}$/.test(base.userId)) return base.userId; const fromArg = String(preferred || "").trim(); if (/^\d{10,}$/.test(fromArg)) return fromArg; return ""; } function _spu(uid, name) { const id = _ppu(uid); if (id) state.userId = id; if (name) state.userName = String(name); try { if (state.userId) GM_setValue(USER_KEY, { id: state.userId, realName: state.userName }); } catch (_) {} } function _ptk(raw, opts) { let t = String(raw || "").trim(); if (!t) return false; t = t.replace(/^Bearer\s+/i, ""); if (t.length < 20) return false; const prev = state.token; state.token = t; try { GM_setValue(TOKEN_KEY, t); } catch (_) {} const base = _rbu(); const payload = _djp(t); const uid = _ppu( (base && base.userId) || (payload && (payload.user_id || payload.userId)) || "" ); const name = String( (payload && (payload.real_name || payload.realName || payload.username)) || "" ); _spu(uid, name); if (opts && opts.reload && prev !== t) { scheduleReloadCourses("token-updated"); } return true; } function readCookie(name) { try { const w = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; const raw = String(w.document?.cookie || document.cookie || ""); const parts = raw.split(/;\s*/); for (const p of parts) { const i = p.indexOf("="); if (i < 0) continue; const k = p.slice(0, i).trim(); if (k !== name) continue; let v = p.slice(i + 1); try { v = decodeURIComponent(v); } catch (_) {} return v.replace(/^"+|"+$/g, ""); } } catch (_) {} return ""; } function normalizeJwt(raw) { let s = String(raw || "").trim().replace(/^Bearer\s+/i, ""); s = s.replace(/^"+|"+$/g, ""); if (!s || s.length < 20) return ""; if (s.startsWith("eyJ")) return s; return ""; } function _rpt() { const candidates = []; const cookieAuth = readCookie("Authorization"); if (cookieAuth) candidates.push(cookieAuth); try { const store = pageStorage("local"); const local = store && store.getItem("Authorization_local"); if (local) candidates.push(local); const auth = store && store.getItem("Authorization"); if (auth) candidates.push(auth); } catch (_) {} for (const raw of candidates) { const s = normalizeJwt(raw); if (s) return s; } return ""; } function clearToken(reason) { state.token = ""; try { GM_setValue(TOKEN_KEY, ""); } catch (_) {} if (reason) state.lastError = String(reason); } function pageStorage(kind) { try { const w = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; return kind === "session" ? w.sessionStorage : w.localStorage; } catch (_) { try { return kind === "session" ? sessionStorage : localStorage; } catch (e) { return null; } } } function getToken() { const live = _rpt() || _spt({ silent: true }); if (live) { if (state.token !== live) _ptk(live); return live; } if (state.token) return state.token; try { const t = String(GM_getValue(TOKEN_KEY, "") || "").trim(); if (t) { state.token = t; return t; } } catch (_) {} return ""; } function _spt(opts) { const silent = !!(opts && opts.silent); const plat = _rpt(); if (plat) { _ptk(plat, { reload: !silent }); return state.token; } const stores = [pageStorage("local"), pageStorage("session")].filter(Boolean); const jwtRe = /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/; const preferKeys = [ "Authorization_local", "Authorization", "accessToken", "access_token", "Admin-Token", "token", "TOKEN", ]; for (const store of stores) { for (const key of preferKeys) { let v = ""; try { v = store.getItem(key) || ""; } catch (_) { continue; } if (!v) continue; if (jwtRe.test(v)) { const m = v.match(jwtRe); if (m && _ptk(m[0], { reload: !silent })) return state.token; } if (v.startsWith("{")) { try { const obj = JSON.parse(v); const cand = obj.accessToken || obj.access_token || obj.token || (obj.data && obj.data.accessToken) || ""; if (cand && _ptk(cand, { reload: !silent })) return state.token; } catch (_) {} } } } for (const store of stores) { let n = 0; try { n = store.length; } catch (_) { continue; } for (let i = 0; i < n; i++) { let k = ""; let v = ""; try { k = store.key(i) || ""; v = store.getItem(k) || ""; } catch (_) { continue; } if (!v) continue; if (/token|auth|user|login|access/i.test(k) && jwtRe.test(v)) { const m = v.match(jwtRe); if (m && _ptk(m[0], { reload: !silent })) return state.token; } } } return state.token || ""; } let __reloadTimer = 0; function scheduleReloadCourses(reason) { if (state.running) return; clearTimeout(__reloadTimer); __reloadTimer = setTimeout(async () => { try { updateAuthUi(); await _lc({ quiet: true, keepSelection: true }); if (!state.courses.length) { state.lastError = "\u5df2\u767b\u5f55\u4f46\u672a\u62c9\u5230\u5df2\u8d2d\u8bfe\u7a0b\uff0c\u8bf7\u70b9\u5237\u65b0\u6216\u7a0d\u540e\u518d\u8bd5"; _rcb(); } else { state.lastError = ""; _plog(`\u8bfe\u8868\u5df2\u66f4\u65b0\uff08${reason || "auto"}\uff09`, ""); } } catch (e) { state.lastError = String(e.message || e); _rcb(); log(`\u62c9\u8bfe\u5931\u8d25\uff1a${state.lastError}`, { info: true }); } }, 400); } function _ith() { const captureAuth = (v) => { const s = String(v || ""); if (/Bearer\s+eyJ/i.test(s) || /^eyJ/.test(s)) { _ptk(s, { reload: true }); updateAuthUi(); } }; try { const XHR = (typeof unsafeWindow !== "undefined" && unsafeWindow.XMLHttpRequest) || XMLHttpRequest; const setRequestHeader = XHR.prototype.setRequestHeader; XHR.prototype.setRequestHeader = function (name, value) { if (/^authorization$/i.test(String(name || ""))) captureAuth(value); return setRequestHeader.apply(this, arguments); }; } catch (_) {} try { const rawFetch = (typeof unsafeWindow !== "undefined" && unsafeWindow.fetch) || window.fetch; if (typeof rawFetch === "function") { const wrapped = function (input, init) { try { const h = init && init.headers; if (h) { if (typeof h.get === "function") { captureAuth(h.get("Authorization") || h.get("authorization")); } else if (Array.isArray(h)) { h.forEach((pair) => { if (pair && /^authorization$/i.test(String(pair[0] || ""))) captureAuth(pair[1]); }); } else if (typeof h === "object") { captureAuth(h.Authorization || h.authorization); } } } catch (_) {} return rawFetch.apply(this, arguments); }; if (typeof unsafeWindow !== "undefined") unsafeWindow.fetch = wrapped; window.fetch = wrapped; } } catch (_) {} } function log(text, opts) { const o = opts || {}; const line = { t: Date.now(), text: String(text || ""), color: o.color || "", info: !!o.info, done: !!o.done, }; state.logLines.unshift(line); if (state.logLines.length > LOG_LIMIT) state.logLines.length = LOG_LIMIT; renderLog(); try { console.log("[chinamde\u770b\u8bfe]", line.text); } 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("sczj-current-task"); if (el) { el.textContent = state.currentTask || "\u5f85\u547d"; el.title = state.currentTask || ""; } } function syncRunButtons() { const startBtn = document.getElementById("sczj-start"); const stopBtn = document.getElementById("sczj-stop"); if (startBtn) { startBtn.disabled = !!state.running; startBtn.classList.toggle("sczj-btn-off", !!state.running); } if (stopBtn) { stopBtn.disabled = !state.running; stopBtn.classList.toggle("sczj-btn-off", !state.running); } } function setFooter(text) { const el = document.getElementById("sczj-footer-text"); if (el) el.textContent = text; } 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 _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/sczj/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") { if ( detail.free_used_videos != null || detail.freeUsedVideos != null || detail.proBuyUrl || detail.free_video_limit != null || detail.panelNotice ) { _alp(detail); } const err = new Error(String(detail.message || detail.code || "cloud_error")); err.code = String(detail.code || detail.message || ""); err.detail = detail; throw err; } const raw = String(data.detail || data.message || data.code || `http_${res.status}`); const err = new Error(raw); err.code = String(data.code || data.message || raw || ""); if (/free_quota/i.test(raw)) err.code = "free_quota_exhausted"; if (/^revoked$/i.test(raw)) err.code = "revoked"; if (/^expired$/i.test(raw)) err.code = "expired"; if (/expired_after_activation/i.test(raw)) err.code = "expired_after_activation"; if (/invalid token/i.test(raw)) err.code = "invalid token"; throw err; } 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, tokenBoundBlocked: state.tokenBoundBlocked, }); } catch (_) {} } function _rcl() { try { return GM_getValue(CLOUD_LEASE_KEY) || null; } catch (_) { return null; } } function _llf() { try { const map = GM_getValue(LOCAL_FREE_USED_KEY) || {}; const uid = String(state.userId || "anon"); state.freeUsedVideos = Number(map[uid] || 0) || 0; } catch (_) {} } function _slf() { try { const map = GM_getValue(LOCAL_FREE_USED_KEY) || {}; const uid = String(state.userId || "anon"); map[uid] = Number(state.freeUsedVideos) || 0; GM_setValue(LOCAL_FREE_USED_KEY, map); } catch (_) {} } 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; if (cached.tokenBoundBlocked != null) state.tokenBoundBlocked = !!cached.tokenBoundBlocked; _ucp(); return !!state.cloudLease; } } } try { const data = await _crq("/api/sczj/lease", "POST", { token: _gct(), learning_user_id: String(state.userId || ""), userid: String(state.userId || ""), site: SITE_HOST, site_name: SITE_NAME, }); _alp(data); if (data.token_bound_other_user || data.tokenBoundBlocked || data.token_bound_blocked) { state.tokenBoundBlocked = true; state.cloudTier = "free"; _ucp(); _plog("Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\uff0c\u672c\u8d26\u53f7\u6309\u514d\u8d39\u4f53\u9a8c"); if (force) _ptb(); } return !!state.cloudLease; } catch (e) { if (_iqe(e)) throw e; if (/token_bound|\u5df2\u7ed1\u5b9a\u5176\u4ed6/i.test(String(e.message || e))) { state.tokenBoundBlocked = true; state.cloudTier = "free"; _ucp(); _plog("Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\uff0c\u672c\u8d26\u53f7\u6309\u514d\u8d39\u4f53\u9a8c"); if (force) _ptb(); return false; } if (_gct() && _ita(e)) { state.cloudTier = "free"; state.tokenBoundBlocked = false; _ucp(); _pta(e); return false; } _llf(); if (!state.cloudLease) { state.cloudTier = _gct() ? "free" : "free"; state.cloudLease = "local-dev"; state.cloudLeaseExp = now + 600; log("\u4e91\u7aef\u6682\u4e0d\u53ef\u7528\uff0c\u6309\u672c\u5730\u514d\u8d39\u989d\u5ea6\u8ba1\u6570", { info: true }); } _ucp(); return true; } } function _ita(e) { const msg = String((e && e.message) || e || ""); const code = String((e && e.code) || ""); return ( /^(revoked|expired|expired_after_activation|invalid token)$/i.test(code) || /revoked|expired_after_activation|\binvalid token\b|token.?revoked|\u5df2\u7981\u7528|\u5df2\u8fc7\u671f|\u65e0\u6548.?token|token.?\u65e0\u6548/i.test(msg) ); } function _cta(e) { const msg = String((e && e.message) || e || "").toLowerCase(); const code = String((e && e.code) || "").toLowerCase(); if (code === "revoked" || /revoked|\u5df2\u7981\u7528/.test(msg)) return "revoked"; if (code === "expired_after_activation" || /expired_after_activation/.test(msg)) return "expired_after_activation"; if (code === "expired" || /\bexpired\b|\u5df2\u8fc7\u671f/.test(msg)) return "expired"; if (code === "invalid token" || /invalid token|\u65e0\u6548/.test(msg)) return "invalid"; return "invalid"; } function _pta(e) { const kind = _cta(e); const map = { revoked: { title: "Token \u5df2\u7981\u7528", message: "\u8be5 Pro Token \u5df2\u88ab\u7ba1\u7406\u5458\u7981\u7528\uff0c\u65e0\u6cd5\u4f7f\u7528 Pro\u3002\n\n\u8bf7\u66f4\u6362\u6709\u6548 Token\uff0c\u6216\u91cd\u65b0\u8d2d\u4e70\u3002", }, expired: { title: "Token \u5df2\u8fc7\u671f", message: "\u8be5 Pro Token \u5df2\u8fc7\u671f\uff0c\u65e0\u6cd5\u4f7f\u7528 Pro\u3002\n\n\u8bf7\u66f4\u6362\u6709\u6548 Token\uff0c\u6216\u91cd\u65b0\u8d2d\u4e70\u3002", }, expired_after_activation: { title: "Token \u5df2\u8fc7\u671f", message: "\u8be5 Pro Token \u6fc0\u6d3b\u540e\u7684\u6709\u6548\u671f\u5df2\u7ed3\u675f\uff0c\u65e0\u6cd5\u4f7f\u7528 Pro\u3002\n\n\u8bf7\u66f4\u6362\u6709\u6548 Token\uff0c\u6216\u91cd\u65b0\u8d2d\u4e70\u3002", }, invalid: { title: "Token \u65e0\u6548", message: "Pro Token \u65e0\u6548\u6216\u4e0d\u5b58\u5728\uff0c\u65e0\u6cd5\u5f00\u901a Pro\u3002\n\n\u8bf7\u68c0\u67e5\u662f\u5426\u590d\u5236\u5b8c\u6574\uff0c\u6216\u91cd\u65b0\u8d2d\u4e70\u3002", }, }; const info = map[kind] || map.invalid; state.tokenBoundBlocked = false; state.cloudTier = "free"; _ucp(); _plog(info.message.replace(/\n+/g, " ")); _spd({ title: info.title, message: info.message, confirmText: "\u53bb\u8d2d\u4e70", cancelText: "\u77e5\u9053\u4e86", primaryClass: "sczj-btn-start", onConfirm: () => openUrl(state.proBuyUrl || PRO_BUY_URL), }); } function _iqe(e) { const msg = String((e && e.message) || e || ""); const code = String((e && e.code) || ""); return ( code === "free_quota_exhausted" || /free_quota_exhausted|free_quota|\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c/i.test(msg) ); } async function _cc(hour) { if (!CLOUD_QUOTA_ENABLED) return { tier: "open" }; const hourId = hour && typeof hour === "object" ? hour.hourId : hour; const courseId = hour && typeof hour === "object" ? hour.courseId : ""; 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; _slf(); _ucp(); return { tier: "free", used: state.freeUsedVideos }; } const payload = { cs_id: String(hourId || ""), res_id: String(hourId || ""), hour_id: String(hourId || ""), course_id: String(courseId || ""), site: SITE_HOST, }; try { const data = await _crq("/api/sczj/video/consume", "POST", payload); _alp(data); _ucp(); return data; } catch (e) { if (_iqe(e) && _gct()) { await _cl(true); if (_ipu()) { const data = await _crq("/api/sczj/video/consume", "POST", payload); _alp(data); _ucp(); return data; } } throw e; } } function _fcm(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 (/^revoked$/i.test(msg) || /token.?revoked|\u5df2\u7981\u7528/i.test(msg)) { return "\u8be5 Pro Token \u5df2\u88ab\u7981\u7528\uff0c\u8bf7\u66f4\u6362\u6709\u6548 Token \u6216\u91cd\u65b0\u8d2d\u4e70\u3002"; } if (/expired_after_activation/i.test(msg)) { return "\u8be5 Pro Token \u6fc0\u6d3b\u6709\u6548\u671f\u5df2\u7ed3\u675f\uff0c\u8bf7\u66f4\u6362\u6216\u91cd\u65b0\u8d2d\u4e70\u3002"; } if (/^expired$/i.test(msg) || /token.?expired|\u5df2\u8fc7\u671f/i.test(msg)) { return "\u8be5 Pro Token \u5df2\u8fc7\u671f\uff0c\u8bf7\u66f4\u6362\u6216\u91cd\u65b0\u8d2d\u4e70\u3002"; } if (/invalid token/i.test(msg)) { return "Pro Token \u65e0\u6548\uff0c\u8bf7\u68c0\u67e5\u662f\u5426\u590d\u5236\u5b8c\u6574\u3002"; } return msg; } function _upn() { const el = document.getElementById("sczj-ann-text"); if (el) el.textContent = String(state.panelNotice || getPanelNotice()); } function _ucp() { const tierEl = document.getElementById("sczj-cloud-tier"); const expireEl = document.getElementById("sczj-cloud-expire"); const settingsTier = document.getElementById("sczj-settings-tier"); const settingsExpire = document.getElementById("sczj-settings-expire"); const settingsQuota = document.getElementById("sczj-settings-quota"); const isPro = _ipu(); const expireText = _fpe(state.cloudProExpireAt); if (tierEl) { tierEl.textContent = isPro ? "Pro" : state.tokenBoundBlocked ? "\u5df2\u7ed1\u5176\u4ed6\u8d26\u53f7" : CLOUD_QUOTA_ENABLED ? "\u514d\u8d39\u7248" : "\u5f00\u53d1\u4e2d"; tierEl.style.color = isPro ? "#166534" : "#9a3412"; tierEl.style.background = isPro ? "#f0fdf4" : "#fff7ed"; tierEl.style.borderColor = isPro ? "#86efac" : "#fdba74"; } if (expireEl) { expireEl.textContent = !CLOUD_QUOTA_ENABLED ? "\u6682\u4e0d\u9650\u989d" : isPro ? expireText ? `\u5230\u671f ${expireText}` : "Pro \u5df2\u5f00\u901a" : `\u4f53\u9a8c ${state.freeUsedVideos}/${state.freeVideoLimit} \u7ae0\u8282`; } if (settingsTier) { settingsTier.textContent = isPro ? "Pro" : state.tokenBoundBlocked ? "\u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7" : CLOUD_QUOTA_ENABLED ? "\u514d\u8d39\u7248" : "\u5f00\u53d1\u4e2d\uff08\u6682\u4e0d\u9650\u989d\uff09"; } if (settingsExpire) { settingsExpire.textContent = isPro && expireText ? expireText : isPro ? "\u5df2\u5f00\u901a" : "—"; } if (settingsQuota) { settingsQuota.textContent = !CLOUD_QUOTA_ENABLED || isPro ? "\u4e0d\u9650" : `${state.freeUsedVideos}/${state.freeVideoLimit}`; } } function buildApiUrl(path, query) { let url = String(path || "").startsWith("http") ? String(path) : API_BASE + path; if (query && typeof query === "object") { const qs = Object.keys(query) .filter((k) => query[k] != null && query[k] !== "") .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(query[k])}`) .join("&"); if (qs) url += (url.includes("?") ? "&" : "?") + qs; } return url; } function parseApiJson(text, httpStatus) { let raw = String(text || "").trim(); if (raw && !raw.startsWith("{") && !raw.startsWith("[")) { const i = raw.indexOf("{"); if (i >= 0) raw = raw.slice(i); } let json; try { json = JSON.parse(raw); } catch (_) { const snip = raw.replace(/\s+/g, " ").slice(0, 120); throw new Error( httpStatus && (httpStatus < 200 || httpStatus >= 300) ? `HTTP ${httpStatus}${snip ? ` · ${snip}` : ""}` : "\u89e3\u6790\u54cd\u5e94\u5931\u8d25" ); } if (httpStatus && (httpStatus < 200 || httpStatus >= 300)) { const detail = json.message || json.msg || json.detail || ""; throw new Error(detail ? `HTTP ${httpStatus}\uff1a${detail}` : `HTTP ${httpStatus}`); } const code = Number(json.code); if (code === 200) return json; if (code === 401) { clearToken("\u5e73\u53f0\u8fd4\u56de 401"); throw new Error("\u767b\u5f55\u5df2\u5931\u6548\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55\u5e73\u53f0\u540e\u5237\u65b0"); } throw new Error(json.message || json.msg || "\u8bf7\u6c42\u5931\u8d25"); } function _css(obj) { if (!obj || typeof obj !== "object" || Array.isArray(obj)) return ""; const keys = Object.keys(obj).sort(); let r = ""; keys.forEach((k) => { const v = obj[k]; if (v === null || v === undefined) return; if (Array.isArray(v)) r += `${k}=${JSON.stringify(v)}&`; else r += `${k}=${v}&`; }); return r; } function bytesToHexUpper(buf) { return Array.from(new Uint8Array(buf)) .map((b) => b.toString(16).padStart(2, "0")) .join("") .toUpperCase(); } async function _hs1(message) { const enc = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", enc.encode(SIGN_KEY), { name: "HMAC", hash: "SHA-1" }, false, ["sign"] ); const sig = await crypto.subtle.sign("HMAC", key, enc.encode(String(message || ""))); return bytesToHexUpper(sig); } function getAppNameHeader() { try { const raw = readCookie("browserInfo") || ""; if (raw) { const obj = JSON.parse(raw); if (obj && obj.name) return encodeURIComponent(String(obj.name)); return encodeURIComponent(raw); } } catch (_) {} return "Chrome"; } async function _bph({ isGet, signObj, token }) { const signature = await _hs1(_css(signObj || {})); return { Accept: "application/json, text/plain, */*", ...(isGet ? {} : { "Content-Type": "application/json" }), Authorization: `Bearer ${token}`, signature, appkey: APP_KEY, appName: getAppNameHeader(), }; } async function _arq({ method = "GET", path, data, query }) { const token = getToken(); if (!token) { throw new Error("\u672a\u83b7\u53d6\u5230\u767b\u5f55 Token\uff0c\u8bf7\u5148\u767b\u5f55\u672c\u7ad9\u5e76\u5237\u65b0\u9875\u9762"); } const isGet = String(method || "GET").toUpperCase() === "GET"; const signObj = isGet ? query || data || {} : data || {}; const url = buildApiUrl(path, query || (isGet ? data : null)); const headers = await _bph({ isGet, signObj, token }); const body = isGet ? undefined : data != null ? JSON.stringify(data) : undefined; const pageFetch = async () => { const w = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; const nativeFetch = w.fetch || fetch; const res = await nativeFetch.call(w, url, { method: isGet ? "GET" : "POST", headers, body, credentials: "include", }); const text = await res.text(); return parseApiJson(text, res.status); }; const gmFetch = () => new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: isGet ? "GET" : "POST", url, headers: { ...headers, Origin: PAGE_ORIGIN, Referer: PAGE_ORIGIN + "/", }, data: body, timeout: 60000, onload(res) { try { resolve(parseApiJson(res.responseText || "", res.status)); } catch (e) { reject(e); } }, onerror: () => reject(new Error("\u7f51\u7edc\u8bf7\u6c42\u5931\u8d25")), ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6")), }); }); try { return await pageFetch(); } catch (e1) { try { return await gmFetch(); } catch (e2) { throw e2 || e1; } } } async function _fub() { const res = await _arq({ path: "/api/website/user/center/getUserBaseInfo" }); const d = res.data || {}; const uid = _ppu(d.userId); _spu(uid, d.realName || state.userName); return d; } async function _fbc() { const out = []; let page = 1; let total = Infinity; while (out.length < total && page <= 30) { const res = await _arq({ method: "POST", path: "/api/website/courseCourse/getBuyCourseRecordByIds", data: { pageNum: page, pageSize: 50 }, }); const rows = Array.isArray(res.rows) ? res.rows : Array.isArray(res.data) ? res.data : []; total = Number(res.total != null ? res.total : rows.length) || rows.length; for (const row of rows) { out.push({ id: String(row.id || ""), name: String(row.courseName || row.id || "\u672a\u547d\u540d\u8bfe\u7a0b"), year: String(row.year || state.year), typeName: String(row.courseTypeName || row.natureTypeName || ""), hourCount: Number(row.hourCount) || 0, courseNum: Number(row.courseNum) || 0, finishClassHour: Number(row.finishClassHour) || 0, percent: Number(row.percent) || 0, percentName: String(row.percentName || ""), finish: Number(row.finish) || 0, }); } if (!rows.length) break; page += 1; } return out; } async function _fli(courseId, year) { const res = await _arq({ path: "/api/website/courseCourse/getLearnInfo", query: { courseId: String(courseId), year: String(year || state.year), }, }); return Array.isArray(res.data) ? res.data : []; } async function _fv(courseId, hourId) { const res = await _arq({ path: "/api/website/courseCourse/getVideo", query: { courseId: String(courseId), hourId: String(hourId), }, }); return res.data || {}; } async function playerSetContinueKey() { try { await _arq({ path: "/api/website/courseCourse/playerSetContinueKey" }); } catch (_) {} } async function learningCheckReal(courseId) { try { await _arq({ path: "/api/website/courseCourse/learningCheckReal", query: { courseId: String(courseId) }, }); } catch (_) {} } async function _slr(courseId, classHourId, second, duration) { const res = await _arq({ method: "POST", path: "/api/website/courseCourse/saveLeaningRecord", data: { courseId: String(courseId), classHourId: String(classHourId), second: String(Math.floor(Number(second) || 0)), duration: String(Math.floor(Number(duration) || 0)), remoteType: "pc", }, }); const pct = Number(res.data); return { percent: Number.isFinite(pct) ? pct : pctOf(second, duration), raw: res, }; } function _fh(course, chapters) { const out = []; (chapters || []).forEach((ch, ci) => { const children = Array.isArray(ch.children) ? ch.children : []; children.forEach((h, hi) => { const percent = Number(h.percent) || Number(String(h.percentName || "").replace(/%/g, "")) || 0; const pct = percent <= 1 && percent > 0 && !String(h.percentName || "").includes("%") ? Math.round(percent * 10000) / 100 : percent; let progress = Number(h.percentName); if (!Number.isFinite(progress)) { progress = pct <= 1 ? Math.round(pct * 10000) / 100 : pct; } out.push({ courseId: String(course.id), courseName: course.name, chapterId: String(ch.id || ""), chapterTitle: String(ch.title || `\u7ae0\u8282${ci + 1}`), hourId: String(h.id || ""), name: String(h.title || `\u8bfe\u65f6${hi + 1}`), progress: Math.min(100, Math.max(0, progress)), duration: 0, studied: 0, done: progress >= COMPLETE_THRESHOLD, }); }); }); return out; } async function _ess() { _spt({ silent: true }); let token = getToken(); if (!token) throw new Error("\u8bf7\u5148\u767b\u5f55\u672c\u7ad9\u540e\u518d\u5237\u65b0\u9875\u9762"); _spu(_ppu(state.userId), state.userName); if (!state.userId) { try { const u = GM_getValue(USER_KEY); if (u?.id) _spu(u.id, u.realName || ""); } catch (_) {} } if (!state.userId) { const payload = _djp(token); if (payload?.user_id || payload?.userId) { _spu(payload.user_id || payload.userId, payload.real_name || payload.username || ""); } } try { await _fub(); state.lastError = ""; } catch (e) { const msg = String(e.message || e); if (/\u767b\u5f55\u5df2\u5931\u6548|401|\u5931\u6548/i.test(msg)) { clearToken(msg); const fresh = _rpt(); if (fresh) _ptk(fresh); if (!getToken()) { throw new Error("\u672a\u8bfb\u5230\u5b98\u7f51\u767b\u5f55 Cookie\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u6216\u91cd\u65b0\u767b\u5f55\u540e\u518d\u70b9\u52a9\u624b\u300c\u5237\u65b0\u300d"); } await _fub(); state.lastError = ""; } else { state.lastError = msg; } } _llf(); } async function _lc({ quiet, keepSelection } = {}) { await _ess(); const prev = keepSelection ? new Set(state.selectedCourseIds) : null; let list = []; try { list = await _fbc(); state.loadSource = "api"; state.lastError = ""; } catch (e) { state.lastError = String(e.message || e); if (!quiet) log(`\u52a0\u8f7d\u5df2\u8d2d\u8bfe\u7a0b\u5931\u8d25\uff1a${state.lastError}`); throw e; } state.courses = list; if (list[0]?.year) state.year = String(list[0].year); if (prev) { const valid = new Set(list.map((c) => c.id)); state.selectedCourseIds = new Set([...prev].filter((id) => valid.has(id))); } else if (!state.selectedCourseIds.size && list.length === 1) { state.selectedCourseIds.add(list[0].id); } if (!state.activeCourseId || !state.selectedCourseIds.has(state.activeCourseId)) { state.activeCourseId = [...state.selectedCourseIds][0] || list[0]?.id || null; } if (!quiet) log(`\u5df2\u52a0\u8f7d\u5df2\u8d2d\u8bfe\u7a0b ${list.length} \u95e8`, { info: true }); await _lh({ quiet: true }); updateAuthUi(); _usm(); _rcl2(); _rcb(); } async function _lh({ quiet } = {}) { const ids = [...state.selectedCourseIds]; const all = []; for (const cid of ids) { const course = state.courses.find((c) => c.id === cid); if (!course) continue; try { const chapters = await _fli(course.id, course.year || state.year); all.push(...flattenHours(course, chapters)); } catch (e) { log(`\u52a0\u8f7d\u7ae0\u8282\u5931\u8d25\u300c${truncate(course.name, 16)}\u300d\uff1a${e.message || e}`); } } state.hours = all; if (!quiet) log(`\u7ae0\u8282\u8bfe\u65f6 ${all.length} \u4e2a`, { info: true }); _rcp(); _usm(); } function _gsh() { let list = state.hours.filter((h) => state.selectedCourseIds.has(h.courseId)); if (state.onlyIncomplete) { list = list.filter((h) => !h.done && (h.progress || 0) < COMPLETE_THRESHOLD); } return list; } async function _soh(hour) { if (hour.done || (hour.progress || 0) >= COMPLETE_THRESHOLD) return true; await _cc(hour); await playerSetContinueKey(); await learningCheckReal(hour.courseId); let video = {}; try { video = await _fv(hour.courseId, hour.hourId); } catch (e) { throw new Error(`\u83b7\u53d6\u89c6\u9891\u4fe1\u606f\u5931\u8d25\uff1a${e.message || e}`); } const duration = Math.max( 1, Number(video.hourCount) || 0, Number(video.duration) || 0, Number(video.videotime) || 0, Number(hour.duration) || 0 ); let cur = Math.min( duration, Math.max(0, Number(video.second) || 0, Number(hour.studied) || 0) ); if (!cur && hour.progress > 0 && hour.progress < 100) { cur = Math.floor((hour.progress / 100) * duration); } hour.duration = duration; hour.studied = cur; if (cur >= duration || hour.progress >= COMPLETE_THRESHOLD) { hour.done = true; hour.progress = 100; _usm(); _rcp(); return true; } const times = REPORT_TIMES; const interval = INTERVAL_MS; const remain = duration - cur; const chunk = Math.max(1, Math.ceil(remain / times)); _plog(`\u5f00\u59cb\u5b66\u4e60\u300c${truncate(hour.name, 22)}\u300d`, hour.name); let reportIdx = 0; while (!state.stopFlag && cur < duration) { reportIdx += 1; const next = Math.min(duration, cur + chunk); setCurrentTask( `${truncate(hour.name, 12)} · ${reportIdx}/${times} · ${formatTime(next)}/${formatTime(duration)}` ); const data = await _slr(hour.courseId, hour.hourId, next, duration); cur = next; hour.studied = cur; hour.progress = Math.max(Number(data.percent) || 0, pctOf(cur, duration)); const doneFlag = hour.progress >= COMPLETE_THRESHOLD || cur >= duration; hour.done = doneFlag; if (doneFlag) hour.progress = 100; state.lastHourId = String(hour.hourId); _rcp(); _usm(); if (doneFlag) { _plog(`\u300c${truncate(hour.name, 22)}\u300d\u5b66\u4e60\u5b8c\u6210 100%`, hour.name); return true; } const due = Date.now() + interval; while (!state.stopFlag && Date.now() < due) { const left = due - Date.now(); setCurrentTask(`${truncate(hour.name, 12)} · \u4e0b\u6b21 ${formatEta(left)}`); await sleep(Math.min(1000, Math.max(200, left))); } } return !state.stopFlag && (hour.done || hour.progress >= COMPLETE_THRESHOLD); } async function _rs() { const list = _gsh().filter( (h) => !h.done && (h.progress || 0) < COMPLETE_THRESHOLD ); if (!list.length) { _spa("\u6ca1\u6709\u53ef\u5b66\u4e60\u7684\u8bfe\u65f6\u3002\u8bf7\u52fe\u9009\u8bfe\u7a0b\uff0c\u6216\u5173\u95ed\u300c\u4ec5\u672a\u5b66\u5b8c\u300d\u540e\u518d\u8bd5\u3002", "\u6682\u65e0\u53ef\u5b66"); return "empty"; } await _cl(false); if (state.tokenBoundBlocked && _gct()) { _ptb(); } if (!_ipu() && state.freeUsedVideos >= state.freeVideoLimit) { _pup(); return "quota"; } _plog(`\u5f00\u59cb\u5b66\u4e60 · \u5171 ${list.length} \u4e2a\u8bfe\u65f6\uff08\u987a\u5e8f\uff09`); let okN = 0; let failN = 0; let consecFail = 0; for (let i = 0; i < list.length; i++) { const h = list[i]; if (state.stopFlag) break; try { const ok = await _soh(h); if (ok) { okN += 1; consecFail = 0; } else if (!state.stopFlag) { failN += 1; consecFail += 1; } } catch (e) { failN += 1; consecFail += 1; const msg = String(e.message || e); if (_iqe(e)) { _pup(msg); return "quota"; } _plog(`\u300c${truncate(h.name, 16)}\u300d\u4e2d\u65ad\uff1a${_fcm(msg)}`); if (consecFail >= 2) { _spa( `\u8fde\u7eed\u5931\u8d25 ${consecFail} \u6b21\uff0c\u5df2\u6682\u505c\u3002\n\n\u539f\u56e0\uff1a${_fcm(msg)}`, "\u5b66\u4e60\u5df2\u6682\u505c" ); break; } const waited = await _wc(8000, "\u7a0d\u540e\u91cd\u8bd5"); if (!waited) break; } } if (!state.stopFlag) { _plog(`\u672c\u8f6e\u7ed3\u675f · \u5b8c\u6210 ${okN} · \u5931\u8d25 ${failN} · \u5171 ${list.length}`); } } async function _ss() { if (state.running) return; if (!state.selectedCourseIds.size) { _spa("\u8bf7\u5148\u5728\u8bfe\u7a0b\u5217\u8868\u52fe\u9009\u8981\u5b66\u4e60\u7684\u516c\u9700\u8bfe\u6216\u4e13\u4e1a\u8bfe\u3002", "\u672a\u9009\u62e9\u8bfe\u7a0b"); return; } state.running = true; state.stopFlag = false; syncRunButtons(); setCurrentTask("\u5b66\u4e60\u4e2d…"); showLogTab(); try { await _ess(); await _lh({ quiet: true }); const result = await _rs(); if (!state.stopFlag && result !== "quota" && result !== "empty") { _plog("\u672c\u8f6e\u4efb\u52a1\u7ed3\u675f\uff08\u672a\u5b8c\u6210\u7684\u53ef\u7a0d\u540e\u518d\u70b9\u5f00\u59cb\u7eed\u5b66\uff09"); } } catch (e) { if (_iqe(e)) { _pup(e.message || e); } else { _plog(`\u770b\u8bfe\u4e2d\u65ad\uff1a${e.message || e}`); _spa(_fcm(e.message || e), "\u770b\u8bfe\u4e2d\u65ad"); } } finally { state.running = false; state.stopFlag = false; syncRunButtons(); setCurrentTask("\u5f85\u547d"); try { await _lc({ quiet: true, keepSelection: true }); } catch (_) {} } } function _st() { if (!state.running) return; state.stopFlag = true; setCurrentTask("\u6b63\u5728\u505c\u6b62…"); } function showLogTab() { const panel = document.getElementById("sczj-auto-panel"); if (!panel) return; panel.querySelectorAll(".sczj-tab-btn").forEach((b) => b.classList.remove("active")); panel.querySelectorAll(".sczj-pane").forEach((p) => p.classList.remove("active")); const btn = panel.querySelector('.sczj-tab-btn[data-tab="log"]'); const pane = panel.querySelector('.sczj-pane[data-pane="log"]'); if (btn) btn.classList.add("active"); if (pane) pane.classList.add("active"); } function injectStyles() { let st = document.getElementById("sczj-panel-style"); if (!st) { st = document.createElement("style"); st.id = "sczj-panel-style"; document.head.appendChild(st); } st.textContent = ` #sczj-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;} #sczj-auto-panel.sczj-panel-max{max-height:min(92vh,760px);} #sczj-auto-panel.sczj-panel-min #sczj-panel-body,#sczj-auto-panel.sczj-panel-min #sczj-panel-footer,#sczj-auto-panel.sczj-panel-min .sczj-footer-extra{display:none!important;} #sczj-panel-header{flex:0 0 auto;padding:7px 10px;background:linear-gradient(180deg,#e8f5e9,#dcedc8);border-bottom:1px solid #c5e1a5;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;} #sczj-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #sczj-panel-logo{width:30px;height:30px;border-radius:9px;display:block;object-fit:cover;background:#fff;border:1px solid rgba(148,163,184,.45);flex:0 0 auto;} #sczj-panel-title{font-size:13px;font-weight:900;color:#1b5e20;line-height:1.26;display:flex;align-items:flex-start;gap:5px;flex-wrap:wrap;} .sczj-panel-title-text{flex:1 1 14em;min-width:0;white-space:normal;line-height:1.28;} .sczj-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;} #sczj-panel-sub{display:flex;flex-wrap:wrap;gap:3px 5px;margin-top:3px;} .sczj-sub-chip{font-size:11px;color:#33691e;background:rgba(255,255,255,.62);padding:2px 7px;border-radius:999px;border:1px solid rgba(85,139,47,.22);font-weight:700;} .sczj-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);} .sczj-sub-chip-mute{color:#57534e;background:rgba(255,255,255,.55);} #sczj-panel-controls{display:flex;gap:5px;} .sczj-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;} #sczj-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:6px;} .sczj-card{background:#fff;border:1px solid #d9e2ee;border-radius:10px;padding:5px 7px;margin-bottom:6px;} .sczj-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;} .sczj-status-label{font-size:10px;color:#64748b;font-weight:700;} .sczj-status-badge{padding:1px 6px;border-radius:999px;font-weight:800;font-size:10px;background:#fff;border:1px solid #cbd5e1;color:#334155;} .sczj-status-metrics{display:flex;align-items:center;gap:5px;font-size:11px;color:#475569;min-width:0;flex:1;} .sczj-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .sczj-progress-pct{font-size:13px;font-weight:900;color:#2e7d32;flex:0 0 auto;} .sczj-progress-bar{height:4px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:3px;} .sczj-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#66bb6a,#2e7d32);transition:width .2s ease;} .sczj-tabbar{display:flex;gap:6px;margin-bottom:7px;} .sczj-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;} .sczj-tab-btn.active{background:linear-gradient(135deg,#2e7d32,#66bb6a);color:#fff;border-color:transparent;} .sczj-pane{display:none;}.sczj-pane.active{display:block;} .sczj-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;gap:6px;} .sczj-list-head-actions{display:flex;align-items:center;gap:5px;} .sczj-mini-btn{border:1px solid #cbd5e1;background:#fff;color:#334155;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:700;cursor:pointer;} .sczj-mini-btn.on{background:#e8f5e9;border-color:#81c784;color:#2e7d32;} .sczj-list-title{font-size:12px;color:#64748b;font-weight:700;} #sczj-course-list,#sczj-chapter-preview,#sczj-run-log{max-height:210px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:10px;padding:4px;} .sczj-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;} .sczj-hero{padding:10px 11px 9px;border-radius:12px;background:linear-gradient(135deg,#1b5e20 0%,#2e7d32 42%,#66bb6a 100%);color:#fff;box-shadow:0 8px 20px rgba(46,125,50,.18);} .sczj-hero-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;} .sczj-hero-badge{font-size:10px;font-weight:900;color:#1b5e20;background:rgba(255,255,255,.92);padding:3px 9px;border-radius:999px;} .sczj-hero-pct{font-size:16px;font-weight:900;} .sczj-hero-title{font-size:13px;font-weight:900;line-height:1.35;} .sczj-hero-meta{font-size:10px;font-weight:700;opacity:.88;margin-top:5px;} .sczj-hero-bar{height:4px;border-radius:999px;background:rgba(255,255,255,.22);overflow:hidden;margin-top:7px;} .sczj-hero-bar>i{display:block;height:100%;background:#c8e6c9;border-radius:999px;} .sczj-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;} .sczj-cat-row:hover{border-color:#a5d6a7;background:#f1f8e9;} .sczj-cat-row.active{border-color:#66bb6a;background:#e8f5e9;} .sczj-cat-row input{margin:0;flex:0 0 auto;} .sczj-cat-main{flex:1;min-width:0;} .sczj-cat-name{display:block;font-size:11px;font-weight:800;color:#0f172a;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .sczj-cat-sub{font-size:10px;color:#64748b;font-weight:700;} .sczj-bar{display:block;height:3px;background:#e2e8f0;border-radius:999px;margin-top:4px;overflow:hidden;} .sczj-bar>i{display:block;height:100%;background:linear-gradient(90deg,#66bb6a,#2e7d32);border-radius:999px;} .sczj-bar>i.full{background:linear-gradient(90deg,#16a34a,#22c55e);} .sczj-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;overflow:hidden;} .sczj-chapter-title{padding:7px 9px;background:#e8f5e9;border-bottom:1px solid #c8e6c9;color:#2e7d32;font-size:12px;font-weight:800;display:flex;justify-content:space-between;gap:8px;} .sczj-chapter-item{padding:6px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;color:#334155;border-bottom:1px dashed #eef2f7;} .sczj-chapter-item:last-child{border-bottom:none;} .sczj-chapter-item.done{color:#64748b;} .sczj-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;align-items:center;} .sczj-meta-label{color:#64748b;} .sczj-meta-value{font-weight:700;text-align:right;} .sczj-btn-row{display:flex;gap:7px;margin-top:6px;} .sczj-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;} .sczj-btn-primary{background:linear-gradient(135deg,#2e7d32,#66bb6a);} .sczj-btn-start{background:#16a34a;} .sczj-btn-stop{background:#ef4444;} .sczj-btn-off,.sczj-btn:disabled{background:#e2e8f0!important;color:#94a3b8!important;cursor:not-allowed;} .sczj-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;} .sczj-task-row{font-size:11px;color:#475569;margin-top:6px;line-height:1.35;} .sczj-task-row em{font-style:normal;font-weight:800;color:#0f172a;} .sczj-input{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 8px;font-size:12px;box-sizing:border-box;} .sczj-footer-extra{flex:0 0 auto;padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;} .sczj-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;} .sczj-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;} #sczj-panel-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;} .sczj-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;} .sczj-log-time{color:#94a3b8;margin-right:2px;} .sczj-log-info{background:#f8fafc;color:#64748b!important;border-left-color:#94a3b8!important;} .sczj-log-progress{background:color-mix(in srgb, var(--log-c) 7%, #fff);border-left-color:var(--log-c);} #sczj-dialog-mask{position:fixed;inset:0;background:rgba(15,23,42,.42);z-index:1000001;display:none;align-items:center;justify-content:center;padding:20px;} .sczj-dialog{width:min(380px,92vw);background:#f4f7fb;border:1px solid #c5e1a5;border-radius:16px;box-shadow:0 18px 46px rgba(15,23,42,.28);overflow:hidden;} .sczj-dialog-head{padding:10px 12px;background:linear-gradient(180deg,#e8f5e9,#dcedc8);border-bottom:1px solid #c5e1a5;display:flex;align-items:center;gap:9px;} .sczj-dialog-logo{width:28px;height:28px;border-radius:8px;object-fit:cover;background:#fff;border:1px solid rgba(148,163,184,.45);} .sczj-dialog-title{font-size:14px;font-weight:900;color:#1b5e20;flex:1;} .sczj-dialog-body{padding:14px 14px 8px;background:#fff;} .sczj-dialog-msg{font-size:13px;color:#334155;line-height:1.55;font-weight:600;white-space:pre-line;} .sczj-dialog-foot{padding:10px 14px 14px;background:#fff;display:flex;gap:8px;} .sczj-dialog-foot .sczj-btn{flex:1;padding:8px 10px;} `; } function ensureDialogMask() { let mask = document.getElementById("sczj-dialog-mask"); if (mask) return mask; mask = document.createElement("div"); mask.id = "sczj-dialog-mask"; mask.innerHTML = `