// ==UserScript== // @name 交通职业资格网在线学习助手 // @namespace https://card.wlxy.top/links/09A67934 // @version 1.0 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @description 交通职业资格网继续教育学习辅助:一键完成已购课程学时和自测考试,高效率完成。已适配公路水运工程试验检测学时+自测;其他系统可联系作者适配 // @author 柠檬真酸 // @match https://syjc.jtzyzg.org.cn/* // @match https://*.jtzyzg.org.cn/* // @match http://*.jtzyzg.org.cn/* // @match https://jtzyzg.org.cn/* // @match http://jtzyzg.org.cn/* // @match https://jxjy.cdeledu.com/* // @match http://jxjy.cdeledu.com/* // @connect www.jtzyzg.org.cn // @connect jtzyzg.org.cn // @connect syjc.jtzyzg.org.cn // @connect 1.tel.jtzyzg.org.cn // @connect tel.jtzyzg.org.cn // @connect *.jtzyzg.org.cn // @connect jxjy.cdeledu.com // @connect huaweicloudobs.ahjxjy.cn // @connect oa10.ahzsksw.cn // @connect fa.ahsxks.com // @connect card.wlxy.live // @grant GM_openInTab // @grant GM_xmlhttpRequest // @run-at document-idle // ==/UserScript== (function () { "use strict"; const _w=(()=>{ const S=[ "jMXVz4jb0MDIg97a2tTIndba0t/Z3ZbJz93Pyg==", "jMXVz4jb0MDIg97a2tTIndba0t/Z3ZbJz9nN", "jMXVz4jb0MDIg87CxtXfxp7X2tjR0d4=", "jMXVz4jb0MDIg93PwdXdn93bwd/U3Q==", "jMXVz4jb0MDIg8HLzsPU", "jMXVz4jb0MDIg87c1sDF3ZzH0Nfb", "jMXVz4jEwMnOwt7LgMbUwNrSzA==", "y9DR1tSShoXEzZyegdHZyMDfxsGZ29c=" ]; const k=1443; return (i)=>{ const b=atob(S[i]); let r=""; for (let j=0;j m.hostRe.test(HOST)); if (!hostHits.length) { if (/jtzyzg\.org\.cn$/i.test(HOST) || /cdeledu\.com$/i.test(HOST)) { return { id: "portal", title: "\u4ea4\u901a\u804c\u4e1a\u8d44\u683c\u7f51\u7ee7\u7eed\u6559\u80b2", shortTitle: "\u7ee7\u7eed\u6559\u80b2\u95e8\u6237", status: "portal", note: "\u8bf7\u8fdb\u5165\u5177\u4f53\u7ee7\u7eed\u6559\u80b2\u7cfb\u7edf\u540e\u4f7f\u7528", }; } return null; } const withPath = hostHits.find((m) => m.pathHint && m.pathHint.test(path)); if (withPath) return withPath; const generic = hostHits.find((m) => !m.pathHint); return generic || hostHits[0]; } const CURRENT_CE = detectCeModule(); if (!CURRENT_CE) return; const IS_SYJC_ADAPTED = CURRENT_CE.id === "syjc"; function cePendingStatusText(mod) { const m = mod || CURRENT_CE; if (m.status === "portal") return "\u8bf7\u8fdb\u5165\u5177\u4f53\u7cfb\u7edf"; if (m.status === "adapted") return "\u5df2\u9002\u914d"; return "\u5f85\u9002\u914d"; } const SYSCODE = 2; const API_PATH = "/SYJC/restservices/http/single/query"; const QUEUE_KEY = "syjc_course_queue_v1"; const PANEL_POS_KEY = "syjc_panel_pos_v1"; const PANEL_COLLAPSED_KEY = "syjc_panel_collapsed_v1"; const CLOUD_TOKEN_KEY = "syjc_cloud_token_v1"; const CLOUD_LEASE_CACHE_KEY = "syjc_cloud_lease_cache_v1"; const CLOUD_PRO_EXPIRE_CACHE_KEY = "syjc_cloud_pro_expire_cache_v1"; const CLOUD_LAST_STATE_KEY = "syjc_cloud_last_state_v1"; const PANEL_LOGO_URL = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const QQ_GROUP_NUMBER = "903117129"; const QQ_GROUP_LINK = "https://qun.qq.com/universal-share/share?ac=1&authKey=rxdL6YIJ0%2FxOEemjLqTGULvl5aAfJIVQcIvkvnwvmL%2FAmpFZnSafajYHgSXMUXvx&busi_data=eyJncm91cENvZGUiOiI5MDMxMTcxMjkiLCJ0b2tlbiI6IlB2dkFGSm5XRXBrSEhtQVFTUGQzdVNZakhNWDNMbW1kODA2enpoMi9obDh4SWp0YzBDODNFaGtwRU44Z0hyU0siLCJ1aW4iOiIxMjU0MzE1MTQ0In0%3D&bcg=0&shareid=1&r=1"; const DEFAULT_CLOUD_API_BASE = _w(7); const PRO_BUY_URL = "https://card.wlxy.live/links/09A67934"; const PANEL_NOTICE_FALLBACK = "\u5f53\u524d\u4e1a\u52a1\uff1a\u8bd5\u9a8c\u68c0\u6d4b\u7ee7\u7eed\u6559\u80b2\u3002\u8bf7\u52ff\u540c\u65f6\u6253\u5f00\u8bfe\u4ef6\u64ad\u653e\u9875\uff0c\u4ee5\u514d\u62a2\u9501\u3002"; const SCRIPT_VERSION = (() => { try { if (typeof GM_info !== "undefined" && GM_info?.script?.version) { return String(GM_info.script.version); } } catch (_) {} return "2.2.4"; })(); const FRIENDLY_LOG_RE = /\u7ae0\u8282\u5f00\u59cb\u5b66\u4e60\u4e2d|\u81ea\u6d4b\u8003\u8bd5\u5f00\u59cb|\u7ae0\u8282\u5b66\u4e60\u5b8c\u6210|\u81ea\u6d4b\u8003\u8bd5\u5b8c\u6210|\u7ae0\u8282\u5b66\u4e60\u5931\u8d25|\u81ea\u6d4b\u8003\u8bd5\u5931\u8d25|\u514d\u8d39\u989d\u5ea6|\u8bf7\u5148\u767b\u5f55|Token|\u6388\u6743|Pro|\u52a0\u8f7d\u5931\u8d25|\u8fd0\u884c\u5931\u8d25/; const REJECT_LOG_RE = /SYJCEdu_|maintain|heartbeat|DES|tick|getExam|uploading|efficiency|\u63a2\u6d4b|platform_|engine|lease|bean|videoid|session|seal/i; const isVideoPage = /\/videonew\.html/i.test(location.pathname); const state = { enabled: false, busy: false, stopFlag: false, userBean: null, courses: [], chaptersByCourse: {}, queue: loadQueue(), logLines: [], panelHint: "", currentCourse: "", currentChapter: "", currentTask: "\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u63d0\u4ea4", queueVideoTotal: 0, queueVideoDone: 0, cloudApiBase: DEFAULT_CLOUD_API_BASE, cloudToken: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(), cloudTier: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim() ? "unknown" : "free", cloudLease: "", cloudLeaseExp: 0, cloudProExpireAt: 0, cloudRevoked: false, freeCourseLimit: 1, freeUsedCourses: 0, panelNoticePath: _w(3), remotePanelNotice: "", proBuyUrl: PRO_BUY_URL, }; function loadQueue() { try { const raw = JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]"); return Array.isArray(raw) ? raw.map(String) : []; } catch (_) { return []; } } function saveQueue(ids) { state.queue = (ids || []).map(String); localStorage.setItem(QUEUE_KEY, JSON.stringify(state.queue)); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function escapeHtml(s) { return String(s || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function shouldLogEngineLine(text) { const s = String(text || "").trim(); if (!s) return false; if (REJECT_LOG_RE.test(s)) return false; if (/\u7ae0\u8282\u5f00\u59cb\u5b66\u4e60\u4e2d|\u81ea\u6d4b\u8003\u8bd5\u5f00\u59cb/.test(s)) return true; if (/\u7ae0\u8282\u5b66\u4e60\u4e2d|\u81ea\u6d4b\u8003\u8bd5\u4e2d|\u961f\u5217/.test(s)) return false; return FRIENDLY_LOG_RE.test(s); } function log(msg) { const s = String(msg || "").trim(); if (!s) return; if (REJECT_LOG_RE.test(s)) return; const forcePanel = /\u7ae0\u8282\u5f00\u59cb\u5b66\u4e60\u4e2d|\u81ea\u6d4b\u8003\u8bd5\u5f00\u59cb|\u7ae0\u8282\u5b66\u4e60\u5b8c\u6210|\u81ea\u6d4b\u8003\u8bd5\u5b8c\u6210/.test(s); if ( !forcePanel && !FRIENDLY_LOG_RE.test(s) && !/\u8bf7\u5347\u7ea7 Pro|\u8bf7\u5148\u767b\u5f55|\u8bf7\u7a0d\u540e\u91cd\u8bd5|\u8bf7\u68c0\u67e5|\u6388\u6743\u5df2\u66f4\u65b0/.test(s) ) { console.log("[\u4ea4\u901a\u804c\u4e1a\u8d44\u683c\u52a9\u624b]", s); return; } const line = `[${new Date().toLocaleTimeString()}] ${s}`; state.logLines.unshift(line); if (state.logLines.length > 200) state.logLines.length = 200; const box = document.getElementById("syjc-run-log"); if (box) { box.innerHTML = state.logLines .slice(0, 80) .map((t) => `
${escapeHtml(t)}
`) .join(""); } console.log("[\u4ea4\u901a\u804c\u4e1a\u8d44\u683c\u52a9\u624b]", s); } function maybeLogEngine(text, quiet, dedupeSet) { if (quiet) return; const s = String(text || "").trim(); if (!shouldLogEngineLine(s)) return; if (dedupeSet && /\u7ae0\u8282\u5b66\u4e60\u5b8c\u6210|\u81ea\u6d4b\u8003\u8bd5\u5b8c\u6210/.test(s)) { if (dedupeSet.has(s)) return; dedupeSet.add(s); } log(s); } function setPanelHint(t) { state.panelHint = t || ""; const el = document.getElementById("syjc-start-hint"); if (!el) return; if (!t) { el.style.display = "none"; el.textContent = ""; return; } el.style.display = ""; el.textContent = t; } function courseIdOf(c) { return String(c.classid || c.id || ""); } function courseUuidOf(c) { return String(c.id || c.courseid || "").trim(); } function isCourseDone(c) { const rate = Number(c.learnrate); if (!Number.isNaN(rate) && rate >= 1) return true; const name = String(c.coursestatename || ""); if (/\u5df2\u5b8c\u6210/.test(name)) return true; if (/\u672a\u5b8c\u6210/.test(name)) return false; return String(c.coursestate) === "1"; } function getLearningUserId() { return String(state.userBean?.id || "").trim(); } function _gb() { return String(state.cloudApiBase || DEFAULT_CLOUD_API_BASE).replace(/\/$/, ""); } function mactite() { return String(navigator.userAgent || "") .replace(/['"\\/\b\f\n\r\t]/g, "") .replace(/[@#$%^&*{}:"L<>?]/g, ""); } function normalizeServiceName(name) { let e = String(name || ""); if (/^(LTPUSYJC_|LTPU|SYJCEdu_|VerifyCode_|SYJC_|LTPUEdu_)/.test(e)) return e; if (e.indexOf("_ub") !== -1) return "LTPUSYJC_" + e.replace(/^LTPUSYJC_/, ""); if (e.indexOf("getPersonInfo") !== -1) return "LTPUSYJC_" + e.replace(/^LTPUSYJC_/, ""); if (e.indexOf("getCodeValues") !== -1) return "LTPU_" + e.replace(/^LTPU_/, ""); if (e.indexOf("ZhaohangQrcode") !== -1) return e; return "SYJCEdu_" + e.replace(/^SYJCEdu_/, ""); } async function _cs(service, dataObj) { const svc = normalizeServiceName(service); const payload = { service: svc, data: dataObj && typeof dataObj === "object" ? dataObj : {}, }; if (state.cloudLease) payload.lease = state.cloudLease; const data = await _rq(_w(5), "POST", payload); if (!data || !data.service || !data.data) throw new Error("\u4e91\u7aef\u5bc6\u5c01\u5931\u8d25"); return { service: String(data.service), version: String(data.version || ""), data: String(data.data), }; } async function _pp(sealedBody) { const res = await fetch(API_PATH, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "X-Requested-With": "XMLHttpRequest", }, body: JSON.stringify(sealedBody), }); let json; try { json = await res.json(); } catch (_) { throw new Error("\u5e73\u53f0\u54cd\u5e94\u5f02\u5e38"); } if (!json || json.status !== 1) { throw new Error((json && (json.msg || json.message)) || "\u5e73\u53f0\u8bf7\u6c42\u5931\u8d25"); } return json; } function unwrapPlatformResult(json) { return json && json.result && json.result.result != null ? json.result.result : null; } async function _ar(service, data, opts) { opts = opts || {}; const svc = normalizeServiceName(service); let bodyObj; if (data == null) bodyObj = {}; else if (typeof data === "object") bodyObj = JSON.parse(JSON.stringify(data)); else bodyObj = {}; const needMactite = opts.mactite === true || /^(LTPUSYJC_|LTPU)/.test(svc); if (needMactite && bodyObj.mactite == null) bodyObj.mactite = mactite(); const sealed = await _cs(svc, bodyObj); const json = await _pp(sealed); return unwrapPlatformResult(json); } async function _ub() { const r = await _ar("LTPUSYJC_getUserBean", {}, { mactite: true }); const bean = r && r.id ? r : r?.result?.result || r?.result || r; if (!bean || !bean.id) throw new Error("\u672a\u767b\u5f55\u6216\u65e0\u6cd5\u83b7\u53d6\u7528\u6237\u4fe1\u606f\uff0c\u8bf7\u5148\u5728\u7f51\u7ad9\u767b\u5f55"); state.userBean = bean; try { const cardno = String(bean.cardno || "").trim(); const info = await _ar( "LTPUSYJC_getPersonInfo", { bean: { id: bean.id, cardno: cardno || String(bean.idcardno || "").trim(), cardtype: bean.idcardtype != null ? String(bean.idcardtype) : "0", isSearchCodeValue: true, }, }, { mactite: true } ); const person = info && info.id ? info : info?.result?.result || info?.result || info; if (person && typeof person === "object") { const org = String( person.nameoforganization || person.organizationname || person.companyname || person.unitname || person.officename || "" ).trim(); state.userBean = Object.assign({}, bean, { cardno: person.cardno || bean.cardno || "", contactnumber: person.phone || person.contactnumber || bean.phone || "", phone: person.phone || bean.phone || person.contactnumber || "", nameoforganization: org, name: person.name || bean.name || "", studentno: person.studentno || bean.studentno || "", idcardtype: person.idcardtype != null ? person.idcardtype : bean.idcardtype, idcardno: bean.idcardno, }); } } catch (_) {} return state.userBean; } async function getBoughtCourseList() { const u = state.userBean || (await _ub()); const all = []; let pageNum = 1; const pageSize = "50"; for (;;) { const body = { bean: { userid: u.id, pageSize, pageNum, cardno: u.idcardno || u.cardno, cardtype: u.idcardtype != null ? String(u.idcardtype) : "0", studentno: u.studentno || "", coursename: null, coursestate: "3", }, }; const r = await _ar("SYJCEdu_getBoughtCourseList", body, { mactite: false }); const list = Array.isArray(r?.result) ? r.result : Array.isArray(r) ? r : []; all.push(...list); const count = Number(r?.count || 0); if (!list.length) break; if (count && all.length >= count) break; if (list.length < Number(pageSize)) break; pageNum += 1; if (pageNum > 30) break; } return all; } async function getCoursewareList(courseId) { const body = { bean: { pageSize: 50, pageNum: 1, courseid: courseId, }, }; const r = await _ar("SYJCEdu_getcoursewareList", body, { mactite: false }); if (Array.isArray(r?.result)) return r.result; if (Array.isArray(r)) return r; return []; } async function getOnlineTestByCourse(course) { const u = state.userBean; if (!u) return null; const courseid = courseUuidOf(course) || courseIdOf(course); return _ar( "SYJCEdu_getOnlineTestByCourse", { bean: { userid: u.id, cardno: u.idcardno || u.cardno, cardtype: u.idcardtype != null ? String(u.idcardtype) : "0", studentno: u.studentno || "", courseid, }, }, { mactite: false } ); } function summarizeExamState(online) { if (!online) return "\u81ea\u6d4b\u672a\u77e5"; const list = Array.isArray(online.testname) ? online.testname : []; if (!list.length) { if (String(online.ishadwork || "") === "0") return "\u65e0\u81ea\u6d4b"; return "\u65e0\u81ea\u6d4b"; } const states = list.map((t) => String(t.teststate || "").trim()); if (states.every((s) => /\u5df2\u901a\u8fc7|\u5df2\u5b8c\u6210|\u53ca\u683c/.test(s))) return "\u81ea\u6d4b\u5df2\u901a\u8fc7"; if (states.some((s) => /\u672a\u901a\u8fc7|\u4e0d\u53ca\u683c/.test(s))) return "\u81ea\u6d4b\u672a\u901a\u8fc7"; if (states.some((s) => /\u672a\u7b54\u9898|\u672a\u5b8c\u6210|\u672a\u4ea4/.test(s))) return "\u81ea\u6d4b\u672a\u7b54\u9898"; const first = states.find(Boolean); return first ? `\u81ea\u6d4b${first}` : "\u81ea\u6d4b\u672a\u77e5"; } async function enrichCoursesExamState(courses) { const list = Array.isArray(courses) ? courses : []; const concurrency = 3; let idx = 0; async function worker() { while (idx < list.length) { const i = idx++; const c = list[i]; if (!c || c._examStateLoading) continue; c._examStateLoading = true; try { const online = await getOnlineTestByCourse(c); c.examStateText = summarizeExamState(online); } catch (_) { c.examStateText = c.examStateText || "\u81ea\u6d4b\u672a\u77e5"; } finally { c._examStateLoading = false; } if (i % 3 === 0 || i === list.length - 1) renderCourseList(); } } const workers = []; for (let w = 0; w < concurrency; w++) workers.push(worker()); await Promise.all(workers); renderCourseList(); } function probeDurationViaElement(src) { return new Promise((resolve, reject) => { const v = document.createElement("video"); v.preload = "metadata"; v.muted = true; v.playsInline = true; let done = false; const finish = (fn, arg) => { if (done) return; done = true; try { v.removeAttribute("src"); v.load(); v.remove(); } catch (_) {} fn(arg); }; v.onloadedmetadata = () => { const d = Number(v.duration); if (d && isFinite(d) && d > 1) finish(resolve, d); else finish(reject, new Error("invalid")); }; v.onerror = () => finish(reject, new Error("media error")); setTimeout(() => finish(reject, new Error("timeout")), 20000); v.style.cssText = "position:fixed;left:-99px;top:-99px;width:1px;height:1px;opacity:0;pointer-events:none"; (document.body || document.documentElement).appendChild(v); v.src = src; }); } function probeDurationViaGm(url) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== "function") { reject(new Error("no gm")); return; } GM_xmlhttpRequest({ method: "GET", url, responseType: "arraybuffer", headers: { Range: "bytes=0-4194303" }, timeout: 30000, onload: (res) => { try { const buf = res.response; if (!buf || !buf.byteLength) throw new Error("empty"); const d = parseMp4Duration(buf); if (d && d > 1) resolve(d); else reject(new Error("no duration")); } catch (e) { reject(e); } }, onerror: () => reject(new Error("fetch fail")), ontimeout: () => reject(new Error("timeout")), }); }); } function parseMp4Duration(arrayBuffer) { const u8 = new Uint8Array(arrayBuffer); const view = new DataView(arrayBuffer); const readStr = (off) => String.fromCharCode(u8[off], u8[off + 1], u8[off + 2], u8[off + 3]); let offset = 0; while (offset + 8 <= u8.length) { let size = view.getUint32(offset); const type = readStr(offset + 4); if (size === 1 && offset + 16 <= u8.length) { size = view.getUint32(offset + 12); if (size < 16) break; } else if (size === 0) { size = u8.length - offset; } if (size < 8) break; if (type === "moov" || type === "trak" || type === "mdia" || type === "mvex") { offset += 8; continue; } if (type === "mvhd") { const version = u8[offset + 8]; let timescale; let duration; if (version === 0) { timescale = view.getUint32(offset + 20); duration = view.getUint32(offset + 24); } else { timescale = view.getUint32(offset + 28); duration = view.getUint32(offset + 36); } if (timescale > 0 && duration > 0) return duration / timescale; } offset += size; } return 0; } async function probeDuration(playurl) { if (!playurl) throw new Error("no url"); const raw = String(playurl).trim(); const candidates = []; const https = raw.replace(/^http:\/\//i, "https://"); const http = raw.replace(/^https:\/\//i, "http://"); candidates.push(https); if (http !== https) candidates.push(http); if (!candidates.includes(raw)) candidates.push(raw); let lastErr = null; for (const src of candidates) { try { return await probeDurationViaElement(src); } catch (e) { lastErr = e; } } for (const src of candidates.filter((u) => /^https:\/\//i.test(u))) { try { return await probeDurationViaGm(src); } catch (e) { lastErr = e; } } throw lastErr || new Error("probe fail"); } function estimateDurationSec(course, video, info) { const wareSec = Number(video.timelength || video.duration || info?.timelength || 0); if (wareSec > 30) return wareSec; const hours = Number(course.coursetimelength || course.classtime || info?.learnhours || 0); const courseId = courseIdOf(course); const n = Math.max(1, (state.chaptersByCourse[courseId] || []).length || 1); if (hours > 0) return Math.max(300, Math.floor((hours * 3600) / n)); return 45 * 60; } async function ensureChapters(courseId) { if (state.chaptersByCourse[courseId]) return state.chaptersByCourse[courseId]; const list = await getCoursewareList(courseId); state.chaptersByCourse[courseId] = list.map((w) => ({ id: String(w.id), name: w.videoname || w.name || w.id, code: w.videocode || "", timelength: Number(w.timelength || w.duration || 0) || 0, playurl: w.playurl || w.videourl || "", done: false, })); return state.chaptersByCourse[courseId]; } function _gm(url, method, headers, data) { return new Promise((resolve, reject) => { const req = { method: method || "GET", url, headers: headers || {}, timeout: 30000, withCredentials: false, responseType: "text", onload: (res) => resolve({ status: res.status, text: res.responseText || "" }), onerror: () => reject(new Error("\u7f51\u7edc\u9519\u8bef")), ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6")), }; if (data != null) req.data = data; if (typeof GM_xmlhttpRequest === "function") { GM_xmlhttpRequest(req); return; } const xhr = new XMLHttpRequest(); xhr.open(req.method, url, true); xhr.timeout = req.timeout; Object.keys(req.headers).forEach((k) => xhr.setRequestHeader(k, req.headers[k])); xhr.onload = () => resolve({ status: xhr.status, text: xhr.responseText || "" }); xhr.onerror = () => reject(new Error("\u7f51\u7edc\u9519\u8bef")); xhr.ontimeout = () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6")); xhr.send(data); }); } function formatLeaseExpireText(expSec) { const ex = Number(expSec || 0); if (!Number.isFinite(ex) || ex <= 0) return "—"; const d = new Date(ex * 1000); if (Number.isNaN(d.getTime())) return "—"; return d.toLocaleString("zh-CN", { hour12: false }); } function resolveLeaseExpireSec(data) { const raw = data?.exp ?? data?.expire_at ?? data?.expires_at ?? data?.lease_exp ?? 0; const n = Number(raw || 0); if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000); return Number.isFinite(n) ? Math.floor(n) : 0; } function resolveProExpireSec(data) { const raw = data?.pro_expires_at ?? data?.proExpiresAt ?? data?.pro_expire_at ?? 0; const n = Number(raw || 0); if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000); return Number.isFinite(n) ? Math.floor(n) : 0; } function readCloudLeaseCache() { try { const raw = localStorage.getItem(CLOUD_LEASE_CACHE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") return null; const lease = String(parsed.lease || "").trim(); const exp = Number(parsed.exp || 0); if (!lease || !Number.isFinite(exp) || exp <= 0) return null; return { lease, exp, tier: String(parsed.tier || "").trim(), freeCourseLimit: Number(parsed.freeCourseLimit ?? parsed.free_course_limit ?? 1), freeUsedCourses: Number(parsed.freeUsedCourses ?? parsed.free_used_courses ?? 0), proExpireAt: Number(parsed.proExpireAt ?? parsed.pro_expire_at ?? 0), }; } catch (_) { return null; } } function writeCloudLeaseCache(lease, exp, extra) { if (!lease || !exp) { localStorage.removeItem(CLOUD_LEASE_CACHE_KEY); return; } localStorage.setItem(CLOUD_LEASE_CACHE_KEY, JSON.stringify(Object.assign({ lease, exp }, extra || {}))); } function readCloudLastState() { try { const raw = localStorage.getItem(CLOUD_LAST_STATE_KEY); if (!raw) return null; const p = JSON.parse(raw); if (!p || typeof p !== "object") return null; return { tier: String(p.tier || "").trim(), freeCourseLimit: Number(p.freeCourseLimit ?? p.free_course_limit ?? 1), freeUsedCourses: Number(p.freeUsedCourses ?? p.free_used_courses ?? 0), }; } catch (_) { return null; } } function writeCloudLastState(partial) { try { const prev = readCloudLastState() || {}; localStorage.setItem( CLOUD_LAST_STATE_KEY, JSON.stringify(Object.assign({}, prev, partial || {}, { ts: Date.now() })) ); } catch (_) {} } function writeCloudProExpireCache(token, exp) { try { const tk = String(token || "").trim(); if (!tk || !exp) { localStorage.removeItem(CLOUD_PRO_EXPIRE_CACHE_KEY); return; } localStorage.setItem(CLOUD_PRO_EXPIRE_CACHE_KEY, JSON.stringify({ token: tk, exp: Math.floor(exp) })); } catch (_) {} } async function _rq(path, method, payload) { const base = _gb(); const url = `${base}${path.startsWith("/") ? path : `/${path}`}`; const headers = { Accept: "application/json", "Content-Type": "application/json" }; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; if (state.cloudToken) headers.Authorization = `Bearer ${state.cloudToken}`; let reqPayload = payload; if (reqPayload && typeof reqPayload === "object") { reqPayload = Object.assign({}, reqPayload); if (path !== _w(4) && state.cloudLease && reqPayload.lease == null) { reqPayload.lease = state.cloudLease; } } const body = reqPayload == null ? null : JSON.stringify(reqPayload); const res = await _gm(url, method || "POST", headers, body); const text = String(res.text || "").trim(); let data = {}; if (text) data = JSON.parse(text); if (res.status < 200 || res.status >= 300) { const errCode = String(data.detail || data.message || data.code || `http_${res.status}`); throw new Error(errCode); } return data; } async function _el(forceRefresh) { const now = Math.floor(Date.now() / 1000); if (!forceRefresh && state.cloudLease && state.cloudLeaseExp - now > 60) return true; if (!forceRefresh) { const cached = readCloudLeaseCache(); if (cached && cached.exp - now > 60) { state.cloudLease = cached.lease; state.cloudLeaseExp = cached.exp; state.cloudProExpireAt = Number(cached.proExpireAt || 0); if (cached.tier) state.cloudTier = cached.tier; if (cached.freeCourseLimit > 0) state.freeCourseLimit = cached.freeCourseLimit; if (cached.freeUsedCourses >= 0) state.freeUsedCourses = cached.freeUsedCourses; updateCloudPanelUI(); return true; } } const luid = getLearningUserId(); if (!luid) throw new Error("\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u83b7\u53d6\u4e91\u7aef\u6388\u6743"); let data; try { const u = state.userBean || {}; const leaseBody = { learning_user_id: luid, name: u.name || u.loginusername || "", shortName: u.name || u.loginusername || "", idcarNo: u.cardno || "", userName: u.cardno || "", phone: u.phone || u.contactnumber || "", phoneNum: u.phone || u.contactnumber || "", office: u.nameoforganization || "", schoolName: u.nameoforganization || "", studentno: u.studentno || "", }; 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 e; } const lease = String(data.lease || ""); const exp = resolveLeaseExpireSec(data); const tier = String(data.tier || "").trim() || (state.cloudToken ? "pro" : "free"); const proExpireAt = resolveProExpireSec(data); state.cloudLease = lease; state.cloudLeaseExp = exp; state.cloudProExpireAt = proExpireAt > 0 ? proExpireAt : 0; state.cloudTier = tier; state.freeCourseLimit = Number( data.free_course_limit ?? data.freeCourseLimit ?? state.freeCourseLimit ?? 1 ); state.freeUsedCourses = Number( data.free_used_courses ?? data.freeUsedCourses ?? 0 ); writeCloudLeaseCache(lease, exp, { tier: state.cloudTier, freeCourseLimit: state.freeCourseLimit, freeUsedCourses: state.freeUsedCourses, proExpireAt: state.cloudProExpireAt, }); writeCloudLastState({ tier: state.cloudTier, freeCourseLimit: state.freeCourseLimit, freeUsedCourses: state.freeUsedCourses, }); if (tier === "pro" && state.cloudProExpireAt > 0) { writeCloudProExpireCache(state.cloudToken, state.cloudProExpireAt); } updateCloudPanelUI(); return !!lease; } async function _vt() { if (!state.cloudToken) return true; const url = `${_gb()}${_w(6)}`; 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(String(data.message || data.detail || "Token \u6821\u9a8c\u5931\u8d25")); } return true; } function _sq(data) { if (!data || typeof data !== "object") return; if (data.tier) state.cloudTier = String(data.tier); const lim = data.free_course_limit ?? data.freeCourseLimit ?? data.limit; const used = data.free_used_courses ?? data.freeUsedCourses ?? data.used; if (lim != null) state.freeCourseLimit = Number(lim); if (used != null) state.freeUsedCourses = Number(used); writeCloudLastState({ tier: state.cloudTier, freeCourseLimit: state.freeCourseLimit, freeUsedCourses: state.freeUsedCourses, }); updateCloudPanelUI(); } async function _es(kind, context, cfg) { await _el(false); const data = await _rq(_w(0), "POST", { kind: String(kind || ""), 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_syjc") { let sealed = c.body; if (!sealed || !sealed.service) { const svc = c.service; let bodyObj = c.data && typeof c.data === "object" ? JSON.parse(JSON.stringify(c.data)) : {}; if (c.mactite === true && bodyObj.mactite == null) bodyObj.mactite = mactite(); sealed = await _cs(svc, bodyObj); } const json = await _pp(sealed); return { event: "submit_result", lastResult: unwrapPlatformResult(json) }; } 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, msg: c.message || "\u5b8c\u6210", }; } if (t === "failed") { return { terminal: true, ok: false, msg: c.message || "\u5931\u8d25" }; } return { event: "tick", lastResult: null }; } async function _ve(startRes, options) { const quiet = !!(options && options.quiet); const dedupeSet = options?.dedupeSet || null; let sessionId = String(startRes.session_id || ""); let cmd = startRes.command; maybeLogEngine(startRes.log, quiet, dedupeSet); const maxSteps = Number(options?.maxSteps) || 120; for (let i = 0; i < maxSteps && 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); maybeLogEngine(next.log, quiet, dedupeSet); cmd = next.command; } return { terminal: true, ok: false, msg: "\u5b66\u4e60\u8d85\u65f6" }; } async function _cr() { if (!state.userBean?.id) { try { await _ub(); } catch (_) { log("\u8bf7\u5148\u767b\u5f55\u5e73\u53f0"); return false; } } try { if (state.cloudToken) await _vt(); await _el(false); } catch (err) { const em = String(err?.message || err); if (/free_quota|\u989d\u5ea6/.test(em)) { log("\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"); } else { log("\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff1a" + (em.includes("invalid") ? "\u8bf7\u68c0\u67e5 Token" : "\u8bf7\u7a0d\u540e\u91cd\u8bd5")); } return false; } const tier = String(state.cloudTier || "").toLowerCase(); if (tier === "pro") return true; if (tier === "free") { if (Number(state.freeUsedCourses) >= Number(state.freeCourseLimit)) { log("\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"); switchPanelTab("settings"); openProModal(); return false; } return true; } log("\u4e91\u7aef\u6388\u6743\u72b6\u6001\u5f02\u5e38\uff0c\u8bf7\u68c0\u67e5 Token"); return false; } function buildVideoContext(course, video, durationSec, consumeQuota) { const u = state.userBean; const courseId = courseIdOf(course); const courseUuid = courseUuidOf(course) || courseId; const videoid = String(video.id); return { course_id: courseUuid, course_uuid: courseUuid, course_name: course.classname || course.coursename || courseId, videoid, video_name: video.name, userid: u.id, cardno: u.idcardno || u.cardno, cardtype: u.idcardtype != null ? String(u.idcardtype) : "0", studentno: u.studentno || "", user_name: u.name || u.loginusername || "", start_sec: 0, duration_sec: durationSec, redisvalue: videoid + String(u.id), platform_course_id: courseId, consume_course_quota: !!consumeQuota, }; } function buildExamContext(course) { const u = state.userBean; const cname = course.classname || course.coursename || courseIdOf(course); const courseUuid = courseUuidOf(course) || courseIdOf(course); return { course_uuid: courseUuid, course_id: courseUuid, course_name: cname, userid: u.id, cardno: u.idcardno || u.cardno, cardtype: u.idcardtype != null ? String(u.idcardtype) : "0", studentno: u.studentno || "", user_name: u.name || u.loginusername || "", }; } async function resolveVideoDuration(course, video) { let duration = Number(video.timelength || 0); if (duration > 30) return duration; const playurl = video.playurl || ""; if (playurl) { try { duration = await probeDuration(playurl); if (duration > 5) return duration; } catch (_) {} } return estimateDurationSec(course, video, null); } async function runQueue() { if (state.busy) return; state.busy = true; state.stopFlag = false; state.enabled = true; updatePanel(); const engineLogDedupe = new Set(); try { await _ub(); if (!state.userBean.idcardno) { throw new Error("\u8bf7\u5148\u5b8c\u5584\u4e2a\u4eba\u4fe1\u606f\uff08\u8bc1\u4ef6\u53f7\uff09\u540e\u518d\u5b66\u4e60"); } if (!(await _cr())) return; const selected = state.courses.filter((c) => state.queue.includes(courseIdOf(c))); if (!selected.length) { setPanelHint("\u8bf7\u5148\u52fe\u9009\u81f3\u5c11\u4e00\u95e8\u8bfe\u7a0b\u518d\u70b9\u5f00\u59cb"); return; } let total = 0; let done = 0; const jobs = []; for (const c of selected) { const chs = await ensureChapters(courseIdOf(c)); for (const v of chs) { total += 1; jobs.push({ course: c, video: v, firstInCourse: false }); } } const seenCourse = new Set(); for (const j of jobs) { const cid = courseIdOf(j.course); if (!seenCourse.has(cid)) { j.firstInCourse = true; seenCourse.add(cid); } } state.queueVideoTotal = total; state.queueVideoDone = done; updatePanel(); for (let i = 0; i < jobs.length; i++) { if (state.stopFlag) break; const { course, video, firstInCourse } = jobs[i]; const cid = courseIdOf(course); const cname = course.classname || course.coursename || cid; state.currentCourse = cname; state.currentChapter = video.name; state.currentTask = "\u5b66\u4e60\u4e2d"; updatePanel(); log(`\u7ae0\u8282\u5f00\u59cb\u5b66\u4e60\u4e2d\uff1a${video.name}`); try { const durationSec = await resolveVideoDuration(course, video); const ctx = buildVideoContext(course, video, durationSec, firstInCourse); const startRes = await _es("video", ctx, {}); const result = await _ve(startRes, { quiet: true, dedupeSet: engineLogDedupe, }); if (result.ok) { video.done = true; done += 1; state.queueVideoDone = done; const doneKey = `\u7ae0\u8282\u5b66\u4e60\u5b8c\u6210\uff1a${video.name}`; if (!engineLogDedupe.has(doneKey)) { log(doneKey); engineLogDedupe.add(doneKey); } updatePanel(); renderChapterPreview(); } else if (result.msg && /free_quota|\u989d\u5ea6/.test(String(result.msg))) { log("\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"); switchPanelTab("settings"); openProModal(); break; } else if (!result.ok) { log(`\u7ae0\u8282\u5b66\u4e60\u5931\u8d25\uff1a${video.name}`); } } catch (e) { const em = String(e?.message || e); if (/free_quota|\u989d\u5ea6/.test(em)) { log("\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"); switchPanelTab("settings"); openProModal(); break; } log(`\u7ae0\u8282\u5b66\u4e60\u5931\u8d25\uff1a${video.name}`); if (/\u7528\u6237\u505c\u6b62|\u5360\u7528\u9501/.test(em)) break; } const next = jobs[i + 1]; const courseJustFinished = !next || courseIdOf(next.course) !== cid; if (courseJustFinished && !state.stopFlag) { state.currentChapter = "\u81ea\u6d4b"; state.currentTask = "\u81ea\u6d4b\u4e2d"; updatePanel(); log(`\u81ea\u6d4b\u8003\u8bd5\u5f00\u59cb\uff1a${cname}`); try { const examCtx = buildExamContext(course); const examStart = await _es("exam", examCtx, {}); const examRes = await _ve(examStart, { quiet: true, dedupeSet: engineLogDedupe, }); if (examRes.ok) { const examKey = `\u81ea\u6d4b\u8003\u8bd5\u5b8c\u6210\uff1a${cname}`; if (!engineLogDedupe.has(examKey)) { log(examKey); engineLogDedupe.add(examKey); } } else if (!examRes.skipped) { log(`\u81ea\u6d4b\u8003\u8bd5\u5931\u8d25\uff1a${cname}`); } } catch (e) { log(`\u81ea\u6d4b\u8003\u8bd5\u5931\u8d25\uff1a${cname}`); if (/\u7528\u6237\u505c\u6b62/.test(String(e?.message || e))) break; } await refreshCourses({ keepChapters: true, silent: true }); } if (next && !state.stopFlag) await sleep(3000); } if (!state.stopFlag) { state.currentTask = "\u5df2\u5b8c\u6210"; await refreshCourses({ keepChapters: true, silent: true }); } } catch (e) { log("\u8fd0\u884c\u5931\u8d25\uff1a" + (String(e?.message || e).includes("\u767b\u5f55") ? "\u8bf7\u5148\u767b\u5f55" : "\u8bf7\u7a0d\u540e\u91cd\u8bd5")); setPanelHint(String(e.message || e)); } finally { state.busy = false; state.enabled = false; updatePanel(); } } function stopAutoStudy(reason) { state.stopFlag = true; state.enabled = false; if (reason) log(reason); state.currentTask = reason || "\u5df2\u505c\u6b62"; updatePanel(); } function _so() { try { return new URL(_gb()).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", {}, null); if (res.status >= 200 && res.status < 300) { state.remotePanelNotice = String(res.text || "").trim() || PANEL_NOTICE_FALLBACK; const el = document.getElementById("syjc-ann-text"); if (el) el.textContent = state.remotePanelNotice; } } catch (_) {} } async function _cf() { try { const data = await _gm( `${_gb()}${_w(2)}`, "GET", { Accept: "application/json" }, null ); if (data.status >= 200 && data.status < 300 && data.text) { const j = JSON.parse(data.text); if (j?.panelNoticePath) state.panelNoticePath = String(j.panelNoticePath); if (j?.freeCourseLimit != null) state.freeCourseLimit = Number(j.freeCourseLimit) || 1; if (j?.cloudApiBase) state.cloudApiBase = String(j.cloudApiBase).replace(/\/$/, ""); if (j?.proBuyUrl) state.proBuyUrl = String(j.proBuyUrl).trim() || PRO_BUY_URL; if (j?.ceModuleStatus) _acm(j.ceModuleStatus); updatePendingPanelLiveStatus(); const adaptModal = document.getElementById("syjc-adapt-modal"); if (adaptModal) createAdaptModal(); } } catch (_) {} await _pn(); } function getProBuyUrl() { return String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL; } function openProBuyPage() { const url = getProBuyUrl(); try { if (typeof GM_openInTab === "function") GM_openInTab(url, { active: true, insert: true, setParent: true }); else window.open(url, "_blank", "noopener,noreferrer"); } catch (_) { window.open(url, "_blank"); } } function openProModal() { const vm = document.getElementById("syjc-pro-modal"); if (vm) vm.style.display = "flex"; } function closeProModal() { const vm = document.getElementById("syjc-pro-modal"); if (vm) vm.style.display = "none"; } function switchPanelTab(name) { document.querySelectorAll(".syjc-tab-btn").forEach((el) => { el.classList.toggle("active", el.dataset.tab === name); }); document.querySelectorAll(".syjc-pane").forEach((el) => { el.classList.toggle("active", el.dataset.pane === name); }); } function formatCloudTierText(tier) { if (state.cloudRevoked) return "\u5df2\u7981\u7528"; const t = String(tier || "").trim().toLowerCase(); if (t === "pro") return "Pro"; if (t === "free") return "\u514d\u8d39\u4f53\u9a8c"; if (t === "revoked") return "\u5df2\u7981\u7528"; if (t === "unknown") return "\u672a\u6821\u9a8c"; return tier ? String(tier) : "\u672a\u6821\u9a8c"; } function updateCloudPanelUI() { const tierEl = document.getElementById("syjc-cloud-tier"); const freeEl = document.getElementById("syjc-cloud-free"); const freeLabelEl = document.getElementById("syjc-cloud-free-label"); const tokenInput = document.getElementById("syjc-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\uff081\u95e8\u8bfe\uff09"; if (freeEl) freeEl.textContent = `${state.freeUsedCourses}/${state.freeCourseLimit}`; } if (tokenInput && tokenInput !== document.activeElement) tokenInput.value = state.cloudToken || ""; } function ceStatusLabel(status) { if (status === "adapted") return "\u5df2\u9002\u914d"; if (status === "adaptable") return "\u53ef\u9002\u914d"; if (status === "portal") return "\u95e8\u6237"; return "\u5f85\u786e\u8ba4"; } function _acm(map) { const src = map && typeof map === "object" ? map : {}; for (const mod of CE_MODULES) { const raw = src[mod.id]; let st = ""; let note = ""; if (typeof raw === "string") st = raw; else if (raw && typeof raw === "object") { st = String(raw.status || ""); note = String(raw.note || "").trim(); } st = String(st || "").trim().toLowerCase(); if (st === "adapted" || st === "adaptable") { mod.status = st; if (note) mod.note = note; else if (st === "adapted") mod.note = "\u5df2\u652f\u6301\u4e91\u7aef\u5b66\u65f6 + \u81ea\u52a8\u81ea\u6d4b"; else mod.note = "\u53ef\u9002\u914d\uff0c\u8054\u7cfb\u4f5c\u8005\u5f00\u901a"; } else if (mod.id === "syjc") { mod.status = "adapted"; mod.note = "\u5df2\u652f\u6301\u4e00\u952e\u5b66\u4e60 + \u81ea\u52a8\u81ea\u6d4b"; } else { mod.status = "adaptable"; mod.note = "\u53ef\u9002\u914d\uff0c\u8054\u7cfb\u4f5c\u8005\u5f00\u901a"; } } if (CURRENT_CE && CURRENT_CE.id && CURRENT_CE.id !== "portal") { const live = CE_MODULES.find((m) => m.id === CURRENT_CE.id); if (live) { CURRENT_CE.status = live.status; CURRENT_CE.note = live.note; } } } function updatePendingPanelLiveStatus() { if (IS_SYJC_ADAPTED) return; const statusText = cePendingStatusText(CURRENT_CE); const title = CURRENT_CE.shortTitle || CURRENT_CE.title || "\u7ee7\u7eed\u6559\u80b2"; const footer = document.getElementById("syjc-panel-footer"); if (footer) footer.textContent = `\u5f53\u524d\u7cfb\u7edf\uff1a${title} · ${statusText}`; const chips = document.querySelectorAll("#syjc-panel-sub .syjc-sub-chip"); if (chips.length >= 2) chips[1].textContent = statusText; const desc = document.querySelector(".syjc-pending-desc"); if (desc) { desc.textContent = CURRENT_CE.status === "portal" ? "\u8bf7\u4ece\u95e8\u6237\u8fdb\u5165\u5177\u4f53\u7ee7\u7eed\u6559\u80b2\u7cfb\u7edf\u3002\u8bd5\u9a8c\u68c0\u6d4b\u5df2\u9002\u914d\uff1b\u5176\u5b83\u7cfb\u7edf\u53ef\u8054\u7cfb\u4f5c\u8005\u9002\u914d\u3002" : CURRENT_CE.status === "adapted" ? "\u5f53\u524d\u7cfb\u7edf\u5df2\u9002\u914d\uff0c\u8bf7\u8fdb\u5165\u5b66\u4e60\u9875\u9762\u4f7f\u7528\u5b8c\u6574\u529f\u80fd\u3002" : "\u8be5\u7ee7\u7eed\u6559\u80b2\u5c1a\u672a\u63a5\u5165\u81ea\u52a8\u5b66\u4e60\u3002\u70b9\u51fb\u4e0b\u65b9\u6309\u94ae\u67e5\u770b\u5df2\u9002\u914d\u4e0e\u53ef\u9002\u914d\u5217\u8868\uff0c\u5e76\u8054\u7cfb\u4f5c\u8005\u5f00\u901a\u3002"; } } function openAdaptModal() { createAdaptModal(); const modal = document.getElementById("syjc-adapt-modal"); if (modal) modal.style.display = "flex"; } function closeAdaptModal() { const modal = document.getElementById("syjc-adapt-modal"); if (modal) modal.style.display = "none"; } function createAdaptModal() { const old = document.getElementById("syjc-adapt-modal"); const wasOpen = !!(old && old.style.display === "flex"); if (old) old.remove(); const adapted = CE_MODULES.filter((m) => m.status === "adapted"); const adaptable = CE_MODULES.filter((m) => m.status === "adaptable"); const row = (m, badgeClass) => `
${escapeHtml(m.title)}
${escapeHtml(m.note || "")}
${ceStatusLabel(m.status)}
`; const modal = document.createElement("div"); modal.id = "syjc-adapt-modal"; if (wasOpen) modal.style.display = "flex"; modal.innerHTML = `
\u7ee7\u7eed\u6559\u80b2\u9002\u914d\u60c5\u51b5
${escapeHtml(PRODUCT_NAME)} · \u5f53\u524d\u7ad9\u70b9\uff1a${escapeHtml(CURRENT_CE.shortTitle || CURRENT_CE.title)}

