// ==UserScript== // @name 【效率版】学历教育云·自考云服务平台助手 // @namespace https://card.wlxy.live/details/65B09949 // @version 1.0 // @description 学历教育云/自考云(edu-xl.com)一键学习:课时、知识点、阶段作业、综合测评流水线;满分自动跳过;免费3视频小节,会员不限次数且含作业测评;会员开通后30天有效。目前已经适配兰州大学高等教育自学考试综合管理平台,其他平台如果不适配请联系作者QQ125431514 // @antifeature payment 免费可学3个视频小节,升级会员不限次数 // @antifeature membership 需云端授权 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @match *://*.edu-xl.com/* // @match *://cws.edu-edu.com/* // @match *://*.edu-edu.com/* // @match *://*.edu-edu.com.cn/* // @run-at document-idle // @noframes // @connect resgk.edu-xl.com // @connect *.edu-xl.com // @connect cws.edu-edu.com // @connect *.edu-edu.com // @connect e13.edu-edu.com.cn // @connect e14.edu-edu.com.cn // @connect em.edu-edu.com.cn // @connect *.edu-edu.com.cn // @connect oa28.ahzsksw.cn // @connect card.wlxy.live // @connect huaweicloudobs.ahjxjy.cn // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_openInTab // @grant GM_info // @grant unsafeWindow // @tag 学历教育云 // @tag 自考云 // @tag 成教 // @tag 学习辅助 // @license All Rights Reserved // ==/UserScript== (function () { "use strict"; try { if (window.top !== window.self) return; } catch (_) { return; } const WIN = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; const BOOT_GUARD = "__eduxlAssistantBooted__"; try { if (WIN[BOOT_GUARD]) return; Object.defineProperty(WIN, BOOT_GUARD, { value: true, writable: false, configurable: false, enumerable: false, }); } catch (_) { return; } function readScriptVersion() { try { if (typeof GM_info !== "undefined" && GM_info.script && GM_info.script.version) { return String(GM_info.script.version).trim(); } } catch (_) {} try { const nodes = document.getElementsByTagName("script"); for (let i = 0; i < nodes.length; i++) { const m = String(nodes[i].textContent || "").match(/\/\/\s*@version\s+(\S+)/); if (m) return m[1].trim(); } } catch (_) {} return "dev"; } const VERSION = readScriptVersion(); const PANEL_LOGO = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png"; const SALT = "14daab0a-4aff-4f6e-b303-c85f09c39f42"; const PANEL_ID = "eduxl-panel"; const STYLE_ID = "eduxl-panel-style-v3"; const FACE_CACHE_KEY = "eduxl_face_cache_v1"; const CLIENT_CODE_KEY = "eduxl_client_code_v1"; const CLOUD_API_BASE = "https://oa28.ahzsksw.cn"; const PRO_BUY_URL = "https://card.wlxy.live/details/65B09949"; const CLOUD_TOKEN_KEY = "eduxl_cloud_token_v1"; const CLOUD_LEASE_KEY = "eduxl_cloud_lease_v1"; const LEARNING_UID_KEY = "eduxl_learning_uid_v1"; const QUOTA_UID_KEY = "eduxl_quota_uid_v1"; const QUOTA_SITE_KEY = "eduxl_quota_site_v1"; const CWS = "https://cws.edu-edu.com"; const E14 = "https://e14.edu-edu.com.cn"; const EM = "https://em.edu-edu.com.cn"; const BACK_URL_DEFAULT = "https://resgk.edu-xl.com/LearnReturnUrl/LearnReturnUrlIndex"; function examBase(sess) { const b = (sess && sess.examBase) || ""; if (/^https?:\/\/e\d+\.edu-edu\.com\.cn$/i.test(b)) return b.replace(/\/$/, ""); return E14; } function rememberExamBase(sess, urlOrHtml) { if (!sess) return; const s = String(urlOrHtml || ""); const m = s.match(/https?:\/\/(e\d+\.edu-edu\.com\.cn)/i); if (m) sess.examBase = `https://${m[1]}`; } const LS = { gap: "eduxl_gap_ms", step: "eduxl_step_sec", mode: "eduxl_engine_mode", collapsed: "eduxl_panel_collapsed", rate: "eduxl_play_rate", tab: "eduxl_panel_tab", }; const host = location.hostname.toLowerCase(); const isEduEdu = /(^|\.)edu-edu\.com$/i.test(host); const isEduXl = /(^|\.)edu-xl\.com$/i.test(host); if (!isEduEdu && !isEduXl) return; function md5(str) { function cm(x, y) { const l = (x & 0xffff) + (y & 0xffff); return (((x >> 16) + (y >> 16) + (l >> 16)) << 16) | (l & 0xffff); } function rl(n, c) { return (n << c) | (n >>> (32 - c)); } function cmn(q, a, b, x, s, t) { return cm(rl(cm(cm(a, q), cm(x, t)), s), b); } function ff(a, b, c, d, x, s, t) { return cmn((b & c) | (~b & d), a, b, x, s, t); } function gg(a, b, c, d, x, s, t) { return cmn((b & d) | (c & ~d), a, b, x, s, t); } function hh(a, b, c, d, x, s, t) { return cmn(b ^ c ^ d, a, b, x, s, t); } function ii(a, b, c, d, x, s, t) { return cmn(c ^ (b | ~d), a, b, x, s, t); } function toBytes(s) { const utf8 = unescape(encodeURIComponent(s)); const out = []; for (let i = 0; i < utf8.length; i++) out.push(utf8.charCodeAt(i)); return out; } const bytes = toBytes(String(str)); const n = bytes.length; const words = []; for (let i = 0; i < n; i++) words[i >> 2] |= bytes[i] << ((i % 4) * 8); words[n >> 2] |= 0x80 << ((n % 4) * 8); const bitLen = n * 8; words[(((bitLen + 64) >>> 9) << 4) + 14] = bitLen; let a = 1732584193; let b = -271733879; let c = -1732584194; let d = 271733878; for (let i = 0; i < words.length; i += 16) { const oa = a, ob = b, oc = c, od = d; const w = []; for (let j = 0; j < 16; j++) w[j] = words[i + j] || 0; a = ff(a, b, c, d, w[0], 7, -680876936); d = ff(d, a, b, c, w[1], 12, -389564586); c = ff(c, d, a, b, w[2], 17, 606105819); b = ff(b, c, d, a, w[3], 22, -1044525330); a = ff(a, b, c, d, w[4], 7, -176418897); d = ff(d, a, b, c, w[5], 12, 1200080426); c = ff(c, d, a, b, w[6], 17, -1473231341); b = ff(b, c, d, a, w[7], 22, -45705983); a = ff(a, b, c, d, w[8], 7, 1770035416); d = ff(d, a, b, c, w[9], 12, -1958414417); c = ff(c, d, a, b, w[10], 17, -42063); b = ff(b, c, d, a, w[11], 22, -1990404162); a = ff(a, b, c, d, w[12], 7, 1804603682); d = ff(d, a, b, c, w[13], 12, -40341101); c = ff(c, d, a, b, w[14], 17, -1502002290); b = ff(b, c, d, a, w[15], 22, 1236535329); a = gg(a, b, c, d, w[1], 5, -165796510); d = gg(d, a, b, c, w[6], 9, -1069501632); c = gg(c, d, a, b, w[11], 14, 643717713); b = gg(b, c, d, a, w[0], 20, -373897302); a = gg(a, b, c, d, w[5], 5, -701558691); d = gg(d, a, b, c, w[10], 9, 38016083); c = gg(c, d, a, b, w[15], 14, -660478335); b = gg(b, c, d, a, w[4], 20, -405537848); a = gg(a, b, c, d, w[9], 5, 568446438); d = gg(d, a, b, c, w[14], 9, -1019803690); c = gg(c, d, a, b, w[3], 14, -187363961); b = gg(b, c, d, a, w[8], 20, 1163531501); a = gg(a, b, c, d, w[13], 5, -1444681467); d = gg(d, a, b, c, w[2], 9, -51403784); c = gg(c, d, a, b, w[7], 14, 1735328473); b = gg(b, c, d, a, w[12], 20, -1926607734); a = hh(a, b, c, d, w[5], 4, -378558); d = hh(d, a, b, c, w[8], 11, -2022574463); c = hh(c, d, a, b, w[11], 16, 1839030562); b = hh(b, c, d, a, w[14], 23, -35309556); a = hh(a, b, c, d, w[1], 4, -1530992060); d = hh(d, a, b, c, w[4], 11, 1272893353); c = hh(c, d, a, b, w[7], 16, -155497632); b = hh(b, c, d, a, w[10], 23, -1094730640); a = hh(a, b, c, d, w[13], 4, 681279174); d = hh(d, a, b, c, w[0], 11, -358537222); c = hh(c, d, a, b, w[3], 16, -722521979); b = hh(b, c, d, a, w[6], 23, 76029189); a = hh(a, b, c, d, w[9], 4, -640364487); d = hh(d, a, b, c, w[12], 11, -421815835); c = hh(c, d, a, b, w[15], 16, 530742520); b = hh(b, c, d, a, w[2], 23, -995338651); a = ii(a, b, c, d, w[0], 6, -198630844); d = ii(d, a, b, c, w[7], 10, 1126891415); c = ii(c, d, a, b, w[14], 15, -1416354905); b = ii(b, c, d, a, w[5], 21, -57434055); a = ii(a, b, c, d, w[12], 6, 1700485571); d = ii(d, a, b, c, w[3], 10, -1894986606); c = ii(c, d, a, b, w[10], 15, -1051523); b = ii(b, c, d, a, w[1], 21, -2054922799); a = ii(a, b, c, d, w[8], 6, 1873313359); d = ii(d, a, b, c, w[15], 10, -30611744); c = ii(c, d, a, b, w[6], 15, -1560198380); b = ii(b, c, d, a, w[13], 21, 1309151649); a = ii(a, b, c, d, w[4], 6, -145523070); d = ii(d, a, b, c, w[11], 10, -1120210379); c = ii(c, d, a, b, w[2], 15, 718787259); b = ii(b, c, d, a, w[9], 21, -343485551); a = cm(a, oa); b = cm(b, ob); c = cm(c, oc); d = cm(d, od); } function hex(n) { let s = ""; for (let i = 0; i < 4; i++) s += ((n >> (i * 8)) & 0xff).toString(16).padStart(2, "0"); return s; } return hex(a) + hex(b) + hex(c) + hex(d); } function signLearnRecord(rec) { const ts = Date.now(); const raw = String(rec.clientCode ?? "") + String(rec.userId ?? "") + String(rec.coursewareCode ?? "") + String(rec.catalogId ?? "") + String(rec.accumulativeTime ?? "") + SALT + String(ts); return { md5: md5(raw), md5Timestamp: ts }; } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); function esc(s) { return String(s || "") .replace(/&/g, "&") .replace(//g, ">"); } function formatDuration(sec) { const s = Math.max(0, Math.floor(Number(sec) || 0)); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const ss = s % 60; if (h > 0) return `${h}\u65f6${String(m).padStart(2, "0")}\u5206`; return `${m}\u5206${String(ss).padStart(2, "0")}\u79d2`; } function getGapMs() { return 2000; } function getStepSec() { return 60; } const SCORE_PARTS = ["\u8bfe\u7a0b\u5b66\u4e60", "\u77e5\u8bc6\u70b9\u6d4b\u8bc4", "\u9636\u6bb5\u4f5c\u4e1a", "\u5b66\u4e60\u8868\u73b0", "\u7efc\u5408\u6d4b\u8bc4"]; const SCORE_WEIGHTS = { \u8bfe\u7a0b\u5b66\u4e60: 10, \u77e5\u8bc6\u70b9\u6d4b\u8bc4: 15, \u9636\u6bb5\u4f5c\u4e1a: 30, \u5b66\u4e60\u8868\u73b0: 15, \u7efc\u5408\u6d4b\u8bc4: 30, }; const SCORE_FULL = { \u8bfe\u7a0b\u5b66\u4e60: 10, \u77e5\u8bc6\u70b9\u6d4b\u8bc4: 15, \u9636\u6bb5\u4f5c\u4e1a: 30, \u5b66\u4e60\u8868\u73b0: 15, \u7efc\u5408\u6d4b\u8bc4: 30, }; function isScoreFull(score, full) { const s = parseFloat(score); const f = parseFloat(full); if (!Number.isFinite(s) || !Number.isFinite(f) || f <= 0) return false; return Math.round(s) >= Math.round(f); } function scoreRowOf(detail, name) { return ((detail && detail.rows) || []).find((r) => r.name === name) || null; } function isPartFull(detail, name) { const r = scoreRowOf(detail, name); if (!r) return false; if (isScoreFull(r.score, r.full)) return true; if (name === "\u8bfe\u7a0b\u5b66\u4e60" && detail && detail.studyNeed > 0 && detail.studyMin != null) { if (Math.round((detail.studyMin / detail.studyNeed) * 10) >= 10) return true; } return false; } function gmRequest(opt) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== "function") { reject(new Error("\u9700\u8981 GM_xmlhttpRequest")); return; } const req = { method: opt.method || "GET", url: opt.url, headers: opt.headers || {}, data: opt.data, responseType: opt.responseType || "text", withCredentials: opt.withCredentials !== false, timeout: opt.timeout || 30000, onload: resolve, onerror: () => reject(new Error("\u7f51\u7edc\u9519\u8bef " + (opt.url || ""))), ontimeout: () => reject(new Error("\u8d85\u65f6 " + (opt.url || ""))), }; if (opt.redirect) req.redirect = opt.redirect; GM_xmlhttpRequest(req); }); } function headerLocation(headers) { if (!headers) return ""; if (typeof headers === "object" && !Array.isArray(headers)) { for (const k of Object.keys(headers)) { if (/^location$/i.test(k)) return String(headers[k] || "").trim(); } } const m = String(headers).match(/(?:^|[\r\n])Location:\s*(\S+)/i); return m ? m[1].trim() : ""; } function extractRestartUrl(htmlOrUrl, base) { const s0 = String(htmlOrUrl || "").replace(/&/g, "&").trim(); if (!s0) return ""; if (/\/single\/restart\//i.test(s0) && /^https?:\/\//i.test(s0.split(/\s/)[0])) { try { const u = new URL(s0.split(/\s/)[0]); if (u.searchParams.get("m") && u.searchParams.get("d")) return u.href; } catch (_) {} } const m = s0.match(/https?:\/\/em\.edu-edu\.com\.cn\/exam-admin\/student\/exam\/single\/restart\/[^"'\\\s<>]+/i) || s0.match(/\/exam-admin\/student\/exam\/single\/restart\/[^"'\\\s<>]+/i); if (!m) return ""; try { const u = new URL(m[0], base || EM); if (!u.searchParams.get("m") || !u.searchParams.get("d")) return ""; return u.href; } catch (_) { return ""; } } function pickRestartFromHops(hops) { for (const u of hops || []) { const got = extractRestartUrl(u, EM); if (got) return got; } return ""; } async function gmGetStay(url, maxRedirects) { let cur = url; const hops = []; let last = { url: cur, text: "", status: 0 }; for (let i = 0; i < (maxRedirects || 8); i++) { if (hops.includes(cur)) break; hops.push(cur); let res = null; try { res = await gmRequest({ method: "GET", url: cur, redirect: "manual", timeout: 45000, headers: { Accept: "text/html,application/xhtml+xml", "Upgrade-Insecure-Requests": "1" }, }); } catch (_) { res = null; } if (!res || !res.status || res.status === 0) { res = await gmRequest({ method: "GET", url: cur, timeout: 60000, headers: { Accept: "text/html,application/xhtml+xml", "Upgrade-Insecure-Requests": "1" }, }); } const finalUrl = res.finalUrl || cur; if (finalUrl && finalUrl !== cur && !hops.includes(finalUrl)) hops.push(finalUrl); last = { url: finalUrl, text: res.responseText || "", status: res.status, res }; const loc = headerLocation(res.responseHeaders); if (loc && res.status >= 300 && res.status < 400) { try { cur = new URL(loc, cur).href; } catch (_) { cur = loc; } continue; } break; } return { url: last.url, text: last.text, status: last.status, hops, res: last.res }; } async function fetchSignedRestartUrl(sess, exam) { let restartUrl = ""; if (sess.restartUrl) { restartUrl = extractRestartUrl(sess.restartUrl, EM) || ""; } const tryAbsorb = (label, html, pageUrl, hops) => { const fromHops = pickRestartFromHops(hops); const fromHtml = extractRestartUrl(html, pageUrl || EM); const got = fromHtml || fromHops; if (got) { restartUrl = got; dbg(`${label} \u53d6\u5230\u7ee7\u7eed\u8003\u8bd5\u94fe\u63a5`); return true; } return false; }; try { const j2 = await postGetExamUrl(exam, "2"); if (j2 && j2.state && j2.msg) { const stay = await gmGetStay(j2.msg, 8); tryAbsorb("\u6210\u7ee9\u9875", stay.text, stay.url, stay.hops); let doresultUrl = ""; if (/\/view\/result\//.test(stay.url || "")) { doresultUrl = stay.url.replace("/view/result/", "/view/doresult/"); } else if (/\/view\/doresult\//.test(stay.url || "")) { doresultUrl = stay.url; } if (!doresultUrl) { const dm = String(stay.text || "").match( /\/exam-admin\/student\/exam\/single\/view\/doresult\/[^"'\\\s]+/i ); if (dm) { try { doresultUrl = new URL(dm[0].replace(/&/g, "&"), EM).href; } catch (_) {} } } if (!doresultUrl) { for (const u of stay.hops || []) { if (/\/view\/result\//.test(u)) { doresultUrl = u.replace("/view/result/", "/view/doresult/"); break; } if (/\/view\/doresult\//.test(u)) { doresultUrl = u; break; } } } if (doresultUrl && !restartUrl) { const dr = await gmGetStay(doresultUrl, 4); tryAbsorb("doresult", dr.text, dr.url, dr.hops); } else if (doresultUrl) { const dr = await gmGetStay(doresultUrl, 4); const fresh = extractRestartUrl(dr.text, dr.url) || pickRestartFromHops(dr.hops); if (fresh) { restartUrl = fresh; dbg("doresult \u5df2\u5237\u65b0\u7ee7\u7eed\u8003\u8bd5\u7b7e\u540d"); } } } } catch (e) { dbg(`mode=2 \u53d6\u7eed\u8003\u94fe\u63a5\uff1a${e.message || e}`); } if (!restartUrl) { try { const j1 = await postGetExamUrl(exam, "1"); if (j1 && j1.state && j1.msg) { const stay = await gmGetStay(j1.msg, 10); tryAbsorb("\u5f00\u8003\u6210\u7ee9\u9875", stay.text, stay.url, stay.hops); if (!restartUrl && /\/view\/result\//.test(stay.url || "")) { const dr = await gmGetStay(stay.url.replace("/view/result/", "/view/doresult/"), 4); tryAbsorb("\u5f00\u8003doresult", dr.text, dr.url, dr.hops); } if (!restartUrl) restartUrl = pickRestartFromHops(stay.hops); } } catch (e) { dbg(`mode=1 \u53d6\u7eed\u8003\u94fe\u63a5\uff1a${e.message || e}`); } } if (!restartUrl) { throw new Error("\u6210\u7ee9\u9875\u65e0\u5e26 m/d \u7b7e\u540d\u7684\u7ee7\u7eed\u8003\u8bd5\u94fe\u63a5"); } return restartUrl; } function doexamUrlFromStart(startUrl) { try { const u = new URL(startUrl, E14); const sm = u.pathname.match(/\/exam2\/start\/(\d+)\/(\d+)\/(\d+)\/([^/]+)\/(\d+)/); if (!sm) return ""; return `${u.origin}/exam/student/exam2/doexam/${sm[4]}/${sm[3]}/${sm[5]}`; } catch (_) { return ""; } } async function followRestartToDoexam(restartUrl) { const logHops = []; const getOnce = async (url, note) => { let res = null; let loc = ""; try { res = await gmRequest({ method: "GET", url, redirect: "manual", timeout: 45000, headers: { Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Upgrade-Insecure-Requests": "1", }, }); loc = headerLocation(res.responseHeaders); } catch (_) { res = null; } if (!res || !res.status || res.status === 0) { res = await gmRequest({ method: "GET", url, timeout: 60000, headers: { Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Upgrade-Insecure-Requests": "1", }, }); loc = headerLocation(res.responseHeaders); } const finalUrl = res.finalUrl || url; const text = res.responseText || ""; logHops.push(`${note}:${res.status}@${(finalUrl || url).slice(0, 90)}`); return { res, loc, finalUrl, text, status: res.status }; }; let cur = restartUrl; let r = await getOnce(cur, "restart"); let startUrl = ""; if (r.loc && /exam2\/start\//i.test(r.loc)) { startUrl = new URL(r.loc, cur).href; } else if (/exam2\/start\//i.test(r.finalUrl || "")) { startUrl = r.finalUrl; } else if (/ExamObj\s*\(/.test(r.text)) { return { url: r.finalUrl, text: r.text, hops: logHops }; } else { const m = (r.text || "").match(/https?:\/\/e14\.edu-edu\.com\.cn\/exam\/student\/exam2\/start\/[^\s"'<>]+/i); if (m) startUrl = m[0].replace(/&/g, "&"); } if (!startUrl && r.loc) { try { startUrl = new URL(r.loc, cur).href; } catch (_) {} } if (!startUrl) { const auto = await gmRequest({ method: "GET", url: restartUrl, timeout: 60000, headers: { Accept: "text/html,application/xhtml+xml", "Upgrade-Insecure-Requests": "1" }, }); logHops.push(`restart-auto:${auto.status}@${(auto.finalUrl || "").slice(0, 90)}`); if (/ExamObj\s*\(/.test(auto.responseText || "")) { return { url: auto.finalUrl || restartUrl, text: auto.responseText || "", hops: logHops }; } if (/exam2\/start\//i.test(auto.finalUrl || "")) startUrl = auto.finalUrl; const m2 = String(auto.responseText || "").match( /https?:\/\/e14\.edu-edu\.com\.cn\/exam\/student\/exam2\/start\/[^\s"'<>]+/i ); if (!startUrl && m2) startUrl = m2[0].replace(/&/g, "&"); } if (!startUrl) { dbg(`\u7eed\u8003\u8df3\u8f6c\u5931\u8d25 hops=${logHops.join(" | ")} · body=${(r.text || "").slice(0, 100)}`); throw new Error("restart \u672a\u8fd4\u56de exam2/start\uff08\u68c0\u67e5 m/d \u7b7e\u540d\uff09"); } r = await getOnce(startUrl, "start"); let doexamUrl = ""; if (r.loc && /exam2\/doexam\//i.test(r.loc)) { doexamUrl = new URL(r.loc, r.finalUrl || startUrl || E14).href; } else if (/exam2\/doexam\//i.test(r.finalUrl || "")) { doexamUrl = r.finalUrl; } else if (/ExamObj\s*\(/.test(r.text)) { return { url: r.finalUrl || startUrl, text: r.text, hops: logHops }; } else { doexamUrl = doexamUrlFromStart(startUrl); } if (!doexamUrl) { dbg(`start \u540e\u65e0 doexam hops=${logHops.join(" | ")}`); throw new Error("exam2/start \u672a\u8f6c\u5230 doexam"); } r = await getOnce(doexamUrl, "doexam"); if (!/ExamObj\s*\(/.test(r.text || "")) { r = await getOnce(doexamUrl, "doexam2"); } if (!/ExamObj\s*\(/.test(r.text || "")) { dbg(`\u8fdb\u8003\u573a\u5931\u8d25 hops=${logHops.join(" | ")} · len=${(r.text || "").length}`); throw new Error("\u7eed\u8003\u672a\u8fdb\u5165\u8003\u573a\uff08\u4f1a\u8bdd key \u672a\u5efa\u7acb\uff09"); } return { url: r.finalUrl || doexamUrl, text: r.text, hops: logHops, startUrl }; } function pickClientCode(...texts) { for (const t of texts) { if (!t) continue; const m = String(t).match(/clientCode=([0-9]{3,12})/i); if (m) return m[1]; const m2 = String(t).match(/["']clientCode["']\s*[:=]\s*["']?([0-9]{3,12})/); if (m2) return m2[1]; } try { return localStorage.getItem(CLIENT_CODE_KEY) || ""; } catch (_) { return ""; } } function rememberClientCode(code) { if (code) { try { localStorage.setItem(CLIENT_CODE_KEY, String(code)); } catch (_) {} } } async function resolveLearnParams(learnNewUrl, course, meta) { const out = { clientCode: "", userId: meta.userid || meta.userId || "", userName: meta.uname || meta.userName || "", coursewareCode: meta.courseCode || course.courseCode || "", businessLineCode: "", courseCodeN: "", catalogId: "", }; const absorbUrl = (url) => { if (!url) return; const q = parseQuery(url); if (q.clientCode) out.clientCode = q.clientCode; if (q.userId) out.userId = q.userId; if (q.userName) out.userName = q.userName; if (q.coursewareCode) out.coursewareCode = q.coursewareCode; if (q.businessLineCode != null) out.businessLineCode = q.businessLineCode; if (q.courseCodeN != null) out.courseCodeN = q.courseCodeN; if (q.catalogId != null) out.catalogId = q.catalogId; }; const absorbText = (text) => { if (!text) return; const m = String(text).match(/https?:\/\/[^\s"'<>]*\/client\/user\/learn\?[^\s"'<>]+/i) || String(text).match(/\/client\/user\/learn\?[^\s"'<>]+/i); if (m) absorbUrl(m[0].replace(/&/g, "&")); const cc = pickClientCode(text); if (cc) out.clientCode = cc; }; try { let url = learnNewUrl; for (let hopi = 0; hopi < 8; hopi++) { const hop = await gmRequest({ method: "GET", url, redirect: "manual", timeout: 45000, }); const loc = headerLocation(hop.responseHeaders); absorbUrl(url); absorbUrl(hop.finalUrl); absorbText(hop.responseText); absorbText(hop.responseHeaders); if (hopi === 0) { dbg( `getLearnNew manual status=${hop.status} loc=${loc ? "\u6709" : "\u65e0"} final=${(hop.finalUrl || url || "").slice(0, 80)}` ); } if (loc && hop.status >= 300 && hop.status < 400) { try { url = new URL(loc, url).href; } catch (_) { url = loc; } absorbUrl(url); if (/client\/user\/learn/i.test(url) && /clientCode=/i.test(url)) break; continue; } if (!hop.status || hop.status === 0) break; break; } } catch (e) { dbg(`manual \u8df3\u8f6c\u63a2\u6d4b\u5931\u8d25\uff1a${e.message || e}`); } try { const hop2 = await gmRequest({ method: "GET", url: learnNewUrl, timeout: 45000 }); absorbUrl(hop2.finalUrl); absorbText(hop2.responseText); absorbText(hop2.responseHeaders); const loc2 = headerLocation(hop2.responseHeaders); if (loc2) absorbUrl(loc2); dbg(`getLearnNew follow status=${hop2.status} final=${(hop2.finalUrl || "").slice(0, 100)}`); } catch (e) { dbg(`follow getLearnNew \u5931\u8d25\uff1a${e.message || e}`); } if (!out.clientCode) out.clientCode = pickClientCode(); if (out.clientCode) rememberClientCode(out.clientCode); if (!out.clientCode && /lzugk/i.test(location.hostname + (meta.uname || ""))) { out.clientCode = "657657"; rememberClientCode(out.clientCode); dbg("\u4f7f\u7528 lzugk \u9ed8\u8ba4 clientCode=657657\uff08\u82e5\u4e0d\u5bf9\u8bf7\u624b\u52a8\u8fdb\u4e00\u6b21\u64ad\u653e\u9875\u4ee5\u7ea0\u6b63\u7f13\u5b58\uff09"); } if (!out.userId && meta.userid) out.userId = meta.userid; if (!out.userName && meta.uname) out.userName = meta.uname; if (!out.coursewareCode) out.coursewareCode = course.courseCode; return out; } function parseQuery(url) { const q = {}; try { const u = new URL(url, location.href); u.searchParams.forEach((v, k) => { q[k] = v; }); } catch (_) { const m = String(url).split("?")[1] || ""; m.split("&").forEach((p) => { const [k, v] = p.split("="); if (k) q[decodeURIComponent(k)] = decodeURIComponent(v || ""); }); } return q; } async function gmJson(opt) { const res = await gmRequest(opt); const text = res.responseText || ""; let j = null; try { j = text ? JSON.parse(text) : null; } catch (_) { j = null; } return { res, text, j, finalUrl: res.finalUrl || opt.url, status: res.status }; } const state = { running: false, logLines: [], courses: [], chapters: [], session: null, activeCourse: null, sofar: 0, lastTime: 0, videoTime: 0, title: "", catalogTitle: "", tick: 0, stall: 0, learnRecord: null, started: false, patched: false, origInsert: null, vm: null, cloudToken: "", cloudLease: "", cloudLeaseExp: 0, cloudTier: "free", cloudProExpireAt: 0, tokenBoundBlocked: false, freeVideoLimit: 3, freeUsedVideos: 0, proBuyUrl: PRO_BUY_URL, panelNotice: "\u514d\u8d39\u53ef\u5b66 3 \u4e2a\u89c6\u9891\u5c0f\u8282\uff1b\u9636\u6bb5\u4f5c\u4e1a\u4e0e\u7efc\u5408\u6d4b\u8bc4\u9700\u5f00\u901a\u4f1a\u5458\u3002\u4f1a\u5458\u5f00\u901a\u540e 30 \u5929\u5185\u4e0d\u9650\u3002", lastChapterFail: "", learningUserId: "", }; function dbg(msg) { try { console.log(`[EDUXL] ${msg}`); } catch (_) {} } function trace(msg) { const s = String(msg || "").trim(); if (!s) return; try { console.log(`[EDUXL] ${s}`); } catch (_) {} state.logLines.unshift(`${new Date().toLocaleTimeString()} ${s}`); state.logLines = state.logLines.slice(0, 60); renderRunLog(); } function renderRunLog() { const box = document.getElementById("eduxl-run-log"); if (!box) return; if (!state.logLines.length) { box.innerHTML = `
\u6682\u65e0\u8fd0\u884c\u65e5\u5fd7
`; return; } box.innerHTML = state.logLines .map((l) => { const err = /\u5931\u8d25|\u5f02\u5e38|\u9519\u8bef|\u672a\u901a\u8fc7|\u53d6\u6d88/.test(l); const warn = /\u8df3\u8fc7|\u672a\u6ee1|\u5efa\u8bae|\u8b66\u544a/.test(l); const cls = err ? "eduxl-log-err" : warn ? "eduxl-log-warn" : ""; return `
${esc(l)}
`; }) .join(""); } function switchTab(tab) { const panel = document.getElementById(PANEL_ID); if (!panel) return; const name = tab || "course"; localStorage.setItem(LS.tab, name); panel.querySelectorAll(".eduxl-tab-btn").forEach((b) => { b.classList.toggle("active", b.getAttribute("data-tab") === name); }); panel.querySelectorAll(".eduxl-pane").forEach((p) => { p.classList.toggle("active", p.getAttribute("data-pane") === name); }); } function scoreBarPct(score, full) { const s = parseFloat(score); const f = parseFloat(full); if (!Number.isFinite(s) || !Number.isFinite(f) || f <= 0) return 0; return Math.max(0, Math.min(100, (s / f) * 100)); } function shortPartName(name) { return ( { 课程学习: "\u5b66\u65f6", 知识点测评: "\u77e5\u8bc6\u70b9", 阶段作业: "\u4f5c\u4e1a", 学习表现: "\u8868\u73b0", 综合测评: "\u7efc\u5408", }[name] || name ); } function gmGet(key, def) { try { if (typeof GM_getValue === "function") return GM_getValue(key, def); } catch (_) {} try { const raw = localStorage.getItem(key); if (raw == null) return def; return JSON.parse(raw); } catch (_) { return def; } } function gmSet(key, val) { try { if (typeof GM_setValue === "function") { GM_setValue(key, val); return; } } catch (_) {} try { localStorage.setItem(key, JSON.stringify(val)); } catch (_) {} } function getCloudSite() { const locked = String(gmGet(QUOTA_SITE_KEY, "") || "").trim(); if (locked) return locked; const host = String(location.hostname || "") .toLowerCase() .replace(/^www\./, ""); if (host) gmSet(QUOTA_SITE_KEY, host); return host || "edu-xl.com"; } function isAnonLearningId(id) { return /^anon_/i.test(String(id || "").trim()); } function getLearningUserId() { const locked = String(gmGet(QUOTA_UID_KEY, "") || "").trim(); if (locked) { state.learningUserId = locked; return locked; } let portal = ""; try { const examDate = (state.activeCourse && state.activeCourse.examDate) || (state.courses[0] && state.courses[0].examDate) || ""; portal = String( (state.session && state.session.userId) || pickPortalUserId(examDate) || "" ).trim(); } catch (_) { portal = String((state.session && state.session.userId) || "").trim(); } const stored = String(gmGet(LEARNING_UID_KEY, "") || state.learningUserId || "").trim(); let id = portal || (!isAnonLearningId(stored) ? stored : "") || stored; if (!id) { id = "anon_" + Math.random().toString(36).slice(2, 10) + Date.now().toString(36); } if (portal) id = portal; state.learningUserId = id; gmSet(LEARNING_UID_KEY, id); return id; } function lockQuotaIdentity(opts) { const force = !!(opts && opts.force); const id = getLearningUserId(); getCloudSite(); if (!id) return; if (!force && isAnonLearningId(id)) return; if (!String(gmGet(QUOTA_UID_KEY, "") || "").trim()) { gmSet(QUOTA_UID_KEY, id); } } function getCloudToken() { const t = String(gmGet(CLOUD_TOKEN_KEY, "") || state.cloudToken || "").trim(); state.cloudToken = t; return t; } function setCloudToken(token) { const t = String(token || "").trim(); state.cloudToken = t; gmSet(CLOUD_TOKEN_KEY, t); } function isProUser() { return String(state.cloudTier || "").toLowerCase() === "pro" && !state.tokenBoundBlocked; } function canDoExamWork() { return isProUser(); } function denyExamWorkMsg(label) { return `\u300c${label || "\u4f5c\u4e1a\u6d4b\u8bc4"}\u300d\u9700\u8981\u5f00\u901a\u4f1a\u5458\u540e\u624d\u80fd\u505a\uff0c\u514d\u8d39\u8bd5\u7528\u53ea\u542b\u89c6\u9891\u5c0f\u8282`; } function formatProExpireAt(ts) { const n = Number(ts) || 0; if (!n) return ""; const ms = n > 1e12 ? n : n * 1000; const d = new Date(ms); if (Number.isNaN(d.getTime())) return ""; return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } function cloudGmRequest(url, method, headers, body) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== "function") { reject(new Error("\u9700\u8981 GM_xmlhttpRequest")); return; } GM_xmlhttpRequest({ method: method || "GET", url, headers: headers || {}, data: body == null ? undefined : body, timeout: 45000, onload(res) { resolve({ status: res.status, text: res.responseText || "" }); }, onerror: () => reject(new Error("\u7f51\u7edc\u4e0d\u592a\u7a33\uff0c\u7a0d\u540e\u518d\u8bd5")), ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5")), }); }); } async function cloudRequest(path, method, payload) { const url = `${CLOUD_API_BASE}${path.startsWith("/") ? path : "/" + path}`; const isGet = String(method || "POST").toUpperCase() === "GET"; const headers = { Accept: "application/json, text/plain, */*", ...(isGet ? {} : { "Content-Type": "application/json" }), }; const luid = getLearningUserId(); if (luid) headers["x-learning-user-id"] = luid; const tk = getCloudToken(); if (tk) headers.Authorization = `Bearer ${tk}`; let bodyObj = payload; if (bodyObj && typeof bodyObj === "object") { bodyObj = Object.assign({}, bodyObj); if (state.cloudLease && bodyObj.lease == null && !/\/lease(\?|$)/.test(path)) { bodyObj.lease = state.cloudLease; } } const body = isGet || bodyObj == null ? null : JSON.stringify(bodyObj); const res = await cloudGmRequest(url, isGet ? "GET" : method || "POST", headers, body); let data = {}; try { data = res.text ? JSON.parse(res.text) : {}; } catch (_) { data = {}; } if (res.status < 200 || res.status >= 300) { const detail = data.detail; if (detail && typeof detail === "object") { applyCloudLeaseData(detail); const err = new Error(String(detail.message || detail.code || "cloud_error")); err.code = String(detail.code || detail.message || ""); err.detail = detail; throw err; } const raw = String(data.detail || data.message || data.code || `http_${res.status}`); const err = new Error(raw); err.code = String(data.code || data.message || raw || ""); if (/free_quota/i.test(raw)) err.code = "free_quota_exhausted"; if (/^revoked$/i.test(raw)) err.code = "revoked"; if (/^expired$/i.test(raw)) err.code = "expired"; if (/expired_after_activation/i.test(raw)) err.code = "expired_after_activation"; if (/invalid token/i.test(raw)) err.code = "invalid token"; throw err; } return data; } function cacheCloudLease() { gmSet(CLOUD_LEASE_KEY, { lease: state.cloudLease, exp: state.cloudLeaseExp, tier: state.cloudTier, freeVideoLimit: state.freeVideoLimit, freeUsedVideos: state.freeUsedVideos, proExpireAt: state.cloudProExpireAt, tokenBoundBlocked: state.tokenBoundBlocked, }); } function applyCloudLeaseData(data) { if (!data || typeof data !== "object") return; if (data.lease) state.cloudLease = String(data.lease); if (data.lease_exp != null || data.exp != null) { state.cloudLeaseExp = Number(data.lease_exp ?? data.exp) || 0; } if (data.tier) state.cloudTier = String(data.tier); if (data.free_video_limit != null || data.freeVideoLimit != null) { state.freeVideoLimit = Number(data.free_video_limit ?? data.freeVideoLimit) || 3; } if (data.free_used_videos != null || data.freeUsedVideos != null) { state.freeUsedVideos = Number(data.free_used_videos ?? data.freeUsedVideos) || 0; } if (data.pro_expires_at != null || data.proExpireAt != null) { state.cloudProExpireAt = Number(data.pro_expires_at ?? data.proExpireAt) || 0; } if (data.token_bound_blocked != null) state.tokenBoundBlocked = !!data.token_bound_blocked; if (data.proBuyUrl) state.proBuyUrl = String(data.proBuyUrl).trim() || PRO_BUY_URL; if (data.panelNotice) state.panelNotice = String(data.panelNotice); if (state.freeUsedVideos > 0) lockQuotaIdentity({ force: true }); else lockQuotaIdentity(); cacheCloudLease(); updateCloudPanelUI(); } function isQuotaError(e) { const msg = String((e && e.message) || e || ""); const code = String((e && e.code) || ""); return code === "free_quota_exhausted" || /free_quota_exhausted|free_quota|\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c/i.test(msg); } function isTokenAuthError(e) { const msg = String((e && e.message) || e || ""); const code = String((e && e.code) || ""); return ( /^(revoked|expired|expired_after_activation|invalid token)$/i.test(code) || /revoked|expired_after_activation|\binvalid token\b|\u5df2\u7981\u7528|\u5df2\u8fc7\u671f|\u65e0\u6548.?token/i.test(msg) ); } function friendlyCloudMessage(raw) { const msg = String((raw && raw.message) || raw || "").trim(); if (isQuotaError(raw) || /free_quota|\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c/i.test(msg)) { return `\u514d\u8d39\u6b21\u6570\u7528\u5b8c\u4e86\uff08${state.freeUsedVideos}/${state.freeVideoLimit}\uff09\uff0c\u5f00\u901a\u4f1a\u5458\u540e\u53ef\u7ee7\u7eed\u5b66\u3002`; } if (/token.?bound|\u5df2\u7ed1\u5b9a\u5176\u4ed6/i.test(msg)) return "\u8fd9\u5f20\u4f1a\u5458\u5361\u5df2\u5728\u522b\u7684\u8d26\u53f7\u7528\u8fc7\uff0c\u8fd9\u8fb9\u4ecd\u6309\u514d\u8d39\u8bd5\u7528\u3002"; if (/revoked|\u5df2\u7981\u7528/i.test(msg)) return "\u8fd9\u5f20\u4f1a\u5458\u5361\u4e0d\u53ef\u7528\u4e86\uff0c\u8bf7\u6362\u4e00\u5f20\u6216\u91cd\u65b0\u5f00\u901a\u3002"; if (/expired_after_activation/i.test(msg)) return "\u4f1a\u5458\u5df2\u5230\u671f\uff08\u81ea\u5f00\u901a\u8d77 30 \u5929\uff09\uff0c\u8bf7\u91cd\u65b0\u5f00\u901a\u3002"; if (/expired|\u5df2\u8fc7\u671f/i.test(msg)) return "\u4f1a\u5458\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u5f00\u901a\u3002"; if (/invalid token/i.test(msg)) return "\u4f1a\u5458\u5361\u5bc6\u4e0d\u5bf9\uff0c\u8bf7\u68c0\u67e5\u540e\u91cd\u65b0\u7c98\u8d34\u3002"; if (/cloud_unavailable|\u6682\u65f6\u65e0\u6cd5\u786e\u8ba4/i.test(msg)) return "\u6682\u65f6\u8fde\u4e0d\u4e0a\u6388\u6743\u670d\u52a1\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5\u3002"; return "\u6682\u65f6\u6ca1\u6cd5\u7ee7\u7eed\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5\u3002"; } async function ensureCloudLease(force) { const now = Math.floor(Date.now() / 1000); const hasToken = !!getCloudToken(); if (hasToken && String(state.cloudTier || "").toLowerCase() !== "pro") force = true; if (!force && state.cloudLease && state.cloudLeaseExp - now > 60) return true; if (!force) { const cached = gmGet(CLOUD_LEASE_KEY, null); if (cached && Number(cached.exp) - now > 60) { const cachedTier = String(cached.tier || "").toLowerCase(); if (!(hasToken && cachedTier !== "pro")) { state.cloudLease = cached.lease || ""; state.cloudLeaseExp = Number(cached.exp) || 0; state.cloudTier = String(cached.tier || "free"); if (cached.freeVideoLimit != null) state.freeVideoLimit = Number(cached.freeVideoLimit) || 3; if (cached.freeUsedVideos != null) state.freeUsedVideos = Number(cached.freeUsedVideos) || 0; if (cached.proExpireAt != null) state.cloudProExpireAt = Number(cached.proExpireAt) || 0; if (cached.tokenBoundBlocked != null) state.tokenBoundBlocked = !!cached.tokenBoundBlocked; updateCloudPanelUI(); return !!state.cloudLease; } } } const data = await cloudRequest("/api/eduxl/lease", "POST", { token: getCloudToken(), learning_user_id: getLearningUserId(), userid: getLearningUserId(), site: getCloudSite(), site_name: "\u6210\u6559\u4e91\u5b66\u8d77", }); applyCloudLeaseData(data); if (data.token_bound_blocked || data.tokenBoundBlocked) { state.tokenBoundBlocked = true; state.cloudTier = "free"; updateCloudPanelUI(); dbg("Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\uff0c\u672c\u8d26\u53f7\u6309\u514d\u8d39\u4f53\u9a8c"); } return !!state.cloudLease; } async function cloudConsumeChapter(chapter) { await ensureCloudLease(!!getCloudToken()); if (isProUser()) return { tier: "pro" }; if (!state.cloudLease) { const err = new Error("\u6682\u65f6\u65e0\u6cd5\u786e\u8ba4\u5b66\u4e60\u6b21\u6570\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5"); err.code = "cloud_unavailable"; throw err; } const payload = { cs_id: String((chapter && chapter.catalogId) || ""), res_id: String((chapter && chapter.catalogId) || ""), hour_id: String((chapter && chapter.catalogId) || ""), course_id: String((state.activeCourse && state.activeCourse.courseCode) || ""), site: getCloudSite(), }; try { const data = await cloudRequest("/api/eduxl/video/consume", "POST", payload); applyCloudLeaseData(data); lockQuotaIdentity({ force: true }); return data; } catch (e) { if (isQuotaError(e) && getCloudToken()) { await ensureCloudLease(true); if (isProUser()) { const data = await cloudRequest("/api/eduxl/video/consume", "POST", payload); applyCloudLeaseData(data); lockQuotaIdentity({ force: true }); return data; } } if (isQuotaError(e)) lockQuotaIdentity({ force: true }); throw e; } } function updateCloudPanelUI() { const tierEl = document.getElementById("eduxl-pro-tier"); const quotaEl = document.getElementById("eduxl-pro-quota"); const tokenEl = document.getElementById("eduxl-pro-token"); const noticeEl = document.getElementById("eduxl-pro-notice"); const pro = isProUser(); if (tierEl) { if (pro) { const exp = formatProExpireAt(state.cloudProExpireAt); tierEl.textContent = exp ? `\u4f1a\u5458 · \u81f3 ${exp}` : "\u4f1a\u5458"; tierEl.className = "eduxl-pro-badge is-pro"; } else { tierEl.textContent = state.tokenBoundBlocked ? "\u514d\u8d39\u8bd5\u7528" : "\u514d\u8d39\u8bd5\u7528"; tierEl.className = "eduxl-pro-badge"; } } if (quotaEl) { quotaEl.textContent = pro ? "\u4e0d\u9650\u6b21\u6570" : `\u5df2\u7528 ${state.freeUsedVideos}/${state.freeVideoLimit} \u5c0f\u8282`; } if (tokenEl && document.activeElement !== tokenEl) { tokenEl.value = getCloudToken(); } if (noticeEl) { let n = state.panelNotice || ""; n = n .replace(/oa\d+/gi, "") .replace(/\u4e91\u7aef/g, "") .replace(/Token/gi, "\u5361\u5bc6") .replace(/\bPro\b/g, "\u4f1a\u5458") .replace(/\s{2,}/g, " ") .trim(); noticeEl.textContent = n || "\u514d\u8d39\u53ef\u5b66 3 \u4e2a\u89c6\u9891\u5c0f\u8282\uff1b\u5f00\u901a\u4f1a\u5458\u540e 30 \u5929\u5185\u4e0d\u9650\u6b21\u6570\u3002"; } updateFaceStatusUi(); } async function refreshCloudClientConfig() { try { const data = await cloudRequest("/api/eduxl/client-config", "GET"); if (data.freeVideoLimit != null || data.free_video_limit != null) { state.freeVideoLimit = Number(data.freeVideoLimit ?? data.free_video_limit) || 3; } if (data.proBuyUrl) state.proBuyUrl = String(data.proBuyUrl).trim() || PRO_BUY_URL; if (data.panelNotice) state.panelNotice = String(data.panelNotice); updateCloudPanelUI(); } catch (_) {} } async function initCloudAuth() { getCloudToken(); if (!(state.session && state.session.userId) && !(state.courses && state.courses.length)) { await sleep(800); } getLearningUserId(); await refreshCloudClientConfig(); try { await ensureCloudLease(true); } catch (e) { dbg(`\u4e91\u7aef\u6388\u6743\uff1a${friendlyCloudMessage(e)}`); } updateCloudPanelUI(); } function openProBuy() { const url = state.proBuyUrl || PRO_BUY_URL; try { if (typeof GM_openInTab === "function") GM_openInTab(url, { active: true }); else window.open(url, "_blank"); } catch (_) { window.open(url, "_blank"); } } function parseCoursesFromHtml(html, pageUrl) { const list = []; const re2 = /kjxx_click2\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*\)/g; let m; while ((m = re2.exec(html))) { list.push({ courseGUID: m[1], stemcode: m[2], timeStart: m[3], timeEnd: m[4], proceduralTypeID: m[5], examDate: m[6], courseId: m[7], msgTitle: m[8], courseCode: m[9], title: m[10] || m[9], studentCourseID: m[11], classNum: m[11], needMin: 0, learnedMin: 0, pageUrl: pageUrl || "", }); } if (!list.length) { const re = /kjxx_click\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*\)/g; while ((m = re.exec(html))) { const pre = html.slice(Math.max(0, m.index - 1200), m.index); let title = ""; const h3 = pre.match(/]*class=["'][^"']*course-title[^"']*["'][^>]*>\s*([^<\s][^<]{0,60})/i); if (h3) title = h3[1].trim(); if (!title) { const all = pre.match(/[\u4e00-\u9fff][\u4e00-\u9fffA-Za-z0-9\uff08\uff09()\-·]{3,39}/g); if (all && all.length) title = all[all.length - 1]; } list.push({ courseGUID: m[1], courseCode: m[2], stemcode: m[3], proceduralTypeID: m[4], examDate: m[5], title: title || m[2], needMin: 0, learnedMin: 0, pageUrl: pageUrl || "", }); } } const seen = new Set(); return list.filter((c) => { const k = c.courseGUID + "|" + c.courseCode; if (seen.has(k)) return false; seen.add(k); return true; }); } function collectExCourseUrlsFromHtml(html) { const urls = new Set(); const re = /\/StudentZkSite\/StudentZkSiteKC\/StudentZkSiteExCourseIndex\?[^"'<\s]+/gi; let m; while ((m = re.exec(html))) { urls.add(m[0].replace(/&/g, "&")); } const re2 = /courseGUID=([a-f0-9]{32})/gi; while ((m = re2.exec(html))) { const guid = m[1]; const nearby = html.slice(m.index, m.index + 200); const codeM = nearby.match(/courseCode=([a-zA-Z0-9_]+)/); const uidM = html.slice(Math.max(0, m.index - 100), m.index + 300).match(/userID=([^&"'<\s]+)/); let u = `/StudentZkSite/StudentZkSiteKC/StudentZkSiteExCourseIndex?courseGUID=${guid}` + (uidM ? `&userID=${uidM[1]}` : "") + `&ProceduralTypeID=1` + (codeM ? `&courseCode=${codeM[1]}` : ""); urls.add(u); } return [...urls]; } async function sameOriginGet(pathOrUrl) { const url = pathOrUrl.startsWith("http") ? pathOrUrl : location.origin + pathOrUrl; const r = await fetch(url, { credentials: "include" }); return { url: r.url, html: await r.text() }; } async function refreshPortalCourses() { dbg("\u6b63\u5728\u62c9\u53d6\u8bfe\u7a0b\u5217\u8868…"); const found = []; const tryPages = []; tryPages.push({ url: location.href, html: document.documentElement.innerHTML }); try { document.querySelectorAll("iframe").forEach((fr) => { try { const doc = fr.contentDocument; if (doc && doc.documentElement) { tryPages.push({ url: fr.src || location.href + "#iframe", html: doc.documentElement.innerHTML, }); } } catch (_) {} }); } catch (_) {} try { const kc = await sameOriginGet("/StudentZkSite/StudentZkSiteKC/StudentZkSiteKCNewIndex"); tryPages.push(kc); const n2 = (kc.html.match(/kjxx_click2\(/g) || []).length; const n1 = (kc.html.match(/kjxx_click\(/g) || []).length; dbg(`KCNewIndex \u5df2\u53d6\u5230 · kjxx_click2=${n2} kjxx_click=${n1}`); collectExCourseUrlsFromHtml(kc.html).forEach((u) => tryPages.push({ url: u, html: null })); } catch (e) { dbg(`KCNewIndex \u5931\u8d25\uff1a${e.message || e}`); } document.querySelectorAll("a[href*='ExCourseIndex'],a[href*='courseGUID']").forEach((a) => { const href = a.getAttribute("href"); if (href) tryPages.push({ url: href, html: null }); }); const queue = [...tryPages]; const seenUrl = new Set(); while (queue.length) { const item = queue.shift(); let html = item.html; let url = item.url; if (!html) { const abs = url.startsWith("http") ? url : location.origin + (url.startsWith("/") ? url : "/" + url); if (seenUrl.has(abs)) continue; seenUrl.add(abs); try { const got = await sameOriginGet(abs); html = got.html; url = got.url; } catch (e) { continue; } } else { if (seenUrl.has(url)) continue; seenUrl.add(url); } const courses = parseCoursesFromHtml(html, url); courses.forEach((c) => found.push(c)); collectExCourseUrlsFromHtml(html).forEach((u) => { const abs = u.startsWith("http") ? u : location.origin + u; if (!seenUrl.has(abs)) queue.push({ url: abs, html: null }); }); if (found.length > 50) break; } const map = new Map(); found.forEach((c) => map.set(c.courseGUID + "|" + c.courseCode, c)); state.courses = [...map.values()]; state.chapters = []; state.activeCourse = null; state.session = null; dbg(state.courses.length ? `\u8bfe\u7a0b ${state.courses.length} \u95e8` : "\u672a\u627e\u5230\u8bfe\u7a0b\uff1a\u8bf7\u5148\u6253\u5f00\u5de6\u4fa7\u300c\u8fc7\u7a0b\u6027/\u8bfe\u4ef6\u5b66\u4e60\u300d\u518d\u70b9\u5237\u65b0"); if (state.courses.length) trace(`\u5df2\u52a0\u8f7d ${state.courses.length} \u95e8\u8bfe\uff0c\u52fe\u9009\u540e\u70b9\u5f00\u59cb\u5373\u53ef`); else trace("\u8fd8\u6ca1\u62c9\u5230\u8bfe\u7a0b\uff0c\u8bf7\u786e\u8ba4\u5df2\u6253\u5f00\u5b66\u4e60\u9875\u9762"); renderCourseList(); renderChapterList(); updatePanel(); if (state.courses.length) { loadScoreDetailsForCourses(state.courses).catch((e) => dbg(`\u6210\u7ee9\u62c9\u53d6\u5f02\u5e38\uff1a${e.message || e}`)); } return state.courses; } function parseScoreDetailHtml(html) { const detail = { total: null, rows: [], studyMin: null, studyNeed: null }; const totalHidden = html.match(/id=["']setTotalValue["']\s+value=['"]([^'"]+)['"]/i); if (totalHidden) { const t = String(totalHidden[1]).trim(); if (t && t !== "-1") detail.total = t; } if (detail.total == null) { const t2 = html.match(/id=["']lblTotalScore["'][^>]*>\s*([^<]+)\s*/i) || [])[0] || (html.match(/\u53c2\u4e0e\u8ba1\u5206\u7684\u6a21\u5757[\s\S]*?<\/table>/i) || [])[0] || ""; const rulesSrc = rulesChunk || html; const rulesRe = /(\u8bfe\u7a0b\u5b66\u4e60|\u77e5\u8bc6\u70b9\u6d4b\u8bc4|\u9636\u6bb5\u4f5c\u4e1a|\u5b66\u4e60\u8868\u73b0|\u7efc\u5408\u6d4b\u8bc4)\s*<\/td>\s*([\s\S]*?)<\/td>/gi; let rm; while ((rm = rulesRe.exec(rulesSrc))) { const pctM = String(rm[2]).match(/(\d+)\s*%/); if (pctM) pctMap[rm[1]] = parseInt(pctM[1], 10); } const timeM = html.match( /class=["']time["'][^>]*>\s*([^<]+)\s*<\/em>\s*\/\s*]*class=["']all["'][^>]*>\s*([^<]+)/i ); if (timeM) { detail.studyMin = parseInt(String(timeM[1]).trim(), 10) || 0; detail.studyNeed = parseInt(String(timeM[2]).trim(), 10) || 0; } let kpOk = null; let kpTotal = null; const kpM = html.match(/\u77e5\u8bc6\u70b9\u6d4b\u8bc4\u9898\u603b\u6570\s*[:\uff1a]?\s*(\d+)\s*[,\uff0c]\s*\u7b54\u5bf9\s*[:\uff1a]?\s*(\d+)/i); if (kpM) { kpTotal = parseInt(kpM[1], 10); kpOk = parseInt(kpM[2], 10); } function blockFor(name) { const re = new RegExp( "class=[\"']titleSec[\"'][^>]*>\\s*" + name + "\\s*<[\\s\\S]*?(?=class=[\"']titleSec[\"']|$)", "i" ); const m = html.match(re); return m ? m[0] : ""; } const names = SCORE_PARTS.filter((n) => pctMap[n] != null); const useNames = names.length ? names : SCORE_PARTS.slice(); useNames.forEach((name) => { const chunk = blockFor(name); let full = null; let score = null; if (chunk) { const fullM = chunk.match(/\u6ee1\u5206\s*[\uff1a:]\s*([\d.]+)/); const scoreM = chunk.match(/\u5f97\u5206\s*[\uff1a:]\s*([\d.]+|\u672a\u53c2\u52a0)/); if (fullM) full = fullM[1]; if (scoreM) score = scoreM[1] === "\u672a\u53c2\u52a0" ? "0" : scoreM[1]; } if (full == null && pctMap[name] != null) full = String(pctMap[name]); if (full == null && SCORE_FULL[name] != null) full = String(SCORE_FULL[name]); let progress = ""; if (name === "\u8bfe\u7a0b\u5b66\u4e60" && detail.studyMin != null && detail.studyNeed != null) { progress = `${detail.studyMin}/${detail.studyNeed}\u5206\u949f`; } else if (name === "\u77e5\u8bc6\u70b9\u6d4b\u8bc4" && kpTotal != null) { progress = `${kpOk ?? 0}/${kpTotal}`; } const pct = pctMap[name] != null ? pctMap[name] : SCORE_WEIGHTS[name] != null ? SCORE_WEIGHTS[name] : null; detail.rows.push({ name, pct, progress, score: score != null ? score : "—", full: full != null ? full : "—", }); }); return detail; } async function fetchScoreDetail(course) { const id = course.studentCourseID || course.classNum; if (!id) return null; const urls = [ `/StudentZkSiteKC/ScoreDetailListZK/ScoreDetailListZKNewIndex?type=1&id=${encodeURIComponent(id)}`, `/StudentZkSite/ScoreDetailListZK/ScoreDetailListZKNewIndex?type=1&id=${encodeURIComponent(id)}`, ]; for (const path of urls) { try { const got = await sameOriginGet(path); if (got.html && /\u8bfe\u7a0b\u5b66\u4e60|setTotalValue|titleSec/i.test(got.html)) { return parseScoreDetailHtml(got.html); } } catch (_) {} } return null; } async function loadScoreDetailsForCourses(courses) { const list = courses || state.courses; for (const c of list) { try { const d = await fetchScoreDetail(c); if (d) { c.scoreDetail = d; if (d.studyMin != null) c.learnedMin = d.studyMin; if (d.studyNeed != null) c.needMin = d.studyNeed; renderCourseList(); } } catch (_) {} await sleep(120); } } function readFaceCache() { try { const raw = localStorage.getItem(FACE_CACHE_KEY); if (!raw) return null; const j = JSON.parse(raw); if (j && j.base64 && String(j.base64).startsWith("data:image")) return j; } catch (_) {} return null; } function writeFaceCache(base64, source) { const rec = { base64, ts: Date.now(), source: source || "camera" }; localStorage.setItem(FACE_CACHE_KEY, JSON.stringify(rec)); updateFaceStatusUi(); return rec; } function clearFaceCache() { localStorage.removeItem(FACE_CACHE_KEY); updateFaceStatusUi(); } function updateFaceStatusUi() { const el = document.getElementById("eduxl-face-status"); const prev = document.getElementById("eduxl-face-preview"); const meta = document.getElementById("eduxl-face-meta-line"); const uploadBtn = document.getElementById("eduxl-btn-face-upload"); const c = readFaceCache(); if (el) { if (c) { const ageMin = Math.max(0, Math.floor((Date.now() - (c.ts || 0)) / 60000)); const via = c.source === "upload" ? "\u4e0a\u4f20" : "\u62cd\u7167"; el.textContent = ageMin < 1 ? `\u4eba\u8138\u5df2\u5c31\u7eea\uff08${via}\uff09` : `\u4eba\u8138\u5df2\u5c31\u7eea\uff08${via} · ${ageMin} \u5206\u949f\u524d\uff09`; el.className = "eduxl-face-ok"; } else { el.textContent = "\u8fd8\u6ca1\u91c7\u4eba\u8138\uff0c\u5f00\u59cb\u524d\u8bf7\u5148\u62cd\u4e00\u5f20"; el.className = "eduxl-face-off"; } } if (meta) { meta.textContent = c ? "\u5f00\u59cb\u540e\u4f1a\u81ea\u52a8\u7528\u4eba\u8138\u7167\u7247" : isProUser() ? "\u53ef\u62cd\u7167\uff0c\u6216\u4f1a\u5458\u4e0a\u4f20\u672c\u5730\u7167\u7247" : "\u62cd\u4e00\u6b21\u5c31\u884c\uff0c\u540e\u9762\u4e0d\u7528\u518d\u91c7"; } if (uploadBtn) { uploadBtn.style.display = isProUser() ? "" : "none"; uploadBtn.disabled = false; uploadBtn.title = isProUser() ? "\u4ece\u76f8\u518c\u9009\u62e9\u4eba\u8138\u7167\u7247" : "\u5f00\u901a\u4f1a\u5458\u540e\u53ef\u7528"; } if (prev) { if (c && c.base64) { prev.src = c.base64; prev.style.display = "block"; } else { prev.removeAttribute("src"); prev.style.display = "none"; } } } function normalizeFaceDataUrl(dataUrl) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { try { const maxW = 640; let w = img.naturalWidth || img.width || 640; let h = img.naturalHeight || img.height || 480; if (w > maxW) { h = Math.round((h * maxW) / w); w = maxW; } const canvas = document.createElement("canvas"); canvas.width = w; canvas.height = h; canvas.getContext("2d").drawImage(img, 0, 0, w, h); resolve(canvas.toDataURL("image/png")); } catch (e) { reject(e); } }; img.onerror = () => reject(new Error("\u56fe\u7247\u65e0\u6cd5\u8bc6\u522b")); img.src = dataUrl; }); } async function uploadFaceToCache() { if (!isProUser()) { const err = new Error("\u4e0a\u4f20\u4eba\u8138\u7167\u7247\u4ec5\u4f1a\u5458\u53ef\u7528"); err.code = "pro_required"; throw err; } return new Promise((resolve, reject) => { const input = document.createElement("input"); input.type = "file"; input.accept = "image/jpeg,image/png,image/webp,image/bmp,.jpg,.jpeg,.png,.webp"; input.style.display = "none"; document.body.appendChild(input); const done = (fn) => { try { input.remove(); } catch (_) {} fn(); }; input.onchange = async () => { const file = input.files && input.files[0]; if (!file) { done(() => reject(new Error("\u5df2\u53d6\u6d88\u4e0a\u4f20"))); return; } if (!/^image\//i.test(file.type) && !/\.(jpe?g|png|webp|bmp)$/i.test(file.name || "")) { done(() => reject(new Error("\u8bf7\u9009\u62e9\u56fe\u7247\u6587\u4ef6"))); return; } if (file.size > 8 * 1024 * 1024) { done(() => reject(new Error("\u56fe\u7247\u592a\u5927\uff0c\u8bf7\u9009 8MB \u4ee5\u5185\u7684\u7167\u7247"))); return; } try { const raw = await new Promise((res, rej) => { const reader = new FileReader(); reader.onload = () => res(String(reader.result || "")); reader.onerror = () => rej(new Error("\u8bfb\u53d6\u6587\u4ef6\u5931\u8d25")); reader.readAsDataURL(file); }); if (!raw.startsWith("data:image")) throw new Error("\u4e0d\u662f\u6709\u6548\u7684\u56fe\u7247"); const dataUrl = await normalizeFaceDataUrl(raw); writeFaceCache(dataUrl, "upload"); done(() => resolve(dataUrl)); } catch (e) { done(() => reject(e)); } }; input.addEventListener("cancel", () => done(() => reject(new Error("\u5df2\u53d6\u6d88\u4e0a\u4f20")))); setTimeout(() => { try { input.click(); } catch (e) { done(() => reject(e)); } }, 0); }); } function getSubSchoolId() { const el = document.querySelector("#hidSubSchool_id") || [...document.querySelectorAll("iframe")].map((f) => { try { return f.contentDocument && f.contentDocument.querySelector("#hidSubSchool_id"); } catch (_) { return null; } }).find(Boolean); return (el && el.value) || "596"; } async function captureFaceToCache() { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new Error("\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u6444\u50cf\u5934"); } return new Promise((resolve, reject) => { const mask = document.createElement("div"); mask.id = "eduxl-face-mask"; mask.innerHTML = `
\u62cd\u4e00\u5f20\u4eba\u8138\u7167\u7247
\u8bf7\u6b63\u5bf9\u955c\u5934\uff0c\u5149\u7ebf\u5145\u8db3\u540e\u70b9\u300c\u62cd\u7167\u4fdd\u5b58\u300d
`; document.body.appendChild(mask); const video = mask.querySelector("#eduxl-face-video"); let stream = null; const cleanup = () => { try { if (stream) stream.getTracks().forEach((t) => t.stop()); } catch (_) {} mask.remove(); }; navigator.mediaDevices .getUserMedia({ video: { facingMode: "user", width: { ideal: 640 }, height: { ideal: 480 } }, audio: false }) .then((s) => { stream = s; video.srcObject = s; return video.play(); }) .catch((e) => { cleanup(); reject(e); }); mask.querySelector("#eduxl-face-cancel").onclick = () => { cleanup(); reject(new Error("\u5df2\u53d6\u6d88\u91c7\u96c6")); }; mask.querySelector("#eduxl-face-snap").onclick = () => { try { const w = video.videoWidth || 640; const h = video.videoHeight || 480; const canvas = document.createElement("canvas"); canvas.width = w; canvas.height = h; canvas.getContext("2d").drawImage(video, 0, 0, w, h); const dataUrl = canvas.toDataURL("image/png"); writeFaceCache(dataUrl, "camera"); cleanup(); resolve(dataUrl); } catch (e) { cleanup(); reject(e); } }; }); } async function acquireFaceCacheIfMissing() { const existing = readFaceCache(); if (existing && existing.base64) return existing; if (isProUser()) { trace("\u8bf7\u5148\u5728\u4e0a\u65b9\u300c\u91c7\u96c6\u4eba\u8138\u300d\u6216\u300c\u4e0a\u4f20\u7167\u7247\u300d\u51c6\u5907\u597d\u540e\u518d\u5f00\u59cb"); throw new Error("\u5c1a\u672a\u51c6\u5907\u4eba\u8138\u7167\u7247"); } dbg("\u65e0\u4eba\u8138\u7f13\u5b58\uff0c\u6253\u5f00\u6444\u50cf\u5934\u91c7\u96c6…"); await captureFaceToCache(); const cache = readFaceCache(); if (!cache || !cache.base64) throw new Error("\u4eba\u8138\u91c7\u96c6\u5931\u8d25"); return cache; } async function submitFaceForCourse(course, base64) { const body = new URLSearchParams({ SourceImageBase64: base64, CourseId: String(course.courseId || ""), TypeName: course.msgTitle || "\u8bfe\u4ef6\u5b66\u4e60", ExamDate: String(course.examDate || ""), catalogId: String(course.courseCode || ""), ProceduralTypeID: String(course.proceduralTypeID || "1"), }); const r = await fetch("/StudentZkSite/StudentZkSiteIndex/SaveFaceCjPicAndCheck", { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest", Accept: "application/json, text/javascript, */*; q=0.01", }, body: body.toString(), }); const j = await r.json().catch(() => null); return j; } async function ensureFaceBeforeLearn(course, forceResubmit) { let cache = await acquireFaceCacheIfMissing(); let needDb = true; let needCj = false; try { const fd = new URLSearchParams({ ExamDate: String(course.examDate || ""), CourseId: String(course.courseId || ""), proceduralTypeID: String(course.proceduralTypeID || "1"), subSchool_id: getSubSchoolId(), }); const r = await fetch("/StudentZkSite/StudentZkSiteKC/GetFaceCjAndDbModel", { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest", }, body: fd.toString(), }); const model = await r.json(); needCj = Number(model.faceKJ_cj || 0) > 0; needDb = Number(model.faceKJ_db || 0) > 0; if (!needCj && !needDb && !forceResubmit) { dbg("\u672c\u8bfe\u914d\u7f6e\u53ef\u4e0d\u5f3a\u5236\u4eba\u8138\uff0c\u4ecd\u63d0\u4ea4\u7f13\u5b58\u7167\u4ee5\u7eed\u671f"); } } catch (_) {} const label = forceResubmit ? "\u91cd\u65b0\u63d0\u4ea4\u4eba\u8138" : "\u63d0\u4ea4\u7f13\u5b58\u4eba\u8138"; dbg(`${label}\uff1a${course.title || course.courseCode}`); const j = await submitFaceForCourse(course, cache.base64); if (j && j.state) { dbg("\u4eba\u8138\u63d0\u4ea4\u6210\u529f\uff0c\u53ef\u7ee7\u7eed\u53d6\u5b66\u4e60\u51ed\u8bc1"); return true; } const msg = (j && j.msg) || "\u4eba\u8138\u63d0\u4ea4\u5931\u8d25"; dbg(`\u4eba\u8138\u63d0\u4ea4\u672a\u901a\u8fc7\uff1a${msg}`); if (cache.source === "upload") { trace(`\u4e0a\u4f20\u7684\u4eba\u8138\u5e73\u53f0\u672a\u901a\u8fc7\uff1a${msg}\uff0c\u8bf7\u6362\u4e00\u5f20\u6b63\u8138\u6e05\u6670\u7167\u7247\u518d\u70b9\u300c\u4e0a\u4f20\u7167\u7247\u300d`); throw new Error(msg); } trace(`\u4eba\u8138\u672a\u901a\u8fc7\uff1a${msg}\uff0c\u8bf7\u91cd\u65b0\u62cd\u6444`); await captureFaceToCache(); cache = readFaceCache(); if (!cache || !cache.base64) throw new Error("\u4eba\u8138\u91c7\u96c6\u5931\u8d25"); const j2 = await submitFaceForCourse(course, cache.base64); if (!j2 || !j2.state) { trace(`\u4eba\u8138\u4ecd\u672a\u901a\u8fc7\uff1a${(j2 && j2.msg) || msg}`); throw new Error((j2 && j2.msg) || msg); } dbg("\u91cd\u62cd\u540e\u4eba\u8138\u63d0\u4ea4\u6210\u529f"); return true; } function examAnswerSign(userExamId, questionId, psqId, answer, stime) { const raw = `answer=${answer}&psqId=${psqId}&stime=${stime}&key=${questionId}${userExamId}`; return md5(raw); } function pickPortalUserId(examDate) { if (state.session && state.session.userId) return state.session.userId; const fromCourse = (state.courses || []).map((c) => c.userId).find(Boolean); if (fromCourse) return fromCourse; try { const m = document.documentElement.innerHTML.match(/userID=([0-9]{15,18}_\d{6}_[a-z0-9]+)/i) || document.documentElement.innerHTML.match(/userid['\"]?\s*[:=]\s*['\"]([0-9]{15,18}_\d{6}_[a-z0-9]+)/i); if (m) return m[1]; } catch (_) {} const pidM = String(document.documentElement.innerHTML || "").match(/622\d{15}|[1-9]\d{16}[0-9Xx]/); const school = /lzugk/i.test(location.hostname) ? "lzugk" : "lzugk"; if (pidM && examDate) return `${pidM[0]}_${examDate}_${school}`; return ""; } function parsePszyListClick(html) { const list = []; const re = /pszy_click\(\s*['"]([^'"]+)['"]\s*,\s*['"]?(\d+)['"]?\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]*)['"]\s*,\s*['"]([^'"]*)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]*)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]*)['"]\s*\)/g; let m; while ((m = re.exec(html))) { list.push({ courseGUID: m[1], type: String(m[2]), jobCodes: m[3], examTimes: m[4], timeStart: m[5], timeEnd: m[6], courseName: m[7], proceduralTypeID: m[8], examDate: m[9], courseId: m[10], msgTitle: m[11], languageScore: m[12], studentCourseID: m[13], completeType: m[14], }); } return list; } function parsePszyExams(html) { const list = []; const re = /pszy_click2\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]*)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*\)/g; let m; while ((m = re.exec(html))) { list.push({ courseGUID: m[1], jobCode: m[2], examTimes: m[3], courseId: m[4], examDate: m[5], proceduralTypeID: m[6], subSchoolId: m[7], buttonName: m[8], title: m[9], startOrRecord: m[10], studentCourseID: m[12], signZK: m[13], }); } if (!list.length) { const mm = html.match(/var\s+ModelData\s*=\s*(\[[\s\S]*?\])\s*;/); if (mm) { try { const arr = JSON.parse(mm[1]); (arr || []).forEach((row) => { if (!row || !row.jobCode) return; list.push({ courseGUID: "", jobCode: String(row.jobCode), examTimes: "2", courseId: String(row.course_id || ""), examDate: "", proceduralTypeID: "1", subSchoolId: String(row.subSchool_id || getSubSchoolId() || "596"), buttonName: row.ZYButtonName || "\u9636\u6bb5\u4f5c\u4e1a", title: row.courseName || row.jobCode, startOrRecord: "1", studentCourseID: String(row.studentCourseID || ""), signZK: "", }); }); } catch (_) {} } } const map = new Map(); list.forEach((e) => { const prev = map.get(e.jobCode); if (!prev || e.startOrRecord === "1") map.set(e.jobCode, e); }); return [...map.values()]; } async function fetchExamIndex(course, opt) { const buttonName = (opt && opt.buttonName) || "\u9636\u6bb5\u4f5c\u4e1a"; const typeWanted = String((opt && opt.type) != null ? opt.type : "0"); const msgTitle = (opt && opt.msgTitle) || buttonName; const sid = course.studentCourseID || course.classNum || ""; const guid = course.courseGUID; let listHtml = ""; try { const kc = await sameOriginGet("/StudentZkSite/StudentZkSiteKC/StudentZkSiteKCNewIndex"); listHtml = kc.html || ""; } catch (e) { dbg(`KCNewIndex \u62c9\u53d6\u5931\u8d25\uff1a${e.message || e}`); } const all = parsePszyListClick(listHtml); const entries = all.filter( (e) => (e.msgTitle === msgTitle || e.type === typeWanted) && (e.courseGUID === guid || String(e.studentCourseID) === String(sid) || e.courseName === course.title) ); if (!entries.length) { dbg( `KC \u5217\u8868\u65e0${msgTitle}\u5165\u53e3 · guid=${guid || "?"} sid=${sid || "?"} pszy\u547d\u4e2d=${all.length} type\u8981=${typeWanted}` ); } const entry = entries[0] || null; const jobCode = entry ? entry.jobCodes : ""; const examTimes = entry ? entry.examTimes : ""; const courseName = (entry && entry.courseName) || course.title || ""; const examDate = (entry && entry.examDate) || course.examDate || ""; const proceduralTypeID = (entry && entry.proceduralTypeID) || course.proceduralTypeID || "1"; const type = entry ? entry.type : typeWanted; const studentCourseID = (entry && entry.studentCourseID) || sid; const userID = pickPortalUserId(examDate) || course.userId || ""; const tries = []; if (jobCode) { tries.push( `/StudentZkSite/StudentZkSiteKC/StudentZkSiteExIndex?courseGUID=${encodeURIComponent(guid)}` + `&jobCode=${encodeURIComponent(jobCode)}` + `&courseName=${encodeURIComponent(courseName)}` + `&examTimes=${encodeURIComponent(examTimes)}` + `&ButtonName=${encodeURIComponent(buttonName)}` + `&userID=${encodeURIComponent(userID)}` + `&ProceduralTypeID=${encodeURIComponent(proceduralTypeID)}` + `&type=${encodeURIComponent(type)}` + `&studentCourseID=${encodeURIComponent(studentCourseID)}` ); } tries.push( `/StudentZkSite/StudentZkSiteKC/StudentZkSiteExIndex?courseGUID=${encodeURIComponent(guid)}` + `&userID=${encodeURIComponent(userID)}&ProceduralTypeID=${encodeURIComponent(proceduralTypeID)}` + `&type=${encodeURIComponent(typeWanted)}&studentCourseID=${encodeURIComponent(studentCourseID)}` + `&ButtonName=${encodeURIComponent(buttonName)}` + (courseName ? `&courseName=${encodeURIComponent(courseName)}` : "") ); for (const u of tries) { try { const got = await sameOriginGet(u); const html = got.html || ""; const exams = parsePszyExams(html); if (exams.length) { exams.forEach((ex) => { if (!ex.courseGUID) ex.courseGUID = guid; if (!ex.examDate) ex.examDate = examDate; if (!ex.studentCourseID) ex.studentCourseID = studentCourseID; ex.examType = typeWanted; if (!ex.buttonName) ex.buttonName = buttonName; }); dbg(`ExIndex \u5df2\u53d6\u5230 · ${msgTitle} ${exams.length} \u5957 · jobCode=${(jobCode || exams[0].jobCode || "").slice(0, 40)}`); return { url: got.url || u, html, exams }; } dbg(`ExIndex \u65e0\u5377 · len=${html.length} title=${msgTitle} url=${u.slice(0, 90)}`); } catch (e) { dbg(`ExIndex \u8bf7\u6c42\u5931\u8d25\uff1a${e.message || e}`); } } return { url: "", html: "", exams: [] }; } async function fetchStageExamIndex(course) { return fetchExamIndex(course, { type: "0", buttonName: "\u9636\u6bb5\u4f5c\u4e1a", msgTitle: "\u9636\u6bb5\u4f5c\u4e1a" }); } async function fetchFinalExamIndex(course) { return fetchExamIndex(course, { type: "2", buttonName: "\u7efc\u5408\u6d4b\u8bc4", msgTitle: "\u7efc\u5408\u6d4b\u8bc4" }); } function parsePaperQuestions(html) { const qs = []; const seen = new Set(); const add = (qid, psq) => { const k = String(qid); if (!k || seen.has(k)) return; seen.add(k); qs.push({ questionId: k, psqId: String(psq) }); }; const src = String(html || ""); let m; const re = /id=["']q_(\d+)["'][\s\S]{0,320}?code=["']psq_(\d+)["']/gi; while ((m = re.exec(src))) add(m[1], m[2]); if (!qs.length) { const re2 = /code=["']psq_(\d+)["'][\s\S]{0,320}?id=["']q_(\d+)["']/gi; while ((m = re2.exec(src))) add(m[2], m[1]); } return qs; } function pickNextExamHop(text, base) { if (!text) return ""; const abs = (u) => { try { return new URL(String(u).replace(/&/g, "&").trim(), base || E14).href; } catch (_) { return ""; } }; let m = String(text).match(/http-equiv=["']refresh["'][^>]*content=["'][^"']*url=([^"']+)/i); if (m) { const u = abs(m[1]); if (u) return u; } m = String(text).match(/https?:\/\/e14\.edu-edu\.com\.cn\/exam\/student\/exam2\/start\/[^\s"'<>]+/i); if (m) return m[0].replace(/&/g, "&"); m = String(text).match(/\/exam\/student\/exam2\/start\/[^\s"'<>]+/i); if (m) { const u = abs(m[0]); if (u) return u; } m = String(text).match( /https?:\/\/em\.edu-edu\.com\.cn\/exam-admin\/student\/exam\/single\/restart\/[^\s"'<>]+/i ); if (m) return m[0].replace(/&/g, "&"); m = String(text).match(/\/exam-admin\/student\/exam\/single\/restart\/[^\s"'<>]+/i); if (m) { const u = abs(m[0]); if (u) return u; } m = String(text).match(/location(?:\.href)?\s*=\s*['"]([^'"]+)['"]/i); if (m && /exam|edu-edu/i.test(m[1])) { const u = abs(m[1]); if (u) return u; } return ""; } async function gmFollowGet(url, maxHops) { let cur = url; const seen = []; let last = { url: cur, text: "", status: 0, res: null }; let foundRestart = ""; for (let i = 0; i < (maxHops || 14); i++) { if (seen.includes(cur)) break; seen.push(cur); const asRestart = extractRestartUrl(cur, EM); if (asRestart) foundRestart = asRestart; let res = null; try { res = await gmRequest({ method: "GET", url: cur, redirect: "manual", timeout: 45000 }); } catch (_) { res = null; } if (!res || !res.status || res.status === 0) { try { res = await gmRequest({ method: "GET", url: cur, timeout: 60000 }); } catch (e) { break; } } const finalUrl = res.finalUrl || cur; if (finalUrl && finalUrl !== cur && !seen.includes(finalUrl)) seen.push(finalUrl); last = { url: finalUrl, text: res.responseText || "", status: res.status, res }; const fromHtml = extractRestartUrl(last.text, last.url); if (fromHtml) foundRestart = fromHtml; const loc = headerLocation(res.responseHeaders); if (loc && res.status >= 300 && res.status < 400) { try { cur = new URL(loc, cur).href; } catch (_) { cur = loc; } continue; } const next = pickNextExamHop(last.text, last.url); if (next && !seen.includes(next) && next !== last.url) { const nr = extractRestartUrl(next, EM); if (nr) foundRestart = nr; cur = next; continue; } break; } return { url: last.url, text: last.text, status: last.status, hops: seen, res: last.res, restartUrl: foundRestart || pickRestartFromHops(seen), }; } async function postGetExamUrl(exam, startOrRecord) { const body = new URLSearchParams({ courseGUID: exam.courseGUID, courseCode: exam.jobCode, ProceduralTypeID: exam.proceduralTypeID || "1", type: String(exam.examType != null ? exam.examType : exam.type || "0"), startOrRecord: String(startOrRecord || exam.startOrRecord || "1"), subSchool_id: exam.subSchoolId || getSubSchoolId() || "596", ExamDate: exam.examDate, course_id: exam.courseId, signZK: exam.signZK, }); const r = await fetch("/StudentZkSite/StudentZkSiteKC/GetExamUrl", { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest", Accept: "application/json, text/javascript, */*; q=0.01", }, body: body.toString(), }); return r.json().catch(() => null); } function absorbExamSession(url, text, out) { if (!url) return; rememberExamBase(out, url); try { const u = new URL(url, examBase(out)); const m = u.pathname.match(/\/exam2\/start\/(\d+)\/(\d+)\/(\d+)\/([^/]+)\/(\d+)/); if (m) { out.eid = m[1]; out.pid = m[2]; out.psid = m[3]; out.studentId = m[4]; out.bst = m[5]; out.userExamId = `${m[1]}_${m[4]}_${m[5]}`; } const m2 = u.pathname.match(/\/exam2\/doexam\/([^/]+)\/(\d+)\/(\d+)/); if (m2) { out.studentId = out.studentId || m2[1]; out.psid = out.psid || m2[2]; out.bst = out.bst || m2[3]; } const mRestart = u.pathname.match(/\/single\/restart\/([^/]+)\/([^/?]+)/); if (mRestart) { out.userExamId = out.userExamId || mRestart[1]; out.clientId = mRestart[2]; } const mStartEm = u.pathname.match(/\/single\/start\/([^/]+)\/([^/]+)\/([^/?]+)/); if (mStartEm) { out.clientId = mStartEm[2]; out.studentId = out.studentId || mStartEm[3]; } const tk = u.searchParams.get("take_exam_params_key"); if (tk) { try { const p = JSON.parse(decodeURIComponent(tk)); if (p.eid) out.eid = String(p.eid); if (p.pid) out.pid = String(p.pid); if (p.psid) out.psid = String(p.psid); if (p.bst) out.bst = String(p.bst); if (p.studentId) out.studentId = String(p.studentId); if (p.clientId) out.clientId = String(p.clientId); if (out.eid && out.studentId && out.bst) out.userExamId = `${out.eid}_${out.studentId}_${out.bst}`; } catch (_) {} } } catch (_) {} if (text) { const html = String(text); rememberExamBase(out, html); const eo = html.match(/ExamObj\s*\(\s*\d+\s*,\s*['"]([^'"]+)['"]/); if (eo) out.userExamId = out.userExamId || eo[1]; const pc = html.match(/paper_card2\/(\d+)\/(\d+)\//); if (pc) { out.pid = out.pid || pc[1]; out.psid = out.psid || pc[2]; } const ma = html.match(/\/myanswer\/(?:list|newSave)\/([0-9A-Za-z_]+)/); if (ma && ma[1].includes("_")) out.userExamId = out.userExamId || ma[1]; const rsFull = html.match(/https?:\/\/em\.edu-edu\.com\.cn\/exam-admin\/student\/exam\/single\/restart\/[^"'\\\s]+/i) || html.match(/\/exam-admin\/student\/exam\/single\/restart\/[^"'\\\s]+/i); if (rsFull) { try { out.restartUrl = new URL(String(rsFull[0]).replace(/&/g, "&"), EM).href; } catch (_) {} const rs = String(rsFull[0]).match(/\/single\/restart\/([^/"'\s?]+)\/([^/"'\s?]+)/); if (rs) { out.userExamId = out.userExamId || rs[1]; out.clientId = out.clientId || rs[2]; } } const tm = html.match(/take_exam_params_key=([^&"'<>]+)/); if (tm) { try { const p = JSON.parse(decodeURIComponent(tm[1])); if (p.eid) out.eid = String(p.eid); if (p.pid) out.pid = String(p.pid); if (p.psid) out.psid = String(p.psid); if (p.bst) out.bst = String(p.bst); if (p.studentId) out.studentId = String(p.studentId); if (p.clientId) out.clientId = String(p.clientId); if (out.eid && out.studentId && out.bst) out.userExamId = `${out.eid}_${out.studentId}_${out.bst}`; } catch (_) {} } const sm = html.match(/\/exam2\/start\/(\d+)\/(\d+)\/(\d+)\/([^/"']+)\/(\d+)/); if (sm) { out.eid = sm[1]; out.pid = sm[2]; out.psid = sm[3]; out.studentId = sm[4]; out.bst = sm[5]; out.userExamId = `${sm[1]}_${sm[4]}_${sm[5]}`; } } if (!out.userExamId && out.eid && out.studentId && out.bst) { out.userExamId = `${out.eid}_${out.studentId}_${out.bst}`; } if (out.userExamId && !out.eid) { const parts = String(out.userExamId).split("_"); if (parts.length >= 2 && /^\d+$/.test(parts[0])) out.eid = parts[0]; if (parts.length >= 2 && /^\d+$/.test(parts[parts.length - 1])) out.bst = out.bst || parts[parts.length - 1]; if (parts.length >= 4) out.studentId = out.studentId || parts.slice(1, -1).join("_"); } } function applyPaperUrlFromHtml(sess, html) { const m = String(html || "").match(/\/exam\/student\/exam\/resource\/(paper_card2\/\d+\/\d+\/[^"'?\s]+)/i); if (!m) return false; const segs = m[1].match(/paper_card2\/(\d+)\/(\d+)\//); if (segs) { sess.pid = segs[1]; sess.psid = segs[2]; } rememberExamBase(sess, html); sess.paperUrl = `${examBase(sess)}/exam/student/exam/resource/${m[1]}`; return true; } function finalizeExamPaperUrl(sess) { applyPaperUrlFromHtml(sess, sess.doexamHtml); if (sess.pid && sess.psid) { const base = examBase(sess); if (!sess.paperUrl || !/paper_card2\//.test(sess.paperUrl) || !sess.paperUrl.startsWith(base)) { sess.paperUrl = `${base}/exam/student/exam/resource/paper_card2/${sess.pid}/${sess.psid}/92__index.html`; } } } async function openExamSession(exam, startOrRecord) { const faceCourse = { courseGUID: exam.courseGUID, courseCode: exam.jobCode, courseId: exam.courseId, examDate: exam.examDate, proceduralTypeID: exam.proceduralTypeID, msgTitle: exam.buttonName || "\u9636\u6bb5\u4f5c\u4e1a", title: exam.title, studentCourseID: exam.studentCourseID, }; await ensureFaceBeforeLearn(faceCourse); const j = await postGetExamUrl(exam, startOrRecord); if (!j || !j.state || !j.msg) throw new Error((j && (j.msg || j.rurl)) || "GetExamUrl \u5931\u8d25"); const examNewUrl = j.msg; dbg(`\u8fdb\u5165\u8003\u8bd5\uff1a${exam.title || exam.jobCode} · mode=${startOrRecord}`); const hop = await gmFollowGet(examNewUrl, 14); const sess = { userExamId: "", eid: "", pid: "", psid: "", bst: "", studentId: "", clientId: "guokaogansu", paperUrl: "", doexamHtml: "", restartUrl: "", examBase: E14, }; const absorbHop = (h) => { (h.hops || []).forEach((u) => absorbExamSession(u, "", sess)); absorbExamSession(h.url, h.text, sess); if (h.text && (/ExamObj\s*\(/.test(h.text) || /paper_card2\//.test(h.text))) { sess.doexamHtml = h.text; } if (h.text) { const signed = extractRestartUrl(h.text, h.url); if (signed) sess.restartUrl = signed; } }; absorbHop(hop); if (hop.restartUrl) sess.restartUrl = hop.restartUrl; else { const fromHops = pickRestartFromHops(hop.hops); if (fromHops) sess.restartUrl = fromHops; } if (!sess.userExamId || !sess.pid || !sess.doexamHtml) { const next = sess.restartUrl || pickNextExamHop(hop.text || "", hop.url); if (next) { dbg(`\u8003\u8bd5\u7eed\u8df3\uff1a${next.slice(0, 96)}`); const h2 = await gmFollowGet(next, 12); absorbHop(h2); if (h2.restartUrl) sess.restartUrl = h2.restartUrl; } } if (!sess.doexamHtml || !/ExamObj\s*\(/.test(sess.doexamHtml || "")) { const next = sess.restartUrl || pickNextExamHop(hop.text || "", hop.url); if (next) { dbg(`\u8865\u8fdb\u8003\u573a\uff1a${next.slice(0, 96)}`); const h3 = await gmFollowGet(next, 12); absorbHop(h3); if (h3.restartUrl) sess.restartUrl = h3.restartUrl; } } finalizeExamPaperUrl(sess); if (!sess.userExamId) { dbg(`\u8003\u8bd5\u89e3\u6790\u5931\u8d25 · final=${(hop.url || "").slice(0, 140)} · hops=${(hop.hops || []).length}`); throw new Error("\u672a\u80fd\u89e3\u6790 userExamId\uff08\u8003\u8bd5\u4f1a\u8bdd\uff09"); } if (!sess.paperUrl) { dbg(`\u8bd5\u5377\u5730\u5740\u7f3a\u5931 · uid=${sess.userExamId} · final=${(hop.url || "").slice(0, 100)}`); throw new Error("\u672a\u80fd\u89e3\u6790\u8bd5\u5377\u5730\u5740 paper_card2"); } if (sess.examBase && sess.examBase !== E14) dbg(`\u8003\u573a\u8282\u70b9\uff1a${sess.examBase}`); return sess; } async function restartExamSession(sess, exam) { const restartUrl = await fetchSignedRestartUrl(sess, exam); dbg(`\u7eed\u8003 restart\uff1a${restartUrl.slice(0, 140)}`); const entered = await followRestartToDoexam(restartUrl); dbg(`\u7eed\u8003\u8fdb\u573a hops=${(entered.hops || []).join(" → ")}`); const out = { userExamId: sess.userExamId, eid: sess.eid, pid: sess.pid, psid: sess.psid, bst: sess.bst, studentId: sess.studentId, clientId: sess.clientId || "guokaogansu", paperUrl: sess.paperUrl, doexamHtml: entered.text || "", restartUrl, examBase: sess.examBase || E14, }; rememberExamBase(out, entered.url); rememberExamBase(out, entered.text); if (entered.startUrl) { rememberExamBase(out, entered.startUrl); absorbExamSession(entered.startUrl, "", out); } absorbExamSession(entered.url, entered.text, out); applyPaperUrlFromHtml(out, out.doexamHtml); if (sess.pid && sess.psid) { out.pid = sess.pid; out.psid = sess.psid; } finalizeExamPaperUrl(out); if (!out.userExamId) out.userExamId = sess.userExamId; if (!/ExamObj\s*\(/.test(out.doexamHtml || "")) { throw new Error("\u7eed\u8003\u672a\u8fdb\u5165\u8003\u573a\uff08\u4f1a\u8bdd key \u672a\u5efa\u7acb\uff09"); } if (out.examBase && out.examBase !== E14) { dbg(`\u8003\u573a\u8282\u70b9\uff1a${out.examBase}`); } return out; } async function saveExamAnswer(sess, q, answer) { const stime = String(Date.now()); const ans = String(answer || "").toLowerCase(); const mdig = examAnswerSign(sess.userExamId, q.questionId, q.psqId, ans, stime); const body = `psqId=${encodeURIComponent(q.psqId)}&answer=${encodeURIComponent(ans)}&attach=&stime=${stime}&m=${mdig}`; const base = examBase(sess); const { j, status, text } = await gmJson({ method: "POST", url: `${base}/exam/student/exam/myanswer/newSave/${encodeURIComponent(sess.userExamId)}/${encodeURIComponent(q.questionId)}`, headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", Accept: "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", Origin: base, Referer: `${base}/exam/student/exam2/doexam/${encodeURIComponent(sess.studentId || "")}/${encodeURIComponent( sess.psid || "" )}/${encodeURIComponent(sess.bst || "")}`, }, data: body, }); const ok = status >= 200 && status < 300 && (!j || j.success !== false); if (!ok) throw new Error(`\u4fdd\u5b58\u7b54\u6848\u5931\u8d25 q=${q.questionId} ${(text || "").slice(0, 80)}`); return j; } async function submitExamPaper(sess) { const base = examBase(sess); const { j, status, text } = await gmJson({ method: "POST", url: `${base}/exam/student/exam/submit/${encodeURIComponent(sess.userExamId)}`, headers: { Accept: "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", Origin: base, Referer: `${base}/exam/student/exam2/doexam/${encodeURIComponent(sess.studentId || "")}/${encodeURIComponent( sess.psid || "" )}/${encodeURIComponent(sess.bst || "")}`, }, data: "", }); const ok = status >= 200 && status < 300 && (!j || j.success === true || j.success === "true"); if (!ok) throw new Error(`\u4ea4\u5377\u5931\u8d25 ${(text || "").slice(0, 100)}`); return j; } async function openExamResultViews(sess) { const uid = sess.userExamId; const base = examBase(sess); const finishedUrl = `${base}/exam/student/exam/finished/${encodeURIComponent(uid)}?&clientTheme=client_card2`; const fin = await gmRequest({ method: "GET", url: finishedUrl, timeout: 45000, headers: { Accept: "text/html,application/xhtml+xml", Referer: `${base}/` }, }); const finHtml = String(fin.responseText || "").replace(/&/g, "&"); let viewUrl = ""; const vm = finHtml.match(/\/exam\/student\/exam2\/view\/[^"'\\\s]+/); if (vm) { try { viewUrl = new URL(vm[0], base).href; } catch (_) { viewUrl = base + vm[0]; } } if (!viewUrl) { const params = { cc: "0", isUserQuestion: "false", d: String(Date.now()), limitedTime: "0", userViewType: "1", co: "0", asa: "1", ase: "true", atf: "0", furl: "", hasJoinErrorBook: "true", clientTheme: "client_card2", isCensorwords: "false", }; viewUrl = `${base}/exam/student/exam2/view/${encodeURIComponent(uid)}/1?&view_paper_params_key=${encodeURIComponent( JSON.stringify(params) )}&clientTheme=client_card2`; dbg("finished \u9875\u672a\u89e3\u6790\u5230 view \u94fe\u63a5\uff0c\u4f7f\u7528\u515c\u5e95 asa=1"); } await gmRequest({ method: "GET", url: viewUrl, timeout: 45000, headers: { Accept: "text/html,application/xhtml+xml", Referer: finishedUrl }, }); const doviewUrl = `${base}/exam/student/exam2/doview/${encodeURIComponent(uid)}/1`; const dv = await gmRequest({ method: "GET", url: doviewUrl, timeout: 45000, headers: { Accept: "text/html,application/xhtml+xml", Referer: viewUrl }, }); const dvHtml = dv.responseText || ""; if (/allowSeeAnswer\s*=\s*true/i.test(dvHtml)) { dbg("\u7b54\u5377\u9875 allowSeeAnswer=true"); } else { dbg(`\u7b54\u5377\u9875\u672a\u786e\u8ba4 allowSeeAnswer · ${(dvHtml || "").slice(0, 80)}`); } try { await gmJson({ method: "GET", url: `${base}/exam/student/exam/myanswer/list/${encodeURIComponent(uid)}?_=${Date.now()}`, headers: { Accept: "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", Referer: doviewUrl, Origin: base, }, }); } catch (_) {} for (const html of [finHtml, dvHtml]) { const signed = extractRestartUrl(html, EM); if (signed) { sess.restartUrl = signed; break; } } return { finishedUrl, viewUrl, doviewUrl }; } async function fetchExamAnswerKey(sess, doviewUrl) { const base = examBase(sess); const referer = doviewUrl || `${base}/exam/student/exam2/doview/${encodeURIComponent(sess.userExamId)}/1`; for (let attempt = 1; attempt <= 5; attempt++) { const { j, text, status } = await gmJson({ method: "GET", url: `${base}/exam/student/exam/answer/${encodeURIComponent(sess.userExamId)}?_=${Date.now()}`, headers: { Accept: "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", Referer: referer, Origin: base, }, }); if (status < 200 || status >= 300) { dbg(`\u62c9\u6807\u7b54 HTTP ${status} · try ${attempt}`); } const answers = (j && Array.isArray(j.answers) && j.answers) || []; if (answers.length) return answers; dbg(`\u6807\u7b54\u4e3a\u7a7a · try ${attempt}/${5} · ${(text || "").slice(0, 120)}`); await sleep(500 * attempt); if (attempt === 2 || attempt === 4) { try { await openExamResultViews(sess); } catch (e) { dbg(`\u91cd\u5f00\u7b54\u5377\u9875\u5931\u8d25\uff1a${e.message || e}`); } } } return []; } async function loadPaperQuestions(sess) { let questions = []; let paperText = ""; finalizeExamPaperUrl(sess); const base = examBase(sess); const referer = `${base}/exam/student/exam2/doexam/${encodeURIComponent(sess.studentId || "")}/${encodeURIComponent( sess.psid || "" )}/${encodeURIComponent(sess.bst || "")}`; for (let i = 0; i < 3 && !questions.length; i++) { const paper = await gmRequest({ method: "GET", url: sess.paperUrl, timeout: 60000, headers: { Accept: "text/html,application/xhtml+xml,*/*", Referer: referer }, }); paperText = paper.responseText || ""; questions = parsePaperQuestions(paperText); if (!questions.length) await sleep(350); } if (!questions.length && sess.doexamHtml) { applyPaperUrlFromHtml(sess, sess.doexamHtml); finalizeExamPaperUrl(sess); if (sess.paperUrl) { try { const paper = await gmRequest({ method: "GET", url: sess.paperUrl, timeout: 60000, headers: { Accept: "text/html,application/xhtml+xml,*/*", Referer: referer }, }); paperText = paper.responseText || ""; questions = parsePaperQuestions(paperText); } catch (_) {} } if (!questions.length) questions = parsePaperQuestions(sess.doexamHtml); } if (!questions.length) { dbg(`\u8bd5\u5377HTML len=${(paperText || "").length} · host=${base} · ${(paperText || "").slice(0, 120)}`); } return questions; } async function runOneStageExam(course, exam) { let sess = await openExamSession(exam, "1"); let questions = await loadPaperQuestions(sess); if (!questions.length) { sess = await restartExamSession(sess, exam); questions = await loadPaperQuestions(sess); } if (!questions.length) throw new Error("\u8bd5\u5377\u672a\u89e3\u6790\u5230\u9898\u76ee\uff08paper_card2\uff09"); dbg(`${exam.title} \u9898\u76ee ${questions.length} \u9053 · uid=…${String(sess.userExamId).slice(-18)}`); let keys = []; try { const views0 = await openExamResultViews(sess); keys = await fetchExamAnswerKey(sess, views0.doviewUrl); if (keys.length) dbg(`${exam.title} \u6cbf\u7528\u5df2\u4ea4\u5377\u6807\u7b54 ${keys.length} \u9053`); } catch (e) { dbg(`\u9884\u62c9\u6807\u7b54\u8df3\u8fc7\uff1a${e.message || e}`); } if (!keys.length) { dbg(`${exam.title} \u5148\u7eed\u8003\u4f1a\u8bdd\uff0c\u518d\u968f\u4fbf\u4ea4\u4e00\u9898\u53d6\u6807\u7b54`); sess = await restartExamSession(sess, exam); await saveExamAnswer(sess, questions[0], "a"); await submitExamPaper(sess); dbg(`${exam.title} \u5df2\u4ea4\u5377\uff0c\u6253\u5f00\u7b54\u5377\u9875…`); await sleep(500); const views = await openExamResultViews(sess); keys = await fetchExamAnswerKey(sess, views.doviewUrl); } if (!keys.length) throw new Error("\u672a\u62ff\u5230\u6807\u7b54\uff08answer \u63a5\u53e3\u4e3a\u7a7a\uff09"); const keyMap = new Map(); keys.forEach((a) => { if (a && a.questionId != null && a.answer != null && a.answer !== "") { keyMap.set(String(a.questionId), String(a.answer).toLowerCase()); } }); dbg(`${exam.title} \u6807\u7b54 ${keyMap.size} \u9053\uff0crestart \u540e\u7eed\u8003\u5199\u5165…`); sess = await restartExamSession(sess, exam); if (!sess.doexamHtml || !/ExamObj\s*\(/.test(sess.doexamHtml || "")) { sess = await restartExamSession(sess, exam); } dbg(`${exam.title} \u7eed\u8003\u4f1a\u8bdd\u5c31\u7eea · uid=…${String(sess.userExamId).slice(-18)}`); let okN = 0; let miss = 0; let failN = 0; for (const q of questions) { if (!state.running) break; const ans = keyMap.get(String(q.questionId)); if (!ans) { miss += 1; continue; } try { await saveExamAnswer(sess, q, ans); okN += 1; } catch (e) { failN += 1; const msg = String(e.message || e); if (failN <= 3 || /10001|\u6ce8\u9500|\u6ca1\u6709\u8003\u8bd5/.test(msg)) { dbg(`\u6539\u7b54\u5931\u8d25 ${q.questionId}\uff1a${msg.slice(0, 100)}`); } if (/10001|\u6ce8\u9500|\u6ca1\u6709\u8003\u8bd5/.test(msg)) { try { sess = await restartExamSession(sess, exam); await saveExamAnswer(sess, q, ans); okN += 1; failN -= 1; dbg(`\u6539\u7b54\u6062\u590d\u6210\u529f ${q.questionId}`); } catch (e2) { dbg(`\u6539\u7b54\u6062\u590d\u5931\u8d25\uff1a${e2.message || e2}`); break; } } } await sleep(60); } if (miss) dbg(`${exam.title} \u65e0\u5339\u914d\u6807\u7b54 ${miss}/${questions.length}`); if (okN < 1) throw new Error("\u7eed\u8003\u672a\u80fd\u5199\u5165\u4efb\u4f55\u6807\u7b54\uff08\u4f1a\u8bdd\u65e0\u6548\uff09"); await submitExamPaper(sess); dbg(`${exam.title} \u6ee1\u5206\u5377\u5df2\u4ea4 · \u5199\u5165 ${okN}/${questions.length}`); return okN; } async function startExamFullScoreRun(kind) { const label = (kind && kind.msgTitle) || "\u9636\u6bb5\u4f5c\u4e1a"; const type = String((kind && kind.type) != null ? kind.type : "0"); const buttonName = (kind && kind.buttonName) || label; if (state.running) return; if (!canDoExamWork()) { try { await ensureCloudLease(!!getCloudToken()); } catch (_) {} if (!canDoExamWork()) { trace(denyExamWorkMsg(label)); return; } } if (!isEduXl) { dbg(`\u8bf7\u5728 edu-xl \u95e8\u6237\u4f7f\u7528\u300c${label}\u300d`); return; } const checkedCourses = getCheckedCourses(); if (!checkedCourses.length) { dbg("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b"); return; } if (!readFaceCache()) { try { await captureFaceToCache(); } catch (e) { dbg(`\u4eba\u8138\u91c7\u96c6\u5931\u8d25\uff1a${e.message || e}`); return; } } state.running = true; updatePanel(); try { for (const course of checkedCourses) { if (!state.running) break; dbg(`\u62c9\u53d6${label}\u5217\u8868\uff1a${course.title || course.courseCode}`); const idx = await fetchExamIndex(course, { type, buttonName, msgTitle: label }); if (!idx.exams.length) { dbg(`${course.title} \u672a\u627e\u5230${label}\uff08\u8bf7\u5148\u624b\u52a8\u70b9\u5f00\u4e00\u6b21\u300c${label}\u300d\u518d\u91cd\u8bd5\uff09`); continue; } dbg(`${course.title} ${label} ${idx.exams.length} \u5957`); for (const exam of idx.exams) { if (!state.running) break; exam.examType = type; if (!exam.buttonName) exam.buttonName = buttonName; try { await runOneStageExam(course, exam); } catch (e) { dbg(`${label}\u5931\u8d25 ${exam.title || exam.jobCode}\uff1a${e.message || e}`); } await sleep(500); } try { const detail = await fetchScoreDetail(course); if (detail) { course.scoreDetail = detail; renderCourseList(); } } catch (_) {} } dbg(`${label}\u961f\u5217\u7ed3\u675f`); } catch (e) { dbg(`${label}\u5f02\u5e38\uff1a${e.message || e}`); } finally { state.running = false; updatePanel(); } } async function startStageHomeworkRun() { return startExamFullScoreRun({ type: "0", buttonName: "\u9636\u6bb5\u4f5c\u4e1a", msgTitle: "\u9636\u6bb5\u4f5c\u4e1a" }); } async function startFinalExamRun() { return startExamFullScoreRun({ type: "2", buttonName: "\u7efc\u5408\u6d4b\u8bc4", msgTitle: "\u7efc\u5408\u6d4b\u8bc4" }); } async function openLearnSession(course) { await ensureFaceBeforeLearn(course); dbg(`\u83b7\u53d6\u5b66\u4e60\u51ed\u8bc1\uff1a${course.title || course.courseCode}`); const body = new URLSearchParams({ courseGUID: course.courseGUID, courseCode: course.courseCode, stemcode: course.stemcode, ProceduralTypeID: course.proceduralTypeID, ExamDate: course.examDate, }); const r = await fetch("/StudentZkSite/StudentZkSiteKC/GetCourseUrl", { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest", }, body: body.toString(), }); const j = await r.json(); if (!j || !j.state || !j.msg) { const msg = (j && j.msg) || "GetCourseUrl \u5931\u8d25"; if (/\u4eba\u8138|\u56fe\u50cf\u91c7\u96c6|\u957f\u65f6\u95f4\u672a\u8fdb\u5165/i.test(String(msg))) { dbg(`\u51ed\u8bc1\u88ab\u62d2\uff08\u4eba\u8138\uff09\uff1a${msg}`); await ensureFaceBeforeLearn(course, true); const r2 = await fetch("/StudentZkSite/StudentZkSiteKC/GetCourseUrl", { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest", }, body: body.toString(), }); const j2 = await r2.json(); if (!j2 || !j2.state || !j2.msg) throw new Error((j2 && j2.msg) || msg); return continueOpenLearnSession(course, j2.msg); } throw new Error(msg); } return continueOpenLearnSession(course, j.msg); } async function continueOpenLearnSession(course, learnNewUrl) { const meta = parseQuery(learnNewUrl); dbg(`getLearnNew \u53c2\u6570 userid=${meta.userid || meta.userId || "?"} course=${meta.courseCode || course.courseCode}`); const learnQ = await resolveLearnParams(learnNewUrl, course, meta); if (!learnQ.userId || !learnQ.coursewareCode) { throw new Error( `\u7f3a\u5c11 userId/coursewareCode\uff08userId=${learnQ.userId || "\u65e0"} cw=${learnQ.coursewareCode || "\u65e0"}\uff09` ); } if (!learnQ.clientCode) { throw new Error( "\u672a\u80fd\u89e3\u6790 clientCode\u3002\u8bf7\u5141\u8bb8\u6cb9\u7334\u8fde\u63a5 resgk.edu-xl.com / cws.edu-edu.com\uff1b\u6216\u5148\u624b\u52a8\u70b9\u4e00\u6b21\u300c\u8bfe\u7a0b\u5b66\u4e60\u300d\u8fdb\u64ad\u653e\u9875\u518d\u56de\u95e8\u6237\uff08\u4f1a\u7f13\u5b58 clientCode\uff09" ); } const learnUrl = `${CWS}/client/user/learn?businessLineCode=${encodeURIComponent(learnQ.businessLineCode || "")}` + `&coursewareCode=${encodeURIComponent(learnQ.coursewareCode)}` + `&courseCodeN=${encodeURIComponent(learnQ.courseCodeN || "")}` + `&catalogId=${encodeURIComponent(learnQ.catalogId || "")}` + `&clientCode=${encodeURIComponent(learnQ.clientCode)}` + `&userId=${encodeURIComponent(learnQ.userId)}` + `&userName=${encodeURIComponent(learnQ.userName || "")}` + `&lastTime=×tamp=${Date.now()}`; try { await gmRequest({ method: "GET", url: learnUrl, timeout: 45000 }); await gmRequest({ method: "GET", url: `${CWS}/page/client`, timeout: 30000 }); } catch (e) { dbg(`\u5efa\u7acb\u8bfe\u4ef6\u4f1a\u8bdd\u8b66\u544a\uff1a${e.message || e}`); } let coursewareName = course.title || course.courseCode; try { const cw = await gmJson({ method: "GET", url: `${CWS}/client/${encodeURIComponent(learnQ.coursewareCode)}/findCwByCode?userId=${encodeURIComponent( learnQ.userId )}&clientCode=${encodeURIComponent(learnQ.clientCode)}`, }); if (cw.j && cw.j.data && typeof cw.j.data === "object") { coursewareName = cw.j.data.coursewareName || cw.j.data.title || coursewareName; } else if (cw.j && cw.j.coursewareName) { coursewareName = cw.j.coursewareName; } } catch (_) {} const session = { clientCode: learnQ.clientCode, userId: learnQ.userId, userName: learnQ.userName || meta.uname || "", coursewareCode: learnQ.coursewareCode || course.courseCode, classNum: String(course.studentCourseID || course.classNum || meta.eplanid || meta.studentCourseID || ""), backUrl: BACK_URL_DEFAULT, coursewareName: coursewareName || course.title || course.courseCode, maxTimePerDay: parseInt(meta.maxTimePerDay || "0", 10) || 0, openId: meta.Openid || course.courseGUID, courseId: course.courseId || "", course, }; state.session = session; state.activeCourse = course; state.title = coursewareName; rememberClientCode(session.clientCode); dbg(`\u4f1a\u8bdd\u5c31\u7eea client=${session.clientCode} user=${session.userName} cw=${session.coursewareCode}`); if (session.userId) { if (!String(gmGet(QUOTA_UID_KEY, "") || "").trim()) { state.learningUserId = String(session.userId); gmSet(LEARNING_UID_KEY, state.learningUserId); } } return session; } function catalogChildren(node) { if (!node || typeof node !== "object") return null; const keys = [ "children", "childrensForCatalog", "childList", "child", "nodes", "list", "catalogs", "subList", "items", "subs", "chapterList", "catalogList", "treeList", ]; for (const k of keys) { if (Array.isArray(node[k]) && node[k].length) return node[k]; } return null; } function parseEduCatalogFlatList(data) { if (!Array.isArray(data) || !data.length) return []; const out = []; const pushNode = (node, requireMedia) => { if (!node || typeof node !== "object") return; const id = catalogIdOf(node); if (!id) return; const videoTime = catalogVideoTimeOf(node); const isMedia = node.isMedia === 1 || node.isMedia === "1" || node.isMedia === true; if (requireMedia && !isMedia && videoTime <= 0) return; const title = resolveCatalogTitle(node, id); const sofar = catalogSofarOf(node); const lastTime = catalogLastTimeOf(node); out.push({ catalogId: id, catalogTitle: title || fallbackChapterTitle({ _raw: node, catalogId: id }, out.length), videoTime, sofar, lastTime, done: false, isMedia: !!isMedia, _raw: node, }); applyChapterDone(out[out.length - 1]); }; data.forEach((n) => pushNode(n, true)); if (!out.length) data.forEach((n) => pushNode(n, false)); return out; } function catalogIdOf(node) { if (!node || typeof node !== "object") return ""; const id = node.id ?? node.catalogId ?? node.catalogID ?? node.cataLogId ?? node.CatalogId ?? node.chapterId ?? node.resourceId ?? node.scoId ?? node.nodeId ?? node.cid ?? node.uid ?? node.key ?? node.value; return id == null || id === "" ? "" : String(id); } function isBareCatalogTitle(title, catalogId) { const t = String(title || "").trim(); if (!t) return true; if (catalogId && t === String(catalogId)) return true; if (/^\d{12,22}$/.test(t)) return true; return false; } function catalogTitleOf(node) { if (!node || typeof node !== "object") return ""; const media = node.media && typeof node.media === "object" ? node.media : null; const lr = node.learnRecord && typeof node.learnRecord === "object" ? node.learnRecord : null; const candidates = [ node.title, node.catalogTitle, node.catalogName, node.name, node.chapterName, node.chapterTitle, node.resourceName, node.resourceTitle, node.label, node.text, node.nodeName, node.nodeTitle, node.coursewareName, node.scoTitle, node.mediaTitle, node.mediaName, node.displayName, node.cname, media && media.title, media && media.name, media && media.resourceName, lr && lr.catalogTitle, lr && lr.title, lr && lr.catalogName, ]; for (const c of candidates) { const s = String(c || "").trim(); if (s) return s; } return ""; } function resolveCatalogTitle(node, catalogId) { const direct = catalogTitleOf(node); if (direct && !isBareCatalogTitle(direct, catalogId)) return direct.trim(); if (!node || typeof node !== "object") return ""; const stack = [node]; const seen = new Set(); while (stack.length) { const cur = stack.pop(); if (!cur || typeof cur !== "object" || seen.has(cur)) continue; seen.add(cur); for (const [k, v] of Object.entries(cur)) { if (Array.isArray(v)) { v.forEach((it) => { if (it && typeof it === "object") stack.push(it); }); continue; } if (v && typeof v === "object") { stack.push(v); continue; } if (typeof v !== "string") continue; const s = v.trim(); if (s.length < 2 || s.length > 120) continue; if (isBareCatalogTitle(s, catalogId)) continue; if (/title|name|chapter|catalog|resource|node|label|text|sco/i.test(k) && /[\u4e00-\u9fa5a-zA-Z]/.test(s)) { return s; } } } return ""; } function fallbackChapterTitle(ch, index) { const fromRaw = ch && ch._raw ? resolveCatalogTitle(ch._raw, ch.catalogId) : ""; const t = fromRaw || String((ch && ch.catalogTitle) || "").trim(); if (t && !isBareCatalogTitle(t, ch && ch.catalogId)) return t; const n = Number(index); return Number.isFinite(n) && n >= 0 ? `\u7b2c${n + 1}\u8282` : "\u672c\u8282"; } function chapterDisplayTitle(ch, index) { const t = String((ch && ch.catalogTitle) || "").trim(); if (t && !isBareCatalogTitle(t, ch && ch.catalogId)) return t; return fallbackChapterTitle(ch, index); } function mergeChapterFromInfo(ch, info) { if (!ch || !info) return; const title = resolveCatalogTitle(info._raw || info, ch.catalogId) || String(info.catalogTitle || "").trim(); if (title && !isBareCatalogTitle(title, ch.catalogId)) ch.catalogTitle = title; if (info.videoTime > 0) ch.videoTime = info.videoTime; if (info.sofar != null) ch.sofar = Math.max(ch.sofar || 0, info.sofar); if (info.lastTime != null) ch.lastTime = Math.max(ch.lastTime || 0, info.lastTime); if (info.questions && info.questions.length) ch.questions = info.questions; if (info._raw) ch._raw = info._raw; applyChapterDone(ch); } function catalogVideoTimeOf(node) { if (!node || typeof node !== "object") return 0; const raw = node.videoTime ?? node.duration ?? node.totalTime ?? node.mediaLength ?? node.mediaDuration ?? node.length ?? node.timeLength ?? node.playTime ?? node.second ?? node.seconds ?? node.videoLength; const n = parseInt(raw, 10); return Number.isFinite(n) && n > 0 ? n : 0; } function catalogSofarOf(node) { if (!node || typeof node !== "object") return 0; const lr = node.learnRecord && typeof node.learnRecord === "object" ? node.learnRecord : null; const raw = (lr && (lr.accumulativeTimesofar ?? lr.accumulativeTime)) ?? node.accumulativeTimesofar ?? node.accumulativeTime ?? node.learnedTime ?? node.studyTime ?? node.learnTime ?? node.studiedTime ?? node.finishTime; const n = parseInt(raw, 10); return Number.isFinite(n) && n > 0 ? n : 0; } function catalogLastTimeOf(node) { if (!node || typeof node !== "object") return 0; const lr = node.learnRecord && typeof node.learnRecord === "object" ? node.learnRecord : null; const raw = (lr && lr.lastTime) ?? node.lastTime; const n = parseInt(raw, 10); return Number.isFinite(n) && n > 0 ? n : 0; } function computeChapterDone(ch) { if (!ch) return false; const vt = Number(ch.videoTime) || 0; if (vt <= 0) return !!ch.done; const last = Number(ch.lastTime) || 0; const sofar = Number(ch.sofar) || 0; return last >= vt - 2 || sofar >= vt - 2; } function applyChapterDone(ch) { if (!ch) return false; ch.done = computeChapterDone(ch); return ch.done; } function refreshAllChapterDone(chapters) { (chapters || []).forEach(applyChapterDone); } async function enrichChaptersProgress(session, chapters) { const list = (chapters || []).filter((ch) => ch && ch.catalogId && !computeChapterDone(ch)); if (!list.length) return 0; const cap = Math.min(list.length, 80); dbg(`\u8865\u62c9\u7ae0\u8282\u8fdb\u5ea6 ${cap}/${list.length} \u4e2a…`); let marked = 0; for (let i = 0; i < cap; i++) { const ch = list[i]; try { const info = await fetchCatalogInfo(session, ch.catalogId); if (info) { mergeChapterFromInfo(ch, info); if (ch.done) marked += 1; } } catch (_) {} await sleep(50); } if (marked) dbg(`\u8fdb\u5ea6\u8865\u62c9\u540e\u65b0\u8bc6\u522b\u5df2\u6ee1 ${marked} \u8282`); return marked; } async function enrichChapterTitles(session, chapters) { const list = (chapters || []).filter( (ch) => ch && ch.catalogId && isBareCatalogTitle(ch.catalogTitle, ch.catalogId) ); if (!list.length) return 0; const cap = Math.min(list.length, 80); dbg(`\u8865\u62c9\u7ae0\u8282\u540d\u79f0 ${cap} \u4e2a…`); let fixed = 0; for (let i = 0; i < cap; i++) { const ch = list[i]; try { const info = await fetchCatalogInfo(session, ch.catalogId); if (info) { const before = ch.catalogTitle; mergeChapterFromInfo(ch, info); if (ch.catalogTitle !== before && !isBareCatalogTitle(ch.catalogTitle, ch.catalogId)) fixed += 1; } } catch (_) {} await sleep(40); } if (fixed) dbg(`\u7ae0\u8282\u540d\u79f0\u8865\u62c9\u6210\u529f ${fixed} \u4e2a`); return fixed; } function countPendingChapters(chapters) { return (chapters || []).filter((ch) => ch && !computeChapterDone(ch)).length; } function isLikelyVideoNode(node) { if (!node || typeof node !== "object") return false; const type = String(node.type || node.resourceType || node.nodeType || node.catalogType || node.mediaType || "").toLowerCase(); if (/video|media|mp4|vod|sco|leaf|courseware/.test(type)) return true; if (node.leaf === true || node.isLeaf === true || node.isVideo === true) return true; if (catalogVideoTimeOf(node) > 0) return true; const id = catalogIdOf(node); if (id && /^\d{10,}$/.test(id) && !catalogChildren(node)) return true; return false; } function describeCatalogPayload(j) { try { if (!j || typeof j !== "object") return String(j); const d = j.data; if (d == null) return `success=${j.success} code=${j.code} data=null msg=${j.message || ""}`; if (Array.isArray(d)) { const first = d[0]; return `success=${j.success} code=${j.code} data=array(${d.length}) [0]keys=${ first && typeof first === "object" ? Object.keys(first).join(",") : typeof first }`; } if (typeof d === "object") { return `success=${j.success} code=${j.code} data.keys=${Object.keys(d).join(",")} msg=${j.message || ""}`; } return `success=${j.success} dataType=${typeof d}`; } catch (e) { return String(e.message || e); } } async function fetchCatalogInfo(session, catalogId) { const { j, status, text } = await gmJson({ method: "POST", url: `${CWS}/client/catalogInfo/${encodeURIComponent(catalogId)}`, headers: { "Content-Type": "application/json;charset=UTF-8", Accept: "application/json", "X-Requested-With": "XMLHttpRequest", Referer: `${CWS}/page/client`, Origin: CWS, }, data: JSON.stringify({ clientCode: session.clientCode, userId: session.userId, isQuestion: true, coursewareCode: session.coursewareCode, }), }); if (status < 200 || status >= 300) return null; const data = j && j.data; const questions = extractCatalogQuestions(data); const flat = []; flattenCatalogs(j, flat, 0); if (flat.length) { flat[0].questions = questions; flat[0]._raw = data || flat[0]._raw; if (data) { const sofar = catalogSofarOf(data); const lastTime = catalogLastTimeOf(data); if (sofar > (flat[0].sofar || 0)) flat[0].sofar = sofar; if (lastTime > (flat[0].lastTime || 0)) flat[0].lastTime = lastTime; const title = resolveCatalogTitle(data, catalogId); if (title) flat[0].catalogTitle = title; applyChapterDone(flat[0]); } return flat[0]; } if (data && typeof data === "object") { const videoTime = catalogVideoTimeOf(data) || catalogVideoTimeOf(data.media || {}) || 0; const title = resolveCatalogTitle(data, catalogId); const sofar = catalogSofarOf(data); const lastTime = catalogLastTimeOf(data); if (videoTime > 0 || title || questions.length) { return { catalogId: String(catalogId), catalogTitle: title || fallbackChapterTitle({ _raw: data, catalogId: String(catalogId) }, 0), videoTime, sofar, lastTime, done: computeChapterDone({ videoTime, sofar, lastTime }), questions, _raw: data, }; } } if (!j) dbg(`catalogInfo ${catalogId} \u7a7a\u54cd\u5e94: ${String(text).slice(0, 120)}`); return null; } function pickCorrectFromQuestion(q) { if (!q || typeof q !== "object") return null; const opts = Array.isArray(q.optionList) ? q.optionList : []; const hit = opts.find((o) => o && (o.correct === true || o.correct === "true" || o.correct === 1)); if (!hit) return null; const stem = String(q.questionStem || "") .replace(/<[^>]+>/g, "") .replace(/\s+/g, " ") .trim() .slice(0, 36); return { questionId: q.questionId, questionType: q.questionType, examinePoint: q.examinePoint || "", mediaTime: parseInt(q.mediaTime, 10) || 0, respond: String(hit.quesValue || ""), optionId: hit.optionId || "", stem: stem || String(q.questionId), }; } function extractCatalogQuestions(data) { if (!data || !Array.isArray(data.questions)) return []; return data.questions.map(pickCorrectFromQuestion).filter(Boolean); } async function submitKnowledgeAnswers(session, chapter, playSec, answers) { if (!session || !chapter || !answers || !answers.length) return 0; let okN = 0; const lastTime = Math.max(1, parseInt(playSec, 10) || 1); for (const q of answers) { try { const rec = buildRecord(session, chapter, 0, lastTime); rec._method = "PUT"; const qres = await gmJson({ method: "POST", url: `${CWS}/client/learnRecord/question`, headers: { "Content-Type": "application/json; charset=UTF-8", Accept: "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", Referer: `${CWS}/page/client`, Origin: CWS, }, data: JSON.stringify(rec), }); const base = qres.j && qres.j.data && typeof qres.j.data === "object" ? qres.j.data : null; if (!base) { dbg(`\u77e5\u8bc6\u70b9\u9898 cws \u5931\u8d25\uff1a${(qres.text || "").slice(0, 120) || qres.status}`); continue; } const payload = Object.assign({}, base, { examinePoint: q.examinePoint || "", pass: true, questionId: q.questionId, questionType: q.questionType, respond: "", }); const ok = await postBackUrl(payload); if (ok) okN += 1; dbg(`\u77e5\u8bc6\u70b9\u9898${ok ? "✓" : "✗"} ${q.respond || "?"} · ${q.stem}`); } catch (e) { dbg(`\u77e5\u8bc6\u70b9\u9898\u5f02\u5e38\uff1a${e.message || e}`); } await sleep(200); } return okN; } function flattenCatalogs(node, out, depth) { if (node == null || depth > 50) return; if (typeof node === "string") { const s = node.trim(); if (!s) return; try { flattenCatalogs(JSON.parse(s), out, depth + 1); } catch (_) {} return; } if (Array.isArray(node)) { node.forEach((n) => flattenCatalogs(n, out, depth + 1)); return; } if (typeof node !== "object") return; const kids = catalogChildren(node); Object.keys(node).forEach((k) => { const v = node[k]; if (Array.isArray(v)) flattenCatalogs(v, out, depth + 1); else if (v && typeof v === "object" && (k === "data" || k === "result" || k === "tree" || k === "root")) { flattenCatalogs(v, out, depth + 1); } }); const id = catalogIdOf(node); if (!id) return; if (kids && kids.length) return; // 目录节点,只要叶子 const title = resolveCatalogTitle(node, id); const videoTime = catalogVideoTimeOf(node); const sofar = catalogSofarOf(node); const lastTime = catalogLastTimeOf(node); const likely = isLikelyVideoNode(node) || !!title || videoTime > 0 || /^\d{8,}$/.test(id); if (!likely) return; out.push({ catalogId: id, catalogTitle: title || fallbackChapterTitle({ _raw: node, catalogId: id }, out.length), videoTime, sofar, lastTime, done: computeChapterDone({ videoTime, sofar, lastTime }), _raw: node, }); } async function loadChaptersForCourse(course) { const session = await openLearnSession(course); try { const warm = `${CWS}/client/user/learn?businessLineCode=` + `&coursewareCode=${encodeURIComponent(session.coursewareCode)}` + `&courseCodeN=&catalogId=` + `&clientCode=${encodeURIComponent(session.clientCode)}` + `&userId=${encodeURIComponent(session.userId)}` + `&userName=${encodeURIComponent(session.userName || "")}` + `&lastTime=×tamp=${Date.now()}`; await gmRequest({ method: "GET", url: warm, timeout: 45000 }); await gmRequest({ method: "GET", url: `${CWS}/page/client`, timeout: 30000 }); } catch (e) { dbg(`learn \u9884\u70ed\u8b66\u544a\uff1a${e.message || e}`); } const postCatalogs = async () => gmJson({ method: "POST", url: `${CWS}/client/catalogs`, headers: { "Content-Type": "application/json;charset=UTF-8", Accept: "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", Referer: `${CWS}/page/client`, Origin: CWS, }, data: JSON.stringify({ coursewareCode: session.coursewareCode, clientCode: session.clientCode, userId: session.userId, }), }); let cat = await postCatalogs(); if (cat.status < 200 || cat.status >= 300) throw new Error(`catalogs HTTP ${cat.status}`); if (cat.j && typeof cat.j.data === "string") { try { cat.j.data = JSON.parse(cat.j.data); } catch (_) {} } let chapters = parseEduCatalogFlatList(cat.j && cat.j.data); if (!chapters.length) { const flat = []; flattenCatalogs(cat.j, flat, 0); const map = new Map(); flat.forEach((ch) => { if (!map.has(ch.catalogId)) map.set(ch.catalogId, ch); }); chapters = [...map.values()]; } const needInfo = chapters.filter((c) => !c.videoTime); if (needInfo.length && needInfo.length <= 100) { dbg(`\u8865\u62c9\u7ae0\u8282\u8be6\u60c5 ${needInfo.length} \u4e2a…`); for (const ch of needInfo) { try { const info = await fetchCatalogInfo(session, ch.catalogId); if (info) { ch.videoTime = info.videoTime || ch.videoTime; ch.catalogTitle = info.catalogTitle || ch.catalogTitle; ch.sofar = info.sofar || ch.sofar; if (info.lastTime != null) ch.lastTime = info.lastTime; applyChapterDone(ch); } } catch (_) {} await sleep(60); } } if (!chapters.length) { const ok = cat.j && (cat.j.success === true || cat.j.success === "true" || cat.j.code === 0 || cat.j.code === "0"); const data = cat.j && cat.j.data; const dataLen = Array.isArray(data) ? data.length : data == null ? "null" : typeof data; dbg(`\u7ae0\u8282\u4e3a\u7a7a · success=${cat.j && cat.j.success} code=${cat.j && cat.j.code} dataLen=${dataLen} msg=${(cat.j && cat.j.message) || ""}`); dbg("\u7ed3\u6784 · " + describeCatalogPayload(cat.j)); try { const sample = JSON.stringify(data != null ? data : cat.j).slice(0, 500); dbg("data\u6837\u4f8b: " + sample); } catch (_) {} try { const cw = await gmJson({ method: "GET", url: `${CWS}/client/${encodeURIComponent(session.coursewareCode)}/findCwByCode?userId=${encodeURIComponent( session.userId )}&clientCode=${encodeURIComponent(session.clientCode)}`, }); chapters = parseEduCatalogFlatList(cw.j && cw.j.data); if (!chapters.length) { const flat2 = []; flattenCatalogs(cw.j, flat2, 0); chapters = flat2; } if (chapters.length) { dbg(`\u4ece findCwByCode \u89e3\u6790\u5230 ${chapters.length} \u4e2a\u8282\u70b9`); } else { dbg("findCwByCode \u6837\u4f8b: " + JSON.stringify(cw.j && cw.j.data != null ? cw.j.data : cw.j).slice(0, 300)); } } catch (e) { dbg(`findCwByCode \u5931\u8d25\uff1a${e.message || e}`); } if (!ok || dataLen === 0 || dataLen === "null") { dbg("catalogs \u53ef\u80fd\u672a\u6388\u6743\u6216\u4f1a\u8bdd\u672a\u843d\u5730\uff1a\u8bf7\u5141\u8bb8\u6cb9\u7334\u8fde\u63a5 cws.edu-edu.com\uff0c\u6216\u5148\u624b\u52a8\u8fdb\u4e00\u6b21\u64ad\u653e\u9875\u518d\u56de\u95e8\u6237"); } } else { const withTime = chapters.filter((c) => c.videoTime > 0); if (withTime.length) chapters = withTime; dbg(`\u7ae0\u8282 ${chapters.length} \u4e2a\uff08\u6709\u65f6\u957f ${chapters.filter((c) => c.videoTime > 0).length}\uff09`); } refreshAllChapterDone(chapters); await enrichChaptersProgress(session, chapters); await enrichChapterTitles(session, chapters); refreshAllChapterDone(chapters); chapters.forEach((ch, idx) => { if (isBareCatalogTitle(ch.catalogTitle, ch.catalogId)) { ch.catalogTitle = fallbackChapterTitle(ch, idx); } }); state.chapters = chapters; state.activeCourse = course; renderChapterList(); updatePanel(); return state.chapters; } function buildRecord(session, chapter, accumulativeTime, lastTime) { const rec = { learnRecordId: 0, userId: session.userId, userName: session.userName, clientCode: session.clientCode, businessLineCode: "", coursewareCode: session.coursewareCode, catalogId: String(chapter.catalogId), catalogTitle: chapter.catalogTitle || "", classNum: session.classNum || "", coursewareName: session.coursewareName || "", videoTime: String(chapter.videoTime || 0), backUrl: session.backUrl || BACK_URL_DEFAULT, validTime: 0, accumulativeTime, lastTime, deviceType: "TYPE_PC", orderNo: "", }; if (session.maxTimePerDay > 0) rec.maxTimePerDay = session.maxTimePerDay; const sig = signLearnRecord(rec); rec.md5 = sig.md5; rec.md5Timestamp = sig.md5Timestamp; return rec; } async function putLearnCws(kind, rec) { const path = kind === "start" ? "/client/learnRecords/start" : "/client/learnRecords/timing"; const { res, j, text, status } = await gmJson({ method: "PUT", url: CWS + path, headers: { "Content-Type": "application/json;charset=UTF-8", Accept: "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", }, data: JSON.stringify(rec), }); return { j, text, status, res }; } async function postBackUrl(data) { const url = data.backUrl || BACK_URL_DEFAULT; const payload = Object.assign({}, data); if (!payload.backUrl) payload.backUrl = BACK_URL_DEFAULT; try { const { status, text, j } = await gmJson({ method: "POST", url, headers: { "Content-Type": "application/json; charset=utf-8", Accept: "application/json, text/javascript, */*; q=0.01", }, data: JSON.stringify(payload), }); const okHttp = status >= 200 && status < 300; const okBody = j == null || j.success === true || j.success === "true" || /\u540c\u6b65\u6210\u529f|success/i.test(String((j && j.message) || text || "")); return okHttp && okBody; } catch (e) { dbg(`LearnReturnUrl \u5931\u8d25\uff1a${e.message || e}`); return false; } } async function syncChapterProgressToPortal(session, chapter) { const vt = chapter.videoTime || 0; let tipSofar = chapter.sofar || 0; let tipLast = chapter.lastTime || 0; try { const info = await fetchCatalogInfo(session, chapter.catalogId); if (info) { if (info.videoTime > 0) chapter.videoTime = info.videoTime; if (info.sofar != null) { chapter.sofar = info.sofar; tipSofar = info.sofar; } if (info.lastTime != null) { chapter.lastTime = info.lastTime; tipLast = info.lastTime; } if (info.done) chapter.done = true; const lr = info._raw && info._raw.learnRecord; if (lr && lr.lastTime != null) tipLast = Math.max(tipLast, Number(lr.lastTime) || 0); } } catch (_) {} const vtNow = chapter.videoTime || vt; const playAt = vtNow > 0 ? vtNow : Math.max(1, tipLast, tipSofar || 1); let data = null; try { const startRec = buildRecord(session, chapter, 0, Math.max(1, tipLast || 1)); const s = await putLearnCws("start", startRec); if (s.j && s.j.data) data = s.j.data; if (vtNow > 0 && playAt >= vtNow - 1) { const seal = buildRecord(session, chapter, 0, vtNow); const t = await putLearnCws("timing", seal); if (t.j && t.j.data) data = t.j.data; } else if (vtNow > 0 && tipLast < vtNow - 1) { const seal = buildRecord(session, chapter, 0, vtNow); const t = await putLearnCws("timing", seal); if (t.j && t.j.data) data = t.j.data; } } catch (e) { dbg(`\u8865\u540c\u6b65 start/seal \u5931\u8d25\uff1a${e.message || e}`); } if (!data) { data = Object.assign(buildRecord(session, chapter, 0, playAt), { accumulativeTimesofar: String(tipSofar || playAt), lastTime: String(playAt), videoTime: String(vtNow || playAt), success: true, }); } const ok = await postBackUrl(data); const sofar = data.accumulativeTimesofar != null ? data.accumulativeTimesofar : tipSofar; const last = data.lastTime != null ? data.lastTime : playAt; dbg( `\u95e8\u6237\u8865\u540c\u6b65${ok ? "✓" : "✗"} · ${chapter.catalogTitle} · sofar=${sofar} last=${last}/${data.videoTime || vtNow}` ); if (data.accumulativeTimesofar != null) { chapter.sofar = parseInt(data.accumulativeTimesofar, 10) || chapter.sofar; } if (data.lastTime != null) chapter.lastTime = Number(data.lastTime) || chapter.lastTime; if (vtNow > 0 && (chapter.lastTime || 0) >= vtNow - 2) chapter.done = true; return ok; } async function sealLastTimeToVideoEnd(session, chapter, fromLast) { const vt = chapter.videoTime || 0; if (vt <= 0) return null; let last = Math.max(0, Number(fromLast) || 0); if (last >= vt - 1) { const seal = buildRecord(session, chapter, 0, vt); const t = await putLearnCws("timing", seal); if (t.j && t.j.data) { await postBackUrl(t.j.data); return t.j.data; } return null; } const sofarFull = (chapter.sofar || 0) >= vt - 2; const step = getStepSec(); while (state.running && last < vt) { const nextLast = Math.min(vt, last + step); const accum = sofarFull ? 0 : Math.min(step, nextLast - last || step); const rec = buildRecord(session, chapter, accum, nextLast); try { const t = await putLearnCws("timing", rec); if (t.j && t.j.data) { last = nextLast; chapter.lastTime = last; if (t.j.data.accumulativeTimesofar != null) { chapter.sofar = parseInt(t.j.data.accumulativeTimesofar, 10) || chapter.sofar; } await postBackUrl(t.j.data); dbg(`\u5c01\u8fdb\u5ea6 ${chapter.catalogTitle} → ${formatDuration(last)}/${formatDuration(vt)}`); } } catch (e) { dbg(`\u5c01\u8fdb\u5ea6\u5931\u8d25\uff1a${e.message || e}`); break; } await sleep(Math.min(getGapMs(), 800)); } if (state.running && last < vt) { const seal = buildRecord(session, chapter, sofarFull ? 0 : Math.max(1, vt - last), vt); const t = await putLearnCws("timing", seal); if (t.j && t.j.data) { await postBackUrl(t.j.data); last = vt; chapter.lastTime = vt; return t.j.data; } } chapter.lastTime = last; return null; } async function loadChapterKnowledgeQs(session, chapter) { let knowledgeQs = Array.isArray(chapter.questions) ? chapter.questions.slice() : []; if (knowledgeQs.length) return knowledgeQs; try { const info = await fetchCatalogInfo(session, chapter.catalogId); if (info) { mergeChapterFromInfo(chapter, info); if (info.questions && info.questions.length) { knowledgeQs = info.questions; chapter.questions = knowledgeQs; } } } catch (e) { dbg(`\u62c9\u77e5\u8bc6\u70b9\u9898\u5931\u8d25\uff1a${e.message || e}`); } return knowledgeQs.length ? knowledgeQs : Array.isArray(chapter.questions) ? chapter.questions : []; } async function runChapterKnowledgeOnly(session, chapter) { const vt = chapter.videoTime || 0; state.catalogTitle = chapter.catalogTitle; state.videoTime = vt; updatePanel(); let pos = chapter.lastTime || 0; if (vt > 0 && pos < vt - 1) { dbg(`\u8865\u5c01\u64ad\u653e\u8fdb\u5ea6 lastTime ${pos} → ${vt}\uff1a${chapter.catalogTitle}`); await sealLastTimeToVideoEnd(session, chapter, pos); pos = chapter.lastTime || vt; } state.lastTime = Math.max(pos, vt); await syncChapterProgressToPortal(session, chapter); const knowledgeQs = await loadChapterKnowledgeQs(session, chapter); if (!knowledgeQs.length) { dbg(`\u5df2\u5230\u7247\u5c3e · \u65e0\u77e5\u8bc6\u70b9\u9898\uff1a${chapter.catalogTitle}`); chapter.done = true; return true; } dbg(`\u8865\u4ea4\u77e5\u8bc6\u70b9 ${knowledgeQs.length} \u9053\uff1a${chapter.catalogTitle}`); await submitKnowledgeAnswers(session, chapter, Math.max(vt, chapter.lastTime || 0, 1), knowledgeQs); chapter.done = true; return true; } async function runChapterApi(session, chapter) { const gap = getGapMs(); const step = getStepSec(); const vt = chapter.videoTime || 0; if (!isProUser() && state.freeUsedVideos >= state.freeVideoLimit) { state.lastChapterFail = "quota"; trace(`\u514d\u8d39\u6b21\u6570\u7528\u5b8c\u4e86\uff08${state.freeUsedVideos}/${state.freeVideoLimit}\uff09\uff0c\u5f00\u901a\u4f1a\u5458\u540e\u53ef\u7ee7\u7eed\u770b\u8bfe`); return false; } trace(`\u51c6\u5907\u5b66\u4e60\u300c${chapterDisplayTitle(chapter)}\u300d…`); await loadChapterKnowledgeQs(session, chapter); applyChapterDone(chapter); if (chapter.done) { dbg(`\u7ae0\u8282\u5df2\u6ee1\uff0c\u8df3\u8fc7\uff1a${chapter.catalogTitle}`); return true; } let lastTime = Math.min(chapter.lastTime || 0, vt); if (lastTime < 1) lastTime = 0; state.catalogTitle = chapter.catalogTitle; state.videoTime = vt; state.lastTime = lastTime; state.sofar = chapter.sofar || 0; state.tick = 0; state.stall = 0; updatePanel(); if (vt <= 0) { const qs = chapter.questions || []; if (qs.length) { dbg(`\u65e0\u89c6\u9891\u65f6\u957f\uff0c\u4ec5\u4ea4\u77e5\u8bc6\u70b9 ${qs.length} \u9053\uff1a${chapter.catalogTitle}`); await submitKnowledgeAnswers(session, chapter, 1, qs); } else { dbg(`\u8df3\u8fc7\u65e0\u65f6\u957f\u7ae0\u8282\uff1a${chapter.catalogTitle}`); } return true; } const sofarFull = (chapter.sofar || 0) >= vt - 2; const posDone = lastTime >= vt - 1; if (posDone) { return runChapterKnowledgeOnly(session, chapter); } if (sofarFull) { dbg(`\u7d2f\u8ba1\u5df2\u6ee1\u4f46\u8fdb\u5ea6\u672a\u5230\u5934 last=${lastTime}/${vt}\uff0c\u8865\u5c01\uff1a${chapter.catalogTitle}`); return runChapterKnowledgeOnly(session, chapter); } const knowledgeQs = Array.isArray(chapter.questions) ? chapter.questions.slice() : []; const answeredQ = new Set(); try { await cloudConsumeChapter(chapter); state.lastChapterFail = ""; trace(`\u6b63\u5728\u5b66\u4e60\u300c${chapterDisplayTitle(chapter)}\u300d…`); } catch (e) { state.lastChapterFail = isQuotaError(e) ? "quota" : "auth"; trace(friendlyCloudMessage(e)); return false; } if (knowledgeQs.length) dbg(`\u672c\u7ae0\u77e5\u8bc6\u70b9\u9898 ${knowledgeQs.length} \u9053`); const flushDueQuestions = async (playSec) => { if (!knowledgeQs.length) return; const due = knowledgeQs.filter( (q) => q && q.questionId != null && !answeredQ.has(String(q.questionId)) && playSec >= (q.mediaTime || 0) ); if (!due.length) return; await submitKnowledgeAnswers(session, chapter, playSec, due); due.forEach((q) => answeredQ.add(String(q.questionId))); }; const startRec = buildRecord(session, chapter, 0, Math.max(1, lastTime || 1)); try { const s = await putLearnCws("start", startRec); if (s.j && s.j.data) { if (s.j.data.lastTime != null) { const srv = Number(s.j.data.lastTime) || 0; lastTime = Math.max(lastTime, srv); } if (s.j.data.accumulativeTimesofar != null) state.sofar = parseInt(s.j.data.accumulativeTimesofar, 10) || state.sofar; await postBackUrl(s.j.data); await flushDueQuestions(lastTime); } dbg(`start OK · ${chapter.catalogTitle} · last=${lastTime}/${vt}`); } catch (e) { dbg(`start \u5f02\u5e38\uff1a${e.message || e}\uff08\u7ee7\u7eed timing\uff09`); } while (state.running && lastTime < vt) { const nextLast = Math.min(vt, lastTime + step); const accum = Math.min(step, nextLast - lastTime || step); const rec = buildRecord(session, chapter, accum, nextLast); try { const t = await putLearnCws("timing", rec); state.tick += 1; const ok = t.j && (t.j.code === 0 || t.j.success === true || t.j.success === "true" || (t.j.data && (t.j.data.lastTime != null || t.j.data.accumulativeTimesofar != null))); if (!ok) { state.stall += 1; dbg(`timing \u5931\u8d25\uff1a${(t.text || "").slice(0, 120) || t.status}`); if (String(t.j && t.j.code) === "-1") { dbg("\u5355\u65e5\u4e0a\u9650\uff0c\u505c\u6b62"); trace("\u4eca\u5929\u5b66\u65f6\u5230\u4e0a\u9650\u4e86\uff0c\u660e\u5929\u518d\u7ee7\u7eed\u5427"); return false; } if (state.stall >= 6) return false; } else { state.stall = 0; lastTime = nextLast; state.lastTime = lastTime; chapter.lastTime = lastTime; if (t.j.data) { if (t.j.data.accumulativeTimesofar != null) { state.sofar = parseInt(t.j.data.accumulativeTimesofar, 10) || state.sofar; } await postBackUrl(t.j.data); await flushDueQuestions(lastTime); } chapter.sofar = state.sofar; dbg(`#${state.tick} ${chapter.catalogTitle} → ${formatDuration(lastTime)}/${formatDuration(vt)}`); } } catch (e) { state.stall += 1; dbg(`timing \u5f02\u5e38\uff1a${e.message || e}`); if (state.stall >= 6) return false; } updatePanel(); renderChapterList(); await sleep(gap); } if (state.running && vt > 0) { try { const seal = buildRecord(session, chapter, 0, vt); const t = await putLearnCws("timing", seal); if (t.j && t.j.data) { await postBackUrl(t.j.data); lastTime = vt; chapter.lastTime = vt; if (t.j.data.accumulativeTimesofar != null) { chapter.sofar = parseInt(t.j.data.accumulativeTimesofar, 10) || chapter.sofar; } dbg(`\u7247\u5c3e\u5c01\u9876 lastTime=${vt} · ${chapter.catalogTitle}`); } else { lastTime = Math.max(lastTime, vt); chapter.lastTime = lastTime; dbg(`\u7247\u5c3e\u5c01\u9876\u65e0 data · ${chapter.catalogTitle}`); } } catch (e) { dbg(`\u7247\u5c3e\u5c01\u9876\u5931\u8d25\uff1a${e.message || e}`); lastTime = Math.max(lastTime, vt); chapter.lastTime = lastTime; } let synced = false; for (let i = 0; i < 3 && state.running; i++) { try { synced = await syncChapterProgressToPortal(session, chapter); if (synced) break; } catch (e) { dbg(`\u5b66\u5b8c\u63a8\u95e8\u6237\u5931\u8d25 try${i + 1}\uff1a${e.message || e}`); } await sleep(600); } if (!synced) dbg(`\u95e8\u6237\u540c\u6b65\u672a\u786e\u8ba4 · ${chapter.catalogTitle}\uff08\u7a0d\u540e\u53ef\u518d\u8dd1\u4e00\u904d\u8865\u5206\u949f\uff09`); } if (state.running && knowledgeQs.length) { const left = knowledgeQs.filter((q) => q && !answeredQ.has(String(q.questionId))); if (left.length) await flushDueQuestions(vt); } chapter.done = (chapter.lastTime || lastTime) >= vt - 1 || lastTime >= vt - 1; if (chapter.done) { await sleep(800); trace(`\u300c${chapterDisplayTitle(chapter)}\u300d\u5b66\u5b8c\u4e86`); state.lastChapterFail = ""; } else { state.lastChapterFail = "progress"; trace(`\u300c${chapterDisplayTitle(chapter)}\u300d\u8fdb\u5ea6\u8fd8\u6ca1\u62a5\u5230\u5934`); } return chapter.done; } async function startPortalResyncOnly() { if (state.running) return; if (!isEduXl) { dbg("\u8bf7\u5728 edu-xl \u95e8\u6237\u9875\u4f7f\u7528\u300c\u8865\u540c\u6b65\u95e8\u6237\u300d"); return; } const checkedCourses = getCheckedCourses(); if (!checkedCourses.length) { dbg("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b"); return; } if (!readFaceCache()) { try { dbg("\u8865\u540c\u6b65\u524d\u5148\u91c7\u96c6\u4eba\u8138…"); await captureFaceToCache(); } catch (e) { dbg(`\u4eba\u8138\u91c7\u96c6\u53d6\u6d88/\u5931\u8d25\uff1a${e.message || e}`); return; } } state.running = true; updatePanel(); try { for (const course of checkedCourses) { if (!state.running) break; await loadChaptersForCourse(course); const chans = getCheckedChapters(); const targets = chans.length ? chans : state.chapters.slice(); if (!targets.length) { dbg(`${course.title} \u65e0\u7ae0\u8282\u53ef\u540c\u6b65`); continue; } dbg(`${course.title} \u8865\u540c\u6b65\u95e8\u6237 ${targets.length} \u7ae0\uff08\u4e0d\u5237\u65b0\u5b66\u65f6\uff0c\u53ea\u63a8 cws→\u95e8\u6237\uff09`); for (const ch of targets) { if (!state.running) break; try { await syncChapterProgressToPortal(state.session, ch); } catch (e) { dbg(`\u540c\u6b65\u5f02\u5e38 ${ch.catalogTitle}\uff1a${e.message || e}`); } await sleep(250); } try { const detail = await fetchScoreDetail(course); if (detail) { course.scoreDetail = detail; renderCourseList(); } } catch (_) {} } dbg("\u8865\u540c\u6b65\u7ed3\u675f · \u8bf7\u770b\u8bfe\u7a0b\u5b66\u4e60\u5206\u949f\u662f\u5426\u56de\u5347"); } catch (e) { dbg(`\u8865\u540c\u6b65\u5f02\u5e38\uff1a${e.message || e}`); } finally { state.running = false; updatePanel(); renderChapterList(); } } async function refreshCourseScore(course) { try { const detail = await fetchScoreDetail(course); if (detail) { course.scoreDetail = detail; if (detail.studyMin != null) course.learnedMin = detail.studyMin; if (detail.studyNeed != null) course.needMin = detail.studyNeed; renderCourseList(); } return detail; } catch (_) { return course.scoreDetail || null; } } async function runCourseHomeworkPhase(course, kind) { const label = kind.msgTitle; if (!canDoExamWork()) { trace(denyExamWorkMsg(label)); return; } const idx = await fetchExamIndex(course, kind); if (!idx.exams.length) { trace(`\u6ca1\u6709\u627e\u5230\u300c${label}\u300d\uff0c\u5148\u8df3\u8fc7`); return; } let i = 0; for (const exam of idx.exams) { if (!state.running) break; i += 1; exam.examType = String(kind.type); if (!exam.buttonName) exam.buttonName = kind.buttonName; const ename = exam.title || `${label} ${i}`; try { trace(`\u6b63\u5728\u505a\u300c${ename}\u300d…`); await runOneStageExam(course, exam); trace(`\u300c${ename}\u300d\u4ea4\u597d\u4e86`); } catch (e) { dbg(`${label}\u5931\u8d25 ${exam.title || exam.jobCode}\uff1a${e.message || e}`); trace(`\u300c${ename}\u300d\u6ca1\u505a\u6210\uff0c\u5148\u8df3\u8fc7`); } await sleep(400); } } async function startPortalRun() { if (state.running) return; state.running = true; updatePanel(); try { if (!isEduXl) { trace("\u8bf7\u5148\u6253\u5f00\u5b66\u6821\u5b66\u4e60\u9996\u9875\u518d\u5f00\u59cb"); return; } const checkedCourses = getCheckedCourses(); if (!checkedCourses.length) { trace("\u8bf7\u5148\u52fe\u9009\u8981\u5b66\u7684\u8bfe\u7a0b"); return; } if (!readFaceCache()) { if (isProUser()) { trace("\u8bf7\u5148\u5728\u4e0a\u65b9\u300c\u91c7\u96c6\u4eba\u8138\u300d\u6216\u300c\u4e0a\u4f20\u7167\u7247\u300d\u51c6\u5907\u597d\u540e\u518d\u5f00\u59cb"); return; } try { trace("\u8fd8\u6ca1\u6709\u4eba\u8138\u7167\u7247\uff0c\u5148\u62cd\u4e00\u5f20\u5427…"); await captureFaceToCache(); trace("\u4eba\u8138\u5df2\u4fdd\u5b58\uff0c\u540e\u9762\u4f1a\u81ea\u52a8\u7528"); } catch (e) { trace("\u4eba\u8138\u91c7\u96c6\u53d6\u6d88\u4e86\uff0c\u7a0d\u540e\u518d\u8bd5"); return; } } try { await ensureCloudLease(true); } catch (e) { trace(friendlyCloudMessage(e)); return; } if (!state.cloudLease) { trace("\u6682\u65f6\u8fde\u4e0d\u4e0a\u6388\u6743\u670d\u52a1\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5"); return; } updateCloudPanelUI(); if (!isProUser()) { trace(`\u514d\u8d39\u8bd5\u7528\u5df2\u7528 ${state.freeUsedVideos}/${state.freeVideoLimit} \u5c0f\u8282`); if (state.freeUsedVideos >= state.freeVideoLimit) { trace("\u514d\u8d39\u6b21\u6570\u5df2\u7528\u5b8c\uff0c\u5f00\u901a\u4f1a\u5458\u540e\u53ef\u7ee7\u7eed\u770b\u8bfe\u548c\u505a\u4f5c\u4e1a"); } } for (const course of checkedCourses) { if (!state.running) break; const cname = course.title || "\u672c\u95e8\u8bfe"; trace(`\u5f00\u59cb\u5b66\u4e60\u300c${cname}\u300d`); let detail = await refreshCourseScore(course); let stoppedByQuota = false; const learnFull = isPartFull(detail, "\u8bfe\u7a0b\u5b66\u4e60"); const kpFull = isPartFull(detail, "\u77e5\u8bc6\u70b9\u6d4b\u8bc4"); if (learnFull && kpFull) { trace("\u8bfe\u65f6\u548c\u77e5\u8bc6\u70b9\u90fd\u6ee1\u4e86\uff0c\u8fd9\u4e00\u6b65\u8df3\u8fc7"); } else { await loadChaptersForCourse(course); try { await ensureCloudLease(true); updateCloudPanelUI(); } catch (_) {} const targets = state.chapters.slice(); refreshAllChapterDone(targets); if (!targets.length) { trace("\u8fd9\u95e8\u8bfe\u8fd8\u6ca1\u6709\u7ae0\u8282"); } else { const pendN = countPendingChapters(targets); const skipN = targets.length - pendN; if (learnFull) { trace(`\u8bfe\u65f6\u5df2\u6ee1\uff0c\u8865\u4e00\u4e0b\u77e5\u8bc6\u70b9\uff08\u5171 ${targets.length} \u8282\uff09`); } else { if (skipN > 0) trace(`\u5df2\u6709 ${skipN} \u8282\u5df2\u6ee1\uff0c\u8df3\u8fc7`); trace(`\u63a5\u7740\u770b\u8bfe\uff0c\u8fd8\u6709 ${pendN} \u8282\u5f85\u5b66`); } for (const ch of targets) { if (!state.running) break; if (learnFull) { try { await runChapterKnowledgeOnly(state.session, ch); } catch (e) { dbg(`\u8865\u77e5\u8bc6\u70b9\u5931\u8d25 ${ch.catalogTitle}\uff1a${e.message || e}`); trace(`\u300c${ch.catalogTitle}\u300d\u77e5\u8bc6\u70b9\u6ca1\u8865\u4e0a\uff0c\u5148\u8df3\u8fc7`); } } else { applyChapterDone(ch); if (ch.done) continue; if (!isProUser() && state.freeUsedVideos >= state.freeVideoLimit) { state.lastChapterFail = "quota"; trace(`\u514d\u8d39\u6b21\u6570\u7528\u5b8c\u4e86\uff08${state.freeUsedVideos}/${state.freeVideoLimit}\uff09\uff0c\u5f00\u901a\u4f1a\u5458\u540e\u53ef\u7ee7\u7eed\u770b\u8bfe`); stoppedByQuota = true; break; } const ok = await runChapterApi(state.session, ch); if (!ok) { if (state.lastChapterFail === "quota" || state.lastChapterFail === "auth") { stoppedByQuota = state.lastChapterFail === "quota"; } else if (state.running) { trace("\u8fd9\u4e00\u8282\u6ca1\u5b66\u5b8c\uff0c\u5148\u505c\u4e00\u4e0b"); } break; } } } } detail = await refreshCourseScore(course); } if (!state.running) break; if (stoppedByQuota) { const total = detail && detail.total != null ? detail.total : "—"; trace(`\u300c${cname}\u300d\u5148\u505c\u5728\u8fd9\u513f\uff08\u514d\u8d39\u6b21\u6570\u7528\u5c3d\uff09\uff0c\u5f53\u524d\u603b\u5206 ${total}`); trace("\u540e\u9762\u52fe\u9009\u7684\u8bfe\u4e5f\u4e0d\u518d\u7ee7\u7eed\uff0c\u5f00\u901a\u4f1a\u5458\u540e\u53ef\u63a5\u7740\u5b66"); state.running = false; break; } if (isPartFull(detail, "\u9636\u6bb5\u4f5c\u4e1a")) { trace("\u9636\u6bb5\u4f5c\u4e1a\u5df2\u6ee1\u5206\uff0c\u8df3\u8fc7"); } else if (!canDoExamWork()) { trace(denyExamWorkMsg("\u9636\u6bb5\u4f5c\u4e1a")); } else { trace("\u5f00\u59cb\u505a\u9636\u6bb5\u4f5c\u4e1a…"); await runCourseHomeworkPhase(course, { type: "0", buttonName: "\u9636\u6bb5\u4f5c\u4e1a", msgTitle: "\u9636\u6bb5\u4f5c\u4e1a", }); detail = await refreshCourseScore(course); if (state.running) trace("\u9636\u6bb5\u4f5c\u4e1a\u505a\u5b8c\u4e86"); } if (!state.running) break; if (isPartFull(detail, "\u7efc\u5408\u6d4b\u8bc4")) { trace("\u7efc\u5408\u6d4b\u8bc4\u5df2\u6ee1\u5206\uff0c\u8df3\u8fc7"); } else if (!canDoExamWork()) { trace(denyExamWorkMsg("\u7efc\u5408\u6d4b\u8bc4")); } else { trace("\u5f00\u59cb\u505a\u7efc\u5408\u6d4b\u8bc4…"); await runCourseHomeworkPhase(course, { type: "2", buttonName: "\u7efc\u5408\u6d4b\u8bc4", msgTitle: "\u7efc\u5408\u6d4b\u8bc4", }); detail = await refreshCourseScore(course); if (state.running) trace("\u7efc\u5408\u6d4b\u8bc4\u505a\u5b8c\u4e86"); } if (isPartFull(detail, "\u5b66\u4e60\u8868\u73b0")) { } else { dbg("\u5b66\u4e60\u8868\u73b0\u672a\u6ee1\uff08\u5e73\u53f0\u81ea\u52a8\u9879\uff09"); } const total = detail && detail.total != null ? detail.total : "—"; trace(`\u300c${cname}\u300d\u5b66\u5b8c\u4e86\uff0c\u5f53\u524d\u603b\u5206 ${total}`); } if (state.running) trace("\u5168\u90e8\u52fe\u9009\u8bfe\u7a0b\u90fd\u8dd1\u5b8c\u4e86"); } catch (e) { dbg(`\u5f02\u5e38\uff1a${e.message || e}`); trace("\u51fa\u4e86\u70b9\u95ee\u9898\uff0c\u5df2\u6682\u505c\uff0c\u770b\u770b\u65e5\u5fd7\u6216\u7a0d\u540e\u518d\u8bd5"); } finally { state.running = false; updatePanel(); renderChapterList(); } } function getCheckedCourses() { const box = document.getElementById("eduxl-course-list"); if (!box) return state.courses.slice(0, 1); const ids = [...box.querySelectorAll(".eduxl-course-check:checked")].map((el) => el.dataset.key); return state.courses.filter((c) => ids.includes(c.courseGUID + "|" + c.courseCode)); } function getCheckedChapters() { const box = document.getElementById("eduxl-chapter-list"); if (!box) return []; const ids = [...box.querySelectorAll(".eduxl-ch-check:checked")].map((el) => el.dataset.id); return state.chapters.filter((c) => ids.includes(c.catalogId)); } function renderCourseList() { const box = document.getElementById("eduxl-course-list"); if (!box) return; if (!state.courses.length) { box.innerHTML = `
\u6b63\u5728\u81ea\u52a8\u62c9\u53d6\u8bfe\u7a0b…
`; return; } const checked = new Set( [...document.querySelectorAll(".eduxl-course-check:checked")].map((el) => el.dataset.key) ); const hadChecks = document.querySelectorAll(".eduxl-course-check").length > 0; box.innerHTML = state.courses .map((c) => { const key = c.courseGUID + "|" + c.courseCode; const keyEsc = esc(key); const d = c.scoreDetail; const isChecked = !hadChecks || checked.has(key) || c._selected !== false; const total = d && d.total != null && d.total !== "" && d.total !== "-1" ? String(d.total) : null; const totalBadge = total != null ? `\u603b\u5206 ${esc(total)}` : ""; let scoreHtml = `
\u6210\u7ee9\u52a0\u8f7d\u4e2d…
`; if (d && d.rows && d.rows.length) { const rows = d.rows .map((r) => { const fullOk = isScoreFull(r.score, r.full); const bar = scoreBarPct(r.score, r.full); const short = shortPartName(r.name); const pctLabel = r.pct != null ? `${r.pct}%` : "—"; const prog = r.progress ? `${esc(r.progress)}` : ""; const got = r.score === "—" || r.score == null ? `—/${esc(r.full)}` : `${esc(r.score)}/${esc(r.full)}`; return `
${esc(short)}${esc(pctLabel)} ${got}${fullOk ? '\u6ee1' : ""}
${prog}
`; }) .join(""); scoreHtml = `
${rows}
`; } else if (c.needMin || c.learnedMin) { scoreHtml = `
${c.learnedMin || 0}/${c.needMin || "?"} \u5206\u949f
`; } return `
${scoreHtml}
`; }) .join(""); box.querySelectorAll("[data-load-ch]").forEach((btn) => { btn.addEventListener("click", async (e) => { e.preventDefault(); e.stopPropagation(); const key = btn.getAttribute("data-load-ch"); const course = state.courses.find((c) => c.courseGUID + "|" + c.courseCode === key); if (!course) return; try { btn.disabled = true; await loadChaptersForCourse(course); switchTab("chapter"); } catch (err) { dbg(`\u62c9\u7ae0\u8282\u5931\u8d25\uff1a${err.message || err}`); } finally { btn.disabled = false; } }); }); } function renderChapterList() { const box = document.getElementById("eduxl-chapter-list"); if (!box) return; const courseTitle = state.activeCourse?.title || state.title || ""; if (!state.chapters.length) { box.innerHTML = `
\u5728\u300c\u8bfe\u7a0b\u5217\u8868\u300d\u70b9\u67d0\u8bfe\u7684\u300c\u7ae0\u8282\u300d\u9884\u89c8
`; return; } const head = courseTitle ? `
${esc(courseTitle)}
` : ""; const items = state.chapters .map((ch, idx) => { const disp = chapterDisplayTitle(ch, idx); if (disp !== ch.catalogTitle && !isBareCatalogTitle(disp, ch.catalogId)) ch.catalogTitle = disp; const done = computeChapterDone(ch) || !!ch.done; if (done) ch.done = true; const pct = ch.videoTime > 0 ? Math.min(100, Math.round((Number(ch.sofar || 0) / ch.videoTime) * 100)) : done ? 100 : 0; const mark = done ? "✓" : pct > 0 ? "…" : "•"; const cls = done ? "done" : pct > 0 ? "partial" : ""; return `
${mark}${esc(disp)} ${done ? "\u5df2\u5b66" : pct + "%"} · ${formatDuration(ch.sofar)}/${formatDuration(ch.videoTime)}
`; }) .join(""); box.innerHTML = `${head}
${items}
`; } function rememberRecord(obj) { if (!obj || !obj.userId || !obj.coursewareCode) return; state.learnRecord = Object.assign({}, obj); state.title = obj.coursewareName || state.title; state.catalogTitle = obj.catalogTitle || state.catalogTitle; state.videoTime = parseInt(obj.videoTime, 10) || state.videoTime; state.lastTime = Number(obj.lastTime) || state.lastTime; updatePanel(); } function installNetworkSniffer() { if (!isEduEdu) return; const grab = (body) => { try { const j = typeof body === "string" ? JSON.parse(body) : body; if (j && j.userId && j.coursewareCode) rememberRecord(j); } catch (_) {} }; const rawFetch = WIN.fetch; if (typeof rawFetch === "function") { WIN.fetch = function (input, init) { try { const url = String(typeof input === "string" ? input : input && input.url); if (/learnRecords\/(start|timing)/i.test(url) && init && init.body) grab(init.body); } catch (_) {} return rawFetch.apply(this, arguments); }; } } async function startPlayerAssist() { state.running = true; updatePanel(); dbg("\u8bfe\u4ef6\u9875\u8f85\u52a9\uff1a\u8bf7\u7528\u95e8\u6237\u9875\u505a\u7eaf API\uff1b\u6b64\u5904\u4ec5\u9632\u4f11\u7720\u63d0\u793a"); while (state.running) { try { document.querySelectorAll("video").forEach((v) => { v.muted = true; const p = v.play(); if (p && p.catch) p.catch(() => {}); }); } catch (_) {} await sleep(5000); } updatePanel(); } function stopRun() { state.running = false; updatePanel(); trace("\u5df2\u505c\u4e0b\uff0c\u60f3\u7ee7\u7eed\u518d\u70b9\u5f00\u59cb"); } async function startRun() { if (state.running) return; if (isEduXl) return startPortalRun(); return startPlayerAssist(); } function injectStyles() { document.getElementById("eduxl-panel-style")?.remove(); document.getElementById("eduxl-panel-style-v2")?.remove(); if (document.getElementById(STYLE_ID)) return; const st = document.createElement("style"); st.id = STYLE_ID; st.textContent = ` #${PANEL_ID}{position:fixed;right:16px;top:64px;z-index:999999;width:420px;display:flex;flex-direction:column;max-height:min(92vh,820px);background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font:12px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:#0f172a;overflow:hidden;} #${PANEL_ID}.eduxl-min #eduxl-body,#${PANEL_ID}.eduxl-min #eduxl-footer{display:none !important;} #${PANEL_ID}.eduxl-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);} #eduxl-header{flex:0 0 auto;padding:9px 12px;background:linear-gradient(180deg,#dbeafe,#eff6ff);border-bottom:1px solid #bfdbfe;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;gap:8px;} #eduxl-logo{width:36px;height:36px;border-radius:9px;object-fit:contain;background:#fff;border:1px solid rgba(37,99,235,.15);flex:0 0 auto;box-shadow:0 1px 3px rgba(15,23,42,.08);} #eduxl-brand{min-width:0;flex:1;} #eduxl-title{font-size:12px;font-weight:900;color:#1d4ed8;display:flex;align-items:center;gap:6px;flex-wrap:wrap;line-height:1.35;} #eduxl-title .eduxl-ver{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;} #eduxl-sub{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px;} .eduxl-chip{font-size:10px;font-weight:700;color:#1e40af;background:rgba(255,255,255,.75);border:1px solid rgba(37,99,235,.2);border-radius:999px;padding:2px 7px;} .eduxl-chip-em{color:#0f766e;background:rgba(236,253,245,.95);border-color:rgba(15,118,110,.28);} .eduxl-panel-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1);font-size:14px;font-weight:900;line-height:1;flex:0 0 auto;} #eduxl-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;} .eduxl-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:8px 10px;margin-bottom:7px;} .eduxl-card-status{padding:7px 9px;} .eduxl-status-row{display:flex;justify-content:space-between;align-items:center;gap:8px;} .eduxl-status-label{font-size:11px;color:#64748b;font-weight:700;} #eduxl-status{padding:2px 8px;border-radius:999px;font-weight:900;font-size:11px;background:#fff;border:1px solid #cbd5e1;color:#334155;} #eduxl-status.is-run{background:#ecfdf5;border-color:#6ee7b7;color:#047857;} .eduxl-metrics{display:flex;align-items:center;gap:5px;margin-top:4px;font-size:12px;color:#475569;min-width:0;} .eduxl-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .eduxl-metric-div{color:#cbd5e1;} .eduxl-progress-pct{font-size:14px;font-weight:900;color:#0369a1;flex:0 0 auto;margin-left:auto;} .eduxl-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:5px;} .eduxl-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;} .eduxl-meta-row{display:flex;justify-content:space-between;gap:8px;font-size:11px;margin-bottom:3px;align-items:flex-start;line-height:1.3;} .eduxl-meta-row:last-child{margin-bottom:0;} .eduxl-meta-label{color:#64748b;flex:0 0 auto;white-space:nowrap;font-size:10px;} .eduxl-meta-value{font-weight:700;text-align:right;min-width:0;word-break:break-all;} .eduxl-tabbar{display:flex;gap:5px;margin-bottom:8px;} .eduxl-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:5px 2px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;} .eduxl-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .eduxl-pane{display:none;} .eduxl-pane.active{display:block;} .eduxl-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;gap:6px;} .eduxl-list-title{font-size:12px;color:#64748b;font-weight:700;} .eduxl-list-tag{font-size:10px;color:#1d4ed8;background:#dbeafe;border:1px solid #93c5fd;border-radius:999px;padding:2px 7px;font-weight:700;} .eduxl-list-head-actions{display:flex;align-items:center;gap:5px;} .eduxl-log-clear-btn{border:1px solid #cbd5e1;background:#fff;color:#64748b;padding:1px 8px;border-radius:999px;font-size:10px;font-weight:700;cursor:pointer;} #eduxl-course-list,#eduxl-chapter-list,#eduxl-run-log{max-height:240px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:6px;} .eduxl-empty{padding:16px 8px;text-align:center;color:#94a3b8;font-size:12px;font-weight:800;} .eduxl-course-block{border:1px solid #e2e8f0;border-radius:10px;background:#fff;padding:6px 8px 8px;margin-bottom:7px;} .eduxl-course-block:last-child{margin-bottom:0;} .eduxl-course-card{display:flex;align-items:flex-start;gap:7px;cursor:pointer;} .eduxl-course-card input{margin-top:3px;flex:0 0 auto;} .eduxl-meta{flex:1;min-width:0;} .eduxl-title-row{display:flex;align-items:center;gap:6px;min-width:0;} .eduxl-item-title{font-weight:800;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0;color:#0f172a;} .eduxl-total-badge{flex:0 0 auto;font-size:10px;font-weight:900;color:#fff;background:linear-gradient(90deg,#2563eb,#0ea5e9);border-radius:999px;padding:2px 7px;line-height:1.3;} .eduxl-muted{color:#64748b;font-size:10px;margin-top:1px;} .eduxl-link{border:none;background:#e0f2fe;color:#0369a1;border-radius:7px;padding:3px 8px;font-size:11px;font-weight:800;cursor:pointer;flex:0 0 auto;} .eduxl-link:disabled{opacity:.55;cursor:not-allowed;} .eduxl-score{margin-top:7px;display:grid;grid-template-columns:1fr 1fr;gap:5px;} .eduxl-score-item:last-child:nth-child(odd){grid-column:1 / -1;} .eduxl-score-item{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:5px 7px;} .eduxl-score-item.is-full{background:linear-gradient(180deg,#ecfdf5,#f0fdf4);border-color:#a7f3d0;} .eduxl-score-top{display:flex;justify-content:space-between;align-items:baseline;gap:4px;margin-bottom:3px;} .eduxl-score-name{font-size:11px;font-weight:800;color:#0f172a;white-space:nowrap;} .eduxl-score-name em{font-style:normal;font-weight:700;color:#0369a1;margin-left:3px;font-size:10px;} .eduxl-score-got{font-size:10px;font-weight:800;color:#047857;white-space:nowrap;} .eduxl-full-tag{display:inline-block;margin-left:3px;font-style:normal;font-size:9px;padding:0 4px;border-radius:999px;background:#059669;color:#fff;font-weight:900;vertical-align:1px;} .eduxl-mini-bar{height:4px;border-radius:999px;background:#e2e8f0;overflow:hidden;} .eduxl-mini-bar>span{display:block;height:100%;background:linear-gradient(90deg,#38bdf8,#2563eb);border-radius:999px;} .eduxl-score-item.is-full .eduxl-mini-bar>span{background:linear-gradient(90deg,#34d399,#059669);} .eduxl-score-extra{display:block;margin-top:3px;font-size:9px;color:#64748b;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} .eduxl-chapter-course-title{padding:6px 9px;background:#eaf1ff;border:1px solid #dbe4f0;border-radius:9px 9px 0 0;color:#1d4ed8;font-size:12px;font-weight:800;} .eduxl-chapter-wrap{border:1px solid #dbe4f0;border-top:none;border-radius:0 0 9px 9px;background:#fff;overflow:hidden;} .eduxl-chapter-item{padding:6px 9px;font-size:11px;display:flex;justify-content:space-between;gap:8px;border-bottom:1px dashed #e2e8f0;align-items:flex-start;} .eduxl-chapter-item:last-child{border-bottom:none;} .eduxl-chapter-item.done{background:#f0fdf4;} .eduxl-chapter-item.partial{background:#fffbeb;} .eduxl-chapter-left{min-width:0;flex:1;font-weight:650;color:#0f172a;} .eduxl-dot{display:inline-block;width:1.1em;font-weight:900;color:#64748b;} .eduxl-chapter-item.done .eduxl-dot{color:#059669;} .eduxl-chapter-right{flex:0 0 auto;color:#64748b;font-size:10px;white-space:nowrap;} .eduxl-log-line{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:11px;line-height:1.4;color:#334155;} .eduxl-log-line.eduxl-log-err{color:#b91c1c;background:#fef2f2;} .eduxl-log-line.eduxl-log-warn{color:#b45309;} .eduxl-card-face{padding:6px 8px;} .eduxl-face-box{display:flex;gap:10px;align-items:center;padding:0;background:transparent;border:none;border-radius:0;margin-bottom:0;} #eduxl-face-preview{width:56px;height:56px;border-radius:8px;object-fit:cover;background:#e2e8f0;display:none;flex:0 0 auto;} .eduxl-face-meta{flex:1;min-width:0;} .eduxl-face-ok{color:#047857;font-weight:800;font-size:12px;} .eduxl-face-off{color:#b45309;font-weight:800;font-size:12px;} .eduxl-settings-hint{font-size:11px;color:#475569;line-height:1.5;padding:8px;background:#fffbeb;border:1px solid #fcd34d;border-radius:9px;margin-top:8px;} .eduxl-settings-kv{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px dashed #e2e8f0;font-size:12px;} .eduxl-settings-kv span:last-child{font-weight:800;color:#0f172a;} .eduxl-pro-badge{padding:2px 8px;border-radius:999px;border:1px solid #cbd5e1;background:#f8fafc;font-size:11px !important;font-weight:900 !important;color:#64748b !important;} .eduxl-pro-badge.is-pro{background:#ecfdf5;border-color:#6ee7b7;color:#047857 !important;} .eduxl-pro-token-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:8px;} #eduxl-pro-token{flex:1;min-width:120px;border:1px solid #cbd5e1;border-radius:8px;padding:5px 8px;font-size:12px;background:#fff;color:#0f172a;} .eduxl-btn-pro{flex:0 0 auto;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;border:none;padding:5px 10px;font-size:12px;border-radius:9px;cursor:pointer;font-weight:800;} .eduxl-btn-pro:hover{filter:brightness(1.05);} .eduxl-btn-row{display:flex;gap:7px;} .eduxl-btn{flex:1;border:none;border-radius:10px;padding:8px 10px;font-size:13px;font-weight:900;color:#fff;cursor:pointer;} .eduxl-btn-go{background:#16a34a;} .eduxl-btn-stop{background:#ef4444;} .eduxl-btn-ghost{background:#fff;color:#0f172a;border:1px solid #cbd5e1;flex:0 0 auto;} .eduxl-btn:disabled{opacity:.5;cursor:not-allowed;} .eduxl-btns{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;} #eduxl-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:11px;color:#475569;} #eduxl-face-mask{position:fixed;inset:0;z-index:1000001;background:rgba(15,23,42,.55);display:flex;align-items:center;justify-content:center;} .eduxl-face-modal{width:min(420px,92vw);background:#fff;border-radius:14px;padding:14px;box-shadow:0 20px 50px rgba(0,0,0,.25);} .eduxl-face-modal-title{font-weight:800;font-size:14px;margin-bottom:8px;} #eduxl-face-video{width:100%;max-height:280px;background:#0f172a;border-radius:10px;object-fit:cover;} .eduxl-face-modal-hint{font-size:12px;color:#64748b;margin:8px 0;} .eduxl-face-modal-btns{display:flex;gap:8px;} @media (max-width:480px){.eduxl-score{grid-template-columns:1fr;}} `; (document.head || document.documentElement).appendChild(st); } function updatePanel() { const panel = document.getElementById(PANEL_ID); if (!panel) return; const vt = state.videoTime || 0; const pct = vt > 0 ? Math.min(100, (state.lastTime / vt) * 100) : 0; const set = (sel, text) => { const el = panel.querySelector(sel); if (el) el.textContent = text; }; const courseName = state.title || state.activeCourse?.title || "—"; const catalog = state.catalogTitle || "—"; set("#eduxl-course-title", courseName); set("#eduxl-catalog", catalog); set("#eduxl-last", formatDuration(state.lastTime)); set("#eduxl-video", vt ? formatDuration(vt) : "—"); const statusEl = panel.querySelector("#eduxl-status"); if (statusEl) { statusEl.textContent = state.running ? "\u5b66\u4e60\u4e2d" : "\u5f85\u547d"; statusEl.classList.toggle("is-run", !!state.running); } const pctEl = panel.querySelector("#eduxl-progress-pct"); if (pctEl) pctEl.textContent = `${pct.toFixed(0)}%`; const bar = panel.querySelector("#eduxl-bar-inner"); if (bar) bar.style.width = `${pct.toFixed(1)}%`; const foot = panel.querySelector("#eduxl-queue-text"); if (foot) { const n = getCheckedCourses().length || state.courses.length; foot.textContent = state.running ? `\u6b63\u5728\u5b66\uff1a${courseName}${catalog && catalog !== "—" ? " · " + catalog : ""}` : n ? `\u5df2\u9009 ${getCheckedCourses().length || n} \u95e8\u8bfe\uff0c\u70b9\u5f00\u59cb\u5373\u53ef` : "\u5148\u52fe\u9009\u8bfe\u7a0b\u518d\u5f00\u59cb"; } const go = panel.querySelector("#eduxl-btn-go"); const stop = panel.querySelector("#eduxl-btn-stop"); if (go) go.disabled = state.running; if (stop) stop.disabled = !state.running; } function bindDrag(panel, handle) { let ox = 0, oy = 0, dragging = false; handle.addEventListener("mousedown", (e) => { if (e.target.closest("button")) return; dragging = true; const rect = panel.getBoundingClientRect(); ox = e.clientX - rect.left; oy = e.clientY - rect.top; e.preventDefault(); }); window.addEventListener("mousemove", (e) => { if (!dragging) return; panel.style.left = `${Math.max(0, e.clientX - ox)}px`; panel.style.top = `${Math.max(0, e.clientY - oy)}px`; panel.style.right = "auto"; }); window.addEventListener("mouseup", () => { dragging = false; }); } function mountPanel() { document.querySelectorAll("#" + PANEL_ID).forEach((el) => el.remove()); injectStyles(); const panel = document.createElement("div"); panel.id = PANEL_ID; if (localStorage.getItem(LS.collapsed) === "1") panel.classList.add("eduxl-min"); const savedTab = localStorage.getItem(LS.tab) || "course"; panel.innerHTML = `
\u5b66\u5386\u6559\u80b2\u4e91·\u81ea\u8003\u4e91\u670d\u52a1\u5e73\u53f0\u52a9\u624b v${VERSION}
${isEduXl ? "\u4e00\u952e\u5b66\u5b8c" : "\u8bfe\u4ef6\u8f85\u52a9"} \u5b66\u65f6 ·\u77e5\u8bc6\u70b9· \u4f5c\u4e1a · \u7efc\u5408 \u6ee1\u5206\u81ea\u52a8\u8df3\u8fc7
\u8fd0\u884c\u72b6\u6001 \u5f85\u547d
\u8fdb\u5ea6 0\u520600\u79d2/ · 0%
${ isEduXl ? `
\u672a\u91c7\u96c6
\u62cd\u4e00\u6b21\u5c31\u884c\uff0c\u540e\u9762\u4e0d\u7528\u518d\u91c7
` : "" }
\u52fe\u9009\u540e\u70b9\u5f00\u59cb \u8bfe\u7a0b\u5217\u8868
${isEduXl ? "\u6b63\u5728\u81ea\u52a8\u62c9\u53d6\u8bfe\u7a0b…" : "\u8bf7\u5230\u95e8\u6237\u9875\u4f7f\u7528\u8bfe\u7a0b\u5217\u8868"}
\u7ae0\u8282\u9884\u89c8 \u5b9e\u65f6\u8fdb\u5ea6
\u5728\u300c\u8bfe\u7a0b\u5217\u8868\u300d\u70b9\u67d0\u8bfe\u7684\u300c\u7ae0\u8282\u300d
\u8fd0\u884c\u65e5\u5fd7
\u5b9e\u65f6
\u6682\u65e0\u8fd0\u884c\u65e5\u5fd7
\u4f1a\u5458 \u8bbe\u7f6e
\u5f53\u524d\u8eab\u4efd\u514d\u8d39\u8bd5\u7528
\u53ef\u7528\u6b21\u6570
\u514d\u8d39\u53ef\u5b66 3 \u4e2a\u89c6\u9891\u5c0f\u8282\uff1b\u9636\u6bb5\u4f5c\u4e1a\u4e0e\u7efc\u5408\u6d4b\u8bc4\u9700\u5f00\u901a\u4f1a\u5458\u3002\u4f1a\u5458\u5f00\u901a\u540e 30 \u5929\u5185\u4e0d\u9650\u3002
\u5f53\u524d\u8bfe\u7a0b
\u5f53\u524d\u7ae0\u8282
`; (document.body || document.documentElement).appendChild(panel); bindDrag(panel, panel.querySelector("#eduxl-header")); panel.querySelectorAll(".eduxl-tab-btn").forEach((btn) => { btn.addEventListener("click", () => switchTab(btn.getAttribute("data-tab"))); }); switchTab(isEduXl ? savedTab : "log"); panel.querySelector("#eduxl-min-btn").addEventListener("click", () => { panel.classList.toggle("eduxl-min"); const min = panel.classList.contains("eduxl-min"); localStorage.setItem(LS.collapsed, min ? "1" : "0"); panel.querySelector("#eduxl-min-btn").textContent = min ? "+" : "–"; }); panel.querySelector("#eduxl-btn-go").addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); if (state.running) return; startRun(); }); panel.querySelector("#eduxl-btn-stop").addEventListener("click", () => stopRun()); const clearLog = panel.querySelector("#eduxl-clear-log"); if (clearLog) { clearLog.addEventListener("click", () => { state.logLines = []; renderRunLog(); }); } const faceBtn = panel.querySelector("#eduxl-btn-face"); if (faceBtn) { faceBtn.addEventListener("click", async () => { faceBtn.disabled = true; try { await captureFaceToCache(); trace("\u4eba\u8138\u5df2\u4fdd\u5b58\uff0c\u5f00\u59cb\u5b66\u4e60\u65f6\u4f1a\u81ea\u52a8\u7528"); } catch (e) { dbg(`\u91c7\u96c6\u5931\u8d25\uff1a${e.message || e}`); trace("\u4eba\u8138\u6ca1\u91c7\u6210\uff0c\u8bf7\u5141\u8bb8\u6444\u50cf\u5934\u540e\u518d\u8bd5"); } finally { faceBtn.disabled = false; updateFaceStatusUi(); } }); } const faceUpload = panel.querySelector("#eduxl-btn-face-upload"); if (faceUpload) { faceUpload.addEventListener("click", async () => { if (!isProUser()) { try { await ensureCloudLease(true); } catch (_) {} if (!isProUser()) { trace("\u4e0a\u4f20\u7167\u7247\u4ec5\u4f1a\u5458\u53ef\u7528\uff0c\u8bf7\u5148\u5f00\u901a\u4f1a\u5458"); updateFaceStatusUi(); return; } } faceUpload.disabled = true; try { await uploadFaceToCache(); trace("\u5df2\u4e0a\u4f20\u4eba\u8138\u7167\u7247\uff0c\u5f00\u59cb\u5b66\u4e60\u65f6\u4f1a\u81ea\u52a8\u7528"); } catch (e) { const msg = String((e && e.message) || e || ""); if (/\u53d6\u6d88/i.test(msg)) { } else if (/\u4ec5\u4f1a\u5458|pro_required/i.test(msg)) { trace("\u4e0a\u4f20\u7167\u7247\u4ec5\u4f1a\u5458\u53ef\u7528\uff0c\u8bf7\u5148\u5f00\u901a\u4f1a\u5458"); } else { dbg(`\u4e0a\u4f20\u4eba\u8138\u5931\u8d25\uff1a${msg}`); trace("\u7167\u7247\u6ca1\u4f20\u6210\uff0c\u8bf7\u6362\u4e00\u5f20\u6b63\u9762\u6e05\u6670\u7684\u7167\u7247\u518d\u8bd5"); } } finally { faceUpload.disabled = false; updateFaceStatusUi(); } }); } const faceClear = panel.querySelector("#eduxl-btn-face-clear"); if (faceClear) { faceClear.addEventListener("click", () => { clearFaceCache(); trace("\u5df2\u6e05\u9664\u4eba\u8138\u7167\u7247"); }); } const proSave = panel.querySelector("#eduxl-pro-save"); if (proSave) { proSave.addEventListener("click", async () => { const input = panel.querySelector("#eduxl-pro-token"); const tok = input ? String(input.value || "").trim() : ""; setCloudToken(tok); proSave.disabled = true; try { await ensureCloudLease(true); if (isProUser()) trace("\u4f1a\u5458\u5df2\u5f00\u901a\uff0c\u53ef\u4ee5\u653e\u5fc3\u5b66\u4e86"); else if (tok) trace("\u5361\u5bc6\u5df2\u4fdd\u5b58\uff1b\u82e5\u672a\u5f00\u901a\u6210\u529f\uff0c\u8bf7\u6838\u5bf9\u540e\u518d\u8bd5"); else trace("\u5df2\u9000\u51fa\u4f1a\u5458\uff0c\u6309\u514d\u8d39\u8bd5\u7528"); } catch (e) { trace(friendlyCloudMessage(e)); } finally { proSave.disabled = false; updateCloudPanelUI(); } }); } const proUp = panel.querySelector("#eduxl-pro-upgrade"); if (proUp) proUp.addEventListener("click", () => openProBuy()); renderCourseList(); renderChapterList(); renderRunLog(); updatePanel(); updateFaceStatusUi(); updateCloudPanelUI(); } installNetworkSniffer(); async function autoRefreshPortalCourses() { if (!isEduXl) return; const tryOnce = async (label) => { try { await refreshPortalCourses(); } catch (e) { dbg(`${label}\u5931\u8d25\uff1a${e.message || e}`); } return state.courses.length > 0; }; if (await tryOnce("\u81ea\u52a8\u62c9\u53d6")) { try { await ensureCloudLease(true); } catch (_) {} return; } for (let i = 1; i <= 5; i++) { await sleep(1500); if (!state.running && state.courses.length === 0) { if (await tryOnce(`\u81ea\u52a8\u91cd\u8bd5 ${i}/5`)) return; } else if (state.courses.length > 0) { return; } } document.querySelectorAll("iframe").forEach((fr) => { fr.addEventListener( "load", () => { if (state.courses.length) return; refreshPortalCourses().catch(() => {}); }, { once: true } ); }); } const boot = () => { mountPanel(); if (isEduXl) { trace("\u52a9\u624b\u5df2\u5c31\u7eea\uff0c\u6b63\u5728\u52a0\u8f7d\u8bfe\u7a0b…"); autoRefreshPortalCourses(); initCloudAuth().catch(() => {}); } else { trace("\u8bf7\u5230\u5b66\u6821\u5b66\u4e60\u9996\u9875\u4f7f\u7528\u5b8c\u6574\u529f\u80fd"); initCloudAuth().catch(() => {}); } }; if (document.body) boot(); else document.addEventListener("DOMContentLoaded", boot); })();