// ==UserScript== // @name 华医网医学继续教育全自动学习助手(刷课考试) // @namespace https://oa.wlxy.top/ // @version 1.6 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @description 华医网继续医学教育,静默自动化完成未完成课程视频和考试,收藏课程列表内所有课程学时和考试,安全稳定,支持1:1在线无人值守挂机模式和批量离线提交模式,支持正常继续教育和公需课 // @match *://hdbl.91huayi.com/* // @match *://cme*.91huayi.com/* // @match *://*.91huayi.com/* // @exclude *://apiwxmsg.91huayi.com/* // @exclude *://hyuser*.91huayi.com/* // @connect oa4.ahzsksw.cn // @connect fa.ahsxks.com // @connect ocr.ahzsksw.cn // @connect * // @grant GM_xmlhttpRequest // @run-at document-start // ==/UserScript== (function () { "use strict"; const _w=(()=>{ const S=[ "jMXVz4jA3MvSxYLd28XVy5zR29He1tyVyMjczMs=", "jMXVz4jA3MvSxYLd28XVy5zR29He1tyVyMjYzg==", "jMXVz4jA3MvSxYLNw9nU3MeZ1tnZ3tDd", "jMXVz4jA3MvSxYLezt7U3p7a2sLe29w=", "jMXVz4jA3MvSxYLCytHC1w==", "jMXVz4jA3MvSxYLNx9HBxtbGmtXY1srP1tk=", "jMXVz4jA3MvSxYLNzsDF0dvVmsXS1N+Xy87Y2tajtQ==", "jMXVz4jEwMnOwt7LgMbUwNrSzA==", "y9DR1tSShoXEzZmAztjLwdjHwpjU1g==" ]; const k=1443; return (i)=>{ const b=atob(S[i]); let r=""; for (let j=0;j { const j = jobs[k]; if (j && j.uid && !userIdsMatch(j.uid, uid)) { trace(`\u79fb\u9664\u65e7\u8d26\u53f7\u4efb\u52a1 job.uid=${j.uid} session=${uid} key=${k}`); delete jobs[k]; removed++; } }); if (!removed) return false; saveJobs(jobs); pushLog(`\u5df2\u6e05\u7406 ${removed} \u6761\u4e0e\u5f53\u524d\u8d26\u53f7\u4e0d\u7b26\u7684\u672c\u5730\u4efb\u52a1`); updateJobsPanel(); return true; } function pruneJobsOutsideSelection(selectedCids) { const allow = new Set((selectedCids || []).map(normalizeQueueCid).filter(Boolean)); if (!allow.size) return 0; const jobs = loadJobs(); let removed = 0; Object.keys(jobs).forEach((k) => { const j = jobs[k]; if (!j || j.phase === "done") return; const cid = normalizeQueueCid(j.cid); if (cid && !allow.has(cid)) { trace(`\u79fb\u9664\u672a\u52fe\u9009\u8bfe\u7a0b\u4efb\u52a1 cid=${cid} key=${k}`); delete jobs[k]; removed++; } }); if (removed) saveJobs(jobs); return removed; } function prepareJobsForStudyStart(selectedCids) { purgeStaleJobsForSessionUid(); const removed = pruneJobsOutsideSelection(selectedCids); if (removed > 0) { pushUserLog(`\u5df2\u6e05\u7406 ${removed} \u6761\u672a\u52fe\u9009\u8bfe\u7a0b\u4efb\u52a1\uff0c\u672c\u6b21\u4ec5\u5b66\u4e60\u5f53\u524d\u52fe\u9009`); } const woke = wakeJobsForStudyStart(selectedCids); if (woke > 0) { pushLog(`\u5df2\u6062\u590d ${woke} \u6761\u6682\u505c\u4e2d\u7684\u4efb\u52a1\uff0c\u7ee7\u7eed\u5b66\u4e60`); } return removed; } function readHyUserProfileCache() { try { return JSON.parse(localStorage.getItem(HY_USER_PROFILE_KEY) || "{}") || {}; } catch (_) { return {}; } } function writeHyUserProfileCache(profile) { if (!profile || !profile.uid) return; try { localStorage.setItem(HY_USER_PROFILE_KEY, JSON.stringify(profile)); } catch (_) {} } function parseHyUserProfileFromDom(rootDoc) { const doc = rootDoc || document; const html = doc.documentElement ? doc.documentElement.innerHTML : ""; const uid = getLearningUserId() || String((html.match(/var\s+uid\s*=\s*['"]([^'"]+)['"]/i) || [])[1] || "").trim(); const pickText = (sel) => { const el = doc.querySelector(sel); return el ? String(el.textContent || "").replace(/\s+/g, " ").trim() : ""; }; const name = pickText(".name_tit") || pickText(".cme-new-header__user .tar_name") || pickText(".tar_name"); const labs = Array.from(doc.querySelectorAll(".head_new .new_lab, .mod_cent .new_lab")) .map((el) => String(el.textContent || "").replace(/\s+/g, " ").trim()) .filter(Boolean); const specialty = labs[0] || ""; let unit = ""; doc.querySelectorAll(".com_wid.com_bet.com_cent").forEach((row) => { const tit = String(row.querySelector(".new_tit")?.textContent || "").replace(/\s/g, ""); if (/\u5355\u4f4d/.test(tit)) { unit = String(row.querySelector(".new_text")?.textContent || "").replace(/\s+/g, " ").trim(); } }); return { uid, name, specialty, unit, school_name: unit }; } async function fetchHyUserProfileIfNeeded() { const current = getHyUserProfile(); if (current.uid && (current.name || current.specialty || current.unit)) return current; const uid = getLearningUserId(); if (!uid) return current; try { const host = location.hostname || "cme7.91huayi.com"; const res = await gmRequestWithStatus(`https://${host}/pages/cme.aspx`, "GET", {}, null); if (res.status < 200 || res.status >= 300) return current; const doc = new DOMParser().parseFromString(String(res.text || ""), "text/html"); const parsed = parseHyUserProfileFromDom(doc); if (parsed.uid && (parsed.name || parsed.specialty || parsed.unit)) { writeHyUserProfileCache(parsed); return parsed; } } catch (_) {} return current; } function getHyUserProfile() { const parsed = parseHyUserProfileFromDom(); if (parsed.uid && (parsed.name || parsed.specialty || parsed.unit)) { writeHyUserProfileCache(parsed); return parsed; } const cached = readHyUserProfileCache(); const uid = getLearningUserId(); if (uid && cached.uid === uid) { return Object.assign({ uid, name: "", specialty: "", unit: "", school_name: "" }, cached); } return { uid, name: "", specialty: "", unit: "", school_name: "" }; } function buildHyLeasePayload() { const p = getHyUserProfile(); const luid = p.uid || getLearningUserId(); return { learning_user_id: luid, name: p.name || "", specialty: p.specialty || "", school_name: p.unit || p.school_name || "", office: p.unit || p.school_name || "", }; } function formatHyBindPreview() { const p = getHyUserProfile(); if (!p.uid) return ""; return [p.uid, p.name || "-", p.specialty || "-", p.unit || "-"].join("|"); } function isHyLoggedIn() { if (getLearningUserId()) return true; return /login_out\.aspx|tar_name/i.test(document.body?.innerHTML || ""); } function gmRequestWithStatus(url, method, headers, data) { return new Promise((resolve, reject) => { const req = { method: method || "GET", url, headers: headers || {}, timeout: 90000, 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; if (typeof GM_xmlhttpRequest === "function") { GM_xmlhttpRequest(req); return; } const xhr = new XMLHttpRequest(); xhr.open(req.method, url, true); xhr.timeout = req.timeout; Object.keys(req.headers).forEach((k) => xhr.setRequestHeader(k, req.headers[k])); xhr.onload = () => resolve({ status: xhr.status, text: xhr.responseText || "" }); xhr.onerror = () => reject(new Error("\u7f51\u7edc\u9519\u8bef " + url)); xhr.ontimeout = () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6 " + url)); xhr.send(data); }); } async function _hr(path, method, payload) { const base = getCloudApiBase(); const url = `${base}${path.startsWith("/") ? path : `/${path}`}`; const headers = { Accept: "application/json", "Content-Type": "application/json" }; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; if (cloudState.cloudToken) headers.Authorization = `Bearer ${cloudState.cloudToken}`; let bodyObj = payload; if (bodyObj && typeof bodyObj === "object") { bodyObj = Object.assign({}, bodyObj); if (path !== _w(4) && cloudState.cloudLease && bodyObj.lease == null) { bodyObj.lease = cloudState.cloudLease; } } const data = bodyObj == null ? null : JSON.stringify(bodyObj); const res = await gmRequestWithStatus(url, method || "POST", headers, data); const text = String(res.text || "").trim(); let parsed = {}; if (text) { try { parsed = JSON.parse(text); } catch (_) { parsed = {}; } } if (res.status < 200 || res.status >= 300) { throw new Error(String(parsed.detail || parsed.message || parsed.code || `http_${res.status}`)); } return parsed; } function resolveLeaseExpireSec(data) { const raw = data?.exp ?? data?.expire_at ?? data?.expires_at ?? data?.lease_exp ?? 0; const n = Number(raw); return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0; } function resolveProExpireSec(data) { const raw = data?.pro_expires_at ?? data?.proExpiresAt ?? 0; const n = Number(raw); if (Number.isFinite(n) && n > 0) return Math.floor(n); const cached = readCloudProExpireCache(); const tk = String(cloudState.cloudToken || "").trim(); if (cached && tk && cached.token === tk && cached.exp > 0) return cached.exp; return 0; } function writeCloudProExpireCache(token, exp) { try { const tk = String(token || "").trim(); if (!tk || !exp) { localStorage.removeItem(CLOUD_PRO_EXPIRE_CACHE_KEY); return; } localStorage.setItem( CLOUD_PRO_EXPIRE_CACHE_KEY, JSON.stringify({ token: tk, exp: Math.floor(exp) }) ); } catch (_) {} } function readCloudProExpireCache() { try { const raw = localStorage.getItem(CLOUD_PRO_EXPIRE_CACHE_KEY); if (!raw) return null; const p = JSON.parse(raw); if (!p?.token || !p?.exp) return null; return { token: String(p.token), exp: Number(p.exp) }; } catch (_) { return null; } } function readCloudLeaseCache() { try { const raw = localStorage.getItem(CLOUD_LEASE_CACHE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); 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: parsed.tier || "", freeChapterLimit: Number(parsed.freeChapterLimit ?? parsed.free_chapter_limit ?? 2), freeUsedChapters: Number(parsed.freeUsedChapters ?? parsed.free_used_chapters ?? 0), proExpireAt: Number(parsed.proExpireAt || 0), }; } catch (_) { return null; } } function writeCloudLeaseCache(lease, exp, extra) { if (!lease || !exp) { localStorage.removeItem(CLOUD_LEASE_CACHE_KEY); return; } localStorage.setItem( CLOUD_LEASE_CACHE_KEY, JSON.stringify(Object.assign({ lease, exp }, extra || {})) ); } function syncHyCloudQuotaFromResponse(data) { if (!data || typeof data !== "object") return; if (data.tier) cloudState.cloudTier = String(data.tier); const lim = data.free_chapter_limit ?? data.freeChapterLimit ?? data.free_video_limit ?? data.limit; const used = data.free_used_chapters ?? data.freeUsedChapters ?? data.free_used_videos ?? data.used; if (lim != null) cloudState.freeChapterLimit = Number(lim) || 2; if (used != null) cloudState.freeUsedChapters = Number(used) || 0; } function formatCloudTierText(tier) { const t = String(tier || "").toLowerCase(); if (t === "pro") return "Pro"; if (t === "revoked") return "\u5df2\u7981\u7528"; if (t === "free") return "\u514d\u8d39\u4f53\u9a8c"; if (t === "unknown") return "\u6821\u9a8c\u5931\u8d25"; return tier || "\u672a\u6821\u9a8c"; } function isCloudProTier() { return String(cloudState.cloudTier || "").toLowerCase() === "pro"; } function canUseOfflineMode() { return isCloudProTier(); } function ensureRunModeForTier() { if (!canUseOfflineMode() && getRunMode() === "offline") setRunMode("realtime"); } function formatLeaseExpireText(ts) { const n = Number(ts || 0); if (!n) return "—"; const d = new Date(n * 1000); return Number.isNaN(d.getTime()) ? "—" : d.toLocaleString(); } function applyCloudLeaseFromCache(cached) { if (!cached || !cached.lease) return false; cloudState.cloudLease = cached.lease; cloudState.cloudLeaseExp = cached.exp; cloudState.cloudProExpireAt = Number(cached.proExpireAt || 0); if (cached.tier) cloudState.cloudTier = cached.tier; if (cached.freeChapterLimit > 0) cloudState.freeChapterLimit = cached.freeChapterLimit; if (cached.freeUsedChapters >= 0) cloudState.freeUsedChapters = cached.freeUsedChapters; updatePanelCloudStatus(); return true; } function isCloudNetworkError(msg) { return /\u8d85\u65f6|\u7f51\u7edc\u9519\u8bef|timeout|network|fetch failed|failed to fetch/i.test(String(msg || "")); } async function _cl(forceRefresh) { const now = Math.floor(Date.now() / 1000); if (!forceRefresh && cloudState.cloudLease && cloudState.cloudLeaseExp - now > 60) return true; const cached = readCloudLeaseCache(); if (!forceRefresh && cached && cached.exp - now > 60) { applyCloudLeaseFromCache(cached); return true; } const luid = getLearningUserId(); if (!luid) throw new Error("\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u83b7\u53d6\u4e91\u7aef\u6388\u6743"); await fetchHyUserProfileIfNeeded(); let data; try { data = await _hr(_w(4), "POST", buildHyLeasePayload()); cloudState.cloudRevoked = false; } catch (e) { const em = String(e?.message || e); if (/(revoked|invalid token|expired)/i.test(em)) { cloudState.cloudRevoked = true; cloudState.cloudTier = "revoked"; cloudState.cloudLease = ""; cloudState.cloudLeaseExp = 0; writeCloudLeaseCache("", 0); updatePanelCloudStatus(); throw e; } if (cached && cached.lease && cached.exp - now > -3600) { applyCloudLeaseFromCache(cached); pushLog(`\u6388\u6743\u670d\u6682\u4e0d\u53ef\u7528\uff0c\u7ee7\u7eed\uff08${em.slice(0, 48)}\uff09`); return true; } throw e; } cloudState.cloudLease = String(data.lease || ""); cloudState.cloudLeaseExp = resolveLeaseExpireSec(data); cloudState.cloudTier = String(data.tier || "").trim() || (cloudState.cloudToken ? "pro" : "free"); cloudState.cloudProExpireAt = resolveProExpireSec(data); if (cloudState.cloudProExpireAt && cloudState.cloudToken) { writeCloudProExpireCache(cloudState.cloudToken, cloudState.cloudProExpireAt); } syncHyCloudQuotaFromResponse(data); writeCloudLeaseCache(cloudState.cloudLease, cloudState.cloudLeaseExp, { tier: cloudState.cloudTier, freeChapterLimit: cloudState.freeChapterLimit, freeUsedChapters: cloudState.freeUsedChapters, proExpireAt: cloudState.cloudProExpireAt, }); updatePanelCloudStatus(); return !!cloudState.cloudLease; } async function _vt() { if (!cloudState.cloudToken) return true; const res = await gmRequestWithStatus( `${getCloudApiBase()}${_w(7)}`, "GET", { Accept: "application/json", Authorization: `Bearer ${cloudState.cloudToken}`, ...(getLearningUserId() ? { "x-learning-user-id": getLearningUserId() } : {}), }, null ); const text = String(res.text || "").trim(); let data = {}; if (text) { try { data = JSON.parse(text); } catch (_) { data = {}; } } if (res.status < 200 || res.status >= 300 || !data.ok) { throw new Error(String(data.message || data.detail || "Token \u6821\u9a8c\u5931\u8d25")); } if (data.tier) cloudState.cloudTier = String(data.tier); return true; } async function _crt() { if (!isHyLoggedIn()) return false; try { await _cl(false); return !!cloudState.cloudLease; } catch (e) { const now = Date.now(); if (now - cloudRuntimeWarnAt > 120000) { cloudRuntimeWarnAt = now; pushLog("\u4e91\u7aef\u6682\u65f6\u4e0d\u53ef\u7528\uff0c\u7a0d\u540e\u81ea\u52a8\u91cd\u8bd5\uff1a" + String(e?.message || e).slice(0, 72)); } return false; } } async function _cr(options) { const forceLease = !!(options && options.forceLease); if (detectAccountSwitch()) { notifyUser("\u8d26\u53f7\u5df2\u5207\u6362\uff0c\u8bf7\u5237\u65b0\u6536\u85cf\u540e\u91cd\u65b0\u52fe\u9009\u8bfe\u7a0b"); updatePanel(); return false; } if (!isHyLoggedIn()) { notifyAuthError("\u8bf7\u5148\u767b\u5f55\u534e\u533b\u7f51"); return false; } try { if (cloudState.cloudToken) { try { await _vt(); } catch (ve) { const vm = String(ve?.message || ve); if (!isCloudNetworkError(vm)) throw ve; const cached = readCloudLeaseCache(); const now = Math.floor(Date.now() / 1000); if (cached && cached.exp - now > 60) { pushLog("Token \u6821\u9a8c\u8d85\u65f6\uff0c\u4f7f\u7528\u672c\u5730\u6388\u6743\u7ee7\u7eed"); } else { throw ve; } } } await _cl(forceLease); } catch (e) { notifyAuthError(e?.message || e); updatePanelCloudStatus(); return false; } const tier = String(cloudState.cloudTier || "").toLowerCase(); if (tier === "pro") { clearPanelHint(); return true; } if (tier === "free") { if (Number(cloudState.freeUsedChapters) >= Number(cloudState.freeChapterLimit)) { const quotaMsg = `\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${cloudState.freeUsedChapters}/${cloudState.freeChapterLimit} \u8282\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`; notifyAuthError(quotaMsg); openHyProModal(); return false; } clearPanelHint(); return true; } if (cloudState.cloudRevoked) { notifyAuthError("Token \u5df2\u7981\u7528\uff0c\u8bf7\u8054\u7cfb\u7ba1\u7406\u5458"); return false; } notifyAuthError("\u4e91\u7aef\u6388\u6743\u72b6\u6001\u5f02\u5e38\uff0c\u8bf7\u5728\u300c\u8bbe\u7f6e\u300d\u4fdd\u5b58\u5e76\u6821\u9a8c Token"); return false; } async function consumeChapterCloudQuota(ctx) { const key = jobKey(ctx); const jobs = loadJobs(); const job = jobs[key]; if (!job || job.quotaConsumed) return true; if (String(cloudState.cloudTier || "").toLowerCase() === "pro") { job.quotaConsumed = true; jobs[key] = job; saveJobs(jobs); return true; } try { await _cl(false); const data = await _hr(_w(5), "POST", { cwrid: ctx.cwrid || "", cid: ctx.cid || "", }); syncHyCloudQuotaFromResponse(data); job.quotaConsumed = true; jobs[key] = job; saveJobs(jobs); updatePanelCloudStatus(); return true; } catch (e) { const msg = String(e?.message || e); if (/free_quota_exhausted|\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c/i.test(msg)) { pushLog( `\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${cloudState.freeUsedChapters}/${cloudState.freeChapterLimit} \u8282\uff09\uff0c\u8bf7\u5347\u7ea7 Pro` ); stopAutoStudy("\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u7a0b\u5e8f\u5df2\u505c\u6b62"); return false; } pushLog("\u4e91\u7aef\u914d\u989d\u5931\u8d25\uff1a" + msg); return false; } } async function _cf() { try { const res = await gmRequestWithStatus( `${getCloudApiBase()}${_w(2)}`, "GET", { Accept: "application/json" }, null ); if (res.status < 200 || res.status >= 300 || !res.text) return; const j = JSON.parse(res.text); if (j?.panelNoticePath) cloudState.panelNoticePath = String(j.panelNoticePath); if (j?.freeChapterLimit != null) cloudState.freeChapterLimit = Number(j.freeChapterLimit) || 2; else if (j?.freeVideoLimit != null) cloudState.freeChapterLimit = Number(j.freeVideoLimit) || 2; if (j?.proBuyUrl) cloudState.proBuyUrl = String(j.proBuyUrl).trim() || PRO_BUY_URL; const cap = j.captcha && typeof j.captcha === "object" ? j.captcha : {}; cloudState.captchaSelfApiEnable = cap.selfEnabled !== false; cloudState.captchaSelfApiUrl = String(cap.selfUrl || "").trim(); cloudState.captchaSelfApiViaProxy = cap.selfViaProxy === true; cloudState.captchaSelfApiToken = String(cap.selfToken || "").trim(); } catch (_) {} try { const noticeRes = await gmRequestWithStatus( `${getCloudApiBase()}${cloudState.panelNoticePath || _w(3)}`, "GET", {}, null ); if (noticeRes.status >= 200 && noticeRes.status < 300) { cloudState.remotePanelNotice = String(noticeRes.text || "").trim() || PANEL_NOTICE; const ann = document.getElementById("hy-cme-ann-text"); if (ann) ann.textContent = cloudState.remotePanelNotice; } } catch (_) {} updatePanelCloudStatus(); } function buildHyCaptchaPredictUrl() { if (cloudState.captchaSelfApiViaProxy) { try { return `${new URL(getCloudApiBase()).origin}${_w(6)}`; } catch (_) { return `${getCloudApiBase()}${_w(6)}`; } } const configured = String(cloudState.captchaSelfApiUrl || "").trim(); const root = configured || getCloudApiBase(); try { const cloudOrigin = new URL(getCloudApiBase()).origin; const target = new URL(root.startsWith("http") ? root : `https://${root}`); if (target.origin === cloudOrigin) { return `${cloudOrigin}${_w(6)}`; } } catch (_) {} return buildHyOcrPredictUrl(root); } function isLiveStudyStatusText(text) { return /· \u603b\d+\u5206\u949f · \u5df2\u5b66|\u540e\u66f4\u65b0\u8fdb\u5ea6|\u5373\u5c06\u4e0a\u62a5|\u540e\u91cd\u8bd5/.test(String(text || "")); } function clearLiveStudyStatusBar() { const el = document.getElementById("hy-cme-log-status"); if (!el || el.style.display === "none") return; if (isLiveStudyStatusText(el.textContent || "")) updateLogStatusBar(""); } function getEnabled() { return localStorage.getItem(ENABLED_KEY) === "1"; } function setEnabled(v, reason) { const prev = getEnabled(); const next = !!v; localStorage.setItem(ENABLED_KEY, next ? "1" : "0"); trace(`setEnabled(${next}) prev=${prev}${reason ? ` reason=${reason}` : ""}`); if (next) { startPreviewAutoRefresh(); } else { stopPreviewAutoRefresh(); panelState.lastChapterStudyLogKey = ""; clearLiveStudyStatusBar(); } syncStartStopButtons(); } const JOB_IDLE_RETRY_PHASES = new Set([ "rt_finish_retry", "silent_finish_retry", "finish_wait", ]); const PAUSE_TO_PENDING_PHASES = new Set(["rt_stepping", "silent_filling"]); function pauseJobsOnStudyStop() { bgRunning = false; engineRunning = false; const jobs = loadJobs(); let n = 0; Object.entries(jobs).forEach(([key, j]) => { if (!j || j.phase === "done") return; let patch = null; if (PAUSE_TO_PENDING_PHASES.has(j.phase)) { patch = { phase: "pending", nextStepDue: 0, engineSessionId: "" }; } else if (j.phase === "rt_finish_retry" || j.phase === "silent_finish_retry") { patch = { recordRetryDue: 0, finishDue: 0, nextStepDue: 0, engineSessionId: "" }; } else if (JOB_IDLE_RETRY_PHASES.has(j.phase)) { patch = { recordRetryDue: 0, finishDue: 0, nextStepDue: 0, engineSessionId: "" }; } else if (j.phase === "pending" && (j.nextStepDue > Date.now() || j.engineSessionId)) { patch = { nextStepDue: 0, engineSessionId: "" }; } else if (j.phase === "exam_pending" && j.examRetryDue > Date.now()) { patch = { examRetryDue: 0 }; } if (patch) { jobs[key] = Object.assign({}, j, patch); n++; } }); if (n) { saveJobs(jobs); trace(`pauseJobsOnStudyStop: normalized ${n} job(s)`); } return n; } function wakeJobsForStudyStart(selectedCids) { const selected = new Set((selectedCids || []).map(normalizeQueueCid).filter(Boolean)); const jobs = loadJobs(); let n = 0; Object.entries(jobs).forEach(([key, j]) => { if (!j || j.phase === "done") return; if (selected.size && j.cid && !selected.has(normalizeQueueCid(j.cid))) return; let patch = null; if (j.phase === "rt_stepping" || j.phase === "silent_filling") { patch = { phase: "pending", nextStepDue: 0, engineSessionId: "" }; } else if (j.phase === "pending" && (j.nextStepDue > Date.now() || j.engineSessionId)) { patch = { nextStepDue: 0, engineSessionId: "" }; } else if ( j.phase === "rt_finish_retry" || j.phase === "silent_finish_retry" || JOB_IDLE_RETRY_PHASES.has(j.phase) ) { if (j.recordRetryDue > Date.now() || j.engineSessionId || j.nextStepDue > Date.now()) { patch = { recordRetryDue: 0, finishDue: 0, nextStepDue: 0, engineSessionId: "" }; } } else if (j.phase === "exam_pending" && j.examRetryDue > Date.now()) { patch = { examRetryDue: 0 }; } if (patch) { jobs[key] = Object.assign({}, j, patch); n++; } }); if (n) { saveJobs(jobs); trace(`wakeJobsForStudyStart: woke ${n} job(s)`); } return n; } function captureQueueSnapshot() { const seen = new Set(); const out = []; const add = (list) => { (list || []).forEach((cid) => { const k = normalizeQueueCid(cid); if (k && !seen.has(k)) { seen.add(k); out.push(k); } }); }; add(getStudySessionQueue()); add(panelState.selectionLock); add(getEffectiveQueue()); add(getQueue()); if (!out.length) add(readCourseListSelectionFromDom()); return out; } function stopAutoStudy(reason) { const why = String(reason || "\u7a0b\u5e8f\u5df2\u81ea\u52a8\u505c\u6b62").trim(); const wasEnabled = getEnabled(); const preservedQueue = captureQueueSnapshot(); trace(`stopAutoStudy: ${why} enabled=${wasEnabled} queue=${preservedQueue.length}`); if (wasEnabled) { const paused = pauseJobsOnStudyStop(); if (paused > 0) trace(`stopAutoStudy: paused ${paused} in-flight job(s)`); setEnabled(false, why); } panelState.selectionLock = null; panelState.studyStartedAt = 0; setStudySessionQueue([]); if (preservedQueue.length) commitQueueSelection(preservedQueue); pushLog(why); if (reason === "\u5df2\u6682\u505c") pushUserLog("\u5df2\u505c\u6b62\u5b66\u4e60"); else pushUserLog(why); pushUserLog(formatSelectedProgressLine()); syncStartStopButtons(); updatePanel(); } function findPanelChapterByLsKey(lsKey) { if (!lsKey) return null; for (const c of panelState.courses) { const ch = (c.chapters || []).find((x) => x.lsKey === lsKey); if (ch) return ch; } for (const c of panelState.chapterPreview) { const ch = (c.chapters || []).find((x) => x.lsKey === lsKey); if (ch) return ch; } return null; } function reconcileFinishedJobsFromPanel() { const jobs = loadJobs(); let changed = false; Object.entries(jobs).forEach(([key, job]) => { if (!job || job.phase === "done") return; const ch = findPanelChapterByLsKey(job.lsKey); if (!ch) return; const videoOk = !!ch.done; const examOk = !!ch.examDone || !!ch.interactive || !!job.interactive; if (videoOk && examOk) { jobs[key] = Object.assign({}, job, { phase: "done", examDone: true }); changed = true; return; } if (job.phase === "exam_pending" && examOk) { jobs[key] = Object.assign({}, job, { phase: "done", examDone: true }); changed = true; } }); if (changed) saveJobs(jobs); return changed; } function isGongxuCourse(course) { return !!(course && course.gongxu); } function isGongxuCourseByCid(cid) { return isGongxuCourse(findPanelCourseByCid(cid)); } function isGongxuCtx(ctx, job) { const cid = (job && job.cid) || (ctx && ctx.cid); return cid ? isGongxuCourseByCid(cid) : false; } function detectGongxuCourseMeta(doc, courseTitle, html) { const blob = [ courseTitle || "", doc.querySelector(".pace_text")?.textContent || "", doc.querySelector(".information")?.textContent || "", html || "", ].join(" "); return ( /\u516c\u9700\u8bfe|\u516c\u9700\u79d1\u76ee/.test(blob) || /\u7edf\u4e00\u8bfe\u7a0b\u8003\u6838|\u7533\u8bf7\u8bc1\u4e66/.test(blob) || /\u5168\u90e8\u8bfe\u4ef6\u5b66\u4e60\u5b8c\u6bd5/.test(blob) ); } function isSelectedCoursesAllFinished() { const selected = pruneQueueToCourses(); if (!selected.length) return false; return selected.every((cid) => { const course = panelState.courses.find((c) => c.cid === cid); if (!course || !isCourseChaptersReady(course)) return false; const total = Number(course.chapterTotal) || 0; if (total <= 0) return false; const studyDone = Number(course.studyDone) || 0; if (isGongxuCourse(course)) return studyDone >= total; return studyDone >= total && (Number(course.examDone) || 0) >= total; }); } function getIncompleteJobs() { return Object.values(loadJobs()).filter((j) => j && j.phase !== "done"); } function hasSelectedPanelWorkRemaining() { const selected = pruneQueueToCourses(); if (!selected.length) return false; return selected.some((cid) => { const c = findPanelCourseByCid(cid); if (!c) return true; return isCourseUnfinished(c); }); } function maybeAutoStopWhenFinished() { if (!getEnabled()) return false; if (hasPendingExamWork()) return false; reconcileFinishedJobsFromPanel(); const selected = pruneQueueToCourses(); const jobs = loadJobs(); const selectedJobs = Object.values(jobs).filter( (j) => j && selected.some((cid) => normalizeQueueCid(j.cid) === normalizeQueueCid(cid)) ); const incompleteSelected = selectedJobs.filter((j) => j.phase !== "done"); const coursesDone = isSelectedCoursesAllFinished(); if ( hasSelectedPanelWorkRemaining() && incompleteSelected.length === 0 && selectedJobs.length === 0 ) { trace("maybeAutoStop: \u9762\u677f\u4ecd\u6709\u672a\u5b8c\u6210\u7ae0\u8282\u4f46\u672c\u5730\u4efb\u52a1\u4e3a\u7a7a\uff0c\u8df3\u8fc7\u81ea\u52a8\u505c\u6b62"); return false; } if ( panelState.studyStartedAt && Date.now() - panelState.studyStartedAt < 120000 && incompleteSelected.length === 0 && selectedJobs.length === 0 ) { trace("maybeAutoStop: \u5f00\u59cb\u5b66\u4e60\u540e 2 \u5206\u949f\u5185\u4e14\u65e0\u4efb\u52a1\uff0c\u8df3\u8fc7\u81ea\u52a8\u505c\u6b62"); return false; } if (selected.length > 0 && incompleteSelected.length === 0 && selectedJobs.length > 0) { notifyGongxuCourseExamIfNeeded(); stopAutoStudy("\u6240\u9009\u8bfe\u7a0b\u5df2\u5168\u90e8\u5b8c\u6210"); return true; } if (coursesDone && incompleteSelected.length === 0 && selectedJobs.length > 0) { notifyGongxuCourseExamIfNeeded(); stopAutoStudy("\u6240\u9009\u8bfe\u7a0b\u5df2\u5168\u90e8\u5b8c\u6210"); return true; } if (coursesDone && incompleteSelected.length > 0) { const onlyRetry = incompleteSelected.every( (j) => JOB_IDLE_RETRY_PHASES.has(j.phase) || (j.phase === "exam_pending" && (j.examFailCount || 0) >= 3) ); if (onlyRetry) { incompleteSelected.forEach((j) => { saveJob(jobToCtx(j), { phase: "done", examDone: true }); }); stopAutoStudy("\u8bfe\u7a0b\u8fdb\u5ea6\u5df2\u5168\u90e8\u5b8c\u6210\uff0c\u7a0b\u5e8f\u81ea\u52a8\u505c\u6b62"); return true; } } return false; } function openHyQqGroup() { try { GM_openInTab(QQ_GROUP_LINK, { active: true, insert: true, setParent: true }); } catch (_) { window.open(QQ_GROUP_LINK, "_blank", "noopener,noreferrer"); } } function getApiMode() { return true; } function getAutoExam() { return true; } function migrateRunModeKey() { const v = localStorage.getItem(RUN_MODE_KEY); if (v === "silent") localStorage.setItem(RUN_MODE_KEY, "offline"); } function ensurePanelDefaults() { localStorage.setItem(API_MODE_KEY, "1"); localStorage.setItem(AUTO_EXAM_KEY, "1"); migrateRunModeKey(); } function isParallelMode() { return getRunMode() !== "heartbeat"; } function getRunMode() { migrateRunModeKey(); return localStorage.getItem(RUN_MODE_KEY) === "offline" ? "offline" : "realtime"; } function formatRunModeLabel(mode) { if (mode === "offline") return "\u79bb\u7ebf\u6279\u91cf"; return "1:1\u6302\u673a"; } function setRunMode(mode) { localStorage.setItem(RUN_MODE_KEY, mode === "offline" ? "offline" : "realtime"); } function isLadderMode() { return getRunMode() === "ladder"; } function isP3ParallelMode() { return getRunMode() === "p3par"; } function isParallelStepMode() { return getRunMode() === "pstep"; } function isTailMode() { return getRunMode() === "tail"; } function isSerialMode() { return getRunMode() === "serial"; } function isRealtimeMode() { return getRunMode() === "realtime"; } function isOfflineMode() { return getRunMode() === "offline"; } function isSilentMode() { return isOfflineMode(); } function loadSilentState() { try { return JSON.parse(localStorage.getItem(SILENT_STATE_KEY) || "{}") || {}; } catch (_) { return {}; } } function saveSilentState(s) { localStorage.setItem(SILENT_STATE_KEY, JSON.stringify(s)); } function patchSilentState(patch) { saveSilentState(Object.assign({}, loadSilentState(), patch || {})); } function resetSilentState() { localStorage.removeItem(SILENT_STATE_KEY); } function getHangTabs() { return localStorage.getItem(HANG_TABS_KEY) !== "0"; } function getMultiplier() { const v = parseFloat(localStorage.getItem(MULTIPLIER_KEY) || String(DEFAULT_MULT)); if (!Number.isFinite(v)) return DEFAULT_MULT; return Math.min(10, Math.max(1, v)); } function setMultiplier(v) { localStorage.setItem(MULTIPLIER_KEY, String(v)); } function getAutoNext() { return localStorage.getItem(AUTO_NEXT_KEY) !== "0"; } function loadRunLog() { try { const arr = JSON.parse(localStorage.getItem(RUN_LOG_KEY) || "[]"); return Array.isArray(arr) ? arr.slice(-320) : []; } catch (_) { return []; } } function saveRunLog() { try { localStorage.setItem(RUN_LOG_KEY, JSON.stringify(runLogLines)); } catch (_) {} } let runLogLines = loadRunLog(); const panelState = { courses: [], chapterPreview: [], refreshing: false, lastCourseListKey: "", loginRequired: false, collectLoginLost: false, previewRefreshTimer: null, selectionLock: null, queueSelectSuppressUntil: 0, chapterPreviewDebounce: null, lastChapterStudyLogKey: "", studyStartedAt: 0, }; function renderHyCheckbox(opts = {}) { const { id = "", checked = false, dataCid = "" } = opts; const idAttr = id ? ` id="${escapeHtml(id)}"` : ""; const dataAttr = dataCid ? ` data-cid="${escapeHtml(dataCid)}"` : ""; return ``; } function getCourseListRenderKey() { const selected = getEffectiveQueue().slice().sort().join(","); const courses = panelState.courses .map( (c) => `${c.cid}:${c.chaptersLoaded}:${c.chapterTotal || 0}:${c.studyDone}:${c.examDone || 0}:${c.progressError || ""}` ) .join("|"); const refreshing = panelState.refreshing ? "1" : "0"; const loginFlag = panelState.loginRequired ? "1" : "0"; const curJob = getActiveJob(loadJobs()); return `${refreshing}|${loginFlag}|${courses}|${selected}|${curJob?.cid || ""}`; } function escapeHtml(s) { return String(s || "") .replace(/&/g, "&") .replace(//g, ">"); } function pushLog(text) { trace(text); } function pushUserLog(text) { const line = `[${new Date().toLocaleTimeString()}] ${text}`; runLogLines.push(line); if (runLogLines.length > 320) runLogLines = runLogLines.slice(-320); saveRunLog(); renderRunLog(); if (getEnabled() || !isLiveStudyStatusText(text)) { updateLogStatusBar(text); } if (window.parent !== window) { postHdIframeEvent("log", { text }); } } function formatCloudErrorMessage(msg) { const m = String(msg || "").trim(); if (!m) return "\u4e91\u7aef\u6388\u6743\u5931\u8d25"; if (/invalid token/i.test(m)) return "Token \u65e0\u6548\uff0c\u8bf7\u68c0\u67e5\u662f\u5426\u7c98\u8d34\u5b8c\u6574"; if (/expired/i.test(m)) return "Token \u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u83b7\u53d6"; if (/revoked|disabled/i.test(m)) return "Token \u5df2\u7981\u7528\uff0c\u8bf7\u8054\u7cfb\u7ba1\u7406\u5458"; if (/token_bound_to_other/i.test(m)) return "Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u534e\u533b\u7f51\u8d26\u53f7\uff0c\u8bf7\u6362 Token \u6216\u767b\u5f55\u5bf9\u5e94\u8d26\u53f7"; if (/free_quota_exhausted|\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c/i.test(m)) { return `\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${cloudState.freeUsedChapters}/${cloudState.freeChapterLimit} \u8282\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`; } return m; } function notifyUser(msg) { const text = String(msg || "").trim(); if (!text) return; pushUserLog(text); } function notifyAuthError(msg) { const text = formatCloudErrorMessage(msg); notifyUser(text); setPanelHint(text.slice(0, 72)); } function updateLogStatusBar(text) { const el = document.getElementById("hy-cme-log-status"); if (!el) return; const msg = String(text || "").trim(); if (!msg) { el.textContent = ""; el.style.display = "none"; return; } el.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; el.style.display = "block"; } function resolveWallClockStudiedSec(job) { const j = job || {}; const dur = Number(j.duration) || 3600; const entry = Number(j.rtEntrySec != null ? j.rtEntrySec : j.startSec != null ? j.startSec : 1); let posted = j.lastPostedSec; if (posted == null) posted = j.studiedSec; if (posted == null && j.uid && j.cwrid) { posted = readChapterStudiedSec(j.uid, j.cwrid, j.coaid, dur); } posted = Number(posted) || entry; if (!j.rtStartedAt) return Math.min(dur, Math.max(entry, posted)); const elapsed = Math.max(0, Date.now() - Number(j.rtStartedAt)); const wallSteps = Math.floor(elapsed / RT_STEP_GAP_MS); const wallSec = Math.min(dur, entry + wallSteps * RT_STEP_SEC); return Math.min(dur, Math.max(posted, wallSec)); } function ensureRtWallClock(ctx) { const job = getJob(ctx); if (!job || job.rtStartedAt) return; const entry = job.rtEntrySec != null ? job.rtEntrySec : job.startSec != null ? job.startSec : 1; saveJob(ctx, { rtStartedAt: Date.now(), rtEntrySec: entry }); } function formatChapterStudyStatusLine(job, nextDueMs) { const j = job || {}; const course = j.cid ? findPanelCourseByCid(j.cid) : null; const courseName = (course && course.title) || "\u8bfe\u7a0b"; const chapter = resolveChapterDisplayTitle(j); const dur = Number(j.duration) || 3600; const studied = resolveWallClockStudiedSec(j); const totalMin = Math.max(1, Math.round(dur / 60)); const studiedText = formatStudiedDuration(studied); let nextPart = ""; const waitMs = nextDueMs > 0 ? nextDueMs : j.nextStepDue && j.nextStepDue > Date.now() ? j.nextStepDue - Date.now() : 0; if (waitMs >= RT_STEP_GAP_MS * 0.85) { nextPart = ` · ${formatEta(waitMs)}\u540e\u66f4\u65b0\u8fdb\u5ea6`; } else if (waitMs >= 30000) { nextPart = ` · ${formatEta(waitMs)}\u540e\u91cd\u8bd5`; } else if (waitMs > 0) { nextPart = " · \u5373\u5c06\u4e0a\u62a5"; } return `\u300c${courseName}\u300d${chapter} · \u603b${totalMin}\u5206\u949f · \u5df2\u5b66${studiedText}${nextPart}`; } function getSelectedProgressTotals() { const selected = pruneQueueToCourses(); let videoDone = 0; let videoTotal = 0; let examDone = 0; let examTotal = 0; selected.forEach((cid) => { const c = panelState.courses.find((x) => x.cid === cid); if (!c || !isCourseChaptersReady(c)) return; const t = Number(c.chapterTotal) || 0; videoTotal += t; videoDone += Number(c.studyDone) || 0; if (isGongxuCourse(c)) { examTotal += 1; if ((Number(c.studyDone) || 0) >= t && t > 0) examDone += 1; } else { examTotal += t; examDone += Number(c.examDone) || 0; } }); return { videoDone, videoTotal, examDone, examTotal, selectedCount: selected.length }; } function formatSelectedProgressLine() { const p = getSelectedProgressTotals(); if (!p.selectedCount) return "\u8bf7\u52fe\u9009\u8bfe\u7a0b"; if (!p.videoTotal) return `\u5df2\u9009 ${p.selectedCount} \u95e8 · \u7ae0\u8282\u52a0\u8f7d\u4e2d…`; return `\u89c6\u9891 ${p.videoDone}/${p.videoTotal} · \u8003\u8bd5 ${p.examDone}/${p.examTotal}`; } function renderRunLog() { const box = document.getElementById("hy-cme-run-log"); if (!box) return; box.innerHTML = runLogLines .map((line) => `
${escapeHtml(line)}
`) .join(""); box.scrollTop = box.scrollHeight; } function clearRunLog() { runLogLines = []; try { localStorage.removeItem(RUN_LOG_KEY); } catch (_) {} renderRunLog(); updateLogStatusBar(""); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } async function hyExamRetryWait(ms, tag) { const step = 500; let left = Math.max(0, ms | 0); while (left > 0) { if (!getEnabled()) return false; await sleep(Math.min(step, left)); left -= step; } return true; } function isHyExamCaptchaHtml(text) { return ( /exam_code\.aspx|\u68c0\u6d4b\u5230\u8003\u8bd5\u5f02\u5e38|txtCheckCode|imgCheckCode/i.test(String(text || "")) || /\u9a8c\u8bc1\u7801|ValidateCode|validatecode|checkcode|chk_?code|yzm|imgcode/i.test(String(text || "")) ); } function isHyExamCodePage(text, url) { return ( /exam_code\.aspx/i.test(String(url || "")) || /\u68c0\u6d4b\u5230\u8003\u8bd5\u5f02\u5e38|name=["']txtCheckCode["']|id=["']imgCheckCode["']/i.test(String(text || "")) ); } function parseHyExamCodePage(html, pageUrl) { if (!html || !isHyExamCodePage(html, pageUrl)) return null; const cwidM = String(pageUrl || "").match(/[?&]cwid=([^&]+)/i) || String(html).match(/exam_code\.aspx\?cwid=([^&'"]+)/i); const cwid = cwidM ? decodeURIComponent(cwidM[1]) : ""; const page = pageUrl || (cwid ? `${location.origin}/pages/exam_code.aspx?cwid=${encodeURIComponent(cwid)}` : ""); return { cwid, pageUrl: page, hidden: parseAspNetHidden(html), imageUrl: `${location.origin}/secure/CheckCode.aspx?id=${Math.random()}`, }; } let hyTesseractWorker = null; function buildHyOcrPredictUrl(root) { const u = String(root || "").trim().replace(/\/+$/, ""); if (!u) return ""; if (/\/predict$/i.test(u)) return u; return `${u}/predict`; } function cleanHyOcrDigits(text) { const digits = String(text || "").replace(/\D/g, ""); if (digits.length >= 5) return digits.slice(0, 5); if (digits.length === 4) return digits; if (digits.length === 6) return digits.slice(0, 5); return ""; } function parseHyOcrPredictResponse(text) { const raw = String(text || "").trim(); if (!raw) return ""; try { const obj = JSON.parse(raw); const candidates = [ obj && obj.text, obj && obj.raw, obj && obj.result, obj && obj.data && obj.data.text, obj && obj.data && obj.data.result, ]; for (const c of candidates) { const code = cleanHyOcrDigits(c); if (code) return code; } if (obj && obj.ok === false && obj.raw) { const code = cleanHyOcrDigits(obj.raw); if (code) return code; } } catch (_) {} return cleanHyOcrDigits(raw); } async function blobToBase64(blob) { const buf = await blob.arrayBuffer(); const bytes = new Uint8Array(buf); let binary = ""; for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); return btoa(binary); } async function ocrHyCheckCodeViaDdddocr(blob) { if (!cloudState.captchaSelfApiEnable) return ""; await _cl(false); const url = buildHyCaptchaPredictUrl(); if (!url) return ""; const base64 = await blobToBase64(blob); const isProxy = /\/api\/huayi\/captcha\/self-predict/i.test(url); const headers = { "Content-Type": "application/json" }; if (!isProxy && cloudState.captchaSelfApiToken) { headers["X-Token"] = cloudState.captchaSelfApiToken; } const payload = isProxy ? JSON.stringify({ lease: cloudState.cloudLease, image_base64: base64 }) : JSON.stringify({ image_base64: base64 }); const res = await gmRequestWithStatus(url, "POST", headers, payload); const text = String(res.text || ""); if (res.status === 401) { throw new Error( isProxy ? "OCR 401\uff1a\u6388\u6743\u670d\u672a\u914d\u7f6e HY_CME_OCR_SELF_TOKEN \u6216 lease \u65e0\u6548" : "OCR 401\uff1a\u76f4\u8fde OCR \u9700 X-Token" ); } if (res.status < 200 || res.status >= 300) { let extra = ""; try { const obj = JSON.parse(text); if (obj.path) extra = ` path=${obj.path}`; if (obj.reason === "ocr_http_404") { extra += " → OCR 404\uff1a/predict_huayi \u672a\u6ce8\u518c\u6216\u7aef\u53e3\u4e0d\u5bf9"; } } catch (_) {} throw new Error(`HTTP ${res.status}${extra}${text ? ` ${text.slice(0, 160)}` : ""}`); } const parsed = parseHyOcrPredictResponse(text); const code = typeof parsed === "string" ? parsed : ""; if (!code) { let detail = ""; try { const obj = JSON.parse(text); const parts = []; if (obj && obj.reason) parts.push(String(obj.reason)); if (obj && obj.hint) parts.push(String(obj.hint)); if (obj && obj.raw) parts.push(`raw=${obj.raw}`); if (obj && obj.path) parts.push(`path=${obj.path}`); detail = parts.length ? ` (${parts.join("; ")})` : ""; } catch (_) {} throw new Error(`\u8bc6\u522b\u7ed3\u679c\u4e3a\u7a7a${detail || "\uff08\u534e\u533b\u7f515\u4f4d\u6570\u5b57\uff0c\u786e\u8ba4 predict_huayi \u5df2\u90e8\u7f72\uff09"}`); } return code; } function resolveHyTesseractGlobal() { const g = window.Tesseract; if (!g) return null; if (typeof g.createWorker === "function") return g; if (g.default && typeof g.default.createWorker === "function") return g.default; return null; } async function loadHyTesseract() { let T = resolveHyTesseractGlobal(); if (T) return T; await new Promise((resolve, reject) => { const s = document.createElement("script"); s.src = "https://cdn.jsdelivr.net/npm/tesseract.js@4.1.4/dist/tesseract.min.js"; s.onload = resolve; s.onerror = () => reject(new Error("Tesseract \u52a0\u8f7d\u5931\u8d25")); document.head.appendChild(s); }); for (let i = 0; i < 30; i++) { T = resolveHyTesseractGlobal(); if (T) return T; await sleep(100); } throw new Error("Tesseract \u52a0\u8f7d\u5931\u8d25"); } async function getHyTesseractWorker() { if (hyTesseractWorker) return hyTesseractWorker; const Tesseract = await loadHyTesseract(); if (!Tesseract || typeof Tesseract.createWorker !== "function") { throw new Error("Tesseract.createWorker \u4e0d\u53ef\u7528"); } const worker = await Tesseract.createWorker("eng", 1, { logger: () => {} }); await worker.setParameters({ tessedit_char_whitelist: "0123456789", tessedit_pageseg_mode: "7", }); hyTesseractWorker = worker; return worker; } async function preprocessHyCheckCodeBlob(blob) { const bmp = await createImageBitmap(blob); const scale = 3; const canvas = document.createElement("canvas"); canvas.width = Math.max(1, bmp.width * scale); canvas.height = Math.max(1, bmp.height * scale); const ctx = canvas.getContext("2d"); ctx.imageSmoothingEnabled = false; ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(bmp, 0, 0, canvas.width, canvas.height); const img = ctx.getImageData(0, 0, canvas.width, canvas.height); for (let i = 0; i < img.data.length; i += 4) { const r = img.data[i]; const g = img.data[i + 1]; const b = img.data[i + 2]; const isDigit = b > 70 && b > r + 15 && r + g + b < 650; const v = isDigit ? 0 : 255; img.data[i] = v; img.data[i + 1] = v; img.data[i + 2] = v; img.data[i + 3] = 255; } ctx.putImageData(img, 0, 0); return new Promise((resolve, reject) => { canvas.toBlob((b) => (b ? resolve(b) : reject(new Error("\u9a8c\u8bc1\u7801\u56fe\u5904\u7406\u5931\u8d25"))), "image/png"); }); } async function ocrHyCheckCodeViaTesseract(blob) { const processed = await preprocessHyCheckCodeBlob(blob); const worker = await getHyTesseractWorker(); const { data } = await worker.recognize(processed); return cleanHyOcrDigits(data.text || ""); } async function ocrHyCheckCodeImage(blob, tag) { if (cloudState.captchaSelfApiEnable && (cloudState.captchaSelfApiUrl || getCloudApiBase())) { try { const code = await ocrHyCheckCodeViaDdddocr(blob); if (code) { if (tag) pushLog(`[${tag}] OCR\uff1a${code}`); return code; } } catch (e) { if (tag) { pushLog(`[${tag}] OCR \u5931\u8d25\uff1a${e && e.message ? e.message : e}`); } } } try { const code = await ocrHyCheckCodeViaTesseract(blob); if (code) { if (tag) pushLog(`[${tag}] Tesseract\uff1a${code}`); return code; } if (tag) pushLog(`[${tag}] Tesseract \u4e5f\u672a\u8bc6\u522b\u51fa\u6570\u5b57`); } catch (e) { if (tag) pushLog(`[${tag}] Tesseract \u5931\u8d25\uff1a${e && e.message ? e.message : e}`); } return ""; } async function fetchHyExamCodePage(cwid, referer) { const pageUrl = `${location.origin}/pages/exam_code.aspx?cwid=${encodeURIComponent(cwid)}`; const r = await fetch(pageUrl, { credentials: "include", headers: { Accept: "text/html,application/xhtml+xml", Referer: referer || pageUrl, }, __hyEngineMark: true, }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const html = await r.text(); const page = parseHyExamCodePage(html, r.url || pageUrl); if (!page) throw new Error("\u9a8c\u8bc1\u7801\u9875\u89e3\u6790\u5931\u8d25"); return { html, page }; } async function fetchHyCheckCodeBlob(pageUrl) { const imageUrl = `${location.origin}/secure/CheckCode.aspx?id=${Math.random()}`; const r = await fetch(imageUrl, { credentials: "include", headers: { Accept: "image/*", Referer: pageUrl }, __hyEngineMark: true, }); if (!r.ok) throw new Error(`\u9a8c\u8bc1\u7801\u56fe HTTP ${r.status}`); return r.blob(); } async function submitHyExamCode(cwid, code, hidden, referer) { const pageUrl = `${location.origin}/pages/exam_code.aspx?cwid=${encodeURIComponent(cwid)}`; const parts = [ "__EVENTTARGET=", "__EVENTARGUMENT=", `__VIEWSTATE=${encodeURIComponent(hidden.__VIEWSTATE || "")}`, `__VIEWSTATEGENERATOR=${encodeURIComponent(hidden.__VIEWSTATEGENERATOR || "")}`, `__EVENTVALIDATION=${encodeURIComponent(hidden.__EVENTVALIDATION || "")}`, `txtCheckCode=${encodeURIComponent(code)}`, `btnYes=${encodeURIComponent("\u63d0\u4ea4")}`, ]; const r = await fetch(pageUrl, { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "text/html,application/xhtml+xml", Referer: referer || pageUrl, }, body: parts.join("&"), redirect: "follow", __hyEngineMark: true, }); const text = await r.text(); const finalUrl = r.url || pageUrl; if (/exam\.aspx/i.test(finalUrl) && !/exam_code/i.test(finalUrl)) { return { ok: true, url: finalUrl, text }; } if (isHyExamCodePage(text, finalUrl)) { return { ok: false, wrongCode: true, html: text, hidden: parseAspNetHidden(text), url: finalUrl }; } return { ok: false, msg: "\u9a8c\u8bc1\u7801\u63d0\u4ea4\u672a\u901a\u8fc7", html: text, url: finalUrl }; } function removeHyExamCodeModal() { const el = document.getElementById("hy-cme-exam-code-modal"); if (!el) return; const img = el.querySelector("#hy-cme-cap-img"); if (img && img._objUrl) URL.revokeObjectURL(img._objUrl); el.remove(); } async function promptHyExamCodeManual(tag, blob, pageUrl) { let imageBlob = blob || null; if (!imageBlob && pageUrl) { try { imageBlob = await fetchHyCheckCodeBlob(pageUrl); } catch (_) {} } return new Promise((resolve) => { injectHyPanelStyles(); removeHyExamCodeModal(); const wrap = document.createElement("div"); wrap.id = "hy-cme-exam-code-modal"; const title = escapeHtml(tag || "\u8003\u8bd5\u9a8c\u8bc1\u7801"); wrap.innerHTML = ` `; document.body.appendChild(wrap); const img = wrap.querySelector("#hy-cme-cap-img"); const input = wrap.querySelector("#hy-cme-cap-input"); const setImageBlob = (b) => { if (!img || !b) return; if (img._objUrl) URL.revokeObjectURL(img._objUrl); img._objUrl = URL.createObjectURL(b); img.src = img._objUrl; }; if (imageBlob) setImageBlob(imageBlob); const finish = (code) => { removeHyExamCodeModal(); resolve(String(code || "").replace(/\D/g, "")); }; wrap.querySelector("#hy-cme-cap-ok")?.addEventListener("click", () => finish(input.value)); wrap.querySelector("#hy-cme-cap-cancel")?.addEventListener("click", () => finish("")); wrap.querySelector("#hy-cme-cap-refresh")?.addEventListener("click", async () => { if (!pageUrl) return; try { const b = await fetchHyCheckCodeBlob(pageUrl); imageBlob = b; setImageBlob(b); input.value = ""; input.focus(); } catch (e) { pushLog(`[${tag}] \u6362\u9a8c\u8bc1\u7801\u56fe\u5931\u8d25\uff1a${e && e.message ? e.message : e}`); } }); wrap.addEventListener("click", (e) => { if (e.target === wrap) finish(""); }); input.addEventListener("keydown", (e) => { if (e.key === "Enter") finish(input.value); if (e.key === "Escape") finish(""); }); setTimeout(() => input.focus(), 50); }); } async function solveHyExamCodeChallenge(cwid, tag, seedHtml, referer) { const maxTry = HY_EXAM_CODE_OCR_MAX + HY_EXAM_CODE_MANUAL_MAX; let html = seedHtml || ""; let pageReferer = referer || `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(cwid)}`; const ocrMax = cloudState.captchaSelfApiEnable && (cloudState.captchaSelfApiUrl || getCloudApiBase()) ? HY_EXAM_CODE_OCR_MAX : 0; pushLog( `[${tag}] \u8003\u8bd5\u9a8c\u8bc1\u7801\uff1a${ocrMax ? `OCR ${buildHyCaptchaPredictUrl()}` : "\u624b\u52a8\u8f93\u5165"}…` ); for (let i = 0; i < maxTry; i++) { if (!getEnabled()) return { ok: false, msg: "\u5df2\u6682\u505c", paused: true }; let page; if (html && isHyExamCodePage(html)) { page = parseHyExamCodePage(html, `${location.origin}/pages/exam_code.aspx?cwid=${encodeURIComponent(cwid)}`); html = ""; } else { try { const fetched = await fetchHyExamCodePage(cwid, pageReferer); page = fetched.page; pageReferer = page.pageUrl; } catch (e) { return { ok: false, msg: e && e.message ? e.message : "\u62c9\u53d6\u9a8c\u8bc1\u7801\u9875\u5931\u8d25" }; } } if (!page) return { ok: false, msg: "\u9a8c\u8bc1\u7801\u9875\u89e3\u6790\u5931\u8d25" }; let code = ""; let captchaBlob = null; try { captchaBlob = await fetchHyCheckCodeBlob(page.pageUrl); if (i < ocrMax) { code = await ocrHyCheckCodeImage(captchaBlob, tag); if (code) pushLog(`[${tag}] \u91c7\u7528\u9a8c\u8bc1\u7801 ${code}\uff08${i + 1}/${ocrMax}\uff09`); } } catch (e) { pushLog(`[${tag}] \u9a8c\u8bc1\u7801\u56fe\u62c9\u53d6\u5931\u8d25\uff1a${e && e.message ? e.message : e}`); } if (!code || code.length < 4) { if (i >= ocrMax) { code = await promptHyExamCodeManual(tag, captchaBlob, page.pageUrl); if (!code) return { ok: false, msg: "\u672a\u8f93\u5165\u9a8c\u8bc1\u7801" }; pushLog(`[${tag}] \u4f7f\u7528\u624b\u52a8\u8f93\u5165\u9a8c\u8bc1\u7801`); } else { await sleep(1200); continue; } } const sub = await submitHyExamCode(cwid, code, page.hidden, page.pageUrl); if (sub.ok) { markHyExamCodePassed(cwid); pushLog(`[${tag}] \u9a8c\u8bc1\u7801\u5df2\u901a\u8fc7\uff0c\u7ee7\u7eed\u8003\u8bd5`); return { ok: true, url: sub.url }; } if (sub.wrongCode) { pushLog(`[${tag}] \u9a8c\u8bc1\u7801\u9519\u8bef\uff0c\u6362\u56fe\u91cd\u8bd5\uff08${i + 1}/${maxTry}\uff09`); html = sub.html || ""; if (!(await hyExamRetryWait(2000, tag))) { return { ok: false, msg: "\u5df2\u6682\u505c", paused: true }; } continue; } return { ok: false, msg: sub.msg || "\u9a8c\u8bc1\u7801\u63d0\u4ea4\u5931\u8d25" }; } return { ok: false, msg: "\u9a8c\u8bc1\u7801\u591a\u6b21\u5931\u8d25" }; } async function tryPassHyExamCaptcha(cwid, tag, html, referer) { if (!cwid) return { ok: false, msg: "\u7f3a\u5c11 cwid" }; if (isHyExamCodeRecentlyPassed(cwid)) { try { const url = `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(cwid)}`; const r = await fetch(url, { credentials: "include", headers: { Accept: "text/html", Referer: referer || url }, __hyEngineMark: true, }); const text = await r.text(); if (!isHyExamCodePage(text, r.url || url) && parseHyExamQuestions(text).length) { pushLog(`[${tag}] \u9a8c\u8bc1\u7801\u6709\u6548\u671f\u5185\uff0c\u8df3\u8fc7\u91cd\u590d\u9a8c\u8bc1`); return { ok: true, skipped: true }; } } catch (_) {} } return solveHyExamCodeChallenge(cwid, tag, html, referer); } function waitUntil(fn, timeoutMs = 30000, stepMs = 300) { return new Promise((resolve, reject) => { const start = Date.now(); const tick = () => { try { const v = fn(); if (v) return resolve(v); } catch (_) {} if (Date.now() - start > timeoutMs) return reject(new Error("wait timeout")); setTimeout(tick, stepMs); }; tick(); }); } function formatEta(ms) { if (ms <= 0) return "\u5df2\u5230\u70b9"; if (ms < 60000) return `${Math.max(1, Math.ceil(ms / 1000))}\u79d2`; const m = Math.ceil(ms / 60000); return m < 60 ? `${m}\u5206\u949f` : `${Math.floor(m / 60)}h${m % 60}m`; } function formatStudiedDuration(studiedSec) { const s = Math.max(0, Math.round(Number(studiedSec) || 0)); if (s <= 0) return "0\u5206\u949f"; if (s < 60) return `${s}\u79d2`; return `${Math.round(s / 60)}\u5206\u949f`; } function saveEnginePostedSec(ctx, sec) { const n = Math.max(0, Math.round(Number(sec) || 0)); if (!ctx || n <= 0) return; const prev = getJob(ctx); const prevSec = prev && prev.lastPostedSec != null ? Number(prev.lastPostedSec) : 0; const nextSec = Math.max(prevSec, n); if (prev && prev.lastPostedSec === nextSec) return; saveJob(ctx, { lastPostedSec: nextSec, rtEntrySec: prev && prev.rtEntrySec != null ? prev.rtEntrySec : n, }); } function chapterStudyWaitMs(job, nextDueMs) { const j = job || {}; return nextDueMs > 0 ? nextDueMs : j.nextStepDue && j.nextStepDue > Date.now() ? j.nextStepDue - Date.now() : 0; } function shouldShowChapterStudyUserLog(job, nextDueMs) { const waitMs = chapterStudyWaitMs(job, nextDueMs); if (waitMs >= 30000 && waitMs < RT_STEP_GAP_MS * 0.85) return false; return true; } function pushUserChapterStudyLog(job, nextDueMs) { if (!getEnabled()) return; const j = job || {}; if (!shouldShowChapterStudyUserLog(j, nextDueMs)) return; const waitKey = nextDueMs > 0 ? Math.round(nextDueMs / 1000) : j.nextStepDue && j.nextStepDue > Date.now() ? Math.round((j.nextStepDue - Date.now()) / 1000) : 0; const studiedKey = Math.floor(resolveWallClockStudiedSec(j) / 60); const logKey = `${j.lsKey || j.cwrid || ""}:${studiedKey}:${waitKey}`; if (panelState.lastChapterStudyLogKey === logKey) return; panelState.lastChapterStudyLogKey = logKey; pushUserLog(formatChapterStudyStatusLine(j, nextDueMs || 0)); } function formatBeijingDateTime(ts) { const n = Number(ts || 0); if (!n) return "—"; try { return new Date(n).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }); } catch (_) { return new Date(n).toLocaleString(); } } function formatDurationCn(sec) { const s = Math.max(0, Math.round(sec || 0)); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); if (h > 0) return `${h}\u5c0f\u65f6${m}\u5206\u949f`; return `${m}\u5206\u949f`; } function offlineBatchBufferMs(st) { return Number(st && st.batchBufferMs) || OFFLINE_BATCH_BUFFER_MS; } function offlineBatchDueMs(st, courseTotalMs, sessionStart) { if (st && st.batchCompleteDue) return Number(st.batchCompleteDue); const start = sessionStart || (st && st.sessionStart) || Date.now(); const courseMs = courseTotalMs != null ? courseTotalMs : Number(st && st.totalMs) || 0; return start + courseMs + offlineBatchBufferMs(st); } function getOfflineBatchSnapshot() { if (!isOfflineMode()) return null; const selected = pruneQueueToCourses(); if (!selected.length) return null; const selectedSet = new Set(selected.map((s) => normalizeQueueCid(s))); const allClocked = Object.values(loadJobs()).filter( (j) => j && ["silent_clock", "silent_finish_retry"].includes(j.phase) ); if (!allClocked.length) return null; const clocked = allClocked.filter((j) => j.cid && selectedSet.has(normalizeQueueCid(j.cid))); if (!clocked.length) return null; const st = loadSilentState(); const filledAt = Number(st.filledAt || 0); const sessionStart = Number(st.sessionStart || 0); if (!filledAt || !sessionStart) return null; const batchTotalMs = allClocked.reduce((s, j) => s + (j.duration || 3600) * 1000, 0); const matchTotalMs = clocked.reduce((s, j) => s + (j.duration || 3600) * 1000, 0); const due = offlineBatchDueMs(st, batchTotalMs, sessionStart); const totalSec = Math.round(matchTotalMs / 1000); return { due, totalSec, totalMs: matchTotalMs, totalCourses: clocked.length, batchCourses: allClocked.length, sessionStart, filledAt, waitingCount: clocked.length, ready: Date.now() >= due, beijingDue: st.beijingDue || formatBeijingDateTime(due), filledReady: true, }; } function notifyOfflineBatchFilled(due, remain) { const st = loadSilentState(); const logKey = String(st.filledAt || st.sessionStart || due); if (st.filledUserLogKey === logKey) return; patchSilentState({ filledUserLogKey: logKey, lastWaitUserLogAt: Date.now() }); pushUserLog(`\u6279\u91cf\u62c9\u6ee1\u5b8c\u6210 · ${formatSelectedProgressLine()}`); pushUserLog( `\u7ea6 ${formatEta(remain)} \u540e\u8bf7\u518d\u70b9\u300c\u5f00\u59cb\u300d\uff0c\u5c06\u68c0\u67e5\u8bfe\u7a0b\u5b8c\u6210\u5e76\u81ea\u52a8\u8003\u8bd5\uff08\u5317\u4eac\u65f6\u95f4 ${formatBeijingDateTime(due)}\uff09` ); pushUserLog(`\u53ef\u5173\u95ed\u672c\u7f51\u9875\uff0c\u7a0b\u5e8f\u5df2\u505c\u6b62\uff1b\u5230\u65f6\u95f4\u540e\u91cd\u65b0\u6253\u5f00\u534e\u533b\u7f51\u518d\u70b9\u300c\u5f00\u59cb\u300d\u5373\u53ef`); } function tickOfflineBatchWaitUserLog() { if (!isOfflineMode()) return; const snap = getOfflineBatchSnapshot(); if (!snap || !snap.filledReady) return; const now = Date.now(); const remain = Math.max(0, snap.due - now); const dueText = snap.beijingDue || formatBeijingDateTime(snap.due); const st = loadSilentState(); const coursePart = snap.totalCourses ? `${snap.totalCourses} \u8bfe` : "\u6279\u91cf\u4efb\u52a1"; if (snap.ready) { const key = `ready:${snap.due}`; if (st.readyUserLogKey === key) return; patchSilentState({ readyUserLogKey: key }); pushUserLog(`\u5df2\u5230\u7b49\u5f85\u65f6\u95f4 · \u8bf7\u70b9\u300c\u5f00\u59cb\u300d\u68c0\u67e5\u8bfe\u7a0b\u5b8c\u6210\u5e76\u81ea\u52a8\u8003\u8bd5\uff08\u5317\u4eac\u65f6\u95f4 ${dueText}\uff09`); return; } const lastAt = Number(st.lastWaitUserLogAt || st.filledAt || 0); if (now - lastAt < OFFLINE_WAIT_LOG_INTERVAL_MS) return; const slot = Math.floor(remain / OFFLINE_WAIT_LOG_INTERVAL_MS); const key = `wait:${snap.due}:${slot}`; if (st.waitUserLogKey === key) return; patchSilentState({ waitUserLogKey: key, lastWaitUserLogAt: now }); pushUserLog( `\u79bb\u7ebf\u7b49\u5f85\u4e2d · ${coursePart}\u8fdb\u5ea6\u5df2\u63d0\u4ea4 · \u7ea6 ${formatEta(remain)} \u540e\u518d\u70b9\u300c\u5f00\u59cb\u300d\u68c0\u67e5\u6536\u5c3e\u4e0e\u8003\u8bd5\uff08\u5317\u4eac\u65f6\u95f4 ${dueText}\uff09· \u7a0b\u5e8f\u5df2\u505c\u6b62\uff0c\u53ef\u5173\u95ed\u7f51\u9875` ); } function clearOfflinePanelHint() { const el = document.getElementById("hy-cme-start-hint"); if (el && /^\u79bb\u7ebf\u6279\u91cf\uff1a/.test(String(el.textContent || ""))) { clearPanelHint(); } } function isLoginRelatedPage() { const host = location.hostname.toLowerCase(); const path = (location.pathname + location.search).toLowerCase(); if (/^apiwxmsg\.|^hyuser\d*\./.test(host)) return true; if (/\/secure\/login|login_sso|login_sq|wxuserlogin/i.test(path)) return true; return false; } let hdRunnerBusy = false; function isHdblHost() { return /^hdbl\.91huayi\.com$/i.test(location.hostname); } function isCmeHost() { const host = location.hostname.toLowerCase(); if (/^apiwxmsg\.|^hyuser\d*\./.test(host)) return false; return /^cme[\w-]*\.91huayi\.com$/i.test(host); } function shouldRunScript() { if (!location.host.includes("91huayi.com")) return false; if (isHdblHost()) return isHdInteractiveEnabled(); if (!isCmeHost()) return false; if (isLoginRelatedPage()) return false; return true; } function pageType() { const p = location.pathname.toLowerCase(); if (isHdblHost()) return "hdbl"; if (p.includes("exam_result_hd.aspx")) return "hd_result"; if (p.includes("/course_ware/course_ware_polyv.aspx")) return "play"; if (p.includes("/course_ware/course_ware_hd.aspx")) return "ware_hd"; if (p.includes("/course_ware/course_ware.aspx")) return "ware"; if (p.includes("/pages/course.aspx")) return "course"; if (p.includes("/personalcenter/course_collect.aspx")) return "collect"; if (p.includes("/pages/exam_code.aspx")) return "exam_code"; if (p.includes("/pages/exam.aspx")) return "exam"; if (p.includes("/pages/exam_result.aspx")) return "exam_result"; return "other"; } function shouldShowPanel() { if (pageType() === "play") return false; return isLeaderTab(); } let tabIsLeader = false; let leaderHeartbeatTimer = null; let leaderRoleTimer = null; function getTabId() { try { let id = sessionStorage.getItem(TAB_ID_SESSION_KEY); if (!id) { id = "tab_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 10); sessionStorage.setItem(TAB_ID_SESSION_KEY, id); } return id; } catch (_) { return "tab_fb_" + Math.random().toString(36).slice(2, 10); } } function loadLeaderRecord() { try { return JSON.parse(localStorage.getItem(LEADER_TAB_KEY) || "null"); } catch (_) { return null; } } function saveLeaderRecord(rec) { try { localStorage.setItem(LEADER_TAB_KEY, JSON.stringify(rec || {})); } catch (_) {} } function clearLeaderRecord() { try { localStorage.removeItem(LEADER_TAB_KEY); } catch (_) {} } function isLeaderStale(rec) { if (!rec || !rec.tabId) return true; return Date.now() - (Number(rec.ts) || 0) > LEADER_STALE_MS; } function hasActiveLeader() { const rec = loadLeaderRecord(); return !!(rec && rec.tabId && !isLeaderStale(rec)); } function isOurLeaderRecord(rec) { return !!(rec && rec.tabId && rec.tabId === getTabId()); } function formatLeaderPageHint(rec) { if (!rec) return "\u53e6\u4e00\u6807\u7b7e\u9875"; if (rec.title) return `\u300c${String(rec.title).slice(0, 24)}\u300d`; if (rec.path && /collect/i.test(rec.path)) return "\u9879\u76ee\u6536\u85cf\u9875"; if (rec.path && /course/i.test(rec.path)) return "\u8bfe\u7a0b\u9875"; return "\u53e6\u4e00\u6807\u7b7e\u9875"; } function pulseLeaderHeartbeat() { const rec = loadLeaderRecord(); if (!isOurLeaderRecord(rec)) return; saveLeaderRecord({ tabId: getTabId(), ts: Date.now(), path: location.pathname, title: String(document.title || "").slice(0, 48), host: location.hostname, }); } function claimLeaderTab(force) { const rec = loadLeaderRecord(); if (!force && hasActiveLeader() && !isOurLeaderRecord(rec)) { tabIsLeader = false; return false; } saveLeaderRecord({ tabId: getTabId(), ts: Date.now(), path: location.pathname, title: String(document.title || "").slice(0, 48), host: location.hostname, }); tabIsLeader = true; return true; } function releaseLeaderTab() { const rec = loadLeaderRecord(); if (isOurLeaderRecord(rec)) clearLeaderRecord(); tabIsLeader = false; stopLeaderHeartbeat(); } function refreshLeaderRole() { const rec = loadLeaderRecord(); if (!rec || !rec.tabId || isLeaderStale(rec)) { tabIsLeader = false; return false; } tabIsLeader = isOurLeaderRecord(rec); return tabIsLeader; } function isLeaderTab() { if (!tabIsLeader) refreshLeaderRole(); return tabIsLeader; } function stopLeaderHeartbeat() { if (leaderHeartbeatTimer) { clearInterval(leaderHeartbeatTimer); leaderHeartbeatTimer = null; } } function startLeaderHeartbeat() { stopLeaderHeartbeat(); if (!tabIsLeader) return; pulseLeaderHeartbeat(); leaderHeartbeatTimer = setInterval(() => { if (!tabIsLeader) { stopLeaderHeartbeat(); return; } pulseLeaderHeartbeat(); }, LEADER_HEARTBEAT_MS); } function stopGlobalTicker() { if (window.__hyJobTicker) { clearInterval(window.__hyJobTicker); window.__hyJobTicker = null; } } function stopPanelUpdateTimer() { if (window.__hyPanelUpdateTimer) { clearInterval(window.__hyPanelUpdateTimer); window.__hyPanelUpdateTimer = null; } } function removePanelDom() { const panel = document.getElementById("hy-cme-auto-panel"); if (panel) panel.remove(); stopPreviewAutoRefresh(); stopPanelUpdateTimer(); } function showFollowerBanner() { if (isLeaderTab()) return; const rec = loadLeaderRecord(); if (!getEnabled() && !hasActiveLeader()) return; let el = document.getElementById("hy-cme-follower-banner"); if (!el) { el = document.createElement("div"); el.id = "hy-cme-follower-banner"; el.style.cssText = "position:fixed;right:16px;top:72px;z-index:2147483646;max-width:320px;padding:10px 12px;border-radius:10px;background:#eff6ff;border:1px solid #93c5fd;color:#1e3a8a;font:700 12px/1.45 -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;box-shadow:0 8px 24px rgba(30,64,175,.12);"; document.body.appendChild(el); } const hint = formatLeaderPageHint(rec); const running = getEnabled() ? "\u8fd0\u884c\u4e2d" : "\u5df2\u6253\u5f00\u9762\u677f"; el.textContent = `\u52a9\u624b\u5728 ${hint} ${running}\uff0c\u8bf7\u5207\u56de\u8be5\u6807\u7b7e\u67e5\u770b\u9762\u677f\u4e0e\u65e5\u5fd7`; el.style.display = "block"; } function removeFollowerBanner() { const el = document.getElementById("hy-cme-follower-banner"); if (el) el.remove(); } function syncFollowerBanner() { if (isLeaderTab()) removeFollowerBanner(); else showFollowerBanner(); } function onGainedLeadership() { removeFollowerBanner(); startLeaderHeartbeat(); if (!document.getElementById("hy-cme-auto-panel")) buildPanel(); else { renderRunLog(); updatePanel(); syncRunModePanel(); } if (getEnabled()) { startPreviewAutoRefresh(); startGlobalTicker(); } } function onLostLeadership() { stopLeaderHeartbeat(); stopGlobalTicker(); removePanelDom(); syncFollowerBanner(); } function prepareLeaderOnBoot() { refreshLeaderRole(); if (!hasActiveLeader()) { claimLeaderTab(false); } else if (getEnabled() && isLeaderStale(loadLeaderRecord())) { claimLeaderTab(true); } if (tabIsLeader) startLeaderHeartbeat(); } function installLeaderTabSystem() { const onLeaderStorage = () => { const was = tabIsLeader; refreshLeaderRole(); if (!was && tabIsLeader) onGainedLeadership(); else if (was && !tabIsLeader) onLostLeadership(); else syncFollowerBanner(); if (tabIsLeader && getEnabled() && !leaderHeartbeatTimer) startLeaderHeartbeat(); if (tabIsLeader && getEnabled() && !window.__hyJobTicker) startGlobalTicker(); }; window.addEventListener("storage", (e) => { if (e.key === LEADER_TAB_KEY) onLeaderStorage(); }); window.addEventListener("beforeunload", () => releaseLeaderTab()); window.addEventListener("pagehide", () => releaseLeaderTab()); if (leaderRoleTimer) clearInterval(leaderRoleTimer); leaderRoleTimer = setInterval(() => { const was = tabIsLeader; const rec = loadLeaderRecord(); if (getEnabled() && (!rec || isLeaderStale(rec))) { if (claimLeaderTab(true)) { if (!was) onGainedLeadership(); else startLeaderHeartbeat(); return; } } refreshLeaderRole(); if (was && !tabIsLeader) onLostLeadership(); else if (!was && tabIsLeader) onGainedLeadership(); else syncFollowerBanner(); }, LEADER_HEARTBEAT_MS); } function shouldBlockNativeProgress() { return getEnabled() && getApiMode(); } const PROGRESS_URL_RE = /addCourseWareProcess\.ashx|addCourseWarePlayRecord\.ashx/i; function fakeBlockedXhrResponse(xhr) { try { Object.defineProperty(xhr, "readyState", { configurable: true, get: () => 4 }); Object.defineProperty(xhr, "status", { configurable: true, get: () => 200 }); Object.defineProperty(xhr, "responseText", { configurable: true, get: () => '{"code":0}' }); if (typeof xhr.onreadystatechange === "function") xhr.onreadystatechange(); xhr.dispatchEvent(new Event("load")); } catch (_) {} } function installNetworkHooks() { if (window.__hyCmeNetHooked) return; window.__hyCmeNetHooked = true; const blockNative = (xhr) => { if (!shouldBlockNativeProgress()) return false; if (xhr && xhr.__hyEngineMark) return false; const url = (xhr && xhr.__hyUrl) || ""; return PROGRESS_URL_RE.test(String(url)); }; try { const origFetch = window.fetch; if (typeof origFetch === "function") { window.fetch = function (input, init) { const url = typeof input === "string" ? input : input && input.url; if (shouldBlockNativeProgress() && PROGRESS_URL_RE.test(String(url || ""))) { if (!(init && init.__hyEngineMark)) { return Promise.resolve( new Response('{"code":0}', { status: 200, headers: { "Content-Type": "application/json" } }) ); } } return origFetch.apply(this, arguments); }; } } catch (_) {} const proto = XMLHttpRequest.prototype; const origOpen = proto.open; const origSend = proto.send; const patchedOpen = function (...args) { this.__hyUrl = args[1] != null ? String(args[1]) : ""; return origOpen.apply(this, args); }; const patchedSend = function (body) { if (blockNative(this)) { setTimeout(() => fakeBlockedXhrResponse(this), 0); return; } return origSend.call(this, body); }; let xhrHooked = false; try { proto.open = patchedOpen; proto.send = patchedSend; xhrHooked = true; } catch (_) {} if (!xhrHooked) { try { Object.defineProperty(proto, "open", { configurable: true, writable: true, value: patchedOpen, }); Object.defineProperty(proto, "send", { configurable: true, writable: true, value: patchedSend, }); xhrHooked = true; } catch (_) {} } if (!xhrHooked) { try { const NativeXHR = window.XMLHttpRequest; if (NativeXHR && !NativeXHR.__hyWrapped) { function WrappedXHR() { const xhr = new NativeXHR(); const bindOpen = Function.prototype.call.bind(origOpen, xhr); const bindSend = Function.prototype.call.bind(origSend, xhr); xhr.open = function (...args) { xhr.__hyUrl = args[1] != null ? String(args[1]) : ""; return bindOpen(...args); }; xhr.send = function (body) { if (blockNative(xhr)) { setTimeout(() => fakeBlockedXhrResponse(xhr), 0); return; } return bindSend(body); }; return xhr; } WrappedXHR.prototype = NativeXHR.prototype; WrappedXHR.__hyWrapped = true; window.XMLHttpRequest = WrappedXHR; } } catch (_) {} } try { window.blockAbnormalPlugin = function () {}; } catch (_) {} } if (shouldRunScript()) installNetworkHooks(); function setHdMetaCookie(meta) { try { const val = encodeURIComponent(JSON.stringify(meta || {})); document.cookie = `${HD_META_COOKIE}=${val}; path=/; max-age=3600; SameSite=Lax`; if (location.hostname.includes("91huayi.com")) { document.cookie = `${HD_META_COOKIE}=${val}; path=/; domain=.91huayi.com; max-age=3600; SameSite=Lax`; } } catch (_) {} } function readHdMetaCookie() { try { const m = document.cookie.match(new RegExp(`(?:^|;\\s*)${HD_META_COOKIE}=([^;]*)`)); if (!m) return null; return JSON.parse(decodeURIComponent(m[1])); } catch (_) { return null; } } function clearHdMetaCookie() { document.cookie = `${HD_META_COOKIE}=; path=/; max-age=0`; if (location.hostname.includes("91huayi.com")) { document.cookie = `${HD_META_COOKIE}=; path=/; domain=.91huayi.com; max-age=0`; } } function saveHdLaunchSnapshot(params, launchUrl) { if (!params || !params.courseWareId || !params.userId) return; try { localStorage.setItem( HD_LAUNCH_KEY, JSON.stringify({ params, launchUrl: launchUrl || location.href, capturedAt: Date.now() }) ); } catch (_) {} } function loadHdLaunchSnapshot() { try { return JSON.parse(localStorage.getItem(HD_LAUNCH_KEY) || "null"); } catch (_) { return null; } } function clearHdLaunchSnapshot() { localStorage.removeItem(HD_LAUNCH_KEY); } function captureHdLaunchParamsEarly() { try { const href = location.href.split("#")[0]; const params = parseHdLaunchParams(href); if (params.courseWareId && params.userId && params.hash) { saveHdLaunchSnapshot(params, href); } } catch (_) {} } function resolveHdLaunchBundle() { captureHdLaunchParamsEarly(); const meta = readHdMetaCookie(); const snap = loadHdLaunchSnapshot(); const liveHref = location.href.split("#")[0]; const live = parseHdLaunchParams(liveHref); if ( snap && meta && snap.params && snap.params.businessCustomParams && meta.cwid && snap.params.businessCustomParams !== meta.cwid ) { clearHdLaunchSnapshot(); } if (live.hash && live.courseWareId && live.userId) { return { params: live, launchReferer: liveHref }; } if (snap && snap.params && snap.params.hash && snap.params.courseWareId && snap.params.userId) { return { params: snap.params, launchReferer: snap.launchUrl || liveHref }; } if (snap && snap.launchUrl) { const fromSnap = parseHdLaunchParams(snap.launchUrl); if (fromSnap.courseWareId && fromSnap.userId && fromSnap.hash) { return { params: fromSnap, launchReferer: snap.launchUrl }; } } return { params: live, launchReferer: liveHref }; } function resolveHdLaunchParams() { return resolveHdLaunchBundle().params; } function isHdLaunchReady() { const { params } = resolveHdLaunchBundle(); return !!(params.courseWareId && params.userId && params.hash); } function isHdDetachedRunner() { if (window.parent !== window) return true; try { return !!(window.opener && !window.opener.closed); } catch (_) { return false; } } function closeHdPopup() { if (hdPopupWindow && !hdPopupWindow.closed) { try { hdPopupWindow.close(); } catch (_) {} } hdPopupWindow = null; } function clearHdHostSurface() { removeHdIframe(); closeHdPopup(); } function removeHdIframe() { const frame = document.getElementById(HD_IFRAME_ID); if (frame) frame.remove(); } function launchHdInSilentFrame(url) { let frame = document.getElementById(HD_IFRAME_ID); if (!frame) { frame = document.createElement("iframe"); frame.id = HD_IFRAME_ID; frame.name = HD_IFRAME_ID; frame.style.cssText = "position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;border:0;opacity:0;pointer-events:none"; frame.setAttribute("aria-hidden", "true"); document.documentElement.appendChild(frame); } frame.src = url; } function resetHdRunningJob(cwid) { const jobs = loadJobs(); const key = Object.keys(jobs).find( (k) => jobs[k] && jobs[k].interactive && jobs[k].phase === "hd_running" && (jobs[k].cwid === cwid || !cwid) ); if (!key) return; saveJob(jobToCtx(jobs[key]), { phase: "pending" }); updateJobsPanel(); } function clearHdIframeWatchdog() { if (hdIframeWatchdogTimer) { clearTimeout(hdIframeWatchdogTimer); hdIframeWatchdogTimer = null; } } function launchHdPopupWindow(url, job) { clearHdIframeWatchdog(); clearHdHostSurface(); const title = job.title || job.cwrid || job.cwid || ""; const w = HD_POPUP_W; const h = HD_POPUP_H; const features = [ `width=${w}`, `height=${h}`, `left=${Math.max(0, Math.round((screen.availWidth - w) / 2))}`, `top=${Math.max(0, Math.round((screen.availHeight - h) / 2))}`, "menubar=no", "toolbar=no", "status=no", "resizable=yes", "scrollbars=yes", ].join(","); hdPopupWindow = window.open(url, HD_POPUP_NAME, features); if (!hdPopupWindow) { pushLog(`[${title}] [\u4e92\u52a8] \u5f39\u7a97\u88ab\u6d4f\u89c8\u5668\u62e6\u622a\uff0c\u6539\u4e3a\u5f53\u524d\u9875\u6253\u5f00…`); location.href = url; return false; } try { hdPopupWindow.focus(); } catch (_) {} pushLog(`[${title}] [\u4e92\u52a8] \u5df2\u6253\u5f00\u8ff7\u4f60\u7a97\u53e3\uff08\u4e3b\u9875\u9762\u4fdd\u7559\uff0c\u5b8c\u6210\u540e\u81ea\u52a8\u5173\u95ed\uff09…`); return true; } function armHdHostWatchdog(job, launchUrl, mode) { clearHdIframeWatchdog(); const started = Date.now(); const cwid = job.cwid; const title = job.title || ""; const hostMode = mode || "popup"; hdIframeWatchdogTimer = setTimeout(() => { hdIframeWatchdogTimer = null; const jobs = loadJobs(); const j = Object.values(jobs).find( (x) => x && x.interactive && x.phase === "hd_running" && x.cwid === cwid ); if (!j) return; const progressAt = j.hdProgressAt || 0; const launchedAt = j.hdLaunchedAt || started; if (progressAt >= launchedAt) return; if (hostMode === "iframe") { pushLog( `[${title}] [\u4e92\u52a8] iframe ${HD_IFRAME_WATCHDOG_MS / 1000}s \u65e0\u54cd\u5e94\uff0c\u6539\u5f00\u8ff7\u4f60\u7a97\u53e3…` ); launchHdPopupWindow(launchUrl, job); armHdHostWatchdog(job, launchUrl, "popup"); return; } pushLog( `[${title}] [\u4e92\u52a8] \u8ff7\u4f60\u7a97\u53e3 ${HD_IFRAME_WATCHDOG_MS / 1000}s \u65e0\u54cd\u5e94\uff08\u8bf7\u786e\u8ba4 Tampermonkey \u5df2\u6ce8\u5165 hdbl \u9875\u9762\uff09` ); if (hdPopupWindow && !hdPopupWindow.closed) { try { hdPopupWindow.focus(); } catch (_) {} } }, HD_IFRAME_WATCHDOG_MS); } function installHdIframeBridge() { if (window.__hyHdIframeBridge) return; window.__hyHdIframeBridge = true; window.addEventListener("message", (e) => { const d = e.data; if (!d || d.type !== HD_MSG_TYPE) return; clearHdIframeWatchdog(); if (d.event === "ping") { const where = [d.host, d.path].filter(Boolean).join(""); pushLog(`[\u4e92\u52a8] iframe \u811a\u672c\u5df2\u6ce8\u5165 · ${where || d.stage || "ok"}`); const jobs = loadJobs(); const key = Object.keys(jobs).find( (k) => jobs[k] && jobs[k].interactive && jobs[k].phase === "hd_running" ); if (key) { saveJob(jobToCtx(jobs[key]), { hdProgressAt: Date.now(), hdProgressText: "iframe \u5df2\u8fde\u63a5" }); updateJobsPanel(); } } else if (d.event === "log" && d.text) { pushLog(d.text); if (d.cwid || d.title) { const jobs = loadJobs(); const key = Object.keys(jobs).find( (k) => jobs[k] && jobs[k].interactive && jobs[k].phase === "hd_running" && (d.cwid ? jobs[k].cwid === d.cwid : d.title && jobs[k].title === d.title) ); if (key) { saveJob(jobToCtx(jobs[key]), { hdLastLog: d.text, hdProgressAt: Date.now() }); updateJobsPanel(); updatePanel(); } } } else if (d.event === "progress") { const jobs = loadJobs(); const key = Object.keys(jobs).find((k) => jobs[k] && jobs[k].interactive && jobs[k].phase === "hd_running"); if (key) { saveJob(jobToCtx(jobs[key]), { hdStep: d.step, hdStepTotal: d.total, hdProgressText: d.detail || "", hdLastLog: d.detail || "", hdProgressAt: Date.now(), }); updateJobsPanel(); updatePanel(); } } else if (d.event === "done") { clearHdIframeWatchdog(); if (d.cwid) completeInteractiveJobByCwid(d.cwid); clearHdHostSurface(); clearHdMetaCookie(); pushLog("[\u4e92\u52a8] \u540e\u53f0\u4e92\u52a8\u8bfe\u5b8c\u6210"); runBgScheduler(); } else if (d.event === "fail") { clearHdIframeWatchdog(); clearHdHostSurface(); clearHdMetaCookie(); resetHdRunningJob(d.cwid); pushLog("[\u4e92\u52a8] \u540e\u53f0\u5931\u8d25: " + (d.message || "\u672a\u77e5\u9519\u8bef")); runBgScheduler(); } }); } function postHdIframeEvent(event, payload) { if (!isHdDetachedRunner()) return; const meta = readHdMetaCookie(); const msg = Object.assign({ type: HD_MSG_TYPE, event }, payload || {}, { cwid: (payload && payload.cwid) || (meta && meta.cwid) || "", title: (payload && payload.title) || (meta && meta.title) || "", }); try { if (window.parent !== window) window.parent.postMessage(msg, "*"); } catch (_) {} try { if (window.opener && !window.opener.closed) window.opener.postMessage(msg, "*"); } catch (_) {} } function tryCloseHdRunnerWindow() { try { if (window.opener && !window.opener.closed) window.close(); } catch (_) {} } function postHdProgress(sess, detail, step, total) { postHdIframeEvent("progress", { detail, step, total, cwid: sess.businessCustomParams, title: sess.label, }); } function readPageVar(name) { const html = document.documentElement.innerHTML; const m = html.match(new RegExp("var\\s+" + name + "\\s*=\\s*['\"]([^'\"]+)['\"]", "i")); return m ? m[1] : ""; } function readGroupId() { const m = document.documentElement.innerHTML.match(/group_id:\s*['"]([^'"]+)['"]/i); return m ? m[1] : ""; } function readProvinceId() { const m = document.documentElement.innerHTML.match(/province_id:\s*['"]([^'"]+)['"]/i); return m ? m[1] : ""; } function readWareMeta(uid, cwrid, coaid) { const row = document.querySelector( `.cw-progress-row[data-cwrid="${cwrid}"][data-coaid="${coaid}"]` ); let maxtime = 0; let totaltime = 0; if (row) { maxtime = parseInt(row.getAttribute("data-maxtime") || "0", 10) || 0; totaltime = parseInt(row.getAttribute("data-totaltime") || "0", 10) || 0; } const lsKey = uid + cwrid + coaid; const local = parseInt(localStorage.getItem(lsKey) || "0", 10) || 0; let base = Math.max(maxtime, local); if (totaltime > 0 && base >= totaltime - 15) base = 0; return { maxtime: base, totaltime, lsKey }; } function resolveStartSec(stored, duration) { const d = Math.max(60, duration || 3600); let s = Math.max(0, Math.floor(stored || 0)); if (s >= d - 15) return 1; if (s <= 0) return 1; return Math.min(s, d - 1); } function resolveRtEntrySec(stored, duration) { const d = Math.max(60, duration || 3600); let s = Math.max(0, Math.floor(stored || 0)); if (s <= 0) return 1; if (s >= d - 2) { const blocks = Math.floor((d - 1) / RT_STEP_SEC); return Math.max(1, Math.min(blocks * RT_STEP_SEC, d - 1)); } const floored = Math.floor(s / RT_STEP_SEC) * RT_STEP_SEC; return Math.max(1, Math.min(floored || 1, d - 1)); } function readChapterStudiedSec(uid, cwrid, coaid, duration) { const meta = readWareMeta(uid, cwrid, coaid); const ls = parseInt(localStorage.getItem(meta.lsKey || uid + cwrid + coaid) || "0", 10) || 0; return Math.max(0, meta.maxtime, ls); } function resolveChapterRtStart(ch) { const studied = readChapterStudiedSec(ch.uid, ch.cwrid, ch.coaid, ch.duration); const duration = Math.max(60, ch.duration || 3600); return { studiedSec: studied, startSec: resolveRtEntrySec(studied, duration), }; } function formatPlayProgressPct(sec, total) { const t = Math.max(1, total || 1); return Math.min(100, Math.max(0, Math.round((Math.max(0, sec || 0) / t) * 100))); } function formatProgressLabelFromSec(sec, duration) { const pct = formatPlayProgressPct(sec, duration); if (pct >= 100) return "\u89c6\u9891\u5df2\u5b66\u5b8c"; return `\u64ad\u653e\u81f3\uff1a${pct}%`; } function readCourseId() { const q = new URLSearchParams(location.search).get("cid"); if (q) return q; const m = document.documentElement.innerHTML.match(/courseId:\s*["']([^"']+)["']/i); return m ? m[1] : ""; } function getCachedApiMeta(cid) { try { const all = JSON.parse(localStorage.getItem(META_KEY) || "{}"); return all[cid] || all._default || null; } catch (_) { return null; } } function setCachedApiMeta(cid, meta) { try { const all = JSON.parse(localStorage.getItem(META_KEY) || "{}"); all[cid] = meta; all._default = meta; localStorage.setItem(META_KEY, JSON.stringify(all)); } catch (_) {} } async function ensureCourseApiMeta(cid, sampleCwrid) { const cached = getCachedApiMeta(cid); if (cached && cached.groupId && cached.provinceId) return cached; pushLog("\u62c9\u53d6 group_id…"); const url = "/course_ware/course_ware_polyv.aspx?cwid=" + encodeURIComponent(sampleCwrid) + "&ff=0&ft=0&t=0"; const html = await fetch(url, { credentials: "include" }).then((r) => r.text()); const meta = { groupId: (html.match(/group_id:\s*['"]([^'"]+)['"]/i) || [])[1] || "", provinceId: (html.match(/province_id:\s*['"]([^'"]+)['"]/i) || [])[1] || "", }; if (meta.groupId) setCachedApiMeta(cid, meta); return meta; } function readVarFromHtml(html, name) { const m = html.match(new RegExp("var\\s+" + name + "\\s*=\\s*['\"]([^'\"]+)['\"]", "i")); return m ? m[1] : ""; } function getChapterMetaEl(el) { if (!el) return null; return el.querySelector(".play_process") || el.querySelector(".cw-progress-row"); } function chapterHasInteractiveTag(el) { if (!el) return false; return Array.from(el.querySelectorAll(".cw-tag-list span, h3 label")).some((node) => /\u4e92\u52a8/.test((node.textContent || "").replace(/\s+/g, "")) ); } function isInteractiveChapter(el) { return isHdInteractiveEnabled() && chapterHasInteractiveTag(el); } function purgeInteractiveJobs() { if (isHdInteractiveEnabled()) return; const jobs = loadJobs(); const keys = Object.keys(jobs).filter((k) => jobs[k] && jobs[k].interactive); if (!keys.length) return; keys.forEach((k) => delete jobs[k]); saveJobs(jobs); pushLog("[\u4e92\u52a8] \u4e92\u52a8\u8bfe\u529f\u80fd\u5df2\u5173\u95ed\uff0c\u5df2\u6e05\u7406\u961f\u5217\u4e2d\u7684\u4e92\u52a8\u8bfe\u4efb\u52a1"); } function findChapterElementByCwid(cwid) { if (!cwid) return null; const target = String(cwid).toUpperCase(); for (const el of document.querySelectorAll(".course[data-href]")) { const href = el.getAttribute("data-href") || ""; const m = href.match(/cwid=([^&]+)/i); if (m && String(m[1]).toUpperCase() === target) return el; } return null; } function readChapterBriefFromElement(el) { if (!el) return null; const meta = readChapterMeta(el); if (!meta) return null; const href = el.getAttribute("data-href") || ""; const cwidM = href.match(/cwid=([^&]+)/i); const titleEl = el.querySelector(".cw-title-link strong") || el.querySelector("h3 strong") || el.querySelector("h3 a strong") || el.querySelector("h3 a"); const title = titleEl ? titleEl.textContent.replace(/\s+/g, " ").trim().slice(0, 24) : ""; return { cwrid: meta.cwrid, coaid: meta.coaid, cwid: cwidM ? cwidM[1] : "", interactive: meta.interactive, title, }; } function findAnyChapterOnPageByCwid(cwid) { const el = findChapterElementByCwid(cwid); if (!el) return null; const brief = readChapterBriefFromElement(el); if (!brief || !brief.cwid) return null; const uid = window.uid || readPageVar("uid"); const cid = readCourseId(); return Object.assign({ uid, cid, el }, brief); } function ensureInteractiveRunReady() { if (!getEnabled()) { setEnabled(true); pushLog("\u5df2\u81ea\u52a8\u5f00\u542f\uff08\u4e92\u52a8\u8bfe\u6d4b\u8bd5\uff09"); } ensurePanelDefaults(); } function rememberInteractiveChapterLaunch(ch) { if (!ch || !ch.cwid) return; const ctx = { uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, lsKey: ch.uid && ch.cwrid && ch.coaid ? ch.uid + ch.cwrid + ch.coaid : "", title: ch.title, }; setHdMetaCookie({ title: ch.title || ch.cwid.slice(0, 8), cmeOrigin: location.origin, jobKey: ctx.lsKey ? jobKey(ctx) : "", cwid: ch.cwid, }); saveHdActive({ jobKey: ctx.lsKey ? jobKey(ctx) : "", cwid: ch.cwid, title: ch.title || ch.cwid.slice(0, 8), cmeOrigin: location.origin, launchedAt: Date.now(), manual: true, }); } function installInteractiveChapterClickHook() { if (!isHdInteractiveEnabled()) return; if (window.__hyInteractiveClickHook) return; window.__hyInteractiveClickHook = true; document.addEventListener( "click", (e) => { if (pageType() !== "course") return; const courseEl = e.target.closest(".course[data-href]"); if (!courseEl || !isInteractiveChapter(courseEl)) return; const ch = readChapterBriefFromElement(courseEl); if (!ch || !ch.cwid) return; const uid = window.uid || readPageVar("uid"); const cid = readCourseId(); rememberInteractiveChapterLaunch(Object.assign({ uid, cid }, ch)); ensureInteractiveRunReady(); pushLog(`[\u4e92\u52a8] \u6253\u5f00\u300c${ch.title || ch.cwid.slice(0, 8)}\u300d· \u8df3\u8f6c\u540e\u81ea\u52a8\u8dd1 API…`); }, true ); } async function runWareHdPage() { if (!isHdInteractiveEnabled()) return; const cwid = new URLSearchParams(location.search).get("cwid"); if (!cwid) { pushLog("[\u4e92\u52a8] \u5165\u53e3\u9875\u7f3a\u5c11 cwid"); return; } ensureInteractiveRunReady(); if (isHdLaunchReady()) { pushLog("[\u4e92\u52a8] \u5df2\u643a\u5e26\u542f\u52a8\u53c2\u6570\uff0c\u5f00\u59cb API…"); scheduleHdInteractiveBoot(); return; } let meta = readHdMetaCookie(); if (!meta || String(meta.cwid || "").toUpperCase() !== String(cwid).toUpperCase()) { const onPage = findAnyChapterOnPageByCwid(cwid); rememberInteractiveChapterLaunch( onPage || { cwid, title: cwid.slice(0, 8), uid: "", cid: "", cwrid: "", coaid: "" } ); } pushLog(`[\u4e92\u52a8] \u89e3\u6790\u5165\u53e3 cwid=${cwid.slice(0, 8)}…`); const probe = await probeHdChapterEntry(cwid); if (probe.status === "done") { pushLog("[\u4e92\u52a8] \u8be5\u4e92\u52a8\u8bfe\u5df2\u5b8c\u6210"); location.replace( `${location.origin}/pages/exam_result_hd.aspx?businessCustomParams=${encodeURIComponent(cwid)}` ); return; } const launchUrl = probe.status === "hdbl" && probe.url ? probe.url : probe.status === "notice" && probe.url ? probe.url.startsWith("http") ? probe.url : new URL(probe.url, location.origin).href : buildHdCmeEntryUrl(cwid); if (probe.status === "hdbl" && probe.url) { const p = parseHdLaunchParams(probe.url); if (p.courseWareId && p.userId && p.hash) { saveHdLaunchSnapshot(p, probe.url); } } pushLog("[\u4e92\u52a8] \u8df3\u8f6c\u4e92\u52a8\u8bfe\u9875\u9762…"); location.replace(launchUrl); } function scheduleHdInteractiveBoot() { if (!isHdInteractiveEnabled()) return; if (window.__hyHdBootTimer) return; let tries = 0; const tick = () => { window.__hyHdBootTimer = null; captureHdLaunchParamsEarly(); if (isHdLaunchReady()) { if (!getEnabled()) setEnabled(true); ensurePanelDefaults(); if (!hdRunnerBusy) { const delay = tries === 0 ? 1200 : 400; setTimeout(() => runInteractiveHdFlow(), delay); } return; } tries++; if (tries < 24) { window.__hyHdBootTimer = setTimeout(tick, 500); return; } pushLog("[\u4e92\u52a8] \u672a\u68c0\u6d4b\u5230\u542f\u52a8\u53c2\u6570\uff08hash/courseWareId\uff09\uff0c\u8bf7\u4ece\u8bfe\u7a0b\u9875\u91cd\u65b0\u70b9\u51fb\u8fdb\u5165"); }; tick(); } function readChapterMeta(el) { const ps = getChapterMetaEl(el); if (!ps) return null; const cwrid = ps.getAttribute("data-cwrid") || ""; const coaid = ps.getAttribute("data-coaid") || ""; const maxtime = parseInt(ps.getAttribute("data-maxtime") || "0", 10) || 0; const totaltime = parseInt(ps.getAttribute("data-totaltime") || "0", 10) || 0; const interactive = isInteractiveChapter(el); if (!cwrid || !coaid) return null; if (totaltime <= 0 && !interactive) return null; return { cwrid, coaid, maxtime, totaltime, interactive, metaEl: ps }; } function getChapterStatusParts(el) { const metaEl = getChapterMetaEl(el); const studyState = (metaEl?.getAttribute("data-studystate") || "").replace(/\s+/g, " ").trim(); let progressText = (el.querySelector(".cw-progress-text")?.textContent || "").replace(/\s+/g, " ").trim(); if (!progressText && metaEl?.classList?.contains("play_process")) { progressText = (metaEl.textContent || "").replace(/\s+/g, " ").trim(); } const pctM = progressText.match(/^(\d+)%$/); if (pctM && pctM[1] !== "100") progressText = `\u64ad\u653e\u81f3\uff1a${pctM[1]}%`; const btn = el.querySelector(".cw-status button"); const btnLabel = btn ? (btn.textContent || "").replace(/\s+/g, " ").trim() : ""; let label = btnLabel || studyState; if (!label) { const statusBox = el.querySelector(".cw-status"); if (statusBox) label = (statusBox.textContent || "").replace(/\s+/g, " ").trim(); } if (!label) { const h3 = el.querySelector("h3"); if (h3) { for (const sp of h3.querySelectorAll("span[style]")) { if (sp.classList.contains("play_process")) continue; const t = (sp.textContent || "").replace(/\s+/g, " ").trim(); if (t) { label = t; break; } } } } return { label, progressText, studyStateAttr: metaEl?.getAttribute("data-studystate") || "" }; } function inferHyChapterStatuses(labelText, progressText, studyStateAttr, interactive) { const label = String(labelText || "").replace(/\s+/g, " ").trim(); const progress = String(progressText || "").replace(/\s+/g, " ").trim(); const studyStateNum = parseInt(studyStateAttr || "", 10); const studyDone = /\u5df2\u5b8c\u6210|\u5b66\u4e60\u5b8c\u6bd5|\u5df2\u5b66\u5b8c/.test(label) || studyStateNum >= 2; if (interactive) { const studying = /\u5b66\u4e60\u4e2d/.test(label); const waitStudy = /\u5f85\u5b66\u4e60|\u672a\u5b66\u4e60/.test(label); const partialPct = /\u64ad\u653e\u81f3\uff1a\d+%/.test(progress); const blank = !label; const untouched = !studyDone && !studying && (blank || waitStudy || partialPct || /\u64ad\u653e\u81f3\uff1a/.test(progress) || /\u5f85\u5b66\u4e60/.test(label)); return { videoDone: studyDone, examDone: studyDone, untouched, examStatus: "none", pendingExam: false, progress, }; } const examPassedLabel = /\u8003\u8bd5\u901a\u8fc7|\u5df2\u901a\u8fc7|\u5df2\u7ecf\u901a\u8fc7|\u5408\u683c/.test(label); const pendingExam = /\u5f85\u8003\u8bd5|\u5f85\u8003/.test(label) && !examPassedLabel; const chapterComplete = /\u5df2\u5b8c\u6210/.test(label) && !pendingExam && !/\u5f85\u5b66\u4e60|\u672a\u5b66\u4e60|\u5b66\u4e60\u4e2d/.test(label); const examDone = examPassedLabel || chapterComplete; const videoDone = pendingExam || examDone || studyDone; const partialPct = /\u64ad\u653e\u81f3\uff1a\d+%/.test(progress); const waitStudy = /\u5f85\u5b66\u4e60|\u672a\u5b66\u4e60/.test(label); const studying = /\u5b66\u4e60\u4e2d/.test(label); const blank = !label; const untouched = !videoDone && !studying && (blank || waitStudy || partialPct || /\u64ad\u653e\u81f3\uff1a/.test(progress) || /\u5f85\u5b66\u4e60/.test(label)); const examStatus = examDone ? "done" : pendingExam ? "exam" : "none"; return { videoDone, examDone, untouched, examStatus, pendingExam, progress }; } function isElectiveChapter(el) { if (!el) return true; const tagText = Array.from(el.querySelectorAll(".cw-tag-list span, h3 label")) .map((node) => node.textContent || "") .join(" "); return /\u9009\u4fee/.test(tagText); } function formatChapterVideoTag(ch) { if (ch.interactive) { if (ch.done) return "\u4e92\u52a8\u5df2\u5b8c\u6210"; if (ch.studyState === "\u5b66\u4e60\u4e2d") return "\u4e92\u52a8\u5b66\u4e60\u4e2d"; return "\u89c6\u9891\u5b66\u4e60\u4e2d"; } const live = applyLiveProgressToChapter(ch); if (live.progressLabel && /\u64ad\u653e\u81f3\uff1a\d+%/.test(live.progressLabel)) return live.progressLabel; if (ch.done) return "\u89c6\u9891\u5df2\u5b66\u5b8c"; if (ch.studyState === "\u5b66\u4e60\u4e2d") return "\u89c6\u9891\u5b66\u4e60\u4e2d"; if (ch.progressLabel && /\u64ad\u653e\u81f3\uff1a\d+%/.test(ch.progressLabel)) return ch.progressLabel; if (ch.untouched) return "\u89c6\u9891\u672a\u5b66"; return "\u89c6\u9891\u5b66\u4e60\u4e2d"; } function isChapterNeedStudy(ch) { return !ch.done; } function isChapterNeedExam(ch) { return !!( ch && !ch.interactive && ch.done && !ch.examDone && (ch.pendingExam || ch.examStatus === "exam") ); } function isChapterRegistrable(ch) { return isChapterNeedStudy(ch) || isChapterNeedExam(ch); } function isChapterAutoStudy(ch) { return isChapterNeedStudy(ch) && !ch.interactive; } function formatChapterExamTag(status, ch, course) { if (course && isGongxuCourse(course)) return ch && ch.done ? "\u95ee\u7b54\u9898\u5df2\u5b8c" : "\u89c6\u9891\u95ee\u7b54"; if (ch && ch.interactive) return ch.done ? "\u65e0\u8003\u8bd5·\u5df2\u5b8c\u6210" : "\u65e0\u8003\u8bd5"; if (status === "done") return "\u5df2\u901a\u8fc7"; if (status === "exam") return "\u5f85\u8003\u8bd5"; return "\u672a\u8003\u8bd5"; } function formatChapterPreviewLine(ch, course) { const live = applyLiveProgressToChapter(ch); const dot = live.examDone ? "✓" : live.done ? "◐" : live.untouched ? "○" : "◐"; const videoTag = formatChapterVideoTag(live); const examTag = formatChapterExamTag(live.examStatus, live, course); const suffix = live.interactive ? escapeHtml(videoTag) : `${escapeHtml(videoTag)} · ${escapeHtml(examTag)}`; return `
${dot} ${escapeHtml(live.title)}${suffix}
`; } function parseCourseChapterElements(root, cid, uid) { if (!uid || !cid) return []; const list = []; root.querySelectorAll(".course[data-href]").forEach((el, idx) => { if (isElectiveChapter(el)) return; if (!isHdInteractiveEnabled() && chapterHasInteractiveTag(el)) return; const meta = readChapterMeta(el); if (!meta) return; const { cwrid, coaid, maxtime, totaltime, interactive } = meta; const titleEl = el.querySelector(".cw-title-link strong") || el.querySelector("h3 strong") || el.querySelector("h3 a strong") || el.querySelector("h3 a"); const title = titleEl ? titleEl.textContent.replace(/\s+/g, " ").trim() : `\u7b2c${idx + 1}\u8282`; const { label, progressText, studyStateAttr } = getChapterStatusParts(el); const st = inferHyChapterStatuses(label, progressText, studyStateAttr, interactive); const href = el.getAttribute("data-href") || ""; const cwidM = href.match(/cwid=([^&]+)/i); const hrefCwid = cwidM ? cwidM[1] : ""; list.push({ uid, cid, cwrid, coaid, cwid: interactive ? hrefCwid || cwrid : cwrid, wareCwid: !interactive && hrefCwid && hrefCwid !== cwrid ? hrefCwid : "", interactive: !!interactive, duration: totaltime, startSec: interactive ? 1 : resolveRtEntrySec(maxtime, totaltime), studiedSec: interactive ? 0 : Math.max(maxtime, parseInt(localStorage.getItem(uid + cwrid + coaid) || "0", 10) || 0), lsKey: uid + cwrid + coaid, title, done: st.videoDone, examDone: st.examDone, examStatus: st.examStatus, untouched: st.untouched, studyState: label, progressLabel: st.progress || progressText, }); }); return list; } function parseCourseChaptersFromHtml(html, cid) { const uid = resolveStudyUserId(readVarFromHtml(html, "uid")); const courseId = cid || (html.match(/courseId:\s*["']([^"']+)["']/i) || [])[1] || (html.match(/cid=([^&'"]+)/i) || [])[1] || ""; if (!uid || !courseId) { return { uid, cid: courseId, chapters: [], courseTitle: "" }; } const doc = new DOMParser().parseFromString(html, "text/html"); const chapters = parseCourseChapterElements(doc, courseId, uid); const courseTitle = ( doc.querySelector(".course_lord .info h3 strong")?.textContent || doc.querySelector(".course_lord strong.f14blue")?.textContent || doc.querySelector(".course_title h1")?.textContent || doc.querySelector("h1")?.textContent || "" ) .replace(/\s+/g, " ") .trim(); return { uid, cid: courseId, chapters, courseTitle, gongxu: detectGongxuCourseMeta(doc, courseTitle, html) }; } function parseCourseChapterJobs() { const uid = resolveStudyUserId(window.uid || readPageVar("uid")); const cid = readCourseId(); if (!uid || !cid) return []; return parseCourseChapterElements(document, cid, uid); } function jobToCtx(job) { return { uid: job.uid, cwrid: job.cwrid, coaid: job.coaid, cid: job.cid, cwid: job.cwid, wareCwid: job.wareCwid, groupId: job.groupId, provinceId: job.provinceId, duration: job.duration, baseSec: job.startSec || 1, lsKey: job.lsKey, title: job.title, }; } function buildPlayContext() { const uid = window.uid || readPageVar("uid"); const cwrid = window.cwrid || readPageVar("cwrid"); const coaid = window.coaid || readPageVar("coaid"); const cid = window.cid || readPageVar("cid"); const meta = readWareMeta(uid, cwrid, coaid); let duration = meta.totaltime; try { const pd = Math.floor(window.player.j2s_getDuration()); if (pd > 0) duration = pd; } catch (_) {} duration = Math.max(60, duration || 3600); return { uid, cwrid, coaid, cid, groupId: readGroupId(), provinceId: readProvinceId(), duration, baseSec: resolveStartSec(meta.maxtime, duration), lsKey: meta.lsKey, title: (document.title || cwrid || "").slice(0, 24), }; } function jobKey(ctx) { return `${ctx.uid}:${ctx.cwrid}:${ctx.coaid}`; } function loadJobs() { try { return JSON.parse(localStorage.getItem(JOBS_KEY) || "{}"); } catch (_) { return {}; } } function saveJobs(jobs) { localStorage.setItem(JOBS_KEY, JSON.stringify(jobs)); } function getJob(ctx) { return loadJobs()[jobKey(ctx)] || null; } function saveJob(ctx, patch) { const jobs = loadJobs(); const key = jobKey(ctx); jobs[key] = Object.assign({}, jobs[key], patch, { uid: ctx.uid, cwrid: ctx.cwrid, coaid: ctx.coaid, cid: ctx.cid, groupId: ctx.groupId, provinceId: ctx.provinceId, lsKey: ctx.lsKey, title: ctx.title || (jobs[key] && jobs[key].title) || key.slice(-8), }); saveJobs(jobs); } function apiFetch(url, bodyObj) { return fetch(url, { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, body: new URLSearchParams(bodyObj).toString(), __hyEngineMark: true, }) .then((r) => r.text()) .then(parseApiResponse); } function isApiLoginLost(text) { if (!text) return false; return ( text.indexOf("%2fsecure%2flogin_sso.aspx") > -1 || text.indexOf("/secure/login_sso.aspx") > -1 || (text.length < 600 && /checkLogin/i.test(text)) ); } function isLoggedInPageHtml(text) { if (!text) return false; return /var\s+uid\s*=|course_ware_polyv|id=["']player["']|window\.player|polyvPlayer|class=["']course["'][^>]*data-href|\u8bfe\u7a0b\u5b66\u4e60\u4e0e\u8003\u8bd5|ul\.sub_ul|course_collect\.aspx/i.test( text ); } function isPageLoginRedirect(text) { if (!text) return false; if (isLoggedInPageHtml(text)) return false; return ( /login_sso\.aspx/i.test(text) || /%2fsecure%2flogin_sso/i.test(text) || (text.length < 800 && /checkLogin/i.test(text)) ); } function warnLoginIfNeeded(quiet) { if (quiet || Date.now() - loginWarnAt <= 600000) return; loginWarnAt = Date.now(); pushLog("⚠ \u767b\u5f55\u53ef\u80fd\u5df2\u5931\u6548\uff0c\u8bf7\u5728\u672c\u9875 F5 \u5237\u65b0\u91cd\u65b0\u767b\u5f55\uff0c\u6536\u5c3e\u4f1a\u81ea\u52a8\u91cd\u8bd5"); } function parseApiResponse(text) { if (!text) return { code: -1, msg: "empty" }; if (isApiLoginLost(text)) return { code: -2, msg: "\u767b\u5f55\u5df2\u5931\u6548\uff0c\u8bf7\u5237\u65b0\u9875\u9762" }; const m = text.match(/\{"code"[\s\S]*\}/); if (m) { try { return JSON.parse(m[0]); } catch (_) {} } try { return JSON.parse(text); } catch (_) { return { code: -1, msg: String(text).slice(0, 120) }; } } function apiGet(url, params) { const qs = new URLSearchParams(params).toString(); return fetch(url + (url.includes("?") ? "&" : "?") + qs, { credentials: "include", __hyEngineMark: true, }) .then((r) => r.text()) .then(parseApiResponse); } function normalizeProcessExamQuestions(raw) { if (Array.isArray(raw)) return raw; if (raw && Array.isArray(raw.listBody)) return raw.listBody; if (raw && Array.isArray(raw.body)) return raw.body; if (raw && Array.isArray(raw.list)) return raw.list; return []; } function pickProcessExamUserAnswer(choices) { if (!Array.isArray(choices) || !choices.length) return null; let correctIdx = -1; choices.forEach((c, i) => { if (c && Number(c.right_answer) === 1) correctIdx = i; }); if (correctIdx < 0) { choices.forEach((c, i) => { if (c && Number(c.right_answer) === i) correctIdx = i; }); } if (correctIdx < 0) correctIdx = 0; return { letter: String.fromCharCode(65 + correctIdx), isRight: 1 }; } async function resolveWareCwid(ctx, job) { const j = job || (ctx ? getJob(ctx) : null); const cached = (j && j.wareCwid) || (ctx && ctx.wareCwid) || ""; if (cached) return cached; try { const ai = await apiGet("/ashx/getAiQuestionList.ashx", { relationId: ctx.cwrid }); const wid = ai && ai.listBody && ai.listBody[0] && ai.listBody[0].course_ware_id; if (wid) { if (j) saveJob(ctx, { wareCwid: wid }); return wid; } } catch (_) {} try { const url = "/course_ware/course_ware_polyv.aspx?cwid=" + encodeURIComponent(ctx.cwrid) + "&ff=0&ft=0&t=0"; const html = await fetch(url, { credentials: "include", __hyEngineMark: true }).then((r) => r.text() ); const m = html.match(/var\s+cwid\s*=\s*['"]([^'"]+)['"]/i); if (m && m[1]) { if (j) saveJob(ctx, { wareCwid: m[1] }); return m[1]; } } catch (_) {} return ""; } async function submitChapterProcessExams(ctx, job, label) { const tag = label || (ctx && ctx.title) || (ctx && ctx.cwrid ? ctx.cwrid.slice(0, 8) : "\u7ae0\u8282"); const wareCwid = await resolveWareCwid(ctx, job); if (!wareCwid) { pushLog(`[${tag}] \u516c\u9700\u8bfe\uff1a\u672a\u83b7\u53d6\u8bfe\u4ef6ID\uff0c\u8df3\u8fc7\u89c6\u9891\u5185\u95ee\u7b54\u9898`); return { ok: false, count: 0, total: 0 }; } let questions = []; try { const raw = await apiGet("/ashx/get_course_ware_process.ashx", { cwid: wareCwid, video_type: "polyv", }); questions = normalizeProcessExamQuestions(raw); } catch (e) { pushLog(`[${tag}] \u516c\u9700\u8bfe\uff1a\u62c9\u53d6\u89c6\u9891\u95ee\u7b54\u9898\u5931\u8d25`); return { ok: false, count: 0, total: 0 }; } if (!questions.length) return { ok: true, count: 0, total: 0 }; let okCount = 0; for (const q of questions) { const examid = q.examid || q.questionId || q.question_id; if (!examid) continue; const pick = pickProcessExamUserAnswer(q.choices); if (!pick) continue; try { const res = await apiFetch("/ashx/addCourseWareProcessExamLog.ashx", { userId: ctx.uid, relationId: ctx.cwrid, questionId: examid, isRight: pick.isRight, userAnswer: pick.letter, videoSource: 2, }); if (res && res.code === 0) okCount++; await sleep(200); } catch (_) {} } if (okCount > 0) { pushLog(`[${tag}] \u516c\u9700\u8bfe\uff1a\u5df2\u63d0\u4ea4\u89c6\u9891\u5185\u95ee\u7b54\u9898 ${okCount}/${questions.length}`); pushUserLog(`${tag} · \u89c6\u9891\u95ee\u7b54\u9898 ${okCount}/${questions.length}`); } return { ok: okCount >= questions.length, count: okCount, total: questions.length }; } async function warmPlaySession(ctx, quiet) { const cwid = ctx.cwid || ctx.cwrid; if (!cwid) return false; const label = ctx.title || ctx.cwrid.slice(0, 8); if (!quiet) pushLog(`[${label}] \u540e\u53f0\u9884\u70ed\u64ad\u653e\u9875\uff08\u5237\u65b0\u767b\u5f55\u6001\uff09…`); const url = "/course_ware/course_ware_polyv.aspx?cwid=" + encodeURIComponent(cwid) + "&ff=0&ft=0&t=0"; const text = await fetch(url, { credentials: "include", __hyEngineMark: true }).then((r) => r.text() ); if (isPageLoginRedirect(text)) { warnLoginIfNeeded(quiet); return false; } if (!quiet) await sleep(800); return true; } function postProcess(ctx, status, sec) { if (HY_ENGINE_REQUIRED) { return Promise.reject(new Error("HY_ENGINE_ONLY")); } const sessionUid = currentSessionUserId(); if (sessionUid && ctx.uid && !userIdsMatch(sessionUid, ctx.uid)) { trace( `postProcess \u62e6\u622a\uff1asession=${sessionUid} job.uid=${ctx.uid} status=${status} cwrid=${ctx.cwrid}` ); purgeStaleJobsForSessionUid(sessionUid); if (getEnabled()) stopAutoStudy("\u7528\u6237ID\u4e0e\u5f53\u524d\u767b\u5f55\u4e0d\u4e00\u81f4\uff0c\u5df2\u505c\u6b62"); return Promise.resolve({ code: -3, msg: "\u7528\u6237id\u4e0d\u5339\u914d(\u672c\u5730\u4efb\u52a1\u4e0e\u5f53\u524d\u767b\u5f55\u4e0d\u4e00\u81f4)" }); } return apiFetch("/ashx/addCourseWareProcess.ashx?r=" + Math.random(), { group_id: ctx.groupId, course_organ_assign_id: ctx.coaid, relation_id: ctx.cwrid, user_id: ctx.uid, province_id: ctx.provinceId, player_current_time: Math.floor(sec), course_id: ctx.cid, status, }).then((data) => { traceProgressPostResult(ctx, data, `status=${status}@${sec}`); return data; }); } function postPlayRecord(ctx, sec) { if (HY_ENGINE_REQUIRED) { return Promise.reject(new Error("HY_ENGINE_ONLY")); } return apiGet("/ashx/addCourseWarePlayRecord.ashx", { relation_id: ctx.cwrid, user_id: ctx.uid, group_id: ctx.groupId, player_current_time: Math.floor(sec), }); } function syncLocal(ctx, sec) { try { const studied = Math.max(0, Math.floor(sec || 0)); localStorage.setItem(ctx.lsKey, String(studied)); const row = document.querySelector( `.cw-progress-row[data-cwrid="${ctx.cwrid}"][data-coaid="${ctx.coaid}"]` ); const total = (row && parseInt(row.getAttribute("data-totaltime") || "0", 10)) || ctx.duration || 3600; if (row) row.setAttribute("data-maxtime", String(studied)); updateDomChapterProgress(ctx.cwrid, ctx.coaid, studied, total); syncPanelChapterProgress(ctx, studied, total); } catch (_) {} } function updateDomChapterProgress(cwrid, coaid, sec, total) { const row = document.querySelector( `.cw-progress-row[data-cwrid="${cwrid}"][data-coaid="${coaid}"]` ); if (!row) return; const pct = formatPlayProgressPct(sec, total); const courseEl = row.closest(".course[data-href]"); if (!courseEl) return; const pt = courseEl.querySelector(".play_process .cw-progress-text") || courseEl.querySelector(".cw-progress-text") || (row.classList.contains("play_process") ? row : null); if (pt) { pt.textContent = pct >= 100 ? "100%" : `${pct}%`; } } function syncPanelChapterProgress(ctx, sec, total) { const label = formatPlayProgressPct(sec, total) >= 100 ? "\u64ad\u653e\u81f3\uff1a100%" : formatProgressLabelFromSec(sec, total); const patch = (ch) => { if (!ch || ch.lsKey !== ctx.lsKey) return; ch.progressLabel = label.replace("\u89c6\u9891\u5df2\u5b66\u5b8c", "\u64ad\u653e\u81f3\uff1a100%"); ch.studiedSec = sec; if (formatPlayProgressPct(sec, total) < 100) { ch.done = false; ch.studyState = "\u5b66\u4e60\u4e2d"; } }; panelState.courses.forEach((c) => (c.chapters || []).forEach(patch)); panelState.chapterPreview.forEach((c) => (c.chapters || []).forEach(patch)); if (document.getElementById("hy-cme-chapter-preview")) renderChapterPreview(); } function engineCtxFromJob(job) { if (!job) return {}; return { uid: job.uid, cwrid: job.cwrid, coaid: job.coaid, cid: job.cid, cwid: job.cwid || job.cwrid, wareCwid: job.wareCwid || "", groupId: job.groupId, provinceId: job.provinceId, duration: job.duration || 3600, startSec: job.startSec, studiedSec: job.studiedSec, title: job.title, lsKey: job.lsKey, }; } async function _es(kind, context, cfg) { await _cl(false); const data = await _hr(_w(0), "POST", { kind: String(kind || ""), context: context || {}, config: cfg || {}, }); syncHyCloudQuotaFromResponse(data); return data; } async function _ep(sessionId, event, lastResult, contextPatch) { await _cl(false); const data = await _hr(_w(1), "POST", { session_id: String(sessionId || ""), event: String(event || "tick"), last_result: lastResult == null ? null : lastResult, context_patch: contextPatch == null ? null : contextPatch, }); syncHyCloudQuotaFromResponse(data); return data; } async function executeHyEngineCaptcha(cmd) { const examId = cmd.exam_id || ""; const html = cmd.page_html || ""; const tag = examId.slice(0, 8); const cap = await tryPassHyExamCaptcha(examId, tag, html, cmd.referer || ""); return { code: cap.ok ? cap.code || "ok" : "", ok: !!cap.ok, msg: cap.msg || "" }; } async function _xc(cmd, ctx) { const c = cmd || {}; const t = String(c.type || ""); if (t === "wait") { return { pause: true, ms: Math.max(500, Number(c.ms || 1000)) }; } if (t === "done") { return { terminal: true, ok: c.success !== false, skipped: !!c.skipped, score: c.score, studied_sec: c.studied_sec, early_finish: !!c.early_finish, filled: !!c.filled, }; } if (t === "failed") { return { terminal: true, ok: false, msg: c.message || "failed" }; } if (t === "local_sync") { const sec = Math.max(0, Number(c.sec || 0)); if (ctx) { ensureRtWallClock(ctx); syncLocal(ctx, sec); saveEnginePostedSec(ctx, sec); } return { event: "local_sync_done", lastResult: { ok: true, sec } }; } if (t === "document_fetch") { const path = c.path || c.url || ""; const text = await fetch(path, { credentials: "include", __hyEngineMark: true }).then((r) => r.text()); if (isPageLoginRedirect(text)) warnLoginIfNeeded(true); return { event: c.event || "submit_result", lastResult: { text, status: isPageLoginRedirect(text) ? 401 : 200, url: path }, }; } if (t === "platform_post") { if (typeof c.body === "string") { const res = await fetch(c.path, { method: "POST", credentials: "include", headers: Object.assign({ "Content-Type": "application/x-www-form-urlencoded" }, c.headers || {}), body: c.body, __hyEngineMark: true, }); const text = await res.text(); const data = parseApiResponse(text); return { event: c.event || "process_result", lastResult: { text, data, status: res.status, url: c.path } }; } const data = await apiFetch(c.path, c.payload || {}); if (ctx && c.payload && c.payload.player_current_time != null) { traceProgressPostResult(ctx, data, c.label || "platform_post"); const sessionUid = currentSessionUserId(); const jobUid = String((c.payload && c.payload.user_id) || ctx.uid || "").trim(); if (sessionUid && jobUid && !userIdsMatch(sessionUid, jobUid)) { trace(`engine platform_post uid\u4e0d\u4e00\u81f4 session=${sessionUid} job=${jobUid}`); purgeStaleJobsForSessionUid(sessionUid); if (getEnabled()) stopAutoStudy("\u7528\u6237ID\u4e0e\u5f53\u524d\u767b\u5f55\u4e0d\u4e00\u81f4\uff0c\u5df2\u505c\u6b62"); return { event: c.event || "process_result", lastResult: { text: JSON.stringify({ code: -3, msg: "\u7528\u6237id\u4e0d\u5339\u914d" }), data: { code: -3, msg: "\u7528\u6237id\u4e0d\u5339\u914d" }, status: 200, url: c.path, }, }; } const code = data && data.code; if (code === 0 || code === 3) saveEnginePostedSec(ctx, c.payload.player_current_time); } return { event: c.event || "process_result", lastResult: { text: JSON.stringify(data), data, status: 200, url: c.path }, }; } if (t === "platform_get") { const data = await apiGet(c.path, c.params || {}); return { event: c.event || "play_record_result", lastResult: { text: JSON.stringify(data), data, status: 200, url: c.path }, }; } if (t === "captcha") { const cap = await executeHyEngineCaptcha(c); return { event: c.event || "captcha_result", lastResult: cap }; } return { event: "tick", lastResult: null }; } async function _ve(startRes, options) { const opts = options || {}; const ctx = opts.ctx; let sessionId = String((startRes && startRes.session_id) || ""); let cmd = startRes && startRes.command; const maxSteps = Number(opts.maxSteps) || 64; trace(startRes && startRes.log); for (let i = 0; i < maxSteps; i++) { if (!cmd) { const next = await _ep(sessionId, "tick", null, null); sessionId = String(next.session_id || sessionId); trace(next.log); cmd = next.command; } if (!cmd) break; if (cmd.type === "wait") { return { pause: true, sessionId, ms: Math.max(500, Number(cmd.ms || 1000)), ok: true }; } const out = await _xc(cmd, ctx); if (out.pause) return Object.assign({ sessionId }, out); if (out.terminal) return Object.assign({ sessionId }, out); const next = await _ep(sessionId, out.event, out.lastResult, null); sessionId = String(next.session_id || sessionId); trace(next.log); cmd = next.command; } return { terminal: true, ok: false, msg: "\u4e91\u7aef\u5f15\u64ce\u6b65\u6570\u8d85\u9650", sessionId }; } async function _vf(job, kind, cfg, options) { const ctx = engineCtxFromJob(job); const opts = Object.assign({ ctx, maxSteps: 64 }, options || {}); let sessionId = job.engineSessionId || ""; let startRes; if (sessionId) { startRes = { session_id: sessionId, command: null }; } else { startRes = await _es(kind, ctx, cfg || {}); sessionId = String(startRes.session_id || ""); saveJob(jobToCtx(job), { engineSessionId: sessionId }); } return _ve(startRes, opts); } function applyHyEnginePause(ctx, loopOut) { if (!loopOut || !loopOut.pause) return; ensureRtWallClock(ctx); const ms = loopOut.ms || RT_STEP_GAP_MS; saveJob(ctx, { engineSessionId: loopOut.sessionId || "", nextStepDue: Date.now() + ms, }); if (ms >= RT_STEP_GAP_MS * 0.85) { const job = getJob(ctx) || ctx; pushUserChapterStudyLog(job, ms); } } function handleHyEngineFailure(ctx, job, label, loopOut, tag) { const now = Date.now(); const msg = String(loopOut?.msg || "\u5931\u8d25"); if (/free_quota_exhausted|\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c/i.test(msg)) { syncHyCloudQuotaFromResponse({ tier: "free", free_used_chapters: cloudState.freeChapterLimit }); updatePanelCloudStatus(); pushUserLog(`\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${cloudState.freeUsedChapters}/${cloudState.freeChapterLimit} \u8282\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`); stopAutoStudy("\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u7a0b\u5e8f\u5df2\u505c\u6b62"); return; } const isLoginLost = /login_lost/i.test(msg); if (isLoginLost) { warnLoginIfNeeded(false); pushLog(`[${label}] ${tag} \u767b\u5f55\u6001\u5f02\u5e38\uff0c\u5df2\u6e05\u9664\u4f1a\u8bdd · \u8bf7 F5 \u5237\u65b0\u540e\u81ea\u52a8\u91cd\u8bd5`); pushUserLog(`${label} · \u767b\u5f55\u6001\u5f02\u5e38\uff0c\u8bf7\u5237\u65b0\u9875\u9762`); } else { pushLog(`[${label}] ${tag} \u4e91\u7aef\u5f15\u64ce\uff1a${msg}`); } saveJob(ctx, { nextStepDue: now + (isLoginLost ? 120000 : 60000), engineSessionId: "", }); } async function handleHyEngineVideoDone(ctx, job, label, loopOut) { const studiedSec = loopOut.studied_sec != null ? loopOut.studied_sec : job.duration; saveJob(ctx, { engineSessionId: "", nextStepDue: 0, progressFilled: true, status2Sent: true, lastPostedSec: studiedSec }); await finalizeVideoChapterDone(ctx, job, label, studiedSec, ""); return true; } function applyLiveProgressToChapter(ch) { const jobs = loadJobs(); const j = jobs[ch.lsKey]; if (!j || j.phase === "done") return ch; const sec = resolveWallClockStudiedSec(j) || (j.lastPostedSec != null ? j.lastPostedSec : j.rtEntrySec != null ? j.rtEntrySec : j.startSec); if (sec == null || sec <= 0) return ch; const dur = j.duration || ch.duration || 3600; const pct = formatPlayProgressPct(sec, dur); return Object.assign({}, ch, { progressLabel: pct >= 100 ? "\u64ad\u653e\u81f3\uff1a100%" : `\u64ad\u653e\u81f3\uff1a${pct}%`, studiedSec: sec, done: pct >= 100 ? ch.done : false, }); } function startPreviewAutoRefresh() { stopPreviewAutoRefresh(); panelState.previewRefreshTimer = setInterval(() => { if (!getEnabled()) return; if (pageType() === "course") { const chapters = parseCourseChapterJobs(); chapters.forEach((ch) => { const live = applyLiveProgressToChapter(ch); syncPanelChapterProgress( { lsKey: ch.lsKey, cwrid: ch.cwrid, coaid: ch.coaid, duration: ch.duration }, live.studiedSec != null ? live.studiedSec : readChapterStudiedSec(ch.uid, ch.cwrid, ch.coaid, ch.duration), ch.duration ); }); } renderChapterPreview(); }, 5000); } function stopPreviewAutoRefresh() { if (panelState.previewRefreshTimer) { clearInterval(panelState.previewRefreshTimer); panelState.previewRefreshTimer = null; } } async function postProcessOnce(ctx, status, sec) { try { return await postProcess(ctx, status, sec); } catch (e) { return { code: -1, msg: e && e.message ? e.message : String(e) }; } } function isRtIgnorableCode3(res) { return !!(res && res.code === 3); } function handleRealtimeApiMiss(job, ctx, res, label, tag, stepGapMs) { const now = Date.now(); const gap = stepGapMs || RT_STEP_GAP_MS; const code = res && res.code != null ? res.code : "?"; const msg = res && res.msg ? " " + res.msg : ""; if (res && res.code === -2) { warnLoginIfNeeded(false); pushLog(`[${label}] ${tag} \u767b\u5f55\u5931\u6548\uff0c\u8bf7 F5 \u5237\u65b0\u540e\u81ea\u52a8\u91cd\u8bd5${msg}`); saveJob(ctx, { nextStepDue: now + 60000 }); return false; } if (isRtIgnorableCode3(res)) { pushLog( `[${label}] ${tag} code=3 \u9650\u6d41\uff08\u5df2\u5ffd\u7565\uff09\uff0c${Math.round(gap / 60000)}\u5206\u949f\u540e\u6309\u65e2\u5b9a\u95f4\u9694\u7ee7\u7eed` ); saveJob(ctx, { nextStepDue: now + gap }); return false; } pushLog(`[${label}] ${tag} code=${code}${msg}\uff0c${Math.round(gap / 60000)}\u5206\u949f\u540e\u91cd\u8bd5`); saveJob(ctx, { nextStepDue: now + gap }); return false; } function markRtEntered(job, ctx, entrySec, duration, now, code3Ignored) { saveJob(ctx, { rtEntered: true, rtStartedAt: now, rtEntrySec: entrySec, lastPostedSec: entrySec, nextStepDue: now + RT_STEP_GAP_MS, rtStepCount: 1, }); const suffix = code3Ignored ? " code=3 \u9650\u6d41\uff08\u5df2\u5ffd\u7565\uff09" : ""; pushLog( `[${labelFromJob(job)}] [1:1] \u8fdb\u5165 status=0@${entrySec}s${suffix}\uff0c${formatRtStudyLine(entrySec, duration)}\uff0c${RT_STEP_SEC / 60}\u5206\u949f\u540e status=1` ); } function labelFromJob(job) { return job.title || (job.cwrid || "").slice(0, 8); } async function postProcessCode3Retry(ctx, status, sec) { return postProcessOnce(ctx, status, sec); } async function instantFillToFull(ctx, job) { const label = ctx.title || ctx.cwrid.slice(0, 8); const start = ctx.baseSec || (job && job.startSec) || 1; const duration = ctx.duration; const warmed = await warmPlaySession(ctx, true); if (!warmed) { pushLog(`[${label}] \u9884\u70ed\u5931\u8d25\uff0c\u8bf7\u5148 F5 \u767b\u5f55`); return false; } await sleep(800); const r0 = await postProcessCode3Retry(ctx, 0, start); if (!r0 || r0.code !== 0) { pushLog( `[${label}] \u8fdb\u5165 status=0 code=${r0 && r0.code != null ? r0.code : "?"}${r0 && r0.msg ? " " + r0.msg : ""}` ); if (!r0 || r0.code === 3) return false; } await sleep(500); const r2 = await postProcessCode3Retry(ctx, 2, duration); pushLog( `[${label}] \u6ee1\u8fdb\u5ea6 status=2@${duration}s code=${r2 && r2.code != null ? r2.code : "?"}${r2 && r2.msg ? " " + r2.msg : ""}` ); if (r2 && r2.code === 0) { syncLocal(ctx, duration); return true; } return false; } function formatOfflineFillUserLine(job, index, total) { const course = job.cid ? findPanelCourseByCid(job.cid) : null; const coursePrefix = course?.title ? `\u300c${course.title}\u300d` : ""; const chapter = resolveChapterDisplayTitle(job); return `\u6279\u91cf\u62c9\u6ee1\u4e2d ${index}/${total} · ${coursePrefix}${chapter}`; } async function fillChapterSegmentsToFull(ctx, job, fillProgress) { const label = ctx.title || ctx.cwrid.slice(0, 8); if (fillProgress) { pushUserLog(formatOfflineFillUserLine(job, fillProgress.index, fillProgress.total)); } if (!HY_ENGINE_REQUIRED) { return false; } try { let liveJob = Object.assign({}, job, loadJobs()[jobKey(ctx)] || {}); let sessionId = String(liveJob.engineSessionId || ""); const maxRounds = 48; for (let round = 0; round < maxRounds && getEnabled(); round++) { const loop = await _vf( liveJob, "video_offline_fill", { mode: "offline" }, { ctx, maxSteps: 32 } ); sessionId = String(loop.sessionId || sessionId || ""); if (loop.pause) { saveJob(ctx, { engineSessionId: sessionId, phase: "silent_filling" }); const waitMs = Math.max(500, Number(loop.ms || 1000)); await sleep(waitMs); liveJob = Object.assign({}, liveJob, loadJobs()[jobKey(ctx)] || {}, { engineSessionId: sessionId, }); continue; } if (loop.terminal && loop.ok) { saveJob(ctx, { engineSessionId: "" }); return true; } if (loop.terminal && !loop.ok) { handleHyEngineFailure(ctx, liveJob, label, loop, "[\u79bb\u7ebf]"); saveJob(ctx, { engineSessionId: "" }); return false; } if (!loop.ok) { pushLog(`[${label}] [\u79bb\u7ebf] \u4e91\u7aef\u62c9\u6ee1\u5931\u8d25\uff1a${loop.msg || "\u672a\u77e5"}`); saveJob(ctx, { engineSessionId: "" }); return false; } liveJob = Object.assign({}, liveJob, loadJobs()[jobKey(ctx)] || {}, { engineSessionId: sessionId, }); } pushLog(`[${label}] [\u79bb\u7ebf] \u4e91\u7aef\u62c9\u6ee1\u6b65\u6570\u8d85\u9650`); return false; } catch (e) { pushLog(`[${label}] [\u79bb\u7ebf] \u4e91\u7aef\u5f15\u64ce\uff1a${e && e.message ? e.message : e}`); return false; } } function getNextRtStep(last, duration) { const step = RT_STEP_SEC; const next = last + step; const tail = duration % step; if (last >= duration - 2) return { type: "done" }; if (tail > 0 && next >= duration - tail) { return { type: "finish", sec: duration, status: 2, gapMs: Math.max(30000, (duration - last) * 1000), final: true, }; } if (next >= duration - 2) { return { type: "finish", sec: duration, status: 2, gapMs: Math.max(30000, (duration - last) * 1000), final: true, }; } return { type: "progress", sec: next, status: 1, gapMs: RT_STEP_GAP_MS }; } function formatRtStudyLine(studiedSec, totalSec) { const studied = Math.max(0, Math.round((studiedSec || 0) / 60)); const total = Math.max(1, Math.round((totalSec || 0) / 60)); return `\u5df2\u5b66 ${studied}\u5206\u949f/\u603b${total}\u5206\u949f`; } function countRtPlanSteps(duration, start) { let last = start; let n = 1; for (;;) { const s = getNextRtStep(last, duration); if (s.type === "done") break; n++; if (s.final) break; last = s.sec; } return n; } function planRealtimeCourse(job) { const duration = job.duration || 3600; const studied = job.studiedSec != null ? job.studiedSec : readChapterStudiedSec(job.uid, job.cwrid, job.coaid, duration); const entrySec = job.startSec != null ? job.startSec : resolveRtEntrySec(studied, duration); return { stepCount: countRtPlanSteps(duration, entrySec), stepGapMs: RT_STEP_GAP_MS, courseMin: Math.round(duration / 60), entrySec, studiedSec: studied, }; } function getRealtimeFinishAt(job) { if (!job || !job.rtStartedAt) return 0; return job.rtStartedAt + (job.duration || 3600) * 1000; } function assignSilentBatchClock(jobs) { const now = Date.now(); const st = loadSilentState(); const sessionStart = st.sessionStart || now; const filled = Object.entries(jobs).filter(([, j]) => j && j.phase === "silent_filled"); if (!filled.length) return 0; const totalMs = filled.reduce((s, [, j]) => s + (j.duration || 3600) * 1000, 0); const batchCompleteDue = sessionStart + totalMs + OFFLINE_BATCH_BUFFER_MS; filled.forEach(([key, job]) => { job.phase = "silent_clock"; job.progressFilled = true; job.status2Sent = true; job.lastPostedSec = job.duration; jobs[key] = job; }); saveSilentState({ sessionStart, filledAt: now, totalMs, totalSec: Math.round(totalMs / 1000), totalCourses: filled.length, batchBufferMs: OFFLINE_BATCH_BUFFER_MS, batchCompleteDue, beijingDue: formatBeijingDateTime(batchCompleteDue), filledUserLogKey: "", waitUserLogKey: "", readyUserLogKey: "", lastWaitUserLogAt: 0, }); saveJobs(jobs); return totalMs; } function readPanelCollapsed() { return localStorage.getItem(PANEL_COLLAPSED_KEY) === "1"; } function writePanelCollapsed(collapsed) { localStorage.setItem(PANEL_COLLAPSED_KEY, collapsed ? "1" : "0"); } function readPanelPos() { try { return JSON.parse(localStorage.getItem(PANEL_POS_KEY) || "{}"); } catch (_) { return null; } } function writePanelPos(left, top) { localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left, top })); } function clampPanelInViewport(panel) { if (!panel) return; const rect = panel.getBoundingClientRect(); const maxLeft = Math.max(0, window.innerWidth - panel.offsetWidth); const maxTop = Math.max(0, window.innerHeight - panel.offsetHeight); const left = Math.max(0, Math.min(maxLeft, rect.left)); const top = Math.max(0, Math.min(maxTop, rect.top)); panel.style.right = "auto"; panel.style.bottom = "auto"; panel.style.left = `${left}px`; panel.style.top = `${top}px`; writePanelPos(left, top); } function applyPanelCollapsed(panel, collapsed) { const btnMin = panel.querySelector("#hy-cme-btn-min"); const btnMax = panel.querySelector("#hy-cme-btn-max"); panel.classList.toggle("cme-panel-min", !!collapsed); panel.classList.toggle("cme-panel-max", !collapsed); if (btnMin) btnMin.style.display = collapsed ? "none" : ""; if (btnMax) btnMax.style.display = collapsed ? "" : "none"; const footerExtra = panel.querySelector(".cme-footer-extra"); if (footerExtra) footerExtra.style.display = collapsed ? "none" : ""; clampPanelInViewport(panel); } function enablePanelDrag(panel) { const header = panel.querySelector("#hy-cme-panel-header"); if (!header) return; let dragging = false; let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; header.addEventListener("mousedown", (e) => { if (e.target?.closest("#hy-cme-panel-controls, .cme-panel-ctl")) return; dragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; panel.style.right = "auto"; panel.style.bottom = "auto"; e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (!dragging) return; const left = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, startLeft + (e.clientX - startX))); const top = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, startTop + (e.clientY - startY))); panel.style.left = `${left}px`; panel.style.top = `${top}px`; }); document.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; const rect = panel.getBoundingClientRect(); writePanelPos(rect.left, rect.top); }); } function switchPanelTab(name) { document.querySelectorAll("#hy-cme-auto-panel .cme-tab-btn").forEach((el) => { el.classList.toggle("active", el.dataset.tab === name); }); document.querySelectorAll("#hy-cme-auto-panel .cme-pane").forEach((el) => { el.classList.toggle("active", el.dataset.pane === name); }); } async function clearJobQueue() { const jobs = loadJobs(); const keys = Object.keys(jobs); if (!keys.length) { pushLog("\u961f\u5217\u5df2\u662f\u7a7a\u7684"); return; } const active = keys.filter((k) => jobs[k] && jobs[k].phase !== "done").length; const message = active ? `\u5f53\u524d\u6709 ${active} \u8282\u8fdb\u884c\u4e2d\u3001\u5171 ${keys.length} \u6761\u4efb\u52a1\u3002\n\u6e05\u7a7a\u540e\u4e0d\u5f71\u54cd\u7f51\u7ad9\u4e0a\u7684\u5b66\u4e60\u8bb0\u5f55\uff0c\u662f\u5426\u7ee7\u7eed\uff1f` : `\u5171 ${keys.length} \u6761\u4efb\u52a1\u5c06\u88ab\u5220\u9664\uff0c\u662f\u5426\u7ee7\u7eed\uff1f`; const ok = await askHyPanelConfirm({ title: "\u6e05\u7a7a\u961f\u5217", message, okText: "\u6e05\u7a7a\u961f\u5217", cancelText: "\u53d6\u6d88", }); if (!ok) return; saveJobs({}); resetSilentState(); panelState.lastCourseListKey = ""; updateJobsPanel(); updatePanelCloudStatus(); renderCourseList(); void refreshChapterPreview(); pushLog(`\u5df2\u6e05\u7a7a\u961f\u5217\uff08${keys.length} \u6761\u4efb\u52a1\uff09`); } function setPanelHint(text) { const el = document.getElementById("hy-cme-start-hint"); if (!el) return; if (text) { el.textContent = String(text); el.style.display = "block"; } else { el.textContent = ""; el.style.display = "none"; } } function clearPanelHint() { setPanelHint(""); } function syncStartStopButtons() { const running = getEnabled(); const startBtn = document.getElementById("hy-cme-start"); const stopBtn = document.getElementById("hy-cme-stop"); const modeSel = document.getElementById("hy-cme-run-mode"); const clearBtn = document.getElementById("hy-cme-clear-queue"); if (startBtn) { startBtn.classList.toggle("cme-btn-off", running); startBtn.disabled = running; } if (stopBtn) { stopBtn.classList.toggle("cme-btn-off", !running); stopBtn.disabled = !running; } if (modeSel) modeSel.disabled = running; if (clearBtn) clearBtn.disabled = running; if (!running) clearLiveStudyStatusBar(); } let modeHintCollapseTimer = null; function scheduleModeHintCollapse() { const hint = document.getElementById("hy-cme-mode-desc"); if (!hint) return; hint.classList.remove("cme-study-mode-hint-collapsed"); if (modeHintCollapseTimer) clearTimeout(modeHintCollapseTimer); modeHintCollapseTimer = setTimeout(() => { hint.classList.add("cme-study-mode-hint-collapsed"); }, 5000); } function syncRunModePanel() { const sel = document.getElementById("hy-cme-run-mode"); const hint = document.getElementById("hy-cme-mode-desc"); const metric = document.getElementById("hy-cme-mode-metric"); ensureRunModeForTier(); const mode = getRunMode(); if (sel) { const offlineOpt = sel.querySelector('option[value="offline"]'); const pro = canUseOfflineMode(); if (offlineOpt) { offlineOpt.disabled = !pro; offlineOpt.textContent = pro ? "\u79bb\u7ebf\u5f02\u6b65\u6279\u91cf\u6a21\u5f0f" : "\u79bb\u7ebf\u5f02\u6b65\u6279\u91cf\u6a21\u5f0f\uff08pro\u7528\u6237\u53ef\u7528\uff09"; } if (sel.value !== mode) sel.value = mode; } if (hint) { const rt = mode === "realtime"; hint.classList.toggle("cme-study-mode-hint-realtime", rt); hint.classList.toggle("cme-study-mode-hint-offline", !rt); const title = hint.querySelector(".cme-study-mode-hint-title"); const body = hint.querySelector(".cme-study-mode-hint-body"); if (title) title.textContent = rt ? "1:1\u6302\u673a\u6a21\u5f0f" : "\u79bb\u7ebf\u5f02\u6b65\u6279\u91cf\u6a21\u5f0f"; if (body) body.textContent = RUN_MODE_INTRO[mode] || ""; } if (metric) metric.textContent = formatRunModeLabel(mode); const subMetric = document.getElementById("hy-cme-mode-metric-sub"); if (subMetric) subMetric.textContent = formatRunModeLabel(mode); updateOfflineBatchPanel(); scheduleModeHintCollapse(); } function updateOfflineBatchPanel() { const box = document.getElementById("hy-cme-offline-batch"); if (!box) return; if (!isOfflineMode()) { box.style.display = "none"; box.textContent = ""; clearOfflinePanelHint(); return; } const snap = getOfflineBatchSnapshot(); if (!snap || !snap.filledReady) { box.style.display = "none"; box.textContent = ""; clearOfflinePanelHint(); return; } box.style.display = ""; const remain = snap.due - Date.now(); const totalText = formatDurationCn(snap.totalSec); const dueText = snap.beijingDue || formatBeijingDateTime(snap.due); if (snap.ready) { box.innerHTML = `\u7d2f\u8ba1\u8bfe\u957f ${totalText}\uff08${snap.totalCourses || snap.waitingCount} \u8bfe\uff09+ \u7f13\u51b230\u5206\u949f · \u5df2\u5230\u65f6\u95f4\uff08\u5317\u4eac\u65f6\u95f4 ${dueText}\uff09\uff0c\u8bf7\u70b9\u300c\u5f00\u59cb\u300d\u68c0\u67e5\u6536\u5c3e\u4e0e\u8003\u8bd5`; if (!getEnabled()) { setPanelHint("\u5df2\u5230\u65f6\u95f4\uff0c\u8bf7\u70b9\u300c\u5f00\u59cb\u300d\u68c0\u67e5\u8bfe\u7a0b\u5b8c\u6210\u5e76\u81ea\u52a8\u8003\u8bd5"); } } else { box.innerHTML = `\u7d2f\u8ba1\u8bfe\u957f ${totalText}\uff08${snap.totalCourses || snap.waitingCount} \u8bfe\uff09+ \u7f13\u51b230\u5206\u949f · \u8bf7\u4e8e\u5317\u4eac\u65f6\u95f4 ${dueText} \u540e\u518d\u70b9\u300c\u5f00\u59cb\u300d\uff08\u7ea6 ${formatEta(remain)}\uff09`; if (!getEnabled()) { setPanelHint(`\u7ea6 ${formatEta(remain)} \u540e\u53ef\u7ee7\u7eed\uff1b\u53ef\u5173\u95ed\u7f51\u9875\uff0c\u5230\u70b9\u518d\u6253\u5f00\u70b9\u300c\u5f00\u59cb\u300d`); } } } function ensureHyConfirmModal() { let modal = document.getElementById("hy-cme-confirm-modal"); if (modal) return modal; modal = document.createElement("div"); modal.id = "hy-cme-confirm-modal"; modal.innerHTML = `
`; document.body.appendChild(modal); modal.addEventListener("click", (e) => { if (e.target === modal && modal.__hyConfirmReject) modal.__hyConfirmReject(false); }); return modal; } function askHyPanelConfirm(options) { const opts = options || {}; const modal = ensureHyConfirmModal(); return new Promise((resolve) => { const finish = (ok) => { modal.style.display = "none"; modal.__hyConfirmReject = null; resolve(!!ok); }; modal.__hyConfirmReject = finish; const titleEl = modal.querySelector(".cme-confirm-title"); const bodyEl = modal.querySelector(".cme-confirm-body"); const cancelBtn = modal.querySelector(".cme-confirm-cancel"); const okBtn = modal.querySelector(".cme-confirm-ok"); if (titleEl) titleEl.textContent = opts.title || "\u786e\u8ba4"; if (bodyEl) { bodyEl.textContent = opts.message || ""; bodyEl.style.whiteSpace = (opts.message || "").includes("\n") ? "pre-line" : ""; } if (cancelBtn) { cancelBtn.textContent = opts.cancelText || "\u53d6\u6d88"; cancelBtn.onclick = () => finish(false); } if (okBtn) { okBtn.textContent = opts.okText || "\u786e\u5b9a"; okBtn.onclick = () => finish(true); } modal.style.display = "flex"; }); } function clearStalePanelSelection() { setQueue([]); setStudySessionQueue([]); localStorage.removeItem(QUEUE_TOUCHED_KEY); panelState.chapterPreview = []; panelState.selectionLock = null; } function getStudySessionQueue() { try { return JSON.parse(localStorage.getItem(STUDY_SESSION_QUEUE_KEY) || "[]") .map(normalizeQueueCid) .filter(Boolean); } catch (_) { return []; } } function setStudySessionQueue(list) { try { const next = (list || []).map(normalizeQueueCid).filter(Boolean); if (!next.length) localStorage.removeItem(STUDY_SESSION_QUEUE_KEY); else localStorage.setItem(STUDY_SESSION_QUEUE_KEY, JSON.stringify(next)); } catch (_) {} } function getEffectiveQueue() { if (getEnabled()) { const session = getStudySessionQueue(); if (session.length) return session; if (panelState.selectionLock && panelState.selectionLock.length) { return panelState.selectionLock.map(normalizeQueueCid).filter(Boolean); } const q = getQueue(); if (q.length) return q; } return getQueue(); } function ensureStudyQueueSelection() { if (!getEnabled()) return; const expected = getStudySessionQueue(); if (!expected.length) return; const current = getQueue(); const a = expected.slice().sort().join(","); const b = current.slice().sort().join(","); if (a === b) return; commitQueueSelection(expected); } function pruneQueueToCourses() { const raw = getEffectiveQueue(); if (!panelState.courses.length) return raw; const index = new Map(); panelState.courses.forEach((c) => { const k = normalizeQueueCid(c.cid); if (k) index.set(k, c.cid); }); const q = []; raw.forEach((cid) => { const k = normalizeQueueCid(cid); const canon = index.get(k); if (canon) q.push(canon); }); if (q.length === 0 && raw.length > 0) { return raw; } const stored = getQueue(); const changed = q.length !== stored.length || q.some((cid, i) => normalizeQueueCid(cid) !== normalizeQueueCid(stored[i])); if (changed) setQueue(q); if (getEnabled() && getStudySessionQueue().length) { const sessionRaw = getStudySessionQueue(); const sessionNext = []; sessionRaw.forEach((cid) => { const k = normalizeQueueCid(cid); const canon = index.get(k); if (canon) sessionNext.push(normalizeQueueCid(canon)); }); if (sessionNext.length === 0 && sessionRaw.length > 0) { return q.length ? q : raw; } const sessionChanged = sessionNext.length !== sessionRaw.length || sessionNext.some((cid, i) => cid !== sessionRaw[i]); if (sessionChanged) setStudySessionQueue(sessionNext); } return q.length ? q : raw; } function findPanelCourseByCid(cid) { const k = normalizeQueueCid(cid); if (!k) return null; return panelState.courses.find((c) => normalizeQueueCid(c.cid) === k) || null; } function findPanelChapterByJob(job) { if (!job) return null; const lsKey = job.lsKey; const cwrid = job.cwrid; const cid = job.cid; for (const course of panelState.courses) { if (cid && normalizeQueueCid(course.cid) !== normalizeQueueCid(cid)) continue; if (!course.chapters || !course.chapters.length) continue; const ch = (lsKey && course.chapters.find((c) => c.lsKey === lsKey)) || (cwrid && course.chapters.find((c) => c.cwrid === cwrid)) || (job.cwid && course.chapters.find((c) => c.cwid === job.cwid)); if (ch) return ch; } for (const preview of panelState.chapterPreview || []) { if (cid && preview.cid !== cid) continue; if (!preview.chapters) continue; const ch = (lsKey && preview.chapters.find((c) => c.lsKey === lsKey)) || (cwrid && preview.chapters.find((c) => c.cwrid === cwrid)); if (ch) return ch; } return null; } function resolveChapterDisplayTitle(job) { const ch = findPanelChapterByJob(job); return (ch && ch.title) || (job && job.title) || "\u7ae0\u8282"; } function syncJobTitlesFromPanelCourses() { const jobs = loadJobs(); let changed = false; Object.keys(jobs).forEach((k) => { const job = jobs[k]; if (!job || job.phase === "done") return; const ch = findPanelChapterByJob(job); if (ch && ch.title && ch.title !== job.title) { job.title = ch.title; changed = true; } }); if (changed) saveJobs(jobs); return changed; } function readCourseListSelectionFromDom() { const el = document.getElementById("hy-cme-course-list"); if (!el) return []; const selected = []; el.querySelectorAll("input[type='checkbox'][data-cid]").forEach((box) => { if (box.checked && box.dataset.cid) selected.push(normalizeQueueCid(box.dataset.cid)); }); return selected.filter(Boolean); } function commitQueueSelection(list) { const next = (list || []).map(normalizeQueueCid).filter(Boolean); if (getEnabled() && !next.length) return; localStorage.setItem(QUEUE_TOUCHED_KEY, "1"); setQueue(next); panelState.queueSelectSuppressUntil = Date.now() + 2000; panelState.lastCourseListKey = getCourseListRenderKey(); } function scheduleChapterPreviewRefresh() { if (panelState.chapterPreviewDebounce) clearTimeout(panelState.chapterPreviewDebounce); panelState.chapterPreviewDebounce = setTimeout(() => { panelState.chapterPreviewDebounce = null; void refreshChapterPreview(); }, 500); } function restoreLockedQueueSelection() { const list = panelState.selectionLock && panelState.selectionLock.length ? panelState.selectionLock : getStudySessionQueue(); if (list && list.length) commitQueueSelection(list); } function detectCollectPageIssue(html) { if (!html || html.length < 300) return { login: true, error: "\u672a\u767b\u5f55" }; if (isPageLoginRedirect(html)) return { login: true, error: "\u672a\u767b\u5f55" }; if (/course_collect\.aspx|ul\.sub_ul|\u9879\u76ee\u6536\u85cf/i.test(html)) return null; if (/\u8bf7\u5148\u767b\u5f55|\u8bf7\u767b\u5f55|password|login_sso/i.test(html) && !/\u9000\u51fa\u767b\u5f55|login_out/i.test(html)) { return { login: true, error: "\u672a\u767b\u5f55" }; } return null; } function isQueueTouched() { return localStorage.getItem(QUEUE_TOUCHED_KEY) === "1"; } function initDefaultQueueIfNeeded() { if (panelState.loginRequired || !panelState.courses.length) return; pruneQueueToCourses(); if (isQueueTouched()) return; const q = getQueue(); if (!q.length) { setQueue(panelState.courses.map((c) => c.cid)); } } function syncQueueFromCourseList() { if (getEnabled()) return; const selected = readCourseListSelectionFromDom(); commitQueueSelection(selected); scheduleChapterPreviewRefresh(); updateOfflineBatchPanel(); renderSelectAllBar(); } function bindCourseListEvents() { const panel = document.getElementById("hy-cme-auto-panel"); if (!panel || panel.__hyCourseBound) return; panel.__hyCourseBound = true; panel.addEventListener("change", (e) => { const t = e.target; if (t.id === "hy-cme-select-all") { document.querySelectorAll("#hy-cme-course-list input[data-cid]").forEach((cb) => { cb.checked = t.checked; }); syncQueueFromCourseList(); return; } if (t.matches("#hy-cme-course-list input[data-cid]")) { syncQueueFromCourseList(); } }); } function renderSelectAllBar() { const bar = document.getElementById("hy-cme-select-all-bar"); if (!bar) return; if (panelState.refreshing || !panelState.courses.length) { bar.style.display = "none"; bar.dataset.key = ""; return; } bar.style.display = "block"; const selectedSet = new Set(pruneQueueToCourses().map(normalizeQueueCid)); const total = panelState.courses.length; const allChecked = total > 0 && selectedSet.size === total; const indeterminate = selectedSet.size > 0 && selectedSet.size < total; const key = `${selectedSet.size}/${total}/${allChecked}/${indeterminate}`; if (bar.dataset.key === key) return; bar.dataset.key = key; bar.innerHTML = ` `; const box = bar.querySelector("#hy-cme-select-all"); if (box) box.indeterminate = indeterminate; } function getModeHintText() { if (isOfflineMode()) { return "\u79bb\u7ebf\u6279\u91cf\uff1a\u9996\u6b21\u5206\u6bb5\u62c9\u6ee1\u5168\u90e8\u8fdb\u5ea6\uff0c\u7d2f\u8ba1\u8bfe\u957f\u5199\u5165\u672c\u5730\uff0c\u5230\u70b9\u540e\u518d\u8fd0\u884c\u6536\u5c3e\u4e0e\u8003\u8bd5"; } return "1:1\u6302\u673a\uff1a\u8bfe\u957f=\u6302\u949f\uff08\u8fdb\u5165 status=0\uff0c\u6bcf5\u5206\u949f status=1+\u5b66\u5b8c\u68c0\u6d4b\uff0c\u672b\u6bb5 status=2\uff09"; } function formatJobQueueLine(j, now, mode) { const name = j.title || j.cwrid.slice(0, 8); const filling = hasPendingFill(); const state = mode === "serial" ? loadSerialState() : null; const apiWait = isTailMode() ? msUntilNextProcessApi(now) : 0; const slot = apiWait > 0 ? ` API${formatEta(apiWait)}` : mode === "tail" && TAIL_STEP_PHASES.includes(j.phase) ? " API\u8fdb\u884c\u4e2d" : ""; if (mode === "ladder" && j.phase === "pending") return `${name} \u5f85\u9636\u68af\uff08\u6392\u961f\uff09`; if (mode === "ladder" && j.phase === "ladder_jump") { const jd = j.ladderJumpDue || 0; const tag = jd <= now ? "\u5230\u70b9\u8df3" : `${formatEta(jd - now)}\u540e\u8df3`; return `${name} \u9636\u68af#${j.ladderSlot || "?"} \u5f85status=3@${j.ladderJumpSec || "?"}s ${tag}`; } if (mode === "ladder" && j.phase === "ladder_finish") { const fd = j.ladderFinishDue || 0; const tag = fd <= now ? "\u5230\u70b9\u6536\u5c3e" : `${formatEta(fd - now)}\u540estatus=2+\u64ad\u5b8c`; return `${name} \u9636\u68af#${j.ladderSlot || "?"} \u5df2\u8df3@${j.lastPostedSec || "?"}s ${tag}`; } if (mode === "p3par" && j.phase === "pending") { const nd = j.nextStepDue || 0; const tag = nd <= now ? "\u5373\u5c06\u6b65\u8fdb" : `${formatEta(nd - now)}\u540e\u6b65\u8fdb`; return `${name} \u5f85\u5e76\u884c ${tag}`; } if (mode === "pstep" && j.phase === "pending") return `${name} \u5f85\u6b65\u8fdb\uff08\u8f6e\u6d41\uff09`; if ((mode === "p3par" || mode === "pstep") && j.phase === "stepping") { const last = j.lastPostedSec || j.startSec || 0; const dur = j.duration || 1; const pct = Math.min(99, Math.round((last / dur) * 100)); const nd = j.nextStepDue || 0; const tag = nd <= now ? "\u5230\u70b9+300s" : `${formatEta(nd - now)}\u540e+300s`; return `${name} \u6b65\u8fdb@${last}/${dur}s ${pct}% \u7b2c${j.stepCount || 0}\u6b65 ${tag}`; } if (mode === "tail" && j.phase === "pending") return `${name} \u5f85\u5feb\u586b\u81f3\u672b5\u5206${slot}`; if (mode === "tail" && TAIL_STEP_PHASES.includes(j.phase)) { const tailSec = getTailSec(j); const map = { fill_s0: "\u5feb\u586b\u8fdb\u5165", fill_step: `\u5feb\u586b+300s→@${tailSec}s \u7b2c${j.fillStepCount || 0}\u6b65`, fill_s1: `\u5feb\u586b+300s→@${tailSec}s`, fill_s3: "\u5feb\u586b\u9000\u51fa", tail_s0: "\u672b5\u5206\u8fdb\u5165", tail_s1: `\u672b5\u5206@${j.duration}s`, tail_s2: "\u5b8c\u6bd5", tail_s3: "\u9000\u51fa", }; return `${name} ${map[j.phase] || j.phase}${slot}`; } if (mode === "tail" && j.phase === "waiting") { const last = j.lastPostedSec || j.startSec || 0; const dur = j.duration || 1; const pct = Math.min(99, Math.round((last / dur) * 100)); const td = j.tailDue || 0; const tag = td <= now ? "\u5f85\u672b5\u5206" : `${formatEta(td - now)}\u540e\u672b5\u5206`; return `${name} \u6302\u949f@${last}/${dur}s ${pct}% ${tag}${slot}`; } if (mode === "tail" && j.phase === "finish_wait") { const rd = j.recordRetryDue || 0; const tag = rd <= now ? "\u5f851\u6b21\u5fc3\u8df3+\u64ad\u5b8c" : `${formatEta(rd - now)}\u540e\u6536\u5c3e\u91cd\u8bd5`; return `${name} \u6302\u949f\u7b49\u64ad\u5b8c\u8bb0\u5f55 ${tag}${slot}`; } if (mode === "tail" && j.phase === "ready") return `${name} \u5f85\u63d0\u4ea4\u64ad\u5b8c\u8bb0\u5f55${slot}`; if (mode === "tail" && (j.phase === "serial" || j.phase === "stepping")) { return `${name} \u5f85\u5feb\u586b\uff08\u65e7\u8f6e\u8be2\u72b6\u6001\uff0c\u81ea\u52a8\u8fc1\u79fb\u4e2d\uff09${slot}`; } if (mode === "serial" && j.phase === "pending") return `${name} \u6392\u961f\u4e2d\uff08\u5f85\u8f6e\u8be2\uff09`; if (mode === "serial" && (j.phase === "serial" || j.phase === "stepping")) { const last = j.lastPostedSec || j.startSec || 0; const dur = j.duration || 1; const pct = Math.min(100, Math.round((last / dur) * 100)); const roundWait = state && now < (state.roundDue || 0) ? ` \u4e0b\u8f6e${formatEta((state.roundDue || 0) - now)}` : state && (state.roundIdx || 0) > 0 ? ` \u672c\u8f6e\u8fdb\u884c\u4e2d` : ""; return `${name} \u8f6e\u8be2@${last}/${dur}s ${pct}% \u7b2c${j.stepCount || 0}\u6b65${roundWait}`; } if (mode === "serial" && j.phase === "ready") return `${name} \u5df2\u6ee1\uff0c\u5f85\u63d0\u4ea4\u64ad\u5b8c\u8bb0\u5f55`; if (mode === "realtime" && j.phase === "pending") return `${name} [1:1] \u6392\u961f\u4e2d`; if (mode === "realtime" && j.phase === "rt_stepping") { const dur = j.duration || 1; const nd = j.nextStepDue || 0; if (!j.rtEntered) { return `${name} [1:1] \u7b49\u5f85\u8fdb\u5165 ${nd <= now ? "\u5373\u5c06\u4e0a\u62a5" : formatEta(nd - now) + "\u540e\u4e0a\u62a5"}`; } const studiedSec = j.lastPostedSec != null ? j.lastPostedSec : j.rtEntrySec != null ? j.rtEntrySec : 0; const prog = formatRtStudyLine(studiedSec, dur); const finishAt = getRealtimeFinishAt(j); const tag = finishAt && finishAt <= now ? "\u5230\u70b9\u64ad\u5b8c" : finishAt ? `${formatEta(finishAt - now)}\u540e\u64ad\u5b8c` : nd <= now ? "\u5230\u70b9\u4e0a\u62a5" : `${formatEta(nd - now)}\u540e\u4e0a\u62a5`; return `${name} [1:1] ${prog} ${tag}`; } if (mode === "realtime" && j.phase === "rt_finish_retry") { const rd = j.recordRetryDue || 0; return `${name} [1:1] \u64ad\u5b8c\u91cd\u8bd5 ${rd <= now ? "\u5230\u70b9" : formatEta(rd - now)}`; } if (j.interactive && j.phase === "pending") return `${name} [\u4e92\u52a8] \u6392\u961f\u4e2d\uff08API\uff09`; if (j.interactive && j.phase === "hd_running") { const prog = j.hdProgressText || j.hdLastLog || ""; const step = j.hdStep && j.hdStepTotal ? ` \u7b2c${j.hdStep}/${j.hdStepTotal}\u9875` : j.hdStep ? ` \u7b2c${j.hdStep}\u9875` : ""; const tail = prog ? ` ${prog}` : " API \u6267\u884c\u4e2d…"; return `${name} [\u4e92\u52a8]${step}${tail}`; } if (mode === "offline" && j.phase === "pending") return `${name} [\u79bb\u7ebf] \u5f85\u6279\u91cf\u62c9\u6ee1`; if (mode === "offline" && j.phase === "silent_filling") return `${name} [\u79bb\u7ebf] \u5206\u6bb5\u62c9\u6ee1\u4e2d…`; if (mode === "offline" && j.phase === "silent_clock") { const st = loadSilentState(); const due = st.batchCompleteDue || 0; const tag = due <= now ? "\u5230\u70b9\u5f85\u6536\u5c3e" : `${formatEta(due - now)}\u540e\u6536\u5c3e`; return `${name} [\u79bb\u7ebf] \u5df2\u62c9\u6ee1 ${tag}`; } if (mode === "offline" && j.phase === "silent_finish_retry") { const rd = j.recordRetryDue || 0; return `${name} [\u79bb\u7ebf] \u64ad\u5b8c\u91cd\u8bd5 ${rd <= now ? "\u5230\u70b9" : formatEta(rd - now)}`; } if (mode === "heartbeat" && j.phase === "pending") return `${name} \u5f85\u5feb\u586b`; if (j.phase === "anchor") { const tag = filling ? "\u951a\u70b9\u4fdd\u6d3b" : "\u6392\u961f"; return `${name} ${tag} ${formatEta(j.finishDue - now)}\u540e\u6536\u5c3e`; } if (j.phase === "active") { const sec = j.lastPostedSec || j.startSec || 1; const hb = j.heartbeatOk ? `\u6709\u6548\u5fc3\u8df3${j.heartbeatOk}\u6b21` : `\u9996\u5fc3\u8df3${formatEta((j.nextHeartbeatDue || now) - now)}`; return `${name} ★active@${sec}s ${hb} ${formatEta(j.finishDue - now)}\u540e\u6536\u5c3e`; } if (j.phase === "waiting" && mode !== "tail") { const qd = getQueueDue(j); const tag = qd <= now ? "\u5373\u5c06\u8f6e\u5230" : formatEta(qd - now) + "\u540e\u8f6e\u5230"; return `${name} \u6392\u961f(${tag}\uff0c\u7ea6${Math.round((j.duration || 0) / 60)}\u5206\u949f\u5fc3\u8df3)`; } return `${name} ${j.phase}`; } function getActiveJob(jobs) { const active = Object.values(jobs).filter((j) => j && j.phase !== "done"); const runningPhases = [ "hd_running", "rt_stepping", "rt_finish_retry", "silent_filling", "silent_clock", "silent_finish_retry", "stepping", "active", "ladder_jump", "ladder_finish", "anchor", ]; return ( active.find((j) => runningPhases.includes(j.phase)) || active.sort((a, b) => (getQueueDue(a) || a.createdAt || 0) - (getQueueDue(b) || b.createdAt || 0))[0] || null ); } function renderCourseList() { const el = document.getElementById("hy-cme-course-list"); if (!el) return; if (Date.now() < (panelState.queueSelectSuppressUntil || 0)) { renderSelectAllBar(); return; } if (panelState.refreshing) { el.innerHTML = '
\u6b63\u5728\u52a0\u8f7d\u6536\u85cf\u8bfe\u7a0b…
'; return; } const selectedSet = new Set(pruneQueueToCourses().map(normalizeQueueCid)); const jobs = loadJobs(); const curJob = getActiveJob(jobs); if (!panelState.courses.length) { const active = Object.values(jobs).filter((j) => j && j.phase !== "done"); if (panelState.loginRequired) { el.innerHTML = '
\u672a\u767b\u5f55
\u8bf7\u5148\u767b\u5f55\u534e\u533b\u7f51\uff0c\u767b\u5f55\u540e\u70b9\u300c\u5237\u65b0\u6536\u85cf\u300d
'; return; } if (!active.length) { el.innerHTML = '
\u6536\u85cf\u5939\u6682\u65e0\u8bfe\u7a0b
\u8bf7\u5148\u5728\u7f51\u7ad9\u6536\u85cf\u8981\u5b66\u4e60\u7684\u8bfe\u7a0b
'; return; } el.innerHTML = active .sort((a, b) => (getQueueDue(a) || a.createdAt || 0) - (getQueueDue(b) || b.createdAt || 0)) .map((j) => { const isCur = curJob && curJob.lsKey === j.lsKey; const title = resolveChapterDisplayTitle(j); return `
${escapeHtml(title)}\u5b66\u4e60\u4e2d
`; }) .join(""); return; } const allChecked = panelState.courses.length > 0 && selectedSet.size === panelState.courses.length; el.innerHTML = panelState.courses .map((c) => { const total = Number(c.chapterTotal) || 0; const studyDone = Number(c.studyDone) || 0; const examDone = Number(c.examDone) || 0; const hasProgress = c.chaptersLoaded && total > 0 && !c.progressError; const studyPct = hasProgress ? Math.round((studyDone / total) * 100) : 0; const examPct = hasProgress ? Math.round((examDone / total) * 100) : 0; const finished = hasProgress && !isCourseUnfinished(c); const checked = selectedSet.has(normalizeQueueCid(c.cid)); const isActive = curJob && curJob.cid === c.cid; const border = isActive ? "#16a34a" : "#e2e8f0"; const bgColor = isActive ? "#f0fdf4" : finished ? "#f1f5f9" : "#f8fafc"; const titleColor = finished ? "#64748b" : "#0f172a"; const progressLine = getCourseProgressLine(c); const studyPctDisplay = studyPct; const examPctDisplay = examPct; return ` `; }) .join(""); renderSelectAllBar(); panelState.lastCourseListKey = getCourseListRenderKey(); } function renderChapterPreview() { const el = document.getElementById("hy-cme-chapter-preview"); if (!el) return; const list = panelState.chapterPreview; if (!list.length) { if (pageType() === "course") { const chapters = parseCourseChapterJobs(); if (!chapters.length) { el.innerHTML = '
\u8bf7\u52fe\u9009\u8bfe\u7a0b\u6216\u6253\u5f00\u7ae0\u8282\u9875
'; return; } const jobs = loadJobs(); const title = (document.querySelector("h1,h2,.course_title")?.textContent || document.title || "\u5f53\u524d\u8bfe\u7a0b") .replace(/\s+/g, " ") .trim() .slice(0, 40); const cid = readCourseId(); const courseOnPage = cid ? findPanelCourseByCid(cid) : null; const items = chapters .map((ch) => formatChapterPreviewLine(applyLiveProgressToChapter(ch), courseOnPage)) .join(""); el.innerHTML = `
${escapeHtml(title)}
${items}
`; return; } el.innerHTML = '
\u8bf7\u52fe\u9009\u8bfe\u7a0b\u67e5\u770b\u7ae0\u8282
'; return; } el.innerHTML = list .map((course) => { const items = (course.chapters || []) .map((ch) => formatChapterPreviewLine(applyLiveProgressToChapter(ch), course)) .join(""); const emptyHint = course.progressError ? course.progressError : course.chaptersLoaded ? "\u672a\u8bc6\u522b\u5230\u5fc5\u4fee\u7ae0\u8282" : "\u7ae0\u8282\u672a\u52a0\u8f7d\uff0c\u8bf7 F5 \u540e\u70b9\u300c\u5237\u65b0\u6536\u85cf\u300d"; return `
${escapeHtml(course.title)}
${items || `
${escapeHtml(emptyHint)}
`}
`; }) .join(""); } function updatePanel() { const statusEl = document.getElementById("hy-cme-auto-status"); if (!statusEl) return; ensureStudyQueueSelection(); const jobs = loadJobs(); const all = Object.values(jobs).filter(Boolean); const active = all.filter((j) => j.phase !== "done"); const progress = getSelectedProgressTotals(); const now = Date.now(); syncStartStopButtons(); const videoEl = document.getElementById("hy-cme-queue-video"); const examEl = document.getElementById("hy-cme-queue-exam"); const pctEl = document.getElementById("hy-cme-queue-percent"); const barEl = document.getElementById("hy-cme-queue-progress"); if (videoEl) { videoEl.textContent = progress.videoTotal ? `${progress.videoDone}/${progress.videoTotal}` : "—"; } if (examEl) { examEl.textContent = progress.examTotal ? `${progress.examDone}/${progress.examTotal}` : "—"; } let pct = 0; if (progress.videoTotal) { pct = Math.round((progress.videoDone / progress.videoTotal) * 100); } else if (progress.examTotal) { pct = Math.round((progress.examDone / progress.examTotal) * 100); } if (getEnabled()) { if (panelState.refreshing) statusEl.textContent = "\u5237\u65b0\u4e2d…"; else if (active.length || progress.videoDone < progress.videoTotal || progress.examDone < progress.examTotal) { statusEl.textContent = "\u8fd0\u884c\u4e2d"; } else statusEl.textContent = "\u8fd0\u884c\u4e2d · \u5f85\u767b\u8bb0"; statusEl.style.color = "#047857"; statusEl.style.borderColor = "#6ee7b7"; } else { statusEl.textContent = "\u5df2\u505c\u6b62"; statusEl.style.color = "#64748b"; statusEl.style.borderColor = "#cbd5e1"; } if (pctEl) pctEl.textContent = `${pct}%`; if (barEl) barEl.style.width = `${pct}%`; const footerEl = document.getElementById("hy-cme-queue-text"); if (footerEl) { if (panelState.courses.length) { footerEl.textContent = progress.selectedCount ? formatSelectedProgressLine() : `\u6536\u85cf ${panelState.courses.length} \u95e8 · \u8bf7\u52fe\u9009\u8bfe\u7a0b`; } else if (!all.length) footerEl.textContent = "\u672a\u767b\u8bb0\u7ae0\u8282"; else if (!active.length) footerEl.textContent = formatSelectedProgressLine(); else footerEl.textContent = formatSelectedProgressLine(); } const listKey = getCourseListRenderKey(); const suppressListRender = Date.now() < (panelState.queueSelectSuppressUntil || 0); if (!suppressListRender && listKey !== panelState.lastCourseListKey) { panelState.lastCourseListKey = listKey; renderCourseList(); } else { renderSelectAllBar(); } renderChapterPreview(); syncRunModePanel(); const selectedQueue = pruneQueueToCourses(); if ( getEnabled() && selectedQueue.length > 0 && all.length > 0 && !active.length && !hasPendingExamWork() ) { maybeAutoStopWhenFinished(); } tickOfflineBatchWaitUserLog(); } function updatePanelCloudStatus() { const tierEl = document.getElementById("hy-cme-cloud-tier"); const freeEl = document.getElementById("hy-cme-cloud-free"); const freeLabelEl = document.getElementById("hy-cme-cloud-free-label"); const tokenInput = document.getElementById("hy-cme-cloud-token"); if (tierEl) { tierEl.textContent = formatCloudTierText(cloudState.cloudTier); tierEl.style.color = cloudState.cloudRevoked || String(cloudState.cloudTier || "").toLowerCase() === "revoked" ? "#dc2626" : "#0f172a"; } if (cloudState.cloudRevoked) { if (freeLabelEl) freeLabelEl.textContent = "\u6388\u6743\u72b6\u6001"; if (freeEl) freeEl.textContent = "Token \u5df2\u7981\u7528"; } else if (String(cloudState.cloudTier || "").toLowerCase() === "pro") { if (freeLabelEl) freeLabelEl.textContent = "Pro \u5230\u671f"; if (freeEl) freeEl.textContent = formatLeaseExpireText(cloudState.cloudProExpireAt || cloudState.cloudLeaseExp); } else { if (freeLabelEl) freeLabelEl.textContent = `\u514d\u8d39\u4f53\u9a8c\uff08${cloudState.freeChapterLimit} \u8282\uff09`; if (freeEl) freeEl.textContent = `${cloudState.freeUsedChapters}/${cloudState.freeChapterLimit}`; } if (tokenInput && tokenInput !== document.activeElement) { tokenInput.value = cloudState.cloudToken || ""; } updatePanel(); } function buildSegmentTargets(start, end) { const span = Math.max(0, end - start); if (span <= 0) return [end]; const targets = []; for (let i = 1; i <= API_SEGMENTS; i++) { targets.push(Math.floor(start + (span * i) / API_SEGMENTS)); } targets[targets.length - 1] = end; return targets.filter((t, i, arr) => i === 0 || t > arr[i - 1]); } function segmentGapMs() { return Math.max(3000, Math.round(API_GAP_MS / getMultiplier())); } function unlockExamButton() { try { const btn = document.getElementById("jrks"); if (!btn) return; btn.removeAttribute("disabled"); btn.classList.remove("inputstyle2_2"); btn.classList.add("inputstyle2"); btn.textContent = "\u8fdb\u5165\u8003\u8bd5"; } catch (_) {} } function countSteps(duration, start) { const s = start || 1; return Math.max(1, Math.ceil((duration - s) / STEP_SEC)); } function loadSerialState() { try { return JSON.parse(localStorage.getItem(SERIAL_STATE_KEY) || "{}") || {}; } catch (_) { return {}; } } function saveSerialState(s) { localStorage.setItem(SERIAL_STATE_KEY, JSON.stringify(s)); } function resetSerialState() { localStorage.removeItem(SERIAL_STATE_KEY); } function getTailState() { try { return JSON.parse(localStorage.getItem(TAIL_STATE_KEY) || "{}") || {}; } catch (_) { return {}; } } function saveTailState(s) { localStorage.setItem(TAIL_STATE_KEY, JSON.stringify(s)); } function resetTailState() { localStorage.removeItem(TAIL_STATE_KEY); } function randomStepIntervalMs() { return ( STEP_INTERVAL_MIN + Math.floor(Math.random() * (STEP_INTERVAL_MAX - STEP_INTERVAL_MIN + 1)) ); } function randomStaggerMs() { return ( PARALLEL_STAGGER_MIN + Math.floor(Math.random() * (PARALLEL_STAGGER_MAX - PARALLEL_STAGGER_MIN + 1)) ); } async function runOneProcessPstep(ctx, status, sec, label, tag) { let res = null; try { res = await postProcess(ctx, status, sec); } catch (e) { pushLog(`[${label}] ${tag} status=${status}@${sec}s \u5f02\u5e38: ${e && e.message ? e.message : e}`); return { ok: false, code: -1 }; } const code = res && res.code; if (code === 3) { const hint = status === 2 ? "\uff08\u6ee1\u8fdb\u5ea6\u4e0d\u5e94\u9891\u7e41\uff0c\u8bf7\u68c0\u67e5\uff09" : "\uff08\u6b65\u8fdb+300s \u9650\u6d41\uff0c\u7ea65\u5206\u949f1\u6b21\uff1b\u624b\u6d4b status=2 \u6ee1\u8fdb\u5ea6\u51e0\u79d2\u6362\u8bfe\u4ecd code=0\uff09"; pushLog(`[${label}] ${tag} status=${status}@${sec}s code=3 ${hint}`); return { ok: false, code: 3 }; } if (code !== 0) { pushLog( `[${label}] ${tag} status=${status}@${sec}s code=${code != null ? code : "?"}${res && res.msg ? " " + res.msg : ""}` ); return { ok: false, code }; } return { ok: true, code: 0, res }; } function jobStepIntervalReady(job, now) { if (!job || !job.lastPstepAt) return true; return now - job.lastPstepAt >= STEP_INTERVAL_MIN; } function countPstepDueKeys(jobs, now) { return Object.keys(jobs).filter((k) => isPstepJobDue(jobs[k], now)).length; } function isPstepJobDue(j, now) { if (!j || j.phase === "done") return false; if (j.phase === "pending" && (!j.nextStepDue || j.nextStepDue <= now)) return true; if (j.phase === "stepping" && (!j.nextStepDue || j.nextStepDue <= now)) return true; return false; } function sortPstepDueKeys(jobs, now) { return Object.keys(jobs) .filter((k) => isPstepJobDue(jobs[k], now)) .sort((a, b) => { const ja = jobs[a]; const jb = jobs[b]; const sa = ja.stepCount || 0; const sb = jb.stepCount || 0; if (sa !== sb) return sa - sb; return (ja.nextStepDue || ja.createdAt || 0) - (jb.nextStepDue || jb.createdAt || 0); }); } function preparePstepJob(jobs, key) { const job = jobs[key]; if (!job || job.phase !== "pending") return job; job.phase = "stepping"; job.lastPostedSec = job.startSec || 1; job.stepCount = 0; jobs[key] = job; saveJobs(jobs); return job; } function estimateP3ParallelWallMin(jobs) { const list = Object.values(jobs).filter((j) => j && j.phase !== "done"); if (!list.length) return 0; const maxSteps = Math.max( ...list.map((j) => countFillSteps(j.duration || 3600, j.startSec || 1)) ); return maxSteps * Math.round(STEP_INTERVAL_MIN / 60000); } async function runParallelStepVisit(jobKey, job) { const ctx = jobToCtx(job); const label = job.title || job.cwrid.slice(0, 8); const start = job.startSec || 1; const duration = job.duration; const last = job.lastPostedSec != null ? job.lastPostedSec : start; const next = Math.min(duration, last + STEP_SEC); const isFinal = next >= duration - 2; const stepsTotal = countFillSteps(duration, start); const now = Date.now(); if (!isFinal && !jobStepIntervalReady(job, now)) { saveJob(ctx, { nextStepDue: job.lastPstepAt + randomStepIntervalMs() }); return; } const warmed = await warmPlaySession(ctx, true); if (!warmed) { saveJob(ctx, { nextStepDue: Date.now() + 60000 }); pushLog(`[${label}] \u9884\u70ed\u64ad\u653e\u9875\u5931\u8d25\uff0c60s\u540e\u91cd\u8bd5\uff08\u8bf7\u5148 F5 \u786e\u8ba4\u5df2\u767b\u5f55\uff09`); return; } await sleep(1500); if (!isFinal) { const r = await runOneProcessPstep(ctx, 3, next, label, "\u6b65\u8fdb"); if (!r.ok) { saveJob(ctx, { lastPstepAt: Date.now(), nextStepDue: Date.now() + randomStepIntervalMs(), }); if (isP3ParallelMode() && r.code === 3) { pushLog(`[${label}] \u82e5\u591a\u8bfe\u8fde\u7eedcode=3 → \u9650\u6d41\u53ef\u80fd\u662f\u5168\u8d26\u53f7\uff0c\u6539\u300c\u8f6e\u6d41+300s\u300d`); } return; } job.stepCount = (job.stepCount || 0) + 1; syncLocal(ctx, next); const stepAt = Date.now(); saveJob(ctx, { phase: "stepping", stepCount: job.stepCount, lastPostedSec: next, lastPstepAt: stepAt, nextStepDue: stepAt + randomStepIntervalMs(), }); pushLog( `[${label}] \u6b65\u8fdb status=3@${next}s code=0 · \u7b2c${job.stepCount}/${stepsTotal}\u6b65 · \u4e0b\u6b65\u7ea6${Math.round(STEP_INTERVAL_MIN / 60000)}\u5206\u949f\u540e` ); return; } const r2 = await runOneProcessPstep(ctx, 2, duration, label, "\u5b8c\u6bd5"); if (!r2.ok) { saveJob(ctx, { lastPstepAt: Date.now(), nextStepDue: Date.now() + randomStepIntervalMs(), }); return; } saveJob(ctx, { lastPstepAt: Date.now() }); pushLog(`[${label}] \u5b8c\u6bd5 status=2@${duration}s code=0 → \u64ad\u5b8c\u8bb0\u5f55…`); let rec = null; try { rec = await postPlayRecord(ctx, duration); pushLog( `[${label}] \u64ad\u5b8c\u8bb0\u5f55 @${duration}s code=${rec && rec.code}${rec && rec.msg ? " " + rec.msg : ""}` ); } catch (e) { pushLog(`[${label}] \u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`); } if (rec && rec.code === 1) { saveJob(ctx, { phase: "stepping", lastPostedSec: Math.max(start, duration - STEP_SEC * 2), nextStepDue: Date.now() + randomStepIntervalMs(), }); pushLog(`[${label}] \u65f6\u957f\u4e0d\u8db3\uff0c\u9000\u56de\u7ee7\u7eed\u6b65\u8fdb`); return; } if (rec && rec.code === -2) { return; } try { await postProcess(ctx, 3, duration); } catch (_) {} syncLocal(ctx, duration); unlockExamButton(); saveJob(ctx, { phase: "done", lastPostedSec: duration }); pushLog(`[${label}] ✓ \u5b8c\u6210 status=2+\u64ad\u5b8c\u8bb0\u5f55`); } function getLadderState() { try { return JSON.parse(localStorage.getItem(LADDER_STATE_KEY) || "{}"); } catch (_) { return {}; } } function markLadderProcess() { localStorage.setItem(LADDER_STATE_KEY, JSON.stringify({ lastAt: Date.now() })); } function msUntilLadderProcessGap(now) { const last = getLadderState().lastAt || 0; if (!last) return 0; return Math.max(0, last + STEP_INTERVAL_MIN - now); } function applyLadderSchedule(job, slotInBatch, base) { const slot = slotInBatch + 1; const dur = job.duration || 3600; job.phase = "ladder_jump"; job.ladderSlot = slot; job.ladderJumpSec = Math.min(slot * STEP_SEC, Math.max(1, dur - 2)); job.ladderJumpDue = base + slotInBatch * 2 * STEP_INTERVAL_MIN; job.ladderFinishDue = job.ladderJumpDue + STEP_INTERVAL_MIN; job.ladderJumpDone = false; } function assignLadderBatch(jobs) { const inLadder = Object.values(jobs).filter( (j) => j && (j.phase === "ladder_jump" || j.phase === "ladder_finish") ).length; const slots = LADDER_BATCH_SIZE - inLadder; if (slots <= 0) return 0; const pending = Object.entries(jobs) .filter(([, j]) => j && j.phase === "pending") .sort((a, b) => (a[1].createdAt || 0) - (b[1].createdAt || 0)) .slice(0, slots); if (!pending.length) return 0; const base = Date.now(); pending.forEach(([k, job], i) => { applyLadderSchedule(job, i, base); jobs[k] = job; }); saveJobs(jobs); pushLog( `\u9636\u68af\u4e32\u884c\u672c\u6279 ${pending.length} \u8bfe\uff08process\u7ea65\u5206\u949f1\u6b21\uff0c\u8df3/\u6ee1\u4e0d\u649e\u8f66\uff09\uff1a${pending .map(([, j], i) => { const slot = i + 1; const jumpAt = i === 0 ? "\u7acb\u5373" : `${i * 10}\u5206`; const finAt = `${(i * 2 + 1) * 5}\u5206`; return `\u8bfe${slot} ${jumpAt} status=3@${slot * 300}s → ${finAt} status=2\u6ee1+\u64ad\u5b8c\u8bb0\u5f55`; }) .join("\uff1b")}` ); return pending.length; } function collectLadderDueActions(jobs, now) { const actions = []; Object.entries(jobs).forEach(([key, j]) => { if (!j) return; if (j.phase === "ladder_jump" && !j.ladderJumpDone && j.ladderJumpDue && j.ladderJumpDue <= now) { actions.push({ key, type: "jump", due: j.ladderJumpDue, slot: j.ladderSlot || 0 }); } if (j.phase === "ladder_finish" && j.ladderFinishDue && j.ladderFinishDue <= now) { actions.push({ key, type: "finish", due: j.ladderFinishDue, slot: j.ladderSlot || 0 }); } }); actions.sort((a, b) => a.due - b.due || a.slot - b.slot); return actions; } async function runLadderJump(jobKey, job) { const ctx = jobToCtx(job); const label = job.title || job.cwrid.slice(0, 8); const sec = job.ladderJumpSec; const slot = job.ladderSlot || "?"; if (!(await warmPlaySession(ctx, true))) { saveJob(ctx, { ladderJumpDue: Date.now() + 60000 }); pushLog(`[${label}] \u9636\u68af\u8df3#${slot}\uff1a\u9884\u70ed\u5931\u8d25\uff0c60s\u540e\u91cd\u8bd5`); return; } await sleep(1500); const gap = msUntilLadderProcessGap(Date.now()); if (gap > 0) { saveJob(ctx, { ladderJumpDue: Date.now() + gap }); pushLog(`[${label}] \u9636\u68af\u8df3#${slot}\uff1a\u8ddd\u4e0a\u6b21process ${formatEta(gap)}\uff0c\u5df2\u63a8\u8fdf`); return; } const r = await runOneProcessPstep(ctx, 3, sec, label, `\u9636\u68af\u8df3#${slot}`); if (!r.ok) { const wait = Math.max(randomStepIntervalMs(), msUntilLadderProcessGap(Date.now())); saveJob(ctx, { ladderJumpDue: Date.now() + wait }); return; } markLadderProcess(); syncLocal(ctx, sec); saveJob(ctx, { phase: "ladder_finish", lastPostedSec: sec, ladderJumpDone: true }); pushLog( `[${label}] ★\u9636\u68af\u8df3#${slot} status=3@${sec}s code=0 → ${formatEta( Math.max(0, (job.ladderFinishDue || Date.now()) - Date.now()) )}\u540e status=2@${job.duration}s+\u64ad\u5b8c\u8bb0\u5f55` ); } async function runLadderFinish(jobKey, job) { const ctx = jobToCtx(job); const label = job.title || job.cwrid.slice(0, 8); const duration = job.duration; const slot = job.ladderSlot || "?"; if (!(await warmPlaySession(ctx, true))) { saveJob(ctx, { ladderFinishDue: Date.now() + 60000 }); pushLog(`[${label}] \u9636\u68af\u5b8c\u6bd5#${slot}\uff1a\u9884\u70ed\u5931\u8d25\uff0c60s\u540e\u91cd\u8bd5`); return; } await sleep(1500); const gap = msUntilLadderProcessGap(Date.now()); if (gap > 0) { saveJob(ctx, { ladderFinishDue: Date.now() + gap }); pushLog(`[${label}] \u9636\u68af\u5b8c\u6bd5#${slot}\uff1a\u8ddd\u4e0a\u6b21process ${formatEta(gap)}\uff0c\u5df2\u63a8\u8fdf`); return; } const r2 = await runOneProcessPstep(ctx, 2, duration, label, `\u9636\u68af\u5b8c\u6bd5#${slot}`); if (!r2.ok) { const wait = Math.max(randomStepIntervalMs(), msUntilLadderProcessGap(Date.now())); saveJob(ctx, { ladderFinishDue: Date.now() + wait }); return; } markLadderProcess(); pushLog(`[${label}] ★\u9636\u68af\u5b8c\u6bd5#${slot} status=2@${duration}s code=0 → \u64ad\u5b8c\u8bb0\u5f55…`); let rec = null; try { rec = await postPlayRecord(ctx, duration); } catch (e) { pushLog(`[${label}] ★\u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`); } pushLog( `[${label}] ★\u64ad\u5b8c\u8bb0\u5f55#${slot} @${duration}s code=${rec && rec.code != null ? rec.code : "?"}${rec && rec.msg ? " " + rec.msg : ""}` ); if (rec && rec.code === 1) { pushLog( `[${label}] \u8bd5\u9a8c\u8bf4\u660e\uff1a\u4ec5\u8df3@${job.lastPostedSec || "?"}s \u64ad\u5b8c\u5e38 code=1\uff1b\u957f\u8bfe\u9700\u66f4\u591a\u6b65\u8fdb\u6216\u6302\u949f` ); } if (rec && rec.code === -2) return; syncLocal(ctx, duration); saveJob(ctx, { phase: "done", lastPostedSec: duration }); const jobs = loadJobs(); const batchActive = Object.values(jobs).some( (j) => j && (j.phase === "ladder_jump" || j.phase === "ladder_finish") ); if (!batchActive && Object.values(jobs).some((j) => j && j.phase === "pending")) { assignLadderBatch(jobs); } } async function runLadderScheduler() { await keepSessionAlive(); if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return; bgRunning = true; try { let jobs = loadJobs(); const now = Date.now(); let actions = collectLadderDueActions(jobs, now); if (!actions.length) { const inLadder = Object.values(jobs).some( (j) => j && (j.phase === "ladder_jump" || j.phase === "ladder_finish") ); if (!inLadder) assignLadderBatch(jobs); return; } if (msUntilLadderProcessGap(now) > 0) return; const act = actions[0]; const job = jobs[act.key]; if (!job) return; if (act.type === "jump") await runLadderJump(act.key, job); else await runLadderFinish(act.key, job); } catch (e) { pushLog("\u9636\u68af\u4e32\u884c: " + (e && e.message ? e.message : e)); } finally { bgRunning = false; updateJobsPanel(); } } async function runP3ParallelScheduler() { await keepSessionAlive(); if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return; bgRunning = true; try { const now = Date.now(); let jobs = loadJobs(); let dueKeys = sortPstepDueKeys(jobs, now); if (!dueKeys.length) return; const batch = dueKeys.slice(0, P3PAR_MAX_PER_TICK); for (let i = 0; i < batch.length; i++) { jobs = loadJobs(); let job = jobs[batch[i]]; if (!job || !isPstepJobDue(job, Date.now())) continue; job = preparePstepJob(jobs, batch[i]) || job; await runParallelStepVisit(batch[i], job); if (i < batch.length - 1) await sleep(P3PAR_COURSE_GAP_MS); } } catch (e) { pushLog("status=3\u771f\u5e76\u884c: " + (e && e.message ? e.message : e)); } finally { bgRunning = false; updateJobsPanel(); } } async function runParallelStepScheduler() { await keepSessionAlive(); if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return; bgRunning = true; try { const now = Date.now(); let jobs = loadJobs(); const dueKeys = sortPstepDueKeys(jobs, now); if (!dueKeys.length) return; const key = dueKeys[0]; let job = jobs[key]; if (!job) return; job = preparePstepJob(jobs, key) || job; await runParallelStepVisit(key, job); } catch (e) { pushLog("\u5e76\u884c\u6b65\u8fdb: " + (e && e.message ? e.message : e)); } finally { bgRunning = false; updateJobsPanel(); } } function getTailSec(job) { const start = job.startSec || 1; const dur = job.duration || 3600; return Math.max(start + 1, dur - STEP_SEC); } function msUntilNextProcessApi(now) { const last = getTailState().lastApiAt || 0; return Math.max(0, last - now); } function countFillSteps(tailSec, start) { const s = start || 1; return Math.max(1, Math.ceil((tailSec - s) / STEP_SEC)); } function lockProcessApi(ms) { saveTailState({ ...getTailState(), lastApiAt: Date.now() + (ms || PROCESS_SLOT_MS) }); } function processApiReady(now) { return msUntilNextProcessApi(now) <= 0; } async function runOneProcess(ctx, status, sec, label, tag) { let res = null; try { res = await postProcess(ctx, status, sec); } catch (e) { pushLog(`[${label}] ${tag} status=${status}@${sec}s \u5f02\u5e38: ${e && e.message ? e.message : e}`); } const code = res && res.code; if (code === 3) { lockProcessApi(); pushLog( `[${label}] ${tag} status=${status}@${sec}s code=3 \u5931\u8d25\uff085\u5206\u949f1\u6b21\u4e14\u6bcf\u6b21\u6700\u591a+300s\uff09\uff0c${Math.round(PROCESS_SLOT_MS / 60000)}\u5206\u949f\u540e\u91cd\u8bd5` ); return { ok: false, code: 3 }; } if (code !== 0) { lockProcessApi(); pushLog( `[${label}] ${tag} status=${status}@${sec}s code=${code != null ? code : "?"}${res && res.msg ? " " + res.msg : ""}` ); return { ok: false, code }; } lockProcessApi(); return { ok: true, code: 0 }; } function normalizeTailPhase(job) { if (job.phase === "fill_s1") job.phase = "fill_step"; } async function runTailStep(jobKey, job) { normalizeTailPhase(job); const ctx = jobToCtx(job); const label = job.title || job.cwrid.slice(0, 8); const start = job.startSec || 1; const tailSec = getTailSec(job); const duration = job.duration; if (job.phase === "fill_s0" || job.phase === "tail_s0") { await warmPlaySession(ctx, true); } if (job.phase === "fill_s0") { const r = await runOneProcess(ctx, 0, start, label, "\u5feb\u586b\u8fdb\u5165"); if (!r.ok) return; job.sessionStartAt = Date.now(); job.lastPostedSec = start; job.fillStepCount = 0; job.phase = "fill_step"; const steps = countFillSteps(tailSec, start); pushLog( `[${label}] status=0 @${start}s → \u6bcf5\u5206\u949f+300s\u81f3@${tailSec}s\uff0c\u7ea6${steps}\u6b65\uff08\u4e0d\u53ef\u4e00\u6b21\u8df3\u6ee1\uff09` ); } else if (job.phase === "fill_step") { const last = job.lastPostedSec != null ? job.lastPostedSec : start; if (last >= tailSec - 1) { job.phase = "fill_s3"; } else { const next = Math.min(tailSec, last + STEP_SEC); const steps = countFillSteps(tailSec, start); const r = await runOneProcess(ctx, 1, next, label, `\u5feb\u586b+300s`); if (!r.ok) return; syncLocal(ctx, next); job.lastPostedSec = next; job.fillStepCount = (job.fillStepCount || 0) + 1; pushLog( `[${label}] \u5feb\u586b \u7b2c${job.fillStepCount}/${steps}\u6b65 @${next}/${tailSec}s code=0` ); if (next >= tailSec - 1) { job.phase = "fill_s3"; pushLog(`[${label}] \u5feb\u586b@${tailSec}s \u8fbe\u6807 → \u4e0b\u69fd status=3 \u9000\u51fa`); } } } else if (job.phase === "fill_s3") { const r = await runOneProcess(ctx, 3, tailSec, label, "\u5feb\u586b\u9000\u51fa"); if (!r.ok) return; const base = job.sessionStartAt || Date.now(); job.phase = "waiting"; job.filledAt = Date.now(); job.tailDue = base + duration * 1000; pushLog( `[${label}] \u5feb\u586b\u5b8c\u6210@${tailSec}s → \u6302\u949f${Math.round(duration / 60)}\u5206\u949f\u540e\u672b5\u5206\u771f\u5b9e\u5fc3\u8df3` ); } else if (job.phase === "tail_s0") { const r = await runOneProcess(ctx, 0, tailSec, label, "\u672b5\u5206\u8fdb\u5165"); if (!r.ok) return; job.tailEnterAt = Date.now(); job.phase = "tail_s1"; pushLog(`[${label}] status=0 @${tailSec}s → \u4e0b\u69fd\u672b5\u5206\u5fc3\u8df3@${duration}s`); } else if (job.phase === "tail_s1") { const r = await runOneProcess(ctx, 1, duration, label, `\u672b5\u5206@${duration}s`); if (!r.ok) return; syncLocal(ctx, duration); job.lastPostedSec = duration; job.phase = "tail_s2"; } else if (job.phase === "tail_s2") { const r = await runOneProcess(ctx, 2, duration, label, "\u64ad\u653e\u5b8c\u6bd5"); if (!r.ok) return; job.status2Sent = true; job.status2At = Date.now(); job.phase = "tail_s3"; } else if (job.phase === "tail_s3") { const r = await runOneProcess(ctx, 3, duration, label, "\u9000\u51fa"); if (!r.ok) return; job.phase = "ready"; pushLog(`[${label}] \u672b5\u5206\u5b8c\u6210 → \u6392\u961f\u63d0\u4ea4\u64ad\u5b8c\u8bb0\u5f55`); } const jobs = loadJobs(); jobs[jobKey] = job; saveJobs(jobs); } function migrateJobsForTailMode(jobs) { let changed = false; Object.keys(jobs).forEach((k) => { const j = jobs[k]; if (!j || j.phase === "done") return; if (j.phase === "serial" || j.phase === "stepping") { const tailSec = getTailSec(j); const last = j.lastPostedSec != null ? j.lastPostedSec : j.startSec || 1; if (last >= tailSec - 1) { j.phase = "waiting"; j.tailDue = j.tailDue || Date.now() + (j.duration || 3600) * 1000; } else if (last > (j.startSec || 1)) { j.phase = "fill_step"; } else { j.phase = "pending"; } changed = true; } }); if (changed) saveJobs(jobs); return changed; } function getRecordRetryDue(job) { const duration = job.duration || 3600; const base = job.sessionStartAt || job.filledAt || job.status2At || Date.now(); return base + duration * 1000; } async function runFinishRetry(jobKey, job) { const ctx = jobToCtx(job); const label = job.title || job.cwrid.slice(0, 8); const duration = job.duration; await warmPlaySession(ctx, true); const r = await runOneProcess(ctx, 1, duration, label, "\u6536\u5c3e\u5fc3\u8df3(1\u6b21)"); if (!r.ok) return; await sleep(800); let rec = null; try { rec = await postPlayRecord(ctx, duration); pushLog( `[${label}] \u64ad\u5b8c\u8bb0\u5f55 @${duration}s code=${rec && rec.code}${rec && rec.msg ? " " + rec.msg : ""}` ); } catch (e) { pushLog(`[${label}] \u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`); } if (rec && rec.code === 1) { job.recordRetryDue = Date.now() + duration * 1000; job.phase = "finish_wait"; pushLog( `[${label}] \u4ecd\u65f6\u957f\u4e0d\u8db3\uff0c\u518d\u6302\u949f${Math.round(duration / 60)}\u5206\u949f\u540e\u91cd\u8bd51\u6b21\u5fc3\u8df3+\u64ad\u5b8c\u8bb0\u5f55` ); } else if (rec && rec.code === 0) { try { await postProcess(ctx, 3, duration); } catch (_) {} syncLocal(ctx, duration); job.phase = "done"; pushLog(`[${label}] ✓ \u6536\u5c3e\u6210\u529f\uff08\u6302\u949f\u540e\u5355\u5fc3\u8df3+\u64ad\u5b8c\u8bb0\u5f55\uff09`); } const jobs = loadJobs(); jobs[jobKey] = job; saveJobs(jobs); updateJobsPanel(); } async function runTailScheduler() { await keepSessionAlive(); if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return; bgRunning = true; try { let jobs = loadJobs(); if (migrateJobsForTailMode(jobs)) { pushLog("\u5df2\u5c06\u65e7\u300c\u4e32\u884c\u8f6e\u8be2\u300d\u4efb\u52a1\u8f6c\u4e3a\u672b\u6bb5\u6302\u949f\u961f\u5217"); jobs = loadJobs(); } const now = Date.now(); const finishWaitKey = Object.keys(jobs) .filter((k) => { const j = jobs[k]; return j && j.phase === "finish_wait" && j.recordRetryDue && j.recordRetryDue <= now; }) .sort((a, b) => (jobs[a].recordRetryDue || 0) - (jobs[b].recordRetryDue || 0))[0]; if (finishWaitKey) { if (!processApiReady(now)) return; jobs[finishWaitKey].phase = "finish_retry"; saveJobs(jobs); await runFinishRetry(finishWaitKey, jobs[finishWaitKey]); return; } const readyKey = Object.keys(jobs).find((k) => jobs[k] && jobs[k].phase === "ready"); if (readyKey) { if (!processApiReady(now)) return; await runFinishPhase(jobToCtx(jobs[readyKey])); return; } if (!processApiReady(now)) return; const midKey = Object.keys(jobs).find((k) => { const p = jobs[k] && jobs[k].phase; return p && TAIL_STEP_PHASES.includes(p); }); if (midKey) { await runTailStep(midKey, jobs[midKey]); return; } const waitingKey = Object.keys(jobs) .filter((k) => { const j = jobs[k]; return j && j.phase === "waiting" && j.tailDue && j.tailDue <= now; }) .sort((a, b) => (jobs[a].tailDue || 0) - (jobs[b].tailDue || 0))[0]; if (waitingKey) { jobs[waitingKey].phase = "tail_s0"; saveJobs(jobs); await runTailStep(waitingKey, jobs[waitingKey]); return; } const pendingKey = Object.keys(jobs) .filter((k) => jobs[k] && jobs[k].phase === "pending") .sort((a, b) => (jobs[a].createdAt || 0) - (jobs[b].createdAt || 0))[0]; if (pendingKey) { jobs[pendingKey].phase = "fill_s0"; saveJobs(jobs); await runTailStep(pendingKey, jobs[pendingKey]); } } catch (e) { pushLog("\u672b\u6bb5\u6302\u949f: " + (e && e.message ? e.message : e)); } finally { bgRunning = false; updateJobsPanel(); } } function jobNeedsSerialStep(job) { if (!job || !job.duration) return false; const last = job.lastPostedSec != null ? job.lastPostedSec : job.startSec || 1; return last < job.duration - 2; } function getSerialQueue(jobs) { return Object.keys(jobs) .filter((k) => { const j = jobs[k]; if (!j || j.phase === "done" || j.phase === "ready") return false; if (j.phase === "pending" || j.phase === "serial" || j.phase === "stepping") { return jobNeedsSerialStep(j); } return false; }) .sort((a, b) => (jobs[a].createdAt || 0) - (jobs[b].createdAt || 0)); } async function runSerialVisit(jobKey, job) { const ctx = jobToCtx(job); if (!(await consumeChapterCloudQuota(ctx))) return; const label = job.title || job.cwrid.slice(0, 8); const duration = job.duration; const start = job.startSec || 1; const last = job.lastPostedSec != null ? job.lastPostedSec : start; const sec = Math.min(duration, last + STEP_SEC); await warmPlaySession(ctx, true); let r0 = null; let r1 = null; try { r0 = await postProcess(ctx, 0, last); await sleep(400); r1 = await postProcess(ctx, 1, sec); if (r1 && r1.code === 1) { await postProcess(ctx, 0, sec); await sleep(400); r1 = await postProcess(ctx, 1, sec); } } catch (e) { pushLog(`[${label}] \u8bbf\u95ee\u5f02\u5e38: ${e && e.message ? e.message : e}`); } const code = r1 && r1.code; job.phase = "serial"; job.stepCount = (job.stepCount || 0) + 1; job.lastPostedSec = sec; syncLocal(ctx, sec); try { await postProcess(ctx, 3, sec); } catch (_) {} const note = code === 3 ? "\uff08\u9891\u7e41\uff0c\u5df2\u63a8\u8fdb\uff09" : code !== 0 && r1 && r1.msg ? " " + r1.msg : ""; pushLog( `[${label}] \u8f6e\u8be2 \u7b2c${job.stepCount}\u6b65 \u8fdb\u5165@${last}s → +300s@${sec}s code=${code != null ? code : "?"} → \u5df2\u9000\u51fa${note}` ); if (sec >= duration - 2) { job.lastPostedSec = duration; syncLocal(ctx, duration); job.phase = "ready"; try { await postProcess(ctx, 2, duration); } catch (_) {} pushLog(`[${label}] \u8fdb\u5ea6\u5df2\u6ee1\uff0c\u6392\u961f\u63d0\u4ea4\u64ad\u5b8c\u8bb0\u5f55`); } const jobs = loadJobs(); jobs[jobKey] = job; saveJobs(jobs); } async function runSerialRoundScheduler() { await keepSessionAlive(); if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return; bgRunning = true; try { const jobs = loadJobs(); const now = Date.now(); const state = loadSerialState(); let roundDue = state.roundDue || 0; let roundIdx = state.roundIdx || 0; if (roundDue > now + ROUND_GAP_MS + 60000) { pushLog("\u68c0\u6d4b\u5230\u5168\u5c40\u7b49\u5f85\u5f02\u5e38\uff08\u65e7\u7248\u5355\u8bfe\u5931\u8d25\u8bef\u4f24\uff09\uff0c\u5df2\u6062\u590d5\u5206\u949f\u4e00\u8f6e"); roundDue = 0; saveSerialState({ roundDue: 0, roundIdx }); } const readyKey = Object.keys(jobs).find((k) => jobs[k] && jobs[k].phase === "ready"); if (readyKey) { await runFinishPhase(jobToCtx(jobs[readyKey])); return; } const queue = getSerialQueue(jobs); if (!queue.length) { if (roundIdx > 0) saveSerialState({ roundDue: 0, roundIdx: 0 }); return; } if (now < roundDue) return; if (roundIdx >= queue.length) { saveSerialState({ roundDue: now + ROUND_GAP_MS, roundIdx: 0 }); pushLog( `✓ \u4e00\u8f6e\u5b8c\u6210\uff08${queue.length}\u8bfe\u5404+300s\uff09\uff0c${Math.round(ROUND_GAP_MS / 60000)}\u5206\u949f\u540e\u4e0b\u4e00\u8f6e` ); return; } const key = queue[roundIdx]; const job = jobs[key]; if (!job || !jobNeedsSerialStep(job)) { saveSerialState({ roundDue, roundIdx: roundIdx + 1 }); return; } if (roundIdx === 0 && job.stepCount > 0) { pushLog(`--- \u65b0\u4e00\u8f6e\u5f00\u59cb\uff08${queue.length}\u8bfe\uff09---`); } await runSerialVisit(key, job); saveSerialState({ roundDue, roundIdx: roundIdx + 1 }); await sleep(VISIT_GAP_MS); } catch (e) { pushLog("\u4e32\u884c\u8f6e\u8be2: " + (e && e.message ? e.message : e)); } finally { bgRunning = false; updateJobsPanel(); } } function hasPendingFill() { return Object.values(loadJobs()).some((j) => j && j.phase === "pending"); } function hangPlayTab(job) { const playId = job.wareCwid || job.cwid; if (!getHangTabs() || !playId) return; const url = location.origin + "/course_ware/course_ware_polyv.aspx?cwid=" + encodeURIComponent(playId) + "&ff=0&ft=0&t=0"; try { window.open(url, "hy_cme_" + job.cwrid.slice(0, 8)); pushLog(`[${job.title}] \u5df2\u5f00\u64ad\u653e\u9875\u6807\u7b7e\uff08prtas \u5e76\u884c\u6302\u8bfe\uff09`); } catch (_) {} } async function runPlayHangMode(ctx, job) { pushLog(`[${job.title || ctx.cwrid.slice(0, 8)}] \u6302\u9875\u6a21\u5f0f\uff1a\u4fdd\u6301\u6253\u5f00\uff0c\u6302\u949f\u8ba1\u65f6\u4e2d`); window.blockAbnormalPlugin = function () {}; try { await waitUntil(() => window.player, 20000); try { window.player.j2s_pauseVideo(); } catch (_) {} } catch (_) {} } function getQueueDue(job) { if (job.phase === "ladder_jump" && job.ladderJumpDue) return job.ladderJumpDue; if (job.phase === "ladder_finish" && job.ladderFinishDue) return job.ladderFinishDue; if (job.phase === "silent_clock") return loadSilentState().batchCompleteDue || job.completeDue || 0; if (job.phase === "silent_finish_retry" && job.recordRetryDue) return job.recordRetryDue; if (job.phase === "rt_stepping" && job.nextStepDue) return job.nextStepDue; if (job.phase === "rt_finish_retry" && job.recordRetryDue) return job.recordRetryDue; return job.queueDue || job.tailDue || job.finishDue || job.nextStepDue || 0; } function getRemainSec(job) { const start = job.startSec || 1; return Math.max(60, (job.duration || 3600) - start + 1); } function beginActivePhase(job) { const now = Date.now(); const start = job.startSec || 1; const remain = getRemainSec(job); job.activeSince = now; job.filledAt = now; job.finishDue = now + remain * 1000; job.lastPostedSec = start; job.heartbeatCount = 0; job.heartbeatOk = 0; job.nextHeartbeatDue = now + PROGRESS_MS; return job; } function assignAnchorIfNeeded() { const jobs = loadJobs(); const entries = Object.entries(jobs).filter(([, j]) => j && j.phase !== "done"); const pending = entries.filter(([, j]) => j.phase === "pending"); if (!pending.length) return false; if (entries.some(([, j]) => j.phase === "anchor" || j.isAnchor)) return false; const [key, job] = pending.sort((a, b) => a[1].duration - b[1].duration)[0]; const now = Date.now(); const remain = getRemainSec(job); job.phase = "anchor"; job.isAnchor = true; job.activeSince = now; job.filledAt = now; job.queueDue = now; job.finishDue = now + remain * 1000; job.nextHeartbeatDue = now + PROGRESS_MS; job.lastPostedSec = job.startSec || 1; job.heartbeatCount = 0; job.heartbeatOk = 0; jobs[key] = job; saveJobs(jobs); pushLog( `[${job.title}] \u6700\u77ed\u8bfe→\u4fdd\u6d3b\u951a\u70b9\uff0c\u7ea6 ${Math.round(remain / 60)} \u5206\u949f\u771f\u5b9e\u5fc3\u8df3\u540e\u6536\u5c3e` ); return true; } function getRealPlaySec(job) { const base = job.activeSince || job.filledAt || Date.now(); const start = job.lastPostedSec || job.startSec || 1; const elapsed = Math.floor((Date.now() - base) / 1000); return Math.min(job.duration, start + elapsed); } function getHeartbeatSec(job) { const wall = getRealPlaySec(job); const last = job.lastPostedSec || job.startSec || 1; return Math.min(job.duration, Math.max(last + 1, Math.min(wall, last + 305))); } async function postHeartbeat(ctx, sec) { const r0 = await postProcess(ctx, 0, sec); await sleep(800); let r1 = await postProcess(ctx, 1, sec); if (r1 && r1.code === 1) { await warmPlaySession(ctx, true); await postProcess(ctx, 0, sec); await sleep(800); r1 = await postProcess(ctx, 1, sec); } return { r0, r1 }; } async function releaseActiveSession() { const jobs = loadJobs(); const activeKey = Object.keys(jobs).find((k) => jobs[k] && jobs[k].phase === "active"); if (!activeKey) return; const job = jobs[activeKey]; const sec = getRealPlaySec(job); try { await postProcess(jobToCtx(job), 3, sec); } catch (_) {} job.phase = job.isAnchor ? "anchor" : "waiting"; jobs[activeKey] = job; saveJobs(jobs); } async function ensureActiveSession(reEnter) { const jobs = loadJobs(); const now = Date.now(); if (Object.values(jobs).some((j) => j && j.phase === "active")) return; const filling = hasPendingFill(); let candidates; if (filling) { candidates = Object.entries(jobs).filter(([, j]) => j && j.phase === "anchor"); } else { candidates = Object.entries(jobs).filter( ([, j]) => j && (j.phase === "anchor" || j.phase === "waiting") ); } if (!candidates.length) return; candidates.sort((a, b) => getQueueDue(a[1]) - getQueueDue(b[1])); const [key, job] = candidates[0]; const label = job.title || job.cwrid.slice(0, 8); if (!reEnter && job.phase === "waiting") { beginActivePhase(job); pushLog( `[${label}] \u8f6e\u5230\u672c\u8bfe\uff0c\u5f00\u59cb\u7ea6 ${Math.round(getRemainSec(job) / 60)} \u5206\u949f\u771f\u5b9e\u5fc3\u8df3\uff085 \u5206\u949f/\u6b21\uff09\u540e\u6536\u5c3e` ); } job.phase = "active"; if (!job.nextHeartbeatDue) job.nextHeartbeatDue = now + PROGRESS_MS; jobs[key] = job; saveJobs(jobs); const ctx = jobToCtx(job); const sec = job.lastPostedSec || job.startSec || 1; try { if (!reEnter) await warmPlaySession(ctx); const r0 = await postProcess(ctx, 0, sec); pushLog( `[${label}] ${reEnter ? "\u6062\u590d" : "\u8fdb\u5165"}\u4fdd\u6d3b status=0 @${sec}s code=${r0 && r0.code}\uff08${formatEta(job.finishDue - now)}\u540e\u6536\u5c3e\uff09` ); } catch (e) { pushLog(`[${label}] \u8fdb\u5165\u4f1a\u8bdd\u5f02\u5e38: ${e && e.message ? e.message : e}`); } } async function tickRealHeartbeat(job) { const now = Date.now(); if (job.phase !== "active" || now >= job.finishDue) return false; const nextDue = job.nextHeartbeatDue || now + PROGRESS_MS; if (now < nextDue) return false; const sec = getHeartbeatSec(job); const ctx = jobToCtx(job); const label = job.title || job.cwrid.slice(0, 8); try { const { r0, r1 } = await postHeartbeat(ctx, sec); job.lastHeartbeatAt = now; job.heartbeatCount = (job.heartbeatCount || 0) + 1; if (r1 && r1.code === 0) { job.lastPostedSec = sec; job.heartbeatOk = (job.heartbeatOk || 0) + 1; syncLocal(ctx, sec); } job.nextHeartbeatDue = now + PROGRESS_MS; const jobs = loadJobs(); jobs[`${job.uid}:${job.cwrid}:${job.coaid}`] = job; saveJobs(jobs); const ok = r1 && r1.code === 0; pushLog( `[${label}] ★\u5fc3\u8df3 \u7b2c${job.heartbeatCount}\u6b21 status=0→1 @${sec}s code=${r1 && r1.code}${r1 && r1.msg && r1.code !== 0 ? " " + r1.msg : ""}${ok ? "" : "\uff08\u4e0b\u8f6e\u5c06\u91cd\u8bd5\u8fdb\u5165\uff09"}` ); return true; } catch (e) { pushLog(`[${label}] \u5fc3\u8df3\u5f02\u5e38: ${e && e.message ? e.message : e}`); return false; } } async function runFillPhase(ctx) { if (isParallelMode()) { pushLog("\u4e32\u884c\u8f6e\u8be2\u6a21\u5f0f\uff1a\u8bf7\u5728\u7ae0\u8282\u9875\u767b\u8bb0\uff0c\u540e\u53f0\u8f6e\u6d41\u8fdb\u5165\u5404\u8bfe+300s\u540e\u9000\u51fa"); return; } if (!(await consumeChapterCloudQuota(ctx))) return; const duration = ctx.duration; const start = ctx.baseSec; const label = ctx.title || ctx.cwrid.slice(0, 8); const targets = buildSegmentTargets(start, duration); const segText = targets.map((t) => `${t}s`).join(" → "); await warmPlaySession(ctx); pushLog(`[${label}] \u5feb\u586b \u603b\u957f${duration}s \u8d77\u70b9${start}s \u500d\u7387${getMultiplier()}x`); pushLog(`\u5206${targets.length}\u6bb5: ${segText}`); let t0 = Date.now(); let processOk = false; try { const r0 = await postProcess(ctx, 0, start); pushLog(`status=0 @${start}s code=${r0 && r0.code}`); if (r0 && r0.code === 0) { t0 = Date.now(); processOk = true; } } catch (e) { pushLog(`status=0 \u5f02\u5e38: ${e && e.message ? e.message : e}`); } for (let i = 0; i < targets.length; i++) { if (!getEnabled()) return; if (i > 0) { const waitMs = segmentGapMs(); pushLog(`\u6bb5\u95f4\u7b49\u5f85 ${(waitMs / 1000).toFixed(1)}s…`); await sleep(waitMs); } const target = targets[i]; const isLast = i === targets.length - 1; const status = isLast ? 2 : 1; syncLocal(ctx, target); try { const res = await postProcess(ctx, status, target); pushLog( `API \u7b2c${i + 1}/${targets.length}\u6bb5 ${target}/${duration}s status=${status} code=${res && res.code}${res && res.msg && res.code !== 0 ? " " + res.msg : ""}` ); if (res && res.code === 0) processOk = true; } catch (e) { pushLog(`API \u7b2c${i + 1}\u6bb5 \u5f02\u5e38: ${e && e.message ? e.message : e}`); } } try { await postProcess(ctx, 3, duration); } catch (_) {} pushLog(`[${label}] status=3 \u5df2\u9000\u51fa → \u4e0b\u4e00\u8bfe`); const now = Date.now(); saveJob(ctx, { phase: "waiting", filledAt: now, finishDue: now + getRemainSec({ startSec: start, duration }) * 1000, duration, lastPostedSec: duration, activeSince: null, }); pushLog(`[${label}] \u5df2\u6392\u961f\uff0c\u8f6e\u5230\u540e\u9700\u7ea6 ${Math.round(duration / 60)} \u5206\u949f\u771f\u5b9e\u5fc3\u8df3`); updateJobsPanel(); } async function runFinishPhase(ctx) { const job = getJob(ctx); const duration = (job && job.duration) || ctx.duration; const label = ctx.title || ctx.cwrid.slice(0, 8); const fullCtx = Object.assign({}, ctx, { cwid: (job && job.cwid) || ctx.cwid }); if (HY_ENGINE_REQUIRED && (isRealtimeMode() || isSilentMode() || isOfflineMode())) { try { const loop = await _vf( job || ctx, "video_finish", { skip_enter: !!(job && job.progressFilled), progress_filled: !!(job && job.progressFilled), }, { ctx: fullCtx, maxSteps: 24 } ); if (loop.pause) { applyHyEnginePause(fullCtx, loop); return; } if (loop.ok) { saveJob(fullCtx, { engineSessionId: "", progressFilled: true, status2Sent: true, lastPostedSec: duration }); await finalizeVideoChapterDone(fullCtx, job, label, duration, isSilentMode() ? "[\u79bb\u7ebf]" : "[1:1]"); return; } if (/code=1|finish_code_1/i.test(String(loop.msg || ""))) { const retryDue = Date.now() + duration * 1000; saveJob(fullCtx, { phase: isSilentMode() ? "silent_finish_retry" : "rt_finish_retry", recordRetryDue: retryDue, finishDue: retryDue, status2Sent: true, progressFilled: true, lastPostedSec: duration, engineSessionId: loop.sessionId || "", }); return; } pushLog(`[${label}] \u4e91\u7aef\u6536\u5c3e\u5931\u8d25\uff1a${loop.msg || "\u672a\u77e5"}`); saveJob(fullCtx, { engineSessionId: "" }); } catch (e) { pushLog(`[${label}] \u4e91\u7aef\u6536\u5c3e\uff1a${e && e.message ? e.message : e}`); } return; } pushLog(`[${label}] \u63d0\u4ea4\u64ad\u5b8c\u8bb0\u5f55 @${duration}s`); await warmPlaySession(fullCtx); const skipEnter = (isTailMode() && job && job.status2Sent) || ((isRealtimeMode() || isSilentMode()) && job && job.progressFilled); if (!skipEnter) { try { await postProcess(fullCtx, 0, Math.max(1, duration - 1)); if (isTailMode()) lockProcessApi(); await sleep(1500); } catch (_) {} } else { pushLog(`[${label}] \u5df2\u6709 status=2\uff0c\u8df3\u8fc7 status=0\uff0c\u76f4\u63a5\u64ad\u5b8c\u8bb0\u5f55`); } let rec = null; try { rec = await postPlayRecord(fullCtx, duration); pushLog(`\u64ad\u5b8c\u8bb0\u5f55 @${duration}s code=${rec && rec.code}${rec && rec.msg ? " " + rec.msg : ""}`); } catch (e) { pushLog(`\u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`); } if (rec && rec.code === 1) { if (isRealtimeMode()) { const retryDue = Date.now() + duration * 1000; pushLog( `[${label}] [1:1] \u64ad\u5b8c code=1\uff0c${formatEta(retryDue - Date.now())}\u540e\u518d\u8bd5\u64ad\u5b8c\u8bb0\u5f55` ); saveJob(fullCtx, { phase: "rt_finish_retry", recordRetryDue: retryDue, finishDue: retryDue, status2Sent: true, progressFilled: true, lastPostedSec: duration, }); } else if (isSilentMode()) { const retryDue = Date.now() + duration * 1000; pushLog( `[${label}] [\u9759\u9ed8] \u64ad\u5b8c code=1\uff0c${formatEta(retryDue - Date.now())}\u540e\u518d\u8bd5\u64ad\u5b8c\u8bb0\u5f55` ); saveJob(fullCtx, { phase: "silent_finish_retry", recordRetryDue: retryDue, completeDue: retryDue, status2Sent: true, progressFilled: true, lastPostedSec: duration, }); } else if (isTailMode()) { const retryDue = getRecordRetryDue(job || { duration, sessionStartAt: Date.now() }); pushLog( `[${label}] \u65f6\u957f\u4e0d\u8db3\uff08status=2\u5df2\u8fc7\uff09\uff0c\u6302\u949f${formatEta(retryDue - Date.now())}\u540e\u300c1\u6b21\u5fc3\u8df3+\u64ad\u5b8c\u8bb0\u5f55\u300d\u518d\u8bd5\uff08\u9694\u65e5\u4ea6\u53ef\uff09` ); saveJob(fullCtx, { phase: "finish_wait", lastPostedSec: duration, status2Sent: true, status2At: (job && job.status2At) || Date.now(), sessionStartAt: (job && job.sessionStartAt) || (job && job.filledAt) || Date.now(), recordRetryDue: retryDue, }); } else if (isSerialMode()) { const cur = (job && job.lastPostedSec) || duration; const backSec = Math.max((job && job.startSec) || 1, cur - STEP_SEC); pushLog( `[${label}] \u89c2\u770b\u65f6\u957f\u4e0d\u8db3\uff0c\u9000\u56de@${backSec}s \u7ee7\u7eed\u8f6e\u8be2\uff08\u4ec5\u672c\u8bfe\uff0c\u5176\u4ed6\u8bfe\u4ecd5\u5206\u949f\u4e00\u8f6e\uff09` ); saveJob(fullCtx, { phase: "serial", lastPostedSec: backSec, }); } else { const remainMs = Math.max(ROUND_GAP_MS, getRemainSec(job || { startSec: 1, duration }) * 1000); pushLog(`[${label}] \u6709\u6548\u65f6\u957f\u4e0d\u8db3\uff0c${Math.ceil(remainMs / 60000)} \u5206\u949f\u540e\u91cd\u8bd5\u64ad\u5b8c\u8bb0\u5f55`); saveJob(fullCtx, { phase: "active", activeSince: (job && job.activeSince) || Date.now(), finishDue: Date.now() + remainMs, nextHeartbeatDue: Date.now(), }); await ensureActiveSession(); } updateJobsPanel(); return; } if (rec && rec.code === -2) { updateJobsPanel(); return; } try { await postProcess(fullCtx, 3, duration); } catch (_) {} pushLog(`[${label}] status=3 \u5df2\u9000\u51fa → \u4e0b\u4e00\u8bfe`); await finalizeVideoChapterDone(fullCtx, job, label, duration, ""); } function getKeepAliveIntervalMs(jobs) { if ( (isTailMode() && jobs.some((j) => j && j.phase === "waiting")) || (isP3ParallelMode() && jobs.some((j) => j && (j.phase === "stepping" || j.phase === "pending"))) || (isParallelStepMode() && jobs.some((j) => j && (j.phase === "stepping" || j.phase === "pending"))) ) { return TAIL_WAIT_KEEPALIVE_MS; } return KEEPALIVE_MS; } async function pingKeepAliveUrl(url) { const text = await fetch(url, { credentials: "include", __hyEngineMark: true }).then((r) => r.text() ); if (isPageLoginRedirect(text)) return { ok: false, login: true }; return { ok: true, login: false }; } async function pingPlayState(job) { if (!job.cwrid || !job.uid) return { ok: false, skip: true }; try { const res = await apiGet("/ashx/getCourseWarePlayState.ashx", { relationId: job.cwrid, userId: job.uid, }); if (res && res.code === -2) return { ok: false, login: true }; return { ok: true, login: false }; } catch (_) { return { ok: false, skip: true }; } } async function keepSessionAlive() { if (!getEnabled() || !getApiMode()) return; const jobs = Object.values(loadJobs()).filter( (j) => j && (j.phase === "waiting" || j.phase === "finish_wait" || j.phase === "ready" || TAIL_STEP_PHASES.includes(j.phase) || j.phase === "serial" || j.phase === "stepping" || j.phase === "pending" || j.phase === "anchor" || j.phase === "active" || j.phase === "rt_stepping" || j.phase === "rt_finish_retry" || j.phase === "silent_clock" || j.phase === "silent_filling" || j.phase === "silent_finish_retry") ); if (!jobs.length) return; const interval = getKeepAliveIntervalMs(jobs); if (Date.now() - lastKeepAliveAt < interval) return; lastKeepAliveAt = Date.now(); const waitingJobs = jobs.filter((j) => j.phase === "waiting"); const pool = waitingJobs.length && isTailMode() ? waitingJobs : jobs; const job = pool[keepAliveIdx % pool.length]; keepAliveIdx++; const urls = []; if (pageType() === "course" && /course\.aspx/i.test(location.pathname)) { urls.push(location.pathname + location.search); } if (job.cid) { const courseUrl = "/pages/course.aspx?cid=" + encodeURIComponent(job.cid); if (!urls.includes(courseUrl)) urls.push(courseUrl); } if (job.cwid || job.wareCwid) { urls.push( "/course_ware/course_ware_polyv.aspx?cwid=" + encodeURIComponent(job.wareCwid || job.cwid) + "&ff=0&ft=0&t=0" ); } let ok = false; let loginLost = false; for (const url of urls) { try { const r = await pingKeepAliveUrl(url); if (r.login) { loginLost = true; break; } if (r.ok) { ok = true; trace(`\u9875\u9762\u4fdd\u6d3b OK ${url.slice(0, 48)} (${job.title || job.cwrid.slice(0, 8)})`); } } catch (_) {} } if (!loginLost && !ok) { const st = await pingPlayState(job); if (st.login) loginLost = true; else if (st.ok) ok = true; } if (loginLost) { warnLoginIfNeeded(false); return; } if (waitingJobs.length && isTailMode()) { const nearest = waitingJobs .filter((j) => j.tailDue) .sort((a, b) => a.tailDue - b.tailDue)[0]; if (nearest) { pushLog( `\u6302\u949f\u4fdd\u6d3b OK\uff08${Math.round(interval / 60000)}\u5206\u949f/\u6b21\uff09\uff0c\u300c${nearest.title || nearest.cwrid.slice(0, 8)}\u300d${formatEta(Math.max(0, nearest.tailDue - Date.now()))}\u540e\u672b5\u5206\uff0c\u5171${waitingJobs.length}\u8bfe\u6302\u949f\u4e2d` ); } } else if (ok) { trace(`\u4f1a\u8bdd\u4fdd\u6d3b OK (${job.title || job.cwrid.slice(0, 8)})`); } } function recountCourseProgress(course) { if (!course || !course.chapters) return; course.chapterTotal = course.chapters.length; course.studyDone = course.chapters.filter((ch) => ch.done).length; course.examDone = course.chapters.filter((ch) => ch.examDone).length; course.untouchedCount = course.chapters.filter((ch) => ch.untouched && !ch.done).length; } function markPanelChapterStudyDone(ctx) { if (!ctx || !ctx.cid) return; const course = panelState.courses.find((c) => c.cid === ctx.cid); if (!course || !course.chapters || !course.chapters.length) return; const key = ctx.lsKey || ctx.uid + ctx.cwrid + ctx.coaid; const ch = course.chapters.find((c) => c.lsKey === key || (ctx.cwrid && c.cwrid === ctx.cwrid)); if (!ch) return; const wasDone = !!ch.done; const job = getJob({ uid: ctx.uid, cwrid: ctx.cwrid, coaid: ctx.coaid, cid: ctx.cid, lsKey: key }); const isInteractive = ch.interactive || (job && job.interactive); ch.done = true; ch.untouched = false; if (isInteractive) { ch.examDone = true; ch.examStatus = "none"; ch.studyState = "\u5df2\u5b8c\u6210"; ch.progressLabel = ""; } else if (course && isGongxuCourse(course)) { ch.examDone = true; ch.examStatus = "none"; ch.studyState = "\u5df2\u5b8c\u6210"; ch.progressLabel = ""; } else { ch.examDone = false; ch.examStatus = "exam"; ch.studyState = "\u5f85\u8003\u8bd5"; ch.progressLabel = ""; } recountCourseProgress(course); if (!wasDone) { const courseTitle = course.title || ""; const chapterTitle = ch.title || "\u7ae0\u8282"; pushUserLog(`${courseTitle ? `\u300c${courseTitle}\u300d` : ""}${chapterTitle} · \u89c6\u9891\u5df2\u5b66\u5b8c`); pushUserLog(getCourseProgressLine(course)); } const preview = panelState.chapterPreview.find((p) => p.cid === ctx.cid); if (preview && preview.chapters) { const pch = preview.chapters.find((c) => c.lsKey === key || (ctx.cwrid && c.cwrid === ctx.cwrid)); if (pch) { pch.done = true; pch.untouched = false; if (isInteractive) { pch.examDone = true; pch.examStatus = "none"; pch.studyState = "\u5df2\u5b8c\u6210"; pch.progressLabel = ""; } else if (course && isGongxuCourse(course)) { pch.examDone = true; pch.examStatus = "none"; pch.studyState = "\u5df2\u5b8c\u6210"; pch.progressLabel = ""; } else { pch.examDone = false; pch.examStatus = "exam"; pch.studyState = "\u5f85\u8003\u8bd5"; pch.progressLabel = ""; } } } } function findPanelChapterCtxByCwid(cwid) { if (!cwid) return null; const target = String(cwid).toUpperCase(); for (const course of panelState.courses) { if (!course.chapters || !course.chapters.length) continue; const ch = course.chapters.find((c) => String(c.cwid || "").toUpperCase() === target); if (ch) { return { uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, cwid: ch.cwid, lsKey: ch.lsKey, title: ch.title, }; } } return null; } function markPanelChapterExamDone(ctx) { if (!ctx) return; let course = ctx.cid ? panelState.courses.find((c) => c.cid === ctx.cid) : null; const key = ctx.lsKey || (ctx.uid && ctx.cwrid && ctx.coaid ? ctx.uid + ctx.cwrid + ctx.coaid : ""); let ch = null; if (course && course.chapters && course.chapters.length) { ch = course.chapters.find((c) => c.lsKey === key || (ctx.cwrid && c.cwrid === ctx.cwrid)) || (ctx.cwid ? course.chapters.find((c) => c.cwid === ctx.cwid) : null); } if (!ch && ctx.cwid) { const hit = findPanelChapterCtxByCwid(ctx.cwid); if (hit) { ctx = hit; course = panelState.courses.find((c) => c.cid === hit.cid); ch = course?.chapters?.find((c) => c.cwid === hit.cwid); } } if (!course || !ch) return; if (ch.examDone) return; ch.examDone = true; ch.examStatus = "done"; ch.studyState = "\u8003\u8bd5\u901a\u8fc7"; ch.progressLabel = ""; recountCourseProgress(course); pushUserLog(`${course.title ? `\u300c${course.title}\u300d` : ""}${ch.title || "\u7ae0\u8282"} · \u8003\u8bd5\u901a\u8fc7`); pushUserLog(getCourseProgressLine(course)); const preview = panelState.chapterPreview.find((p) => p.cid === ctx.cid); if (preview && preview.chapters) { const pch = preview.chapters.find((c) => c.lsKey === key || (ctx.cwrid && c.cwrid === ctx.cwrid)); if (pch) { pch.examDone = true; pch.examStatus = "done"; pch.studyState = "\u8003\u8bd5\u901a\u8fc7"; pch.progressLabel = ""; } } } function shouldQueueExamAfterVideo(ctx, job) { if (isGongxuCtx(ctx, job)) return false; if (!getAutoExam()) return false; const j = job || (ctx ? getJob(ctx) : null); if (j && j.interactive) return false; if (j && j.examDone) return false; const ch = findPanelChapterByLsKey( (ctx && ctx.lsKey) || (j && j.lsKey) || (ctx && ctx.uid && ctx.cwrid && ctx.coaid ? ctx.uid + ctx.cwrid + ctx.coaid : "") ); if (ch && ch.interactive) return false; if (ch && ch.examDone) return false; return !!resolveExamCwid(ctx, j); } function hasPendingExamWork() { if (!getAutoExam()) return false; const pendingJobs = Object.values(loadJobs()).some((j) => j && j.phase === "exam_pending"); if (pendingJobs) return true; const selected = pruneQueueToCourses(); for (const cid of selected) { const course = panelState.courses.find((c) => c.cid === cid); if (!course || !course.chapters) continue; if (isGongxuCourse(course)) continue; if (course.chapters.some((ch) => !ch.interactive && ch.done && !ch.examDone)) return true; } return false; } async function finalizeVideoChapterDone(ctx, job, label, studiedSec, sourceTag) { const duration = (job && job.duration) || ctx.duration; const fullCtx = Object.assign({}, ctx, { cwid: (job && job.cwid) || ctx.cwid }); try { await postProcess(fullCtx, 3, duration); } catch (_) {} syncLocal(fullCtx, duration); unlockExamButton(); const tag = sourceTag ? `${sourceTag} ` : ""; const prog = formatRtStudyLine(studiedSec, duration); if (isGongxuCtx(fullCtx, job)) { try { await submitChapterProcessExams(fullCtx, job, label); } catch (_) {} } if (shouldQueueExamAfterVideo(fullCtx, job)) { saveJob(fullCtx, { phase: "exam_pending", lastPostedSec: duration, progressFilled: !!(job && job.progressFilled), status2Sent: !!(job && job.status2Sent), examRetryDue: 0, }); pushLog(`[${label}] ${tag}✓ \u89c6\u9891\u5b66\u5b8c ${prog}\uff0c\u6392\u961f\u8003\u8bd5…`); refreshPanelAfterChapterDone(fullCtx); updateJobsPanel(); if (!isParallelMode()) await ensureActiveSession(); if (getEnabled() && getApiMode()) setTimeout(() => runBgScheduler(), 500); return { queuedExam: true }; } saveJob(fullCtx, { phase: "done", lastPostedSec: duration, progressFilled: !!(job && job.progressFilled), status2Sent: !!(job && job.status2Sent), }); if (isGongxuCtx(fullCtx, job)) { pushLog(`[${label}] ${tag}✓ \u6302\u949f\u6536\u5c3e\u5b8c\u6210 ${prog}\uff0c\u95ee\u7b54\u9898\u5df2\u63d0\u4ea4`); } else { pushLog(`[${label}] ${tag}✓ \u89c6\u9891\u5b66\u5b8c ${prog}`); } refreshPanelAfterChapterDone(fullCtx); updateJobsPanel(); if (!isParallelMode()) await ensureActiveSession(); maybeAutoStopWhenFinished(); return { queuedExam: false }; } function refreshPanelAfterChapterDone(ctx) { markPanelChapterStudyDone(ctx); panelState.lastCourseListKey = ""; renderCourseList(); renderChapterPreview(); updatePanel(); } async function completeChapterFromProbe(ctx, job, label, studiedSec) { pushLog( `[${label}] [1:1] ✓ \u5b66\u5b8c\u68c0\u6d4b\u901a\u8fc7\uff0c\u63d0\u524d\u5b8c\u6210 ${formatRtStudyLine(studiedSec, job.duration || ctx.duration)}` ); await finalizeVideoChapterDone(ctx, job, label, studiedSec, "[1:1]"); return true; } async function probeRealtimeFinish(ctx, job, label, studiedSec) { const duration = job.duration || ctx.duration; let rec = null; try { await sleep(500); rec = await postPlayRecord(ctx, duration); } catch (e) { pushLog(`[${label}] [1:1] \u5b66\u5b8c\u68c0\u6d4b\u5f02\u5e38: ${e && e.message ? e.message : e}`); return false; } const code = rec && rec.code; pushLog( `[${label}] [1:1] \u5b66\u5b8c\u68c0\u6d4b ${formatRtStudyLine(studiedSec, duration)} code=${code != null ? code : "?"}${rec && rec.msg ? " " + rec.msg : ""}` ); if (code === -2) { warnLoginIfNeeded(false); return false; } if (code === 0) return completeChapterFromProbe(ctx, job, label, studiedSec); return false; } async function tryProbeAfterApiMiss(ctx, job, label, studiedSec, res) { if (!res || res.code !== 3) return false; return probeRealtimeFinish(ctx, job, label, studiedSec); } async function tickRealtimeStep(job) { const ctx = jobToCtx(job); const label = job.title || job.cwrid.slice(0, 8); const now = Date.now(); const nextDue = job.nextStepDue || 0; if (now < nextDue) return false; if (!HY_ENGINE_REQUIRED) return false; try { const loop = await _vf( Object.assign({}, job, loadJobs()[jobKey(ctx)]), "video_realtime", { mode: "realtime" }, { ctx, maxSteps: 24 } ); if (loop.pause) { applyHyEnginePause(ctx, loop); return true; } if (loop.terminal && loop.ok) { return handleHyEngineVideoDone(ctx, job, label, loop); } if (loop.terminal && !loop.ok) { handleHyEngineFailure(ctx, job, label, loop, "[1:1]"); return false; } return true; } catch (e) { pushLog(`[${label}] [1:1] \u4e91\u7aef\u5f15\u64ce\uff1a${e && e.message ? e.message : e}`); saveJob(ctx, { nextStepDue: now + 60000 }); return false; } } function genUuid() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === "x" ? r : (r & 0x3) | 0x8; return v.toString(16); }); } function saveHdActive(payload) { localStorage.setItem(HD_ACTIVE_KEY, JSON.stringify(payload || {})); } function loadHdActive() { try { return JSON.parse(localStorage.getItem(HD_ACTIVE_KEY) || "null"); } catch (_) { return null; } } function clearHdActive() { localStorage.removeItem(HD_ACTIVE_KEY); clearHdLaunchSnapshot(); clearHdMetaCookie(); } function clearHdSessionArtifacts() { clearHdActive(); removeHdIframe(); } function hdApiOk(res) { if (!res) return false; const st = res._httpStatus; if (st === 401 || st === 403) return false; if (res.code === 0 || res.code === "0" || res.success === true) return true; if (res.code != null && res.code !== 0 && res.code !== "0" && res.success !== true) { if (typeof res.code === "number" && res.code !== 200) return false; } if (st >= 200 && st < 300) { if (res.token || res.courseWareId || res.userId || res.vid || res.catalogId != null) return true; if (res.code == null && res.success == null && !res._raw) return true; } return false; } function extractHdToken(res, body) { const b = body || hdApiBody(res); if (b && b.token) return b.token; if (res && res.token) return res.token; return ""; } function hdApiBody(res) { if (!res) return null; if (res.body !== undefined && res.body !== null) return res.body; if (res.data !== undefined && res.data !== null) return res.data; return res; } function extractHdblUrlFromText(text) { if (!text) return ""; const patterns = [ /href=["'](https?:\/\/hdbl\.91huayi\.com[^"']+)["']/i, /href=["'](\/\/hdbl\.91huayi\.com[^"']+)["']/i, /(https?:\/\/hdbl\.91huayi\.com\/\?[^"'<>\s]+)/i, ]; for (const re of patterns) { const m = text.match(re); if (m) return m[1].replace(/&/g, "&").replace(/^\/\//, "https://"); } return ""; } function classifyHdProbeTarget(url, status, text) { const u = String(url || ""); const html = String(text || ""); if (/hdbl\.91huayi\.com/i.test(u)) return { status: "hdbl", url: u.replace(/&/g, "&") }; if (/exam_result_hd\.aspx/i.test(u)) return { status: "done" }; if (/exam_notice_hd\.aspx/i.test(u)) return { status: "notice", url: u }; const fromHtml = extractHdblUrlFromText(html); if (fromHtml) return { status: "hdbl", url: fromHtml }; if (/exam_result_hd\.aspx/i.test(html)) return { status: "done" }; if (/exam_notice_hd\.aspx/i.test(html)) return { status: "notice", url: u }; return { status: "unknown", url: u, httpStatus: status }; } function xhrGetFollow(url) { return new Promise((resolve) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url, true); xhr.withCredentials = true; xhr.onload = () => resolve({ url: xhr.responseURL || url, status: xhr.status, text: xhr.responseText || "", }); xhr.onerror = () => resolve({ url, status: 0, text: "" }); xhr.send(); }); } async function probeHdChapterEntry(cwid) { const bases = [ `${location.origin}/course_ware/course_ware_hd.aspx?cwid=${encodeURIComponent(cwid)}&t=0`, `${location.origin}/course_ware/course_ware.aspx?cwid=${encodeURIComponent(cwid)}&t=0`, ]; for (const startUrl of bases) { const xhrRes = await xhrGetFollow(startUrl); const hit = classifyHdProbeTarget(xhrRes.url, xhrRes.status, xhrRes.text); if (hit.status === "hdbl" || hit.status === "done" || hit.status === "notice") return hit; let url = startUrl; for (let i = 0; i < 8; i++) { try { const r = await fetch(url, { credentials: "include", redirect: "manual" }); const loc = (r.headers && (r.headers.get("Location") || r.headers.get("location"))) || ""; if (r.type === "opaqueredirect" || (r.status >= 300 && r.status < 400 && loc)) { url = loc.startsWith("http") ? loc : new URL(loc, location.origin).href; const hop = classifyHdProbeTarget(url, r.status, ""); if (hop.status !== "unknown") return hop; continue; } const html = await r.text().catch(() => ""); const bodyHit = classifyHdProbeTarget(url, r.status, html); if (bodyHit.status !== "unknown") return bodyHit; break; } catch (_) { break; } } try { const r = await fetch(startUrl, { credentials: "include", redirect: "follow" }); const hit = classifyHdProbeTarget(r.url || startUrl, r.status, await r.text().catch(() => "")); if (hit.status !== "unknown") return hit; } catch (_) {} } return { status: "unknown" }; } function buildHdCmeEntryUrl(cwid) { return `${location.origin}/course_ware/course_ware_hd.aspx?cwid=${encodeURIComponent(cwid)}&t=0`; } async function resolveHdLaunchUrl(cwid) { const probe = await probeHdChapterEntry(cwid); if (probe.status === "hdbl") return probe.url; return null; } function findChapterOnPageByCwid(cwid) { if (!cwid) return null; return parseCourseChapterJobs().find((ch) => ch.cwid === cwid) || findAnyChapterOnPageByCwid(cwid); } function syncInteractiveJobsFromChapters(chapters) { if (!isHdInteractiveEnabled()) return 0; if (!chapters || !chapters.length) return 0; let n = 0; for (const ch of chapters) { if (!ch.interactive || !ch.done || !ch.cwid) continue; const jobs = loadJobs(); const key = Object.keys(jobs).find( (k) => jobs[k] && jobs[k].interactive && jobs[k].cwid === ch.cwid && jobs[k].phase !== "done" ); if (!key) continue; saveJob(jobToCtx(jobs[key]), { phase: "done" }); pushLog(`[${ch.title || ch.cwrid}] [\u4e92\u52a8] \u9875\u9762\u5df2\u5b8c\u6210\uff0c\u5df2\u540c\u6b65\u961f\u5217`); n++; } if (n) updateJobsPanel(); return n; } function parseHdLaunchParams(urlStr) { const u = new URL(urlStr); const p = u.searchParams; const num = (k, d = 0) => { const v = parseFloat(p.get(k)); return isNaN(v) ? d : v; }; return { aiAnalysis: num("aiAnalysis", 1), appId: p.get("appId") || "1", backUrl: decodeURIComponent(p.get("backUrl") || ""), businessCourseId: p.get("businessCourseId") || "", businessCustomParams: p.get("businessCustomParams") || "", checkDuration: num("checkDuration", 1), checkDurationNum: num("checkDurationNum", 1), checkStudyPath: num("checkStudyPath", 1), clientType: num("clientType", 0), courseWareId: p.get("courseWareId") || "", hash: p.get("hash") || "", noticeUrl: decodeURIComponent(p.get("noticeUrl") || ""), platformId: p.get("platformId") || "", userId: p.get("userId") || "", watchCompletelyFirst: num("watchCompletelyFirst", 1), timer: num("timer", Math.floor(Date.now() / 1000)), noDragFirst: num("noDragFirst", 1), noSpeedFirst: num("noSpeedFirst", 1), speedMaxNum: num("speedMaxNum", 1), isShowReturnBtn: num("isShowReturnBtn", 0), isShowWenjuan: num("isShowWenjuan", 1), }; } async function hdFetchJson(path, opts = {}) { const method = (opts.method || "GET").toUpperCase(); let url = path.startsWith("http") ? path : HDBL_ORIGIN + path; if (opts.query) { const u = new URL(url); Object.entries(opts.query).forEach(([k, v]) => { if (v != null && v !== "") u.searchParams.set(k, String(v)); }); url = u.href; } const headers = { Accept: "application/json, text/plain, */*", "Content-Type": "application/json;charset=UTF-8", Origin: HDBL_ORIGIN, }; if (opts.referer) headers.Referer = opts.referer; if (opts.token) headers.token = opts.token; const init = { method, credentials: "include", headers, __hyEngineMark: true }; if (method !== "GET" && opts.body != null) init.body = JSON.stringify(opts.body); const r = await fetch(url, init); const txt = await r.text(); let json; try { json = JSON.parse(txt); } catch (_) { json = { code: r.ok ? 0 : -1, _raw: txt.slice(0, 200) }; } json._httpStatus = r.status; return json; } function hdCatalogPayload(sess) { return { userId: sess.userId, courseWareId: sess.courseWareId, businessCourseId: sess.businessCourseId, platformId: sess.platformId, catalogId: sess.catalogId ?? null, pageType: sess.pageType || "start", token: sess.token || "", infoSource: null, branchQuestionCatalogId: sess.branchQuestionCatalogId ?? null, branchQuestionOptionId: sess.branchQuestionOptionId ?? null, branchCatalogId: sess.branchCatalogId ?? null, mainCatalogId: sess.mainCatalogId ?? null, currentCatalogType: sess.currentCatalogType ?? null, }; } function hdInsertCatalogPayload(sess) { return Object.assign({}, hdCatalogPayload(sess), { pageType: "start" }); } function hdPlayRecordPayload(sess) { return { userId: sess.userId, courseWareId: sess.courseWareId, businessCourseId: sess.businessCourseId, platformId: sess.platformId, infoSource: 0, }; } function hdDurationPayload(sess) { return { userId: sess.userId, courseWareId: sess.courseWareId, businessCourseId: sess.businessCourseId, platformId: sess.platformId, catalogId: sess.catalogId ?? null, pageType: sess.pageType || "start", token: sess.token || "", infoSource: 0, }; } function hdBasePayload(sess) { return hdDurationPayload(sess); } function extractHdMedias(data) { if (!data || typeof data !== "object") return []; const out = []; const seen = new Set(); const add = (m) => { if (!m || typeof m !== "object") return; const vid = m.vid || m.videoId || m.mediaId || m.polyvVid || m.videoVid || m.vId; if (!vid || seen.has(vid)) return; seen.add(String(vid)); out.push({ vid: String(vid), fileSize: m.fileSize || m.videoSize || "", duration: Math.max( 0, Math.floor(Number(m.duration || m.videoDuration || m.playDuration || m.totalTime || 0) || 0) ), }); }; if (Array.isArray(data.catalogInfoList)) { data.catalogInfoList.forEach((cat) => { const vos = cat && cat.catalogViewInfoVO; if (!Array.isArray(vos)) return; vos.forEach((vo) => { if (!vo || !vo.vid) return; const ct = String(vo.contentType || vo.vidType || "").toLowerCase(); if (ct === "videoupload" || ct === "video" || vo.vidType === "video") add(vo); }); }); } ["videoList", "mediaList", "hdVideoList", "videos", "hdMediaList", "playList", "mediaInfoList"].forEach( (k) => { const arr = data[k]; if (Array.isArray(arr)) arr.forEach(add); } ); add(data); if (data.video && typeof data.video === "object") add(data.video); const walk = (node, depth) => { if (!node || depth > 10) return; if (typeof node === "string") { if (/^[0-9a-f]{10,}_[0-9]+$/i.test(node)) add({ vid: node, duration: 0 }); return; } if (Array.isArray(node)) { node.forEach((x) => walk(x, depth + 1)); return; } if (typeof node === "object") { if (node.vid || node.videoId || node.mediaId) add(node); Object.values(node).forEach((x) => walk(x, depth + 1)); } }; walk(data, 0); return out; } function extractDurationForVid(data, vid) { if (!data || !vid) return 0; const target = String(vid).toLowerCase(); let found = 0; const walk = (node, depth) => { if (found > 0 || !node || depth > 14) return; if (Array.isArray(node)) { node.forEach((x) => walk(x, depth + 1)); return; } if (typeof node !== "object") return; const nodeVid = node.vid || node.videoId || node.mediaId; if (nodeVid && String(nodeVid).toLowerCase() === target) { const d = Number( node.duration || node.videoDuration || node.playDuration || node.totalTime || node.mediaDuration || 0 ); if (d > 0) found = Math.ceil(d); } Object.values(node).forEach((x) => walk(x, depth + 1)); }; walk(data, 0); return found; } function estimateHdVideoDurationFromFileSize(fileSize) { const bytes = Number(String(fileSize || "").replace(/[^\d.]/g, "")); if (!bytes || bytes <= 0) return 0; const rate = bytes < 8000000 ? 180000 : bytes < 30000000 ? 220000 : 280000; const pad = bytes < 8000000 ? 25 : bytes < 30000000 ? 20 : 15; return Math.max(60, Math.min(7200, Math.ceil(bytes / rate) + pad)); } function loadHdVidDurCache() { try { return JSON.parse(localStorage.getItem(HD_VID_DUR_KEY) || "{}"); } catch (_) { return {}; } } function getHdVidDurFromCache(vid) { const sec = Number(loadHdVidDurCache()[String(vid || "").toLowerCase()] || 0); return sec > 0 ? Math.ceil(sec) : 0; } function saveHdVidDurCache(vid, sec) { const n = Math.ceil(Number(sec) || 0); if (!vid || n <= 0) return; const cache = loadHdVidDurCache(); cache[String(vid).toLowerCase()] = n; const keys = Object.keys(cache); if (keys.length > 400) { keys.slice(0, keys.length - 400).forEach((k) => delete cache[k]); } localStorage.setItem(HD_VID_DUR_KEY, JSON.stringify(cache)); } function normalizePolyvDurationSec(val) { const n = Number(val); if (!n || n <= 0) return 0; if (n > 86400) return Math.floor(n / 1000); return Math.max(1, Math.floor(n)); } function normalizePlayerDurationSec(val) { const n = Number(val); if (!n || !isFinite(n) || n <= 0) return 0; return Math.max(1, Math.floor(n)); } function hdParseClockTextToSec(text) { const s = String(text || "").trim(); const hm = s.match(/^(\d{1,2}):(\d{2})$/); if (hm) return parseInt(hm[1], 10) * 60 + parseInt(hm[2], 10); const hms = s.match(/^(\d{1,2}):(\d{2}):(\d{2})$/); if (hms) return parseInt(hms[1], 10) * 3600 + parseInt(hms[2], 10) * 60 + parseInt(hms[3], 10); return 0; } function hdReadPolyvDurationFromDom(doc) { if (!doc) return 0; const selectors = [ ".pv-duration", ".pv-time-duration", ".plv-duration", ".plv-time-duration", ".pv-time .pv-duration", ".pv-controls-right .pv-duration", "[class*='time-duration']", "[class*='pv-duration']", ]; for (const q of selectors) { try { const el = doc.querySelector(q); if (!el) continue; const d = hdParseClockTextToSec((el.textContent || "").trim()); if (d >= 5 && d <= 7200) return d; } catch (_) {} } const collectClockSecs = (root) => { const secs = []; if (!root) return secs; try { root.querySelectorAll("span, div, em, i, b, p, label, time").forEach((el) => { if (el.children.length > 2) return; const d = hdParseClockTextToSec((el.textContent || "").trim()); if (d >= 5 && d <= 7200) secs.push(d); }); } catch (_) {} return secs; }; try { const roots = [ doc.querySelector(".pv-video-player"), doc.querySelector(".plv-player"), doc.querySelector(".polyv-player"), doc.querySelector("#video"), doc.querySelector("video") && doc.querySelector("video").closest("[class*='player'], [class*='video']"), ].filter(Boolean); for (const root of roots) { const secs = collectClockSecs(root); if (secs.length) return Math.max.apply(null, secs); } } catch (_) {} try { const video = doc.querySelector("video"); if (video) { let node = video.parentElement; for (let depth = 0; depth < 10 && node; depth++) { const secs = collectClockSecs(node); if (secs.length) return Math.max.apply(null, secs); node = node.parentElement; } } } catch (_) {} return 0; } function extractPolyvDurationFromPayload(data) { if (!data) return 0; const walk = (node, depth) => { if (!node || depth > 10) return 0; if (typeof node !== "object") return 0; for (const k of ["duration", "videoDuration", "totalDuration", "playDuration", "pd"]) { if (node[k] != null) { const v = normalizePolyvDurationSec(node[k]); if (v > 0) return v; } } const nested = [node.body, node.data, node.video, node.meta, node.result]; for (const x of nested) { const d = walk(x, depth + 1); if (d > 0) return d; } const list = Array.isArray(node) ? node : Object.values(node); for (const x of list) { const d = walk(x, depth + 1); if (d > 0) return d; } return 0; }; return walk(data, 0); } function parsePolyvPdxDuration(buf) { if (!buf || !buf.byteLength) return 0; let text = ""; try { text = new TextDecoder("utf-8").decode(buf); } catch (_) {} if (text) { const m1 = text.match( /\b(?:duration|videoDuration|totalDuration|playDuration|pd)\b[=:"\s]+(\d+(?:\.\d+)?)/i ); if (m1) return normalizePolyvDurationSec(Number(m1[1])); const m2 = text.match(/\bduration\s*=\s*["'](\d+(?:\.\d+)?)["']/i); if (m2) return normalizePolyvDurationSec(Number(m2[1])); try { const d = extractPolyvDurationFromPayload(JSON.parse(text)); if (d > 0) return d; } catch (_) {} } return 0; } function buildPolyvPlayinfoUrls(vid, param, token) { const userid = param.userid || param.userId || param.uid || ""; const ts = param.ts != null ? String(param.ts) : ""; const sign = param.sign || ""; const hash = param.hash || param.h || ""; const out = []; const push = (base, extra) => { const u = new URL(base); u.searchParams.set("vid", vid); if (userid) u.searchParams.set("userid", userid); if (ts) u.searchParams.set("ts", ts); if (sign) u.searchParams.set("sign", sign); if (hash) u.searchParams.set("hash", hash); if (token) u.searchParams.set("token", token); if (extra) { Object.keys(extra).forEach((k) => u.searchParams.set(k, extra[k])); } out.push(u.toString()); }; if (userid || token) { push("https://hls.videocc.net/player/playinfo", token ? { device: "desktop" } : null); } if (userid && ts && sign) { push("https://player.polyv.net/secure/" + encodeURIComponent(vid) + ".json"); } return out; } async function hdFetchExternalJson(url) { const r = await fetch(url, { credentials: "omit", mode: "cors" }); const text = await r.text(); try { return JSON.parse(text); } catch (_) { const m = text.match( /\b(?:duration|videoDuration|totalDuration|playDuration|pd)\b['"]?\s*[:=]\s*(\d+(?:\.\d+)?)/i ); return m ? { duration: Number(m[1]) } : null; } } async function hdFetchPolyvPdxDuration(vid, userid, token) { if (!vid || !userid || !token) return 0; const baseVid = String(vid).replace(/_9$/i, "_2"); for (const bucket of "0123456789abcdef") { const url = "https://hls.videocc.net/" + encodeURIComponent(userid) + "/" + bucket + "/" + encodeURIComponent(baseVid) + ".pdx?device=desktop&token=" + encodeURIComponent(token); try { const r = await fetch(url, { credentials: "omit", mode: "cors" }); if (!r.ok) continue; const d = parsePolyvPdxDuration(await r.arrayBuffer()); if (d > 0) return d; } catch (_) {} } return 0; } function unwrapPolyvApiBody(res) { let data = hdApiBody(res); if (typeof data === "string") { const s = data.trim(); if (s.startsWith("{") || s.startsWith("[")) { try { data = JSON.parse(s); } catch (_) {} } } if (data && typeof data === "object" && data.body != null) { const inner = data.body; if (typeof inner === "object" && (inner.userid || inner.userId || inner.sign || inner.ts)) { return inner; } if (typeof inner === "string" && inner.length > 8 && !/^\{/.test(inner)) { return { token: inner }; } } return data && typeof data === "object" ? data : {}; } function extractPlaySafeToken(res) { const data = unwrapPolyvApiBody(res); if (typeof data === "string" && data.length > 8) return data.trim(); if (!data || typeof data !== "object") return ""; return String( data.token || data.playSafeToken || data.playsafeToken || data.body || data.data || "" ).trim(); } function hdJsonpRequest(url, paramName, timeoutMs) { return new Promise((resolve) => { const cb = "_hyPv" + Date.now() + Math.floor(Math.random() * 1e5); let settled = false; const finish = (val) => { if (settled) return; settled = true; clearTimeout(timer); try { delete window[cb]; } catch (_) {} if (script.parentNode) script.remove(); resolve(val); }; const timer = setTimeout(() => finish(null), timeoutMs || 9000); window[cb] = (data) => finish(data); const sep = url.includes("?") ? "&" : "?"; const script = document.createElement("script"); script.src = url + sep + encodeURIComponent(paramName || "callback") + "=" + encodeURIComponent(cb); script.onerror = () => finish(null); document.head.appendChild(script); }); } async function hdTryPolyvPlayinfoUrls(urls) { const cbNames = ["callback", "jsonpCallback", "cb"]; for (const url of urls) { try { const data = await hdFetchExternalJson(url); const d = extractPolyvDurationFromPayload(data); if (d > 0) return d; } catch (_) {} for (const p of cbNames) { const data = await hdJsonpRequest(url, p); const d = extractPolyvDurationFromPayload(data); if (d > 0) return d; } } return 0; } function hdReadPolyvDurationFromWindow(win) { if (!win) return 0; let fromDom = 0; let doc = null; try { doc = win.document; fromDom = hdReadPolyvDurationFromDom(doc); } catch (_) {} const apiSecs = []; try { const players = [ win.player, win.polyvPlayerobject, win.polyvVideoPlayer, win.polyvPlayer, ]; for (const pl of players) { if (pl && typeof pl.j2s_getDuration === "function") { const d = normalizePlayerDurationSec(pl.j2s_getDuration()); if (d > 0) apiSecs.push(d); } } } catch (_) {} try { if (doc) { for (const v of doc.querySelectorAll("video")) { if (v.duration && isFinite(v.duration) && v.duration > 1) { const d = normalizePlayerDurationSec(v.duration); if (d > 0) apiSecs.push(d); } } } } catch (_) {} const fromApi = apiSecs.length ? Math.min.apply(null, apiSecs) : 0; if (fromDom > 0) { if (fromApi > 0 && fromApi !== fromDom && Math.abs(fromApi - fromDom) <= 2) { return fromDom; } return fromDom; } return fromApi; } async function hdProbePolyvDurationFromDom(maxMs) { const start = Date.now(); while (Date.now() - start < (maxMs || 12000)) { const d = hdReadPolyvDurationFromWindow(window); if (d > 0) return d; await sleep(500); } return 0; } async function hdSyncPageToCurrentCatalog(sess) { if (!isHdblHost() || !sess || !sess.catalogId) return; const target = buildHdProblemReferer(sess); const cur = location.href.split("#")[0]; const tgt = target.split("#")[0]; if (cur === tgt) return; try { history.replaceState(null, "", target); window.dispatchEvent(new PopStateEvent("popstate")); } catch (_) { return; } await sleep(2800); } function hdPauseVisiblePlayers() { try { const pause = (pl) => { if (pl && typeof pl.j2s_pauseVideo === "function") pl.j2s_pauseVideo(); }; pause(window.player); pause(window.polyvPlayerobject); pause(window.polyvVideoPlayer); pause(window.polyvPlayer); document.querySelectorAll("video").forEach((v) => { try { v.pause(); } catch (_) {} }); } catch (_) {} } function hdEnsureVideoApiReferer(sess) { if (!sess) return; sess.launchReferer = buildHdProblemReferer(sess); } async function hdRetreatToHomePage(sess, reason) { if (!isHdblHost()) return; const target = `${HDBL_ORIGIN}/home`; const cur = location.href.split("#")[0]; hdPauseVisiblePlayers(); if (!cur.endsWith("/home")) { try { history.replaceState(null, "", target); window.dispatchEvent(new PopStateEvent("popstate")); } catch (_) {} } hdEnsureVideoApiReferer(sess); if (reason) pushLog(`[${sess.label}] [\u4e92\u52a8] ${reason}`); await sleep(350); } async function hdSyncVideoPageForApi(sess) { hdEnsureVideoApiReferer(sess); if (!isHdblHost() || !sess || !sess.catalogId) return; const target = sess.launchReferer; const cur = location.href.split("#")[0]; const tgt = String(target || "").split("#")[0]; if (cur === tgt) return; try { history.replaceState(null, "", target); window.dispatchEvent(new PopStateEvent("popstate")); } catch (_) {} await sleep(300); } async function hdFinalizeVideoPlayback(sess, vid, basePayload, finalTime) { await hdSyncVideoPageForApi(sess); await hdSendMediaProgress( sess, Object.assign({}, basePayload, { eventType: 17, currentPlayTime: finalTime }) ); await hdSendMediaProgress( sess, Object.assign({}, basePayload, { eventType: 19, currentPlayTime: finalTime }) ); await hdPost( "/api/hdCourseWare/insertUserHdVideoPlayRecord", { businessCourseId: sess.businessCourseId, courseWareId: String(sess.courseWareId).toUpperCase(), platformId: sess.platformId, userId: sess.userId, vid, type: 2, infoSource: 0, ban_seek: true, }, sess ); pushLog( `[${sess.label}] [\u4e92\u52a8] \u5df2\u4e0a\u62a5\u5b8c\u64ad\u8bb0\u5f55 type=2 · ${Math.round(finalTime)}\u79d2` ); } async function hdProbePolyvDurationViaIframe(sess, maxMs) { if (!isHdblHost() || !sess || !sess.catalogId) return 0; const url = buildHdProblemReferer(sess); return new Promise((resolve) => { const iframe = document.createElement("iframe"); iframe.setAttribute( "style", "position:fixed;left:0;bottom:0;width:320px;height:180px;opacity:0.02;pointer-events:none;z-index:2147483646;border:0" ); let settled = false; const finish = (v) => { if (settled) return; settled = true; clearInterval(timer); iframe.remove(); resolve(v || 0); }; document.body.appendChild(iframe); iframe.src = url; const start = Date.now(); const timer = setInterval(() => { if (Date.now() - start > (maxMs || 18000)) return finish(0); try { const d = hdReadPolyvDurationFromWindow(iframe.contentWindow); if (d > 0) return finish(d); } catch (_) {} }, 600); }); } function resolveHdVideoDuration(sess, media, catalogData) { if (media && media.duration > 0) { return { duration: media.duration, source: "\u76ee\u5f55\u5b57\u6bb5" }; } const fromData = extractDurationForVid(catalogData, media && media.vid); if (fromData > 0) return { duration: fromData, source: "\u76ee\u5f55\u6570\u636e" }; const fromSize = estimateHdVideoDurationFromFileSize(media && media.fileSize); if (fromSize > 0) { return { duration: fromSize, source: media && media.fileSize ? `\u6587\u4ef6\u5927\u5c0f ${media.fileSize} \u5b57\u8282\u4f30\u7b97` : "\u6587\u4ef6\u5927\u5c0f\u4f30\u7b97", }; } const std = Number(sess && sess.hdStandardDuration) || 0; if (std > 0) { return { duration: Math.max(120, Math.ceil(std / 5)), source: "\u8bfe\u4ef6\u6807\u51c6\u65f6\u957f\u5206\u644a" }; } const chk = Number(sess && sess.checkDurationNum) || 0; if (sess && sess.checkDuration && chk > 0) { return { duration: Math.max(300, Math.ceil((chk * 3600) / 5)), source: "checkDurationNum \u5206\u644a", }; } return { duration: 0, source: "" }; } function syncHdCoursewareMeta(sess, body) { if (!body || typeof body !== "object") return; const info = body.coursewareInfo; if (info && info.hdStandardDuration != null) { const std = Number(info.hdStandardDuration); if (std > 0) sess.hdStandardDuration = std; } } function extractHdDirectoryList(data) { if (!data || typeof data !== "object") return []; const list = data.directoryList; if (!Array.isArray(list)) return []; return list .slice() .sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)) .map((x) => ({ catalogId: String(x.catalogId || ""), pageType: x.pageType || "view", title: x.title || x.catalogTitle || "", disabled: !!x.disabled, })) .filter((x) => x.catalogId); } function extractHdCatalogList(data) { const out = []; const seen = new Set(); const walk = (node, depth) => { if (!node || depth > 12) return; if (Array.isArray(node)) { node.forEach((x) => walk(x, depth + 1)); return; } if (typeof node !== "object") return; const cid = node.catalogId || node.pageCatalogId || node.id; if (cid && /^[0-9A-F-]{36}$/i.test(String(cid)) && !seen.has(String(cid).toUpperCase())) { seen.add(String(cid).toUpperCase()); out.push({ catalogId: String(cid), pageType: node.pageType || "view", title: node.title || node.catalogTitle || node.name || "", }); } Object.values(node).forEach((x) => walk(x, depth + 1)); }; walk(data, 0); return out; } function extractPreNextEntry(body, option) { if (!Array.isArray(body)) return null; const opt = String(option || "").toLowerCase(); return body.find((x) => String((x && x.option) || "").toLowerCase() === opt) || null; } function extractNextCatalogId(body, currentId) { if (!body || typeof body !== "object") return ""; const cur = String(currentId || "").toUpperCase(); const pick = (id) => { if (!id) return ""; const s = String(id); return s.toUpperCase() !== cur ? s : ""; }; const nextEntry = extractPreNextEntry(body, "next"); if (nextEntry) return pick(nextEntry.catalogId); if (Array.isArray(body)) return ""; const direct = [ body.nextCatalogId, body.nextPageCatalogId, body.nextId, body.next && (body.next.catalogId || body.next.id), body.nextCatalog && (body.nextCatalog.catalogId || body.nextCatalog.id), body.afterCatalog && body.afterCatalog.catalogId, ]; for (const f of direct) { const n = pick(f); if (n) return n; } if (String(body.option || "").toLowerCase() === "next") { const n = pick(body.catalogId); if (n) return n; } const list = extractHdCatalogList(body); if (list.length >= 2) { const idx = list.findIndex((x) => String(x.catalogId).toUpperCase() === cur); if (idx >= 0 && idx + 1 < list.length) return list[idx + 1].catalogId; } return pick(body.catalogId); } function extractNextPageType(body) { const nextEntry = extractPreNextEntry(body, "next"); if (nextEntry && nextEntry.pageType) return String(nextEntry.pageType); if (!body || Array.isArray(body)) return ""; return ( body.pageType || (body.nextCatalog && body.nextCatalog.pageType) || (body.catalog && body.catalog.pageType) || "" ); } function syncBranchFieldsFromData(sess, data) { if (!data || typeof data !== "object") return; [ "branchQuestionCatalogId", "branchQuestionOptionId", "branchCatalogId", "mainCatalogId", "currentCatalogType", ].forEach((k) => { if (data[k] !== undefined) sess[k] = data[k]; }); } async function hdFetchCatalogNav(sess, withOption) { const query = { userId: sess.userId, courseWareId: sess.courseWareId, businessCourseId: sess.businessCourseId, platformId: sess.platformId, pageType: sess.pageType || "view", token: sess.token, }; if (sess.catalogId) query.catalogId = sess.catalogId; if (withOption) query.option = "next"; const res = await hdGet("/api/hdCatalog/getCatalogPreAndNextV2", query, sess); if (res._httpStatus === 401 || res._httpStatus === 403) return { nextId: "", pageType: "" }; const body = hdApiBody(res) || res; const nextId = extractNextCatalogId(body, sess.catalogId); const pageType = extractNextPageType(body); const nextEntry = extractPreNextEntry(body, "next"); if (nextEntry) syncBranchFieldsFromData(sess, nextEntry); return { nextId, pageType, _navBody: body }; } async function hdIsCourseFinished(sess) { try { const res = await hdGet( "/api/hdCourseWare/getEndPageInfo", { userId: sess.userId, courseWareId: sess.courseWareId, businessCourseId: sess.businessCourseId, platformId: sess.platformId, pageType: "start", token: sess.token, }, sess ); if (!hdApiOk(res) && res._httpStatus && res._httpStatus >= 400) return false; const body = hdApiBody(res) || res; if (body.isFinish === 1 || body.isFinish === true) return true; if (body.canFinish === 1 || body.canFinish === true) return true; if (body.finishFlag === 1 || body.finishFlag === true) return true; if (body.hasRecord === 1 || body.hasRecord === true) return true; return false; } catch (_) { return false; } } function hdPickNextFromDirectory(sess, data) { if (!data || typeof data !== "object") return null; const cur = String(sess.catalogId || "").toUpperCase(); const pick = (id, pageType, source, title) => { if (!id || String(id).toUpperCase() === cur) return null; return { catalogId: String(id), pageType: pageType || "view", source: source || "\u76ee\u5f55", title: title || "", }; }; if (data.pageSkipParams && data.pageSkipParams.catalogId) { const x = pick( data.pageSkipParams.catalogId, data.pageSkipParams.pageType, "pageSkipParams", data.pageSkipParams.title ); if (x) return x; } const dirs = extractHdDirectoryList(data); if (!dirs.length) return null; const idx = dirs.findIndex((x) => String(x.catalogId).toUpperCase() === cur); for (let i = idx + 1; i < dirs.length; i++) { const x = dirs[i]; if (x.disabled) { const r = pick(x.catalogId, x.pageType, "\u76ee\u5f55\u4e0b\u4e00\u672a\u5b66\u5b8c", x.title); if (r) return r; } } if (sess.mainCatalogId) { const mainIdx = dirs.findIndex( (x) => String(x.catalogId).toUpperCase() === String(sess.mainCatalogId).toUpperCase() ); if (mainIdx >= 0 && mainIdx + 1 < dirs.length) { const x = dirs[mainIdx + 1]; const r = pick(x.catalogId, x.pageType, "\u4e3b\u76ee\u5f55\u4e0b\u4e00\u9879", x.title); if (r) return r; } } const any = dirs.find((x) => x.disabled && String(x.catalogId).toUpperCase() !== cur); if (any) { const r = pick(any.catalogId, any.pageType, "\u76ee\u5f55\u672a\u5b66\u5b8c\u9879", any.title); if (r) return r; } return null; } function hdIsDirectoryComplete(data) { const dirs = extractHdDirectoryList(data); if (!dirs.length) return false; return dirs.every((x) => !x.disabled); } async function hdIsCoursewareProgressComplete(sess) { const pct = await hdFetchProgressPct(sess); return pct >= 99; } function extractFirstCatalogId(data) { if (!data || typeof data !== "object") return ""; if (data.pageSkipParams && data.pageSkipParams.catalogId) { return String(data.pageSkipParams.catalogId); } const nextEntry = extractPreNextEntry(data, "next"); if (nextEntry && nextEntry.catalogId) return String(nextEntry.catalogId); const direct = data.firstCatalogId || data.startCatalogId || data.defaultCatalogId || data.nextCatalogId || data.currentCatalogId; if (direct) return String(direct); const list = extractHdCatalogList(data); if (list.length) return list[0].catalogId; if (data.catalogId) return String(data.catalogId); const next = data.nextCatalog; if (next && next.catalogId) return String(next.catalogId); return ""; } function buildHdProblemReferer(sess, extra) { const u = new URL(`${HDBL_ORIGIN}/problem/view`); u.searchParams.set("userId", sess.userId); u.searchParams.set("courseWareId", sess.courseWareId); u.searchParams.set("businessCourseId", sess.businessCourseId); u.searchParams.set("platformId", sess.platformId); if (sess.catalogId) u.searchParams.set("catalogId", sess.catalogId); u.searchParams.set("pageType", sess.pageType || "view"); if (extra && extra.option) u.searchParams.set("option", extra.option); return u.href; } function buildHdEndReferer(sess) { const u = new URL(`${HDBL_ORIGIN}/problem/end`); u.searchParams.set("option", "next"); u.searchParams.set("catalogId", sess.catalogId || ""); u.searchParams.set("pageType", sess.pageType === "end" ? "" : sess.pageType || ""); if (sess.branchQuestionCatalogId) { u.searchParams.set("branchQuestionCatalogId", sess.branchQuestionCatalogId); } if (sess.branchQuestionOptionId) { u.searchParams.set("branchQuestionOptionId", sess.branchQuestionOptionId); } if (sess.branchCatalogId) u.searchParams.set("branchCatalogId", sess.branchCatalogId); if (sess.mainCatalogId) u.searchParams.set("mainCatalogId", sess.mainCatalogId); if (sess.currentCatalogType) { u.searchParams.set("currentCatalogType", sess.currentCatalogType); } return u.href; } function hdNavIndicatesEnd(navBody) { if (!Array.isArray(navBody)) return false; const hasNext = navBody.some( (x) => String((x && x.option) || "").toLowerCase() === "next" && x && x.catalogId ); if (hasNext) return false; return navBody.some((x) => String((x && x.option) || "").toLowerCase() === "pre"); } async function hdFetchEndPageInfo(sess) { const res = await hdGet( "/api/hdCourseWare/getEndPageInfo", { userId: sess.userId, courseWareId: sess.courseWareId, businessCourseId: sess.businessCourseId, platformId: sess.platformId, pageType: "start", token: sess.token, }, sess ); return hdApiBody(res) || {}; } async function hdRunEndPageFlow(sess) { pushLog(`[${sess.label}] [\u4e92\u52a8] \u5df2\u5230\u6700\u540e\u4e00\u9875\uff0c\u8fdb\u5165\u7ed3\u675f\u6d41\u7a0b…`); const endRefererCtx = Object.assign({}, sess, { branchQuestionCatalogId: sess.branchQuestionCatalogId, branchQuestionOptionId: sess.branchQuestionOptionId, branchCatalogId: sess.branchCatalogId, mainCatalogId: sess.mainCatalogId, currentCatalogType: sess.currentCatalogType || "question", }); sess.launchReferer = buildHdEndReferer(endRefererCtx); try { const endInfo = await hdFetchEndPageInfo(sess); if (endInfo && endInfo.score != null) { pushLog(`[${sess.label}] [\u4e92\u52a8] \u7ed3\u675f\u9875 · \u5f97\u5206 ${endInfo.score}`); } } catch (_) {} sess.pageType = "end"; sess.catalogId = null; await hdPost( "/api/hdCourseWare/insertHdDurationLog", Object.assign({}, hdDurationPayload(sess), { catalogId: null, pageType: "end" }), sess ); const finishRes = await hdPost( "/api/hdCourseWare/finishCourseWare", Object.assign({}, hdDurationPayload(sess), { catalogId: null, pageType: "start" }), sess ); const finishBody = hdApiBody(finishRes) || finishRes; const msg = (finishBody && finishBody.resultMsg) || (finishBody && finishBody.message) || "\u5b66\u4e60\u5b8c\u6bd5"; pushLog(`[${sess.label}] [\u4e92\u52a8] finishCourseWare · ${msg}`); sess._endFlowDone = true; return true; } async function hdFetchCoursewareProgress(sess) { const res = await hdGet( "/api/hdCatalog/getHdCoursewareProgress", { userId: sess.userId, courseWareId: sess.courseWareId, businessCourseId: sess.businessCourseId, platformId: sess.platformId, pageType: sess.pageType || "start", token: sess.token, }, sess ); return hdApiBody(res) || res; } async function hdFetchProgressPct(sess) { try { const body = await hdFetchCoursewareProgress(sess); const pct = Number(body); return isFinite(pct) && pct > 0 ? pct : 0; } catch (_) { return 0; } } async function hdResolveFirstCatalog(sess, data, logLabel) { let first = extractFirstCatalogId(data); if (!first) { const progress = await hdFetchCoursewareProgress(sess); first = extractFirstCatalogId(progress); } if (!first) { const res = await hdGet( "/api/hdCatalog/getCatalogPreAndNextV2", { userId: sess.userId, courseWareId: sess.courseWareId, businessCourseId: sess.businessCourseId, platformId: sess.platformId, pageType: "view", token: sess.token, option: "next", }, sess ); const body = hdApiBody(res) || res; first = extractNextCatalogId(body, "") || extractFirstCatalogId(body); } if (!first) return false; sess.catalogId = first; sess.pageType = "view"; sess.pageSessionId = genUuid(); sess.launchReferer = buildHdProblemReferer(sess); pushLog( `[${sess.label}] [\u4e92\u52a8] ${logLabel || "\u8fdb\u5165\u9996\u76ee\u5f55"} · catalog=${String(first).slice(0, 8)}…` ); return true; } async function hdResolveResumeCatalog(sess, data) { const progressPct = await hdFetchProgressPct(sess); if (!progressPct || progressPct <= HD_PROGRESS_FRESH_PCT) { return hdResolveFirstCatalog(sess, data, "\u8fdb\u5165\u9996\u76ee\u5f55"); } const dirs = extractHdDirectoryList(data); const next = dirs.find((x) => x.disabled); if (next) { sess.catalogId = next.catalogId; sess.pageType = next.pageType || "view"; sess.pageSessionId = genUuid(); sess.launchReferer = buildHdProblemReferer(sess); const label = next.title || next.catalogId.slice(0, 8) + "…"; pushLog( `[${sess.label}] [\u4e92\u52a8] \u65ad\u70b9\u7eed\u5b66 · \u4ece\u300c${label}\u300d\u7ee7\u7eed\uff08\u8fdb\u5ea6 ${Math.round(progressPct)}%\uff09` ); return true; } if (await hdIsCourseFinished(sess)) return "finished"; return hdResolveFirstCatalog(sess, data, "\u672a\u8bc6\u522b\u65ad\u70b9\uff0c\u4ece\u9996\u76ee\u5f55"); } function extractHdQuestionId(data) { if (!data || typeof data !== "object") return ""; let found = ""; const walk = (node, depth) => { if (found || !node || depth > 14) return; if (Array.isArray(node)) { node.forEach((x) => walk(x, depth + 1)); return; } if (typeof node !== "object") return; if (node.questionId || node.questionID) { found = String(node.questionId || node.questionID); return; } if (node.question && typeof node.question === "object") { const qid = node.question.id || node.question.questionId || node.question.questionID; if (qid) { found = String(qid); return; } } Object.values(node).forEach((x) => walk(x, depth + 1)); }; walk(data, 0); return found; } function pickChoiceLetter(options) { if (!options || !options.length) return "A"; const norm = options.map((o, i) => ({ label: String(o.optionLabel || o.label || o.optionKey || String.fromCharCode(65 + i)).toUpperCase(), raw: o, })); const correct = norm.find( (o) => o.raw.isCorrect === 1 || o.raw.isCorrect === true || o.raw.correct === 1 ); if (correct) return correct.label.replace(/[^A-Z]/g, "").slice(0, 1) || "A"; return norm[0].label.replace(/[^A-Z]/g, "").slice(0, 1) || "A"; } async function hdPost(path, body, sess) { return hdFetchJson(path, { method: "POST", body, token: sess && sess.token, referer: sess && sess.launchReferer, }); } async function hdGet(path, query, sess) { return hdFetchJson(path, { query, token: sess && sess.token, referer: sess && sess.launchReferer, }); } async function initHdSessionFromLocation() { const meta = readHdMetaCookie(); const active = loadHdActive(); const { params, launchReferer } = resolveHdLaunchBundle(); if (!params.courseWareId || !params.userId) { throw new Error("\u7f3a\u5c11\u4e92\u52a8\u8bfe\u542f\u52a8\u53c2\u6570\uff08URL \u5df2\u88ab SPA \u6e05\u7a7a\uff0c\u4e14\u65e0\u7f13\u5b58\uff09"); } if (!params.hash) { throw new Error("\u7f3a\u5c11 hash \u7b7e\u540d\uff08\u8bf7\u56de\u8bfe\u7a0b\u9875\u91cd\u65b0\u542f\u52a8\u4e92\u52a8\u8bfe\uff09"); } const sess = Object.assign( { catalogId: null, pageType: "start", token: "", infoSource: 0, pendingNextCatalogId: null, branchQuestionCatalogId: null, branchQuestionOptionId: null, branchCatalogId: null, mainCatalogId: null, currentCatalogType: null, coursewareSessionId: genUuid(), pageSessionId: genUuid(), launchReferer: launchReferer || location.href, label: (meta && meta.title) || (active && active.title) || (params.businessCustomParams || "").slice(0, 8), jobKey: (meta && meta.jobKey) || (active && active.jobKey), cmeOrigin: (meta && meta.cmeOrigin) || (active && active.cmeOrigin) || (params.backUrl ? String(params.backUrl).replace(/\/pages\/.*$/i, "") : ""), }, params ); await sleep(1500); const signRes = await hdPost("/api/hdCourseWare/hdCheckSkipSign", params, sess); if (!hdApiOk(signRes)) { const hint = signRes.message || signRes.msg || signRes._raw || "\u7b7e\u540d\u65e0\u6548"; throw new Error( `hdCheckSkipSign \u5931\u8d25(${signRes._httpStatus || "?"}): ${hint}\uff08\u8bf7\u56de\u8bfe\u7a0b\u9875\u91cd\u65b0\u8fdb\u5165\uff09` ); } const signToken = extractHdToken(signRes); if (signToken) sess.token = signToken; pushLog(`[${sess.label}] [\u4e92\u52a8] \u7b7e\u540d\u6821\u9a8c\u901a\u8fc7`); const cfgRes = await hdGet( "/api/hdCourseWare/getHdCoursewareConfig", { userId: sess.userId, courseWareId: sess.courseWareId, platformId: sess.platformId, businessCourseId: sess.businessCourseId, }, sess ); if (cfgRes && cfgRes._httpStatus === 401) { throw new Error("getHdCoursewareConfig 401\uff08Referer/hash \u65e0\u6548\uff0c\u8bf7\u56de\u8bfe\u7a0b\u9875\u91cd\u8fdb\uff09"); } sess.launchReferer = `${HDBL_ORIGIN}/home`; const initialCatalog = await refreshHdCatalog(sess); sess._initialCatalog = initialCatalog; await hdPost("/api/hdCourseWare/insertHdDurationLog", hdDurationPayload(sess), sess); await hdPost("/api/hdCourseWare/insertCourseWarePlayRecord", hdPlayRecordPayload(sess), sess); pushLog( `[${sess.label}] [\u4e92\u52a8] \u4f1a\u8bdd\u5c31\u7eea · courseWare=${String(sess.courseWareId).slice(0, 8)}… · token=${sess.token ? "ok" : "\u5f85\u53d6"}` ); return sess; } async function refreshHdCatalog(sess) { const res = await hdPost("/api/hdCourseWare/getCourseWareAndCatalogInfo", hdCatalogPayload(sess), sess); if (!hdApiOk(res)) { const hint = res.message || res.msg || res._raw || ""; throw new Error(`getCourseWareAndCatalogInfo \u5931\u8d25(${res._httpStatus || "?"}): ${hint}`); } const body = hdApiBody(res); const tok = extractHdToken(res, body); if (tok) sess.token = tok; if (body && body.catalogId) sess.catalogId = body.catalogId; if (body && body.pageType) sess.pageType = body.pageType; syncBranchFieldsFromData(sess, body); syncHdCoursewareMeta(sess, body); const pending = extractNextCatalogId(body, sess.catalogId); if (pending) sess.pendingNextCatalogId = pending; return body; } async function hdInsertCatalogRecord(sess) { await hdPost("/api/hdCourseWare/insertCatalogRecord", hdInsertCatalogPayload(sess), sess); } async function hdPreparePolyvVideo(sess, vid) { try { await hdGet("/api/polyv/getParam", { vid }, sess); } catch (_) {} try { await hdGet("/api/polyv/getPlaySafeToken", { vid }, sess); } catch (_) {} } async function hdFetchPolyvDuration(sess, vid, catalogData) { const fromData = extractDurationForVid(catalogData, vid); if (fromData > 0) return { duration: fromData, source: "\u76ee\u5f55\u6570\u636e" }; let param = {}; try { const paramRes = await hdGet("/api/polyv/getParam", { vid }, sess); param = unwrapPolyvApiBody(paramRes); } catch (_) {} let token = ""; try { const tokenRes = await hdGet("/api/polyv/getPlaySafeToken", { vid }, sess); token = extractPlaySafeToken(tokenRes); } catch (_) {} const userid = param.userid || param.userId || param.uid || (String(vid).match(/^([0-9a-f]{10})/i) || [])[1] || ""; const playinfoUrls = buildPolyvPlayinfoUrls(vid, param, token); let d = await hdTryPolyvPlayinfoUrls(playinfoUrls); if (d > 0) return { duration: d, source: "PolyV playinfo" }; if (token && userid) { try { d = await hdFetchPolyvPdxDuration(vid, userid, token); if (d > 0) return { duration: d, source: "PolyV PDX" }; } catch (_) {} } if (isHdblHost()) { d = hdReadPolyvDurationFromWindow(window); if (d > 0) return { duration: d, source: "\u64ad\u653e\u5668\u754c\u9762" }; d = await hdProbePolyvDurationFromDom(8000); if (d > 0) return { duration: d, source: "\u64ad\u653e\u5668\u754c\u9762" }; d = await hdProbePolyvDurationViaIframe(sess, 16000); if (d > 0) return { duration: d, source: "\u64ad\u653e\u5668\u63a2\u6d4b" }; } return { duration: 0, source: "" }; } async function hdCheckMediaComplete(sess, vid) { try { const res = await hdGet( "/api/hdCatalog/getHdMediaIsPlayComplete", { userId: sess.userId, businessCourseId: sess.businessCourseId, platformId: sess.platformId, courseWareId: String(sess.courseWareId).toUpperCase(), vid, }, sess ); const body = hdApiBody(res); return body === true || body === 1 || body === "1"; } catch (_) { return false; } } async function hdCheckAllVideoComplete(sess, catalogId) { try { const res = await hdGet( "/api/hdCatalog/getHdVideoIsAllPlayComplete", { userId: sess.userId, courseWareId: sess.courseWareId, businessCourseId: sess.businessCourseId, platformId: sess.platformId, catalogId: catalogId || sess.catalogId, pageType: sess.pageType || "start", token: sess.token, }, sess ); const body = hdApiBody(res); return body === true || body === 1 || body === "1"; } catch (_) { return false; } } async function hdWaitMediaComplete(sess, vid, maxMs = 20000) { const start = Date.now(); while (Date.now() - start < maxMs) { if (await hdCheckMediaComplete(sess, vid)) return true; await sleep(1500); } return false; } async function hdWaitAllVideoComplete(sess, catalogId) { const start = Date.now(); while (Date.now() - start < 25000) { if (await hdCheckAllVideoComplete(sess, catalogId)) return true; await sleep(1500); } return false; } async function hdSendMediaProgress(sess, payload) { await hdPost( "/api/hdLog/mediaProgress", Object.assign({}, payload, { userId: sess.userId, courseWareId: sess.courseWareId, businessCourseId: sess.businessCourseId, pageCatalogId: payload.pageCatalogId || sess.catalogId, coursewareSessionId: sess.coursewareSessionId, pageSessionId: sess.pageSessionId, infoSource: 0, recordTime: new Date().toISOString().slice(0, 19).replace("T", " "), }), sess ); } async function runHdVideoWatch(sess, media, catalogData) { const vid = media.vid; if (await hdCheckMediaComplete(sess, vid)) { pushLog( `[${sess.label}] [\u4e92\u52a8] \u89c6\u9891\u5df2\u770b\u5b8c\uff0c\u8df3\u8fc7 · vid=${String(vid).slice(0, 12)}…` ); return; } if (await hdCheckAllVideoComplete(sess, sess.catalogId)) { pushLog( `[${sess.label}] [\u4e92\u52a8] \u672c\u9875\u89c6\u9891\u5df2\u5168\u90e8\u770b\u5b8c\uff0c\u8df3\u8fc7 · catalog=${String(sess.catalogId || "").slice(0, 8)}…` ); return; } let duration = 0; let durationSource = ""; duration = getHdVidDurFromCache(vid); if (duration > 0) durationSource = "\u672c\u5730\u7f13\u5b58(vid)"; if (duration <= 0) { const fromCatalog = resolveHdVideoDuration(sess, media, catalogData); if (fromCatalog.duration > 0 && !/\u4f30\u7b97|\u5206\u644a/.test(fromCatalog.source || "")) { duration = fromCatalog.duration; durationSource = fromCatalog.source; } } if (duration <= 0) { const polyv = await hdFetchPolyvDuration(sess, vid, catalogData); if (polyv.duration > 0) { duration = polyv.duration; durationSource = polyv.source; saveHdVidDurCache(vid, duration); } } if (duration <= 0 && isHdblHost()) { await hdSyncPageToCurrentCatalog(sess); const liveDur = await hdProbePolyvDurationFromDom(8000); if (liveDur > 0) { duration = liveDur; durationSource = "\u64ad\u653e\u5668\u754c\u9762"; saveHdVidDurCache(vid, liveDur); } } if (duration <= 0) { const fromSize = estimateHdVideoDurationFromFileSize(media && media.fileSize); if (fromSize > 0) { duration = fromSize; durationSource = media && media.fileSize ? `\u6587\u4ef6\u5927\u5c0f ${media.fileSize} \u5b57\u8282\u4f30\u7b97` : "\u6587\u4ef6\u5927\u5c0f\u4f30\u7b97"; pushLog( `[${sess.label}] [\u4e92\u52a8] \u672a\u80fd\u8bfb\u53d6 PolyV/\u64ad\u653e\u5668\u771f\u5b9e\u65f6\u957f\uff0c\u6682\u7528\u6587\u4ef6\u5927\u5c0f\u4f30\u7b97\uff08\u53ef\u80fd\u4e0d\u51c6\uff09` ); } } if (duration <= 0) { duration = Math.max(300, Math.ceil((Number(sess.hdStandardDuration) || 1800) / 5)); durationSource = "\u8bfe\u4ef6\u6807\u51c6\u65f6\u957f\u5206\u644a(\u515c\u5e95)"; pushLog( `[${sess.label}] [\u4e92\u52a8] \u89c6\u9891\u65f6\u957f\u672a\u8fd4\u56de\uff0c\u6309\u8bfe\u4ef6\u6807\u51c6\u65f6\u957f\u9884\u4f30 · \u7ea6 ${Math.ceil(duration / 60)} \u5206\u949f` ); } if (isHdblHost()) { await hdRetreatToHomePage( sess, durationSource === "\u64ad\u653e\u5668\u754c\u9762" ? "\u5df2\u8bfb\u53d6\u64ad\u653e\u5668\u65f6\u957f\uff0c\u8fd4\u56de\u9996\u9875\u540e\u53f0\u7b49\u5f85\uff08\u8bf7\u52ff\u70b9\u64ad\u653e\uff09" : "\u8fd4\u56de\u9996\u9875\uff0c\u540e\u53f0\u4e0a\u62a5\u89c6\u9891\u8fdb\u5ea6" ); } hdEnsureVideoApiReferer(sess); const initialTarget = duration; let reportCap = initialTarget; let target = initialTarget; let extendCount = 0; const maxExtend = 2; pushLog( `[${sess.label}] [\u4e92\u52a8] \u89c6\u9891\u5f00\u59cb · \u7ea6 ${Math.ceil(target / 60)} \u5206\u949f (${Math.round(target)} \u79d2${durationSource ? " · " + durationSource : ""})` ); postHdProgress(sess, `\u89c6\u9891\u64ad\u653e\u4e2d 0% · \u7ea6${Math.ceil(target / 60)}\u5206\u949f`, sess.hdStep, sess.hdStepTotal); const mediaSessionId = genUuid(); const basePayload = { pageCatalogId: sess.catalogId, mediaSessionId, mediaType: 6, mediaId: vid, infoSource: 0, }; await hdPost( "/api/hdCourseWare/insertUserHdVideoPlayRecord", { businessCourseId: sess.businessCourseId, courseWareId: String(sess.courseWareId).toUpperCase(), platformId: sess.platformId, userId: sess.userId, vid, type: 1, infoSource: 0, ban_seek: true, }, sess ); await hdSendMediaProgress(sess, Object.assign({}, basePayload, { eventType: 16, currentPlayTime: 0 })); let played = 0; let lastLogPct = -1; let forceFinish = false; while (true) { while (played < target) { if (!getEnabled() && !isHdblHost()) throw new Error("\u5df2\u6682\u505c"); const remainSec = Math.max(1, target - played); const waitMs = Math.min(HD_MEDIA_TICK_MS, Math.max(1000, remainSec * 1000)); await sleep(waitMs); played = Math.min(reportCap, played + waitMs / 1000); await hdSendMediaProgress( sess, Object.assign({}, basePayload, { eventType: 22, currentPlayTime: played }) ); const pct = Math.min(100, Math.round((played / target) * 100)); if (pct >= lastLogPct + 15 || played >= target) { const detail = `\u89c6\u9891 ${pct}% (${Math.round(played)}/${Math.round(target)}\u79d2)`; pushLog(`[${sess.label}] [\u4e92\u52a8] ${detail}`); postHdProgress(sess, detail, sess.hdStep, sess.hdStepTotal); lastLogPct = pct; } } await hdFinalizeVideoPlayback(sess, vid, basePayload, reportCap); if ( (await hdWaitMediaComplete(sess, vid, 8000)) || (await hdCheckAllVideoComplete(sess, sess.catalogId)) ) { break; } if (/\u4f30\u7b97|\u5206\u644a/.test(durationSource) && extendCount < maxExtend) { const polyv = await hdFetchPolyvDuration(sess, vid, catalogData); if (polyv.duration > target) { extendCount++; target = polyv.duration; reportCap = polyv.duration; durationSource = polyv.source; saveHdVidDurCache(vid, target); pushLog( `[${sess.label}] [\u4e92\u52a8] \u83b7\u53d6\u771f\u5b9e\u65f6\u957f ${Math.round(target)} \u79d2\uff0c\u7ee7\u7eed\u7b49\u5f85 (${extendCount}/${maxExtend})…` ); continue; } } if (extendCount >= maxExtend) { forceFinish = true; pushLog( `[${sess.label}] [\u4e92\u52a8] \u670d\u52a1\u7aef\u672a\u786e\u8ba4\u5b8c\u64ad\uff0c\u6309\u5df2\u4e0a\u62a5\u65f6\u957f\u7ed3\u675f\uff08${Math.round(played)}/${Math.round(initialTarget)}\u79d2\uff09` ); break; } extendCount++; pushLog( `[${sess.label}] [\u4e92\u52a8] \u5b8c\u64ad\u6821\u9a8c\u672a\u901a\u8fc7\uff0c\u4fdd\u6301 ${Math.round(reportCap)} \u79d2\u8fdb\u5ea6\u5fc3\u8df3 (${extendCount}/${maxExtend})…` ); for (let i = 0; i < 2; i++) { await sleep(15000); await hdSendMediaProgress( sess, Object.assign({}, basePayload, { eventType: 22, currentPlayTime: reportCap }) ); if ( (await hdCheckMediaComplete(sess, vid)) || (await hdCheckAllVideoComplete(sess, sess.catalogId)) ) { break; } } forceFinish = true; break; } await hdWaitMediaComplete(sess, vid, forceFinish ? 4000 : 20000); if (!forceFinish) await hdWaitAllVideoComplete(sess, sess.catalogId); if (isHdblHost()) { await hdRetreatToHomePage(sess, "\u89c6\u9891\u5df2\u5b66\u5b8c\uff0c\u8fd4\u56de\u9996\u9875"); } pushLog(`[${sess.label}] [\u4e92\u52a8] \u89c6\u9891\u9875\u5b8c\u6210`); } async function answerHdQuestion(sess, questionId) { if (!questionId) return; const qRes = await hdGet("/api/hdQuestion/getQuestionInfoIncludeOptionList", { questionId }, sess); const qBody = hdApiBody(qRes) || {}; const options = qBody.optionList || qBody.options || qBody.answerList || []; const letter = pickChoiceLetter(Array.isArray(options) ? options : []); pushLog(`[${sess.label}] [\u4e92\u52a8] \u7b54\u9898 ${letter}`); await hdPost( "/api/hdAnswerRecord/insertAnswerRecord", { userId: sess.userId, courseWareId: sess.courseWareId, businessCourseId: sess.businessCourseId, platformId: sess.platformId, questionId, userChoiceAnswer: letter, source: "pc", questionType: 0, shortAnswer: "", }, sess ); } async function hdGoNextCatalog(sess) { if (!sess.catalogId && String(sess.pageType || "").toLowerCase() === "start") { return null; } if (sess.pendingNextCatalogId) { const pending = String(sess.pendingNextCatalogId); sess.pendingNextCatalogId = null; if (pending.toUpperCase() !== String(sess.catalogId || "").toUpperCase()) { sess.catalogId = pending; sess.pageType = "view"; sess.pageSessionId = genUuid(); sess.launchReferer = buildHdProblemReferer(sess, { option: "next" }); pushLog(`[${sess.label}] [\u4e92\u52a8] \u4e0b\u4e00\u9875(\u76ee\u5f55\u7f13\u5b58) · catalog=${pending.slice(0, 8)}…`); return { catalogId: pending }; } } let nextId = ""; let nextPageType = ""; let navSource = ""; let nav1 = null; let nav2 = null; try { const beforeId = sess.catalogId; const fresh = await refreshHdCatalog(sess); if ( sess.catalogId && String(sess.catalogId).toUpperCase() !== String(beforeId || "").toUpperCase() ) { nextId = String(sess.catalogId); nextPageType = sess.pageType || "view"; navSource = "\u76ee\u5f55\u5237\u65b0\u8df3\u8f6c"; } syncBranchFieldsFromData(sess, fresh); if (!nextId) { const dirPick = hdPickNextFromDirectory( Object.assign({}, sess, { catalogId: beforeId }), fresh ); if (dirPick) { nextId = dirPick.catalogId; nextPageType = dirPick.pageType; navSource = dirPick.source; } } if (!nextId) { const fromFresh = extractNextCatalogId(fresh, beforeId); if (fromFresh) { nextId = fromFresh; navSource = "\u76ee\u5f55\u6570\u636e"; } } } catch (_) {} if (!nextId) { const nav1 = await hdFetchCatalogNav(sess, true); nextId = nav1.nextId; nextPageType = nav1.pageType; if (nextId) navSource = "\u7ffb\u9875API(next)"; if (!nextId) { nav2 = await hdFetchCatalogNav(sess, false); nextId = nav2.nextId; if (!nextPageType) nextPageType = nav2.pageType; if (nextId) navSource = "\u7ffb\u9875API"; } } if (!nextId) { const progress = await hdFetchCoursewareProgress(sess); nextId = extractNextCatalogId(progress, sess.catalogId); if (nextId) navSource = "\u8fdb\u5ea6API"; if (!nextId) { const list = extractHdCatalogList(progress); const cur = String(sess.catalogId || "").toUpperCase(); const idx = list.findIndex((x) => String(x.catalogId).toUpperCase() === cur); if (idx >= 0 && idx + 1 < list.length) { nextId = list[idx + 1].catalogId; nextPageType = list[idx + 1].pageType || nextPageType; navSource = "\u8fdb\u5ea6\u5217\u8868"; } } if (!nextId) { const dirPick = hdPickNextFromDirectory(sess, progress); if (dirPick) { nextId = dirPick.catalogId; nextPageType = dirPick.pageType; navSource = dirPick.source; } } } if (!nextId) { let navHint = ""; let navSnap = null; try { navSnap = nav1 && nav1._navBody || nav2 && nav2._navBody; if (navSnap) { navHint = ` · nav=${JSON.stringify(navSnap).slice(0, 180)}`; } } catch (_) {} if (hdNavIndicatesEnd(navSnap)) { pushLog( `[${sess.label}] [\u4e92\u52a8] \u7ffb\u9875 API \u65e0\u4e0b\u4e00\u9875\uff08\u4ec5 pre\uff09\uff0c\u5224\u5b9a\u4e3a\u6700\u540e\u4e00\u9875${navHint}` ); return { end: true }; } pushLog( `[${sess.label}] [\u4e92\u52a8] \u7ffb\u9875 API \u672a\u8fd4\u56de\u4e0b\u4e00 catalog\uff08\u5f53\u524d ${String(sess.catalogId || "").slice(0, 8)}…${navHint}\uff09` ); return null; } sess.catalogId = String(nextId); sess.pageType = nextPageType || "view"; sess.pageSessionId = genUuid(); sess.launchReferer = buildHdProblemReferer(sess, { option: "next" }); pushLog( `[${sess.label}] [\u4e92\u52a8] \u4e0b\u4e00\u9875 · catalog=${String(sess.catalogId).slice(0, 8)}…${navSource ? " · " + navSource : ""}` ); return { catalogId: sess.catalogId }; } async function processHdCatalogPage(sess, data) { const pageLabel = String(sess.pageType || "view").toLowerCase(); const catShort = (sess.catalogId || "").slice(0, 8); const stepInfo = sess.hdStep ? `\u7b2c ${sess.hdStep} \u9875` : "\u5f53\u524d\u9875"; const medias = extractHdMedias(data); const qid = extractHdQuestionId(data); const pageKind = medias.length ? "\u89c6\u9891" : qid || pageLabel === "question" ? "\u7b54\u9898" : pageLabel; pushLog( `[${sess.label}] [\u4e92\u52a8] ${stepInfo} · ${pageKind} · catalog=${catShort || "?"}` ); postHdProgress(sess, `${stepInfo} · ${pageKind}`, sess.hdStep, sess.hdStepTotal); await hdInsertCatalogRecord(sess); if (medias.length && (await hdCheckAllVideoComplete(sess, sess.catalogId))) { pushLog( `[${sess.label}] [\u4e92\u52a8] \u672c\u9875\u89c6\u9891\u5df2\u5168\u90e8\u770b\u5b8c\uff0c\u8df3\u8fc7\u7b49\u5f85 · catalog=${catShort || "?"}` ); } else { for (const m of medias) { if (await hdCheckMediaComplete(sess, m.vid)) { pushLog( `[${sess.label}] [\u4e92\u52a8] \u89c6\u9891\u5df2\u770b\u5b8c\uff0c\u8df3\u8fc7 · vid=${String(m.vid).slice(0, 12)}…` ); continue; } await runHdVideoWatch(sess, m, data); } } if (qid || pageLabel === "question") { await answerHdQuestion(sess, qid); } await hdPost("/api/hdCourseWare/insertHdDurationLog", hdDurationPayload(sess), sess); try { const fresh = await refreshHdCatalog(sess); const dirPick = hdPickNextFromDirectory(sess, fresh); if (dirPick) sess.pendingNextCatalogId = dirPick.catalogId; else { const pending = extractNextCatalogId(fresh, sess.catalogId); if (pending) sess.pendingNextCatalogId = pending; } } catch (_) {} } async function runInteractiveHdFlow() { if (!isHdInteractiveEnabled()) return; if (hdRunnerBusy) return; hdRunnerBusy = true; let sess; if (isHdblHost() && !getEnabled()) setEnabled(true); try { pushLog("[\u4e92\u52a8] \u5f00\u59cb API \u6d41\u7a0b"); sess = await initHdSessionFromLocation(); let data = sess._initialCatalog; delete sess._initialCatalog; if (!data) data = await refreshHdCatalog(sess); sess.hdStepTotal = 0; let guard = 0; let endFlowDone = false; while (getEnabled() && guard < 60) { guard++; sess.hdStep = guard; if (String(sess.pageType || "").toLowerCase() === "start" && !sess.catalogId) { pushLog(`[${sess.label}] [\u4e92\u52a8] \u6b22\u8fce\u9875\uff0c\u8df3\u8fc7\u7b49\u5f85\uff0c\u5b9a\u4f4d\u5b66\u4e60\u4f4d\u7f6e…`); const resolved = await hdResolveResumeCatalog(sess, data); if (resolved === "finished") { await hdRunEndPageFlow(sess); endFlowDone = true; pushLog(`[${sess.label}] [\u4e92\u52a8] \u68c0\u6d4b\u5230\u5df2\u5168\u90e8\u5b66\u5b8c\uff0c\u76f4\u63a5\u5b8c\u6210`); break; } if (!resolved) { throw new Error("\u65e0\u6cd5\u89e3\u6790\u9996\u76ee\u5f55\uff08\u6b22\u8fce\u9875\u540e\u65e0 catalogId\uff09"); } data = await refreshHdCatalog(sess); continue; } await processHdCatalogPage(sess, data); const next = await hdGoNextCatalog(sess); if (next && next.end) { await hdRunEndPageFlow(sess); endFlowDone = true; pushLog(`[${sess.label}] [\u4e92\u52a8] \u8bfe\u7a0b\u5df2\u5168\u90e8\u5b8c\u6210\uff08\u5171 ${guard} \u9875\uff09`); break; } if (!next) { let finished = await hdIsCourseFinished(sess); if (!finished) finished = await hdIsCoursewareProgressComplete(sess); if (!finished) { try { const fresh = await refreshHdCatalog(sess); finished = hdIsDirectoryComplete(fresh); } catch (_) {} } if (!finished) { try { const endInfo = await hdFetchEndPageInfo(sess); if (endInfo && (endInfo.hasRecord === 1 || endInfo.hasRecord === true)) { finished = true; } } catch (_) {} } if (finished) { await hdRunEndPageFlow(sess); endFlowDone = true; pushLog(`[${sess.label}] [\u4e92\u52a8] \u8bfe\u7a0b\u5df2\u5168\u90e8\u5b8c\u6210\uff08\u5171 ${guard} \u9875\uff09`); break; } throw new Error( `\u65e0\u6cd5\u7ffb\u5230\u4e0b\u4e00\u9875\uff08\u5f53\u524d catalog=${String(sess.catalogId || "").slice(0, 8)}…\uff0c\u5df2\u5b8c\u6210 ${guard} \u9875\uff0c\u8bfe\u7a0b\u5c1a\u672a\u7ed3\u675f\uff09` ); } data = await refreshHdCatalog(sess); } if (!endFlowDone) { await hdPost("/api/hdCourseWare/finishCourseWare", hdDurationPayload(sess), sess); } pushLog(`[${sess.label}] [\u4e92\u52a8] \u5b8c\u6210\uff0c\u8fd4\u56de CME…`); const cwidParam = sess.businessCustomParams || ""; const metaDone = readHdMetaCookie(); const back = sess.backUrl || (sess.cmeOrigin ? `${sess.cmeOrigin}/pages/exam_result_hd.aspx?businessCustomParams=${encodeURIComponent(cwidParam)}` : ""); const cwidDone = cwidParam || (metaDone && metaDone.cwid) || ""; if (isHdDetachedRunner()) { clearHdSessionArtifacts(); postHdIframeEvent("done", { cwid: cwidDone }); setTimeout(tryCloseHdRunnerWindow, 800); return; } clearHdSessionArtifacts(); if (!back) throw new Error("\u65e0\u6cd5\u786e\u5b9a\u8fd4\u56de CME \u5730\u5740"); location.replace(back.startsWith("http") ? back : new URL(back, HDBL_ORIGIN).href); } catch (e) { const errMsg = e && e.message ? e.message : String(e); pushLog("[\u4e92\u52a8] \u5931\u8d25: " + errMsg); const meta = readHdMetaCookie(); if (isHdDetachedRunner()) { postHdIframeEvent("fail", { cwid: meta && meta.cwid, message: errMsg, }); setTimeout(tryCloseHdRunnerWindow, 1200); } clearHdSessionArtifacts(); } finally { hdRunnerBusy = false; } } async function launchInteractiveJob(job) { if (!isHdInteractiveEnabled()) return false; const cwid = job.cwid; if (!cwid) { pushLog(`[${job.title || job.cwrid}] [\u4e92\u52a8] \u7f3a\u5c11 cwid`); return false; } const onPage = findChapterOnPageByCwid(cwid); if (onPage && onPage.done) { completeInteractiveJobByCwid(cwid); pushLog(`[${job.title || job.cwrid}] [\u4e92\u52a8] \u9875\u9762\u5df2\u5b8c\u6210\uff0c\u8df3\u8fc7`); return true; } const probe = await probeHdChapterEntry(cwid); if (probe.status === "done") { completeInteractiveJobByCwid(cwid); pushLog(`[${job.title || job.cwrid}] [\u4e92\u52a8] \u5165\u53e3\u6307\u5411\u7ed3\u679c\u9875\uff0c\u89c6\u4e3a\u5df2\u5b8c\u6210`); return true; } const ctx = jobToCtx(job); setHdMetaCookie({ title: job.title, cmeOrigin: location.origin, jobKey: jobKey(ctx), cwid, }); saveHdActive({ jobKey: jobKey(ctx), cwid, title: job.title, cmeOrigin: location.origin, launchedAt: Date.now(), launchUrl: probe.status === "hdbl" ? probe.url : "", }); const entryUrl = buildHdCmeEntryUrl(cwid); const launchUrl = probe.status === "hdbl" && probe.url ? probe.url : probe.status === "notice" && probe.url ? probe.url.startsWith("http") ? probe.url : new URL(probe.url, location.origin).href : entryUrl; saveJob(ctx, { phase: "hd_running", hdLaunchUrl: launchUrl, hdStep: 0, hdProgressText: "\u542f\u52a8\u4e2d…", hdLaunchedAt: Date.now(), }); const probeHint = probe.status === "hdbl" ? "\u5df2\u89e3\u6790 hdbl+hash" : probe.status === "notice" ? "\u7ecf\u987b\u77e5\u9875" : probe.status === "done" ? "\u5df2\u5b8c\u6210" : `\u8d70 CME \u91cd\u5b9a\u5411\u94fe → ${(probe.url || "").replace(/^https?:\/\/[^/]+/, "").slice(0, 48)}`; pushLog(`[${job.title}] [\u4e92\u52a8] \u767b\u8bb0\u5b8c\u6210 · cwid=${cwid.slice(0, 8)}… · ${probeHint}`); await sleep(300); const useSilentIframe = probe.status === "hdbl" && probe.url; if (useSilentIframe) { pushLog(`[${job.title}] [\u4e92\u52a8] \u5c1d\u8bd5\u540e\u53f0 iframe\uff08${HD_IFRAME_WATCHDOG_MS / 1000}s \u65e0\u54cd\u5e94\u5219\u6539\u8ff7\u4f60\u7a97\u53e3\uff09…`); launchHdInSilentFrame(launchUrl); armHdHostWatchdog(job, launchUrl, "iframe"); } else { launchHdPopupWindow(launchUrl, job); armHdHostWatchdog(job, launchUrl, "popup"); } return true; } function completeInteractiveJobByCwid(cwid) { if (!cwid) return false; const jobs = loadJobs(); const key = Object.keys(jobs).find((k) => jobs[k] && jobs[k].interactive && jobs[k].cwid === cwid); if (!key) return false; const job = jobs[key]; saveJob(jobToCtx(job), { phase: "done" }); refreshPanelAfterChapterDone(jobToCtx(job)); return true; } async function runRealtimeScheduler() { await keepSessionAlive(); if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return; bgRunning = true; try { const jobs = loadJobs(); const now = Date.now(); const hdRunning = Object.values(jobs).some((j) => j && j.phase === "hd_running"); if (hdRunning) return; const retryKey = Object.keys(jobs).find( (k) => jobs[k] && jobs[k].phase === "rt_finish_retry" && jobs[k].recordRetryDue <= now ); if (retryKey) { await runFinishPhase(jobToCtx(jobs[retryKey])); return; } const steppingKey = Object.keys(jobs).find((k) => jobs[k] && jobs[k].phase === "rt_stepping"); if (steppingKey) { await tickRealtimeStep(jobs[steppingKey]); return; } const pending = Object.entries(jobs) .filter(([, j]) => j && j.phase === "pending" && !j.interactive) .sort((a, b) => (a[1].createdAt || 0) - (b[1].createdAt || 0)); if (!pending.length) { const busy = Object.values(jobs).some( (j) => j && (j.phase === "rt_stepping" || j.phase === "rt_finish_retry" || j.phase === "silent_filling" || j.phase === "silent_clock" || j.phase === "silent_finish_retry" || j.phase === "exam_pending") ); if (!busy) maybeAutoStopWhenFinished(); return; } const [, job] = pending[0]; const ctx = jobToCtx(job); const label = job.title || job.cwrid.slice(0, 8); if (!HY_ENGINE_REQUIRED && !(await consumeChapterCloudQuota(ctx))) return; saveJob(ctx, { phase: "rt_stepping", nextStepDue: Date.now(), engineSessionId: "", }); ensureRtWallClock(ctx); const liveJob = Object.assign({}, job, loadJobs()[jobKey(ctx)]); pushUserChapterStudyLog(liveJob, 0); await tickRealtimeStep(liveJob); } catch (e) { pushLog("[1:1] \u8c03\u5ea6: " + (e && e.message ? e.message : e)); } finally { bgRunning = false; updateJobsPanel(); updatePanelCloudStatus(); } } async function runSilentScheduler() { await keepSessionAlive(); if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return; bgRunning = true; try { let jobs = loadJobs(); const now = Date.now(); const st = loadSilentState(); const retryKey = Object.keys(jobs).find( (k) => jobs[k] && jobs[k].phase === "silent_finish_retry" && jobs[k].recordRetryDue <= now ); if (retryKey) { await runFinishPhase(jobToCtx(jobs[retryKey])); return; } const batchDue = offlineBatchDueMs(st); if (batchDue && now >= batchDue) { const ready = Object.entries(jobs) .filter(([, j]) => j && j.phase === "silent_clock") .sort((a, b) => (a[1].createdAt || 0) - (b[1].createdAt || 0)); if (ready.length) { const [, job] = ready[0]; pushLog( `[${job.title}] [\u79bb\u7ebf\u6279\u91cf] \u5230\u70b9\u63d0\u4ea4\u6536\u5c3e\uff08\u5269${ready.length}\u8bfe\uff09…` ); await runFinishPhase(jobToCtx(job)); return; } } if (Object.values(jobs).some((j) => j && j.phase === "silent_filling")) return; if (Object.values(jobs).some((j) => j && j.phase === "silent_clock")) return; const pending = Object.entries(jobs) .filter(([, j]) => j && j.phase === "pending" && !j.interactive) .sort((a, b) => (a[1].createdAt || 0) - (b[1].createdAt || 0)); const filledCount = Object.values(jobs).filter((j) => j && j.phase === "silent_filled").length; const batchInProgress = filledCount > 0 || !!st.batchFillActive; if (!pending.length) { if (filledCount) { const totalMs = assignSilentBatchClock(jobs); if (totalMs > 0) { patchSilentState({ batchFillActive: false }); const st2 = loadSilentState(); const due = offlineBatchDueMs(st2, totalMs, st2.sessionStart || now); notifyOfflineBatchFilled(due, Math.max(0, due - Date.now())); syncRunModePanel(); stopAutoStudy("\u79bb\u7ebf\u6279\u91cf\u62c9\u6ee1\u5b8c\u6210\uff0c\u8bf7\u7b49\u5f85\u5230\u70b9\u540e\u518d\u70b9\u5f00\u59cb"); updatePanel(); } } return; } if (!batchInProgress) { const totalSec = pending.reduce((s, [, j]) => s + (j.duration || 3600), 0); const sessionStart = Date.now(); resetSilentState(); saveSilentState({ sessionStart, batchFillActive: true }); pushLog( `[\u79bb\u7ebf\u6279\u91cf] \u5f00\u59cb\u5206\u6bb5\u62c9\u6ee1 ${pending.length} \u8bfe\uff0c\u7d2f\u8ba1\u8bfe\u957f\u7ea6 ${formatDurationCn(totalSec)}` ); pushUserLog( `\u6b63\u5728\u6279\u91cf\u62c9\u6ee1\u8fdb\u5ea6\uff0c\u5171 ${pending.length} \u8282\uff08\u7d2f\u8ba1\u7ea6 ${formatDurationCn(totalSec)}\uff09\uff0c\u8bf7\u7a0d\u5019…` ); } let fillIdx = filledCount; const batchTotal = filledCount + pending.length; for (const [, job] of pending) { if (!getEnabled()) break; fillIdx++; const ctx = jobToCtx(job); const label = job.title || job.cwrid.slice(0, 8); if (!HY_ENGINE_REQUIRED && !(await consumeChapterCloudQuota(ctx))) break; saveJob(ctx, { phase: "silent_filling" }); const ok = await fillChapterSegmentsToFull(ctx, job, { index: fillIdx, total: batchTotal, }); if (ok) { saveJob(ctx, { phase: "silent_filled", progressFilled: true, status2Sent: true, lastPostedSec: job.duration, filledAt: Date.now(), engineSessionId: "", }); } else { saveJob(ctx, { phase: "pending", engineSessionId: "" }); } await sleep(1500); } jobs = loadJobs(); const stillPending = Object.values(jobs).some((j) => j && j.phase === "pending"); const stillFilling = Object.values(jobs).some((j) => j && j.phase === "silent_filling"); if (!stillPending && !stillFilling) { const totalMs = assignSilentBatchClock(jobs); if (totalMs > 0) { patchSilentState({ batchFillActive: false }); const st2 = loadSilentState(); const due = offlineBatchDueMs(st2, totalMs, st2.sessionStart || now); const remain = Math.max(0, due - Date.now()); notifyOfflineBatchFilled(due, remain); syncRunModePanel(); stopAutoStudy("\u79bb\u7ebf\u6279\u91cf\u62c9\u6ee1\u5b8c\u6210\uff0c\u8bf7\u7b49\u5f85\u5230\u70b9\u540e\u518d\u70b9\u5f00\u59cb"); updatePanel(); } } } catch (e) { pushLog("[\u79bb\u7ebf\u6279\u91cf] \u8c03\u5ea6: " + (e && e.message ? e.message : e)); } finally { bgRunning = false; updateJobsPanel(); updatePanelCloudStatus(); } } async function runExamPendingScheduler() { if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return false; const jobs = loadJobs(); const now = Date.now(); const pending = Object.entries(jobs) .filter( ([, j]) => j && j.phase === "exam_pending" && (!j.examRetryDue || j.examRetryDue <= now) ) .sort((a, b) => (a[1].createdAt || 0) - (b[1].createdAt || 0)); if (!pending.length) { return false; } bgRunning = true; try { const [, job] = pending[0]; const ctx = jobToCtx(job); const label = job.title || (job.cwrid || "").slice(0, 8) || "\u8003\u8bd5"; if (!HY_ENGINE_REQUIRED && !(await consumeChapterCloudQuota(ctx))) return; pushLog(`[${label}] \u89c6\u9891\u5df2\u5b66\u5b8c\uff0cAPI \u7b54\u9898\u4e2d…`); const res = await afterVideoMaybeExam(ctx, label); if (res.ok) { saveJob(ctx, { phase: "done", examDone: true, examFailCount: 0, examRetryDue: 0 }); try { sessionStorage.removeItem(EXAM_NAV_KEY); } catch (_) {} maybeAutoStopWhenFinished(); } else if (res.needNavigate && pageType() !== "exam") { navigateToExamPage(ctx, label); } else if (res.paused) { saveJob(ctx, { phase: "exam_pending", examRetryDue: 0 }); } else { const fails = (job.examFailCount || 0) + 1; saveJob(ctx, { phase: "exam_pending", examFailCount: fails, examRetryDue: now + Math.min(30000, 4000 * fails), }); if (fails >= 3 && /\u672a\u89e3\u6790\u5230\u8bd5\u9898|\u672a\u83b7\u53d6\u8bd5\u5377|ID \u65e0\u6548/.test(res.msg || "")) { pushLog(`[${label}] \u8fde\u7eed\u62c9\u5377\u5931\u8d25 ${fails} \u6b21\uff0c\u8bf7\u624b\u52a8\u6253\u5f00\u8be5\u8282\u300c\u8fdb\u5165\u8003\u8bd5\u300d\u786e\u8ba4\u9875\u9762\u6b63\u5e38`); } } } catch (e) { pushLog("\u8003\u8bd5\u8c03\u5ea6: " + (e && e.message ? e.message : e)); } finally { bgRunning = false; updateJobsPanel(); updatePanelCloudStatus(); } return true; } async function runBgScheduler() { if (!isLeaderTab()) return; if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return; if (detectAccountSwitch()) { updatePanel(); return; } if (!(await _crt())) { updatePanel(); return; } if (await runExamPendingScheduler()) return; const jobs = loadJobs(); if ( isHdInteractiveEnabled() && Object.values(jobs).some((j) => j && j.phase === "hd_running") ) { return; } if (isHdInteractiveEnabled()) { const hdPending = Object.entries(jobs) .filter(([, j]) => j && j.phase === "pending" && j.interactive) .sort((a, b) => (a[1].createdAt || 0) - (b[1].createdAt || 0)); if (hdPending.length) { bgRunning = true; try { await launchInteractiveJob(hdPending[0][1]); } catch (e) { pushLog("[\u4e92\u52a8] \u8c03\u5ea6: " + (e && e.message ? e.message : e)); } finally { bgRunning = false; updateJobsPanel(); updatePanelCloudStatus(); } return; } } const mode = getRunMode(); if (mode === "realtime") { await runRealtimeScheduler(); maybeAutoStopWhenFinished(); return; } if (mode === "offline") return runSilentScheduler(); if (mode === "ladder") return runLadderScheduler(); if (mode === "p3par") return runP3ParallelScheduler(); if (mode === "pstep") return runParallelStepScheduler(); if (mode === "tail") return runTailScheduler(); if (mode === "serial") return runSerialRoundScheduler(); return runHeartbeatScheduler(); } async function runHeartbeatScheduler() { await keepSessionAlive(); if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return; bgRunning = true; try { for (let round = 0; round < 20; round++) { assignAnchorIfNeeded(); let jobs = loadJobs(); const now = Date.now(); const keys = Object.keys(jobs); const readyFinish = keys .filter((k) => { const j = jobs[k]; return j && j.phase === "active" && j.activeSince && j.finishDue <= now; }) .sort((a, b) => jobs[a].finishDue - jobs[b].finishDue); if (readyFinish.length) { await runFinishPhase(jobToCtx(jobs[readyFinish[0]])); continue; } await ensureActiveSession(); jobs = loadJobs(); const jobKeys = Object.keys(jobs); const activeKey = jobKeys.find((k) => jobs[k] && jobs[k].phase === "active"); if (activeKey && jobs[activeKey].finishDue > now) { if (await tickRealHeartbeat(jobs[activeKey])) { await sleep(COURSE_GAP_MS); continue; } } const pending = jobKeys .filter((k) => jobs[k] && jobs[k].phase === "pending" && !jobs[k].interactive) .sort((a, b) => (jobs[a].createdAt || 0) - (jobs[b].createdAt || 0)); if (pending.length) { const job = jobs[pending[0]]; pushLog(`[${job.title}] \u5feb\u586b (\u5269 ${pending.length} \u8bfe\uff0c\u951a\u70b9\u4fdd\u6d3b\u4e2d)`); await releaseActiveSession(); await runFillPhase(jobToCtx(job)); await ensureActiveSession(true); await sleep(COURSE_GAP_MS); continue; } if (activeKey || jobKeys.some((k) => jobs[k] && jobs[k].phase === "anchor")) break; break; } } catch (e) { pushLog("\u5fc3\u8df3\u8c03\u5ea6: " + (e && e.message ? e.message : e)); } finally { bgRunning = false; updateJobsPanel(); } } function registerJob(ch, meta) { const rt = resolveChapterRtStart(ch); const ctx = { uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, groupId: meta.groupId, provinceId: meta.provinceId, duration: ch.duration, baseSec: rt.startSec, lsKey: ch.lsKey, title: ch.title, }; const key = jobKey(ctx); const jobs = loadJobs(); if (jobs[key] && jobs[key].phase !== "done") return false; jobs[key] = { uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, cwid: ch.cwrid, wareCwid: ch.wareCwid || "", groupId: meta.groupId, provinceId: meta.provinceId, duration: ch.duration, startSec: rt.startSec, studiedSec: rt.studiedSec, lsKey: ch.lsKey, title: ch.title, interactive: !!ch.interactive, phase: "pending", createdAt: Date.now(), }; const sessionUid = getLearningUserId(); if (sessionUid && ch.uid && sessionUid !== String(ch.uid)) { trace(`registerJob uid\u4e0d\u4e00\u81f4 session=${sessionUid} chapter.uid=${ch.uid} key=${key}`); } saveJobs(jobs); return true; } function registerExamJob(ch) { const ctx = { uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, lsKey: ch.lsKey, title: ch.title, }; const key = jobKey(ctx); const jobs = loadJobs(); if (jobs[key] && jobs[key].phase === "exam_pending") return false; jobs[key] = { uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, cwid: ch.cwrid, lsKey: ch.lsKey, title: ch.title, interactive: false, examOnly: true, phase: "exam_pending", createdAt: Date.now(), }; saveJobs(jobs); return true; } function registerInteractiveJob(ch) { const ctx = { uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, lsKey: ch.lsKey, title: ch.title, }; const key = jobKey(ctx); const jobs = loadJobs(); if (jobs[key] && jobs[key].phase !== "done") return false; jobs[key] = { uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, cwid: ch.cwid || ch.cwrid || "", lsKey: ch.lsKey, title: ch.title, interactive: true, duration: ch.duration || 0, phase: "pending", createdAt: Date.now(), }; saveJobs(jobs); return true; } async function registerPickedChapters(picked, courseTitle) { if (!picked.length) return { n: 0, skip: 0, registeredKeys: [] }; const visible = isHdInteractiveEnabled() ? picked : picked.filter((ch) => !ch.interactive); const normal = visible.filter((ch) => !ch.interactive && isChapterAutoStudy(ch)); const examOnly = visible.filter((ch) => !ch.interactive && isChapterNeedExam(ch)); const interactive = isHdInteractiveEnabled() ? visible.filter((ch) => ch.interactive && isChapterNeedStudy(ch)) : []; let n = 0; let skip = 0; const registeredKeys = []; for (const ch of examOnly) { if (registerExamJob(ch)) { n++; registeredKeys.push( jobKey({ uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, lsKey: ch.lsKey }) ); pushLog(` · \u5f85\u8003\u8bd5\u300c${ch.title}\u300d→ \u8003\u8bd5\u961f\u5217`); } else skip++; } if (normal.length) { const meta = await ensureCourseApiMeta(normal[0].cid, normal[0].cwrid); if (!meta.groupId) { pushLog(`\u300c${courseTitle}\u300d\u7f3a\u5c11 group_id`); } else { for (const ch of normal) { if (registerJob(ch, meta)) { n++; registeredKeys.push( jobKey({ uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, groupId: meta.groupId, provinceId: meta.provinceId, lsKey: ch.lsKey, }) ); } else skip++; } } } for (const ch of interactive) { if (registerInteractiveJob(ch)) { n++; registeredKeys.push(jobKey({ uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, lsKey: ch.lsKey })); pushLog(` · \u4e92\u52a8\u8bfe\u300c${ch.title}\u300d→ \u961f\u5217\uff08${ch.studyState || "\u5f85\u5b66"}\uff09`); } else skip++; } if (interactive.length) { pushLog(`\u300c${courseTitle}\u300d\u542b ${interactive.length} \u8282\u4e92\u52a8\u8bfe\uff1aAPI \u6309\u9875\u63a8\u8fdb\uff0c\u89c6\u9891\u6309\u771f\u5b9e\u65f6\u957f\u7b49\u5f85`); } if (examOnly.length && !normal.length) { pushLog(`\u300c${courseTitle}\u300d${examOnly.length} \u8282\u5f85\u8003\u8bd5\uff08\u89c6\u9891\u5df2\u5b66\u5b8c\uff0c\u4ec5 API \u7b54\u9898\uff09`); } return { n, skip, registeredKeys, picked: [...normal, ...interactive, ...examOnly], meta: null }; } function finalizeChapterRegistration(result, beforeActive, courseTitle) { const { n = 0, skip = 0, registeredKeys = [], picked = [] } = result || {}; if (!registeredKeys.length && !n) return; if (isP3ParallelMode() && registeredKeys.length) { const jobs = loadJobs(); const base = Date.now(); registeredKeys.forEach((k, i) => { if (jobs[k] && jobs[k].phase === "pending") { jobs[k].nextStepDue = base + i * P3PAR_START_STAGGER_MS; } }); saveJobs(jobs); const wall = estimateP3ParallelWallMin(loadJobs()); pushLog( registeredKeys.length > 1 ? `status=3\u771f\u5e76\u884c\uff1a${registeredKeys.length}\u8bfe\u5404\u72ec\u7acb5\u5206\u949f\u6b65\u8fdb\uff0c\u8bfe\u95f4\u9519\u5f0030s\uff1b\u82e5\u9650\u6d41\u6309\u5355\u8bfe\u5219\u7ea6${wall}\u5206\u5168\u90e8\u8dd1\u5b8c` : `status=3\u771f\u5e76\u884c\uff1a\u4ec5status=3\u6b65\u8fdb+300s\uff0c\u7ea6${wall}\u5206/\u8bfe` ); } else if (isLadderMode() && registeredKeys.length) { assignLadderBatch(loadJobs()); } else if (isParallelStepMode() && registeredKeys.length > 1) { const jobs = loadJobs(); registeredKeys.sort((a, b) => (jobs[a]?.createdAt || 0) - (jobs[b]?.createdAt || 0)); const base = Date.now(); registeredKeys.forEach((k, i) => { if (jobs[k] && jobs[k].phase === "pending") { jobs[k].nextStepDue = base + i * randomStepIntervalMs(); } }); saveJobs(jobs); } const afterActive = Object.values(loadJobs()).filter((j) => j && j.phase !== "done").length; pushLog( `\u300c${courseTitle}\u300d\u65b0\u767b\u8bb0 ${n} \u8282${skip ? `\uff08${skip} \u8282\u5df2\u5728\u961f\u5217\u8df3\u8fc7\uff09` : ""}\u3002\u5168\u7ad9\u961f\u5217\u5171 ${afterActive} \u8282` ); if (beforeActive > 0 && n > 0) { pushLog(`\u5df2\u6709 ${beforeActive} \u8282\u5728\u8ba1\u65f6/\u6392\u961f\uff0c\u65b0\u8282\u63a5\u5728\u540e\u9762\uff0c\u52ff\u70b9\u6e05\u7a7a`); } if (isLadderMode()) { pushLog("\u9636\u68af\u4e32\u884c\u8bd5\u9a8c\uff1a\u591a\u8bfe\u6309\u9636\u68af\u69fd\u6392\u961f"); } else if (isParallelStepMode()) { pushLog("\u5e76\u884c+300s\uff1astatus=3 \u6b65\u8fdb+300s\uff1b\u672b\u6b65 status=2+\u64ad\u5b8c\u8bb0\u5f55"); } else if (isTailMode()) { pushLog("\u672b\u6bb5\u6302\u949f\uff1a\u5feb\u586b→\u6302\u949f→\u672b5\u5206"); } else if (isSerialMode()) { pushLog("\u4e32\u884c\u8f6e\u8be2\uff1a\u9010\u8bfe+300s\u9000\u51fa"); } else if (isRealtimeMode()) { pushLog(`[1:1\u6302\u673a] \u4e32\u884c\uff1a\u8bfe\u957f=\u6302\u949f\uff08${afterActive} \u8282\u6392\u961f\uff09`); } else if (isOfflineMode()) { const totalSec = picked.reduce((s, ch) => s + (ch.duration || 3600), 0); pushLog( `[\u79bb\u7ebf\u6279\u91cf] \u5c06\u5206\u6bb5\u62c9\u6ee1\uff0c\u7d2f\u8ba1\u7ea6 ${formatDurationCn(totalSec)} + \u7f13\u51b230\u5206\u949f \u540e\u7edf\u4e00\u6536\u5c3e\u4e0e\u8003\u8bd5` ); } else { assignAnchorIfNeeded(); } } async function registerChaptersFromCourses(mode, cids) { if (!getEnabled()) { setEnabled(true); pushLog("\u5df2\u81ea\u52a8\u5f00\u542f"); } if (!getApiMode()) { localStorage.setItem(API_MODE_KEY, "1"); pushLog("\u5df2\u81ea\u52a8\u5f00\u542f"); } if (!cids || !cids.length) { pushLog("\u8bf7\u5148\u52fe\u9009\u81f3\u5c11\u4e00\u95e8\u8bfe\u7a0b"); return 0; } const beforeActive = Object.values(loadJobs()).filter((j) => j && j.phase !== "done").length; let totalN = 0; for (const cid of cids) { let course = findPanelCourseByCid(cid); if (!course) { course = blankPanelCourse({ cid, title: cid.slice(0, 8), url: coursePageUrl(cid) }); panelState.courses.push(course); } await enrichPanelCourse(course, true); const chapters = course.chapters || []; if (!chapters.length) { pushLog(`\u300c${course.title}\u300d${course.progressError || "\u672a\u8bc6\u522b\u5230\u7ae0\u8282"}`); continue; } let picked = chapters; if (mode === "untouched") picked = chapters.filter(isChapterRegistrable); else if (mode === "all") picked = chapters.filter(isChapterRegistrable); if (!picked.length) { const examLeft = chapters.filter(isChapterNeedExam).length; if (examLeft) { pushLog(`\u300c${course.title}\u300d${examLeft} \u8282\u5f85\u8003\u8bd5\u4f46\u672a\u80fd\u767b\u8bb0\uff08\u8bf7\u70b9\u5237\u65b0\u540e\u91cd\u8bd5\uff09`); } else { pushLog(`\u300c${course.title}\u300d\u6ca1\u6709\u53ef\u767b\u8bb0\u7ae0\u8282`); } continue; } const result = await registerPickedChapters(picked, course.title || cid.slice(0, 8)); totalN += result.n || 0; finalizeChapterRegistration(result, beforeActive, course.title || cid.slice(0, 8)); await sleep(200); } restoreLockedQueueSelection(); renderCourseList(); await refreshChapterPreview(); if (totalN > 0) { startGlobalTicker(); runBgScheduler(); } updatePanel(); return totalN; } async function registerCourseChapters(mode) { if (!getEnabled()) { setEnabled(true); pushLog("\u5df2\u81ea\u52a8\u5f00\u542f"); } if (!getApiMode()) { localStorage.setItem(API_MODE_KEY, "1"); pushLog("\u5df2\u81ea\u52a8\u5f00\u542f"); } const chapters = parseCourseChapterJobs(); if (!chapters.length) { const selected = getQueue(); if (selected.length) { return registerChaptersFromCourses(mode, selected); } pushLog("\u8bf7\u5728\u7ae0\u8282\u5217\u8868\u9875 /pages/course.aspx\uff0c\u6216\u52fe\u9009\u6536\u85cf\u8bfe\u7a0b"); return; } let picked = chapters; if (mode === "untouched") picked = chapters.filter(isChapterRegistrable); else if (mode === "all") picked = chapters.filter(isChapterRegistrable); if (!picked.length) { const examLeft = chapters.filter(isChapterNeedExam).length; if (examLeft) pushLog(`${examLeft} \u8282\u5f85\u8003\u8bd5\u4f46\u672a\u80fd\u767b\u8bb0\uff08\u8bf7 F5 \u5237\u65b0\u8bfe\u7a0b\u9875\uff09`); else pushLog("\u6ca1\u6709\u53ef\u767b\u8bb0\u7684\u7ae0\u8282"); return; } const beforeActive = Object.values(loadJobs()).filter((j) => j && j.phase !== "done").length; const courseTitle = (document.querySelector(".course_title h1") || document.querySelector("h1"))?.textContent ?.replace(/\s+/g, " ") .trim() .slice(0, 20) || chapters[0].cid.slice(0, 8); const result = await registerPickedChapters(picked, courseTitle); finalizeChapterRegistration(result, beforeActive, courseTitle); renderCourseList(); await refreshChapterPreview(); startGlobalTicker(); runBgScheduler(); } function startGlobalTicker() { if (!isLeaderTab()) return; stopGlobalTicker(); window.__hyJobTicker = setInterval(() => { if (getEnabled() && getApiMode() && isLeaderTab()) runBgScheduler(); }, TICK_MS); runBgScheduler(); } async function runPlayPage() { if (!getEnabled() || !getApiMode()) return; if (engineRunning || bgRunning) { pushLog("\u5f15\u64ce\u8fd0\u884c\u4e2d"); return; } pushLog("\u64ad\u653e\u9875\uff1a\u521d\u59cb\u5316 API \u5f15\u64ce…"); window.blockAbnormalPlugin = function () {}; try { await waitUntil(() => window.player, 15000); try { window.player.j2s_pauseVideo(); } catch (_) {} } catch (_) { pushLog("\u672a\u68c0\u6d4b\u5230\u64ad\u653e\u5668\uff0c\u4f7f\u7528\u9875\u9762\u65f6\u957f"); } const ctx = buildPlayContext(); if (!ctx.cwrid || !ctx.uid) { pushLog("\u7f3a\u5c11 cwrid/uid"); return; } const job = getJob(ctx); if (job && job.phase === "done") { pushLog("\u672c\u8bfe\u5df2\u5b8c\u6210"); return; } engineRunning = true; try { if (job && isParallelMode() && (job.phase === "serial" || job.phase === "stepping" || job.phase === "pending" || job.phase === "ready" || TAIL_STEP_PHASES.includes(job.phase) || (job.phase === "waiting" && isTailMode()))) { pushLog("\u540e\u53f0\u6a21\u5f0f\u7531\u7ae0\u8282\u9875\u8c03\u5ea6\uff0c\u65e0\u9700\u5728\u64ad\u653e\u9875\u624b\u52a8\u89e6\u53d1"); return; } if (job && (job.phase === "waiting" || job.phase === "anchor" || job.phase === "active")) { if (Date.now() < job.finishDue) { pushLog( `\u5df2\u5728\u961f\u5217\uff0c${formatEta(job.finishDue - Date.now())} \u540e\u81ea\u52a8\u6536\u5c3e\uff08\u540e\u53f0\u5355\u8bfe\u4fdd\u6d3b\u5fc3\u8df3\u4e2d\uff09` ); return; } await runFinishPhase(ctx); return; } if (job && job.isAnchor) { pushLog("\u672c\u8bfe\u4e3a\u4fdd\u6d3b\u951a\u70b9\uff0c\u7531\u540e\u53f0\u8c03\u5ea6"); return; } await runFillPhase(ctx); } finally { engineRunning = false; } } async function resolveTestContext() { if (pageType() === "play") { const ctx = buildPlayContext(); if (ctx.cwrid && ctx.uid) return ctx; } const jobs = Object.values(loadJobs()) .filter((j) => j && j.phase !== "done") .sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0)); if (jobs.length) return jobToCtx(jobs[0]); const chapters = parseCourseChapterJobs(); const ch = chapters.find((c) => !c.done) || chapters[0]; if (!ch) return null; const meta = await ensureCourseApiMeta(ch.cid, ch.cwrid); return { uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, cwid: ch.cwid, groupId: meta.groupId, provinceId: meta.provinceId, duration: ch.duration, baseSec: ch.startSec, lsKey: ch.lsKey, title: ch.title, }; } async function runStatus2ThenPlayRecordTest() { const ctx = await resolveTestContext(); if (!ctx || !ctx.cwrid || !ctx.uid) { pushLog("\u6d4b\u8bd5\u5931\u8d25\uff1a\u8bf7\u5728\u64ad\u653e\u9875\uff0c\u6216\u7ae0\u8282\u9875\u767b\u8bb0/\u6709\u672a\u5b66\u7ae0\u8282"); return; } const label = ctx.title || ctx.cwrid.slice(0, 8); const duration = ctx.duration; pushLog(`[\u6d4b\u8bd5] ${label} status=2 @${duration}s → \u7acb\u5373\u64ad\u5b8c\u8bb0\u5f55`); await warmPlaySession(ctx, true); let s2 = null; try { s2 = await postProcess(ctx, 2, duration); pushLog( `[\u6d4b\u8bd5] status=2 code=${s2 && s2.code}${s2 && s2.msg ? " " + s2.msg : ""}` ); } catch (e) { pushLog(`[\u6d4b\u8bd5] status=2 \u5f02\u5e38: ${e && e.message ? e.message : e}`); } await sleep(800); let rec = null; try { rec = await postPlayRecord(ctx, duration); pushLog( `[\u6d4b\u8bd5] \u64ad\u5b8c\u8bb0\u5f55 code=${rec && rec.code}${rec && rec.msg ? " " + rec.msg : ""}` ); } catch (e) { pushLog(`[\u6d4b\u8bd5] \u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`); } if (rec && rec.code === 0) { syncLocal(ctx, duration); try { await postProcess(ctx, 3, duration); pushLog(`[\u6d4b\u8bd5] status=3 \u5df2\u9000\u51fa`); } catch (_) {} pushLog(`[\u6d4b\u8bd5] ✓ \u7ec4\u5408\u6d4b\u8bd5\u901a\u8fc7\uff08status=2 + \u64ad\u5b8c\u8bb0\u5f55 ok\uff09`); } else if (rec && rec.code === 1) { pushLog(`[\u6d4b\u8bd5] ✗ \u64ad\u5b8c\u8bb0\u5f55\u65f6\u957f\u4e0d\u8db3 → \u9700\u6302\u949f\u540e\u518d 1 \u6b21 status=1 + \u64ad\u5b8c\u8bb0\u5f55\uff08\u9694\u65e5\u4e5f\u884c\uff09`); } } async function runStatus2OnlyTest() { const ctx = await resolveTestContext(); if (!ctx || !ctx.cwrid || !ctx.uid) { pushLog("\u6d4b\u8bd5\u5931\u8d25\uff1a\u8bf7\u5728\u64ad\u653e\u9875\uff0c\u6216\u7ae0\u8282\u9875\u767b\u8bb0/\u6709\u672a\u5b66\u7ae0\u8282"); return; } const label = ctx.title || ctx.cwrid.slice(0, 8); const duration = ctx.duration; pushLog(`[\u6d4b\u8bd5] ${label} \u4ec5 POST status=2 @${duration}s\uff08\u4e0d\u53d1 0/1/3\uff09`); await warmPlaySession(ctx, true); try { const res = await postProcess(ctx, 2, duration); pushLog( `[\u6d4b\u8bd5] status=2 code=${res && res.code}${res && res.msg ? " " + res.msg : ""}` ); } catch (e) { pushLog(`[\u6d4b\u8bd5] status=2 \u5f02\u5e38: ${e && e.message ? e.message : e}`); } pushLog("[\u6d4b\u8bd5] \u7ae0\u8282\u300c\u5b8c\u6210\u300d\u9760\u64ad\u5b8c\u8bb0\u5f55\uff0c\u4e0d\u662f status=2\uff1b\u53ef\u70b9\u300c\u6d4b\u64ad\u5b8c\u8bb0\u5f55\u300d"); } async function runTailDoubleHeartbeatTest() { if (singleCourseTestRunning) { pushLog("[\u6d4b\u8bd5] \u672b5\u5206\u53cc\u5fc3\u8df3\u8fdb\u884c\u4e2d\uff0c\u8bf7\u52ff\u91cd\u590d\u70b9\u51fb"); return; } const ctx = await resolveTestContext(); if (!ctx || !ctx.cwrid || !ctx.uid) { pushLog("\u6d4b\u8bd5\u5931\u8d25\uff1a\u8bf7\u5728\u64ad\u653e\u9875\uff0c\u6216\u7ae0\u8282\u9875\u767b\u8bb0/\u6709\u672a\u5b66\u7ae0\u8282\uff08\u53ea\u6d4b\u961f\u5217\u7b2c\u4e00\u8bfe\uff09"); return; } const label = ctx.title || ctx.cwrid.slice(0, 8); const duration = Math.max(60, ctx.duration || 3600); const tailSec = Math.max(1, duration - STEP_SEC); singleCourseTestRunning = true; pushLog( `[\u6d4b\u8bd5·\u5355\u8bfe] ${label} ① status=1@${tailSec}s(\u603b-${STEP_SEC}s) → \u7b495\u520630\u79d2 → ② status=1@${duration}s → \u64ad\u5b8c\u8bb0\u5f55` ); try { if (!(await warmPlaySession(ctx, true))) { pushLog("[\u6d4b\u8bd5] \u9884\u70ed\u64ad\u653e\u9875\u5931\u8d25\uff0c\u8bf7\u5148 F5 \u767b\u5f55"); return; } await sleep(1500); let r1 = null; try { r1 = await postProcess(ctx, 1, tailSec); } catch (e) { pushLog(`[\u6d4b\u8bd5] ① status=1@${tailSec}s \u5f02\u5e38: ${e && e.message ? e.message : e}`); return; } pushLog( `[\u6d4b\u8bd5] ① status=1@${tailSec}s code=${r1 && r1.code != null ? r1.code : "?"}${r1 && r1.msg ? " " + r1.msg : ""}` ); if (!r1 || r1.code === 3) { pushLog("[\u6d4b\u8bd5] ① code=3 \u9650\u6d41\uff0c\u7ea65\u5206\u949f\u540e\u518d\u8bd5"); return; } if (r1.code !== 0) return; syncLocal(ctx, tailSec); pushLog("[\u6d4b\u8bd5] \u6302\u949f 5\u520630\u79d2…\uff08\u7ae0\u8282\u9875\u4fdd\u6301\u5f00\u7740\uff09"); const waitUntil = Date.now() + TAIL_HEARTBEAT_WAIT_MS; while (Date.now() < waitUntil) { await sleep(60000); const left = waitUntil - Date.now(); if (left > 0) pushLog(`[\u6d4b\u8bd5] \u8fd8\u5269 ${Math.ceil(left / 60000)} \u5206\u949f…`); } if (!(await warmPlaySession(ctx, true))) { pushLog("[\u6d4b\u8bd5] ② \u524d\u9884\u70ed\u5931\u8d25\uff0c\u8bf7 F5 \u540e\u624b\u52a8\u70b9\u300c\u6d4b\u64ad\u5b8c\u8bb0\u5f55\u300d"); return; } await sleep(800); let r2 = null; try { r2 = await postProcess(ctx, 1, duration); } catch (e) { pushLog(`[\u6d4b\u8bd5] ② status=1@${duration}s \u5f02\u5e38: ${e && e.message ? e.message : e}`); return; } pushLog( `[\u6d4b\u8bd5] ② status=1@${duration}s code=${r2 && r2.code != null ? r2.code : "?"}${r2 && r2.msg ? " " + r2.msg : ""}` ); if (!r2 || r2.code !== 0) return; syncLocal(ctx, duration); await sleep(800); let rec = null; try { rec = await postPlayRecord(ctx, duration); } catch (e) { pushLog(`[\u6d4b\u8bd5] \u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`); return; } pushLog( `[\u6d4b\u8bd5] ★\u64ad\u5b8c\u8bb0\u5f55 @${duration}s code=${rec && rec.code != null ? rec.code : "?"}${rec && rec.msg ? " " + rec.msg : ""}` ); if (rec && rec.code === 0) { pushLog("[\u6d4b\u8bd5] ✓ \u672b5\u5206\u53cc\u5fc3\u8df3 + \u64ad\u5b8c\u8bb0\u5f55\u6210\u529f"); } else if (rec && rec.code === 1) { pushLog("[\u6d4b\u8bd5] ✗ \u4ecd\u65f6\u957f\u4e0d\u8db3 → \u524d\u9762\u6709\u6548\u5fc3\u8df3\u6b21\u6570/\u6302\u949f\u53ef\u80fd\u8fd8\u4e0d\u591f"); } } finally { singleCourseTestRunning = false; } } async function sleepWithMinuteLogs(totalMs, tag) { const waitUntil = Date.now() + totalMs; pushLog( `[\u6d4b\u8bd5] ${tag} \u6302\u949f ${Math.round(totalMs / 60000)}\u5206${Math.round((totalMs % 60000) / 1000)}\u79d2…` ); while (Date.now() < waitUntil) { await sleep(60000); const left = waitUntil - Date.now(); if (left > 0) pushLog(`[\u6d4b\u8bd5] ${tag} \u8fd8\u5269 ${Math.ceil(left / 60000)} \u5206\u949f…`); } } async function postProcessRetryOnCode3(ctx, status, sec, label, stepTag) { for (let attempt = 1; attempt <= CODE3_RETRY_MAX; attempt++) { let res = null; try { res = await postProcess(ctx, status, sec); } catch (e) { pushLog( `[\u6d4b\u8bd5] ${stepTag} status=${status}@${sec}s \u5f02\u5e38: ${e && e.message ? e.message : e}\uff0c1\u5206\u949f\u540e\u91cd\u8bd5` ); await sleep(CODE3_RETRY_MS); continue; } const code = res && res.code; const msg = res && res.msg ? " " + res.msg : ""; if (code === 3) { pushLog( `[\u6d4b\u8bd5] ${stepTag} status=${status}@${sec}s code=3${msg}\uff0c1\u5206\u949f\u540e\u91cd\u8bd5\uff08${attempt}/${CODE3_RETRY_MAX}\uff09` ); await sleep(CODE3_RETRY_MS); continue; } pushLog( `[\u6d4b\u8bd5] ${stepTag} status=${status}@${sec}s code=${code != null ? code : "?"}${msg}` ); if (code !== 0) return { ok: false, code, res }; return { ok: true, code: 0, res }; } pushLog(`[\u6d4b\u8bd5] ${stepTag} code=3 \u91cd\u8bd5\u5df2\u8fbe\u4e0a\u9650\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5`); return { ok: false, code: 3 }; } function buildTenMinLadderTargets(duration) { const targets = []; for (let sec = BIGSTEP_JUMP_SEC; sec < duration; sec += BIGSTEP_JUMP_SEC) { targets.push(sec); } return targets; } function formatStepTag(index) { const circled = "①②③④⑤⑥⑦⑧⑨⑩"; if (index >= 1 && index <= circled.length) return circled[index - 1]; return `#${index}`; } async function runJump10Min20MinTest() { if (singleCourseTestRunning) { pushLog("[\u6d4b\u8bd5] \u5355\u8bfe\u6d4b\u8bd5\u8fdb\u884c\u4e2d\uff0c\u8bf7\u52ff\u91cd\u590d\u70b9\u51fb"); return; } const ctx = await resolveTestContext(); if (!ctx || !ctx.cwrid || !ctx.uid) { pushLog("\u6d4b\u8bd5\u5931\u8d25\uff1a\u8bf7\u5728\u64ad\u653e\u9875\uff0c\u6216\u7ae0\u8282\u9875\u767b\u8bb0/\u6709\u672a\u5b66\u7ae0\u8282\uff08\u53ea\u6d4b\u7b2c\u4e00\u8bfe\uff09"); return; } const label = ctx.title || ctx.cwrid.slice(0, 8); const duration = Math.max(60, ctx.duration || 3600); const targets = buildTenMinLadderTargets(duration); if (!targets.length) { pushLog(`[\u6d4b\u8bd5] \u89c6\u9891\u4ec5${duration}s\uff0c\u4e0d\u8db310\u5206\u949f\uff0c\u8bf7\u6362\u66f4\u957f\u8bfe\u65f6\u518d\u8bd5`); return; } const total = targets.length; const wallMin = Math.max(0, Math.round(((total - 1) * BIGSTEP_GAP_MS) / 60000)); const wallSec = Math.max(0, Math.round(((total - 1) * BIGSTEP_GAP_MS) / 1000)); const lastMin = Math.round(targets[targets.length - 1] / 60); singleCourseTestRunning = true; pushLog( `[\u6d4b\u8bd5·2\u500d\u901f\u9636\u68af] ${label} ${duration}s\uff1a10\u5206→20\u5206→30\u5206…\u6b65\u95f41\u52061\u79d2\uff0c\u5171${total}\u6b21\u81f3${lastMin}\u5206 → status=2+\u64ad\u5b8c` ); pushLog( `[\u6d4b\u8bd5] \u9884\u8ba1\u6302\u949f\u7ea6 ${wallMin > 0 ? wallMin + "\u5206" : wallSec + "\u79d2"}\uff08\u4e0d\u542b code=3\uff09\uff1b\u5931\u8d25 code=3 \u7b491\u5206\u949f\u91cd\u8bd5` ); try { if (!(await warmPlaySession(ctx, true))) { pushLog("[\u6d4b\u8bd5] \u9884\u70ed\u5931\u8d25\uff0c\u8bf7\u5148 F5 \u767b\u5f55"); return; } await sleep(1500); for (let i = 0; i < total; i++) { const sec = targets[i]; const min = Math.round(sec / 60); const tag = `${formatStepTag(i + 1)}/${total} ${min}\u5206@${sec}s`; if (i > 0) { const prevMin = Math.round(targets[i - 1] / 60); await sleepWithMinuteLogs(BIGSTEP_GAP_MS, `${prevMin}\u5206→${min}\u5206`); if (!(await warmPlaySession(ctx, true))) { pushLog(`[\u6d4b\u8bd5] ${tag} \u524d\u9884\u70ed\u5931\u8d25`); return; } await sleep(800); } const r = await postProcessRetryOnCode3(ctx, 1, sec, label, tag); if (!r.ok) return; syncLocal(ctx, sec); } await sleep(800); const r2 = await postProcessRetryOnCode3(ctx, 2, duration, label, "status=2"); if (r2.ok) syncLocal(ctx, duration); await sleep(800); let rec = null; try { rec = await postPlayRecord(ctx, duration); } catch (e) { pushLog(`[\u6d4b\u8bd5] \u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`); return; } pushLog( `[\u6d4b\u8bd5] ★\u64ad\u5b8c\u8bb0\u5f55 @${duration}s code=${rec && rec.code != null ? rec.code : "?"}${rec && rec.msg ? " " + rec.msg : ""}` ); if (rec && rec.code === 0) { pushLog("[\u6d4b\u8bd5] ✓ \u64ad\u5b8c\u8bb0\u5f55\u6210\u529f\uff0c\u8bf7\u5728\u7ae0\u8282\u9875\u786e\u8ba4\u662f\u5426\u663e\u793a\u5df2\u5b8c\u6210"); } else if (rec && rec.code === 1) { pushLog("[\u6d4b\u8bd5] \u4ecd\u65f6\u957f\u4e0d\u8db3 → \u53ef\u80fd\u8fd8\u9700\u66f4\u591a\u9636\u68af\u6b65\u6216\u6302\u949f\u540e\u518d\u8865 1 \u6b21\u5fc3\u8df3+\u64ad\u5b8c"); } } finally { singleCourseTestRunning = false; } } async function runPlayRecordOnlyTest() { const ctx = await resolveTestContext(); if (!ctx || !ctx.cwrid || !ctx.uid) { pushLog("\u6d4b\u8bd5\u5931\u8d25\uff1a\u8bf7\u5728\u64ad\u653e\u9875\uff0c\u6216\u7ae0\u8282\u9875\u767b\u8bb0/\u6709\u672a\u5b66\u7ae0\u8282"); return; } const label = ctx.title || ctx.cwrid.slice(0, 8); const duration = ctx.duration; pushLog(`[\u6d4b\u8bd5] ${label} \u4ec5 GET addCourseWarePlayRecord @${duration}s`); await warmPlaySession(ctx, true); try { const rec = await postPlayRecord(ctx, duration); pushLog( `[\u6d4b\u8bd5] \u64ad\u5b8c\u8bb0\u5f55 code=${rec && rec.code}${rec && rec.msg ? " " + rec.msg : ""}` ); if (rec && rec.code === 0) syncLocal(ctx, duration); } catch (e) { pushLog(`[\u6d4b\u8bd5] \u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`); } } function tryEnterExam() { const btn = document.getElementById("jrks"); if (!btn || btn.getAttribute("disabled") === "disabled") return; pushLog("\u81ea\u52a8\u8fdb\u5165\u8003\u8bd5"); btn.click(); } function resolveExamCwid(ctx, job) { const j = job || (ctx ? getJob(ctx) : null); return ( (ctx && ctx.cwrid) || (j && j.cwrid) || (j && j.cwid) || (ctx && ctx.cwid) || new URLSearchParams(location.search).get("cwid") || "" ); } function diagnoseExamPageHtml(html) { const text = String(html || ""); if (isPageLoginRedirect(text) || (/checkLogin|login_sso/i.test(text) && text.length < 4000)) { return "\u767b\u5f55\u5931\u6548\uff0c\u8bf7 F5 \u5237\u65b0"; } if (/\u5b66\u4e60\u65f6\u957f|\u8bf7\u5148\u5b66\u4e60|\u4e0d\u80fd\u7b54\u5377|\u672a\u5b66\u5b8c|\u65f6\u957f\u4e0d\u8db3/.test(text)) return "\u5b66\u65f6\u672a\u6ee1\uff0c\u4e0d\u80fd\u7b54\u5377"; if (/\u53c2\u6570\u9519\u8bef|\u4e0d\u5b58\u5728|\u65e0\u6548\u8bfe\u4ef6|\u627e\u4e0d\u5230\u8bfe\u4ef6|\u672a\u627e\u5230/.test(text)) return "\u8003\u8bd5 ID \u65e0\u6548"; if (/class=["']test["']|\.test\s*>\s*table/i.test(text) && !/q_name|tablestyle/.test(text)) { return "\u8bd5\u5377\u7ed3\u6784\u5df2\u8bc6\u522b\u4f46\u9009\u9879\u672a\u52a0\u8f7d\uff08\u8bf7\u624b\u52a8\u5237\u65b0\u8003\u8bd5\u9875\uff09"; } return ""; } function parseHyExamQuestionsV1(doc) { const questions = []; const seen = new Set(); function pushQuestion(qEl, scope) { const qid = qEl.getAttribute("data-qid") || ""; if (!qid || seen.has(qid)) return; const root = scope || qEl.closest("table, .tablestyle, tr, li, .exam_item, form") || qEl.parentElement; const options = []; const radioSel = `input[type='radio'][name='radio_${qid}'], input.qo_name[type='radio']`; (root || doc).querySelectorAll(radioSel).forEach((input) => { const id = input.value || ""; if (!id) return; options.push({ id, name: input.getAttribute("name") || `radio_${qid}`, label: (input.closest("label")?.textContent || "").replace(/\s+/g, " ").trim() }); }); if (!options.length) { doc.querySelectorAll(radioSel).forEach((input) => { const id = input.value || ""; if (!id || options.some((o) => o.id === id)) return; options.push({ id, name: input.getAttribute("name") || `radio_${qid}`, label: (input.closest("label")?.textContent || "").replace(/\s+/g, " ").trim() }); }); } if (qid && options.length) { seen.add(qid); questions.push({ qid, format: "v1", radioName: `radio_${qid}`, title: (qEl.textContent || "").replace(/\s+/g, " ").trim(), options, }); } } doc.querySelectorAll(".tablestyle").forEach((table) => { const qEl = table.querySelector(".q_name"); if (qEl) pushQuestion(qEl, table); }); if (!questions.length) { doc.querySelectorAll(".q_name[data-qid], label.q_name[data-qid]").forEach((qEl) => pushQuestion(qEl)); } return questions; } function parseHyExamQuestionsV2(doc) { const questions = []; const seen = new Set(); doc.querySelectorAll(".test > table, .test table, div.test table").forEach((table, idx) => { const titleEl = table.querySelector("thead th, thead td, thead"); let title = titleEl ? (titleEl.textContent || "").replace(/\s+/g, " ").trim() : ""; title = title.replace(/^\d+[\u3001.\s]+/, "").trim(); const options = []; table.querySelectorAll("tbody input[type='radio']").forEach((input) => { const name = input.getAttribute("name") || ""; const id = input.value || ""; if (!name || !id) return; options.push({ id, name, label: (input.closest("label")?.textContent || input.parentElement?.textContent || "").replace(/\s+/g, " ").trim(), }); }); if (!options.length) return; const radioName = options[0].name; if (seen.has(radioName)) return; seen.add(radioName); questions.push({ qid: radioName, format: "v2", radioName, title: title || `\u7b2c${idx + 1}\u9898`, options, }); }); return questions; } function parseHyExamQuestions(html) { const doc = new DOMParser().parseFromString(html, "text/html"); const v1 = parseHyExamQuestionsV1(doc); if (v1.length) return v1; const v2 = parseHyExamQuestionsV2(doc); if (v2.length) return v2; return []; } function isHyExamPassHtml(html, url) { const text = String(html || ""); const u = String(url || ""); if (/exam_result/i.test(u) && /\u8003\u8bd5\u672a\u901a\u8fc7|tips_fail\.png|\u91cd\u65b0\u8003\u8bd5/i.test(text)) return false; if (/\u8003\u8bd5\u901a\u8fc7|\u606d\u559c.*\u901a\u8fc7|\u5408\u683c/.test(text) && !/\u8003\u8bd5\u672a\u901a\u8fc7/.test(text)) return true; if (/tips_success\.png/i.test(text) && /exam_result/i.test(u)) return true; if (/exam_result\.aspx/i.test(u) && !/\u8003\u8bd5\u672a\u901a\u8fc7|\u4e0d\u53ca\u683c|\u672a\u80fd\u901a\u8fc7|\u91cd\u65b0\u7b54\u9898|\u672a\u901a\u8fc7|tips_fail/.test(text)) { return /\u8003\u8bd5\u901a\u8fc7|tips_success/.test(text); } return false; } function isHyExamAlreadyPassedHtml(html) { return /\u5df2\u5b8c\u6210|\u5df2\u901a\u8fc7|\u4e0d\u8981\u91cd\u590d|\u5df2\u7ecf\u901a\u8fc7|\u8003\u8bd5\u901a\u8fc7/.test(String(html || "")); } function findHyOptionByLetter(q, letter) { if (!q || !letter) return null; const L = String(letter).toUpperCase().slice(0, 1); const byLabel = (q.options || []).find((o) => new RegExp(`^${L}[\u3001.\\s]`).test(o.label || "")); if (byLabel) return byLabel; const idx = L.charCodeAt(0) - 65; if (idx >= 0 && idx < (q.options || []).length) return q.options[idx]; return null; } function hyExamOptionLetter(q, opt) { if (!opt) return ""; const m = String(opt.label || "").match(/^([A-Ha-h])[\u3001.\s]/); if (m) return m[1].toUpperCase(); const idx = (q.options || []).indexOf(opt); return idx >= 0 ? String.fromCharCode(65 + idx) : ""; } function ensureHyExamTriedLetters(state, qid) { if (!state.triedLetters) state.triedLetters = {}; if (!state.triedLetters[qid]) state.triedLetters[qid] = new Set(); return state.triedLetters[qid]; } function syncHyExamTriedLetters(state, q) { const triedLet = ensureHyExamTriedLetters(state, q.qid); const triedIds = ensureHyExamTried(state, q.qid); q.options.forEach((o) => { if (triedIds.has(o.id)) { const L = hyExamOptionLetter(q, o); if (L) triedLet.add(L); } }); return triedLet; } function formatHyExamPicksLog(questions, picks) { return picks .map((p) => { const q = questions.find((x) => x.qid === p.qid); const o = q && q.options.find((x) => x.id === p.optionId); return q && o ? hyExamOptionLetter(q, o) || "?" : "?"; }) .join(""); } function markHyExamCodePassed(cwid) { if (!cwid) return; try { sessionStorage.setItem( HY_EXAM_CODE_PASS_KEY, JSON.stringify({ cwid: String(cwid).toUpperCase(), ts: Date.now() }) ); } catch (_) {} } function isHyExamCodeRecentlyPassed(cwid) { if (!cwid) return false; try { const raw = JSON.parse(sessionStorage.getItem(HY_EXAM_CODE_PASS_KEY) || "null"); const target = String(cwid).toUpperCase(); return !!(raw && raw.cwid === target && Date.now() - (raw.ts || 0) < 15 * 60 * 1000); } catch (_) { return false; } } function parseHyExamResultPage(html, url) { const text = String(html || ""); const u = String(url || ""); if (!/exam_result/i.test(u) && !/state_cour_ul|state_cour_lis/.test(text)) return null; const failed = /\u8003\u8bd5\u672a\u901a\u8fc7|tips_fail\.png/i.test(text); const passed = !failed && isHyExamPassHtml(text, u); const scoreM = text.match(/test_score:\s*["'](\d+)/i); const score = scoreM ? scoreM[1] : ""; const doc = new DOMParser().parseFromString(text, "text/html"); const wrong = []; const correct = []; doc.querySelectorAll("ul.state_cour_ul > li.state_cour_lis, .state_cour_ul .state_cour_lis").forEach((li, idx) => { const inner = li.innerHTML || ""; const isWrong = /error_icon\.png/i.test(inner); const isRight = !isWrong && /bar_img\.png/i.test(inner); const texts = li.querySelectorAll(".state_lis_text"); const titleRaw = (texts[0]?.getAttribute("title") || texts[0]?.textContent || "") .replace(/\s+/g, " ") .trim(); const ansRaw = (texts[1]?.getAttribute("title") || texts[1]?.textContent || "") .replace(/\s+/g, " ") .trim(); const numM = titleRaw.match(/^(\d+)[\u3001.]/); const qIndex = numM ? parseInt(numM[1], 10) - 1 : idx; const letterM = ansRaw.match(/\u3010\u60a8\u7684\u7b54\u6848\uff1a\s*([A-Ha-h])\u3001?/) || ansRaw.match(/([A-Ha-h])[\u3001.]/); const optionLetter = letterM ? letterM[1].toUpperCase() : ""; const title = titleRaw.replace(/^\d+[\u3001.]\s*/, "").trim(); const entry = { qIndex, title, optionLetter, userAnswer: ansRaw }; if (isWrong) wrong.push(entry); else if (isRight && optionLetter) correct.push(entry); }); if (!wrong.length && !correct.length && !passed && !failed) return null; return { passed, failed, score, wrong, correct }; } function matchHyQuestionForResult(questions, item) { if (!questions || !questions.length || !item) return null; if (item.qIndex >= 0 && item.qIndex < questions.length) { const q = questions[item.qIndex]; if (!item.title || !q.title) return q; const a = item.title.slice(0, 18); const b = q.title.replace(/^\d+[\u3001.]\s*/, "").slice(0, 18); if (!a || !b || a === b || a.includes(b) || b.includes(a)) return q; } if (item.title) { const t = item.title.slice(0, 18); return ( questions.find((q) => { const qt = q.title.replace(/^\d+[\u3001.]\s*/, "").slice(0, 18); return qt === t || qt.includes(t) || t.includes(qt); }) || (item.qIndex >= 0 && item.qIndex < questions.length ? questions[item.qIndex] : null) ); } return item.qIndex >= 0 && item.qIndex < questions.length ? questions[item.qIndex] : null; } function loadHyExamLetterCache(examId) { if (!examId) return { lock: {}, tried: {} }; try { const all = JSON.parse(localStorage.getItem(HY_EXAM_ANSWER_CACHE_KEY) || "{}"); const hit = all[examId] || {}; return { lock: hit.lock || {}, tried: hit.tried || {} }; } catch (_) { return { lock: {}, tried: {} }; } } function hyExamCacheQid(item, questions) { const q = matchHyQuestionForResult(questions, item); return q ? q.qid : String(item.qIndex); } function isHyExamOptionId(val) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(val || "")); } function resolveHyExamCachedOptionId(q, val) { if (!val || !q) return ""; if (isHyExamOptionId(val)) return val; const opt = findHyOptionByLetter(q, val); return opt ? opt.id : ""; } function saveHyExamLetterCache(examId, feedback, questions) { if (!examId || !feedback) return; try { const all = JSON.parse(localStorage.getItem(HY_EXAM_ANSWER_CACHE_KEY) || "{}"); const cache = all[examId] || { lock: {}, tried: {} }; (feedback.correct || []).forEach((item) => { const q = questions ? matchHyQuestionForResult(questions, item) : null; const k = q ? q.qid : String(item.qIndex); const opt = q && item.optionLetter ? findHyOptionByLetter(q, item.optionLetter) : null; if (opt) cache.lock[k] = opt.id; }); (feedback.wrong || []).forEach((item) => { const q = questions ? matchHyQuestionForResult(questions, item) : null; const k = q ? q.qid : String(item.qIndex); delete cache.lock[k]; if (!cache.tried[k]) cache.tried[k] = []; const opt = q && item.optionLetter ? findHyOptionByLetter(q, item.optionLetter) : null; const oid = opt ? opt.id : item.optionLetter ? String(item.optionLetter).toUpperCase() : ""; if (oid && !cache.tried[k].includes(oid)) cache.tried[k].push(oid); }); all[examId] = cache; localStorage.setItem(HY_EXAM_ANSWER_CACHE_KEY, JSON.stringify(all)); } catch (_) {} } function applyHyExamLetterCache(state, questions, examId) { const cache = loadHyExamLetterCache(examId); let n = 0; questions.forEach((q, i) => { const lockedRaw = cache.lock[q.qid] || cache.lock[String(i)]; const lockedId = resolveHyExamCachedOptionId(q, lockedRaw); if (lockedId) { state.byQid[q.qid] = lockedId; n++; } const triedRaw = cache.tried[q.qid] || cache.tried[String(i)] || []; const triedLet = ensureHyExamTriedLetters(state, q.qid); const triedIds = ensureHyExamTried(state, q.qid); triedRaw.forEach((raw) => { if (isHyExamOptionId(raw)) { triedIds.add(raw); const o = q.options.find((x) => x.id === raw); const L = o && hyExamOptionLetter(q, o); if (L) triedLet.add(L); } else { const opt = findHyOptionByLetter(q, raw); if (opt) { triedIds.add(opt.id); triedLet.add(String(raw).toUpperCase()); } } }); }); return n; } function applyHyExamResultFeedback(state, questions, feedback) { let locked = 0; let adjusted = 0; (feedback.correct || []).forEach((item) => { const q = matchHyQuestionForResult(questions, item); if (!q || !item.optionLetter) return; const opt = findHyOptionByLetter(q, item.optionLetter); if (!opt) return; state.byQid[q.qid] = opt.id; ensureHyExamTriedLetters(state, q.qid).add(item.optionLetter.toUpperCase()); locked++; }); (feedback.wrong || []).forEach((item) => { const q = matchHyQuestionForResult(questions, item); if (!q) return; if (item.optionLetter) { const L = item.optionLetter.toUpperCase(); ensureHyExamTriedLetters(state, q.qid).add(L); const opt = findHyOptionByLetter(q, L); if (opt) ensureHyExamTried(state, q.qid).add(opt.id); } delete state.byQid[q.qid]; adjusted++; }); return { locked, adjusted }; } function parseHyExamWrongQids(html, questions) { const resultFb = parseHyExamResultPage(html, html.includes("exam_result") ? "exam_result.aspx" : ""); if (resultFb && resultFb.wrong.length) { return resultFb.wrong .map((item) => { const q = matchHyQuestionForResult(questions, item); return q ? q.qid : ""; }) .filter(Boolean); } const out = new Set(); const text = String(html || ""); const doc = new DOMParser().parseFromString(text, "text/html"); doc.querySelectorAll(".q_name[data-qid], label.q_name[data-qid]").forEach((el) => { const row = el.closest("table, .tablestyle, tr, li"); const ctx = (row && row.textContent) || el.textContent || ""; if (/\u9519\u8bef|\u4e0d\u6b63\u786e|\u7b54\u9519|×|✗|\u672a\u901a\u8fc7/.test(ctx)) { const qid = el.getAttribute("data-qid"); if (qid) out.add(qid); } }); doc.querySelectorAll(".test > table, .test table").forEach((table) => { const rowText = (table.textContent || "").replace(/\s+/g, " "); if (!/\u9519\u8bef|\u4e0d\u6b63\u786e|\u7b54\u9519|×|✗|\u672a\u901a\u8fc7|\u4f60\u7684\u7b54\u6848/.test(rowText)) return; const radio = table.querySelector("tbody input[type='radio']"); const name = radio && radio.getAttribute("name"); if (name) out.add(name); }); const re = /data-qid=["']([0-9a-f-]{36})["'][^>]*>[\s\S]{0,240}?(?:\u9519\u8bef|\u4e0d\u6b63\u786e|\u7b54\u9519)/gi; let m; while ((m = re.exec(text))) out.add(m[1]); if (!out.size && questions && questions.length) { questions.forEach((q, i) => { const block = text.split(q.title || "").slice(1, 2)[0] || ""; if (/\u9519\u8bef|\u4e0d\u6b63\u786e|\u7b54\u9519|×|✗/.test(block)) out.add(q.qid || `q${i}`); }); } return Array.from(out); } function createHyExamAnswerState() { return { byQid: {}, tried: {}, triedLetters: {}, rotateIdx: 0, rotateSeq: 0 }; } function ensureHyExamTried(state, qid) { if (!state.tried[qid]) state.tried[qid] = new Set(); return state.tried[qid]; } function fillHyExamPicks(questions, state) { if (state.rotateSeq == null) state.rotateSeq = 0; return questions.map((q) => { if (state.byQid[q.qid]) { return { qid: q.qid, optionId: state.byQid[q.qid] }; } const triedIds = ensureHyExamTried(state, q.qid); syncHyExamTriedLetters(state, q); let chosen = q.options.find((o) => !triedIds.has(o.id)); if (!chosen) { const idx = state.rotateSeq % Math.max(1, q.options.length); state.rotateSeq++; chosen = q.options[idx] || q.options[0]; } return { qid: q.qid, optionId: chosen.id }; }); } function buildHyExamPostBody(hidden, picks, questions) { const parts = []; if (hidden.__VIEWSTATE != null) parts.push(`__VIEWSTATE=${encodeURIComponent(hidden.__VIEWSTATE || "")}`); if (hidden.__VIEWSTATEGENERATOR != null) { parts.push(`__VIEWSTATEGENERATOR=${encodeURIComponent(hidden.__VIEWSTATEGENERATOR || "")}`); } if (hidden.__EVENTVALIDATION) { parts.push(`__EVENTVALIDATION=${encodeURIComponent(hidden.__EVENTVALIDATION)}`); } const skipHidden = new Set(["__VIEWSTATE", "__VIEWSTATEGENERATOR", "__EVENTVALIDATION", "hd_result"]); Object.keys(hidden || {}).forEach((k) => { if (skipHidden.has(k) || hidden[k] == null || hidden[k] === "") return; parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(hidden[k])}`); }); const pickMap = new Map((picks || []).map((p) => [p.qid, p.optionId])); const ordered = (questions || []) .map((q) => ({ qid: q.qid, optionId: pickMap.get(q.qid) })) .filter((p) => p.qid && p.optionId); const hdParts = []; const qMap = new Map((questions || []).map((q) => [q.qid, q])); ordered.forEach((p) => { const q = qMap.get(p.qid); if (q && q.format === "v2" && q.radioName) { parts.push(`${encodeURIComponent(q.radioName)}=${encodeURIComponent(p.optionId)}`); } else { parts.push(`radio_${p.qid}=${encodeURIComponent(p.optionId)}`); hdParts.push(`${p.qid}|${p.optionId}`); } }); if (hdParts.length) { parts.push(`hd_result=${encodeURIComponent(hdParts.join(","))}`); } parts.push("btn_submit.x=48", "btn_submit.y=17"); return parts.join("&"); } async function resolveHyExamPostResponse(cwid, post, referer) { let text = post.text || ""; let url = post.url || ""; if (parseHyExamResultPage(text, url) || /state_cour_ul/i.test(text)) { return { text, url }; } if (!/exam_result/i.test(url)) { const resultUrl = `${location.origin}/pages/exam_result.aspx?cwid=${encodeURIComponent(cwid)}`; try { const r = await fetch(resultUrl, { credentials: "include", headers: { Accept: "text/html,application/xhtml+xml", Referer: referer || `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(cwid)}`, }, __hyEngineMark: true, }); if (r.ok) { const resultText = await r.text(); const resultUrlFinal = r.url || resultUrl; if ( parseHyExamResultPage(resultText, resultUrlFinal) || /state_cour_ul/i.test(resultText) || isHyExamPassHtml(resultText, resultUrlFinal) ) { return { text: resultText, url: resultUrlFinal }; } } } catch (_) {} } return { text, url }; } function readLiveExamPageBundle(examId) { if (pageType() !== "exam") return null; const cur = new URLSearchParams(location.search).get("cwid"); if (!cur || String(cur).toUpperCase() !== String(examId).toUpperCase()) return null; const html = document.documentElement ? document.documentElement.outerHTML : ""; const questions = parseHyExamQuestions(html); if (!questions.length) return null; return { ok: true, url: location.href, html, hidden: parseAspNetHidden(html), questions, examId: cur, live: true, }; } function packHyExamPage(html, url, examId, live) { if (isHyExamCodePage(html, url)) { return { ok: false, needCaptcha: true, html, url, examId, msg: "\u9700\u8981\u9a8c\u8bc1\u7801" }; } if (isHyExamAlreadyPassedHtml(html) && !parseHyExamQuestions(html).length) { return { ok: false, alreadyPassed: true, url, html, examId }; } const hidden = parseAspNetHidden(html); const questions = parseHyExamQuestions(html); if (!questions.length) { const hint = diagnoseExamPageHtml(html); return { ok: false, msg: hint || "\u672a\u89e3\u6790\u5230\u8bd5\u9898", url, html, examId }; } return { ok: true, url, html, hidden, questions, examId, live: !!live }; } async function fetchHyExamPageOnce(examId, refererCwrid) { const live = readLiveExamPageBundle(examId); if (live) return live; const url = `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(examId)}`; const referer = refererCwrid ? `${location.origin}/course_ware/course_ware_polyv.aspx?cwid=${encodeURIComponent(refererCwrid)}` : `${location.origin}/pages/course.aspx`; const r = await fetch(url, { credentials: "include", headers: { Accept: "text/html,application/xhtml+xml", Referer: referer, }, }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const html = await r.text(); const finalUrl = r.url || url; if (isHyExamCodePage(html, finalUrl)) { return { ok: false, needCaptcha: true, html, url: finalUrl, examId, msg: "\u9700\u8981\u9a8c\u8bc1\u7801" }; } return packHyExamPage(html, finalUrl, examId, false); } async function fetchHyExamPage(examIds, refererCwrid) { const ids = [...new Set((Array.isArray(examIds) ? examIds : [examIds]).filter(Boolean))]; let last = null; for (const id of ids) { try { last = await fetchHyExamPageOnce(id, refererCwrid || id); if (last.ok || last.alreadyPassed) return last; } catch (e) { last = { ok: false, msg: e && e.message ? e.message : "\u62c9\u53d6\u8bd5\u5377\u5931\u8d25", examId: id }; } } return last || { ok: false, msg: "\u672a\u89e3\u6790\u5230\u8bd5\u9898" }; } async function postHyExamPage(cwid, hidden, picks, referer, questions) { const url = `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(cwid)}`; const body = buildHyExamPostBody(hidden, picks, questions); const r = await fetch(url, { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "text/html,application/xhtml+xml", }, body, redirect: "follow", referrer: referer || url, __hyEngineMark: true, }); const raw = { text: await r.text(), url: r.url || url, status: r.status }; return resolveHyExamPostResponse(cwid, raw, referer || url); } function shouldNavigateToExamPage(res) { return !!(res && !res.ok && /\u672a\u89e3\u6790\u5230\u8bd5\u9898|\u672a\u83b7\u53d6\u8bd5\u5377|\u767b\u5f55\u5931\u6548|\u9009\u9879\u672a\u52a0\u8f7d/.test(String(res.msg || ""))); } function navigateToExamPage(ctx, label) { const examId = resolveExamCwid(ctx); if (!examId) return false; try { sessionStorage.setItem( EXAM_NAV_KEY, JSON.stringify({ key: jobKey(ctx), examId, label: label || ctx.title || "", ts: Date.now() }) ); } catch (_) {} pushLog(`[${label || ctx.title || examId.slice(0, 8)}] \u8df3\u8f6c\u8003\u8bd5\u9875\uff08DOM \u62c9\u5377\uff09…`); location.href = `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(examId)}`; return true; } async function verifyHyExamPlatformPassed(examId, refererCwrid) { const ids = [...new Set([examId].filter(Boolean))]; for (const id of ids) { const resultUrl = `${location.origin}/pages/exam_result.aspx?cwid=${encodeURIComponent(id)}`; try { const r = await fetch(resultUrl, { credentials: "include", headers: { Accept: "text/html,application/xhtml+xml", Referer: `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(id)}`, }, __hyEngineMark: true, }); const text = await r.text(); const url = r.url || resultUrl; const fb = parseHyExamResultPage(text, url); if (fb && fb.failed) { return { passed: false, msg: fb.score ? `\u672a\u901a\u8fc7\uff08${fb.score}\u5206\uff09` : "\u5e73\u53f0\u663e\u793a\u672a\u901a\u8fc7" }; } if (isHyExamPassHtml(text, url)) { return { passed: true, score: (fb && fb.score) || "" }; } } catch (_) {} try { const page = await fetchHyExamPageOnce(id, refererCwrid || id); if (page.alreadyPassed) return { passed: true, skipped: true }; if (page.ok && page.questions && page.questions.length) { return { passed: false, msg: "\u5e73\u53f0\u4ecd\u53ef\u7b54\u5377\uff0c\u8003\u8bd5\u672a\u901a\u8fc7" }; } if (isHyExamAlreadyPassedHtml(page.html || "")) return { passed: true, skipped: true }; } catch (_) {} } return { passed: false, msg: "\u5e73\u53f0\u672a\u786e\u8ba4\u8003\u8bd5\u901a\u8fc7" }; } async function runHyClientExam(cwid, label, altIds, refererCwrid) { if (!cwid && (!altIds || !altIds.length)) return { ok: false, msg: "\u7f3a\u5c11\u8003\u8bd5 cwid" }; const tag = label || String(cwid || altIds[0] || "").slice(0, 8); const state = createHyExamAnswerState(); const examIds = [...new Set([cwid, ...(altIds || [])].filter(Boolean))]; pushLog(`[${tag}] \u5f00\u59cb\u8003\u8bd5\uff08API \u66b4\u529b\u63d0\u4ea4\uff09…`); for (let t = 0; t < HY_EXAM_MAX_TRIES; t++) { if (!getEnabled()) { pushLog(`[${tag}] \u5df2\u6682\u505c\uff0c\u505c\u6b62\u8003\u8bd5`); return { ok: false, msg: "\u5df2\u6682\u505c", paused: true }; } let fresh; try { fresh = await fetchHyExamPage(examIds, refererCwrid || cwid || examIds[0]); } catch (e) { return { ok: false, msg: e && e.message ? e.message : "\u62c9\u53d6\u8bd5\u5377\u5931\u8d25" }; } if (!fresh.ok) { if (fresh.alreadyPassed) { return { ok: true, skipped: true, msg: "\u5df2\u901a\u8fc7" }; } if (fresh.needCaptcha || isHyExamCodePage(fresh.html, fresh.url) || isHyExamCaptchaHtml(fresh.html)) { const cap = await tryPassHyExamCaptcha( fresh.examId || cwid || examIds[0], tag, fresh.html, refererCwrid || fresh.url ); if (!cap.ok) return cap; continue; } return { ok: false, msg: fresh.msg || "\u672a\u83b7\u53d6\u8bd5\u5377", needNavigate: shouldNavigateToExamPage(fresh) }; } applyHyExamLetterCache(state, fresh.questions, fresh.examId || cwid); if (t === 0) { pushLog( `[${tag}] \u5df2\u89e3\u6790 ${fresh.questions.length} \u9898\uff08${fresh.live ? "\u5f53\u524d\u8003\u8bd5\u9875" : "API \u62c9\u5377"}·${fresh.questions[0].format || "v1"}${Object.keys(state.byQid).length ? `\uff0c\u5df2\u8bb0\u5fc6 ${Object.keys(state.byQid).length} \u9898` : ""}\uff09` ); } const picks = fillHyExamPicks(fresh.questions, state); const pickLetters = formatHyExamPicksLog(fresh.questions, picks); pushLog(`[${tag}] \u672c\u6b21\u9009\u9898 ${pickLetters} (${t + 1}/${HY_EXAM_MAX_TRIES})`); let post; try { post = await postHyExamPage( fresh.examId || cwid || examIds[0], fresh.hidden, picks, fresh.url, fresh.questions ); } catch (e) { return { ok: false, msg: e && e.message ? e.message : "\u4ea4\u5377\u5931\u8d25" }; } const resultFbEarly = parseHyExamResultPage(post.text, post.url); if ( !resultFbEarly && !isHyExamPassHtml(post.text, post.url) && /exam\.aspx/i.test(post.url || "") && parseHyExamQuestions(post.text).length ) { pushLog(`[${tag}] \u4ea4\u5377\u672a\u8df3\u8f6c\u7ed3\u679c\u9875\uff08hd_result \u53ef\u80fd\u65e0\u6548\uff09`); } else if (t === 0 && !resultFbEarly && !isHyExamPassHtml(post.text, post.url)) { const pageName = (post.url || "").split("/").pop() || "?"; pushLog(`[${tag}] \u4ea4\u5377\u54cd\u5e94 ${pageName}\uff08${(post.text || "").length} \u5b57\u8282\uff09`); } if (isHyExamPassHtml(post.text, post.url)) { const verify = await verifyHyExamPlatformPassed(fresh.examId || cwid, refererCwrid || cwid); if (!verify.passed) { pushLog(`[${tag}] \u4ea4\u5377\u54cd\u5e94\u50cf\u901a\u8fc7\u4f46\u5e73\u53f0\u672a\u786e\u8ba4\uff1a${verify.msg || "\u672a\u901a\u8fc7"}`); if (/\u5e73\u53f0\u4ecd\u53ef\u7b54\u5377/.test(verify.msg || "")) { const resultFb = parseHyExamResultPage(post.text, post.url); if (resultFb && (resultFb.correct.length || resultFb.wrong.length)) { saveHyExamLetterCache(fresh.examId || cwid, resultFb, fresh.questions); applyHyExamResultFeedback(state, fresh.questions, resultFb); } pushLog(`[${tag}] \u7b49\u5f85 ${HY_EXAM_RETRY_GAP_MS / 1000} \u79d2\u540e\u91cd\u8bd5…`); if (!(await hyExamRetryWait(HY_EXAM_RETRY_GAP_MS, tag))) { return { ok: false, msg: "\u5df2\u6682\u505c", paused: true }; } continue; } return { ok: false, msg: verify.msg || "\u5e73\u53f0\u672a\u786e\u8ba4\u8003\u8bd5\u901a\u8fc7" }; } picks.forEach((p) => { state.byQid[p.qid] = p.optionId; }); const passFb = parseHyExamResultPage(post.text, post.url); if (passFb) saveHyExamLetterCache(fresh.examId || cwid, passFb, fresh.questions); const scoreM = verify.score || (passFb && passFb.score) || (post.text.match(/test_score:\s*["'](\d+)/i) || [])[1] || (post.text.match(/(\d+)\s*\u5206/) || [])[1]; return { ok: true, skipped: !!verify.skipped, score: scoreM || "", msg: "\u8003\u8bd5\u901a\u8fc7" }; } if (/\u5b66\u4e60\u65f6\u957f|\u8bf7\u5148\u5b66\u4e60|\u4e0d\u80fd\u7b54\u5377|\u672a\u5b66\u5b8c/.test(post.text)) { return { ok: false, msg: "\u5b66\u65f6\u672a\u6ee1\uff0c\u4e0d\u80fd\u7b54\u5377" }; } if (isHyExamCodePage(post.text, post.url) || isHyExamCaptchaHtml(post.text)) { const cap = await tryPassHyExamCaptcha( fresh.examId || cwid || examIds[0], tag, post.text, fresh.url ); if (!cap.ok) return cap; continue; } const resultFb = parseHyExamResultPage(post.text, post.url); if (resultFb && (resultFb.correct.length || resultFb.wrong.length)) { saveHyExamLetterCache(fresh.examId || cwid, resultFb, fresh.questions); const applied = applyHyExamResultFeedback(state, fresh.questions, resultFb); const scoreTxt = resultFb.score ? ` ${resultFb.score}\u5206` : ""; const triedSummary = fresh.questions .map((q) => { const letters = [...(state.triedLetters[q.qid] || [])].join("") || "-"; return `${hyExamOptionLetter(q, q.options.find((o) => o.id === state.byQid[q.qid])) || "?"}[\u8bd5${letters}]`; }) .join(" "); pushLog( `[${tag}] \u7ed3\u679c\u9875${scoreTxt}\uff1a\u4fdd\u7559 ${applied.locked} \u9898\u6b63\u786e\uff0c\u8c03\u6574 ${applied.adjusted} \u9898 (${t + 1}/${HY_EXAM_MAX_TRIES})` ); pushLog(`[${tag}] \u5df2\u8bd5\u9009\u9879 ${triedSummary}`); pushLog(`[${tag}] \u7b49\u5f85 ${HY_EXAM_RETRY_GAP_MS / 1000} \u79d2\u540e\u91cd\u8bd5…`); if (!(await hyExamRetryWait(HY_EXAM_RETRY_GAP_MS, tag))) { pushLog(`[${tag}] \u5df2\u6682\u505c\uff0c\u505c\u6b62\u8003\u8bd5`); return { ok: false, msg: "\u5df2\u6682\u505c", paused: true }; } continue; } const wrongQids = parseHyExamWrongQids(post.text, fresh.questions); if (wrongQids.length) { wrongQids.forEach((qid) => { const pick = picks.find((p) => p.qid === qid); if (pick) ensureHyExamTried(state, qid).add(pick.optionId); delete state.byQid[qid]; }); pushLog(`[${tag}] \u8003\u8bd5\u7ea0\u9519\uff1a\u8c03\u6574 ${wrongQids.length} \u9898 (${t + 1}/${HY_EXAM_MAX_TRIES})`); } else if (!/exam_result/i.test(post.url || "")) { const q = fresh.questions[state.rotateIdx % fresh.questions.length]; state.rotateIdx++; const pick = picks.find((p) => p.qid === q.qid); if (pick) ensureHyExamTried(state, q.qid).add(pick.optionId); delete state.byQid[q.qid]; pushLog( `[${tag}] \u65e0\u7ed3\u679c\u53cd\u9988\uff0c\u8bd5\u9519\u7b2c ${((state.rotateIdx - 1) % fresh.questions.length) + 1} \u9898 (${t + 1}/${HY_EXAM_MAX_TRIES})` ); } else { pushLog(`[${tag}] \u7ed3\u679c\u9875\u672a\u8bc6\u522b\u5bf9\u9519\uff0c\u8bf7\u624b\u52a8\u67e5\u770b`); return { ok: false, msg: "\u7ed3\u679c\u9875\u89e3\u6790\u5931\u8d25" }; } if (!getEnabled()) { pushLog(`[${tag}] \u5df2\u6682\u505c\uff0c\u505c\u6b62\u8003\u8bd5`); return { ok: false, msg: "\u5df2\u6682\u505c", paused: true }; } pushLog(`[${tag}] \u7b49\u5f85 ${HY_EXAM_RETRY_GAP_MS / 1000} \u79d2\u540e\u91cd\u8bd5…`); if (!(await hyExamRetryWait(HY_EXAM_RETRY_GAP_MS, tag))) { pushLog(`[${tag}] \u5df2\u6682\u505c\uff0c\u505c\u6b62\u8003\u8bd5`); return { ok: false, msg: "\u5df2\u6682\u505c", paused: true }; } } return { ok: false, msg: "\u66b4\u529b\u7b54\u9898\u5931\u8d25" }; } function findJobCtxByCwid(cwid) { if (!cwid) return null; const target = String(cwid).toUpperCase(); const jobs = loadJobs(); const hit = Object.values(jobs).find( (j) => j && (String(j.cwid || "").toUpperCase() === target || String(j.cwrid || "").toUpperCase() === target) ); return hit ? jobToCtx(hit) : null; } async function afterVideoMaybeExam(ctx, label) { const examId = resolveExamCwid(ctx); const altIds = [ctx && ctx.wareCwid, ctx && ctx.cwid].filter( (id, i, arr) => id && id !== examId && arr.indexOf(id) === i ); if (!examId) return { ok: false, msg: "\u7f3a\u5c11\u8003\u8bd5 cwid" }; const res = await runHyClientExam(examId, label || ctx.title, altIds, ctx && ctx.cwrid); if (res.ok) { markPanelChapterExamDone(ctx); panelState.lastCourseListKey = ""; renderCourseList(); renderChapterPreview(); updatePanel(); maybeAutoStopWhenFinished(); } else if (res.needNavigate && pageType() !== "exam") { navigateToExamPage(ctx, label || ctx.title); } return res; } async function handleExamResultPage() { const cwid = new URLSearchParams(location.search).get("cwid"); if (!cwid) return; const html = document.documentElement ? document.documentElement.outerHTML : ""; const fb = parseHyExamResultPage(html, location.href); if (fb && (fb.correct.length || fb.wrong.length)) { saveHyExamLetterCache(cwid, fb, parseHyExamQuestions(html)); } if (fb && fb.failed) { return; } if (!isHyExamPassHtml(html, location.href)) return; let ctx = findJobCtxByCwid(cwid) || findPanelChapterCtxByCwid(cwid); if (ctx) { markPanelChapterExamDone(ctx); panelState.lastCourseListKey = ""; renderCourseList(); renderChapterPreview(); updatePanel(); pushLog(`[${ctx.title || cwid.slice(0, 8)}] \u8003\u8bd5\u7ed3\u679c\u9875\uff1a\u5df2\u901a\u8fc7`); return; } await enrichPanelCoursesProgress(getQueue(), true); ctx = findPanelChapterCtxByCwid(cwid); if (ctx) { markPanelChapterExamDone(ctx); panelState.lastCourseListKey = ""; renderCourseList(); renderChapterPreview(); updatePanel(); pushLog(`[${ctx.title || cwid.slice(0, 8)}] \u8003\u8bd5\u7ed3\u679c\u9875\uff1a\u5df2\u901a\u8fc7\uff08\u5df2\u540c\u6b65\u6536\u85cf\u8fdb\u5ea6\uff09`); } else { pushLog(`\u8003\u8bd5\u7ed3\u679c\u9875\uff1a\u5df2\u901a\u8fc7\uff08${cwid.slice(0, 8)}\uff0c\u8bf7\u70b9\u9762\u677f\u300c\u5237\u65b0\u300d\u540c\u6b65\u8fdb\u5ea6\uff09`); } } function updateJobsPanel() { updatePanel(); } function refreshPanelActions() { updatePanel(); } function parseCourseChapters() { return Array.from(document.querySelectorAll(".course[data-href]")) .filter((el) => !isElectiveChapter(el)) .filter((el) => isHdInteractiveEnabled() || !chapterHasInteractiveTag(el)) .map((el) => { const href = el.getAttribute("data-href") || ""; const { label, progressText, studyStateAttr } = getChapterStatusParts(el); const interactive = isInteractiveChapter(el); const st = inferHyChapterStatuses(label, progressText, studyStateAttr, interactive); return { href, done: st.videoDone, examDone: st.examDone, untouched: st.untouched, el, }; }); } function coursePageUrl(cid) { return `${location.origin}/pages/course.aspx?cid=${encodeURIComponent(cid)}`; } function normalizeQueueCid(item) { if (!item) return ""; const s = String(item).trim(); const m = s.match(/cid=([^&]+)/i); const cid = m ? m[1] : s; return cid ? cid.toLowerCase() : ""; } async function hyFetchHtml(url, postBody) { const opts = { credentials: "include", headers: { Accept: "text/html,application/xhtml+xml" } }; if (postBody) { opts.method = "POST"; opts.headers["Content-Type"] = "application/x-www-form-urlencoded"; opts.body = postBody; } const r = await fetch(url, opts); if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.text(); } function parseAspNetHidden(html) { const pick = (name) => { const m1 = html.match(new RegExp(`name="${name}"[^>]*value="([^"]*)"`, "i")); if (m1) return m1[1]; const m2 = html.match(new RegExp(`id="${name}"[^>]*value="([^"]*)"`, "i")); return m2 ? m2[1] : ""; }; const hidden = { __VIEWSTATE: pick("__VIEWSTATE"), __VIEWSTATEGENERATOR: pick("__VIEWSTATEGENERATOR"), __EVENTVALIDATION: pick("__EVENTVALIDATION"), }; const re = /]+type=["']hidden["'][^>]*>/gi; let m; while ((m = re.exec(html))) { const tag = m[0]; const nm = tag.match(/name=["']([^"']+)["']/i); const val = tag.match(/value=["']([^"']*)["']/i); if (!nm || !nm[1]) continue; const name = nm[1]; if (name === "hd_result") continue; if (/^(radio_|btn_)/i.test(name)) continue; if (hidden[name] == null) hidden[name] = val ? val[1] : ""; } return hidden; } function isFallbackCourseTitle(title, cid) { const t = String(title || "").trim(); if (!t) return true; if (t.toLowerCase() === String(cid || "").slice(0, 8).toLowerCase()) return true; return /^[0-9a-f]{8}$/i.test(t); } function parseCollectCourseItem(li) { const boxA = li.querySelector(".sub_box a[href*='course.aspx?cid=']"); const centA = li.querySelector(".sub_cent a[href*='course.aspx?cid=']"); const a = boxA || centA || li.querySelector("a[href*='course.aspx?cid=']"); if (!a) return null; const href = a.getAttribute("href") || ""; const m = href.match(/cid=([^&'"]+)/i); if (!m) return null; const cid = normalizeQueueCid(m[1]); const title = (li.querySelector(".sub_tit")?.textContent || "").replace(/\s+/g, " ").trim(); const credits = (li.querySelector(".sub_text")?.textContent || "").replace(/\s+/g, " ").trim(); return { cid, title: title || "", credits, url: href.startsWith("http") ? href : new URL(href, location.origin).href, }; } function parseCollectCoursesFromHtml(html) { const doc = new DOMParser().parseFromString(html, "text/html"); const seen = new Set(); const list = []; doc.querySelectorAll("ul.sub_ul > li").forEach((li) => { const item = parseCollectCourseItem(li); if (!item || seen.has(item.cid)) return; seen.add(item.cid); list.push(item); }); return list; } function parseCollectMaxPage(html) { const pages = new Set([1]); const normalized = String(html || "") .replace(/'/g, "'") .replace(/"/g, '"') .replace(/&/g, "&"); const postRe = /__doPostBack\s*\(\s*['"]AspNetPager['"]\s*,\s*['"](\d+)['"]\s*\)/gi; let m; while ((m = postRe.exec(normalized))) { const n = parseInt(m[1], 10); if (n > 0) pages.add(n); } const pagerBlock = normalized.match(/id=["']AspNetPager["'][\s\S]*?<\/div>\s*<\/div>/i); if (pagerBlock) { const numRe = />(\d+) 0 && n < 50) pages.add(n); } } return Math.max(...pages); } async function fetchCollectPageHtml(pageNum, form) { const url = `${location.origin}/PersonalCenter/course_collect.aspx`; if (pageNum <= 1 && !form) { const html = await hyFetchHtml(url); return { html, hidden: parseAspNetHidden(html) }; } const hidden = form || {}; const body = new URLSearchParams({ __EVENTTARGET: "AspNetPager", __EVENTARGUMENT: String(pageNum), __VIEWSTATE: hidden.__VIEWSTATE || "", __VIEWSTATEGENERATOR: hidden.__VIEWSTATEGENERATOR || "", __EVENTVALIDATION: hidden.__EVENTVALIDATION || "", hd_coa_id: "", }); const html = await hyFetchHtml(url, body.toString()); return { html, hidden: parseAspNetHidden(html) }; } async function fetchAllCollectCourses() { const all = []; const seen = new Set(); let hidden = null; let maxPage = 1; const first = await fetchCollectPageHtml(1, null); const issue = detectCollectPageIssue(first.html); if (issue?.login) { panelState.collectLoginLost = true; return []; } panelState.collectLoginLost = false; hidden = first.hidden; maxPage = Math.max(1, parseCollectMaxPage(first.html)); parseCollectCoursesFromHtml(first.html).forEach((c) => { if (!seen.has(c.cid)) { seen.add(c.cid); all.push(c); } }); for (let page = 2; page <= maxPage; page++) { const res = await fetchCollectPageHtml(page, hidden); hidden = res.hidden; maxPage = Math.max(maxPage, parseCollectMaxPage(res.html)); parseCollectCoursesFromHtml(res.html).forEach((c) => { if (!seen.has(c.cid)) { seen.add(c.cid); all.push(c); } }); if (page < maxPage) await sleep(320); } panelState.collectPages = maxPage; return all; } function blankPanelCourse(raw) { const cid = normalizeQueueCid(raw.cid); const title = raw.title && !isFallbackCourseTitle(raw.title, cid) ? raw.title : ""; return { cid, title: title || "\u52a0\u8f7d\u4e2d…", credits: raw.credits || "", url: raw.url || coursePageUrl(raw.cid), chapterTotal: 0, studyDone: 0, examDone: 0, gongxu: false, untouchedCount: 0, chaptersLoaded: false, chapters: [], progressError: "", titleLoaded: !!title, }; } function isCourseChaptersReady(course) { return !!(course && course.chaptersLoaded && (course.chapterTotal || 0) > 0 && !course.progressError); } function getCourseProgressLine(course) { if (!course) return "—"; if (course.progressError) return course.progressError; if (!course.chaptersLoaded) return "\u7ae0\u8282\u52a0\u8f7d\u4e2d…"; const total = Number(course.chapterTotal) || 0; if (total <= 0) return "\u7ae0\u8282\u672a\u52a0\u8f7d"; const studyDone = Number(course.studyDone) || 0; const examDone = Number(course.examDone) || 0; if (course.gongxu) { if (studyDone < total) return `\u89c6\u9891 ${studyDone}/${total} · \u8bfe\u7ea7\u8003\u6838\u5f85\u5b66\u5b8c`; return `\u89c6\u9891 ${studyDone}/${total} · \u8bfe\u7ea7\u8003\u6838\u5f85\u7533\u8bf7\u8bc1\u4e66`; } return `\u89c6\u9891 ${studyDone}/${total} · \u8003\u8bd5 ${examDone}/${total}`; } function detectChapterLoadIssue(html, parsed) { if (parsed.uid && parsed.chapters.length > 0) return null; if (parsed.uid && !parsed.chapters.length) { if (/class=["']course["'][^>]*data-href|\u8bfe\u7a0b\u5b66\u4e60\u4e0e\u8003\u8bd5|exam_tit/i.test(html)) { return { error: "\u672a\u8bc6\u522b\u5230\u5fc5\u4fee\u7ae0\u8282\uff08\u53ef\u80fd\u5747\u4e3a\u9009\u4fee\u6216\u9875\u9762\u7ed3\u6784\u53d8\u5316\uff09" }; } return { error: "\u7ae0\u8282\u672a\u52a0\u8f7d\uff0c\u8bf7 F5 \u540e\u5237\u65b0\u6536\u85cf" }; } if (isPageLoginRedirect(html)) { return { error: "\u767b\u5f55\u5931\u6548\uff0c\u8bf7 F5 \u5237\u65b0", login: true }; } return { error: "\u7ae0\u8282\u9875\u89e3\u6790\u5931\u8d25\uff0c\u8bf7 F5 \u5237\u65b0" }; } function applyCourseChapterParse(course, parsed, html) { const issue = detectChapterLoadIssue(html, parsed); if (issue) { course.chapters = []; course.chapterTotal = 0; course.studyDone = 0; course.examDone = 0; course.untouchedCount = 0; course.chaptersLoaded = false; course.progressError = issue.error; if (issue.login) warnLoginIfNeeded(false); return false; } course.chapters = parsed.chapters; course.chapterTotal = parsed.chapters.length; course.gongxu = !!parsed.gongxu; course.studyDone = parsed.chapters.filter((ch) => ch.done).length; course.examDone = parsed.chapters.filter((ch) => ch.examDone).length; course.untouchedCount = parsed.chapters.filter((ch) => ch.untouched && !ch.done).length; if (parsed.courseTitle) { course.title = parsed.courseTitle; course.titleLoaded = true; } else if (isFallbackCourseTitle(course.title, course.cid)) { course.title = "\u672a\u547d\u540d\u8bfe\u7a0b"; } course.chaptersLoaded = true; course.progressError = ""; return true; } async function enrichPanelCourse(course, force) { if (!course) return course; if (!force && isCourseChaptersReady(course)) return course; try { const html = await hyFetchHtml(course.url || coursePageUrl(course.cid)); const parsed = parseCourseChaptersFromHtml(html, course.cid); if (!applyCourseChapterParse(course, parsed, html)) { pushLog(`\u7ae0\u8282\u52a0\u8f7d\u5931\u8d25\uff1a${course.title || course.cid}\uff08${course.progressError}\uff09`); } else if (course.chapters && course.chapters.length) { syncInteractiveJobsFromChapters(course.chapters); syncJobTitlesFromPanelCourses(); } } catch (err) { course.chapters = []; course.chapterTotal = 0; course.studyDone = 0; course.examDone = 0; course.untouchedCount = 0; course.chaptersLoaded = false; course.progressError = "\u7ae0\u8282\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7 F5 \u5237\u65b0"; pushLog(`\u7ae0\u8282\u52a0\u8f7d\u5931\u8d25\uff1a${course.title || course.cid}`); } return course; } async function enrichPanelCoursesProgress(cids, force) { if (panelState.loginRequired) return; const ids = (cids && cids.length ? cids : panelState.courses.map((c) => c.cid)).filter((cid) => panelState.courses.some((c) => c.cid === cid) ); const targets = ids.map((cid) => panelState.courses.find((c) => c.cid === cid)).filter(Boolean); for (const course of targets) { await enrichPanelCourse(course, !!force); await sleep(120); } } async function fetchPanelCourses() { panelState.refreshing = true; updatePanel(); try { let raw = []; if (pageType() === "collect") { raw = parseCollectCourses(); } const remote = await fetchAllCollectCourses(); const map = new Map(); raw.forEach((c) => map.set(c.cid, c)); remote.forEach((c) => { const prev = map.get(c.cid); const title = c.title && !isFallbackCourseTitle(c.title, c.cid) ? c.title : prev?.title && !isFallbackCourseTitle(prev.title, c.cid) ? prev.title : c.title || prev?.title || ""; map.set(c.cid, { ...(prev || {}), ...c, title }); }); panelState.courses = Array.from(map.values()).map(blankPanelCourse); panelState.courses.sort((a, b) => a.title.localeCompare(b.title, "zh-CN")); if (panelState.collectLoginLost) { panelState.courses = []; panelState.loginRequired = true; clearStalePanelSelection(); pushLog("\u672a\u767b\u5f55\uff0c\u5df2\u6e05\u7a7a\u672c\u5730\u8bfe\u7a0b\u52fe\u9009\u7f13\u5b58"); return panelState.courses; } panelState.loginRequired = false; initDefaultQueueIfNeeded(); return panelState.courses; } catch (err) { panelState.courses = []; pushLog("\u52a0\u8f7d\u6536\u85cf\u5931\u8d25\uff1a" + (err.message || err)); return []; } finally { panelState.refreshing = false; updatePanel(); } } async function refreshPanelCourses() { detectAccountSwitch(); panelState.refreshing = true; updatePanel(); try { await fetchPanelCourses(); localStorage.removeItem(QUEUE_TOUCHED_KEY); setQueue(panelState.courses.map((c) => c.cid)); pushLog(`\u5df2\u5237\u65b0\u6536\u85cf ${panelState.courses.length} \u95e8\uff08${panelState.collectPages || 1} \u9875\uff09\uff0c\u9ed8\u8ba4\u5168\u9009`); renderCourseList(); await enrichPanelCoursesProgress(getQueue(), true); await refreshChapterPreview(); } catch (err) { pushLog("\u5237\u65b0\u8bfe\u7a0b\u5931\u8d25\uff1a" + (err.message || err)); } finally { panelState.refreshing = false; updatePanel(); } } async function refreshChapterPreview() { if (panelState.loginRequired) { panelState.chapterPreview = []; renderChapterPreview(); return; } const selected = pruneQueueToCourses(); if (!selected.length) { panelState.chapterPreview = []; renderChapterPreview(); return; } const list = []; for (const cid of selected) { const course = panelState.courses.find((c) => c.cid === cid); if (!course) continue; if (!isCourseChaptersReady(course)) await enrichPanelCourse(course); list.push({ cid, title: course.title || cid, chapters: course.chapters || [], progressError: course.progressError || "", chaptersLoaded: !!course.chaptersLoaded, }); } panelState.chapterPreview = list; renderChapterPreview(); } async function initPanelData() { detectAccountSwitch(); cloudState.cloudToken = String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(); void _cf(); try { await fetchPanelCourses(); if (getEnabled()) ensureStudyQueueSelection(); if (panelState.courses.length) { const pages = panelState.collectPages || 1; pushLog(`\u5df2\u52a0\u8f7d\u6536\u85cf ${panelState.courses.length} \u95e8\uff08${pages} \u9875\uff09`); } renderCourseList(); void enrichPanelCoursesProgress(getQueue()).then(() => { renderCourseList(); refreshChapterPreview(); }); } catch (_) {} } function isCourseUnfinished(course) { if (!isCourseChaptersReady(course)) return true; const total = course.chapterTotal || 0; if (course.gongxu) return (course.studyDone || 0) < total; return (course.studyDone || 0) < total || (course.examDone || 0) < total; } function notifyGongxuCourseExamIfNeeded() { const selected = pruneQueueToCourses(); selected.forEach((cid) => { const course = findPanelCourseByCid(cid); if (!course || !isGongxuCourse(course) || !isCourseChaptersReady(course)) return; const total = Number(course.chapterTotal) || 0; if (total <= 0 || (Number(course.studyDone) || 0) < total) return; const key = `hy_gongxu_tip_${cid}`; try { if (sessionStorage.getItem(key)) return; sessionStorage.setItem(key, "1"); } catch (_) {} pushUserLog( `\u300c${course.title || "\u516c\u9700\u8bfe"}\u300d\u5168\u90e8\u89c6\u9891\u5df2\u5b66\u5b8c\uff0c\u8bf7\u5230\u8bfe\u7a0b\u9875\u70b9\u51fb\u300c\u7533\u8bf7\u8bc1\u4e66\u300d\u5b8c\u6210\u8bfe\u7ea7\u8003\u6838\uff08≥60\u5206\uff09` ); }); } function parseCollectCourses() { const seen = new Set(); const list = []; document.querySelectorAll("ul.sub_ul > li").forEach((li) => { const item = parseCollectCourseItem(li); if (!item || seen.has(item.cid)) return; seen.add(item.cid); list.push(item); }); return list; } function getQueue() { try { return JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]") .map(normalizeQueueCid) .filter(Boolean); } catch (_) { return []; } } function setQueue(list) { localStorage.setItem(QUEUE_KEY, JSON.stringify((list || []).map(normalizeQueueCid).filter(Boolean))); } async function runCoursePage() { updateJobsPanel(); if (!getEnabled() || !getAutoNext() || getApiMode()) return; await sleep(1500); const chapters = parseCourseChapters(); const next = chapters.find((c) => c.untouched || !c.done); if (!next) { pushLog("\u672c\u8bfe\u7a0b\u5168\u90e8\u8bfe\u4ef6\u5df2\u5b66\u4e60"); advanceQueue(); return; } const title = (next.el.querySelector("h3") || next.el).textContent.replace(/\s+/g, " ").trim(); pushLog("\u6253\u5f00\u4e0b\u4e00\u8bfe\u4ef6\uff1a" + title.slice(0, 40)); const url = next.href.startsWith("http") ? next.href : new URL(next.href, location.origin).href; window.open(url, "new_courseWare"); } function advanceQueue() { const queue = getQueue(); if (queue.length <= 1) { setQueue([]); pushLog("\u961f\u5217\u5df2\u5168\u90e8\u5b8c\u6210"); return; } const rest = queue.slice(1); setQueue(rest); if (getApiMode()) { pushLog("\u6a21\u5f0f\uff1a\u4e0d\u4e0b\u94bb\u6253\u5f00\u8bfe\u7a0b\u9875"); return; } location.href = coursePageUrl(rest[0]); } async function runCollectPage() { if (!panelState.courses.length) { try { await fetchPanelCourses(); } catch (_) {} } renderCourseList(); } async function runWareRedirect() { if (!getEnabled()) return; const pathLower = location.pathname.toLowerCase(); if (pathLower.includes("course_ware_hd.aspx")) return; const cwid = new URLSearchParams(location.search).get("cwid"); if (!cwid) return; const jobs = loadJobs(); const interactiveJob = Object.values(jobs).some( (j) => j && j.interactive && j.cwid === cwid && j.phase !== "done" ); if (interactiveJob) return; await sleep(800); if (pageType() !== "ware") return; const target = location.pathname.replace("course_ware.aspx", "course_ware_polyv.aspx") + location.search; if (!location.pathname.includes("polyv")) { location.replace(target + (target.includes("?") ? "&" : "?") + "ff=0&ft=0&t=0"); } } function syncPanelDebugLayout() { const panel = document.getElementById("hy-cme-auto-panel"); if (!panel) return; panel.classList.toggle("hy-api-debug", getApiMode()); } function injectHyPanelStyles() { const id = "hy-cme-panel-style-v9"; document.getElementById("hy-cme-panel-style-v4")?.remove(); document.getElementById("hy-cme-panel-style-v5")?.remove(); document.getElementById("hy-cme-panel-style-v6")?.remove(); document.getElementById("hy-cme-panel-style-v7")?.remove(); document.getElementById("hy-cme-panel-style-v8")?.remove(); if (document.getElementById(id) || !document.head) return; const st = document.createElement("style"); st.id = id; st.textContent = ` #hy-cme-auto-panel{position:fixed;right:20px;top:80px;z-index:2147483647;width:412px;max-width:calc(100vw - 24px);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;box-sizing:border-box;} #hy-cme-auto-panel *,#hy-cme-auto-panel *::before,#hy-cme-auto-panel *::after{box-sizing:border-box;} #hy-cme-auto-panel.cme-panel-max{max-height:min(92vh,780px);} #hy-cme-auto-panel.cme-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);} #hy-cme-auto-panel.cme-panel-min #hy-cme-panel-header{border-bottom:none;} #hy-cme-auto-panel.cme-panel-min #hy-cme-panel-body,#hy-cme-auto-panel.cme-panel-min #hy-cme-panel-actions,#hy-cme-auto-panel.cme-panel-min #hy-cme-panel-footer,#hy-cme-auto-panel.cme-panel-min .cme-footer-extra{display:none !important;} #hy-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;} #hy-cme-panel-body{flex:1 1 auto;min-height:0;overflow:hidden;padding:8px 8px 4px;display:flex;flex-direction:column;gap:6px;} #hy-cme-panel-actions{flex:0 0 auto;margin:0 8px 6px;} .cme-footer-extra{flex:0 0 auto;} #hy-cme-panel-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;} #hy-cme-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #hy-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;} #hy-cme-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;display:flex;align-items:flex-start;gap:5px;flex-wrap:wrap;} .cme-panel-title-text{flex:1 1 12em;min-width:0;letter-spacing:-0.01em;} #hy-cme-panel-sub{display:flex;flex-wrap:wrap;align-items:center;gap:3px 5px;margin-top:3px;line-height:1.32;} .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;font-weight:400;} .cme-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;} #hy-cme-panel-controls{display:flex;align-items:center;gap:5px;flex:0 0 auto;} .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{width:10px;height:2px;background:#64748b;border-radius:1px;display:block;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:0;} .cme-card-status{padding:6px 8px;flex:0 0 auto;} .cme-card-tabs{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden;} .cme-card-tabs .cme-tabbar{flex:0 0 auto;} .cme-card-tabs .cme-pane.active{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden;} .cme-card-tabs .cme-list-head{flex:0 0 auto;} .cme-card-tabs #hy-cme-select-all-bar{flex:0 0 auto;} .cme-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;line-height:1.28;} .cme-status-label{font-size:11px;color:#64748b;font-weight:700;} #hy-cme-auto-status{padding:2px 7px;border-radius:999px;font-weight:800;font-size:11px;background:#fff;border:1px solid #cbd5e1;} .cme-status-main{margin-top:3px;} .cme-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;} .cme-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .cme-metric-div{color:#cbd5e1;} .cme-card-status .cme-progress-pct{font-size:14px;font-weight:900;color:#0369a1;flex:0 0 auto;} .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;} #hy-cme-auto-panel .cme-pane{display:none;} #hy-cme-auto-panel .cme-card-tabs .cme-pane.active{display:flex;} .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-list-head-actions{display:flex;align-items:center;gap:5px;} .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;line-height:1.4;} .cme-log-clear-btn:hover{background:#f8fafc;color:#0f172a;} #hy-cme-select-all-bar{margin-bottom:6px;} .hy-course-select-all{display:flex;align-items:center;gap:7px;padding:2px 4px 6px;font-size:12px;font-weight:700;color:#475569;cursor:pointer;user-select:none;} .hy-course-selected-count{margin-left:auto;font-size:11px;color:#64748b;font-weight:600;} .hy-cme-cb-wrap{position:relative;display:inline-flex;flex:0 0 16px;width:16px;height:16px;margin-top:2px;} .hy-cme-cb-wrap .hy-cme-course-cb{position:absolute;inset:0;width:16px;height:16px;margin:0;opacity:0;cursor:pointer;z-index:2;} .hy-cme-cb-box{width:16px;height:16px;border:2px solid #94a3b8;border-radius:4px;background:#fff;display:flex;align-items:center;justify-content:center;pointer-events:none;box-sizing:border-box;} .hy-cme-cb-wrap .hy-cme-course-cb:checked+.hy-cme-cb-box{background:#2563eb;border-color:#2563eb;} .hy-cme-cb-wrap .hy-cme-course-cb:checked+.hy-cme-cb-box::after{content:"✓";color:#fff;font-size:11px;font-weight:900;line-height:1;} .hy-cme-cb-wrap .hy-cme-course-cb:indeterminate+.hy-cme-cb-box{background:#2563eb;border-color:#2563eb;} .hy-cme-cb-wrap .hy-cme-course-cb:indeterminate+.hy-cme-cb-box::after{content:"−";color:#fff;font-size:13px;font-weight:900;line-height:1;} #hy-cme-auto-panel .cme-settings-check input[type=checkbox]{all:revert!important;appearance:checkbox!important;-webkit-appearance:checkbox!important;width:14px!important;height:14px!important;min-width:14px!important;opacity:1!important;visibility:visible!important;display:inline-block!important;position:static!important;margin:0!important;cursor:pointer!important;accent-color:#2563eb;} #hy-cme-course-list,#hy-cme-chapter-preview,#hy-cme-run-log{flex:1 1 auto;min-height:80px;max-height:none;overflow-y:auto;overflow-x:hidden;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} #hy-cme-auto-panel.hy-api-debug #hy-cme-run-log{max-height:min(280px,36vh);} #hy-cme-course-list label{max-width:100%;} #hy-cme-course-list label span{display:block;max-width:100%;overflow-wrap:anywhere;word-break:break-word;} .cme-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;} .cme-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;} .cme-chapter-title{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;} .cme-chapter-item{padding:5px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;} .cme-dot{font-weight:700;} .cme-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;} .cme-settings-group{background:#f8fafc;border:1px solid #dbe4f0;border-radius:12px;padding:8px;margin-bottom:8px;} .cme-settings-title{font-size:12px;color:#64748b;font-weight:900;margin-bottom:7px;} .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-card-current{padding:5px 8px;margin-bottom:6px;} .cme-card-current .cme-meta-row{font-size:11px;margin-bottom:2px;} .cme-card-current .cme-meta-value{word-break:break-all;line-height:1.28;} .cme-card-actions{padding:6px 8px;margin-bottom:0;width:100%;} #hy-cme-auto-panel .cme-btn-row{display:flex !important;flex-direction:row !important;align-items:stretch;gap:7px;margin-bottom:0;width:100%;} .cme-btn-row-nowrap{flex-wrap:nowrap !important;} #hy-cme-auto-panel button.cme-btn::before,#hy-cme-auto-panel button.cme-btn::after{content:none !important;display:none !important;} #hy-cme-auto-panel .cme-btn{position:static !important;display:inline-flex !important;align-items:center;justify-content:center;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;min-width:0 !important;flex:1 1 0 !important;width:auto !important;height:auto !important;float:none !important;} #hy-cme-auto-panel .cme-btn-ico{margin-right:4px;font-size:12px;line-height:1;} .cme-log-status{display:block;font-size:11px;line-height:1.42;color:#334155;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:6px 8px;margin-bottom:5px;word-break:break-word;} .cme-study-mode-hint-collapsed .cme-study-mode-hint-body{display:none;} .cme-study-mode-hint-collapsed{padding:5px 9px;} .cme-study-mode-hint-title{cursor:pointer;} .cme-btn-start{background:#16a34a;}.cme-btn-stop{background:#ef4444;}.cme-btn-clear{background:#d97706;} .cme-btn-start.cme-btn-off,.cme-btn-start:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;pointer-events:none;} .cme-btn-stop.cme-btn-off,.cme-btn-stop:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;pointer-events:none;} .cme-btn-action{min-width:0 !important;flex:1 1 0 !important;} .cme-btn-ico{font-size:13px;margin-right:5px;} .cme-plan-select,.cme-settings-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 9px;font-size:12px;background:#fff;color:#0f172a;} .cme-settings-check{display:flex;align-items:center;gap:6px;margin:5px 0;font-size:12px;} .cme-exam-hint{font-size:11px;color:#64748b;margin-top:4px;line-height:1.42;} .cme-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;opacity:.95;} .cme-ann-title{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:900;color:#9a3412;margin-bottom:3px;} .cme-ann-text{display:block;word-break:break-word;} .cme-qq-row{display:flex;align-items:center;justify-content:space-between;gap:9px;margin-top:5px;} .cme-qq-text{font-size:12px;color:#475569;} .cme-qq-title{font-size:13px;font-weight:800;color:#0f172a;} .cme-qq-btn{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:5px 12px;border-radius:999px;font-size:12px;font-weight:800;cursor:pointer;} .cme-start-hint{font-size:11px;color:#b45309;background:#fffbeb;border:1px solid #fcd34d;border-radius:7px;padding:5px 7px;margin-bottom:5px;line-height:1.32;text-align:center;font-weight:600;} .cme-ann{position:relative;font-size:12px;color:#334155;line-height:1.42;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:11px;padding:8px 9px 8px 14px;} .cme-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-top-left-radius:11px;border-bottom-left-radius:11px;} .cme-clock-row{font-size:11px;color:#475569;margin-top:4px;display:flex;justify-content:space-between;gap:6px;} .cme-clock-row span:last-child{font-weight:700;color:#0369a1;text-align:right;} .cme-mode-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:8px;padding-top:8px;border-top:1px dashed #e2e8f0;} .cme-mode-label{font-size:11px;color:#64748b;font-weight:700;white-space:nowrap;} .cme-mode-select{flex:1;min-width:0;border:1px solid #cbd5e1;border-radius:8px;padding:4px 6px;font-size:11px;background:#fff;color:#0f172a;} .cme-plan-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 9px;font-size:12px;background:#fff;color:#0f172a;} .cme-study-mode-block{margin-bottom:4px;width:100%;} .cme-study-mode-block .cme-study-mode-row{display:flex;align-items:center;gap:5px;margin-bottom:0;width:100%;} .cme-study-mode-hint{margin-top:5px;padding:7px 9px;border-radius:9px;border:1px solid #e2e8f0;font-size:11px;line-height:1.42;overflow-wrap:anywhere;word-break:break-word;width:100%;} .cme-study-mode-hint-title{display:block;font-weight:800;margin-bottom:3px;} .cme-study-mode-hint-body{margin:0;color:#475569;} .cme-study-mode-hint-realtime{background:linear-gradient(135deg,#fffbeb,#fef3c7);border-color:#f59e0b;color:#78350f;} .cme-study-mode-hint-realtime .cme-study-mode-hint-body{color:#92400e;} .cme-study-mode-hint-offline{background:linear-gradient(135deg,#eff6ff,#f0f9ff);border-color:#bfdbfe;color:#1e3a8a;} .cme-study-mode-hint-offline .cme-study-mode-hint-body{color:#334155;} .cme-offline-batch{margin-top:5px;padding:7px 9px;border-radius:8px;background:#eff6ff;border:1px solid #bfdbfe;font-size:11px;line-height:1.5;color:#1e40af;} #hy-cme-exam-code-modal{position:fixed;inset:0;z-index:2147483647;background:rgba(15,23,42,.52);display:flex;align-items:center;justify-content:center;padding:16px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;} #hy-cme-exam-code-dialog{background:#fff;border-radius:16px;box-shadow:0 24px 60px rgba(15,23,42,.28);padding:18px 20px 16px;width:min(360px,92vw);border:1px solid #dbe4f0;} .hy-cap-title{font-size:14px;font-weight:800;color:#0f172a;line-height:1.35;margin-bottom:4px;} .hy-cap-sub{font-size:12px;color:#64748b;margin-bottom:12px;} .hy-cap-imgbox{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding:10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;} #hy-cme-cap-img{display:block;height:40px;min-width:100px;max-width:200px;object-fit:contain;background:#fff;border:1px solid #dbe4f0;border-radius:6px;padding:2px 6px;flex:1;} .hy-cap-refresh{flex:0 0 auto;border:1px solid #cbd5e1;background:#fff;color:#334155;border-radius:8px;padding:6px 10px;font-size:12px;font-weight:700;cursor:pointer;} .hy-cap-refresh:hover{background:#f1f5f9;} #hy-cme-cap-input{width:100%;box-sizing:border-box;border:1px solid #cbd5e1;border-radius:10px;padding:10px 12px;font-size:18px;letter-spacing:.12em;text-align:center;font-weight:700;margin-bottom:12px;} #hy-cme-cap-input:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px rgba(37,99,235,.15);} .hy-cap-actions{display:flex;gap:8px;justify-content:flex-end;} .hy-cap-btn{border:none;border-radius:10px;padding:8px 16px;font-size:13px;font-weight:800;cursor:pointer;} .hy-cap-btn-ghost{background:#f1f5f9;color:#475569;} .hy-cap-btn-primary{background:#188aae;color:#fff;} #hy-cme-settings-pane{padding:2px 0;overflow-y:auto;flex:1 1 auto;min-height:0;} .cme-cloud-token-block{margin-top:4px;padding-top:6px;border-top:1px dashed #e2e8f0;} .cme-cloud-token-label{display:block;font-size:12px;color:#64748b;margin-bottom:5px;} .cme-cloud-token-input{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 8px;font-size:12px;background:#fff;color:#0f172a;box-sizing:border-box;} .cme-cloud-token-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;align-items:center;} #hy-cme-cloud-save{flex:1 1 120px !important;min-width:120px !important;white-space:nowrap !important;padding:6px 10px !important;font-size:12px !important;color:#0f172a !important;background:#fff !important;border:1px solid #cbd5e1 !important;} #hy-cme-open-pro{flex:1 1 88px !important;min-width:88px !important;} .cme-btn-pro{flex:0 0 auto;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;border:none;box-shadow:0 3px 10px rgba(239,68,68,.22);padding:6px 10px;font-size:12px;border-radius:10px;cursor:pointer;font-weight:800;} .cme-btn-pro:hover{filter:brightness(1.06);} .cme-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;flex:0 0 auto;} #hy-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;} #hy-cme-confirm-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000002;display:none;align-items:center;justify-content:center;padding:20px;} .cme-confirm-card{width:min(92vw,340px);background:#fff;border:1px solid #dbe4f0;border-radius:14px;padding:14px 16px;box-shadow:0 16px 40px rgba(15,23,42,.18);} .cme-confirm-title{font-size:14px;font-weight:900;color:#0f172a;margin-bottom:8px;} .cme-confirm-body{font-size:12px;line-height:1.55;color:#475569;margin-bottom:14px;} .cme-confirm-actions{display:flex;justify-content:flex-end;gap:8px;} .cme-confirm-actions .cme-btn{min-width:72px;padding:6px 12px;font-size:12px;} .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:24px;font-weight:900;color:#0f172a;} .cme-pro-sub{font-size:13px;color:#64748b;margin-top:4px;line-height:1.45;} .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;color:#0f172a;} .cme-pro-sec p{margin:4px 0;font-size:13px;color:#334155;line-height:1.5;} .cme-pro-sec ul{margin:6px 0 0 18px;padding:0;} .cme-pro-sec li{margin:4px 0;font-size:13px;color:#334155;line-height:1.45;} .cme-pro-buy{display:flex;flex-direction:column;gap:10px;margin-top:8px;} .cme-pro-muted{color:#64748b;font-size:12px;line-height:1.5;} .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;align-self:flex-start;} .cme-pro-tip{margin-top:8px;background:#fef3c7;border:1px solid #fcd34d;border-radius:10px;padding:8px 10px;font-size:13px;color:#92400e;font-weight:700;line-height:1.45;} .cme-pro-tip-info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:600;} .cme-pro-actions{margin-top:14px;display:flex;justify-content:flex-end;} .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;} `; document.head.appendChild(st); } function openHyProBuyPage() { const url = String(cloudState.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL; try { GM_openInTab(url, { active: true, insert: true, setParent: true }); } catch (_) { window.open(url, "_blank", "noopener,noreferrer"); } } function openHyProModal() { const vm = document.getElementById("hy-cme-pro-modal"); if (vm) vm.style.display = "flex"; } function closeHyProModal() { const vm = document.getElementById("hy-cme-pro-modal"); if (vm) vm.style.display = "none"; } function createHyProModal() { const old = document.getElementById("hy-cme-pro-modal"); if (old) old.remove(); const modal = document.createElement("div"); modal.id = "hy-cme-pro-modal"; modal.innerHTML = `
\u5f00\u901a Pro
6\u670822\u65e520\u70b9\u5f00\u653e\u8d2d\u4e70

Pro \u6743\u76ca

  • \u89e3\u9664\u514d\u8d39\u4f53\u9a8c\u7ae0\u8282\u6b21\u6570\u9650\u5236
  • \u89e3\u9501\u79bb\u7ebf\u5f02\u6b65\u6279\u91cf\u6a21\u5f0f
  • \u652f\u6301\u5b66\u5b8c\u540e\u81ea\u52a8\u8003\u8bd5\u4ea4\u5377
  • Pro \u6709\u6548\u671f 30 \u5929\uff08\u6fc0\u6d3b\u540e\u8d77\u7b97\uff0c\u7ed1\u5b9a\u534e\u533b\u7f51\u8d26\u53f7\uff09
\u516c\u6d4b\u622a\u6b626\u670822\u65e520\u70b9\uff0c\u516c\u6d4b\u671f\u95f4 QQ \u7fa4\uff08${QQ_GROUP_NUMBER}\uff09\u514d\u8d39\u83b7\u53d6 Token\u3002

\u5f00\u901a\u65b9\u5f0f

\u516c\u6d4b\u671f\u95f4\uff1a\u52a0\u5165 QQ \u7fa4 ${QQ_GROUP_NUMBER}\uff0c\u8054\u7cfb\u7ba1\u7406\u5458\u514d\u8d39\u83b7\u53d6 Token

\u6b63\u5f0f\u8d2d\u4e70\uff1a6\u670822\u65e520\u70b9\u8d77\u524d\u5f80\u8d2d\u4e70\u9875\u81ea\u52a9\u83b7\u53d6 Token

\u8d2d\u5f97 Token \u540e\uff1a\u5728\u300c\u8bbe\u7f6e\u300d\u9875\u7c98\u8d34\uff0c\u70b9\u51fb\u300c\u4fdd\u5b58\u5e76\u6821\u9a8c\u300d\u3002
\u91cd\u8981\uff1a\u8bf7\u52ff\u968f\u610f\u6cc4\u9732 Token\uff0c\u907f\u514d\u8d26\u53f7\u88ab\u591a\u4eba\u5171\u7528\u5bfc\u81f4\u5931\u6548\u3002
`; document.body.appendChild(modal); modal.addEventListener("click", (e) => { if (e.target === modal) closeHyProModal(); }); modal.querySelector("#hy-cme-pro-close")?.addEventListener("click", closeHyProModal); const buyBtn = modal.querySelector("#hy-cme-pro-buy-link"); if (buyBtn) { const buyOpenAt = new Date("2026-06-22T20:00:00+08:00").getTime(); if (Date.now() >= buyOpenAt) { buyBtn.disabled = false; buyBtn.style.opacity = ""; buyBtn.style.cursor = ""; buyBtn.textContent = "\u524d\u5f80\u8d2d\u4e70\u9875\u5f00\u901a Pro"; buyBtn.addEventListener("click", openHyProBuyPage); } } } function buildPanel() { if (!shouldShowPanel()) { syncFollowerBanner(); return; } removeFollowerBanner(); if (document.getElementById("hy-cme-auto-panel")) return; injectHyPanelStyles(); const panel = document.createElement("div"); panel.id = "hy-cme-auto-panel"; panel.className = "cme-panel-max"; panel.innerHTML = `
\u534e\u533b\u7f51\u7ee7\u7eed\u6559\u80b2\u81ea\u52a8\u5b66\u4e60\u52a9\u624bv${VERSION}
\u81ea\u52a8\u5316\u5b8c\u6210·1:1\u6302\u673a·\u7701\u65f6\u7701\u5fc3
\u8fd0\u884c\u72b6\u6001 \u5df2\u505c\u6b62
\u89c6\u9891 0/0 · \u8003\u8bd5
0%
\u8bfe\u7a0b\u9009\u62e9
\u9879\u76ee\u6536\u85cf
\u6b63\u5728\u52a0\u8f7d\u6536\u85cf\u8bfe\u7a0b…
\u7ae0\u8282\u9884\u89c8\u5f53\u524d\u9875
\u8bf7\u5728\u7ae0\u8282\u5217\u8868\u9875\u67e5\u770b
\u8fd0\u884c\u65e5\u5fd7
\u8fdb\u5ea6
\u6388\u6743\u4e0e\u9009\u9879\u8bbe\u7f6e
\u7528\u6237\u7c7b\u578b\u672a\u6821\u9a8c
\u514d\u8d39\u4f53\u9a8c\uff082 \u8282\uff090/2
Token
1:1\u6302\u673a\u6a21\u5f0f

`; document.body.appendChild(panel); createHyProModal(); const savedPos = readPanelPos(); if (savedPos && savedPos.left != null && savedPos.top != null) { panel.style.right = "auto"; panel.style.left = `${savedPos.left}px`; panel.style.top = `${savedPos.top}px`; } else { panel.style.top = "80px"; panel.style.right = "20px"; } applyPanelCollapsed(panel, readPanelCollapsed()); enablePanelDrag(panel); syncPanelDebugLayout(); panel.querySelector("#hy-cme-btn-min")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); panel.querySelector("#hy-cme-btn-max")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); panel.querySelectorAll(".cme-tab-btn").forEach((btn) => { btn.addEventListener("click", () => switchPanelTab(btn.dataset.tab)); }); panel.querySelector("#hy-cme-clear-log")?.addEventListener("click", () => clearRunLog()); panel.querySelector("#hy-cme-refresh-courses")?.addEventListener("click", () => refreshPanelCourses()); bindCourseListEvents(); panel.querySelector("#hy-cme-open-pro")?.addEventListener("click", openHyProModal); panel.querySelector("#hy-cme-join-qq")?.addEventListener("click", openHyQqGroup); const modeHintTitle = panel.querySelector("#hy-cme-mode-desc .cme-study-mode-hint-title"); modeHintTitle?.addEventListener("click", () => { scheduleModeHintCollapse(); }); const modeSel = panel.querySelector("#hy-cme-run-mode"); modeSel?.addEventListener("change", async () => { const next = modeSel.value === "offline" ? "offline" : "realtime"; if (next === "offline" && !canUseOfflineMode()) { modeSel.value = getRunMode() === "offline" ? "realtime" : getRunMode(); pushUserLog("\u79bb\u7ebf\u6279\u91cf\u6a21\u5f0f\u4ec5 Pro \u53ef\u7528\uff0c\u514d\u8d39\u6863\u8bf7\u4f7f\u7528 1:1 \u6302\u673a"); openHyProModal(); syncRunModePanel(); return; } const prev = getRunMode(); const active = Object.values(loadJobs()).some((j) => j && j.phase !== "done"); if (active && next !== prev) { const ok = await askHyPanelConfirm({ title: "\u5207\u6362\u5b66\u4e60\u6a21\u5f0f", message: "\u5f53\u524d\u961f\u5217\u4e2d\u6709\u8fdb\u884c\u4e2d\u7684\u4efb\u52a1\uff0c\u5207\u6362\u6a21\u5f0f\u5efa\u8bae\u5148\u6e05\u7a7a\u961f\u5217\u3002\u662f\u5426\u7ee7\u7eed\u5207\u6362\uff1f", okText: "\u7ee7\u7eed\u5207\u6362", cancelText: "\u53d6\u6d88", }); if (!ok) { modeSel.value = prev; return; } } setRunMode(next); syncRunModePanel(); pushLog(`\u5df2\u5207\u6362\u4e3a\uff1a${formatRunModeLabel(next)}`); }); panel.querySelector("#hy-cme-cloud-save")?.addEventListener("click", async () => { const input = document.getElementById("hy-cme-cloud-token"); const btn = document.getElementById("hy-cme-cloud-save"); cloudState.cloudToken = String((input && input.value) || "").trim(); cloudState.cloudRevoked = false; localStorage.setItem(CLOUD_TOKEN_KEY, cloudState.cloudToken); cloudState.cloudLease = ""; cloudState.cloudLeaseExp = 0; if (btn) btn.disabled = true; try { if (!cloudState.cloudToken) { cloudState.cloudTier = "free"; cloudState.cloudRevoked = false; notifyUser("\u5df2\u6e05\u9664 Pro Token\uff0c\u4f7f\u7528\u514d\u8d39\u4f53\u9a8c"); clearPanelHint(); } else { await _vt(); await _cl(true); notifyUser(`Token \u6821\u9a8c\u901a\u8fc7 · ${formatCloudTierText(cloudState.cloudTier)}`); clearPanelHint(); } updatePanelCloudStatus(); } catch (e) { cloudState.cloudTier = "unknown"; cloudState.cloudRevoked = /revoked|invalid token|expired/i.test(String(e?.message || e)); cloudState.cloudLease = ""; cloudState.cloudLeaseExp = 0; writeCloudLeaseCache("", 0); notifyAuthError(e?.message || e); updatePanelCloudStatus(); } finally { if (btn) btn.disabled = false; } }); panel.querySelector("#hy-cme-start")?.addEventListener("click", async () => { if (getEnabled()) return; syncQueueFromCourseList(); let selected = pruneQueueToCourses().slice(); if (!selected.length) { const fallback = getQueue().slice(); if (fallback.length) { commitQueueSelection(fallback); selected = pruneQueueToCourses().slice(); } } if (!selected.length) { pushUserLog("\u8bf7\u5148\u52fe\u9009\u81f3\u5c11\u4e00\u95e8\u8bfe\u7a0b"); updatePanel(); return; } panelState.selectionLock = selected.slice(); setStudySessionQueue(selected); commitQueueSelection(selected); try { if (isOfflineMode() && !canUseOfflineMode()) { setRunMode("realtime"); syncRunModePanel(); panelState.selectionLock = null; setStudySessionQueue([]); pushUserLog("\u79bb\u7ebf\u6279\u91cf\u6a21\u5f0f\u4ec5 Pro \u53ef\u7528\uff0c\u514d\u8d39\u6863\u8bf7\u4f7f\u7528 1:1 \u6302\u673a"); openHyProModal(); updatePanel(); return; } if (!(await _cr({ forceLease: true }))) { panelState.selectionLock = null; setStudySessionQueue([]); updatePanel(); return; } restoreLockedQueueSelection(); prepareJobsForStudyStart(selected); claimLeaderTab(true); startLeaderHeartbeat(); panelState.studyStartedAt = Date.now(); setEnabled(true); ensurePanelDefaults(); clearPanelHint(); startPreviewAutoRefresh(); startGlobalTicker(); await registerChaptersFromCourses("untouched", panelState.selectionLock.slice()); restoreLockedQueueSelection(); pushUserLog(`\u5f00\u59cb\u5b66\u4e60 · ${formatSelectedProgressLine()}`); updatePanel(); if (getApiMode() && isLeaderTab()) setTimeout(() => runBgScheduler(), 300); } catch (e) { const em = String(e?.message || e); panelState.studyStartedAt = 0; if (getEnabled()) { stopAutoStudy(`\u5f00\u59cb\u5b66\u4e60\u5931\u8d25\uff1a${em.slice(0, 80)}`); } else { const q = captureQueueSnapshot(); panelState.selectionLock = null; setStudySessionQueue([]); if (q.length) commitQueueSelection(q); pushUserLog("\u5f00\u59cb\u5b66\u4e60\u5931\u8d25\uff1a" + em.slice(0, 80)); } pushLog("\u5f00\u59cb\u5b66\u4e60\u5931\u8d25\uff1a" + em); updatePanel(); } }); panel.querySelector("#hy-cme-stop")?.addEventListener("click", () => { if (!getEnabled()) return; stopAutoStudy("\u5df2\u6682\u505c"); }); panel.querySelector("#hy-cme-clear-queue")?.addEventListener("click", () => void clearJobQueue()); updatePanel(); syncRunModePanel(); try { const logVerKey = "hy_cme_user_log_v"; if (localStorage.getItem(logVerKey) !== VERSION) { clearRunLog(); localStorage.setItem(logVerKey, VERSION); } } catch (_) {} renderRunLog(); startGlobalTicker(); stopPanelUpdateTimer(); window.__hyPanelUpdateTimer = setInterval(() => { if (isLeaderTab()) updatePanel(); }, 1000); initPanelData(); if (getEnabled()) startPreviewAutoRefresh(); } async function bootPage(manual) { const type = pageType(); if (manual) pushLog("\u624b\u52a8\uff1a" + type); if (type === "hdbl") { if (!isHdInteractiveEnabled()) return; captureHdLaunchParamsEarly(); scheduleHdInteractiveBoot(); return; } if (type === "hd_result") { if (!isHdInteractiveEnabled()) return; const cwid = new URLSearchParams(location.search).get("businessCustomParams") || new URLSearchParams(location.search).get("cwid") || ""; if (isHdDetachedRunner()) { postHdIframeEvent("done", { cwid }); setTimeout(tryCloseHdRunnerWindow, 800); return; } if (cwid && completeInteractiveJobByCwid(cwid)) { pushLog(`[\u4e92\u52a8] ${cwid.slice(0, 8)} \u5df2\u5b8c\u6210\uff0c\u7ee7\u7eed\u961f\u5217`); } clearHdMetaCookie(); if (getEnabled() && getApiMode()) setTimeout(() => runBgScheduler(), 1500); return; } if (type === "exam_code") { if (getEnabled() && getAutoExam()) { const cwid = new URLSearchParams(location.search).get("cwid"); if (cwid) { const runCode = async () => { const tag = cwid.slice(0, 8); const html = document.documentElement ? document.documentElement.outerHTML : ""; const cap = await solveHyExamCodeChallenge(cwid, tag, html, location.href); if (cap.ok) { location.href = `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(cwid)}`; } else if (!cap.paused) { pushLog(`[${tag}] \u9a8c\u8bc1\u7801\u672a\u901a\u8fc7: ${cap.msg || "\u672a\u77e5"}`); } }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => setTimeout(runCode, 600)); } else { setTimeout(runCode, 600); } } } return; } if (type === "exam") { if (getEnabled() && getAutoExam()) { const cwid = new URLSearchParams(location.search).get("cwid"); if (cwid) { const runExam = async () => { const ctx = findJobCtxByCwid(cwid) || findPanelChapterCtxByCwid(cwid) || { cwid, cwrid: cwid, title: cwid.slice(0, 8) }; await afterVideoMaybeExam(ctx, ctx.title || cwid.slice(0, 8)); if (getApiMode()) setTimeout(() => runBgScheduler(), 1500); }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => setTimeout(runExam, 600)); } else { setTimeout(runExam, 600); } } } return; } if (type === "exam_result") { await handleExamResultPage(); if (getEnabled() && getApiMode()) setTimeout(() => runBgScheduler(), 1500); return; } if (type === "ware_hd") return runWareHdPage(); if (type === "play" && manual) await runPlayPage(); if (type === "ware") return runWareRedirect(); if (type === "course") { updateJobsPanel(); if (isHdInteractiveEnabled()) { installInteractiveChapterClickHook(); syncInteractiveJobsFromChapters(parseCourseChapterJobs()); } if (getEnabled() && getApiMode()) runBgScheduler(); return; } if (type === "collect") return runCollectPage(); } function init() { if (isHdInteractiveEnabled()) { captureHdLaunchParamsEarly(); } purgeInteractiveJobs(); if (isCmeHost() && isHdInteractiveEnabled()) installHdIframeBridge(); if (isHdDetachedRunner()) { postHdIframeEvent("ping", { stage: "init", host: location.hostname, path: location.pathname, }); } ensurePanelDefaults(); installLeaderTabSystem(); const boot = () => { prepareLeaderOnBoot(); buildPanel(); syncFollowerBanner(); bootPage(false); }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } } if (shouldRunScript()) { try { if (isHdInteractiveEnabled()) captureHdLaunchParamsEarly(); } catch (_) {} init(); } })();