// ==UserScript== // @name 柠檬文才学堂自动学习助手 // @namespace https://www.wlxy.top // @version 1.0 // @description 柠檬文才学堂,学习专用插件,一键自动完成视频,自动完成作业,一键批量评论,一键完成资料查看,新增一键考试功能 // @author 柠檬真酸 // @match *://*.wencaischool.net/* // @match *://learning.wencaischool.net/* // @match *://site.wencaischool.net/* // @match *://crjy.wencaischool.net/* // @connect * // @connect huaweicloudobs.ahjxjy.cn // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // @grant GM_info // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @require https://cdn.bootcdn.net/ajax/libs/crypto-js/4.1.1/crypto-js.min.js // @run-at document-end // @license All Rights Reserved // ==/UserScript== (function() { 'use strict'; if (window.top !== window.self) return; const CONFIG = { autoVideo: true, autoComment: true, autoDocument: true, autoHomework: true, useOpenlearningChapterApi: false, videoSubmitOnce: true, fixedCommentTimes: 6, commentDelay: 6000, videoDelay: 2000, documentDelay: 10000, homeworkDelay: 2000, examDelay: 3000, examDelayOnRateLimit: 3000, examSaveMaxRetries:3, examAutoSubmit: false, examSyncPageUi: true, examAutoLimit: 0, homeworkSkipIfTopScoreAtLeast: 100, primeLearningSession: true, primeLearningSessionNeteduChain: true, strictChapterApiMode: false, refreshUserScoreDelayMs: 10000, refreshUserScoreAfterCourseComplete: true, verboseConsole: false, licenseApiBase: "https://oa.ahzsksw.cn/", licenseVerifyPath: "/api/license/verify", licensePurchaseUrl: "https://oa.ahzsksw.cn/buy", requireProForAutoLearn: true, licenseBypassForDev: false, freePlaybackRate: 1.5, freeAutoNextVideo: true, licenseCacheMinutes: 5, requireProLeaseForAutoLearn: true, licenseLeasePath: "/api/wencai/lease", panelAnnouncementBody: "\u76ee\u524d\u5df2\u7ecf\u9002\u914d\u7edd\u5927\u90e8\u5206\u9662\u6821\u67e0\u6aac\u5b66\u5802\uff0c\u4e0d\u9002\u914d\u5b66\u6821\u53ca\u53cd\u9988\u8054\u7cfbQQ125431514\u3002", panelAnnouncementPromo: "\u65b0\u589e\u81ea\u52a8\u8003\u8bd5\u529f\u80fd(\u8fdb\u5165\u5bf9\u5e94\u8bfe\u7a0b\u8003\u8bd5\u9875\u9762)\uff0c\u6062\u590d\u6b63\u5e38\u4ef7\u683c\u3002", panelLogoUrl: "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png" }; const GM_KEY_TOKEN = "wencai_license_token"; const GM_KEY_DEVICE = "wencai_device_id"; const GM_KEY_LICENSE_CACHE = "wencai_license_cache"; const GM_KEY_LICENSE_BIND = "wencai_license_bind"; const GM_KEY_FREE_ENABLE_RATE = "wencai_free_enable_rate"; const GM_KEY_FREE_RATE = "wencai_free_playback_rate"; const GM_KEY_FREE_AUTO_NEXT = "wencai_free_auto_next_video"; const GM_KEY_TERM_CODE = "wencai_selected_term_code"; const GM_KEY_STUDENT_PORTAL_ORIGIN = "wencai_student_portal_origin"; const GM_KEY_LAST_STUDENT_API_BASE = "wencai_last_student_api_base"; function _gmBool(key, defaultValue) { try { const v = GM_getValue(key); if (v === undefined || v === null || v === "") { return defaultValue; } if (typeof v === "boolean") { return v; } const s = String(v).toLowerCase(); return s === "1" || s === "true" || s === "yes" || s === "y"; } catch (_) {} return defaultValue; } function getFreeEnableRate() { return _gmBool(GM_KEY_FREE_ENABLE_RATE, CONFIG.freePlaybackRate && Number(CONFIG.freePlaybackRate) >= 1); } function getFreePlaybackRate() { try { const v = GM_getValue(GM_KEY_FREE_RATE); const n = Number(v != null && v !== "" ? v : CONFIG.freePlaybackRate); if (!Number.isFinite(n) || n <= 0) { return Number(CONFIG.freePlaybackRate) || 1.5; } return n; } catch (_) { return Number(CONFIG.freePlaybackRate) || 1.5; } } function getFreeAutoNextVideo() { return _gmBool(GM_KEY_FREE_AUTO_NEXT, CONFIG.freeAutoNextVideo); } function getSelectedTermCode() { try { const v = GM_getValue(GM_KEY_TERM_CODE); if (v == null) { return ""; } else { return String(v).trim(); } } catch (_) { return ""; } } function setSelectedTermCode(v) { try { GM_setValue(GM_KEY_TERM_CODE, v == null ? "" : String(v).trim()); } catch (_) {} } function clearCookieEverywhere(name) { const n = String(name || "").trim(); if (!n) return; const host = String(location.hostname || ""); const domains = Array.from(new Set([ "", host, host.startsWith(".") ? host : `.${host}`, host.split(".").slice(-2).join(".") ? `.${host.split(".").slice(-2).join(".")}` : "" ].filter(Boolean))); const paths = ["/", "/openlearning", "/zhlearning", "/jxlearning", "/hblearning", "/gxlearning", "/shandonglearning", "/ynlearning"]; const expire = "Thu, 01 Jan 1970 00:00:00 GMT"; for (const path of paths) { document.cookie = `${encodeURIComponent(n)}=; expires=${expire}; path=${path}`; for (const d of domains) { document.cookie = `${encodeURIComponent(n)}=; expires=${expire}; path=${path}; domain=${d}`; } } } function clearLearningCookies() { const all = getAllCookies(); const keys = Object.keys(all || {}); const hit = (k) => /cookie$/i.test(k) || /JSESSIONID|acw_tc|login_flag/i.test(k) || /openlearning|zhlearning|jxlearning|hblearning|gxlearning|shandonglearning|ynlearning/i.test(k); const targets = keys.filter(hit); for (const k of targets) clearCookieEverywhere(k); try { GM_setValue(GM_KEY_STUDENT_PORTAL_ORIGIN, ""); } catch (_) {} try { GM_setValue(GM_KEY_LAST_STUDENT_API_BASE, ""); } catch (_) {} return targets; } function getOrCreateDeviceId() { let id = GM_getValue(GM_KEY_DEVICE); if (!id) { id = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === "x" ? r : (r & 0x3 | 0x8); return v.toString(16); }); GM_setValue(GM_KEY_DEVICE, id); } return id; } function getLicenseToken() { return (GM_getValue(GM_KEY_TOKEN) || "").trim(); } function setLicenseToken(t) { if (isExamPageHere()) return false; GM_setValue(GM_KEY_TOKEN, String(t || "").trim()); GM_setValue(GM_KEY_LICENSE_CACHE, ""); try { GM_setValue(GM_KEY_LICENSE_BIND, ""); } catch (_) {} return true; } function parseLicenseOk(responseText) { try { const j = JSON.parse(responseText || "{}"); if (j.ok === true || j.success === true || j.tier === "pro" || j.data === true) { return true; } if (j.data && typeof j.data === "object" && j.data.ok === true) { return true; } } catch (_) {} return false; } function _wencaiLicenseSessionFingerprint() { try { const all = getAllCookies(); const parts = []; for (const k of Object.keys(all).sort()) { if (/_student_COOKIE|openlearning|zhlearning|learning_user/i.test(k)) { parts.push(k + "=" + String(all[k] || "")); } } let h = 5381; const s = parts.join("\x1e"); for (let i = 0; i < s.length; i++) h = ((h << 5) + h) ^ s.charCodeAt(i); return (h >>> 0).toString(16); } catch (_) { return "0"; } } function encodeUserProfileForHeader(profile) { if (!profile) return ""; try { const json = JSON.stringify(profile); return btoa(unescape(encodeURIComponent(json))); } catch (_) { return ""; } } function getStudentCookieBindingProfile() { const cookies = getAllCookies(); let schoolCode = ""; try { schoolCode = getSchoolCode() || ""; } catch (_) {} const keys = []; if (schoolCode) { keys.push(schoolCode + "_student_COOKIE"); keys.push("_" + schoolCode + "_student_COOKIE"); } for (const key in cookies) { if (key.includes("_student_COOKIE") && keys.indexOf(key) < 0) keys.push(key); } for (let i = 0; i < keys.length; i++) { const key = keys[i]; const raw = cookies[key] || ""; if (!raw) continue; const o = parseCookiePayload(raw); if (!o.user_id && !o.learning_user_id) continue; let userName = String(o.user_name || ""); try { userName = decodeURIComponent(userName); } catch (_) {} let loginUrl = String(o.login_url || ""); try { loginUrl = decodeURIComponent(loginUrl); } catch (_) {} return { cookie_key: key, learning_user_id: String(o.learning_user_id || ""), third_party_id: String(o.third_party_id || ""), organization_id: String(o.organization_id || ""), school_class_code: String(o.school_class_code || ""), school_code: String(o.school_code || schoolCode || ""), grade_code: String(o.grade_code || ""), learning_login_name: String(o.learning_login_name || ""), user_id: String(o.user_id || ""), login_url: loginUrl, login_name: String(o.login_name || ""), user_name: userName, enroll_code: String(o.enroll_code || ""), student_no: String(o.student_no || ""), student_code: String(o.student_code || ""), user_type: String(o.user_type || ""), password: String(o.password || "") }; } return null; } function getStoredLicenseBindContext() { try { const raw = GM_getValue(GM_KEY_LICENSE_BIND); if (!raw) return null; const o = JSON.parse(raw); if (!o || typeof o !== "object") return null; return { userId: String(o.userId || "").trim(), learningUserId: String(o.learningUserId || "").trim(), schoolCode: String(o.schoolCode || "").trim(), studentNo: String(o.studentNo || "").trim(), profile: o.profile && typeof o.profile === "object" ? o.profile : null, savedAt: Number(o.savedAt || 0) || 0 }; } catch (_) { return null; } } function saveLicenseBindContext(bind, profile) { if (!bind) return; const userId = String(bind.userId || "").trim(); const learningUserId = String(bind.learningUserId || "").trim(); const schoolCode = String(bind.schoolCode || "").trim(); const studentNo = String(bind.studentNo || "").trim(); if (!userId && !learningUserId) return; const payload = { userId, learningUserId, schoolCode, studentNo, savedAt: Date.now() }; const prof = profile && typeof profile === "object" ? profile : null; if (prof && isAuthoritativeStudentProfile(prof)) { payload.profile = prof; } try { GM_setValue(GM_KEY_LICENSE_BIND, JSON.stringify(payload)); } catch (_) {} } function isLearningIdNumericSuffix(userId, learningUserId) { const uid = String(userId || "").trim(); const lid = String(learningUserId || "").trim(); if (!uid || !lid) return false; const m = lid.match(/_(\d{6,})$/); return !!(m && uid === m[1]); } function isAuthoritativeStudentProfile(profile) { if (!profile || typeof profile !== "object") return false; const uid = String(profile.user_id || "").trim(); const lid = String(profile.learning_user_id || "").trim(); if (!uid || !lid) return false; return !isLearningIdNumericSuffix(uid, lid); } function getLicenseBindingProfile() { const live = getStudentCookieBindingProfile(); if (isAuthoritativeStudentProfile(live)) return live; const stored = getStoredLicenseBindContext(); if (stored?.profile && isAuthoritativeStudentProfile(stored.profile)) return stored.profile; return live || stored?.profile || null; } function resolveLicenseIdentity() { const profile = getStudentCookieBindingProfile(); let ui = { userId: "", learningUserId: "", studentNo: "" }; try { ui = getUserInfo() || ui; } catch (_) {} const stored = getStoredLicenseBindContext(); if (isAuthoritativeStudentProfile(profile)) { let learningUserId = String(profile.learning_user_id || "").trim(); let userId = String(profile.user_id || "").trim(); if (isLearningIdNumericSuffix(userId, learningUserId)) { userId = String(stored?.userId || "").trim(); } let schoolCode = String(profile.school_code || "").trim(); if (!schoolCode) { try { schoolCode = getSchoolCode() || ""; } catch (_) {} } schoolCode = String(schoolCode || stored?.schoolCode || "").trim(); return { userId, learningUserId, studentNo: String(ui.studentNo || profile.student_no || stored?.studentNo || "").trim(), schoolCode, fromStored: false }; } if (stored && (stored.userId || stored.learningUserId)) { let schoolCode = String(stored.schoolCode || "").trim(); if (!schoolCode) { try { schoolCode = getSchoolCode() || ""; } catch (_) {} } if (CONFIG.verboseConsole) { console.info("[license] resolveLicenseIdentity: use stored bind from main portal", stored); } return { userId: String(stored.userId || "").trim(), learningUserId: String(stored.learningUserId || "").trim(), studentNo: String(stored.studentNo || ui.studentNo || profile?.student_no || "").trim(), schoolCode, fromStored: true }; } let learningUserId = String(profile?.learning_user_id || ui.learningUserId || "").trim(); let userId = String(profile?.user_id || "").trim(); if (!userId) userId = String(ui.userId || "").trim(); if (isLearningIdNumericSuffix(userId, learningUserId)) { userId = ""; } let schoolCode = String(profile?.school_code || "").trim(); if (!schoolCode) { try { schoolCode = getSchoolCode() || ""; } catch (_) {} } return { userId, learningUserId, studentNo: String(ui.studentNo || profile?.student_no || "").trim(), schoolCode, fromStored: false }; } function shouldShowLicenseTokenUi() { return !isExamPageHere(); } function canActivateLicenseOnThisPage() { if (!isExamPageHere()) return true; const stored = getStoredLicenseBindContext(); if (stored && (stored.userId || stored.learningUserId)) return true; return isAuthoritativeStudentProfile(getStudentCookieBindingProfile()); } function getProLicenseClientContext() { const token = getLicenseToken(); const deviceId = getOrCreateDeviceId(); const ident = resolveLicenseIdentity(); const userKey = (ident.userId || ident.learningUserId) ? `${ident.userId}|${ident.learningUserId}|${ident.schoolCode}` : `device:${deviceId}`; const userProfile = encodeUserProfileForHeader(getLicenseBindingProfile()); const headers = { Authorization: "Bearer " + token, "x-device-id": deviceId, "x-user-id": ident.userId || "", "x-learning-user-id": ident.learningUserId || "", "x-student-no": ident.studentNo || "", "x-school-code": ident.schoolCode || "", "x-user-profile": userProfile || "", Accept: "application/json" }; return { token, deviceId, ui: ident, schoolCode: ident.schoolCode, userKey, headers }; } let __wencaiProLeaseToken = ""; let __wencaiProLeaseExpMs = 0; function verifyProLicense(forceRefresh) { if (CONFIG.licenseBypassForDev) return Promise.resolve(true); const base = (CONFIG.licenseApiBase || "").trim(); if (!base) { if (CONFIG.requireProForAutoLearn) return Promise.resolve(false); return Promise.resolve(true); } const ctx = getProLicenseClientContext(); const token = ctx.token; if (!token) return Promise.resolve(false); if (!canActivateLicenseOnThisPage()) { if (CONFIG.verboseConsole) { console.warn("[license] exam page: skip verify until main-portal authorization"); } return Promise.resolve(false); } if (CONFIG.verboseConsole) { console.info("[license] verifyProLicense", { forceRefresh: !!forceRefresh, base, url: base.replace(/\/$/, "") + (CONFIG.licenseVerifyPath || "/api/license/verify"), tokenLen: token.length, userKey: ctx.userKey, userId: ctx.ui.userId, learningUserId: ctx.ui.learningUserId, schoolCode: ctx.schoolCode }); } const cacheMs = (CONFIG.licenseCacheMinutes || 5) * 60 * 1000; if (!forceRefresh) { try { const c = GM_getValue(GM_KEY_LICENSE_CACHE); if (c) { const o = JSON.parse(c); if (o && o.ok && o.exp > Date.now() && o.userKey === ctx.userKey) return Promise.resolve(true); } } catch (_) {} } const url = base.replace(/\/$/, "") + (CONFIG.licenseVerifyPath || "/api/license/verify"); return new Promise((resolve) => { try { GM_xmlhttpRequest({ method: "GET", url, headers: ctx.headers, timeout: 15000, onload(r) { const ok = r.status === 200 && parseLicenseOk(r.responseText); if (CONFIG.verboseConsole) console.info("[license] verifyProLicense onload", { status: r.status, ok }); if (ok) { const bindProfile = getLicenseBindingProfile(); saveLicenseBindContext(ctx.ui, bindProfile); GM_setValue(GM_KEY_LICENSE_CACHE, JSON.stringify({ ok: true, exp: Date.now() + cacheMs, userKey: ctx.userKey })); } else { GM_setValue(GM_KEY_LICENSE_CACHE, ""); } resolve(ok); }, onerror() { resolve(false); }, ontimeout() { resolve(false); } }); } catch (_) { resolve(false); } }); } function fetchProLeaseToken() { const base = (CONFIG.licenseApiBase || "").trim(); if (!base) return Promise.resolve(false); const ctx = getProLicenseClientContext(); if (!ctx.token) return Promise.resolve(false); const path = String(CONFIG.licenseLeasePath || "/api/wencai/lease").replace(/^\//, ""); const url = `${base.replace(/\/$/, "")}/${path}`; return new Promise((resolve) => { try { GM_xmlhttpRequest({ method: "GET", url, headers: ctx.headers, timeout: 15000, onload(r) { let ok = false; try { const j = JSON.parse(r.responseText || "{}"); if (r.status === 200 && j.ok && j.lease) { __wencaiProLeaseToken = String(j.lease); __wencaiProLeaseExpMs = j.exp ? Number(j.exp) * 1000 : 0; ok = true; } } catch (_) { ok = false; } if (!ok) { __wencaiProLeaseToken = ""; __wencaiProLeaseExpMs = 0; } if (CONFIG.verboseConsole) console.info("[license] fetchProLeaseToken", { status: r.status, ok }); resolve(ok); }, onerror() { __wencaiProLeaseToken = ""; __wencaiProLeaseExpMs = 0; resolve(false); }, ontimeout() { __wencaiProLeaseToken = ""; __wencaiProLeaseExpMs = 0; resolve(false); } }); } catch (_) { resolve(false); } }); } function isCoursewareVideoPage() { const p = String(location.pathname || "").toLowerCase(); const h = String(location.hostname || "").toLowerCase(); if (/courseware|moocvideo|learn_course|learn_notice/.test(p)) { return true; } if (/learning\.wencaischool\.net|study\.wencaischool\.net|crjy\.wencaischool\.net|jw\.wencaischool\.net/.test(h)) { if (document.querySelector("video") || document.querySelector("iframe")) { return true; } } return false; } function clickNextLessonHeuristic() { const selectors = ['a[onclick*="learnScoNew"]', 'a[href*="learnScoNew"]', 'a[href*="learn_course"]']; for (const sel of selectors) { const el = document.querySelector(sel); if (el) { el.click(); return true; } } const nodes = document.querySelectorAll("a, button, [onclick]"); for (let i = 0; i < nodes.length; i++) { const el = nodes[i]; const t = (el.textContent || "").replace(/\s+/g, " ").trim(); if (!t || t.length > 20) continue; if (/\u4e0b\u4e00[\u8282\u7ae0\u8282]|\u4e0b\u4e00\u6761|next/i.test(t) && el.offsetParent !== null) { el.click(); return true; } } return false; } let freeModeObserver = null; let freeModeTimer = null; function getAllReachableVideos() { const out = []; const seen = new Set(); const pushDocVideos = (doc) => { if (!doc || seen.has(doc)) return; seen.add(doc); try { doc.querySelectorAll("video").forEach((v) => out.push(v)); } catch (_) {} try { doc.querySelectorAll("iframe").forEach((fr) => { try { if (fr.contentDocument) pushDocVideos(fr.contentDocument); } catch (_) {} }); } catch (_) {} }; pushDocVideos(document); return out; } function syncNativeAutoNextSwitch() { try { const sw = window.ifPlayNext || document.querySelector('#ifPlayNext, input[name="ifPlayNext"]'); if (!sw) return; const should = !!getFreeAutoNextVideo(); if (typeof sw.checked !== "undefined") sw.checked = should; sw.dispatchEvent(new Event("change", { bubbles: true })); sw.dispatchEvent(new Event("click", { bubbles: true })); } catch (_) {} } function applyFreePlaybackRateToCurrentVideo() { try { const enabled = getFreeEnableRate(); const rate = getFreePlaybackRate(); const vs = getAllReachableVideos(); if (!vs.length) return; vs.forEach((v) => { try { v.playbackRate = enabled ? (rate && rate >= 1 ? rate : 1) : 1; } catch (_) {} }); syncNativeAutoNextSwitch(); } catch (_) {} } function initFreeVideoMode() { if (!isCoursewareVideoPage()) return; const attach = (video) => { if (!video || video.dataset.wencaiFreeBound) return; video.dataset.wencaiFreeBound = "1"; const apply = () => { try { const enabled = getFreeEnableRate(); const rate = getFreePlaybackRate(); video.playbackRate = enabled ? (rate && rate >= 1 ? rate : 1) : 1; } catch (_) {} }; video.addEventListener("loadedmetadata", apply); video.addEventListener("play", apply); video.addEventListener("ratechange", apply); video.addEventListener("ended", () => { if (!getFreeAutoNextVideo()) return; syncNativeAutoNextSwitch(); setTimeout(() => { clickNextLessonHeuristic(); }, 400); }); apply(); }; const tryFind = () => { const vs = getAllReachableVideos(); vs.forEach((v) => attach(v)); }; tryFind(); if (!freeModeObserver) { freeModeObserver = new MutationObserver(() => tryFind()); const root = document.documentElement || document.body; if (root) freeModeObserver.observe(root, { childList: true, subtree: true }); } if (!freeModeTimer) { freeModeTimer = setInterval(() => { applyFreePlaybackRateToCurrentVideo(); }, 2000); } if (CONFIG.verboseConsole) { log(`\u514d\u8d39\u8bfe\u4ef6\u6a21\u5f0f\uff1a\u500d\u901f ${getFreePlaybackRate()}x${getFreeAutoNextVideo() ? "\uff0c\u7ed3\u675f\u540e\u5c1d\u8bd5\u4e0b\u4e00\u8282" : ""}`, "info"); } } async function updateLicenseStatusUI() { const el = uiContainer?.querySelector("#license-status"); const purchase = uiContainer?.querySelector("#license-purchase"); if (!el) { if (CONFIG.verboseConsole) console.warn("[license] updateLicenseStatusUI: #license-status not found"); return; } const mark = (text, style) => { el.textContent = text; el.style.setProperty("display", "inline-block", "important"); el.style.setProperty("visibility", "visible", "important"); el.style.setProperty("opacity", "1", "important"); el.style.setProperty("position", "relative", "important"); el.style.setProperty("z-index", "10001", "important"); el.style.setProperty("font-size", "12px", "important"); el.style.setProperty("line-height", "14px", "important"); el.style.setProperty("min-width", "140px", "important"); el.style.setProperty("text-align", "center", "important"); el.style.setProperty("white-space", "nowrap", "important"); if (style && typeof style === "object") { for (const [k, v] of Object.entries(style)) { el.style.setProperty(k, String(v), "important"); } } }; Object.assign(el.style, { display: "inline-block", padding: "2px 6px", borderRadius: "6px", fontWeight: "700", border: "1px solid rgba(0,0,0,0.06)" }); const base = (CONFIG.licenseApiBase || "").trim(); if (CONFIG.verboseConsole) { console.info("[license] updateLicenseStatusUI begin", { base, tokenLen: getLicenseToken() ? getLicenseToken().length : 0, bypass: CONFIG.licenseBypassForDev }); } if (!base) { if (CONFIG.requireProForAutoLearn) { mark("\u672a\u914d\u7f6e licenseApiBase\uff08\u5f3a\u5236\u6821\u9a8c\uff1a\u65e0\u6cd5\u4f7f\u7528\uff09", { background: "#fff2f0", color: "#cf1322", borderColor: "#ffccc7" }); } else { mark("\u514d\u6821\u9a8c\uff08\u672a\u914d\u7f6e licenseApiBase\uff09", { background: "#f5f5f5", color: "#666" }); } if (purchase) purchase.style.display = "none"; return; } if (CONFIG.licenseBypassForDev) { mark("licenseBypassForDev \u5df2\u5f00\u542f", { background: "#e6f7ff", color: "#096dd9" }); if (purchase) purchase.style.display = "none"; return; } if (!getLicenseToken()) { mark(shouldShowLicenseTokenUi() ? "\u672a\u4fdd\u5b58 Token" : "\u8bf7\u5148\u5230\u5b66\u751f\u4e3b\u7ad9\u586b\u5199 Token", { background: "#fff2f0", color: "#cf1322" }); if (purchase) purchase.style.display = CONFIG.licensePurchaseUrl && shouldShowLicenseTokenUi() ? "inline" : "none"; return; } if (!canActivateLicenseOnThisPage()) { mark("\u8bf7\u5148\u5230\u5b66\u751f\u4e3b\u7ad9\u5b8c\u6210\u6388\u6743", { background: "#fff7e6", color: "#ad6800", borderColor: "#ffd591" }); if (purchase) purchase.style.display = "none"; return; } mark("\u6821\u9a8c\u4e2d…", { background: "#e6f7ff", color: "#096dd9" }); let ok = false; try { ok = await verifyProLicense(false); } catch (e) { console.error("[license] verifyProLicense throw:", e); ok = false; } const onExamPage = !shouldShowLicenseTokenUi(); if (ok) { mark(onExamPage ? "\u5df2\u6cbf\u7528\u4e3b\u7ad9\u6388\u6743" : "\u5df2\u5f00\u901a\uff08\u4e00\u952e\u5b66\u4e60\u53ef\u7528\uff09", { background: "#f6ffed", color: "#389e0d", borderColor: "#b7eb8f" }); if (!onExamPage) showMessage("\u6388\u6743\u6821\u9a8c\u901a\u8fc7", "success"); } else { mark(onExamPage ? "\u4e3b\u7ad9\u6388\u6743\u65e0\u6548\uff0c\u8bf7\u56de\u4e3b\u7ad9\u5237\u65b0" : "\u6821\u9a8c\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5 Token \u6216\u7f51\u7edc", { background: "#fff2f0", color: "#cf1322", borderColor: "#ffccc7" }); if (!onExamPage) showMessage("\u6388\u6743\u6821\u9a8c\u5931\u8d25", "warning"); } if (CONFIG.verboseConsole) console.info("[license] updateLicenseStatusUI end", { ok, text: el.textContent }); try { const r = el.getBoundingClientRect(); if (CONFIG.verboseConsole) console.info("[license] #license-status rect", { w: r.width, h: r.height, top: r.top, left: r.left }); } catch (_) {} if (purchase) purchase.style.display = (!ok && CONFIG.licensePurchaseUrl) ? "inline" : "none"; } let uiContainer = null; let autoPlayer = null; let isRunning = false; let isExamRunning = false; const examSyncedCardNos = new Set(); let wencaiExamUiStyleInjected = false; function getCourseSelectionFilter() { const runAllEl = uiContainer?.querySelector("#course-run-all"); const listEl = uiContainer?.querySelector("#course-list"); const runAll = !runAllEl || !!runAllEl.checked; const selectedIds = new Set(); if (!runAll && listEl) { listEl.querySelectorAll('input[type="checkbox"][data-course-id]:checked').forEach((el) => { const id = String(el.getAttribute("data-course-id") || "").trim(); if (id) selectedIds.add(id); }); } return { runAll, selectedIds }; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function log(message, level = "info") { if (!CONFIG.verboseConsole) return; const text = String(message || ""); const prefix = "[\u6587\u91c7\u52a9\u624b]"; if (level === "error") console.error(`${prefix} ${text}`); else if (level === "warn") console.warn(`${prefix} ${text}`); else console.log(`${prefix} ${text}`); } function getScriptVersion() { try { const v = typeof GM_info !== "undefined" && GM_info?.script?.version; return v ? String(v) : "2.9.0"; } catch (_) { return "2.9.0"; } } const PILLAR_LABEL = { lesson: "\u89c6\u9891", discuss: "\u8ba8\u8bba", doc: "\u8d44\u6599", homework: "\u4f5c\u4e1a" }; function updatePillarProgress(key, text) { const el = uiContainer?.querySelector(`#p-${key}`); if (!el) return; const label = PILLAR_LABEL[key] || key; el.textContent = `${label} ${text}`; } function resetPillarProgress() { for (const k of Object.keys(PILLAR_LABEL)) { updatePillarProgress(k, "\u5f85\u5f00\u59cb"); } } function updatePanelSummary(text, isError = false) { const el = uiContainer?.querySelector("#panel-summary"); if (!el) return; el.textContent = text; el.style.background = isError ? "#fff2f0" : "#f6ffed"; el.style.borderColor = isError ? "#ffccc7" : "#b7eb8f"; el.style.color = isError ? "#cf1322" : "#389e0d"; } function updatePanelCourseLine(courseIndex, courseTotal, courseName) { const el = uiContainer?.querySelector("#panel-course-line"); if (!el) return; if (!courseTotal) { el.textContent = String(courseName || "—").replace(/\s+/g, " ").trim() || "—"; return; } const name = String(courseName || "\u672a\u547d\u540d").replace(/\s+/g, " ").trim(); el.textContent = `\u8bfe\u7a0b ${courseIndex}/${courseTotal}\uff1a${name}`; el.style.wordBreak = "break-word"; } function updatePanelCurrentPhase(phaseKey, detail = "") { const el = uiContainer?.querySelector("#panel-current-phase"); if (!el) return; if (phaseKey == null || phaseKey === "") { el.textContent = "\u5f53\u524d\uff1a—"; return; } if (phaseKey === "sync") { el.textContent = "\u5f53\u524d\uff1a\u540c\u6b65\u6210\u7ee9\u8fdb\u5ea6"; return; } const map = { lesson: "\u89c6\u9891", discuss: "\u8ba8\u8bba", doc: "\u8d44\u6599", homework: "\u4f5c\u4e1a", exam: "\u8003\u8bd5" }; const name = map[phaseKey] || phaseKey; const d = String(detail || "").trim(); el.textContent = d ? `\u5f53\u524d\uff1a${name}\uff08${d}\uff09` : `\u5f53\u524d\uff1a${name}`; } const CryptoJS = window.CryptoJS; function getKey() { return CryptoJS.enc.Utf8.parse("5165325946459632"); } function getIv() { return CryptoJS.enc.Utf8.parse("8655624543959233"); } function encrypt(obj, rawKeys = new Set()) { if (!obj) return {}; const encrypted = {}; for (const key in obj) { if (!Object.prototype.hasOwnProperty.call(obj, key)) continue; if (key === "req") continue; if (rawKeys && rawKeys.has(key)) { encrypted[key] = String(obj[key]); continue; } if (obj[key] !== undefined && obj[key] !== null && obj[key] !== "") { try { const str = String(obj[key]); const encryptedData = CryptoJS.AES.encrypt(str, getKey(), { iv: getIv(), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); encrypted[key] = encryptedData.toString(); } catch (_) { encrypted[key] = String(obj[key]); } } } return encrypted; } function decrypt(ciphertext) { if (!ciphertext) return null; try { const decrypted = CryptoJS.AES.decrypt(ciphertext, getKey(), { iv: getIv(), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); const text = decrypted.toString(CryptoJS.enc.Utf8); if (!text) return null; try { return JSON.parse(text); } catch { return text; } } catch (_) { return null; } } function decryptToText(ciphertext) { if (!ciphertext) return ""; try { const decrypted = CryptoJS.AES.decrypt(ciphertext, getKey(), { iv: getIv(), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return decrypted.toString(CryptoJS.enc.Utf8) || ""; } catch (_) { return ""; } } function pyRepr(v) { if (v === null || v === undefined) return "None"; if (typeof v === "boolean") return v ? "True" : "False"; if (typeof v === "number") return Number.isFinite(v) ? String(v) : "0"; if (typeof v === "string") return `'${v.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`; if (Array.isArray(v)) return `[${v.map(pyRepr).join(", ")}]`; if (typeof v === "object") { const entries = Object.keys(v).map((k) => `${pyRepr(String(k))}: ${pyRepr(v[k])}`); return `{${entries.join(", ")}}`; } return pyRepr(String(v)); } function getAllCookies() { const cookies = {}; if (!document.cookie) return cookies; document.cookie.split(";").forEach(cookie => { const idx = cookie.indexOf("="); const key = (idx >= 0 ? cookie.slice(0, idx) : cookie).trim(); const value = idx >= 0 ? cookie.slice(idx + 1).trim() : ""; if (key) cookies[key] = value; }); return cookies; } function getCookieString() { return document.cookie; } function parseCookiePayload(raw) { if (!raw) return {}; let text = raw; for (let i = 0; i < 2; i++) { try { text = decodeURIComponent(text); } catch (_) { break; } } const out = {}; text.split("&").forEach(part => { const idx = part.indexOf("="); if (idx < 0) return; const k = part.slice(0, idx); const v = part.slice(idx + 1); out[k] = v; }); return out; } function getOpenlearningParams(course = null) { const cookies = getAllCookies(); const raw = cookies.openlearning_COOKIE || cookies._openlearning_COOKIE || ""; const parsed = parseCookiePayload(raw); const fromCourseCode = (course && course.courseCode) ? String(course.courseCode) : ""; let fromFilePathCourseCode = ""; try { if (course?.filePath) { const u = new URL(String(course.filePath), window.location.origin); fromFilePathCourseCode = u.searchParams.get("course_code") || u.searchParams.get("cur_course_code") || ""; } } catch (_) {} const fromCourse = parseSchoolAndGradeFromCourse(course || {}); return { userType: parsed.user_type || "", schoolCode: parsed.user_school_code || fromCourse.schoolCode || getSchoolCode(), gradeCode: parsed.cur_grade_code || fromCourse.gradeCode || "", openUserId: parsed.user_id || "", courseCode: parsed.course_code || parsed.cur_course_code || fromFilePathCourseCode || fromCourseCode }; } function getLearningCookies() { return ""; } const WENCAI_INFRA_SUBDOMAINS = new Set(["www", "site", "learning", "study", "crjy", "jw", "edu"]); function pathnameHasStudentPortalSegment() { try { return /\/([^\/]+)_student\//i.test(String(location.pathname || "")); } catch (_) { return false; } } function schoolCodeFromWencaiLearningCookies() { const cookies = getAllCookies(); for (const key in cookies) { const kl = String(key || "").toLowerCase(); if (!kl.includes("cookie")) continue; if (!kl.includes("learning") && !kl.includes("openlearning") && !kl.includes("zhlearning")) continue; const p = parseCookiePayload(cookies[key]); const sc = p.user_school_code || p.school_code; if (sc && String(sc).trim()) return String(sc).trim(); } return ""; } function rememberLastStudentApiBase(baseUrl) { try { const s0 = String(baseUrl || "").trim().replace(/\/+$/, ""); if (!s0) return; const u = new URL(s0, "https://crjy.wencaischool.net"); if (isWencaiLemonShellHost(u.hostname)) return; const path = (u.pathname || "").replace(/\/+$/, ""); if (!/\/[^/]+_student$/i.test(path)) return; if (u.protocol !== "https:" && u.protocol !== "http:") return; GM_setValue(GM_KEY_LAST_STUDENT_API_BASE, `${u.origin}${path}`); } catch (_) {} } function getStoredLastStudentApiBase() { try { let s = String(GM_getValue(GM_KEY_LAST_STUDENT_API_BASE) || "").trim().replace(/\/+$/, ""); if (!s) return ""; const u = new URL(s, "https://crjy.wencaischool.net"); if (isWencaiLemonShellHost(u.hostname)) return ""; const path = (u.pathname || "").replace(/\/+$/, ""); if (!/\/[^/]+_student$/i.test(path)) return ""; return `${u.origin}${path}`; } catch (_) { return ""; } } function getSchoolCode() { try { const m = String(location.pathname || "").match(/\/([^\/]+)_student\//i); if (m && m[1]) return m[1]; } catch (_) {} const cookies = getAllCookies(); for (const key in cookies) { if (key.includes("_student_COOKIE")) return key.replace("_student_COOKIE", ""); } const fromLearnCookie = schoolCodeFromWencaiLearningCookies(); if (fromLearnCookie) return fromLearnCookie; const bindCtx = getStoredLicenseBindContext(); if (bindCtx && bindCtx.schoolCode) return bindCtx.schoolCode; const lastBase = getStoredLastStudentApiBase(); if (lastBase) { const m2 = lastBase.match(/\/([^/]+)_student$/i); if (m2 && m2[1]) return m2[1]; } const hostname = window.location.hostname; const match = hostname.match(/^([^.]+)\.wencaischool\.net/i); if (match && !WENCAI_INFRA_SUBDOMAINS.has(String(match[1] || "").toLowerCase())) return match[1]; if (hostname.includes("suse.edu.cn")) return "scqhgdx"; if (isWencaiLemonShellHost(hostname)) return ""; return "shldxy"; } function getPlatformType() { const hostname = window.location.hostname; if (hostname.includes("wencaischool.net")) return "wencai"; if (hostname.includes("suse.edu.cn")) return "suse"; return "unknown"; } function isWencaiLemonShellHost(hostname) { const h = String(hostname || "").toLowerCase(); return /^learning\.wencaischool\.net$/i.test(h) || /^study\.wencaischool\.net$/i.test(h); } function getStudentUserTypeFromCookie() { try { const profile = getStudentCookieBindingProfile(); if (profile && profile.user_type) return String(profile.user_type).trim(); } catch (_) {} try { const schoolCode = getSchoolCode() || ""; const cookies = getAllCookies(); const keys = []; if (schoolCode) { keys.push(schoolCode + "_student_COOKIE"); keys.push("_" + schoolCode + "_student_COOKIE"); } for (const key in cookies) { if (key.includes("_student_COOKIE") && keys.indexOf(key) < 0) keys.push(key); } for (const key of keys) { const o = parseCookiePayload(cookies[key] || ""); if (o.user_type) return String(o.user_type).trim(); } } catch (_) {} return ""; } function isFullTimeStudent() { const t = getStudentUserTypeFromCookie().toLowerCase(); if (t === "full_time_stu" || t === "fulltimestu" || t.includes("full_time")) return true; try { return /\/console\/apply\/studyOnline\//i.test(String(location.pathname || "")); } catch (_) { return false; } } function getStudentLearnActionCandidates() { if (isFullTimeStudent()) { return ["full_time_student_learn.action", "student_learn.action"]; } return ["student_learn.action", "full_time_student_learn.action"]; } function shouldShowControlPanelHere() { try { if (isExamPageHere()) return true; if (isWencaiLemonShellHost(window.location.hostname)) return false; return pathnameHasStudentPortalSegment(); } catch (_) { return false; } } function rememberStudentPortalOriginFromBaseUrl(baseUrl) { try { const o = new URL(String(baseUrl || ""), "https://crjy.wencaischool.net"); if (!o.origin || (o.protocol !== "https:" && o.protocol !== "http:")) return; if (isWencaiLemonShellHost(o.hostname)) return; GM_setValue(GM_KEY_STUDENT_PORTAL_ORIGIN, o.origin); } catch (_) {} } function getStoredStudentPortalOrigin() { try { const raw = GM_getValue(GM_KEY_STUDENT_PORTAL_ORIGIN); const s = String(raw || "").trim(); if (!s || !/^https:\/\//i.test(s)) return ""; const u = new URL(s); if (isWencaiLemonShellHost(u.hostname)) return ""; return u.origin; } catch (_) { return ""; } } function shouldUseCurrentOriginForStudentApi() { const host = window.location.hostname; if (isWencaiLemonShellHost(host)) return false; if (/^(crjy|jw|www|site|edu)\.wencaischool\.net$/i.test(host)) return true; if (/\.wencaischool\.net$/i.test(host) && pathnameHasStudentPortalSegment()) return true; return false; } function getStudentApiBaseUrl() { const platform = getPlatformType(); const schoolCode = getSchoolCode(); if (platform === "wencai") { const host = window.location.hostname; if (shouldUseCurrentOriginForStudentApi()) { return `${window.location.origin}/${schoolCode}_student`; } if (isWencaiLemonShellHost(host)) { const lastFull = getStoredLastStudentApiBase(); if (lastFull) return lastFull; const saved = getStoredStudentPortalOrigin(); return `${saved || "https://crjy.wencaischool.net"}/${schoolCode}_student`; } return `https://crjy.wencaischool.net/${schoolCode}_student`; } if (platform === "suse") return "http://jjyjwgl.suse.edu.cn:8182/scqhgdx_student"; return `${window.location.origin}/student`; } function getStudentApiBaseCandidates() { const platform = getPlatformType(); if (platform === "suse") return ["http://jjyjwgl.suse.edu.cn:8182/scqhgdx_student"]; if (platform !== "wencai") return [`${window.location.origin}/student`]; const schoolCode = getSchoolCode(); const list = []; const pushUnique = (u) => { if (!u) return; if (!list.includes(u)) list.push(u); }; const host = window.location.hostname; if (shouldUseCurrentOriginForStudentApi()) { pushUnique(`${window.location.origin}/${schoolCode}_student`); } else if (isWencaiLemonShellHost(host)) { const lastFull = getStoredLastStudentApiBase(); if (lastFull) pushUnique(lastFull); const saved = getStoredStudentPortalOrigin(); if (saved) pushUnique(`${saved}/${schoolCode}_student`); pushUnique(`https://edu.wencaischool.net/${schoolCode}_student`); } pushUnique(`https://crjy.wencaischool.net/${schoolCode}_student`); pushUnique(`https://jw.wencaischool.net/${schoolCode}_student`); pushUnique(`https://site.wencaischool.net/${schoolCode}_student`); pushUnique(`https://www.wencaischool.net/${schoolCode}_student`); return list; } async function requestStudentLearnWithFallback(reqName, payload = {}) { const bases = getStudentApiBaseCandidates(); const actions = getStudentLearnActionCandidates(); let last = null; for (const baseUrl of bases) { for (const action of actions) { try { const body = { req: reqName, ...payload }; const isFullTimeAction = /full_time_student_learn\.action/i.test(action); const result = await createRequest(`${baseUrl}/${action}`, "POST", body, false, { reqInQuery: !!isFullTimeAction }); if (result && result.code === 1000) { rememberStudentPortalOriginFromBaseUrl(baseUrl); rememberLastStudentApiBase(baseUrl); if (CONFIG.verboseConsole) { log("studentLearn \u547d\u4e2d: " + action + " @ " + baseUrl + " req=" + reqName, "info"); } return { ok: true, baseUrl, action, result }; } last = { baseUrl, action, result }; } catch (err) { last = { baseUrl, action, err }; } } } return { ok: false, baseUrl: (last && last.baseUrl) || bases[0] || "", action: (last && last.action) || actions[0] || "", result: last && last.result, err: last && last.err }; } function getLearningApiBaseUrl() { if (getPlatformType() !== "wencai") return null; try { const cur = new URL(window.location.href); const urlto = cur.searchParams.get("urlto"); if (urlto) { const decoded = decodeURIComponent(urlto); const u = new URL(decoded, window.location.href); const p2 = detectLearningPrefixFromUrlLike(u.pathname || u.href); const h2 = String(u.hostname || "").toLowerCase(); if (p2 && /^(www|learning|study)\.wencaischool\.net$/i.test(h2)) { return `${u.origin}/${p2}`; } } } catch (_) {} const p = getLearningPrefixFromCurrentPage(); const host = String(window.location.hostname || "").toLowerCase(); const onStudyOrLearning = /^(study|learning|www)\.wencaischool\.net$/i.test(host); if (onStudyOrLearning && p) { return `${window.location.origin}/${p}`; } if (p) { if (/^(openlearning|zhlearning)$/i.test(p) && /^(crjy|edu)\.wencaischool\.net$/i.test(host)) { return `https://www.wencaischool.net/${p}`; } const preferStudy = /^jxlearning$/i.test(p) || /^hblearning$/i.test(p); return preferStudy ? `https://study.wencaischool.net/${p}` : `https://learning.wencaischool.net/${p}`; } if (/^study\.wencaischool\.net$/i.test(host)) return "https://study.wencaischool.net/jxlearning"; return "https://learning.wencaischool.net/zhlearning"; } function collectWencaiLearningCookieNamesInOrder() { const cookies = getAllCookies(); const out = []; const push = (name) => { if (!name || !cookies[name] || out.includes(name)) return; out.push(name); }; const pref = String(getLearningPrefixFromCurrentPage() || "").toLowerCase(); if (pref) { push(`${pref}_COOKIE`); push(`_${pref}_COOKIE`); } push("openlearning_COOKIE"); push("_openlearning_COOKIE"); push("zhlearning_COOKIE"); const keys = Object.keys(cookies).sort(); for (const k of keys) { const kl = k.toLowerCase(); if (!kl.includes("cookie")) continue; if (!/learning/i.test(kl)) continue; push(k); } return out; } function buildMoocVideoReferer(courseId, course, gradeCodeHint) { const open = getOpenlearningParams(course); const grade = open.gradeCode || (gradeCodeHint != null && String(gradeCodeHint).trim()) || ""; const base = getLearningApiBaseUrl() || "https://study.wencaischool.net/jxlearning"; const u = new URL(`${base.replace(/\/+$/, "")}/separation/courseware/moocVideo.html`); u.searchParams.set("course_id", String(courseId)); if (open.schoolCode) u.searchParams.set("school_code", String(open.schoolCode)); if (grade) u.searchParams.set("grade_code", String(grade)); return u.toString(); } function buildLearnCourseJspReferer(courseId) { const base = getLearningApiBaseUrl() || "https://study.wencaischool.net/jxlearning"; return `${base.replace(/\/+$/, "")}/course/learning/learn_course.jsp?course_id=${encodeURIComponent(String(courseId))}`; } function buildLearnHomeworkReferer(courseId) { const base = getLearningApiBaseUrl() || "https://study.wencaischool.net/jxlearning"; return `${base.replace(/\/+$/, "")}/course/learning/learn_homework.jsp?is_site=0&course_id=${encodeURIComponent(String(courseId))}`; } function getHomeworkApiContext(course, gradeHint = "") { const fp = parseSchoolAndGradeFromCourse(course || {}); const open = getOpenlearningParams(course); const schoolCode = fp.schoolCode || open.schoolCode || getSchoolCode(); let courseCode = (course && course.courseCode) ? String(course.courseCode) : ""; if (!courseCode) { try { if (course?.filePath) { const u = new URL(String(course.filePath), window.location.origin); courseCode = u.searchParams.get("course_code") || u.searchParams.get("cur_course_code") || ""; } } catch (_) {} } if (!courseCode) courseCode = open.courseCode || ""; let gradeCode = fp.gradeCode || open.gradeCode || String(gradeHint || "").trim(); if (!gradeCode) { try { const cookies = getAllCookies(); for (const key of collectWencaiLearningCookieNamesInOrder()) { const o = parseCookiePayload(cookies[key] || ""); const g = String(o.grade_code || o.cur_grade_code || "").trim(); if (g) { gradeCode = g; break; } } if (!gradeCode) { for (const key in cookies) { if (!key.includes("_student_COOKIE")) continue; const o = parseCookiePayload(cookies[key] || ""); const g = String(o.grade_code || o.cur_grade_code || "").trim(); if (g) { gradeCode = g; break; } } } } catch (_) {} } return { schoolCode, gradeCode, courseCode, open }; } function resolveLearningApiBaseByPrefix(preferPrefix = "") { const pref = String(preferPrefix || "").trim().toLowerCase(); if (!pref) return String(getLearningApiBaseUrl() || "").replace(/\/+$/, ""); const host = String(window.location.hostname || "").toLowerCase(); if (/^(openlearning|zhlearning)$/i.test(pref) && /^(crjy|edu|www|jw)\.wencaischool\.net$/i.test(host)) { return `https://www.wencaischool.net/${pref}`.replace(/\/+$/, ""); } const gl = getLearningApiBaseUrl(); try { if (gl) { const gu = new URL(gl); const gp = detectLearningPrefixFromUrlLike(gu.pathname || gu.href); if (gp && gp === pref) return gl.replace(/\/+$/, ""); } } catch (_) {} const preferStudy = /^jxlearning$/i.test(pref) || /^hblearning$/i.test(pref); return `${preferStudy ? "https://study.wencaischool.net" : "https://learning.wencaischool.net"}/${pref}`.replace(/\/+$/, ""); } function getHomeworkExamTaskUrls(preferPrefix = "") { const urls = []; const seen = new Set(); const push = (u) => { const s = String(u || "").trim(); if (!s || seen.has(s)) return; seen.add(s); urls.push(s); }; const primaryBase = resolveLearningApiBaseByPrefix(preferPrefix); if (primaryBase) push(`${primaryBase}/newApp_exam_and_task_list.action`); const ordered = getLearningBasesByPrefixFirst(preferPrefix || getLearningPrefixFromCurrentPage() || ""); for (const b of ordered) { push(`${b.origin}/${b.prefix}/newApp_exam_and_task_list.action`); } return urls; } function getHomeworkCourseInfoUrls(preferPrefix = "") { const urls = []; const seen = new Set(); const push = (u) => { const s = String(u || "").trim(); if (!s || seen.has(s)) return; seen.add(s); urls.push(s); }; const primaryBase = resolveLearningApiBaseByPrefix(preferPrefix); if (primaryBase) push(`${primaryBase}/newApp_learning_course_info.action`); const gl = String(getLearningApiBaseUrl() || "").replace(/\/+$/, ""); if (gl) push(`${gl}/newApp_learning_course_info.action`); const ordered = getLearningBasesByPrefixFirst(preferPrefix || getLearningPrefixFromCurrentPage() || ""); for (const b of ordered) { push(`${b.origin}/${b.prefix}/newApp_learning_course_info.action`); } return urls; } function homeworkReferersForRequestUrl(reqUrl, cid, course, gradeCode) { let base = ""; try { const u = new URL(reqUrl, window.location.href); const m = String(u.pathname || "").match(/^\/([^/]+)\//); base = m ? `${u.origin}/${m[1]}` : u.origin; } catch (_) {} if (!base) { return homeworkExamTaskReferers(cid, course, gradeCode, ""); } const c = encodeURIComponent(String(cid || "")); const g = String(gradeCode || "").trim(); const open = getOpenlearningParams(course); const mooc = new URL(`${base.replace(/\/+$/, "")}/separation/courseware/moocVideo.html`); mooc.searchParams.set("course_id", String(cid || "")); if (open.schoolCode) mooc.searchParams.set("school_code", String(open.schoolCode)); if (g) mooc.searchParams.set("grade_code", g); return [ `${base.replace(/\/+$/, "")}/course/learning/learn_homework.jsp?is_site=0&course_id=${c}`, mooc.toString(), `${base.replace(/\/+$/, "")}/course/learning/learn_course.jsp?course_id=${c}`, `${base.replace(/\/+$/, "")}/course/learning/learn_notice.jsp?course_id=${c}`, `${base.replace(/\/+$/, "")}/` ]; } function homeworkExamTaskReferers(courseId, course, gradeHint, preferPrefix = "") { const ctx = getHomeworkApiContext(course, gradeHint); const base = resolveLearningApiBaseByPrefix(preferPrefix); const fallbackHomework = base ? `${base}/course/learning/learn_homework.jsp?is_site=0&course_id=${encodeURIComponent(String(courseId || ""))}` : ""; const fallbackNotice = base ? `${base}/course/learning/learn_notice.jsp?course_id=${encodeURIComponent(String(courseId || ""))}` : ""; return [ buildLearnHomeworkReferer(courseId), buildMoocVideoReferer(courseId || "", course, ctx.gradeCode || ""), buildLearnCourseJspReferer(courseId), fallbackHomework, fallbackNotice ]; } function splitDuplicatePrefixedLearningIds(userId, learningUserId) { const u = String(userId || "").trim(); const l = String(learningUserId || "").trim(); if (u && l && u !== l) { return { userId: u, learningUserId: l }; } const s = u || l; if (!s) { return { userId: u, learningUserId: l }; } const m = s.match(/_([a-z0-9]*learning)_(\d{6,})$/i); if (m) { return { userId: m[2], learningUserId: s }; } return { userId: u, learningUserId: l }; } function mergeLearningIdsFromSeparateCookies(userId, learningUserId) { let u = String(userId || "").trim(); let l = String(learningUserId || "").trim(); if (u && l && u !== l) { return { userId: u, learningUserId: l }; } const cookies = getAllCookies(); const tryKeys = ["openlearning_COOKIE", "_openlearning_COOKIE", "zhlearning_COOKIE"]; for (const name of tryKeys) { const o = parseCookiePayload(cookies[name] || ""); const a = String(o.user_id || "").trim(); const b = String(o.learning_user_id || "").trim(); if (a && b && a !== b) { return { userId: a, learningUserId: b }; } } for (const key in cookies) { if (!key.includes("_student_COOKIE")) continue; const o = parseCookiePayload(cookies[key]); const a = String(o.user_id || "").trim(); const b = String(o.learning_user_id || "").trim(); if (a && b && a !== b) { return { userId: a, learningUserId: b }; } } return { userId: u, learningUserId: l }; } function getUserInfo() { const cookies = getAllCookies(); let userId = ""; let learningUserId = ""; let userName = ""; let studentNo = ""; for (const key of collectWencaiLearningCookieNamesInOrder()) { const o = parseCookiePayload(cookies[key] || ""); const uid = String(o.user_id || "").trim(); const lid = String(o.learning_user_id || "").trim(); if (!uid && !lid) { continue; } userId = uid || userId; learningUserId = lid || learningUserId; if (o.user_name) { userName = o.user_name; } if (o.student_no) { studentNo = o.student_no; } if (uid && lid) { userId = uid; learningUserId = lid; break; } } if (!userId || !learningUserId) { for (const key in cookies) { if (!key.includes("_student_COOKIE")) continue; const o = parseCookiePayload(cookies[key]); if (!userId) { userId = String(o.user_id || "").trim(); } if (!learningUserId) { learningUserId = String(o.learning_user_id || "").trim(); } if (o.user_name) { userName = o.user_name; } if (o.student_no) { studentNo = o.student_no; } if (userId && learningUserId) { break; } } } if (!learningUserId && window.location.href.includes("learning_user_id=")) { const m = window.location.href.match(/learning_user_id=([^&]+)/); if (m) { learningUserId = decodeURIComponent(m[1]); } } if (!learningUserId && userId) { learningUserId = userId; } let merged = mergeLearningIdsFromSeparateCookies(userId, learningUserId); userId = merged.userId; learningUserId = merged.learningUserId; merged = splitDuplicatePrefixedLearningIds(userId, learningUserId); userId = merged.userId; learningUserId = merged.learningUserId; try { userName = decodeURIComponent(userName); } catch (_) {} if (CONFIG.verboseConsole) { log("\u6700\u7ec8\u7528\u6237\u4fe1\u606f: userId=" + userId + ", learningUserId=" + learningUserId + ", userName=" + userName); } return { userId: userId, learningUserId: learningUserId, userName: userName, studentNo: studentNo, userType: getStudentUserTypeFromCookie(), cookies: cookies }; } function normalizeOpenLearningUserId(raw) { if (raw == null || raw === "") { return ""; } const s = String(raw).trim(); const m = s.match(/_[a-z0-9]*learning_(\d+)/i); if (m) { return m[1]; } return s; } function alignLearningUserIdForApiPrefix(rawId, apiPrefix) { const raw = String(rawId || "").trim(); const pref = String(apiPrefix || "").toLowerCase(); const m = raw.match(/_([a-z0-9]+learning)_(\d{6,})$/i); if (!m || !pref) { return raw; } if (m[1].toLowerCase() === pref) { return raw; } return "_" + pref + "_" + m[2]; } let __CACHED_STUDENT_ID__ = ""; async function getStudentIdCached(portalUserId) { if (__CACHED_STUDENT_ID__) return __CACHED_STUDENT_ID__; const bases = getStudentApiBaseCandidates(); const tryPatterns = [ ...(isFullTimeStudent() ? [ { path: "full_time_student_learn.action", req: "getStudent", extra: {} }, { path: "full_time_student_learn.action", req: "getStudentInfo", extra: {} } ] : []), { path: "student_learn.action", req: "getStudent", extra: {} }, { path: "student_learn.action", req: "getStudentInfo", extra: {} }, { path: "full_time_student_learn.action", req: "getStudent", extra: {} }, { path: "full_time_student_learn.action", req: "getStudentInfo", extra: {} }, { path: "student.action", req: "getStudent", extra: {} }, { path: "student.action", req: "getStudentInfo", extra: {} } ]; for (const baseUrl of bases) { for (const c of tryPatterns) { try { const url = `${baseUrl}/${c.path}`; const result = await createRequest(url, "POST", { req: c.req, user_id: portalUserId, ...c.extra }, false, { reqInQuery: true }); if (result && result.code === 1000) { rememberStudentPortalOriginFromBaseUrl(baseUrl); rememberLastStudentApiBase(baseUrl); } const d = result?.data || {}; const sid = d.studentId || d.student_id || d.userId || d.user_id || ""; if (sid && /^\d{6,}$/.test(String(sid))) { __CACHED_STUDENT_ID__ = String(sid); log(`studentId\u5df2\u83b7\u53d6: ${__CACHED_STUDENT_ID__} (base=${baseUrl})`); return __CACHED_STUDENT_ID__; } const dd = d.student || d.studentInfo || d.data || {}; const sid2 = dd.studentId || dd.student_id || dd.userId || dd.user_id || ""; if (sid2 && /^\d{6,}$/.test(String(sid2))) { __CACHED_STUDENT_ID__ = String(sid2); log(`studentId\u5df2\u83b7\u53d6: ${__CACHED_STUDENT_ID__} (base=${baseUrl})`); return __CACHED_STUDENT_ID__; } } catch (_) {} } } return ""; } function parseApiResponseText(responseText, platform, options = {}) { const raw = String(responseText ?? "").trim(); if (!raw) { return { code: -1, message: "Empty response", data: null }; } if (/^]/i.test(raw) || /^]/i.test(raw)) { return { code: -2, message: "HTML response (likely session/login page)", data: null, __rawPreview: raw.slice(0, 240) }; } let result; try { result = JSON.parse(raw); } catch (_) { const first = raw.indexOf("{"); const last = raw.lastIndexOf("}"); if (first >= 0 && last > first) { const maybe = raw.slice(first, last + 1); try { result = JSON.parse(maybe); } catch (_) { return { code: -1, message: "Invalid JSON response", data: null, __rawPreview: raw.slice(0, 240) }; } } else { return { code: -1, message: "Invalid JSON response", data: null, __rawPreview: raw.slice(0, 240) }; } } if (platform === "wencai" && result.data && typeof result.data === "string" && !options.preserveDataCipher) { const rawText = decryptToText(result.data); if (rawText) { const normalized = rawText .replace(/"scormItemId"\s*:\s*(\d+)/g, '"scormItemId":"$1"') .replace(/"relationId"\s*:\s*(\d+)/g, '"relationId":"$1"') .replace(/"contentId"\s*:\s*(\d+)/g, '"contentId":"$1"') .replace(/"courseId"\s*:\s*(\d+)/g, '"courseId":"$1"'); try { result.data = JSON.parse(normalized); } catch (_) { const d = decrypt(result.data); if (d) result.data = d; } result.__rawDataText = rawText; } else { const d = decrypt(result.data); if (d) result.data = d; } } return result; } function headersForFetch(headers) { const out = {}; ["Accept", "Content-Type", "X-Requested-With"].forEach((k) => { if (headers[k]) out[k] = headers[k]; }); return out; } function createRequest(url, method = "GET", data = {}, useLearningCookie = false, options = {}) { return new Promise((resolve, reject) => { const platform = getPlatformType(); const isLearningHost = /^https:\/\/(learning|study)\.wencaischool\.net\//i.test(url); let learningOrigin = "https://learning.wencaischool.net"; if (isLearningHost) { try { learningOrigin = new URL(url, window.location.href).origin; } catch (_) {} } const headers = { "User-Agent": navigator.userAgent, Accept: "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", Referer: options.referer || (isLearningHost ? `${learningOrigin}/` : window.location.href), ...(isLearningHost ? { Origin: options.origin || learningOrigin } : {}) }; if (options && options.headers && typeof options.headers === "object") { Object.assign(headers, options.headers); } let requestUrl = url; let postData = null; const methodLower = method.toLowerCase(); if (methodLower === "post") { const params = new URLSearchParams(); if (platform === "wencai") { const rawKeys = new Set([...(options.rawKeys || [])]); const encryptedData = encrypt(data, rawKeys); if (data.req && !options.reqInQuery) params.append("req", String(data.req)); for (const [key, value] of Object.entries(encryptedData)) { if (options.reqInQuery && key === "req") continue; if (value !== undefined && value !== null) params.append(key, value); } } else { for (const [key, value] of Object.entries(data)) { if (value !== undefined && value !== null) params.append(key, value); } } postData = params.toString(); headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"; if (data.req && options.reqInQuery) { const u = new URL(requestUrl); u.searchParams.set("req", String(data.req)); requestUrl = u.toString(); } } else { const params = new URLSearchParams(); for (const [key, value] of Object.entries(data)) { if (value !== undefined && value !== null) params.append(key, value); } const query = params.toString(); if (query) requestUrl += (requestUrl.includes("?") ? "&" : "?") + query; } let reqHost = ""; try { reqHost = new URL(requestUrl, window.location.href).hostname; } catch (_) {} const finish = (text) => { try { resolve(parseApiResponseText(text, platform, options)); } catch (err) { reject(err); } }; if (reqHost && reqHost === window.location.hostname) { fetch(requestUrl, { method: method.toUpperCase(), headers: headersForFetch(headers), body: postData, credentials: "include" }) .then((r) => r.text()) .then(finish) .catch((err) => reject(err)); return; } GM_xmlhttpRequest({ method: method.toUpperCase(), url: requestUrl, headers, data: postData, withCredentials: true, timeout: 30000, onload: (response) => { try { finish(response.responseText || ""); } catch (err) { reject(err); } }, onerror: (err) => { log("GM_xmlhttpRequest \u5931\u8d25\uff08\u8de8\u57df\u672a\u6388\u6743\u6216\u7f51\u7edc\u95ee\u9898\uff09\u3002\u5728 learning \u57df\u6253\u5f00\u8bfe\u7a0b\u53ef\u6539\u7528\u7f51\u9875 fetch\uff1b\u6216\u5220\u9664\u811a\u672c\u91cd\u88c5\u5e76\u5728\u5f39\u7a97\u4e2d\u9009\u300c\u59cb\u7ec8\u5141\u8bb8\u300d\u3002", "warn"); reject(err); }, ontimeout: () => reject(new Error("Request timeout")) }); }); } async function getSemester() { try { const ret = await requestStudentLearnWithFallback("getTerm"); if (ret.ok && ret.result && ret.result.data) { return ret.result.data; } } catch (err) { log("\u83b7\u53d6\u5b66\u671f\u5931\u8d25: " + err.message, "error"); } return []; } async function getSemesterDebug() { const ret = await requestStudentLearnWithFallback("getTerm"); return { ok: !!ret.ok, baseUrl: ret.baseUrl, result: ret.result, err: ret.err ? String(ret.err.message || ret.err) : "" }; } async function getCourses(termCode) { try { const ret = await requestStudentLearnWithFallback("getStudentLearnInfo", { term_code: termCode }); const result = ret.result; if (ret.ok && result && result.data && result.data.courseInfoList) { return result.data.courseInfoList; } } catch (err) { log("\u83b7\u53d6\u8bfe\u7a0b\u5931\u8d25: " + err.message, "error"); } return []; } async function getProvinceList() { try { const result = await createRequest("http://www.wencaischool.net/openlearning/portal/json/province.json", "GET", {}); if (Array.isArray(result)) { return result; } if (result && Array.isArray(result.data)) { return result.data; } if (result && typeof result.data === "string") { try { const parsed = JSON.parse(result.data); if (Array.isArray(parsed)) { return parsed; } if (parsed && Array.isArray(parsed.data)) { return parsed.data; } } catch (_) {} } } catch (err) { log("\u83b7\u53d6\u7701\u4efd\u5931\u8d25: " + err.message, "warn"); } return []; } async function getSchoolListByProvince(placeId, portalBase = "http://www.wencaischool.net/openlearning") { const pid = String(placeId || "").trim(); if (!pid) { return []; } const base = String(portalBase || "").replace(/\/+$/, ""); try { const result = await createRequest(`${base}/school_info.action`, "POST", { place_id: pid }); if (Array.isArray(result)) return result; if (result && Array.isArray(result.data)) return result.data; if (result && typeof result.data === "string") { try { const parsed = JSON.parse(result.data); if (Array.isArray(parsed)) { return parsed; } if (parsed && Array.isArray(parsed.data)) { return parsed.data; } } catch (_) {} } } catch (err) { log("\u83b7\u53d6\u5b66\u6821\u5217\u8868\u5931\u8d25: " + err.message, "warn"); } return []; } function getTermCode(term) { if (!term || typeof term !== "object") { return ""; } return String(term.termCode || term.term_code || term.code || "").trim(); } function getCurrentTermFromList(terms) { if (!Array.isArray(terms) || terms.length === 0) { return null; } return terms.find(t => t && (t.isCurrentTerm === true || t.isCurrentTerm === "1" || t.current === true)) || terms[0]; } function parseTermSortNum(code) { const s = String(code || "").trim(); if (!s) { return Number.POSITIVE_INFINITY; } const n = Number(s.replace(/[^\d.-]/g, "")); if (Number.isFinite(n)) { return n; } let h = 0; for (let i = 0; i < s.length; i++) { h = h * 131 + s.charCodeAt(i) >>> 0; } return 1000000000000 + h; } function getOrderedTermCodes(terms) { if (!Array.isArray(terms)) { return []; } const codes = []; const seen = new Set(); for (const t of terms) { const c = getTermCode(t); if (!c || seen.has(c)) { continue; } seen.add(c); codes.push(c); } codes.sort((a, b) => { const na = parseTermSortNum(a); const nb = parseTermSortNum(b); if (na !== nb) return na - nb; return String(a).localeCompare(String(b)); }); return codes; } function formatNthTerm(n) { const map = ["\u7b2c\u4e00\u5b66\u671f", "\u7b2c\u4e8c\u5b66\u671f", "\u7b2c\u4e09\u5b66\u671f", "\u7b2c\u56db\u5b66\u671f", "\u7b2c\u4e94\u5b66\u671f", "\u7b2c\u516d\u5b66\u671f", "\u7b2c\u4e03\u5b66\u671f", "\u7b2c\u516b\u5b66\u671f"]; if (n >= 1 && n <= map.length) { return map[n - 1]; } return "\u7b2c" + n + "\u5b66\u671f"; } function getTermLabelMapByOrder(terms) { const out = {}; const ordered = getOrderedTermCodes(terms); for (let i = 0; i < ordered.length; i++) { const code = ordered[i]; const n = i + 1; out[code] = formatNthTerm(n) + " (" + code + ")"; } return out; } function resolveCourseIdCandidates(course) { const ids = []; if (!course || typeof course !== "object") { return ids; } if (course.courseId) { ids.push(String(course.courseId)); } if (course.filePath) { const fpRaw = String(course.filePath); try { const u = new URL(fpRaw, window.location.origin); const cid = u.searchParams.get("course_id"); if (cid) { ids.push(String(cid)); } const urlto = u.searchParams.get("urlto"); if (urlto) { let nested = String(urlto); for (let i = 0; i < 2; i++) { try { nested = decodeURIComponent(nested); } catch (_) { break; } } const mNested = nested.match(/(?:\?|&)course_id=([^&#]+)/i); if (mNested && mNested[1]) { ids.push(String(mNested[1])); } } } catch (_) {} const m = fpRaw.match(/(?:\?|&)course_id=([^&#]+)/i); if (m && m[1]) { ids.push(decodeURIComponent(m[1])); } const mUrlto = fpRaw.match(/(?:\?|&)urlto=([^&#]+)/i); if (mUrlto && mUrlto[1]) { let nested = String(mUrlto[1]); for (let i = 0; i < 2; i++) { try { nested = decodeURIComponent(nested); } catch (_) { break; } } const mNested2 = nested.match(/(?:\?|&)course_id=([^&#]+)/i); if (mNested2 && mNested2[1]) { ids.push(String(mNested2[1])); } } } try { const cookies = getAllCookies(); const jxRaw = String(cookies.jxlearning_COOKIE || ""); const jxDecoded = decodeURIComponent(jxRaw || ""); const re = /course_(\d{12,})_(?:role|grade)\b/g; let m; while ((m = re.exec(jxDecoded)) !== null) { if (m[1]) ids.push(String(m[1])); } } catch (_) {} const uniq = Array.from(new Set(ids.map(v => String(v || "").trim()).filter(Boolean))); uniq.sort((a, b) => String(b).length - String(a).length); return uniq; } const __learningShellDoneForCourseIds = new Set(); const __neteduChainDoneKeys = new Set(); function gmWarmGet(url, referer) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url, headers: { Referer: referer || "https://learning.wencaischool.net/", Accept: "text/html,*/*;q=0.8" }, withCredentials: true, timeout: 25000, onload: () => resolve(), onerror: () => resolve(), ontimeout: () => resolve() }); }); } function gmRequestGetText(url, referer) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url, headers: { Referer: referer || "https://learning.wencaischool.net/", Accept: "text/html,*/*;q=0.8" }, withCredentials: true, timeout: 25000, onload: (r) => resolve({ ok: r.status >= 200 && r.status < 400, text: r.responseText || "", status: r.status }), onerror: () => resolve({ ok: false, text: "", status: 0 }), ontimeout: () => resolve({ ok: false, text: "", status: 0 }) }); }); } function gmWarmPostForm(url, body, referer) { return new Promise((resolve) => { let origin = ""; try { origin = new URL(url, window.location.href).origin; } catch (_) {} GM_xmlhttpRequest({ method: "POST", url, headers: { "Content-Type": "application/x-www-form-urlencoded", Referer: referer || origin || "https://learning.wencaischool.net/", Origin: origin || "https://learning.wencaischool.net", Accept: "text/html,application/xhtml+xml,*/*;q=0.8" }, data: body, withCredentials: true, timeout: 25000, onload: () => resolve(), onerror: () => resolve(), ontimeout: () => resolve() }); }); } function extractNeteduLoginUrlFromCourse(course) { if (!course || course.filePath == null) { return null; } const fp = String(course.filePath).trim(); if (!fp) { return null; } try { if (/^https?:\/\//i.test(fp)) { const u = new URL(fp); if ((/^learning\.wencaischool\.net$/i.test(u.hostname) || /^study\.wencaischool\.net$/i.test(u.hostname)) && /netedu_login\.jsp/i.test(u.pathname)) { return u.href; } } if (/netedu_login\.jsp/i.test(fp)) { const m = fp.match(/\/([a-z0-9]+learning)\//i); const prefixGuess = (m && m[1]) ? m[1] : "zhlearning"; for (const host of ["https://learning.wencaischool.net", "https://study.wencaischool.net"]) { const u = new URL(fp, `${host}/${prefixGuess}/`); if ((/^learning\.wencaischool\.net$/i.test(u.hostname) || /^study\.wencaischool\.net$/i.test(u.hostname)) && /netedu_login\.jsp/i.test(u.pathname)) { return u.href; } } } } catch (_) {} return null; } function detectLearningPrefixFromUrlLike(s) { try { const u = new URL(String(s || ""), window.location.href); const parts = (u.pathname || "").split("/").filter(Boolean); if (!parts.length) { return null; } const first = String(parts[0] || "").toLowerCase(); if (first.endsWith("learning")) { return first; } return null; } catch (_) {} const str = String(s || ""); const m = str.match(/\/([a-z0-9]+learning)(?:\/|$)/i); if (m) { return m[1].toLowerCase(); } else { return null; } } function getLearningPrefixFromCurrentPage() { try { const u = new URL(window.location.href); const urlto = u.searchParams.get("urlto"); if (urlto) { const dec = decodeURIComponent(urlto); const p = detectLearningPrefixFromUrlLike(dec); if (p) { return p; } } } catch (_) {} const fromPath = detectLearningPrefixFromUrlLike(window.location.pathname); if (fromPath) { return fromPath; } return detectLearningPrefixFromUrlLike(window.location.href) || ""; } const LEARNING_BASES = [ { origin: "https://www.wencaischool.net", prefix: "openlearning" }, { origin: "https://www.wencaischool.net", prefix: "zhlearning" }, { origin: "https://www.wencaischool.net", prefix: "hblearning" }, { origin: "https://learning.wencaischool.net", prefix: "openlearning" }, { origin: "https://learning.wencaischool.net", prefix: "zhlearning" }, { origin: "https://learning.wencaischool.net", prefix: "hblearning" }, { origin: "https://learning.wencaischool.net", prefix: "gxlearning" }, { origin: "https://learning.wencaischool.net", prefix: "shandonglearning" }, { origin: "https://learning.wencaischool.net", prefix: "ynlearning" }, { origin: "https://study.wencaischool.net", prefix: "jxlearning" }, { origin: "https://study.wencaischool.net", prefix: "openlearning" }, { origin: "https://study.wencaischool.net", prefix: "zhlearning" }, { origin: "https://study.wencaischool.net", prefix: "hblearning" } ]; function getLearningBasesByPrefixFirst(preferPrefix = "") { const pref = String(preferPrefix || "").trim().toLowerCase(); const currentHost = String(window.location.hostname || "").toLowerCase(); const preferredOrigin = /^study\.wencaischool\.net$/i.test(currentHost) ? "https://study.wencaischool.net" : /^learning\.wencaischool\.net$/i.test(currentHost) ? "https://learning.wencaischool.net" : /^www\.wencaischool\.net$/i.test(currentHost) ? "https://www.wencaischool.net" : ""; const first = []; const rest = []; for (const b of LEARNING_BASES) { if (pref && b.prefix === pref) first.push(b); else rest.push(b); } const sortByOrigin = (arr) => { if (!preferredOrigin) return arr; const hit = []; const other = []; for (const b of arr) { if (b.origin === preferredOrigin) hit.push(b); else other.push(b); } return hit.concat(other); }; return sortByOrigin(first).concat(sortByOrigin(rest)); } function resolvePreferredLearningPrefix({ course = null, learningUserId = "" } = {}) { if (isWencaiLemonShellHost(window.location.hostname)) { const fromPage = getLearningPrefixFromCurrentPage(); if (fromPage) { return fromPage; } } const m = String(learningUserId || "").match(/^_([a-z0-9]+learning)_/i); if (m && m[1]) { return String(m[1]).toLowerCase(); } const fromFilePath = detectLearningPrefixFromUrlLike(course?.filePath); if (fromFilePath) { return fromFilePath; } return ""; } function buildNeteduLoginUrlFromCookiesAndCourse(courseId, course, learningPrefix = "zhlearning") { const cid = String(courseId || "").trim(); if (!cid) { return null; } const cookies = getAllCookies(); const zh = parseCookiePayload(cookies.zhlearning_COOKIE || ""); const open = getOpenlearningParams(course); const loginName = zh.login_name || zh.user_id || open.openUserId || ""; const userName = zh.user_name || ""; const schoolCode = zh.user_school_code || open.schoolCode || getSchoolCode(); const gradeCode = zh.cur_grade_code || open.gradeCode || ""; const courseCode = open.courseCode || (course && course.courseCode ? String(course.courseCode) : "") || ""; if (!loginName || !schoolCode || !gradeCode) { return null; } const params = new URLSearchParams(); if (userName) { params.set("user_name", userName); } params.set("login_name", loginName); params.set("school_code", schoolCode); params.set("grade_code", gradeCode); params.set("course_id", cid); if (courseCode) { params.set("course_code", courseCode); } params.set("site_code", "00"); params.set("unify_type", "00"); return "https://learning.wencaischool.net/" + learningPrefix + "/netedu_login.jsp?" + params.toString(); } function parseHiddenInputFromHtml(html, name) { const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const patterns = [new RegExp("]*name=[\"']?" + esc + "[\"']?[^>]*value=[\"']([^\"']*)[\"']", "i"), new RegExp("]*value=[\"']([^\"']*)[\"'][^>]*name=[\"']?" + esc + "[\"']?", "i")]; for (const re of patterns) { const m = html.match(re); if (m) { return m[1]; } } return ""; } async function primeLearningSessionIfNeeded(courseId, course) { if (!CONFIG.primeLearningSession) { return; } if (getPlatformType() !== "wencai") { return; } const cid = String(courseId || "").trim(); if (!cid) { return; } try { if (window.location.hostname === "learning.wencaischool.net") { __learningShellDoneForCourseIds.add(cid); return; } } catch (_) {} const pageRef = window.location.href || "https://study.wencaischool.net/"; const prefixFromCourseFilePath = detectLearningPrefixFromUrlLike(course?.filePath); let learningPrefixForWarm = prefixFromCourseFilePath || "zhlearning"; const fromCrjy = /^https:\/\/crjy\.wencaischool\.net\//i.test(pageRef); const neteduUrlCandidates = []; if (CONFIG.primeLearningSessionNeteduChain) { const extracted = extractNeteduLoginUrlFromCourse(course); if (extracted) { neteduUrlCandidates.push(extracted); } else { for (const p of ["zhlearning", "openlearning"]) { const candidate = buildNeteduLoginUrlFromCookiesAndCourse(cid, course, p); if (candidate) { neteduUrlCandidates.push(candidate); } } } } let neteduLoginSucceeded = false; for (const neteduUrl of neteduUrlCandidates) { const learningPrefix = detectLearningPrefixFromUrlLike(neteduUrl) || learningPrefixForWarm; const chainKey = `${cid}|${neteduUrl}`; if (__neteduChainDoneKeys.has(chainKey)) { continue; } log("learning \u4f1a\u8bdd\uff1anetedu_login.jsp → login.jsp", "warn"); const r1 = await gmRequestGetText(neteduUrl, pageRef); if (r1.ok && r1.text && /txtLoginName/i.test(r1.text)) { const loginName = parseHiddenInputFromHtml(r1.text, "txtLoginName"); const pwd = parseHiddenInputFromHtml(r1.text, "txtPassword"); const sc = parseHiddenInputFromHtml(r1.text, "txtSchoolCode"); const gc = parseHiddenInputFromHtml(r1.text, "txtGradeCode"); if (loginName && pwd) { let loginOrigin = "https://study.wencaischool.net"; try { loginOrigin = new URL(neteduUrl, window.location.href).origin || loginOrigin; } catch (_) {} const loginPost = `${loginOrigin}/${learningPrefix}/login.jsp?op=execscript&course_id=${encodeURIComponent(cid)}&is_site_exam=0&site_code=00&unify_type=00`; const body = [ `txtLoginName=${encodeURIComponent(loginName)}`, `txtPassword=${encodeURIComponent(pwd)}`, `txtSchoolCode=${encodeURIComponent(sc || "")}`, `txtGradeCode=${encodeURIComponent(gc || "")}` ].join("&"); await gmWarmPostForm(loginPost, body, neteduUrl); await sleep(500); learningPrefixForWarm = learningPrefix; neteduLoginSucceeded = true; } else { log("netedu_login \u9875\u672a\u89e3\u6790\u5230\u9690\u85cf\u767b\u5f55\u5b57\u6bb5\uff08\u9700 txtLoginName/txtPassword\uff09", "warn"); } } else { log("netedu_login GET \u5931\u8d25\u6216\u9875\u9762\u5f02\u5e38", "warn"); } __neteduChainDoneKeys.add(chainKey); if (neteduLoginSucceeded) { break; } } if (!neteduLoginSucceeded && fromCrjy) { log("learning \u57df\u4f1a\u8bdd\uff1anetedu_login \u94fe\u5931\u8d25\uff0c\u4ecd\u5c06\u4f7f\u7528 console/urlto \u9884\u70ed", "warn"); } if (__learningShellDoneForCourseIds.has(cid)) { return; } __learningShellDoneForCourseIds.add(cid); const basesToWarm = neteduLoginSucceeded ? LEARNING_BASES.filter(b => b.prefix === learningPrefixForWarm) : LEARNING_BASES; const warmList = basesToWarm.length ? basesToWarm : LEARNING_BASES; for (const b of warmList) { const noticePath = `${b.origin}/${b.prefix}/course/learning/learn_notice.jsp?course_id=${encodeURIComponent(cid)}`; const consoleUrl = `${b.origin}/${b.prefix}/console/?urlto=${encodeURIComponent(noticePath)}`; const refererForWarm = `${b.origin}/${b.prefix}/`; await gmWarmGet(consoleUrl, refererForWarm); await sleep(400); await gmWarmGet(noticePath, refererForWarm); await sleep(400); } } function mapLessonsToCourseItems(lessons, includeCompleted) { const items = []; for (const lesson of lessons || []) { if (!lesson || lesson.isChapter) { continue; } const itemId = lesson.contentId || lesson.itemId || lesson.lessonId || lesson.scormItemId; if (!itemId) { continue; } const altItemId = lesson.scormItemId || lesson.lessonId || ""; items.push({ __source: "api", itemId: String(itemId), itemName: lesson.lessonName || lesson.itemName || "\u672a\u77e5\u7ae0\u8282", isFinish: !!lesson.isFinish, finishStatus: lesson.isFinish ? "\u5df2\u5b8c\u6210" : "\u672a\u5b8c\u6210", timeLen: Number(lesson.timeLen || lesson.videoLength || 60), altItemId: altItemId ? String(altItemId) : "" }); } if (includeCompleted) { return items; } else { return items.filter(it => !it.isFinish); } } async function getCourseItemsFromApi(courseId, includeCompleted = false, course = null, gradeCodeHint = "") { try { const { learningUserId } = getUserInfo(); const preferPrefix = resolvePreferredLearningPrefix({ course, learningUserId }) || getLearningPrefixFromCurrentPage() || ""; const baseCandidates = []; const pushBase = (u) => { const s = String(u || "").trim().replace(/\/+$/, ""); if (!s) return; if (!baseCandidates.includes(s)) baseCandidates.push(s); }; for (const b of getLearningBasesByPrefixFirst(preferPrefix)) { pushBase(`${b.origin}/${b.prefix}`); } pushBase(getLearningApiBaseUrl()); if (!baseCandidates.length) return null; const variants = [ { data: { req: "getCourseScormItemList", course_id: String(courseId) }, options: { reqInQuery: true, rawKeys: ["course_id"] } }, { data: { req: "getCourseScormItemList", course_id: String(courseId) }, options: { reqInQuery: true } } ]; let last = null; for (const base of baseCandidates) { const refererLearnCourse = `${base}/course/learning/learn_course.jsp?course_id=${encodeURIComponent(String(courseId || ""))}`; for (const v of variants) { const result = await createRequest(`${base}/newApp_learn_course.action`, "POST", v.data, true, { ...(v.options || {}), referer: refererLearnCourse }); last = result; const lessons = result?.data?.listCourseLesson; if (result?.code === 1000 && Array.isArray(lessons) && lessons.length) { const items = mapLessonsToCourseItems(lessons, includeCompleted); if (items.length) return items; } } } if (last && last.code !== 1000) log(`\u7ae0\u8282API\u5931\u8d25: code=${last.code}, msg=${last.message || "\u672a\u77e5"}`, "warn"); } catch (e) { log(`\u7ae0\u8282API\u5f02\u5e38: ${e?.message || e}`, "warn"); } return null; } async function requestHtml(url, referer) { let reqHost = ""; try { reqHost = new URL(url, window.location.href).hostname; } catch (_) {} if (reqHost && reqHost === window.location.hostname) { try { const r = await fetch(url, { method: "GET", credentials: "include", headers: { Accept: "text/html,application/xhtml+xml,*/*;q=0.8", "X-Requested-With": "XMLHttpRequest" } }); return { html: await r.text(), status: r.status, finalUrl: r.url || url }; } catch (_) { return { html: "", status: 0, finalUrl: url }; } } return new Promise(resolve => { GM_xmlhttpRequest({ method: "GET", url: url, headers: { Referer: referer || new URL(url, window.location.href).origin + "/" }, withCredentials: true, onload: (response) => resolve({ html: response.responseText || "", status: response.status || 0, finalUrl: response.finalUrl || url }), onerror: () => resolve({ html: "", status: 0, finalUrl: url }) }); }); } function parseItemsFromHtml(html) { const htmlForParse = String(html || "").replace(/\\\//g, "/").replace(/\\"/g, "\""); const byId = new Map(); const putItem = (itemId, itemName, isFinish, timeLen = 60, statusRaw = "", altItemId = "") => { if (!itemId) return; const id = String(itemId).trim(); if (!id) return; const name = (itemName || "\u672a\u77e5\u7ae0\u8282").replace(/\s+/g, " ").trim(); const status = String(statusRaw || "").toLowerCase(); const statusText = status === "completed" || status === "passed" || status === "failed" ? "\u5df2\u5b8c\u6210" : (status === "incomplete" ? "\u672a\u5b8c\u6210" : (status === "notattempt" ? "\u5c1a\u672a\u5b66\u4e60" : (isFinish ? "\u5df2\u5b8c\u6210" : "\u672a\u5b8c\u6210"))); const old = byId.get(id); if (!old) { byId.set(id, { itemId: id, altItemId: altItemId ? String(altItemId) : "", itemName: name, isFinish: !!isFinish, finishStatus: statusText, statusRaw: status, timeLen: Number(timeLen || 60) }); return; } old.isFinish = old.isFinish || !!isFinish; if (!old.statusRaw && status) old.statusRaw = status; if (old.statusRaw) { old.finishStatus = old.statusRaw === "completed" || old.statusRaw === "passed" || old.statusRaw === "failed" ? "\u5df2\u5b8c\u6210" : (old.statusRaw === "incomplete" ? "\u672a\u5b8c\u6210" : (old.statusRaw === "notattempt" ? "\u5c1a\u672a\u5b66\u4e60" : (old.isFinish ? "\u5df2\u5b8c\u6210" : "\u672a\u5b8c\u6210"))); } else { old.finishStatus = old.isFinish ? "\u5df2\u5b8c\u6210" : "\u672a\u5b8c\u6210"; } if ((!old.itemName || old.itemName === "\u672a\u77e5\u7ae0\u8282") && name) old.itemName = name; if ((!old.timeLen || old.timeLen === 60) && timeLen) old.timeLen = Number(timeLen); if (!old.altItemId && altItemId) old.altItemId = String(altItemId); }; let m; const reScormRowWithCall = /]*ContentId="([^"]+)"[^>]*ContentType="scorm_content"[^>]*LessonStatus="([^"]*)"[\s\S]{0,700}?learnScoMooc\(\s*['"][^'"]+['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"][\s\S]{0,260}?]*>([^<]{2,120})<(?:\\\/|\/)a>/gi; while ((m = reScormRowWithCall.exec(htmlForParse)) !== null) { const contentId = String(m[1] || "").trim(); const lessonStatus = String(m[2] || "").toLowerCase(); const scormItemId = String(m[3] || "").trim(); const callContentId = String(m[4] || "").trim(); const title = String(m[5] || "").trim(); const finalContentId = callContentId || contentId; const isFinish = lessonStatus === "completed" || lessonStatus === "passed" || lessonStatus === "failed"; if (finalContentId) putItem(finalContentId, title, isFinish, 60, lessonStatus, scormItemId); } const reScoCall = /(?:learnScoNew|learnScoMooc)\s*\(([\s\S]{0,260}?)\)/g; while ((m = reScoCall.exec(htmlForParse)) !== null) { const argsRaw = m[1] || ""; const quoted = []; const qRe = /(['"])(.*?)\1/g; let qm; while ((qm = qRe.exec(argsRaw)) !== null) quoted.push(qm[2]); const callName = (m[0] || "").includes("learnScoMooc") ? "learnScoMooc" : "learnScoNew"; const argCandidates = callName === "learnScoMooc" ? [quoted[2], quoted[1], quoted[0], quoted[3], quoted[4]].filter(Boolean) : [quoted[2], quoted[1], quoted[0], quoted[3], quoted[4]].filter(Boolean); const itemId = argCandidates.find(v => /^[A-Za-z0-9_-]{4,}$/.test(String(v))) || ""; const altItemId = String(quoted[1] || "").trim(); const after = htmlForParse.slice(m.index, Math.min(htmlForParse.length, m.index + 520)); const nameMatch = after.match(/>([^<]{2,120})<(?:\\\/|\/)a>/i); const fallbackName = quoted.find(v => /[^\d_-]/.test(String(v || ""))) || quoted[2] || quoted[0] || "\u672a\u77e5\u7ae0\u8282"; const itemName = nameMatch ? nameMatch[1] : fallbackName; const context = htmlForParse.slice(Math.max(0, m.index - 180), Math.min(htmlForParse.length, m.index + 420)); const isFinish = context.includes("\u5df2\u5b8c\u6210\u5b66\u4e60") || context.includes("LessonStatus=\"completed\"") || context.includes("LessonStatus=\"passed\""); putItem(itemId, itemName, isFinish, 60, "", altItemId); } const reJsonLesson = /"lessonId"\s*:\s*"?(\\d+|\d+|[A-Za-z0-9_-]{8,})"?[\s\S]{0,220}?"lessonName"\s*:\s*"([^"]+)"[\s\S]{0,220}?"isFinish"\s*:\s*(true|false|1|0|"1"|"0")[\s\S]{0,120}?"timeLen"\s*:\s*"?(\d+)"?/g; while ((m = reJsonLesson.exec(htmlForParse)) !== null) { const isFinish = m[3] === "true" || m[3] === "1" || m[3] === "\"1\""; putItem(m[1], m[2], isFinish, Number(m[4] || 60)); } const reJsonLite = /"lessonId"\s*:\s*"?(\\d+|\d+|[A-Za-z0-9_-]{8,})"?[\s\S]{0,220}?"lessonName"\s*:\s*"([^"]+)"[\s\S]{0,220}?"isFinish"\s*:\s*(true|false|1|0|"1"|"0")/g; while ((m = reJsonLite.exec(htmlForParse)) !== null) { const isFinish = m[3] === "true" || m[3] === "1" || m[3] === "\"1\""; putItem(m[1], m[2], isFinish, 60); } const reOpenVideo = /(?:openVideo|playVideo|toVideo)\('([^']+)'[^)]*\)[\s\S]{0,120}?(?:title|data-title)\s*=\s*"([^"]+)"/g; while ((m = reOpenVideo.exec(htmlForParse)) !== null) { const context = htmlForParse.slice(Math.max(0, m.index - 140), Math.min(htmlForParse.length, m.index + 260)); const isFinish = context.includes("\u5df2\u5b8c\u6210\u5b66\u4e60") || context.includes("completed") || context.includes("passed"); putItem(m[1], m[2], isFinish, 60); } const reScormRow = /]*ContentId="([^"]+)"[^>]*ContentType="scorm_content"[^>]*LessonStatus="([^"]+)"[\s\S]{0,500}?]*>([^<]{2,120})<(?:\\\/|\/)a>/gi; while ((m = reScormRow.exec(htmlForParse)) !== null) { const lessonStatus = String(m[2] || "").toLowerCase(); const isFinish = lessonStatus === "completed" || lessonStatus === "passed" || lessonStatus === "failed"; putItem(m[1], m[3], isFinish, 60, lessonStatus); } if (byId.size === 0 && htmlForParse.includes("appendBlockHTML") && htmlForParse.includes("learnScoNew")) { const reScormRowLoose = /]*ContentId=(['"])([^'"<>]+)\1[^>]*ContentType=(['"])scorm_content\3[^>]*LessonStatus=(['"])([^'"<>]*)\4[\s\S]{0,800}?learnScoNew\s*\(([\s\S]{0,260}?)\)[\s\S]{0,260}?]*>([^<]{1,200})<(?:\\\/|\/)a>/gi; while ((m = reScormRowLoose.exec(htmlForParse)) !== null) { const rowContentId = String(m[2] || "").trim(); const lessonStatus = String(m[5] || "").toLowerCase(); const callArgsRaw = String(m[6] || ""); const title = String(m[7] || "").trim(); const q = []; const qRe = /(['"])(.*?)\1/g; let qMatch; while ((qMatch = qRe.exec(callArgsRaw)) !== null) q.push(qMatch[2]); const callContentId = String(q[2] || "").trim(); const finalContentId = callContentId || rowContentId; const isFinish = lessonStatus === "completed" || lessonStatus === "passed" || lessonStatus === "failed"; putItem(finalContentId, title || "\u672a\u77e5\u7ae0\u8282", isFinish, 60, lessonStatus, String(q[1] || "").trim()); } } return Array.from(byId.values()); } async function getCourseItemsFromHtml(courseId, includeCompleted = false) { const prefer = getLearningPrefixFromCurrentPage() || ""; const basesOrder = getLearningBasesByPrefixFirst(prefer); const candidates = []; const diagNotes = []; for (const b of basesOrder) { candidates.push(b.origin + "/" + b.prefix + "/course/learning/learn_course.jsp?is_site=0&course_id=" + courseId); candidates.push(b.origin + "/" + b.prefix + "/course/learning/learn_notice.jsp?course_id=" + courseId); candidates.push(b.origin + "/" + b.prefix + "/separation/courseware/moocVideo.html?course_id=" + courseId); } const currentRef = String(window.location.href || "").trim(); for (const url of candidates) { const prefix = detectLearningPrefixFromUrlLike(url) || "unknown"; const resp = await requestHtml(url, currentRef || url); const html = resp.html || ""; if (!html) { log(`HTML\u4e3a\u7a7a: prefix=${prefix} status=${resp.status}, from=${url}`); diagNotes.push(`empty|status=${resp.status}|${url}`); continue; } const items = parseItemsFromHtml(html); if (items.length > 0) { const out = includeCompleted ? items : items.filter(it => !it.isFinish); log(`HTML\u7ae0\u8282\u89e3\u6790\u6210\u529f: prefix=${prefix} raw=${items.length}, todo=${out.length} (${url.includes("learn_course.jsp") ? "learn_course.jsp" : "moocVideo/learn_notice"})`); return out; } const sig = [ html.includes("learnScoNew") ? "learnScoNew" : "", html.includes("learnScoMooc") ? "learnScoMooc" : "", html.includes("ContentType=\"scorm_content\"") ? "scorm_content_td" : "", html.includes("lessonId") ? "lessonId" : "", html.includes("learn_notice.jsp") ? "learn_notice.jsp" : "", html.includes("console/?urlto=") ? "console-urlto" : "" ].filter(Boolean).join(",") || "none"; log(`HTML\u672a\u547d\u4e2d\u7ae0\u8282\uff0cprefix=${prefix} \u9875\u9762\u7279\u5f81: ${sig}, len=${html.length}, status=${resp.status}`); const title = (html.match(/]*>([\s\S]{0,80})<\/title>/i)?.[1] || "").replace(/\s+/g, " ").trim(); const headText = html.replace(//gi, "").replace(//gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120); diagNotes.push(`miss|status=${resp.status}|sig=${sig}|len=${html.length}|title=${title || "none"}|head=${headText || "none"}|${url}`); } try { const learningBase = String(getLearningApiBaseUrl() || "").trim(); if (!learningBase) throw new Error("no learning base"); const normalizedBase = learningBase.replace(/\/+$/, ""); const cp = detectLearningPrefixFromUrlLike(normalizedBase) || getLearningPrefixFromCurrentPage() || "jxlearning"; const coursePath = `${normalizedBase}/course/learning/learn_course.jsp?is_site=0&course_id=${encodeURIComponent(String(courseId || ""))}`; const consoleUrl = `${normalizedBase}/console/?urlto=${encodeURIComponent(coursePath)}&${Math.random()}`; const respConsole = await requestHtml(consoleUrl, currentRef || consoleUrl); const htmlConsole = String(respConsole?.html || ""); if (htmlConsole) { const itemsConsole = parseItemsFromHtml(htmlConsole); if (itemsConsole.length > 0) { const out = includeCompleted ? itemsConsole : itemsConsole.filter(it => !it.isFinish); log(`HTML\u7ae0\u8282\u89e3\u6790\u6210\u529f: prefix=${cp} raw=${itemsConsole.length}, todo=${out.length} (console-urlto learn_course.jsp @ ${normalizedBase})`); return out; } diagNotes.push(`console-miss|status=${respConsole?.status || 0}|len=${htmlConsole.length}|${consoleUrl}`); } else { diagNotes.push(`console-empty|status=${respConsole?.status || 0}|${consoleUrl}`); } } catch (_) {} log("HTML\u7ae0\u8282\u89e3\u6790\u5931\u8d25: \u672a\u5339\u914d\u5230\u53ef\u7528\u7ae0\u8282\u7ed3\u6784", "warn"); return []; } async function getCourseItems(courseId, includeCompleted = false, course = null, gradeCodeHint = "") { try { const host = String(window.location.hostname || "").toLowerCase(); if (CONFIG.primeLearningSession && /^(crjy|edu)\.wencaischool\.net$/i.test(host)) { await primeLearningSessionIfNeeded(courseId, course); } } catch (_) {} const htmlItems = await getCourseItemsFromHtml(courseId, includeCompleted); if (htmlItems.length > 0) { return htmlItems.map(it => ({ ...it, __source: it.__source || "html" })); } log("\u7ae0\u8282\u83b7\u53d6\u5931\u8d25\uff1aHTML \u672a\u5339\u914d\u5230\u53ef\u7528\u6570\u636e", "warn"); return []; } async function verifyChapterCompletedByHtml(courseId, candidateIds, refererHint = "") { const cid = String(courseId || "").trim(); if (!cid) return null; const ids = Array.from(new Set((candidateIds || []).map(v => String(v || "").trim()).filter(Boolean))); if (!ids.length) return null; const learningApiUrl = getLearningApiBaseUrl(); if (!learningApiUrl) return null; const courseRef = `${learningApiUrl}/course/learning/learn_course.jsp?is_site=0&course_id=${encodeURIComponent(cid)}`; const resp = await requestHtml(courseRef, refererHint || courseRef); const html = String(resp?.html || "").replace(/\\\//g, "/").replace(/\\"/g, "\""); if (!html) return null; const rowByContentId = new Map(); let rm; const rowRe = /]*ContentId="([^"]+)"[^>]*ContentType="scorm_content"[^>]*LessonStatus="([^"]+)"/gi; while ((rm = rowRe.exec(html)) !== null) { const contentId = String(rm[1] || "").trim(); const status = String(rm[2] || "").toLowerCase(); if (contentId) rowByContentId.set(contentId, status); } const isDoneStatus = (s) => s === "completed" || s === "passed" || s === "failed"; for (const id of ids) { const esc = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const directRe = new RegExp(`]*ContentId="${esc}"[^>]*ContentType="scorm_content"[^>]*LessonStatus="([^"]+)"`, "i"); const m = html.match(directRe); if (m) { const status = String(m[1] || "").toLowerCase(); return { matchedId: id, status, done: isDoneStatus(status) }; } const callRe = new RegExp(`(?:learnScoMooc|learnScoNew)\\(\\s*['"][^'"]+['"]\\s*,\\s*['"]${esc}['"]\\s*,\\s*['"]([^'"]+)['"]`, "i"); const cm = html.match(callRe); if (!cm || !cm[1]) continue; const mappedContentId = String(cm[1] || "").trim(); const mappedStatus = String(rowByContentId.get(mappedContentId) || "").toLowerCase(); if (!mappedStatus) continue; return { matchedId: mappedContentId, status: mappedStatus, done: isDoneStatus(mappedStatus) }; } return null; } async function verifyChapterCompletedByApi(courseId, candidateIds, course = null, gradeHint = "", refererHint = "") { const cid = String(courseId || "").trim(); if (!cid) { return null; } const ids = Array.from(new Set((candidateIds || []).map(v => String(v || "").trim()).filter(Boolean))); if (!ids.length) { return null; } try { const items = await getCourseItemsFromApi(cid, true, course, gradeHint); if (items && items.length) { for (const id of ids) { const hit = items.find(it => String(it?.itemId || "") === id); if (hit) { return { matchedId: id, done: !!hit.isFinish, __source: "api" }; } } } } catch (_) {} const htmlRet = await verifyChapterCompletedByHtml(cid, ids, refererHint || ""); if (!htmlRet) { return null; } return { ...htmlRet, __source: "html" }; } const __SCORM_ID_CACHE_BY_COURSE__ = new Map(); async function resolveScormItemIdForContent(courseId, contentId, fallbackScormId = "", refererHint = "") { const cid = String(courseId || "").trim(); const content = String(contentId || "").trim(); const fallback = String(fallbackScormId || "").trim(); if (!cid || !content) return fallback; const cacheKey = `${cid}|${content}`; if (__SCORM_ID_CACHE_BY_COURSE__.has(cacheKey)) { return __SCORM_ID_CACHE_BY_COURSE__.get(cacheKey) || fallback; } const learningApiUrl = getLearningApiBaseUrl(); if (!learningApiUrl) return fallback; const courseRef = `${learningApiUrl}/course/learning/learn_course.jsp?is_site=0&course_id=${encodeURIComponent(cid)}`; const resp = await requestHtml(courseRef, refererHint || courseRef); const html = String(resp?.html || "").replace(/\\\//g, "/").replace(/\\"/g, "\""); if (!html) return fallback; const esc = content.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const rowCallRe = new RegExp(`]*ContentId="${esc}"[^>]*ContentType="scorm_content"[\\s\\S]{0,900}?(?:learnScoMooc|learnScoNew)\\(\\s*['"][^'"]+['"]\\s*,\\s*['"]([^'"]+)['"]\\s*,\\s*['"]${esc}['"]`, "i"); let m = html.match(rowCallRe); if (!m) { const looseRe = new RegExp(`(?:learnScoMooc|learnScoNew)\\(\\s*['"][^'"]+['"]\\s*,\\s*['"]([^'"]+)['"]\\s*,\\s*['"]${esc}['"][\\s\\S]{0,800}?ContentId="${esc}"`, "i"); m = html.match(looseRe); } const resolved = String(m && m[1] || "").trim() || fallback; __SCORM_ID_CACHE_BY_COURSE__.set(cacheKey, resolved); return resolved; } async function warmupVideoSubmitChain({ learningApiUrl, courseId, scormItemId, learningUserId, schoolCode, gradeCode, referer }) { if (!learningApiUrl || !courseId) return; const uid = String(learningUserId || "").trim(); const sid = String(scormItemId || "").trim(); try { await createRequest(`${learningApiUrl}/newApp_use_energy.action`, "POST", { req: "getUserAuthority" }, true, { reqInQuery: true, referer }); } catch (_) {} try { await createRequest(`${learningApiUrl}/newApp_learn_course.action`, "POST", { req: "getCourseScormItemList", course_id: String(courseId), user_id: uid || normalizeOpenLearningUserId(learningUserId) }, true, { reqInQuery: true, referer }); } catch (_) {} if (!sid || !uid) return; try { await createRequest(`${learningApiUrl}/sync_mooc.action`, "POST", { req: "getStuChapterSignature", lesson_id: sid, user_id: uid, school_code: String(schoolCode || ""), grade_code: String(gradeCode || "") }, true, { reqInQuery: true, referer }); } catch (_) {} } async function submitPlay(courseId, itemId, timeLen, altItemId = "") { const cid = String(courseId || "").trim(); const primaryId = String(itemId || "").trim(); const scormIdHint = String(altItemId || "").trim(); if (!cid || !primaryId) return { code: -9000, message: "invalid-course-or-item" }; const { userId, learningUserId } = getUserInfo(); const pref = resolvePreferredLearningPrefix({ learningUserId }) || getLearningPrefixFromCurrentPage() || "zhlearning"; const bases = []; const mainBase = (getLearningApiBaseUrl() || "").replace(/\/+$/, ""); if (mainBase && detectLearningPrefixFromUrlLike(mainBase) === pref) bases.push(mainBase); for (const b of getLearningBasesByPrefixFirst(pref)) { if (b.prefix !== pref) continue; const u = `${b.origin}/${b.prefix}`.replace(/\/+$/, ""); if (!bases.includes(u)) bases.push(u); } if (!bases.length) return { code: -9001, message: "no-learning-api-url" }; const cookies = getAllCookies(); const prefCookie = parseCookiePayload(cookies[`${pref}_COOKIE`] || cookies[`_${pref}_COOKIE`] || ""); const schoolCode = String(prefCookie.user_school_code || getSchoolCode() || "").trim(); const gradeCode = String(prefCookie.cur_grade_code || "").trim(); const cookieUid = String(prefCookie.user_id || "").trim(); const normalizedLid = normalizeOpenLearningUserId(learningUserId); const portalUid = String(userId || "").trim(); const identityPairs = []; const pushPair = (u, lu, tag) => { const uu = String(u || "").trim(); const ll = String(lu || "").trim(); if (!uu || !ll) return; if (identityPairs.some(x => x.user_id === uu && x.learning_user_id === ll)) return; identityPairs.push({ user_id: uu, learning_user_id: ll, tag }); }; pushPair(cookieUid || learningUserId, portalUid || normalizedLid || cookieUid, "demo-cross"); pushPair(normalizedLid, portalUid || normalizedLid, "numeric+portal"); pushPair(cookieUid, cookieUid, "cookie-same"); pushPair(normalizedLid, normalizedLid, "learningId-same"); pushPair(portalUid, portalUid, "portal-same"); if (!identityPairs.length) return { code: -9004, message: `no-user-id-for-submit(${pref})` }; const tRaw = Math.max(450, Number(timeLen || 0)); const t1 = Math.max(1, tRaw - Math.floor(Math.random() * 7)); const t2 = Math.max(1, tRaw - Math.floor(Math.random() * 7)); const t3 = Math.max(1, tRaw - Math.floor(Math.random() * 7)); const submitPlans = []; const pushPlan = (planItemId, contentId, lessonId) => { const i = String(planItemId || "").trim(); const c = String(contentId || "").trim(); const l = String(lessonId || "").trim(); if (!i || !c) return; if (submitPlans.some(p => p.item_id === i && p.content_id === c && p.lesson_id === l)) return; submitPlans.push({ item_id: i, content_id: c, lesson_id: l || i }); }; if (scormIdHint && primaryId) pushPlan(scormIdHint, primaryId, scormIdHint); pushPlan(primaryId, primaryId, scormIdHint || primaryId); if (scormIdHint) pushPlan(scormIdHint, scormIdHint, scormIdHint); let lastResult = null; try { for (const base of bases) { const courseRef = `${base}/course/learning/learn_course.jsp?is_site=0&course_id=${encodeURIComponent(cid)}`; const noticeRef = `${base}/course/learning/learn_notice.jsp?course_id=${encodeURIComponent(cid)}`; for (const submitPlan of submitPlans) { const submitItemId = String(submitPlan.item_id || "").trim(); const submitContentId = String(submitPlan.content_id || "").trim(); const resolvedScormId = await resolveScormItemIdForContent(cid, primaryId, submitPlan.lesson_id || submitItemId, courseRef); for (const idPair of identityPairs) { const moocRef = (() => { const u = new URL(`${base}/separation/courseware/index.html`); u.searchParams.set("course_id", cid); if (schoolCode) u.searchParams.set("school_code", schoolCode); if (gradeCode) u.searchParams.set("grade_code", gradeCode); if (resolvedScormId) u.searchParams.set("scorm_item_id", String(resolvedScormId)); const userIdForRef = String(prefCookie.user_id || idPair.user_id || "").trim(); if (userIdForRef) u.searchParams.set("user_id", userIdForRef); return u.toString(); })(); try { await createRequest(`${base}/newApp_use_energy.action`, "POST", { req: "saveUseEnergyInfo", learning_user_id: idPair.user_id, course_id: cid, type_code: "progress", item_id: submitItemId }, true, { reqInQuery: true, referer: moocRef }); } catch (_) {} const payloadCore = { req: "submitScormAndHistorySave", user_id: idPair.user_id, course_id: cid, time: t1, item_id: submitItemId, view_time: t2, last_view_time: t3, video_length: tRaw, learning_user_id: idPair.learning_user_id }; const payloadExtended = { ...payloadCore, content_id: submitContentId, lesson_id: resolvedScormId || submitItemId, scorm_item_id: resolvedScormId || submitItemId }; const referers = [moocRef, courseRef, noticeRef, window.location.href].filter(Boolean); for (const ref of referers) { let r = await createRequest(`${base}/learning.action`, "POST", payloadCore, true, { reqInQuery: true, referer: ref }); if (!r || Number(r.code) !== 1000) { const r2 = await createRequest(`${base}/learning.action`, "POST", payloadExtended, true, { reqInQuery: true, referer: ref }); if (r2 && (Number(r2.code) === 1000 || !r)) r = r2; } lastResult = r; if (r && Number(r.code) === -2) continue; if (r && r.code === 1000) { await sleep(300); const verify = await verifyChapterCompletedByHtml(cid, [primaryId, submitItemId, submitContentId, resolvedScormId], ref); if (!verify || verify.done) { log(`\u89c6\u9891\u63d0\u4ea4\u6210\u529f: course_id=${cid}, item_id=${submitItemId}, content_id=${submitContentId}, api=${base}`); return r; } } if (r && Number(r.code) === 2000) break; } } } } return lastResult || { code: -9002, message: "no-submit-attempt-result" }; } catch (e) { log(`\u89c6\u9891\u63d0\u4ea4\u5f02\u5e38: course_id=${cid}, item_id=${primaryId}, err=${e?.message || e}`, "warn"); return { code: -9003, message: `submitPlay-exception:${String(e?.message || e)}` }; } } function parseSchoolAndGradeFromCourse(course) { const out = { schoolCode: getSchoolCode(), gradeCode: "" }; try { if (course?.filePath) { const u = new URL(String(course.filePath), window.location.origin); out.schoolCode = u.searchParams.get("school_code") || out.schoolCode; out.gradeCode = u.searchParams.get("grade_code") || ""; } } catch (_) {} return out; } function randomQuote() { const quotes = [" \u5b66\u800c\u4e0d\u601d\u5219\u7f54\uff0c\u601d\u800c\u4e0d\u5b66\u5219\u6b86\u3002", " \u8def\u6f2b\u6f2b\u5176\u4fee\u8fdc\u516e\uff0c\u543e\u5c06\u4e0a\u4e0b\u800c\u6c42\u7d22\u3002", " \u77e5\u4e4b\u8005\u4e0d\u5982\u597d\u4e4b\u8005\uff0c\u597d\u4e4b\u8005\u4e0d\u5982\u4e50\u4e4b\u8005\u3002", " \u8bfb\u4e66\u7834\u4e07\u5377\uff0c\u4e0b\u7b14\u5982\u6709\u795e\u3002", " \u4e09\u4eba\u884c\uff0c\u5fc5\u6709\u6211\u5e08\u7109\u3002", " \u5df1\u6240\u4e0d\u6b32\uff0c\u52ff\u65bd\u4e8e\u4eba\u3002", " \u5b66\u800c\u65f6\u4e60\u4e4b\uff0c\u4e0d\u4ea6\u8bf4\u4e4e\uff1f", " \u6e29\u6545\u800c\u77e5\u65b0\uff0c\u53ef\u4ee5\u4e3a\u5e08\u77e3\u3002", " \u4e1a\u7cbe\u4e8e\u52e4\uff0c\u8352\u4e8e\u5b09\uff1b\u884c\u6210\u4e8e\u601d\uff0c\u6bc1\u4e8e\u968f\u3002", " \u4e66\u5c71\u6709\u8def\u52e4\u4e3a\u5f84\uff0c\u5b66\u6d77\u65e0\u6daf\u82e6\u4f5c\u821f\u3002", " \u9ed1\u53d1\u4e0d\u77e5\u52e4\u5b66\u65e9\uff0c\u767d\u9996\u65b9\u6094\u8bfb\u4e66\u8fdf\u3002", " \u7eb8\u4e0a\u5f97\u6765\u7ec8\u89c9\u6d45\uff0c\u7edd\u77e5\u6b64\u4e8b\u8981\u8eac\u884c\u3002", " \u95ee\u6e20\u90a3\u5f97\u6e05\u5982\u8bb8\uff1f\u4e3a\u6709\u6e90\u5934\u6d3b\u6c34\u6765\u3002", " \u5b9d\u5251\u950b\u4ece\u78e8\u783a\u51fa\uff0c\u6885\u82b1\u9999\u81ea\u82e6\u5bd2\u6765\u3002", " \u6b32\u7a77\u5343\u91cc\u76ee\uff0c\u66f4\u4e0a\u4e00\u5c42\u697c\u3002", " \u4e0d\u79ef\u8dec\u6b65\uff0c\u65e0\u4ee5\u81f3\u5343\u91cc\uff1b\u4e0d\u79ef\u5c0f\u6d41\uff0c\u65e0\u4ee5\u6210\u6c5f\u6d77\u3002", " \u5929\u884c\u5065\uff0c\u541b\u5b50\u4ee5\u81ea\u5f3a\u4e0d\u606f\u3002", " \u5730\u52bf\u5764\uff0c\u541b\u5b50\u4ee5\u539a\u5fb7\u8f7d\u7269\u3002", " \u5c11\u58ee\u4e0d\u52aa\u529b\uff0c\u8001\u5927\u5f92\u4f24\u60b2\u3002", " \u6d77\u5185\u5b58\u77e5\u5df1\uff0c\u5929\u6daf\u82e5\u6bd4\u90bb\u3002", " \u83ab\u6101\u524d\u8def\u65e0\u77e5\u5df1\uff0c\u5929\u4e0b\u8c01\u4eba\u4e0d\u8bc6\u541b\u3002", " \u5c71\u91cd\u6c34\u590d\u7591\u65e0\u8def\uff0c\u67f3\u6697\u82b1\u660e\u53c8\u4e00\u6751\u3002", " \u6625\u7720\u4e0d\u89c9\u6653\uff0c\u5904\u5904\u95fb\u557c\u9e1f\u3002", " \u968f\u98ce\u6f5c\u5165\u591c\uff0c\u6da6\u7269\u7ec6\u65e0\u58f0\u3002", " \u4e0d\u8bc6\u5e90\u5c71\u771f\u9762\u76ee\uff0c\u53ea\u7f18\u8eab\u5728\u6b64\u5c71\u4e2d\u3002", " \u6a2a\u770b\u6210\u5cad\u4fa7\u6210\u5cf0\uff0c\u8fdc\u8fd1\u9ad8\u4f4e\u5404\u4e0d\u540c\u3002", " \u4e0d\u754f\u6d6e\u4e91\u906e\u671b\u773c\uff0c\u81ea\u7f18\u8eab\u5728\u6700\u9ad8\u5c42\u3002", " \u5c71\u91cd\u6c34\u590d\u7591\u65e0\u8def\uff0c\u67f3\u6697\u82b1\u660e\u53c8\u4e00\u6751\u3002", " \u5148\u5929\u4e0b\u4e4b\u5fe7\u800c\u5fe7\uff0c\u540e\u5929\u4e0b\u4e4b\u4e50\u800c\u4e50\u3002", " \u4eba\u751f\u81ea\u53e4\u8c01\u65e0\u6b7b\uff0c\u7559\u53d6\u4e39\u5fc3\u7167\u6c57\u9752\u3002", " \u9752\u5c71\u906e\u4e0d\u4f4f\uff0c\u6bd5\u7adf\u4e1c\u6d41\u53bb\u3002", " \u4f17\u91cc\u5bfb\u4ed6\u5343\u767e\u5ea6\uff0c\u84e6\u7136\u56de\u9996\uff0c\u90a3\u4eba\u5374\u5728\u706f\u706b\u9611\u73ca\u5904\u3002", " \u67af\u85e4\u8001\u6811\u660f\u9e26\uff0c\u5c0f\u6865\u6d41\u6c34\u4eba\u5bb6\u3002", " \u5915\u9633\u897f\u4e0b\uff0c\u65ad\u80a0\u4eba\u5728\u5929\u6daf\u3002", " \u529d\u541b\u66f4\u5c3d\u4e00\u676f\u9152\uff0c\u897f\u51fa\u9633\u5173\u65e0\u6545\u4eba\u3002", " \u6d1b\u9633\u4eb2\u53cb\u5982\u76f8\u95ee\uff0c\u4e00\u7247\u51b0\u5fc3\u5728\u7389\u58f6\u3002", " \u9ec4\u6c99\u767e\u6218\u7a7f\u91d1\u7532\uff0c\u4e0d\u7834\u697c\u5170\u7ec8\u4e0d\u8fd8\u3002", " \u8461\u8404\u7f8e\u9152\u591c\u5149\u676f\uff0c\u6b32\u996e\u7435\u7436\u9a6c\u4e0a\u50ac\u3002", " \u9189\u5367\u6c99\u573a\u541b\u83ab\u7b11\uff0c\u53e4\u6765\u5f81\u6218\u51e0\u4eba\u56de\u3002", " \u660e\u6708\u677e\u95f4\u7167\uff0c\u6e05\u6cc9\u77f3\u4e0a\u6d41\u3002", " \u91c7\u83ca\u4e1c\u7bf1\u4e0b\uff0c\u60a0\u7136\u89c1\u5357\u5c71\u3002", " \u7ed3\u5e90\u5728\u4eba\u5883\uff0c\u800c\u65e0\u8f66\u9a6c\u55a7\u3002", " \u79cd\u8c46\u5357\u5c71\u4e0b\uff0c\u8349\u76db\u8c46\u82d7\u7a00\u3002", " \u6668\u5174\u7406\u8352\u79fd\uff0c\u5e26\u6708\u8377\u9504\u5f52\u3002", " \u95ee\u541b\u4f55\u80fd\u5c14\uff1f\u5fc3\u8fdc\u5730\u81ea\u504f\u3002", " \u6b64\u4e2d\u6709\u771f\u610f\uff0c\u6b32\u8fa8\u5df2\u5fd8\u8a00\u3002", " \u5343\u5c71\u9e1f\u98de\u7edd\uff0c\u4e07\u5f84\u4eba\u8e2a\u706d\u3002", " \u5b64\u821f\u84d1\u7b20\u7fc1\uff0c\u72ec\u9493\u5bd2\u6c5f\u96ea\u3002", " \u79bb\u79bb\u539f\u4e0a\u8349\uff0c\u4e00\u5c81\u4e00\u67af\u8363\u3002", " \u91ce\u706b\u70e7\u4e0d\u5c3d\uff0c\u6625\u98ce\u5439\u53c8\u751f\u3002", " \u540c\u662f\u5929\u6daf\u6ca6\u843d\u4eba\uff0c\u76f8\u9022\u4f55\u5fc5\u66fe\u76f8\u8bc6\u3002", " \u5927\u5f26\u5608\u5608\u5982\u6025\u96e8\uff0c\u5c0f\u5f26\u5207\u5207\u5982\u79c1\u8bed\u3002", " \u5608\u5608\u5207\u5207\u9519\u6742\u5f39\uff0c\u5927\u73e0\u5c0f\u73e0\u843d\u7389\u76d8\u3002", " \u522b\u6709\u5e7d\u6101\u6697\u6068\u751f\uff0c\u6b64\u65f6\u65e0\u58f0\u80dc\u6709\u58f0\u3002", " \u4e1c\u8fb9\u65e5\u51fa\u897f\u8fb9\u96e8\uff0c\u9053\u662f\u65e0\u6674\u5374\u6709\u6674\u3002", " \u6c89\u821f\u4fa7\u7554\u5343\u5e06\u8fc7\uff0c\u75c5\u6811\u524d\u5934\u4e07\u6728\u6625\u3002", " \u65e7\u65f6\u738b\u8c22\u5802\u524d\u71d5\uff0c\u98de\u5165\u5bfb\u5e38\u767e\u59d3\u5bb6\u3002", " \u4eba\u4e16\u51e0\u56de\u4f24\u5f80\u4e8b\uff0c\u5c71\u5f62\u4f9d\u65e7\u6795\u5bd2\u6d41\u3002", " \u81ea\u53e4\u9022\u79cb\u60b2\u5bc2\u5be5\uff0c\u6211\u8a00\u79cb\u65e5\u80dc\u6625\u671d\u3002", " \u6674\u7a7a\u4e00\u9e64\u6392\u4e91\u4e0a\uff0c\u4fbf\u5f15\u8bd7\u60c5\u5230\u78a7\u9704\u3002", " \u7af9\u6756\u8292\u978b\u8f7b\u80dc\u9a6c\uff0c\u8c01\u6015\uff1f\u4e00\u84d1\u70df\u96e8\u4efb\u5e73\u751f\u3002", " \u56de\u9996\u5411\u6765\u8427\u745f\u5904\uff0c\u5f52\u53bb\uff0c\u4e5f\u65e0\u98ce\u96e8\u4e5f\u65e0\u6674\u3002", " \u4e71\u77f3\u7a7f\u7a7a\uff0c\u60ca\u6d9b\u62cd\u5cb8\uff0c\u5377\u8d77\u5343\u5806\u96ea\u3002", " \u5927\u6c5f\u4e1c\u53bb\uff0c\u6d6a\u6dd8\u5c3d\uff0c\u5343\u53e4\u98ce\u6d41\u4eba\u7269\u3002", " \u4eba\u751f\u5982\u68a6\uff0c\u4e00\u5c0a\u8fd8\u9179\u6c5f\u6708\u3002", " \u4f46\u613f\u4eba\u957f\u4e45\uff0c\u5343\u91cc\u5171\u5a75\u5a1f\u3002", " \u4eba\u6709\u60b2\u6b22\u79bb\u5408\uff0c\u6708\u6709\u9634\u6674\u5706\u7f3a\uff0c\u6b64\u4e8b\u53e4\u96be\u5168\u3002", " \u4e0d\u5e94\u6709\u6068\uff0c\u4f55\u4e8b\u957f\u5411\u522b\u65f6\u5706\uff1f", " \u9ad8\u5904\u4e0d\u80dc\u5bd2\uff0c\u8d77\u821e\u5f04\u6e05\u5f71\uff0c\u4f55\u4f3c\u5728\u4eba\u95f4\u3002", " \u4f1a\u633d\u96d5\u5f13\u5982\u6ee1\u6708\uff0c\u897f\u5317\u671b\uff0c\u5c04\u5929\u72fc\u3002", " \u5341\u5e74\u751f\u6b7b\u4e24\u832b\u832b\uff0c\u4e0d\u601d\u91cf\uff0c\u81ea\u96be\u5fd8\u3002", " \u5343\u91cc\u5b64\u575f\uff0c\u65e0\u5904\u8bdd\u51c4\u51c9\u3002", " \u7eb5\u4f7f\u76f8\u9022\u5e94\u4e0d\u8bc6\uff0c\u5c18\u6ee1\u9762\uff0c\u9b13\u5982\u971c\u3002", " \u76f8\u987e\u65e0\u8a00\uff0c\u60df\u6709\u6cea\u5343\u884c\u3002", " \u6599\u5f97\u5e74\u5e74\u80a0\u65ad\u5904\uff0c\u660e\u6708\u591c\uff0c\u77ed\u677e\u5188\u3002", " \u8001\u592b\u804a\u53d1\u5c11\u5e74\u72c2\uff0c\u5de6\u7275\u9ec4\uff0c\u53f3\u64ce\u82cd\u3002", " \u9526\u5e3d\u8c82\u88d8\uff0c\u5343\u9a91\u5377\u5e73\u5188\u3002", " \u4e3a\u62a5\u503e\u57ce\u968f\u592a\u5b88\uff0c\u4eb2\u5c04\u864e\uff0c\u770b\u5b59\u90ce\u3002", " \u9152\u9163\u80f8\u80c6\u5c1a\u5f00\u5f20\uff0c\u9b13\u5fae\u971c\uff0c\u53c8\u4f55\u59a8\uff01", " \u6301\u8282\u4e91\u4e2d\uff0c\u4f55\u65e5\u9063\u51af\u5510\uff1f", " \u4f1a\u633d\u96d5\u5f13\u5982\u6ee1\u6708\uff0c\u897f\u5317\u671b\uff0c\u5c04\u5929\u72fc\u3002", " \u5927\u6c5f\u4e1c\u53bb\uff0c\u6d6a\u6dd8\u5c3d\uff0c\u5343\u53e4\u98ce\u6d41\u4eba\u7269\u3002", " \u4e71\u77f3\u7a7f\u7a7a\uff0c\u60ca\u6d9b\u62cd\u5cb8\uff0c\u5377\u8d77\u5343\u5806\u96ea\u3002", " \u6c5f\u5c71\u5982\u753b\uff0c\u4e00\u65f6\u591a\u5c11\u8c6a\u6770\u3002", " \u9065\u60f3\u516c\u747e\u5f53\u5e74\uff0c\u5c0f\u4e54\u521d\u5ac1\u4e86\uff0c\u96c4\u59ff\u82f1\u53d1\u3002", " \u7fbd\u6247\u7eb6\u5dfe\uff0c\u8c08\u7b11\u95f4\uff0c\u6a2f\u6a79\u7070\u98de\u70df\u706d\u3002", " \u6545\u56fd\u795e\u6e38\uff0c\u591a\u60c5\u5e94\u7b11\u6211\uff0c\u65e9\u751f\u534e\u53d1\u3002", " \u4eba\u751f\u5982\u68a6\uff0c\u4e00\u5c0a\u8fd8\u9179\u6c5f\u6708\u3002", " \u83ab\u542c\u7a7f\u6797\u6253\u53f6\u58f0\uff0c\u4f55\u59a8\u541f\u5578\u4e14\u5f90\u884c\u3002", " \u7af9\u6756\u8292\u978b\u8f7b\u80dc\u9a6c\uff0c\u8c01\u6015\uff1f\u4e00\u84d1\u70df\u96e8\u4efb\u5e73\u751f\u3002", " \u6599\u5ced\u6625\u98ce\u5439\u9152\u9192\uff0c\u5fae\u51b7\uff0c\u5c71\u5934\u659c\u7167\u5374\u76f8\u8fce\u3002", " \u56de\u9996\u5411\u6765\u8427\u745f\u5904\uff0c\u5f52\u53bb\uff0c\u4e5f\u65e0\u98ce\u96e8\u4e5f\u65e0\u6674\u3002", " \u6c34\u5149\u6f4b\u6edf\u6674\u65b9\u597d\uff0c\u5c71\u8272\u7a7a\u8499\u96e8\u4ea6\u5947\u3002", " \u6b32\u628a\u897f\u6e56\u6bd4\u897f\u5b50\uff0c\u6de1\u5986\u6d53\u62b9\u603b\u76f8\u5b9c\u3002", " \u4e0d\u8bc6\u5e90\u5c71\u771f\u9762\u76ee\uff0c\u53ea\u7f18\u8eab\u5728\u6b64\u5c71\u4e2d\u3002"]; return quotes[Math.floor(Math.random() * quotes.length)]; } async function getBbsScore({ courseId, schoolCode, gradeCode, learningUserId, course = null }) { const preferPrefix = resolvePreferredLearningPrefix({ course, learningUserId }); const bases = getLearningBasesByPrefixFirst(preferPrefix); for (const b of bases) { try { const url = `${b.origin}/${b.prefix}/forum_article.action`; const result = await createRequest(url, "POST", { req: "getBbsScore", user_id: learningUserId, course_id: courseId, school_code: schoolCode, grade_code: gradeCode }, true, { reqInQuery: true }); if (result?.code === 1000) return result.data || null; log(`\u67e5\u8be2\u8bc4\u8bba\u5206\u6570\u5931\u8d25: prefix=${b.prefix} code=${result?.code}, msg=${result?.message || "\u672a\u77e5"}`, "warn"); } catch (e) { log(`\u67e5\u8be2\u8bc4\u8bba\u5206\u6570\u5f02\u5e38: prefix=${b.prefix} ${e.message}`, "warn"); } } return null; } function buildForumReferer({ courseId, schoolCode, gradeCode, courseCode, base }) { const entry = base || LEARNING_BASES[0]; const u = new URL(`${entry.origin}/${entry.prefix}/separation/coursePost/topicList.html`); if (courseId) u.searchParams.set("course_id", String(courseId)); if (schoolCode) u.searchParams.set("school_code", String(schoolCode)); if (gradeCode) u.searchParams.set("grade_code", String(gradeCode)); if (courseCode) u.searchParams.set("course_code", String(courseCode)); return u.toString(); } async function saveUseEnergyInfo({ courseId, learningUserId, schoolCode, gradeCode, courseCode, course = null }) { const preferPrefix = resolvePreferredLearningPrefix({ course, learningUserId }); const bases = getLearningBasesByPrefixFirst(preferPrefix); for (const b of bases) { const url = `${b.origin}/${b.prefix}/newApp_use_energy.action`; try { const referer = buildForumReferer({ courseId, schoolCode, gradeCode, courseCode, base: b }); const aligned = alignLearningUserIdForApiPrefix(learningUserId, b.prefix); const attempts = [ { label: "aligned", learning_user_id: aligned }, { label: "raw", learning_user_id: learningUserId }, { label: "numeric", learning_user_id: normalizeOpenLearningUserId(learningUserId) } ]; for (const att of attempts) { if (!att.learning_user_id) continue; const payload = { req: "saveUseEnergyInfo", learning_user_id: att.learning_user_id, course_id: courseId, type_code: "bbs", item_id: "0098" }; const result = await createRequest(url, "POST", payload, true, { reqInQuery: true, referer, origin: (() => { try { return new URL(url).origin; } catch (_) { return ""; } })() }); log(`\u8bc4\u8bba\u524d\u7f6e\u63a5\u53e3\u8fd4\u56de(prefix=${b.prefix}, id=${att.label}): ${JSON.stringify(result).slice(0, 300)}`); if (result?.code === 1000) return { ok: true, base: b, learningUserIdForForum: att.learning_user_id }; const msg = String(result?.message || ""); if (result?.code === 2000 && (msg.includes("\u591a\u7528\u6237") || msg.includes("\u91cd\u65b0\u767b\u5f55"))) { if (att.label === "numeric") { log("\u8bc4\u8bba\u524d\u7f6e\uff1a\u591a\u7528\u6237\u6821\u9a8c\u4ecd\u5931\u8d25\uff0c\u4e0d\u518d\u6362 prefix", "warn"); return { ok: false, base: null }; } continue; } log(`\u8bc4\u8bba\u524d\u7f6e\u63a5\u53e3\u5931\u8d25: prefix=${b.prefix} id=${att.label} code=${result?.code}, msg=${result?.message || "\u672a\u77e5"}`, "warn"); } } catch (e) { log(`\u8bc4\u8bba\u524d\u7f6e\u63a5\u53e3\u5f02\u5e38: prefix=${b.prefix} ${e.message}`, "warn"); } } return { ok: false, base: null }; } async function publishComment({ courseId, schoolCode, gradeCode, courseCode, learningUserId }) { try { const energyRet = await saveUseEnergyInfo({ courseId, learningUserId, schoolCode, gradeCode, courseCode }); if (!energyRet.ok) { log("\u8df3\u8fc7\u8bc4\u8bba\u63d0\u4ea4\uff1a\u524d\u7f6e\u80fd\u91cf\u63a5\u53e3\u672a\u901a\u8fc7", "warn"); return false; } const forumUserId = energyRet.learningUserIdForForum || alignLearningUserIdForApiPrefix(learningUserId, energyRet.base.prefix); const referer = buildForumReferer({ courseId, schoolCode, gradeCode, courseCode, base: energyRet.base }); const body = { req: "publishArticle", user_id: forumUserId, course_id: courseId, school_code: schoolCode, grade_code: gradeCode, course_code: courseCode || "", content: `${randomQuote()}`, is_ask: "P9UdazN874Ud/dXSFB15bA==", img_url: "vMdG0uq2bC114oxfD37j/Q==", time: "P9UdazN874Ud/dXSFB15bA==" }; const url = `${energyRet.base.origin}/${energyRet.base.prefix}/forum_article.action`; log(`\u8bc4\u8bba\u8bf7\u6c42: url=${url}, course_id=${courseId}, school_code=${schoolCode}, grade_code=${gradeCode}, course_code=${courseCode || ""}`); const result = await createRequest(url, "POST", body, true, { reqInQuery: true, rawKeys: ["is_ask", "img_url", "time"], referer }); if (result?.code === 1000) { log(`\u8bc4\u8bba\u6210\u529f: ${result?.message || "ok"}`); return true; } log(`\u8bc4\u8bba\u5931\u8d25(\u539f\u59cb\u8fd4\u56de): ${JSON.stringify(result).slice(0, 500)}`, "warn"); return false; } catch (e) { log(`\u8bc4\u8bba\u5f02\u5e38: ${e.message}`, "warn"); return false; } } async function enterDocumentCheck({ endpoint, courseId, uid, schoolCode, gradeCode, studentType, courseCode }) { try { const payload = { req: "learnContentDocumentEnterCheckNew", user_id: uid, learning_user_id: uid, course_id: courseId, school_code: schoolCode || "", grade_code: gradeCode || "", student_type: "kU3gxRasa472peQC+cvl7A==", course_code: courseCode || "", phone_type: "mQ1mB1fHcgoWRJNXMOtUyw==", app_release: "nOqVkA13Jv74+ugChBaZFg==", current_version: "1OT8zweFkdJnoGkuQyZ2rg==", type: "1" }; const r1 = await createRequest(endpoint, "POST", payload, true, { reqInQuery: true, rawKeys: ["student_type", "phone_type", "app_release", "current_version", "type"] }); if (r1?.code === 1000) return true; const r2 = await createRequest(endpoint, "POST", payload, true, { reqInQuery: false, rawKeys: ["student_type", "phone_type", "app_release", "current_version", "type"] }); return r2?.code === 1000; } catch (_) { return false; } } async function getLearnContentDocumentList({ courseId, learningUserId, userId, course }) { const open = getOpenlearningParams(course); log(`\u8d44\u6599\u4e0a\u4e0b\u6587: school_code=${open.schoolCode || "empty"}, grade_code=${open.gradeCode || "empty"}, user_type=${open.userType || "empty"}`); const unwrapData = (maybe) => { if (!maybe) return null; if (typeof maybe === "string") { const d = decrypt(maybe); if (d) return d; try { return JSON.parse(maybe); } catch (_) { return null; } } return maybe; }; const extractList = (data) => { const d = unwrapData(data); if (!d) return []; if (Array.isArray(d)) return d; const dbg = unwrapData(d.debugData); if (Array.isArray(dbg)) return dbg; const candidates = [ dbg?.documentList, dbg?.list, dbg?.rows, dbg?.learnContentList, d.documentList, d.list, d.rows, d.learnContentList, d.data?.documentList, d.data?.list, d.data?.rows, d.data?.learnContentList ]; for (const c of candidates) { const cc = unwrapData(c); if (Array.isArray(cc) && cc.length) return cc; } return []; }; const endpoints = []; for (const b of LEARNING_BASES) { endpoints.push(`${b.origin}/${b.prefix}/newApp_learn_type.action`); endpoints.push(`${b.origin}/${b.prefix}/newApp_learning_course_info.action`); } const uidCandidates = Array.from(new Set([open.openUserId, learningUserId, userId].filter(Boolean))); for (const endpoint of endpoints) { for (const uid of uidCandidates) { await enterDocumentCheck({ endpoint, courseId, uid, schoolCode: open.schoolCode, gradeCode: open.gradeCode, studentType: open.userType || "student", courseCode: open.courseCode }); try { const payload = { req: "getLearnContentDocumentList", user_id: uid, learning_user_id: uid, course_id: courseId, school_code: open.schoolCode || "", grade_code: open.gradeCode || "", course_code: open.courseCode || "", content_type: "6gAHDFe5Eygo3rVYLKF61Q==", phone_type: "mQ1mB1fHcgoWRJNXMOtUyw==", app_release: "nOqVkA13Jv74+ugChBaZFg==", current_version: "1OT8zweFkdJnoGkuQyZ2rg==", type: "1", student_type: "kU3gxRasa472peQC+cvl7A==" }; const resultQ = await createRequest(endpoint, "POST", payload, true, { reqInQuery: true, rawKeys: ["student_type", "content_type", "phone_type", "app_release", "current_version", "type"] }); const resultB = resultQ?.code === 1000 ? resultQ : await createRequest(endpoint, "POST", payload, true, { reqInQuery: false, rawKeys: ["student_type", "content_type", "phone_type", "app_release", "current_version", "type"] }); if (resultB?.code === 1000 && resultB?.data) { const list = extractList(resultB.data); if (list.length) return list; try { const d = unwrapData(resultB.data) || {}; const keys = Object.keys(d).slice(0, 18).join(","); log(`\u8d44\u6599\u5217\u8868\u89e3\u6790\u4e3a\u7a7a(code=1000): keys=${keys}, debugType=${typeof d.debugData}`, "warn"); } catch (_) {} } } catch (_) {} } } return []; } function extractDocIds(doc) { const ids = [doc?.scormItemId, doc?.relationId, doc?.contentId, doc?.scorm_item_id, doc?.item_id, doc?.smallItemId, doc?.itemId, doc?.id].filter(Boolean).map(v => String(v)); return Array.from(new Set(ids)); } function buildDocumentTasks(docList) { const tasks = []; if (!Array.isArray(docList)) { return tasks; } for (const d of docList) { const allowOpen = (d?.allowOpen === undefined) ? true : !!d.allowOpen; const isFinish = !!d?.isFinish; if (!allowOpen || isFinish) continue; const ids = extractDocIds(d); if (!ids.length) { continue; } tasks.push({ title: String(d?.title || d?.itemName || d?.courseName || "\u672a\u547d\u540d\u8d44\u6599"), ids: ids }); } return tasks; } async function submitDocumentProgress({ courseId, docId, learningUserId, userId, course }) { const open = getOpenlearningParams(course); const endpoints = getLearningBasesByPrefixFirst(getLearningPrefixFromCurrentPage() || "").map(b => ({ submit: b.origin + "/" + b.prefix + "/newApp_Scorm.action", points: b.origin + "/" + b.prefix + "/newApp_point.action" })); const learningUid = open.openUserId || learningUserId || userId || ""; const studentId = (await getStudentIdCached(userId)) || userId || ""; const baseSubmit = { course_id: courseId, item_id: docId, scorm_item_id: docId, user_id: learningUid, school_code: open.schoolCode || "", grade_code: open.gradeCode || "", phone_type: "mQ1mB1fHcgoWRJNXMOtUyw==", app_release: "nOqVkA13Jv74+ugChBaZFg==", current_version: "1OT8zweFkdJnoGkuQyZ2rg==", time: "3Rqf9QAfRTCy6NKORwd24Q==", type: "1", student_type: "kU3gxRasa472peQC+cvl7A==" }; const basePoints = { item_id: docId, user_id: studentId, learning_user_id: learningUid, school_code: open.schoolCode || "", grade_code: open.gradeCode || "", course_code: open.courseCode || "", phone_type: "mQ1mB1fHcgoWRJNXMOtUyw==", app_release: "nOqVkA13Jv74+ugChBaZFg==", current_version: "1OT8zweFkdJnoGkuQyZ2rg==", learn_type: "6gAHDFe5Eygo3rVYLKF61Q==", type: "1", student_type: "kU3gxRasa472peQC+cvl7A==" }; const failNotes = []; for (const endpoint of endpoints) { try { const pointsQ = await createRequest(endpoint.points, "POST", { req: "savePoints", ...basePoints }, true, { reqInQuery: true, rawKeys: ["student_type", "learn_type", "phone_type", "app_release", "current_version", "type"] }); const pointsB = pointsQ?.code === 1000 ? pointsQ : await createRequest(endpoint.points, "POST", { req: "savePoints", ...basePoints }, true, { reqInQuery: false, rawKeys: ["student_type", "learn_type", "phone_type", "app_release", "current_version", "type"] }); if (pointsB?.code === 1000 || pointsQ?.code === 1000) { return true; } failNotes.push("savePoints(" + (endpoint.points.includes("/openlearning/") ? "openlearning" : "zhlearning") + "): " + (pointsB?.message || pointsQ?.message || "\u672a\u77e5")); const submitQ = await createRequest(endpoint.submit, "POST", { req: "submitText", ...baseSubmit }, true, { reqInQuery: true, rawKeys: ["student_type", "time", "phone_type", "app_release", "current_version", "type"] }); const submitB = submitQ?.code === 1000 ? submitQ : await createRequest(endpoint.submit, "POST", { req: "submitText", ...baseSubmit }, true, { reqInQuery: false, rawKeys: ["student_type", "time", "phone_type", "app_release", "current_version", "type"] }); if (submitB?.code === 1000 || submitQ?.code === 1000) { return true; } failNotes.push("submitText(" + (endpoint.submit.includes("/openlearning/") ? "openlearning" : "zhlearning") + "): " + (submitB?.message || submitQ?.message || "\u672a\u77e5")); } catch (_) {} } if (failNotes.length) { log("\u8d44\u6599\u63d0\u4ea4\u5931\u8d25\u6c47\u603b(item_id=" + docId + "): " + failNotes.slice(0, 4).join(" | "), "warn"); } return false; } async function handleDocumentsForCourse({ courseId, learningUserId, userId, course }) { const docs = await getLearnContentDocumentList({ courseId, learningUserId, userId, course }); if (!docs.length) { log("\u8d44\u6599\u5217\u8868\u4e3a\u7a7a\u6216\u672a\u83b7\u53d6\u5230\uff0c\u8df3\u8fc7\u8d44\u6599\u5904\u7406", "warn"); return; } const tasks = buildDocumentTasks(docs); if (!tasks.length) { log("\u8d44\u6599\u5217\u8868\u5df2\u83b7\u53d6\uff0c\u4f46\u5747\u4e3a\u4e0d\u53ef\u6253\u5f00\u6216\u5df2\u5b8c\u6210\uff0c\u8df3\u8fc7\u8d44\u6599\u5904\u7406"); return; } log("\u5f00\u59cb\u5904\u7406\u8d44\u6599: " + tasks.length + " \u6761"); const visited = new Set(); let ok = 0; let fail = 0; for (let idx = 0; idx < tasks.length; idx++) { const task = tasks[idx]; updatePillarProgress("doc", task.title + "\u5904\u7406\u4e2d\uff08" + (idx + 1) + "/" + tasks.length + "\uff09"); const ids = task.ids; let done = false; for (const docId of ids) { if (visited.has(docId)) { continue; } visited.add(docId); const pass = await submitDocumentProgress({ courseId, docId, learningUserId, userId, course }); if (pass) { ok += 1; done = true; log("✓ \u8d44\u6599\u8fdb\u5ea6 " + (idx + 1) + "/" + tasks.length + " item_id=" + docId + ", title=" + task.title); updatePillarProgress("doc", task.title + "\u5df2\u5b8c\u6210\uff08" + (idx + 1) + "/" + tasks.length + "\uff09"); await sleep(Math.max(1000, Number(CONFIG.documentDelay || 10000))); break; } } if (!done) { fail += 1; log("✗ \u8d44\u6599\u8fdb\u5ea6 " + (idx + 1) + "/" + tasks.length + " item_id=" + (ids[0] || "none") + ", title=" + task.title, "warn"); updatePillarProgress("doc", task.title + "\u5931\u8d25\uff08" + (idx + 1) + "/" + tasks.length + "\uff09"); } } log("\u8d44\u6599\u5904\u7406\u5b8c\u6210: \u6210\u529f " + ok + "\uff0c\u5931\u8d25 " + fail); } function pickExamScoreIdFromItemListData(data) { const walk = (d) => { if (!d || typeof d !== "object") return ""; const keys = ["examScoreId", "exam_score_id", "scoreId", "score_id", "userExamScoreId", "examScoreID", "userScoreId"]; for (const k of keys) { const v = d[k]; if (v != null && String(v).trim() !== "" && String(v).toLowerCase() !== "null") { return String(v).trim(); } } if (Array.isArray(d.mallInfoList) && d.mallInfoList.length) { const s = walk(d.mallInfoList[0]); if (s) return s; } if (d.debugData && typeof d.debugData === "object") return walk(d.debugData); return ""; }; return walk(data); } function pickHomeworkListFromData(data) { if (Array.isArray(data) && data.length) return data; const d = (data && typeof data === "object") ? data : {}; const keys = [ "mallInfoList", "mall_info_list", "homeworkList", "workList", "examList", "taskList", "exerciseList", "exercise_list", "learnCourseExerciseList", "homework_list", "list", "rows", "records" ]; const candidates = keys.map(k => d[k]).concat([ d.homeworkList, d.workList, d.examList, d.taskList, d.list, d.rows, d.debugData, d.data, d.result ]); for (const c of candidates) { if (Array.isArray(c) && c.length) return c; if (c && typeof c === "object") { for (const k of keys) { if (Array.isArray(c[k]) && c[k].length) return c[k]; } } } return []; } function parseHomeworkItemsFromHtml(html) { const text = String(html || "").replace(/\\\//g, "/"); const out = []; const seen = new Set(); const addItem = (item) => { const examId = String(item.examId || item.exam_id || item.id || "").trim(); if (!examId || seen.has(examId)) return; seen.add(examId); out.push(item); }; const objRe = /\{[^{}]{0,1600}?"examId"\s*:\s*"?(\d+)"?[^{}]{0,1600}?\}/gi; let m; while ((m = objRe.exec(text)) !== null) { const frag = m[0]; const examId = m[1]; const title = (frag.match(/"(?:examName|title|name)"\s*:\s*"([^"]+)"/i) || [])[1] || "\u672a\u547d\u540d\u4f5c\u4e1a"; const topScore = parseFloat((frag.match(/"(?:examTopScore|topScore|examScore)"\s*:\s*"?([\d.]+)"?/i) || [])[1] || "NaN"); const isFinish = /"(?:isFinish|finish|completed)"\s*:\s*(true|1|"1")/i.test(frag); addItem({ examId, examName: title, examTopScore: Number.isNaN(topScore) ? null : topScore, isFinish }); } const attrRe = /ExamId\s*=\s*["']([^"']+)["'][\s\S]{0,1200}/gi; while ((m = attrRe.exec(text)) !== null) { const chunk = m[0]; const examId = m[1]; const title = (chunk.match(/(?:examName|ExamName|title)\s*=\s*["']([^"']+)["']/i) || [])[1] || "\u672a\u547d\u540d\u4f5c\u4e1a"; const topM = chunk.match(/(?:examTopScore|TopScore|Score)\s*=\s*["']?([\d.]+)/i); const topScore = topM ? parseFloat(topM[1]) : null; const isFinish = /\u5df2\u5b8c\u6210|\u5df2\u4ea4\u5377|isFinish\s*=\s*["']?(?:true|1)/i.test(chunk); addItem({ examId, examName: String(title).trim(), examTopScore: Number.isNaN(topScore) ? null : topScore, isFinish }); } return out; } async function getHomeworkListFromHtml(courseId, preferPrefix = "") { const prefer = preferPrefix || getLearningPrefixFromCurrentPage() || ""; const basesOrder = getLearningBasesByPrefixFirst(prefer); const candidates = []; const cid = encodeURIComponent(String(courseId || "")); for (const b of basesOrder) { candidates.push(`${b.origin}/${b.prefix}/course/learning/learn_homework.jsp?is_site=0&course_id=${cid}`); } const currentRef = String(window.location.href || "").trim(); for (const url of candidates) { const prefix = detectLearningPrefixFromUrlLike(url) || "unknown"; const resp = await requestHtml(url, currentRef || url); const html = resp.html || ""; if (!html) { log(`\u4f5c\u4e1aHTML\u4e3a\u7a7a: prefix=${prefix} status=${resp.status}, from=${url}`); continue; } const items = parseHomeworkItemsFromHtml(html); if (items.length) { log(`\u4f5c\u4e1aHTML\u89e3\u6790\u6210\u529f: prefix=${prefix} count=${items.length} (learn_homework.jsp)`); return items; } log(`\u4f5c\u4e1aHTML\u672a\u547d\u4e2d: prefix=${prefix} len=${html.length}, status=${resp.status}`); } return []; } async function getLearnCourseExerciseList({ courseId, learningUserId, userId: portalUserId, course, gradeHint = "" }) { const ctx = getHomeworkApiContext(course, gradeHint); const preferPrefix = resolvePreferredLearningPrefix({ course, learningUserId }) || getLearningPrefixFromCurrentPage() || ""; const reqUrls = getHomeworkCourseInfoUrls(preferPrefix); const uidNorm = normalizeOpenLearningUserId(learningUserId); const zhForm = uidNorm && !/_zhlearning_/i.test(String(learningUserId || "")) ? "_zhlearning_" + uidNorm : null; const userIdVariants = Array.from(new Set([learningUserId, portalUserId, uidNorm, zhForm, ctx.open.openUserId].filter(Boolean))); const cidCandidates = Array.from(new Set([String(courseId || "").trim(), ...resolveCourseIdCandidates(course || {})].filter(Boolean))); const opts = { reqInQuery: true, rawKeys: ["phone_type", "app_release", "current_version", "type", "type_code", "student_type"] }; let lastRes = null; for (const cid of cidCandidates) { for (const reqUrl of reqUrls) { for (const uid of userIdVariants) { for (const referer of homeworkReferersForRequestUrl(reqUrl, cid, course, ctx.gradeCode)) { const payload = { req: "getLearnCourseExerciseList", course_id: cid, phone_type: "mQ1mB1fHcgoWRJNXMOtUyw==", school_code: ctx.schoolCode || "", app_release: "nOqVkA13Jv74+ugChBaZFg==", course_code: ctx.courseCode, user_id: uid, current_version: "1OT8zweFkdJnoGkuQyZ2rg==", grade_code: ctx.gradeCode || "", type: "1", type_code: "YmWS4kf/UO40OYP03qneGw==", student_type: "kU3gxRasa472peQC+cvl7A==" }; const res = await createRequest(reqUrl, "POST", payload, true, { ...opts, referer: referer }); lastRes = res; if (res?.code === 1000 && res.data != null) { const list = pickHomeworkListFromData(res.data); if (list.length) { if (cid !== String(courseId)) { log("getLearnCourseExerciseList: \u4f7f\u7528\u5907\u9009 course_id=" + cid + " \u62c9\u53d6\u5230\u4f5c\u4e1a", "info"); } log("getLearnCourseExerciseList: \u547d\u4e2d " + reqUrl + " user_id=" + uid, "info"); return list; } } } } } } const htmlList = await getHomeworkListFromHtml(String(courseId || "").trim(), preferPrefix); if (htmlList.length) { return htmlList; } if (lastRes && lastRes.code !== 1000) { const c = Number(lastRes.code || 0); if (c === 1001 || c === 1003) { log("getLearnCourseExerciseList: code=" + lastRes.code + ", msg=" + (lastRes.message || "\u65e0\u4f5c\u4e1a") + "\uff08\u6309\u672c\u8bfe\u65e0\u4f5c\u4e1a\u5904\u7406\u5e76\u7ee7\u7eed\uff09", "info"); } else { log("getLearnCourseExerciseList: code=" + lastRes.code + ", msg=" + (lastRes.message || "\u672a\u77e5") + "\uff08\u5df2\u8bd5\u591a\u57df\u540d open/zh \u7aef\u70b9\u3001HTML \u515c\u5e95\u3001\u591a course_id/referer\uff09", "warn"); } } else if (lastRes && lastRes.code === 1000) { log("getLearnCourseExerciseList: \u8fd4\u56de\u6210\u529f\u4f46\u4f5c\u4e1a\u5217\u8868\u4e3a\u7a7a\uff08\u672c\u8bfe\u53ef\u80fd\u65e0\u6d4b\u9a8c/\u4f5c\u4e1a\u6216\u63a5\u53e3\u5b57\u6bb5\u53d8\u66f4\uff09", "warn"); } else { log("getLearnCourseExerciseList: \u5168\u90e8\u8bf7\u6c42\u5931\u8d25\uff1b\u4e0a\u4e0b\u6587 school=" + (ctx.schoolCode || "\u7a7a") + ", grade=" + (ctx.gradeCode || "\u7a7a") + ", course_code=" + (ctx.courseCode || "\u7a7a"), "warn"); } return []; } async function getItemTypeTotalCount({ examId, learningUserId, course, courseId, gradeHint = "" }) { const ctx = getHomeworkApiContext(course, gradeHint); const preferPrefix = resolvePreferredLearningPrefix({ course, learningUserId }) || getLearningPrefixFromCurrentPage() || ""; const payload = { req: "getItemTypeTotalCount", phone_type: "mQ1mB1fHcgoWRJNXMOtUyw==", school_code: ctx.schoolCode || "", app_release: "nOqVkA13Jv74+ugChBaZFg==", course_code: ctx.courseCode, user_id: learningUserId || "", current_version: "1OT8zweFkdJnoGkuQyZ2rg==", grade_code: ctx.gradeCode || "", type: "1", exam_id: String(examId || ""), student_type: "kU3gxRasa472peQC+cvl7A==" }; const opts = { reqInQuery: true, rawKeys: ["phone_type", "app_release", "current_version", "type", "student_type"] }; for (const url of getHomeworkExamTaskUrls(preferPrefix)) { for (const referer of homeworkExamTaskReferers(courseId, course, gradeHint, preferPrefix)) { const res = await createRequest(url, "POST", payload, true, { ...opts, referer: referer }); if (res?.code === 1000) { return true; } } } return false; } async function getHomeworkItemList({ examId, learningUserId, course, courseId, gradeHint = "" }) { const ctx = getHomeworkApiContext(course, gradeHint); const preferPrefix = resolvePreferredLearningPrefix({ course, learningUserId }) || getLearningPrefixFromCurrentPage() || ""; const payload = { req: "getItemList", exam_status: "P9UdazN874Ud/dXSFB15bA==", phone_type: "mQ1mB1fHcgoWRJNXMOtUyw==", school_code: ctx.schoolCode || "", app_release: "nOqVkA13Jv74+ugChBaZFg==", course_code: ctx.courseCode, user_id: learningUserId || "", current_version: "1OT8zweFkdJnoGkuQyZ2rg==", grade_code: ctx.gradeCode || "", type: "1", exam_id: String(examId || ""), student_type: "kU3gxRasa472peQC+cvl7A==" }; const opts = { reqInQuery: true, rawKeys: ["exam_status", "phone_type", "app_release", "current_version", "type", "student_type"] }; let lastRes = null; for (const url of getHomeworkExamTaskUrls(preferPrefix)) { for (const referer of homeworkExamTaskReferers(courseId, course, gradeHint, preferPrefix)) { const res = await createRequest(url, "POST", payload, true, { ...opts, referer: referer }); lastRes = res; if (res?.code !== 1000) { continue; } const d = res.data || {}; const list = pickHomeworkListFromData(d); if (list.length) { let sid = pickExamScoreIdFromItemListData(d); if (!sid && list[0] && typeof list[0] === "object") { const r0 = list[0]; sid = r0.examScoreId || r0.exam_score_id || r0.scoreId || ""; if (sid != null) { sid = String(sid).trim(); } } if (!sid || String(sid).toLowerCase() === "null") { log("getItemList \u5df2\u62ff\u5230\u9898\u76ee\u4f46\u672a\u89e3\u6790\u5230 examScoreId\uff0c\u4ea4\u5377\u53ef\u80fd\u5931\u8d25\uff1b\u53ef\u5f00\u63a7\u5236\u53f0\u770b data \u7ed3\u6784", "warn"); } return { items: list, examScoreId: sid && String(sid).toLowerCase() !== "null" ? sid : "" }; } if (!list.length && d && typeof d === "object") { try { const kk = Object.keys(d).slice(0, 24).join(","); log("getItemList: code=1000 \u4f46\u672a\u89e3\u6790\u5230\u9898\u76ee\u5217\u8868\uff0cdata \u9876\u5c42 keys=" + (kk || "\u65e0"), "warn"); } catch (_) {} } } } if (lastRes) { log("getItemList: \u5df2\u5168\u90e8\u5c1d\u8bd5 open/zh \u4e0e\u591a Referer\uff0c\u6700\u540e code=" + lastRes.code + ", msg=" + (lastRes.message || "\u672a\u77e5"), "warn"); } return { items: [], examScoreId: "" }; } async function automaticSubmit({ itemData, examId, course, courseId, gradeHint = "" }) { const ctx = getHomeworkApiContext(course, gradeHint); const preferPrefix = resolvePreferredLearningPrefix({ course, learningUserId: getUserInfo().learningUserId }) || getLearningPrefixFromCurrentPage() || ""; const answers = Array.isArray(itemData?.smallItemAnswer) ? itemData.smallItemAnswer : []; if (!answers.length || answers.some(a => !a || !a.myOptionKey)) { return false; } const listObject = answers.map(a => ({ itemType: itemData.smallItemType, optionContent: a.optionContent, optionContentKey: a.myOptionKey, optionSerial: a.optionContent, score: a.score })); const payload = { req: "automaticSubmit", listObject: pyRepr(listObject), phone_type: "mQ1mB1fHcgoWRJNXMOtUyw==", school_code: ctx.schoolCode || "", app_release: "nOqVkA13Jv74+ugChBaZFg==", course_code: ctx.courseCode, item_id: String(itemData.smallItemId || ""), exam_score_detail_id: String(itemData.examScoreDetailId || ""), current_version: "1OT8zweFkdJnoGkuQyZ2rg==", grade_code: ctx.gradeCode || "", type: "1", exam_id: String(examId || ""), student_type: "kU3gxRasa472peQC+cvl7A==" }; const opts = { reqInQuery: true, rawKeys: ["phone_type", "app_release", "current_version", "type", "student_type"] }; for (const url of getHomeworkExamTaskUrls(preferPrefix)) { for (const referer of homeworkExamTaskReferers(courseId, course, gradeHint, preferPrefix)) { const res = await createRequest(url, "POST", payload, true, { ...opts, referer: referer }); if (res?.code === 1000) { return true; } } } return false; } async function submitExam({ courseId, examId, examScoreId, courseName, learningUserId, course, gradeHint = "" }) { const ctx = getHomeworkApiContext(course, gradeHint); const preferPrefix = resolvePreferredLearningPrefix({ course, learningUserId }) || getLearningPrefixFromCurrentPage() || ""; const payload = { req: "submitExam", course_id: courseId, phone_type: "mQ1mB1fHcgoWRJNXMOtUyw==", school_code: ctx.schoolCode || "", app_release: "nOqVkA13Jv74+ugChBaZFg==", course_name: String(courseName || ""), current_version: "1OT8zweFkdJnoGkuQyZ2rg==", is_formal: "P9UdazN874Ud/dXSFB15bA==", type: "1", exam_score_id: String(examScoreId || ""), course_code: ctx.courseCode, user_id: learningUserId || "", grade_code: ctx.gradeCode || "", exam_id: String(examId || ""), student_type: "kU3gxRasa472peQC+cvl7A==" }; const opts = { reqInQuery: true, rawKeys: ["phone_type", "app_release", "current_version", "is_formal", "type", "student_type"] }; for (const url of getHomeworkExamTaskUrls(preferPrefix)) { for (const referer of homeworkExamTaskReferers(courseId, course, gradeHint, preferPrefix)) { const res = await createRequest(url, "POST", payload, true, { ...opts, referer: referer }); if (res?.code === 1000) { return true; } } } return false; } function parseHomeworkTopScore(raw) { const o = raw && typeof raw === "object" ? raw : {}; const keys = ["examTopScore", "exam_top_score", "topScore", "examScore", "userScore", "score", "rawScore", "totalScore", "getScore", "lastScore"]; for (const k of keys) { const v = o[k]; if (v == null || v === "") { continue; } const n = parseFloat(String(v).replace(/[^\d.-]/g, "")); if (!Number.isNaN(n)) { return n; } } return null; } function normalizeHomeworkItem(x) { const obj = (x && typeof x === "object") ? x : {}; const title = String(obj.title || obj.name || obj.workName || obj.examName || obj.homeworkName || "\u672a\u547d\u540d\u4f5c\u4e1a"); const isFinish = !!(obj.isFinish || obj.finish || obj.completed || obj.isCompleted); const url = obj.url || obj.workUrl || obj.examUrl || obj.homeworkUrl || obj.href || ""; const topScore = parseHomeworkTopScore(obj); return { raw: obj, title, isFinish, url, topScore }; } function shouldSkipHomeworkItem(it, skipIfScoreAtLeast) { if (!it) { return true; } if (it.isFinish) { return true; } const line = Number(skipIfScoreAtLeast); if (!line || line <= 0 || it.topScore == null || Number.isNaN(it.topScore)) { return false; } return it.topScore >= line; } function resolveHomeworkFlowId(raw) { const o = raw && typeof raw === "object" ? raw : {}; const pick = [o.examId, o.exam_id, o.content_id, o.contentId, o.scormContentId, o.scorm_content_id, o.id].find(v => v != null && String(v).trim() !== ""); if (pick != null) { return String(pick).trim(); } else { return ""; } } async function handleHomeworkForCourse({ courseId, learningUserId, userId, course, gradeHint = "", getStop }) { const result = { fetched: false, allDone: true, pending: 0, message: "" }; try { await primeLearningSessionIfNeeded(courseId, course); const listRaw = await getLearnCourseExerciseList({ courseId, learningUserId, userId, course, gradeHint }); if (!listRaw.length) { result.message = "\u672a\u83b7\u53d6\u5230\u4f5c\u4e1a\u5217\u8868"; log("\u4f5c\u4e1a\u5217\u8868\u672a\u83b7\u53d6\u5230(getLearnCourseExerciseList)\uff0c\u5df2\u8df3\u8fc7", "warn"); return result; } result.fetched = true; const list = listRaw.map(normalizeHomeworkItem); const passLine = Number(CONFIG.homeworkSkipIfTopScoreAtLeast || 0); const todo = list.filter(it => !shouldSkipHomeworkItem(it, passLine)); const skippedByScore = list.filter(it => { if (it.isFinish) { return false; } if (!passLine || passLine <= 0) { return false; } return it.topScore != null && !Number.isNaN(it.topScore) && it.topScore >= passLine; }); log("\u4f5c\u4e1a\u5217\u8868: \u5171" + list.length + "\uff0c\u5f85\u5904\u7406" + todo.length + (passLine > 0 ? "\uff08\u5217\u8868\u5f97\u5206≥" + passLine + " \u8df3\u8fc7\uff09" : "")); for (const s of skippedByScore) { log("\u8df3\u8fc7\u300c" + s.title + "\u300d\uff1a\u5217\u8868\u6210\u7ee9 " + s.topScore + " ≥ " + passLine, "info"); } if (!todo.length) { result.allDone = true; result.pending = 0; return result; } result.allDone = false; result.pending = todo.length; let unresolved = 0; for (let i = 0; i < todo.length; i++) { if (typeof getStop === "function" && getStop()) { log("\u5df2\u505c\u6b62\uff1a\u672c\u8bfe\u7a0b\u5269\u4f59\u4f5c\u4e1a\u672a\u5904\u7406", "warn"); unresolved += Math.max(0, todo.length - i); break; } const work = todo[i]; updatePillarProgress("homework", work.title + "\u5904\u7406\u4e2d\uff08" + (i + 1) + "/" + todo.length + "\uff09"); const hwFlowId = resolveHomeworkFlowId(work.raw); if (!hwFlowId) { log("\u300c" + work.title + "\u300d\u7f3a\u5c11 examId\uff0c\u8df3\u8fc7", "warn"); updatePillarProgress("homework", work.title + "\u8df3\u8fc7\uff08" + (i + 1) + "/" + todo.length + "\uff09"); unresolved += 1; continue; } log("\u4f5c\u4e1a " + (i + 1) + "/" + todo.length + ": " + work.title + " (exam_id=" + hwFlowId + ")"); try { await getItemTypeTotalCount({ examId: hwFlowId, learningUserId, course, courseId, gradeHint }); const { items, examScoreId: examScoreFromList } = await getHomeworkItemList({ examId: hwFlowId, learningUserId, course, courseId, gradeHint }); if (!items.length) { log("\u300c" + work.title + "\u300d\u9898\u76ee\u5217\u8868\u4e3a\u7a7a(getItemList)\uff0c\u8df3\u8fc7", "warn"); unresolved += 1; continue; } let examScoreId = examScoreFromList || work.raw.examScoreId || work.raw.exam_score_id || work.raw.scoreId || ""; if (examScoreId) { examScoreId = String(examScoreId).trim(); } if (!examScoreId || examScoreId.toLowerCase() === "null") { examScoreId = ""; } let answered = 0; let skipped = 0; for (const it of items) { if (typeof getStop === "function" && getStop()) { break; } const ok = await automaticSubmit({ itemData: it, examId: hwFlowId, course, courseId, gradeHint }); if (ok) { answered += 1; } else { skipped += 1; } await sleep(500); if (!examScoreId && it && typeof it === "object") { const mid = it.examScoreId || it.exam_score_id; if (mid != null && String(mid).trim() && String(mid).toLowerCase() !== "null") { examScoreId = String(mid).trim(); } } } log("\u300c" + work.title + "\u300d\u4f5c\u7b54: \u6210\u529f" + answered + "\uff0c\u8df3\u8fc7" + skipped); if (examScoreId) { const submitOk = await submitExam({ courseId, examId: hwFlowId, examScoreId, courseName: course?.courseName || "", learningUserId, course, gradeHint }); log(submitOk ? "\u300c" + work.title + "\u300d\u4ea4\u5377\u6210\u529f" : "\u300c" + work.title + "\u300d\u4ea4\u5377\u5931\u8d25", submitOk ? "info" : "warn"); if (!submitOk) unresolved += 1; } else { log("\u300c" + work.title + "\u300d\u4ecd\u7f3a\u5c11 exam_score_id\uff0c\u672a\u4ea4\u5377", "warn"); unresolved += 1; } updatePillarProgress("homework", work.title + "\u5df2\u5b8c\u6210\uff08" + (i + 1) + "/" + todo.length + "\uff09"); } catch (inner) { log("\u300c" + work.title + "\u300d\u5904\u7406\u5f02\u5e38: " + (inner?.message || inner), "warn"); updatePillarProgress("homework", work.title + "\u5f02\u5e38\uff08" + (i + 1) + "/" + todo.length + "\uff09"); unresolved += 1; } if (i < todo.length - 1) { await sleep(Math.max(500, Number(CONFIG.homeworkDelay || 2000))); } } result.pending = unresolved; result.allDone = unresolved === 0; } catch (e) { log("\u4f5c\u4e1a\u5904\u7406\u5f02\u5e38\uff08\u5df2\u8df3\u8fc7\u672c\u8bfe\u7a0b\u4f5c\u4e1a\uff09: " + (e?.message || e), "warn"); result.message = String(e?.message || e || "\u4f5c\u4e1a\u5904\u7406\u5f02\u5e38"); result.allDone = false; } return result; } async function requestUserScoreRecalc(courseId) { const cid = String(courseId || "").trim(); if (!cid) return false; for (const b of LEARNING_BASES) { const r = Math.random(); const url = `${b.origin}/${b.prefix}/course/learning/user_score.jsp?calc=1&course_id=${encodeURIComponent(cid)}&score_type=score&r=${encodeURIComponent(String(r))}`; const notice = `${b.origin}/${b.prefix}/course/learning/learn_notice.jsp?course_id=${encodeURIComponent(cid)}`; const referer = `${b.origin}/${b.prefix}/console/?urlto=${encodeURIComponent(notice)}&${Math.random()}`; let reqHost = ""; try { reqHost = new URL(url).hostname; } catch (_) {} if (reqHost && reqHost === window.location.hostname) { try { const res = await fetch(url, { method: "GET", credentials: "include", headers: { Accept: "text/html,application/xhtml+xml,*/*;q=0.8", Referer: referer } }); const ok = res.status >= 200 && res.status < 400; log(`\u5b9e\u65f6\u8ba1\u7b97\u6210\u7ee9(GET user_score.jsp): ${b.prefix} HTTP ${res.status} course_id=${cid}${ok ? " ✓" : ""}`, ok ? "info" : "warn"); if (ok) return true; } catch (e) { log(`\u5b9e\u65f6\u8ba1\u7b97\u6210\u7ee9: fetch \u5f02\u5e38 prefix=${b.prefix} ${e?.message || e}`, "warn"); } } else { const ok = await new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url, headers: { Referer: referer, Accept: "text/html,*/*" }, withCredentials: true, timeout: 30000, onload: (response) => { const st = response.status || 0; const ok = st >= 200 && st < 400; log(`\u5b9e\u65f6\u8ba1\u7b97\u6210\u7ee9(GET user_score.jsp): ${b.prefix} HTTP ${st} course_id=${cid}${ok ? " ✓" : ""}`, ok ? "info" : "warn"); resolve(ok); }, onerror: () => { log(`\u5b9e\u65f6\u8ba1\u7b97\u6210\u7ee9(GET user_score.jsp): ${b.prefix} \u8bf7\u6c42\u5931\u8d25 course_id=${cid}`, "warn"); resolve(false); }, ontimeout: () => { log(`\u5b9e\u65f6\u8ba1\u7b97\u6210\u7ee9(GET user_score.jsp): ${b.prefix} \u8d85\u65f6 course_id=${cid}`, "warn"); resolve(false); } }); }); if (ok) return true; } } return false; } async function checkCourseAllPillarsComplete({ courseId, course, termCode, learningUserId, userId, discussionHint = null, completionHint = null }) { const gradeHint = (() => { const { gradeCode } = parseSchoolAndGradeFromCourse(course); return gradeCode || String(termCode || ""); })(); const { schoolCode, gradeCode } = parseSchoolAndGradeFromCourse(course); let items = await getCourseItems(courseId, true, course, termCode || ""); if (!items.length) { return { ok: false, reason: "\u89c6\u9891\u5217\u8868\u672a\u83b7\u53d6\uff0c\u65e0\u6cd5\u786e\u8ba4\u662f\u5426\u5b66\u5b8c" }; } let pendingLessons = items.filter(it => !it.isFinish && it.isChapter !== true); if (pendingLessons.length > 0) { await sleep(2000); const itemsRetry = await getCourseItems(courseId, true, course, termCode || ""); if (itemsRetry && itemsRetry.length) { pendingLessons = itemsRetry.filter(it => !it.isFinish && it.isChapter !== true); } } if (pendingLessons.length) { return { ok: false, reason: "\u89c6\u9891\u672a\u5b8c\u6210 " + pendingLessons.length + " \u8282" }; } const bbs = await getBbsScore({ courseId, schoolCode, gradeCode: gradeCode || String(termCode || ""), learningUserId }); if (!bbs) { return { ok: false, reason: "\u8ba8\u8bba\u5206\u6570\u672a\u83b7\u53d6\uff0c\u65e0\u6cd5\u786e\u8ba4\u8ba8\u8bba\u662f\u5426\u5b8c\u6210" }; } const bbsScore = Number(bbs.bbsScore || 0); const regularScore = Number(bbs.regularScore || 0); const maxScore = regularScore > 0 ? regularScore : 0; const fixedN = Number(CONFIG.fixedCommentTimes || 6); const sessionOk = discussionHint && (discussionHint.alreadyFullAtStart === true || Number(discussionHint.sessionSuccessCount || 0) >= fixedN); if (maxScore <= 0) {} else if (bbsScore < maxScore && !sessionOk) { return { ok: false, reason: "\u8ba8\u8bba\u5206 " + bbsScore + "/" + maxScore }; } if (bbsScore < maxScore && sessionOk) { log("\u8ba8\u8bba\u533a\u5206\u6570\u63a5\u53e3\u53ef\u80fd\u6ede\u540e\uff08" + bbsScore + "/" + maxScore + "\uff09\uff0c\u672c\u8bfe\u5df2\u6309\u56fa\u5b9a " + fixedN + " \u6761/\u5f00\u5c40\u6ee1\u5206\u5904\u7406\uff0c\u6682\u89c6\u4e3a\u8ba8\u8bba\u5b8c\u6210", "info"); } if (!completionHint || completionHint.documentDone !== true) { const docs = await getLearnContentDocumentList({ courseId, learningUserId, userId, course }); const docTasks = buildDocumentTasks(docs); if (docTasks.length) { return { ok: false, reason: "\u8d44\u6599\u672a\u5b8c\u6210 " + docTasks.length + " \u6761" }; } } if (!completionHint || completionHint.homeworkDone !== true) { await primeLearningSessionIfNeeded(courseId, course); const listRaw = await getLearnCourseExerciseList({ courseId, learningUserId, userId, course, gradeHint }); if (listRaw.length) { const list = listRaw.map(normalizeHomeworkItem); const passLine = Number(CONFIG.homeworkSkipIfTopScoreAtLeast || 0); const todo = list.filter(it => !shouldSkipHomeworkItem(it, passLine)); if (todo.length) { return { ok: false, reason: "\u4f5c\u4e1a\u672a\u5b8c\u6210 " + todo.length + " \u4efd" }; } } } return { ok: true, reason: "" }; } async function maybeRecalcUserScoreIfCourseFullyComplete({ courseId, course, termCode, getStop, discussionHint = null, completionHint = null }) { if (typeof getStop === "function" && getStop()) { return; } if (CONFIG.refreshUserScoreAfterCourseComplete === false) { return; } const { learningUserId, userId } = getUserInfo(); if (!learningUserId) { log("\u5b9e\u65f6\u8ba1\u7b97\u6210\u7ee9: \u65e0 learningUserId\uff0c\u8df3\u8fc7", "warn"); return; } let check; try { check = await checkCourseAllPillarsComplete({ courseId, course, termCode: termCode || "", learningUserId, userId, discussionHint, completionHint }); } catch (e) { log("\u5b9e\u65f6\u8ba1\u7b97\u6210\u7ee9: \u5b8c\u6210\u5ea6\u68c0\u6d4b\u5f02\u5e38 " + (e?.message || e), "warn"); return; } if (!check.ok) { log("\u8bfe\u7a0b\u672a\u56db\u9879\u5168\u6ee1\uff0c\u8df3\u8fc7\u5b9e\u65f6\u8ba1\u7b97: " + check.reason); return; } const delay = Math.max(0, Number(CONFIG.refreshUserScoreDelayMs ?? 10000)); log("\u56db\u9879\u5747\u5df2\u5b8c\u6210\u540e\u7b49\u5f85 " + delay / 1000 + "s \u4ee5\u89e6\u53d1\u670d\u52a1\u7aef\u5b9e\u65f6\u8ba1\u7b97\u6210\u7ee9…"); updatePanelCurrentPhase("sync"); await sleep(delay); if (typeof getStop === "function" && getStop()) { return; } await requestUserScoreRecalc(courseId); } const EXAM_ACCESS_TYPE_ENC = "rtfInTn4dY1LD4b82pvPh7ot/3RJvoRQH79uAXIJLHg="; const EXAM_STATUS_ENC = "P9UdazN874Ud/dXSFB15bA=="; function isExamPageHere() { try { return /\/separation\/exam\/index\.html/i.test(window.location.pathname || ""); } catch (_) { return false; } } function getExamPageReferer() { return window.location.href; } function getExamPageInfoToken() { try { const info = new URL(window.location.href).searchParams.get("info"); return info ? String(info).trim() : ""; } catch (_) { return ""; } } function encryptExamField(plain) { const enc = encrypt({ v: String(plain ?? "") }); return enc.v || String(plain ?? ""); } function pickExamIdFromData(data) { const walk = (d) => { if (!d || typeof d !== "object") return ""; const keys = ["examId", "exam_id", "content_id", "contentId", "id"]; for (const k of keys) { const v = d[k]; if (v != null && String(v).trim() !== "" && String(v).toLowerCase() !== "null") { return String(v).trim(); } } if (Array.isArray(d)) { for (const it of d) { const s = walk(it); if (s) return s; } } else { for (const v of Object.values(d)) { if (v && typeof v === "object") { const s = walk(v); if (s) return s; } } } return ""; }; return walk(data); } function unwrapExamPaperItems(data) { const d = data && typeof data === "object" ? data : {}; if (Array.isArray(d.itemInfoList) && d.itemInfoList.length) return d.itemInfoList; if (Array.isArray(d.items) && d.items.length) return d.items; if (Array.isArray(d.mallInfoList) && d.mallInfoList.length) return d.mallInfoList; if (Array.isArray(d)) return d; return []; } function stripHtmlText(html) { const s = String(html || ""); if (!s) return ""; if (!/<[^>]+>/.test(s)) return s.trim(); const el = document.createElement("div"); el.innerHTML = s; return (el.textContent || el.innerText || "").trim(); } function isExamMultiBlankItemType(itemType) { const t = String(itemType ?? "").trim(); return t === "1" || t === "12" || Number(t) === 1 || Number(t) === 12; } function buildExamSaveDetailFromAnswer(ans) { if (!ans || typeof ans !== "object") return null; const optionContent = stripHtmlText(ans.optionContent || ""); const optionKey = String(ans.myOptionKey || "").trim(); if (!optionContent || !optionKey) return null; return { optionContentKey: optionKey, optionContent: optionContent, myOption: optionContent, imgUrl: "", score: String(ans.score || "0") }; } function buildExamSaveItemPayload(item) { const itemType = String(item?.itemType || "3"); const answers = Array.isArray(item?.itemAnswer) ? item.itemAnswer : []; let detailList = []; if (isExamMultiBlankItemType(itemType)) { for (const ans of answers) { const detail = buildExamSaveDetailFromAnswer(ans); if (detail) detailList.push(detail); } } else { const detail = buildExamSaveDetailFromAnswer(answers[0]); if (detail) detailList = [detail]; } if (!detailList.length) return null; return [{ examScoreDetailId: String(item.examScoreDetailId || ""), itemId: String(item.itemId || ""), itemType: itemType, saveItemDetailVoList: detailList }]; } function parseExamAnswerLetters(text) { const raw = String(text || "").trim().toUpperCase(); if (!raw) return []; if (/[,\uff0c\u3001|\s/;\uff1b]/.test(raw)) { return raw.split(/[,\uff0c\u3001|\s/;\uff1b]+/).map((s) => s.trim()).filter((s) => /^[A-Z]$/.test(s)); } return raw.split("").filter((c) => c >= "A" && c <= "Z"); } function ensureWencaiExamUiStyles() { if (wencaiExamUiStyleInjected) return; wencaiExamUiStyleInjected = true; const style = document.createElement("style"); style.id = "wencai-exam-ui-sync-style"; style.textContent = [ "#answerCard span.pccicle.wencai-card-saved{background-color:#4dabf7!important;color:#fff!important;border-color:#4dabf7!important;}", "#paperExam .perRad.wencai-opt-picked{position:relative;}", "#paperExam .perRad.wencai-opt-picked>label{position:relative;display:block;padding-left:28px!important;color:#096dd9!important;}", "#paperExam .perRad.wencai-opt-picked>input{position:absolute;opacity:0;pointer-events:none;width:18px;height:18px;margin:0;}", "#paperExam .perRad.wencai-opt-picked>label::before{content:\"\";position:absolute;left:0;top:0.15em;width:16px;height:16px;border:2px solid #1890ff;border-radius:50%;background:#fff;box-sizing:border-box;}", "#paperExam .perRad.wencai-opt-picked>label::after{content:\"\";position:absolute;left:5px;top:calc(0.15em + 5px);width:6px;height:6px;border-radius:50%;background:#1890ff;}", "#paperExam .perRad.wencai-opt-picked:has(>input[type=\"checkbox\"])>label::before{border-radius:3px;background:#1890ff;}", "#paperExam .perRad.wencai-opt-picked:has(>input[type=\"checkbox\"])>label::after{left:4px;top:calc(0.15em + 2px);width:5px;height:9px;border-radius:0;background:transparent;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg);}", "#paperExam .tiankong input.wencai-fill-picked,#paperExam input.wencai-fill-picked{border-color:#1890ff!important;background:#e6f7ff!important;color:#096dd9!important;}" ].join("\n"); document.head.appendChild(style); } function cleanupLegacyWencaiExamUiMarks() { const strip = ["answered", "done", "is-answer", "isAnswer", "yizuo", "active", "on", "checked", "is-checked"]; document.querySelectorAll("#onlineExamArea .bword, #onlineExamArea span.pccicle, #paperExam .perRad").forEach((el) => { strip.forEach((c) => el.classList.remove(c)); if (el.classList.contains("wencai-opt-picked")) el.classList.remove("wencai-opt-picked"); if (el.classList.contains("wencai-card-saved")) el.classList.remove("wencai-card-saved"); delete el.dataset.wencaiSaved; delete el.dataset.wencaiPick; }); document.querySelectorAll("#paperExam .perRad input").forEach((input) => { input.checked = false; }); examSyncedCardNos.clear(); } function findWencaiQuestionBlock(questionNo) { const n = Number(questionNo || 0); if (!n) return null; const title = document.getElementById("q" + n); if (title) return title.closest(".tmc.tm") || title.closest(".tmList"); return document.querySelector("#paperExam .tmc.tm[tindex=\"" + (n - 1) + "\"]"); } function findWencaiAnswerCardCircle(questionNo) { const n = Number(questionNo || 0); if (!n) return null; const card = document.querySelector("#answerCard"); if (!card) return null; for (const el of card.querySelectorAll("span.pccicle")) { if (String(el.textContent || "").trim() === String(n)) return el; } return null; } function markWencaiExamCardSaved(questionNo) { const n = Number(questionNo || 0); if (!n || examSyncedCardNos.has(n)) return false; ensureWencaiExamUiStyles(); const el = findWencaiAnswerCardCircle(n); if (!el) return false; el.classList.add("wencai-card-saved"); el.dataset.wencaiSaved = "1"; examSyncedCardNos.add(n); return true; } function clearWencaiQuestionPickedOptions(qBlock, keepLetters) { if (!qBlock) return; const keep = new Set(Array.isArray(keepLetters) ? keepLetters : []); qBlock.querySelectorAll(".perRad.wencai-opt-picked").forEach((el) => { const letter = String(el.dataset.wencaiPick || "").trim(); if (keep.has(letter)) return; el.classList.remove("wencai-opt-picked"); delete el.dataset.wencaiPick; }); } function paintWencaiExamQuestionOptions(questionNo, letters) { ensureWencaiExamUiStyles(); const n = Number(questionNo || 0); const letterList = parseExamAnswerLetters(Array.isArray(letters) ? letters.join("") : letters); if (!n || !letterList.length) return false; const qBlock = findWencaiQuestionBlock(n); if (!qBlock) return false; const isMulti = String(qBlock.getAttribute("ttype") || "") === "4"; if (!isMulti) { qBlock.querySelectorAll("input[name=\"q" + n + "\"]").forEach((inp) => { inp.checked = false; }); clearWencaiQuestionPickedOptions(qBlock, letterList.slice(0, 1)); } let painted = false; for (const letter of letterList) { const input = document.getElementById("q" + n + letter) || qBlock.querySelector("input[name=\"q" + n + "\"][value=\"" + letter + "\"]"); if (!input) continue; const perRad = input.closest(".perRad"); if (!perRad) continue; input.checked = true; perRad.classList.add("wencai-opt-picked"); perRad.dataset.wencaiPick = letter; painted = true; } return painted; } function collectWencaiFillInputs(qBlock) { if (!qBlock) return []; const selectors = [".tiankong input", ".tmGap input", ".gap input", "input.gap-input"]; const seen = new Set(); const out = []; for (const sel of selectors) { qBlock.querySelectorAll(sel).forEach((inp) => { if (!inp || seen.has(inp)) return; seen.add(inp); out.push(inp); }); } if (!out.length) { qBlock.querySelectorAll("input[type=\"text\"], textarea").forEach((inp) => { if (!inp || seen.has(inp)) return; if (inp.closest(".perRad")) return; seen.add(inp); out.push(inp); }); } return out; } function emitWencaiFillInputChange(input, value) { if (!input) return; const val = String(value ?? ""); input.value = val; try { input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); } catch (_) {} let host = input; for (let i = 0; i < 4 && host; i++) { const vue = host.__vue__; if (vue) { if (typeof vue.$emit === "function") { try { vue.$emit("input", val); } catch (_) {} try { vue.$emit("change", val); } catch (_) {} } if (vue.$parent && typeof vue.$parent.$emit === "function") { try { vue.$parent.$emit("change", val); } catch (_) {} } break; } host = host.parentElement; } } function paintWencaiExamFillBlanks(questionNo, fillTexts, item) { ensureWencaiExamUiStyles(); const n = Number(questionNo || 0); const texts = Array.isArray(fillTexts) ? fillTexts.map((t) => String(t ?? "")) : [String(fillTexts ?? "")]; if (!n || !texts.length) return false; const qBlock = findWencaiQuestionBlock(n); if (!qBlock) return false; const inputs = collectWencaiFillInputs(qBlock); if (!inputs.length) return false; let painted = false; inputs.forEach((inp, idx) => { const val = texts[idx] != null ? String(texts[idx]) : ""; if (!val) return; emitWencaiFillInputChange(inp, val); inp.classList.add("wencai-fill-picked"); painted = true; }); if (painted && CONFIG.verboseConsole) { log("\u586b\u7a7a UI \u540c\u6b65: \u7b2c " + n + " \u9898 " + texts.filter(Boolean).length + " \u4e2a\u7a7a", "info"); } return painted; } async function syncExamPageUiAfterSave(item, savePayload, questionNo) { if (CONFIG.examSyncPageUi === false) return; try { const detailList = savePayload?.[0]?.saveItemDetailVoList || []; markWencaiExamCardSaved(questionNo); const itemType = String(item?.itemType || savePayload?.[0]?.itemType || ""); if (isExamMultiBlankItemType(itemType)) { const fillTexts = detailList.map((d) => d.myOption || d.optionContent || ""); paintWencaiExamFillBlanks(questionNo, fillTexts, item); return; } const detail = detailList[0] || {}; const letters = parseExamAnswerLetters(detail.optionContent || detail.myOption); if (letters.length) paintWencaiExamQuestionOptions(questionNo, letters); } catch (e) { log("\u8003\u8bd5 UI \u540c\u6b65\u5931\u8d25(\u7b2c " + questionNo + " \u9898): " + (e?.message || e), "warn"); } } async function createExamModularRequest(url, payload, { referer, reqInQuery = false, preserveDataCipher = false, rawKeys = [] } = {}) { const rk = new Set(["user_info", "accesst_type", "exam_status", "exam_id", "save_item", "exam_score_id", ...rawKeys]); return createRequest(url, "POST", payload, true, { reqInQuery, rawKeys: rk, referer: referer || getExamPageReferer(), preserveDataCipher, headers: { Accept: "application/json, text/plain, */*" } }); } function getExamModularBases() { const pref = getLearningPrefixFromCurrentPage() || "openlearning"; return getLearningBasesByPrefixFirst(pref); } async function examModularLogin(initialUserInfo, baseUrl, referer) { const url = `${baseUrl}/examModular_login_info.action`; const res = await createExamModularRequest(url, { user_info: initialUserInfo, accesst_type: EXAM_ACCESS_TYPE_ENC }, { referer, preserveDataCipher: true }); if (res?.code !== 1000 || !res.data) return ""; return typeof res.data === "string" ? res.data : String(res.data); } async function examModularGetInfo(sessionUserInfo, baseUrl, referer) { const url = `${baseUrl}/examModular_exam_info.action`; return createExamModularRequest(url, { user_info: sessionUserInfo }, { referer }); } async function examModularJudgment(sessionUserInfo, examIdPlain, baseUrl, referer) { const url = `${baseUrl}/examModular_exam_judgment.action`; return createExamModularRequest(url, { user_info: sessionUserInfo, exam_id: encryptExamField(examIdPlain) }, { referer }); } async function examModularItemPrecheck(sessionUserInfo, examIdPlain, baseUrl, referer) { const url = `${baseUrl}/examModular_exam_item.action`; return createExamModularRequest(url, { user_info: sessionUserInfo, exam_id: encryptExamField(examIdPlain), exam_status: EXAM_STATUS_ENC }, { referer }); } async function examModularGetItemList(sessionUserInfo, examIdPlain, baseUrl, referer) { const url = `${baseUrl}/examModular_exam_item.action?req=getItemList`; return createExamModularRequest(url, { user_info: sessionUserInfo, exam_id: encryptExamField(examIdPlain) }, { referer, reqInQuery: true }); } async function examModularSaveItem(sessionUserInfo, examIdPlain, savePayload, baseUrl, referer) { const url = `${baseUrl}/examModular_exam_save.action`; return createExamModularRequest(url, { user_info: sessionUserInfo, exam_id: encryptExamField(examIdPlain), save_item: encryptExamField(JSON.stringify(savePayload)) }, { referer }); } async function examModularSubmitPaper(sessionUserInfo, examIdPlain, examScoreId, baseUrl, referer) { const url = `${baseUrl}/examModular_exam_submit.action`; return createExamModularRequest(url, { user_info: sessionUserInfo, exam_id: encryptExamField(examIdPlain), exam_score_id: encryptExamField(examScoreId) }, { referer }); } function isExamSaveRateLimited(res) { const code = Number(res?.code || 0); const msg = String(res?.message || ""); return code === 2001 || /\u8fc7\u4e8e\u9891\u7e41|\u7a0d\u540e\u518d\u8bd5/.test(msg); } async function examModularSaveItemWithRetry(sessionUserInfo, examIdPlain, savePayload, baseUrl, referer, questionNo) { const maxRetries = Math.max(1, Number(CONFIG.examSaveMaxRetries || 6)); const normalDelay = Math.max(1000, Number(CONFIG.examDelay || 6000)); const rateLimitDelay = Math.max(normalDelay, Number(CONFIG.examDelayOnRateLimit || 15000)); let lastRes = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { if (!isExamRunning) { return { ok: false, stopped: true, res: lastRes }; } lastRes = await examModularSaveItem(sessionUserInfo, examIdPlain, savePayload, baseUrl, referer); if (lastRes?.code === 1000) { return { ok: true, res: lastRes }; } if (isExamSaveRateLimited(lastRes) && attempt < maxRetries) { const waitMs = rateLimitDelay; log("\u7b2c " + questionNo + " \u9898\u4fdd\u5b58\u88ab\u9650\u6d41(code=" + lastRes.code + ")\uff0c" + waitMs / 1000 + "s \u540e\u7b2c " + (attempt + 1) + " \u6b21\u91cd\u8bd5", "warn"); updatePanelCurrentPhase("exam", "\u7b2c " + questionNo + " \u9898\u9650\u6d41\uff0c\u7b49\u5f85 " + waitMs / 1000 + "s"); await sleep(waitMs); continue; } break; } return { ok: false, res: lastRes, rateLimited: isExamSaveRateLimited(lastRes) }; } async function runExamAutoFlow() { const infoToken = getExamPageInfoToken(); if (!infoToken) { throw new Error("\u8bf7\u5728\u8003\u8bd5\u7b54\u9898\u9875\u4f7f\u7528\uff08URL \u9700\u542b info \u53c2\u6570\uff09"); } const referer = getExamPageReferer(); const bases = getExamModularBases(); let lastErr = "\u672a\u627e\u5230\u53ef\u7528\u8003\u8bd5\u7aef\u70b9"; for (const b of bases) { const baseUrl = `${b.origin}/${b.prefix}`; try { updatePanelSummary("\u72b6\u6001\uff1a\u8003\u8bd5\u767b\u5f55…"); const sessionUserInfo = await examModularLogin(infoToken, baseUrl, referer); if (!sessionUserInfo) { lastErr = "\u8003\u8bd5\u767b\u5f55\u5931\u8d25(" + b.prefix + ")"; continue; } updatePanelSummary("\u72b6\u6001\uff1a\u62c9\u53d6\u8bd5\u5377\u4fe1\u606f…"); const infoRes = await examModularGetInfo(sessionUserInfo, baseUrl, referer); if (infoRes?.code !== 1000) { lastErr = "exam_info: " + (infoRes?.message || "\u5931\u8d25") + " (" + b.prefix + ")"; continue; } let examIdPlain = pickExamIdFromData(infoRes.data); if (!examIdPlain && infoRes.__rawDataText) { try { examIdPlain = pickExamIdFromData(JSON.parse(infoRes.__rawDataText)); } catch (_) {} } if (!examIdPlain) { lastErr = "\u672a\u89e3\u6790\u5230 exam_id (" + b.prefix + ")"; continue; } await examModularJudgment(sessionUserInfo, examIdPlain, baseUrl, referer); await examModularItemPrecheck(sessionUserInfo, examIdPlain, baseUrl, referer); updatePanelSummary("\u72b6\u6001\uff1a\u62c9\u53d6\u9898\u76ee…"); const listRes = await examModularGetItemList(sessionUserInfo, examIdPlain, baseUrl, referer); if (listRes?.code !== 1000 || !listRes.data) { lastErr = "getItemList: " + (listRes?.message || "\u5931\u8d25") + " (" + b.prefix + ")"; continue; } const paper = listRes.data && typeof listRes.data === "object" ? listRes.data : {}; const items = unwrapExamPaperItems(paper); if (!items.length) { lastErr = "\u8bd5\u5377\u9898\u76ee\u4e3a\u7a7a (" + b.prefix + ")"; continue; } let examScoreId = String(paper.examScoreId || pickExamScoreIdFromItemListData(paper) || "").trim(); if (!examScoreId || examScoreId.toLowerCase() === "null") examScoreId = ""; log("\u8003\u8bd5: " + items.length + " \u9898, exam_id=" + examIdPlain + ", prefix=" + b.prefix); updatePanelCurrentPhase("exam", "\u5171 " + items.length + " \u9898"); const autoLimit = Math.max(0, Number(CONFIG.examAutoLimit || 0)); const totalToRun = autoLimit > 0 ? Math.min(autoLimit, items.length) : items.length; if (autoLimit > 0) { log("examAutoLimit=" + autoLimit + "\uff0c\u4ec5\u4f5c\u7b54\u524d " + totalToRun + " \u9898", "info"); } let ok = 0; let skip = 0; for (let i = 0; i < totalToRun; i++) { if (!isExamRunning) break; const item = items[i]; const savePayload = buildExamSaveItemPayload(item); updatePanelCurrentPhase("exam", "\u4f5c\u7b54 " + (i + 1) + "/" + totalToRun); if (!savePayload) { skip += 1; log("\u8df3\u8fc7\u7b2c " + (i + 1) + " \u9898\uff1a\u65e0\u6807\u51c6\u7b54\u6848\u5b57\u6bb5", "warn"); continue; } const blankCount = savePayload[0]?.saveItemDetailVoList?.length || 0; if (isExamMultiBlankItemType(item?.itemType) && blankCount > 1) { log("\u7b2c " + (i + 1) + " \u9898\u586b\u7a7a\u5171 " + blankCount + " \u4e2a\u7a7a", "info"); } const saveResult = await examModularSaveItemWithRetry(sessionUserInfo, examIdPlain, savePayload, baseUrl, referer, i + 1); if (saveResult.ok) { ok += 1; if (CONFIG.examSyncPageUi !== false) { await syncExamPageUiAfterSave(item, savePayload, i + 1); } updatePanelCurrentPhase("exam", "\u5df2\u4fdd\u5b58 " + ok + "/" + totalToRun); updatePanelSummary("\u72b6\u6001\uff1a\u9010\u9898\u4fdd\u5b58\u4e2d " + ok + "/" + totalToRun + "\uff08\u672a\u5168\u90e8\u4fdd\u5b58\u5b8c\u8bf7\u52ff\u4ea4\u5377\uff09"); } else if (saveResult.stopped) { break; } else { skip += 1; const errMsg = saveResult.res?.message || "\u672a\u77e5"; log("\u7b2c " + (i + 1) + " \u9898\u4fdd\u5b58\u5931\u8d25: code=" + (saveResult.res?.code || "?") + ", " + errMsg, "warn"); } if (!examScoreId && item.examScoreDetailId) { examScoreId = String(paper.examScoreId || "").trim(); } if (i < totalToRun - 1 && isExamRunning) { const gap = Math.max(1000, Number(CONFIG.examDelay || 6000)); await sleep(gap); } } if (!examScoreId) { examScoreId = String(paper.examScoreId || pickExamScoreIdFromItemListData(paper) || "").trim(); } if (CONFIG.examAutoSubmit === false) { updatePanelCurrentPhase("exam", "\u4f5c\u7b54\u5b8c\u6210\uff08\u672a\u4ea4\u5377\uff09"); updatePanelSummary("\u72b6\u6001\uff1a\u4f5c\u7b54\u5b8c\u6210\uff08\u672a\u4ea4\u5377\uff0c\u6210\u529f " + ok + "\uff0c\u8df3\u8fc7 " + skip + "\uff09"); showMessage("\u4f5c\u7b54\u5b8c\u6210\uff08\u672a\u81ea\u52a8\u4ea4\u5377\uff09", "success"); log("examAutoSubmit=false\uff0c\u5df2\u8df3\u8fc7\u4ea4\u5377\u3002exam_score_id=" + (examScoreId || "\u65e0"), "info"); return; } if (!examScoreId) { throw new Error("\u7f3a\u5c11 exam_score_id\uff0c\u65e0\u6cd5\u4ea4\u5377\uff08\u5df2\u4fdd\u5b58 " + ok + " \u9898\uff09"); } updatePanelSummary("\u72b6\u6001\uff1a\u4ea4\u5377\u4e2d…"); const submitRes = await examModularSubmitPaper(sessionUserInfo, examIdPlain, examScoreId, baseUrl, referer); if (submitRes?.code !== 1000) { throw new Error("\u4ea4\u5377\u5931\u8d25: " + (submitRes?.message || "\u672a\u77e5") + "\uff08\u5df2\u4fdd\u5b58 " + ok + " \u9898\uff09"); } updatePanelCurrentPhase("exam", "\u4ea4\u5377\u6210\u529f"); updatePanelSummary("\u72b6\u6001\uff1a\u8003\u8bd5\u5b8c\u6210\uff08\u6210\u529f " + ok + "\uff0c\u8df3\u8fc7 " + skip + "\uff09"); showMessage("\u8003\u8bd5\u4ea4\u5377\u6210\u529f", "success"); return; } catch (e) { lastErr = e?.message || String(e); log("\u8003\u8bd5\u6d41\u7a0b\u5f02\u5e38(" + b.prefix + "): " + lastErr, "warn"); } } throw new Error(lastErr || "\u8003\u8bd5\u6d41\u7a0b\u5931\u8d25"); } async function ensureProLicenseForAuto(actionLabel) { const base = (CONFIG.licenseApiBase || "").trim(); if (!CONFIG.requireProForAutoLearn) return true; if (!base) { showMessage("\u9700\u8981\u5148\u914d\u7f6e licenseApiBase \u624d\u80fd" + actionLabel, "warning"); updateLicenseStatusUI(); return false; } showMessage("\u6b63\u5728\u6821\u9a8c\u6388\u6743…", "info"); const ok = await verifyProLicense(false); if (!ok) { showMessage(actionLabel + "\u9700\u4ed8\u8d39\u5f00\u901a\uff1a\u8bf7\u4fdd\u5b58 Token \u6216\u524d\u5f80\u8d2d\u4e70\u9875", "warning"); if (CONFIG.licensePurchaseUrl) { window.open(CONFIG.licensePurchaseUrl, "_blank", "noopener"); } updateLicenseStatusUI(); return false; } if (CONFIG.requireProLeaseForAutoLearn) { showMessage("\u6b63\u5728\u83b7\u53d6\u4f1a\u8bdd\u8bb8\u53ef…", "info"); const leaseOk = await fetchProLeaseToken(); if (!leaseOk) { showMessage("\u65e0\u6cd5\u83b7\u53d6\u4f1a\u8bdd\u8bb8\u53ef\uff08\u8bf7\u66f4\u65b0\u670d\u52a1\u7aef\u5e76\u5f00\u542f lease \u63a5\u53e3\uff09", "warning"); updateLicenseStatusUI(); return false; } } return true; } function stopExamAuto() { isExamRunning = false; updatePanelCurrentPhase(null); updatePanelSummary("\u72b6\u6001\uff1a\u8003\u8bd5\u5df2\u505c\u6b62"); showMessage("\u8003\u8bd5\u5df2\u505c\u6b62", "info"); } async function startExamAuto() { if (isExamRunning) return showMessage("\u8003\u8bd5\u8fdb\u884c\u4e2d\uff0c\u8bf7\u52ff\u91cd\u590d\u70b9\u51fb", "warning"); if (isRunning) return showMessage("\u4e00\u952e\u5b66\u4e60\u8fdb\u884c\u4e2d\uff0c\u8bf7\u5148\u505c\u6b62", "warning"); if (!isExamPageHere()) { return showMessage("\u8bf7\u5148\u8fdb\u5165\u8003\u8bd5\u7b54\u9898\u9875\u9762\u518d\u70b9\u300c\u5f00\u59cb\u8003\u8bd5\u300d", "warning"); } const licensed = await ensureProLicenseForAuto("\u5f00\u59cb\u8003\u8bd5"); if (!licensed) return; cleanupLegacyWencaiExamUiMarks(); ensureWencaiExamUiStyles(); isExamRunning = true; updatePanelSummary("\u72b6\u6001\uff1a\u8003\u8bd5\u51c6\u5907…"); updatePanelCurrentPhase("exam", "\u521d\u59cb\u5316"); showMessage("\u5f00\u59cb\u8003\u8bd5", "success"); try { await runExamAutoFlow(); } catch (e) { log("\u8003\u8bd5\u5931\u8d25: " + (e?.message || e), "error"); updatePanelSummary("\u72b6\u6001\uff1a\u8003\u8bd5\u5931\u8d25", true); showMessage(String(e?.message || e || "\u8003\u8bd5\u5931\u8d25"), "error"); } finally { isExamRunning = false; } } class AutoPlayCore { constructor(callback) { this.callback = callback; this.isRunning = false; this.stopFlag = false; this.courses = []; } async start() { this.isRunning = true; this.stopFlag = false; try { updatePanelSummary("\u72b6\u6001\uff1a\u52a0\u8f7d\u5b66\u671f…"); const terms = await getSemester(); if (!terms || terms.length === 0) throw new Error("\u6ca1\u6709\u627e\u5230\u5b66\u671f\u4fe1\u606f"); const currentTerm = getCurrentTermFromList(terms); const selectedTermCode = getSelectedTermCode(); const pickedTerm = selectedTermCode ? (terms.find(t => getTermCode(t) === selectedTermCode) || currentTerm) : currentTerm; const activeTermCode = getTermCode(pickedTerm); updatePanelSummary("\u72b6\u6001\uff1a\u52a0\u8f7d\u8bfe\u7a0b…"); this.courses = await getCourses(activeTermCode); if (!this.courses.length) throw new Error("\u6ca1\u6709\u627e\u5230\u8bfe\u7a0b"); const courseFilter = getCourseSelectionFilter(); if (!courseFilter.runAll) { if (!courseFilter.selectedIds.size) throw new Error("\u8bf7\u5148\u8bfb\u53d6\u5e76\u52fe\u9009\u81f3\u5c11\u4e00\u95e8\u8bfe\u7a0b"); this.courses = this.courses.filter((course) => { const ids = resolveCourseIdCandidates(course); return ids.some((id) => courseFilter.selectedIds.has(String(id))); }); if (!this.courses.length) throw new Error("\u5f53\u524d\u52fe\u9009\u8bfe\u7a0b\u5728\u6240\u9009\u5b66\u671f\u4e2d\u4e0d\u53ef\u7528"); } const totalCourses = this.courses.length; for (let i = 0; i < this.courses.length && !this.stopFlag; i++) { const course = this.courses[i]; updatePanelCourseLine(i + 1, totalCourses, course.courseName || ""); updatePanelCurrentPhase(null); updatePanelSummary("\u72b6\u6001\uff1a\u5b66\u4e60\u4e2d"); resetPillarProgress(); if (!CONFIG.autoVideo) updatePillarProgress("lesson", "\u5df2\u5173\u95ed"); if (!CONFIG.autoComment) updatePillarProgress("discuss", "\u5df2\u5173\u95ed"); if (!CONFIG.autoDocument) updatePillarProgress("doc", "\u5df2\u5173\u95ed"); if (!CONFIG.autoHomework) updatePillarProgress("homework", "\u5df2\u5173\u95ed"); const candidates = resolveCourseIdCandidates(course); if (!candidates.length) { updatePanelSummary("\u72b6\u6001\uff1a\u8df3\u8fc7\uff08\u65e0\u8bfe\u7a0b\u6807\u8bc6\uff09", true); await sleep(1200); continue; } let items = []; let chosenCourseId = ""; for (const cid of candidates) { const curItemsAll = await getCourseItems(cid, true, course, activeTermCode || ""); if (curItemsAll && curItemsAll.length) { items = curItemsAll; chosenCourseId = cid; break; } } if (!chosenCourseId) { updatePanelSummary("\u72b6\u6001\uff1a\u8df3\u8fc7\uff08\u65e0\u7ae0\u8282\uff09", true); await sleep(1200); continue; } let commentSkippedAlreadyFull = false; let commentSessionSuccess = 0; let documentHandledDone = false; let homeworkHandledDone = false; if (CONFIG.autoVideo && !this.stopFlag) { const shouldSubmitFinished = false; let videoItems = items; const src = videoItems.find(Boolean)?.__source || "unknown"; log(`\u89c6\u9891\u7ae0\u8282\u6765\u6e90=${src}: count=${videoItems.length}, done=${videoItems.filter(it => !!it.isFinish).length}`); if (src === "api") { const htmlItemsAll = await getCourseItemsFromHtml(chosenCourseId, true); if (htmlItemsAll && htmlItemsAll.length) { const norm = (s) => String(s || "").replace(/\s+/g, "").trim().toLowerCase(); const byName = new Map(); for (const h of htmlItemsAll) { const k = norm(h.itemName || h.lessonName || h.title); if (k && !byName.has(k)) byName.set(k, h); } let patched = 0; videoItems = videoItems.map((it) => { const k = norm(it.itemName || it.lessonName || it.title); const hit = k ? byName.get(k) : null; if (!hit) return it; const next = { ...it }; if (hit.itemId && String(hit.itemId) !== String(it.itemId || "")) { next.itemId = String(hit.itemId); patched += 1; } if (!next.altItemId && hit.altItemId) next.altItemId = String(hit.altItemId); if (!next.timeLen && hit.timeLen) next.timeLen = Number(hit.timeLen || 60); return next; }); if (patched > 0) { log(`\u89c6\u9891\u7ae0\u8282\u6620\u5c04\u4fee\u6b63: \u5df2\u6309HTML\u8865\u9f50 ${patched}/${videoItems.length} \u9879`); } } } const videoTotal = videoItems.filter((it) => !(it.isFinish && !shouldSubmitFinished)).length; log(`\u89c6\u9891\u9636\u6bb5\u5f00\u59cb: course_id=${chosenCourseId}, total=${videoTotal}`); if (videoTotal === 0) { updatePillarProgress("lesson", "\u5df2\u5b8c\u6210"); updatePanelCurrentPhase("lesson", "\u65e0\u5f85\u5904\u7406"); } else { updatePanelCurrentPhase("lesson", `0/${videoTotal}`); updatePillarProgress("lesson", `0/${videoTotal} \u5904\u7406\u4e2d`); let videoDone = 0; for (let j = 0; j < videoItems.length && !this.stopFlag; j++) { const item = videoItems[j]; if (item.isFinish && !shouldSubmitFinished) continue; const title = item.itemName || item.lessonName || item.title || "\u672a\u77e5\u7ae0\u8282"; const nextIndex = videoDone + 1; updatePillarProgress("lesson", `${title}\u5904\u7406\u4e2d\uff08${nextIndex}/${videoTotal}\uff09`); if (!CONFIG.videoSubmitOnce) { const verifyBefore = await verifyChapterCompletedByApi(chosenCourseId, [item.itemId], course, activeTermCode || ""); if (verifyBefore && verifyBefore.done) { videoDone += 1; updatePanelCurrentPhase("lesson", `${videoDone}/${videoTotal}`); updatePillarProgress("lesson", `${title}\u5df2\u5b8c\u6210\uff08${videoDone}/${videoTotal}\uff09`); continue; } } let chapterDone = false; let observedFinishLen = 0; let observedMinTime = Number(item.timeLen || 60); let lastSubmitCode = ""; let lastSubmitMsg = ""; let optimisticSuccessCount = 0; const maxRetry = CONFIG.videoSubmitOnce ? 2 : 6; const chapterStartMs = Date.now(); const chapterMaxMs = Math.max(45000, Number(observedMinTime || 60) * 1000 + 15000); for (let round = 0; round < maxRetry && !this.stopFlag; round++) { const elapsed = Date.now() - chapterStartMs; if (elapsed >= chapterMaxMs) { log(`\u89c6\u9891\u9636\u6bb5\u8b66\u544a: \u5355\u7ae0\u8282\u5904\u7406\u8d85\u65f6\uff0c\u81ea\u52a8\u8df3\u8fc7 item_id=${item.itemId}, elapsed=${elapsed}ms`, "warn"); break; } updatePanelCurrentPhase("lesson", `${videoDone}/${videoTotal}`); const submitTimeLen = Math.max(450, Number(item.timeLen || 0), Number(observedMinTime || 0)); const r = await submitPlay(chosenCourseId, item.itemId, submitTimeLen, item.altItemId); if (!r) { lastSubmitCode = "null"; lastSubmitMsg = "submitPlay-return-null"; log(`\u89c6\u9891\u9636\u6bb5\u8b66\u544a: \u63d0\u4ea4\u8fd4\u56de\u7a7a\uff0citem_id=${item.itemId}, round=${round + 1}/${maxRetry}`, "warn"); } else { lastSubmitCode = String(r.code ?? ""); lastSubmitMsg = String(r.message || ""); if (Number(r.code) !== 1000) { const rawPreview = String(r.__rawPreview || "").replace(/\s+/g, " ").slice(0, 120); log(`\u89c6\u9891\u63d0\u4ea4\u5931\u8d25: item_id=${item.itemId}, altItemId=${item.altItemId || ""}, code=${lastSubmitCode}, msg=${lastSubmitMsg || "\u672a\u77e5"}${rawPreview ? `, raw=${rawPreview}` : ""}`, "warn"); } const dbg = r.debugData || {}; const fLen = Number(dbg.finishLen || 0); const mTime = Number(dbg.minTime || 0); const isFinishByDebug = !!dbg.isFinish || (mTime > 0 && fLen >= mTime); if (fLen > observedFinishLen) observedFinishLen = fLen; if (mTime > 0) observedMinTime = mTime; if (Number(r.code) === 1000) { optimisticSuccessCount += 1; if (CONFIG.videoSubmitOnce) { if (isFinishByDebug) { chapterDone = true; break; } if (round + 1 >= maxRetry) { chapterDone = true; break; } continue; } await sleep(700); const verifyFast = await verifyChapterCompletedByApi(chosenCourseId, [item.itemId], course, activeTermCode || ""); if (verifyFast && verifyFast.done) { chapterDone = true; log(`[DBG-VIDEO] panel-accept-success code=1000+verify item=${item.itemId} alt=${item.altItemId || ""}`); break; } if (optimisticSuccessCount >= 2) { chapterDone = true; log(`[DBG-VIDEO] panel-accept-optimistic code=1000x${optimisticSuccessCount} item=${item.itemId} alt=${item.altItemId || ""}`, "warn"); break; } log(`[DBG-VIDEO] panel-delay-success code=1000-but-not-verified item=${item.itemId} alt=${item.altItemId || ""}`); } else { optimisticSuccessCount = 0; } } if (!CONFIG.videoSubmitOnce) { await sleep(600); const verifyAfter = await verifyChapterCompletedByApi(chosenCourseId, [item.itemId], course, activeTermCode || ""); if (verifyAfter && verifyAfter.done) { chapterDone = true; break; } } } if (chapterDone) { videoDone += 1; updatePanelCurrentPhase("lesson", `${videoDone}/${videoTotal}`); updatePillarProgress("lesson", `${title}\u5df2\u5b8c\u6210\uff08${videoDone}/${videoTotal}\uff09`); } else { updatePillarProgress("lesson", `${title}\u672a\u5b8c\u6210\uff08${nextIndex}/${videoTotal}\uff09`); log(`\u89c6\u9891\u9636\u6bb5\u8b66\u544a: \u591a\u8f6e\u8865\u63d0\u540e\u4ecd\u672a\u5b8c\u6210\uff0citem_id=${item.itemId}, altItemId=${item.altItemId || ""}, finishLen=${observedFinishLen}, minTime=${observedMinTime}`, "warn"); } const normalDelay = Number(CONFIG.videoDelay || 2000); const quickDelay = Math.max(500, Math.min(900, normalDelay)); await sleep(chapterDone && String(lastSubmitCode) === "1000" ? quickDelay : normalDelay); } if (this.stopFlag) updatePillarProgress("lesson", "\u5df2\u505c\u6b62"); else updatePillarProgress("lesson", "\u5df2\u5b8c\u6210"); } } else if (!CONFIG.autoVideo) {} if (CONFIG.autoComment && !this.stopFlag) { const { learningUserId } = getUserInfo(); const { schoolCode, gradeCode } = parseSchoolAndGradeFromCourse(course); updatePanelCurrentPhase("discuss", "\u83b7\u53d6\u5206\u6570\u4e2d"); updatePillarProgress("discuss", "\u8fdb\u884c\u4e2d"); const score = await getBbsScore({ courseId: chosenCourseId, schoolCode, gradeCode: gradeCode || String(activeTermCode || ""), learningUserId }); const bbsScore = Number(score?.bbsScore || 0); const regularScore = Number(score?.regularScore || 0); const maxScore = regularScore > 0 ? regularScore : 0; const cm = Number(CONFIG.fixedCommentTimes || 6); if (maxScore <= 0) { updatePanelCurrentPhase("discuss", "\u65e0\u9700\u8ba8\u8bba\uff080/0\uff09"); updatePillarProgress("discuss", "\u5df2\u5b8c\u6210"); } else if (bbsScore >= maxScore) { commentSkippedAlreadyFull = true; updatePanelCurrentPhase("discuss", "\u5df2\u8fbe\u6807"); updatePillarProgress("discuss", "\u5df2\u5b8c\u6210"); } else { for (let k = 0; k < cm && !this.stopFlag; k++) { updatePanelCurrentPhase("discuss", `${k + 1}/${cm}`); updatePillarProgress("discuss", `\u8fdb\u884c\u4e2d\uff08${k + 1}/${cm}\uff09`); const ok = await publishComment({ courseId: chosenCourseId, schoolCode, gradeCode: gradeCode || String(activeTermCode || ""), courseCode: course.courseCode, learningUserId }); if (ok) commentSessionSuccess += 1; updatePillarProgress("discuss", ok ? `\u5df2\u5b8c\u6210\uff08${k + 1}/${cm}\uff09` : `\u8fdb\u884c\u4e2d\uff08${k + 1}/${cm}\uff09`); await sleep(CONFIG.commentDelay); } if (this.stopFlag) updatePillarProgress("discuss", "\u5df2\u505c\u6b62"); else updatePillarProgress("discuss", "\u5df2\u5b8c\u6210"); } } if (CONFIG.autoDocument && !this.stopFlag) { const { learningUserId, userId } = getUserInfo(); updatePanelCurrentPhase("doc", ""); updatePillarProgress("doc", "\u8fdb\u884c\u4e2d"); await handleDocumentsForCourse({ courseId: chosenCourseId, learningUserId, userId, course }); documentHandledDone = true; if (this.stopFlag) updatePillarProgress("doc", "\u5df2\u505c\u6b62"); else updatePillarProgress("doc", "\u5df2\u5b8c\u6210"); } let homeworkResult = { fetched: false, allDone: false, pending: 0, message: "" }; if (CONFIG.autoHomework && !this.stopFlag) { const { learningUserId, userId } = getUserInfo(); updatePanelCurrentPhase("homework", ""); updatePillarProgress("homework", "\u8fdb\u884c\u4e2d"); const { gradeCode: hwGrade } = parseSchoolAndGradeFromCourse(course); homeworkResult = await handleHomeworkForCourse({ courseId: chosenCourseId, learningUserId, userId, course, gradeHint: hwGrade || String(activeTermCode || ""), getStop: () => this.stopFlag }) || homeworkResult; homeworkHandledDone = !!(homeworkResult.fetched && homeworkResult.allDone); if (!homeworkResult.fetched) { updatePillarProgress("homework", "\u672a\u83b7\u53d6\u5230\u5217\u8868"); } else if (this.stopFlag) { updatePillarProgress("homework", "\u5df2\u505c\u6b62"); } else if (!homeworkResult.allDone) { updatePillarProgress("homework", "\u4ecd\u6709 " + homeworkResult.pending + " \u4efd\u672a\u5b8c\u6210"); } else { updatePillarProgress("homework", "\u5df2\u5b8c\u6210"); } } const discussionHint = CONFIG.autoComment ? { alreadyFullAtStart: commentSkippedAlreadyFull, sessionSuccessCount: commentSessionSuccess } : null; await maybeRecalcUserScoreIfCourseFullyComplete({ courseId: chosenCourseId, course, termCode: activeTermCode || "", getStop: () => this.stopFlag, discussionHint, completionHint: { documentDone: documentHandledDone, homeworkDone: homeworkHandledDone } }); updatePanelCurrentPhase(null); await sleep(1500); } this.isRunning = false; if (this.callback) this.callback(true); } catch (err) { log(`❌ \u9519\u8bef: ${err.message}`, "error"); updatePanelSummary(`\u51fa\u9519\uff1a${String(err.message || "").slice(0, 96)}`, true); this.isRunning = false; if (this.callback) this.callback(false); } } stop() { this.stopFlag = true; this.isRunning = false; log("⏸ \u5df2\u505c\u6b62"); } } function updateUIStatus(status, isError = false) { updatePanelSummary("\u72b6\u6001\uff1a" + status, isError); } function showMessage(content, type = "info") { const colors = { info: "#1890ff", success: "#52c41a", warning: "#faad14", error: "#ff4d4f" }; const msgDiv = document.createElement("div"); msgDiv.style.cssText = "position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 10001; padding: 10px 20px; border-radius: 8px; background: " + (colors[type] || colors.info) + "; color: #fff; font-size: 14px;"; msgDiv.textContent = content; document.body.appendChild(msgDiv); setTimeout(() => { msgDiv.remove(); }, 2500); } async function startAutoPlay() { if (isRunning) return showMessage("\u6b63\u5728\u5b66\u4e60\u4e2d\uff0c\u8bf7\u52ff\u91cd\u590d\u70b9\u51fb", "warning"); if (isExamRunning) return showMessage("\u8003\u8bd5\u8fdb\u884c\u4e2d\uff0c\u8bf7\u5148\u505c\u6b62\u8003\u8bd5", "warning"); const licensed = await ensureProLicenseForAuto("\u4f7f\u7528\u4e00\u952e\u5b66\u4e60"); if (!licensed) return; showMessage("\u5f00\u59cb\u5b66\u4e60", "success"); resetPillarProgress(); updatePanelCourseLine(0, 0, "\u51c6\u5907\u4e2d"); updatePanelCurrentPhase(null); updateUIStatus("\u5b66\u4e60\u4e2d…"); autoPlayer = new AutoPlayCore(success => { isRunning = false; if (success) { showMessage("\u5b66\u4e60\u5b8c\u6bd5!", "success"); updateUIStatus("\u5df2\u5b8c\u6210"); } else { showMessage("\u5b66\u4e60\u5931\u8d25", "error"); updateUIStatus("\u5931\u8d25", true); } }); isRunning = true; autoPlayer.start().catch(err => { log("\u51fa\u9519: " + err.message, "error"); isRunning = false; updatePanelSummary("\u51fa\u9519\uff1a" + String(err.message || "").slice(0, 96), true); }); } function stopAutoPlay() { if (autoPlayer) { autoPlayer.stop(); } autoPlayer = null; isRunning = false; resetPillarProgress(); updatePanelCourseLine(0, 0, "—"); updatePanelCurrentPhase(null); updateUIStatus("\u5df2\u505c\u6b62"); showMessage("\u5df2\u505c\u6b62", "info"); } function configureLicenseBlockForPage() { if (!uiContainer) return; const block = uiContainer.querySelector("#license-block"); const tokenRow = uiContainer.querySelector("#license-token-row"); const tokenTip = uiContainer.querySelector("#license-token-tip"); const recheck = uiContainer.querySelector("#license-recheck"); const purchase = uiContainer.querySelector("#license-purchase"); const saveTok = uiContainer.querySelector("#license-token-save"); const tokIn = uiContainer.querySelector("#license-token-input"); if (shouldShowLicenseTokenUi()) return; if (tokenRow) tokenRow.style.display = "none"; if (tokenTip) tokenTip.style.display = "none"; if (recheck) recheck.style.display = "none"; if (purchase) purchase.style.display = "none"; if (saveTok) saveTok.style.display = "none"; if (tokIn) { tokIn.style.display = "none"; tokIn.disabled = true; } let examHint = uiContainer.querySelector("#license-exam-hint"); if (!examHint && block) { examHint = document.createElement("div"); examHint.id = "license-exam-hint"; examHint.style.cssText = "font-size:11px;color:#ad6800;margin-top:6px;line-height:1.5;"; examHint.textContent = "\u8003\u8bd5\u9875\u4e0d\u652f\u6301\u586b\u5199 Token\u3002\u8bf7\u5148\u5230\u5b66\u6821\u5b66\u751f\u4e3b\u7ad9\u5b8c\u6210\u6388\u6743\uff0c\u672c\u9875\u5c06\u81ea\u52a8\u6cbf\u7528\u4e3b\u7ad9\u7ed1\u5b9a\u3002"; block.appendChild(examHint); } if (examHint) examHint.style.display = "block"; } function createUI() { const ver = getScriptVersion(); try { if (uiContainer) { const oldVer = uiContainer.getAttribute("data-wencai-ver") || ""; const hasPillars = !!uiContainer.querySelector("#p-lesson") && !!uiContainer.querySelector("#p-doc"); if (oldVer !== String(ver) || !hasPillars) { uiContainer.remove(); uiContainer = null; } else { return; } } } catch (_) {} uiContainer = document.createElement("div"); uiContainer.id = "wencai-auto-player"; uiContainer.setAttribute("data-wencai-ver", String(ver)); uiContainer.setAttribute("data-wencai-template", "B"); uiContainer.style.cssText = "position:fixed;bottom:20px;right:20px;z-index:2147483647;background:#fff;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,.15);width:460px;font-size:14px;"; uiContainer.innerHTML = "\n
\n
\n \"logo\"\n \u6587\u624d\u5b66\u5802\u81ea\u52a8\u5b66\u4e60\u52a9\u624b v" + ver + "\n
\n
\n \n \n
\n
\n
\n
\n
\u6388\u6743\uff1a\n \n
\n
\n \n \n
\n
\u514d\u8d39\uff1a\u8bfe\u4ef6\u9875\u5df2\u542f\u7528\u500d\u901f/\u4e0b\u4e00\u8282\u3002\u4e00\u952e\u5b66\u4e60\u529f\u80fd\u9700\u586b\u5199token\u3002
\n
\n
\n
\u514d\u8d39\u8bbe\u7f6e
\n
\n \n
\n \n \n \n \n \n \n\n
\n
\n
\n \n
\n
\n \u5b66\u671f\uff1a\n \n
\n \n
\n
\n
\u8bfe\u7a0b\u9009\u62e9
\n
\n \n \n
\n
\u70b9\u51fb“\u8bfb\u53d6\u5f53\u524d\u5b66\u671f\u8bfe\u7a0b”\u540e\u53ef\u52fe\u9009\u6307\u5b9a\u8bfe\u7a0b\u3002
\n
\n
\n
\u72b6\u6001\uff1a\u5f85\u547d
\n
\n
\u5f53\u524d\uff1a—
\n
\n
\u89c6\u9891\uff1a—
\n
\u4f5c\u4e1a\uff1a—
\n
\u8ba8\u8bba\uff1a—
\n
\u8d44\u6599\uff1a—
\n
\n
\n
\n \n \n \n
\n
\n \n \n
\n
\n "; document.body.appendChild(uiContainer); const shieldStyle = document.createElement("style"); shieldStyle.textContent = "#wencai-auto-player #start-btn,#wencai-auto-player #stop-btn,#wencai-auto-player #exam-btn,#wencai-auto-player #exam-stop-btn{opacity:1!important;visibility:visible!important;text-indent:0!important;color:#fff!important;-webkit-text-fill-color:#fff!important;font-size:14px!important;}"; uiContainer.insertBefore(shieldStyle, uiContainer.firstChild); const panelStyle = document.createElement("style"); panelStyle.textContent = "\n #wencai-auto-player #wencai-panel-logo{width:34px!important;height:34px!important;border-radius:10px!important;object-fit:cover!important;box-shadow:0 2px 10px rgba(15,23,42,.12)!important;border:1px solid rgba(148,163,184,.45)!important;background:#fff!important;flex-shrink:0!important;}\n #wencai-auto-player { color:#333 !important; }\n #wencai-auto-player { font-size:12px !important; }\n #wencai-auto-player * { box-sizing:border-box; font-family: -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Arial,\"PingFang SC\",\"Microsoft YaHei\",sans-serif !important; }\n #wencai-auto-player #license-block,\n #wencai-auto-player #free-block { color:#555 !important; }\n #wencai-auto-player #free-block * { color:#333 !important; }\n #wencai-auto-player #free-block label { color:#333 !important; font-weight:400 !important; }\n #wencai-auto-player label { color:#333 !important; font-size:12px !important; }\n #wencai-auto-player span { color:inherit !important; font-size:12px !important; }\n #wencai-auto-player input,\n #wencai-auto-player select,\n #wencai-auto-player textarea { font-size:12px !important; color:#111 !important; }\n #wencai-auto-player input,\n #wencai-auto-player select,\n #wencai-auto-player textarea { color:#111 !important; }\n #wencai-auto-player #term-select {\n color:#111 !important;\n background:#fff !important;\n -webkit-text-fill-color:#111 !important;\n }\n #wencai-auto-player #term-select option {\n color:#111 !important;\n background:#fff !important;\n -webkit-text-fill-color:#111 !important;\n }\n #wencai-auto-player a { color:#1890ff !important; }\n /* \u500d\u901f\uff1a\u539f\u751f select \u5728\u90e8\u5206 Win/Chrome+\u9875\u9762\u6837\u5f0f\u4e0b\u4f1a\u51fa\u73b0\u7a7a\u767d/\u4e0d\u6e32\u67d3\uff0c\u6539\u7528\u6309\u94ae\u7ec4\uff1b\u9690\u85cf select \u4ec5\u540c\u6b65\u5b58\u50a8 */\n #wencai-auto-player #free-block { display:block !important; overflow:visible !important; }\n #wencai-auto-player .wencai-hidden-free-rate-select {\n position:absolute !important;\n left:-9999px !important;\n width:1px !important;\n height:1px !important;\n opacity:0 !important;\n pointer-events:none !important;\n margin:0 !important;\n padding:0 !important;\n border:0 !important;\n clip:rect(0,0,0,0) !important;\n }\n #wencai-auto-player .wencai-hidden-term-select {\n position:absolute !important;\n left:-9999px !important;\n width:1px !important;\n height:1px !important;\n opacity:0 !important;\n pointer-events:none !important;\n margin:0 !important;\n padding:0 !important;\n border:0 !important;\n clip:rect(0,0,0,0) !important;\n }\n #wencai-auto-player .wencai-free-rate-btn {\n display:inline-block !important;\n padding:2px 8px !important;\n font-size:11px !important;\n line-height:1.45 !important;\n border:1px solid #d9d9d9 !important;\n border-radius:4px !important;\n background:#fff !important;\n color:#111 !important;\n cursor:pointer !important;\n -webkit-text-fill-color:#111 !important;\n font-weight:500 !important;\n box-sizing:border-box !important;\n }\n #wencai-auto-player .wencai-free-rate-btn:hover {\n border-color:#40a9ff !important;\n color:#096dd9 !important;\n }\n #wencai-auto-player .wencai-free-rate-btn.wencai-free-rate-btn--active {\n border-color:#1890ff !important;\n background:#e6f7ff !important;\n color:#096dd9 !important;\n -webkit-text-fill-color:#096dd9 !important;\n }\n #wencai-auto-player .wencai-term-btn {\n display:inline-block !important;\n padding:2px 8px !important;\n font-size:11px !important;\n line-height:1.45 !important;\n border:1px solid #d9d9d9 !important;\n border-radius:4px !important;\n background:#fff !important;\n color:#111 !important;\n cursor:pointer !important;\n -webkit-text-fill-color:#111 !important;\n font-weight:500 !important;\n box-sizing:border-box !important;\n }\n #wencai-auto-player .wencai-term-btn:hover {\n border-color:#40a9ff !important;\n color:#096dd9 !important;\n }\n #wencai-auto-player .wencai-term-btn.wencai-term-btn--active {\n border-color:#52c41a !important;\n background:#f6ffed !important;\n color:#389e0d !important;\n -webkit-text-fill-color:#389e0d !important;\n }\n /* \u5f3a\u5236“\u6388\u6743\u884c / \u56db\u9879\u8fdb\u5ea6”\u53ef\u89c1\uff0c\u907f\u514d\u88ab\u9875\u9762\u5168\u5c40 CSS \u8986\u76d6\u4e3a\u9690\u85cf/\u900f\u660e/\u584c\u9677 */\n #wencai-auto-player #license-line { display:block !important; visibility:visible !important; opacity:1 !important; height:auto !important; min-height:16px !important; overflow:visible !important; color:#333 !important; background:#fffbe6 !important; border:1px solid #ffd666 !important; border-radius:6px !important; padding:4px 6px !important; }\n #wencai-auto-player #license-status { display:inline-block !important; visibility:visible !important; opacity:1 !important; position:relative !important; z-index:10001 !important; color:#000 !important; font-size:12px !important; font-weight:700 !important; line-height:14px !important; }\n #wencai-auto-player #license-line * { visibility:visible !important; opacity:1 !important; }\n #wencai-auto-player #pillars-block { display:block !important; visibility:visible !important; opacity:1 !important; height:auto !important; min-height:40px !important; overflow:visible !important; color:#333 !important; background:#fafafa !important; border:1px solid #f0f0f0 !important; border-radius:8px !important; }\n #wencai-auto-player #pillars-block > div { display:block !important; color:#333 !important; font-size:13px !important; }\n #wencai-auto-player #panel-announcement.wencai-panel-announcement {\n display:block !important;\n margin-bottom:12px !important;\n padding:10px 12px !important;\n border-radius:8px !important;\n border:1px solid #91d5ff !important;\n background:#e6f7ff !important;\n color:#333 !important;\n font-size:12px !important;\n line-height:1.55 !important;\n white-space:normal !important;\n word-break:break-word !important;\n min-height:40px !important;\n box-sizing:border-box !important;\n }\n #wencai-auto-player #panel-announcement .wencai-announce-body {\n display:block !important;\n visibility:visible !important;\n opacity:1 !important;\n position:relative !important;\n z-index:2 !important;\n margin:0 0 10px 0 !important;\n padding:0 !important;\n color:#1f1f1f !important;\n -webkit-text-fill-color:#1f1f1f !important;\n font-size:13px !important;\n font-weight:500 !important;\n line-height:1.6 !important;\n white-space:pre-wrap !important;\n word-break:break-word !important;\n min-height:1.2em !important;\n }\n #wencai-auto-player #panel-announcement .wencai-announce-promo {\n display:block !important;\n visibility:visible !important;\n opacity:1 !important;\n position:relative !important;\n z-index:1 !important;\n margin-top:0 !important;\n padding:10px 12px !important;\n border-radius:8px !important;\n border:2px solid #fa8c16 !important;\n background:linear-gradient(180deg,#fff7e6 0%,#fffbe6 100%) !important;\n color:#ad4e00 !important;\n -webkit-text-fill-color:#ad4e00 !important;\n font-size:13px !important;\n font-weight:700 !important;\n line-height:1.5 !important;\n text-align:center !important;\n box-shadow:0 2px 8px rgba(250,140,22,.18) !important;\n }\n #wencai-auto-player #panel-announcement.wencai-announce-empty { color:#888 !important; }\n "; uiContainer.appendChild(panelStyle); const startBtn = uiContainer.querySelector("#start-btn"); const stopBtn = uiContainer.querySelector("#stop-btn"); const examBtn = uiContainer.querySelector("#exam-btn"); const examStopBtn = uiContainer.querySelector("#exam-stop-btn"); const cookieClearBtn = uiContainer.querySelector("#cookie-clear-btn"); if (startBtn) { startBtn.textContent = "\u5f00\u59cb\u5b66\u4e60"; } if (stopBtn) { stopBtn.textContent = "\u505c\u6b62"; } const annEl = uiContainer.querySelector("#panel-announcement"); if (annEl) { const bodyText = CONFIG.panelAnnouncementBody != null && String(CONFIG.panelAnnouncementBody).trim() ? String(CONFIG.panelAnnouncementBody).trim() : "\u76ee\u524d\u5df2\u7ecf\u9002\u914d\u7edd\u5927\u90e8\u5206\u9662\u6821\u67e0\u6aac\u5b66\u5802\uff0c\u4e0d\u9002\u914d\u5b66\u6821\u53ca\u53cd\u9988\u8054\u7cfbQQ125431514\u3002"; const promoText = CONFIG.panelAnnouncementPromo != null && String(CONFIG.panelAnnouncementPromo).trim() ? String(CONFIG.panelAnnouncementPromo).trim() : "\u9650\u65f6\u7279\u60e01\u5143\u5373\u53ef\u4eab\u53d7\u5168\u90e8\u529f\u80fd\uff0c\u8be6\u60c5\u8bf7\u70b9\u51fb\u5f00\u59cb\u5b66\u4e60\u3002"; annEl.innerHTML = ""; const bodyEl = document.createElement("div"); bodyEl.className = "wencai-announce-body"; bodyEl.textContent = bodyText; const promoEl = document.createElement("div"); promoEl.className = "wencai-announce-promo"; promoEl.textContent = promoText; annEl.appendChild(bodyEl); annEl.appendChild(promoEl); annEl.classList.remove("wencai-announce-empty"); } resetPillarProgress(); updatePanelCourseLine(0, 0, "—"); updatePanelCurrentPhase(null); updatePanelSummary("\u72b6\u6001\uff1a\u5f85\u547d"); if (startBtn) { startBtn.onclick = () => startAutoPlay(); } if (stopBtn) { stopBtn.onclick = stopAutoPlay; } if (examBtn) { examBtn.textContent = "\u5f00\u59cb\u8003\u8bd5"; examBtn.onclick = () => startExamAuto(); } if (examStopBtn) { examStopBtn.textContent = "\u505c\u6b62\u8003\u8bd5"; examStopBtn.onclick = stopExamAuto; } if (cookieClearBtn) { cookieClearBtn.onclick = () => { const ok = window.confirm("\u5c06\u6e05\u7406\u5b66\u4e60\u76f8\u5173 Cookie\uff08open/zh/jx \u7b49\u524d\u7f00\uff09\u5e76\u5237\u65b0\u9875\u9762\u3002\n\n\u5982\u679c\u4f60\u6b63\u5728\u5b66\u4e60\u4e2d\u4f1a\u88ab\u8feb\u9000\u51fa\u767b\u5f55\u3002\n\u786e\u5b9a\u7ee7\u7eed\u5417\uff1f"); if (!ok) { return; } try { stopAutoPlay(); } catch (_) {} const cleared = clearLearningCookies(); showMessage("\u5df2\u6e05\u7406 Cookie\uff1a" + cleared.length + "\u9879\uff0c\u6b63\u5728\u5237\u65b0…", "success"); setTimeout(() => { try { location.reload(); } catch (_) {} }, 600); }; } const closeBtn = uiContainer.querySelector("#close-btn"); const minBtn = uiContainer.querySelector("#min-btn"); const restoreBtnId = "wencai-auto-player-restore-btn"; const ensureRestoreBtn = () => { let btn = document.getElementById(restoreBtnId); if (btn) { return btn; } btn = document.createElement("button"); btn.id = restoreBtnId; btn.type = "button"; btn.textContent = "\u6062\u590d"; btn.style.cssText = "position:fixed;bottom:20px;right:20px;z-index:10000;background:#1890ff;color:#fff;border:none;border-radius:999px;padding:10px 14px;cursor:pointer;font-size:13px;box-shadow:0 6px 20px rgba(0,0,0,.15);display:none;"; btn.style.setProperty("z-index", "2147483647", "important"); btn.onclick = () => { if (uiContainer) { uiContainer.style.display = "block"; } const b = document.getElementById(restoreBtnId); if (b) { b.style.display = "none"; } }; document.body.appendChild(btn); return btn; }; const minimizePanel = () => { if (!uiContainer) { return; } uiContainer.style.display = "none"; ensureRestoreBtn().style.display = "block"; }; if (minBtn) { minBtn.onclick = () => minimizePanel(); } if (closeBtn) { closeBtn.onclick = () => { try { const b = document.getElementById(restoreBtnId); if (b) { b.remove(); } } catch (_) {} uiContainer.remove(); uiContainer = null; }; } const purchase = uiContainer.querySelector("#license-purchase"); if (purchase) { purchase.onclick = e => { e.preventDefault(); if (CONFIG.licensePurchaseUrl) { window.open(CONFIG.licensePurchaseUrl, "_blank", "noopener"); } }; } const recheck = uiContainer.querySelector("#license-recheck"); if (recheck) { recheck.onclick = () => { const st = uiContainer?.querySelector("#license-status"); if (st) { st.textContent = "\u6821\u9a8c\u4e2d…"; } showMessage("\u6b63\u5728\u6821\u9a8c\u6388\u6743…", "info"); verifyProLicense(true).then(t => { updateLicenseStatusUI(); if (t) { showMessage("\u6388\u6743\u6821\u9a8c\u901a\u8fc7", "success"); } else { showMessage("\u6388\u6743\u6821\u9a8c\u5931\u8d25", "warning"); } }); }; } const saveTok = uiContainer.querySelector("#license-token-save"); const tokIn = uiContainer.querySelector("#license-token-input"); if (saveTok && tokIn) { saveTok.onclick = () => { if (!shouldShowLicenseTokenUi()) { showMessage("\u8bf7\u52ff\u5728\u8003\u8bd5\u9875\u586b\u5199 Token\uff0c\u8bf7\u5230\u5b66\u6821\u5b66\u751f\u4e3b\u7ad9\u64cd\u4f5c", "warning"); return; } const raw = tokIn.value || ""; const trimmed = raw.trim(); if (!setLicenseToken(trimmed)) return; tokIn.value = ""; const termSelectEl = uiContainer.querySelector("#license-status"); if (termSelectEl) { termSelectEl.textContent = "\u6821\u9a8c\u4e2d…"; if (CONFIG.verboseConsole) { console.info("[license] #license-status set to \u6821\u9a8c\u4e2d…"); } } else if (CONFIG.verboseConsole) { console.warn("[license] save: #license-status not found"); } if (CONFIG.verboseConsole) { console.info("[license] token saved, len=", trimmed.length); } showMessage(trimmed.length ? "Token \u5df2\u4fdd\u5b58" : "Token \u4e3a\u7a7a\uff0c\u672a\u4fdd\u5b58", trimmed.length ? "success" : "warning"); updateLicenseStatusUI(); }; } if (tokIn && getLicenseToken()) { tokIn.placeholder = "\u5df2\u4fdd\u5b58 Token\uff0c\u53ef\u7c98\u8d34\u65b0\u503c\u8986\u76d6"; } const tokenRow = uiContainer.querySelector("#license-token-row"); const tokenTip = uiContainer.querySelector("#license-token-tip"); if (tokenRow) { const showTokenUi = !!(CONFIG.licenseApiBase || "").trim(); tokenRow.style.display = showTokenUi ? "flex" : "none"; if (tokenTip) { tokenTip.style.display = showTokenUi ? "block" : "none"; } } const freeEnableRateEl = uiContainer.querySelector("#free-enable-rate"); const freeRateSelectEl = uiContainer.querySelector("#free-rate-select"); const freeAutoNextEl = uiContainer.querySelector("#free-auto-next-video"); const termSelectEl = uiContainer.querySelector("#term-select"); const termBtnWrapEl = uiContainer.querySelector("#term-btn-wrap"); const termRefreshEl = uiContainer.querySelector("#term-refresh"); const courseRunAllEl = uiContainer.querySelector("#course-run-all"); const courseLoadBtnEl = uiContainer.querySelector("#course-load-btn"); const courseListEl = uiContainer.querySelector("#course-list"); const freeRateBtns = uiContainer.querySelectorAll(".wencai-free-rate-btn"); const setCourseListDisabled = (disabled) => { if (!courseListEl) return; courseListEl.querySelectorAll("input[type=\"checkbox\"][data-course-id]").forEach((el) => { el.disabled = !!disabled; }); courseListEl.style.opacity = disabled ? "0.65" : "1"; }; const renderCourseList = (courses) => { if (!courseListEl) return; courseListEl.innerHTML = ""; if (!Array.isArray(courses) || courses.length === 0) { courseListEl.textContent = "\u5f53\u524d\u5b66\u671f\u672a\u8bfb\u53d6\u5230\u8bfe\u7a0b\u3002"; return; } for (const course of courses) { const ids = resolveCourseIdCandidates(course); const courseId = ids[0] ? String(ids[0]) : ""; if (!courseId) continue; const row = document.createElement("label"); row.style.display = "flex"; row.style.gap = "6px"; row.style.alignItems = "center"; row.style.marginBottom = "4px"; const cb = document.createElement("input"); cb.type = "checkbox"; cb.setAttribute("data-course-id", courseId); cb.checked = true; const text = document.createElement("span"); text.textContent = `${String(course.courseName || "\u672a\u547d\u540d\u8bfe\u7a0b")} (${courseId})`; row.appendChild(cb); row.appendChild(text); courseListEl.appendChild(row); } if (!courseListEl.children.length) { courseListEl.textContent = "\u5f53\u524d\u5b66\u671f\u8bfe\u7a0b\u7f3a\u5c11\u53ef\u8bc6\u522b course_id\u3002"; } setCourseListDisabled(!!courseRunAllEl?.checked); }; const loadCoursesForPicker = async () => { if (!courseListEl) return; courseListEl.textContent = "\u8bfe\u7a0b\u8bfb\u53d6\u4e2d…"; const terms = await getSemester(); if (!terms || terms.length === 0) { courseListEl.textContent = "\u672a\u8bfb\u53d6\u5230\u5b66\u671f\u4fe1\u606f\u3002"; return; } const currentTerm = getCurrentTermFromList(terms); const selectedTermCode = getSelectedTermCode(); const pickedTerm = selectedTermCode ? (terms.find(t => getTermCode(t) === selectedTermCode) || currentTerm) : currentTerm; const activeTermCode = getTermCode(pickedTerm); const courses = await getCourses(activeTermCode); renderCourseList(courses); }; const renderTermButtons = () => { if (!termSelectEl || !termBtnWrapEl) return; const selected = String(termSelectEl.value || ""); termBtnWrapEl.innerHTML = ""; const opts = Array.from(termSelectEl.options || []); for (const op of opts) { const btn = document.createElement("button"); btn.type = "button"; btn.className = "wencai-term-btn" + (String(op.value) === selected ? " wencai-term-btn--active" : ""); btn.textContent = String(op.textContent || op.label || op.value || "").trim() || String(op.value || "\u5b66\u671f"); btn.setAttribute("data-term-code", String(op.value || "")); btn.onclick = () => { termSelectEl.value = String(op.value || ""); if (typeof termSelectEl.onchange === "function") { termSelectEl.onchange(); } renderTermButtons(); }; termBtnWrapEl.appendChild(btn); } }; const populateTermSelect = async () => { if (!termSelectEl) return; const previous = getSelectedTermCode(); termSelectEl.innerHTML = ""; const autoOpt = document.createElement("option"); autoOpt.value = ""; autoOpt.textContent = "\u5f53\u524d\u5b66\u671f\uff08\u81ea\u52a8\uff09"; termSelectEl.appendChild(autoOpt); const ver = await getSemester(); if (!Array.isArray(ver) || ver.length === 0) { termSelectEl.value = ""; renderTermButtons(); return; } const labelMap = getTermLabelMapByOrder(ver); const current = getCurrentTermFromList(ver); const currentCode = getTermCode(current); const orderedCodes = getOrderedTermCodes(ver); const currentIdx = currentCode ? orderedCodes.indexOf(currentCode) : -1; const visibleCodes = new Set(currentIdx >= 0 ? orderedCodes.slice(0, currentIdx + 1) : orderedCodes); for (const t of ver) { const code = getTermCode(t); if (!code) continue; if (!visibleCodes.has(code)) continue; const op = document.createElement("option"); op.value = code; const coreLabel = labelMap[code] || code; op.textContent = coreLabel + (getTermCode(current) === code ? "\uff08\u5f53\u524d\uff09" : ""); termSelectEl.appendChild(op); } if (previous && visibleCodes.has(previous)) termSelectEl.value = previous; else termSelectEl.value = ""; renderTermButtons(); }; if (termSelectEl) { populateTermSelect().catch(() => {}); termSelectEl.onchange = () => { const v = String(termSelectEl.value || ""); setSelectedTermCode(v); showMessage(v ? "\u5df2\u5207\u6362\u4e3a\u81ea\u9009\u5b66\u671f" : "\u5df2\u5207\u56de\u5f53\u524d\u5b66\u671f\uff08\u81ea\u52a8\uff09", "success"); renderTermButtons(); if (courseListEl) courseListEl.textContent = "\u5b66\u671f\u5df2\u5207\u6362\uff0c\u8bf7\u70b9\u51fb“\u8bfb\u53d6\u5f53\u524d\u5b66\u671f\u8bfe\u7a0b”\u3002"; }; } if (termRefreshEl) { termRefreshEl.onclick = async () => { await populateTermSelect(); showMessage("\u5b66\u671f\u5217\u8868\u5df2\u5237\u65b0", "success"); }; } if (courseRunAllEl) { courseRunAllEl.onchange = () => { setCourseListDisabled(!!courseRunAllEl.checked); }; setCourseListDisabled(!!courseRunAllEl.checked); } if (courseLoadBtnEl) { courseLoadBtnEl.onclick = async () => { await loadCoursesForPicker(); showMessage("\u8bfe\u7a0b\u5217\u8868\u5df2\u5237\u65b0", "success"); }; } if (freeEnableRateEl && freeRateSelectEl && freeAutoNextEl) { const RATE_OPTS = ["0.75", "1", "1.25", "1.5", "2", "3", "4"]; const normalizeRateStr = raw => { const s = String(raw); if (RATE_OPTS.includes(s)) { return s; } const n = Number(s); if (!Number.isFinite(n)) { return "1.5"; } const nums = RATE_OPTS.map(Number); let best = RATE_OPTS[0]; let d = Math.abs(n - nums[0]); for (let i = 0; i < nums.length; i++) { const dd = Math.abs(n - nums[i]); if (dd < d) { d = dd; best = RATE_OPTS[i]; } } return best; }; freeEnableRateEl.checked = !!getFreeEnableRate(); freeRateSelectEl.value = normalizeRateStr(getFreePlaybackRate()); freeAutoNextEl.checked = !!getFreeAutoNextVideo(); const syncRateButtons = () => { const v = String(freeRateSelectEl.value); freeRateBtns.forEach(t => { const is = t.getAttribute("data-rate") === v; t.classList.toggle("wencai-free-rate-btn--active", is); }); }; syncRateButtons(); const saveFree = () => { GM_setValue(GM_KEY_FREE_ENABLE_RATE, !!freeEnableRateEl.checked); GM_setValue(GM_KEY_FREE_RATE, String(freeRateSelectEl.value)); GM_setValue(GM_KEY_FREE_AUTO_NEXT, !!freeAutoNextEl.checked); applyFreePlaybackRateToCurrentVideo(); initFreeVideoMode(); showMessage("\u514d\u8d39\u8bbe\u7f6e\u5df2\u4fdd\u5b58", "success"); }; freeEnableRateEl.onchange = saveFree; freeAutoNextEl.onchange = saveFree; freeRateBtns.forEach(t => { t.onclick = () => { const r = t.getAttribute("data-rate"); if (!r) { return; } freeRateSelectEl.value = r; syncRateButtons(); saveFree(); }; }); } configureLicenseBlockForPage(); updateLicenseStatusUI(); } function init() { try { initFreeVideoMode(); } catch (e) { if (CONFIG.verboseConsole) { log("initFreeVideoMode: " + e, "warn"); } } if (!shouldShowControlPanelHere()) { return; } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => setTimeout(createUI, 1200)); } else { setTimeout(createUI, 1200); } } init(); })();