\u5df2\u9002\u914d

${adapted.map((m) => row(m, "syjc-adapt-ok")).join("") || `
\u6682\u65e0
`}

\u53ef\u9002\u914d\uff08\u8054\u7cfb\u4f5c\u8005\uff09

${adaptable.map((m) => row(m, "syjc-adapt-wait")).join("") || `
\u6682\u65e0
`}

\u8054\u7cfb\u4f5c\u8005\u9002\u914d

\u52a0\u5165 QQ \u7fa4 ${QQ_GROUP_NUMBER}\uff0c\u8bf4\u660e\u4f60\u9700\u8981\u9002\u914d\u7684\u7ee7\u7eed\u6559\u80b2\u7cfb\u7edf\u540d\u79f0\uff0c\u4f5c\u8005\u8bc4\u4f30\u540e\u5f00\u901a\u3002

\u8bd5\u9a8c\u68c0\u6d4b\u5df2\u53ef\u7528\uff1b\u5176\u5b83\u7cfb\u7edf\u6309\u9700\u9002\u914d\uff0c\u6b22\u8fce\u53cd\u9988\u3002
`; document.body.appendChild(modal); modal.addEventListener("click", (e) => { if (e.target === modal) closeAdaptModal(); }); modal.querySelector("#syjc-adapt-close")?.addEventListener("click", closeAdaptModal); modal.querySelector("#syjc-adapt-join-qq")?.addEventListener("click", () => { if (typeof GM_openInTab === "function") GM_openInTab(QQ_GROUP_LINK, { active: true }); else window.open(QQ_GROUP_LINK, "_blank"); }); } function createProModal() { const old = document.getElementById("syjc-pro-modal"); if (old) old.remove(); const modal = document.createElement("div"); modal.id = "syjc-pro-modal"; modal.innerHTML = `
\u5f00\u901a Pro
Pro \u53ef\u65e0\u9650\u5236\u5b66\u4e60\u8bfe\u7a0b\uff08\u89c6\u9891+\u81ea\u6d4b\uff09\uff0c\u514d\u8d39\u4f53\u9a8c\u9650 1 \u95e8\u8bfe

