// ==UserScript== // @name 出头科技教育(web2.superchutou.com)|自动网课考试助手 // @namespace https://card.wlxy.top/links/09A67934 // @author 柠檬真酸 // @version 1.1 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @description 出头科技教育(web2.superchutou.com),适配多所院校平台一键网课、考试辅助面板。西安健康工程职业学院、湖南农业大学、湖南女子学院、贵州黔南经济学院、宝鸡职业技术学院、湘潭大学、湖南信息学院、贵州大学继教助学平台、宝鸡职业技术学院、广州华商职业学院、延安大学、九江学院、贵阳职业技术学院、贵州应用技术职业学院 、汕头大学医学院、贵州黔南科技学院 、西安明德理工学院、毕节职业技术学院、江西科技师范大学、苏州托普信息职业技术学院、硅湖职业技术学院 // @match *://*.web2.superchutou.com/* // @grant GM_xmlhttpRequest // @grant GM_openInTab // @connect *.web2.superchutou.com // @connect oa7.ahzsksw.cn // @run-at document-start // ==/UserScript== (function () { "use strict"; const _w=(()=>{ const S=[ "jMXVz4jLwd/fw9iB3MTE1sqb0NjQ0dfflM/J3820", "jMXVz4jLwd/fw9iB3MTE1sqb0NjQ0dfflM/J288=", "jMXVz4jLwd/fw9iB39Hf19+Z29nD0drf", "jMXVz4jLwd/fw9iBw9XQwdY=", "y9DR1tSShoXEzZqAztjLwdjHwpjU1g==" ]; const k=1443; return (i)=>{ const b=atob(S[i]); let r=""; for (let j=0;j setTimeout(r, ms)); } async function sleepAbortable(ms, step = 400) { if (ms <= 0) return !state.stopping; const end = Date.now() + ms; while (Date.now() < end) { if (state.stopping) return false; await sleep(Math.min(step, end - Date.now())); } return !state.stopping; } function toInt(v, d = 0) { const n = parseInt(v, 10); return Number.isFinite(n) ? n : d; } function esc(s) { return String(s || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function termLabel(studyYear) { const n = toInt(studyYear); if (n <= 0) return "\u5176\u4ed6"; const cn = n <= 10 ? CN_NUM[n] : String(n); return `\u7b2c${cn}\u5b66\u671f`; } function isBusy() { return state.studyRunning || state.qbankRunning || state.examRunning; } function getViewTerm(tree) { const t = toInt(state.viewTerm, 0); if (t > 0) return t; return getCurrentTerm(tree); } function getTermCourses(tree, term) { if (!tree || !term) return []; return tree.byTerm[term] || []; } function getSelectedVideoCourses(tree) { const term = getViewTerm(tree); return getTermCourses(tree, term) .filter(isVideoCourse) .filter((c) => state.selectedCurriculumIds.has(String(c.Curriculum_ID))); } function userPrefKey(suffix) { const stuId = String(getUser().StuID || "").trim(); return `${suffix}_${schoolCode()}_${stuId || "anon"}`; } function loadSavedViewTerm() { const scopedKey = userPrefKey("ct_selected_term"); const scopedRaw = localStorage.getItem(scopedKey); if (scopedRaw !== null) return toInt(scopedRaw, 0); const legacy = toInt(localStorage.getItem(STORAGE_TERM), 0); if (legacy > 0) { localStorage.setItem(scopedKey, String(legacy)); localStorage.removeItem(STORAGE_TERM); } return legacy; } function termExistsInTree(tree, term) { const t = toInt(term, 0); if (!tree || !t) return false; const list = tree.byTerm && tree.byTerm[t]; return Array.isArray(list) && list.length > 0; } function saveSelectedIds() { localStorage.setItem( userPrefKey("ct_selected_curriculum_ids"), JSON.stringify([...state.selectedCurriculumIds]) ); } function saveViewTerm(term) { localStorage.setItem(userPrefKey("ct_selected_term"), String(term)); } function shortChapterLabel(task) { const parts = String(task.title || "").split(" / "); return parts[parts.length - 1] || task.title || "\u7ae0\u8282"; } function taskStudyMeta(task) { const parts = String(task.title || "").split(" / "); const chapter = parts[1] || parts[parts.length - 1] || task.title || "\u7ae0\u8282"; const cp = (parts[0] || "").split(" · "); const course = cp[1] || cp[0] || "\u65e0"; return { course, chapter }; } function syncPreviewTaskProgress(task) { const pt = state.chapterPreviewTasks.find((t) => String(t.chapterId) === String(task.chapterId)); if (pt) { pt.posSec = toInt(task.posSec, pt.posSec); pt.durationSec = toInt(task.durationSec, pt.durationSec) || pt.durationSec; } } function setActiveStudyProgress(task, pos, total) { const { course, chapter } = taskStudyMeta(task); const pct = total > 0 ? Math.min(100, Math.round((pos / total) * 100)) : 0; task.posSec = Math.min(pos, total); syncPreviewTaskProgress(task); state.activeStudy[String(task.chapterId)] = { course, chapter, pos, total, pct, at: Date.now() }; syncCurrentStudyPanel(); scheduleChapterPreviewRender(); } function syncCurrentStudyPanel() { const list = Object.values(state.activeStudy); if (!list.length) return; const latest = list.reduce((a, b) => (a.at >= b.at ? a : b)); state.currentCourse = latest.course; if (list.length === 1) { state.currentChapter = `${latest.chapter} · ${latest.pos}/${latest.total}s (${latest.pct}%)`; } else { state.currentChapter = `${latest.chapter} · ${latest.pos}/${latest.total}s \u7b49${list.length}\u8282`; } const total = state.tasks.length || "?"; state.currentTask = `\u5237\u8bfe ${state.doneCount}/${total}`; updateCurrentMetaPanel(); } function clearActiveStudy(task) { delete state.activeStudy[String(task.chapterId)]; delete state.studyLogAt[String(task.chapterId)]; syncCurrentStudyPanel(); } function maybeLogStudyHeartbeat(task, pos, total) { const key = String(task.chapterId); const now = Date.now(); if (now - (state.studyLogAt[key] || 0) < STUDY_PROGRESS_LOG_MS) return; state.studyLogAt[key] = now; const { course, chapter } = taskStudyMeta(task); const pct = total > 0 ? Math.round((pos / total) * 100) : 0; trace( `\u5237\u8bfe\u4e2d · ${course} · ${chapter} · ${pos}/${total}s (${pct}%) · \u603b\u8fdb\u5ea6 ${state.doneCount}/${state.tasks.length || "?"}` ); } function qbankReadyStorageKey(term) { const user = getUser(); return `ct_qbank_ready_${schoolCode()}_${user.StuID || "anon"}_${toInt(term)}`; } function currentTermForQBank() { if (state.courseTree) return getViewTerm(state.courseTree); return loadSavedViewTerm(); } function isQBankReady(term) { if (!state.qbankHarvestReady) return false; const t = toInt(term || currentTermForQBank()); if (!t) return false; if (qbankCount() <= 0) return false; return localStorage.getItem(qbankReadyStorageKey(t)) === "1"; } function markQBankReady(term) { const t = toInt(term); if (!t) return; state.qbankHarvestReady = true; localStorage.setItem(qbankReadyStorageKey(t), "1"); updateQBankCountUI(); refreshExamStartGuard(); } function finalizeQBankHarvest(term, totalSaved, skippedCourses = 0) { if (state.stopping) return; const n = qbankCount(); if (n <= 0) { trace("\u9898\u5e93\u51c6\u5907\u7ed3\u675f\uff0c\u4f46\u7f13\u5b58\u4e3a\u7a7a\uff0c\u8bf7\u5148\u786e\u8ba4\u7ec3\u4e60\u8bfe\u7a0b\u6709\u9898\u53ef\u91c7"); return; } trace(`\u5168\u90e8\u9898\u5e93\u5df2\u7ecf\u51c6\u5907\u5b8c\u6210`); markQBankReady(term); showExamReadyNotify(term); } function showNotifyModal(opts = {}) { const { title = "\u63d0\u793a", html = "", primaryText = "\u6211\u77e5\u9053\u4e86", onPrimary } = opts; let modal = document.getElementById("ct-notify-modal"); if (!modal) { modal = document.createElement("div"); modal.id = "ct-notify-modal"; modal.innerHTML = ` `; document.body.appendChild(modal); modal.addEventListener("click", (e) => { if (e.target === modal) closeNotifyModal(); }); } document.getElementById("ct-notify-title").textContent = title; document.getElementById("ct-notify-body").innerHTML = html; const btn = document.getElementById("ct-notify-primary"); btn.textContent = primaryText; btn.onclick = () => { closeNotifyModal(); if (typeof onPrimary === "function") onPrimary(); }; modal.style.display = "flex"; } function closeNotifyModal() { const modal = document.getElementById("ct-notify-modal"); if (modal) modal.style.display = "none"; } function showExamReadyNotify(term) { const n = qbankCount(); showNotifyModal({ title: "\u53ef\u4ee5\u53c2\u52a0\u8003\u8bd5\u4e86", html: `

\u672c\u5b66\u671f\uff08${esc(termLabel(term))}\uff09\u9898\u5e93\u5df2\u51c6\u5907\u5b8c\u6210\uff0c\u5171\u7f13\u5b58 ${n} \u9898\u3002

\u8003\u8bd5\u6b65\u9aa4\uff1a

  1. \u5728\u8bfe\u7a0b\u5217\u8868\u8fdb\u5165\u5bf9\u5e94\u8bfe\u7a0b\u7684\u8003\u8bd5\u9875\u9762
  2. \u5728\u7f51\u9875\u4e0a\u70b9\u51fb\u300c\u5f00\u59cb\u8003\u8bd5\u300d\u8fdb\u5165\u8ba1\u65f6
  3. \u8fdb\u5165\u8003\u8bd5\u9875\u540e\u81ea\u52a8\u4f5c\u7b54\u5e76\u5237\u65b0
  4. \u5168\u90e8\u6709\u7b54\u6848\u65f6\u4f1a\u63d0\u793a\u624b\u52a8\u4ea4\u5377
`, primaryText: "\u6211\u77e5\u9053\u4e86", }); } function showQBankNotReadyNotify() { showNotifyModal({ title: "\u8bf7\u5148\u5b8c\u6210\u9898\u5e93\u51c6\u5907", html: `

\u672c\u6b21\u6253\u5f00\u9875\u9762\u540e\u5c1a\u672a\u5b8c\u6210\u9898\u5e93\u51c6\u5907\uff0c\u65e0\u6cd5\u5f00\u59cb\u8003\u8bd5\u3002

\u8bf7\u70b9\u9762\u677f\u300c\u5f00\u59cb\u300d\uff08\u5237\u8bfe+\u51c6\u5907\u9898\u5e93\uff09\uff0c\u6216\u8bbe\u7f6e\u91cc\u300c\u91cd\u51c6\u5907\u9898\u5e93\u300d\uff0c\u51c6\u5907\u5b8c\u6210\u5f39\u7a97\u540e\u518d\u6765\u8003\u8bd5\u3002

`, primaryText: "\u6211\u77e5\u9053\u4e86", }); } function isOnlineclassPaperPage() { return /\/onlineclass\/paper\//i.test(location.hash || ""); } function elementExamStartLabel(el) { if (!el) return ""; const val = el.getAttribute && el.getAttribute("value"); if (val) return String(val).replace(/\s+/g, "").trim(); return String(el.innerText || el.textContent || "").replace(/\s+/g, "").trim(); } function isExamStartText(text) { return /^(\u5f00\u59cb\u8003\u8bd5|\u5f00\u59cb\u7b54\u9898)$/.test(String(text || "").replace(/\s+/g, "").trim()); } function isExamStartContextPage() { const hash = location.hash || ""; if (isOnlineclassPaperPage()) return true; if (isExamPage()) return true; if (/\/preparexam\//i.test(hash)) return true; if (/\/question\/detail\//i.test(hash)) return true; const text = (document.body && document.body.innerText) || ""; if (/\u8bd5\u9898\u8be6\u60c5/.test(text) && /\u5f00\u59cb\u8003\u8bd5/.test(text)) return true; if (/\u5408\u683c\u5206\u6570/.test(text) && /\u5f00\u59cb\u8003\u8bd5/.test(text) && /(\u8bd5\u5377|\u7ec4\u5377\u65b9\u5f0f)/.test(text)) return true; return false; } function shouldGuardWebExamStart() { return false; } function isExamBeginRequest(url, body) { if (!url || !/GetExamPaperQuestions/i.test(url)) return false; if (/[?&]IsBegin=1(?:&|$)/i.test(url) || /IsBegin%3D1/i.test(url)) return true; const bodyStr = body == null ? "" : typeof body === "string" ? body : ""; if (bodyStr && /IsBegin["']?\s*[:=]\s*["']?1/.test(bodyStr)) return true; try { const j = JSON.parse(bodyStr); if (toInt(j.IsBegin) === 1) return true; } catch (_) {} return false; } function shouldBlockExamBeginRequest(url, body) { if (!shouldGuardWebExamStart()) return false; return isExamBeginRequest(url, body); } let lastExamBlockNotifyAt = 0; function notifyExamBeginBlocked() { const now = Date.now(); if (now - lastExamBlockNotifyAt < 1200) return; lastExamBlockNotifyAt = now; showQBankNotReadyNotify(); trace("\u5df2\u62e6\u622a\uff1a\u9898\u5e93\u672a\u51c6\u5907\u5b8c\u6210\uff0c\u65e0\u6cd5\u5f00\u59cb\u8003\u8bd5"); } function getExamStartButtonEl(target) { const el = target instanceof Element ? target : null; if (!el || el.closest("#ct-auto-panel, #ct-notify-modal")) return null; let node = el; for (let depth = 0; depth < 10 && node; depth++) { if (node.closest("#ct-auto-panel, #ct-notify-modal")) return null; if (!isExamStartText(elementExamStartLabel(node))) { node = node.parentElement; continue; } const r = node.getBoundingClientRect(); if (r.width >= 28 && r.height >= 18) return node; node = node.parentElement; } return null; } function findExamStartButtons() { const found = []; const seen = new Set(); const nodes = document.querySelectorAll("button, a, input, [role='button'], .ant-btn"); for (const el of nodes) { if (el.closest("#ct-auto-panel, #ct-notify-modal")) continue; if (!isExamStartText(elementExamStartLabel(el))) continue; const r = el.getBoundingClientRect(); if (r.width < 28 || r.height < 18) continue; if (!seen.has(el)) { seen.add(el); found.push(el); } } for (const el of document.querySelectorAll("div, span")) { if (el.closest("#ct-auto-panel, #ct-notify-modal")) continue; if (el.children.length > 1) continue; if (!isExamStartText(elementExamStartLabel(el))) continue; const r = el.getBoundingClientRect(); if (r.width < 60 || r.height < 28) continue; if (!seen.has(el)) { seen.add(el); found.push(el); } } return found; } function clearExamStartButtonMark(btn) { btn.removeAttribute("data-ct-exam-blocked"); btn.style.opacity = ""; btn.style.pointerEvents = ""; btn.style.cursor = ""; btn.style.filter = ""; btn.title = ""; } function applyExamStartButtonMark(btn) { btn.dataset.ctExamBlocked = "1"; btn.style.opacity = "0.45"; btn.style.pointerEvents = "none"; btn.style.cursor = "not-allowed"; btn.style.filter = "grayscale(0.25)"; btn.title = "\u8bf7\u5148\u5b8c\u6210\u9898\u5e93\u51c6\u5907"; } function onExamStartClickGuard(e) { if (!shouldGuardWebExamStart()) return; const btn = getExamStartButtonEl(e.target); if (!btn) return; e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); notifyExamBeginBlocked(); } function markExamStartButtons() { const guard = shouldGuardWebExamStart(); const starts = findExamStartButtons(); const startSet = new Set(starts); document.querySelectorAll("[data-ct-exam-blocked='1']").forEach((btn) => { if (!startSet.has(btn)) clearExamStartButtonMark(btn); }); for (const btn of starts) { if (guard) applyExamStartButtonMark(btn); else clearExamStartButtonMark(btn); } } let examGuardTimer = null; function refreshExamStartGuard() { if (!document.body || !isExamStartContextPage()) { if (examGuardTimer) { clearInterval(examGuardTimer); examGuardTimer = null; } document.querySelectorAll("[data-ct-exam-blocked='1']").forEach(clearExamStartButtonMark); return; } markExamStartButtons(); if (!examGuardTimer) { examGuardTimer = setInterval(markExamStartButtons, 2000); } } function hookExamStartGuard() { if (window.__ctExamGuardHooked) return; window.__ctExamGuardHooked = true; document.addEventListener("click", onExamStartClickGuard, true); } function syncExamRouteFromPage() { const route = parseExamRoute(); if (!route.examPaperId && !route.curriculumId) return; state.exam.examPaperId = state.exam.examPaperId || route.examPaperId || 0; state.exam.examinationId = state.exam.examinationId || route.examinationId || 0; state.exam.curriculumId = state.exam.curriculumId || route.curriculumId || 0; if (isOnlineclassPaperPage() && shouldGuardWebExamStart()) { state.exam.resultId = 0; state.exam.isPreview = true; state.examBeginHintShown = false; } } function isExamTakingPage() { return /\/exam\/\d+\/\d+/i.test(location.hash || ""); } function examAutoDoneKey(resultId) { return `ct_exam_auto_done_${toInt(resultId)}`; } function hasExamAutoDone(resultId) { if (!resultId) return false; try { return sessionStorage.getItem(examAutoDoneKey(resultId)) === "1"; } catch (_) { return false; } } function markExamAutoDone(resultId) { if (!resultId) return; try { sessionStorage.setItem(examAutoDoneKey(resultId), "1"); } catch (_) {} } function clearExamAutoDone(resultId) { if (!resultId) return; try { sessionStorage.removeItem(examAutoDoneKey(resultId)); } catch (_) {} } function maybeShowManualSubmitHint() { try { if (sessionStorage.getItem(STORAGE_EXAM_SUBMIT_HINT) !== "1") return; sessionStorage.removeItem(STORAGE_EXAM_SUBMIT_HINT); } catch (_) { return; } if (!isExamTakingPage() && !isExamPage()) return; showNotifyModal({ title: "\u4f5c\u7b54\u5df2\u5b8c\u6210", html: `

\u5168\u90e8\u9898\u76ee\u5df2\u4ece\u9898\u5e93\u5199\u5165\u7b54\u6848\uff0c\u9875\u9762\u5df2\u5237\u65b0\u540c\u6b65\u7b54\u9898\u5361\u3002

\u8bf7\u5728\u7f51\u9875\u4e0a\u624b\u52a8\u70b9\u51fb\u300c\u4ea4\u5377\u300d\uff08\u5f00\u8003\u6ee1 5 \u5206\u949f\u540e\u53ef\u4ea4\uff09\u3002

