// ==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 ? `` : "";
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 ``;
})
.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 = `
\u8fd0\u884c\u72b6\u6001
\u5f85\u547d
\u8fdb\u5ea6 0\u520600\u79d2/—
·
0%
${
isEduXl
? ` `
: ""
}
\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);
})();
|