Pro \u6743\u76ca

  • \u89e3\u9664\u514d\u8d39 1 \u95e8\u8bfe\u9650\u5236\uff0c\u961f\u5217\u53ef\u6301\u7eed\u5b66\u4e60
  • \u5b66\u5b8c\u540e\u81ea\u52a8\u5b8c\u6210\u81ea\u6d4b\u8003\u8bd5
  • Pro \u6709\u6548\u671f 10 \u5929\uff08\u7ed1\u5b9a\u5f53\u524d\u767b\u5f55\u8d26\u53f7\uff09
\u63d0\u793a\uff1aPro \u6743\u9650\u7ed1\u5b9a\u5f53\u524d\u767b\u5f55\u8d26\u53f7\uff0c\u8bf7\u52ff\u4e0e\u4ed6\u4eba\u5171\u7528 Token\u3002

\u5f00\u901a\u65b9\u5f0f

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

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

\u8d2d\u5f97 Token \u540e\uff1a\u5728\u8bbe\u7f6e\u9875\u7c98\u8d34\uff0c\u70b9\u51fb\u300c\u4fdd\u5b58\u5e76\u6821\u9a8c\u300d\u3002
\u91cd\u8981\uff1a\u8bf7\u52ff\u968f\u610f\u6cc4\u9732 Token\uff0c\u907f\u514d\u8d26\u53f7\u88ab\u591a\u4eba\u5171\u7528\u5bfc\u81f4\u5931\u6548\u3002
`; document.body.appendChild(modal); modal.addEventListener("click", (e) => { if (e.target === modal) closeProModal(); }); modal.querySelector("#syjc-pro-close")?.addEventListener("click", closeProModal); modal.querySelector("#syjc-pro-buy-link")?.addEventListener("click", openProBuyPage); } function readPanelPos() { try { return JSON.parse(localStorage.getItem(PANEL_POS_KEY) || "null"); } catch (_) { return null; } } function writePanelPos(pos) { localStorage.setItem(PANEL_POS_KEY, JSON.stringify(pos)); } function readPanelCollapsed() { return localStorage.getItem(PANEL_COLLAPSED_KEY) === "1"; } function writePanelCollapsed(v) { localStorage.setItem(PANEL_COLLAPSED_KEY, v ? "1" : "0"); } function applyPanelCollapsed(panel, collapsed) { panel.classList.toggle("syjc-panel-min", !!collapsed); panel.classList.toggle("syjc-panel-max", !collapsed); const btnMin = document.getElementById("syjc-btn-min"); const btnMax = document.getElementById("syjc-btn-max"); if (btnMin) btnMin.style.display = collapsed ? "none" : ""; if (btnMax) btnMax.style.display = collapsed ? "" : "none"; } function enablePanelDrag(panel) { const header = panel.querySelector("#syjc-panel-header"); if (!header) return; let dragging = false; let ox = 0; let oy = 0; header.addEventListener("mousedown", (e) => { if (e.target?.closest("#syjc-panel-controls, .syjc-panel-ctl")) return; dragging = true; const rect = panel.getBoundingClientRect(); ox = e.clientX - rect.left; oy = e.clientY - rect.top; panel.style.right = "auto"; e.preventDefault(); }); window.addEventListener("mousemove", (e) => { if (!dragging) return; const left = Math.min(window.innerWidth - 80, Math.max(0, e.clientX - ox)); const top = Math.min(window.innerHeight - 40, Math.max(0, e.clientY - oy)); panel.style.left = left + "px"; panel.style.top = top + "px"; }); window.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; writePanelPos({ left: parseInt(panel.style.left, 10) || 0, top: parseInt(panel.style.top, 10) || 0 }); }); } function injectPanelStyles() { const id = "syjc-panel-style-v2"; if (document.getElementById(id) || !document.head) return; const st = document.createElement("style"); st.id = id; st.textContent = ` #syjc-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;} #syjc-auto-panel.syjc-panel-hidden{display:none !important;} #syjc-auto-panel.syjc-panel-max{max-height:min(92vh,780px);} #syjc-auto-panel.syjc-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);} #syjc-auto-panel.syjc-panel-min #syjc-panel-header{border-bottom:none;} #syjc-auto-panel.syjc-panel-min #syjc-panel-body,#syjc-auto-panel.syjc-panel-min #syjc-panel-footer,#syjc-auto-panel.syjc-panel-min .syjc-footer-extra{display:none !important;} #syjc-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;} #syjc-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;} #syjc-panel-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;} .syjc-footer-extra{flex:0 0 auto;padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;} #syjc-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #syjc-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;} #syjc-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;} .syjc-panel-title-text{flex:1 1 12em;min-width:0;} #syjc-panel-sub{display:flex;flex-wrap:wrap;align-items:center;gap:3px 5px;margin-top:3px;} .syjc-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;} .syjc-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);} .syjc-sub-dot{color:#d6d3d1;font-size:10px;} .syjc-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;} #syjc-panel-controls{display:flex;align-items:center;gap:5px;} .syjc-panel-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1);font-size:15px;font-weight:900;padding:0;} .syjc-ctl-min{display:block;width:10px;height:2px;background:#64748b;border-radius:1px;margin:0 auto;} .syjc-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .syjc-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;} .syjc-status-label{font-size:11px;color:#64748b;font-weight:700;} #syjc-auto-status{padding:2px 7px;border-radius:999px;font-weight:900;font-size:11px;background:#fff;border:1px solid #cbd5e1;} .syjc-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;flex:1;min-width:0;} .syjc-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .syjc-progress-pct{font-size:14px;font-weight:900;color:#0369a1;} .syjc-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;} .syjc-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;} .syjc-tabbar{display:flex;gap:6px;margin-bottom:7px;} .syjc-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;} .syjc-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .syjc-pane{display:none;}.syjc-pane.active{display:block;} .syjc-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;} .syjc-list-title{font-size:12px;color:#64748b;font-weight:700;} .syjc-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;} #syjc-course-list,#syjc-chapter-preview,#syjc-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} .syjc-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;} .syjc-course-item{display:flex;gap:8px;align-items:flex-start;padding:6px 7px;border-bottom:1px dashed #d4deea;font-size:12px;} .syjc-course-item label{flex:1;cursor:pointer;line-height:1.35;} .syjc-course-meta{display:block;color:#64748b;font-size:11px;margin-top:2px;} .syjc-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;} .syjc-chapter-title{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;} .syjc-chapter-item{padding:5px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;} .syjc-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;} .syjc-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;} .syjc-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:11px;margin-bottom:2px;align-items:flex-start;} .syjc-meta-label{color:#64748b;flex:0 0 auto;font-size:10px;white-space:nowrap;} .syjc-meta-value{font-weight:600;text-align:right;flex:1;min-width:0;word-break:break-all;} .syjc-settings-val{font-weight:600;text-align:right;color:#0f172a;} .syjc-btn-row{display:flex;gap:7px;margin-bottom:6px;} .syjc-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;} .syjc-btn-start{background:#16a34a;}.syjc-btn-stop{background:#ef4444;}.syjc-btn-refresh{background:#64748b;} .syjc-btn-start:disabled,.syjc-btn-stop:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;} .syjc-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;flex:0 0 auto;} .syjc-btn-pro{background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;border:none;padding:4px 8px;font-size:12px;border-radius:10px;cursor:pointer;font-weight:800;} .syjc-plan-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 9px;font-size:12px;background:#fff;} .syjc-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;} .syjc-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;} .syjc-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;} .syjc-ann-text{display:block;word-break:break-word;} .syjc-ann-adapt-link{display:inline-flex;align-items:center;gap:4px;margin-top:6px;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:900;color:#9a3412;background:linear-gradient(135deg,#fef3c7,#fdba74);border:1px solid #f59e0b;box-shadow:0 1px 0 rgba(245,158,11,.35);cursor:pointer;text-decoration:none;line-height:1.3;user-select:none;} .syjc-ann-adapt-link:hover{filter:brightness(1.04);color:#7c2d12;} .syjc-ann-adapt-link:active{transform:translateY(1px);} .syjc-qq-row{display:flex;align-items:center;justify-content:space-between;gap:9px;margin-top:5px;} .syjc-qq-btn{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:5px 12px;border-radius:999px;font-size:12px;font-weight:800;cursor:pointer;} .syjc-filter-row{display:flex;gap:6px;margin-bottom:6px;} .syjc-filter-row .syjc-plan-select{flex:1;} #syjc-pro-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000001;display:none;align-items:center;justify-content:center;padding:20px;} .syjc-pro-card{width:min(560px,92vw);max-height:88vh;overflow:auto;background:#fff;border-radius:18px;border:1px solid #dbe4f0;box-shadow:0 18px 46px rgba(15,23,42,.25);padding:16px;} .syjc-pro-title{font-size:24px;font-weight:900;color:#0f172a;} .syjc-pro-sub{font-size:13px;color:#64748b;margin-top:4px;line-height:1.45;} .syjc-pro-sec{margin-top:12px;border:1px solid #dbe4f0;border-radius:12px;padding:12px;background:#f8fafc;} .syjc-pro-sec h4{margin:0 0 8px;font-size:15px;color:#0f172a;} .syjc-pro-sec p{margin:4px 0;font-size:13px;color:#334155;line-height:1.5;} .syjc-pro-sec ul{margin:6px 0 0 18px;padding:0;} .syjc-pro-sec li{margin:4px 0;font-size:13px;color:#334155;line-height:1.45;} .syjc-pro-buy{display:flex;flex-direction:column;gap:10px;margin-top:8px;} .syjc-pro-muted{color:#64748b;font-size:12px;line-height:1.5;} .syjc-pro-buy-btn{display:inline-block;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 16px;border-radius:12px;font-size:14px;font-weight:800;border:none;cursor:pointer;align-self:flex-start;} .syjc-pro-tip{margin-top:8px;background:#fef3c7;border:1px solid #fcd34d;border-radius:10px;padding:8px 10px;font-size:13px;color:#92400e;font-weight:700;line-height:1.45;} .syjc-pro-tip-info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:600;} .syjc-pro-actions{margin-top:14px;display:flex;justify-content:flex-end;} .syjc-pro-close{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 18px;border-radius:12px;font-size:14px;font-weight:800;cursor:pointer;} #syjc-adapt-modal{position:fixed;inset:0;z-index:1000001;display:none;align-items:center;justify-content:center;background:rgba(15,23,42,.45);padding:16px;} .syjc-adapt-card{max-height:min(86vh,640px);overflow:auto;width:min(520px,100%);} .syjc-adapt-item{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:1px dashed #e2e8f0;} .syjc-adapt-item:last-child{border-bottom:none;} .syjc-adapt-item-title{font-size:12px;font-weight:800;color:#0f172a;line-height:1.4;} .syjc-adapt-item-note{font-size:11px;color:#64748b;margin-top:3px;line-height:1.4;} .syjc-adapt-badge{flex:0 0 auto;font-size:11px;font-weight:900;padding:2px 8px;border-radius:999px;border:1px solid transparent;} .syjc-adapt-ok{color:#047857;background:#ecfdf5;border-color:#a7f3d0;} .syjc-adapt-wait{color:#b45309;background:#fffbeb;border-color:#fcd34d;} .syjc-adapt-pause{color:#64748b;background:#f8fafc;border-color:#cbd5e1;} .syjc-adapt-empty{font-size:12px;color:#94a3b8;padding:4px 0;} .syjc-sub-chip-link{cursor:pointer;} .syjc-sub-chip-link:hover{filter:brightness(0.97);} .syjc-pending-box{padding:10px 4px 4px;} .syjc-pending-title{font-size:14px;font-weight:900;color:#0f172a;margin-bottom:6px;} .syjc-pending-desc{font-size:12px;color:#475569;line-height:1.55;margin-bottom:10px;} `; document.head.appendChild(st); } function createPanel() { if (isVideoPage) return; const old = document.getElementById("syjc-auto-panel"); if (old) old.remove(); injectPanelStyles(); const panel = document.createElement("div"); panel.id = "syjc-auto-panel"; panel.className = "syjc-panel-max"; panel.innerHTML = `
${PRODUCT_NAME} v${SCRIPT_VERSION}
\u5f53\u524d\uff1a\u8bd5\u9a8c\u68c0\u6d4b · \u5176\u4ed6\u7ee7\u7eed\u6559\u80b2 · \u4e00\u952e\u5b8c\u6210\u8bfe\u65f6\u8003\u8bd5
\u8fd0\u884c\u72b6\u6001 \u5df2\u505c\u6b62
0/0 \u89c6\u9891\u5df2\u5b66\u4e60
0%
\u5df2\u8d2d\u8bfe\u7a0b \u52fe\u9009\u961f\u5217
\u767b\u5f55\u540e\u81ea\u52a8\u52a0\u8f7d
\u7ae0\u8282\u9884\u89c8\u5df2\u9009\u8bfe\u7a0b
\u8bf7\u52fe\u9009\u8bfe\u7a0b
\u8fd0\u884c\u65e5\u5fd7
\u6388\u6743\u4e0e\u9009\u9879\u8bbe\u7f6e
\u7528\u6237\u7c7b\u578b\u672a\u6821\u9a8c
\u514d\u8d39\u4f53\u9a8c\uff081\u95e8\u8bfe\uff090/1
Token
\u5f53\u524d\u8bfe\u7a0b\u65e0
\u5f53\u524d\u7ae0\u8282\u65e0
\u5f53\u524d\u4efb\u52a1\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u63d0\u4ea4
`; document.documentElement.appendChild(panel); createProModal(); createAdaptModal(); document.getElementById("syjc-open-adapt")?.addEventListener("click", (e) => { e.stopPropagation(); openAdaptModal(); }); document.getElementById("syjc-ann-adapt")?.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); openAdaptModal(); }); const savedPos = readPanelPos(); if (savedPos && savedPos.left != null) { panel.style.right = "auto"; panel.style.left = savedPos.left + "px"; panel.style.top = (savedPos.top || 80) + "px"; } applyPanelCollapsed(panel, readPanelCollapsed()); enablePanelDrag(panel); document.getElementById("syjc-btn-min")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); document.getElementById("syjc-btn-max")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); panel.querySelectorAll(".syjc-tab-btn").forEach((btn) => { btn.addEventListener("click", () => { switchPanelTab(btn.dataset.tab); if (btn.dataset.tab === "chapter") renderChapterPreview(); }); }); document.getElementById("syjc-start")?.addEventListener("click", () => { if (state.busy) return; if (!state.queue.length) { setPanelHint("\u8bf7\u5148\u52fe\u9009\u81f3\u5c11\u4e00\u95e8\u8bfe\u7a0b\u518d\u70b9\u5f00\u59cb"); return; } setPanelHint(""); void runQueue(); }); document.getElementById("syjc-stop")?.addEventListener("click", () => stopAutoStudy("\u7528\u6237\u70b9\u51fb\u505c\u6b62")); document.getElementById("syjc-refresh")?.addEventListener("click", () => void refreshCourses()); document.getElementById("syjc-clear-log")?.addEventListener("click", () => { state.logLines = []; const box = document.getElementById("syjc-run-log"); if (box) box.innerHTML = ""; }); document.getElementById("syjc-sel-undone")?.addEventListener("click", () => { const ids = state.courses.filter((c) => !isCourseDone(c)).map(courseIdOf); saveQueue(ids); renderCourseList(); updatePanel(); }); document.getElementById("syjc-sel-none")?.addEventListener("click", () => { saveQueue([]); renderCourseList(); updatePanel(); }); document.getElementById("syjc-year-filter")?.addEventListener("change", () => renderCourseList()); document.getElementById("syjc-join-qq")?.addEventListener("click", () => { if (typeof GM_openInTab === "function") GM_openInTab(QQ_GROUP_LINK, { active: true }); else window.open(QQ_GROUP_LINK, "_blank"); }); document.getElementById("syjc-open-pro")?.addEventListener("click", () => openProModal()); document.getElementById("syjc-cloud-save")?.addEventListener("click", async () => { const input = document.getElementById("syjc-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); log("\u6388\u6743\u5df2\u66f4\u65b0"); } catch (err) { log("Token \u6821\u9a8c\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u540e\u91cd\u8bd5"); } updateCloudPanelUI(); }); state.cloudToken = String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(); const lastState = readCloudLastState(); if (lastState) { if (lastState.tier) state.cloudTier = lastState.tier; if (lastState.freeCourseLimit > 0) state.freeCourseLimit = lastState.freeCourseLimit; if (lastState.freeUsedCourses >= 0) state.freeUsedCourses = lastState.freeUsedCourses; } updateCloudPanelUI(); updatePanel(); void _cf(); } function updatePanel() { const statusEl = document.getElementById("syjc-auto-status"); const startBtn = document.getElementById("syjc-start"); const stopBtn = document.getElementById("syjc-stop"); if (statusEl) statusEl.textContent = state.busy ? "\u8fd0\u884c\u4e2d" : "\u5df2\u505c\u6b62"; if (startBtn) startBtn.disabled = !!state.busy; if (stopBtn) stopBtn.disabled = !state.busy; const total = state.queueVideoTotal || 0; const done = state.queueVideoDone || 0; const pct = total ? Math.min(100, Math.round((done / total) * 100)) : 0; const dEl = document.getElementById("syjc-queue-done"); const tEl = document.getElementById("syjc-queue-total"); const pEl = document.getElementById("syjc-queue-percent"); const bar = document.getElementById("syjc-queue-progress"); if (dEl) dEl.textContent = String(done); if (tEl) tEl.textContent = String(total); if (pEl) pEl.textContent = pct + "%"; if (bar) bar.style.width = pct + "%"; const cc = document.getElementById("syjc-current-course"); const ch = document.getElementById("syjc-current-chapter"); const tk = document.getElementById("syjc-current-task"); if (cc) cc.textContent = state.currentCourse || "\u65e0"; if (ch) ch.textContent = state.currentChapter || "\u65e0"; if (tk) tk.textContent = state.currentTask || state.panelHint || "\u70b9\u5f00\u59cb\u540e\u81ea\u52a8\u63d0\u4ea4"; const qText = document.getElementById("syjc-queue-text"); if (qText) qText.textContent = state.queue.length ? `\u5df2\u9009 ${state.queue.length} \u95e8\u8bfe` : "\u672a\u9009\u62e9"; const nameEl = document.getElementById("syjc-user-name"); if (nameEl && state.userBean) { nameEl.textContent = state.userBean.name || state.userBean.loginusername || state.userBean.id; } const stuEl = document.getElementById("syjc-user-student"); if (stuEl && state.userBean) { stuEl.textContent = state.userBean.studentno || "—"; } updateCloudPanelUI(); } function renderCourseList() { const box = document.getElementById("syjc-course-list"); if (!box) return; const year = String(document.getElementById("syjc-year-filter")?.value || ""); let list = state.courses.slice(); if (year) list = list.filter((c) => String(c.years || c.batchname || "") === year); if (!list.length) { box.innerHTML = `
${state.courses.length ? "\u8be5\u5e74\u5ea6\u65e0\u8bfe\u7a0b" : "\u6682\u65e0\u5df2\u8d2d\u8bfe\u7a0b"}
`; return; } box.innerHTML = list .map((c) => { const id = courseIdOf(c); const checked = state.queue.includes(id) ? "checked" : ""; const done = isCourseDone(c); const rate = Math.round((Number(c.learnrate) || 0) * 100); const title = escapeHtml(c.classname || c.coursename || id); const statusText = done ? "\u5df2\u5b8c\u6210" : String(c.coursestatename || "\u672a\u5b8c\u6210"); const examText = c.examStateText || "\u81ea\u6d4b\u52a0\u8f7d\u4e2d"; const meta = escapeHtml( `${c.years || c.batchname || "-"} · ${statusText} · ${rate}% · ${c.coursetimelength || c.classtime || "-"}\u5b66\u65f6 · ${examText}` ); return `
`; }) .join(""); box.querySelectorAll("input[type=checkbox]").forEach((inp) => { inp.addEventListener("change", () => { const id = inp.getAttribute("data-cid"); const set = new Set(state.queue); if (inp.checked) set.add(id); else set.delete(id); saveQueue([...set]); updatePanel(); }); }); } async function renderChapterPreview() { const box = document.getElementById("syjc-chapter-preview"); if (!box) return; const selected = state.courses.filter((c) => state.queue.includes(courseIdOf(c))); if (!selected.length) { box.innerHTML = `
\u8bf7\u52fe\u9009\u8bfe\u7a0b
`; return; } box.innerHTML = `
\u52a0\u8f7d\u7ae0\u8282…
`; const parts = []; for (const c of selected.slice(0, 8)) { const cid = courseIdOf(c); try { const chs = await ensureChapters(cid); parts.push(`
${escapeHtml(c.classname || c.coursename || cid)}
${chs .map( (v) => `
${escapeHtml(v.name)}${v.done ? "✓" : "○"}
` ) .join("")}
`); } catch (_) { parts.push( `
${escapeHtml(c.classname || cid)}
\u52a0\u8f7d\u5931\u8d25
` ); } } box.innerHTML = parts.join("") || `
\u65e0\u7ae0\u8282
`; } function fillYearFilter() { const sel = document.getElementById("syjc-year-filter"); if (!sel) return; const years = [...new Set(state.courses.map((c) => String(c.years || c.batchname || "")).filter(Boolean))].sort( (a, b) => Number(b) - Number(a) ); const cur = sel.value; sel.innerHTML = `` + years.map((y) => ``).join(""); if (years.includes(cur)) sel.value = cur; } async function refreshCourses(opts = {}) { const keepChapters = !!opts.keepChapters; const silent = !!opts.silent; try { await _ub(); const list = await getBoughtCourseList(); const prevExam = {}; for (const c of state.courses || []) { const id = courseIdOf(c); if (c.examStateText) prevExam[id] = c.examStateText; } state.courses = (Array.isArray(list) ? list : []).map((c) => { const id = courseIdOf(c); if (prevExam[id]) c.examStateText = prevExam[id]; return c; }); if (!keepChapters) state.chaptersByCourse = {}; fillYearFilter(); const allIds = new Set(state.courses.map(courseIdOf)); saveQueue(state.queue.filter((id) => allIds.has(id))); renderCourseList(); updatePanel(); updateCloudPanelUI(); void enrichCoursesExamState(state.courses); try { await _el(true); } catch (_) {} } catch (e) { if (!silent) log("\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55"); if (!silent) { const box = document.getElementById("syjc-course-list"); if (box) box.innerHTML = `
\u8bf7\u5148\u767b\u5f55\u5e73\u53f0
`; } } } function createPendingPanel() { const old = document.getElementById("syjc-auto-panel"); if (old) old.remove(); injectPanelStyles(); const title = CURRENT_CE.shortTitle || CURRENT_CE.title || "\u7ee7\u7eed\u6559\u80b2"; const statusText = cePendingStatusText(CURRENT_CE); const panel = document.createElement("div"); panel.id = "syjc-auto-panel"; panel.className = "syjc-panel-max"; panel.innerHTML = `
${PRODUCT_NAME} v${SCRIPT_VERSION}
\u5f53\u524d\uff1a${escapeHtml(title)} · ${escapeHtml(statusText)}
${escapeHtml(CURRENT_CE.title || title)}
${ CURRENT_CE.status === "portal" ? "\u8bf7\u4ece\u95e8\u6237\u8fdb\u5165\u5177\u4f53\u7ee7\u7eed\u6559\u80b2\u7cfb\u7edf\u3002\u8bd5\u9a8c\u68c0\u6d4b\u5df2\u9002\u914d\uff1b\u5176\u5b83\u7cfb\u7edf\u53ef\u8054\u7cfb\u4f5c\u8005\u9002\u914d\u3002" : CURRENT_CE.status === "adapted" ? "\u5f53\u524d\u7cfb\u7edf\u5df2\u9002\u914d\uff0c\u8bf7\u8fdb\u5165\u5b66\u4e60\u9875\u9762\u4f7f\u7528\u5b8c\u6574\u529f\u80fd\u3002" : "\u8be5\u7ee7\u7eed\u6559\u80b2\u5c1a\u672a\u63a5\u5165\u81ea\u52a8\u5b66\u4e60\u3002\u70b9\u51fb\u4e0b\u65b9\u6309\u94ae\u67e5\u770b\u5df2\u9002\u914d\u4e0e\u53ef\u9002\u914d\u5217\u8868\uff0c\u5e76\u8054\u7cfb\u4f5c\u8005\u5f00\u901a\u3002" }
`; document.documentElement.appendChild(panel); createAdaptModal(); const savedPos = readPanelPos(); if (savedPos && savedPos.left != null) { panel.style.right = "auto"; panel.style.left = savedPos.left + "px"; panel.style.top = (savedPos.top || 80) + "px"; } applyPanelCollapsed(panel, readPanelCollapsed()); enablePanelDrag(panel); document.getElementById("syjc-btn-min")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); document.getElementById("syjc-btn-max")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); document.getElementById("syjc-pending-adapt")?.addEventListener("click", openAdaptModal); document.getElementById("syjc-pending-list")?.addEventListener("click", openAdaptModal); document.getElementById("syjc-ann-adapt")?.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); openAdaptModal(); }); document.getElementById("syjc-join-qq")?.addEventListener("click", () => { if (typeof GM_openInTab === "function") GM_openInTab(QQ_GROUP_LINK, { active: true }); else window.open(QQ_GROUP_LINK, "_blank"); }); } function boot() { if (IS_SYJC_ADAPTED && isVideoPage) { console.log("[\u4ea4\u901a\u804c\u4e1a\u8d44\u683c\u52a9\u624b] \u64ad\u653e\u9875\u4e0d\u663e\u793a\u9762\u677f"); return; } const ready = () => { if (IS_SYJC_ADAPTED) { createPanel(); void refreshCourses(); } else { createPendingPanel(); } void _cf(); }; if (document.body) ready(); else document.addEventListener("DOMContentLoaded", ready); } boot(); })();