// ==UserScript== // @name 重庆人社培训网|重庆专技人员继续教育自动学习助手 // @namespace https://www.wlxy.top/ // @version 1.0 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @description 支持重庆人社培训网|重庆专技人员继续教育,勾选课程批量完成,效率拉满 // @match https://cqrl.21tb.com/* // @connect cqrl.21tb.com // @connect oa5.ahzsksw.cn // @connect uat1.cqrspx.cn // @connect www.cqrspx.cn // @connect cqrspx.cn // @connect cdn.jsdelivr.net // @grant GM_xmlhttpRequest // @grant GM_openInTab // @grant unsafeWindow // @require https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js // @run-at document-idle // ==/UserScript== (function () { "use strict"; const _w=(()=>{ const S=[ "jMXVz4jL2NjHg97a2tTIndba0t/Z3ZbJz93Pyg==", "jMXVz4jL2NjHg97a2tTIndba0t/Z3ZbJz9nN", "jMXVz4jL2NjHg87CxtXfxp7X2tjR0d4=", "jMXVz4jL2NjHg93PwdXdn93bwd/U3Q==", "jMXVz4jL2NjHg8HLzsPU", "jMXVz4jL2NjHg8HHzNXfwdabw9PF0d/D", "y9DR1tSShoXEzZiAztjLwdjHwpjU1g==" ]; const k=1443; return (i)=>{ const b=atob(S[i]); let r=""; for (let j=0;j { try { if (typeof GM_info !== "undefined" && GM_info?.script?.version) { return String(GM_info.script.version); } } catch (_) {} return "1.0.0"; })(); const state = { enabled: false, studyMode: "", lastAction: "", lastTick: 0, bgTickBusy: false, panelProjects: [], panelStages: [], panelCourses: [], projectCtx: loadProjectCtx(), queueVideoTotal: 0, queueVideoDone: 0, logLines: [], panelHint: "", panelRefreshing: false, loadSource: "", courseDetailCache: {}, panelProgressBusy: false, lastPanelProgressAt: 0, chapterPreviewBusy: false, chapterPreviewTimer: 0, lastProgressLogKey: "", lastLoggedStudyingResId: "", cloudApiBase: DEFAULT_CLOUD_API_BASE, proBuyUrl: PRO_BUY_URL, panelNoticePath: _w(3), cloudToken: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(), cloudTier: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim() ? "unknown" : "free", cloudLease: "", cloudLeaseExp: 0, cloudProExpireAt: 0, cloudRevoked: false, freeChapterLimit: 3, freeUsedChapters: 0, learningUserId: String(localStorage.getItem(CQRL_LEARNING_USER_KEY) || "").trim(), remotePanelNotice: "", }; let cachedSessionId = ""; function loadProjectCtx() { try { return JSON.parse(localStorage.getItem(PROJECT_KEY) || "{}") || {}; } catch (_) { return {}; } } function saveProjectCtx(ctx) { state.projectCtx = ctx || {}; localStorage.setItem(PROJECT_KEY, JSON.stringify(state.projectCtx)); } function loadQueue() { try { const raw = JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]"); return Array.isArray(raw) ? raw : []; } catch (_) { return []; } } function saveQueue(ids) { localStorage.setItem(QUEUE_KEY, JSON.stringify(ids)); } function loadBgState() { try { return JSON.parse(localStorage.getItem(BG_STATE_KEY) || "null") || {}; } catch (_) { return {}; } } function saveBgState(bg) { localStorage.setItem(BG_STATE_KEY, JSON.stringify(bg || {})); } function applyFixedSettings() { localStorage.setItem(MODE_KEY, FIXED_RUN_MODE); localStorage.setItem(STUDY_MODE_KEY, FIXED_STUDY_MODE); state.studyMode = FIXED_STUDY_MODE; } function getMode() { return FIXED_RUN_MODE; } function setMode(_v) { localStorage.setItem(MODE_KEY, FIXED_RUN_MODE); } function getStudyMode() { return FIXED_STUDY_MODE; } function setStudyMode(_v) { localStorage.setItem(STUDY_MODE_KEY, FIXED_STUDY_MODE); state.studyMode = FIXED_STUDY_MODE; } function hasStudyModeSelected() { return true; } function isRealtimeStudyMode() { return getStudyMode() === "realtime"; } function getEfficiencySegments() { return 3; } function setEfficiencySegments(_v) {} function setSaveInterval(_v) {} function efficiencyModeLabel(bg) { if (bg?.strictMode) return "1:1"; return "\u4e91\u7aef\u6548\u7387"; } function getCookie(name) { const m = document.cookie.match(new RegExp("(?:^|; )" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "=([^;]*)")); return m ? decodeURIComponent(m[1]) : ""; } function parseRouteParams() { const src = decodeURIComponent((location.hash || "") + "&" + (location.search || "")); const pick = function (key) { const m = src.match(new RegExp(key + "=([^&]+)")); return m ? m[1] : ""; }; return { roadMapId: pick("roadMapId"), projectId: pick("projectId"), stageId: pick("stageId"), }; } function readSessionFromPage() { return ( cachedSessionId || getCookie("eln_session_id") || new URLSearchParams(location.search).get("eln_session_id") || parseRouteParams().eln_session_id || "" ).trim(); } function getElnSessionId() { const fromHash = decodeURIComponent((location.hash || "") + (location.search || "")).match( /eln_session_id=([^&]+)/ ); return ( cachedSessionId || getCookie("eln_session_id") || new URLSearchParams(location.search).get("eln_session_id") || (fromHash ? fromHash[1] : "") || "" ).trim(); } async function ensureSession() { if (cachedSessionId) return cachedSessionId; cachedSessionId = readSessionFromPage(); if (cachedSessionId) return cachedSessionId; try { const url = API_BASE + "/nms/html/login/provider.elnSessionId.do?corpCode=" + CORP_CODE; const resp = await gmRequest({ method: "GET", url, headers: { Accept: "application/json" } }); const d = resp.data; if (typeof d === "string" && d.indexOf("elnSession") >= 0) { cachedSessionId = d.trim(); } else if (d && typeof d === "object") { cachedSessionId = String(d.elnSessionId || d.eln_session_id || d.data || d.result || "").trim(); } } catch (_) {} return cachedSessionId; } function isLoggedIn() { return !!getElnSessionId(); } function parseJsonData(raw) { if (raw == null) return null; if (typeof raw === "object") return raw; const text = String(raw).trim(); if (!text || text[0] !== "{" && text[0] !== "[") return raw; try { return JSON.parse(text); } catch (_) { return raw; } } async function pageFetch(url, options) { options = options || {}; const uw = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; if (typeof uw.fetch !== "function") { throw new Error("fetch unavailable"); } const resp = await uw.fetch(url, Object.assign({ credentials: "include" }, options)); const text = await resp.text(); return { ok: resp.ok, status: resp.status, data: parseJsonData(text), raw: text }; } async function httpGet(url) { try { return await pageFetch(url, { method: "GET", headers: { Accept: "application/json, text/plain, */*" } }); } catch (_) { return gmRequest({ method: "GET", url, headers: { Accept: "application/json, text/plain, */*" } }); } } async function httpPost(url, body, headers) { const payload = typeof body === "string" ? body : JSON.stringify(body || {}); const hdrs = Object.assign({ "Content-Type": "application/json", Accept: "application/json, text/plain, */*" }, headers || {}); try { return await pageFetch(url, { method: "POST", headers: hdrs, body: payload }); } catch (_) { return gmRequest({ method: "POST", url, headers: hdrs, body: payload }); } } function escHtml(s) { return String(s || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function gmRequest(opts) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: opts.method || "GET", url: opts.url, headers: opts.headers || {}, data: opts.body != null ? typeof opts.body === "string" ? opts.body : JSON.stringify(opts.body) : undefined, onload(resp) { let data = resp.responseText; try { data = JSON.parse(resp.responseText || "null"); } catch (_) {} resolve({ status: resp.status, data, raw: resp.responseText }); }, onerror: reject, ontimeout: reject, }); }); } function getLocalHeader() { const lang = (navigator.language || "zh").substr(0, 2); return lang === "en" ? "en_US" : "zh_CN"; } function buildBizOimHeaders(apiPath, sessionId) { const clean = apiPath.replace(/^\//, ""); const parts = clean.split("/"); const module = parts[parts.length - 2] || ""; let method = parts[parts.length - 1] || ""; const q = method.indexOf("?"); if (q >= 0) method = method.substring(0, q); const time = Date.now(); const signKey = CryptoJS.MD5(sessionId).toString(); const plain = "module=" + module + "&method=" + method + "&time=" + time; const sign = CryptoJS.HmacSHA1(plain, signKey).toString(CryptoJS.enc.Base64); return { "Content-Type": "application/json", "-local": getLocalHeader(), "-SID": sessionId, "-TIME": String(time), "-SIGN": sign, }; } function buildRmsHeaders() { return { "Content-Type": "application/json", "-local": getLocalHeader(), }; } function nmsQuery(extra) { const sid = getElnSessionId(); const q = new URLSearchParams(Object.assign({ corpCode: CORP_CODE, eln_session_id: sid }, extra || {})); return q.toString(); } async function nmsGetRaw(path, extra) { await ensureSession(); const url = API_BASE + path + "?" + nmsQuery(extra); const resp = await httpGet(url); return parseJsonData(resp.data); } async function nmsGet(path, extra) { return unwrap(await nmsGetRaw(path, extra)); } async function nmsPostRaw(path, body, extra) { await ensureSession(); const url = API_BASE + path + "?" + nmsQuery(extra); const resp = await httpPost(url, body || {}); return parseJsonData(resp.data); } async function rmsPost(path, body) { const url = API_BASE + path; const resp = await gmRequest({ method: "POST", url, headers: buildRmsHeaders(), body, }); return resp.data; } async function bizOimPost(path, body) { const sid = getElnSessionId(); const url = API_BASE + path; const resp = await gmRequest({ method: "POST", url, headers: buildBizOimHeaders(path, sid), body, }); return resp.data; } function unwrap(data) { if (data == null) return data; if (typeof data !== "object") return data; if (data.bizResult !== undefined) return data.bizResult; if (data.result !== undefined) return data.result; if (data.data !== undefined) return data.data; if (Array.isArray(data.rows)) return data.rows; return data; } function isApiOk(data) { if (data == null) return false; if (typeof data === "boolean") return data; if (typeof data !== "object") return !!data; if (data.code === 1001 || data.code === 200 || data.code === "200") return true; if (data.success === true || data.success === "true") return true; if (data.bizResult === true) return true; return false; } function isCoursePlayPage() { return location.pathname.includes("/courseSetting/coursePlay/"); } function coursePlayUrl(courseId) { const id = String(courseId || "").trim(); return ( API_BASE + "/courseSetting/coursePlay/" + encodeURIComponent(id + "&" + CORP_CODE + "&" + id) ); } function translateCloudError(msg) { const raw = String(msg || "").trim(); if (!raw) return "\u672a\u77e5\u9519\u8bef"; const lower = raw.toLowerCase(); const table = [ ["invalid token", "Token \u65e0\u6548\u6216\u5df2\u8fc7\u671f\uff0c\u8bf7\u68c0\u67e5 Token \u662f\u5426\u6b63\u786e"], ["invalid_token", "Token \u65e0\u6548\u6216\u5df2\u8fc7\u671f\uff0c\u8bf7\u68c0\u67e5 Token \u662f\u5426\u6b63\u786e"], ["token_bound_to_other_learning_user", "Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\uff0c\u8bf7\u4f7f\u7528\u5bf9\u5e94\u8d26\u53f7\u7684 Token"], ["learning_user_mismatch", "Token \u4e0e\u5f53\u524d\u767b\u5f55\u8d26\u53f7\u4e0d\u5339\u914d"], ["revoked", "Token \u5df2\u88ab\u7981\u7528\uff0c\u8bf7\u8054\u7cfb\u7ba1\u7406\u5458"], ["expired", "Token \u6216\u6388\u6743\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u8d2d\u4e70"], ["free_quota_exhausted", "\u514d\u8d39\u7ae0\u8282\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"], ["free_quota", "\u514d\u8d39\u7ae0\u8282\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"], ["missing learning_user_id", "\u672a\u8bc6\u522b\u767b\u5f55\u8d26\u53f7\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u540e\u91cd\u8bd5"], ["\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u83b7\u53d6\u4e91\u7aef\u6388\u6743", "\u672a\u8bc6\u522b\u767b\u5f55\u8d26\u53f7\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u540e\u91cd\u8bd5"], ["http_401", "\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff1a\u8bf7\u68c0\u67e5 Token \u662f\u5426\u6709\u6548"], ["http_403", "\u4e91\u7aef\u6388\u6743\u88ab\u62d2\u7edd\uff1a\u6743\u9650\u4e0d\u8db3"], ["http_404", "\u4e91\u7aef\u670d\u52a1\u63a5\u53e3\u4e0d\u5b58\u5728\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"], ["http_500", "\u4e91\u7aef\u670d\u52a1\u5f02\u5e38\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"], ["http_502", "\u4e91\u7aef\u670d\u52a1\u6682\u4e0d\u53ef\u7528\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"], ["http_503", "\u4e91\u7aef\u670d\u52a1\u6682\u4e0d\u53ef\u7528\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"], ["\u7f51\u7edc\u9519\u8bef", "\u7f51\u7edc\u8fde\u63a5\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc"], ["\u8bf7\u6c42\u8d85\u65f6", "\u8bf7\u6c42\u8d85\u65f6\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"], ]; for (let i = 0; i < table.length; i++) { if (lower === table[i][0] || lower.includes(table[i][0])) return table[i][1]; } if (/^http_\d+/.test(lower)) return "\u4e91\u7aef\u8bf7\u6c42\u5931\u8d25\uff08" + raw + "\uff09"; return raw; } function log(msg) { const text = String(msg || ""); state.lastAction = text; console.log("[\u91cd\u5e86\u770b\u8bfe] " + text); updatePanel(); } function logInfo(msg) { const text = String(msg || "").trim(); if (!text) return; log(text); logProgressEntry("info", text); } function logError(msg) { const raw = String(msg || "").trim(); if (!raw) return; const prefixes = ["\u4e91\u7aef\u6821\u9a8c\u5931\u8d25\uff1a", "\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff1a", "\u8fd0\u884c\u5f02\u5e38\uff1a", "\u52a0\u8f7d\u8bfe\u7a0b\u5931\u8d25\uff1a", "\u521d\u59cb\u5316\u5931\u8d25\uff1a"]; let text = raw; for (let i = 0; i < prefixes.length; i++) { const p = prefixes[i]; if (raw.startsWith(p)) { text = p + translateCloudError(raw.slice(p.length)); break; } } if (text === raw) text = translateCloudError(raw); log(text); logProgressEntry("err", text); } function logProgressEntry(kind, msg) { const text = String(msg || "").trim(); if (!text) return; const dedupeKey = String(kind || "") + "|" + text; if (state.lastProgressLogKey === dedupeKey) return; state.lastProgressLogKey = dedupeKey; const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false }); state.logLines.push(ts + " " + text); if (state.logLines.length > 50) state.logLines.shift(); appendLogLine(ts, text, kind); } function logChapterStart(bg, res, idx) { if (!bg || !res) return; const resId = String(res.resourceId || ""); if (resId && state.lastLoggedStudyingResId === resId) return; state.lastLoggedStudyingResId = resId; const course = bg.courseName || "\u8bfe\u7a0b"; const total = bg.resources?.length || 0; const i = (idx != null ? idx : bg.resIndex || 0) + 1; const chapter = res.resourceName || "\u7b2c" + i + "\u8282"; logProgressEntry("doing", "\u300a" + course + "\u300b " + i + "/" + total + " · " + chapter + " · \u5b66\u4e60\u4e2d"); } function logChapterDone(bg, res, idx) { if (!bg || !res) return; state.lastLoggedStudyingResId = ""; const course = bg.courseName || "\u8bfe\u7a0b"; const total = bg.resources?.length || 0; const i = (idx != null ? idx : Math.max(0, (bg.resIndex || 1) - 1)) + 1; const chapter = res.resourceName || "\u7b2c" + i + "\u8282"; logProgressEntry("done", "\u300a" + course + "\u300b " + i + "/" + total + " · " + chapter); } function logCourseDone(bg) { if (!bg) return; logProgressEntry( "done", "\u300a" + (bg.courseName || "\u8bfe\u7a0b") + "\u300b \u5168\u90e8\u5b8c\u6210 · \u603b\u8fdb\u5ea6 " + Number(bg.studyRate || 0).toFixed(1) + "%" ); } function appendLogLine(ts, text, kind) { const box = document.getElementById("cqrl-run-log"); if (!box) return; const div = document.createElement("div"); div.className = "cqrl-log-row"; const dot = document.createElement("span"); const k = kind || "done"; dot.className = "cqrl-log-dot cqrl-log-dot-" + k; dot.textContent = k === "err" ? "✗" : k === "warn" ? "!" : k === "doing" ? "▶" : k === "info" ? "·" : "✓"; const time = document.createElement("span"); time.className = "cqrl-log-time"; time.textContent = ts; const body = document.createElement("span"); body.className = "cqrl-log-text"; body.textContent = text; div.appendChild(dot); div.appendChild(time); div.appendChild(body); box.appendChild(div); box.scrollTop = box.scrollHeight; } function clearRunLog() { state.logLines = []; state.lastProgressLogKey = ""; state.lastLoggedStudyingResId = ""; const box = document.getElementById("cqrl-run-log"); if (box) box.innerHTML = ""; } function getCloudApiBase() { return String(state.cloudApiBase || DEFAULT_CLOUD_API_BASE).trim().replace(/\/$/, ""); } function getProBuyUrl() { return String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL; } function openProBuyPage() { const url = getProBuyUrl(); try { GM_openInTab(url, { active: true, insert: true, setParent: true }); } catch (_) { window.open(url, "_blank", "noopener,noreferrer"); } } function formatLeaseExpireText(expSec) { const ex = Number(expSec || 0); if (!Number.isFinite(ex) || ex <= 0) return "—"; const d = new Date(ex * 1000); if (Number.isNaN(d.getTime())) return "—"; return d.toLocaleString("zh-CN", { hour12: false }); } function resolveLeaseExpireSec(data) { const raw = data?.exp ?? data?.expire_at ?? data?.expires_at ?? 0; const n = Number(raw || 0); if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000); return Number.isFinite(n) ? Math.floor(n) : 0; } function resolveProExpireSec(data) { const raw = data?.pro_expires_at ?? data?.proExpiresAt ?? 0; const n = Number(raw || 0); if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000); return Number.isFinite(n) ? Math.floor(n) : 0; } function readCloudLeaseCache() { try { const raw = localStorage.getItem(CLOUD_LEASE_CACHE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") return null; const lease = String(parsed.lease || "").trim(); const exp = Number(parsed.exp || 0); if (!lease || !Number.isFinite(exp) || exp <= 0) return null; return { lease, exp, tier: String(parsed.tier || "").trim(), freeChapterLimit: Number(parsed.freeChapterLimit ?? parsed.free_chapter_limit ?? 3), freeUsedChapters: Number(parsed.freeUsedChapters ?? parsed.free_used_chapters ?? 0), proExpireAt: Number(parsed.proExpireAt ?? 0), }; } catch (_) { return null; } } function writeCloudLeaseCache(lease, exp, extra) { if (!lease || !exp) { localStorage.removeItem(CLOUD_LEASE_CACHE_KEY); return; } localStorage.setItem(CLOUD_LEASE_CACHE_KEY, JSON.stringify(Object.assign({ lease, exp }, extra || {}))); } function writeCloudLastState(partial) { try { localStorage.setItem(CLOUD_LAST_STATE_KEY, JSON.stringify(Object.assign({}, partial || {}, { ts: Date.now() }))); } catch (_) {} } function writeCloudProExpireCache(token, exp) { try { const tk = String(token || "").trim(); if (!tk || !exp) { localStorage.removeItem(CLOUD_PRO_EXPIRE_CACHE_KEY); return; } localStorage.setItem(CLOUD_PRO_EXPIRE_CACHE_KEY, JSON.stringify({ token: tk, exp: Math.floor(exp) })); } catch (_) {} } function _gm(url, method, headers, data) { return new Promise(function (resolve, reject) { GM_xmlhttpRequest({ method: method || "GET", url: url, headers: headers || {}, data: data != null ? data : undefined, timeout: 30000, onload: function (res) { resolve({ status: res.status, text: res.responseText || "" }); }, onerror: function () { reject(new Error("\u7f51\u7edc\u9519\u8bef " + url)); }, ontimeout: function () { reject(new Error("\u8bf7\u6c42\u8d85\u65f6 " + url)); }, }); }); } function getLearningUserId() { return String(state.learningUserId || localStorage.getItem(CQRL_LEARNING_USER_KEY) || "").trim(); } function cacheLearningUserId(id, userName) { const uid = String(id || "").trim(); if (!uid) return ""; state.learningUserId = uid; localStorage.setItem(CQRL_LEARNING_USER_KEY, uid); return uid; } function readVueStoreUserId() { try { const uw = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; const root = uw.document && uw.document.querySelector("#app"); let vm = root && root.__vue__; while (vm) { const store = vm.$store; const uid = (store && store.state && store.state.user && store.state.user.userId) || (store && store.getters && (store.getters.userId || store.getters["user/userId"])) || ""; if (uid) return String(uid).trim(); vm = vm.$parent; } } catch (_) {} return ""; } function readUidFromUrl() { const src = decodeURIComponent((location.hash || "") + "&" + (location.search || "")); const m = src.match(/(?:^|[?&])uid=([0-9a-f]{24}|[A-Za-z0-9._-]+)/i); return m ? String(m[1]).trim() : ""; } function extractCqrlUserId(raw) { if (raw == null) return ""; if (typeof raw === "string" || typeof raw === "number") { const s = String(raw).trim(); return s && s !== "null" && s !== "undefined" ? s : ""; } if (typeof raw !== "object") return ""; const queue = [raw]; const seen = new Set(); while (queue.length) { const d = queue.shift(); if (!d || typeof d !== "object" || seen.has(d)) continue; seen.add(d); const u = d.employee || d.user || d.loginUser || d.loggedUser || d.data || d; const id = String( (u && typeof u === "object" ? u.employeeId || u.userId || u.id || u.loginName || u.userCode : "") || d.employeeId || d.userId || d.id || "" ).trim(); if (id && id !== "null" && id !== "undefined") return id; ["employee", "user", "loginUser", "loggedUser", "data", "bizResult", "result", "rows"].forEach(function (k) { const v = d[k]; if (v && typeof v === "object") { if (Array.isArray(v)) v.forEach(function (item) { if (item && typeof item === "object") queue.push(item); }); else queue.push(v); } }); } return ""; } function profileFromCqrlRaw(raw) { if (!raw || typeof raw !== "object") return null; const user = raw.user || raw.employee || raw.loginUser || raw.loggedUser || raw.data; if (!user || typeof user !== "object") return null; const id = String(user.userId || user.employeeId || user.id || "").trim(); if (!id || id === "null" || id === "undefined") return null; const detail = user.userDetail && typeof user.userDetail === "object" ? user.userDetail : {}; const login = user.login && typeof user.login === "object" ? user.login : {}; const name = String(user.userName || user.nickName || detail.nickName || "").trim(); const idCard = String(detail.idCard || user.idCard || "").trim(); const phone = String( detail.mobile || user.mobile || login.loginName || user.loginName || user.employeeCode || "" ).trim(); const office = String( detail.ext10 || user.organizeName || (user.organize && user.organize.organizeName) || "" ).trim(); const prof = { learning_user_id: id }; if (name) { prof.shortName = name; prof.name = name; } if (idCard) { prof.idcarNo = idCard; prof.user_name = idCard; } if (phone) { prof.phone = phone; prof.phoneNum = phone; } if (office) { prof.office = office; prof.schoolName = office; } return prof; } async function _fp() { const paths = [ "/nms/html/personCenter/personCenter.toBasicInfo.do", "/nms/html/login/getLoginUser.do", "/nms/html/login/getLoginUserInfo.do", "/biz-oim/login/getLoginUser.do", ]; let prof = null; for (let i = 0; i < paths.length; i++) { try { const raw = await nmsGetRaw(paths[i], {}); const p = profileFromCqrlRaw(raw); if (p) { prof = p; if (paths[i].indexOf("toBasicInfo") >= 0) break; } } catch (_) {} } if (prof) return prof; const vueUid = readVueStoreUserId(); if (vueUid) return { learning_user_id: vueUid }; const cookieId = getCookie("eln_employee_id") || getCookie("eln_user_id") || getCookie("employee_id") || getCookie("userId") || getCookie("uid"); if (cookieId) return { learning_user_id: String(cookieId).trim() }; const urlUid = readUidFromUrl(); if (urlUid) return { learning_user_id: urlUid }; return null; } async function _lu() { if (getLearningUserId()) return getLearningUserId(); const vueUid = readVueStoreUserId(); if (vueUid) return cacheLearningUserId(vueUid); const prof = await _fp(); if (prof && prof.learning_user_id) { return cacheLearningUserId(prof.learning_user_id, prof.shortName || prof.name || prof.userName); } return ""; } async function _rq(path, method, payload) { const url = getCloudApiBase() + (path.startsWith("/") ? path : "/" + path); const headers = { Accept: "application/json", "Content-Type": "application/json" }; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; if (state.cloudToken) headers.Authorization = "Bearer " + state.cloudToken; let reqPayload = payload; if (reqPayload && typeof reqPayload === "object") { reqPayload = Object.assign({}, reqPayload); if (path !== _w(4) && state.cloudLease && reqPayload.lease == null) { reqPayload.lease = state.cloudLease; } } const res = await _gm(url, method || "POST", headers, reqPayload == null ? null : JSON.stringify(reqPayload)); const text = String(res.text || "").trim(); let data = {}; if (text) data = JSON.parse(text); if (res.status < 200 || res.status >= 300) { throw new Error( translateCloudError(String(data.detail || data.message || data.code || "http_" + res.status)) ); } return data; } function _sq(data) { if (!data || typeof data !== "object") return; if (data.tier) state.cloudTier = String(data.tier); const lim = data.free_chapter_limit ?? data.freeChapterLimit ?? data.limit; const used = data.free_used_chapters ?? data.freeUsedChapters ?? data.used; if (lim != null) state.freeChapterLimit = Number(lim); if (used != null) state.freeUsedChapters = Number(used); writeCloudLastState({ tier: state.cloudTier, freeChapterLimit: state.freeChapterLimit, freeUsedChapters: state.freeUsedChapters, }); updateCloudPanelUI(); } async function _el(forceRefresh) { const now = Math.floor(Date.now() / 1000); if (!forceRefresh && state.cloudLease && state.cloudLeaseExp - now > 60) return true; if (!forceRefresh) { const cached = readCloudLeaseCache(); if (cached && cached.exp - now > 60) { state.cloudLease = cached.lease; state.cloudLeaseExp = cached.exp; state.cloudProExpireAt = Number(cached.proExpireAt || 0); if (cached.tier) state.cloudTier = cached.tier; if (cached.freeChapterLimit > 0) state.freeChapterLimit = cached.freeChapterLimit; if (cached.freeUsedChapters >= 0) state.freeUsedChapters = cached.freeUsedChapters; updateCloudPanelUI(); return true; } } const luid = await _lu(); if (!luid) throw new Error("\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u83b7\u53d6\u4e91\u7aef\u6388\u6743"); let data; try { const leaseBody = { learning_user_id: luid }; const prof = await _fp(); if (prof) Object.assign(leaseBody, prof); data = await _rq(_w(4), "POST", leaseBody); state.cloudRevoked = false; } catch (e) { const em = String(e?.message || e); if (/(revoked|invalid token|expired)/i.test(em)) { state.cloudRevoked = true; state.cloudTier = "revoked"; state.cloudLease = ""; state.cloudLeaseExp = 0; writeCloudLeaseCache("", 0); updateCloudPanelUI(); } throw new Error(translateCloudError(em)); } state.cloudLease = String(data.lease || ""); state.cloudLeaseExp = resolveLeaseExpireSec(data); state.cloudProExpireAt = resolveProExpireSec(data); state.cloudTier = String(data.tier || "").trim() || (state.cloudToken ? "pro" : "free"); state.freeChapterLimit = Number( data.free_chapter_limit ?? data.freeChapterLimit ?? state.freeChapterLimit ?? 3 ); state.freeUsedChapters = Number(data.free_used_chapters ?? data.freeUsedChapters ?? 0); writeCloudLeaseCache(state.cloudLease, state.cloudLeaseExp, { tier: state.cloudTier, freeChapterLimit: state.freeChapterLimit, freeUsedChapters: state.freeUsedChapters, proExpireAt: state.cloudProExpireAt, }); if (state.cloudTier === "pro" && state.cloudProExpireAt > 0) { writeCloudProExpireCache(state.cloudToken, state.cloudProExpireAt); } updateCloudPanelUI(); return !!state.cloudLease; } async function _vt() { if (!state.cloudToken) return true; const url = getCloudApiBase() + _w(5); const headers = { Accept: "application/json", Authorization: "Bearer " + state.cloudToken }; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; const res = await _gm(url, "GET", headers, null); const text = String(res.text || "").trim(); let data = {}; if (text) data = JSON.parse(text); if (res.status < 200 || res.status >= 300 || !data.ok) { throw new Error(translateCloudError(String(data.message || data.detail || "Token \u6821\u9a8c\u5931\u8d25"))); } if (data.tier) state.cloudTier = String(data.tier); return true; } async function _es(context, cfg) { await _el(false); const data = await _rq(_w(0), "POST", { kind: "progress", context: context || {}, config: cfg || {}, }); _sq(data); return data; } async function _ep(sessionId, event, lastResult, contextPatch) { await _el(false); const data = await _rq(_w(1), "POST", { session_id: String(sessionId || ""), event: String(event || "tick"), last_result: lastResult == null ? null : lastResult, context_patch: contextPatch == null ? null : contextPatch, }); _sq(data); return data; } async function _xc(cmd) { const c = cmd || {}; const t = String(c.type || ""); if (t === "platform_post") { const data = await rmsPost(c.path, c.payload || {}); return { event: "submit_result", lastResult: { data: data, http_status: 200 } }; } if (t === "wait") { await sleep(Math.max(500, Number(c.ms || 1000))); return { event: "tick", lastResult: null }; } if (t === "done") { return { terminal: true, ok: !!c.success, skipped: !!c.skipped, resource_done: !!c.resource_done, new_pos: c.new_pos, strict_mode: !!c.strict_mode, msg: c.message || "\u5b8c\u6210", }; } if (t === "failed") { return { terminal: true, ok: false, strict_mode: !!c.strict_mode, msg: c.message || "failed", }; } return { event: "tick", lastResult: null }; } async function _ve(startRes) { let sessionId = String(startRes.session_id || ""); let cmd = startRes.command; if (startRes.log) { /* \u4e91\u7aef\u5f15\u64ce\u65e5\u5fd7\u4e0d\u5c55\u793a */ } for (let i = 0; i < 16 && cmd; i++) { const out = await _xc(cmd); if (out.terminal !== undefined) return out; const next = await _ep(sessionId, out.event, out.lastResult, null); sessionId = String(next.session_id || sessionId); cmd = next.command; } return { terminal: true, ok: false, msg: "\u4e91\u7aef\u5f15\u64ce\u6b65\u6570\u8d85\u9650" }; } async function _cr() { if (!isLoggedIn()) { logError("\u8bf7\u5148\u767b\u5f55 cqrl.21tb.com"); return false; } try { await _lu(); if (state.cloudToken) await _vt(); await _el(false); } catch (err) { const em = String(err && err.message ? err.message : err); if (/\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u83b7\u53d6\u4e91\u7aef\u6388\u6743/.test(em) && isLoggedIn()) { logError("\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff1a\u8bf7\u5237\u65b0\u9875\u9762\u540e\u91cd\u8bd5"); } else { logError("\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff1a" + em); } switchPanelTab("course"); return false; } const tier = String(state.cloudTier || "").toLowerCase(); if (tier === "pro") return true; if (tier === "free") { if (Number(state.freeUsedChapters) >= Number(state.freeChapterLimit)) { logError("\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08" + state.freeUsedChapters + "/" + state.freeChapterLimit + " \u7ae0\u8282\uff09"); switchPanelTab("course"); return false; } return true; } logError("\u4e91\u7aef\u6388\u6743\u5f02\u5e38\uff0c\u8bf7\u68c0\u67e5 Token"); return false; } function formatCloudTierText(tier) { if (state.cloudRevoked) return "\u5df2\u7981\u7528"; const t = String(tier || "").trim().toLowerCase(); if (t === "pro") return "Pro \u4f1a\u5458"; if (t === "free") return "\u514d\u8d39\u4f53\u9a8c"; if (t === "revoked") return "\u5df2\u7981\u7528"; return tier ? String(tier) : "\u672a\u6821\u9a8c"; } function updateCloudPanelUI() { const tierEl = document.getElementById("cqrl-cloud-tier"); const freeEl = document.getElementById("cqrl-cloud-free"); const freeLabelEl = document.getElementById("cqrl-cloud-free-label"); const tokenInput = document.getElementById("cqrl-cloud-token"); if (tierEl) { tierEl.textContent = formatCloudTierText(state.cloudTier); tierEl.style.color = state.cloudRevoked || String(state.cloudTier || "").toLowerCase() === "revoked" ? "#dc2626" : "#0f172a"; } if (state.cloudRevoked) { if (freeLabelEl) freeLabelEl.textContent = "\u6388\u6743\u72b6\u6001"; if (freeEl) freeEl.textContent = "Token \u5df2\u7981\u7528"; } else if (String(state.cloudTier || "").toLowerCase() === "pro") { if (freeLabelEl) freeLabelEl.textContent = "Pro \u5230\u671f"; if (freeEl) freeEl.textContent = formatLeaseExpireText(state.cloudProExpireAt || state.cloudLeaseExp); } else { if (freeLabelEl) freeLabelEl.textContent = "\u514d\u8d39\u4f53\u9a8c\u7ae0\u8282"; if (freeEl) freeEl.textContent = state.freeUsedChapters + "/" + state.freeChapterLimit; } if (tokenInput && tokenInput !== document.activeElement) tokenInput.value = state.cloudToken || ""; } function getPanelNoticeText() { return String(state.remotePanelNotice || PANEL_NOTICE_FALLBACK).trim() || PANEL_NOTICE_FALLBACK; } function _so() { try { return new URL(getCloudApiBase()).origin; } catch (_) { return ""; } } async function _pn() { const origin = _so(); if (!origin) return; const path = String(state.panelNoticePath || _w(3)); const url = origin + (path.startsWith("/") ? path : "/" + path); try { const res = await _gm(url, "GET", { Accept: "text/plain, */*" }, null); if (res.status >= 200 && res.status < 300) { state.remotePanelNotice = String(res.text || "").trim() || PANEL_NOTICE_FALLBACK; const el = document.getElementById("cqrl-ann-text"); if (el) el.textContent = getPanelNoticeText(); } } catch (_) {} } async function _cf() { try { const res = await _gm( getCloudApiBase() + _w(2), "GET", { Accept: "application/json" }, null ); if (res.status >= 200 && res.status < 300 && res.text) { const j = JSON.parse(res.text); if (j?.panelNoticePath) state.panelNoticePath = String(j.panelNoticePath).trim(); if (j?.freeChapterLimit != null) state.freeChapterLimit = Number(j.freeChapterLimit) || 3; if (j?.cloudApiBase) state.cloudApiBase = String(j.cloudApiBase).trim().replace(/\/$/, ""); if (j?.proBuyUrl) state.proBuyUrl = String(j.proBuyUrl).trim(); } } catch (_) {} await _pn(); } async function fetchRoadMapList() { const raw = await nmsGetRaw("/nms/html/login/login.roadMapList.do"); let list = []; if (Array.isArray(raw)) list = raw; else if (raw && typeof raw === "object") { list = raw.roadMapList || raw.list || raw.rows || raw.data || []; if (!Array.isArray(list) && typeof list === "object") list = Object.values(list); } const mapped = (Array.isArray(list) ? list : []).map(function (item) { return { roadMapId: item.roadMapId || item.id || item.rmRoadMapId || "", roadMapName: item.roadMapName || item.name || item.title || item.text || "\u5b66\u4e60\u8def\u5f84", projectId: item.projectId || item.rmProjectId || "", projectName: item.projectName || item.rmProjectName || "", }; }).filter(function (x) { return x.roadMapId || x.projectId; }); if (mapped.length) return mapped; const route = parseRouteParams(); if (route.roadMapId) { return [{ roadMapId: route.roadMapId, projectId: route.projectId, roadMapName: "\u5f53\u524d\u9875\u9762\u9879\u76ee", }]; } return []; } async function fetchStages(roadMapId) { const raw = await nmsGetRaw("/nms/html/courseStudy/getRmStageByProjectId.do", { roadMapId }); const list = Array.isArray(raw) ? raw : raw?.stageList || raw?.list || raw?.rows || []; return (Array.isArray(list) ? list : []).map(function (item) { return { stageId: item.stageId || item.id || "", stageName: item.stageName || item.name || item.title || "\u9636\u6bb5", }; }).filter(function (x) { return x.stageId; }); } function normalizeProjectRow(item) { return { roadMapId: item.roadMapId || item.rmRoadMapId || "", projectId: item.projectId || item.rmProjectId || "", projectName: item.projectName || item.name || item.title || item.text || "\u57f9\u8bad\u9879\u76ee", roadMapName: item.roadMapName || item.projectName || "", complete: !!item.complete, raw: item, }; } function projectYearFromName(name) { const m = String(name || "").match(/(20\d{2})/); return m ? parseInt(m[1], 10) : 0; } function sortProjectsByYear(projects) { return (projects || []).slice().sort(function (a, b) { const ya = projectYearFromName(a.projectName || a.roadMapName); const yb = projectYearFromName(b.projectName || b.roadMapName); if (yb !== ya) return yb - ya; return String(a.projectName || a.roadMapName || "").localeCompare( String(b.projectName || b.roadMapName || ""), "zh-CN" ); }); } function buildPanelProjects(roads, catalog) { const map = new Map(); (catalog || []).forEach(function (p) { if (p?.projectId && p.roadMapId) map.set(String(p.projectId), p); }); (roads || []).forEach(function (p) { if (!p?.projectId) return; const prev = map.get(String(p.projectId)); map.set( String(p.projectId), Object.assign({}, prev || {}, p, { projectName: p.projectName || prev?.projectName || p.roadMapName || prev?.roadMapName || "\u57f9\u8bad\u9879\u76ee", roadMapId: p.roadMapId || prev?.roadMapId || "", }) ); }); return sortProjectsByYear(Array.from(map.values())); } function pickDefaultProject(projects, ctx, route) { const list = projects || []; if (!list.length) return null; const routeId = route?.projectId ? String(route.projectId) : ""; const savedId = ctx?.projectId ? String(ctx.projectId) : ""; if (savedId && list.some(function (p) { return String(p.projectId) === savedId; })) { return list.find(function (p) { return String(p.projectId) === savedId; }); } if (routeId && list.some(function (p) { return String(p.projectId) === routeId; })) { return list.find(function (p) { return String(p.projectId) === routeId; }); } const year = new Date().getFullYear(); return ( list.find(function (p) { return projectYearFromName(p.projectName || p.roadMapName) === year; }) || list[0] ); } function applyRouteToCtx(ctx, route, allowOverride) { if (!route?.projectId) return ctx; if (!allowOverride && ctx?.projectId) return ctx; const next = Object.assign({}, ctx); next.projectId = route.projectId; if (route.roadMapId) next.roadMapId = route.roadMapId; if (route.stageId) next.stageId = route.stageId; return next; } function findProject(projectId) { return state.panelProjects.find(function (p) { return String(p.projectId) === String(projectId); }); } function mergeProjects(listA, listB) { return buildPanelProjects(listA, listB); } function pickCategoryPair(treeRaw) { const roots = Array.isArray(treeRaw) ? treeRaw : treeRaw?.children ? [treeRaw] : []; const pairs = []; function walk(node, depth, parentOne) { if (!node) return; const text = String(node.text || node.name || node.title || ""); if (depth === 1) { (node.children || []).forEach(function (child) { walk(child, 2, node); }); } else if (depth === 2 && parentOne) { pairs.push({ one: parentOne, two: node, text: text }); } } roots.forEach(function (r) { walk(r, 0, null); }); const prefer = pairs.find(function (p) { return /\u516c\u9700/.test(p.text); }) || pairs[0]; if (!prefer?.one?.id || !prefer?.two?.id) { const one = roots[0]?.children?.[0] || roots[0]; const two = one?.children?.[0]; if (one?.id && two?.id) return { oneCategoryId: one.id, twoCategoryId: two.id }; return null; } return { oneCategoryId: prefer.one.id, twoCategoryId: prefer.two.id }; } function normalizeCourseRow(item) { const info = item?.courseInfo || {}; const courseId = info.courseId || item.courseId || item.rmCourseId || ""; const courseName = info.courseTitle || info.courseName || item.courseName || item.title || courseId; let rate = Number(item.currentStepRate ?? item.studyRate ?? item.studyProgress ?? item.progress ?? 0); if (typeof item.currentStepRate === "string") { rate = parseFloat(String(item.currentStepRate).replace("%", "")) || rate; } const done = rate >= 99.9 || item.studyStatus === "FINISH" || item.finishStatus === "FINISH" || item.status === "FINISH" || item.completeStatus === "FINISH"; return { courseId, courseName, studyRate: rate, done, raw: item }; } async function fetchCoursesByType(stageId, courseType) { const raw = await nmsGetRaw("/nms/html/courseStudy/getCourseDetailByProjectId.do", { pageNo: 1, pageSize: 100, courseStatus: "ALL", stageId, courseType, "courseInfo.categoryId": "", }); const rows = raw?.rows || (Array.isArray(raw) ? raw : raw?.courseList || raw?.list || []); return (Array.isArray(rows) ? rows : []).map(function (item) { const c = normalizeCourseRow(item); c.courseType = courseType; return c; }).filter(function (x) { return x.courseId; }); } async function fetchCourses(stageId) { const must = await fetchCoursesByType(stageId, "MUST"); const selective = await fetchCoursesByType(stageId, "SELECTIVE"); const map = new Map(); must.forEach(function (c) { map.set(c.courseId, c); }); selective.forEach(function (c) { if (!map.has(c.courseId)) map.set(c.courseId, c); }); return Array.from(map.values()); } function courseTypeLabel(courseType) { if (courseType === "MUST") return "\u5fc5\u4fee"; if (courseType === "SELECTIVE") return "\u9009\u4fee"; return "\u8bfe\u7a0b"; } function courseTypeTagHtml(courseType) { const label = courseTypeLabel(courseType); const cls = courseType === "MUST" ? "cqrl-type-tag cqrl-type-must" : courseType === "SELECTIVE" ? "cqrl-type-tag cqrl-type-selective" : "cqrl-type-tag"; return '' + label + ""; } async function fetchCatalogProjects() { const treeRaw = await nmsPostRaw("/nms/html/courseStudy/loadCategroyTree.do", {}); const pair = pickCategoryPair(treeRaw); if (!pair) return []; const raw = await nmsGetRaw("/nms/html/courseStudy/loadCoursePage.do", { oneCategoryId: pair.oneCategoryId, twoCategoryId: pair.twoCategoryId, assignType: "BUY", projectName: "", pageNo: 1, pageSize: 100, }); const rows = raw?.rows || []; return (Array.isArray(rows) ? rows : []) .map(normalizeProjectRow) .filter(function (x) { return x.projectId && x.roadMapId; }); } function coursePayload(courseId) { return { courseId, sourceId: courseId, providerCorpCode: CORP_CODE, }; } async function fetchChapters(courseId) { const data = await rmsPost("/tbc-rms/course/showCourseChapter", coursePayload(courseId)); const list = unwrap(data); const chapters = Array.isArray(list) ? list : []; const resources = []; chapters.forEach(function (ch) { const chapterId = ch.chapterId || ch.id || ""; (ch.resourceDTOS || ch.resources || []).forEach(function (res) { const resourceId = res.resourceId || res.id || ""; if (!resourceId) return; const duration = Number(res.timeToFinish || res.duration || res.videoDuration || 0); const finishPercent = Number(ch.finishPercent ?? res.finishPercent ?? 1); const timeToFinish = duration > 0 ? Math.round(duration * (finishPercent > 0 ? finishPercent : 1)) : Number(res.timeToFinish || 1800); resources.push({ chapterId, resourceId, resourceName: res.resourceName || res.name || res.title || resourceId, type: res.type || res.resourceType || "video", timeToFinish: timeToFinish > 0 ? timeToFinish : 1800, finishPercent, }); }); }); return resources.filter(isVideoResource); } function isVideoResource(res) { if (!res) return false; const type = String(res.type || res.resourceType || "video").toLowerCase(); if (/doc|document|pdf|text|exam|test|html|scorm|ppt|audio|mp3/.test(type)) return false; const dur = Number(res.timeToFinish || res.duration || 0); return dur > 0 || type === "video" || /video|mp4|flv|media/.test(type); } function isCoursePlayable(detail) { return !!(detail && Array.isArray(detail.resources) && detail.resources.some(isVideoResource)); } function removeFromQueue(courseId, logMsg) { const q = loadQueue().filter(function (id) { return id !== courseId; }); if (q.length !== loadQueue().length) saveQueue(q); if (logMsg) state.panelHint = logMsg; } function applyCourseDetailMeta(courseId, detail) { const course = state.panelCourses.find(function (c) { return c.courseId === courseId; }); if (!detail || detail.loading) return; const playable = isCoursePlayable(detail); if (course) course.noVideo = !playable; if (!playable) { const name = (course && course.courseName) || courseId; if (loadQueue().includes(courseId)) { removeFromQueue(courseId, "\u5df2\u79fb\u51fa\u961f\u5217\uff08\u975e\u89c6\u9891\u8bfe\uff09\uff1a" + name); } renderCourseList(); renderChapterPreview(); updateQueueStats(); updatePanel(); } } async function fetchStudyRecords(courseId) { const data = await rmsPost("/tbc-rms/record/getStudyRecordList", coursePayload(courseId)); const list = unwrap(data); return Array.isArray(list) ? list : list?.recordList || []; } async function syncStudyRecord(courseId) { return rmsPost("/tbc-rms/record/syncStudyRecord", coursePayload(courseId)); } async function saveStudyLog(courseId) { const sid = getElnSessionId(); const path = "/biz-oim/course/saveStudyLog.do?courseId=" + encodeURIComponent(courseId); return gmRequest({ method: "POST", url: API_BASE + path, headers: buildBizOimHeaders(path, sid), body: { courseId, providerCorpCode: CORP_CODE }, }); } async function fetchStudyRate(courseId) { const data = await rmsPost("/tbc-rms/record/getStudyRate", coursePayload(courseId)); const rate = unwrap(data); return Number(rate ?? data?.bizResult ?? 0); } function findRecord(records, resourceId) { return (records || []).find(function (r) { return String(r.resourceId) === String(resourceId); }); } function isResourceDone(records, resourceId) { const r = findRecord(records, resourceId); if (!r) return false; return r.confirmFinish === 1 || r.confirmFinish === true || r.confirmFinish === "1"; } function getResourcePosition(bg, res) { const rec = findRecord(bg.records, res.resourceId); return Number(bg.positions?.[res.resourceId] ?? rec?.currentPosition ?? 0); } function syncProgressFromRecords(bg) { bg.positions = bg.positions || {}; (bg.records || []).forEach(function (rec) { if (!rec?.resourceId) return; const serverPos = Number(rec.currentPosition || 0); const localPos = Number(bg.positions[rec.resourceId] || 0); bg.positions[rec.resourceId] = Math.max(serverPos, localPos); }); } function findFirstPendingResourceIndex(bg) { const list = bg.resources || []; for (let i = 0; i < list.length; i++) { const res = list[i]; if (isResourceDone(bg.records, res.resourceId)) continue; const pos = getResourcePosition(bg, res); if (pos >= res.timeToFinish - 1) continue; return i; } return list.length; } function isResourcePending(bg, res) { if (isResourceDone(bg.records, res.resourceId)) return false; return getResourcePosition(bg, res) < res.timeToFinish - 1; } async function refreshBgProgress(bg, logSync) { if (!bg?.courseId) return bg; bg.records = await fetchStudyRecords(bg.courseId); bg.studyRate = await fetchStudyRate(bg.courseId); syncProgressFromRecords(bg); bg.resIndex = findFirstPendingResourceIndex(bg); if (logSync) { const cnt = countPendingResources(bg); if (cnt.total > 0 && cnt.done >= cnt.total) { logCourseDone(bg); } } return bg; } async function prepareCourse(courseId, courseName) { await syncStudyRecord(courseId); try { await saveStudyLog(courseId); } catch (_) {} const resources = await fetchChapters(courseId); const records = await fetchStudyRecords(courseId); const studyRate = await fetchStudyRate(courseId); if (!resources.length) { throw new Error("\u8be5\u8bfe\u7a0b\u65e0\u89c6\u9891\u7ae0\u8282\uff08\u8bf7\u4ece\u9879\u76ee\u9636\u6bb5\u91cc\u9009\u62e9\u5177\u4f53\u8bfe\u7a0b\uff0c\u4e0d\u8981\u9009\u57f9\u8bad\u9879\u76ee\u540d\u79f0\uff09"); } const bg = { courseId, courseName: courseName || courseId, resources, records, studyRate, resIndex: 0, positions: {}, phase: "study", lastSaveAt: 0, progressSynced: false, }; await refreshBgProgress(bg, false); bg.progressSynced = true; return bg; } async function learnOneResource(bg, res) { if (isResourceDone(bg.records, res.resourceId)) return true; const prevPos = getResourcePosition(bg, res); if (prevPos >= res.timeToFinish - 1) return true; if (!(await _cr())) return false; logChapterStart(bg, res, bg.resIndex); const rec = findRecord(bg.records, res.resourceId); const quotaKey = bg.courseId + ":" + res.resourceId; bg.quotaMarked = bg.quotaMarked || {}; const consumeQuota = !bg.quotaMarked[quotaKey]; let startRes; try { startRes = await _es( { course_id: bg.courseId, resource: { chapterId: res.chapterId, resourceId: res.resourceId, resourceName: res.resourceName, timeToFinish: res.timeToFinish, type: res.type || "video", }, prev_pos: prevPos, record_id: rec?.recordId || null, strict_mode: !!bg.strictMode, consume_quota: consumeQuota, }, { study_mode: bg.strictMode ? "realtime" : getStudyMode() } ); } catch (err) { const msg = String(err?.message || err); if (/free_quota|\u514d\u8d39\u989d\u5ea6/i.test(msg)) { stopAutoStudy("\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u7a0b\u5e8f\u5df2\u505c\u6b62"); return false; } throw err; } if (consumeQuota) bg.quotaMarked[quotaKey] = true; const loop = await _ve(startRes); if (!loop.ok) { if (/free_quota|\u514d\u8d39\u989d\u5ea6/i.test(loop.msg || "")) { stopAutoStudy("\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u7a0b\u5e8f\u5df2\u505c\u6b62"); return false; } if (loop.strict_mode || /\u5b66\u9738\u541b|\u884c\u4e3a|\u9632\u5237|strict/i.test(loop.msg || "")) { bg.strictMode = true; saveBgState(bg); logProgressEntry("warn", "\u300a" + (bg.courseName || "\u8bfe\u7a0b") + "\u300b\u89e6\u53d1\u9632\u5237\uff0c\u5df2\u5207\u6362\u6162\u901f"); return false; } throw new Error(loop.msg || "\u4e91\u7aef\u5237\u8bfe\u5931\u8d25"); } if (loop.new_pos != null) bg.positions[res.resourceId] = Number(loop.new_pos); bg.records = await fetchStudyRecords(bg.courseId); bg.studyRate = await fetchStudyRate(bg.courseId); syncProgressFromRecords(bg); return ( !!loop.resource_done || getResourcePosition(bg, res) >= res.timeToFinish - 1 || isResourceDone(bg.records, res.resourceId) ); } function countPendingResources(bg) { let total = 0; let done = 0; (bg.resources || []).forEach(function (res) { total += 1; if (isResourceDone(bg.records, res.resourceId)) done += 1; else if (getResourcePosition(bg, res) >= res.timeToFinish - 1) done += 1; }); return { total, done }; } async function initBackgroundTarget() { const queue = loadQueue(); const courses = state.panelCourses.length ? state.panelCourses : await refreshPanelCourses(false); const selected = courses.filter(function (c) { return queue.includes(c.courseId) && !c.done; }); if (!selected.length) return null; for (let i = 0; i < selected.length; i++) { const course = selected[i]; try { const bg = await prepareCourse(course.courseId, course.courseName); if (bg.resources?.length) return bg; } catch (err) { } const q = loadQueue().filter(function (id) { return id !== course.courseId; }); saveQueue(q); } return null; } async function backgroundLearnTick() { if (state.bgTickBusy) return; state.bgTickBusy = true; try { if (isCoursePlayPage()) { state.panelHint = "\u8bf7\u5173\u95ed\u8bfe\u7a0b\u64ad\u653e\u9875\u540e\u518d\u540e\u53f0\u5237\u8bfe"; return; } let bg = loadBgState(); if (bg?.courseId && bg.resources?.length) { if (!bg.progressSynced) { await refreshBgProgress(bg, false); bg.progressSynced = true; saveBgState(bg); } } if (!bg?.courseId || !bg.resources?.length) { bg = await initBackgroundTarget(); if (!bg) { if (state.enabled) stopAutoStudy("\u6240\u9009\u8bfe\u7a0b\u5747\u5df2\u5b66\u5b8c\uff0c\u81ea\u52a8\u505c\u6b62"); return; } saveBgState(bg); const cnt = countPendingResources(bg); if (bg.resIndex >= bg.resources.length || cnt.done >= cnt.total) { saveBgState({}); return; } } while (bg.resIndex < bg.resources.length) { const res = bg.resources[bg.resIndex]; if (isResourceDone(bg.records, res.resourceId)) { bg.resIndex += 1; continue; } const pos = getResourcePosition(bg, res); if (pos >= res.timeToFinish - 1) { bg.resIndex += 1; continue; } const finished = await learnOneResource(bg, res); bg.lastSaveAt = Date.now(); saveBgState(bg); if (finished === false) return; if (bg.courseId) { loadCourseDetail(bg.courseId, true).then(function () { updateCourseListProgress(); }); } if (finished) { const doneIdx = bg.resIndex; logChapterDone(bg, res, doneIdx); bg.resIndex += 1; saveBgState(bg); } return; } bg.studyRate = await fetchStudyRate(bg.courseId); const cnt = countPendingResources(bg); if (cnt.total === 0) { saveQueue(loadQueue().filter(function (id) { return id !== bg.courseId; })); saveBgState({}); return; } if (cnt.done >= cnt.total || Number(bg.studyRate) >= 99.5) { logCourseDone(bg); const q = loadQueue().filter(function (id) { return id !== bg.courseId; }); saveQueue(q); saveBgState({}); await refreshPanelCourses(false); } else { bg.resIndex = 0; bg.records = await fetchStudyRecords(bg.courseId); saveBgState(bg); } } catch (err) { const msg = String(err.message || err); if (/\u5f02\u5e38|\u5b66\u9738\u541b|\u884c\u4e3a|\u6309\u8981\u6c42/.test(msg)) { const bg = loadBgState(); if (bg?.courseId) { bg.strictMode = true; bg.lastSaveAt = Date.now(); saveBgState(bg); } logProgressEntry("warn", "\u300a" + (bg?.courseName || "\u8bfe\u7a0b") + "\u300b\u89e6\u53d1\u9632\u5237\uff0c\u5df2\u5207\u6362\u6162\u901f"); } else { logError("\u8fd0\u884c\u5f02\u5e38\uff1a" + msg); } } finally { state.bgTickBusy = false; } } async function tick() { if (!state.enabled) return; const now = Date.now(); if (now - state.lastTick < TICK_MS) return; state.lastTick = now; if (!isLoggedIn()) { logError("\u672a\u767b\u5f55\uff0c\u8bf7\u5148\u767b\u5f55 cqrl.21tb.com"); return; } const mode = getMode(); if (mode === "background") { await backgroundLearnTick(); return; } if (mode === "manual") { state.lastAction = "\u624b\u52a8\u6a21\u5f0f\uff1a\u8bf7\u81ea\u884c\u6253\u5f00\u8bfe\u7a0b\u64ad\u653e\u9875"; updatePanel(); return; } await autoplayTick(); } async function autoplayTick() { const queue = loadQueue(); if (!queue.length) { state.lastAction = "\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b"; updatePanel(); return; } if (isCoursePlayPage()) { state.lastAction = "\u64ad\u653e\u9875\u7531\u9875\u9762\u81ea\u52a8\u4e0a\u62a5\uff0c\u4fdd\u6301\u64ad\u653e\u5373\u53ef"; updatePanel(); return; } const courses = state.panelCourses.filter(function (c) { return queue.includes(c.courseId) && !c.done; }); if (!courses.length) { stopAutoStudy("\u6240\u9009\u8bfe\u7a0b\u5747\u5df2\u5b66\u5b8c"); return; } const c = courses[0]; const target = coursePlayUrl(c.courseId); if (!location.href.startsWith(target.split("?")[0])) { location.href = target; } } function stopAutoStudy(reason) { state.enabled = false; localStorage.setItem(STORAGE_KEY, "0"); state.lastLoggedStudyingResId = ""; if (reason) { if (/\u5df2\u624b\u52a8\u505c\u6b62|\u5747\u5df2\u5b66\u5b8c|\u81ea\u52a8\u505c\u6b62/.test(reason)) logInfo(reason); else logError(reason); } syncStartStopButtons(); updatePanel(); } function startAutoStudy() { if (!loadQueue().length) { logError("\u8bf7\u5148\u52fe\u9009\u8981\u5b66\u4e60\u7684\u8bfe\u7a0b"); return; } void (async function () { if (!(await _cr())) return; const bg = loadBgState(); if (bg?.strictMode) { delete bg.strictMode; saveBgState(bg); } state.enabled = true; localStorage.setItem(STORAGE_KEY, "1"); state.lastTick = 0; state.panelHint = ""; syncStartStopButtons(); updatePanel(); })(); } function studyModeSummaryText() { return "\u4e91\u7aef\u6548\u7387\u6a21\u5f0f · \u53c2\u6570\u7531\u670d\u52a1\u7aef\u63a7\u5236"; } function readPanelCollapsed() { try { return localStorage.getItem(PANEL_COLLAPSED_KEY) === "1"; } catch (_) { return false; } } function writePanelCollapsed(collapsed) { localStorage.setItem(PANEL_COLLAPSED_KEY, collapsed ? "1" : "0"); } function readPanelPos() { try { return JSON.parse(localStorage.getItem(PANEL_POS_KEY) || "null"); } catch (_) { return null; } } function writePanelPos(left, top) { localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left, top })); } function clampPanelInViewport(panel) { if (!panel) return; const maxLeft = Math.max(0, window.innerWidth - panel.offsetWidth); const maxTop = Math.max(0, window.innerHeight - panel.offsetHeight); const rect = panel.getBoundingClientRect(); const left = Math.max(0, Math.min(maxLeft, rect.left)); const top = Math.max(0, Math.min(maxTop, rect.top)); panel.style.right = "auto"; panel.style.bottom = "auto"; panel.style.left = left + "px"; panel.style.top = top + "px"; writePanelPos(left, top); } function applyPanelCollapsed(panel, collapsed) { const btnMin = panel.querySelector("#cqrl-btn-min"); const btnMax = panel.querySelector("#cqrl-btn-max"); panel.classList.toggle("cqrl-panel-min", !!collapsed); panel.classList.toggle("cqrl-panel-max", !collapsed); if (btnMin) btnMin.style.display = collapsed ? "none" : ""; if (btnMax) btnMax.style.display = collapsed ? "" : "none"; const footerExtra = panel.querySelector(".cqrl-footer-extra"); if (footerExtra) footerExtra.style.display = collapsed ? "none" : ""; clampPanelInViewport(panel); } function isChapterPaneActive() { const pane = document.querySelector('.cqrl-pane[data-pane="chapter"]'); return !!(pane && pane.classList.contains("active")); } function scheduleChapterPreviewRefresh(delayMs) { if (state.chapterPreviewTimer) clearTimeout(state.chapterPreviewTimer); state.chapterPreviewTimer = setTimeout(function () { state.chapterPreviewTimer = 0; void refreshChapterPreview(); }, delayMs != null ? delayMs : 400); } function switchPanelTab(tab) { const panel = document.getElementById("cqrl-auto-panel"); if (!panel) return; panel.querySelectorAll(".cqrl-tab-btn").forEach(function (btn) { btn.classList.toggle("active", btn.getAttribute("data-tab") === tab); }); panel.querySelectorAll(".cqrl-pane").forEach(function (pane) { pane.classList.toggle("active", pane.getAttribute("data-pane") === tab); }); if (tab === "chapter") scheduleChapterPreviewRefresh(0); } function renderChapterPreview() { const box = document.getElementById("cqrl-chapter-preview"); if (!box) return; const q = loadQueue(); if (!q.length) { box.innerHTML = '
\u8bf7\u52fe\u9009\u8bfe\u7a0b\u540e\u67e5\u770b\u7ae0\u8282\u8fdb\u5ea6
'; return; } const parts = []; q.forEach(function (courseId) { const course = state.panelCourses.find(function (c) { return c.courseId === courseId; }); if (!course || course.noVideo) return; const detail = state.courseDetailCache[courseId]; if (detail && !detail.loading && !isCoursePlayable(detail)) return; parts.push( '
' + courseTypeTagHtml(course.courseType) + '' + escHtml(course.courseName) + "
" + renderChapterRows(courseId, detail) + "
" ); }); box.innerHTML = parts.length ? parts.join("") : '
\u6240\u9009\u8bfe\u7a0b\u4e0d\u5728\u5f53\u524d\u9879\u76ee\u5217\u8868\u4e2d
'; } async function refreshChapterPreview() { if (state.chapterPreviewBusy) return; const q = loadQueue(); if (!q.length) { renderChapterPreview(); updateQueueStats(); updatePanel(); return; } state.chapterPreviewBusy = true; try { if (isChapterPaneActive()) { await Promise.all( q.map(function (courseId) { const cached = state.courseDetailCache[courseId]; if ( cached && !cached.loading && Date.now() - (cached.updatedAt || 0) < COURSE_DETAIL_STALE_MS ) { return Promise.resolve(); } return loadCourseDetail(courseId, false); }) ); } renderChapterPreview(); updateQueueStats(); updatePanel(); } finally { state.chapterPreviewBusy = false; } } function syncStartStopButtons() { const start = document.getElementById("cqrl-start") || document.getElementById("cqrl-btn-start"); const stop = document.getElementById("cqrl-stop") || document.getElementById("cqrl-btn-stop"); const on = state.enabled; if (start) { start.disabled = on; start.classList.toggle("cqrl-btn-off", on); } if (stop) { stop.disabled = !on; stop.classList.toggle("cqrl-btn-off", !on); } const status = document.getElementById("cqrl-auto-status"); if (status) { status.textContent = on ? "\u8fd0\u884c\u4e2d" : "\u5df2\u505c\u6b62"; status.style.color = on ? "#15803d" : "#64748b"; } } async function refreshPanelCourses(render) { if (state.panelRefreshing) return state.panelCourses; state.panelRefreshing = true; try { await ensureSession(); if (!isLoggedIn()) { state.panelHint = "\u672a\u83b7\u53d6\u767b\u5f55 session\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u6216\u91cd\u65b0\u767b\u5f55"; if (render !== false) renderCourseList(); return []; } await _lu(); let ctx = Object.assign({}, loadProjectCtx()); const route = parseRouteParams(); ctx = applyRouteToCtx(ctx, route, !ctx.projectId); const roads = await fetchRoadMapList(); const catalog = await fetchCatalogProjects(); state.panelProjects = buildPanelProjects(roads, catalog); const picked = pickDefaultProject(state.panelProjects, ctx, route); if (picked) { ctx.projectId = picked.projectId; ctx.roadMapId = picked.roadMapId || ctx.roadMapId; ctx.projectName = picked.projectName || picked.roadMapName || ctx.projectName; } else { ctx.projectId = ""; ctx.roadMapId = ""; ctx.stageId = ""; } if (ctx.roadMapId) { state.panelStages = await fetchStages(ctx.roadMapId); if (ctx.stageId && !state.panelStages.some(function (s) { return s.stageId === ctx.stageId; })) { ctx.stageId = ""; ctx.stageName = ""; } if (!ctx.stageId && state.panelStages.length) { ctx.stageId = state.panelStages[0].stageId; ctx.stageName = state.panelStages[0].stageName; } else if (ctx.stageId) { const st = state.panelStages.find(function (s) { return s.stageId === ctx.stageId; }); if (st) ctx.stageName = st.stageName; } } else { state.panelStages = []; ctx.stageId = ""; ctx.stageName = ""; } saveProjectCtx(ctx); state.panelCourses = []; if (ctx.stageId) { state.panelCourses = await fetchCourses(ctx.stageId); state.loadSource = "stage"; } const validIds = new Set(state.panelCourses.map(function (c) { return c.courseId; })); const cleanedQueue = loadQueue().filter(function (id) { return validIds.has(id); }); if (cleanedQueue.length !== loadQueue().length) saveQueue(cleanedQueue); if (!state.panelCourses.length) { state.panelHint = state.panelProjects.length ? "\u8bf7\u5728\u4e0a\u65b9\u9009\u62e9\u57f9\u8bad\u9879\u76ee\u548c\u9636\u6bb5\uff0c\u4e0b\u65b9\u4f1a\u663e\u793a\u9879\u76ee\u5185\u7684\u5177\u4f53\u8bfe\u7a0b" : "\u672a\u627e\u5230\u57f9\u8bad\u9879\u76ee\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55\u5e76\u5df2\u8d2d\u4e70\u8bfe\u7a0b"; } else { state.panelHint = ""; } updateQueueStats(); if (render !== false) { renderProjectSelectors(); renderCourseList(); renderChapterPreview(); scheduleCourseDetailsLoad(state.panelCourses.map(function (c) { return c.courseId; })); } return state.panelCourses; } catch (err) { logError("\u52a0\u8f7d\u8bfe\u7a0b\u5931\u8d25\uff1a" + (err.message || err)); state.panelHint = String(err.message || err); if (render !== false) renderCourseList(); return []; } finally { state.panelRefreshing = false; } } function getResourceProgressFromRecords(records, res) { const rec = findRecord(records, res.resourceId); return Number(rec?.currentPosition || 0); } function getChapterProgressLabel(records, res) { if (isResourceDone(records, res.resourceId)) return "\u5df2\u5b8c\u6210"; const pos = getResourceProgressFromRecords(records, res); if (res.timeToFinish > 0 && pos >= res.timeToFinish - 1) return "\u5df2\u5b8c\u6210"; const pct = res.timeToFinish > 0 ? Math.round((pos / res.timeToFinish) * 100) : 0; return pct + "%"; } function isChapterActive(courseId, resourceId) { const bg = loadBgState(); if (!state.enabled || !bg?.courseId || bg.courseId !== courseId) return false; const idx = bg.resIndex || 0; const cur = bg.resources?.[idx]; return cur && String(cur.resourceId) === String(resourceId); } async function loadCourseDetail(courseId, force) { if (!courseId) return null; const cached = state.courseDetailCache[courseId]; if (!force && cached && !cached.loading && Date.now() - (cached.updatedAt || 0) < COURSE_DETAIL_STALE_MS) { return cached; } if (cached?.loading) return cached; state.courseDetailCache[courseId] = { loading: true, resources: cached?.resources || [], records: cached?.records || [], studyRate: cached?.studyRate || 0, updatedAt: cached?.updatedAt || 0, }; try { const resources = await fetchChapters(courseId); const records = await fetchStudyRecords(courseId); const studyRate = await fetchStudyRate(courseId); state.courseDetailCache[courseId] = { loading: false, resources, records, studyRate, updatedAt: Date.now(), }; const course = state.panelCourses.find(function (c) { return c.courseId === courseId; }); if (course) { course.studyRate = studyRate; course.done = studyRate >= 99.5; } applyCourseDetailMeta(courseId, state.courseDetailCache[courseId]); return state.courseDetailCache[courseId]; } catch (err) { state.courseDetailCache[courseId] = { loading: false, resources: cached?.resources || [], records: cached?.records || [], studyRate: cached?.studyRate || 0, updatedAt: Date.now(), error: String(err.message || err), }; applyCourseDetailMeta(courseId, state.courseDetailCache[courseId]); return state.courseDetailCache[courseId]; } } function scheduleCourseDetailsLoad(courseIds) { const ids = (courseIds || []).filter(Boolean); let delay = 0; ids.forEach(function (courseId) { setTimeout(function () { loadCourseDetail(courseId, false).then(function () { updateCourseListProgress(); }); }, delay); delay += 120; }); } async function refreshPanelProgress(force) { const now = Date.now(); if (state.panelProgressBusy) return; if (!force && now - state.lastPanelProgressAt < PANEL_PROGRESS_MS) return; state.panelProgressBusy = true; state.lastPanelProgressAt = now; try { const q = loadQueue(); const bg = loadBgState(); const idSet = new Set(q); if (bg?.courseId) idSet.add(bg.courseId); const ids = Array.from(idSet); for (let i = 0; i < ids.length; i++) { await loadCourseDetail(ids[i], force); } updateCourseListProgress(); updatePanel(); } finally { state.panelProgressBusy = false; } } function updateCourseListProgress() { const bg = loadBgState(); state.panelCourses.forEach(function (c) { const detail = state.courseDetailCache[c.courseId]; const rate = detail?.studyRate ?? c.studyRate; const pct = Math.min(100, Math.max(0, Number(rate) || 0)); const finished = c.done || pct >= 99.5; c.studyRate = rate; if (finished) c.done = true; const card = document.querySelector( '#cqrl-course-list input[data-id="' + String(c.courseId).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"]' )?.closest(".cqrl-course-card"); if (!card) return; const isActive = bg?.courseId === c.courseId; card.style.borderColor = isActive ? "#16a34a" : "#e2e8f0"; card.style.background = isActive ? "#f0fdf4" : finished ? "#f1f5f9" : "#f8fafc"; const sub = card.querySelector(".cqrl-course-card-sub"); const bar = card.querySelector(".cqrl-course-card-bar > span"); const title = card.querySelector(".cqrl-course-card-title"); if (sub) sub.textContent = "\u8fdb\u5ea6 " + (finished ? "100" : pct.toFixed(1)) + "%"; if (bar) { bar.style.width = (finished ? 100 : pct) + "%"; bar.style.background = finished ? "#16a34a" : "#2563eb"; } if (title) title.style.color = finished ? "#64748b" : "#0f172a"; }); updateQueueStats(); if (isChapterPaneActive()) renderChapterPreview(); } function renderChapterRows(courseId, detail) { if (!detail || detail.loading) { return '
\u7ae0\u8282\u52a0\u8f7d\u4e2d…
'; } if (detail.error && !detail.resources?.length) { return '
\u7ae0\u8282\u52a0\u8f7d\u5931\u8d25
'; } if (!detail.resources?.length) { return '
\u65e0\u89c6\u9891\u7ae0\u8282
'; } return detail.resources .map(function (res, idx) { const label = getChapterProgressLabel(detail.records, res); const active = isChapterActive(courseId, res.resourceId); const done = label === "\u5df2\u5b8c\u6210"; return ( '
' + '' + escHtml(idx + 1 + ". " + (res.resourceName || res.resourceId)) + "" + '' + escHtml(label) + "
" ); }) .join(""); } function updateQueueStats() { const q = loadQueue(); let total = 0; let done = 0; q.forEach(function (courseId) { const detail = state.courseDetailCache[courseId]; const course = state.panelCourses.find(function (c) { return c.courseId === courseId; }); if (course && course.noVideo) return; if (detail && !detail.loading && !isCoursePlayable(detail)) return; const resources = detail?.resources || []; if (!resources.length) return; resources.forEach(function (res) { if (!isVideoResource(res)) return; total += 1; if (getChapterProgressLabel(detail.records, res) === "\u5df2\u5b8c\u6210") done += 1; }); }); state.queueVideoTotal = total; state.queueVideoDone = done; } function renderCourseList() { const box = document.getElementById("cqrl-course-list"); if (!box) return; const q = loadQueue(); const bg = loadBgState(); const activeId = bg?.courseId || ""; if (!state.panelCourses.length) { const hint = state.panelHint || "\u5f53\u524d\u57f9\u8bad\u9879\u76ee\u6682\u65e0\u8bfe\u7a0b
\u53ef\u5207\u6362\u4e0a\u65b9\u57f9\u8bad\u5e74\u5ea6\u67e5\u770b"; box.innerHTML = '
' + hint + "
"; return; } box.innerHTML = state.panelCourses .filter(function (c) { return !c.noVideo; }) .map(function (c) { const checked = q.includes(c.courseId) ? "checked" : ""; const detail = state.courseDetailCache[c.courseId]; const rate = detail?.studyRate ?? c.studyRate; const pct = Math.min(100, Math.max(0, Number(rate) || 0)); const finished = c.done || pct >= 99.5; const isActive = c.courseId === activeId; const border = isActive ? "#16a34a" : "#e2e8f0"; const bgColor = isActive ? "#f0fdf4" : finished ? "#f1f5f9" : "#f8fafc"; const titleColor = finished ? "#64748b" : "#0f172a"; return ( '" ); }) .join(""); box.querySelectorAll('input[type="checkbox"]').forEach(function (el) { el.addEventListener("change", function () { const id = el.getAttribute("data-id"); let queue = loadQueue(); if (el.checked) { if (!queue.includes(id)) queue.push(id); } else { queue = queue.filter(function (x) { return x !== id; }); } saveQueue(queue); const bg2 = loadBgState(); if (bg2?.courseId && bg2.courseId === id && !el.checked) { saveBgState({}); state.panelHint = "\u5f53\u524d\u8bfe\u7a0b\u5df2\u53d6\u6d88\u52fe\u9009"; } updateQueueStats(); updatePanel(); scheduleCourseDetailsLoad([id]); }); }); scheduleCourseDetailsLoad(loadQueue().slice()); } function renderProjectSelectors() { const projSel = document.getElementById("cqrl-roadmap-select"); const stageSel = document.getElementById("cqrl-stage-select"); if (projSel) { if (!state.panelProjects.length) { projSel.innerHTML = ''; } else { projSel.innerHTML = state.panelProjects .map(function (p) { const sel = String(p.projectId) === String(state.projectCtx.projectId) ? " selected" : ""; return ( '" ); }) .join(""); } } if (stageSel) { const wrap = document.getElementById("cqrl-stage-wrap"); if (!state.panelStages.length) { stageSel.innerHTML = ''; if (wrap) wrap.style.display = "none"; } else if (state.panelStages.length === 1) { stageSel.innerHTML = '"; if (wrap) wrap.style.display = "none"; if (state.projectCtx.stageId !== state.panelStages[0].stageId) { const ctx = loadProjectCtx(); ctx.stageId = state.panelStages[0].stageId; ctx.stageName = state.panelStages[0].stageName; saveProjectCtx(ctx); } } else { if (wrap) wrap.style.display = ""; stageSel.innerHTML = state.panelStages .map(function (s) { const sel = s.stageId === state.projectCtx.stageId ? " selected" : ""; return ( '" ); }) .join(""); } } } function updatePanel() { if (isCoursePlayPage() || !document.getElementById("cqrl-auto-panel")) return; syncStartStopButtons(); const doneEl = document.getElementById("cqrl-queue-done"); const totalEl = document.getElementById("cqrl-queue-total"); const pctEl = document.getElementById("cqrl-queue-percent"); const barEl = document.getElementById("cqrl-queue-progress"); const total = state.queueVideoTotal; const done = state.queueVideoDone; if (doneEl) doneEl.textContent = String(done); if (totalEl) totalEl.textContent = String(total); const pct = total > 0 ? Math.round((done / total) * 100) : 0; if (pctEl) pctEl.textContent = pct + "%"; if (barEl) barEl.style.width = pct + "%"; const courseEl = document.getElementById("cqrl-current-course"); const chapterEl = document.getElementById("cqrl-current-chapter"); const taskEl = document.getElementById("cqrl-current-task"); const modeEl = document.getElementById("cqrl-study-mode-summary"); const bg = loadBgState(); if (courseEl) { const t = bg.courseName || "\u65e0"; courseEl.textContent = t; courseEl.title = t; } if (chapterEl) { const res = bg?.resources?.[bg.resIndex || 0]; const t = res?.resourceName || "\u65e0"; chapterEl.textContent = t; chapterEl.title = t; } if (taskEl) { taskEl.textContent = state.enabled ? state.lastAction || "\u8fd0\u884c\u4e2d…" : state.panelHint || "\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u5b66\u4e60"; taskEl.title = taskEl.textContent; } if (modeEl) modeEl.textContent = studyModeSummaryText(); const annEl = document.getElementById("cqrl-ann-text"); if (annEl) annEl.textContent = getPanelNoticeText(); const queueText = document.getElementById("cqrl-queue-text"); if (queueText) { const q = loadQueue(); queueText.textContent = q.length ? "\u5df2\u9009 " + q.length + " \u95e8\u8bfe" : "\u672a\u9009\u62e9\u8bfe\u7a0b"; } } function injectPanelStyles() { const id = "cqrl-panel-style-v2"; document.getElementById("cqrl-panel-style")?.remove(); if (document.getElementById(id) || !document.head) return; const st = document.createElement("style"); st.id = id; st.textContent = ` #cqrl-auto-panel{position:fixed;right:20px;top:80px;z-index:999999;width:412px;display:flex;flex-direction:column;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#0f172a;overflow:hidden;transition:max-height .22s ease,box-shadow .22s ease;} #cqrl-auto-panel.cqrl-panel-max{max-height:min(92vh,780px);} #cqrl-auto-panel.cqrl-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);} #cqrl-auto-panel.cqrl-panel-min #cqrl-panel-header{border-bottom:none;} #cqrl-auto-panel.cqrl-panel-min #cqrl-panel-body,#cqrl-auto-panel.cqrl-panel-min #cqrl-panel-footer,#cqrl-auto-panel.cqrl-panel-min .cqrl-footer-extra{display:none !important;} #cqrl-panel-header{flex:0 0 auto;padding:8px 11px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;} #cqrl-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #cqrl-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:cover;box-shadow:0 2px 9px rgba(15,23,42,.11);border:1px solid rgba(148,163,184,.45);background:#fff;flex:0 0 auto;} #cqrl-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;} .cqrl-panel-title-text{flex:1 1 12em;min-width:0;} #cqrl-panel-sub{display:flex;flex-wrap:wrap;align-items:center;gap:3px 5px;margin-top:3px;line-height:1.32;} .cqrl-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;} .cqrl-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);} .cqrl-sub-dot{color:#d6d3d1;font-size:10px;} .cqrl-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;} #cqrl-panel-controls{display:flex;align-items:center;gap:5px;flex:0 0 auto;} .cqrl-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);padding:0;} .cqrl-ctl-min{display:block;width:10px;height:2px;background:#64748b;border-radius:1px;margin:0 auto;} .cqrl-ctl-plus{font-size:16px;line-height:1;font-weight:700;color:#64748b;} #cqrl-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;} .cqrl-footer-extra{flex:0 0 auto;} #cqrl-panel-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;color:#64748b;} .cqrl-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .cqrl-card-status{padding:6px 8px;margin-bottom:6px;} .cqrl-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;line-height:1.28;} .cqrl-status-label{font-size:11px;color:#64748b;font-weight:700;} .cqrl-status-main{margin-top:3px;} .cqrl-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;} .cqrl-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .cqrl-metric-div{color:#cbd5e1;} .cqrl-progress-pct{font-size:14px;font-weight:900;color:#0369a1;flex:0 0 auto;} .cqrl-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;} .cqrl-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;} .cqrl-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;} .cqrl-list-head-actions{display:flex;align-items:center;gap:5px;} .cqrl-list-title{font-size:12px;color:#64748b;font-weight:700;} .cqrl-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;} .cqrl-log-clear-btn{border:1px solid #cbd5e1;background:#fff;color:#64748b;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;cursor:pointer;} .cqrl-plan-row{padding:0 2px 5px;} .cqrl-plan-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 9px;font-size:12px;background:#fff;color:#0f172a;} .cqrl-plan-row .cqrl-plan-select{padding:4px 8px;font-size:11px;border-radius:7px;} #cqrl-course-list,#cqrl-chapter-preview,#cqrl-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} .cqrl-course-card{display:flex;gap:7px;align-items:flex-start;border:1px solid #e2e8f0;border-radius:8px;padding:7px;margin-bottom:6px;cursor:pointer;} .cqrl-course-card-body{flex:1;min-width:0;} .cqrl-course-card-title-row{display:flex;align-items:flex-start;gap:5px;flex-wrap:wrap;} .cqrl-course-card-title{display:block;font-size:12px;line-height:1.38;flex:1;min-width:0;} .cqrl-type-tag{flex:0 0 auto;font-size:10px;font-weight:800;border-radius:999px;padding:1px 6px;line-height:1.45;} .cqrl-type-must{color:#1d4ed8;background:#dbeafe;border:1px solid #93c5fd;} .cqrl-type-selective{color:#b45309;background:#ffedd5;border:1px solid #fdba74;} .cqrl-course-card-sub{display:block;font-size:11px;color:#64748b;margin-top:2px;} .cqrl-course-card-bar{display:block;height:4px;background:#e2e8f0;border-radius:2px;margin-top:5px;overflow:hidden;} .cqrl-course-card-bar>span{display:block;height:100%;width:0;border-radius:2px;} .cqrl-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;} .cqrl-chapter-title{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;} .cqrl-chapter-title-text{flex:1;min-width:0;line-height:1.38;} .cqrl-chapter-row{display:flex;align-items:center;gap:6px;padding:5px 9px;font-size:11px;color:#64748b;} .cqrl-chapter-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .cqrl-chapter-pct{flex:0 0 auto;font-weight:700;color:#0369a1;min-width:36px;text-align:right;} .cqrl-chapter-active .cqrl-chapter-name{color:#1d4ed8;font-weight:700;} .cqrl-chapter-done .cqrl-chapter-name,.cqrl-chapter-done .cqrl-chapter-pct,.cqrl-done{color:#16a34a !important;} .cqrl-chapter-loading{padding:6px 9px;font-size:10px;color:#94a3b8;} .cqrl-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;} .cqrl-tabbar{display:flex;gap:6px;margin-bottom:7px;} .cqrl-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px 4px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;} .cqrl-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .cqrl-pane{display:none;}.cqrl-pane.active{display:block;} .cqrl-card-current{padding:5px 8px;margin-bottom:6px;} .cqrl-card-current .cqrl-meta-row{font-size:11px;margin-bottom:2px;gap:5px;line-height:1.28;align-items:flex-start;} .cqrl-card-actions{padding:6px 8px;} .cqrl-card-auth{padding:5px 8px;margin-bottom:6px;} .cqrl-card-auth .cqrl-list-head{margin-bottom:3px;} .cqrl-card-auth .cqrl-list-title{font-size:11px;} .cqrl-card-auth .cqrl-meta-row{font-size:11px;margin-bottom:2px;gap:5px;line-height:1.3;} .cqrl-card-auth .cqrl-meta-label{font-size:11px;} .cqrl-card-auth .cqrl-meta-value{font-size:11px;font-weight:700;} .cqrl-token-row{align-items:center;} .cqrl-token-input{flex:1;min-width:0;border:1px solid #cbd5e1;border-radius:6px;padding:3px 6px;font-size:11px;} .cqrl-auth-actions{margin-top:4px;gap:5px;} .cqrl-auth-actions .cqrl-btn{flex:1;padding:5px 6px;font-size:11px;border-radius:8px;} .cqrl-btn-row{display:flex;gap:7px;margin-bottom:0;flex-wrap:nowrap;} .cqrl-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;min-width:68px;} .cqrl-btn-start{background:#16a34a;}.cqrl-btn-stop{background:#ef4444;}.cqrl-btn-refresh{background:#64748b;} .cqrl-btn-off,.cqrl-btn:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;pointer-events:none;} .cqrl-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;flex:0 0 auto;} .cqrl-btn-action{min-width:0 !important;flex:1 1 0 !important;} .cqrl-btn-ico{font-size:13px;margin-right:5px;} .cqrl-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;} .cqrl-meta-label{color:#64748b;}.cqrl-meta-value{font-weight:700;text-align:right;word-break:break-all;} #cqrl-auto-status{padding:2px 7px;border-radius:999px;font-weight:900;font-size:11px;background:#fff;border:1px solid #cbd5e1;} .cqrl-ann{position:relative;font-size:12px;color:#334155;line-height:1.42;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:11px;padding:8px 9px 8px 11px;} .cqrl-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-top-left-radius:11px;border-bottom-left-radius:11px;} .cqrl-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;} .cqrl-log-row{display:flex;align-items:flex-start;gap:6px;padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;} .cqrl-log-dot{flex:0 0 auto;width:14px;text-align:center;font-weight:900;font-size:12px;line-height:1.42;} .cqrl-log-dot-done{color:#16a34a;} .cqrl-log-dot-doing{color:#2563eb;} .cqrl-log-dot-warn{color:#d97706;} .cqrl-log-dot-info{color:#64748b;} .cqrl-log-dot-err{color:#dc2626;} .cqrl-log-time{flex:0 0 auto;color:#94a3b8;font-size:11px;min-width:52px;} .cqrl-log-text{flex:1;min-width:0;color:#334155;word-break:break-word;} .cqrl-start-hint{font-size:11px;color:#b45309;background:#fffbeb;border:1px solid #fcd34d;border-radius:7px;padding:5px 7px;margin-bottom:5px;text-align:center;font-weight:600;} `; document.head.appendChild(st); } function createPanel() { if (isCoursePlayPage()) return; document.getElementById("cqrl-auto-panel")?.remove(); injectPanelStyles(); const panel = document.createElement("div"); panel.id = "cqrl-auto-panel"; panel.className = "cqrl-panel-max"; panel.innerHTML = '
' + '
' + '' + '
\u91cd\u5e86\u4eba\u624d\u57f9\u8bad\u81ea\u52a8\u770b\u8bfe\u52a9\u624bv' + SCRIPT_VERSION + '
' + '
\u540e\u53f0\u5237\u8bfe·\u7ae0\u8282\u8fdb\u5ea6·\u7701\u65f6\u7701\u5fc3
' + '
' + '' + '' + "
" + '
' + '
' + '
\u8fd0\u884c\u72b6\u6001\u5df2\u505c\u6b62
' + '
0/0 \u89c6\u9891\u5df2\u5b66\u4e60
0%
' + '
' + '
' + '' + '' + '
' + '
' + '
\u8bfe\u7a0b\u9009\u62e9\u57f9\u8bad\u9879\u76ee
' + '
' + '
' + '
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
' + '
' + '
\u7ae0\u8282\u9884\u89c8\u5b9e\u65f6\u8fdb\u5ea6
' + '
\u8bf7\u52fe\u9009\u8bfe\u7a0b
' + '
' + '
\u8fd0\u884c\u65e5\u5fd7
' + '
' + '
' + '
\u5f53\u524d\u8bfe\u7a0b\u65e0
' + '
\u5f53\u524d\u7ae0\u8282\u65e0
' + '
\u5f53\u524d\u4efb\u52a1\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u5b66\u4e60
' + '
' + '
\u6388\u6743
' + '
\u7528\u6237\u7c7b\u578b\u672a\u6821\u9a8c
' + '
\u514d\u8d39\u4f53\u9a8c\uff083 \u7ae0\u8282\uff090/3
' + '
Token' + '
' + '
' + '
' + '
' + '
' + '
' + '" + ''; document.body.appendChild(panel); const savedPos = readPanelPos(); if (savedPos && savedPos.left != null && savedPos.top != null) { panel.style.right = "auto"; panel.style.left = savedPos.left + "px"; panel.style.top = savedPos.top + "px"; } applyPanelCollapsed(panel, readPanelCollapsed()); document.getElementById("cqrl-btn-min")?.addEventListener("click", function (e) { e.stopPropagation(); writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); document.getElementById("cqrl-btn-max")?.addEventListener("click", function (e) { e.stopPropagation(); writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); panel.querySelectorAll(".cqrl-tab-btn").forEach(function (btn) { btn.addEventListener("click", function () { switchPanelTab(btn.getAttribute("data-tab")); }); }); document.getElementById("cqrl-clear-log")?.addEventListener("click", clearRunLog); document.getElementById("cqrl-start")?.addEventListener("click", startAutoStudy); document.getElementById("cqrl-stop")?.addEventListener("click", function () { stopAutoStudy("\u5df2\u624b\u52a8\u505c\u6b62"); const bg = loadBgState(); if (bg?.courseId) { bg.progressSynced = false; saveBgState(bg); } }); document.getElementById("cqrl-pro-buy")?.addEventListener("click", openProBuyPage); document.getElementById("cqrl-roadmap-select")?.addEventListener("change", async function (e) { const item = findProject(e.target.value); if (!item) return; const ctx = { roadMapId: item.roadMapId, projectId: item.projectId, projectName: item.projectName || item.roadMapName, stageId: "", stageName: "", }; saveProjectCtx(ctx); state.panelStages = await fetchStages(ctx.roadMapId); if (state.panelStages.length) { ctx.stageId = state.panelStages[0].stageId; ctx.stageName = state.panelStages[0].stageName; saveProjectCtx(ctx); } saveQueue([]); renderProjectSelectors(); await refreshPanelCourses(true); }); document.getElementById("cqrl-stage-select")?.addEventListener("change", async function (e) { const ctx = loadProjectCtx(); ctx.stageId = e.target.value; const st = state.panelStages.find(function (s) { return s.stageId === ctx.stageId; }); ctx.stageName = st?.stageName || ""; saveProjectCtx(ctx); saveQueue([]); renderProjectSelectors(); await refreshPanelCourses(true); }); document.getElementById("cqrl-cloud-save")?.addEventListener("click", async function () { const input = document.getElementById("cqrl-cloud-token"); state.cloudToken = String((input && input.value) || "").trim(); state.cloudRevoked = false; localStorage.setItem(CLOUD_TOKEN_KEY, state.cloudToken); writeCloudLeaseCache("", 0); state.cloudLease = ""; state.cloudLeaseExp = 0; try { if (state.cloudToken) await _vt(); await _el(true); logProgressEntry("done", "\u4e91\u7aef\u6821\u9a8c\u6210\u529f"); log("\u4e91\u7aef\u6388\u6743\u5df2\u66f4\u65b0"); } catch (err) { logError("\u4e91\u7aef\u6821\u9a8c\u5931\u8d25\uff1a" + (err.message || err)); } updateCloudPanelUI(); }); state.cloudToken = String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(); const tokenInput = document.getElementById("cqrl-cloud-token"); if (tokenInput) tokenInput.value = state.cloudToken; updateCloudPanelUI(); void _cf(); if (isLoggedIn()) void _lu().then(function () { _el(false).catch(function () {}); }); enablePanelDrag(panel); updatePanel(); } function enablePanelDrag(panel) { const header = panel.querySelector("#cqrl-panel-header"); if (!header) return; let dragging = false; let sx = 0; let sy = 0; let sl = 0; let st = 0; header.addEventListener("mousedown", function (e) { if (e.target?.closest("#cqrl-panel-controls, .cqrl-panel-ctl")) return; dragging = true; sx = e.clientX; sy = e.clientY; const rect = panel.getBoundingClientRect(); sl = rect.left; st = rect.top; panel.style.right = "auto"; panel.style.bottom = "auto"; e.preventDefault(); }); document.addEventListener("mousemove", function (e) { if (!dragging) return; panel.style.left = Math.max(0, sl + e.clientX - sx) + "px"; panel.style.top = Math.max(0, st + e.clientY - sy) + "px"; }); document.addEventListener("mouseup", function () { if (!dragging) return; dragging = false; const rect = panel.getBoundingClientRect(); writePanelPos(rect.left, rect.top); }); } async function init() { applyFixedSettings(); state.enabled = localStorage.getItem(STORAGE_KEY) === "1"; state.studyMode = FIXED_STUDY_MODE; if (!isCoursePlayPage()) { createPanel(); } await ensureSession(); if (isLoggedIn()) { try { if (!isCoursePlayPage()) await refreshPanelCourses(true); } catch (err) { logError("\u521d\u59cb\u5316\u5931\u8d25\uff1a" + (err.message || err)); } } else if (!isCoursePlayPage()) { state.panelHint = "\u8bf7\u5148\u767b\u5f55 cqrl.21tb.com \u540e\u518d\u70b9\u5237\u65b0"; renderCourseList(); logError("\u8bf7\u5148\u767b\u5f55\u540e\u518d\u4f7f\u7528\u52a9\u624b"); } setInterval(tick, TICK_MS); if (!isCoursePlayPage()) { setInterval(function () { updatePanel(); if (state.enabled || loadQueue().length) { refreshPanelProgress(false); } else { updateCourseListProgress(); } }, 1000); syncStartStopButtons(); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();