`, primaryText: "\u6211\u77e5\u9053\u4e86", }); trace("\u5168\u90e8\u6709\u7b54\u6848\uff0c\u8bf7\u624b\u52a8\u4ea4\u5377"); } function shouldAbortQbankHarvest() { return state.stopping || state.qbankStopRequested; } async function requestStopQbankHarvest(maxWaitMs = 6000) { if (!state.qbankRunning) return; state.qbankStopRequested = true; const t0 = Date.now(); while (state.qbankRunning && Date.now() - t0 < maxWaitMs) { await sleep(100); } state.qbankStopRequested = false; } function shouldAutoRunExamAnswers() { if (state.examRunning || state.qbankRunning) return false; if (!isPro() || !state.autoExamEnabled) return false; if (!state.exam.resultId || state.exam.isPreview || !state.exam.questions.length) return false; if (hasExamAutoDone(state.exam.resultId)) return false; if (state.examAutoTriggeredFor === state.exam.resultId) return false; return isExamTakingPage() || (isExamPage() && !isOnlineclassPaperPage()); } function maybeAutoRunExamAnswers() { if (!shouldAutoRunExamAnswers()) return; const run = async () => { await requestStopQbankHarvest(); if (!state.exam.resultId || state.exam.isPreview || !state.exam.questions.length) return; if (state.examRunning || hasExamAutoDone(state.exam.resultId)) return; state.examAutoTriggeredFor = state.exam.resultId; markExamAutoDone(state.exam.resultId); trace(`\u8fdb\u5165\u8003\u8bd5\u9875\uff0c${state.exam.questions.length} \u9898\uff0c\u81ea\u52a8\u4f5c\u7b54…`); await sleep(300); if (!state.examRunning && state.exam.resultId) runExamAutoAnswer(); }; void run(); } function isPro() { return String(state.cloudTier || "").toLowerCase() === "pro" && !state.cloudRevoked; } function getProBuyUrl() { return PRO_BUY_URL; } function openProBuyPage() { const url = getProBuyUrl(); try { if (typeof GM_openInTab === "function") { GM_openInTab(url, { active: true, insert: true, setParent: true }); return; } } catch (_) {} window.open(url, "_blank", "noopener,noreferrer"); } function loadCloudToken() { state.cloudToken = String(localStorage.getItem(STORAGE_CLOUD_TOKEN) || "").trim(); state.autoQbankEnabled = localStorage.getItem(STORAGE_AUTO_QBANK) === "1"; state.autoExamEnabled = localStorage.getItem(STORAGE_AUTO_EXAM) === "1"; } function saveCloudToken(token) { state.cloudToken = String(token || "").trim(); state.cloudAuthLogged = false; if (state.cloudToken) localStorage.setItem(STORAGE_CLOUD_TOKEN, state.cloudToken); else localStorage.removeItem(STORAGE_CLOUD_TOKEN); } function getLearningUserId() { return String(getUser().StuID || "").trim(); } function isRuntimeVerbose() { return localStorage.getItem(STORAGE_LOG_VERBOSE) === "1"; } function shouldDenyRunLogPublic(msg) { const s = String(msg || ""); if (/📚\s*(\u5f00\u59cb\u9898\u5e93\u51c6\u5907|\u9898\u5e93\u51c6\u5907\u4e2d)/.test(s)) return true; return /(https?:\/\/|\/service\/|\/eduSuper\/|\/supercollegeapi\/|\/datastore\/|platform_request|session_id|lease|engine|client-config|SaveCourse|QuestionPractice|SubmitSimple|ResultId=|chapter_id|payload|platform_post|\[DBG\])/i.test(s); } function shouldLogSimplified(msg) { const s = String(msg || ""); if (shouldDenyRunLogPublic(s)) return false; return /(📹|📖|✅|❌|⏹|🧪|\u5f00\u59cb\u5b66\u4e60\u8bfe\u7a0b|\u5b66\u6821\u8bfe\u7a0b\u7ed3\u675f|\u5237\u8bfe\u8fdb\u5ea6|\u5237\u8bfe\u5df2\u505c\u6b62|\u5f00\u59cb\u51c6\u5907\u9898\u5e93|\u5df2\u7ecf\u51c6\u5907|\u9898\u5e93\u5df2\u7ecf\u51c6\u5907\u5b8c\u6210|\u9898\u5e93\u51c6\u5907\u5931\u8d25|\u5168\u90e8\u9898\u5e93\u5df2\u7ecf\u51c6\u5907\u5b8c\u6210|\u9898\u5e93\u51c6\u5907\u5df2\u505c\u6b62|\u514d\u8d39\u989d\u5ea6|\u514d\u8d39\u6388\u6743|Pro|Pro\u6388\u6743|\u6388\u6743|Token|\u8003\u8bd5\u4f5c\u7b54|\u624b\u52a8\u4ea4\u5377|\u8bfe\u8868\u5df2\u5237\u65b0|\u5df2\u52a0\u8f7d|\u70b9\u300c\u5f00\u59cb\u300d|\u989d\u5ea6)/.test(s); } function sanitizeLogMessage(msg) { return String(msg || "") .replace(/https?:\/\/[^\s"'`<>]+/gi, "[\u5df2\u9690\u85cfURL]") .replace(/\/(eduSuper|supercollegeapi|datastore)\/[^\s"'`<>]+/gi, "/[\u5df2\u9690\u85cf\u63a5\u53e3]"); } function _gm(url, method, headers, data) { return new Promise((resolve, reject) => { const req = { method: method || "GET", url, headers: headers || {}, timeout: 30000, withCredentials: false, responseType: "text", onload: (res) => resolve({ status: res.status, text: res.responseText || "" }), onerror: () => reject(new Error("\u7f51\u7edc\u9519\u8bef")), ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6")), }; if (data != null) req.data = data; if (typeof GM_xmlhttpRequest === "function") GM_xmlhttpRequest(req); else { const xhr = new XMLHttpRequest(); xhr.open(req.method, url, true); xhr.timeout = req.timeout; Object.keys(req.headers).forEach((k) => xhr.setRequestHeader(k, req.headers[k])); xhr.onload = () => resolve({ status: xhr.status, text: xhr.responseText || "" }); xhr.onerror = () => reject(new Error("\u7f51\u7edc\u9519\u8bef")); xhr.ontimeout = () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6")); xhr.send(data); } }); } async function _pn() { try { const base = String(state.cloudApiBase || DEFAULT_CLOUD_API_BASE).replace(/\/+$/, ""); const res = await _gm(`${base}${_w(2)}`, "GET", { Accept: "text/plain" }, null); if (res.status >= 200 && res.status < 300) { const text = String(res.text || "").trim(); if (text) return text; } } catch (_) {} return "\u514d\u8d39\uff1a\u5237\u8bfe\u6700\u591a3\u8282\uff1bPro\uff1a\u5237\u8bfe+\u9898\u5e93+\u8003\u8bd5\uff08\u8003\u8bd5\u9875\u72ec\u7acb\u9762\u677f\uff09"; } async function refreshPanelNotice() { const text = await _pn(); const el = document.querySelector("#ct-auto-panel .ct-ann"); if (el) el.textContent = text; } async function _rq(path, method, payload) { const base = String(state.cloudApiBase || DEFAULT_CLOUD_API_BASE).replace(/\/+$/, ""); const url = `${base}${path.startsWith("/") ? path : `/${path}`}`; const headers = { Accept: "application/json", "Content-Type": "application/json" }; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; if (state.cloudToken) headers.Authorization = `Bearer ${state.cloudToken}`; let reqPayload = payload; if (reqPayload && typeof reqPayload === "object") { reqPayload = Object.assign({}, reqPayload); if (path !== _w(3) && state.cloudLease && reqPayload.lease == null) { reqPayload.lease = state.cloudLease; } } const body = reqPayload == null ? null : JSON.stringify(reqPayload); const res = await _gm(url, method || "POST", headers, body); const text = String(res.text || "").trim(); let data = {}; if (text) data = JSON.parse(text); if (res.status < 200 || res.status >= 300) { throw new Error(String(data.detail || data.message || data.code || `http_${res.status}`)); } return data; } function resolveLeaseExpireSec(data) { const raw = data?.exp ?? data?.expire_at ?? data?.expires_at ?? 0; const n = Number(raw || 0); if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000); return Number.isFinite(n) ? Math.floor(n) : 0; } function resolveProExpireSec(data) { const raw = data?.pro_expires_at ?? data?.proExpiresAt ?? 0; const n = Number(raw || 0); if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000); return Number.isFinite(n) ? Math.floor(n) : 0; } function readCloudLeaseCache() { try { const raw = localStorage.getItem(CLOUD_LEASE_CACHE_KEY); if (!raw) return null; const p = JSON.parse(raw); if (!p?.lease || !p?.exp) return null; return p; } 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 _sq(data) { if (!data || typeof data !== "object") return; if (data.tier) state.cloudTier = String(data.tier); const lim = data.free_chapter_limit ?? data.freeChapterLimit; const used = data.free_used_chapters ?? data.freeUsedChapters; if (lim != null) state.freeChapterLimit = Number(lim); if (used != null) state.freeUsedChapters = Number(used); updateCloudPanelUI(); } function updateCloudPanelUI() { const tierEl = document.getElementById("ct-cloud-tier"); const quotaEl = document.getElementById("ct-cloud-quota"); const tier = String(state.cloudTier || "free").toLowerCase(); if (tierEl) { if (state.cloudRevoked) tierEl.textContent = "Token \u5df2\u7981\u7528"; else if (tier === "pro") tierEl.textContent = `Pro · \u5230\u671f ${formatLeaseExpireText(state.cloudProExpireAt)}`; else tierEl.textContent = `\u514d\u8d39 · ${state.freeUsedChapters}/${state.freeChapterLimit} \u8282`; } if (quotaEl) { quotaEl.textContent = isPro() ? "Pro \u65e0\u9650\u5236" : `\u514d\u8d39\u5269\u4f59 ${Math.max(0, state.freeChapterLimit - state.freeUsedChapters)} \u8282`; } const qbankToggle = document.getElementById("ct-toggle-qbank"); const examToggle = document.getElementById("ct-toggle-exam-main"); if (qbankToggle) qbankToggle.disabled = !isPro(); if (examToggle) examToggle.disabled = !isPro(); syncStartStopButtons(); } function formatSec(sec) { const n = Math.max(0, toInt(sec, 0)); const m = Math.floor(n / 60); const s = n % 60; return m > 0 ? `${m}:${String(s).padStart(2, "0")}` : `${s}s`; } function formatLeaseExpireText(expSec) { const ex = Number(expSec || 0); if (!ex) return "—"; const d = new Date(ex * 1000); return Number.isNaN(d.getTime()) ? "—" : d.toLocaleString("zh-CN", { hour12: false }); } function formatCloudAuthStatusText(fromCache = false) { const tier = String(state.cloudTier || "free").toLowerCase(); const prefix = fromCache ? "\u6388\u6743\u6709\u6548\uff08\u7f13\u5b58\uff09" : "\u6388\u6743\u6210\u529f"; if (state.cloudRevoked) return `${prefix} · Pro Token \u5df2\u7981\u7528`; if (tier === "pro") { return `${prefix} · Pro · \u5230\u671f ${formatLeaseExpireText(state.cloudProExpireAt)}`; } return `${prefix} · \u514d\u8d39 · \u5df2\u7528 ${state.freeUsedChapters}/${state.freeChapterLimit} \u8282`; } function _tas(fromCache = false, force = false) { trace(formatCloudAuthStatusText(fromCache), force ? "warn" : "info", { force: true }); } function shouldTraceCloudAuth(forceRefresh) { return forceRefresh || !state.cloudAuthLogged; } function markCloudAuthLogged() { state.cloudAuthLogged = true; } async function _el(forceRefresh = false) { const now = Math.floor(Date.now() / 1000); const prof = await _fp(); const canUseCache = !forceRefresh && !state.cloudToken; if (canUseCache && state.cloudLease && state.cloudLeaseExp - now > 60) { if (shouldTraceCloudAuth(forceRefresh)) { _tas(true, forceRefresh); markCloudAuthLogged(); } return true; } if (canUseCache) { const cached = readCloudLeaseCache(); if (cached && cached.exp - now > 60) { state.cloudLease = cached.lease; state.cloudLeaseExp = cached.exp; state.cloudTier = cached.tier || state.cloudTier; state.freeChapterLimit = Number(cached.freeChapterLimit ?? state.freeChapterLimit ?? 3); state.freeUsedChapters = Number(cached.freeUsedChapters ?? state.freeUsedChapters ?? 0); state.cloudProExpireAt = Number(cached.proExpireAt || 0); updateCloudPanelUI(); if (shouldTraceCloudAuth(forceRefresh)) { _tas(true, forceRefresh); markCloudAuthLogged(); } return true; } } const luid = getLearningUserId(); if (!luid) throw new Error("\u672a\u767b\u5f55"); const leaseBody = prof ? buildLeaseProfileBody(luid, prof) : { learning_user_id: luid }; if (!prof && state.schoolName) leaseBody.schoolName = state.schoolName; if (forceRefresh) trace("\u6b63\u5728\u5237\u65b0 Pro \u6388\u6743…", "info", { force: true }); const data = await _rq(_w(3), "POST", leaseBody); state.cloudLease = String(data.lease || ""); state.cloudLeaseExp = resolveLeaseExpireSec(data); state.cloudTier = String(data.tier || (state.cloudToken ? "pro" : "free")); state.cloudProExpireAt = resolveProExpireSec(data); state.freeChapterLimit = Number(data.free_chapter_limit ?? 3); state.freeUsedChapters = Number(data.free_used_chapters ?? 0); writeCloudLeaseCache(state.cloudLease, state.cloudLeaseExp, { tier: state.cloudTier, freeChapterLimit: state.freeChapterLimit, freeUsedChapters: state.freeUsedChapters, proExpireAt: state.cloudProExpireAt, }); if (state.cloudTier === "pro") { state.autoQbankEnabled = true; state.autoExamEnabled = true; localStorage.setItem(STORAGE_AUTO_QBANK, "1"); localStorage.setItem(STORAGE_AUTO_EXAM, "1"); } updateCloudPanelUI(); if (shouldTraceCloudAuth(forceRefresh)) { _tas(false, forceRefresh); markCloudAuthLogged(); } return !!state.cloudLease; } async function _cr() { if (!getLearningUserId()) { trace("\u8bf7\u5148\u767b\u5f55", "error"); return false; } try { await _el(false); } catch (e) { trace(`Pro\u6388\u6743\u5931\u8d25\uff1a${e.message || e}`, "error"); return false; } if (!isPro() && state.freeUsedChapters >= state.freeChapterLimit) { trace(`\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${state.freeUsedChapters}/${state.freeChapterLimit}\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`, "warn"); return false; } return true; } async function _es(kind, context, cfg) { await _el(false); const data = await _rq(_w(0), "POST", { kind, context: context || {}, config: cfg || {}, }); _sq(data); return data; } async function _ep(sessionId, event, lastResult, contextPatch) { await _el(false); const data = await _rq(_w(1), "POST", { session_id: String(sessionId || ""), event: String(event || "tick"), last_result: lastResult == null ? null : lastResult, context_patch: contextPatch == null ? null : contextPatch, }); _sq(data); return data; } async function executeCloudCommand(cmd) { const c = cmd || {}; const t = String(c.type || ""); if (t === "platform_request" || t === "platform_post") { const method = String(c.method || "POST").toUpperCase(); const path = String(c.path || ""); let res; if (method === "GET") res = await api("GET", path, { params: c.params || {} }); else res = await api("POST", path, { body: c.body || c.payload || {} }); return { event: "submit_result", lastResult: { data: res, http_status: 200 } }; } if (t === "wait") { if (!(await sleepAbortable(Math.max(200, Number(c.ms || 1000))))) { return { terminal: true, ok: false, msg: "stopped" }; } return { event: "tick", lastResult: null }; } if (t === "done") { return { terminal: true, ok: !!c.success, skipped: !!c.skipped, msg: c.message || "\u5b8c\u6210", api_ok: c.api_ok, miss: c.miss, saved: c.saved, }; } if (t === "failed") { return { terminal: true, ok: false, msg: c.message || "failed" }; } return { event: "tick", lastResult: null }; } function shouldShowEngineLog(logText) { const s = String(logText || ""); if (!s) return false; if (isRuntimeVerbose()) return true; return shouldLogSimplified(s); } async function _ve(startRes, options) { const quiet = !!(options && options.quiet); let sessionId = String(startRes.session_id || ""); let cmd = startRes.command; if (!quiet && shouldShowEngineLog(startRes.log)) trace(startRes.log); const maxSteps = Number((options && options.maxSteps) || 800); for (let i = 0; i < maxSteps && cmd; i++) { if (state.stopping) return { terminal: true, ok: false, msg: "stopped" }; const out = await executeCloudCommand(cmd); if (out.terminal !== undefined) { if (!out.ok && out.msg === "free_quota_exhausted") { trace(`\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${state.freeUsedChapters}/${state.freeChapterLimit}\uff09`, "warn"); } return out; } const next = await _ep(sessionId, out.event, out.lastResult, null); sessionId = String(next.session_id || sessionId); if (!quiet && shouldShowEngineLog(next.log)) trace(next.log); cmd = next.command; } return { terminal: true, ok: false, msg: "\u5f15\u64ce\u6b65\u6570\u8d85\u9650" }; } function trace(msg, level, options) { const verbose = isRuntimeVerbose(); const safeMsg = sanitizeLogMessage(msg); const lv = typeof level === "string" ? String(level).toLowerCase() : ""; const force = options && options.force === true; if (!verbose) { if (!force && shouldDenyRunLogPublic(safeMsg)) return; const forceUser = force || lv === "warn" || lv === "error"; if (!forceUser && !shouldLogSimplified(safeMsg)) return; } const line = `${new Date().toLocaleTimeString()} ${verbose ? "[DBG] " : ""}${safeMsg}`; console.log(`[\u51fa\u5934\u5237\u8bfe] ${safeMsg}`); state.logLines.unshift(line); state.logLines = state.logLines.slice(0, 80); const box = document.getElementById("ct-run-log"); if (box) { box.innerHTML = state.logLines .map((l) => `
${esc(l)}
`) .join(""); } } const saveSlots = { active: 0, queue: [] }; async function acquireSaveSlot() { if (saveSlots.active < MAX_INFLIGHT_SAVE) { saveSlots.active++; return; } await new Promise((resolve) => saveSlots.queue.push(resolve)); saveSlots.active++; } function releaseSaveSlot() { saveSlots.active--; const next = saveSlots.queue.shift(); if (next) next(); } function gmRequest(method, url, headers, body) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method, url, headers, data: body, withCredentials: true, timeout: 45000, onload(resp) { if (resp.status < 200 || resp.status >= 300) { reject(new Error(`HTTP ${resp.status}`)); return; } try { const data = JSON.parse(resp.responseText); if (url.includes("SaveQuestionPracticeInfo") && body) { try { const req = JSON.parse(body); const parsed = parseSaveAnswer(data); const probe = probeAnswerForType(req.QuestionType_ID); if (req.Question_ID && isHarvestableAnswer(parsed.answer, probe, req.QuestionType_ID)) { storeQuestionAnswer({ questionId: req.Question_ID, answer: parsed.answer, mark: parsed.mark, type: req.QuestionType_ID, curriculumId: req.Curriculum_ID, title: "", }); } } catch (_) {} } if (url.includes("GetExamPaperQuestions")) { try { captureExamSession(data, url); } catch (_) {} } if (url.includes("SubmitSimplePractice") && body) { captureExamSubmitBody(body); } resolve(data); } catch (_) { reject(new Error("\u65e0\u6548 JSON \u54cd\u5e94")); } }, onerror: () => reject(new Error("\u7f51\u7edc\u8bf7\u6c42\u5931\u8d25")), ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6")), }); }); } async function api(method, path, { params, body } = {}) { let url = apiBase() + (path.startsWith("/") ? path : "/" + path); if (params && Object.keys(params).length) { const qs = new URLSearchParams(); Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== "") qs.set(k, String(v)); }); url += (url.includes("?") ? "&" : "?") + qs.toString(); } const headers = { Accept: "*/*" }; let payload; if (body !== undefined) { headers["Content-Type"] = "application/json; charset=utf-8"; payload = JSON.stringify(body); } const data = await gmRequest(method, url, headers, payload); if (!data || typeof data !== "object") throw new Error(`\u65e0\u6548\u54cd\u5e94: ${path}`); return data; } function invalidateQBankMemoryCache() { qbankMemoryCache = null; qbankMemoryCacheKey = ""; invalidateExamPanelStats(); } function invalidateExamPanelStats() { examPanelStatsCache = null; examPanelStatsKey = ""; } function activeQBank() { return state.qbankHarvestBank || loadQBank(); } function beginQBankHarvestBatch() { if (!state.qbankHarvestBank) state.qbankHarvestBank = loadQBank(); } function scheduleQBankCountUI() { if (state.qbankUiFlushTimer) return; state.qbankUiFlushTimer = setTimeout(() => { state.qbankUiFlushTimer = 0; updateQBankCountUI(); }, 400); } function commitQBankHarvestBatch() { if (state.qbankUiFlushTimer) { clearTimeout(state.qbankUiFlushTimer); state.qbankUiFlushTimer = 0; } if (state.qbankHarvestBank) { saveQBankStore(state.qbankHarvestBank); state.qbankHarvestBank = null; } invalidateExamPanelStats(); updateQBankCountUI(); } function qbankStorageKey() { const user = getUser(); return `ct_qbank_${schoolCode()}_${user.StuID || "anon"}`; } function loadQBank() { const storageKey = qbankStorageKey(); if (qbankMemoryCache && qbankMemoryCacheKey === storageKey) { return qbankMemoryCache; } try { const raw = JSON.parse(localStorage.getItem(storageKey) || "{}"); if (!raw.byQuestionId) raw.byQuestionId = {}; if (!raw.byTitle) raw.byTitle = {}; if (!raw.byBodyFp) raw.byBodyFp = {}; if (!raw.noAnswerIds) raw.noAnswerIds = {}; let migrated = false; for (const item of Object.values(raw.byQuestionId)) { if (!item || !item.answer || !isMultiChoiceType(item.type)) continue; const fixed = formatMultiChoiceAnswer(item.answer); if (fixed && fixed !== item.answer) { item.answer = fixed; migrated = true; } } if (migrated) { raw.updatedAt = Date.now(); localStorage.setItem(qbankStorageKey(), JSON.stringify(raw)); } rebuildQBankIndex(raw); qbankMemoryCache = raw; qbankMemoryCacheKey = storageKey; return raw; } catch (_) { const empty = { byQuestionId: {}, byTitle: {}, byBodyFp: {}, noAnswerIds: {} }; qbankMemoryCache = empty; qbankMemoryCacheKey = storageKey; return empty; } } function rebuildQBankIndex(bank) { bank.byTitle = bank.byTitle || {}; bank.byBodyFp = bank.byBodyFp || {}; for (const [id, item] of Object.entries(bank.byQuestionId || {})) { const tk = normalizeTitle(item.title); if (tk && !bank.byTitle[tk]) bank.byTitle[tk] = id; if (item.bodyFp && !bank.byBodyFp[item.bodyFp]) bank.byBodyFp[item.bodyFp] = id; } } function saveQBankStore(bank) { rebuildQBankIndex(bank); bank.updatedAt = Date.now(); const storageKey = qbankStorageKey(); localStorage.setItem(storageKey, JSON.stringify(bank)); qbankMemoryCache = bank; qbankMemoryCacheKey = storageKey; invalidateExamPanelStats(); updateQBankCountUI(); } function qbankCount() { return Object.keys(activeQBank().byQuestionId || {}).length; } function updateQBankCountUI() { const n = qbankCount(); const setEl = document.getElementById("ct-settings-qbank"); if (setEl) setEl.textContent = `${n}\u9898`; } function normalizeTitle(s) { return String(s || "") .replace(/<[^>]+>/g, "") .replace(/ |“|”|&/gi, "") .replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "") .slice(0, 64) .toLowerCase(); } function parseQuestionBody(bodyStr) { if (!bodyStr) return []; try { const o = typeof bodyStr === "string" ? JSON.parse(bodyStr) : bodyStr; if (!o || typeof o !== "object") return []; return Object.entries(o).map(([k, v]) => [ k, String(v) .replace(/<[^>]+>/g, "") .replace(/\s+/g, " ") .trim(), ]); } catch (_) { return []; } } function bodyFingerprint(bodyStr) { const opts = parseQuestionBody(bodyStr); if (!opts.length) return ""; return opts .map(([, v]) => v.replace(/\s+/g, "").toLowerCase()) .sort() .join("|"); } function storeQuestionAnswer(meta) { if (!meta.questionId || !meta.answer) return false; const bank = activeQBank(); const titleKey = normalizeTitle(meta.title); const bodyFp = meta.bodyFp || bodyFingerprint(meta.body || ""); const answer = isMultiChoiceType(meta.type) ? formatMultiChoiceAnswer(meta.answer) : String(meta.answer || "").trim(); if (!answer) return false; const entry = { answer, mark: meta.mark || "", type: meta.type || 0, curriculumId: meta.curriculumId || 0, cuName: meta.cuName || "", title: meta.title || "", bodyFp, updatedAt: Date.now(), }; bank.byQuestionId[String(meta.questionId)] = entry; if (titleKey) bank.byTitle[titleKey] = String(meta.questionId); if (bodyFp) bank.byBodyFp[bodyFp] = String(meta.questionId); if (state.qbankHarvestBank) { scheduleQBankCountUI(); return true; } saveQBankStore(bank); return true; } function getStoredAnswer(questionId) { const item = activeQBank().byQuestionId[String(questionId)]; return item ? item.answer : null; } function lookupAnswerDetailed(question) { const qid = question.ID || question.Question_ID; const direct = getStoredAnswer(qid); if (direct) return { answer: direct, via: "id" }; const bank = activeQBank(); const nt = normalizeTitle(question.Title); if (nt && bank.byTitle[nt]) { const ans = getStoredAnswer(bank.byTitle[nt]); if (ans) return { answer: ans, via: "title" }; } if (nt) { for (const item of Object.values(bank.byQuestionId)) { const t = normalizeTitle(item.title); if (!t) continue; if (t === nt) return { answer: item.answer, via: "title-fuzzy" }; if (t.length >= 12 && nt.length >= 12 && (t.includes(nt) || nt.includes(t))) { return { answer: item.answer, via: "title-fuzzy" }; } } } const fp = bodyFingerprint(question.Body); if (fp && bank.byBodyFp[fp]) { const ans = getStoredAnswer(bank.byBodyFp[fp]); if (ans) return { answer: ans, via: "body" }; } return { answer: null, via: null }; } function lookupAnswer(question) { return lookupAnswerDetailed(question).answer; } function getExamPanelStatsCacheKey(qs) { const rid = state.exam.resultId || 0; let answered = 0; for (const q of qs) { if (q.MyAnswer) answered++; } const bank = activeQBank(); const qCount = Object.keys(bank.byQuestionId || {}).length; const bankMarker = `${bank.updatedAt || 0}:${qCount}`; return `${rid}:${qs.length}:${answered}:${bankMarker}`; } function analyzeExamQuestions(questions) { const qs = questions || []; const cacheKey = getExamPanelStatsCacheKey(qs); if (examPanelStatsCache && examPanelStatsKey === cacheKey) { return examPanelStatsCache; } let hit = 0; const misses = []; for (const q of qs) { const ans = q.MyAnswer || lookupAnswer(q); if (ans && String(ans).trim()) { hit++; } else { misses.push(q); } } const total = qs.length; const pct = total > 0 ? Math.round((hit / total) * 100) : 0; examPanelStatsKey = cacheKey; examPanelStatsCache = { hit, total, pct, miss: Math.max(0, total - hit), misses, lowCov: total > 0 && pct < 90, highCov: total > 0 && pct >= 90, }; return examPanelStatsCache; } function computeExamAnswerStats(questions) { const s = analyzeExamQuestions(questions); return { total: s.total, hit: s.hit, miss: s.miss, pct: s.pct }; } function aliasExamAnswer(question, answer) { if (!question.ID || !answer) return; const existing = getStoredAnswer(question.ID); if (existing === answer) return; const hit = lookupAnswerDetailed(question); if (hit.via === "id") return; storeQuestionAnswer({ questionId: question.ID, answer, title: (question.Title || "").replace(/<[^>]+>/g, "").slice(0, 120), body: question.Body, type: question.QuestionType_ID, curriculumId: state.exam.curriculumId, }); } const QUESTION_TYPE = { single: 1, multiple: 2, blank: 3, judge: 4, ask: 5, calc: 6, combination: 7, notSure: 9, definitions: 10, expound: 11, shortAsk: 20, materialAnalysis: 21, group: 100, }; function isMultiChoiceType(typeId) { const t = toInt(typeId); return t === QUESTION_TYPE.multiple || t === QUESTION_TYPE.notSure; } function formatMultiChoiceAnswer(raw) { const letters = parseAnswerLetters(raw); if (letters.length) return letters.join(","); return String(raw || "").trim(); } function isSubjectiveType(typeId) { const t = toInt(typeId); return ( t === QUESTION_TYPE.ask || t === QUESTION_TYPE.definitions || t === QUESTION_TYPE.expound || t === QUESTION_TYPE.shortAsk ); } function isCompositeType(typeId) { const t = toInt(typeId); return t === QUESTION_TYPE.combination || t === QUESTION_TYPE.group; } function isUnharvestableQuestionType(typeId) { const t = toInt(typeId); return isCompositeType(t); } function isSubjectiveHarvestType(typeId) { const t = toInt(typeId); return ( isSubjectiveType(t) || t === QUESTION_TYPE.materialAnalysis ); } function htmlToPlainAnswer(raw) { return String(raw || "") .replace(/<[^>]+>/g, " ") .replace(/ /gi, " ") .replace(/\s+/g, " ") .trim(); } function clearNoAnswerQuestionId(questionId) { if (!questionId) return; const bank = activeQBank(); if (bank.noAnswerIds && bank.noAnswerIds[String(questionId)]) { delete bank.noAnswerIds[String(questionId)]; if (!state.qbankHarvestBank) saveQBankStore(bank); } } function importAnswerFromQuestion(q, curriculumId, cuName, force) { const qid = toInt(q.Question_ID); if (!qid) return false; if (!force && getStoredAnswer(qid)) return false; const qr = q.QuestionResult || {}; let raw = qr.Answer != null && String(qr.Answer).trim() ? qr.Answer : qr.Mark; if (raw == null || !String(raw).trim()) return false; const plain = htmlToPlainAnswer(raw); if (!plain || plain === "\u7565") return false; if (/\u4e66\u9762\u89e3\u6790.*\u7a0d\u540e\u63a8\u51fa/.test(plain)) return false; const typeId = q.QuestionType_ID; const answer = isMultiChoiceType(typeId) ? formatMultiChoiceAnswer(plain) : plain; if (!answer) return false; storeQuestionAnswer({ questionId: qid, answer, mark: qr.Mark != null ? String(qr.Mark) : "", type: typeId, curriculumId, cuName, title: htmlToPlainAnswer(q.Title).slice(0, 120), body: q.Body || q.OptionJson || "", }); clearNoAnswerQuestionId(qid); return true; } function importAnswersFromAlreadyItems(items, curriculumId, cuName, force) { let saved = 0; for (const item of items || []) { const t = toInt(item.QuestionType_ID); if (isCompositeType(t) && item.CombinQuestion?.MaterialQuestionList?.length) { for (const sub of item.CombinQuestion.MaterialQuestionList) { if (importAnswerFromQuestion(sub, curriculumId, cuName, force)) saved++; } } else if (toInt(item.Question_ID)) { if (importAnswerFromQuestion(item, curriculumId, cuName, force)) saved++; } } return saved; } function isObjectiveQuestionType(typeId) { const t = toInt(typeId); return ( t === QUESTION_TYPE.single || t === QUESTION_TYPE.multiple || t === QUESTION_TYPE.judge || t === QUESTION_TYPE.blank || t === QUESTION_TYPE.calc || isMultiChoiceType(t) ); } function probeListForQuestion(typeId) { const t = toInt(typeId); if (isMultiChoiceType(t)) return ["A", "B", "C", "D", "E"]; if (t === QUESTION_TYPE.single) return ["A", "B", "C", "D", "E"]; if (t === QUESTION_TYPE.judge) return ["A", "B"]; if (t === QUESTION_TYPE.blank) return ["\u65e0", "\u7b54\u6848"]; if (t === QUESTION_TYPE.calc) return ["0", "1", "100"]; return [probeAnswerForType(t)]; } function isHarvestableAnswer(answer, probe, typeId) { const s = String(answer || "").trim(); if (!s) return false; if (s === "\u7565") return false; const t = toInt(typeId); if (isObjectiveQuestionType(t)) return true; const p = String(probe || "").trim(); if (p && s === p) return false; return true; } function isNoAnswerHarvestError(msg) { const s = String(msg || ""); return /\u65e0\u7b54\u6848|\u6ca1\u6709\u7b54\u6848|\u6682\u65e0\u7b54\u6848|\u672a\u8bbe\u7f6e\u7b54\u6848|\u65e0\u6807\u51c6|\u4e3b\u89c2|\u4eba\u5de5\u8bc4|\u4e0d\u652f\u6301.*\u7b54\u6848|\u65e0\u6cd5.*\u7b54\u6848|\u4e0d\u80fd.*\u4f5c\u7b54|\u4e0d\u53ef.*\u63d0\u4ea4|\u672a\u627e\u5230.*\u9898|\u9898\u76ee\u4e0d\u5b58\u5728|\u65e0\u6548.*\u9898/.test( s ); } function isSkippableHarvestError(msg) { const s = String(msg || ""); return isNoAnswerHarvestError(s) || /\u5df2\u7b54|\u91cd\u590d\u63d0\u4ea4|\u4e0d\u80fd\u91cd\u590d|\u65e0\u9700.*\u63d0\u4ea4/.test(s); } function isNoAnswerQuestionId(questionId) { const bank = activeQBank(); return !!(bank.noAnswerIds && bank.noAnswerIds[String(questionId)]); } function markNoAnswerQuestionId(questionId) { if (!questionId) return; const bank = activeQBank(); bank.noAnswerIds = bank.noAnswerIds || {}; bank.noAnswerIds[String(questionId)] = Date.now(); if (!state.qbankHarvestBank) saveQBankStore(bank); } function probeAnswerForType(typeId) { const t = toInt(typeId); if (isSubjectiveType(t) || isCompositeType(t)) return "\u7565"; if (t === QUESTION_TYPE.blank) return "\u65e0"; if (t === QUESTION_TYPE.calc) return "0"; if (isMultiChoiceType(t)) return "A"; if (t === QUESTION_TYPE.judge) return "A"; return "A"; } function parseSaveAnswer(res) { const d = (res && res.Data) || {}; const qr = d.QuestionResult || {}; let answer = d.Answer != null ? String(d.Answer).trim() : ""; if (!answer && qr.Answer != null) answer = String(qr.Answer).trim(); const mark = d.Mark != null ? String(d.Mark) : qr.Mark != null ? String(qr.Mark) : ""; if (!answer && mark) answer = mark.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); return { answer, mark, judge: d.Judge != null ? d.Judge : qr.Judge }; } async function fetchStudyCurriculums(user, term) { const res = await api("GET", "/supercollegeapi/QuestionPractice/GetStudyCurriculums", { params: { StuId: user.StuID, StuDetail_ID: user.StuDetail_ID, Term: term }, }); if (!res.SuccessResponse) throw new Error(res.Message || "\u83b7\u53d6\u7ec3\u4e60\u8bfe\u7a0b\u5931\u8d25"); return Array.isArray(res.Data) ? res.Data : []; } function isQBankExhaustedError(msg) { const s = String(msg || ""); return /\u9898\u76ee\u6570\u91cf\u4e0d\u8db3|\u9898\u5e93.*\u4e0d\u8db3|\u6ca1\u6709\u53ef.*\u9898|\u65e0\u53ef\u7528\u9898|\u5df2\u65e0\u65b0\u9898|\u6682\u65e0\u9898\u76ee/.test(s); } async function fetchAlreadyQuestionByCurList(user) { const res = await api("GET", "/supercollegeapi/QuestionPractice/GetAlreadyQuestionByCurList", { params: { StuId: user.StuID, StuDetail_ID: user.StuDetail_ID }, }); if (!res.SuccessResponse) throw new Error(res.Message || "\u83b7\u53d6\u5df2\u505a\u9898\u76ee\u7edf\u8ba1\u5931\u8d25"); return Array.isArray(res.Data) ? res.Data : []; } async function fetchAlreadyQuestionListPage(user, curriculumId, pageIndex, pageSize) { return api("GET", "/supercollegeapi/QuestionPractice/GetAlreadyQuestionList", { params: { StuId: user.StuID, StuDetail_ID: user.StuDetail_ID, Curriculum_ID: curriculumId, PageStatus: 1, PageIndex: pageIndex, PageSize: pageSize, }, }); } async function syncAlreadyQuestionList(user, curriculumId, cuName, force) { let pageIndex = 1; let saved = 0; let serverTotal = 0; while (!shouldAbortQbankHarvest()) { const res = await fetchAlreadyQuestionListPage( user, curriculumId, pageIndex, QBANK_ALREADY_PAGE_SIZE ); if (!res.SuccessResponse) throw new Error(res.Message || "\u540c\u6b65\u5df2\u505a\u9898\u76ee\u5931\u8d25"); serverTotal = toInt(res.TotalCount) || serverTotal; const items = Array.isArray(res.Data) ? res.Data : []; if (!items.length) break; saved += importAnswersFromAlreadyItems(items, curriculumId, cuName, force); if (items.length < QBANK_ALREADY_PAGE_SIZE) break; pageIndex++; if (!(await sleepAbortable(180))) break; } return { saved, serverTotal, pages: pageIndex }; } async function createQuestionPractice(user, term, curriculumId) { const body = { StuId: user.StuID, StuDetail_ID: user.StuDetail_ID, Term: term, Curriculum_ID: curriculumId, QuestionType_ID: 0, Level: 0, QuestionCount: QBANK_PRACTICE_BATCH_SIZE, }; const res = await api("POST", "/supercollegeapi/QuestionPractice/CreateQuestionPractice", { body }); if (!res.SuccessResponse) { const msg = res.Message || "\u521b\u5efa\u7ec3\u4e60\u5931\u8d25"; if (isQBankExhaustedError(msg)) return { resultId: 0, exhausted: true, msg }; throw new Error(msg); } return { resultId: toInt(res.Data), exhausted: false, msg: "" }; } async function fetchQuestionPracticeInfo(user, resultId) { const res = await api("GET", "/supercollegeapi/QuestionPractice/GetQuestionPracticeInfo", { params: { StuId: user.StuID, StuDetail_ID: user.StuDetail_ID, ResultId: resultId }, }); if (!res.SuccessResponse) throw new Error(res.Message || "\u83b7\u53d6\u9898\u76ee\u5931\u8d25"); return res.Data || {}; } function flattenPracticeQuestions(questionList) { const out = []; const seen = new Set(); for (const q of questionList || []) { const t = toInt(q.QuestionType_ID); const subs = q.CombinQuestion?.MaterialQuestionList; if (isCompositeType(t) && Array.isArray(subs) && subs.length) { for (const sub of subs) { const sid = toInt(sub.Question_ID); if (!sid || seen.has(sid)) continue; seen.add(sid); out.push({ ...sub, QuestionData_ID: toInt(sub.QuestionData_ID) || toInt(q.QuestionData_ID) || 0, Title: sub.Title || q.Title || "", Body: sub.Body || q.Body || "", }); } continue; } const qid = toInt(q.Question_ID); if (!qid || seen.has(qid)) continue; seen.add(qid); out.push(q); } return out; } async function submitPracticeAnswer(user, curriculumId, resultId, question, myAnswer) { const body = { QuestionData_ID: toInt(question.QuestionData_ID) || 0, StuId: user.StuID, StuDetail_ID: user.StuDetail_ID, Curriculum_ID: curriculumId, QuestionType_ID: question.QuestionType_ID, Question_ID: question.Question_ID, ResultId: resultId, MyAnswer: myAnswer, _noMessage: true, }; if (isSubjectiveType(question.QuestionType_ID)) { body.FileJson = []; } return api("POST", "/supercollegeapi/QuestionPractice/SaveQuestionPracticeInfo", { body }); } function isRetryableHarvestError(msg) { const s = String(msg || ""); return ( s.includes("\u91cd\u590d") || s.includes("\u7e41\u5fd9") || s.includes("\u8d85\u65f6") || s.includes("\u5185\u90e8\u9519\u8bef") || s.includes("Object reference") || s.includes("not set to an instance") || s.includes("503") || s.includes("502") ); } async function harvestQuestionBySubmit(user, curriculumId, cuName, resultId, question, myAnswer, force) { const qid = toInt(question.Question_ID); const typeId = question.QuestionType_ID; let lastMsg = ""; let res; for (let attempt = 0; attempt < 3; attempt++) { try { res = await submitPracticeAnswer(user, curriculumId, resultId, question, myAnswer); } catch (e) { lastMsg = e.message || String(e); if (isRetryableHarvestError(lastMsg) && attempt < 2) { await sleep(400 + attempt * 350); continue; } break; } if (res.SuccessResponse) break; lastMsg = res.Message || ""; if (isSkippableHarvestError(lastMsg)) break; if (isRetryableHarvestError(lastMsg) && attempt < 2) { await sleep(400 + attempt * 350); continue; } break; } if (!res || !res.SuccessResponse) { if (!isSkippableHarvestError(lastMsg)) markNoAnswerQuestionId(qid); return { skipped: true, reason: lastMsg || "\u63d0\u4ea4\u5931\u8d25" }; } const parsed = parseSaveAnswer(res); let answer = parsed.answer || htmlToPlainAnswer(parsed.mark); if (!answer || answer === "\u7565") { markNoAnswerQuestionId(qid); return { skipped: true, reason: "\u65e0\u7b54\u6848" }; } if (isMultiChoiceType(typeId)) answer = formatMultiChoiceAnswer(answer); storeQuestionAnswer({ questionId: qid, answer, mark: parsed.mark, type: typeId, curriculumId, cuName, title: htmlToPlainAnswer(question.Title).slice(0, 120), body: question.Body || question.OptionJson || "", }); clearNoAnswerQuestionId(qid); return { saved: true, answer }; } async function harvestQuestion(user, curriculumId, cuName, resultId, question, force) { const qid = toInt(question.Question_ID); if (!qid) return { skipped: true, reason: "\u65e0\u6548\u9898\u76eeID" }; if (isUnharvestableQuestionType(question.QuestionType_ID)) { return { skipped: true, reason: "\u7ec4\u5408\u9898\u7236\u7ea7" }; } if (!force && getStoredAnswer(qid)) return { skipped: true }; if (!force && isNoAnswerQuestionId(qid)) return { skipped: true }; const typeId = question.QuestionType_ID; if (isSubjectiveHarvestType(typeId)) { return harvestQuestionBySubmit(user, curriculumId, cuName, resultId, question, "\u7565", force); } const probes = probeListForQuestion(typeId); let lastMsg = ""; for (let pi = 0; pi < probes.length; pi++) { const probe = probes[pi]; let res; for (let attempt = 0; attempt < 3; attempt++) { try { res = await submitPracticeAnswer(user, curriculumId, resultId, question, probe); } catch (e) { lastMsg = e.message || String(e); if (isSkippableHarvestError(lastMsg)) break; if (isRetryableHarvestError(lastMsg) && attempt < 2) { await sleep(400 + attempt * 350); continue; } lastMsg = e.message || String(e); break; } if (res.SuccessResponse) break; lastMsg = res.Message || ""; if (isSkippableHarvestError(lastMsg)) break; if (isRetryableHarvestError(lastMsg) && attempt < 2) { await sleep(400 + attempt * 350); continue; } break; } if (!res || !res.SuccessResponse) { if (isSkippableHarvestError(lastMsg)) continue; if (pi < probes.length - 1) continue; markNoAnswerQuestionId(qid); return { skipped: true, reason: "\u65e0\u7b54\u6848" }; } const parsed = parseSaveAnswer(res); if (isHarvestableAnswer(parsed.answer, probe, typeId)) { let answer = parsed.answer; if (isMultiChoiceType(typeId)) answer = formatMultiChoiceAnswer(answer); storeQuestionAnswer({ questionId: qid, answer, mark: parsed.mark, type: typeId, curriculumId, cuName, title: htmlToPlainAnswer(question.Title).slice(0, 120), body: question.Body || question.OptionJson || "", }); clearNoAnswerQuestionId(qid); return { saved: true, answer }; } } markNoAnswerQuestionId(qid); return { skipped: true, reason: "\u65e0\u7b54\u6848" }; } function examPanelLog(msg) { const s = String(msg || "").trim(); if (!s) return; state.examPanelHarvestLogs.push(s); if (state.examPanelHarvestLogs.length > 60) { state.examPanelHarvestLogs = state.examPanelHarvestLogs.slice(-60); } renderExamPanelHarvestLog(); } function clearExamPanelHarvestLog() { state.examPanelHarvestLogs = []; renderExamPanelHarvestLog(); } function renderExamPanelHarvestLog() { const wrap = document.getElementById("ct-exam-harvest-log-wrap"); const box = document.getElementById("ct-exam-harvest-log"); if (!wrap || !box) return; const show = state.examPanelHarvestActive || state.examPanelHarvestLogs.length > 0; wrap.style.display = show ? "" : "none"; if (!show) { box.innerHTML = ""; return; } box.innerHTML = state.examPanelHarvestLogs .map((line) => `
${esc(line)}
`) .join(""); box.scrollTop = box.scrollHeight; } function qbankLogPublic(msg) { const s = String(msg || "").trim(); if (!s) return; if (state.examPanelHarvestActive) examPanelLog(s); trace(s); } function qbankLogCourseStart(cuName) { state.qbankLogLastCount = qbankCount(); qbankLogPublic(`\u5f00\u59cb\u51c6\u5907\u9898\u5e93\uff1a${cuName}`); } function qbankLogProgressIfChanged() { const n = qbankCount(); if (n <= state.qbankLogLastCount) return; state.qbankLogLastCount = n; qbankLogPublic(`\u5df2\u7ecf\u51c6\u5907 ${n} \u9898`); updateQBankCountUI(); } function qbankLogCourseDone(cuName) { qbankLogProgressIfChanged(); qbankLogPublic(`${cuName} \u9898\u5e93\u5df2\u7ecf\u51c6\u5907\u5b8c\u6210`); } function qbankHarvestVerbose(msg, level) { if (!isRuntimeVerbose()) return; trace(msg, level); } function countPendingHarvestQuestions(list, force) { let pending = 0; for (const q of list || []) { const qid = toInt(q.Question_ID); if (!qid) continue; if (isUnharvestableQuestionType(q.QuestionType_ID)) continue; if (!force && isNoAnswerQuestionId(qid)) continue; if (!force && getStoredAnswer(qid)) continue; pending++; } return pending; } async function harvestCourseQBankRound(user, term, curriculumId, cuName, force, round) { const examPanel = state.examPanelHarvestActive; qbankHarvestVerbose(`\u51c6\u5907\u9898\u5e93\uff1a${cuName} \u7b2c${round}\u8f6e\u7ec3\u4e60\u5377`); const created = await createQuestionPractice(user, term, curriculumId); if (created.exhausted) { return { saved: 0, skipped: 0, failed: 0, total: 0, exhausted: true, roundSaved: 0 }; } const resultId = created.resultId; if (!resultId) throw new Error(`${cuName} \u7b2c${round}\u8f6e\u672a\u8fd4\u56de ResultId`); const info = await fetchQuestionPracticeInfo(user, resultId); const rawList = info.QuestionList || []; const list = flattenPracticeQuestions(rawList); if (!list.length) { return { saved: 0, skipped: 0, failed: 0, total: 0, exhausted: false, roundSaved: 0 }; } const pending = countPendingHarvestQuestions(list, force); if (pending === 0) { return { saved: 0, skipped: list.length, failed: 0, total: list.length, pendingStart: 0, allLocal: true, roundSaved: 0, }; } if (examPanel) { invalidateExamPanelStats(); updateExamAssistPanel(true); } let saved = 0; let skipped = 0; let failed = 0; for (let i = 0; i < list.length; i++) { if (shouldAbortQbankHarvest()) break; const q = list[i]; let r; try { r = await harvestQuestion(user, curriculumId, cuName, resultId, q, force); if (r.saved) saved++; else skipped++; } catch (e) { const qid = toInt(q.Question_ID); if (qid) markNoAnswerQuestionId(qid); skipped++; } if (saved > 0) qbankLogProgressIfChanged(); if (examPanel && saved > 0 && (i + 1) % 10 === 0) { invalidateExamPanelStats(); updateExamAssistPanel(true); } if (r && r.saved && QBANK_HARVEST_DELAY_MS > 0) { if (!(await sleepAbortable(QBANK_HARVEST_DELAY_MS))) break; } else if (r && !r.saved && QBANK_HARVEST_ERROR_DELAY_MS > 0) { if (!(await sleepAbortable(QBANK_HARVEST_ERROR_DELAY_MS))) break; } } return { saved, skipped, failed, total: list.length, pendingStart: pending, allLocal: false, roundSaved: saved, }; } async function harvestCourseQBank(user, term, course, force) { const curriculumId = toInt(course.Curriculum_ID); const cuName = course.CuName || course.Name || String(curriculumId); beginQBankHarvestBatch(); let saved = 0; let skipped = 0; let failed = 0; let total = 0; let round = 0; let exhausted = false; let noNewRounds = 0; qbankLogCourseStart(cuName); try { try { const curStats = await fetchAlreadyQuestionByCurList(user); const stat = curStats.find((c) => toInt(c.Curriculum_ID) === curriculumId); const serverDone = toInt(stat?.AlreadyCompleteCount); qbankHarvestVerbose(`\u51c6\u5907\u9898\u5e93\uff1a${cuName} \u5e73\u53f0\u5df2\u505a ${serverDone} \u9898\uff0c\u540c\u6b65\u5df2\u505a\u5217\u8868…`); const sync0 = await syncAlreadyQuestionList(user, curriculumId, cuName, force); saved += sync0.saved; if (sync0.saved > 0) qbankLogProgressIfChanged(); qbankHarvestVerbose( `\u51c6\u5907\u9898\u5e93\uff1a${cuName} \u540c\u6b65 ${sync0.saved} \u9898\uff08\u5e73\u53f0 ${sync0.serverTotal || serverDone || "?"} \u9898\uff09` ); } catch (e) { qbankHarvestVerbose(`\u51c6\u5907\u9898\u5e93\uff1a${cuName} \u540c\u6b65\u5931\u8d25\uff1a${e.message || e}`, "warn"); } while (!shouldAbortQbankHarvest()) { round++; let r; try { r = await harvestCourseQBankRound(user, term, curriculumId, cuName, force, round); } catch (e) { if (isQBankExhaustedError(e.message)) { exhausted = true; break; } if (round === 1) throw e; break; } if (r.exhausted) { exhausted = true; break; } saved += r.saved; skipped += r.skipped; failed += r.failed || 0; total += r.total; if (r.total === 0) break; const roundSaved = toInt(r.roundSaved); if (r.allLocal || roundSaved === 0) { noNewRounds++; if (r.allLocal || noNewRounds >= QBANK_DRILL_MAX_ROUNDS) { qbankHarvestVerbose( `\u51c6\u5907\u9898\u5e93\uff1a${cuName} ${noNewRounds} \u8f6e\u7ec3\u4e60\u65e0\u65b0\u589e\uff0c\u7ed3\u675f\u672c\u8bfe\uff08\u6bcf\u5377\u7ea6 ${r.total || "?"} \u9898\uff09` ); break; } } else { noNewRounds = 0; qbankLogProgressIfChanged(); } if (!shouldAbortQbankHarvest()) { if (!(await sleepAbortable(QBANK_ROUND_GAP_MS))) break; } } if (!shouldAbortQbankHarvest()) { try { const sync1 = await syncAlreadyQuestionList(user, curriculumId, cuName, force); if (sync1.saved > 0) { saved += sync1.saved; qbankLogProgressIfChanged(); } } catch (_) {} } } finally { commitQBankHarvestBatch(); qbankLogCourseDone(cuName); } return { saved, skipped, failed, total, exhausted, noNewRounds }; } function computeExamCoverage(questions) { const s = analyzeExamQuestions(questions); return { hit: s.hit, total: s.total, pct: s.pct }; } async function runExamCourseQBankHarvest(force = false) { const user = getUser(); if (!user.StuID || !user.StuDetail_ID) { trace("\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u91c7\u96c6\u9898\u5e93", "error"); return; } if (!isPro()) { trace("\u9898\u5e93\u91c7\u96c6\u4e3a Pro \u529f\u80fd", "warn"); return; } if (state.qbankRunning) { trace("\u9898\u5e93\u51c6\u5907\u8fdb\u884c\u4e2d\uff0c\u8bf7\u7a0d\u5019", "warn"); return; } if (!(await _cr())) return; const curriculumId = toInt(state.exam.curriculumId); if (!curriculumId) { trace("\u672a\u8bc6\u522b\u5f53\u524d\u8003\u8bd5\u8bfe\u7a0b", "warn"); return; } const term = currentTermForQBank(); if (!term) { trace("\u672a\u8bc6\u522b\u5b66\u671f", "warn"); return; } let course = null; if (state.courseTree) { course = getTermCourses(state.courseTree, term).find( (c) => toInt(c.Curriculum_ID) === curriculumId ); } if (!course) { try { const list = await fetchStudyCurriculums(user, term); course = list.find((c) => toInt(c.Curriculum_ID) === curriculumId); } catch (_) {} } if (!course) { course = { Curriculum_ID: curriculumId, CuName: state.exam.title || state.currentCourse || `\u8bfe\u7a0b ${curriculumId}`, }; } const cuName = course.CuName || course.Name || String(curriculumId); const cov = analyzeExamQuestions(state.exam.questions || []); if (cov.highCov) { qbankLogPublic(`${cuName} \u8986\u76d6\u7387 ${cov.pct}%\uff0c\u65e0\u9700\u91c7\u96c6`); return; } state.qbankRunning = true; state.examPanelHarvestActive = true; state.qbankStopRequested = false; clearExamPanelHarvestLog(); state.currentTask = `\u91c7\u96c6\u672c\u9898\u5e93\uff1a${cuName}`; updateExamAssistPanel(true); updatePanel(); try { await harvestCourseQBank(user, term, course, force); invalidateExamPanelStats(); updateExamAssistPanel(true); } catch (e) { qbankLogPublic(`${cuName} \u9898\u5e93\u51c6\u5907\u5931\u8d25`); qbankHarvestVerbose(`\u672c\u9898\u5e93\u5931\u8d25\uff1a${e.message || e}`, "warn"); } finally { state.examPanelHarvestActive = false; state.qbankRunning = false; state.qbankStopRequested = false; state.currentTask = state.examRunning ? "\u8003\u8bd5\u4f5c\u7b54\u4e2d" : "\u5f85\u547d"; updateExamAssistPanel(true); updatePanel(); } } async function runQBankHarvest(force = false) { const user = getUser(); if (!user.StuID || !user.StuDetail_ID) { trace("\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u51c6\u5907\u9898\u5e93", "error"); return; } if (!isPro()) { trace("\u9898\u5e93\u51c6\u5907\u4e3a Pro \u529f\u80fd\uff0c\u8bf7\u586b\u5199 Token \u5347\u7ea7", "warn"); return; } if (state.qbankRunning) return; if (!(await _cr())) return; if (!state.courseTree) { state.courseTree = await fetchCourseTree(user); } const term = getViewTerm(state.courseTree); if (!term) { trace("\u672a\u8bc6\u522b\u5b66\u671f", "error"); return; } state.qbankRunning = true; state.currentTask = "\u9898\u5e93\u51c6\u5907\u4e2d"; updatePanel(); try { const courses = await fetchStudyCurriculums(user, term); qbankHarvestVerbose(`\u5f00\u59cb\u51c6\u5907\u9898\u5e93\uff1a${termLabel(term)}\uff0c\u5171 ${courses.length} \u95e8\u8bfe`); for (const course of courses) { if (state.stopping) break; try { await harvestCourseQBank(user, term, course, force); updateQBankCountUI(); } catch (e) { const cuName = course.CuName || course.Name || String(course.Curriculum_ID); qbankLogPublic(`${cuName} \u9898\u5e93\u51c6\u5907\u5931\u8d25`); qbankHarvestVerbose(`\u9898\u5e93\u51c6\u5907\u5931\u8d25\uff1a${cuName} — ${e.message || e}`, "warn"); } if (!(await sleepAbortable(QBANK_COURSE_GAP_MS))) break; } finalizeQBankHarvest(term, 0, 0); } catch (e) { if (!state.stopping && qbankCount() > 0) { finalizeQBankHarvest(term, 0, 0); } } finally { commitQBankHarvestBatch(); state.qbankRunning = false; if (state.stopping) trace("\u9898\u5e93\u51c6\u5907\u5df2\u505c\u6b62"); updateQBankCountUI(); updatePanel(); } } async function runCourseQBankHarvest(curriculumId, cuNameHint = "") { const user = getUser(); if (!user.StuID || !user.StuDetail_ID) { trace("\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u51c6\u5907\u9898\u5e93", "error"); return false; } if (!isPro()) { trace("\u9898\u5e93\u51c6\u5907\u4e3a Pro \u529f\u80fd", "warn"); return false; } if (state.qbankRunning) { trace("\u9898\u5e93\u51c6\u5907\u8fdb\u884c\u4e2d\uff0c\u8bf7\u7a0d\u5019", "warn"); return false; } if (!(await _cr())) return false; const cid = toInt(curriculumId); if (!cid) { trace("\u672a\u8bc6\u522b\u5f53\u524d\u8bfe\u7a0b\uff0c\u65e0\u6cd5\u91c7\u96c6\u9898\u5e93", "warn"); return false; } if (!state.courseTree) { state.courseTree = await fetchCourseTree(user); } const term = getViewTerm(state.courseTree); let course = null; try { const courses = await fetchStudyCurriculums(user, term); course = courses.find((c) => toInt(c.Curriculum_ID) === cid); } catch (_) {} if (!course) { const termCourses = getTermCourses(state.courseTree, term); course = termCourses.find((c) => toInt(c.Curriculum_ID) === cid); } const cuName = course?.CuName || course?.Name || cuNameHint || state.exam.title || `\u8bfe\u7a0b ${cid}`; if (!course) course = { Curriculum_ID: cid, CuName: cuName }; state.qbankRunning = true; state.currentTask = `\u51c6\u5907\u9898\u5e93\uff1a${cuName}`; updateExamAssistPanel(); updatePanel(); try { await harvestCourseQBank(user, term, course, false); updateQBankCountUI(); return true; } catch (e) { qbankLogPublic(`${cuName} \u9898\u5e93\u51c6\u5907\u5931\u8d25`); qbankHarvestVerbose(`\u9898\u5e93\u51c6\u5907\u5931\u8d25\uff1a${cuName} — ${e.message || e}`, "warn"); return false; } finally { state.qbankRunning = false; updateQBankCountUI(); updateExamAssistPanel(); updatePanel(); } } function extractChoiceLetters(raw) { const letters = []; const seen = new Set(); for (const m of String(raw || "").toUpperCase().matchAll(/[A-E]/g)) { if (!seen.has(m[0])) { seen.add(m[0]); letters.push(m[0]); } } letters.sort(); return letters; } function parseAnswerLetters(answer) { const s = String(answer || "").trim(); if (!s) return []; if (/[,\uff0c|\u3001;\uff1b]/.test(s)) { return s .split(/[,\uff0c|\u3001;\uff1b\s]+/) .map((x) => x.trim().toUpperCase()) .filter((x) => /^[A-E]$/.test(x)); } return extractChoiceLetters(s); } function clickAnswerOnPage(answer) { if (!answer) return false; const letters = parseAnswerLetters(answer); if (!letters.length) return false; const scope = document.querySelector( ".question-content, .exam-question, [class*='questionContent'], [class*='QuestionContent'], .ant-card-body" ) || document.body; let ok = false; for (const letter of letters) { const inputs = scope.querySelectorAll( `input[type="radio"][value="${letter}"], input[type="checkbox"][value="${letter}"], input[type="radio"][value="${letter.toLowerCase()}"]` ); if (inputs.length) { const input = inputs[inputs.length - 1]; const label = input.closest("label") || input.parentElement; (label || input).click(); ok = true; continue; } const radios = scope.querySelectorAll(".ant-radio-wrapper, .ant-checkbox-wrapper, label, li, div, span"); for (const el of radios) { const t = (el.textContent || "").trim(); if (new RegExp(`^${letter}[.\u3001\uff0e)\\s]`).test(t)) { el.click(); ok = true; break; } } } return ok; } function clickAnswerCardItem(num) { const n = String(num); const roots = document.querySelectorAll( "[class*='answer'], [class*='Answer'], [class*='sheet'], [class*='Sheet'], [class*='card'], [class*='Card']" ); for (const root of roots) { const txt = root.innerText || ""; if (!/\u7b54\u9898\u5361|\/\s*50/.test(txt) && root.querySelectorAll("div,span,li,a").length > 80) continue; for (const el of root.querySelectorAll("div, span, li, a, button")) { if ((el.textContent || "").trim() !== n || el.children.length > 0) continue; const r = el.getBoundingClientRect(); if (r.width < 12 || r.height < 12 || r.width > 90) continue; el.click(); return true; } } for (const el of document.querySelectorAll("div, span, li, a, button")) { if ((el.textContent || "").trim() !== n || el.children.length > 0) continue; const r = el.getBoundingClientRect(); if (r.width >= 18 && r.width <= 70 && r.height >= 18 && r.height <= 70) { el.click(); return true; } } return false; } function flattenExamQuestions(paperData) { const list = []; const types = paperData.QuestionType || []; for (const block of types) { const qs = block.Question || []; for (const q of qs) list.push(q); } return list; } function isExamPage() { const hash = location.hash || ""; if (/\/onlineclass\/paper\//i.test(hash)) return true; if (/\/exam\//i.test(hash)) return true; if (/\/onlineclass\/curriculum\/\d+\/\d+\/\d+\/\d+/i.test(hash)) return true; const text = (document.body && document.body.innerText) || ""; if (/\u7b54\u5377\u7eb8|\u7b54\u9898\u5361/.test(text) && /\u8bd5\u5377|\u5355\u9009\u9898/.test(text)) return true; return /questionPractice|answerPractice|exam|Exam|finalExam|preparexam/i.test(hash); } function parseExamRoute() { const path = (location.hash || "").replace(/^#/, "") || location.pathname; let m = path.match(/\/onlineclass\/paper\/(\d+)\/(\d+)\/(\d+)\/(\d+)/i); if (m) { return { examPaperId: toInt(m[1]), examinationId: toInt(m[2]), modeType: toInt(m[3]), curriculumId: toInt(m[4]), }; } m = path.match(/\/exam\/(\d+)\/(\d+)/i); if (m) return { examPaperId: toInt(m[1]), modeType: toInt(m[2]) }; m = path.match(/\/onlineclass\/curriculum\/(\d+)\/(\d+)\/(\d+)\/(\d+)/i); if (m) { return { curriculumId: toInt(m[1]), courseId: toInt(m[2]), examinationId: toInt(m[3]), tabIndex: toInt(m[4]), }; } return {}; } function flattenFromSorted(sorted) { const list = []; for (const block of sorted || []) { for (const q of block.Question || []) list.push(q); } return list; } function extractPaperData(paperData) { if (!paperData || typeof paperData !== "object") return null; const questions = flattenExamQuestions(paperData); if (!questions.length) return null; return { resultId: toInt(paperData.ResultId || paperData.resultId), questions, title: (paperData.PaperInfo && paperData.PaperInfo.Title) || "", }; } function persistExamCtx(ctx) { if (!ctx) return; try { sessionStorage.setItem("ct_exam_ctx", JSON.stringify(ctx)); } catch (_) {} if (ctx.resultId > 0) { try { sessionStorage.setItem("ct_exam_result_id", String(ctx.resultId)); } catch (_) {} } else if (ctx.resultId === 0) { try { sessionStorage.removeItem("ct_exam_result_id"); } catch (_) {} } } function clearExamCtx() { state.exam.resultId = 0; state.exam.isPreview = true; try { sessionStorage.removeItem("ct_exam_ctx"); sessionStorage.removeItem("ct_exam_result_id"); } catch (_) {} } function isExamClosedError(msg) { const s = String(msg || ""); return s.includes("\u5df2\u4ea4\u5377") || s.includes("\u5df2\u63d0\u4ea4") || s.includes("\u8003\u8bd5\u5df2\u7ed3\u675f") || s.includes("\u4e0d\u80fd\u4f5c\u7b54"); } function loadPersistedExamCtx() { try { const raw = sessionStorage.getItem("ct_exam_ctx"); if (raw) return JSON.parse(raw); } catch (_) {} return null; } function captureExamSubmitBody(bodyText) { if (!bodyText) return; try { const req = JSON.parse(bodyText); const resultId = toInt(req.resultId); if (resultId <= 0) return; state.exam.resultId = resultId; state.exam.isPreview = false; if (req.Examination_ID) state.exam.examinationId = toInt(req.Examination_ID); persistExamCtx({ resultId, examinationId: state.exam.examinationId, curriculumId: state.exam.curriculumId, examPaperId: state.exam.examPaperId, }); } catch (_) {} } function getDvaStore() { if (window.g_app && window.g_app._store) return window.g_app._store; if (window.app && window.app._store) return window.app._store; if (getDvaStore._cache) return getDvaStore._cache; const root = document.getElementById("root"); if (!root) return null; const fiberKey = Object.keys(root).find( (k) => k.startsWith("__reactFiber$") || k.startsWith("__reactContainer$") ); if (!fiberKey) return null; let fiber = root[fiberKey]; if (fiberKey.startsWith("__reactContainer")) { fiber = fiber?.stateNode?.current ?? fiber?.current ?? fiber; } const seen = new Set(); function looksLikeExamStore(store) { try { const st = store.getState(); return !!(st.doExcer || st.doExcer1 || st.onlineclass || st.practice); } catch (_) { return false; } } function walk(node, depth = 0) { if (!node || depth > 120 || seen.has(node)) return null; seen.add(node); const props = node.memoizedProps; if (props && props.store && props.store.getState && props.store.dispatch) { if (looksLikeExamStore(props.store)) return props.store; } if (node.stateNode && node.stateNode.store && node.stateNode.store.getState) { if (looksLikeExamStore(node.stateNode.store)) return node.stateNode.store; } let hook = node.memoizedState; while (hook) { const ctx = hook.memoizedState; if (ctx && ctx.store && ctx.store.getState && looksLikeExamStore(ctx.store)) { return ctx.store; } hook = hook.next; } return ( walk(node.child, depth + 1) || walk(node.sibling, depth + 1) || walk(node.return, depth + 1) ); } const store = walk(fiber); if (store) getDvaStore._cache = store; return store || null; } function examQuestionKey(q) { return toInt(q.ID || q.Question_ID || q.QuestionId || q.id); } function questionIdsMatch(q, qid) { return examQuestionKey(q) === toInt(qid); } function buildSortedFromFlatQuestions(questions) { const byType = new Map(); for (const q of questions || []) { const tid = toInt(q.QuestionType_ID) || 1; if (!byType.has(tid)) { byType.set(tid, { Question: [], TypeInfo: { QuestionType_ID: tid, QuestionType_Name: tid === 1 ? "\u5355\u9009\u9898" : "\u9898\u76ee", QuestionType_Mark: tid === 1 ? "\u5355\u9009\u9898" : "\u9898\u76ee", }, }); } byType.get(tid).Question.push(q); } return Array.from(byType.values()); } function getExamAnswerCardCountFromDom() { const text = (document.body && document.body.innerText) || ""; const m = text.match(/\u7b54\u9898\u5361[^\d]*(\d+)\s*\/\s*(\d+)/); if (m) return toInt(m[1]); const cells = document.querySelectorAll( "[class*='answerCard'] [class*='active'], [class*='AnswerCard'] [class*='active'], [class*='sheet'] [class*='done'], [class*='sheet'] [class*='finish']" ); if (cells.length) return cells.length; return null; } function readDvaExamState() { const binding = findExamDvaBinding(); if (!binding) return null; return { resultId: binding.resultId, questions: binding.questions, title: binding.title, namespace: binding.namespace, }; } function findExamDvaBinding() { try { const store = getDvaStore(); if (!store) return null; const st = store.getState(); for (const ns of ["doExcer", "doExcer1", "startTest", "startTest1", "onlineclass", "practice"]) { const m = st[ns]; if (!m) continue; const sorted = m.questionTypeSorted || m.questionType; if (Array.isArray(sorted) && sorted.length) { const questions = flattenFromSorted(sorted); if (questions.length) { const resultId = toInt(m.resultId) || toInt(m.paperInfo && m.paperInfo.ResultId) || state.exam.resultId; return { namespace: ns, mode: "sorted", sorted, resultId, questions, title: (m.paperInfo && m.paperInfo.Title) || state.exam.title || "", }; } } const parsed = extractPaperData(m.paperInfo); if (parsed && parsed.questions.length) { const resultId = parsed.resultId || state.exam.resultId; if (resultId > 0 || parsed.questions.length) { return { namespace: ns, mode: "paperInfo", paperInfo: m.paperInfo, resultId, questions: parsed.questions, title: parsed.title, }; } } } } catch (_) {} return null; } function ensureExamDvaBinding() { let binding = findExamDvaBinding(); if (binding) return binding; const store = getDvaStore(); if (!store || !state.exam.questions.length) return null; const sorted = buildSortedFromFlatQuestions(state.exam.questions); store.dispatch({ type: "doExcer/updateQuestionTypeSorted", payload: sorted }); if (state.exam.resultId > 0) { store.dispatch({ type: "doExcer/updateResultId", payload: state.exam.resultId }); } store.dispatch({ type: "doExcer/updateQuestionCount", payload: buildQuestionCountPayload(sorted), }); return findExamDvaBinding(); } function countAnsweredQuestions(sorted) { let n = 0; for (const block of sorted || []) { for (const q of block.Question || []) { if (q.MyAnswer || q.FileJson) n++; } } return n; } function buildQuestionCountPayload(sorted) { let total = 0; let doCount = 0; for (const block of sorted || []) { for (const q of block.Question || []) { total++; if (q.MyAnswer || q.FileJson) doCount++; } } return { total, doCount, sectionTotal: sorted.length, rightCount: 0, wrongCount: 0, subjectiveCount: 0, }; } function getExamAnswerCardCount() { try { const binding = findExamDvaBinding(); if (binding) { const store = getDvaStore(); const m = store && store.getState()[binding.namespace]; if (m && m.questionCount && m.questionCount.doCount != null) { return m.questionCount.doCount; } if (binding.mode === "sorted") { const sorted = m && (m.questionTypeSorted || m.questionType); return countAnsweredQuestions(sorted || binding.sorted); } return countAnsweredQuestions(binding.paperInfo && binding.paperInfo.QuestionType); } } catch (_) {} const dom = getExamAnswerCardCountFromDom(); return dom != null ? dom : 0; } function syncExamAnswerOnPage(questionId, myAnswer) { const store = getDvaStore(); if (!store) return false; let binding = findExamDvaBinding() || ensureExamDvaBinding(); if (!binding) return false; const qid = toInt(questionId); if (binding.mode === "sorted") { const m = store.getState()[binding.namespace]; const sorted = JSON.parse(JSON.stringify(m.questionTypeSorted || m.questionType || binding.sorted)); let found = false; for (const block of sorted) { for (const q of block.Question || []) { if (questionIdsMatch(q, qid)) { q.MyAnswer = myAnswer; q.Judge = q.Judge || 0; found = true; } } } if (!found) return false; store.dispatch({ type: `${binding.namespace}/updateQuestionTypeSorted`, payload: sorted }); store.dispatch({ type: `${binding.namespace}/updateQuestionCount`, payload: buildQuestionCountPayload(sorted), }); flushExamUi(binding.namespace); return true; } if (binding.mode === "paperInfo" && binding.paperInfo) { const paperInfo = JSON.parse(JSON.stringify(binding.paperInfo)); let found = false; for (const block of paperInfo.QuestionType || []) { for (const q of block.Question || []) { if (questionIdsMatch(q, qid)) { q.MyAnswer = myAnswer; q.Judge = q.Judge || 0; found = true; break; } } if (found) break; } if (!found) return false; store.dispatch({ type: `${binding.namespace}/setStateByResult`, payload: { paperInfo }, }); flushExamUi(binding.namespace); return true; } return false; } function flushExamUi(namespace) { const store = getDvaStore(); if (!store || !namespace) return; try { store.dispatch({ type: `${namespace}/updateState` }); } catch (_) {} } function applyExamPaperToDva(paperData) { const store = getDvaStore(); if (!store || !paperData) return false; const binding = findExamDvaBinding() || ensureExamDvaBinding(); const ns = (binding && binding.namespace) || "doExcer"; const questionType = paperData.QuestionType || []; if (!questionType.length) return false; store.dispatch({ type: `${ns}/updateQuestionTypeSorted`, payload: questionType, }); if (paperData.PaperInfo) { store.dispatch({ type: `${ns}/updatePaperInfo`, payload: paperData.PaperInfo }); } const rid = toInt(paperData.ResultId || paperData.resultId); if (rid > 0) { store.dispatch({ type: `${ns}/updateResultId`, payload: rid }); state.exam.resultId = rid; } store.dispatch({ type: `${ns}/updateQuestionCount`, payload: buildQuestionCountPayload(questionType), }); flushExamUi(ns); const flat = flattenExamQuestions(paperData); if (flat.length) { state.exam.questions = flat; invalidateExamPanelStats(); } return true; } async function reloadExamPaperToPage() { const user = getUser(); const exam = state.exam; if (!exam.examPaperId || !exam.examinationId || !user.StuID) return false; const res = await api("GET", "/eduSuper/Question/GetExamPaperQuestions", { params: { examPaperId: exam.examPaperId, StuID: user.StuID, StuDetail_ID: user.StuDetail_ID, Examination_ID: exam.examinationId, Curriculum_ID: exam.curriculumId, SignId: 0, type: 2, }, }); if (!res.SuccessResponse || !res.Data) return false; const fakeUrl = `${apiBase()}/eduSuper/Question/GetExamPaperQuestions?` + `examPaperId=${exam.examPaperId}&Examination_ID=${exam.examinationId}`; captureExamSession(res, fakeUrl); return applyExamPaperToDva(res.Data); } async function applyExamAnswerViaUi(questionIndex, myAnswer) { clickAnswerCardItem(questionIndex + 1); await sleep(200); const ok = clickAnswerOnPage(myAnswer); await sleep(150); return ok; } async function refreshAnswerCardByVisit(indexes) { for (const idx of indexes) { if (state.stopping) break; clickAnswerCardItem(idx + 1); await sleep(70); } } async function submitExamAnswer(question, myAnswer, examinationId, resultId, user) { const res = await submitSimplePractice(user, resultId, examinationId, question, myAnswer); return { api: !!res.SuccessResponse, msg: res.Message || "" }; } function getLiveExamResultId() { const binding = findExamDvaBinding(); if (binding && binding.resultId > 0) return binding.resultId; return state.exam.resultId; } function readFinalExamFromStore() { try { const store = getDvaStore(); if (!store) return null; const st = store.getState(); for (const ns of ["onlineclass", "practice"]) { const list = st[ns] && st[ns].finalExam; if (Array.isArray(list) && list.length) return list[0]; } } catch (_) {} return null; } async function resolveExamParams(user, examPaperId) { const cached = loadPersistedExamCtx(); if (cached && cached.examinationId && (!examPaperId || cached.examPaperId === examPaperId)) { return cached; } const route = parseExamRoute(); if (route.examinationId && route.curriculumId) { const ctx = { examPaperId: examPaperId || route.examPaperId, examinationId: route.examinationId, curriculumId: route.curriculumId, }; persistExamCtx(ctx); return ctx; } const finalItem = readFinalExamFromStore(); if (finalItem) { const ctx = { examPaperId: toInt(finalItem.ExamPaper_ID), examinationId: toInt(finalItem.Examination_ID), curriculumId: toInt(finalItem.Curriculum_ID), }; if (!examPaperId || ctx.examPaperId === examPaperId) { persistExamCtx(ctx); return ctx; } } if (!state.courseTree) { try { state.courseTree = await fetchCourseTree(user); } catch (_) {} } const currentTerm = getViewTerm(state.courseTree); const courses = getTermCourses(state.courseTree, currentTerm); for (const c of courses) { const curriculumId = toInt(c.Curriculum_ID); try { const meta = await fetchFinalExamMeta(user, curriculumId); const ctx = { examPaperId: toInt(meta.ExamPaper_ID), examinationId: toInt(meta.Examination_ID), curriculumId: toInt(meta.Curriculum_ID), }; if (!examPaperId || ctx.examPaperId === examPaperId) { persistExamCtx(ctx); return ctx; } } catch (_) {} } return null; } async function fetchFinalExamMeta(user, curriculumId) { const res = await api("GET", "/eduSuper/Question/GetFinalExamPaperView", { params: { StuDetail_ID: user.StuDetail_ID, Curriculum_ID: curriculumId, Examination_ID: 0, }, }); if (!res.SuccessResponse || !Array.isArray(res.Data) || !res.Data.length) { throw new Error(res.Message || "\u672a\u627e\u5230\u671f\u672b\u8003\u8bd5"); } return res.Data[0]; } async function fetchSuperExamPaper(user, params) { const qp = { examPaperId: params.examPaperId, StuID: user.StuID, StuDetail_ID: user.StuDetail_ID, Examination_ID: params.examinationId, Curriculum_ID: params.curriculumId, SignId: params.signId ?? 0, }; if (params.isBegin) qp.IsBegin = 1; else qp.type = params.type ?? 2; const res = await api("GET", "/eduSuper/Question/GetExamPaperQuestions", { params: qp }); if (!res.SuccessResponse) throw new Error(res.Message || "\u83b7\u53d6\u8bd5\u5377\u5931\u8d25"); if (params.isBegin) state.exam.beginAt = Date.now(); const fakeUrl = `${apiBase()}/eduSuper/Question/GetExamPaperQuestions?` + `examPaperId=${params.examPaperId}&Examination_ID=${params.examinationId}&Curriculum_ID=${params.curriculumId}` + (params.isBegin ? "&IsBegin=1" : ""); captureExamSession(res, fakeUrl); return res.Data; } async function ensureExamSession() { const dvaFirst = readDvaExamState(); if (dvaFirst && dvaFirst.resultId > 0) { const route = parseExamRoute(); state.exam = { ...state.exam, resultId: dvaFirst.resultId, questions: dvaFirst.questions.length ? dvaFirst.questions : state.exam.questions, title: dvaFirst.title || state.exam.title, examinationId: state.exam.examinationId || route.examinationId || 0, curriculumId: state.exam.curriculumId || route.curriculumId || 0, examPaperId: state.exam.examPaperId || route.examPaperId || 0, isPreview: false, }; persistExamCtx({ resultId: state.exam.resultId, examinationId: state.exam.examinationId, curriculumId: state.exam.curriculumId, examPaperId: state.exam.examPaperId, }); trace(`\u5df2\u4ece\u9875\u9762\u72b6\u6001\u8bfb\u53d6: ${state.exam.questions.length} \u9898\uff0cResultId=${state.exam.resultId}`); return true; } if (state.exam.resultId > 0 && state.exam.questions.length && !state.exam.isPreview) { return true; } if (state.exam.isPreview || !state.exam.resultId) { const persisted = loadPersistedExamCtx(); if (persisted && persisted.resultId > 0 && !state.exam.isPreview) { state.exam.resultId = persisted.resultId; state.exam.examinationId = state.exam.examinationId || persisted.examinationId || 0; state.exam.curriculumId = state.exam.curriculumId || persisted.curriculumId || 0; state.exam.examPaperId = state.exam.examPaperId || persisted.examPaperId || 0; } } if (state.exam.resultId > 0 && state.exam.questions.length && !state.exam.isPreview) { return true; } const user = getUser(); if (!user.StuID) return false; const route = parseExamRoute(); const examPaperId = state.exam.examPaperId || route.examPaperId; const params = await resolveExamParams(user, examPaperId); if (!params) { trace("\u65e0\u6cd5\u89e3\u6790\u8003\u8bd5\u53c2\u6570\uff08Examination_ID / Curriculum_ID\uff09"); return false; } state.exam.examinationId = params.examinationId; state.exam.curriculumId = params.curriculumId; state.exam.examPaperId = params.examPaperId; if (!state.exam.questions.length) { await fetchSuperExamPaper(user, { examPaperId: params.examPaperId, examinationId: params.examinationId, curriculumId: params.curriculumId, isBegin: false, type: 2, }); } if (!state.exam.resultId) { const dva2 = readDvaExamState(); if (dva2 && dva2.resultId > 0) { state.exam.resultId = dva2.resultId; if (!state.exam.questions.length) state.exam.questions = dva2.questions; } } persistExamCtx({ resultId: state.exam.resultId, examinationId: state.exam.examinationId, curriculumId: state.exam.curriculumId, examPaperId: state.exam.examPaperId, }); return state.exam.resultId > 0 && state.exam.questions.length > 0; } function captureExamSession(data, url) { if (!data || !data.SuccessResponse || !data.Data) return; const qs = new URL(url, location.origin).searchParams; const questions = flattenExamQuestions(data.Data); const resultId = toInt(data.Data.ResultId); const isBegin = qs.get("IsBegin") === "1" || /IsBegin=1/.test(url); state.exam = { resultId, examinationId: toInt(qs.get("Examination_ID")) || state.exam.examinationId || 0, curriculumId: toInt(qs.get("Curriculum_ID")) || state.exam.curriculumId || 0, examPaperId: toInt(qs.get("examPaperId")) || state.exam.examPaperId || 0, title: (data.Data.PaperInfo && data.Data.PaperInfo.Title) || "", questions, isPreview: !(resultId > 0 || isBegin), }; persistExamCtx({ resultId: state.exam.resultId, examinationId: state.exam.examinationId, curriculumId: state.exam.curriculumId, examPaperId: state.exam.examPaperId, }); if (questions.length) { const msg = resultId > 0 ? `\u5df2\u6355\u83b7\u8003\u8bd5\u5377: ${state.exam.title || "\u8bd5\u5377"}\uff0c${questions.length} \u9898` : `\u5df2\u8bfb\u53d6\u8bd5\u5377 ${questions.length} \u9898\uff08\u8bf7\u5148\u70b9\u7f51\u9875\u300c\u5f00\u59cb\u8003\u8bd5\u300d\uff09`; trace(msg); const btn = document.getElementById("ct-exam-auto"); if (btn) btn.style.display = "inline-block"; if (resultId > 0 && !state.exam.isPreview) maybeAutoRunExamAnswers(); } } function hookPageNetwork() { if (window.__ctNetHooked) return; window.__ctNetHooked = true; const origFetch = window.fetch; if (typeof origFetch === "function") { window.fetch = function (...args) { const req = args[0]; const init = args[1] || {}; const url = typeof req === "string" ? req : req && req.url ? req.url : ""; if (shouldBlockExamBeginRequest(url, init.body)) { notifyExamBeginBlocked(); return Promise.reject(new Error("\u9898\u5e93\u672a\u51c6\u5907\u5b8c\u6210\uff0c\u65e0\u6cd5\u5f00\u59cb\u8003\u8bd5")); } if (url.includes("SubmitSimplePractice") && init.body) { captureExamSubmitBody( typeof init.body === "string" ? init.body : null ); } const p = origFetch.apply(this, args); p.then((res) => { try { if (url.includes("GetExamPaperQuestions")) { res .clone() .json() .then((data) => captureExamSession(data, url)) .catch(() => {}); } } catch (_) {} }).catch(() => {}); return p; }; } const open = XMLHttpRequest.prototype.open; const send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this.__ctReqUrl = url; this.__ctReqMethod = method; return open.call(this, method, url, ...rest); }; XMLHttpRequest.prototype.send = function (...args) { const body = args[0]; let url = this.__ctReqUrl || ""; if (url && !/^https?:/i.test(url)) { try { url = new URL(url, location.origin + "/service/").href; } catch (_) {} } if (shouldBlockExamBeginRequest(url, body)) { notifyExamBeginBlocked(); const xhr = this; setTimeout(() => { try { Object.defineProperty(xhr, "readyState", { configurable: true, value: 4 }); Object.defineProperty(xhr, "status", { configurable: true, value: 403 }); Object.defineProperty(xhr, "responseText", { configurable: true, value: JSON.stringify({ SuccessResponse: false, Message: "\u9898\u5e93\u672a\u51c6\u5907\u5b8c\u6210\uff0c\u65e0\u6cd5\u5f00\u59cb\u8003\u8bd5" }), }); xhr.dispatchEvent(new Event("readystatechange")); xhr.dispatchEvent(new Event("load")); xhr.dispatchEvent(new Event("loadend")); } catch (_) {} }, 0); return; } if (url.includes("SubmitSimplePractice") && body) { captureExamSubmitBody(typeof body === "string" ? body : null); } this.addEventListener("load", function () { try { if (url.includes("GetExamPaperQuestions") && this.responseText) { captureExamSession(JSON.parse(this.responseText), url); } } catch (_) {} }); return send.apply(this, args); }; } function formatExamAnswer(question, raw) { if (raw == null || raw === "") return ""; const t = toInt(question.QuestionType_ID); if (isMultiChoiceType(t)) return formatMultiChoiceAnswer(raw); if (t === QUESTION_TYPE.single) { const letters = extractChoiceLetters(raw); if (letters.length) return letters[0]; return String(raw).trim(); } if (t === QUESTION_TYPE.judge) { const letters = extractChoiceLetters(raw); if (letters.length) return letters[0]; const s = String(raw).trim(); if (/^(\u6b63\u786e|\u5bf9|true|√|T|\u662f|1)$/i.test(s)) return "A"; if (/^(\u9519\u8bef|\u9519|false|×|F|\u5426|0|2)$/i.test(s)) return "B"; return s; } if ( t === QUESTION_TYPE.blank || t === QUESTION_TYPE.calc || isSubjectiveType(t) || isCompositeType(t) ) { return String(raw).trim(); } return String(raw).trim(); } async function submitSimplePractice(user, resultId, examinationId, question, myAnswer) { const qid = examQuestionKey(question); return api("POST", "/eduSuper/Question/SubmitSimplePractice", { body: { resultId, list: [ { ID: qid, MyAnswer: myAnswer, Judge: 0, QuestionType_ID: question.QuestionType_ID, FileJson: question.FileJson || "", }, ], StuDetail_ID: user.StuDetail_ID, StuID: user.StuID, Examination_ID: String(examinationId), }, }); } async function verifyExamSessionActive(user, exam) { if (!exam.resultId || !exam.questions.length) return { ok: false, reason: "\u65e0 ResultId" }; const q = exam.questions.find((item) => toInt(item.QuestionType_ID) === 1 && lookupAnswer(item)) || exam.questions.find((item) => lookupAnswer(item)) || exam.questions[0]; const ans = formatExamAnswer(q, lookupAnswer(q) || "A"); const res = await submitSimplePractice(user, exam.resultId, exam.examinationId, q, ans); if (res.SuccessResponse) return { ok: true }; if (isExamClosedError(res.Message)) return { ok: false, reason: "\u5df2\u4ea4\u5377", msg: res.Message }; return { ok: false, reason: res.Message || "\u6821\u9a8c\u5931\u8d25", msg: res.Message }; } function buildExamSubmitList(exam) { const list = []; for (const q of exam.questions || []) { list.push({ ID: examQuestionKey(q), MyAnswer: q.MyAnswer || "", Judge: toInt(q.Judge, 0), QuestionType_ID: q.QuestionType_ID, FileJson: q.FileJson || "", }); } return list; } async function submitExamFinal(user, exam, endTimeSec = 30) { if (!exam.resultId) throw new Error("\u65e0 ResultId\uff0c\u65e0\u6cd5\u4ea4\u5377"); return api("POST", "/eduSuper/Question/SubmitExamPractice", { body: { resultId: exam.resultId, list: buildExamSubmitList(exam), EndTime: endTimeSec, StuDetail_ID: user.StuDetail_ID, StuID: user.StuID, Examination_ID: String(exam.examinationId), Curriculum_ID: exam.curriculumId, }, }); } async function executeExamAnswers(user, exam, opts = {}) { const { skipVerify = false } = opts; let apiOk = 0; let miss = 0; let fail = 0; const viaStats = { id: 0, title: 0, "title-fuzzy": 0, body: 0 }; const resultId = getLiveExamResultId(); if (resultId > 0) exam.resultId = resultId; if (!skipVerify) { trace(`\u6821\u9a8c\u8003\u8bd5\u4f1a\u8bdd ResultId=${exam.resultId}…`); const check = await verifyExamSessionActive(user, exam); if (!check.ok) { if (check.reason === "\u5df2\u4ea4\u5377") clearExamCtx(); throw new Error(check.msg || check.reason || "\u8003\u8bd5\u4f1a\u8bdd\u65e0\u6548"); } trace("\u8003\u8bd5\u4f1a\u8bdd\u6709\u6548\uff0c\u5f00\u59cb\u6279\u91cf\u4f5c\u7b54"); } trace(`\u8003\u8bd5\u4f5c\u7b54: ${exam.questions.length} \u9898\uff0c\u9898\u5e93 ${qbankCount()} \u6761`); for (let i = 0; i < exam.questions.length; i++) { if (state.stopping) break; const q = exam.questions[i]; const found = lookupAnswerDetailed(q); if (!found.answer) { miss++; continue; } if (found.via && viaStats[found.via] != null) viaStats[found.via]++; aliasExamAnswer(q, found.answer); const myAnswer = formatExamAnswer(q, found.answer); const r = await submitExamAnswer(q, myAnswer, exam.examinationId, exam.resultId, user); if (r.api) { apiOk++; q.MyAnswer = myAnswer; } else { fail++; const errMsg = r.msg || "\u63d0\u4ea4\u9519\u8bef"; trace(`Q${examQuestionKey(q)} \u5931\u8d25: ${errMsg}`); if (isExamClosedError(errMsg)) { clearExamCtx(); throw new Error("\u8003\u8bd5\u5df2\u4ea4\u5377"); } } if ((i + 1) % 10 === 0) trace(` \u8fdb\u5ea6 ${i + 1}/${exam.questions.length}`); await sleepAbortable(100); } trace( `\u8003\u8bd5\u4f5c\u7b54\u7ed3\u675f: \u63d0\u4ea4 ${apiOk}\uff0c\u65e0\u7b54\u6848 ${miss}\uff0c\u5931\u8d25 ${fail} / \u5171 ${exam.questions.length}` + `\uff08ID ${viaStats.id}\uff0c\u9898\u5e72 ${viaStats.title + viaStats["title-fuzzy"]}\uff0c\u9009\u9879 ${viaStats.body}\uff09` ); return { apiOk, miss, fail }; } const EXAM_SUBMIT_WAIT_MS = 5 * 60 * 1000 + 8000; async function waitExamSubmitReady() { const beginAt = toInt(state.exam.beginAt, 0); if (!beginAt) return true; const elapsed = Date.now() - beginAt; if (elapsed >= EXAM_SUBMIT_WAIT_MS) return true; const waitMs = EXAM_SUBMIT_WAIT_MS - elapsed; trace(`\u5f00\u8003\u672a\u6ee15\u5206\u949f\uff0c\u7b49\u5f85\u7ea6 ${Math.ceil(waitMs / 1000)}s \u540e\u4ea4\u5377…`); return sleepAbortable(waitMs); } async function runTermExams() { const user = getUser(); if (!user.StuID || !user.StuDetail_ID) return; if (!state.courseTree) { state.courseTree = await fetchCourseTree(user); } const term = getViewTerm(state.courseTree); const courses = getTermCourses(state.courseTree, term).filter((c) => state.selectedCurriculumIds.has(String(c.Curriculum_ID)) ); if (!courses.length) { trace("\u5f53\u524d\u5b66\u671f\u65e0\u8bfe\u7a0b\uff0c\u8df3\u8fc7\u8003\u8bd5"); return; } state.examRunning = true; state.currentTask = "\u81ea\u52a8\u8003\u8bd5\u4e2d"; updateQueueExamCounts(); updatePanel(); try { trace(`\u81ea\u52a8\u8003\u8bd5: ${termLabel(term)} \u5171 ${courses.length} \u95e8\u8bfe`); for (const c of courses) { if (state.stopping) break; const curriculumId = toInt(c.Curriculum_ID); const cuName = c.CuName || c.Name || String(curriculumId); if (isCourseExamDone(c)) { trace(`${cuName} \u8003\u8bd5\u5df2\u5b8c\u6210\uff08${formatExamInfoLabel(getCourseExamInfo(c))}\uff09\uff0c\u8df3\u8fc7`); continue; } try { const meta = await fetchFinalExamMeta(user, curriculumId); const examPaperId = toInt(meta.ExamPaper_ID); const examinationId = toInt(meta.Examination_ID); if (!examPaperId || !examinationId) continue; clearExamCtx(); state.exam = { resultId: 0, examinationId, curriculumId, examPaperId, title: "", questions: [], isPreview: true, }; trace(`\u5f00\u59cb\u8003\u8bd5: ${cuName}`); await fetchSuperExamPaper(user, { examPaperId, examinationId, curriculumId, isBegin: true, }); if (!state.exam.resultId || state.exam.isPreview || !state.exam.questions.length) { trace(`${cuName} \u65e0\u6cd5\u5f00\u59cb\u8003\u8bd5\uff08\u65e0 ResultId\uff09\uff0c\u8df3\u8fc7`); continue; } const { apiOk } = await executeExamAnswers(user, state.exam, { skipVerify: true }); if (apiOk > 0 && !state.stopping) { if (!(await waitExamSubmitReady())) break; const submitRes = await submitExamFinal(user, state.exam); if (submitRes.SuccessResponse) { trace(`${cuName} \u4ea4\u5377\u6210\u529f: ${submitRes.Message || "\u5b8c\u6210"}`); try { const meta = await fetchFinalExamMeta(user, curriculumId); state.examInfoByCid[String(curriculumId)] = normalizeExamInfo(meta); } catch (_) { const prev = getCourseExamInfo(c); state.examInfoByCid[String(curriculumId)] = normalizeExamInfo({ Score: prev.score >= 0 ? prev.score : 0, ResultCount: (prev.resultCount || 0) + 1, ExamCount: prev.examCount || EXAM_MAX_ATTEMPTS_DEFAULT, }); } renderCoursePanel(); state.queueExamDone = Math.min( state.queueExamTotal, courses.filter(isCourseExamDone).length ); updatePanel(); } else { trace(`${cuName} \u4ea4\u5377\u5931\u8d25: ${submitRes.Message || "\u672a\u77e5\u9519\u8bef"}`); } } else { trace(`${cuName} \u65e0\u6709\u6548\u4f5c\u7b54\uff0c\u672a\u4ea4\u5377`); } clearExamCtx(); if (!(await sleepAbortable(800))) break; } catch (e) { trace(`${cuName} \u8003\u8bd5\u8df3\u8fc7: ${e.message || e}`); } } trace("\u81ea\u52a8\u8003\u8bd5\u6d41\u7a0b\u7ed3\u675f"); } finally { state.examRunning = false; if (state.stopping) trace("\u81ea\u52a8\u8003\u8bd5\u5df2\u505c\u6b62"); updatePanel(); } } async function runAutoPipeline() { if (state.pipelineStarted) return; const user = getUser(); if (!user.StuID || !user.StuDetail_ID) return; if (!(await _cr())) return; state.pipelineStarted = true; state.stopping = false; state.currentTask = isPro() ? "\u81ea\u52a8\u6d41\u7a0b\uff1a\u5237\u8bfe + \u51c6\u5907\u9898\u5e93" : "\u81ea\u52a8\u6d41\u7a0b\uff1a\u514d\u8d39\u5237\u8bfe"; updatePanel(); localStorage.setItem(STORAGE_FAST, "0"); localStorage.setItem(STORAGE_ENABLED, "1"); try { await refreshCourseTree({ quiet: true }); trace(isPro() ? "\u5f00\u59cb\uff1a\u5b66\u4e60\u8bfe\u7a0b + \u51c6\u5907\u9898\u5e93" : "\u5f00\u59cb\uff1a\u514d\u8d39\u5b66\u4e60\uff08\u6700\u591a3\u8282\uff09"); runStudyLoop().catch((e) => trace(`\u5237\u8bfe\u5f02\u5e38: ${e.message || e}`, "error")); if (isPro() && state.autoQbankEnabled) { await runQBankHarvest(false); } if (!state.stopping) { state.currentTask = state.studyRunning ? "\u5237\u8bfe\u4ecd\u5728\u8fdb\u884c" : isPro() ? "\u5237\u8bfe+\u51c6\u5907\u9898\u5e93\u5df2\u5b8c\u6210" : "\u514d\u8d39\u5237\u8bfe\u5df2\u5b8c\u6210"; if (isPro()) trace("\u9898\u5e93\u51c6\u5907\u9636\u6bb5\u7ed3\u675f\uff08\u8003\u8bd5\u8bf7\u8fdb\u8003\u8bd5\u9875\uff09"); updatePanel(); } } catch (e) { trace(`\u81ea\u52a8\u6d41\u7a0b\u5f02\u5e38: ${e.message || e}`, "error"); } finally { if (!state.studyRunning) state.pipelineStarted = false; updatePanel(); } } async function runExamAutoAnswer() { const user = getUser(); if (!user.StuID || !user.StuDetail_ID) { trace("\u672a\u767b\u5f55", "error"); return; } if (!isPro() || !state.autoExamEnabled) { trace("\u8003\u8bd5\u8f85\u52a9\u4e3a Pro \u529f\u80fd", "warn"); return; } if (state.examRunning) return; await requestStopQbankHarvest(); if (!(await _cr())) return; const ex = state.exam; if (!ex.resultId || ex.isPreview || !ex.questions.length) { const ok = await ensureExamSession(); if (!ok || !state.exam.resultId || state.exam.isPreview) { trace("\u8bf7\u5148\u5728\u7f51\u9875\u70b9\u51fb\u300c\u5f00\u59cb\u8003\u8bd5\u300d", "warn"); showNotifyModal({ title: "\u8bf7\u5148\u5f00\u59cb\u8003\u8bd5", html: `

