// ==UserScript== // @name 北京市继续医学教育全员必修课培训助手(1分钟完成) // @namespace https://bjsqypx.haoyisheng.com/ // @version 1.0 // @description 北京市继续医学教育全员必修课培训助手:自动完成学时与章节考试;免费体验1节,Pro不限量10天。速度效率完成,1分钟完成。请在培训首页使用。 // @author 柠檬真酸 // @match https://bjsqypx.haoyisheng.com/* // @match http://bjsqypx.haoyisheng.com/* // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @connect bjsqypx.haoyisheng.com // @connect oa35.ahzsksw.cn // @connect huaweicloudobs.ahjxjy.cn // @connect card.wlxy.live // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @run-at document-idle // @antifeature payment 免费体验1章节,升级Pro不限量 // @antifeature membership 需云端授权 // @license All Rights Reserved // ==/UserScript== (function () { "use strict"; const SCRIPT_VERSION = "1.0"; const API_BASE = location.origin; const BJ = "/qypx/bj"; const EXAM_MAX_TRIES = 48; const PANEL_LOGO_URL = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const PANEL_NOTICE_FALLBACK = "\u5317\u4eac\u5168\u5458\u5fc5\u4fee\u8bfe\u57f9\u8bad\u52a9\u624b"; const DEFAULT_PANEL_NOTICE_PATH = "/api/bjqx/panel-notice"; const PINNED_CLOUD_HOST = "oa35.ahzsksw.cn"; const DEFAULT_CLOUD_API_BASE = `https://${PINNED_CLOUD_HOST}`; const CLOUD_API_PREFIX = "/api/bjqx"; const DEFAULT_FREE_CHAPTER_LIMIT = 1; const PRO_BUY_URL = "https://card.wlxy.live/details/3C227B2A"; const PRO_DAYS = 10; const CLOUD_TOKEN_KEY = "bjqx_cloud_token_v1"; const CLOUD_LEASE_CACHE_KEY = "bjqx_cloud_lease_cache_v1"; const CLOUD_FREE_USED_KEY = "bjqx_cloud_free_used_v1"; const QUEUE_KEY = "bjqx_course_queue_v1"; const PANEL_POS_KEY = "bjqx_panel_pos_v1"; const PANEL_COLLAPSED_KEY = "bjqx_panel_collapsed_v1"; const AUTO_EXAM = true; const KNOWN_COURSE_TITLES = { "202601016940": "2026\u5e74\u4f20\u67d3\u75c5\u9632\u6cbb\u77e5\u8bc6\u5168\u5458\u57f9\u8bad", "202601016941": "2026\u5e74\u5317\u4eac\u5e02\u75be\u75c5\u9884\u9632\u63a7\u5236\u5c40\u75ab\u82d7\u7ba1\u7406\u6cd5\u53ca\u75ab\u82d7\u53ef\u9884\u9632\u4f20\u67d3\u75c5\u9632\u63a7\u77e5\u8bc6\u57f9\u8bad", "202601016942": "2026\u5e74\u9996\u90fd\u536b\u751f\u5065\u5eb7\u6cd5\u5f8b\u8bb2\u5802\u548c“\u533b\u6848\u8bf4\u6cd5”\u6cd5\u5f8b\u6c99\u9f99", "202601016943": "2026\u5e74\u7c7b\u5668\u5b98\u53ca\u5668\u5b98\u82af\u7247\u7684\u7814\u7a76\u3001\u5e94\u7528\u4e0e\u76d1\u7ba1", "202601016944": "2026\u5e74\u7cbe\u795e\u536b\u751f\u548c\u5fc3\u7406\u5065\u5eb7\u4fc3\u8fdb\u6280\u80fd\u57f9\u8bad", }; const state = { enabled: false, stopFlag: false, panelRefreshing: false, panelCourses: [], chapterPreview: [], queueVideoTotal: 0, queueVideoDone: 0, queueExamDone: 0, queueExamTotal: 0, currentCourseTitle: "", currentChapterTitle: "", currentTask: "\u52fe\u9009\u8bfe\u7a0b\u540e\u70b9\u300c\u5f00\u59cb\u300d", panelHint: "", userLabel: "", logs: [], cloudApiBase: "", cloudToken: "", cloudLease: "", cloudTier: "unknown", cloudExpireAt: 0, cloudRevoked: false, freeChapterLimit: DEFAULT_FREE_CHAPTER_LIMIT, freeUsed: 0, remotePanelNotice: "", panelNoticePath: DEFAULT_PANEL_NOTICE_PATH, proBuyUrl: PRO_BUY_URL, cloudReady: false, cloudLastError: "", }; function escHtml(s) { return String(s || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function readJson(key, fallback) { try { const raw = localStorage.getItem(key); if (!raw) return fallback; return JSON.parse(raw); } catch (_) { return fallback; } } function writeJson(key, val) { localStorage.setItem(key, JSON.stringify(val)); } function absUrl(path) { if (!path) return API_BASE; if (/^https?:\/\//i.test(path)) return path; if (path.startsWith("/")) return API_BASE + path; return API_BASE + BJ + "/" + String(path).replace(/^\.\//, ""); } function qs(name, url) { try { return new URL(url || location.href).searchParams.get(name) || ""; } catch (_) { return ""; } } function log(msg) { const line = String(msg || "").trim(); if (!line) return; if (/\u4e91\u7aef\u7f16\u6392|\u672c\u5730\u4ec5\u4ee3\u53d1|\u4ee3\u53d1\u8bf7\u6c42|\u4e91\u7aef\u5904\u7406/.test(line)) return; const row = `[${new Date().toLocaleTimeString()}] ${line}`; state.logs.push(row); if (state.logs.length > 120) state.logs.shift(); const box = document.getElementById("bjqx-run-log"); if (box) { box.innerHTML = state.logs .slice(-80) .map((x) => `
${escHtml(x)}
`) .join(""); box.scrollTop = box.scrollHeight; } } function friendlyErr(err) { const m = String(err?.message || err || ""); if (/\u505c\u6b62|\u5df2\u505c\u6b62/.test(m)) return "\u5df2\u505c\u6b62"; if (/\u767b\u5f55|\u672a\u767b\u5f55|cookie|\u4f1a\u8bdd/i.test(m)) return "\u8bf7\u5148\u767b\u5f55\u5e73\u53f0\u8d26\u53f7"; if (/free_quota|\u989d\u5ea6|\u514d\u8d39.*\u7528\u5b8c/i.test(m)) return "\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"; if (/\u989d\u5ea6|\u514d\u8d39|Pro|\u6388\u6743|lease|token|\u4e91\u7aef|404|\u7f51\u7edc|\u8d85\u65f6|quota|revoked/i.test(m)) { if (/\u7981\u7528|revoked/i.test(m)) return "\u670d\u52a1\u6682\u4e0d\u53ef\u7528\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"; return "\u670d\u52a1\u6821\u9a8c\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"; } if (/\u8003\u8bd5/.test(m)) return "\u8003\u8bd5\u672a\u5b8c\u6210\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"; if (/\u5b66\u65f6|\u8bfe\u4ef6|\u64ad\u653e|\u7b7e\u540d|saveStudy/i.test(m)) return "\u5b66\u65f6\u63d0\u4ea4\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"; return "\u64cd\u4f5c\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"; } function clearRunLog() { state.logs = []; const box = document.getElementById("bjqx-run-log"); if (box) box.innerHTML = `
\u6682\u65e0\u65e5\u5fd7
`; } function gmRequest(method, url, opts) { opts = opts || {}; return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== "function") { reject(new Error("\u9700\u8981 Tampermonkey / ScriptCat")); return; } const headers = Object.assign({ Accept: opts.accept || "*/*" }, opts.headers || {}); if (opts.xhr) headers["X-Requested-With"] = "XMLHttpRequest"; GM_xmlhttpRequest({ method: method || "GET", url, headers, data: opts.body || undefined, responseType: "text", timeout: opts.timeout || 45000, anonymous: false, onload(r) { resolve({ status: r.status, text: r.responseText || "", url: r.finalUrl || r.responseURL || url, headers: r.responseHeaders || "", }); }, onerror() { reject(new Error("\u7f51\u7edc\u9519\u8bef " + url)); }, ontimeout() { reject(new Error("\u8d85\u65f6 " + url)); }, }); }); } async function httpGet(path, opts) { const url = absUrl(path); const o = opts || {}; return gmRequest("GET", url, { accept: o.accept || "text/html,application/json,*/*", xhr: !!o.xhr, headers: o.headers || {}, timeout: o.timeout, }); } async function httpPostForm(path, body, opts) { const url = absUrl(path); const o = opts || {}; const payload = typeof body === "string" ? body : new URLSearchParams(body).toString(); return gmRequest("POST", url, { body: payload, timeout: o.timeout, headers: Object.assign( { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", Accept: "text/html,application/xhtml+xml,*/*", }, o.headers || {} ), }); } function parseDoc(html) { return new DOMParser().parseFromString(String(html || ""), "text/html"); } function getCloudApiBase() { state.cloudApiBase = DEFAULT_CLOUD_API_BASE; return DEFAULT_CLOUD_API_BASE; } function readCloudToken() { return String(localStorage.getItem(CLOUD_TOKEN_KEY) || state.cloudToken || "").trim(); } function writeCloudToken(token) { const t = String(token || "").trim(); state.cloudToken = t; if (t) localStorage.setItem(CLOUD_TOKEN_KEY, t); else localStorage.removeItem(CLOUD_TOKEN_KEY); } function readLocalFreeUsed() { return Math.max(0, Number(localStorage.getItem(CLOUD_FREE_USED_KEY) || 0) || 0); } function writeLocalFreeUsed(n) { localStorage.setItem(CLOUD_FREE_USED_KEY, String(Math.max(0, Number(n) || 0))); state.freeUsed = readLocalFreeUsed(); } function readCloudLeaseCache() { try { const p = JSON.parse(localStorage.getItem(CLOUD_LEASE_CACHE_KEY) || "null"); if (!p || !p.lease) return null; return p; } catch (_) { return null; } } function writeCloudLeaseCache(lease, exp, extra) { if (!lease || !exp) { localStorage.removeItem(CLOUD_LEASE_CACHE_KEY); return; } writeJson(CLOUD_LEASE_CACHE_KEY, Object.assign({ lease, exp }, extra || {})); } function _crq(path, method, body, headerExtra) { const p = String(path || "").trim(); const url = `${getCloudApiBase()}${p.startsWith("/") ? p : `/${p}`}`; const headers = Object.assign( { Accept: "application/json", "Content-Type": "application/json" }, headerExtra || {} ); const token = readCloudToken(); if (token) headers.Authorization = `Bearer ${token}`; return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== "function") { reject(new Error("\u670d\u52a1\u6821\u9a8c\u5931\u8d25")); return; } GM_xmlhttpRequest({ method: method || "GET", url, headers, data: body != null ? JSON.stringify(body) : undefined, responseType: "text", timeout: 20000, anonymous: true, onload(resp) { const status = Number(resp.status || 0); const text = String(resp.responseText || ""); if (status === 404) { reject(new Error("\u670d\u52a1\u6821\u9a8c\u5931\u8d25")); return; } if (status < 200 || status >= 300) { reject(new Error("\u670d\u52a1\u6821\u9a8c\u5931\u8d25")); return; } try { resolve(text ? JSON.parse(text) : {}); } catch (_) { resolve({ raw: text }); } }, onerror() { reject(new Error("\u670d\u52a1\u6821\u9a8c\u5931\u8d25")); }, ontimeout() { reject(new Error("\u670d\u52a1\u6821\u9a8c\u5931\u8d25")); }, }); }); } function _crt(path) { const p = String(path || "").trim() || DEFAULT_PANEL_NOTICE_PATH; const url = `${getCloudApiBase()}${p.startsWith("/") ? p : `/${p}`}`; return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== "function") { reject(new Error("\u670d\u52a1\u6821\u9a8c\u5931\u8d25")); return; } GM_xmlhttpRequest({ method: "GET", url, headers: { Accept: "text/plain, */*" }, timeout: 15000, anonymous: true, onload(resp) { const status = Number(resp.status || 0); if (status < 200 || status >= 300) { reject(new Error(`http_${status}`)); return; } resolve(String(resp.responseText || "").trim()); }, onerror() { reject(new Error("\u670d\u52a1\u6821\u9a8c\u5931\u8d25")); }, ontimeout() { reject(new Error("\u670d\u52a1\u6821\u9a8c\u5931\u8d25")); }, }); }); } function applyLeasePayload(data) { if (!data || typeof data !== "object") return; if (data.lease) state.cloudLease = String(data.lease); const exp = Number(data.exp ?? data.expire_at ?? data.expires_at ?? data.lease_exp ?? 0); if (exp > 0) state.cloudExpireAt = exp; if (data.tier) state.cloudTier = String(data.tier); if (data.revoked) state.cloudRevoked = true; const freeUsed = data.freeUsed ?? data.free_used_chapters ?? data.used; const freeLimit = data.freeLimit ?? data.free_chapter_limit ?? data.limit; if (freeUsed != null) writeLocalFreeUsed(freeUsed); if (freeLimit != null) state.freeChapterLimit = Number(freeLimit) || DEFAULT_FREE_CHAPTER_LIMIT; if (state.cloudLease && state.cloudExpireAt) { writeCloudLeaseCache(state.cloudLease, state.cloudExpireAt, { tier: state.cloudTier }); } state.cloudReady = true; } async function _fpn() { try { const text = await _crt(state.panelNoticePath || DEFAULT_PANEL_NOTICE_PATH); if (text) state.remotePanelNotice = text; renderPanelNotice(); } catch (_) {} } async function _fcc() { try { const data = await _crq(`${CLOUD_API_PREFIX}/client-config`, "GET", null); if (data?.panelNoticePath) { const p = String(data.panelNoticePath).trim(); if (p) state.panelNoticePath = p.startsWith("/") ? p : `/${p}`; } if (data?.freeChapterLimit != null) { state.freeChapterLimit = Number(data.freeChapterLimit) || DEFAULT_FREE_CHAPTER_LIMIT; } if (data?.proBuyUrl) { const buy = String(data.proBuyUrl).trim(); if (buy) state.proBuyUrl = buy; } state.cloudApiBase = DEFAULT_CLOUD_API_BASE; state.cloudLastError = ""; } catch (e) { state.cloudLastError = String(e.message || e); } await _fpn(); } async function _ecl(force) { const cached = readCloudLeaseCache(); const now = Math.floor(Date.now() / 1000); if (!force && cached?.lease && cached.exp - now > 60) { state.cloudLease = cached.lease; state.cloudExpireAt = cached.exp; if (cached.tier) state.cloudTier = cached.tier; state.cloudReady = true; return true; } const data = await _crq(`${CLOUD_API_PREFIX}/lease`, "POST", { learning_user_id: getLearningUserId() || "anonymous", site_id: "bjsqypx", token: readCloudToken() || undefined, lease: state.cloudLease || undefined, }); applyLeasePayload(data); state.cloudLastError = ""; return true; } async function _vct() { try { await _crq(`${CLOUD_API_PREFIX}/license/verify`, "GET", null); await _ecl(true); state.cloudLastError = ""; return true; } catch (e) { state.cloudLastError = String(e.message || e); state.cloudReady = false; return false; } } async function _ccq(meta) { const data = await _crq(`${CLOUD_API_PREFIX}/study/consume`, "POST", { lease: state.cloudLease || undefined, learning_user_id: getLearningUserId() || "anonymous", course_id: meta?.courseId, ware_id: meta?.wareId, site_id: "bjsqypx", }); applyLeasePayload(data); return true; } async function _acp() { try { await _ecl(true); } catch (_) { log("\u670d\u52a1\u6821\u9a8c\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"); return false; } if (state.cloudRevoked) { log("\u670d\u52a1\u6682\u4e0d\u53ef\u7528\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"); return false; } if (!isCloudProTier()) { const used = Math.max(state.freeUsed, readLocalFreeUsed()); const limit = state.freeChapterLimit || DEFAULT_FREE_CHAPTER_LIMIT; if (used >= limit) { log("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"); return false; } } return true; } function isCloudProTier() { return !state.cloudRevoked && String(state.cloudTier || "").toLowerCase() === "pro"; } function getLearningUserId() { const card = (document.cookie.match(/(?:^|;\s*)cookie_ic_card=([^;]+)/) || [])[1]; return decodeURIComponent(card || state.userLabel || "").trim(); } async function _sct(token) { writeCloudToken(token); const ok = await _vct(); if (ok) log(token ? "\u5df2\u751f\u6548" : "\u5df2\u6e05\u9664"); else log("\u6fc0\u6d3b\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u540e\u91cd\u8bd5"); updatePanel(); return ok; } function formatCloudTierText() { if (state.cloudRevoked) return "\u5df2\u7981\u7528"; const t = String(state.cloudTier || "").toLowerCase(); if (t === "pro") return "Pro \u4f1a\u5458"; if (t === "free") return "\u514d\u8d39\u4f53\u9a8c"; return "\u672a\u6821\u9a8c"; } function _ucu() { state.freeUsed = readLocalFreeUsed(); const tierEl = document.getElementById("bjqx-cloud-tier"); if (tierEl) tierEl.textContent = formatCloudTierText(); const freeEl = document.getElementById("bjqx-cloud-free"); const freeLabel = document.getElementById("bjqx-cloud-free-label"); const limit = state.freeChapterLimit || DEFAULT_FREE_CHAPTER_LIMIT; if (freeLabel) freeLabel.textContent = `\u514d\u8d39\u4f53\u9a8c\uff08${limit}\u4e2a\u7ae0\u8282\uff09`; if (freeEl) { freeEl.textContent = isCloudProTier() ? "\u4e0d\u9650\u91cf" : `${state.freeUsed}/${limit} \u8282`; } const tokenInput = document.getElementById("bjqx-cloud-token"); if (tokenInput && document.activeElement !== tokenInput) { tokenInput.value = readCloudToken() || ""; } } function renderPanelNotice() { const el = document.getElementById("bjqx-ann-text"); if (el) { el.textContent = String(state.remotePanelNotice || PANEL_NOTICE_FALLBACK).trim() || PANEL_NOTICE_FALLBACK; } } function openBuyPage() { window.open(state.proBuyUrl || PRO_BUY_URL, "_blank", "noopener"); } function registerCloudMenus() { if (typeof GM_registerMenuCommand !== "function") return; try { GM_registerMenuCommand("\u7c98\u8d34\u6fc0\u6d3b\u7801", () => { const t = window.prompt("\u7c98\u8d34\u6fc0\u6d3b\u7801\u540e\u786e\u5b9a", readCloudToken() || ""); if (t == null) return; void _sct(String(t).trim()); }); GM_registerMenuCommand("\u6e05\u9664\u6fc0\u6d3b\u7801", () => { void _sct(""); }); GM_registerMenuCommand("\u6253\u5f00\u8d2d\u4e70\u9875", () => openBuyPage()); } catch (_) {} } function courseTitleOf(courseId, fallback) { const id = String(courseId || ""); return KNOWN_COURSE_TITLES[id] || fallback || `\u8bfe\u7a0b ${id}`; } function parseIndexCourses(html) { const doc = parseDoc(html); const map = new Map(); doc.querySelectorAll('a[href*="zkbd.jsp"]').forEach((a, idx) => { const href = a.getAttribute("href") || ""; const m = href.match(/course_id=(\d+)/i); if (!m) return; const id = m[1]; if (map.has(id)) return; map.set(id, { courseId: id, title: courseTitleOf(id, `\u8bfe\u7a0b${idx + 1}`), href: absUrl(`${BJ}/zkbd.jsp?course_id=${id}`), selected: false, studyDone: 0, studyTotal: 0, examDone: 0, examTotal: 0, allDone: false, wares: [], statusText: "\u672a\u52a0\u8f7d", }); }); const userM = String(html || "").match(/\u5f53\u524d\u7528\u6237\s*[\uff1a:]\s*([^<&\n]+)/); if (userM) state.userLabel = userM[1].replace(/\s+/g, " ").trim(); return [...map.values()]; } function parseZkbdWares(html, courseId) { const doc = parseDoc(html); const rows = [...doc.querySelectorAll("table.tables tr, table tr")]; const wares = []; const seen = new Set(); for (const tr of rows) { const link = tr.querySelector('a[href*="cc.jsp"][href*="cware_id="]'); if (!link) continue; const href = link.getAttribute("href") || ""; const wid = (href.match(/cware_id=([^&]+)/i) || [])[1]; if (!wid || seen.has(wid)) continue; seen.add(wid); const tds = [...tr.querySelectorAll("td")]; const title = (tds[0] && tds[0].textContent.trim()) || `\u8bfe\u4ef6 ${wid}`; const teacher = (tds[2] && tds[2].textContent.trim()) || ""; const statusText = (tds[3] && tds[3].textContent.replace(/\s+/g, " ").trim()) || ""; const durM = (tr.textContent || "").match(/\((\d{2}:\d{2}:\d{2})\)/); const done = /\u5df2\u5b66\u4e60|\u5df2\u5b8c\u6210|\u5df2\u901a\u8fc7|\u901a\u8fc7/.test(statusText); const studying = /\u5b66\u4e60\u4e2d|\u672a\u8003\u8bd5|\u5f85\u8003\u8bd5/.test(statusText); wares.push({ courseId: String(courseId || ""), wareId: String(wid), title, teacher, duration: durM ? durM[1] : "", statusText: statusText || (done ? "\u5df2\u5b66\u4e60" : "\u672a\u5b66\u4e60"), done, studying, href: absUrl(href.includes("://") || href.startsWith("/") ? href : `${BJ}/${href}`), }); } return wares; } function summarizeCourse(course) { const wares = course.wares || []; course.studyTotal = wares.length; course.studyDone = wares.filter((w) => w.done).length; course.examTotal = wares.length; course.examDone = wares.filter((w) => w.done).length; course.allDone = wares.length > 0 && wares.every((w) => w.done); if (!wares.length) course.statusText = "\u672a\u52a0\u8f7d\u7ae0\u8282"; else if (course.allDone) course.statusText = "\u5df2\u5b8c\u6210"; else if (course.studyDone > 0) course.statusText = "\u5b66\u4e60\u4e2d"; else course.statusText = "\u672a\u5b66\u4e60"; return course; } async function _fzk(courseId) { const r = await httpGet(`${BJ}/zkbd.jsp?course_id=${encodeURIComponent(courseId)}`); if (r.status >= 400) throw new Error("\u7ae0\u8282\u52a0\u8f7d\u5931\u8d25"); return parseZkbdWares(r.text, courseId); } function _pcm(html, courseId, wareId) { const text = String(html || ""); const saveM = text.match(/saveStudy3\.jsp\?[^"'\\\s]+/i); let saveStudy3 = saveM ? saveM[0].replace(/^['"]|['"]$/g, "") : ""; const slogM = text.match(/slog\.jsp\?[^"'\\\s]+/i); const examSlog = slogM ? slogM[0].replace(/^['"]|['"]$/g, "") : ""; const userid = (saveStudy3.match(/userid=([^&]+)/i) || examSlog.match(/user_id=([^&]+)/i) || [])[1] || ""; const paperId = (examSlog.match(/paper_id=([^&]+)/i) || [])[1] || String(wareId || "01"); const lnameM = text.match(/var\s+lname\s*=\s*["']([^"']+)["']/i); return { courseId: String(courseId || (saveStudy3.match(/course_id=(\d+)/i) || [])[1] || ""), wareId: String(wareId || (saveStudy3.match(/cware_id=([^&]+)/i) || [])[1] || ""), saveStudy3, examSlog, userid, paperId, lname: lnameM ? lnameM[1] : "", }; } async function _fcc2(courseId, wareId) { const path = `${BJ}/cc.jsp?next=1&course_id=${encodeURIComponent(courseId)}&cware_id=${encodeURIComponent(wareId)}`; const r = await httpGet(path); if (r.status >= 400) throw new Error("\u5b66\u65f6\u63d0\u4ea4\u5931\u8d25"); return _pcm(r.text, courseId, wareId); } async function _es(kind, context, cfg) { await _ecl(false); const data = await _crq(`${CLOUD_API_PREFIX}/study/engine/start`, "POST", { lease: state.cloudLease, kind: String(kind || "chapter"), context: context || {}, config: Object.assign({ auto_exam: false, free_chapter_limit: state.freeChapterLimit || DEFAULT_FREE_CHAPTER_LIMIT }, cfg || {}), }); applyLeasePayload(data); return data; } async function _ep(sessionId, event, lastResult, contextPatch) { await _ecl(false); const data = await _crq(`${CLOUD_API_PREFIX}/study/engine/step`, "POST", { lease: state.cloudLease, session_id: String(sessionId || ""), event: String(event || "tick"), last_result: lastResult == null ? null : lastResult, context_patch: contextPatch == null ? null : contextPatch, }); applyLeasePayload(data); return data; } function _iec(cmd) { const c = cmd || {}; const t = String(c.type || ""); const p = String(c.path || c.url || c.label || ""); return t === "browser_exam" || /exam\.jsp|examDo|slog\.jsp|examQuizFail/i.test(p); } async function _xc(cmd) { const c = cmd || {}; const t = String(c.type || ""); if (t === "done") { return { terminal: true, ok: c.success !== false, skipped: !!c.skipped, msg: c.message || "\u5b8c\u6210", userid: c.userid || "", paper_id: c.paper_id || "", exam_deferred: true, study_ok: true, command: c, }; } if (t === "failed") { return { terminal: true, ok: false, msg: c.message || "failed" }; } if (_iec(c)) { return { terminal: true, ok: true, study_ok: true, exam_deferred: true, msg: "\u5b66\u65f6\u5df2\u5b8c\u6210" }; } if (t === "platform_get" || t === "platform_fetch" || t === "document_fetch") { const path = c.path || c.url || ""; const r = await httpGet(path, { xhr: !!c.xhr, headers: c.headers || {}, accept: (c.headers && (c.headers.Accept || c.headers.accept)) || undefined, }); return { event: c.event || "submit_result", lastResult: { text: r.text, status: r.status, url: r.url }, }; } if (t === "platform_post") { const headers = Object.assign({}, c.headers || {}); const body = c.body != null ? c.body : typeof c.payload === "string" ? c.payload : new URLSearchParams(c.payload || {}).toString(); const r = await httpPostForm(c.path || c.url || "", body, { headers }); let data; try { data = JSON.parse(r.text); } catch (_) { data = { text: r.text }; } return { event: "submit_result", lastResult: { data, http_status: r.status, text: r.text, url: r.url }, }; } if (t === "wait") { await sleep(Math.max(300, Number(c.ms || 800))); return { event: "tick", lastResult: null }; } return { event: "tick", lastResult: null }; } function createExamAnswerState() { return { byQuesId: Object.create(null), tried: Object.create(null), rotateIdx: 0 }; } function ensureTried(st, quesId) { if (!st.tried[quesId]) st.tried[quesId] = new Set(); return st.tried[quesId]; } function pickAnswer(st, quesId, options) { const opts = options && options.length ? options.map(String) : ["A", "B", "C", "D"]; const locked = st.byQuesId[quesId]; if (locked && opts.includes(String(locked))) return String(locked); const tried = ensureTried(st, quesId); for (const o of opts) if (!tried.has(o)) return o; return opts[st.rotateIdx++ % opts.length] || "A"; } function attrUnquoted(tag, name) { const n = String(name || ""); const m = tag.match(new RegExp(n + '\\s*=\\s*"([^"]*)"', "i")) || tag.match(new RegExp(n + "\\s*=\\s*'([^']*)'", "i")) || tag.match(new RegExp(n + "\\s*=\\s*([^\\s>]+)", "i")); return m ? m[1] : ""; } function _pef(html) { const text = String(html || ""); const formM = text.match(/]*action\s*=\s*["']?([^"'\s>]+)["']?[^>]*>[\s\S]*?<\/form>/i); const chunk = formM ? formM[0] : text; const action = (formM && formM[1]) || "examDo.jsp"; const hidden = {}; const tags = chunk.match(/]*>/gi) || []; for (const tag of tags) { if (!/type\s*=\s*["']?hidden["']?/i.test(tag)) continue; const name = attrUnquoted(tag, "name"); if (!name) continue; const val = attrUnquoted(tag, "value"); if (hidden[name] == null) hidden[name] = val; else if (Array.isArray(hidden[name])) hidden[name].push(val); else hidden[name] = [hidden[name], val]; } if (!hidden.ques_list) { const ql = text.match(/name\s*=\s*["']?ques_list["']?[^>]*value\s*=\s*["']?([^"'\s>]+)/i); if (ql) hidden.ques_list = ql[1]; } const quesList = String(hidden.ques_list || "") .split(",") .map((s) => s.trim()) .filter(Boolean); const entries = []; for (const quesId of quesList) { const name = "ques_" + quesId; const options = []; for (const tag of tags) { if (attrUnquoted(tag, "name") !== name) continue; const v = attrUnquoted(tag, "value"); if (v && !options.includes(v)) options.push(v); } entries.push({ quesId, name, options: options.length ? options : ["A", "B", "C", "D"] }); } if (!entries.length) { const seen = []; for (const tag of tags) { if (!/type\s*=\s*["']?radio["']?/i.test(tag)) continue; const name = attrUnquoted(tag, "name"); if (!name || seen.includes(name)) continue; seen.push(name); const quesId = name.replace(/^ques_/, ""); const options = []; for (const t2 of tags) { if (attrUnquoted(t2, "name") !== name) continue; const v = attrUnquoted(t2, "value"); if (v && !options.includes(v)) options.push(v); } entries.push({ quesId, name, options: options.length ? options : ["A", "B", "C", "D"] }); } } if (!entries.length) return null; return { entries, hidden, action }; } function buildExamBody(parsed, st) { const body = new URLSearchParams(); for (const [k, v] of Object.entries(parsed.hidden || {})) { if (Array.isArray(v)) v.forEach((x) => body.append(k, x)); else body.append(k, v); } for (const e of parsed.entries) { const val = pickAnswer(st, e.quesId, e.options); ensureTried(st, e.quesId).add(val); body.append(e.name, val); e._picked = val; } return body.toString(); } function applyExamFail(st, failUrl, submitted) { let errorQues = new Set(); try { const u = new URL(failUrl, API_BASE); (u.searchParams.get("error_ques") || "") .split(",") .forEach((id) => id && errorQues.add(id.trim())); } catch (_) { const m = String(failUrl).match(/error_ques=([^&]+)/); if (m) m[1].split(",").forEach((id) => id && errorQues.add(id.trim())); } for (const e of submitted || []) { const picked = e._picked; if (errorQues.size && !errorQues.has(e.quesId)) { st.byQuesId[e.quesId] = picked; } else { ensureTried(st, e.quesId).add(picked); if (st.byQuesId[e.quesId] === picked) delete st.byQuesId[e.quesId]; } } } function isExamFailUrl(url) { return /examQuizFail/i.test(String(url || "")); } async function _re(courseId, paperId, userId) { const examState = createExamAnswerState(); let announced = false; const pid = String(paperId || "01"); const cid = String(courseId || ""); const examPath = `${BJ}/exam.jsp?course_id=${encodeURIComponent(cid)}&paper_id=${encodeURIComponent( pid )}`; const examAbs = absUrl(examPath); const ccReferer = absUrl( `${BJ}/cc.jsp?next=1&course_id=${encodeURIComponent(cid)}&cware_id=${encodeURIComponent(pid)}` ); if (userId) { try { await httpGet( `${BJ}/slog.jsp?course_id=${encodeURIComponent(cid)}&paper_id=${encodeURIComponent( pid )}&user_id=${encodeURIComponent(userId)}`, { headers: { Referer: ccReferer } } ); } catch (_) {} await sleep(300); } let emptyTries = 0; for (let tryNo = 1; tryNo <= EXAM_MAX_TRIES; tryNo++) { if (state.stopFlag) throw new Error("\u5df2\u505c\u6b62"); const page = await httpGet(examPath, { headers: { Referer: ccReferer, Accept: "text/html,*/*" }, }); const html = String(page.text || ""); if (/\u5df2\u901a\u8fc7|\u65e0\u9700\u8003\u8bd5|\u5df2\u7ecf\u901a\u8fc7/.test(html)) { log("\u8003\u8bd5\u5df2\u901a\u8fc7"); return true; } if (/\u8bf7\u5148\u5b66\u4e60|\u770b\u5b8c\u8bfe\u4ef6|\u5b66\u4e60\u65f6\u957f\u4e0d\u8db3|\u4e0d\u80fd\u7b54\u5377/.test(html)) { emptyTries += 1; if (emptyTries > 6) throw new Error("\u5b66\u65f6\u672a\u6ee1\uff0c\u4e0d\u80fd\u7b54\u5377"); await sleep(800); continue; } const parsed = _pef(html); if (!parsed || !parsed.entries.length) { emptyTries += 1; if (emptyTries <= 8) { await sleep(500); continue; } throw new Error("\u8003\u8bd5\u672a\u5b8c\u6210"); } if (!announced) { log("\u6b63\u5728\u8003\u8bd5…"); announced = true; } const body = buildExamBody(parsed, examState); const post = await httpPostForm( parsed.action.startsWith("http") || parsed.action.startsWith("/") ? parsed.action : `${BJ}/${parsed.action}`, body, { headers: { Referer: examAbs } } ); const blob = String(post.text || "") + " " + String(post.url || ""); if (isExamFailUrl(post.url) || /examQuizFail/i.test(blob)) { const failUrl = isExamFailUrl(post.url) ? post.url : (String(post.text || "").match(/examQuizFail\.jsp[^"'\s<>]*/i) || [])[0] || post.url; applyExamFail(examState, failUrl, parsed.entries); await sleep(400); continue; } if (/\u606d\u559c|\u901a\u8fc7\u8003\u8bd5|\u8003\u8bd5\u901a\u8fc7|\u95ee\u5377|\u5df2\u901a\u8fc7/.test(blob) || !/exam\.jsp|examDo|examQuizFail/i.test(post.url)) { log("\u8003\u8bd5\u5df2\u901a\u8fc7"); return true; } log("\u8003\u8bd5\u5df2\u901a\u8fc7"); return true; } throw new Error("\u8003\u8bd5\u672a\u5b8c\u6210"); } async function _rce(courseId, wareId) { const start = await _es( "chapter", { course_id: String(courseId), ware_id: String(wareId), auto_exam: false }, { auto_exam: false } ); let sid = start.session_id; let cmd = start.command; if (start.log && !/\u4e91\u7aef|\u4ee3\u53d1|\u7f16\u6392/.test(String(start.log))) log(String(start.log)); if (_iec(cmd)) { return { terminal: true, ok: true, study_ok: true, exam_deferred: true }; } for (let i = 0; i < 80; i++) { if (state.stopFlag) throw new Error("\u5df2\u505c\u6b62"); const out = await _xc(cmd); if (out.terminal) { if (!out.ok) throw new Error(out.msg || "\u5904\u7406\u5931\u8d25"); return out; } const next = await _ep(sid, out.event, out.lastResult, null); applyLeasePayload(next); sid = next.session_id || sid; cmd = next.command; if (_iec(cmd)) { return { terminal: true, ok: true, study_ok: true, exam_deferred: true }; } if (next.log && !/\u8003\u8bd5|\u4e91\u7aef|\u4ee3\u53d1|\u7f16\u6392/.test(String(next.log))) log(String(next.log)); } throw new Error("\u5904\u7406\u8d85\u65f6"); } function loadQueue() { const arr = readJson(QUEUE_KEY, []); return Array.isArray(arr) ? arr.map(String) : []; } function saveQueue(ids) { writeJson(QUEUE_KEY, [...new Set((ids || []).map(String))]); } function selectedCourses() { const q = new Set(loadQueue()); return state.panelCourses.filter((c) => q.has(c.courseId) || c.selected); } function isCourseSelectable(c) { return c && !c.allDone; } async function refreshPanelCourses(opts) { opts = opts || {}; state.panelRefreshing = true; updatePanel(); try { const r = await httpGet(`${BJ}/index.jsp`); if (r.status >= 400) throw new Error("\u8bf7\u5148\u767b\u5f55\u5e73\u53f0"); const list = parseIndexCourses(r.text); const prevSel = new Set(loadQueue()); const oldMap = new Map(state.panelCourses.map((c) => [c.courseId, c])); for (const c of list) { const old = oldMap.get(c.courseId); if (old?.wares?.length) { c.wares = old.wares; summarizeCourse(c); } if (KNOWN_COURSE_TITLES[c.courseId]) c.title = KNOWN_COURSE_TITLES[c.courseId]; else c.title = courseTitleOf(c.courseId, c.title); c.selected = prevSel.has(c.courseId); } state.panelCourses = list; if (!opts.silent) log(`\u5df2\u5237\u65b0\u8bfe\u7a0b\u5217\u8868\uff08${list.length}\uff09`); renderCourseList(); await enrichSelectedChapters(); await refreshChapterPreview(); updateQueueSummary(); } catch (e) { log("\u5237\u65b0\u8bfe\u7a0b\u5931\u8d25\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55"); } finally { state.panelRefreshing = false; updatePanel(); } } async function enrichSelectedChapters() { const targets = selectedCourses(); if (!targets.length && state.panelCourses.length && qs("course_id")) { const cid = qs("course_id"); const hit = state.panelCourses.find((c) => c.courseId === cid); if (hit) targets.push(hit); } for (const c of targets) { if (state.stopFlag) break; try { c.wares = await _fzk(c.courseId); summarizeCourse(c); } catch (e) { log(`\u300c${courseTitleOf(c.courseId, c.title)}\u300d\u7ae0\u8282\u52a0\u8f7d\u5931\u8d25`); } } renderCourseList(); } async function ensureCourseWares(course) { if (course.wares && course.wares.length) return course.wares; course.wares = await _fzk(course.courseId); summarizeCourse(course); return course.wares; } async function refreshChapterPreview() { const courses = selectedCourses(); const blocks = []; for (const c of courses) { try { await ensureCourseWares(c); } catch (_) {} const items = (c.wares || []) .map((w) => { const st = w.done ? "\u5df2\u5b8c\u6210" : w.statusText || "\u672a\u5b66\u4e60"; return `
${escHtml(w.wareId)}. ${escHtml( w.title )}${w.duration ? " (" + escHtml(w.duration) + ")" : ""}${escHtml( st )}
`; }) .join(""); blocks.push( `
${escHtml( courseTitleOf(c.courseId, c.title) )}
${items || `
\u6682\u65e0\u8bfe\u4ef6
`}
` ); } state.chapterPreview = blocks; const box = document.getElementById("bjqx-chapter-preview"); if (box) { box.innerHTML = blocks.length ? blocks.join("") : `
\u8bf7\u52fe\u9009\u8bfe\u7a0b
`; } } function selectUnfinishedCourses() { const ids = state.panelCourses.filter((c) => isCourseSelectable(c)).map((c) => c.courseId); state.panelCourses.forEach((c) => { c.selected = ids.includes(c.courseId); }); saveQueue(ids); renderCourseList(); void refreshChapterPreview().then(updateQueueSummary); log(`\u5df2\u52fe\u9009\u672a\u5b8c\u6210\u8bfe\u7a0b ${ids.length} \u95e8`); } function clearCourseSelection() { state.panelCourses.forEach((c) => { c.selected = false; }); saveQueue([]); renderCourseList(); void refreshChapterPreview().then(updateQueueSummary); } function renderCourseList() { const box = document.getElementById("bjqx-course-list"); if (!box) return; if (!state.panelCourses.length) { box.innerHTML = `
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
`; return; } const q = new Set(loadQueue()); box.innerHTML = state.panelCourses .map((c) => { const checked = q.has(c.courseId) || c.selected; const cls = [ "bjqx-course-item", c.allDone ? "bjqx-course-done" : "", !c.allDone && c.studyDone > 0 ? "bjqx-course-study" : "", ] .filter(Boolean) .join(" "); const studyBadge = c.allDone ? `\u5df2\u5b8c\u6210` : c.studyDone > 0 ? `\u5b66\u4e60\u4e2d ${c.studyDone}/${c.studyTotal || "?"}` : `\u672a\u5b66\u4e60`; const title = courseTitleOf(c.courseId, c.title); return ``; }) .join(""); box.querySelectorAll("input[type=checkbox]").forEach((inp) => { inp.addEventListener("change", () => { const id = inp.getAttribute("data-course-id"); const course = state.panelCourses.find((c) => c.courseId === id); if (course) course.selected = inp.checked; const ids = [...box.querySelectorAll("input[type=checkbox]:checked")].map((x) => x.getAttribute("data-course-id") ); saveQueue(ids); void (async () => { if (inp.checked && course) { try { await ensureCourseWares(course); renderCourseList(); } catch (_) { log("\u7ae0\u8282\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"); } } await refreshChapterPreview(); updateQueueSummary(); })(); }); }); } function updateQueueSummary() { const courses = selectedCourses(); let videoTotal = 0; let videoDone = 0; for (const c of courses) { videoTotal += c.studyTotal || (c.wares || []).length || 0; videoDone += c.studyDone || 0; } state.queueVideoTotal = videoTotal; state.queueVideoDone = videoDone; state.queueExamTotal = videoTotal; state.queueExamDone = videoDone; const pct = videoTotal ? Math.round((videoDone / videoTotal) * 100) : 0; const set = (id, v) => { const el = document.getElementById(id); if (el) el.textContent = v; }; set("bjqx-queue-done", String(videoDone)); set("bjqx-queue-total", String(videoTotal)); set("bjqx-queue-exam", videoTotal ? `${videoDone}/${videoTotal}` : "—"); set("bjqx-queue-percent", pct + "%"); const bar = document.getElementById("bjqx-queue-progress"); if (bar) bar.style.width = pct + "%"; const qt = document.getElementById("bjqx-queue-text"); if (qt) { qt.textContent = courses.length ? `\u5df2\u9009 ${courses.length} \u95e8 · \u8bfe\u4ef6 ${videoDone}/${videoTotal || "?"}` : "\u672a\u9009\u62e9"; } } function updateCurrentMeta() { const set = (id, v) => { const el = document.getElementById(id); if (el) { el.textContent = v; el.title = v; } }; set("bjqx-current-user", state.userLabel || getLearningUserId() || "—"); set("bjqx-current-course", state.currentCourseTitle || "\u65e0"); set("bjqx-current-chapter", state.currentChapterTitle || "\u65e0"); set("bjqx-current-task", state.currentTask || "—"); } function updatePanel() { const status = document.getElementById("bjqx-auto-status"); if (status) status.textContent = state.enabled ? "\u8fd0\u884c\u4e2d" : "\u5df2\u505c\u6b62"; const startBtn = document.getElementById("bjqx-start"); const stopBtn = document.getElementById("bjqx-stop"); if (startBtn) { startBtn.disabled = state.enabled; startBtn.classList.toggle("bjqx-btn-off", state.enabled); } if (stopBtn) { stopBtn.disabled = !state.enabled; stopBtn.classList.toggle("bjqx-btn-off", !state.enabled); } const refreshBtn = document.getElementById("bjqx-refresh"); if (refreshBtn) refreshBtn.disabled = state.panelRefreshing || state.enabled; updateQueueSummary(); updateCurrentMeta(); _ucu(); renderPanelNotice(); } async function _pw(course, ware) { const courseName = courseTitleOf(course.courseId, course.title); state.currentCourseTitle = courseName; state.currentChapterTitle = ware.title || `\u7b2c${ware.wareId}\u8282`; state.currentTask = "\u63d0\u4ea4\u5b66\u65f6"; updateCurrentMeta(); log(`\u5f00\u59cb\uff1a${courseName} · ${state.currentChapterTitle}`); await _rce(course.courseId, ware.wareId); log("\u5b66\u65f6\u5df2\u5b8c\u6210"); if (AUTO_EXAM) { state.currentTask = "\u8003\u8bd5\u4e2d"; updateCurrentMeta(); const meta = await _fcc2(course.courseId, ware.wareId); await _re(course.courseId, meta.paperId || ware.wareId, meta.userid); } ware.done = true; ware.statusText = "\u5df2\u5b66\u4e60"; summarizeCourse(course); state.currentTask = "\u672c\u8282\u5b8c\u6210"; log("\u672c\u8282\u5b8c\u6210"); updatePanel(); } async function _rse() { const courses = selectedCourses(); if (!(await _acp())) { state.enabled = false; updatePanel(); return; } for (const course of courses) { if (!state.enabled || state.stopFlag) break; const courseName = courseTitleOf(course.courseId, course.title); try { await ensureCourseWares(course); } catch (_) { log(`\u300c${courseName}\u300d\u52a0\u8f7d\u5931\u8d25`); continue; } const wares = (course.wares || []).filter((w) => !w.done); if (!wares.length) { log(`\u300c${courseName}\u300d\u5df2\u5168\u90e8\u5b8c\u6210`); continue; } for (const ware of wares) { if (!state.enabled || state.stopFlag) break; if (!(await _acp())) { state.enabled = false; break; } try { await _pw(course, ware); log(`\u5df2\u5b8c\u6210\uff1a${ware.title || "\u672c\u8282"}`); } catch (e) { if (/\u505c\u6b62/.test(String(e.message || e))) break; log(`${friendlyErr(e)}\uff08${ware.title || "\u672c\u8282"}\uff09`); } await sleep(400); updateQueueSummary(); await refreshChapterPreview(); } try { course.wares = await _fzk(course.courseId); summarizeCourse(course); } catch (_) {} renderCourseList(); updateQueueSummary(); } state.enabled = false; state.currentTask = state.stopFlag ? "\u5df2\u505c\u6b62" : "\u672c\u8f6e\u7ed3\u675f"; state.stopFlag = false; log(state.currentTask); updatePanel(); await refreshChapterPreview(); } function switchPanelTab(tab) { document.querySelectorAll("#bjqx-auto-panel .bjqx-tab-btn").forEach((btn) => { btn.classList.toggle("active", btn.dataset.tab === tab); }); document.querySelectorAll("#bjqx-auto-panel .bjqx-pane").forEach((pane) => { pane.classList.toggle("active", pane.dataset.pane === tab); }); } function readPanelCollapsed() { const v = localStorage.getItem(PANEL_COLLAPSED_KEY); return v == null ? false : v === "1"; } function writePanelCollapsed(v) { localStorage.setItem(PANEL_COLLAPSED_KEY, v ? "1" : "0"); } function readPanelPos() { return readJson(PANEL_POS_KEY, null); } function writePanelPos(left, top) { writeJson(PANEL_POS_KEY, { left, top }); } function applyPanelCollapsed(panel, collapsed) { panel.classList.toggle("bjqx-panel-min", collapsed); panel.classList.toggle("bjqx-panel-max", !collapsed); const btnMin = panel.querySelector("#bjqx-btn-min"); const btnMax = panel.querySelector("#bjqx-btn-max"); if (btnMin) btnMin.style.display = collapsed ? "none" : ""; if (btnMax) btnMax.style.display = collapsed ? "" : "none"; } function enablePanelDrag(panel) { const header = panel.querySelector("#bjqx-panel-header"); if (!header) return; let dragging = false; let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; header.addEventListener("mousedown", (e) => { if (e.target?.closest("#bjqx-panel-controls")) return; dragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; panel.style.right = "auto"; panel.style.bottom = "auto"; e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (!dragging) return; const left = Math.max( 0, Math.min(window.innerWidth - panel.offsetWidth, startLeft + (e.clientX - startX)) ); const top = Math.max( 0, Math.min(window.innerHeight - panel.offsetHeight, startTop + (e.clientY - startY)) ); panel.style.left = `${left}px`; panel.style.top = `${top}px`; }); document.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; const rect = panel.getBoundingClientRect(); writePanelPos(rect.left, rect.top); }); } function injectPanelStyles() { const id = "bjqx-panel-style-v6"; document.getElementById("bjqx-panel-style-v1")?.remove(); document.getElementById("bjqx-panel-style-v2")?.remove(); document.getElementById("bjqx-panel-style-v3")?.remove(); document.getElementById("bjqx-panel-style-v4")?.remove(); document.getElementById("bjqx-panel-style-v5")?.remove(); if (document.getElementById(id) || !document.head) return; const st = document.createElement("style"); st.id = id; st.textContent = ` #bjqx-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;} #bjqx-auto-panel.bjqx-panel-max{max-height:min(92vh,780px);} #bjqx-auto-panel.bjqx-panel-min{max-height:none;} #bjqx-auto-panel.bjqx-panel-min #bjqx-panel-body,#bjqx-auto-panel.bjqx-panel-min #bjqx-panel-footer,#bjqx-auto-panel.bjqx-panel-min .bjqx-footer-extra{display:none !important;} #bjqx-panel-header{padding:8px 11px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;} #bjqx-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #bjqx-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:cover;display:block;flex:0 0 auto;background:#e2e8f0;} #bjqx-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;} #bjqx-panel-sub{margin-top:3px;font-size:11px;color:#7c2d12;font-weight:700;} #bjqx-panel-controls{display:flex;gap:5px;} .bjqx-panel-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;font-size:15px;font-weight:900;} #bjqx-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;} #bjqx-panel-footer{padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;} .bjqx-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;} .bjqx-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;} .bjqx-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;} .bjqx-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .bjqx-card-status{padding:6px 8px;} .bjqx-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;} .bjqx-status-metrics{font-size:12px;color:#475569;} .bjqx-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .bjqx-progress-pct{font-size:14px;font-weight:900;color:#0369a1;} .bjqx-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;} .bjqx-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;} .bjqx-tabbar{display:flex;gap:6px;margin-bottom:7px;} .bjqx-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;} .bjqx-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .bjqx-pane{display:none;}.bjqx-pane.active{display:block;} .bjqx-list-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;gap:6px;flex-wrap:wrap;} .bjqx-list-title{font-size:12px;color:#64748b;font-weight:700;} .bjqx-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;} #bjqx-course-list,#bjqx-chapter-preview,#bjqx-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} .bjqx-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:700;} .bjqx-list-head-actions{display:flex;gap:6px;align-items:center;flex-wrap:nowrap;justify-content:flex-start;flex:1 1 100%;} .bjqx-list-head-actions .bjqx-btn{flex:0 0 auto !important;width:auto;min-width:0;white-space:nowrap;padding:4px 8px;font-size:11px;line-height:1.2;} .bjqx-list-head-actions .bjqx-btn-refresh{background:#fff !important;color:#0f172a !important;border:1px solid #cbd5e1;} .bjqx-course-item{display:flex;gap:8px;align-items:flex-start;border:1px solid #e2e8f0;border-radius:10px;padding:8px;margin-bottom:6px;cursor:pointer;background:#f8fafc;} .bjqx-course-item.bjqx-course-study{background:#fffbeb;border-color:#fcd34d;} .bjqx-course-item.bjqx-course-done{background:#e2e8f0;border-color:#cbd5e1;opacity:.85;cursor:not-allowed;} .bjqx-course-item.bjqx-course-done input{pointer-events:none;} .bjqx-course-item>input[type=checkbox]{margin-top:2px;flex:0 0 auto;} .bjqx-course-body{flex:1;min-width:0;} .bjqx-course-title{display:block;font-size:12px;line-height:1.4;font-weight:700;} .bjqx-status-badges{display:flex;flex-wrap:wrap;gap:4px;margin-top:5px;} .bjqx-badge{font-size:10px;font-weight:800;padding:2px 6px;border-radius:999px;border:1px solid;line-height:1.3;} .bjqx-badge-study-done{color:#047857;background:#ecfdf5;border-color:#6ee7b7;} .bjqx-badge-study-ing{color:#1d4ed8;background:#eff6ff;border-color:#93c5fd;} .bjqx-badge-study-todo{color:#64748b;background:#f8fafc;border-color:#cbd5e1;} .bjqx-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;} .bjqx-chapter-title{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;} .bjqx-chapter-item{padding:5px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;} .bjqx-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;} .bjqx-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:11px;margin-bottom:4px;} .bjqx-meta-label{color:#64748b;}.bjqx-meta-value{font-weight:700;text-align:right;max-width:68%;word-break:break-all;} .bjqx-btn-row{display:flex;gap:7px;flex-wrap:wrap;} .bjqx-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;min-width:68px;} .bjqx-btn-start{background:#16a34a;}.bjqx-btn-stop{background:#ef4444;}.bjqx-btn-refresh{background:#64748b;} .bjqx-btn-off,.bjqx-btn:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;} .bjqx-btn-ghost{flex:0 0 auto;background:#fff;color:#0f172a;border:1px solid #cbd5e1;} .bjqx-btn-pro{flex:0 0 auto;background:linear-gradient(135deg,#ea580c,#f59e0b);color:#fff;border:none;border-radius:8px;padding:4px 8px;font-size:12px;font-weight:800;cursor:pointer;} `; document.head.appendChild(st); } function _cp() { const old = document.getElementById("bjqx-auto-panel") || document.getElementById("bjqx-local-panel"); if (old) old.remove(); injectPanelStyles(); const panel = document.createElement("div"); panel.id = "bjqx-auto-panel"; panel.className = "bjqx-panel-max"; panel.innerHTML = `
\u5317\u4eac\u5168\u5458\u5fc5\u4fee\u8bfe\u57f9\u8bad\u52a9\u624b v${SCRIPT_VERSION}
\u81ea\u52a8\u5b66\u65f6 · \u81ea\u52a8\u8003\u8bd5
\u8fd0\u884c\u72b6\u6001 \u5df2\u505c\u6b62
\u8bfe\u4ef6 0/0 · \u8003\u8bd5
0%
\u57f9\u8bad\u8bfe\u7a0b
\u52fe\u9009\u8bfe\u7a0b\u540e\u53ef\u5728\u300c\u7ae0\u8282\u9884\u89c8\u300d\u67e5\u770b\u8fdb\u5ea6\uff1b\u5f00\u59cb\u540e\u81ea\u52a8\u5b8c\u6210\u5b66\u65f6\u4e0e\u8003\u8bd5\u3002
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d
\u7ae0\u8282\u9884\u89c8\u8bfe\u4ef6
\u8bf7\u52fe\u9009\u8bfe\u7a0b
\u8fd0\u884c\u65e5\u5fd7
\u8fdb\u5ea6
\u6682\u65e0\u65e5\u5fd7
\u6388\u6743\u4e0e\u9009\u9879\u4f1a\u5458
\u7528\u6237\u7c7b\u578b\u672a\u6821\u9a8c
\u514d\u8d39\u4f53\u9a8c\uff081\u4e2a\u7ae0\u8282\uff090/1 \u8282
Token
\u514d\u8d39\u4f53\u9a8c\u53ef\u5b8c\u6210 1 \u4e2a\u7ae0\u8282\uff08\u542b\u5b66\u65f6\u4e0e\u8003\u8bd5\uff09\u3002Pro \u4e0d\u9650\u91cf\uff0c\u6709\u6548\u671f\u7ea6 ${PRO_DAYS} \u5929\u3002
\u5f53\u524d\u7528\u6237
\u5f53\u524d\u8bfe\u7a0b\u65e0
\u5f53\u524d\u8bfe\u4ef6\u65e0
\u5f53\u524d\u4efb\u52a1\u52fe\u9009\u8bfe\u7a0b\u540e\u70b9\u300c\u5f00\u59cb\u300d
`; document.body.appendChild(panel); const savedPos = readPanelPos(); if (savedPos?.left != null && savedPos?.top != null) { panel.style.right = "auto"; panel.style.left = `${savedPos.left}px`; panel.style.top = `${savedPos.top}px`; } applyPanelCollapsed(panel, readPanelCollapsed()); enablePanelDrag(panel); panel.querySelector("#bjqx-btn-min")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); panel.querySelector("#bjqx-btn-max")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); panel.querySelectorAll(".bjqx-tab-btn").forEach((btn) => { btn.addEventListener("click", () => switchPanelTab(btn.dataset.tab)); }); document.getElementById("bjqx-refresh")?.addEventListener("click", () => { void refreshPanelCourses({ reason: "manual" }); }); document.getElementById("bjqx-select-unfinished")?.addEventListener("click", () => { selectUnfinishedCourses(); }); document.getElementById("bjqx-clear-select")?.addEventListener("click", () => { clearCourseSelection(); }); document.getElementById("bjqx-clear-log")?.addEventListener("click", () => clearRunLog()); document.getElementById("bjqx-cloud-save")?.addEventListener("click", () => { const input = document.getElementById("bjqx-cloud-token"); void _sct(input?.value || ""); }); document.getElementById("bjqx-open-pro")?.addEventListener("click", () => openBuyPage()); document.getElementById("bjqx-start")?.addEventListener("click", () => { const q = loadQueue().filter((id) => { const c = state.panelCourses.find((x) => x.courseId === id); return c && isCourseSelectable(c); }); saveQueue(q); if (!q.length) { log("\u8bf7\u5148\u52fe\u9009\u672a\u5b8c\u6210\u7684\u8bfe\u7a0b"); updatePanel(); return; } void (async () => { if (!(await _acp())) { updatePanel(); return; } state.enabled = true; state.stopFlag = false; state.currentTask = "\u6b63\u5728\u5f00\u59cb…"; log("\u5df2\u5f00\u59cb"); updatePanel(); void _rse(); })(); }); document.getElementById("bjqx-stop")?.addEventListener("click", () => { state.stopFlag = true; state.enabled = false; state.currentTask = "\u6b63\u5728\u505c\u6b62…"; log("\u5df2\u505c\u6b62"); updatePanel(); }); } async function boot() { if (!/bjsqypx\.haoyisheng\.com$/i.test(location.hostname)) return; state.cloudToken = readCloudToken(); state.freeUsed = readLocalFreeUsed(); state.cloudApiBase = getCloudApiBase(); registerCloudMenus(); _cp(); updatePanel(); log("\u52a9\u624b\u5df2\u5c31\u7eea"); void _fcc().then(() => _ucu()); void _vct().then(() => _ucu()); await refreshPanelCourses({ silent: true }); const cid = qs("course_id"); if (cid) { const hit = state.panelCourses.find((c) => c.courseId === cid); if (hit && !loadQueue().includes(cid)) { const q = loadQueue(); q.push(cid); saveQueue(q); hit.selected = true; try { await ensureCourseWares(hit); renderCourseList(); await refreshChapterPreview(); updateQueueSummary(); } catch (_) {} } } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => void boot()); } else { void boot(); } })();