// ==UserScript== // @name 淄博卫生人力资源管理|继续医学教育培训助手 // @namespace https://card.wlxy.top/links/09A67934 // @version 1.0 // @description 淄博卫生人力管理资源继续医学教育学习辅助,一键学习课程,自动考试,高效率完成。勾选已选课程全自动化一键完成。 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @match *://ui.zbwsrc.cn/* // @match *://vgos.zbwsrc.cn/* // @connect ui.zbwsrc.cn // @connect vgos.zbwsrc.cn // @connect www.cmechina.net // @connect cmechina.net // @connect sdzb.91huayi.com // @connect 91huayi.com // @connect huaweicloudobs.ahjxjy.cn // @connect oa9.ahzsksw.cn // @connect card.wlxy.top // @connect p.bokecc.com // @connect m-flare.bokecc.com // @connect bokecc.com // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_openInTab // @run-at document-idle // @noframes // ==/UserScript== (function () { "use strict"; const _w=(()=>{ const S=[ "jMXVz4jSy92E39nby8me193T3NjSl8rO2s7J", "jMXVz4jSy92E39nby8me193T3NjSl8rO3sw=", "jMXVz4jSy92Ez8HHyt7Fn9Db29De3w==", "jMXVz4jSy92E3MzAytyc3NzA3NXS", "jMXVz4jSy92EwMjP3NU=", "jMXVz4jEwMnOwt7LgMbUwNrSzA==", "y9DR1tSShoXEzZSAztjLwdjHwpjU1g==" ]; const k=1443; return (i)=>{ const b=atob(S[i]); let r=""; for (let j=0;j/g, ">") .replace(/"/g, """); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function trace(...args) { try { console.log("[\u6dc4\u535a\u7ee7\u6559]", ...args); } catch (_) {} } const USER_LOG_PATTERNS = [ /^\u5b66\u4e60\u300a.+\u300b\u7b2c\d+\u7ae0$/, /^\u300a.+\u300b\u7b2c\d+\u7ae0\s*\u5b66\u4e60\u5b8c\u6210$/, /^\u300a.+\u300b\u7b2c\d+\u7ae0\s*\u5f00\u59cb\u8003\u8bd5$/, /^\u300a.+\u300b\u7b2c\d+\u7ae0\s*\u8003\u8bd5\u5b8c\u6210$/, /^\u300a.+\u300b\u7b2c\d+\u7ae0\s*\u5b66\u4e60\u65f6\u957f\u4e0d\u8db3\uff0c\u8865\u5237\u89c6\u9891$/, /^\u300a.+\u300b\u7b2c\d+\u7ae0\s*\u5b66\u65f6\u672a\u5230\uff0c\u5148\u8df3\u8fc7/, /^\u300a.+\u300b\u7b2c\d+\u7ae0\s*\u5b66\u65f6\u672a\u5230\uff0c\u8df3\u8fc7\u8003\u8bd5/, /^\u300a.+\u300b\u6709\u7ae0\u8282\u5b66\u65f6\u672a\u5230/, /^\u5f00\u59cb\u8865\u5237\u5b66\u65f6\u672a\u5230\u7ae0\u8282/, /^\u5b66\u65f6\u4ecd\u4e0d\u8db3[\uff1a:].+$/, /^\u300a.+\u300b\u63d0\u4ea4\u6210\u7ee9/, /^\u300a.+\u300b\u65e0\u9700\u63d0\u4ea4\u6210\u7ee9/, /^\u300a.+\u300b\u5904\u7406\u5b8c\u6bd5$/, /^\u300a.+\u300b\u5931\u8d25[\uff1a:].+$/, /^\u5f00\u59cb\u5b66\u4e60\s*\d+\s*\u95e8\u8bfe\u7a0b$/, /^\u5168\u90e8\u5b8c\u6210|^\u5df2\u505c\u6b62|^\u7ed3\u675f[\uff1a:].+$/, ]; function formatLogTime() { try { return new Date().toLocaleTimeString("zh-CN", { hour12: false }); } catch (_) { const d = new Date(); const p = (n) => String(n).padStart(2, "0"); return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`; } } function userLog(msg) { const text = String(msg || "").trim(); if (!text || !USER_LOG_PATTERNS.some((re) => re.test(text))) return; const line = `${formatLogTime()} ${text}`; const box = document.getElementById("cme-run-log"); if (box) { const div = document.createElement("div"); div.className = "cme-log-row"; div.textContent = line; box.appendChild(div); while (box.children.length > 200) box.removeChild(box.firstChild); box.scrollTop = box.scrollHeight; } trace(text); } function chapLogTitle(courseTitle, chapNo) { return `\u300a${courseTitle}\u300b\u7b2c${chapNo}\u7ae0`; } function setStatus(text) { const el = document.getElementById("cme-auto-status"); if (el) el.textContent = text; } function updateCurrentMeta() { const c = document.getElementById("cme-current-course"); const ch = document.getElementById("cme-current-chapter"); const t = document.getElementById("cme-current-task"); if (c) c.textContent = state.currentCourse || "\u65e0"; if (ch) ch.textContent = state.currentChapter || "\u65e0"; if (t) t.textContent = state.currentTask || "—"; } function updateQueueSummary() { const doneEl = document.getElementById("cme-queue-done"); const totalEl = document.getElementById("cme-queue-total"); const examEl = document.getElementById("cme-queue-exam"); const pctEl = document.getElementById("cme-queue-percent"); const barEl = document.getElementById("cme-queue-progress"); const footEl = document.getElementById("cme-queue-text"); const total = Math.max(0, Number(state.queueTotal) || 0); const rawDone = Math.max(0, Number(state.queueDone) || 0); const done = total > 0 ? Math.min(rawDone, total) : rawDone; const pct = total > 0 ? Math.min(100, Math.round((done / total) * 100)) : 0; if (doneEl) doneEl.textContent = String(done); if (totalEl) totalEl.textContent = String(total); if (examEl) examEl.textContent = state.queueExam || "—"; if (pctEl) pctEl.textContent = pct + "%"; if (barEl) barEl.style.width = pct + "%"; if (footEl) { footEl.textContent = total ? state.running ? `\u961f\u5217 ${done}/${total} \u95e8` + (state.queueExam && state.queueExam !== "—" ? ` · \u7b2c${state.queueExam}\u7ae0` : "") : `\u5df2\u9009 ${total} \u95e8 · \u5b8c\u6210 ${done}` : "\u672a\u9009\u62e9"; } } function syncIdleChapterMetric() { if (state.running) return; const list = state.chapterPreview || []; let n = 0; let pending = false; for (const c of list) { if (c.loading) pending = true; n += (c.chapters || []).length; } if (n > 0) state.queueExam = String(n); else if (pending) state.queueExam = "…"; else state.queueExam = "—"; updateQueueSummary(); } function httpRequest(opts) { const method = (opts.method || "GET").toUpperCase(); const url = opts.url; const headers = Object.assign({}, opts.headers || {}); const data = opts.data; return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== "function") { reject(new Error("\u9700\u8981 Tampermonkey \u7684 GM_xmlhttpRequest")); return; } GM_xmlhttpRequest({ method, url, headers, data, anonymous: false, withCredentials: true, timeout: opts.timeout || 45000, onload(res) { resolve({ status: res.status, text: res.responseText || "", finalUrl: res.finalUrl || url, headers: res.responseHeaders || "", }); }, onerror() { reject(new Error("\u7f51\u7edc\u9519\u8bef " + url)); }, ontimeout() { reject(new Error("\u8d85\u65f6 " + url)); }, }); }); } function httpGet(url, headers) { return httpRequest({ method: "GET", url, headers }); } function ensureOk(res, tip) { if (res.status >= 200 && res.status < 400) return res; throw new Error((tip || "\u8bf7\u6c42\u5931\u8d25") + " HTTP " + res.status); } function stripTags(html) { return String(html || "") .replace(//gi, " ") .replace(//gi, " ") .replace(/<[^>]+>/g, " ") .replace(/ /g, " ") .replace(/\s+/g, " ") .trim(); } function pick(html, res) { for (const re of res) { const m = String(html || "").match(re); if (m) return m[1]; } return ""; } function absUrl(href, base) { try { return new URL(href, base).href; } catch (_) { return href; } } function loadDoneIds() { try { const raw = localStorage.getItem(DONE_IDS_KEY); const arr = raw ? JSON.parse(raw) : []; return new Set(Array.isArray(arr) ? arr.map(String) : []); } catch (_) { return new Set(); } } function saveDoneIds(set) { try { localStorage.setItem(DONE_IDS_KEY, JSON.stringify([...set])); } catch (_) {} } function markCourseDonePersist(id) { if (!id) return; const set = loadDoneIds(); set.add(String(id)); saveDoneIds(set); } function discoverLearningUserIdFromDom() { const html = document.documentElement.outerHTML || ""; const uid = pick(html, [ /[?&]uid=(\d{3,10})\b/i, /EmployeeID["'\s:=]+(\d{3,10})/i, /EmployeePhotograph\/(\d{3,10})\./i, /name=["']EmployeeID["'][^>]*value=["'](\d+)["']/i, /value=["'](\d+)["'][^>]*name=["']EmployeeID["']/i, ]) || ""; if (uid) return uid; const fromLink = pick(html, [/zbuserId=([^&"'\s<>]+)/i, /zbuser_id=([^&"'\s<>]+)/i]); if (fromLink && !/^\d{17}[\dXx]$/.test(fromLink)) return fromLink; return ""; } function looksLikeIdCard(s) { return /^\d{17}[\dXx]$/.test(String(s || "").trim()); } function looksLikePortalUserId(s) { return /^\d{3,10}$/.test(String(s || "").trim()); } function cacheLearningUserId(id) { const v = String(id || "").trim(); if (!v) return; if (looksLikeIdCard(v) && looksLikePortalUserId(state.learningUserId)) return; if (looksLikeIdCard(v) && looksLikePortalUserId(localStorage.getItem(LUID_CACHE_KEY) || "")) return; state.learningUserId = v; try { localStorage.setItem(LUID_CACHE_KEY, v); } catch (_) {} } function getLearningUserId() { if (state.learningUserId && !looksLikeIdCard(state.learningUserId)) return state.learningUserId; try { const cached = localStorage.getItem(LUID_CACHE_KEY); if (cached && !looksLikeIdCard(cached)) { state.learningUserId = cached; return cached; } if (cached && looksLikeIdCard(cached)) localStorage.removeItem(LUID_CACHE_KEY); } catch (_) {} const fromDom = discoverLearningUserIdFromDom(); if (fromDom) cacheLearningUserId(fromDom); return state.learningUserId || ""; } function getUiApiOrigin() { if (/ui\.zbwsrc\.cn/i.test(HOST)) return location.origin; return UI_ORIGIN_DEFAULT; } function extractJwtFromValue(raw) { const s = String(raw || "").trim(); if (!s) return ""; if (/^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/.test(s)) { return s.split(/\s+/)[0].replace(/^Bearer\s+/i, ""); } try { const j = JSON.parse(s); const t = j.access_token || j.accessToken || j.token || j.Token || ""; if (t && /^eyJ/.test(String(t))) return String(t).trim(); } catch (_) {} const m = s.match(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/); return m ? m[0] : ""; } function discoverPortalAccessToken() { try { const cached = localStorage.getItem(PORTAL_JWT_KEY); if (cached && /^eyJ/.test(cached)) return cached; } catch (_) {} const stores = []; try { stores.push(localStorage, sessionStorage); } catch (_) {} const prefer = /token|auth|jwt|access|bearer|login/i; for (const store of stores) { try { const keys = []; for (let i = 0; i < store.length; i++) keys.push(store.key(i)); keys.sort((a, b) => Number(prefer.test(b || "")) - Number(prefer.test(a || ""))); for (const k of keys) { const jwt = extractJwtFromValue(store.getItem(k)); if (jwt) { try { localStorage.setItem(PORTAL_JWT_KEY, jwt); } catch (_) {} return jwt; } } } catch (_) {} } return ""; } function parseEmployeeDetailHtml(html) { const out = { userId: "", displayName: "", phone: "", idCard: "", unitName: "" }; if (!html) return out; out.userId = pick(html, [ /name=["']EmployeeID["'][^>]*value=["'](\d+)["']/i, /value=["'](\d+)["'][^>]*name=["']EmployeeID["']/i, /EmployeePhotograph\/(\d+)\./i, /windows\((\d+)\)/, ]) || ""; out.displayName = pick(html, [/\u59d3\u540d:\s*<\/label>\s*<\/td>\s*]*>\s*([^<\s]{2,20})/i, /\u59d3\u540d[\uff1a:]\s*([^<\s]{2,20})/]) || ""; out.phone = pick(html, [/name=["']MobilePhone["'][^>]*value=["'](\d{11})["']/i, /value=["'](\d{11})["'][^>]*name=["']MobilePhone["']/i]) || ""; return out; } async function fetchPortalCurrentUser() { const token = discoverPortalAccessToken(); if (!token) return null; const url = `${getUiApiOrigin()}/api/_Account/CurrentUser`; const res = await _gm( url, "GET", { Accept: "application/json", Authorization: `Bearer ${token}` }, null ); if (res.status < 200 || res.status >= 300) return null; let data = {}; try { data = JSON.parse(res.text || "{}"); } catch (_) { return null; } const d = data.data || data.Data || {}; if (!d || (!d.UserId && !d.userId)) return null; return { userId: String(d.UserId || d.userId || "").trim(), displayName: String(d.DisplayName || d.displayName || "").trim(), unitName: String(d.UnitName || d.unitName || "").trim(), idCard: String(d.UserIdentifier || d.UserCode || d.userIdentifier || "").trim(), phone: "", }; } async function fetchEmployeeDetailProfile() { try { const res = await httpGet(EMP_DETAIL_URL); if (res.status < 200 || res.status >= 300) return null; const parsed = parseEmployeeDetailHtml(res.text || ""); if (!parsed.userId && !parsed.phone && !parsed.displayName) return null; return parsed; } catch (_) { return null; } } async function resolvePortalProfile(force) { if (!force && state.portalProfile && state.portalProfile.userId) return state.portalProfile; try { const cached = JSON.parse(localStorage.getItem(PORTAL_PROFILE_KEY) || "null"); if (!force && cached && cached.userId && Date.now() - Number(cached.ts || 0) < 6 * 3600 * 1000) { state.portalProfile = cached; cacheLearningUserId(cached.userId); return cached; } } catch (_) {} const cur = await fetchPortalCurrentUser(); const emp = await fetchEmployeeDetailProfile(); const merged = { userId: (cur && cur.userId) || (emp && emp.userId) || "", displayName: (cur && cur.displayName) || (emp && emp.displayName) || "", unitName: (cur && cur.unitName) || "", idCard: (cur && cur.idCard) || "", phone: (emp && emp.phone) || "", ts: Date.now(), }; if (!merged.userId) { const fromDom = discoverLearningUserIdFromDom(); if (fromDom) merged.userId = fromDom; } if (merged.userId) { const prev = getLearningUserId(); cacheLearningUserId(merged.userId); state.portalProfile = merged; try { localStorage.setItem(PORTAL_PROFILE_KEY, JSON.stringify(merged)); } catch (_) {} if (prev && prev !== merged.userId) { state.cloudLease = ""; state.cloudLeaseExp = 0; writeCloudLeaseCache("", 0); } trace("\u95e8\u6237\u8d26\u53f7", merged.userId, merged.displayName || ""); } return merged.userId ? merged : null; } function buildLeaseProfileBody(luid) { const p = state.portalProfile || {}; return { learning_user_id: String(luid || p.userId || "").trim(), shortName: p.displayName || "", name: p.displayName || "", idcarNo: p.idCard || "", userName: p.idCard || "", phone: p.phone || "", phoneNum: p.phone || "", phone_num: p.phone || "", office: p.unitName || "", schoolName: p.unitName || "", school_name: p.unitName || "", }; } function extractZbuserFromUrl(url) { try { return new URL(url).searchParams.get("zbuserId") || new URL(url).searchParams.get("zbuser_id") || ""; } catch (_) { return pick(String(url || ""), [/zbuserId=([^&]+)/i]); } } function _gb() { return String(state.cloudApiBase || DEFAULT_CLOUD_API_BASE).replace(/\/$/, ""); } 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 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(), freeVideoLimit: Number(parsed.freeVideoLimit ?? parsed.free_video_limit ?? 6), freeUsedVideos: Number(parsed.freeUsedVideos ?? parsed.free_used_videos ?? 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 writeCloudLastState(partial) { try { const prev = JSON.parse(localStorage.getItem(CLOUD_LAST_STATE_KEY) || "{}"); localStorage.setItem(CLOUD_LAST_STATE_KEY, JSON.stringify(Object.assign({}, prev, partial || {}, { ts: Date.now() }))); } catch (_) {} } 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 " + url)), ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6 " + url)), }; if (data != null) req.data = data; GM_xmlhttpRequest(req); }); } function safeParseJson(text, label) { const raw = String(text == null ? "" : text).trim(); if (!raw) return {}; if (/^= 300) { throw new Error(`${path} HTTP ${res.status}\uff08\u975e JSON\uff09\uff1a${String(err.message || err)}`); } throw err; } if (res.status < 200 || res.status >= 300) { throw new Error(String(data.message || data.detail || `http_${res.status}`)); } return data; } function _sq(data) { if (!data || typeof data !== "object") return; if (data.tier) state.cloudTier = String(data.tier); const lim = data.free_video_limit ?? data.free_chapter_limit ?? data.limit; const used = data.free_used_videos ?? data.free_used_chapters ?? data.used; if (lim != null) state.freeVideoLimit = Number(lim) || 6; if (used != null) state.freeUsedVideos = Number(used) || 0; writeCloudLastState({ tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, }); updateCloudPanelUI(); } async function _il(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.freeVideoLimit > 0) state.freeVideoLimit = cached.freeVideoLimit; if (cached.freeUsedVideos >= 0) state.freeUsedVideos = cached.freeUsedVideos; updateCloudPanelUI(); return true; } } await resolvePortalProfile(false); const luid = getLearningUserId(); if (!luid) throw new Error("\u672a\u8bc6\u522b\u95e8\u6237\u8d26\u53f7\uff08UserId\uff09\uff0c\u8bf7\u5148\u767b\u5f55 ui.zbwsrc.cn"); let data; try { data = await _crq(_w(4), "POST", buildLeaseProfileBody(luid)); 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"); state.cloudLease = lease; state.cloudLeaseExp = exp; state.cloudTier = tier; const peRaw = data.pro_expires_at ?? data.proExpiresAt ?? data.pro_expire_at ?? 0; let pe = Number(peRaw || 0); if (Number.isFinite(pe) && pe > 1e12) pe = Math.floor(pe / 1000); if (Number.isFinite(pe) && pe > 0) state.cloudProExpireAt = Math.floor(pe); else if (data.pro_expires_at_text) { const parsed = Date.parse(String(data.pro_expires_at_text).replace(/-/g, "/")); if (!Number.isNaN(parsed)) state.cloudProExpireAt = Math.floor(parsed / 1000); } state.freeVideoLimit = Number(data.free_video_limit ?? data.free_chapter_limit ?? state.freeVideoLimit ?? 6); state.freeUsedVideos = Number(data.free_used_videos ?? data.free_used_chapters ?? 0); writeCloudLeaseCache(lease, exp, { tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, proExpireAt: state.cloudProExpireAt, }); writeCloudLastState({ tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, }); updateCloudPanelUI(); return !!lease; } async function _vt() { if (!state.cloudToken) return true; const url = `${_gb()}${_w(5)}`; const headers = { Accept: "application/json", Authorization: `Bearer ${state.cloudToken}` }; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; const res = await _gm(url, "GET", headers, null); const text = String(res.text || "").trim(); let data = {}; try { data = text ? safeParseJson(text, _w(5)) : {}; } catch (err) { throw new Error("Token \u6821\u9a8c\u5931\u8d25\uff1a" + (err.message || err)); } if (res.status < 200 || res.status >= 300 || !data.ok) { throw new Error(String(data.message || data.detail || "Token \u6821\u9a8c\u5931\u8d25")); } return true; } async function _cr() { try { await resolvePortalProfile(true); } catch (err) { trace("\u62c9\u53d6\u95e8\u6237\u8d44\u6599\u5931\u8d25", err); } if (!getLearningUserId()) { const fromDom = discoverLearningUserIdFromDom(); if (fromDom) cacheLearningUserId(fromDom); } if (!getLearningUserId()) { trace("_cr: \u7f3a\u5c11 UserId"); setPanelHint("\u672a\u767b\u5f55\u6216\u672a\u8bc6\u522b\u95e8\u6237\u8d26\u53f7\uff0c\u8bf7\u5148\u767b\u5f55 ui.zbwsrc.cn \u540e\u518d\u8bd5"); return false; } try { if (state.cloudToken) await _vt(); await _il(true); } catch (err) { trace("\u4e91\u7aef\u6388\u6743\u5931\u8d25", err); const msg = String(err.message || err); if (/token_bound_to_other_learning_user|learning_user_mismatch/i.test(msg)) { setPanelHint("Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\uff0c\u8bf7\u5728\u540e\u53f0\u6e05\u9664\u7ed1\u5b9a\u540e\u91cd\u8bd5\uff08\u5e94\u4ee5 UserId \u4e3a\u51c6\uff09"); } else { setPanelHint("\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff1a" + msg); } return false; } const tier = String(state.cloudTier || "").toLowerCase(); if (tier === "pro") return true; if (tier === "free") { if (Number(state.freeUsedVideos) >= Number(state.freeVideoLimit)) { setPanelHint(`\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${state.freeUsedVideos}/${state.freeVideoLimit}\uff09`); switchPanelTab("settings"); openProModal(); return false; } return true; } setPanelHint("\u4e91\u7aef\u6388\u6743\u72b6\u6001\u5f02\u5e38\uff0c\u8bf7\u68c0\u67e5 Token"); return false; } async function _es(kind, context, cfg) { await _il(false); const data = await _crq(_w(0), "POST", { kind: String(kind || ""), context: context || {}, config: cfg || {}, }); _sq(data); return data; } async function _ep(sessionId, event, lastResult, contextPatch) { await _il(false); const data = await _crq(_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 _cf() { try { const res = await _gm(`${_gb()}${_w(2)}`, "GET", { Accept: "application/json" }, null); if (res.status >= 200 && res.status < 300 && res.text) { const raw = String(res.text || "").trim(); if (!/^= 200 && res.status < 300) { state.remotePanelNotice = String(res.text || "").trim() || PANEL_NOTICE_FALLBACK; const el = document.getElementById("cme-ann-text"); if (el) el.textContent = state.remotePanelNotice; } } catch (_) {} } function parseCourseListHtml(html) { if (!html || html.length < 50) return []; if (/\u767b\u5f55|login/i.test(html) && !/gridrow_header|\u8bfe\u7a0b\u540d\u79f0/.test(html)) { throw new Error("\u672a\u767b\u5f55\u6216\u4f1a\u8bdd\u5931\u6548\uff0c\u8bf7\u5148\u5728\u95e8\u6237\u767b\u5f55"); } const doc = new DOMParser().parseFromString(html, "text/html"); const table = doc.querySelector("table.grid") || doc.querySelector("table"); if (!table) return []; const courses = []; table.querySelectorAll("tr").forEach((tr) => { if (tr.querySelector("th")) return; const tds = tr.querySelectorAll("td"); if (tds.length < 6) return; const a = tr.querySelector('a[href*="SendToServer"], a[href*="SendToFreeServer"]'); const href = a ? a.getAttribute("href") || "" : ""; const id = ((href.match(/SendTo(?:Free)?Server\/(\d+)/i) || [])[1]) || ((href.match(/\/(\d+)\s*$/) || [])[1]) || ""; if (!id && !stripTags(tds[1]?.innerHTML || "")) return; const isFree = /SendToFreeServer/i.test(href); let learnUrl = href; if (learnUrl && !/^https?:/i.test(learnUrl)) { learnUrl = learnUrl.startsWith("/") ? VGOS_ORIGIN + learnUrl : VGOS_ORIGIN + "/" + learnUrl.replace(/^\.\//, ""); } courses.push({ id: (isFree ? "f" : "p") + id, rawId: id, listKind: isFree ? "free" : "pay", subject: stripTags(tds[0]?.innerHTML), title: stripTags(tds[1]?.innerHTML), publisher: stripTags(tds[2]?.innerHTML), author: stripTags(tds[3]?.innerHTML), level: stripTags(tds[4]?.innerHTML), credit: stripTags(tds[5]?.innerHTML), expire: stripTags(tds[6]?.innerHTML), nature: stripTags(tds[7]?.innerHTML), source: isFree ? "\u514d\u8d39" : stripTags(tds[8]?.innerHTML), selectTime: isFree ? "" : stripTags(tds[9]?.innerHTML), learnUrl, }); }); return courses; } function getCourseListUrl() { return state.courseListKind === "free" ? LIST_URL_FREE : LIST_URL_PAY; } function parseCoursesFromPage() { if (!/VideoListIs(Pay|Free)/i.test(location.href)) return null; if (!document.querySelector("table.grid, table")) return null; try { return parseCourseListHtml(document.documentElement.outerHTML); } catch (_) { return null; } } async function fetchSelectedCourses() { const wantFree = state.courseListKind === "free"; const fromPage = parseCoursesFromPage(); if (fromPage && fromPage.length) { const pageIsFree = /VideoListIsFree/i.test(location.href); if (pageIsFree === wantFree) { const pageList = wantFree && fromPage.length > FREE_LIST_LIMIT ? fromPage.slice(0, FREE_LIST_LIMIT) : fromPage; trace(`\u4ece\u5f53\u524d\u9875\u9762\u89e3\u6790\u5230 ${pageList.length} \u95e8${wantFree ? "\u514d\u8d39" : "\u5df2\u9009"}\u8bfe\u7a0b`); return pageList; } } const listUrl = getCourseListUrl(); trace(`\u8bf7\u6c42${wantFree ? "\u514d\u8d39" : "\u5df2\u9009"}\u8bfe\u7a0b\u5217\u8868…`); const res = ensureOk(await httpGet(listUrl), "\u8bfe\u7a0b\u5217\u8868"); if (/\/user\/login/i.test(res.finalUrl) || (/\u767b\u5f55|login/i.test(res.text) && !/\u8bfe\u7a0b\u540d\u79f0/.test(res.text))) { throw new Error("\u672a\u767b\u5f55\u6216\u4f1a\u8bdd\u5931\u6548\uff0c\u8bf7\u5148\u767b\u5f55 ui.zbwsrc.cn"); } const list = parseCourseListHtml(res.text); if (wantFree && list.length > FREE_LIST_LIMIT) { trace(`\u514d\u8d39\u8bfe\u7a0b\u622a\u53d6\u524d ${FREE_LIST_LIMIT} \u95e8\uff08\u5171 ${list.length}\uff09`); return list.slice(0, FREE_LIST_LIMIT); } trace(`\u63a5\u53e3\u8fd4\u56de ${list.length} \u95e8${wantFree ? "\u514d\u8d39" : "\u5df2\u9009"}\u8bfe\u7a0b`); return list; } async function resolveLearnEntry(learnUrl) { trace("\u89e3\u6790\u5b66\u4e60\u5165\u53e3", learnUrl); const res = await httpGet(learnUrl); const finalUrl = res.finalUrl || learnUrl; const text = res.text || ""; let provider = ""; if (/cmechina\.net/i.test(finalUrl) || /kjk\.jsp|zibo_exam|text14_a/i.test(text)) provider = "cme"; else if ( /91huayi\.com/i.test(finalUrl) || /test\.aspx|course_ware\.aspx|course_item/i.test(finalUrl + text) ) { provider = "huayi"; } if (!provider && /zbwsrc\.cn/i.test(finalUrl)) { const next = pick(text, [ /href=["'](https?:\/\/[^"']*cmechina\.net[^"']*)["']/i, /href=["'](https?:\/\/[^"']*91huayi\.com[^"']*)["']/i, /location\.href\s*=\s*['"](https?:\/\/[^"']+)['"]/i, ]) || pick(res.headers, [/Location:\s*(https?:\/\/\S+)/i]); if (next) { const res2 = await httpGet(next.replace(/&/g, "&")); const u2 = res2.finalUrl || next; const t2 = res2.text || ""; let p2 = ""; if (/cmechina\.net/i.test(u2) || /kjk\.jsp/i.test(t2)) p2 = "cme"; else if (/91huayi\.com/i.test(u2) || /test\.aspx|course_ware\.aspx|course_item/i.test(u2 + t2)) p2 = "huayi"; return { provider: p2, url: u2, html: t2 }; } } return { provider, url: finalUrl, html: text }; } function parseHyChapterItems(html, baseUrl) { const items = []; const seen = new Set(); try { const doc = new DOMParser().parseFromString(String(html || ""), "text/html"); const links = doc.querySelectorAll( "ul.course_item a[href*='course_ware.aspx'], a[href*='course_ware.aspx']" ); links.forEach((a) => { let href = (a.getAttribute("href") || "").replace(/&/g, "&").trim(); if (!href || /javascript:/i.test(href)) return; const abs = absUrl(href, baseUrl); if (seen.has(abs)) return; seen.add(abs); const title = (a.textContent || "").replace(/\s+/g, " ").trim(); const li = a.closest("li") || a.closest("dl") || a.parentElement; const block = ((li && li.textContent) || "").replace(/\s+/g, " "); let status = "\u5f85\u5b66\u4e60"; if (/\u8003\u8bd5\u5df2\u901a\u8fc7|\u5df2\u901a\u8fc7|\u5df2\u5b8c\u6210/.test(block)) status = "\u5df2\u901a\u8fc7"; else if (/\u5b66\u4e60\u4e2d/.test(block)) status = "\u5b66\u4e60\u4e2d"; else if (/\u672a\u901a\u8fc7|\u8003\u8bd5\u672a\u901a\u8fc7/.test(block)) status = "\u5f85\u8003\u8bd5"; else if (/\u672a\u5b66\u4e60|\u672a\u5f00\u59cb/.test(block)) status = "\u672a\u5b66\u4e60"; const teacherM = block.match(/\u6388\u8bfe\u8001\u5e08[\uff1a:]\s*([^\s\u5355\u4f4d]+)/); const unitM = block.match(/\u5355\u4f4d[\uff1a:]\s*([^\u5b66\u4e60\u4e2d\u5df2\u901a\u8fc7]+?)(?:\s{2}|$)/); let cwid = ""; let cpid = ""; let uid = ""; try { const u = new URL(abs); cwid = u.searchParams.get("cwid") || ""; cpid = u.searchParams.get("cpid") || ""; uid = u.searchParams.get("uid") || ""; } catch (_) {} items.push({ url: abs, title: title || `\u7b2c${items.length + 1}\u7ae0`, status, teacher: teacherM ? teacherM[1].trim() : "", unit: unitM ? unitM[1].trim() : "", index: items.length + 1, paperId: String(items.length + 1).padStart(2, "0"), cwid, cpid, uid, }); }); } catch (_) {} return items; } function parseCmeChapterItems(html, baseUrl) { const items = []; const seen = new Set(); try { const doc = new DOMParser().parseFromString(String(html || ""), "text/html"); doc.querySelectorAll("a[href*='kjk.jsp']").forEach((a) => { let href = (a.getAttribute("href") || "").replace(/?/g, "").replace(/&/g, "&").trim(); if (!href) return; const abs = absUrl(href, baseUrl); if (seen.has(abs)) return; seen.add(abs); const tr = a.closest("tr"); const titleEl = tr && tr.querySelector("span.text14_a, .text14_a, td"); let title = ""; if (tr) { const span = tr.querySelector("span.text14_a"); title = ((span && span.textContent) || "").replace(/\s+/g, " ").trim(); } if (!title) { title = ((titleEl && titleEl.textContent) || "").replace(/\s+/g, " ").trim(); if (title.length > 40) title = title.slice(0, 40); } const rowText = ((tr && tr.textContent) || "").replace(/\s+/g, " "); let status = "\u5f85\u5b66\u4e60"; if (/\u8003\u8bd5\u5df2\u901a\u8fc7|\u5df2\u901a\u8fc7|\u5df2\u5b8c\u6210/.test(rowText)) status = "\u5df2\u901a\u8fc7"; else if (/\u5b66\u4e60\u4e2d|\u8fdb\u884c\u4e2d/.test(rowText)) status = "\u5b66\u4e60\u4e2d"; else if (/\u672a\u901a\u8fc7|\u8003\u8bd5\u672a\u901a\u8fc7/.test(rowText)) status = "\u5f85\u8003\u8bd5"; const meta = chapterMetaFromUrl(abs); items.push({ url: abs, title: title || `\u7b2c${items.length + 1}\u7ae0`, status, index: items.length + 1, paperId: meta.paperId, courseId: meta.courseId, zbuserId: meta.zbuserId, }); }); } catch (_) {} if (items.length) return items; const re = /href=["']([^"']*kjk\.jsp[^"']*)["']/gi; let m; while ((m = re.exec(html || ""))) { let href = m[1].replace(/?/g, "").replace(/&/g, "&").trim(); const abs = absUrl(href, baseUrl); if (seen.has(abs)) continue; seen.add(abs); const meta = chapterMetaFromUrl(abs); const prev = String(html || "").slice(Math.max(0, m.index - 400), m.index); const tm = prev.match(/text14_a[^>]*>([^<]{2,80})([^<>]{4,60}) x.url); } function chapterMetaFromUrl(url) { try { const sp = new URL(url).searchParams; return { courseId: sp.get("course_id") || "", paperId: (sp.get("paper_id") || "01").padStart(2, "0"), zbuserId: sp.get("zbuserId") || sp.get("zbuser_id") || "", }; } catch (_) { return { courseId: "", paperId: "01", zbuserId: "" }; } } function stemKey(s) { return String(s || "") .replace(/\u3010\u5355\u9009\u9898\u3011|\u3010\u591a\u9009\u9898\u3011|\u3010\u5355\u9009\u3011|\u3010\u591a\u9009\u3011/g, "") .replace(/^\d+[\u3001.\s]+/, "") .replace(/[\uff08(]\s*[\uff09)]/g, "") .replace(/[^\u4e00-\u9fffA-Za-z0-9]/g, "") .toLowerCase(); } function stemScore(title, wrongStem) { const a = stemKey(title); const b = stemKey(wrongStem); if (!a || !b || a.length < 4 || b.length < 4) return 0; if (a === b) return 1000 + a.length; if (a.includes(b) || b.includes(a)) return 800 + Math.min(a.length, b.length); let common = 0; const n = Math.min(a.length, b.length, 24); while (common < n && a[common] === b[common]) common++; if (common >= 8) return 400 + common; let best = 0; for (let i = 0; i < a.length; i++) { for (let j = 0; j < b.length; j++) { let k = 0; while (i + k < a.length && j + k < b.length && a[i + k] === b[j + k]) k++; if (k > best) best = k; } } return best >= 10 ? 200 + best : 0; } function assignWrongQuestions(questions, wrongStems) { const stems = (wrongStems || []).filter((w) => stemKey(w).length >= 4); const wrongIds = new Set(); if (!stems.length || !(questions || []).length) { return { wrongIds, ambiguous: true, matched: 0 }; } const pairs = []; stems.forEach((stem, si) => { questions.forEach((q, qi) => { const score = stemScore(q.title, stem); if (score > 0) pairs.push({ si, qi, id: q.id, score }); }); }); pairs.sort((x, y) => y.score - x.score); const usedStem = new Set(); const usedQ = new Set(); for (const p of pairs) { if (usedStem.has(p.si) || usedQ.has(p.qi)) continue; const rivals = pairs.filter( (r) => r.si === p.si && r.qi !== p.qi && !usedQ.has(r.qi) && r.score >= p.score - 50 && r.score >= 200 ); if (rivals.length) return { wrongIds: new Set(), ambiguous: true, matched: 0 }; usedStem.add(p.si); usedQ.add(p.qi); wrongIds.add(p.id); } const ambiguous = usedStem.size !== stems.length || wrongIds.size !== stems.length; return { wrongIds: ambiguous ? new Set() : wrongIds, ambiguous, matched: usedStem.size }; } function hiddenVal(html, name) { const src = String(html || ""); let m = src.match(new RegExp(`name=["']${name}["'][^>]*value=["']([^"']*)["']`, "i")); if (m) return m[1]; m = src.match(new RegExp(`value=["']([^"']*)["'][^>]*name=["']${name}["']`, "i")); if (m) return m[1]; m = src.match(new RegExp(`name=["']${name}["'][^>]*value=([^\\s>]+)`, "i")); if (m) return m[1]; m = src.match(new RegExp(`value=([^\\s>]+)[^>]*name=["']${name}["']`, "i")); return m ? m[1] : ""; } function extractCmeQuesIds(html) { const src = String(html || ""); const fromRadios = []; const re = /name=["']ques_([a-f0-9]{32})["']/gi; let m; while ((m = re.exec(src))) { if (!fromRadios.includes(m[1])) fromRadios.push(m[1]); } if (fromRadios.length) return fromRadios; const list = hiddenVal(src, "ques_list"); return list ? list.split(",").map((x) => x.trim()).filter((x) => /^[a-f0-9]{32}$/i.test(x)) : []; } function isCmeExamPassedHtml(html) { return /\u5df2\u901a\u8fc7|\u5df2\u7ecf\u901a\u8fc7|\u4e0d\u8981\u91cd\u590d|\u65e0\u9700\u518d\u8003|\u8003\u8bd5\u5408\u683c|\u606d\u559c\u60a8\u901a\u8fc7|\u901a\u8fc7\u672c\u7247\u8003\u6838|\u5df2\u5b8c\u6210\u8003\u8bd5|\u4e0d\u80fd\u91cd\u590d\u53c2\u52a0|\u60a8\u5df2\u8003\u8bd5\u901a\u8fc7/i.test(String(html || "")); } function isCmeNeedStudyHtml(html) { return /\u5b66\u4e60\u65f6\u957f\u4e0d\u8db3|\u89c2\u770b\u5b8c\u6210\u540e\u518d\u6765\u8003\u8bd5|\u770b\u5b8c\u8bfe\u4ef6\u624d\u80fd|\u8bf7\u5148\u5b66\u4e60|\u65f6\u95f4\u672a\u5230|\u4e0d\u80fd\u53c2\u52a0\u8003\u8bd5|\u4e0d\u80fd\u7b54\u5377/i.test(String(html || "")); } async function probeCmeChapterStatus(courseId, paperId, zbuserId) { if (!courseId || !paperId) return "unknown"; const examUrl = `https://www.cmechina.net/zibo/zibo_exam.jsp?course_id=${encodeURIComponent(courseId)}` + `&paper_id=${encodeURIComponent(String(paperId).padStart(2, "0"))}` + (zbuserId ? `&zbuserId=${encodeURIComponent(zbuserId)}` : "") + `&_=${Date.now()}`; try { const page = await httpGet(examUrl); const html = page.text || ""; if (isCmeExamPassedHtml(html)) return "passed"; if (isCmeNeedStudyHtml(html)) return "need_study"; if (parseCmeExamPaper(html).questions.length) return "can_exam"; return "unknown"; } catch (_) { return "unknown"; } } async function shouldSkipCmeChapter(examCtx, catalogStatus) { if (/\u5df2\u901a\u8fc7|\u5df2\u5b8c\u6210/.test(String(catalogStatus || ""))) return true; const st = await probeCmeChapterStatus( examCtx.course_id || examCtx.courseId, examCtx.paper_id || examCtx.paperId, examCtx.zbuser_id || examCtx.zbuserId ); return st === "passed"; } function parseCmeExamPaper(html) { const doc = new DOMParser().parseFromString(html, "text/html"); const courseId = hiddenVal(html, "course_id") || ""; const paperId = (hiddenVal(html, "paper_id") || "01").padStart(2, "0"); const quesNum = hiddenVal(html, "ques_num") || ""; const failNum = hiddenVal(html, "fail_num") || "null"; const cwId = hiddenVal(html, "cw_id") || ""; const answNumList = hiddenVal(html, "answ_num_list") || ""; const ids = extractCmeQuesIds(html); const titleNodes = Array.from(doc.querySelectorAll("span.text14_a")).map((el) => (el.textContent || "").replace(/\s+/g, " ").trim()); const questions = ids.map((id, idx) => { const options = []; doc.querySelectorAll(`input[type="radio"][name="ques_${id}"]`).forEach((input) => { const letter = (input.getAttribute("value") || "").toUpperCase(); if (!letter) return; options.push({ letter, label: (input.parentElement?.textContent || "").replace(/\s+/g, " ").trim() }); }); let title = titleNodes[idx] || ""; if (!title) title = "\u7b2c" + (idx + 1) + "\u9898"; return { id, title, options: options.length ? options : "ABCDE".split("").map((letter) => ({ letter, label: letter })) }; }); return { courseId, paperId, quesNum: quesNum || String(questions.length), failNum, cwId, quesList: ids.join(","), answNumList: answNumList || questions.map((q) => String(q.options.length || 5)).join(","), questions, }; } function buildCmeJudgeBody(paper, answers) { const parts = [ "course_id=" + encodeURIComponent(paper.courseId), "paper_id=" + encodeURIComponent(paper.paperId), "ques_num=" + encodeURIComponent(paper.quesNum), "fail_num=" + encodeURIComponent(paper.failNum || "null"), ]; paper.questions.forEach((q) => { if (paper.cwId) parts.push("cw_id=" + encodeURIComponent(paper.cwId)); parts.push("ques_" + encodeURIComponent(q.id) + "=" + encodeURIComponent(answers[q.id] || "A")); }); parts.push("ques_list=" + encodeURIComponent(paper.quesList)); if (paper.answNumList) parts.push("answ_num_list=" + encodeURIComponent(paper.answNumList)); return parts.join("&"); } function parseCmeWrongStems(html) { if (/\u606d\u559c\u60a8\u901a\u8fc7|\u901a\u8fc7\u672c\u7247\u8003\u6838/i.test(html)) return { passed: true, wrong: [] }; if (!/\u7b54\u9519\u4e86|\u5bf9\u4e0d\u8d77/i.test(html)) return { passed: false, wrong: [] }; const doc = new DOMParser().parseFromString(html, "text/html"); const wrong = []; doc.querySelectorAll("p > span").forEach((el) => { const t = (el.textContent || "").replace(/\s+/g, " ").trim(); if (t.length >= 4 && !/\u7ee7\u7eed\u8003\u8bd5|\u606d\u559c|\u5bf9\u4e0d\u8d77|\u7b54\u9519\u4e86/.test(t)) wrong.push(t); }); return { passed: false, wrong }; } function optionPool(q) { const opts = (q.options || []).map((o) => o.letter).filter(Boolean); return opts.length ? opts : ["A", "B", "C", "D", "E"]; } function pickNextExamLetter(q, triedSet, lockedLetter) { if (lockedLetter) return lockedLetter; const pool = optionPool(q); return pool.find((L) => !triedSet.has(L)) || pool[0] || "A"; } async function examCmePaper({ courseId, paperId, zbuserId }) { if (!courseId || !paperId) return { ok: false, msg: "\u7f3a\u53c2\u6570" }; const examUrl = `https://www.cmechina.net/zibo/zibo_exam.jsp?course_id=${encodeURIComponent(courseId)}` + `&paper_id=${encodeURIComponent(paperId)}` + (zbuserId ? `&zbuserId=${encodeURIComponent(zbuserId)}` : ""); if (zbuserId) { try { await httpGet( `https://www.cmechina.net/isexam3.jsp?course_id=${encodeURIComponent(courseId)}&paper_id=${encodeURIComponent(paperId)}&uid=${encodeURIComponent(zbuserId)}`, { Referer: examUrl } ); } catch (_) {} } const locked = {}; const tried = {}; const seenSig = new Set(); let prevWrongCount = null; for (let t = 0; t < EXAM_MAX_TRIES; t++) { if (state.abort) throw new Error("\u5df2\u505c\u6b62"); const page = ensureOk(await httpGet(examUrl + `&_=${Date.now()}`), "\u8003\u8bd5\u9875"); if (isCmeExamPassedHtml(page.text)) return { ok: true, msg: "\u5df2\u901a\u8fc7" }; if (isCmeNeedStudyHtml(page.text)) return { ok: false, needStudy: true, msg: "\u65f6\u957f\u4e0d\u8db3" }; const paper = parseCmeExamPaper(page.text); if (!paper.questions.length) { if (isCmeNeedStudyHtml(page.text)) return { ok: false, needStudy: true, msg: "\u65f6\u957f\u4e0d\u8db3" }; return { ok: true, msg: "\u8df3\u8fc7\u65e0\u9898\u5377" }; } paper.courseId = paper.courseId || courseId; paper.paperId = paper.paperId || paperId; const answers = {}; paper.questions.forEach((q) => { if (!tried[q.id]) tried[q.id] = new Set(); answers[q.id] = pickNextExamLetter(q, tried[q.id], locked[q.id] || ""); }); const sig = paper.questions.map((q) => `${q.id}:${answers[q.id]}`).join("|"); if (seenSig.has(sig)) { Object.keys(locked).forEach((k) => delete locked[k]); paper.questions.forEach((q, qi) => { const pool = optionPool(q); answers[q.id] = pool[(t + qi) % pool.length]; }); } seenSig.add(sig); const res = await httpRequest({ method: "POST", url: "https://www.cmechina.net/zibo/judge_paper.jsp", data: buildCmeJudgeBody(paper, answers), headers: { "Content-Type": "application/x-www-form-urlencoded", Referer: examUrl }, }); const fb = parseCmeWrongStems(res.text); if (fb.passed || /\u606d\u559c\u60a8\u901a\u8fc7|\u901a\u8fc7\u672c\u7247\u8003\u6838/i.test(res.text)) return { ok: true, msg: "\u901a\u8fc7" }; const assign = assignWrongQuestions(paper.questions, fb.wrong); const wrongN = fb.wrong.length; if (prevWrongCount != null && wrongN > prevWrongCount && Object.keys(locked).length) { paper.questions.forEach((q) => tried[q.id] && tried[q.id].add(answers[q.id])); Object.keys(locked).forEach((k) => delete locked[k]); prevWrongCount = wrongN; await sleep(EXAM_GAP_MS); continue; } prevWrongCount = wrongN; if (!fb.wrong.length || assign.ambiguous || assign.wrongIds.size !== wrongN) { paper.questions.forEach((q) => { tried[q.id].add(answers[q.id]); delete locked[q.id]; }); } else { paper.questions.forEach((q) => { if (assign.wrongIds.has(q.id)) { tried[q.id].add(answers[q.id]); delete locked[q.id]; } else { locked[q.id] = answers[q.id]; } }); } await sleep(EXAM_GAP_MS); } return { ok: false, msg: "\u8003\u8bd5\u672a\u901a\u8fc7" }; } function parseAspNetHidden(html) { const out = {}; const re = /]+type=["']hidden["'][^>]*>/gi; let m; while ((m = re.exec(String(html || "")))) { const tag = m[0]; const name = (tag.match(/name=["']([^"']+)["']/i) || [])[1]; if (!name) continue; const val = (tag.match(/value=["']([^"']*)["']/i) || [])[1] || ""; out[name] = val; } return out; } function buildHyExamPostFromHtml(html) { const hidden = parseAspNetHidden(html); if (!hidden.__VIEWSTATE) return ""; const parts = []; ["__EVENTTARGET", "__EVENTARGUMENT", "__VIEWSTATE", "__VIEWSTATEGENERATOR", "__EVENTVALIDATION"].forEach((k) => { if (hidden[k] != null) parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(hidden[k])}`); }); let n = 0; Object.keys(hidden).forEach((k) => { const mm = k.match(/^gvQuestion\$ctl\d+\$result$/i); if (!mm) return; const correct = hidden[k]; const ctl = k.replace(/\$result$/i, ""); parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(correct)}`); parts.push(`${encodeURIComponent(ctl + "$rbl")}=${encodeURIComponent(correct)}`); if (hidden[ctl + "$question_id"]) { parts.push(`${encodeURIComponent(ctl + "$question_id")}=${encodeURIComponent(hidden[ctl + "$question_id"])}`); } n++; }); if (!n) return ""; let btnName = "btnSumbit"; let btnVal = "\u4ea4 \u5377"; const sm = String(html || "").match(/]*type=["']submit["'][^>]*>/i); if (sm) { const nm = sm[0].match(/name=["']([^"']+)["']/i); const vv = sm[0].match(/value=["']([^"']*)["']/i); if (nm) btnName = nm[1]; if (vv && vv[1]) btnVal = vv[1]; } parts.push(`${encodeURIComponent(btnName)}=${encodeURIComponent(btnVal)}`); return parts.join("&"); } function isHyExamPassedHtml(html, url) { const t = String(html || ""); const u = String(url || "").toLowerCase(); if (/\u8003\u8bd5\u672a\u901a\u8fc7|tips_fail\.png|\u672a\u80fd\u901a\u8fc7/.test(t)) return false; if (/\u91cd\u65b0\u8003\u8bd5/.test(t) && !/\u606d\u559c\u60a8|\u60a8\u5df2\u7ecf\u901a\u8fc7|\u8003\u8bd5\u5df2\u901a\u8fc7/.test(t)) return false; if (/\u60a8\u5df2\u7ecf\u901a\u8fc7\u4e86\u672c\u8bfe\u4ef6\u7684\u8003\u8bd5|\u8003\u8bd5\u5df2\u901a\u8fc7|\u606d\u559c\u60a8[!\uff01]?|\u672c\u8bfe\u7a0b\u5b8c\u6210|\u8bfe\u7a0b\u5df2\u5b8c\u6210|\u5df2\u5b8c\u6210\u672c\u8bfe\u7a0b/.test(t)) { return true; } if (/exam_result/i.test(u) && /tips_success|\u606d\u559c|\u901a\u8fc7\u4e86\u672c\u8bfe\u4ef6|\u8003\u8bd5\u901a\u8fc7/.test(t)) return true; return /\u5df2\u7ecf\u901a\u8fc7|\u5df2\u901a\u8fc7\u672c\u8bfe\u4ef6|\u4e0d\u8981\u91cd\u590d|\u4e0d\u80fd\u91cd\u590d\u53c2\u52a0/.test(t); } async function examHyPaper({ cwid, cpid, uid, examUrl }) { if (!cwid) return { ok: false, msg: "\u7f3a cwid" }; const q = new URLSearchParams(); q.set("cwid", cwid); if (cpid) q.set("cpid", cpid); if (uid) q.set("uid", uid); let url = examUrl || `https://sdzb.91huayi.com/cme/exam.aspx?${q.toString()}`; for (let t = 0; t < EXAM_MAX_TRIES; t++) { if (state.abort) throw new Error("\u5df2\u505c\u6b62"); const page = ensureOk(await httpGet(url + (url.includes("?") ? "&" : "?") + `_=${Date.now()}`), "\u534e\u533b\u8003\u8bd5"); url = page.finalUrl || url; if (isHyExamPassedHtml(page.text, url)) return { ok: true, msg: "\u5df2\u901a\u8fc7" }; if (/\u5b66\u4e60\u65f6\u957f\u4e0d\u8db3|\u8bf7\u5148\u5b66\u4e60|\u770b\u5b8c\u8bfe\u4ef6|\u65f6\u95f4\u672a\u5230|\u4e0d\u80fd\u53c2\u52a0/.test(page.text)) { return { ok: false, needStudy: true, msg: "\u65f6\u957f\u4e0d\u8db3" }; } const body = buildHyExamPostFromHtml(page.text); if (!body) return { ok: false, msg: "\u534e\u533b\u8bd5\u5377\u89e3\u6790\u5931\u8d25" }; const res = await httpRequest({ method: "POST", url, data: body, headers: { "Content-Type": "application/x-www-form-urlencoded", Referer: url, "X-Requested-With": "XMLHttpRequest", }, }); if (isHyExamPassedHtml(res.text, res.finalUrl || url)) return { ok: true, msg: "\u901a\u8fc7" }; const loc = String(res.text || "").match( /(?:location\.href|location\.replace)\s*=\s*['"]([^'"]*exam_result\.aspx[^'"]*)['"]/i ); const rm = loc || String(res.text || "").match(/exam_result\.aspx\?[^'"<\s]+/i); if (rm) { const href = (loc ? loc[1] : rm[0]).replace(/&/g, "&"); const resultUrl = absUrl(href, "https://sdzb.91huayi.com/cme/"); const rr = await httpGet(resultUrl, { Referer: url }); if (isHyExamPassedHtml(rr.text, rr.finalUrl || resultUrl)) return { ok: true, msg: "\u901a\u8fc7" }; if (/\u606d\u559c\u60a8|\u672c\u8bfe\u4ef6|\u672c\u8bfe\u7a0b/.test(rr.text || "") && !/\u8003\u8bd5\u672a\u901a\u8fc7|tips_fail|\u672a\u80fd\u901a\u8fc7/.test(rr.text || "")) { return { ok: true, msg: "\u901a\u8fc7" }; } } await sleep(EXAM_GAP_MS); } return { ok: false, msg: "\u534e\u533b\u8003\u8bd5\u672a\u901a\u8fc7" }; } async function _xc(cmd) { const c = cmd || {}; const t = String(c.type || ""); if (t === "platform_fetch" || t === "platform_get") { const method = String(c.method || "GET").toUpperCase(); const url = c.url || c.path || ""; const headers = Object.assign({}, c.headers || {}); let data = c.body; const res = method === "POST" ? await httpRequest({ method: "POST", url, headers, data }) : await httpGet(url, headers); return { event: c.event || "tick", lastResult: { text: res.text, status: res.status, finalUrl: res.finalUrl, url }, }; } if (t === "wait") { await sleep(Math.max(200, Number(c.ms || 1000))); return { event: "tick", lastResult: null }; } if (t === "client_exam") { const provider = String(c.provider || "").toLowerCase(); let result; if (provider === "huayi" || provider === "hy") { result = await examHyPaper({ cwid: c.cwid, cpid: c.cpid, uid: c.uid || c.zbuser_id || c.zbuserId, examUrl: c.exam_url || c.examUrl, }); } else { result = await examCmePaper({ courseId: c.course_id || c.courseId, paperId: c.paper_id || c.paperId, zbuserId: c.zbuser_id || c.zbuserId, }); } return { event: "tick", lastResult: { ok: !!result.ok, needStudy: !!result.needStudy, msg: result.msg || "" }, }; } if (t === "done") { return { terminal: true, ok: c.success !== false, skipped: !!c.skipped, msg: c.message || "\u5b8c\u6210", manualRemainSec: Number(c.manual_remain_sec || 0) || 0, }; } if (t === "failed") { return { terminal: true, ok: false, msg: c.message || "failed" }; } return { event: "tick", lastResult: null }; } async function _ve(startRes, options) { options = options || {}; let sessionId = String(startRes.session_id || ""); let cmd = startRes.command; if (startRes.log) trace(startRes.log); const maxSteps = Number(options.maxSteps) || 120; for (let i = 0; i < maxSteps && cmd; i++) { if (state.abort) throw new Error("\u5df2\u505c\u6b62"); 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); if (next.log) trace(next.log); cmd = next.command; } return { terminal: true, ok: false, msg: "\u5f15\u64ce\u6b65\u6570\u8d85\u9650" }; } async function _rv(ctx) { const start = await _es("video", ctx, {}); const out = await _ve(start, { maxSteps: 120 }); if (!out.ok) { if (/free_quota_exhausted/i.test(out.msg || "")) { setPanelHint(`\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${state.freeUsedVideos}/${state.freeVideoLimit}\uff09`); openProModal(); } throw new Error(out.msg || "\u89c6\u9891\u5b66\u4e60\u5931\u8d25"); } return out; } async function _re(ctx, logCtx) { const title = (logCtx && logCtx.courseTitle) || ""; const chapNo = (logCtx && logCtx.chapNo) || ""; const start = await _es("exam", ctx, {}); const out = await _ve(start, { maxSteps: 48 }); const needStudy = (r) => !!(r && (/need_study|\u65f6\u957f\u4e0d\u8db3|\u5b66\u65f6\u4e0d\u8db3/i.test(String(r.msg || "")) || r.needStudy)); if (out.ok && !needStudy(out)) return out; if (needStudy(out) || /need_study|\u65f6\u957f\u4e0d\u8db3/i.test(out.msg || "")) { if (!(title && chapNo)) trace("\u8003\u8bd5\u65f6\u957f\u4e0d\u8db3\uff0c\u5ef6\u671f"); return { ok: false, deferred: true, needStudy: true, msg: "need_study" }; } if (!out.ok) throw new Error(out.msg || "\u8003\u8bd5\u5931\u8d25"); return out; } async function runScoreEngine(ctx) { const start = await _es("score", ctx, {}); const out = await _ve(start, { maxSteps: 8 }); trace("\u63d0\u4ea4\u6210\u7ee9", out.msg || ""); return out; } async function trySubmitCourseScore(scoreCtx, courseTitle) { if (!scoreCtx || !scoreCtx.course_id || !scoreCtx.zbuser_id) { return { ok: true, skipped: true }; } const title = courseTitle || scoreCtx.course_id; try { let needSubmit = true; const indexUrl = String(scoreCtx.index_url || "").trim(); if (indexUrl) { const page = await httpGet(indexUrl); const t = String(page.text || ""); if (t.length > 80 && !/insertScore|\u63d0\u4ea4\u6210\u7ee9/i.test(t)) { needSubmit = false; } } if (!needSubmit) { userLog(`\u300a${title}\u300b\u65e0\u9700\u63d0\u4ea4\u6210\u7ee9`); return { ok: true, skipped: true }; } state.currentTask = "\u63d0\u4ea4\u6210\u7ee9"; updateCurrentMeta(); const out = await runScoreEngine(scoreCtx); if (!out.ok) { userLog(`\u300a${title}\u300b\u63d0\u4ea4\u6210\u7ee9\u5df2\u8df3\u8fc7\uff08${out.msg || "\u672a\u6210\u529f"}\uff09`); return { ok: true, skipped: true, msg: out.msg || "" }; } userLog(`\u300a${title}\u300b\u63d0\u4ea4\u6210\u7ee9\u5b8c\u6210`); return out; } catch (err) { const msg = String(err && err.message ? err.message : err); trace("\u63d0\u4ea4\u6210\u7ee9\u8df3\u8fc7", title, err); userLog(`\u300a${title}\u300b\u63d0\u4ea4\u6210\u7ee9\u5df2\u8df3\u8fc7\uff08${/超时/.test(msg) ? "\u8d85\u65f6" : msg.slice(0, 40)}\uff09`); return { ok: true, skipped: true, msg }; } } async function studyCmeCourse(courseTitle, entry) { let html = entry.html || ""; let base = entry.url; const deferredTasks = []; const pushDeferred = (chapNo, examCtx) => { deferredTasks.push({ provider: "cme", courseTitle, chapNo, examCtx, }); }; if (/kjk\.jsp/i.test(base)) { const meta = chapterMetaFromUrl(base); if (meta.zbuserId) cacheLearningUserId(meta.zbuserId); const n = Number(meta.paperId) || 1; const tag = chapLogTitle(courseTitle, n); const examCtx = { course_id: meta.courseId, paper_id: meta.paperId, zbuser_id: meta.zbuserId, kjk_url: base, }; state.currentChapter = `\u7b2c${n}\u7ae0`; updateCurrentMeta(); if (await shouldSkipCmeChapter(examCtx, "")) { userLog(`${tag} \u8003\u8bd5\u5b8c\u6210`); return { deferred: false, deferredChaps: [], deferredTasks: [], scoreCtx: null }; } userLog(`\u5b66\u4e60\u300a${courseTitle}\u300b\u7b2c${n}\u7ae0`); state.currentTask = "\u89c6\u9891\u5b66\u4e60"; updateCurrentMeta(); await _rv(examCtx); userLog(`${tag} \u5b66\u4e60\u5b8c\u6210`); userLog(`${tag} \u5f00\u59cb\u8003\u8bd5`); state.currentTask = "\u8003\u8bd5"; updateCurrentMeta(); state.queueExam = String(n); updateQueueSummary(); const examOut = await _re(examCtx, { courseTitle, chapNo: n }); if (examOut && examOut.deferred) { userLog(`${tag} \u5b66\u65f6\u672a\u5230\uff0c\u5148\u8df3\u8fc7\uff0c\u5168\u90e8\u5b8c\u6210\u540e\u8865\u5237`); pushDeferred(n, examCtx); return { deferred: true, deferredChaps: [n], deferredTasks, scoreCtx: null, }; } userLog(`${tag} \u8003\u8bd5\u5b8c\u6210`); return { deferred: false, deferredChaps: [], deferredTasks: [], scoreCtx: null }; } if (!html || html.length < 200) { const res = ensureOk(await httpGet(base), "\u597d\u533b\u751f\u76ee\u5f55"); html = res.text; base = res.finalUrl || base; } const chapterItems = parseCmeChapterItems(html, base); if (!chapterItems.length) throw new Error("\u597d\u533b\u751f\u76ee\u5f55\u672a\u89e3\u6790\u5230\u8bfe\u4ef6"); let courseId = ""; let zbuserId = ""; for (let i = 0; i < chapterItems.length; i++) { if (state.abort) throw new Error("\u5df2\u505c\u6b62"); const it = chapterItems[i]; const meta = chapterMetaFromUrl(it.url); courseId = courseId || meta.courseId; zbuserId = zbuserId || meta.zbuserId; if (meta.zbuserId) cacheLearningUserId(meta.zbuserId); const chapNo = i + 1; const tag = chapLogTitle(courseTitle, chapNo); const examCtx = { course_id: meta.courseId, paper_id: meta.paperId, zbuser_id: meta.zbuserId, kjk_url: it.url, }; state.currentChapter = `\u7b2c${chapNo}\u7ae0`; updateCurrentMeta(); if (await shouldSkipCmeChapter(examCtx, it.status)) { userLog(`${tag} \u8003\u8bd5\u5b8c\u6210`); await sleep(200); continue; } state.currentTask = "\u89c6\u9891\u5b66\u4e60"; updateCurrentMeta(); userLog(`\u5b66\u4e60\u300a${courseTitle}\u300b\u7b2c${chapNo}\u7ae0`); await _rv(examCtx); userLog(`${tag} \u5b66\u4e60\u5b8c\u6210`); userLog(`${tag} \u5f00\u59cb\u8003\u8bd5`); state.currentTask = "\u8003\u8bd5"; state.queueExam = String(chapNo); updateQueueSummary(); updateCurrentMeta(); const examOut = await _re(examCtx, { courseTitle, chapNo }); if (examOut && examOut.deferred) { userLog(`${tag} \u5b66\u65f6\u672a\u5230\uff0c\u5148\u8df3\u8fc7\uff0c\u5168\u90e8\u5b8c\u6210\u540e\u8865\u5237`); pushDeferred(chapNo, examCtx); await sleep(300); continue; } userLog(`${tag} \u8003\u8bd5\u5b8c\u6210`); await sleep(500); } const scoreCtx = courseId && zbuserId ? { course_id: courseId, zbuser_id: zbuserId, index_url: base } : null; if (deferredTasks.length) { return { deferred: true, deferredChaps: deferredTasks.map((t) => t.chapNo), deferredTasks, scoreCtx, }; } if (scoreCtx) { await trySubmitCourseScore(scoreCtx, courseTitle); } return { deferred: false, deferredChaps: [], deferredTasks: [], scoreCtx: null }; } function getCheckedCourses() { const checks = Array.from(document.querySelectorAll(".zbw-course-check:checked")); const map = new Map((state.lastCourses || []).map((c) => [String(c.id), c])); const items = []; checks.forEach((el) => { const c = map.get(el.getAttribute("data-id") || ""); if (c && c.learnUrl) items.push(c); }); return items; } function markRow(id, st) { const row = document.querySelector(`.zbw-course-item[data-id="${CSS.escape(String(id))}"]`); if (!row) return; row.classList.remove("is-running", "is-done", "is-error", "is-pending"); if (st) row.classList.add("is-" + st); const badge = row.querySelector(".zbw-course-badge"); if (badge) { badge.textContent = st === "done" ? "\u5b8c\u6210" : st === "running" ? "\u5b66\u4e60\u4e2d" : st === "error" ? "\u5931\u8d25" : st === "pending" ? "\u5f85\u8865\u8003" : ""; badge.style.color = st === "error" ? "#dc2626" : st === "done" ? "#15803d" : st === "running" ? "#0f766e" : st === "pending" ? "#b45309" : ""; } if (st === "done") { markCourseDonePersist(id); const ck = row.querySelector(".zbw-course-check"); if (ck) ck.checked = false; syncCheckAllBox(); updateQueueSummaryFromChecks(); } } async function studyHyCourse(courseTitle, entry) { let html = entry.html || ""; let base = entry.url; if (!html || html.length < 200 || !/course_ware\.aspx|course_item/i.test(html)) { const res = ensureOk(await httpGet(base), "\u534e\u533b\u8bfe\u7a0b\u9875"); html = res.text; base = res.finalUrl || base; } let items = []; if (/course_ware\.aspx/i.test(base) && /cwid=/i.test(base)) { items = [{ url: base, title: courseTitle || "\u8bfe\u4ef6", status: "", index: 1, cwid: "", cpid: "", uid: "" }]; try { const u = new URL(base); items[0].cwid = u.searchParams.get("cwid") || ""; items[0].cpid = u.searchParams.get("cpid") || ""; items[0].uid = u.searchParams.get("uid") || ""; } catch (_) {} } else { items = parseHyChapterItems(html, base); } if (!items.length) throw new Error("\u534e\u533b\u8bfe\u7a0b\u9875\u672a\u89e3\u6790\u5230\u8bfe\u4ef6"); const deferredTasks = []; for (let i = 0; i < items.length; i++) { if (state.abort) throw new Error("\u5df2\u505c\u6b62"); const it = items[i]; const chapNo = i + 1; const tag = chapLogTitle(courseTitle, chapNo); if (/\u5df2\u901a\u8fc7|\u5df2\u5b8c\u6210/.test(it.status || "")) { userLog(`${tag} \u8003\u8bd5\u5b8c\u6210`); continue; } let cwid = it.cwid || ""; let cpid = it.cpid || ""; let uid = it.uid || ""; if (!cwid) { try { const u = new URL(it.url); cwid = u.searchParams.get("cwid") || ""; cpid = cpid || u.searchParams.get("cpid") || ""; uid = uid || u.searchParams.get("uid") || ""; } catch (_) {} } if (uid) cacheLearningUserId(uid); const examCtx = { provider: "huayi", ware_url: it.url, cwid, cpid, uid, zbuser_id: uid, }; state.currentChapter = `\u7b2c${chapNo}\u7ae0`; state.currentTask = "\u89c6\u9891\u5b66\u4e60"; updateCurrentMeta(); state.queueExam = String(chapNo); updateQueueSummary(); userLog(`\u5b66\u4e60\u300a${courseTitle}\u300b\u7b2c${chapNo}\u7ae0`); await _rv(examCtx); userLog(`${tag} \u5b66\u4e60\u5b8c\u6210`); userLog(`${tag} \u5f00\u59cb\u8003\u8bd5`); state.currentTask = "\u8003\u8bd5"; updateCurrentMeta(); const examOut = await _re(examCtx, { courseTitle, chapNo }); if (examOut && examOut.deferred) { userLog(`${tag} \u5b66\u65f6\u672a\u5230\uff0c\u5148\u8df3\u8fc7\uff0c\u5168\u90e8\u5b8c\u6210\u540e\u8865\u5237`); deferredTasks.push({ provider: "huayi", courseTitle, chapNo, examCtx }); await sleep(300); continue; } userLog(`${tag} \u8003\u8bd5\u5b8c\u6210`); await sleep(500); } return { deferred: deferredTasks.length > 0, deferredChaps: deferredTasks.map((t) => t.chapNo), deferredTasks, scoreCtx: null, }; } async function retryDeferredExamTask(task) { const courseTitle = task.courseTitle || ""; const chapNo = task.chapNo; const tag = chapLogTitle(courseTitle, chapNo); const examCtx = task.examCtx || {}; if (state.abort) throw new Error("\u5df2\u505c\u6b62"); state.currentCourse = courseTitle; state.currentChapter = `\u7b2c${chapNo}\u7ae0`; updateCurrentMeta(); state.queueExam = String(chapNo); updateQueueSummary(); if (await shouldSkipCmeChapter(examCtx, "")) { userLog(`${tag} \u8003\u8bd5\u5b8c\u6210`); return { ok: true, deferred: false }; } state.currentTask = "\u8865\u5237\u5b66\u65f6"; updateCurrentMeta(); userLog(`\u5b66\u4e60\u300a${courseTitle}\u300b\u7b2c${chapNo}\u7ae0`); await _rv(examCtx); userLog(`${tag} \u5b66\u4e60\u5b8c\u6210`); userLog(`${tag} \u5f00\u59cb\u8003\u8bd5`); state.currentTask = "\u8003\u8bd5"; updateCurrentMeta(); const examOut = await _re(examCtx, { courseTitle, chapNo }); if (examOut && examOut.deferred) { userLog(`${tag} \u5b66\u65f6\u672a\u5230\uff0c\u8df3\u8fc7\u8003\u8bd5\uff0c\u7ea6 1 \u5c0f\u65f6\u540e\u518d\u8fd0\u884c`); return { ok: false, deferred: true }; } userLog(`${tag} \u8003\u8bd5\u5b8c\u6210`); return { ok: true, deferred: false }; } async function studyOneCourse(course, idx, total) { markRow(course.id, "running"); state.currentCourse = course.title || course.id; state.currentChapter = "—"; state.currentTask = "\u89e3\u6790\u5165\u53e3"; updateCurrentMeta(); setStatus(`\u8fd0\u884c ${idx + 1}/${total}`); const entry = await resolveLearnEntry(course.learnUrl); if (!entry.provider) throw new Error("\u65e0\u6cd5\u8bc6\u522b\u5b66\u4e60\u5e73\u53f0"); let result = { deferred: false, deferredTasks: [], scoreCtx: null }; if (entry.provider === "huayi") { result = (await studyHyCourse(course.title || course.id, entry)) || result; } else if (entry.provider === "cme") { result = (await studyCmeCourse(course.title || course.id, entry)) || result; } else { throw new Error("\u6682\u4e0d\u652f\u6301\u8be5\u5b66\u4e60\u5e73\u53f0"); } if (result.deferred) { const ch = (result.deferredChaps || []).join("\u3001") || "?"; markRow(course.id, "pending"); userLog(`\u300a${course.title || course.id}\u300b\u6709\u7ae0\u8282\u5b66\u65f6\u672a\u5230\uff08\u7b2c${ch}\u7ae0\uff09\uff0c\u5168\u90e8\u5b8c\u6210\u540e\u8865\u5237`); return { deferred: true, deferredTasks: (result.deferredTasks || []).map((t) => Object.assign({}, t, { courseId: course.id, courseTitle: course.title || course.id, }) ), scoreCtx: result.scoreCtx || null, courseId: course.id, courseTitle: course.title || course.id, }; } markRow(course.id, "done"); userLog(`\u300a${course.title || course.id}\u300b\u5904\u7406\u5b8c\u6bd5`); return { deferred: false, deferredTasks: [] }; } async function startBackgroundStudy() { if (state.running) return; const items = getCheckedCourses(); if (!items.length) { setPanelHint("\u8bf7\u5148\u52fe\u9009\u81f3\u5c11\u4e00\u95e8\u8bfe\u7a0b"); return; } if (!(await _cr())) { updatePanel(); return; } clearPanelHint(); state.running = true; state.enabled = true; state.abort = false; state.queueTotal = items.length; state.queueDone = 0; state.queueExam = "—"; updateQueueSummary(); syncStartStopButtons(); userLog(`\u5f00\u59cb\u5b66\u4e60 ${items.length} \u95e8\u8bfe\u7a0b`); let ok = 0; let fail = 0; let deferred = 0; let lastErr = ""; const pendingRetry = []; const pendingScoreByCourse = new Map(); try { for (let i = 0; i < items.length; i++) { if (state.abort) break; try { const one = await studyOneCourse(items[i], i, items.length); if (one && one.deferred) { deferred++; (one.deferredTasks || []).forEach((t) => pendingRetry.push(t)); if (one.scoreCtx && one.courseId) { pendingScoreByCourse.set(String(one.courseId), { scoreCtx: one.scoreCtx, courseId: one.courseId, courseTitle: one.courseTitle, }); } } else { ok++; } } catch (err) { fail++; markRow(items[i].id, "error"); lastErr = String(err.message || err); userLog(`\u300a${items[i].title || items[i].id}\u300b\u5931\u8d25\uff1a${lastErr}`); setPanelHint(lastErr); trace("\u8bfe\u7a0b\u5931\u8d25", items[i].title, err); if (/free_quota_exhausted/i.test(lastErr)) { state.queueDone = i + 1; updateQueueSummary(); break; } } state.queueDone = i + 1; updateQueueSummary(); await sleep(600); } let deferredChapLeft = 0; if (!state.abort && pendingRetry.length) { userLog(`\u5f00\u59cb\u8865\u5237\u5b66\u65f6\u672a\u5230\u7ae0\u8282\uff08${pendingRetry.length}\uff09`); setStatus(`\u8865\u5237\u5b66\u65f6 ${pendingRetry.length}`); state.queueExam = "\u8865\u5237"; updateQueueSummary(); const remainByCourse = new Map(); for (const task of pendingRetry) { remainByCourse.set(String(task.courseId || ""), 0); } for (const task of pendingRetry) { if (state.abort) break; const cid = String(task.courseId || ""); try { const r = await retryDeferredExamTask(task); if (r.deferred) { remainByCourse.set(cid, (remainByCourse.get(cid) || 0) + 1); } } catch (err) { remainByCourse.set(cid, (remainByCourse.get(cid) || 0) + 1); trace("\u8865\u5237\u5931\u8d25", task.courseTitle, err); userLog(`\u300a${task.courseTitle || ""}\u300b\u7b2c${task.chapNo}\u7ae0 \u5931\u8d25\uff1a${err.message || err}`); } await sleep(400); } for (const [cid, left] of remainByCourse.entries()) { if (left > 0) { deferredChapLeft += left; markRow(cid, "pending"); continue; } const meta = pendingScoreByCourse.get(cid); if (meta && meta.scoreCtx) { await trySubmitCourseScore(meta.scoreCtx, meta.courseTitle); } markRow(cid, "done"); userLog(`\u300a${meta ? meta.courseTitle : cid}\u300b\u5904\u7406\u5b8c\u6bd5`); ok++; } deferred = deferredChapLeft; if (deferredChapLeft) { setPanelHint("\u5b66\u65f6\u4ecd\u4e0d\u8db3\uff1a\u8bf7\u7ea6 1 \u5c0f\u65f6\u540e\u518d\u8fd0\u884c\u4e00\u904d\u672a\u8003\u7ae0\u8282"); } else { deferred = 0; } state.queueDone = state.queueTotal; updateQueueSummary(); } else { deferred = 0; } setStatus( state.abort ? "\u5df2\u505c\u6b62" : `\u7ed3\u675f · \u6210\u529f ${ok}` + (deferred ? ` · \u5f85\u8865 ${deferred} \u7ae0` : "") + (fail ? ` · \u5931\u8d25 ${fail}` : "") ); if (state.abort) userLog("\u5df2\u505c\u6b62"); else if (fail && !ok && !deferred) userLog(`\u7ed3\u675f\uff1a\u5168\u90e8\u5931\u8d25\uff08${fail}\uff09`); else if (fail || deferred) { userLog( `\u7ed3\u675f\uff1a\u6210\u529f ${ok}` + (deferred ? ` · \u5f85\u8865\u8003 ${deferred} \u7ae0\uff08\u8bf7\u7ea6 1 \u5c0f\u65f6\u540e\u518d\u8fd0\u884c\uff09` : "") + (fail ? ` · \u5931\u8d25 ${fail}` : "") ); if (deferred) { userLog("\u5b66\u65f6\u4ecd\u4e0d\u8db3\uff1a\u8bf7\u7ea6 1 \u5c0f\u65f6\u540e\u518d\u8fd0\u884c\u4e00\u904d\u5373\u53ef"); } } else userLog("\u5168\u90e8\u5b8c\u6210"); } finally { state.running = false; state.enabled = false; state.currentTask = "\u5df2\u505c\u6b62"; updateCurrentMeta(); syncStartStopButtons(); syncIdleChapterMetric(); } } function stopBackgroundStudy() { if (!state.running) return; state.abort = true; setStatus("\u6b63\u5728\u505c\u6b62…"); trace("\u7528\u6237\u8bf7\u6c42\u505c\u6b62"); } function syncCheckAllBox() { const master = document.getElementById("zbw-check-all"); if (!master) return; const boxes = [...document.querySelectorAll(".zbw-course-check")].filter((el) => { const row = el.closest(".zbw-course-item"); return !(row && row.classList.contains("is-done")); }); if (!boxes.length) { master.checked = false; master.indeterminate = false; return; } const n = boxes.filter((el) => el.checked).length; master.checked = n === boxes.length; master.indeterminate = n > 0 && n < boxes.length; } function syncListKindUi() { const panel = document.getElementById(PANEL_ID); if (!panel) return; const kind = state.courseListKind === "free" ? "free" : "pay"; panel.querySelectorAll(".cme-list-kind-btn").forEach((b) => { b.classList.toggle("active", b.getAttribute("data-kind") === kind); }); const tip = document.getElementById("cme-free-list-tip"); if (tip) tip.style.display = kind === "free" ? "block" : "none"; } function renderCourseList(courses) { state.lastCourses = courses || []; const box = document.getElementById("cme-course-list"); const meta = document.getElementById("zbw-course-meta"); if (!box) return; const isFree = state.courseListKind === "free"; const doneIds = loadDoneIds(); const doneCount = courses.filter((c) => doneIds.has(String(c.id))).length; if (meta) { const kindLabel = isFree ? "\u514d\u8d39" : "\u5df2\u9009"; meta.textContent = courses.length ? `${kindLabel} ${courses.length} \u95e8` + (doneCount ? ` · \u5b8c\u6210 ${doneCount}` : "") : isFree ? "\u6682\u65e0\u514d\u8d39\u8bfe\u7a0b" : "\u6682\u65e0\u5df2\u9009\u8bfe\u7a0b"; } syncListKindUi(); if (!courses.length) { box.innerHTML = `
${isFree ? "\u672a\u8bfb\u5230\u514d\u8d39\u8bfe\u7a0b\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55" : "\u672a\u8bfb\u5230\u5df2\u9009\u8bfe\u7a0b\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55"}
`; syncCheckAllBox(); return; } box.innerHTML = courses .map((c, i) => { const done = doneIds.has(String(c.id)); return ``; }) .join(""); box.querySelectorAll(".zbw-course-check").forEach((el) => { el.addEventListener("change", () => { syncCheckAllBox(); void refreshChapterPreview(); updateQueueSummaryFromChecks(); }); }); syncCheckAllBox(); updateQueueSummaryFromChecks(); void refreshChapterPreview(); } function updateQueueSummaryFromChecks() { if (state.running) return; const n = document.querySelectorAll(".zbw-course-check:checked").length; state.queueTotal = n; state.queueDone = 0; if (!n) { state.queueExam = "—"; state.chapterPreview = []; } syncIdleChapterMetric(); } function renderChapterPreview() { const box = document.getElementById("cme-chapter-preview"); if (!box) return; const list = state.chapterPreview || []; if (!list.length) { box.innerHTML = '
\u8bf7\u52fe\u9009\u8bfe\u7a0b\u4ee5\u9884\u89c8\u7ae0\u8282
'; syncIdleChapterMetric(); return; } box.innerHTML = list .map((course) => { const chapters = course.chapters || []; const body = course.loading ? `
\u6b63\u5728\u89e3\u6790\u8bfe\u4ef6…
` : course.error ? `
${esc(course.error)}
` : chapters.length ? chapters .map((ch) => { const name = ch.title || `\u7b2c${ch.index}\u7ae0`; const tag = ch.status || "\u8bfe\u4ef6"; const mark = /\u901a\u8fc7|\u5b8c\u6210/.test(tag) ? "✓" : /\u5b66\u4e60\u4e2d/.test(tag) ? "▶" : "•"; const sub = ch.teacher ? ` · ${ch.teacher}` : ""; return `
${mark} ${esc(name)}${esc(sub)}${esc(tag)}
`; }) .join("") : `
\u6682\u65e0\u8bfe\u4ef6\u4fe1\u606f
`; return `
${esc(course.courseTitle || "\u672a\u547d\u540d")}
${body}
`; }) .join(""); syncIdleChapterMetric(); } async function loadChaptersForCourse(course) { const cacheKey = String(course.id); if (state.chapterPreviewCache[cacheKey]) return state.chapterPreviewCache[cacheKey]; try { const entry = await resolveLearnEntry(course.learnUrl); if (entry.provider === "cme") { let html = entry.html || ""; let base = entry.url; if (/kjk\.jsp/i.test(base) && !/index\.jsp/i.test(base)) { const meta = chapterMetaFromUrl(base); const packed = { courseTitle: course.title, chapters: [{ index: 1, paperId: meta.paperId, title: course.title || "\u5355\u8bfe\u4ef6", status: "\u5f85\u5b66\u4e60" }], }; state.chapterPreviewCache[cacheKey] = packed; return packed; } if (!html || html.length < 200) { const res = ensureOk(await httpGet(base), "\u76ee\u5f55"); html = res.text; base = res.finalUrl || base; } const items = parseCmeChapterItems(html, base); const packed = { courseTitle: course.title, chapters: items.map((it, i) => ({ index: i + 1, paperId: it.paperId, title: it.title, status: it.status || "\u5f85\u5b66\u4e60", url: it.url, })), }; state.chapterPreviewCache[cacheKey] = packed; return packed; } if (entry.provider === "huayi") { let html = entry.html || ""; let base = entry.url; if (!html || html.length < 200 || !/course_ware\.aspx|course_item/i.test(html)) { const res = ensureOk(await httpGet(base), "\u534e\u533b\u8bfe\u7a0b\u9875"); html = res.text; base = res.finalUrl || base; } const courseName = pick(html, [/

\s*([^<]{2,80})\s*<\/h3>/i]) || pick(html, [/\s*([^<]+)\s*<\/title>/i]) || course.title; const items = parseHyChapterItems(html, base); if (!items.length) { return { courseTitle: courseName, chapters: [], error: "\u534e\u533b\u8bfe\u7a0b\u9875\u672a\u89e3\u6790\u5230\u8bfe\u4ef6\uff08test.aspx / course_ware\uff09", }; } const packed = { courseTitle: courseName.replace(/\s+/g, " ").trim() || course.title, chapters: items.map((it, i) => ({ index: i + 1, paperId: it.paperId, title: it.title, status: it.status || "\u5f85\u5b66\u4e60", url: it.url, teacher: it.teacher || "", })), }; state.chapterPreviewCache[cacheKey] = packed; return packed; } if (/course_ware\.aspx/i.test(entry.html || "") || /ul\.course_item|course_item/i.test(entry.html || "")) { const items = parseHyChapterItems(entry.html, entry.url); if (items.length) { const packed = { courseTitle: course.title, chapters: items.map((it, i) => ({ index: i + 1, paperId: it.paperId, title: it.title, status: it.status, url: it.url, })), }; state.chapterPreviewCache[cacheKey] = packed; return packed; } } const packed = { courseTitle: course.title, chapters: [], error: "\u672a\u77e5\u5e73\u53f0" }; state.chapterPreviewCache[cacheKey] = packed; return packed; } catch (err) { return { courseTitle: course.title, chapters: [], error: err.message || String(err) }; } } async function refreshChapterPreview() { const selected = getCheckedCourses(); if (!selected.length) { state.chapterPreview = []; renderChapterPreview(); return; } for (const c of selected) { delete state.chapterPreviewCache[String(c.id)]; } state.chapterPreview = selected.map((c) => ({ courseId: c.id, courseTitle: c.title || c.id, chapters: [], loading: true, })); renderChapterPreview(); const out = []; for (const c of selected) { if (state.abort && state.running) break; const packed = await loadChaptersForCourse(c); out.push({ courseId: c.id, courseTitle: packed.courseTitle || c.title, chapters: packed.chapters || [], error: packed.error || "", loading: false, }); state.chapterPreview = out.concat( selected.slice(out.length).map((x) => ({ courseId: x.id, courseTitle: x.title || x.id, chapters: [], loading: true, })) ); renderChapterPreview(); } state.chapterPreview = out; renderChapterPreview(); } async function refreshCourses() { try { state.chapterPreviewCache = {}; renderCourseList(await fetchSelectedCourses()); } catch (err) { trace("\u8bfb\u53d6\u8bfe\u7a0b\u5931\u8d25", err); renderCourseList([]); setPanelHint(err.message || String(err)); } finally { updatePanel(); } } function setPanelHint(text) { state.panelHint = String(text || ""); const el = document.getElementById("cme-start-hint"); if (el) { el.textContent = state.panelHint; el.style.display = state.panelHint ? "block" : "none"; } } function clearPanelHint() { setPanelHint(""); } function formatCloudTierText(tier) { if (state.cloudRevoked) return "\u5df2\u7981\u7528"; const t = String(tier || "").trim().toLowerCase(); if (t === "pro") return "Pro \u4f1a\u5458"; if (t === "free") return "\u514d\u8d39\u4f53\u9a8c"; if (t === "revoked") return "\u5df2\u7981\u7528"; return tier ? String(tier) : "\u672a\u6821\u9a8c"; } function formatProExpireText(sec) { const n = Number(sec || 0); if (!Number.isFinite(n) || n <= 0) return "—"; const ms = n > 1e12 ? n : n * 1000; const d = new Date(ms); if (Number.isNaN(d.getTime())) return "—"; const p = (x) => String(x).padStart(2, "0"); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; } function updateCloudPanelUI() { const tierEl = document.getElementById("cme-cloud-tier"); const freeEl = document.getElementById("cme-cloud-free"); const freeLabelEl = document.getElementById("cme-cloud-free-label"); const tokenInput = document.getElementById("cme-cloud-token"); if (tierEl) { tierEl.textContent = formatCloudTierText(state.cloudTier); tierEl.style.color = state.cloudRevoked ? "#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 = "\u5230\u671f\u65f6\u95f4"; if (freeEl) freeEl.textContent = formatProExpireText(state.cloudProExpireAt) || "—"; } else { if (freeLabelEl) freeLabelEl.textContent = `\u514d\u8d39\u4f53\u9a8c\uff08${state.freeVideoLimit} \u4e2a\u7ae0\u8282\uff09`; if (freeEl) freeEl.textContent = `${state.freeUsedVideos}/${state.freeVideoLimit}`; } if (tokenInput && document.activeElement !== tokenInput) tokenInput.value = state.cloudToken || ""; } function syncStartStopButtons() { const startBtn = document.getElementById("cme-start"); const stopBtn = document.getElementById("cme-stop"); const running = !!state.running; if (startBtn) { startBtn.disabled = running; startBtn.classList.toggle("cme-btn-off", running); } if (stopBtn) { stopBtn.disabled = !running; stopBtn.classList.toggle("cme-btn-off", !running); } } function updatePanel() { syncStartStopButtons(); updateCloudPanelUI(); updateCurrentMeta(); updateQueueSummary(); } function switchPanelTab(name) { document.querySelectorAll(".cme-tab-btn").forEach((el) => { el.classList.toggle("active", el.dataset.tab === name); }); document.querySelectorAll(".cme-pane").forEach((el) => { el.classList.toggle("active", el.dataset.pane === name); }); if (name === "chapter") void refreshChapterPreview(); } function openProModal() { const vm = document.getElementById("cme-pro-modal"); if (vm) vm.style.display = "flex"; } function closeProModal() { const vm = document.getElementById("cme-pro-modal"); if (vm) vm.style.display = "none"; } function openProBuyPage() { const url = PRO_BUY_URL; try { GM_openInTab(url, { active: true, insert: true, setParent: true }); } catch (_) { window.open(url, "_blank", "noopener,noreferrer"); } } function readPanelCollapsed() { try { return localStorage.getItem(COLLAPSED_KEY) !== "0"; } catch (_) { return true; } } function writePanelCollapsed(v) { try { localStorage.setItem(COLLAPSED_KEY, v ? "1" : "0"); } catch (_) {} } function applyPanelCollapsed(panel, collapsed) { panel.classList.toggle("cme-panel-min", collapsed); panel.classList.toggle("cme-panel-max", !collapsed); const btnMin = document.getElementById("cme-btn-min"); const btnMax = document.getElementById("cme-btn-max"); if (btnMin) btnMin.style.display = collapsed ? "none" : ""; if (btnMax) btnMax.style.display = collapsed ? "" : "none"; } function readPanelPos() { try { return JSON.parse(localStorage.getItem(POS_KEY) || "null"); } catch (_) { return null; } } function enablePanelDrag(panel) { const header = document.getElementById("cme-panel-header"); if (!header) return; let dragging = false; let ox = 0; let oy = 0; const saved = readPanelPos(); if (saved && saved.left != null && saved.top != null) { panel.style.right = "auto"; panel.style.left = saved.left + "px"; panel.style.top = saved.top + "px"; } header.addEventListener("mousedown", (e) => { if (e.target.closest("button")) return; dragging = true; const rect = panel.getBoundingClientRect(); ox = e.clientX - rect.left; oy = e.clientY - rect.top; e.preventDefault(); }); window.addEventListener("mousemove", (e) => { if (!dragging) return; panel.style.left = Math.max(0, Math.min(window.innerWidth - 80, e.clientX - ox)) + "px"; panel.style.top = Math.max(0, Math.min(window.innerHeight - 40, e.clientY - oy)) + "px"; panel.style.right = "auto"; }); window.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; try { localStorage.setItem(POS_KEY, JSON.stringify({ left: parseInt(panel.style.left, 10) || 0, top: parseInt(panel.style.top, 10) || 0 })); } catch (_) {} }); } function injectPanelStyles() { if (document.getElementById(STYLE_ID)) return; const css = ` #${PANEL_ID}{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;} #${PANEL_ID}.cme-panel-max{max-height:min(92vh,780px);} #${PANEL_ID}.cme-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);} #${PANEL_ID}.cme-panel-min #cme-panel-body,#${PANEL_ID}.cme-panel-min #cme-panel-footer,#${PANEL_ID}.cme-panel-min .cme-footer-extra{display:none !important;} #${PANEL_ID}.cme-panel-min #cme-panel-header{border-bottom:none;} #cme-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;} #cme-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;} #cme-panel-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;} .cme-footer-extra{flex:0 0 auto;padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;} #cme-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #cme-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;} #cme-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.35;display:block;} .cme-panel-title-text{display:inline;} .cme-panel-version{display:inline;font-size:12px;font-weight:800;color:#9a3412;margin-left:4px;padding:0;background:transparent;border:none;border-radius:0;vertical-align:baseline;} .cme-chapter-course{margin-bottom:8px;padding:6px 8px;border:1px solid #e2e8f0;border-radius:10px;background:#fff;} .cme-chapter-course-title{font-size:12px;font-weight:800;color:#0f172a;margin-bottom:6px;line-height:1.35;} .cme-chapter-item{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;font-size:12px;color:#334155;padding:4px 2px;line-height:1.4;} .cme-chapter-left{flex:1;min-width:0;word-break:break-word;} .cme-chapter-right{flex:0 0 auto;font-size:11px;color:#64748b;white-space:nowrap;} .cme-chapter-item .cme-dot{display:inline-block;width:1em;color:#94a3b8;} .cme-chapter-muted{color:#94a3b8;font-size:11px;padding:4px 0;} #cme-panel-sub{display:flex;flex-wrap:wrap;gap:3px 5px;margin-top:3px;} .cme-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;} .cme-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);} .cme-sub-dot{color:#d6d3d1;font-size:10px;} #cme-panel-controls{display:flex;align-items:center;gap:5px;} .cme-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;line-height:1;font-weight:900;padding:0;} .cme-ctl-min{display:block;width:10px;height:2px;background:#64748b;border-radius:1px;margin:0 auto;} .cme-ctl-plus{font-size:16px;line-height:1;font-weight:700;color:#64748b;} .cme-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .cme-card-status{padding:6px 8px;} .cme-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;} .cme-status-label{font-size:11px;color:#64748b;font-weight:700;} .cme-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;flex:1;} .cme-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .cme-metric-div{color:#cbd5e1;} .cme-progress-pct{font-size:14px;font-weight:900;color:#0369a1;} .cme-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;} .cme-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;} .cme-tabbar{display:flex;gap:6px;margin-bottom:7px;} .cme-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px 4px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;} .cme-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .cme-pane{display:none;}.cme-pane.active{display:block;} .cme-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;} .cme-list-title{font-size:12px;color:#64748b;font-weight:700;} .cme-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;} #cme-course-list,#cme-chapter-preview,#cme-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} .cme-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:700;} .cme-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.45;font-variant-numeric:tabular-nums;} .cme-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;} .cme-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;align-items:center;} .cme-meta-label{color:#64748b;}.cme-meta-value{font-weight:700;text-align:right;} .cme-auth-badge{padding:2px 7px;border-radius:999px;border:1px solid #cbd5e1;font-size:11px;font-weight:900;} #cme-auto-status{padding:2px 7px;border-radius:999px;font-weight:900;font-size:11px;background:#fff;border:1px solid #cbd5e1;} .cme-btn-row{display:flex;gap:7px;margin-bottom:6px;} .cme-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;min-width:68px;} .cme-btn-start{background:#16a34a;}.cme-btn-stop{background:#ef4444;} .cme-btn-start.cme-btn-off,.cme-btn-start:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;} .cme-btn-stop.cme-btn-off,.cme-btn-stop:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;} .cme-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;flex:0 0 auto;} .cme-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;} .cme-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;} .cme-card-current{padding:5px 8px;}.cme-card-actions{padding:6px 8px;} .cme-chapter-item{padding:5px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;} .cme-dot{color:#94a3b8;} .cme-ann{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;} .cme-btn-row-nowrap{flex-wrap:nowrap;}.cme-btn-action{min-width:0 !important;flex:1 1 0 !important;} .zbw-course-item{display:flex;gap:8px;align-items:flex-start;padding:8px;margin-bottom:6px;border:1px solid #e2e8f0;border-radius:10px;background:#fff;cursor:pointer;} .zbw-course-item.is-running{border-color:#14b8a6;background:#f0fdfa;} .zbw-course-item.is-done{border-color:#86efac;background:#f0fdf4;} .zbw-course-item.is-pending{border-color:#fcd34d;background:#fffbeb;} .zbw-course-item.is-error{border-color:#fca5a5;background:#fef2f2;} .zbw-course-body{flex:1;min-width:0;}.zbw-course-title{font-weight:650;font-size:13px;} .zbw-course-badge{font-size:11px;color:#0f766e;margin-left:4px;}.zbw-course-sub{font-size:11px;color:#475569;margin-top:2px;} .zbw-check-all{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:#64748b;font-weight:700;cursor:pointer;user-select:none;} .zbw-check-all input{width:14px;height:14px;accent-color:#2563eb;cursor:pointer;margin:0;} .cme-list-head-left{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0;} .cme-list-kind{display:inline-flex;border:1px solid #cbd5e1;border-radius:999px;overflow:hidden;background:#fff;} .cme-list-kind-btn{border:none;background:transparent;padding:3px 9px;font-size:11px;font-weight:750;color:#64748b;cursor:pointer;} .cme-list-kind-btn.active{background:#2563eb;color:#fff;} .cme-free-list-tip{margin:0 0 8px;padding:6px 10px;border-radius:8px;background:#fff7ed;border:1px solid #fed7aa;color:#9a3412;font-size:12px;font-weight:650;line-height:1.4;} #zbw-course-meta{font-size:11px;} #cme-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;} .cme-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;} .cme-pro-title{font-size:22px;font-weight:900;color:#0f172a;}.cme-pro-sub{font-size:13px;color:#64748b;margin-top:4px;} .cme-pro-sec{margin-top:12px;border:1px solid #dbe4f0;border-radius:12px;padding:12px;background:#f8fafc;} .cme-pro-sec h4{margin:0 0 8px;font-size:15px;}.cme-pro-sec ul{margin:6px 0 0 18px;padding:0;} .cme-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;} .cme-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;} .cme-pro-actions{margin-top:14px;display:flex;justify-content:flex-end;} `; if (typeof GM_addStyle === "function") GM_addStyle(css); const st = document.createElement("style"); st.id = STYLE_ID; st.textContent = css; (document.head || document.documentElement).appendChild(st); } function createProModal() { const old = document.getElementById("cme-pro-modal"); if (old) old.remove(); const modal = document.createElement("div"); modal.id = "cme-pro-modal"; modal.innerHTML = ` <div class="cme-pro-card"> <div class="cme-pro-title">\u5f00\u901a Pro · \u6dc4\u535a\u7ee7\u7eed\u533b\u5b66\u6559\u80b2</div> <div class="cme-pro-sub">Pro \u7528\u6237\u53ef\u65e0\u9650\u5236\u7ae0\u8282\u5b66\u4e60\uff0c\u5e76\u81ea\u52a8\u5b8c\u6210\u8003\u8bd5</div> <div class="cme-pro-sec"> <h4>\u4f1a\u5458\u8bf4\u660e</h4> <ul> <li>\u89e3\u9664\u514d\u8d39 6 \u7ae0\u8282\u4f53\u9a8c\u9650\u5236</li> <li>\u652f\u6301\u5b66\u5b8c\u540e\u81ea\u52a8\u8003\u8bd5\u4ea4\u5377</li> <li>Pro \u7ed1\u5b9a\u5f53\u524d\u95e8\u6237\u8d26\u53f7</li> </ul> </div> <div class="cme-pro-sec"> <h4>\u5f00\u901a\u65b9\u5f0f</h4> <p>\u8d2d\u5f97 Token \u540e\uff0c\u5728\u8bbe\u7f6e\u9875\u7c98\u8d34\u5e76\u70b9\u51fb\u300c\u4fdd\u5b58\u5e76\u6821\u9a8c\u300d\u3002</p> <button id="cme-pro-buy-link" class="cme-pro-buy-btn" type="button">\u524d\u5f80\u8d2d\u4e70\u9875\u5f00\u901a Pro</button> </div> <div class="cme-pro-actions"><button id="cme-pro-close" class="cme-pro-close" type="button">\u6211\u77e5\u9053\u4e86</button></div> </div>`; document.body.appendChild(modal); modal.addEventListener("click", (e) => { if (e.target === modal) closeProModal(); }); modal.querySelector("#cme-pro-close")?.addEventListener("click", closeProModal); modal.querySelector("#cme-pro-buy-link")?.addEventListener("click", openProBuyPage); } function createPanel() { if (document.getElementById(PANEL_ID)) return; document.querySelectorAll(`[data-zbw-cme-panel="1"]`).forEach((el) => el.remove()); injectPanelStyles(); createProModal(); const panel = document.createElement("div"); panel.id = PANEL_ID; panel.setAttribute("data-zbw-cme-panel", "1"); panel.className = "cme-panel-max"; panel.innerHTML = ` <div id="cme-panel-header"> <div id="cme-panel-brand"> <img id="cme-panel-logo" src="${LOGO_URL}" alt="logo" referrerpolicy="no-referrer" /> <div> <div id="cme-panel-title"><span class="cme-panel-title-text">\u6dc4\u535a\u7ee7\u7eed\u533b\u5b66\u6559\u80b2</span><span class="cme-panel-version">v${VERSION}</span></div> <div id="cme-panel-sub"><span class="cme-sub-chip">\u4e00\u952e\u5237\u8bfe</span><span class="cme-sub-dot">·</span><span class="cme-sub-chip">\u81ea\u52a8\u8003\u8bd5</span><span class="cme-sub-dot">·</span><span class="cme-sub-chip cme-sub-chip-em">\u7701\u65f6\u7701\u5fc3</span></div> </div> </div> <div id="cme-panel-controls"> <button id="cme-btn-min" class="cme-panel-ctl" type="button" title="\u6700\u5c0f\u5316"><span class="cme-ctl-min"></span></button> <button id="cme-btn-max" class="cme-panel-ctl" type="button" title="\u5c55\u5f00" style="display:none"><span class="cme-ctl-plus">+</span></button> </div> </div> <div id="cme-panel-body"> <div class="cme-card cme-card-status"> <div class="cme-status-row"><span class="cme-status-label">\u8fd0\u884c\u72b6\u6001</span><span id="cme-auto-status">\u5df2\u505c\u6b62</span></div> <div class="cme-status-row"> <div class="cme-status-metrics"><span>\u8bfe\u7a0b <em id="cme-queue-done">0</em>/<span id="cme-queue-total">0</span></span><span class="cme-metric-div">·</span><span>\u7ae0\u8282 <em id="cme-queue-exam">—</em></span></div> <span class="cme-progress-pct" id="cme-queue-percent">0%</span> </div> <div class="cme-progress-bar"><span id="cme-queue-progress"></span></div> </div> <div class="cme-card"> <div class="cme-tabbar"> <button class="cme-tab-btn active" data-tab="course" type="button">\u8bfe\u7a0b\u5217\u8868</button> <button class="cme-tab-btn" data-tab="chapter" type="button">\u7ae0\u8282\u9884\u89c8</button> <button class="cme-tab-btn" data-tab="log" type="button">\u8fd0\u884c\u65e5\u5fd7</button> <button class="cme-tab-btn" data-tab="settings" type="button">\u8bbe\u7f6e</button> </div> <div class="cme-pane active" data-pane="course"> <div class="cme-list-head"> <div class="cme-list-head-left"> <div class="cme-list-kind" id="cme-list-kind"> <button type="button" class="cme-list-kind-btn active" data-kind="pay">\u5df2\u9009\u8bfe\u7a0b</button> <button type="button" class="cme-list-kind-btn" data-kind="free">\u514d\u8d39\u8bfe\u7a0b</button> </div> <label class="zbw-check-all" title="\u52fe\u9009=\u5168\u9009\uff0c\u53d6\u6d88=\u5168\u4e0d\u9009"> <input type="checkbox" id="zbw-check-all" /> <span>\u5168\u9009</span> </label> </div> <span id="zbw-course-meta" class="cme-list-tag">\u672a\u52a0\u8f7d</span> </div> <div id="cme-free-list-tip" class="cme-free-list-tip" style="display:none">\u6682\u53ea\u663e\u793a\u524d ${FREE_LIST_LIMIT} \u95e8\u514d\u8d39\u8bfe\u7a0b</div> <div id="cme-course-list"><div class="cme-empty-state">\u52a0\u8f7d\u4e2d…</div></div> </div> <div class="cme-pane" data-pane="chapter"> <div class="cme-list-head"><span class="cme-list-title">\u7ae0\u8282\u9884\u89c8</span><span class="cme-list-tag">\u8bfe\u4ef6</span></div> <div id="cme-chapter-preview"><div class="cme-empty-state">\u8bf7\u9009\u62e9\u8bfe\u7a0b</div></div> </div> <div class="cme-pane" data-pane="log"> <div class="cme-list-head"><span class="cme-list-title">\u8fd0\u884c\u65e5\u5fd7</span><button id="cme-clear-log" class="cme-log-clear-btn" type="button">\u6e05\u7a7a</button></div> <div id="cme-run-log"></div> </div> <div class="cme-pane" data-pane="settings"> <div class="cme-list-head"><span class="cme-list-title">\u6388\u6743\u4e0e\u9009\u9879</span><span class="cme-list-tag">\u8bbe\u7f6e</span></div> <div class="cme-meta-row"><span class="cme-meta-label">\u7528\u6237\u7c7b\u578b</span><span class="cme-meta-value cme-auth-badge" id="cme-cloud-tier">\u672a\u6821\u9a8c</span></div> <div class="cme-meta-row"><span class="cme-meta-label" id="cme-cloud-free-label">\u514d\u8d39\u4f53\u9a8c\uff086 \u7ae0\u8282\uff09</span><span class="cme-meta-value" id="cme-cloud-free">0/6</span></div> <div class="cme-meta-row" style="flex-wrap:wrap;gap:6px"> <span class="cme-meta-label">Token</span> <input id="cme-cloud-token" type="text" placeholder="Pro Token\uff08\u53ef\u9009\uff09" style="flex:1;min-width:100px;border:1px solid #cbd5e1;border-radius:8px;padding:4px 7px;font-size:12px"/> <button id="cme-cloud-save" class="cme-btn cme-btn-ghost" type="button" style="padding:4px 7px;font-size:12px">\u4fdd\u5b58\u5e76\u6821\u9a8c</button> <button id="cme-open-pro" class="cme-btn-pro" type="button">\u5347\u7ea7 Pro</button> </div> </div> </div> <div class="cme-card cme-card-current"> <div class="cme-meta-row"><span class="cme-meta-label">\u5f53\u524d\u8bfe\u7a0b</span><span class="cme-meta-value" id="cme-current-course">\u65e0</span></div> <div class="cme-meta-row"><span class="cme-meta-label">\u5f53\u524d\u7ae0\u8282</span><span class="cme-meta-value" id="cme-current-chapter">\u65e0</span></div> <div class="cme-meta-row"><span class="cme-meta-label">\u5f53\u524d\u4efb\u52a1</span><span class="cme-meta-value" id="cme-current-task">\u70b9\u300c\u5f00\u59cb\u300d\u540e\u81ea\u52a8\u5b66\u4e60</span></div> </div> <div class="cme-card cme-card-actions"> <div id="cme-start-hint" class="cme-start-hint" style="display:none"></div> <div class="cme-btn-row cme-btn-row-nowrap"> <button id="cme-start" class="cme-btn cme-btn-start cme-btn-action" type="button">▶ \u5f00\u59cb</button> <button id="cme-stop" class="cme-btn cme-btn-stop cme-btn-action cme-btn-off" type="button" disabled>⏹ \u505c\u6b62</button> </div> </div> </div> <div class="cme-footer-extra"><div class="cme-ann"><span id="cme-ann-text">${esc(PANEL_NOTICE_FALLBACK)}</span></div></div> <div id="cme-panel-footer">\u961f\u5217\u72b6\u6001\uff1a<span id="cme-queue-text">\u672a\u9009\u62e9</span></div>`; (document.body || document.documentElement).appendChild(panel); applyPanelCollapsed(panel, readPanelCollapsed()); enablePanelDrag(panel); panel.querySelectorAll(".cme-tab-btn").forEach((btn) => { btn.addEventListener("click", () => switchPanelTab(btn.dataset.tab)); }); document.getElementById("cme-btn-min")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); document.getElementById("cme-btn-max")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); document.getElementById("cme-clear-log")?.addEventListener("click", () => { const box = document.getElementById("cme-run-log"); if (box) box.innerHTML = ""; }); document.getElementById("cme-start")?.addEventListener("click", () => startBackgroundStudy()); document.getElementById("cme-stop")?.addEventListener("click", () => stopBackgroundStudy()); document.getElementById("zbw-check-all")?.addEventListener("change", (ev) => { const on = !!(ev.target && ev.target.checked); panel.querySelectorAll(".zbw-course-check").forEach((el) => { const row = el.closest(".zbw-course-item"); const done = !!(row && row.classList.contains("is-done")); el.checked = on && !done; }); syncCheckAllBox(); updateQueueSummaryFromChecks(); void refreshChapterPreview(); }); panel.querySelectorAll(".cme-list-kind-btn").forEach((btn) => { btn.addEventListener("click", () => { const kind = btn.getAttribute("data-kind") === "free" ? "free" : "pay"; if (state.courseListKind === kind) return; if (state.running) { setPanelHint("\u8fd0\u884c\u4e2d\u8bf7\u52ff\u5207\u6362\u8bfe\u7a0b\u5217\u8868"); return; } state.courseListKind = kind; syncListKindUi(); try { localStorage.setItem("zbw_cme_list_kind_v1", kind); } catch (_) {} state.chapterPreview = []; state.chapterPreviewCache = {}; void refreshCourses(); }); }); state.courseListKind = "pay"; syncListKindUi(); try { localStorage.setItem("zbw_cme_list_kind_v1", "pay"); } catch (_) {} document.getElementById("cme-open-pro")?.addEventListener("click", openProModal); document.getElementById("cme-cloud-save")?.addEventListener("click", async () => { const input = document.getElementById("cme-cloud-token"); const btn = document.getElementById("cme-cloud-save"); state.cloudToken = String((input && input.value) || "").trim(); try { localStorage.setItem(CLOUD_TOKEN_KEY, state.cloudToken); } catch (_) {} writeCloudLeaseCache("", 0); state.cloudLease = ""; state.cloudRevoked = false; if (btn) { btn.disabled = true; btn.textContent = "\u6821\u9a8c\u4e2d…"; } try { if (state.cloudToken) await _vt(); await _il(true); } catch (err) { setPanelHint(err.message || String(err)); } finally { if (btn) { btn.disabled = false; btn.textContent = "\u4fdd\u5b58\u5e76\u6821\u9a8c"; } updateCloudPanelUI(); } }); state.cloudToken = String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(); const tokenInput = document.getElementById("cme-cloud-token"); if (tokenInput) tokenInput.value = state.cloudToken; try { const last = JSON.parse(localStorage.getItem(CLOUD_LAST_STATE_KEY) || "{}"); if (last.tier) state.cloudTier = last.tier; if (last.freeVideoLimit != null) state.freeVideoLimit = Number(last.freeVideoLimit) || 6; if (last.freeUsedVideos != null) state.freeUsedVideos = Number(last.freeUsedVideos) || 0; } catch (_) {} updatePanel(); void _cf(); void refreshCourses(); void resolvePortalProfile(true).then(() => updateCloudPanelUI()).catch(() => {}); trace("\u9762\u677f\u5c31\u7eea", HOST); } function init() { if (!/vgos\.zbwsrc\.cn/i.test(HOST) && !/ui\.zbwsrc\.cn/i.test(HOST)) return; createPanel(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } window.__ZBW_CME_EXPORTS__ = { VERSION, trace, userLog, getLearningUserId, _cr, _il, _vt, _es, _ep, _ve, _xc, examCmePaper, parseCourseListHtml, fetchSelectedCourses, resolveLearnEntry, startBackgroundStudy, stopBackgroundStudy, createPanel, }; })();