\u8bf7\u5148\u5728\u8003\u8bd5\u9875\u9762\u70b9\u51fb\u300c\u5f00\u59cb\u8003\u8bd5\u300d\uff0c\u8fdb\u5165\u8ba1\u65f6\u540e\u81ea\u52a8\u4f5c\u7b54\u3002

`, primaryText: "\u6211\u77e5\u9053\u4e86", }); return; } } const exam = state.exam; state.examRunning = true; trace("\u6b63\u5728\u81ea\u52a8\u7b54\u9898\u4e2d\uff0c\u5b8c\u6210\u81ea\u52a8\u5237\u65b0\uff0c\u8bf7\u8010\u5fc3\u7b49\u5f85", "info", { force: true }); updateExamAssistPanel(); updatePanel(); try { const { apiOk, miss, fail } = await executeExamAnswers(user, exam); if (apiOk > 0 && !state.stopping) { const cov = computeExamCoverage(exam.questions); if (miss === 0 && fail === 0) { try { sessionStorage.setItem(STORAGE_EXAM_SUBMIT_HINT, "1"); } catch (_) {} } else { trace(`\u90e8\u5206\u9898\u76ee\u65e0\u7b54\u6848\u6216\u63d0\u4ea4\u5931\u8d25\uff0c\u8bf7\u624b\u52a8\u8865\u7b54`, "warn"); } showNotifyModal({ title: "\u4f5c\u7b54\u5df2\u5b8c\u6210", html: `

