// ==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 = /