]*>\s*([\s\S]*?)\s*<\/h[1-4]>/gi)];
for (let i = anyH.length - 1; i >= 0; i--) {
const t = cleanCourseTitle(anyH[i][1]);
if (t) return t;
}
return looksLikeCourseCode(courseCode) ? "" : cleanCourseTitle(courseCode);
}
function collectTermSections(text) {
const sections = [];
const re =
/\u7b2c\s*\s*([\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u96f6\d]+)\s*<\/span>\s*\u5b66\u671f|\u7b2c([\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u96f6\d]+)\u5b66\u671f/g;
let m;
while ((m = re.exec(text))) {
const num = m[1] || m[2];
const label = `\u7b2c${num}\u5b66\u671f`;
const last = sections[sections.length - 1];
if (last && last.label === label && m.index - last.pos < 80) continue;
if (last && last.label === label) continue;
sections.push({ pos: m.index, label, index: sections.length });
}
return sections;
}
function termSectionAt(sections, pos) {
let hit = null;
for (let i = 0; i < sections.length; i++) {
if (sections[i].pos <= pos) hit = sections[i];
else break;
}
return hit;
}
function parseCoursesFromHtml(html) {
const text = String(html || "");
const termSections = collectTermSections(text);
let currentTermIndex = Math.max(0, termSections.length - 1);
const dqxqPos = text.search(/id=["']dqxqTerm["']/i);
if (dqxqPos >= 0) {
const sec = termSectionAt(termSections, dqxqPos);
if (sec) currentTermIndex = sec.index;
else {
const dqxqM =
text.match(/id=["']dqxqTerm["'][^>]*\bvalue=["'](\d+)["']/i) ||
text.match(/\bvalue=["'](\d+)["'][^>]*id=["']dqxqTerm["']/i);
if (dqxqM) currentTermIndex = parseInt(dqxqM[1], 10) || currentTermIndex;
}
} else {
const huoPos = text.search(/title=["']\u5f53\u524d\u5b66\u671f["']/i);
if (huoPos >= 0) {
const sec = termSectionAt(termSections, huoPos);
if (sec) currentTermIndex = sec.index;
}
}
const courses = [];
const re =
/kjxx_click\(\s*'([^']+)'\s*,\s*'([^']*)'\s*,\s*'([^']*)'\s*,\s*'([^']*)'\s*,\s*'([^']*)'\s*,\s*'([^']+)'\s*\)/g;
let m;
while ((m = re.exec(text))) {
const courseCode = m[1];
const timeStart = m[2];
const timeEnd = m[3];
const stemcode = m[4];
const isFaceKJ = m[5];
const termCourseID = m[6];
const before = text.slice(Math.max(0, m.index - 5000), m.index);
const cardStart = Math.max(before.lastIndexOf("single-lists"), before.lastIndexOf("list-content"));
const cardChunk = cardStart >= 0 ? before.slice(cardStart) : before.slice(-1600);
let title = extractTitleNear(cardChunk, courseCode);
if (!title) title = extractTitleNear(before.slice(-2200), courseCode);
if (!title) title = courseCode;
const sec = termSectionAt(termSections, m.index);
const termIndex = sec ? sec.index : currentTermIndex;
const termLabel = sec ? sec.label : "";
let portalScore = null;
const scoreM = before.match(new RegExp(`id=['"]lblcurcj${termCourseID}['"][^>]*>\\s*([^<]+)`, "i"));
if (scoreM) {
const n = parseFloat(String(scoreM[1]).trim());
if (Number.isFinite(n)) portalScore = n;
}
let hasHomework = false;
let hasExam = false;
let zyPortalCode = "";
const zyChunk = text.slice(Math.max(0, m.index - 200), Math.min(text.length, m.index + 1600));
const zyM = zyChunk.match(
/pszy_click\(\s*'([^']+)'\s*,\s*'[^']*'\s*,\s*'[^']*'\s*,\s*'[^']*'\s*,\s*'[^']*'\s*,\s*'[^']*'\s*,\s*'([^']+)'\s*\)/
);
if (zyM && String(zyM[2]) === String(termCourseID)) {
hasHomework = true;
zyPortalCode = zyM[1];
} else if (/pszy_click\(/i.test(zyChunk) && new RegExp(`['"]${termCourseID}['"]`).test(zyChunk)) {
hasHomework = true;
}
if (
/qmks_click\(/i.test(zyChunk) &&
new RegExp(`['"]${termCourseID}['"]`).test(zyChunk)
) {
hasExam = true;
}
if (!courses.some((c) => c.termCourseID === termCourseID && c.courseCode === courseCode)) {
courses.push({
courseCode,
termCourseID,
title,
termLabel,
termIndex,
isCurrentTerm: termIndex === currentTermIndex,
timeStart,
timeEnd,
stemcode,
isFaceKJ: String(isFaceKJ || "0"),
portalScore,
lessonScore: null,
homeworkScore: null,
examScore: null,
needMin: null,
learnedMin: null,
hasHomework,
hasExam,
zyPortalCode,
checked: true,
});
}
}
return { courses, currentTermIndex, termSections };
}
function buildTermTabs(allCourses, currentTermIndex) {
const map = new Map();
for (const c of allCourses) {
const idx = Number(c.termIndex);
if (!Number.isFinite(idx)) continue;
if (!map.has(idx)) map.set(idx, c.termLabel || `\u7b2c${idx + 1}\u5b66\u671f`);
}
const tabs = [{ key: "current", label: "\u5f53\u524d\u5b66\u671f" }];
[...map.keys()]
.sort((a, b) => a - b)
.forEach((idx) => {
const lab = map.get(idx);
const curMark = idx === currentTermIndex ? "\uff08\u5f53\u524d\uff09" : "";
tabs.push({ key: String(idx), label: `${lab}${curMark}` });
});
tabs.push({ key: "all", label: "\u5168\u90e8\u5b66\u671f" });
return tabs;
}
function applyTermFilter() {
const all = state.allCourses || [];
const key = state.termFilter || "current";
let list;
if (key === "all") {
list = all.slice();
} else if (key === "current") {
list = all.filter((c) => c.isCurrentTerm || c.termIndex === state.currentTermIndex);
if (!list.length && all.length) {
const maxIdx = all.reduce((m, c) => Math.max(m, Number(c.termIndex) || 0), 0);
list = all.filter((c) => c.termIndex === maxIdx);
}
} else {
const idx = parseInt(key, 10);
list = Number.isFinite(idx) ? all.filter((c) => c.termIndex === idx) : all.slice();
}
state.courses = list;
return list;
}
function sectionBlockForLabel(text, label) {
const re = new RegExp(
`class=["']titleSec["'][^>]*>\\s*${label}[\\s\\S]*?(?=]*class=["']titleSec["']|$)`,
"i"
);
const m = String(text || "").match(re);
return m ? m[0] : "";
}
function parseWeightPct(text, label) {
const src = String(text || "");
const re = new RegExp(`${label}[\\s\\S]{0,400}?(\\d{1,3})\\s*[%\uff05]`, "i");
const m = src.match(re);
if (!m) return null;
const n = parseInt(m[1], 10);
return Number.isFinite(n) && n > 0 && n <= 100 ? n : null;
}
function parseRawScoresInBlock(block, weightPct) {
const src = String(block || "");
const out = [];
const pairRe = /\u6ee1\u5206\s*[\uff1a:]\s*([\d.]+)[\s\S]{0,80}?\u5f97\u5206\s*[\uff1a:]\s*([\d.]+)/gi;
let pm;
while ((pm = pairRe.exec(src))) {
const n = parseFloat(pm[2]);
if (!Number.isFinite(n)) continue;
if (n > 0 && n <= 100) out.push(n);
}
if (out.length) return out;
const re = /(?:\u5b9e)?\u5f97\u5206\s*[\uff1a:]\s*([\d.]+)/gi;
let m;
while ((m = re.exec(src))) {
const n = parseFloat(m[1]);
if (!Number.isFinite(n)) continue;
if (weightPct != null && weightPct > 0 && n <= Number(weightPct) + 0.5) continue;
if (n > 0 && n <= 100) out.push(n);
}
return out;
}
function isPartScoreFull(score, weightPct, rawScores) {
const w = weightPct != null && weightPct > 0 ? Number(weightPct) : null;
const papers = (rawScores || [])
.map((s) => Number(s))
.filter((n) => Number.isFinite(n) && n > 0 && n <= 100)
.filter((n) => (w == null ? n > 60 || n >= 99.5 : n > w + 0.5));
if (papers.length) {
return papers.every((s) => s >= 99.5);
}
if (score == null || !Number.isFinite(Number(score))) return false;
const n = Number(score);
if (w != null && n > w + 1) return n >= 99.5;
if (n >= 99.5) return true;
if (w != null && n <= w + 0.5) return n >= w * 0.995;
for (const mx of [10, 20, 30, 40, 50, 60]) {
if (n <= mx + 0.05 && n >= mx * 0.995) return true;
}
return false;
}
function parseStudyQuotaHtml(html) {
const detail = {
learnedMin: null,
needMin: null,
lessonScore: null,
homeworkScore: null,
examScore: null,
homeworkFull: false,
examFull: false,
lessonFull: false,
};
const text = String(html || "");
const timeM = text.match(
/class=["']time["'][^>]*>\s*([^<]+)\s*<\/em>\s*\/\s*]*class=["']all["'][^>]*>\s*([^<]+)/i
);
if (timeM) {
detail.learnedMin = parseInt(String(timeM[1]).trim(), 10) || 0;
detail.needMin = parseInt(String(timeM[2]).trim(), 10) || 0;
} else {
const m1 = text.match(/\u5df2\u5b66\u65f6\u957f\s*[\uff1a:]\s*(\d+)\s*\u5206\u949f[\s\S]{0,40}?\u5e94\u5b66\u65f6\u957f\s*[\uff1a:]\s*(\d+)\s*\u5206\u949f/i);
if (m1) {
detail.learnedMin = parseInt(m1[1], 10) || 0;
detail.needMin = parseInt(m1[2], 10) || 0;
} else {
const m2 = text.match(/\u5e94\u5b66\u65f6\u957f\s*[\uff1a:]\s*(\d+)\s*\u5206\u949f[\s\S]{0,40}?\u5df2\u5b66\u65f6\u957f\s*[\uff1a:]\s*(\d+)\s*\u5206\u949f/i);
if (m2) {
detail.needMin = parseInt(m2[1], 10) || 0;
detail.learnedMin = parseInt(m2[2], 10) || 0;
} else {
const needOnly = text.match(/\u5e94\u5b66\u65f6\u957f\s*[\uff1a:]\s*(\d+)\s*\u5206?\u949f?/i);
const learnedOnly = text.match(/\u5df2\u5b66\u65f6\u957f\s*[\uff1a:]\s*(\d+)\s*\u5206?\u949f?/i);
if (needOnly) detail.needMin = parseInt(needOnly[1], 10) || 0;
if (learnedOnly) detail.learnedMin = parseInt(learnedOnly[1], 10) || 0;
if (detail.needMin == null || detail.learnedMin == null) {
const m3 = text.match(/(\d+)\s*\/\s*(\d+)\s*\(?\s*\u5206\u949f/);
if (m3) {
if (detail.learnedMin == null) detail.learnedMin = parseInt(m3[1], 10) || 0;
if (detail.needMin == null) detail.needMin = parseInt(m3[2], 10) || 0;
}
}
}
}
}
const wLesson = parseWeightPct(text, "\u8bfe\u4ef6\u5b66\u4e60");
const wHw = parseWeightPct(text, "\u5e73\u65f6\u4f5c\u4e1a");
const wEx = parseWeightPct(text, "\u671f\u672b\u8003\u8bd5");
const pickPart = (label, weightPct) => {
const block = sectionBlockForLabel(text, label);
if (!block) return { score: null, raw: [] };
const rowspan = block.match(/rowspan\s*=\s*["']?\d+["']?[^>]*>\s*\u5f97\u5206\s*[\uff1a:]\s*([\d.]+)/i);
let score = null;
if (rowspan) {
const n = parseFloat(rowspan[1]);
if (Number.isFinite(n)) score = n;
} else {
const all = [...block.matchAll(/\u5f97\u5206\s*[\uff1a:]\s*([\d.]+)/g)].map((x) => parseFloat(x[1])).filter(Number.isFinite);
if (all.length) {
if (weightPct != null && weightPct > 0) {
const folded = all.filter((n) => n <= weightPct + 0.5);
score = folded.length ? folded[folded.length - 1] : all[all.length - 1];
} else {
score = /\u671f\u672b/.test(label) && all.length > 1 ? all[all.length - 1] : all[0];
}
}
}
return { score, raw: parseRawScoresInBlock(block, weightPct) };
};
const lesson = pickPart("\u8bfe\u4ef6\u5b66\u4e60", wLesson);
const hw = pickPart("\u5e73\u65f6\u4f5c\u4e1a", wHw);
const ex = pickPart("\u671f\u672b\u8003\u8bd5", wEx);
detail.lessonScore = lesson.score;
detail.homeworkScore = hw.score;
detail.examScore = ex.score;
detail.lessonFull = isPartScoreFull(lesson.score, wLesson, lesson.raw);
detail.homeworkFull = isPartScoreFull(hw.score, wHw, hw.raw);
detail.examFull = isPartScoreFull(ex.score, wEx, ex.raw);
const nameM =
text.match(/\u8bfe\u7a0b\u540d\u79f0\s*[\uff1a:]\s*([^<]{2,80})/i) ||
text.match(/\s*([^<]{2,80}?)\s*<\/title>/i) ||
text.match(/class=["'][^"']*courseName[^"']*["'][^>]*>\s*([^<]{2,80})/i);
if (nameM) {
const n = cleanCourseTitle(nameM[1]);
if (n) detail.courseName = n;
}
return detail;
}
function formatScorePart(n) {
if (n == null || !Number.isFinite(Number(n))) return "—";
const v = Number(n);
return Number.isInteger(v) ? String(v) : String(Math.round(v * 10) / 10);
}
function courseScoreLine(c) {
const parts = [
`\u8bfe\u65f6\u5f97\u5206${formatScorePart(c.lessonScore)}`,
`\u4f5c\u4e1a\u5f97\u5206${formatScorePart(c.homeworkScore)}`,
`\u671f\u672b\u5f97\u5206${formatScorePart(c.examScore)}`,
];
if (courseQuotaMet(c, c.learnedMin)) parts.push("\u65f6\u957f\u5df2\u6ee1");
return parts.join(" · ");
}
async function fetchCourseStudyQuota(course) {
const path = `/MyOnlineCourseNew/ScoreDetailListNew/ScoreDetailListNewIndex?type=0&termCourseID=${encodeURIComponent(
course.termCourseID
)}`;
try {
const html = await fetchPortalHtml(path);
const q = parseStudyQuotaHtml(html);
if (q.needMin != null) course.needMin = q.needMin;
if (q.learnedMin != null) course.learnedMin = q.learnedMin;
if (q.lessonScore != null) course.lessonScore = q.lessonScore;
if (q.homeworkScore != null) course.homeworkScore = q.homeworkScore;
if (q.examScore != null) course.examScore = q.examScore;
if (typeof q.homeworkFull === "boolean") course.homeworkFull = q.homeworkFull;
if (typeof q.examFull === "boolean") course.examFull = q.examFull;
if (typeof q.lessonFull === "boolean") course.lessonFull = q.lessonFull;
if (q.homeworkScore != null) course.hasHomework = true;
if (q.examScore != null) course.hasExam = true;
if (q.courseName && looksLikeCourseCode(course.title)) course.title = q.courseName;
return q;
} catch (e) {
dbg(`\u6210\u7ee9\u8be6\u60c5\u5931\u8d25\uff1a${e.message || e}`);
log(`\u300c${course.title}\u300d\u6210\u7ee9\u8be6\u60c5\u6682\u4e0d\u53ef\u7528`);
return { learnedMin: course.learnedMin, needMin: course.needMin };
}
}
function sumChaptersSofarMin(chapters) {
const sec = (chapters || []).reduce((s, ch) => s + (Number(ch.sofar) || 0), 0);
return Math.floor(sec / 60);
}
function courseQuotaMet(course, localMin) {
const need = Number(course.needMin) || 0;
if (need <= 0) return false;
const learned = Math.max(Number(course.learnedMin) || 0, Number(localMin) || 0);
return learned >= need;
}
const COURSE_LIST_PATH = "/MyOnlineCourseNew/OnlineLearningNew/OnlineLearningNewIndex?isCurrent=0&type=0";
function looksLikePortalLoginHtml(html) {
const h = String(html || "");
if (!h) return false;
if (h.length >= 4000) return false;
return /Object moved|ReturnUrl=|\/Account\/Login|\/Home\/Login|\u8bf7\u5148\u767b\u5f55|\u91cd\u65b0\u767b\u5f55/i.test(h);
}
function looksLikeExIndexHtml(html) {
const h = String(html || "");
if (/\u5f00\u59cb\u8003\u8bd5|start_testing|\u5269\u4f59\u8003\u8bd5\u6b21\u6570/i.test(h)) return true;
if (/pszy_click2\s*\(/i.test(h) && /hidSign|#hidcourse_id|StudentSiteNewExIndex/i.test(h)) return true;
if (/titer-li|task_list/i.test(h) && /pszy_click2\s*\(/i.test(h)) return true;
return false;
}
function isValidExPaperFields(termCourseID, signCJ, examCode) {
const tid = String(termCourseID || "").trim();
const sign = String(signCJ || "").trim();
const code = String(examCode || "").trim();
if (!code || !/^[A-Za-z0-9_.-]+$/.test(code)) return false;
if (!/^\d{3,}$/.test(tid)) return false;
if (!/^[a-f0-9]{32}$/i.test(sign)) return false;
return true;
}
function looksLikePortalSysError(html) {
const h = String(html || "");
return /Index\/Error|\u7cfb\u7edf\u9519\u8bef|\u5728\u6267\u884c\s*controller|controller\[OnlineLearningNew\]/i.test(h);
}
function findPortalContentFrame() {
const sels = [
"iframe#mainiframe",
"iframe[name='mainiframe']",
"iframe#iframeMain",
"iframe#mainFrame",
"iframe.mainiframe",
"iframe[name='main']",
"iframe[id*='main' i]",
"iframe[src*='OnlineLearning' i]",
"iframe[src*='StudentSite' i]",
];
for (const s of sels) {
try {
const el = document.querySelector(s);
if (el) return el;
} catch (_) {}
}
let best = null;
let area = 0;
try {
for (const fr of document.querySelectorAll("iframe")) {
const r = fr.getBoundingClientRect();
const a = Math.max(0, r.width) * Math.max(0, r.height);
if (a > area) {
area = a;
best = fr;
}
}
} catch (_) {}
return area >= 20000 ? best : null;
}
function loadUrlInFrame(fr, url, timeoutMs) {
return new Promise((resolve) => {
if (!fr) {
resolve("");
return;
}
const wait = Math.max(4000, Number(timeoutMs) || 12000);
let done = false;
const finish = () => {
if (done) return;
done = true;
try {
clearTimeout(timer);
} catch (_) {}
try {
fr.removeEventListener("load", onLoad);
} catch (_) {}
try {
const doc = fr.contentDocument || (fr.contentWindow && fr.contentWindow.document);
resolve(doc && doc.documentElement ? doc.documentElement.outerHTML : "");
} catch (_) {
resolve("");
}
};
const onLoad = () => setTimeout(finish, 350);
const timer = setTimeout(finish, wait);
try {
fr.addEventListener("load", onLoad);
fr.src = url;
} catch (_) {
finish();
}
});
}
function snippetHtml(html, n) {
return String(html || "")
.replace(/\s+/g, " ")
.trim()
.slice(0, n || 160);
}
async function gmRequestFollow(opt) {
let url = String(opt.url || "");
let res = await gmRequest({ ...opt, url });
for (let hop = 0; hop < 6; hop++) {
const status = Number(res.status) || 0;
const text = String(res.responseText || res.response || "");
const headers = res.responseHeaders || res.headers || "";
let loc = headerLocation(headers);
if (!loc) {
const m = text.match(/Object moved to {
try {
const xhr = new WIN.XMLHttpRequest();
xhr.open("GET", url, true);
xhr.withCredentials = true;
xhr.setRequestHeader("Accept", "text/html,application/xhtml+xml,*/*");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) resolve(String(xhr.responseText || ""));
};
xhr.onerror = () => resolve("");
xhr.send();
} catch (_) {
resolve("");
}
});
} catch (_) {
return "";
}
}
async function xhrPostFormViaPage(url, formBody, headers) {
const portalOrigin = (() => {
try {
return new URL(PORTAL).origin;
} catch (_) {
return "";
}
})();
if (portalOrigin && String(location.origin) !== portalOrigin) {
return { text: "", status: 0, j: null };
}
const body = typeof formBody === "string" ? formBody : String(formBody || "");
try {
if (!WIN.XMLHttpRequest) return { text: "", status: 0, j: null };
return await new Promise((resolve) => {
try {
const xhr = new WIN.XMLHttpRequest();
xhr.open("POST", url, true);
xhr.withCredentials = true;
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
xhr.setRequestHeader("Accept", "application/json, text/javascript, */*; q=0.01");
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
const h = headers || {};
for (const k of Object.keys(h)) {
try {
if (/^(content-type|referer|origin)$/i.test(k)) continue;
xhr.setRequestHeader(k, h[k]);
} catch (_) {}
}
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) return;
const text = String(xhr.responseText || "");
let j = null;
try {
j = text ? JSON.parse(text) : null;
} catch (_) {}
resolve({ text, status: Number(xhr.status) || 0, j });
};
xhr.onerror = () => resolve({ text: "", status: 0, j: null });
xhr.send(body);
} catch (_) {
resolve({ text: "", status: 0, j: null });
}
});
} catch (_) {
return { text: "", status: 0, j: null };
}
}
function iframeXhr(win, method, url, body, headers) {
return new Promise((resolve) => {
try {
const XHR = win.XMLHttpRequest || WIN.XMLHttpRequest;
if (!XHR) return resolve({ text: "", status: 0, j: null });
const xhr = new XHR();
xhr.open(method || "GET", url, true);
xhr.withCredentials = true;
if (method === "POST") {
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
xhr.setRequestHeader("Accept", "application/json, text/javascript, */*; q=0.01");
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
} else {
xhr.setRequestHeader("Accept", "text/html,application/xhtml+xml,*/*");
}
const h = headers || {};
for (const k of Object.keys(h)) {
try {
if (/^(content-type|referer|origin)$/i.test(k)) continue;
xhr.setRequestHeader(k, h[k]);
} catch (_) {}
}
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) return;
const text = String(xhr.responseText || "");
let j = null;
try {
j = text ? JSON.parse(text) : null;
} catch (_) {}
resolve({ text, status: Number(xhr.status) || 0, j, finalUrl: String(xhr.responseURL || url) });
};
xhr.onerror = () => resolve({ text: "", status: 0, j: null });
xhr.send(body || null);
} catch (_) {
resolve({ text: "", status: 0, j: null });
}
});
}
function readIframeHtml(iframe) {
try {
const d = iframe.contentDocument;
return d && d.documentElement ? d.documentElement.outerHTML : "";
} catch (_) {
return "";
}
}
function withAllTermListFrame(runner, timeoutMs) {
const listUrl = `${PORTAL}${COURSE_LIST_PATH}`;
const wait = Math.max(12000, Number(timeoutMs) || 28000);
return new Promise((resolve) => {
let done = false;
const iframe = document.createElement("iframe");
iframe.setAttribute("aria-hidden", "true");
iframe.setAttribute("sandbox", "allow-same-origin");
iframe.style.cssText =
"position:fixed;left:-9999px;top:0;width:1px;height:1px;opacity:0;pointer-events:none;border:0";
const finish = (val) => {
if (done) return;
done = true;
try {
clearTimeout(timer);
} catch (_) {}
try {
iframe.onload = null;
iframe.remove();
} catch (_) {}
resolve(val);
};
const timer = setTimeout(() => {
dbg("allTermFrame \u8d85\u65f6");
finish(null);
}, wait);
const liveXhr = (method, url, body) => {
const win = iframe.contentWindow;
if (!win) return Promise.resolve({ text: "", status: 0, j: null });
return iframeXhr(win, method, url, body);
};
const navigate = (nextUrl, settleMs) =>
new Promise((res) => {
let settled = false;
const doneNav = () => {
if (settled) return;
settled = true;
const delay = Math.max(400, Number(settleMs) || 600);
setTimeout(() => res(readIframeHtml(iframe)), delay);
};
iframe.onload = doneNav;
try {
iframe.src = nextUrl;
} catch (_) {
res("");
return;
}
setTimeout(doneNav, 14000);
});
let started = false;
iframe.onload = () => {
if (started) return;
started = true;
setTimeout(async () => {
try {
const html0 = readIframeHtml(iframe);
if (!html0 || looksLikePortalSysError(html0) || looksLikePortalLoginHtml(html0)) {
dbg(`\u5168\u90e8\u5b66\u671f\u5217\u8868\u672a\u5c31\u7eea snip=${snippetHtml(html0, 100)}`);
return finish(null);
}
const out = await runner({
iframe,
listUrl,
getWin: () => iframe.contentWindow,
getDoc: () => iframe.contentDocument,
getHtml: () => readIframeHtml(iframe),
navigate,
xhr: liveXhr,
});
finish(out);
} catch (e) {
dbg(`allTermFrame\uff1a${e.message || e}`);
finish(null);
}
}, 350);
};
try {
document.documentElement.appendChild(iframe);
iframe.src = listUrl;
} catch (_) {
finish(null);
}
});
}
async function followObjectMovedHtml(html, baseUrl, getter) {
let cur = String(html || "");
let url = String(baseUrl || "");
for (let i = 0; i < 6; i++) {
if (!/Object moved/i.test(cur)) break;
const m = cur.match(/Object moved to {
try {
return new URL(PORTAL).origin;
} catch (_) {
return "";
}
})();
if (portalOrigin && String(location.origin) !== portalOrigin) return "";
let text = await xhrGetText(url);
if (text) text = await followObjectMovedHtml(text, url, xhrGetText);
if (text) return text;
try {
if (typeof WIN.fetch !== "function") return "";
const r = await WIN.fetch(url, {
method: "GET",
credentials: "include",
redirect: "follow",
headers: { Accept: "text/html,application/xhtml+xml,*/*" },
});
text = (await r.text()) || "";
if (text) text = await followObjectMovedHtml(text, r.url || url, xhrGetText);
return text;
} catch (_) {
return "";
}
}
async function fetchPortalHtml(path, opts) {
const forceNet = !!(opts && opts.forceNet);
const url = path.startsWith("http") ? path : PORTAL + path;
if (
!forceNet &&
isPortal &&
location.pathname.includes("OnlineLearningNewIndex") &&
!/isCurrent=0/i.test(path)
) {
return document.documentElement.outerHTML;
}
let text = "";
let status = 0;
let finalUrl = url;
try {
const viaPage = await fetchPortalHtmlViaPage(url);
if (viaPage) text = viaPage;
} catch (_) {}
if (!text || looksLikePortalLoginHtml(text)) {
try {
const res = await gmRequestFollow({
method: "GET",
url,
headers: {
Accept: "text/html,application/xhtml+xml,*/*",
"Accept-Encoding": "identity",
Referer: String(location.href || PORTAL + "/"),
},
withCredentials: true,
});
status = Number(res.status) || 0;
finalUrl = String(res.finalUrl || url);
let gmText = String(res.responseText || res.response || "") || "";
gmText = await followObjectMovedHtml(gmText, finalUrl, async (u) => {
try {
const r2 = await gmRequestFollow({
method: "GET",
url: u,
headers: {
Accept: "text/html,application/xhtml+xml,*/*",
"Accept-Encoding": "identity",
Referer: String(location.href || PORTAL + "/"),
},
withCredentials: true,
});
return String(r2.responseText || r2.response || "") || "";
} catch (_) {
return "";
}
});
if (gmText.length > text.length) text = gmText;
} catch (e) {
dbg(`GM \u62c9\u9875\u5931\u8d25\uff1a${e.message || e}`);
}
}
if (!text) throw new Error(`\u62c9\u8bfe\u7a0b\u9875\u5931\u8d25 HTTP ${status || "net"}`);
if (looksLikePortalLoginHtml(text)) {
dbg(`\u62c9\u9875\u5f02\u5e38 status=${status} final=${finalUrl.slice(0, 120)} snip=${snippetHtml(text, 180)}`);
}
return text;
}
function ensurePortalHostForExam() {
syncPortalFromLocation();
try {
const ph = new URL(PORTAL).hostname.toLowerCase();
const lh = String(location.hostname || "").toLowerCase();
if (ph && lh && ph !== lh) {
log(`\u8bf7\u5728\u5b66\u4e60\u95e8\u6237 ${ph} \u6253\u5f00\u540e\u518d\u70b9\u4f5c\u4e1a/\u8003\u8bd5\uff08\u5f53\u524d\u9875\u9762\u662f ${lh}\uff09`, "err");
try {
if (typeof GM_openInTab === "function") {
GM_openInTab(PORTAL + COURSE_LIST_PATH, { active: true });
} else {
window.open(PORTAL + COURSE_LIST_PATH, "_blank");
}
} catch (_) {}
return false;
}
} catch (_) {}
return true;
}
async function refreshCourses() {
setStatus("\u6b63\u5728\u52a0\u8f7d\u8bfe\u7a0b…");
log("\u6b63\u5728\u52a0\u8f7d\u8bfe\u7a0b\u5217\u8868");
const html = await fetchPortalHtml(COURSE_LIST_PATH, { forceNet: true });
const parsed = parseCoursesFromHtml(html);
const all = parsed.courses || [];
state.allCourses = all;
state.currentTermIndex = parsed.currentTermIndex;
state.termTabs = buildTermTabs(all, state.currentTermIndex);
if (!state.termFilter) state.termFilter = "current";
const list = applyTermFilter();
const curLabel =
(all.find((c) => c.termIndex === state.currentTermIndex) || {}).termLabel || "\u5f53\u524d\u5b66\u671f";
log(`\u5df2\u8f7d\u5165 ${all.length} \u95e8\u8bfe\uff0c\u5f53\u524d\u663e\u793a\uff1a${state.termFilter === "current" ? curLabel : "\u6240\u9009\u5b66\u671f"}\uff08${list.length} \u95e8\uff09`);
renderTermTabs();
renderCourses();
for (const c of list) {
try {
await fetchCourseStudyQuota(c);
log(`\u300c${c.title}\u300d${courseScoreLine(c)}`);
renderCourses();
} catch (_) {}
await sleep(200);
}
setStatus(list.length ? `\u5df2\u52a0\u8f7d ${list.length} \u95e8\u8bfe` : "\u672a\u8bc6\u522b\u5230\u8bfe\u7a0b\uff0c\u8bf7\u5148\u767b\u5f55");
renderCourses();
return list;
}
async function recheckCourseOnList(course) {
try {
const html = await fetchPortalHtml(COURSE_LIST_PATH, { forceNet: true });
const parsed = parseCoursesFromHtml(html);
const list = parsed.courses || [];
const hit = list.find((c) => c.termCourseID === course.termCourseID);
if (hit) {
if (hit.portalScore != null) course.portalScore = hit.portalScore;
if (hit.title && !looksLikeCourseCode(hit.title)) course.title = hit.title;
}
const q = await fetchCourseStudyQuota(course);
log(`\u300c${course.title}\u300d${courseScoreLine(course)}`);
return { listHit: hit, quota: q };
} catch (e) {
dbg(`\u56de\u67e5\u5931\u8d25\uff1a${e.message || e}`);
log(`\u300c${course.title}\u300d\u6210\u7ee9\u5237\u65b0\u5931\u8d25`);
return null;
}
}
async function compareTime(termCourseID) {
try {
const body = new URLSearchParams({ termcourseID: String(termCourseID), type: "0" });
const { j } = await gmJson({
method: "POST",
url: `${PORTAL}/MyOnlineCourseNew/OnlineLearningNew/CompareTime`,
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
},
data: body.toString(),
});
return !j || j.state !== false;
} catch (_) {
return true;
}
}
async function getCourseLearnUrl(course) {
const tid = String(course.termCourseID || "");
const code = String(course.courseCode || "");
const compareUrl = `${PORTAL}/MyOnlineCourseNew/OnlineLearningNew/CompareTime`;
const courseUrl = `${PORTAL}/MyOnlineCourseNew/OnlineLearningNew/GetCourseUrl`;
const courseExUrl = `${PORTAL}/MyOnlineCourseNew/OnlineLearningNew/MyOnlineCourseExIndex?termCourseID=${encodeURIComponent(
tid
)}`;
const compareBody = new URLSearchParams({ termcourseID: tid, type: "0" }).toString();
const courseBody = new URLSearchParams({ termcourseID: tid, courseCode: code }).toString();
let lastErr = "";
try {
const framed = await withAllTermListFrame(async ({ xhr, navigate }) => {
const ct = await xhr("POST", compareUrl, compareBody);
if (ct.j && ct.j.state === false) throw new Error(String(ct.j.msg || "\u4e0d\u5728\u5b66\u4e60\u65f6\u95f4\u7a97\u53e3\u5185"));
const html = await navigate(courseExUrl, 700);
if (looksLikePortalSysError(html) || looksLikePortalLoginHtml(html)) {
dbg(`\u8bfe\u4ef6\u9875\u5f02\u5e38 snip=${snippetHtml(html, 100)}`);
return null;
}
return xhr("POST", courseUrl, courseBody);
}, 32000);
if (framed && framed.j && framed.j.state && framed.j.msg) {
dbg(`GetCourseUrl \u9759\u9ed8\u6210\u529f\u300c${course.title}\u300d`);
return String(framed.j.msg).trim();
}
lastErr =
(framed && framed.j && framed.j.msg) ||
`HTTP ${(framed && framed.status) || "?"} ${String((framed && framed.text) || "").slice(0, 80)}`;
} catch (e) {
if (/\u4e0d\u5728\u5b66\u4e60/.test(String(e.message || e))) throw e;
lastErr = e.message || String(e);
}
throw new Error(`GetCourseUrl \u5931\u8d25 ${lastErr}`);
}
function adoptBackUrl(backUrl) {
try {
const u = new URL(backUrl);
PORTAL = u.origin;
const en = u.searchParams.get("ename");
if (en) SCHOOL = en;
else SCHOOL = schoolAbbFromHost(u.hostname);
gmSet(PORTAL_CACHE_KEY, { portal: PORTAL, school: SCHOOL, host: u.hostname });
refreshSchoolDisplayUI();
} catch (_) {}
}
function sessionFromLearnUrl(learnUrl, course) {
const q = parseQuery(learnUrl);
let backUrl = backUrlOf();
if (q.remark) {
try {
const rem = decodeURIComponent(q.remark);
if (/^https?:\/\//i.test(rem)) backUrl = rem.split("#")[0];
} catch (_) {}
}
if (q.backUrl && /^https?:\/\//i.test(q.backUrl)) backUrl = q.backUrl;
adoptBackUrl(backUrl);
return {
learnUrl,
clientCode: String(q.clientCode || ""),
userId: String(q.userId || ""),
userName: String(q.userName || SCHOOL),
coursewareCode: String(q.coursewareCode || course.courseCode || ""),
businessLineCode: String(q.businessLineCode || ""),
coursewareName: course.title || "",
classNum: "",
backUrl,
termCourseID: course.termCourseID,
courseCode: course.courseCode,
};
}
async function warmLearnSession(session) {
try {
await gmRequest({ method: "GET", url: session.learnUrl, timeout: 45000 });
} catch (_) {}
try {
await gmRequest({ method: "GET", url: `${CWS}/page/client`, timeout: 20000 });
} catch (_) {}
}
function catalogIdOf(node) {
if (!node || typeof node !== "object") return "";
const id =
node.id ??
node.catalogId ??
node.catalogID ??
node.chapterId ??
node.resourceId ??
node.nodeId;
return id == null || id === "" ? "" : String(id);
}
function catalogVideoTimeOf(node) {
const n =
Number(node.mediaDuration) ||
Number(node.videoTime) ||
Number(node.duration) ||
Number(node.media && node.media.duration) ||
0;
return Math.max(0, Math.floor(n));
}
function catalogSofarOf(node) {
const lr = node.learnRecord && typeof node.learnRecord === "object" ? node.learnRecord : null;
const v =
(lr && (lr.accumulativeTimesofar ?? lr.accumulativeTime)) ??
node.accumulativeTimesofar ??
node.accumulativeTime ??
node.sofar ??
0;
return Math.max(0, parseInt(v, 10) || 0);
}
function catalogLastTimeOf(node) {
const lr = node.learnRecord && typeof node.learnRecord === "object" ? node.learnRecord : null;
const v = (lr && lr.lastTime) ?? node.lastTime ?? 0;
return Math.max(0, Number(v) || 0);
}
function catalogTitleOf(node) {
const lr = node.learnRecord && typeof node.learnRecord === "object" ? node.learnRecord : null;
return String(
node.title ||
node.catalogTitle ||
node.name ||
node.chapterName ||
(lr && lr.catalogTitle) ||
""
).trim();
}
function applyChapterDone(ch) {
const vt = ch.videoTime || 0;
if (vt <= 0) {
ch.done = false;
return;
}
const sofar = ch.sofar || 0;
ch.done = sofar >= vt - 2;
}
function chapterPct(ch) {
const vt = ch.videoTime || 0;
if (vt <= 0) return ch.done ? 100 : 0;
return Math.min(100, Math.round(((ch.sofar || 0) / vt) * 100));
}
function parseCatalogFlat(data) {
if (!Array.isArray(data) || !data.length) return [];
const out = [];
const push = (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 ch = {
catalogId: id,
catalogTitle: catalogTitleOf(node) || `\u7ae0\u8282${out.length + 1}`,
videoTime,
sofar: catalogSofarOf(node),
lastTime: catalogLastTimeOf(node),
done: false,
questions: Array.isArray(node.questions) ? node.questions : [],
_raw: node,
};
applyChapterDone(ch);
out.push(ch);
};
data.forEach((n) => push(n, true));
if (!out.length) data.forEach((n) => push(n, false));
return out;
}
function pickNeedMinFromObj(obj) {
if (!obj || typeof obj !== "object") return null;
const keys = [
"needStudyTime",
"needStudyMinute",
"studyNeed",
"studyNeedTime",
"requiredStudyTime",
"requiredTime",
"needTime",
"totalStudyTime",
"standardStudyTime",
"kjNeedTime",
"needMinute",
"needMinutes",
];
for (const k of keys) {
if (obj[k] != null && obj[k] !== "") {
const n = parseInt(obj[k], 10);
if (Number.isFinite(n) && n > 0) {
return n > 10000 ? Math.ceil(n / 60) : n;
}
}
}
return null;
}
async function fetchCatalogs(session, course) {
const { j, status, text } = await 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",
},
data: JSON.stringify({
coursewareCode: session.coursewareCode,
clientCode: session.clientCode,
userId: session.userId,
}),
});
if (status < 200 || status >= 300) throw new Error(`catalogs HTTP ${status}`);
const data = j && j.data != null ? j.data : j;
let chapters = parseCatalogFlat(Array.isArray(data) ? data : []);
try {
const cw = await gmJson({
method: "GET",
url: `${CWS}/client/${encodeURIComponent(session.coursewareCode)}/findCwByCode?userId=${encodeURIComponent(
session.userId
)}&clientCode=${encodeURIComponent(session.clientCode)}`,
});
const d = cw.j && cw.j.data != null ? cw.j.data : cw.j;
if (!chapters.length) {
const arr = Array.isArray(d) ? d : d && Array.isArray(d.catalogs) ? d.catalogs : [];
chapters = parseCatalogFlat(arr);
}
if (d && d.coursewareName) session.coursewareName = d.coursewareName;
if (course) {
const need = pickNeedMinFromObj(d) || pickNeedMinFromObj(cw.j);
if (need && !(course.needMin > 0)) course.needMin = need;
const learned = pickNeedMinFromObj(
d && typeof d === "object"
? {
needStudyTime: d.studiedTime || d.studyTime || d.learnedTime || d.accumulativeTime,
}
: null
);
if (d && typeof d === "object") {
for (const k of ["studiedTime", "studiedMinute", "learnedTime", "learnTime", "studyTimeSofar"]) {
if (d[k] != null) {
const n = parseInt(d[k], 10);
if (Number.isFinite(n) && n >= 0) {
const min = n > 10000 ? Math.floor(n / 60) : n;
course.learnedMin = Math.max(Number(course.learnedMin) || 0, min);
break;
}
}
}
}
}
} catch (e) {
dbg(`findCwByCode \u5931\u8d25\uff1a${e.message || e}`);
}
if (!chapters.length) throw new Error(`\u672a\u62c9\u5230\u7ae0\u8282 ${(text || "").slice(0, 120)}`);
return chapters;
}
async function fetchCatalogInfo(session, catalogId) {
const { j } = await gmJson({
method: "POST",
url: `${CWS}/client/catalogInfo/${encodeURIComponent(catalogId)}`,
headers: {
"Content-Type": "application/json;charset=UTF-8",
Accept: "application/json, text/javascript, */*; q=0.01",
},
data: JSON.stringify({
clientCode: session.clientCode,
userId: session.userId,
isQuestion: true,
coursewareCode: session.coursewareCode,
}),
});
const data = j && j.data != null ? j.data : j;
if (!data || typeof data !== "object") return null;
return {
videoTime: catalogVideoTimeOf(data),
sofar: catalogSofarOf(data),
lastTime: catalogLastTimeOf(data),
questions: Array.isArray(data.questions) ? data.questions : [],
title: catalogTitleOf(data),
_raw: data,
};
}
function buildRecord(session, chapter, accumulativeTime, lastTime, learnRecordId) {
const rec = {
learnRecordId: learnRecordId != null ? learnRecordId : 0,
userId: session.userId,
userName: session.userName,
clientCode: session.clientCode,
businessLineCode: session.businessLineCode || "",
coursewareCode: session.coursewareCode,
catalogId: String(chapter.catalogId),
catalogTitle: chapter.catalogTitle || "",
classNum: session.classNum || "",
coursewareName: session.coursewareName || "",
videoTime: chapter.videoTime || 0,
backUrl: session.backUrl || backUrlOf(),
validTime: 0,
accumulativeTime,
lastTime,
deviceType: "TYPE_PC",
orderNo: "",
token: "",
maxTimePerDay: 0,
};
const sig = signLearnRecord(rec);
rec.md5 = sig.md5;
rec.md5Timestamp = sig.md5Timestamp;
return rec;
}
function timingOk(j) {
if (!j) return false;
if (j.code === 0 || j.success === true || j.success === "true") return true;
if (j.data && (j.data.lastTime != null || j.data.accumulativeTimesofar != null || j.data.learnRecordId != null)) {
return true;
}
return false;
}
function absorbLearnData(chapter, data, preferSofar) {
if (!data || typeof data !== "object") return { sofar: chapter.sofar || 0, lastTime: chapter.lastTime || 0 };
if (data.learnRecordId != null) chapter._learnRecordId = data.learnRecordId;
if (data.lastTime != null) {
const srv = Number(data.lastTime) || 0;
chapter.lastTime = Math.max(chapter.lastTime || 0, srv);
}
if (data.accumulativeTimesofar != null) {
const sf = parseInt(data.accumulativeTimesofar, 10) || 0;
chapter.sofar = preferSofar ? sf : Math.max(chapter.sofar || 0, sf);
} else if (data.accumulativeTime != null && preferSofar) {
}
if (data.videoTime != null) {
const vt = parseInt(data.videoTime, 10) || 0;
if (vt > 0) chapter.videoTime = vt;
}
state.sofar = chapter.sofar || 0;
state.lastTime = chapter.lastTime || 0;
state.videoTime = chapter.videoTime || 0;
applyChapterDone(chapter);
return { sofar: chapter.sofar || 0, lastTime: chapter.lastTime || 0, learnRecordId: chapter._learnRecordId };
}
async function putLearnCws(kind, session, chapter, accumulativeTime, lastTime) {
const learnRecordId = chapter._learnRecordId || 0;
const rec = buildRecord(session, chapter, accumulativeTime, lastTime, learnRecordId);
const path = kind === "start" ? "/client/learnRecords/start" : "/client/learnRecords/timing";
const signed = 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",
Referer: `${CWS}/page/client`,
Origin: CWS,
},
data: JSON.stringify(rec),
});
if (timingOk(signed.j)) {
return { j: signed.j, text: signed.text, status: signed.status, style: "signed" };
}
if (kind === "start") {
const createBody = {
learnRecordId: 0,
userId: session.userId,
userName: session.userName,
clientCode: session.clientCode,
businessLineCode: session.businessLineCode || "",
coursewareCode: session.coursewareCode,
coursewareName: session.coursewareName || "",
catalogId: String(chapter.catalogId),
catalogTitle: chapter.catalogTitle || "",
videoTime: null,
backUrl: session.backUrl || backUrlOf(),
validTime: 0,
accumulativeTime: 0,
token: "",
};
const created = await gmJson({
method: "POST",
url: `${CWS}/client/learnRecords`,
headers: {
"Content-Type": "application/json;charset=UTF-8",
Accept: "application/json, text/javascript, */*; q=0.01",
Referer: `${CWS}/page/client`,
Origin: CWS,
},
data: JSON.stringify(createBody),
});
if (timingOk(created.j) || (created.j && created.j.data && created.j.data.learnRecordId)) {
return { j: created.j, text: created.text, status: created.status, style: "post" };
}
return { j: signed.j || created.j, text: signed.text || created.text, status: signed.status || created.status, style: "fail" };
}
const id = chapter._learnRecordId;
if (id) {
const putBody = {
learnRecordId: String(id),
userId: session.userId,
userName: session.userName,
clientCode: session.clientCode,
businessLineCode: session.businessLineCode || "",
coursewareCode: session.coursewareCode,
coursewareName: session.coursewareName || "",
catalogId: String(chapter.catalogId),
catalogTitle: chapter.catalogTitle || "",
videoTime: chapter.videoTime || 0,
backUrl: session.backUrl || backUrlOf(),
validTime: 0,
accumulativeTime,
token: "",
lastTime,
classNum: session.classNum || "",
maxTimePerDay: 0,
};
const updated = await gmJson({
method: "PUT",
url: `${CWS}/client/learnRecords`,
headers: {
"Content-Type": "application/json;charset=UTF-8",
Accept: "application/json, text/javascript, */*; q=0.01",
Referer: `${CWS}/page/client`,
Origin: CWS,
},
data: JSON.stringify(putBody),
});
if (timingOk(updated.j)) {
return { j: updated.j, text: updated.text, status: updated.status, style: "put" };
}
return { j: updated.j || signed.j, text: updated.text || signed.text, status: updated.status || signed.status, style: "fail" };
}
return { j: signed.j, text: signed.text, status: signed.status, style: "fail" };
}
async function postBackUrl(data) {
const url = (data && data.backUrl) || (state.session && state.session.backUrl) || backUrlOf();
const payload = Object.assign({}, data || {});
if (!payload.backUrl) payload.backUrl = url;
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" ||
j.state === true ||
/\u540c\u6b65\u6210\u529f|success/i.test(String((j && (j.message || j.msg)) || text || ""));
if (!(okHttp && okBody)) {
dbg(`\u95e8\u6237\u56de\u5199\u5931\u8d25 HTTP ${status} ${(text || "").slice(0, 80)}`);
}
return okHttp && okBody;
} catch (e) {
dbg(`LearnReturnUrl \u5931\u8d25\uff1a${e.message || e}`);
return false;
}
}
async function submitKnowledgeAnswers(session, chapter, playSec, answers) {
if (!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, 0);
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",
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) continue;
const payload = Object.assign({}, base, {
examinePoint: q.examinePoint || "",
pass: true,
questionId: q.questionId,
questionType: q.questionType,
respond: q.respond != null && q.respond !== "" ? String(q.respond) : "",
});
if (q.optionId) payload.optionId = q.optionId;
if (await postBackUrl(payload)) okN += 1;
} catch (_) {}
await sleep(200);
}
return okN;
}
async function runChapter(session, chapter) {
const gap = getGapMs();
const step = getStepSec();
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;
if (info.lastTime != null) chapter.lastTime = info.lastTime;
if (info.title) chapter.catalogTitle = info.title;
if (info.questions && info.questions.length) chapter.questions = info.questions;
if (info._raw && info._raw.learnRecord && info._raw.learnRecord.learnRecordId != null) {
chapter._learnRecordId = info._raw.learnRecord.learnRecordId;
}
applyChapterDone(chapter);
}
const vt = chapter.videoTime || 0;
state.catalogTitle = chapter.catalogTitle;
state.videoTime = vt;
state.lastTime = chapter.lastTime || 0;
state.sofar = chapter.sofar || 0;
updatePanel();
renderChapters();
if (chapter.done) {
log(`\u300c${chapter.catalogTitle}\u300d\u5df2\u5b66\u5b8c\uff0c\u8df3\u8fc7`);
return true;
}
if (vt <= 0) {
if (chapter.questions && chapter.questions.length) {
await submitKnowledgeAnswers(session, chapter, 1, chapter.questions);
}
log(`\u300c${chapter.catalogTitle}\u300d\u65e0\u89c6\u9891\uff0c\u8df3\u8fc7`);
return true;
}
try {
await cloudConsumeChapter(chapter, state._activeCourse || null);
} catch (e) {
log(friendlyCloudMessage(e), "err");
throw e;
}
let lastTime = Math.min(Math.max(chapter.lastTime || 0, chapter.sofar || 0), vt);
let sofar = chapter.sofar || 0;
const answered = new Set();
const qs = Array.isArray(chapter.questions) ? chapter.questions.slice() : [];
const flushQs = async (playSec) => {
const due = qs.filter(
(q) => q && q.questionId != null && !answered.has(String(q.questionId)) && playSec >= (q.mediaTime || 0)
);
if (!due.length) return;
await submitKnowledgeAnswers(session, chapter, playSec, due);
due.forEach((q) => answered.add(String(q.questionId)));
};
log(
`\u5f00\u59cb\u5b66\u4e60\uff1a${chapter.catalogTitle}\uff08\u7d2f\u8ba1 ${formatDuration(sofar)} / \u4f4d\u7f6e ${formatDuration(lastTime)} / \u7247\u957f ${formatDuration(
vt
)}\uff09`
);
setStatus(`\u5b66\u4e60\u4e2d · ${chapter.catalogTitle}`);
try {
const s = await putLearnCws("start", session, chapter, 0, Math.max(1, lastTime || 1));
if (s.j && s.j.data) {
const abs = absorbLearnData(chapter, s.j.data, true);
lastTime = Math.max(lastTime, abs.lastTime || 0);
sofar = abs.sofar || sofar;
const backOk = await postBackUrl(s.j.data);
dbg(`start ${s.style || ""} · sofar=${formatDuration(sofar)} · back=${backOk}`);
} else if (!timingOk(s.j)) {
dbg(`start \u672a\u6210\u529f\uff1a${(s.text || "").slice(0, 120) || s.status}`);
log(`\u300c${chapter.catalogTitle}\u300d\u5f00\u59cb\u5b66\u4e60\u5931\u8d25`);
}
} catch (e) {
dbg(`start \u5f02\u5e38\uff1a${e.message || e}`);
log(`\u300c${chapter.catalogTitle}\u300d\u5f00\u59cb\u5b66\u4e60\u5931\u8d25`);
}
state.stall = 0;
state.tick = 0;
while (state.running && !state.stopFlag && sofar < vt - 1) {
const nextLast = Math.min(vt, Math.max(lastTime, sofar) + step);
const accum = Math.min(step, Math.max(1, vt - sofar));
const prevSofar = sofar;
try {
const t = await putLearnCws("timing", session, chapter, accum, nextLast);
state.tick += 1;
if (!timingOk(t.j) || !t.j || !t.j.data) {
state.stall += 1;
dbg(`\u4e0a\u62a5\u5931\u8d25\uff1a${(t.text || "").slice(0, 120) || t.status}`);
if (String(t.j && t.j.code) === "-1") {
log("\u4eca\u65e5\u5b66\u65f6\u5df2\u8fbe\u4e0a\u9650\uff0c\u5df2\u505c\u6b62");
return false;
}
if (state.stall >= 8) {
log(`\u300c${chapter.catalogTitle}\u300d\u4e0a\u62a5\u591a\u6b21\u5931\u8d25\uff0c\u5df2\u6682\u505c`);
return false;
}
} else {
const abs = absorbLearnData(chapter, t.j.data, true);
sofar = abs.sofar;
lastTime = Math.max(nextLast, abs.lastTime || 0);
chapter.lastTime = lastTime;
const backOk = await postBackUrl(t.j.data);
await flushQs(lastTime);
const gained = sofar - prevSofar;
if (gained <= 0) {
state.stall += 1;
dbg(`#${state.tick} sofar\u672a\u6da8 ${formatDuration(sofar)} +${accum}s back=${backOk}`);
if (state.stall >= 8) {
log("\u5b66\u4e60\u8fdb\u5ea6\u672a\u66f4\u65b0\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55\u540e\u91cd\u8bd5");
return false;
}
} else {
state.stall = 0;
if (state.tick === 1 || state.tick % 5 === 0 || sofar >= vt - 1) {
log(`\u300c${chapter.catalogTitle}\u300d\u8fdb\u5ea6 ${chapterPct(chapter)}%`);
} else {
dbg(`#${state.tick} ${chapter.catalogTitle} ${formatDuration(sofar)}/${formatDuration(vt)} +${gained}s`);
}
}
}
} catch (e) {
state.stall += 1;
dbg(`\u4e0a\u62a5\u5f02\u5e38\uff1a${e.message || e}`);
log(`\u300c${chapter.catalogTitle}\u300d\u5b66\u4e60\u4e0a\u62a5\u5931\u8d25`);
if (state.stall >= 8) return false;
}
updatePanel();
renderChapters();
await sleep(gap);
}
if (state.running && !state.stopFlag && sofar >= vt - 1) {
try {
const seal = await putLearnCws("timing", session, chapter, 0, vt);
if (seal.j && seal.j.data) {
absorbLearnData(chapter, seal.j.data, true);
await postBackUrl(seal.j.data);
}
} catch (_) {}
if (qs.length) await submitKnowledgeAnswers(session, chapter, vt, qs);
}
try {
const again = await fetchCatalogInfo(session, chapter.catalogId);
if (again) {
if (again.sofar != null) chapter.sofar = again.sofar;
if (again.lastTime != null) chapter.lastTime = again.lastTime;
}
} catch (_) {}
applyChapterDone(chapter);
if (chapter.done) {
log(`\u300c${chapter.catalogTitle}\u300d\u5df2\u5b8c\u6210`);
return true;
}
log(
`\u672a\u8fbe\u7d2f\u8ba1\u6ee1\u5206\uff1a${chapter.catalogTitle} \u5f53\u524d ${formatDuration(chapter.sofar || 0)}/${formatDuration(vt)}\uff08${chapterPct(
chapter
)}%\uff09`
);
return false;
}
const EM_CJ = "https://cjexamnew.edu-edu.com";
const EXAM_HOST_DEFAULT = "https://cje6.edu-edu.com";
const KS_TYPE_HOMEWORK = "2";
const KS_TYPE_FINAL = "3";
function examAnswerSign(userExamId, questionId, psqId, answer, stime) {
const raw = `answer=${answer}&psqId=${psqId}&stime=${stime}&key=${questionId}${userExamId}`;
return md5(raw);
}
function examBase(sess) {
const b = (sess && sess.examBase) || "";
if (/^https?:\/\/cje\d+\.edu-edu\.com$/i.test(b)) return b.replace(/\/$/, "");
if (/^https?:\/\/[a-z0-9.-]+\.edu-edu\.com$/i.test(b) && /\/exam\//i.test(String(sess.paperUrl || sess.doexamHtml || ""))) {
return b.replace(/\/$/, "");
}
return EXAM_HOST_DEFAULT;
}
function rememberExamBase(sess, urlOrHtml) {
if (!sess) return;
const s = String(urlOrHtml || "");
const m = s.match(/https?:\/\/(cje\d+\.edu-edu\.com)/i);
if (m) sess.examBase = `https://${m[1]}`;
}
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?:\/\/cjexamnew\.edu-edu\.com\/exam-admin\/student\/exam\/single\/restart\/[^"'\\\s<>]+/i) ||
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_CJ);
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_CJ);
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 };
}
function doexamUrlFromStart(startUrl) {
try {
const u = new URL(startUrl, EXAM_HOST_DEFAULT);
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,*/*;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", "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?:\/\/cje\d+\.edu-edu\.com\/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?:\/\/cje\d+\.edu-edu\.com\/exam\/student\/exam2\/start\/[^\s"'<>]+/i
);
if (!startUrl && m2) startUrl = m2[0].replace(/&/g, "&");
}
if (!startUrl) 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).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, startUrl };
else doexamUrl = doexamUrlFromStart(startUrl);
if (!doexamUrl) 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 || "")) 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 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_card4?\/(\d+)\/(\d+)\//i) || html.match(/paper_card2\/(\d+)\/(\d+)\//i);
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?:\/\/cjexamnew\.edu-edu\.com\/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_CJ).href;
} 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}`;
}
}
function applyPaperUrlFromHtml(sess, html) {
const m =
String(html || "").match(/\/exam\/student\/exam\/resource\/(paper_card4\/\d+\/\d+\/[^"'?\s]+)/i) ||
String(html || "").match(/\/exam\/student\/exam\/resource\/(paper_card2\/\d+\/\d+\/[^"'?\s]+)/i);
if (!m) return false;
const segs = m[1].match(/paper_card\d*\/(\d+)\/(\d+)\//i);
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_card/i.test(sess.paperUrl) || !sess.paperUrl.startsWith(base)) {
sess.paperUrl = `${base}/exam/student/exam/resource/paper_card4/${sess.pid}/${sess.psid}/214__index.html`;
}
}
}
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 compareTimeKindForKs(ksType) {
const k = Number(ksType) || 2;
return String(Math.max(1, k - 1));
}
function decodeBasicHtmlEntities(s) {
return String(s || "")
.replace(/&/g, "&")
.replace(/"/g, '"')
.replace(/*39;/g, "'")
.replace(/*27;/gi, "'")
.replace(/'/g, "'")
.replace(/(\d+);/g, (_, n) => {
const c = Number(n);
return Number.isFinite(c) ? String.fromCharCode(c) : _;
});
}
function parseExIndexPapers(html, ksType, expectTermCourseID) {
const list = [];
const seen = new Set();
const ks = String(ksType || KS_TYPE_HOMEWORK);
const expectTid = String(expectTermCourseID || "").trim();
const src = decodeBasicHtmlEntities(String(html || ""));
const pushPaper = (examCode, allowCount, termCourseID, buttonName, title, startOrRecord, signCJ) => {
const code = String(examCode || "").trim();
let tid = String(termCourseID || "").trim();
const sor = String(startOrRecord || "1").trim() || "1";
const sign = String(signCJ || "").trim();
if (sor === "2") return;
if (!isValidExPaperFields(tid, sign, code)) return;
if (expectTid && tid !== expectTid) return;
const titleText = String(title || code).trim();
const btn = String(buttonName || (ks === KS_TYPE_FINAL ? "\u671f\u672b\u8003\u8bd5" : "\u5e73\u65f6\u4f5c\u4e1a")).trim();
if (/\u4e0d\u8ba1\u5165\u6210\u7ee9|\u4e0d\u7b97\u6210\u7ee9|\u4ec5\u590d\u4e60|\u81ea\u6d4b\u7ec3\u4e60/.test(titleText)) return;
if (ks === KS_TYPE_HOMEWORK && /\u7efc\u5408\u6d4b\u8bd5/.test(titleText) && /\u590d\u4e60|\u81ea\u6d4b|\u4e0d\u8ba1\u5165|\u4e0d\u7b97/.test(titleText)) return;
const key = `${code}|${tid}|${sor}`;
if (seen.has(key)) return;
seen.add(key);
list.push({
examCode: code,
allowCount: String(allowCount || ""),
termCourseID: tid,
buttonName: btn,
title: titleText,
startOrRecord: sor,
signCJ: sign,
ksType: ks,
});
};
const reClick2 =
/pszy_click2\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]*)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]*)['"]\s*,\s*['"]([^'"]*)['"]\s*,\s*['"]([^'"]*)['"]\s*,\s*['"]([^'"]+)['"]\s*\)/gi;
let m;
while ((m = reClick2.exec(src))) {
pushPaper(m[1], m[2], m[3], m[4], m[5], m[6], m[7]);
}
if (!list.length && /start_testing|\u5f00\u59cb\u8003\u8bd5/i.test(src)) {
const reClick =
/pszy_click\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]*)['"]\s*,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]*)['"]\s*,\s*['"]([^'"]*)['"]\s*,\s*['"]1['"]\s*,\s*['"]([^'"]+)['"]\s*\)/gi;
while ((m = reClick.exec(src))) {
pushPaper(m[1], m[2], m[3], m[4], m[5], "1", m[6]);
}
}
return list;
}
async function fetchExIndexPapers(course, ksType) {
const ks = String(ksType || KS_TYPE_HOMEWORK);
syncPortalFromLocation();
const tid = String((course && course.termCourseID) || "").trim();
if (!tid) {
log(`\u300c${(course && course.title) || "\u8bfe\u7a0b"}\u300d\u7f3a\u5c11 termCourseID\uff0c\u65e0\u6cd5\u6253\u5f00\u4f5c\u4e1a/\u8003\u8bd5`, "err");
return [];
}
const finishOk = (papers) => {
if (ks === KS_TYPE_FINAL) course.examPapers = papers;
else course.homeworkPapers = papers;
if (ks === KS_TYPE_FINAL) course.hasExam = true;
else course.hasHomework = true;
return papers;
};
const htmlRef = { html: "" };
let papers = [];
if (isPortal && /StudentSiteNewExIndex/i.test(location.pathname || "")) {
try {
const q = new URLSearchParams(location.search || "");
const qid = String(q.get("termcourseID") || q.get("termCourseID") || "");
const typ = String(q.get("type") || "");
if ((!qid || qid === tid) && (!typ || typ === ks)) {
const pageHtml = document.documentElement.outerHTML;
if (looksLikeExIndexHtml(pageHtml) && !looksLikePortalSysError(pageHtml)) {
const papersNow = parseExIndexPapers(pageHtml, ks, tid);
if (papersNow.length) return finishOk(papersNow);
}
}
} catch (_) {}
}
const exUrl = `${PORTAL}/MyOnlineCourseNew/OnlineLearningNew/StudentSiteNewExIndex?termcourseID=${encodeURIComponent(
tid
)}&type=${encodeURIComponent(ks)}`;
const compareUrl = `${PORTAL}/MyOnlineCourseNew/OnlineLearningNew/CompareTime`;
const compareBody = new URLSearchParams({
termcourseID: tid,
type: compareTimeKindForKs(ks),
}).toString();
dbg(`\u9759\u9ed8\u62c9\u8bd5\u5377\uff08isCurrent=0→ExIndex\uff09\u300c${course.title}\u300d${exUrl}`);
try {
const framedHtml = await withAllTermListFrame(async ({ xhr, navigate }) => {
try {
await xhr("POST", compareUrl, compareBody);
} catch (_) {}
return navigate(exUrl, 800);
}, 32000);
if (framedHtml) {
htmlRef.html = framedHtml;
if (!looksLikePortalSysError(framedHtml) && looksLikeExIndexHtml(framedHtml)) {
papers = parseExIndexPapers(framedHtml, ks, tid);
if (papers.length) {
dbg(`\u9759\u9ed8\u53d6\u5f97 ${papers.length} \u4efd${ks === KS_TYPE_FINAL ? "\u8bd5\u5377" : "\u4f5c\u4e1a"}\u300c${course.title}\u300d`);
return finishOk(papers);
}
} else {
dbg(`\u9759\u9ed8 ExIndex \u5f02\u5e38 snip=${snippetHtml(framedHtml, 120)}`);
}
}
} catch (e) {
dbg(`\u9759\u9ed8\u62c9\u5377\u5931\u8d25\uff1a${e.message || e}`);
}
if (!papers.length) {
const html = htmlRef.html || "";
const hasBtn = /\u5f00\u59cb\u8003\u8bd5|start_testing|\u5269\u4f59\u8003\u8bd5\u6b21\u6570/i.test(html);
const hasClick = /pszy_click2?\s*\(/i.test(html);
const login = looksLikePortalLoginHtml(html);
const sysErr = looksLikePortalSysError(html);
const snip = snippetHtml(html, 160);
dbg(
`\u8bd5\u5377\u5217\u8868\u7a7a\u300c${course.title}\u300dtype=${ks} tid=${tid} html=${html.length} btn=${hasBtn} click=${hasClick} login=${login} sysErr=${sysErr} snip=${snip}`
);
if (sysErr) {
log(
`\u300c${course.title}\u300d\u9759\u9ed8\u62c9\u53d6\u4f5c\u4e1a/\u8003\u8bd5\u5931\u8d25\uff08\u9700\u5148\u8fdb\u5165\u5168\u90e8\u5b66\u671f isCurrent=0\uff09\u3002\u8bf7\u5237\u65b0\u95e8\u6237\u540e\u91cd\u8bd5\uff0c\u5e76\u7981\u7528\u300c\u81ea\u8003\u4e91\u300d\u3002`,
"err"
);
} else if (login || /Object moved|\/Login/i.test(html) || html.length < 800) {
log(
`\u300c${course.title}\u300d\u4f5c\u4e1a/\u8003\u8bd5\u9875\u672a\u6253\u5f00\uff08\u767b\u5f55\u5931\u6548\u6216\u88ab\u8df3\u8f6c\uff09\u3002\u8bf7\u5237\u65b0\u540e\u91cd\u8bd5\u3002\u8be6\u60c5\uff1a${snip || "\u7a7a\u54cd\u5e94"}`,
"err"
);
} else if (hasBtn && !hasClick) {
log(`\u300c${course.title}\u300d\u8003\u8bd5\u9875\u6709\u6309\u94ae\u4f46\u672a\u80fd\u8bc6\u522b\u5f00\u8003\u94fe\u63a5\uff0c\u8bf7\u66f4\u65b0\u811a\u672c\u540e\u91cd\u8bd5`, "err");
}
}
if (ks === KS_TYPE_FINAL) course.examPapers = papers;
else course.homeworkPapers = papers;
if (ks === KS_TYPE_FINAL && papers.length) course.hasExam = true;
else if (ks === KS_TYPE_HOMEWORK && papers.length) course.hasHomework = true;
return papers;
}
async function fetchHomeworkPapers(course) {
return fetchExIndexPapers(course, KS_TYPE_HOMEWORK);
}
async function fetchFinalExamPapers(course) {
return fetchExIndexPapers(course, KS_TYPE_FINAL);
}
async function compareTimeType(termCourseID, type) {
try {
const body = new URLSearchParams({ termcourseID: String(termCourseID), type: String(type) });
const { j } = await gmJson({
method: "POST",
url: `${PORTAL}/MyOnlineCourseNew/OnlineLearningNew/CompareTime`,
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
},
data: body.toString(),
});
if (j && j.state === false) throw new Error(j.msg || "\u4e0d\u5728\u8003\u8bd5/\u4f5c\u4e1a\u65f6\u95f4\u7a97\u53e3");
return true;
} catch (e) {
if (/\u4e0d\u5728/.test(String(e.message || e))) throw e;
return true;
}
}
async function ensureExamEntry(exam) {
const ks = String(exam.ksType || KS_TYPE_HOMEWORK);
try {
await gmJson({
method: "POST",
url: `${PORTAL}/MyOnlineCourseNew/OnlineLearningNew/GetExamStuCertificateBySuId`,
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
},
data: new URLSearchParams({ InstructionsType: ks }).toString(),
});
} catch (_) {}
try {
await compareTimeType(exam.termCourseID, compareTimeKindForKs(ks));
if (ks === KS_TYPE_FINAL) await compareTimeType(exam.termCourseID, ks);
} catch (e) {
if (/\u4e0d\u5728/.test(String(e.message || e))) throw e;
}
try {
await gmJson({
method: "POST",
url: `${PORTAL}/StudentSiteNew/StudentSiteIndexNew/GetFaceCjAndDbModel`,
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
},
data: new URLSearchParams({ termCourse_id: String(exam.termCourseID) }).toString(),
});
} catch (_) {}
}
function exIndexRefererFor(exam) {
const tid = encodeURIComponent(String((exam && exam.termCourseID) || ""));
const ks = encodeURIComponent(String((exam && exam.ksType) || KS_TYPE_HOMEWORK));
return `${PORTAL}/MyOnlineCourseNew/OnlineLearningNew/StudentSiteNewExIndex?termcourseID=${tid}&type=${ks}`;
}
function fillExamSignFromDom(exam) {
if (!exam || /^[a-f0-9]{32}$/i.test(String(exam.signCJ || "").trim())) return;
const take = (v) => {
const s = String(v || "").trim();
return /^[a-f0-9]{32}$/i.test(s) ? s : "";
};
try {
const sels = ["#hidSign", "input[name='hidSign']", "#hidSignCJ"];
for (const s of sels) {
const el = document.querySelector(s);
const v = take(el && el.value);
if (v) {
exam.signCJ = v;
return;
}
}
const fr = findPortalContentFrame();
if (fr && fr.contentDocument) {
for (const s of sels) {
const el = fr.contentDocument.querySelector(s);
const v = take(el && el.value);
if (v) {
exam.signCJ = v;
return;
}
}
}
} catch (_) {}
}
async function postGetExamUrlNew(exam, startOrRecord) {
fillExamSignFromDom(exam);
const sor = String(startOrRecord || "1");
const tid = String(exam.termCourseID || "");
const ks = String(exam.ksType || KS_TYPE_HOMEWORK);
const body = new URLSearchParams({
ksType: ks,
termcourseID: tid,
courseCode: String(exam.examCode),
startOrRecord: sor,
signCJ: String(exam.signCJ || ""),
}).toString();
const url = `${PORTAL}/MyOnlineCourseNew/OnlineLearningNew/GetExamUrl_New`;
const exUrl = exIndexRefererFor(exam);
const compareUrl = `${PORTAL}/MyOnlineCourseNew/OnlineLearningNew/CompareTime`;
const compareBody = new URLSearchParams({
termcourseID: tid,
type: compareTimeKindForKs(ks),
}).toString();
const paramHint = `code=${exam.examCode} ks=${ks} tid=${tid} sor=${sor} signLen=${String(exam.signCJ || "").length}`;
try {
await ensureExamEntry(exam);
} catch (e) {
if (/\u4e0d\u5728/.test(String(e.message || e))) throw e;
}
let lastErr = "";
try {
const framed = await withAllTermListFrame(async ({ xhr, navigate }) => {
try {
await xhr("POST", compareUrl, compareBody);
} catch (_) {}
const html = await navigate(exUrl, 700);
if (looksLikePortalSysError(html)) {
dbg(`GetExamUrl \u524d ExIndex \u5f02\u5e38 snip=${snippetHtml(html, 80)}`);
return null;
}
try {
await xhr(
"POST",
`${PORTAL}/MyOnlineCourseNew/OnlineLearningNew/GetExamStuCertificateBySuId`,
new URLSearchParams({ InstructionsType: ks }).toString()
);
} catch (_) {}
try {
await xhr(
"POST",
`${PORTAL}/StudentSiteNew/StudentSiteIndexNew/GetFaceCjAndDbModel`,
new URLSearchParams({ termCourse_id: tid }).toString()
);
} catch (_) {}
return xhr("POST", url, body);
}, 36000);
if (framed && framed.j && framed.j.state && framed.j.msg) return framed.j;
lastErr =
(framed && framed.j && framed.j.msg) ||
`HTTP ${(framed && framed.status) || "?"} ${String((framed && framed.text) || "").slice(0, 80)}`;
} catch (e) {
lastErr = e.message || String(e);
}
if (!/^[a-f0-9]{32}$/i.test(String(exam.signCJ || "").trim())) {
throw new Error(`GetExamUrl_New \u7f3a\u5c11\u6709\u6548 signCJ\uff08${paramHint}\uff09`);
}
throw new Error(`GetExamUrl_New \u5931\u8d25 ${paramHint} ${lastErr}`);
}
function pickCjeExamHop(html, baseUrl) {
const src = String(html || "").replace(/&/g, "&");
const patterns = [
/https?:\/\/cje\d+\.edu-edu\.com\/exam\/student\/exam2\/(?:start|doexam)\/[^\s"'<>\\]+/i,
/https?:\/\/cje\d+\.edu-edu\.com\/exam\/student\/exam2\/(?:start|doexam)\/[^"'\\\s]+/i,
/["'](https?:\/\/cje\d+\.edu-edu\.com\/exam\/student\/exam2\/(?:start|doexam)\/[^"']+)["']/i,
/(?:location\.href|location\.replace|window\.location)\s*=\s*["']([^"']*exam2\/(?:start|doexam)\/[^"']+)["']/i,
/content=["']\d+;\s*url=([^"']*exam2\/(?:start|doexam)\/[^"']+)["']/i,
/(\/exam\/student\/exam2\/(?:start|doexam)\/[^\s"'<>]+)/i,
];
for (const re of patterns) {
const m = src.match(re);
if (!m) continue;
const raw = (m[1] || m[0] || "").replace(/^["']|["']$/g, "");
try {
return new URL(raw, baseUrl || EXAM_HOST_DEFAULT).href;
} catch (_) {}
}
return "";
}
async function gmGetOnceExam(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);
}
return {
note,
status: res.status,
loc,
finalUrl: res.finalUrl || url,
text: res.responseText || "",
res,
};
}
async function followAdminStartToDoexam(adminStartUrl) {
const hops = [];
let cur = adminStartUrl;
let startUrl = "";
let doexamUrl = "";
let last = null;
for (let i = 0; i < 8 && !doexamUrl; i++) {
last = await gmGetOnceExam(cur, `hop${i}`);
hops.push(`${last.note}:${last.status}@${(last.finalUrl || cur).slice(0, 100)}`);
if (last.loc) {
try {
const abs = new URL(last.loc, cur).href;
if (/exam2\/doexam\//i.test(abs)) {
doexamUrl = abs;
break;
}
if (/exam2\/start\//i.test(abs)) {
startUrl = abs;
cur = abs;
continue;
}
if (/cjexamnew|cje\d+\.edu-edu\.com/i.test(abs)) {
cur = abs;
continue;
}
} catch (_) {}
}
if (/exam2\/doexam\//i.test(last.finalUrl || "")) {
doexamUrl = last.finalUrl;
break;
}
if (/exam2\/start\//i.test(last.finalUrl || "")) {
startUrl = last.finalUrl;
}
if (/ExamObj\s*\(/.test(last.text || "")) {
return { url: last.finalUrl || cur, text: last.text, hops, startUrl };
}
const picked = pickCjeExamHop(last.text, last.finalUrl || cur);
if (picked) {
if (/exam2\/doexam\//i.test(picked)) {
doexamUrl = picked;
break;
}
if (/exam2\/start\//i.test(picked)) {
startUrl = picked;
cur = picked;
continue;
}
cur = picked;
continue;
}
if (i === 0 || i === 2) {
const auto = await gmRequest({
method: "GET",
url: cur,
timeout: 60000,
headers: { Accept: "text/html,application/xhtml+xml", "Upgrade-Insecure-Requests": "1" },
});
hops.push(`auto:${auto.status}@${(auto.finalUrl || "").slice(0, 100)}`);
if (/ExamObj\s*\(/.test(auto.responseText || "")) {
return { url: auto.finalUrl || cur, text: auto.responseText || "", hops, startUrl };
}
if (/exam2\/doexam\//i.test(auto.finalUrl || "")) {
doexamUrl = auto.finalUrl;
last = { finalUrl: auto.finalUrl, text: auto.responseText || "", status: auto.status };
break;
}
if (/exam2\/start\//i.test(auto.finalUrl || "")) {
startUrl = auto.finalUrl;
cur = auto.finalUrl;
continue;
}
const p2 = pickCjeExamHop(auto.responseText || "", auto.finalUrl || cur);
if (p2) {
cur = p2;
if (/exam2\/start\//i.test(p2)) startUrl = p2;
if (/exam2\/doexam\//i.test(p2)) {
doexamUrl = p2;
break;
}
continue;
}
}
break;
}
if (!doexamUrl && startUrl) {
doexamUrl = doexamUrlFromStart(startUrl);
last = await gmGetOnceExam(startUrl, "start");
hops.push(`${last.note}:${last.status}@${(last.finalUrl || startUrl).slice(0, 100)}`);
if (last.loc && /exam2\/doexam\//i.test(last.loc)) {
try {
doexamUrl = new URL(last.loc, startUrl).href;
} catch (_) {}
} else if (/exam2\/doexam\//i.test(last.finalUrl || "")) {
doexamUrl = last.finalUrl;
} else if (/ExamObj\s*\(/.test(last.text || "")) {
return { url: last.finalUrl || startUrl, text: last.text, hops, startUrl };
}
}
if (!doexamUrl && startUrl) doexamUrl = doexamUrlFromStart(startUrl);
if (!doexamUrl) {
throw new Error(`\u5f00\u8003\u672a\u8df3\u5230 doexam · ${hops.join(" | ")}`);
}
last = await gmGetOnceExam(doexamUrl, "doexam");
hops.push(`${last.note}:${last.status}@${(last.finalUrl || doexamUrl).slice(0, 100)}`);
if (!/ExamObj\s*\(/.test(last.text || "")) {
last = await gmGetOnceExam(doexamUrl, "doexam2");
hops.push(`${last.note}:${last.status}@${(last.finalUrl || doexamUrl).slice(0, 100)}`);
}
return { url: last.finalUrl || doexamUrl, text: last.text || "", hops, startUrl };
}
function isResultPageUrl(url) {
return /\/view\/(?:do)?result\//i.test(String(url || ""));
}
function parseExamResultScore(html) {
const src = String(html || "");
const patterns = [
/\u5f97\s*\u5206\s*[\uff1a:]\s*([\d.]+)/i,
/\u60a8\u7684\u5f97\u5206\s*[\uff1a:\uff1a]?\s*([\d.]+)/i,
/\u5206\s*\u6570\s*[\uff1a:]\s*([\d.]+)/i,
/\u6210\s*\u7ee9\s*[\uff1a:]\s*([\d.]+)/i,
/\u603b\s*\u5206\s*[\uff1a:]\s*([\d.]+)/i,
/\u672c\u6b21\u5f97\u5206\s*[\uff1a:\uff1a]?\s*([\d.]+)/i,
/["']score["']\s*[:=]\s*["']?([\d.]+)/i,
/class=["'][^"']*score[^"']*["'][^>]*>\s*([\d.]+)/i,
/>([\d.]+)\s*\u5206\s*,
/\u6b63\u786e\u7387\s*[\uff1a:]\s*([\d.]+)\s*%/i,
];
for (const re of patterns) {
const m = src.match(re);
if (!m) continue;
const n = parseFloat(m[1]);
if (Number.isFinite(n)) return n;
}
if (/\u6ee1\u5206|\u5168\u90e8\u6b63\u786e|100\s*\u5206/.test(src) && /\u5f97\u5206|\u6210\u7ee9|\u5206\u6570/.test(src)) return 100;
return null;
}
function isExamScoreFull(score) {
if (score == null) return false;
const n = Number(score);
return Number.isFinite(n) && n >= 99.5;
}
async function ensureDoresultHtml(url, text) {
let cur = String(url || "");
let html = String(text || "");
const refetch = async (u) => {
const stay = await gmGetStay(u, 4);
return { url: stay.url || u, text: stay.text || "", hops: stay.hops || [] };
};
if (/\/view\/result\//i.test(cur) && !/\/view\/doresult\//i.test(cur)) {
return refetch(cur.replace("/view/result/", "/view/doresult/"));
}
if (/\/view\/doresult\//i.test(cur)) {
if (html.length > 300 && /restart|\u5f97\u5206|\u6210\u7ee9|\u5206\u6570/i.test(html)) {
return { url: cur, text: html, hops: [] };
}
return refetch(cur);
}
const m =
html.match(/https?:\/\/cjexamnew\.edu-edu\.com\/exam-admin\/student\/exam\/single\/view\/doresult\/[^"'\\\s]+/i) ||
html.match(/\/exam-admin\/student\/exam\/single\/view\/doresult\/[^"'\\\s]+/i);
if (m) {
try {
const dr = new URL(m[0].replace(/&/g, "&"), EM_CJ).href;
return refetch(dr);
} catch (_) {}
}
return { url: cur, text: html, hops: [] };
}
async function enterExamFromResultPage(exam, resultUrl, resultHtml) {
const dr = await ensureDoresultHtml(resultUrl, resultHtml);
let restartUrl = extractRestartUrl(dr.text, dr.url) || pickRestartFromHops(dr.hops);
if (!restartUrl) {
const again = await gmGetStay(dr.url, 4);
restartUrl = extractRestartUrl(again.text, again.url) || pickRestartFromHops(again.hops);
dr.text = again.text || dr.text;
dr.url = again.url || dr.url;
}
if (!restartUrl) throw new Error("\u6210\u7ee9\u9875\u65e0\u7ee7\u7eed\u8003\u8bd5\u94fe\u63a5\uff08\u9700\u5e26 m/d\uff09");
dbg(`\u7eed\u8003\u94fe\u63a5\uff1a${restartUrl.slice(0, 100)}`);
const entered = await followRestartToDoexam(restartUrl);
return { entered, restartUrl, resultHtml: dr.text, resultUrl: dr.url };
}
function sessFromEntered(exam, entered, restartUrl) {
const sess = {
examCode: exam.examCode,
termCourseID: exam.termCourseID,
title: exam.title,
signCJ: exam.signCJ,
ksType: exam.ksType || KS_TYPE_HOMEWORK,
clientId: "ckc001",
examBase: EXAM_HOST_DEFAULT,
doexamHtml: entered.text || "",
restartUrl: restartUrl || "",
userExamId: "",
eid: "",
pid: "",
psid: "",
bst: "",
studentId: "",
paperUrl: "",
};
for (const u of entered.hops || []) {
const um = String(u).match(/https?:\/\/[^\s|]+/);
if (um) absorbExamSession(um[0], "", sess);
}
if (entered.startUrl) absorbExamSession(entered.startUrl, "", sess);
if (restartUrl) absorbExamSession(restartUrl, "", sess);
absorbExamSession(entered.url, entered.text, sess);
applyPaperUrlFromHtml(sess, sess.doexamHtml);
finalizeExamPaperUrl(sess);
return sess;
}
async function probeHomeworkRecord(exam) {
try {
const j = await postGetExamUrlNew(exam, "2");
const url = String(j.msg || "").trim();
if (!/^https?:\/\//i.test(url)) return { hasRecord: false, score: null, full: false };
const stay = await gmGetStay(url, 10);
const dr = await ensureDoresultHtml(stay.url, stay.text);
const score = parseExamResultScore(dr.text);
const restartUrl = extractRestartUrl(dr.text, dr.url) || pickRestartFromHops(dr.hops || stay.hops);
const full = isExamScoreFull(score);
return {
hasRecord: true,
score,
full,
restartUrl,
resultUrl: dr.url,
resultHtml: dr.text,
};
} catch (e) {
dbg(`\u67e5\u8003\u8bd5\u8bb0\u5f55\uff1a${e.message || e}`);
return { hasRecord: false, score: null, full: false };
}
}
async function openExamSessionAfterUrl(exam, startUrl) {
dbg(`\u5f00\u8003\u8df3\u8f6c\uff1a${String(startUrl || "").slice(0, 100)}`);
let entered = null;
let restartUrl = "";
let landedResult = null;
try {
entered = await followAdminStartToDoexam(startUrl);
} catch (e) {
dbg(`\u9996\u6b21\u8fdb\u573a\u672a\u5230 doexam\uff1a${e.message || e}`);
const stay = await gmGetStay(startUrl, 14);
landedResult = { url: stay.url, text: stay.text, hops: stay.hops || [] };
}
const finalUrl = (entered && entered.url) || (landedResult && landedResult.url) || "";
const finalHtml = (entered && entered.text) || (landedResult && landedResult.text) || "";
if (!entered || !/ExamObj\s*\(/.test(finalHtml) || isResultPageUrl(finalUrl)) {
if (isResultPageUrl(finalUrl) || /doresult|view\/result/i.test(finalHtml)) {
const fromResult = await enterExamFromResultPage(exam, finalUrl, finalHtml);
entered = fromResult.entered;
restartUrl = fromResult.restartUrl;
} else {
const picked = pickCjeExamHop(finalHtml, finalUrl);
if (picked) {
try {
entered = await followAdminStartToDoexam(picked);
} catch (_) {}
}
}
}
if (!entered) throw new Error("\u672a\u80fd\u8fdb\u5165\u8003\u573a\uff08\u65e0\u8df3\u8f6c\u7ed3\u679c\uff09");
const sess = sessFromEntered(exam, entered, restartUrl);
if (!sess.userExamId) {
try {
const rec = await probeHomeworkRecord(exam);
if (rec.restartUrl) {
dbg(`\u6539\u7528\u8003\u8bd5\u8bb0\u5f55\u7eed\u8003\u8fdb\u573a`);
const fromResult = await enterExamFromResultPage(exam, rec.resultUrl, rec.resultHtml);
const sess2 = sessFromEntered(exam, fromResult.entered, fromResult.restartUrl);
if (sess2.userExamId) {
dbg(`\u5f00\u8003\u5c31\u7eea uid=…${String(sess2.userExamId).slice(-18)} · ${examBase(sess2)}`);
return sess2;
}
}
} catch (e) {
dbg(`\u7eed\u8003\u8fdb\u573a\u5931\u8d25\uff1a${e.message || e}`);
}
dbg(`\u5f00\u8003\u89e3\u6790\u5931\u8d25 hops=${(entered.hops || []).join(" → ")}`);
dbg(`final=${String(entered.url || "").slice(0, 140)}`);
throw new Error("\u672a\u80fd\u8fdb\u5165\u8003\u573a");
}
if (!/ExamObj\s*\(/.test(sess.doexamHtml || "") && sess.studentId && sess.psid && sess.bst) {
const dUrl = `${examBase(sess)}/exam/student/exam2/doexam/${encodeURIComponent(sess.studentId)}/${encodeURIComponent(
sess.psid
)}/${encodeURIComponent(sess.bst)}`;
try {
const again = await gmGetOnceExam(dUrl, "doexam-retry");
if (/ExamObj\s*\(/.test(again.text || "")) {
sess.doexamHtml = again.text;
absorbExamSession(again.finalUrl, again.text, sess);
applyPaperUrlFromHtml(sess, sess.doexamHtml);
finalizeExamPaperUrl(sess);
}
} catch (_) {}
}
dbg(`\u5f00\u8003\u5c31\u7eea uid=…${String(sess.userExamId).slice(-18)} · ${examBase(sess)}`);
return sess;
}
async function openExamSession(exam, startOrRecord) {
const j = await postGetExamUrlNew(exam, startOrRecord);
const url = String(j.msg || "").trim();
if (!/^https?:\/\//i.test(url)) throw new Error(`\u5f00\u8003\u94fe\u63a5\u5f02\u5e38\uff1a${url.slice(0, 80)}`);
return openExamSessionAfterUrl(exam, url);
}
async function fetchSignedRestartUrl(sess, exam) {
let restartUrl = extractRestartUrl(sess.restartUrl, EM_CJ) || "";
const tryAbsorb = (label, html, pageUrl, hops) => {
const got = extractRestartUrl(html, pageUrl || EM_CJ) || pickRestartFromHops(hops);
if (got) {
restartUrl = got;
dbg(`${label} \u53d6\u5230\u7ee7\u7eed\u8003\u8bd5\u94fe\u63a5`);
return true;
}
return false;
};
try {
const j2 = await postGetExamUrlNew(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) {
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) {
const dr = await gmGetStay(doresultUrl, 4);
const fresh = extractRestartUrl(dr.text, dr.url) || pickRestartFromHops(dr.hops);
if (fresh) restartUrl = fresh;
}
}
} catch (e) {
dbg(`\u53d6\u7eed\u8003\u94fe\u63a5\uff1a${e.message || e}`);
}
if (!restartUrl) {
const j1 = await postGetExamUrlNew(exam, "1");
const stay = await gmGetStay(j1.msg, 10);
tryAbsorb("\u5f00\u8003\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("doresult", dr.text, dr.url, dr.hops);
}
if (!restartUrl) restartUrl = pickRestartFromHops(stay.hops);
}
if (!restartUrl) throw new Error("\u6210\u7ee9\u9875\u65e0\u5e26 m/d \u7b7e\u540d\u7684\u7ee7\u7eed\u8003\u8bd5\u94fe\u63a5");
return restartUrl;
}
async function restartExamSession(sess, exam) {
const restartUrl = await fetchSignedRestartUrl(sess, exam);
const entered = await followRestartToDoexam(restartUrl);
const out = Object.assign({}, sess, {
doexamHtml: entered.text || "",
restartUrl,
});
if (entered.startUrl) absorbExamSession(entered.startUrl, "", out);
absorbExamSession(entered.url, entered.text, out);
applyPaperUrlFromHtml(out, out.doexamHtml);
finalizeExamPaperUrl(out);
if (!/ExamObj\s*\(/.test(out.doexamHtml || "")) throw new Error("\u7eed\u8003\u672a\u8fdb\u5165\u8003\u573a");
return out;
}
function decodeBasicEntities(s) {
return String(s || "")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/ /g, " ");
}
function stripHtmlToText(html) {
let s = decodeBasicEntities(html);
s = s.replace(/ /gi, "\n").replace(/<\/p>/gi, "\n").replace(/<\/div>/gi, "\n");
s = s.replace(/ ]*alt=["']([^"']*)["'][^>]*>/gi, " $1 ");
s = s.replace(/<[^>]+>/g, " ");
s = s.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n");
return s.replace(/[ \t]{2,}/g, " ").trim();
}
function isChoiceLikeAnswer(ans, type) {
const t = Number(type);
if (t === 1 || t === 2 || t === 3) return true;
const s = String(ans || "").trim();
return /^[a-dA-D]([,\uff0c\u3001\s]*[a-dA-D])*$/.test(s);
}
function normalizeExamAnswer(raw, type) {
let s = decodeBasicEntities(String(raw == null ? "" : raw)).trim();
if (!s) return { text: "", imgOnly: false, raw: "" };
if (isChoiceLikeAnswer(s, type) && !/ = 2 && !/^https?:\/\//i.test(text)) {
return { text, imgOnly: false, raw: s };
}
if (hasImg) {
const urls = [];
const re = / ]+src=["']([^"']+)["']/gi;
let m;
while ((m = re.exec(s))) {
try {
urls.push(new URL(m[1], EM_CJ).href);
} catch (_) {
urls.push(m[1]);
}
}
return { text: s, imgOnly: true, raw: s, imgUrls: urls };
}
return { text: s, imgOnly: false, raw: s };
}
function extractRightAnswerFromDoview(html, questionId) {
const src = String(html || "");
const qid = String(questionId || "");
if (!src || !qid) return "";
const idx = src.search(new RegExp(`id=["']q_${qid}["']`, "i"));
const chunk = idx >= 0 ? src.slice(idx, idx + 12000) : "";
const blocks = [];
if (chunk) {
const m1 = chunk.match(/\u53c2\u8003\u7b54\u6848\s*[\uff1a:]\s*([\s\S]{0,5000}?)(?=\u8003\u751f\u56de\u7b54|\u53c2\u8003\u7b54\u6848|id=["']q_|$)/i);
if (m1) blocks.push(m1[1]);
const m2 = chunk.match(
/class=["'][^"']*(?:right_?answer|exam_rightAnswer|standard_answer|has_standard_answer)[^"']*["'][^>]*>([\s\S]{0,5000}?)<\/div>/i
);
if (m2) blocks.push(m2[1]);
}
const m3 = src.match(
new RegExp(
`data-question-id=["']${qid}["'][\\s\\S]{0,6000}?\u53c2\u8003\u7b54\u6848\\s*[\uff1a:]([\\s\\S]{0,4000}?)(?=\u8003\u751f\u56de\u7b54|\u53c2\u8003\u7b54\u6848|$)`,
"i"
)
);
if (m3) blocks.push(m3[1]);
for (const b of blocks) {
const n = normalizeExamAnswer(b, 0);
if (n.text) return n.text;
}
return "";
}
async function enrichImageAnswersFromDoview(sess, keys, doviewUrl) {
const need = (keys || []).filter((a) => {
const n = normalizeExamAnswer(a && a.answer, a && a.type);
return n.imgOnly || (!n.text && a && a.answer);
});
if (!need.length) return keys;
const base = examBase(sess);
const url = doviewUrl || `${base}/exam/student/exam2/doview/${encodeURIComponent(sess.userExamId)}/1`;
let html = "";
try {
const res = await gmRequest({
method: "GET",
url,
timeout: 60000,
headers: { Accept: "text/html,application/xhtml+xml", Referer: `${base}/` },
});
html = res.responseText || "";
} catch (e) {
dbg(`\u62c9\u53d6\u7b54\u5377\u9875\u8865\u6807\u7b54\u5931\u8d25\uff1a${e.message || e}`);
return keys;
}
let filled = 0;
for (const a of keys) {
const n0 = normalizeExamAnswer(a.answer, a.type);
if (!n0.imgOnly && n0.text) continue;
const got = extractRightAnswerFromDoview(html, a.questionId);
if (got && got !== n0.text) {
a.answer = got;
filled += 1;
}
}
if (filled) dbg(`\u7b54\u5377\u9875\u8865\u5168\u4e3b\u89c2/\u56fe\u7247\u6807\u7b54 ${filled} \u9053`);
return keys;
}
async function saveExamAnswer(sess, q, answer) {
const stime = String(Date.now());
const norm = typeof answer === "object" && answer && answer.text != null ? answer : normalizeExamAnswer(answer, q && q.type);
const ans = String(norm.text || "");
if (!ans) throw new Error(`\u7a7a\u7b54\u6848 q=${q.questionId}`);
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_card4`;
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_card4",
isCensorwords: "false",
};
viewUrl = `${base}/exam/student/exam2/view/${encodeURIComponent(uid)}/1?&view_paper_params_key=${encodeURIComponent(
JSON.stringify(params)
)}&clientTheme=client_card4`;
}
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 || "";
for (const html of [finHtml, dvHtml]) {
const signed = extractRestartUrl(html, EM_CJ);
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,
},
});
const answers = (j && Array.isArray(j.answers) && j.answers) || [];
if (answers.length) return answers;
dbg(`\u6807\u7b54\u4e3a\u7a7a try ${attempt}/5 · HTTP ${status} ${(text || "").slice(0, 80)}`);
await sleep(500 * attempt);
if (attempt === 2 || attempt === 4) {
try {
await openExamResultViews(sess);
} catch (_) {}
}
}
return [];
}
async function loadPaperQuestions(sess) {
let questions = [];
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++) {
if (!sess.paperUrl) break;
const paper = await gmRequest({
method: "GET",
url: sess.paperUrl,
timeout: 60000,
headers: { Accept: "text/html,application/xhtml+xml,*/*", Referer: referer },
});
questions = parsePaperQuestions(paper.responseText || "");
if (!questions.length) await sleep(350);
}
if (!questions.length && sess.doexamHtml) {
applyPaperUrlFromHtml(sess, sess.doexamHtml);
finalizeExamPaperUrl(sess);
questions = parsePaperQuestions(sess.doexamHtml);
}
return questions;
}
async function runOneHomeworkPaper(exam) {
setStatus(`\u4f5c\u7b54\uff1a${exam.title}`);
const rec = await probeHomeworkRecord(exam);
if (rec.hasRecord) {
if (rec.full) {
log(`\u300c${exam.title}\u300d\u5df2\u6ee1\u5206\uff0c\u8df3\u8fc7`);
return true;
}
if (rec.score != null) dbg(`\u300c${exam.title}\u300d\u5f53\u524d\u5f97\u5206 ${rec.score}`);
}
let sess = null;
if (rec.hasRecord && rec.restartUrl) {
try {
const fromResult = await enterExamFromResultPage(exam, rec.resultUrl, rec.resultHtml);
sess = sessFromEntered(exam, fromResult.entered, fromResult.restartUrl);
if (!sess.userExamId) throw new Error("\u672a\u80fd\u8fdb\u5165");
} catch (e) {
dbg(`\u8bb0\u5f55\u7eed\u8003\u5931\u8d25\uff0c\u6539\u5f00\u8003\uff1a${e.message || e}`);
sess = await openExamSession(exam, "1");
}
} else {
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\u6682\u4e0d\u53ef\u7528");
dbg(`\u300c${exam.title}\u300d\u5171 ${questions.length} \u9898`);
let keys = [];
let doviewUrl = "";
try {
const views0 = await openExamResultViews(sess);
doviewUrl = views0.doviewUrl || "";
keys = await fetchExamAnswerKey(sess, 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) {
setStatus(`\u6838\u5bf9\u7b54\u6848\uff1a${exam.title}`);
sess = await restartExamSession(sess, exam);
await saveExamAnswer(sess, questions[0], "a");
await submitExamPaper(sess);
await sleep(500);
const views = await openExamResultViews(sess);
doviewUrl = views.doviewUrl || doviewUrl;
keys = await fetchExamAnswerKey(sess, doviewUrl);
}
if (!keys.length) throw new Error("\u672a\u83b7\u53d6\u5230\u7b54\u6848");
keys = await enrichImageAnswersFromDoview(sess, keys, doviewUrl);
const keyMap = new Map();
let imgN = 0;
let skipN = 0;
keys.forEach((a) => {
if (!a || a.questionId == null) return;
const n = normalizeExamAnswer(a.answer, a.type);
if (!n.text) {
skipN += 1;
return;
}
if (n.imgOnly) imgN += 1;
keyMap.set(String(a.questionId), n);
});
if (imgN) dbg(`${exam.title} \u56fe\u7247\u6807\u7b54 ${imgN} \u9053`);
if (skipN) dbg(`${exam.title} \u7a7a\u6807\u7b54\u8df3\u8fc7 ${skipN} \u9053`);
setStatus(`\u63d0\u4ea4\uff1a${exam.title}`);
sess = await restartExamSession(sess, exam);
let okN = 0;
let failN = 0;
for (const q of questions) {
if (!state.running || state.stopFlag) break;
const ans = keyMap.get(String(q.questionId));
if (!ans) continue;
try {
await saveExamAnswer(sess, q, ans);
okN += 1;
} catch (e) {
failN += 1;
const msg = String(e.message || e);
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;
} catch (e2) {
dbg(`\u6539\u7b54\u6062\u590d\u5931\u8d25\uff1a${e2.message || e2}`);
break;
}
} else if (failN <= 3) dbg(`\u6539\u7b54\u5931\u8d25 ${q.questionId}\uff1a${msg.slice(0, 80)}`);
}
await sleep(60);
}
if (okN < 1) throw new Error("\u672a\u80fd\u63d0\u4ea4\u7b54\u6848");
await submitExamPaper(sess);
log(`\u300c${exam.title}\u300d\u5df2\u5b8c\u6210`);
return okN;
}
async function runPapersForCourse(course, ksType) {
const ks = String(ksType || KS_TYPE_HOMEWORK);
const isFinal = ks === KS_TYPE_FINAL;
const label = isFinal ? "\u671f\u672b\u8003\u8bd5" : "\u5e73\u65f6\u4f5c\u4e1a";
if (!isFinal && !course.hasHomework && !course.zyPortalCode && course.homeworkScore == null) {
log(`\u300c${course.title}\u300d\u6682\u65e0\u4f5c\u4e1a\uff0c\u8df3\u8fc7`);
return true;
}
if (isFinal && !course.hasExam && course.examScore == null) {
log(`\u300c${course.title}\u300d\u6682\u65e0\u671f\u672b\u8003\u8bd5\uff0c\u8df3\u8fc7`);
return true;
}
setStatus(`${label}\uff1a${course.title}`);
log(`\u300c${course.title}\u300d\u5f00\u59cb${label}`);
const papers = await fetchExIndexPapers(course, ks);
if (!papers.length) {
log(`\u300c${course.title}\u300d\u672a\u62c9\u5230${label}\u8bd5\u5377\u5217\u8868\uff0c\u8bf7\u5237\u65b0\u95e8\u6237\u540e\u91cd\u8bd5`, "err");
return false;
}
log(`\u300c${course.title}\u300d${label} ${papers.length} \u4efd`);
let did = 0;
for (const paper of papers) {
if (!state.running || state.stopFlag) break;
try {
await runOneHomeworkPaper(paper);
did += 1;
renderCourses();
} catch (e) {
dbg(`${label}\u5931\u8d25\u300c${paper.title}\u300d\uff1a${e.message || e}`);
log(`\u300c${paper.title}\u300d\u5931\u8d25\uff1a${e.message || e}`, "err");
return false;
}
await sleep(800);
}
if (did > 0) {
log(`\u300c${course.title}\u300d${label}\u5df2\u4ea4\u5377\u3002\u6210\u7ee9\u8be6\u60c5\u53ef\u80fd\u7a0d\u665a\u624d\u66f4\u65b0\uff0c\u8bf7\u8fc7\u51e0\u5206\u949f\u518d\u5237\u65b0\u67e5\u770b\u6ee1\u5206\u72b6\u6001`);
}
try {
await recheckCourseOnList(course);
renderCourses();
updatePanel();
const fullNow = isFinal ? course.examFull : course.homeworkFull;
if (did > 0 && !fullNow) {
log(`\u300c${course.title}\u300d${label}\u6210\u7ee9\u5c1a\u672a\u663e\u793a\u6ee1\u5206\uff08\u5e73\u53f0\u540c\u6b65\u5ef6\u8fdf\u5c5e\u6b63\u5e38\uff09\uff0c\u7a0d\u540e\u91cd\u65b0\u52a0\u8f7d\u8bfe\u7a0b\u5217\u8868\u5373\u53ef`);
}
} catch (_) {}
return did > 0;
}
async function runHomeworkForCourse(course) {
return runPapersForCourse(course, KS_TYPE_HOMEWORK);
}
async function runFinalExamForCourse(course) {
return runPapersForCourse(course, KS_TYPE_FINAL);
}
async function openAndStudyCourse(course, opts) {
const skipFollowup = !!(opts && opts.skipFollowup);
setStatus(`\u5b66\u4e60\uff1a${course.title}`);
log(`\u5f00\u59cb\u5b66\u4e60\u300c${course.title}\u300d`);
state._activeCourse = course;
await fetchCourseStudyQuota(course);
if (course.needMin > 0 && courseQuotaMet(course, course.learnedMin)) {
log(`\u300c${course.title}\u300d\u65f6\u957f\u5df2\u6ee1\uff0c\u8df3\u8fc7\u8bfe\u4ef6`);
await recheckCourseOnList(course);
return true;
}
const learnUrl = await getCourseLearnUrl(course);
const session = sessionFromLearnUrl(learnUrl, course);
if (!session.clientCode || !session.userId || !session.coursewareCode) {
throw new Error("\u5b66\u4e60\u94fe\u63a5\u7f3a\u5c11 clientCode/userId/coursewareCode");
}
state.session = session;
state.courseTitle = course.title;
try {
await ensureCloudLease(true);
} catch (_) {}
await warmLearnSession(session);
const chapters = await fetchCatalogs(session, course);
state.chapters = chapters;
renderChapters();
log(`\u300c${course.title}\u300d\u5171 ${chapters.length} \u4e2a\u5c0f\u8282`);
for (const ch of chapters) {
if (!state.running || state.stopFlag) break;
if (course.needMin > 0 && courseQuotaMet(course, sumChaptersSofarMin(state.chapters))) {
log(`\u300c${course.title}\u300d\u65f6\u957f\u5df2\u6ee1`);
break;
}
const ok = await runChapter(session, ch);
if (!ok) return false;
const localMin = sumChaptersSofarMin(state.chapters);
course.learnedMin = Math.max(Number(course.learnedMin) || 0, localMin);
if (course.needMin > 0 && localMin >= course.needMin) {
await recheckCourseOnList(course);
log(`\u300c${course.title}\u300d\u65f6\u957f\u5df2\u6ee1`);
break;
}
await sleep(getGapMs());
}
await recheckCourseOnList(course);
return true;
}
function findCourseByAction(action, selected) {
const id = String((action && action.termCourseID) || "");
return (
selected.find((c) => String(c.termCourseID) === id) ||
state.allCourses.find((c) => String(c.termCourseID) === id) ||
null
);
}
async function startPipeline(mode) {
if (state.running) return;
const selected = state.courses.filter((c) => c.checked);
if (!selected.length) {
setStatus("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b");
return;
}
const planMode = mode === "homework" ? "homework" : mode === "exam" ? "exam" : "all";
if ((planMode === "homework" || planMode === "exam") && !ensurePortalHostForExam()) {
return;
}
state.running = true;
state.stopFlag = false;
updatePanel();
log(
planMode === "homework"
? `\u5f00\u59cb\u4f5c\u4e1a\uff0c\u5171 ${selected.length} \u95e8`
: planMode === "exam"
? `\u5f00\u59cb\u8003\u8bd5\uff0c\u5171 ${selected.length} \u95e8`
: `\u5f00\u59cb\u5b66\u4e60\uff0c\u5171 ${selected.length} \u95e8`
);
try {
await ensureCloudLease(true);
for (const c of selected) {
if (!state.running || state.stopFlag) break;
try {
await fetchCourseStudyQuota(c);
} catch (_) {}
}
renderCourses();
let actions = [];
try {
actions = await cloudStudyPlan(selected, planMode);
log(`\u4e91\u7aef\u5df2\u7f16\u6392 ${actions.filter((a) => /^(video|homework|exam)$/.test(a.type)).length} \u9879\u4efb\u52a1`);
} catch (e) {
log(`\u4e91\u7aef\u7f16\u6392\u5931\u8d25\uff1a${e.message || e}`, "err");
actions = [];
}
let needProLogged = false;
for (const action of actions) {
if (!state.running || state.stopFlag) break;
const course = findCourseByAction(action, selected);
if (!course && /^(video|homework|exam)$/.test(action.type)) continue;
if (action.type === "video") {
await openAndStudyCourse(course, { skipFollowup: true });
} else if (action.type === "homework") {
await runHomeworkForCourse(course);
} else if (action.type === "exam") {
await runFinalExamForCourse(course);
} else if (action.type === "need_pro") {
if (!needProLogged) {
log(action.message || "\u4f5c\u4e1a/\u8003\u8bd5\u9700\u5f00\u901a\u4f1a\u5458");
needProLogged = true;
}
} else if (/_skip$/.test(action.type) && action.reason) {
log(`\u300c${action.title || (course && course.title) || ""}\u300d${action.reason}`);
}
}
setStatus(state.stopFlag ? "\u5df2\u505c\u6b62" : "\u5168\u90e8\u5b8c\u6210");
log(state.stopFlag ? "\u5df2\u505c\u6b62" : "\u5168\u90e8\u5b8c\u6210");
} catch (e) {
const msg = isQuotaError(e) ? friendlyCloudMessage(e) : e.message || e;
setStatus("\u51fa\u9519\uff1a" + msg);
dbg(`\u6d41\u6c34\u7ebf\u5f02\u5e38\uff1a${e.message || e}`);
log(`\u8fd0\u884c\u51fa\u9519\uff1a${msg}`, "err");
} finally {
state.running = false;
state._activeCourse = null;
updatePanel();
updateCloudPanelUI();
}
}
function stopPipeline() {
state.stopFlag = true;
state.running = false;
setStatus("\u6b63\u5728\u505c\u6b62…");
log("\u6b63\u5728\u505c\u6b62");
updatePanel();
}
const PANEL_NOTICE =
"\u534e\u5bcc\u6559\u80b2\u6210\u6559\u52a9\u624b\uff1a\u514d\u8d39\u53ef\u5b66 3 \u4e2a\u89c6\u9891\u5c0f\u8282\uff1b\u4f5c\u4e1a\u4e0e\u671f\u672b\u9700\u5f00\u901a\u4f1a\u5458\u3002\u4f1a\u5458\u5f00\u901a\u540e 30 \u5929\u5185\u4e0d\u9650\u3002\u8bf7\u4f7f\u7528 Chrome\u3002";
function courseBadgesHtml(c) {
const badges = [];
if (courseQuotaMet(c, c.learnedMin) || c.lessonFull)
badges.push(`\u8bfe\u65f6\u5df2\u6ee1`);
else if (c.lessonScore != null)
badges.push(`\u8bfe\u65f6:${esc(formatScorePart(c.lessonScore))}`);
else badges.push(`\u8bfe\u65f6:—`);
if (!c.hasHomework) badges.push(`\u65e0\u4f5c\u4e1a`);
else if (c.homeworkFull) badges.push(`\u4f5c\u4e1a\u6ee1\u5206`);
else if (c.homeworkScore != null && Number(c.homeworkScore) > 0)
badges.push(`\u4f5c\u4e1a\u672a\u6ee1\u5206`);
else badges.push(`\u4f5c\u4e1a\u5f85\u505a`);
if (!c.hasExam) badges.push(`\u65e0\u8003\u8bd5`);
else if (c.examFull) badges.push(`\u8003\u8bd5\u6ee1\u5206`);
else if (c.examScore != null && Number(c.examScore) > 0)
badges.push(`\u8003\u8bd5\u672a\u6ee1\u5206`);
else badges.push(`\u8003\u8bd5\u5f85\u505a`);
return badges.join("");
}
function ensureStyle() {
let st = document.getElementById(STYLE_ID);
if (!st) {
st = document.createElement("style");
st.id = STYLE_ID;
document.documentElement.appendChild(st);
}
st.textContent = `
#${PANEL_ID}{position:fixed;right:20px;top:80px;z-index:2147483646;width:420px;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:18px;box-shadow:0 18px 44px rgba(15,23,42,.18);font:12px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:#0f172a;overflow:hidden}
#${PANEL_ID}.collapsed .cj-body,#${PANEL_ID}.collapsed .cj-footer-extra,#${PANEL_ID}.collapsed .cj-footer{display:none}
#${PANEL_ID} .cj-hd{padding:10px 12px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none}
#${PANEL_ID} .cj-brand{display:flex;align-items:center;gap:10px;min-width:0}
#${PANEL_ID} .cj-logo{width:34px;height:34px;border-radius:10px;object-fit:cover;box-shadow:0 2px 10px rgba(15,23,42,.12);border:1px solid rgba(148,163,184,.45);background:#fff}
#${PANEL_ID} .cj-title{font-size:15px;font-weight:900;color:#b45309;line-height:1.2;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
#${PANEL_ID} .cj-ver{font-size:11px;font-weight:900;color:#64748b;padding:2px 8px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0}
#${PANEL_ID} .cj-sub{font-size:12px;color:#475569;margin-top:2px}
#${PANEL_ID} .cj-toggle{border:none;background:#fff;color:#64748b;width:30px;height:30px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1)}
#${PANEL_ID} .cj-body{padding:9px}
#${PANEL_ID} .cj-card{background:#fff;border:1px solid #d9e2ee;border-radius:14px;padding:9px 10px;margin-bottom:9px}
#${PANEL_ID} .cj-progress-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;font-size:12px;color:#334155}
#${PANEL_ID} .cj-progress-num{font-size:26px;font-weight:900;color:#0f172a}
#${PANEL_ID} .cj-progress-num small{font-size:14px;color:#64748b}
#${PANEL_ID} .cj-progress-pct{font-size:22px;font-weight:900;color:#0369a1}
#${PANEL_ID} .cj-progress-bar{height:10px;border-radius:999px;background:#e2e8f0;overflow:hidden}
#${PANEL_ID} .cj-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease}
#${PANEL_ID} #csustcj-status{padding:5px 10px;border-radius:999px;border:1px solid rgba(148,163,184,.45);font-weight:900;font-size:12px;background:#fff}
#${PANEL_ID} #csustcj-status.cj-status-running{background:rgba(16,185,129,.12);border-color:rgba(16,185,129,.45);color:#059669}
#${PANEL_ID} #csustcj-status.cj-status-starting{background:rgba(14,165,233,.12);border-color:rgba(14,165,233,.45);color:#0369a1}
#${PANEL_ID} #csustcj-status.cj-status-stopped{background:rgba(100,116,139,.10);border-color:rgba(148,163,184,.45);color:#475569}
#${PANEL_ID} #csustcj-status.cj-status-error{background:rgba(239,68,68,.10);border-color:rgba(239,68,68,.45);color:#dc2626}
#${PANEL_ID} .cj-tabbar{display:flex;gap:8px;margin-bottom:10px}
#${PANEL_ID} .cj-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:6px 8px;border-radius:10px;cursor:pointer;font-weight:700;font-size:12px}
#${PANEL_ID} .cj-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;box-shadow:0 4px 12px rgba(30,64,175,.25)}
#${PANEL_ID} .cj-pane{display:none}
#${PANEL_ID} .cj-pane.active{display:block}
#${PANEL_ID} .cj-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
#${PANEL_ID} .cj-list-title{font-size:12px;color:#64748b;font-weight:700}
#${PANEL_ID} .cj-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 8px}
#${PANEL_ID} .cj-terms{margin:0 0 8px}
#${PANEL_ID} .cj-terms select{width:100%;box-sizing:border-box;border:1px solid #cbd5e1;background:#f8fafc;color:#0f172a;border-radius:10px;padding:7px 10px;font-size:12px;font-weight:800;cursor:pointer;outline:none}
#${PANEL_ID} #csustcj-courses,#csustcj-chapters,#csustcj-log{max-height:170px;overflow:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:12px;padding:6px}
#${PANEL_ID} #csustcj-log{max-height:150px}
#${PANEL_ID} .cj-course-card{display:flex;align-items:center;justify-content:space-between;gap:10px;background:linear-gradient(180deg,#fff,#f8fbff);border:1px solid #dbe4f0;border-radius:12px;padding:10px;margin-bottom:8px;cursor:pointer}
#${PANEL_ID} .cj-course-card:hover{border-color:#93c5fd;box-shadow:0 10px 22px rgba(30,64,175,.10)}
#${PANEL_ID} .cj-course-card.selected{border-color:#2563eb;box-shadow:0 14px 34px rgba(37,99,235,.16)}
#${PANEL_ID} .cj-course-left{display:flex;gap:10px;align-items:center;min-width:0;flex:1}
#${PANEL_ID} .cj-course-left input{accent-color:#2563eb;flex:none}
#${PANEL_ID} .cj-course-title{font-size:13px;font-weight:900;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#${PANEL_ID} .cj-course-sub{font-size:11px;color:#64748b;margin-top:2px;line-height:1.4}
#${PANEL_ID} .cj-course-badges{display:flex;gap:6px;align-items:center;flex:0 0 auto;flex-wrap:wrap;justify-content:flex-end;max-width:46%}
#${PANEL_ID} .cj-badge{font-size:10px;font-weight:900;border-radius:999px;padding:2px 6px;border:1px solid rgba(148,163,184,.35);color:#334155;background:#fff;white-space:nowrap}
#${PANEL_ID} .cj-badge.done{color:#059669;background:rgba(16,185,129,.10);border-color:rgba(16,185,129,.45)}
#${PANEL_ID} .cj-badge.todo{color:#0369a1;background:rgba(14,165,233,.10);border-color:rgba(14,165,233,.45)}
#${PANEL_ID} .cj-badge.warn{color:#b45309;background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.45)}
#${PANEL_ID} .cj-item{display:flex;gap:8px;align-items:flex-start;background:linear-gradient(180deg,#fff,#f8fbff);border:1px solid #dbe4f0;border-radius:11px;padding:8px 9px;margin-bottom:6px}
#${PANEL_ID} .cj-item.done{border-color:rgba(16,185,129,.45)}
#${PANEL_ID} .cj-item.done b{color:#059669}
#${PANEL_ID} .cj-meta-row{display:flex;justify-content:space-between;gap:8px;font-size:12px;margin-bottom:5px}
#${PANEL_ID} .cj-meta-label{color:#64748b}
#${PANEL_ID} .cj-meta-value{font-weight:700;color:#0f172a;text-align:right;max-width:70%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#${PANEL_ID} .cj-btn-row{display:flex;gap:8px;flex-wrap:wrap}
#${PANEL_ID} .cj-btn{flex:1;min-width:0;border:none;color:#fff;padding:10px 10px;border-radius:12px;cursor:pointer;font-weight:800;font-size:13px}
#${PANEL_ID} .cj-btn:disabled{opacity:.55;cursor:not-allowed}
#${PANEL_ID} .cj-btn-go{background:#16a34a}
#${PANEL_ID} .cj-btn-hw{background:#0ea5e9}
#${PANEL_ID} .cj-btn-exam{background:#0f766e}
#${PANEL_ID} .cj-btn-stop{background:#ef4444}
#${PANEL_ID} .cj-btn-buy{background:#2563eb}
#${PANEL_ID} .cj-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a}
#${PANEL_ID} .cj-token-input{width:100%;box-sizing:border-box;border:1px solid #cbd5e1;border-radius:10px;padding:8px 10px;font-size:12px;margin:6px 0 8px;background:#fff}
#${PANEL_ID} .cj-settings-group{background:#f8fafc;border:1px solid #dbe4f0;border-radius:14px;padding:10px;margin-bottom:10px}
#${PANEL_ID} .cj-settings-title{font-size:12px;color:#64748b;font-weight:900;margin-bottom:8px}
#${PANEL_ID} .cj-empty{padding:14px 8px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900}
#${PANEL_ID} .cj-log-line{padding:4px 6px;border-bottom:1px dashed #d4deea;color:#334155;font-size:12px;line-height:1.45;word-break:break-word}
#${PANEL_ID} .cj-footer-extra{padding:7px 10px;background:#f8fafc;border-top:1px solid #e2e8f0}
#${PANEL_ID} .cj-ann{position:relative;font-size:12px;color:#334155;line-height:1.5;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:12px;padding:10px 10px 10px 12px}
#${PANEL_ID} .cj-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-top-left-radius:12px;border-bottom-left-radius:12px}
#${PANEL_ID} .cj-footer{padding:8px 12px;background:#eef2f7;border-top:1px solid #dbe4f0;color:#334155;font-size:12px}
`;
}
function onTermFilterChange(key) {
const tabs = state.termTabs && state.termTabs.length ? state.termTabs : [];
const next = key || "current";
if (String(state.termFilter) === String(next)) return;
state.termFilter = next;
const list = applyTermFilter();
renderTermTabs();
renderCourses();
const label = (tabs.find((t) => String(t.key) === String(next)) || {}).label || "\u6240\u9009\u5b66\u671f";
setStatus(`${label} · ${list.length} \u95e8\u8bfe`);
log(`\u5df2\u5207\u6362\u5230${label}\uff08${list.length} \u95e8\uff09`);
(async () => {
for (const c of list) {
if (c.lessonScore != null || c.homeworkScore != null || c.examScore != null) continue;
try {
await fetchCourseStudyQuota(c);
renderCourses();
} catch (_) {}
await sleep(180);
}
})();
}
function renderTermTabs() {
const box = document.getElementById("csustcj-terms");
if (!box) return;
const tabs = state.termTabs && state.termTabs.length ? state.termTabs : [{ key: "current", label: "\u5f53\u524d\u5b66\u671f" }];
const cur = String(state.termFilter || "current");
box.innerHTML = ``;
const sel = document.getElementById("csustcj-term-select");
if (sel) sel.onchange = () => onTermFilterChange(sel.value);
}
function renderCourses() {
const box = document.getElementById("csustcj-courses");
if (!box) return;
if (!state.courses.length) {
box.innerHTML = `\u8be5\u5b66\u671f\u6682\u65e0\u8bfe\u7a0b\uff0c\u53ef\u5207\u6362\u5176\u5b83\u5b66\u671f `;
return;
}
box.innerHTML = state.courses
.map((c, i) => {
const selected = c.checked ? "selected" : "";
return ``;
})
.join("");
box.querySelectorAll("input[type=checkbox]").forEach((el) => {
el.addEventListener("click", (e) => e.stopPropagation());
el.addEventListener("change", () => {
const i = Number(el.getAttribute("data-i"));
if (state.courses[i]) state.courses[i].checked = !!el.checked;
renderCourses();
updatePanel();
});
});
box.querySelectorAll(".cj-course-card").forEach((card) => {
card.addEventListener("click", (e) => {
if (e.target.closest("input")) return;
const i = Number(card.getAttribute("data-i"));
if (!state.courses[i]) return;
state.courses[i].checked = !state.courses[i].checked;
renderCourses();
updatePanel();
});
});
}
function renderChapters() {
const box = document.getElementById("csustcj-chapters");
if (!box) return;
if (!state.chapters.length) {
box.innerHTML = `\u5c1a\u672a\u5f00\u59cb\u5b66\u4e60 `;
return;
}
box.innerHTML = state.chapters
.map((ch) => {
const pct = chapterPct(ch);
return `
${ch.done ? "✓" : "•"}
${esc(ch.catalogTitle)}\u8fdb\u5ea6 ${pct}%
`;
})
.join("");
}
function renderLog() {
const box = document.getElementById("csustcj-log");
if (!box) return;
if (!state.logs.length) {
box.innerHTML = `\u6682\u65e0\u8fd0\u884c\u65e5\u5fd7 `;
return;
}
box.innerHTML = state.logs
.slice(0, 40)
.map((l) => `${esc(l)} `)
.join("");
}
function updateProgressUI() {
const total = state.chapters.length || 0;
const done = (state.chapters || []).filter((c) => c.done).length;
let pct = 0;
if (total > 0) pct = Math.round((done / total) * 100);
else if (state.videoTime > 0) pct = Math.min(100, Math.round((state.sofar / state.videoTime) * 100));
const doneEl = document.getElementById("csustcj-queue-count");
const totalEl = document.getElementById("csustcj-queue-total");
const pctEl = document.getElementById("csustcj-queue-percent");
const bar = document.getElementById("csustcj-queue-progress");
if (doneEl) doneEl.textContent = String(done);
if (totalEl) totalEl.textContent = String(total);
if (pctEl) pctEl.textContent = `${pct}%`;
if (bar) bar.style.width = `${pct}%`;
}
function updatePanel() {
setStatus(state.status);
updateProgressUI();
const go = document.getElementById("csustcj-go");
const hw = document.getElementById("csustcj-hw");
const ex = document.getElementById("csustcj-exam");
const stop = document.getElementById("csustcj-stop");
if (go) go.disabled = !!state.running;
if (hw) hw.disabled = !!state.running;
if (ex) ex.disabled = !!state.running;
if (stop) stop.disabled = !state.running;
const curCourse = document.getElementById("csustcj-current-course");
const curChapter = document.getElementById("csustcj-current-chapter");
if (curCourse) curCourse.textContent = state.courseTitle || "\u65e0";
if (curChapter) curChapter.textContent = state.catalogTitle || "\u65e0";
const n = (state.courses || []).filter((c) => c.checked).length;
const foot = document.getElementById("csustcj-queue-text");
if (foot && !state.running) foot.textContent = n ? `\u5df2\u9009 ${n} \u95e8\u8bfe` : "\u672a\u9009\u62e9";
}
function switchTab(tab) {
document.querySelectorAll(`#${PANEL_ID} .cj-tab-btn`).forEach((btn) => {
btn.classList.toggle("active", btn.getAttribute("data-tab") === tab);
});
document.querySelectorAll(`#${PANEL_ID} .cj-pane`).forEach((pane) => {
pane.classList.toggle("active", pane.getAttribute("data-pane") === tab);
});
}
function mountPanel() {
ensureStyle();
const old = document.getElementById(PANEL_ID);
if (old) old.remove();
const panel = document.createElement("div");
panel.id = PANEL_ID;
if (gmGet(LS.collapsed, false)) panel.classList.add("collapsed");
panel.innerHTML = `
\u6210\u6559·\u534e\u5bcc\u6559\u80b2\u770b\u8bfe\u52a9\u624b v${esc(VERSION)}
${esc(SCHOOL_DISPLAY)} · \u514d\u8d39 3 \u5c0f\u8282\uff1b\u4f5c\u4e1a/\u8003\u8bd5\u9700\u4f1a\u5458
\u72b6\u6001\u5c31\u7eea
0 /0 \u7ae0\u5df2\u5b8c\u6210
0%
\u8bfe\u7a0b\u9009\u62e9\u6309\u5b66\u671f\u7b5b\u9009
\u7ae0\u8282\u9884\u89c8\u5b9e\u65f6\u8fdb\u5ea6
\u8fd0\u884c\u65e5\u5fd7\u5b9e\u65f6\u65e5\u5fd7
\u8bbe\u7f6e\u4f1a\u5458\u6388\u6743
\u4f1a\u5458\u6388\u6743
\u7528\u6237\u7c7b\u578b\u514d\u8d39\u8bd5\u7528
\u5b66\u4e60\u989d\u5ea6—
\u5f53\u524d\u8bfe\u7a0b\u65e0
\u5f53\u524d\u7ae0\u8282\u65e0
\u5b66\u6821${esc(SCHOOL_DISPLAY)}
`;
document.documentElement.appendChild(panel);
document.getElementById("csustcj-toggle").onclick = () => {
panel.classList.toggle("collapsed");
gmSet(LS.collapsed, panel.classList.contains("collapsed"));
};
document.querySelectorAll(`#${PANEL_ID} .cj-tab-btn`).forEach((btn) => {
btn.onclick = () => switchTab(btn.getAttribute("data-tab") || "course");
});
document.getElementById("csustcj-go").onclick = () => startPipeline("all");
document.getElementById("csustcj-hw").onclick = () => startPipeline("homework");
document.getElementById("csustcj-exam").onclick = () => startPipeline("exam");
document.getElementById("csustcj-stop").onclick = () => stopPipeline();
const buyBtn = document.getElementById("csustcj-buy");
if (buyBtn) buyBtn.onclick = () => openProBuy();
const saveTok = document.getElementById("csustcj-save-token");
if (saveTok) {
saveTok.onclick = async () => {
const input = document.getElementById("csustcj-cloud-token");
const typed = input ? String(input.value || "").trim() : "";
setCloudToken(typed);
const tk = getCloudToken();
const luid = getLearningUserId();
log(tk ? "\u5361\u5bc6\u5df2\u4fdd\u5b58\uff0c\u6b63\u5728\u6821\u9a8c…" : "\u5df2\u6e05\u9664\u5361\u5bc6");
dbg(`\u7ed1\u5361\u6821\u9a8c uid=${luid} server=${state.serverBuild || "\u672a\u77e5"}`);
try {
state.tokenBoundBlocked = false;
await ensureCloudLease(true);
if (isProUser()) {
log(`\u4f1a\u5458\u5df2\u751f\u6548${state.cloudProExpireAt ? "\uff0c\u5230\u671f " + formatProExpireAt(state.cloudProExpireAt) : ""}`);
} else if (!tk) {
log("\u5f53\u524d\u4e3a\u514d\u8d39\u8bd5\u7528");
} else if (state.tokenBoundBlocked) {
log(
state.serverHasRebind
? "\u6b64\u5361\u5df2\u7ed1\u5176\u4ed6\u8d26\u53f7\u3002\u8bf7\u5728\u540e\u53f0\u70b9\u300c\u6e05\u7ed1\u5b9a\u300d\u540e\u91cd\u8bd5\u3002"
: "\u4e91\u7aef\u672a\u90e8\u7f72\u6362\u7ed1\u4fee\u590d\u3002\u8bf7\u4e0a\u4f20\u6700\u65b0 database.py/main.py \u5e76\u91cd\u542f\u540e\u518d\u4fdd\u5b58\u5361\u5bc6\u3002",
"err"
);
} else {
log(
state.serverHasRebind
? "\u5361\u5bc6\u672a\u5347\u7ea7\u4e3a\u4f1a\u5458\uff08\u8bf7\u6838\u5bf9\u5361\u5bc6\u662f\u5426\u6b63\u786e\uff0c\u6216\u540e\u53f0\u6e05\u7ed1\u5b9a\u540e\u91cd\u8bd5\uff09"
: "\u5361\u5bc6\u672a\u751f\u6548\uff1a\u4e91\u7aef\u4ecd\u662f\u65e7\u7248\u3002\u8bf7\u90e8\u7f72 database.py\uff08\u542b shared anon \u6362\u7ed1\uff09\u5e76\u91cd\u542f uvicorn",
"err"
);
}
} catch (e) {
const raw = String((e && e.message) || e || "");
const tip = friendlyCloudMessage(e);
const badToken = /invalid token|revoked|expired_after_activation|expired|\u5361\u5bc6\u4e0d\u5bf9|\u4e0d\u53ef\u7528|\u5df2\u8fc7\u671f|\u5df2\u7981\u7528/i.test(
raw + " " + tip
);
if (badToken) {
setCloudToken("");
await fallBackToFreeLease();
log(tip, "err");
updateCloudPanelUI();
if (input) input.value = typed;
return;
}
if (!state.serverHasRebind && /\u7ed1\u5b9a|token_bound|\u522b\u7684\u8d26\u53f7/i.test(tip + raw)) {
clearProMembershipState();
log("\u4e91\u7aef\u672a\u90e8\u7f72\u6362\u7ed1\u4fee\u590d\u3002\u8bf7\u4e0a\u4f20\u6700\u65b0 database.py/main.py/admin_ui.py\uff0c\u91cd\u542f\u540e\u518d\u4fdd\u5b58\u5361\u5bc6\u3002", "err");
} else {
if (/token_bound|\u522b\u7684\u8d26\u53f7/i.test(raw + tip) || (e && e.code === "token_bound")) {
state.cloudTier = "free";
state.cloudProExpireAt = 0;
state.tokenBoundBlocked = true;
cacheCloudLease();
}
log(tip, "err");
}
}
updateCloudPanelUI();
};
}
const drag = document.getElementById("csustcj-drag");
let sx = 0,
sy = 0,
ox = 0,
oy = 0,
moving = false;
drag.addEventListener("mousedown", (e) => {
if (e.target.closest("button")) return;
moving = true;
sx = e.clientX;
sy = e.clientY;
const r = panel.getBoundingClientRect();
ox = r.left;
oy = r.top;
e.preventDefault();
});
window.addEventListener("mousemove", (e) => {
if (!moving) return;
panel.style.left = ox + e.clientX - sx + "px";
panel.style.top = oy + e.clientY - sy + "px";
panel.style.right = "auto";
});
window.addEventListener("mouseup", () => {
moving = false;
});
renderTermTabs();
renderCourses();
renderChapters();
renderLog();
updatePanel();
refreshSchoolDisplayUI();
}
async function boot() {
gmSet(LS.gap, 2000);
gmSet(LS.step, 60);
mountPanel();
initCloudAuth().catch(() => {});
if (isPortal) {
try {
await refreshCourses();
} catch (e) {
dbg(`\u81ea\u52a8\u62c9\u8bfe\u5931\u8d25\uff1a${e.message || e}`);
log("\u8bfe\u7a0b\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u6216\u91cd\u65b0\u6253\u5f00\u9875\u9762");
}
} else {
setStatus("\u8bf7\u56de\u5230\u5b66\u4e60\u95e8\u6237\u540e\u518d\u5f00\u59cb");
log("\u8bf7\u5728\u5b66\u4e60\u95e8\u6237\u9875\u9762\u4f7f\u7528\u672c\u52a9\u624b");
}
refreshSchoolDisplayUI();
updateCloudPanelUI();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot);
} else {
boot();
}
})();
|