\u5df2\u63d0\u4ea4 ${apiOk} \u9898\uff0c\u9898\u5e93\u8986\u76d6\u7ea6 ${cov.pct}%\u3002

\u9875\u9762\u5373\u5c06\u5237\u65b0\u540c\u6b65\u7b54\u9898\u5361\uff0c\u8bf7\u5728\u7f51\u9875\u4e0a\u624b\u52a8\u70b9\u51fb\u300c\u4ea4\u5377\u300d\uff08\u5f00\u8003\u6ee1 5 \u5206\u949f\u540e\u53ef\u4ea4\uff09\u3002

`, primaryText: "\u6211\u77e5\u9053\u4e86", }); trace("\u4f5c\u7b54\u5b8c\u6210\uff0c\u8bf7\u624b\u52a8\u4ea4\u5377"); await sleep(1200); location.reload(); return; } } catch (e) { trace(`\u8003\u8bd5\u4f5c\u7b54\u5f02\u5e38: ${e.message || e}`, "error"); } finally { state.examRunning = false; updateExamAssistPanel(); updatePanel(); } } function hookQBankPage() { const onQuiz = isExamPage() || isOnlineclassPaperPage(); if (!onQuiz) { state.examBeginHintShown = false; state.examAutoTriggeredFor = 0; } syncExamRouteFromPage(); refreshExamStartGuard(); maybeShowManualSubmitHint(); if (onQuiz) { const fillBtn = document.getElementById("ct-qbank-fill"); if (fillBtn) fillBtn.style.display = "inline-block"; const examBtn = document.getElementById("ct-exam-auto"); if (examBtn) examBtn.style.display = "inline-block"; const dva = readDvaExamState(); if (dva) { const route = parseExamRoute(); state.exam = { ...state.exam, resultId: dva.resultId || state.exam.resultId, questions: dva.questions.length ? dva.questions : state.exam.questions, title: dva.title || state.exam.title, examinationId: state.exam.examinationId || route.examinationId || 0, curriculumId: state.exam.curriculumId || route.curriculumId || 0, examPaperId: state.exam.examPaperId || route.examPaperId || 0, isPreview: !(dva.resultId > 0), }; if (state.exam.resultId > 0) { persistExamCtx({ resultId: state.exam.resultId, examinationId: state.exam.examinationId, curriculumId: state.exam.curriculumId, examPaperId: state.exam.examPaperId, }); if (!state.exam.isPreview) { if (state.qbankRunning) void requestStopQbankHarvest(); maybeAutoRunExamAnswers(); } } } else if (!state.exam.resultId) { ensureExamSession().then((ok) => { if (ok && !state.exam.isPreview) maybeAutoRunExamAnswers(); }).catch(() => {}); } } if (window.__ctQBankHooked) return; window.__ctQBankHooked = true; window.__ctQBank = { count: qbankCount, get: getStoredAnswer, lookup: lookupAnswer, lookupDetail: lookupAnswerDetailed, matchStats: () => { const qs = state.exam.questions || []; let id = 0; let title = 0; let body = 0; let miss = 0; for (const q of qs) { const r = lookupAnswerDetailed(q); if (!r.answer) miss++; else if (r.via === "id") id++; else if (r.via === "body") body++; else title++; } return { total: qs.length, id, title, body, miss }; }, all: loadQBank, fill: clickAnswerOnPage, exam: () => state.exam, clearExam: clearExamCtx, answerCard: getExamAnswerCardCount, dva: () => ({ store: !!getDvaStore(), binding: findExamDvaBinding() }), reloadExam: reloadExamPaperToPage, autoExam: runExamAutoAnswer, }; } function fillCurrentQuestionFromQBank() { const ex = state.exam; let q = null; const titleNode = document.querySelector( ".question-title, .exam-title, [class*='questionTitle'], [class*='QuestionTitle'], .ant-card-head-title" ) || document.querySelector("h3, h4, .title"); const domTitle = titleNode ? normalizeTitle(titleNode.innerHTML || titleNode.textContent) : ""; if (ex.questions && ex.questions.length) { if (domTitle) { q = ex.questions.find((item) => normalizeTitle(item.Title) === domTitle); } if (!q) { const numMatch = (document.body.innerText || "").match(/(\d+)\s*[\u3001.\uff0e]/); const idx = numMatch ? toInt(numMatch[1], 1) - 1 : 0; q = ex.questions[idx] || ex.questions[0]; } } else if (domTitle) { const bank = loadQBank().byQuestionId || {}; for (const item of Object.values(bank)) { if (normalizeTitle(item.title) === domTitle) { if (clickAnswerOnPage(item.answer)) trace(`\u5df2\u9009 → ${item.answer}`); else trace(`\u7b54\u6848: ${item.answer}`); return; } } } if (q) { const ans = lookupAnswer(q); if (!ans) { trace(`\u9898\u5e93\u65e0 Q${q.ID}\uff08${normalizeTitle(q.Title)}\uff09`); return; } const myAnswer = formatExamAnswer(q, ans); if (clickAnswerOnPage(myAnswer)) trace(`\u5df2\u9009 Q${q.ID} → ${myAnswer}`); else trace(`Q${q.ID} \u7b54\u6848: ${myAnswer}`); return; } const active = document.querySelector( '.question.active, .question-item.active, [class*="current"] [class*="question"], .ant-radio-wrapper-checked' ); let qid = 0; if (active) { const m = (active.innerHTML || "").match(/Question_ID["':\s]+(\d+)/i); if (m) qid = toInt(m[1]); } if (!qid) { trace(`\u672a\u80fd\u8bc6\u522b\u9898\u53f7\uff0c\u7f13\u5b58\u5171 ${qbankCount()} \u9898`); return; } const ans = getStoredAnswer(qid); if (!ans) { trace(`\u9898\u5e93\u65e0 Q${qid} \u7684\u7b54\u6848\uff0c\u8bf7\u5148\u51c6\u5907`); return; } if (clickAnswerOnPage(ans)) trace(`\u5df2\u9009 Q${qid} → ${ans.slice(0, 40)}`); else trace(`Q${qid} \u7b54\u6848: ${ans.slice(0, 60)}\uff08\u8bf7\u624b\u52a8\u9009\uff09`); } function chapterTitle(node) { return node.Name || node.ChapterName || node.CourseWare_Name || `\u7ae0\u8282${node.ID}`; } function isLeafChapter(node) { const children = node.ChildNodeList; if (children && children.length) return false; const wt = node.CourseWareType_ID ?? node.WareType; if (wt != null && ![0, 1, 2].includes(toInt(wt))) return false; return node.ID != null; } function* iterLeaves(nodes) { for (const node of nodes || []) { const children = node.ChildNodeList; if (children && children.length) yield* iterLeaves(children); else if (isLeafChapter(node)) yield node; } } function chapterDuration(ch) { const d = toInt(ch.Duration); if (d > 0) return d; return toInt(ch.VideoDuration || ch.WareDuration || ch.TimeLength); } function chapterLearned(ch) { const total = Number(ch.TotalSecond); if (Number.isFinite(total) && total > 0) return Math.floor(total); return toInt(ch.PlayRecord || ch.LookDuration || ch.LastWatchPos || ch.StudyDuration); } function isChapterDone(ch) { const id = toInt(ch.ID); const isLook = toInt(ch.IsLook); if (id > 0 && isLook === id) return true; const duration = chapterDuration(ch); const learned = chapterLearned(ch); if (toInt(ch.LookEndTime) > 0) return true; if (duration > 0 && learned >= duration) return true; const prog = ch.Progress ?? ch.StudyProgress; if (prog != null && toInt(prog) >= 100) return true; return false; } function isVideoCourse(course) { const courseId = toInt(course.Course_ID || course.spcuCourseId); const chapters = toInt(course.CourseChapters); return courseId > 0 && chapters > 0; } async function fetchCourseTree(user) { const res = await api("GET", "/eduSuper/Specialty/GetStuSpecialtyCurriculumList", { params: { StuDetail_ID: user.StuDetail_ID, IsStudyYear: 1, StuID: user.StuID, }, }); if (!res.SuccessResponse) throw new Error(res.Message || "\u83b7\u53d6\u8bfe\u7a0b\u5931\u8d25"); const data = res.Data || {}; let list = data.list || data.rows || data; if (!Array.isArray(list)) list = []; const byTerm = {}; for (const course of list) { const term = toInt(course.StudyYear, 0); if (!byTerm[term]) byTerm[term] = []; byTerm[term].push(course); } const terms = Object.keys(byTerm) .map((k) => toInt(k)) .sort((a, b) => a - b); return { meta: { total: toInt(res.TotalCount, list.length), curStudyYear: data.CurStudyYear, curStudyBatch: data.CurStudyBatch, stuLearningTime: data.StuLearningTime || "", isShowLearnTip: !!data.IsShowLearnTip, }, terms, byTerm, all: list, }; } function getSelectableCourses(tree) { if (!tree) return []; return tree.all.filter((c) => isVideoCourse(c)); } function getCurrentTerm(tree) { if (!tree || !tree.meta) return 0; return toInt(tree.meta.curStudyYear, 0); } function applyTermSelection(tree, term) { state.viewTerm = term; state.selectedCurriculumIds = new Set(); if (!term) { saveSelectedIds(); saveViewTerm(0); return; } getTermCourses(tree, term).forEach((c) => { state.selectedCurriculumIds.add(String(c.Curriculum_ID)); }); saveSelectedIds(); saveViewTerm(term); } function applyCurrentTermDefault(tree) { const current = getCurrentTerm(tree); const saved = loadSavedViewTerm(); let term = current; if (saved > 0 && termExistsInTree(tree, saved)) { term = saved; } else if (current > 0) { term = current; } else if (saved > 0) { term = saved; } applyTermSelection(tree, term); } async function fetchChapters(user, courseId, curriculumId) { const res = await api("GET", "/eduSuper/Question/GetCourse_ChaptersNodeList", { params: { Valid: 1, Course_ID: courseId, StuID: user.StuID, Curriculum_ID: curriculumId, Examination_ID: 0, StuDetail_ID: user.StuDetail_ID, }, }); if (!res.SuccessResponse) return []; const data = res.Data; return Array.isArray(data) ? data : []; } async function buildTasks(onlyIncomplete = true, coursesFilter = null, opts = {}) { const { quiet = false } = opts; const user = getUser(); if (!user.StuID || !user.StuDetail_ID) throw new Error("\u8bf7\u5148\u767b\u5f55\uff08\u9875\u9762\u9700\u6709 user \u4fe1\u606f\uff09"); if (!state.courseTree) { state.courseTree = await fetchCourseTree(user); applyCurrentTermDefault(state.courseTree); } let courses = getSelectedVideoCourses(state.courseTree); if (coursesFilter) { const idSet = new Set(coursesFilter.map(String)); courses = courses.filter((c) => idSet.has(String(c.Curriculum_ID))); } const tasks = []; if (!coursesFilter && !quiet) trace(`\u5df2\u9009\u7f51\u8bfe ${courses.length} \u95e8\uff0c\u6b63\u5728\u62c9\u53d6\u7ae0\u8282…`); for (const course of courses) { if (state.stopping) break; const courseId = toInt(course.Course_ID || course.spcuCourseId); const curriculumId = toInt(course.Curriculum_ID); const term = termLabel(course.StudyYear); const courseName = course.CuName || course.Name || String(courseId); const chapters = await fetchChapters(user, courseId, curriculumId); for (const ch of iterLeaves(chapters)) { if (onlyIncomplete && isChapterDone(ch)) continue; let duration = chapterDuration(ch); let learned = chapterLearned(ch); if (duration <= 0) duration = Math.max(learned, 300); tasks.push({ courseId, curriculumId, chapterId: toInt(ch.ID), studyYear: toInt(course.StudyYear), title: `${term} · ${courseName} / ${chapterTitle(ch)}`, durationSec: duration, posSec: Math.min(learned, duration), lookType: toInt(ch.LookType, 0), }); } } return tasks; } function parseExamScoreVal(raw) { if (raw === null || raw === undefined || raw === "") return -1; const n = parseFloat(String(raw).replace(/[^\d.]/g, "")); return Number.isFinite(n) ? n : -1; } function courseHasExamEntry(course) { return toInt(course.IsExam) > 0 || toInt(course.IsFinanceExam) > 0; } function normalizeExamInfo(meta) { if (!meta) return null; const score = parseExamScoreVal(meta.ExamScore ?? meta.Score); const resultCount = toInt(meta.ResultCount, 0); const examCount = toInt(meta.ExamCount, EXAM_MAX_ATTEMPTS_DEFAULT) || EXAM_MAX_ATTEMPTS_DEFAULT; const done = score >= 100 || resultCount >= examCount; return { score, resultCount, examCount, done, loading: false, noExam: false }; } function getCourseExamInfo(course) { const cid = String(course.Curriculum_ID); if (state.examInfoByCid[cid]) return state.examInfoByCid[cid]; const score = parseExamScoreVal(course.ExamScore ?? course.CourseExamScore ?? course.Score); const resultCount = toInt(course.ResultCount, 0); const examCount = toInt(course.ExamCount, EXAM_MAX_ATTEMPTS_DEFAULT) || EXAM_MAX_ATTEMPTS_DEFAULT; if (score >= 0 || resultCount > 0) { const done = score >= 100 || resultCount >= examCount; return { score, resultCount, examCount, done, loading: false, noExam: false }; } if (!courseHasExamEntry(course)) { return { score: -1, resultCount: 0, examCount: 0, done: false, loading: false, noExam: true }; } return { score: -1, resultCount: 0, examCount, done: false, loading: true, noExam: false }; } function formatExamInfoLabel(info) { if (!info) return "\u8003\u8bd5 —"; if (info.noExam) return "\u65e0\u671f\u672b\u8003\u8bd5"; if (info.loading) return "\u8003\u8bd5\u52a0\u8f7d\u4e2d…"; if (info.error) return "\u8003\u8bd5 —"; const attempts = info.examCount > 0 ? `${info.resultCount}/${info.examCount}\u6b21` : ""; if (info.score >= 0) { const tail = info.done ? " · \u5df2\u5b8c\u6210" : attempts ? ` · ${attempts}` : ""; return `\u8003\u8bd5 ${info.score}\u5206${tail}`; } if (info.resultCount > 0) return `\u8003\u8bd5 \u5df2\u8003 ${attempts}${info.done ? " · \u5df2\u5b8c\u6210" : ""}`; return attempts ? `\u8003\u8bd5 \u672a\u8003 · 0/${info.examCount}\u6b21` : "\u8003\u8bd5 \u672a\u8003"; } function formatExamScoreBadge(info) { if (!info || info.loading || info.noExam || info.error) return ""; if (info.score < 0) return ""; return `${info.score}\u5206`; } async function refreshCourseExamInfo(courses) { const user = getUser(); if (!user.StuID || !courses?.length) return; if (state.examInfoLoading) return; state.examInfoLoading = true; renderCoursePanel(); try { for (const c of courses) { if (state.stopping) break; const cid = String(c.Curriculum_ID); if (!courseHasExamEntry(c)) { state.examInfoByCid[cid] = { score: -1, resultCount: 0, examCount: 0, done: false, loading: false, noExam: true, }; continue; } try { const meta = await fetchFinalExamMeta(user, toInt(c.Curriculum_ID)); state.examInfoByCid[cid] = normalizeExamInfo(meta); } catch (_) { state.examInfoByCid[cid] = { score: -1, resultCount: 0, examCount: EXAM_MAX_ATTEMPTS_DEFAULT, done: false, loading: false, noExam: false, error: true, }; } if (!(await sleepAbortable(100))) break; } } finally { state.examInfoLoading = false; renderCoursePanel(); updateQueueExamCounts(); updatePanel(); } } async function refreshTermExamInfo() { if (!state.courseTree) return; const term = getViewTerm(state.courseTree); const courses = getTermCourses(state.courseTree, term); await refreshCourseExamInfo(courses); } function isCourseExamDone(course) { const info = getCourseExamInfo(course); if (info && !info.loading && !info.error) { if (info.done) return true; if (info.score >= 100) return true; if (info.examCount > 0 && info.resultCount >= info.examCount) return true; } const score = parseExamScoreVal(course.CourseExamScore ?? course.ExamScore ?? course.Score); if (score >= 100) return true; const resultCount = toInt(course.ResultCount, 0); const examCount = toInt(course.ExamCount, EXAM_MAX_ATTEMPTS_DEFAULT) || EXAM_MAX_ATTEMPTS_DEFAULT; if (resultCount >= examCount) return true; const pass = String(course.IsExamPass || course.ExamPass || "").toLowerCase(); if (pass === "1" || pass === "true") return true; const finance = toInt(course.IsFinanceExam, 0); if ((finance === 2 || finance === 3) && score >= 0) return true; return false; } function updateQueueExamCounts() { if (!state.courseTree) { state.queueExamTotal = 0; state.queueExamDone = 0; return; } const term = getViewTerm(state.courseTree); const courses = getTermCourses(state.courseTree, term).filter((c) => state.selectedCurriculumIds.has(String(c.Curriculum_ID)) ); state.queueExamTotal = courses.length; state.queueExamDone = courses.filter(isCourseExamDone).length; } function buildChapterPreview() { const source = state.chapterPreviewTasks.length ? state.chapterPreviewTasks : state.tasks; const groups = new Map(); for (const t of source) { const key = String(t.curriculumId); if (!groups.has(key)) { const parts = t.title.split(" / "); const cp = (parts[0] || "").split(" · "); groups.set(key, { curriculumId: t.curriculumId, courseTitle: cp[1] || cp[0] || `\u8bfe\u7a0b ${key}`, chapters: [], }); } const parts = t.title.split(" / "); let duration = toInt(t.durationSec, 0); let progress = toInt(t.posSec, 0); const liveTask = state.tasks.find((x) => String(x.chapterId) === String(t.chapterId)); if (liveTask) { progress = toInt(liveTask.posSec, progress); duration = toInt(liveTask.durationSec, duration) || duration; } const active = state.activeStudy[String(t.chapterId)]; const chapterId = String(t.chapterId); if (active) { progress = toInt(active.pos, progress); duration = toInt(active.total, duration) || duration; } const done = duration > 0 && progress >= duration; groups.get(key).chapters.push({ chapterId, title: parts[1] || t.title, progress, duration, done, active: !!active, }); } state.chapterPreview = [...groups.values()]; state.queueVideoTotal = state.chapterPreviewTasks.length || state.tasks.length; state.queueVideoDone = state.chapterPreviewTasks.length ? state.chapterPreviewTasks.filter((t) => { const d = toInt(t.durationSec, 0); const p = toInt(t.posSec, 0); const live = state.tasks.find((x) => String(x.chapterId) === String(t.chapterId)); const active = state.activeStudy[String(t.chapterId)]; if (active) return toInt(active.total, d) > 0 && toInt(active.pos, 0) >= toInt(active.total, d); if (live) return toInt(live.durationSec, d) > 0 && toInt(live.posSec, p) >= toInt(live.durationSec, d); return d > 0 && p >= d; }).length : Math.min(state.doneCount, state.queueVideoTotal); updateQueueExamCounts(); } function scheduleChapterPreviewRender() { if (chapterPreviewRenderTimer) return; chapterPreviewRenderTimer = setTimeout(() => { chapterPreviewRenderTimer = 0; if (!state.chapterPreviewTasks.length && !state.tasks.length) return; buildChapterPreview(); renderChapterPreview(); }, 300); } function renderChapterPreview() { const box = document.getElementById("ct-chapter-preview"); const tagEl = document.getElementById("ct-chapter-preview-tag"); if (!box) return; if (!state.chapterPreview.length) { if (tagEl) tagEl.textContent = "\u5168\u90e8\u7ae0\u8282"; box.innerHTML = `
\u8bf7\u9009\u62e9\u8bfe\u7a0b\u6216\u7b49\u5f85\u52a0\u8f7d\u7ae0\u8282
`; return; } let chapterTotal = 0; let chapterDone = 0; for (const c of state.chapterPreview) { for (const ch of c.chapters || []) { chapterTotal++; const pct = ch.duration > 0 ? Math.max(0, Math.min(100, Math.round((ch.progress / ch.duration) * 100))) : 0; if (ch.done || pct >= 100) chapterDone++; } } if (tagEl) { tagEl.textContent = `${state.chapterPreview.length} \u95e8 · ${chapterTotal} \u8282 · \u5df2\u5b66 ${chapterDone}`; } box.innerHTML = state.chapterPreview .map((c) => { const rows = (c.chapters || []) .map((ch) => { const pct = ch.duration > 0 ? Math.max(0, Math.min(100, Math.round((ch.progress / ch.duration) * 100))) : 0; const done = ch.done || pct >= 100; const active = !!ch.active && !done; const barColor = done ? "#16a34a" : active ? "#2563eb" : "#94a3b8"; const progText = done ? "\u5df2\u5b8c\u6210" : ch.duration > 0 ? `${formatSec(ch.progress)}/${formatSec(ch.duration)} · ${pct}%` : `${pct}%`; return `
${esc(ch.title)}
${progText}
`; }) .join(""); return `
${esc( c.courseTitle )}
${rows}
`; }) .join(""); } async function refreshChapterPreview(force = false) { if (!state.courseTree) { state.chapterPreview = []; state.chapterPreviewTasks = []; updateQueueExamCounts(); renderChapterPreview(); updateQueueSummary(); return; } const videoCourses = getSelectedVideoCourses(state.courseTree); updateQueueExamCounts(); if (!videoCourses.length) { _delChapterPreview(); return; } try { if (force || !state.chapterPreviewTasks.length) { state.chapterPreviewTasks = await buildTasks(false, null, { quiet: true }); } buildChapterPreview(); } catch (e) { trace(`\u7ae0\u8282\u9884\u89c8\u5931\u8d25: ${e.message || e}`); } renderChapterPreview(); updateQueueSummary(); } function _delChapterPreview() { state.chapterPreview = []; state.chapterPreviewTasks = []; state.queueVideoTotal = 0; state.queueVideoDone = 0; renderChapterPreview(); updateQueueSummary(); } function renderCoursePanel() { const box = document.getElementById("ct-course-list"); const termSel = document.getElementById("ct-term-select"); if (!box || !state.courseTree) return; const { meta, terms } = state.courseTree; const viewTerm = getViewTerm(state.courseTree); const currentTerm = getCurrentTerm(state.courseTree); const courses = getTermCourses(state.courseTree, viewTerm); const learnTime = meta.stuLearningTime ? `\u5b66\u4e60\u65f6\u6bb5 ${meta.stuLearningTime}` : ""; if (termSel) { termSel.innerHTML = terms .map((t) => { const cur = t === currentTerm ? "\uff08\u5f53\u524d\uff09" : ""; const sel = t === viewTerm ? " selected" : ""; return ``; }) .join(""); } if (!courses.length) { box.innerHTML = `
\u5f53\u524d\u5b66\u671f\u6682\u65e0\u8bfe\u7a0b
\u53ef\u5207\u6362\u4e0a\u65b9\u5b66\u671f\u67e5\u770b
`; return; } const videoN = courses.filter(isVideoCourse).length; const allChecked = courses.length > 0 && courses.every((c) => state.selectedCurriculumIds.has(String(c.Curriculum_ID))); let html = ""; if (learnTime) { html += `
${esc(learnTime)}
`; } html += ``; for (const c of courses) { const cid = String(c.Curriculum_ID); const hasVideo = isVideoCourse(c); const read = toInt(c.CourseReadChapters); const total = toInt(c.CourseChapters); const pct = total > 0 ? Math.round((read / total) * 100) : 0; const checked = state.selectedCurriculumIds.has(cid); const finished = hasVideo && total > 0 && read >= total; const border = finished ? "#e2e8f0" : "#e2e8f0"; const bgColor = finished ? "#f1f5f9" : "#f8fafc"; const titleColor = finished ? "#64748b" : "#0f172a"; const sub = hasVideo ? `\u7ae0\u8282 ${read}/${total} · \u5b66${pct}%` : "\u65e0\u7f51\u8bfe\uff08\u4ec5\u8003\u8bd5\uff09"; const examInfo = getCourseExamInfo(c); const examLabel = formatExamInfoLabel(examInfo); const examBadge = formatExamScoreBadge(examInfo); const examDone = isCourseExamDone(c); const barPct = hasVideo ? pct : 0; const barColor = barPct >= 100 ? "#16a34a" : "#2563eb"; const cardOpacity = finished && examDone ? "opacity:.88;" : finished || examDone ? "opacity:.92;" : ""; html += ``; } box.innerHTML = html; const allCb = document.getElementById("ct-term-all-cb"); if (allCb) { allCb.addEventListener("change", () => { courses.forEach((c) => { const id = String(c.Curriculum_ID); if (allCb.checked) state.selectedCurriculumIds.add(id); else state.selectedCurriculumIds.delete(id); }); saveSelectedIds(); renderCoursePanel(); void refreshChapterPreview(true); updatePanel(); }); } box.querySelectorAll(".ct-course-cb").forEach((cb) => { cb.addEventListener("change", () => { const id = cb.getAttribute("data-cid"); if (cb.checked) state.selectedCurriculumIds.add(id); else state.selectedCurriculumIds.delete(id); saveSelectedIds(); renderCoursePanel(); void refreshChapterPreview(true); updatePanel(); }); }); } function renderCourseTree() { renderCoursePanel(); } async function refreshCourseTree(opts = {}) { const { quiet = false } = opts; const user = getUser(); if (!user.StuID) throw new Error("\u8bf7\u5148\u767b\u5f55"); state.courseTree = await fetchCourseTree(user); applyCurrentTermDefault(state.courseTree); renderCoursePanel(); const viewTerm = getViewTerm(state.courseTree); const courses = getTermCourses(state.courseTree, viewTerm); const videoN = courses.filter(isVideoCourse).length; const msg = `\u8bfe\u8868\u5df2\u5237\u65b0\uff1a${termLabel(viewTerm)}\uff0c\u5df2\u9009 ${state.selectedCurriculumIds.size} \u95e8\uff08${videoN} \u95e8\u7f51\u8bfe\uff09`; if (!quiet) { const now = Date.now(); if (msg !== lastCourseTreeLogMsg || now - lastCourseTreeLogAt >= 5000) { trace(msg); lastCourseTreeLogMsg = msg; lastCourseTreeLogAt = now; } } void refreshTermExamInfo(); await refreshChapterPreview(true); updatePanel(); } async function saveProgress(chapterId, lookTime, lastWatchPos, lookType = 0) { await acquireSaveSlot(); try { return await api("POST", "/datastore/WebCourse/SaveCourse_Look", { body: { CourseChapters_ID: chapterId, LookType: lookType, LookTime: lookTime, LastWatchPos: lastWatchPos, IP: (window.returnCitySN && window.returnCitySN.cip) || "127.0.0.1", }, }); } finally { releaseSaveSlot(); } } function isFastMode() { return localStorage.getItem(STORAGE_FAST) === "1"; } function parallelCount(taskTotal) { if (!isPro()) return 1; const total = Math.max(1, toInt(taskTotal, 1)); const raw = localStorage.getItem(STORAGE_PARALLEL); if (raw === null || raw === "" || raw === "0" || raw === "all") { return total; } const n = toInt(raw, 0); if (n <= 0) return total; return Math.min(n, total); } function isParallelAll() { const raw = localStorage.getItem(STORAGE_PARALLEL); return raw === null || raw === "" || raw === "0" || raw === "all"; } async function runTasksParallel(tasks, limit) { let cursor = 0; state.doneCount = 0; state.activeCount = 0; state.activeStudy = {}; state.studyLogAt = {}; async function worker(workerIdx) { if (workerIdx > 0 && !(await sleepAbortable(workerIdx * 800))) return; while (!state.stopping) { const i = cursor++; if (i >= tasks.length) return; state.activeCount++; updatePanel(); const task = tasks[i]; try { const ok = await studyChapter(task); if (state.stopping) return; if (ok) { state.doneCount++; state.taskIndex = state.doneCount; state.queueVideoDone = state.doneCount; buildChapterPreview(); renderChapterPreview(); if (state.doneCount === 1 || state.doneCount === tasks.length || state.doneCount % 10 === 0) { trace(`\u5237\u8bfe\u8fdb\u5ea6 ${state.doneCount}/${tasks.length}`); } } } catch (e) { if (!state.stopping) trace(`\u5931\u8d25: ${shortChapterLabel(task)} — ${e.message || e}`); } finally { clearActiveStudy(task); state.activeCount--; updatePanel(); } } } const n = Math.min(limit, tasks.length); await Promise.all(Array.from({ length: n }, (_, i) => worker(i))); } async function studyChapter(task) { const { course, chapter } = taskStudyMeta(task); const pos = task.posSec; const total = task.durationSec; setActiveStudyProgress(task, pos, total); state.studyLogAt[String(task.chapterId)] = Date.now(); const start = await _es( "video_chapter", { chapter_id: task.chapterId, pos_sec: pos, duration_sec: total, look_type: task.lookType, title: `${course} / ${chapter}`, }, { pack_sec: PACK_SEC_NORMAL, free_chapter_limit: state.freeChapterLimit } ); const out = await _ve(start, { quiet: true, maxSteps: 600 }); if (state.stopping) return false; if (!out.ok) { if (out.msg === "free_quota_exhausted") return false; throw new Error(out.msg || "\u5237\u8bfe\u5931\u8d25"); } task.posSec = total; syncPreviewTaskProgress(task); scheduleChapterPreviewRender(); maybeLogStudyHeartbeat(task, total, total); return true; } async function runStudyLoop() { const user = getUser(); if (!user.StuID) { trace("\u672a\u767b\u5f55\uff0c\u8bf7\u5148\u624b\u52a8\u767b\u5f55"); return; } const videoCourses = getSelectedVideoCourses(state.courseTree); if (!videoCourses.length) { trace("\u5f53\u524d\u5b66\u671f\u65e0\u5df2\u9009\u7f51\u8bfe\uff0c\u8df3\u8fc7\u5237\u8bfe"); return; } if (state.studyRunning) return; if (!(await _cr())) return; localStorage.setItem(STORAGE_FAST, "0"); state.studyRunning = true; updatePanel(); trace(`\u5f00\u59cb\u5b66\u4e60\u8bfe\u7a0b\uff1a${schoolDisplayName()}`); try { state.chapterPreviewTasks = await buildTasks(false); state.tasks = await buildTasks(true); if (!isPro()) { const remain = Math.max(0, state.freeChapterLimit - state.freeUsedChapters); if (remain <= 0) { trace(`\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${state.freeUsedChapters}/${state.freeChapterLimit}\uff09`, "warn"); return; } if (state.tasks.length > remain) { state.tasks = state.tasks.slice(0, remain); trace(`\u514d\u8d39\u7248\u672c\u6b21\u5237 ${remain} \u8282`); } } state.taskIndex = 0; state.queueVideoTotal = state.tasks.length; buildChapterPreview(); renderChapterPreview(); updateQueueSummary(); if (!state.tasks.length) { trace("\u6240\u9009\u7f51\u8bfe\u6ca1\u6709\u5f85\u5237\u7ae0\u8282"); return; } trace(`\u5171 ${state.tasks.length} \u8282\u5f85\u5237`); await runTasksParallel(state.tasks, parallelCount(state.tasks.length)); if (!state.stopping) { trace("\u7f51\u8bfe\u7ae0\u8282\u5df2\u5168\u90e8\u5904\u7406\u5b8c\u6bd5"); await refreshCourseTree({ quiet: true }); } } catch (e) { trace(`\u5237\u8bfe\u5f02\u5e38: ${e.message || e}`); } finally { state.studyRunning = false; state.activeStudy = {}; state.studyLogAt = {}; if (chapterPreviewRenderTimer) { clearTimeout(chapterPreviewRenderTimer); chapterPreviewRenderTimer = 0; } if (state.tasks.length) { buildChapterPreview(); renderChapterPreview(); } if (!state.qbankRunning && !state.examRunning) state.pipelineStarted = false; if (state.stopping) { state.currentTask = "\u5df2\u505c\u6b62"; trace("\u5237\u8bfe\u5df2\u505c\u6b62"); } else { state.currentTask = "\u5237\u8bfe\u9636\u6bb5\u7ed3\u675f"; trace(`\u5b66\u6821\u8bfe\u7a0b\u7ed3\u675f ${state.doneCount}/${state.tasks.length || state.doneCount} \u8282`); } updatePanel(); } } function stopStudy() { if (!isBusy()) return; state.stopping = true; state.pipelineStarted = false; state.currentTask = "\u6b63\u5728\u505c\u6b62…"; trace("\u6b63\u5728\u505c\u6b62\u5168\u90e8\u4efb\u52a1…"); updatePanel(); } function hookVideoPage() { const hash = location.hash || ""; if (!/\/onlineclass\//i.test(hash) && !/\/video\//i.test(hash)) return; let hooked = false; const timer = setInterval(() => { if (hooked || !window.video_player) return; hooked = true; clearInterval(timer); trace("\u68c0\u6d4b\u5230\u89c6\u9891\u9875\uff0c\u53ef\u70b9\u300c\u5237\u5f53\u524d\u9875\u300d"); const btn = document.getElementById("ct-study-current"); if (btn) btn.style.display = "inline-block"; }, 1500); } async function studyCurrentVideoPage() { const user = getUser(); if (!user.StuID) { trace("\u672a\u767b\u5f55"); return; } const chapterId = toInt(sessionStorage.getItem("bufferChapterID")); if (!chapterId) { trace("\u672a\u8bc6\u522b\u5f53\u524d\u7ae0\u8282 ID"); return; } state.studyRunning = true; updatePanel(); let duration = 600; let pos = 0; try { if (window.video_player && typeof window.video_player.duration === "function") { duration = Math.ceil(window.video_player.duration() || duration); pos = Math.floor(window.video_player.currentTime() || 0); } } catch (_) {} const task = { chapterId, title: `\u5f53\u524d\u9875\u7ae0\u8282 ${chapterId}`, durationSec: duration, posSec: pos, lookType: 0, }; try { await studyChapter(task); } catch (e) { trace(`\u5f53\u524d\u9875\u5237\u8bfe\u5931\u8d25: ${e.message || e}`); } finally { state.studyRunning = false; updatePanel(); } } function switchPanelTab(name) { document.querySelectorAll(".ct-tab-btn").forEach((el) => { el.classList.toggle("active", el.dataset.tab === name); }); document.querySelectorAll(".ct-pane").forEach((el) => { el.classList.toggle("active", el.dataset.pane === name); }); } function clearRunLog() { state.logLines = []; const box = document.getElementById("ct-run-log"); if (box) box.innerHTML = ""; } function readPanelPos() { try { return JSON.parse(localStorage.getItem(STORAGE_PANEL_POS) || "{}"); } catch (_) { return null; } } function writePanelPos(left, top) { localStorage.setItem(STORAGE_PANEL_POS, JSON.stringify({ left, top })); } function readPanelCollapsed() { return localStorage.getItem(STORAGE_COLLAPSED) === "1"; } function writePanelCollapsed(collapsed) { localStorage.setItem(STORAGE_COLLAPSED, collapsed ? "1" : "0"); } function clampPanelInViewport(panel, writePos = writePanelPos) { if (!panel) return; const maxLeft = Math.max(0, window.innerWidth - panel.offsetWidth); const maxTop = Math.max(0, window.innerHeight - panel.offsetHeight); const rect = panel.getBoundingClientRect(); const left = Math.max(0, Math.min(maxLeft, rect.left)); const top = Math.max(0, Math.min(maxTop, rect.top)); panel.style.right = "auto"; panel.style.bottom = "auto"; panel.style.left = `${left}px`; panel.style.top = `${top}px`; writePos(left, top); } function readExamPanelPos() { try { return JSON.parse(localStorage.getItem(STORAGE_EXAM_PANEL_POS) || "{}"); } catch (_) { return null; } } function writeExamPanelPos(left, top) { localStorage.setItem(STORAGE_EXAM_PANEL_POS, JSON.stringify({ left, top })); } function readExamPanelCollapsed() { return localStorage.getItem(STORAGE_EXAM_PANEL_COLLAPSED) === "1"; } function writeExamPanelCollapsed(collapsed) { localStorage.setItem(STORAGE_EXAM_PANEL_COLLAPSED, collapsed ? "1" : "0"); } function applyExamPanelCollapsed(panel, collapsed) { const btnMin = panel.querySelector("#ct-exam-btn-min"); const btnMax = panel.querySelector("#ct-exam-btn-max"); panel.classList.toggle("ct-exam-panel-min", !!collapsed); panel.classList.toggle("ct-exam-panel-max", !collapsed); if (btnMin) btnMin.style.display = collapsed ? "none" : ""; if (btnMax) btnMax.style.display = collapsed ? "" : "none"; clampPanelInViewport(panel, writeExamPanelPos); } function enableExamPanelDrag(panel) { const header = panel.querySelector("#ct-exam-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("#ct-exam-panel-controls, .ct-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; panel.style.left = `${startLeft + e.clientX - startX}px`; panel.style.top = `${startTop + e.clientY - startY}px`; }); document.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; clampPanelInViewport(panel, writeExamPanelPos); }); } function applyPanelCollapsed(panel, collapsed) { const btnMin = panel.querySelector("#ct-btn-min"); const btnMax = panel.querySelector("#ct-btn-max"); panel.classList.toggle("ct-panel-min", !!collapsed); panel.classList.toggle("ct-panel-max", !collapsed); if (btnMin) btnMin.style.display = collapsed ? "none" : ""; if (btnMax) btnMax.style.display = collapsed ? "" : "none"; const footerExtra = panel.querySelector(".ct-footer-extra"); if (footerExtra) footerExtra.style.display = collapsed ? "none" : ""; clampPanelInViewport(panel); } function enablePanelDrag(panel) { const header = panel.querySelector("#ct-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("#ct-panel-controls, .ct-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; panel.style.left = `${startLeft + e.clientX - startX}px`; panel.style.top = `${startTop + e.clientY - startY}px`; }); document.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; clampPanelInViewport(panel); }); } function updateQueueSummary() { const doneEl = document.getElementById("ct-queue-done"); const totalEl = document.getElementById("ct-queue-total"); const pctEl = document.getElementById("ct-queue-percent"); const barEl = document.getElementById("ct-queue-progress"); const examEl = document.getElementById("ct-queue-exam"); const textEl = document.getElementById("ct-queue-text"); const sel = state.selectedCurriculumIds.size; const total = Math.max(0, Number(state.queueVideoTotal || 0)); const done = Math.max(0, Math.min(total, Number(state.queueVideoDone || state.doneCount || 0))); const examTotal = Math.max(0, Number(state.queueExamTotal || 0)); const examDone = Math.max(0, Math.min(examTotal, Number(state.queueExamDone || 0))); if (doneEl) doneEl.textContent = String(done); if (totalEl) totalEl.textContent = String(total); const pct = total > 0 ? Math.max(0, Math.min(100, Math.round((done / total) * 100))) : 0; if (pctEl) pctEl.textContent = `${pct}%`; if (barEl) barEl.style.width = `${pct}%`; if (examEl) examEl.textContent = examTotal > 0 ? `${examDone} / ${examTotal}` : "—"; if (textEl) { if (!sel) textEl.textContent = "\u672a\u9009\u62e9\u8bfe\u7a0b"; else if (total > 0) { const examPart = examTotal > 0 ? ` · \u8003\u8bd5 ${examDone}/${examTotal}` : ""; textEl.textContent = `\u89c6\u9891\u5df2\u5b66\u4e60 ${done}/${total}${examPart}\uff08\u5df2\u9009 ${sel} \u95e8\uff09`; } else { textEl.textContent = `\u5df2\u9009 ${sel} \u95e8\uff0c\u6b63\u5728\u7edf\u8ba1\u7ae0\u8282…`; } } } function updateCurrentMetaPanel() { const courseEl = document.getElementById("ct-current-course"); const chapterEl = document.getElementById("ct-current-chapter"); const taskEl = document.getElementById("ct-current-task"); if (courseEl) { courseEl.textContent = state.currentCourse || "\u65e0"; courseEl.title = state.currentCourse || ""; } if (chapterEl) { chapterEl.textContent = state.currentChapter || "\u65e0"; chapterEl.title = state.currentChapter || ""; } if (taskEl) { taskEl.textContent = state.currentTask || "\u5f85\u547d"; taskEl.title = state.currentTask || ""; } } function syncStartStopButtons() { const running = isBusy(); const startBtn = document.getElementById("ct-start"); const stopBtn = document.getElementById("ct-stop"); if (startBtn) { startBtn.disabled = running; startBtn.classList.toggle("ct-btn-off", running); } if (stopBtn) { stopBtn.disabled = !running; stopBtn.classList.toggle("ct-btn-off", !running); } } function updatePanel() { syncStartStopButtons(); const statusEl = document.getElementById("ct-auto-status"); if (statusEl) { if (state.panelRefreshing) statusEl.textContent = "\u5237\u65b0\u4e2d…"; else if (state.examRunning) statusEl.textContent = "\u8003\u8bd5\u4e2d"; else if (state.qbankRunning && state.studyRunning) statusEl.textContent = "\u5237\u8bfe + \u51c6\u5907\u9898\u5e93"; else if (state.qbankRunning) statusEl.textContent = "\u51c6\u5907\u9898\u5e93\u4e2d"; else if (state.studyRunning) statusEl.textContent = `\u5237\u8bfe ${state.doneCount}/${state.tasks.length || "?"}`; else if (state.pipelineStarted) statusEl.textContent = "\u81ea\u52a8\u6d41\u7a0b"; else if (state.stopping) statusEl.textContent = "\u505c\u6b62\u4e2d…"; else statusEl.textContent = "\u5df2\u505c\u6b62"; } updateCurrentMetaPanel(); updateQueueSummary(); updateQBankCountUI(); if (state.chapterPreview.length || state.chapterPreviewTasks.length) scheduleChapterPreviewRender(); } function updatePanelButtons() { updatePanel(); } function updateProgressText() { updatePanel(); } function injectStyles() { const id = "ct-panel-style-v2"; document.getElementById("ct-auto-style")?.remove(); if (document.getElementById(id) || !document.head) return; const style = document.createElement("style"); style.id = id; style.textContent = ` #ct-auto-panel{position:fixed;right:20px;top:80px;z-index:2147483646;width:412px;display:flex;flex-direction:column;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#0f172a;overflow:hidden;transition:max-height .22s ease,box-shadow .22s ease;} #ct-auto-panel.ct-panel-max{max-height:min(92vh,780px);} #ct-auto-panel.ct-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);} #ct-auto-panel.ct-panel-min #ct-panel-header{border-bottom:none;} #ct-auto-panel.ct-panel-min #ct-panel-body,#ct-auto-panel.ct-panel-min #ct-panel-footer,#ct-auto-panel.ct-panel-min .ct-footer-extra{display:none !important;} #ct-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;} #ct-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;} .ct-footer-extra{flex:0 0 auto;} #ct-panel-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;} #ct-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #ct-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;} #ct-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.32;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} #ct-panel-sub{display:flex;flex-wrap:wrap;align-items:center;gap:3px 5px;margin-top:3px;line-height:1.32;} .ct-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;} .ct-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);} .ct-sub-dot{color:#d6d3d1;font-size:10px;} .ct-panel-ctl .ct-ctl-ico{display:block;box-sizing:border-box;margin:0 auto;} .ct-ctl-min{width:10px;height:2px;background:#64748b;border-radius:1px;} .ct-ctl-plus{font-size:16px;line-height:1;font-weight:700;color:#64748b;} .ct-panel-version{display:inline-block;vertical-align:middle;margin-left:5px;font-size:11px;font-weight:900;color:#64748b;padding:1px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;line-height:1.2;} #ct-panel-controls{display:flex;align-items:center;gap:5px;flex:0 0 auto;} .ct-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;} .ct-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .ct-card-status{padding:6px 8px;margin-bottom:6px;} .ct-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;line-height:1.28;} .ct-status-label{font-size:11px;color:#64748b;font-weight:700;} #ct-auto-status{padding:2px 7px;border-radius:999px;font-weight:900;font-size:11px;background:#fff;border:1px solid #cbd5e1;} .ct-status-main{margin-top:3px;} .ct-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;} .ct-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .ct-metric-div{color:#cbd5e1;} .ct-progress-pct{font-size:14px;font-weight:900;color:#0369a1;flex:0 0 auto;} .ct-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;} .ct-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;} .ct-tabbar{display:flex;gap:6px;margin-bottom:7px;} .ct-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;} .ct-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .ct-pane{display:none;}.ct-pane.active{display:block;} .ct-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;} .ct-list-head-actions{display:flex;align-items:center;gap:5px;} .ct-list-title{font-size:12px;color:#64748b;font-weight:700;} .ct-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;} .ct-list-tag-info{color:#1d4ed8;background:#eff6ff;border-color:#93c5fd;} .ct-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;} .ct-plan-row{padding:0 2px 5px;} .ct-plan-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 9px;font-size:12px;background:#fff;color:#0f172a;} #ct-course-list,#ct-chapter-preview,#ct-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} #ct-settings-pane{padding:2px 0;} .ct-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;} .ct-list-meta{font-size:11px;color:#64748b;padding:2px 4px 6px;} .ct-term-all-row{display:flex;align-items:center;gap:6px;padding:4px 6px 8px;font-size:11px;font-weight:700;color:#334155;} .ct-course-card{display:flex;gap:7px;align-items:flex-start;border:1px solid #e2e8f0;border-radius:8px;padding:7px;margin-bottom:6px;cursor:pointer;} .ct-course-card-body{flex:1;min-width:0;} .ct-course-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;} .ct-course-card-name{display:block;font-size:12px;line-height:1.38;flex:1;min-width:0;} .ct-course-exam-score{flex:0 0 auto;font-size:12px;font-weight:900;color:#0369a1;white-space:nowrap;} .ct-course-exam-score.ct-exam-done{color:#16a34a;} .ct-exam-text{color:#0369a1;} .ct-exam-text-done{color:#16a34a;font-weight:700;} .ct-course-card-sub{display:block;font-size:11px;color:#64748b;margin-top:2px;} .ct-course-card-bar{display:block;height:4px;background:#e2e8f0;border-radius:2px;margin-top:5px;overflow:hidden;} .ct-course-card-bar>span{display:block;height:100%;border-radius:2px;} .ct-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;} .ct-chapter-title{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;} .ct-chapter-item{padding:5px 9px;font-size:12px;display:flex;justify-content:space-between;gap:8px;align-items:flex-start;border-bottom:1px solid #f1f5f9;} .ct-chapter-item:last-child{border-bottom:none;} .ct-chapter-item-main{flex:1;min-width:0;} .ct-chapter-item-title{display:block;line-height:1.32;margin-bottom:3px;} .ct-chapter-bar{height:4px;border-radius:999px;background:#e2e8f0;overflow:hidden;} .ct-chapter-bar>span{display:block;height:100%;border-radius:999px;transition:width .25s ease;} .ct-chapter-prog{flex:0 0 auto;font-size:10px;font-weight:700;white-space:nowrap;padding-top:1px;} .ct-chapter-active{background:#eff6ff;} .ct-chapter-done .ct-chapter-item-title{color:#64748b;} .ct-card-current{padding:5px 8px;margin-bottom:6px;} .ct-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:11px;margin-bottom:2px;line-height:1.28;align-items:flex-start;} .ct-meta-row:last-child{margin-bottom:0;} .ct-meta-label{color:#64748b;flex:0 0 auto;font-size:10px;white-space:nowrap;} .ct-meta-value{font-weight:600;text-align:right;flex:1 1 auto;min-width:0;word-break:break-all;line-height:1.28;} .ct-card-actions{padding:6px 8px;} .ct-btn-row{display:flex;gap:7px;margin-bottom:6px;flex-wrap:wrap;} .ct-btn-row-nowrap{flex-wrap:nowrap;} .ct-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;min-width:68px;} .ct-btn-action{min-width:0 !important;flex:1 1 0 !important;} .ct-btn-start{background:#16a34a;}.ct-btn-stop{background:#ef4444;} .ct-btn-start.ct-btn-off,.ct-btn-start:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;pointer-events:none;} .ct-btn-stop.ct-btn-off,.ct-btn-stop:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;pointer-events:none;} .ct-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;flex:0 0 auto;padding:6px 9px;border-radius:10px;cursor:pointer;font-weight:700;font-size:11px;} .ct-btn-ghost:disabled{opacity:.55;cursor:not-allowed;} .ct-btn-pro{border:1px solid #f59e0b;background:linear-gradient(135deg,#fff7ed,#ffedd5);color:#b45309;flex:0 0 auto;padding:6px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:11px;} .ct-btn-ico{font-size:13px;margin-right:5px;} .ct-settings-group{background:#f8fafc;border:1px solid #dbe4f0;border-radius:12px;padding:8px;margin-bottom:8px;} .ct-settings-title{font-size:12px;color:#64748b;font-weight:900;margin-bottom:7px;} .ct-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;} .ct-ann{position:relative;font-size:12px;color:#334155;line-height:1.42;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:11px;padding:8px 9px 8px 11px;} .ct-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;} .ct-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;word-break:break-all;} #ct-notify-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:2147483647;display:none;align-items:center;justify-content:center;padding:20px;} .ct-notify-card{width:min(92vw,380px);background:#fff;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 20px 50px rgba(15,23,42,.22);padding:16px 18px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;color:#0f172a;} .ct-notify-title{font-size:16px;font-weight:900;color:#9a3412;margin-bottom:10px;} .ct-notify-body{font-size:13px;line-height:1.55;color:#334155;} .ct-notify-body p{margin:0 0 8px;} .ct-notify-list{margin:0 0 4px 18px;padding:0;} .ct-notify-list li{margin-bottom:6px;} .ct-notify-actions{display:flex;justify-content:flex-end;margin-top:14px;} .ct-notify-btn{border:none;background:linear-gradient(135deg,#16a34a,#059669);color:#fff;padding:8px 16px;border-radius:10px;font-weight:800;font-size:13px;cursor:pointer;} #ct-exam-assist-panel{position:fixed;right:450px;top:80px;z-index:2147483645;width:400px;display:flex;flex-direction:column;background:#fff;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.16);font-size:12px;color:#0f172a;overflow:hidden;transition:max-height .22s ease,box-shadow .22s ease;} #ct-exam-assist-panel.ct-exam-panel-max{max-height:min(92vh,720px);} #ct-exam-assist-panel.ct-exam-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);} #ct-exam-assist-panel.ct-exam-panel-min #ct-exam-panel-body{display:none !important;} #ct-exam-assist-panel.ct-exam-panel-min #ct-exam-panel-header{border-bottom:none;} #ct-exam-panel-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 12px;background:linear-gradient(135deg,#eff6ff,#f8fafc);border-bottom:1px solid #dbe4f0;cursor:move;user-select:none;} .ct-exam-head-brand{min-width:0;flex:1;} #ct-exam-panel-controls{display:flex;align-items:center;gap:5px;flex:0 0 auto;} #ct-exam-panel-body{flex:1 1 auto;min-height:0;overflow:hidden;display:flex;flex-direction:column;} .ct-exam-head-title{font-size:14px;font-weight:900;color:#1d4ed8;line-height:1.28;} .ct-exam-head-sub{margin-top:3px;font-size:11px;font-weight:600;color:#64748b;line-height:1.32;} .ct-exam-summary{padding:10px 14px;border-bottom:1px solid #e2e8f0;background:#f8fafc;flex:0 0 auto;} .ct-exam-coverage-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;font-size:12px;color:#475569;} .ct-exam-coverage-row strong{font-size:18px;color:#0f172a;} .ct-exam-coverage-row.ct-exam-coverage-warn strong{color:#dc2626;} .ct-exam-coverage-bar{height:6px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-bottom:8px;} .ct-exam-coverage-bar>span{display:block;height:100%;border-radius:999px;background:linear-gradient(90deg,#22c55e,#16a34a);transition:width .25s ease;} .ct-exam-coverage-bar.ct-exam-coverage-warn>span{background:linear-gradient(90deg,#fbbf24,#f59e0b);} .ct-exam-hint{font-size:11px;color:#b45309;background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:6px 8px;margin-bottom:8px;line-height:1.4;} .ct-exam-harvest-btn,.ct-exam-start-btn{flex:1;border:none;color:#fff;padding:8px 10px;border-radius:10px;font-weight:800;font-size:12px;cursor:pointer;} .ct-exam-btn-row{display:flex;gap:8px;margin-top:2px;} .ct-exam-harvest-btn{background:linear-gradient(135deg,#2563eb,#1d4ed8);} .ct-exam-start-btn{background:linear-gradient(135deg,#16a34a,#059669);} .ct-exam-harvest-btn:disabled,.ct-exam-start-btn:disabled{opacity:.55;cursor:not-allowed;} #ct-exam-harvest-log-wrap{padding:0 14px 8px;border-bottom:1px solid #e2e8f0;background:#fff;} .ct-exam-log-title{font-size:11px;font-weight:800;color:#64748b;margin-bottom:4px;} #ct-exam-harvest-log{max-height:120px;overflow-y:auto;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:4px 6px;} .ct-exam-log-row{font-size:11px;line-height:1.38;color:#334155;padding:2px 0;border-bottom:1px dashed #e2e8f0;} .ct-exam-log-row:last-child{border-bottom:none;} #ct-exam-assist-body{flex:1 1 auto;min-height:0;max-height:340px;overflow:auto;padding:10px 12px;background:#f8fafc;} .ct-exam-q{margin-bottom:8px;padding:9px 10px;border:1px solid #e2e8f0;border-radius:10px;background:#fff;} .ct-exam-q-hit{border-color:#bbf7d0;background:#f0fdf4;} .ct-exam-q-miss{border-color:#fecaca;background:#fef2f2;} .ct-exam-q-title{font-size:12px;line-height:1.38;color:#0f172a;margin-bottom:4px;} .ct-exam-q-ans{font-size:11px;font-weight:700;} .ct-exam-q-ans-hit{color:#15803d;} .ct-exam-q-ans-miss{color:#dc2626;} .ct-exam-list-meta{font-size:11px;font-weight:700;color:#475569;padding:2px 2px 8px;} .ct-exam-all-hit{font-size:12px;color:#15803d;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:10px;text-align:center;font-weight:700;} .ct-toggle-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px;} .ct-token-input{width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #cbd5e1;border-radius:8px;font-size:11px;} `; document.head.appendChild(style); } function isExamRouteContext() { const h = location.hash || ""; return /\/exam\/\d+\/\d+/i.test(h) || /\/onlineclass\/paper\//i.test(h); } function removeExamAssistPanel() { document.getElementById("ct-exam-assist-panel")?.remove(); } function buildExamAssistBodyHtml(stats) { if (!stats.total) { return `
\u5f00\u8003\u540e\u663e\u793a\u672a\u547d\u4e2d\u9898\u76ee
`; } let html = `
\u5df2\u5339\u914d ${stats.hit} \u9898 · \u672a\u547d\u4e2d ${stats.miss} \u9898
`; if (!stats.miss) { html += `
\u5168\u90e8\u9898\u76ee\u5df2\u6709\u7b54\u6848
`; return html; } html += stats.misses .slice(0, EXAM_PANEL_MISS_PREVIEW) .map( (q) => `
${esc(normalizeTitle(q.Title).slice(0, 80))}
\u672a\u547d\u4e2d
` ) .join(""); if (stats.misses.length > EXAM_PANEL_MISS_PREVIEW) { html += `
\u53e6\u6709 ${ stats.misses.length - EXAM_PANEL_MISS_PREVIEW } \u9898\u672a\u547d\u4e2d\u672a\u5c55\u793a
`; } return html; } function updateExamAssistPanel(force = false) { const body = document.getElementById("ct-exam-assist-body"); const status = document.getElementById("ct-exam-assist-status"); const pctEl = document.getElementById("ct-exam-coverage-pct"); const barEl = document.getElementById("ct-exam-coverage-bar"); const rowEl = document.getElementById("ct-exam-coverage-row"); const hintEl = document.getElementById("ct-exam-coverage-hint"); const harvestBtn = document.getElementById("ct-exam-harvest-btn"); const startBtn = document.getElementById("ct-exam-start-btn"); const qs = state.exam.questions || []; const stats = analyzeExamQuestions(qs); const cov = stats; const lowCov = stats.lowCov; const highCov = stats.highCov; if (status) { if (state.examRunning) status.textContent = "\u6b63\u5728\u81ea\u52a8\u7b54\u9898\u4e2d…"; else if (state.qbankRunning) status.textContent = "\u6b63\u5728\u91c7\u96c6\u672c\u9898\u5e93…"; else if (!qs.length) status.textContent = "\u8bf7\u5148\u5728\u7f51\u9875\u70b9\u51fb\u300c\u5f00\u59cb\u8003\u8bd5\u300d"; else status.textContent = `\u5171 ${cov.total} \u9898 · \u5df2\u5339\u914d ${cov.hit} \u9898`; } if (pctEl) pctEl.textContent = `${cov.pct}%`; if (barEl) barEl.style.width = `${cov.pct}%`; if (rowEl) rowEl.classList.toggle("ct-exam-coverage-warn", lowCov); if (barEl && barEl.parentElement) { barEl.parentElement.classList.toggle("ct-exam-coverage-warn", lowCov); } if (hintEl) { if (state.examRunning) { hintEl.style.display = "block"; hintEl.textContent = "\u6b63\u5728\u81ea\u52a8\u7b54\u9898\u4e2d\uff0c\u5b8c\u6210\u81ea\u52a8\u5237\u65b0\uff0c\u8bf7\u8010\u5fc3\u7b49\u5f85"; } else if (lowCov && stats.total > 0) { hintEl.style.display = "block"; hintEl.textContent = `\u7b54\u6848\u8986\u76d6\u7387 ${cov.pct}%\uff08\u4f4e\u4e8e 90%\uff09\uff0c\u5efa\u8bae\u5148\u91c7\u96c6\u672c\u9898\u5e93\u518d\u8003\u8bd5\uff0c\u4e5f\u53ef\u76f4\u63a5\u5f00\u59cb\u8003\u8bd5`; } else { hintEl.style.display = "none"; } } const hasQs = stats.total > 0; if (harvestBtn) { harvestBtn.style.display = lowCov && hasQs ? "" : "none"; harvestBtn.disabled = state.qbankRunning || state.examRunning; harvestBtn.textContent = state.qbankRunning ? "\u91c7\u96c6\u4e2d…" : "\u91c7\u96c6\u672c\u9898\u5e93"; } if (startBtn) { startBtn.style.display = hasQs ? "" : "none"; startBtn.disabled = state.examRunning || state.qbankRunning; startBtn.textContent = state.examRunning ? "\u81ea\u52a8\u7b54\u9898\u4e2d…" : "\u5f00\u59cb\u8003\u8bd5"; } renderExamPanelHarvestLog(); if (!body) return; const bodySig = `${stats.total}:${stats.hit}:${stats.miss}:${lowCov}:${highCov}:${state.examRunning}:${state.qbankRunning}`; if (body.dataset.sig !== bodySig) { body.dataset.sig = bodySig; body.innerHTML = buildExamAssistBodyHtml(stats); } } function createExamAssistPanel() { if (document.getElementById("ct-exam-assist-panel")) return; const panel = document.createElement("div"); panel.id = "ct-exam-assist-panel"; panel.classList.add("ct-exam-panel-max"); panel.innerHTML = `
\u8003\u8bd5\u8f85\u52a9
\u7b49\u5f85\u5f00\u8003
\u9898\u5e93\u7b54\u6848\u8986\u76d6 0%
\u5f00\u8003\u540e\u663e\u793a\u672a\u547d\u4e2d\u9898\u76ee
`; document.body.appendChild(panel); const savedPos = readExamPanelPos(); if (savedPos && savedPos.left != null && savedPos.top != null) { panel.style.right = "auto"; panel.style.left = `${savedPos.left}px`; panel.style.top = `${savedPos.top}px`; } applyExamPanelCollapsed(panel, readExamPanelCollapsed()); enableExamPanelDrag(panel); document.getElementById("ct-exam-btn-min")?.addEventListener("click", (e) => { e.stopPropagation(); writeExamPanelCollapsed(true); applyExamPanelCollapsed(panel, true); }); document.getElementById("ct-exam-btn-max")?.addEventListener("click", (e) => { e.stopPropagation(); writeExamPanelCollapsed(false); applyExamPanelCollapsed(panel, false); }); document.getElementById("ct-exam-harvest-btn")?.addEventListener("click", () => { if (!isPro()) { trace("\u9898\u5e93\u91c7\u96c6\u4e3a Pro \u529f\u80fd", "warn"); return; } void runExamCourseQBankHarvest(false); }); document.getElementById("ct-exam-start-btn")?.addEventListener("click", () => { if (!isPro()) { trace("\u8003\u8bd5\u8f85\u52a9\u4e3a Pro \u529f\u80fd", "warn"); return; } void (async () => { await requestStopQbankHarvest(); clearExamAutoDone(state.exam.resultId); state.examAutoTriggeredFor = 0; runExamAutoAnswer(); })(); }); updateExamAssistPanel(); } function applyUiByRoute() { const main = document.getElementById("ct-auto-panel"); const onExam = isExamRouteContext(); if (onExam && isPro() && state.autoExamEnabled) { if (main) main.style.display = "none"; createExamAssistPanel(); updateExamAssistPanel(); if (shouldAutoRunExamAnswers()) maybeAutoRunExamAnswers(); } else { removeExamAssistPanel(); if (main) main.style.display = ""; } } function createPanel() { const old = document.getElementById("ct-auto-panel"); if (old) old.remove(); injectStyles(); state.school = schoolCode(); const logoUrl = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const panel = document.createElement("div"); panel.id = "ct-auto-panel"; panel.classList.add("ct-panel-max"); panel.innerHTML = `
\u51fa\u5934\u6559\u80b2 · \u81ea\u52a8\u5237\u8bfe\u52a9\u624bv${SCRIPT_VERSION}
\u4e00\u952e\u5237\u7f51\u8bfe·\u51c6\u5907\u9898\u5e93·\u8003\u8bd5\u8f85\u52a9
\u8fd0\u884c\u72b6\u6001\u5df2\u505c\u6b62
0/0 \u89c6\u9891\u5df2\u5b66\u4e60 · \u8003\u8bd5
0%
\u8bfe\u7a0b\u9009\u62e9${esc(schoolDisplayName())}
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
\u7ae0\u8282\u9884\u89c8\u5168\u90e8\u7ae0\u8282
\u8bf7\u9009\u62e9\u8bfe\u7a0b\u6216\u7b49\u5f85\u52a0\u8f7d\u7ae0\u8282
\u8fd0\u884c\u65e5\u5fd7
\u5b9e\u65f6\u65e5\u5fd7
\u9009\u9879\u8bbe\u7f6e
Pro\u6388\u6743
\u6863\u4f4d\u514d\u8d39
\u989d\u5ea6
\u8d2d\u4e70\u53d1\u5361\u7f51\u81ea\u52a9\u8d2d Token · 24h \u53d1\u8d27
\u529f\u80fd\u5f00\u5173\uff08Pro\uff09
\u9898\u5e93\u5df2\u51c6\u59070\u9898
\u5f53\u524d\u8bfe\u7a0b\u65e0
\u5f53\u524d\u7ae0\u8282\u65e0
\u5f53\u524d\u4efb\u52a1\u70b9\u300c\u5f00\u59cb\u300d\u8fd0\u884c
`; document.body.appendChild(panel); const savedPos = readPanelPos(); if (savedPos && savedPos.left != null && savedPos.top != null) { panel.style.right = "auto"; panel.style.left = `${savedPos.left}px`; panel.style.top = `${savedPos.top}px`; } applyPanelCollapsed(panel, readPanelCollapsed()); enablePanelDrag(panel); document.getElementById("ct-btn-min")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); document.getElementById("ct-btn-max")?.addEventListener("click", (e) => { e.stopPropagation(); writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); panel.querySelectorAll(".ct-tab-btn").forEach((btn) => { btn.addEventListener("click", () => switchPanelTab(btn.dataset.tab)); }); document.getElementById("ct-clear-log")?.addEventListener("click", clearRunLog); document.getElementById("ct-term-select")?.addEventListener("change", async (e) => { const term = toInt(e.target.value); if (!state.courseTree || !term) return; state.panelRefreshing = true; updatePanel(); applyTermSelection(state.courseTree, term); renderCoursePanel(); try { await refreshTermExamInfo(); await refreshChapterPreview(true); } finally { state.panelRefreshing = false; updatePanel(); } }); document.getElementById("ct-start")?.addEventListener("click", () => { if (isBusy()) return; state.stopping = false; state.currentTask = "\u542f\u52a8\u4e2d…"; updatePanel(); runAutoPipeline(); }); document.getElementById("ct-stop")?.addEventListener("click", stopStudy); document.getElementById("ct-study-current")?.addEventListener("click", () => { if (state.studyRunning) return; studyCurrentVideoPage(); }); document.getElementById("ct-cloud-save")?.addEventListener("click", async () => { const v = document.getElementById("ct-cloud-token")?.value || ""; saveCloudToken(v); try { await _el(true); trace("Token \u5df2\u4fdd\u5b58\uff0c\u6388\u6743\u5df2\u5237\u65b0", "info", { force: true }); } catch (e) { trace(`Token \u4fdd\u5b58\u5931\u8d25\uff1a${e.message || e}`, "error"); } }); document.getElementById("ct-cloud-refresh")?.addEventListener("click", async () => { try { state.cloudAuthLogged = false; await _el(true); trace("Pro \u6388\u6743\u5df2\u624b\u52a8\u5237\u65b0", "info", { force: true }); void refreshPanelNotice(); } catch (e) { trace(`\u5237\u65b0\u5931\u8d25\uff1a${e.message || e}`, "error"); } }); document.getElementById("ct-pro-buy")?.addEventListener("click", openProBuyPage); const bindProToggle = (id, storageKey, field) => { document.getElementById(id)?.addEventListener("change", (e) => { if (!isPro()) { e.target.checked = false; trace("\u8be5\u529f\u80fd\u4e3a Pro", "warn"); return; } state[field] = !!e.target.checked; localStorage.setItem(storageKey, state[field] ? "1" : "0"); applyUiByRoute(); updateCloudPanelUI(); }); }; bindProToggle("ct-toggle-qbank", STORAGE_AUTO_QBANK, "autoQbankEnabled"); bindProToggle("ct-toggle-exam-main", STORAGE_AUTO_EXAM, "autoExamEnabled"); updateQBankCountUI(); updateCloudPanelUI(); updatePanel(); void refreshPanelNotice(); void fetchSchoolName().then((name) => { trace(`\u5df2\u52a0\u8f7d v${SCRIPT_VERSION} | \u5b66\u6821: ${name}`); }); hookVideoPage(); hookQBankPage(); window.addEventListener("hashchange", () => { hookVideoPage(); hookQBankPage(); refreshExamStartGuard(); applyUiByRoute(); }); setInterval(() => { updatePanel(); applyUiByRoute(); if (document.getElementById("ct-exam-assist-panel") && state.examRunning) { updateExamAssistPanel(true); } }, 1000); if (getUser().StuID) { void fetchSchoolName() .then(() => _el(false)) .catch(() => {}); refreshCourseTree().catch((e) => trace(`\u8bfe\u8868\u52a0\u8f7d\u5931\u8d25: ${e.message || e}`, "error")); } applyUiByRoute(); } function initLoginHelper() { const hash = location.hash || ""; if (!/\/login/i.test(hash) && hash !== "#/login") return; const tryFill = () => { const inputs = document.querySelectorAll("input"); let cardInput = null; let pwdInput = null; inputs.forEach((el) => { const ph = (el.placeholder || "").toLowerCase(); const type = (el.type || "").toLowerCase(); if (!cardInput && (ph.includes("\u8eab\u4efd\u8bc1") || ph.includes("\u8d26\u53f7") || ph.includes("\u7528\u6237\u540d"))) cardInput = el; if (!pwdInput && (type === "password" || ph.includes("\u5bc6\u7801"))) pwdInput = el; }); if (!cardInput || !pwdInput || pwdInput.dataset.ctFilled === "1") return; const card = (cardInput.value || "").trim(); if (card.length < 6) return; pwdInput.value = card.slice(-6); pwdInput.dispatchEvent(new Event("input", { bubbles: true })); pwdInput.dispatchEvent(new Event("change", { bubbles: true })); pwdInput.dataset.ctFilled = "1"; trace("\u767b\u5f55\u9875\u5df2\u586b\u5165\u9ed8\u8ba4\u5bc6\u7801\uff08\u8eab\u4efd\u8bc1\u540e6\u4f4d\uff09"); }; const obs = new MutationObserver(tryFill); obs.observe(document.documentElement, { childList: true, subtree: true }); tryFill(); } function watchLogin() { let lastStuId = getUser().StuID || ""; setInterval(() => { const stuId = getUser().StuID || ""; if (stuId && stuId !== lastStuId) { lastStuId = stuId; state.pipelineStarted = false; state.courseTree = null; state.viewTerm = 0; state.selectedCurriculumIds = new Set(); void fetchSchoolName(); refreshCourseTree().catch(() => {}); } }, 2000); } function boot() { state.school = schoolCode(); loadCloudToken(); hookPageNetwork(); hookExamStartGuard(); const startPanel = () => { if (!document.body) return; createPanel(); initLoginHelper(); watchLogin(); hookQBankPage(); }; if (document.body) startPanel(); else document.addEventListener("DOMContentLoaded", startPanel); } boot(); })();