// ==UserScript==
// @name 【效率版】绎通云课堂继教系列学习助手
// @namespace https://www.wlxy.live/
// @version 1.0
// @description 绎通云课堂继教云系列(*.ytccr.com)学习辅助:视频/作业/考试一键完成,高效率速度快,答题考试搭配AI,成绩高;免费体验1门课,Pro含考试。
// @antifeature payment 免费体验1门课课时与作业(不可考试);Pro不限课程含考试
// @antifeature membership 需云端授权
// @author 柠檬真酸
// @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png
// @match https://*.ytccr.com/*
// @noframes
// @require https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js
// @require https://cdn.jsdelivr.net/npm/jsencrypt@3.3.2/bin/jsencrypt.min.js
// @require https://oa34.ahzsksw.cn/static/ytccr-d9a6.js
// @grant GM_xmlhttpRequest
// @grant GM_info
// @grant GM_openInTab
// @grant GM_getValue
// @grant GM_setValue
// @connect *.ytccr.com
// @connect exam.beeouc.com
// @connect oa34.ahzsksw.cn
// @connect *.ahzsksw.cn
// @connect card.wlxy.live
// @connect card.wlxy.top
// @connect huaweicloudobs.ahjxjy.cn
// @connect cdn.jsdelivr.net
// @run-at document-idle
// @tag 学习辅助
// @tag 自动刷课
// @tag 继续教育
// @license All Rights Reserved
// ==/UserScript==
(function () {
"use strict";
try {
if (window.top !== window.self) return;
} catch (_) {
return;
}
if (window.__YTCCR_HELPER_BOOTED__) return;
window.__YTCCR_HELPER_BOOTED__ = true;
const HOST = location.hostname.replace(/^www\./, "");
if (!/\.ytccr\.com$/i.test(HOST) && HOST !== "ytccr.com") return;
const VER =
(typeof GM_info !== "undefined" && GM_info?.script?.version) || "1.0";
const SCRIPT_NAME = "\u7ece\u901a\u7ee7\u6559\u4e91\u5b66\u4e60\u52a9\u624b";
const ORIGIN = location.origin;
const API_BASE = ORIGIN + "/student_api";
const PANEL_LOGO_URL =
"https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png";
const DEFAULT_CLOUD_API_BASE = "https://oa34.ahzsksw.cn";
const PRO_BUY_URL = "https://card.wlxy.live/details/86AF2873";
const PANEL_NOTICE =
"\u514d\u8d39\u53ef\u4f53\u9a8c 1 \u95e8\u8bfe\u7684\u8bfe\u65f6\u4e0e\u4f5c\u4e1a\uff1b\u4e13\u4e1a\u7248\u5f00\u901a\u540e\u53ef\u4f7f\u7528\u5168\u90e8\u8bfe\u7a0b\u4e0e\u8003\u8bd5\u3002";
const FREE_COURSE_LIMIT = 1;
const QQ_GROUP_NUMBER = "903117129";
const QQ_GROUP_LINK =
"https://qun.qq.com/universal-share/share?ac=1&authKey=rxdL6YIJ0%2FxOEemjLqTGULvl5aAfJIVQcIvkvnwvmL2FAmpFZnSafajYHgSXMUXvx&busi_data=eyJncm91cENvZGUiOiI5MDMxMTcxMjkiLCJ0b2tlbiI6IlB2dkFGSm5XRXBrSEhtQVFTUGQzdVNZakhNWDNMbW1kODA2enpoMi9obDh4SWp0YzBDODNFaGtwRU44Z0hyU0siLCJ1aW4iOiIxMjU0MzE1MTQifQ%3D%3D&data=9oyJixSPcigCQW-saV5eXlcMwV9C6J36XySx-rDHwVwNlofvRmd2ze5sLwFtHTbYbG4nAWIUrI0qftC6aTX9xg&svctype=4&tempid=h5_group_info";
let PUB_E = "";
let PUB_C = "";
let EXAM_API = "";
let EXAM_AES_KEY = "";
let EXAM_BASIC = "";
const STORAGE = {
selected: `ytccr_selected_${HOST}`,
termId: `ytccr_term_${HOST}`,
panelPos: `ytccr_panel_pos_${HOST}`,
collapsed: `ytccr_collapsed_${HOST}`,
cloudToken: `ytccr_cloud_token_${HOST}`,
studentId: `ytccr_student_id_${HOST}`,
hwAnswers: `ytccr_hw_answers_${HOST}`,
examAnswers: `ytccr_exam_answers_${HOST}`,
};
const KEY_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const LOG_LIMIT = 280;
const EFF_MID_COUNT = 1;
const EFF_GAP_MS = 900;
const ACT_GAP_MS = 600;
const COURSE_GAP_MS = 1200;
const COURSE_PARALLEL = true;
const HW_TARGET_SCORE = 100;
const HW_MAX_ROUNDS = 6;
const HW_GAP_MS = 900;
const HW_SUBMIT_WAIT_MS = 600;
const EXAM_TARGET_SCORE = 100;
const EXAM_MAX_ROUNDS = 24;
const EXAM_STUCK_ROUNDS = 3;
const EXAM_GAP_MS = 800;
const CLOUD_EXAM_BATCH = 6;
const CLOUD_EXAM_BATCH_TIMEOUT_MS = 90000;
const DEFAULT_RUN_POLICY = {
autoHomework: true,
autoExam: true,
courseParallel: COURSE_PARALLEL,
allowExam: false,
freeAllowsExam: false,
examTargetScore: EXAM_TARGET_SCORE,
examStuckRounds: EXAM_STUCK_ROUNDS,
examMaxRounds: EXAM_MAX_ROUNDS,
hwTargetScore: HW_TARGET_SCORE,
hwMaxRounds: HW_MAX_ROUNDS,
cloudExamBatch: CLOUD_EXAM_BATCH,
actGapMs: ACT_GAP_MS,
hwGapMs: HW_GAP_MS,
courseGapMs: COURSE_GAP_MS,
examGapMs: EXAM_GAP_MS,
effMidCount: EFF_MID_COUNT,
effGapMs: EFF_GAP_MS,
};
function pemPublic(b64) {
const body = String(b64 || "").replace(/\s+/g, "");
const lines = body.match(/.{1,64}/g) || [];
return (
"-----BEGIN PUBLIC KEY-----\n" +
lines.join("\n") +
"\n-----END PUBLIC KEY-----"
);
}
function ensureD9a6() {
if (typeof window.d9a6 === "function") return true;
if (typeof d9a6 === "function") {
window.d9a6 = d9a6;
return true;
}
return false;
}
function genAesKey16() {
let s = "";
for (let i = 0; i < 16; i++) {
s += KEY_CHARS.charAt(Math.floor(Math.random() * KEY_CHARS.length));
}
return s;
}
function rsaEncryptPkcs1(plain, pubB64) {
const enc = new JSEncrypt();
enc.setPublicKey(pemPublic(pubB64));
const out = enc.encrypt(String(plain));
if (!out) throw new Error("RSA \u52a0\u5bc6\u5931\u8d25");
return out;
}
function makeSign(studentId) {
_rpk();
const ct = rsaEncryptPkcs1(String(studentId), PUB_E);
const len = ct.length;
const mid = parseInt(len / 2, 10);
return (
ct.substring(0, 3) +
ct.substring(len - 3) +
ct.substring(mid, mid + 4) +
String(Date.now())
);
}
function aesEncryptZeroPad(plainText, keyStr) {
const CJ = CryptoJS;
const p = CJ.enc.Utf8.parse(String(plainText));
const k = CJ.enc.Latin1.parse(String(keyStr));
const enc = CJ.AES.encrypt(p, k, {
iv: k,
mode: CJ.mode.CBC,
padding: CJ.pad.ZeroPadding,
});
return enc.toString();
}
function wrapD9a6(raw) {
if (!ensureD9a6()) throw new Error("d9a6 \u672a\u52a0\u8f7d");
const w = window.d9a6(String(raw));
if (!w || w.code === undefined || !w.data) {
throw new Error("d9a6 \u5c01\u88c5\u5931\u8d25");
}
return String(w.data);
}
function _bvs(payload) {
_rpk();
const keyStr = genAesKey16();
const plain = JSON.stringify(payload);
const dataCipher = aesEncryptZeroPad(plain, keyStr);
const secretKeyRaw = rsaEncryptPkcs1(keyStr, PUB_C);
return {
secretKey: wrapD9a6(secretKeyRaw),
data: wrapD9a6(dataCipher),
_meta: {
keyLen: keyStr.length,
skRawLen: secretKeyRaw.length,
wrapSkLen: null,
signProbe: null,
},
};
}
function getCookie(name) {
const m = document.cookie.match(
new RegExp("(?:^|;\\s*)" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "=([^;]*)")
);
return m ? decodeURIComponent(m[1]) : "";
}
function getPlatformToken() {
return String(getCookie("token") || "").trim();
}
function isAuthErrorMsg(msg) {
const s = String(msg || "");
return /\u8ba4\u8bc1\u5931\u8d25|\u65e0\u6cd5\u8bbf\u95ee\u8be5\u8d44\u6e90|\u672a\u767b\u5f55|\u8bf7\u5148\u767b\u5f55|\u672a\u627e\u5230\u767b\u5f55|401|unauthorized|token\s*(invalid|expired)|\u767b\u5f55\u5df2\u8fc7\u671f|\u767b\u5f55\u5931\u6548/i.test(
s
);
}
function isLikelyLoginPage() {
try {
const path = String(location.pathname || "").toLowerCase();
if (/login|signin|passport|sso/.test(path)) return true;
const title = String(document.title || "");
if (/\u767b\u5f55|login/i.test(title)) return true;
const h = document.querySelector("h1,h2,.login-title,.el-form");
if (h && /\u5b66\u751f\u767b\u5f55|\u7528\u6237\u767b\u5f55|\u767b\u5f55/.test(h.textContent || "")) return true;
} catch (_) {}
return false;
}
let _loginWatchTimer = null;
let _loginWatchArmed = false;
let _loginWatchLastToken = "";
let _loginRefreshing = false;
let _lastLoginRefreshAt = 0;
let _loginWatchExpectLeave = false;
function showLoginGuide(msg) {
S.panelHint = msg || "\u8bf7\u5148\u5728\u9875\u9762\u5b8c\u6210\u767b\u5f55";
S.task = "\u5f85\u767b\u5f55";
S.courses = [];
S.terms = [];
_loginWatchExpectLeave = true;
const box = $("ytccr-course-list");
if (box) {
box.innerHTML =
'
' +
'
\u8bf7\u5148\u767b\u5f55\u5e73\u53f0
' +
'
\u5728\u9875\u9762\u5b8c\u6210\u767b\u5f55\u540e\uff0c\u811a\u672c\u5c06\u81ea\u52a8\u5237\u65b0\u8bfe\u7a0b
' +
"
";
}
const sel = $("ytccr-term-select");
if (sel) {
sel.innerHTML = "";
sel.style.display = "none";
}
renderProfile();
renderRunStatus();
armLoginWatcher();
}
function armLoginWatcher() {
if (_loginWatchArmed) return;
_loginWatchArmed = true;
_loginWatchLastToken = getPlatformToken();
log("\u7b49\u5f85\u5e73\u53f0\u767b\u5f55…\u767b\u5f55\u6210\u529f\u540e\u5c06\u81ea\u52a8\u5237\u65b0", "info");
const tryRefresh = async (reason) => {
if (S.running || _loginRefreshing) return;
const tok = getPlatformToken();
if (!tok) return;
const onLogin = isLikelyLoginPage();
const tokenChanged = tok !== _loginWatchLastToken;
if (onLogin) {
if (tokenChanged) {
_loginWatchLastToken = tok;
_loginWatchExpectLeave = true;
S.panelHint = "\u5df2\u68c0\u6d4b\u5230\u767b\u5f55\uff0c\u7b49\u5f85\u8fdb\u5165\u5b66\u4e60\u9875…";
renderRunStatus();
}
return;
}
const leaving = _loginWatchExpectLeave;
if (!tokenChanged && !leaving && reason === "poll") {
if (Date.now() - _lastLoginRefreshAt < 2800) return;
}
_loginWatchLastToken = tok;
_loginWatchExpectLeave = false;
_lastLoginRefreshAt = Date.now();
_loginRefreshing = true;
disarmLoginWatcher();
S.panelHint = "\u767b\u5f55\u6210\u529f\uff0c\u6b63\u5728\u5237\u65b0…";
S.task = "\u5237\u65b0\u4e2d";
renderRunStatus();
log("\u68c0\u6d4b\u5230\u767b\u5f55\u6210\u529f\uff0c\u81ea\u52a8\u5237\u65b0\u8bfe\u7a0b", "ok");
try {
await refreshAll();
} catch (e) {
const raw = e?.message || e;
if (isAuthErrorMsg(raw)) {
showLoginGuide("\u767b\u5f55\u672a\u5b8c\u6210\u6216\u4f1a\u8bdd\u65e0\u6548\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55");
} else {
log(`\u81ea\u52a8\u5237\u65b0\u5931\u8d25\uff1a${_sue(e)}`, "err");
S.panelHint = _sue(e);
renderRunStatus();
armLoginWatcher();
}
} finally {
_loginRefreshing = false;
}
};
_loginWatchTimer = setInterval(() => {
tryRefresh("poll").catch(() => {});
}, 800);
try {
window.addEventListener("focus", () => tryRefresh("focus").catch(() => {}));
document.addEventListener("visibilitychange", () => {
if (!document.hidden) tryRefresh("visible").catch(() => {});
});
} catch (_) {}
}
function disarmLoginWatcher() {
_loginWatchArmed = false;
if (_loginWatchTimer) {
clearInterval(_loginWatchTimer);
_loginWatchTimer = null;
}
}
function tryFindStudentIdFromPage() {
const cached = String(GM_getValue(STORAGE.studentId, "") || "").trim();
if (/^\d{10,}$/.test(cached)) return cached;
const dig = (v) => {
const s = String(v || "").trim();
return /^\d{10,}$/.test(s) ? s : "";
};
const fromObj = (o, depth) => {
if (!o || depth > 4) return "";
if (typeof o !== "object") return "";
const direct = dig(o.studentId || o.student_id);
if (direct) return direct;
for (const k of Object.keys(o)) {
const hit = fromObj(o[k], depth + 1);
if (hit) return hit;
}
return "";
};
try {
for (const store of [localStorage, sessionStorage]) {
for (let i = 0; i < store.length; i++) {
const k = store.key(i);
const raw = store.getItem(k);
if (!raw) continue;
if (/studentid/i.test(k || "")) {
const d = dig(raw);
if (d) return d;
}
try {
const hit = fromObj(JSON.parse(raw), 0);
if (hit) return hit;
} catch (_) {}
}
}
} catch (_) {}
return "";
}
const S = {
enabled: false,
stopFlag: false,
running: false,
studentId: tryFindStudentIdFromPage(),
studentName: "",
majorName: "",
collegeOrgName: "",
terms: [],
termId: String(GM_getValue(STORAGE.termId, "") || ""),
courses: [],
selectedIds: new Set(),
selectMode: "all",
logs: [],
task: "\u5f85\u673a",
currentCourse: "",
currentAct: "",
activeCourses: new Set(),
panelHint: "",
queueDone: 0,
queueTotal: 0,
cloudToken: String(GM_getValue(STORAGE.cloudToken, "") || "").trim(),
cloudLease: "",
cloudLeaseExp: 0,
cloudTier: "unknown",
cloudRevoked: false,
freeUsedCourses: 0,
freeCourseLimit: FREE_COURSE_LIMIT,
freeAllowsExam: false,
examTargetScore: EXAM_TARGET_SCORE,
proBuyUrl: PRO_BUY_URL,
proExpiresAt: 0,
proExpiresText: "",
remotePanelNotice: "",
panelNoticePath: "/api/ytccr/panel-notice",
autoHomework: true,
autoExam: true,
runPolicy: { ...DEFAULT_RUN_POLICY },
policyReady: false,
idCardNo: "",
examToken: "",
examListCache: null,
secretsReady: false,
};
function policyNum(key, fallback) {
const v = Number(S.runPolicy?.[key]);
if (Number.isFinite(v) && v > 0) return v;
const fb = Number(fallback);
return Number.isFinite(fb) && fb > 0 ? fb : 0;
}
function requireCloudPolicy() {
if (!S.policyReady || !S.runPolicy || typeof S.runPolicy !== "object") {
throw new Error("\u4e91\u7aef\u7b56\u7565\u672a\u5c31\u7eea\uff0c\u8bf7\u68c0\u67e5\u6388\u6743\u540e\u91cd\u8bd5");
}
}
function _arp(policy) {
if (!policy || typeof policy !== "object") return;
S.runPolicy = { ...DEFAULT_RUN_POLICY, ...S.runPolicy, ...policy };
S.autoHomework = S.runPolicy.autoHomework !== false;
S.autoExam = S.runPolicy.autoExam !== false;
S.policyReady = true;
if (policy.freeAllowsExam != null || policy.free_allows_exam != null) {
S.freeAllowsExam = !!(policy.freeAllowsExam ?? policy.free_allows_exam);
} else if (S.runPolicy.freeAllowsExam != null) {
S.freeAllowsExam = !!S.runPolicy.freeAllowsExam;
}
if (policy.examTargetScore != null || policy.exam_target_score != null) {
const t = Number(policy.examTargetScore ?? policy.exam_target_score);
if (Number.isFinite(t) && t > 0) S.examTargetScore = t;
} else if (S.runPolicy.examTargetScore != null) {
const t = Number(S.runPolicy.examTargetScore);
if (Number.isFinite(t) && t > 0) S.examTargetScore = t;
}
}
function _ase(sec) {
if (!sec || typeof sec !== "object") return;
const pubE = String(sec.pubE || sec.pub_e || "").trim();
const pubC = String(sec.pubC || sec.pub_c || "").trim();
const examApi = String(sec.examApi || sec.exam_api || "").trim();
const examAes = String(sec.examAesKey || sec.exam_aes_key || "").trim();
const examBasic = String(sec.examBasic || sec.exam_basic || "").trim();
if (pubE) PUB_E = pubE;
if (pubC) PUB_C = pubC;
if (examApi) EXAM_API = examApi.replace(/\/$/, "");
if (examAes) EXAM_AES_KEY = examAes;
if (examBasic) EXAM_BASIC = examBasic;
S.secretsReady = !!(PUB_E && PUB_C);
}
function _rpk() {
if (!PUB_E || !PUB_C) {
throw new Error("\u4e91\u7aef\u53c2\u6570\u672a\u5c31\u7eea\uff0c\u8bf7\u786e\u8ba4\u5df2\u8fde\u63a5\u6388\u6743\u670d\u52a1\u540e\u5237\u65b0");
}
}
function _rek() {
if (!EXAM_API || !EXAM_AES_KEY || !EXAM_BASIC) {
throw new Error("\u8003\u8bd5\u53c2\u6570\u672a\u5c31\u7eea\uff0c\u8bf7\u5148\u5b8c\u6210\u6388\u6743\u6821\u9a8c");
}
}
async function ensureClientBootstrap(force) {
if (!force && S.secretsReady && PUB_E && PUB_C) return;
await _fcc();
if (!PUB_E || !PUB_C) {
throw new Error("\u4e91\u7aef\u53c2\u6570\u672a\u5c31\u7eea\uff0c\u8bf7\u786e\u8ba4\u5df2\u8fde\u63a5\u6388\u6743\u670d\u52a1\u540e\u5237\u65b0");
}
}
(function loadSelected() {
try {
const raw = GM_getValue(STORAGE.selected, null);
const sel = typeof raw === "string" ? JSON.parse(raw) : raw;
if (sel && typeof sel === "object" && !Array.isArray(sel)) {
S.selectMode =
sel.mode === "none" || sel.mode === "partial" ? sel.mode : "all";
S.selectedIds = new Set(
Array.isArray(sel.ids) ? sel.ids.map(String) : []
);
} else if (Array.isArray(sel)) {
S.selectMode = sel.length ? "partial" : "all";
S.selectedIds = new Set(sel.map(String));
}
} catch (_) {}
})();
function saveSelected() {
GM_setValue(STORAGE.selected, {
mode: S.selectMode,
ids: Array.from(S.selectedIds),
});
}
function $(id) {
return document.getElementById(id);
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, Math.max(0, ms)));
}
function _sue(msg) {
let raw = "";
if (msg && typeof msg === "object" && msg.message) raw = String(msg.message);
else raw = String(msg || "");
raw = raw.replace(/^Error:\s*/i, "").trim();
if (!raw) return "\u672a\u77e5\u9519\u8bef";
if (/exam_requires_pro|pro_only/i.test(raw)) return "\u8003\u8bd5\u9700\u4e13\u4e1a\u7248";
if (/free_course_quota|quota/i.test(raw)) return "\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5f00\u901a\u4e13\u4e1a\u7248";
if (/504|502|\u8d85\u65f6|timeout/i.test(raw)) return "\u670d\u52a1\u7e41\u5fd9\u6216\u8d85\u65f6\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5";
if (
/\u672a\u68c0\u6d4b.*token|\u8bf7\u5148\u767b\u5f55|\u8ba4\u8bc1\u5931\u8d25|\u65e0\u6cd5\u8bbf\u95ee\u8be5\u8d44\u6e90|\u672a\u767b\u5f55|\u672a\u627e\u5230\u767b\u5f55/i.test(raw)
) {
return "\u8bf7\u5148\u767b\u5f55\u5e73\u53f0";
}
if (/\u4e91\u7aef\u53c2\u6570\u672a\u5c31\u7eea|\u6388\u6743\u672a\u5c31\u7eea/i.test(raw)) {
return "\u4e91\u7aef\u53c2\u6570\u672a\u5c31\u7eea\uff0c\u8bf7\u786e\u8ba4\u5df2\u8fde\u63a5\u6388\u6743\u670d\u52a1\u540e\u5237\u65b0";
}
if (/\u4e91\u7aef_not_configured|\u4e91\u7aef_disabled/i.test(raw)) {
return "\u7b54\u9898\u670d\u52a1\u672a\u5c31\u7eea\uff0c\u8bf7\u8054\u7cfb\u4f5c\u8005";
}
let s = raw
.replace(/\u4e91\u7aef[_\s-]*/gi, "")
.replace(/\boa34\b|\blease\b|\bqbank\b/gi, "")
.replace(/\u8d44\u6599\u5e93|\u672a\u547d\u4e2d|\u6392\u9664\u96c6|\u515c\u5e95|\u4e91\u7aef|AI\s*\u5f02\u5e38/gi, "")
.replace(/\s{2,}/g, " ")
.trim();
return (s || "\u64cd\u4f5c\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5").slice(0, 120);
}
function esc(s) {
return String(s ?? "")
.replace(/&/g, "&")
.replace(//g, ">");
}
function trunc(s, n) {
const t = String(s || "");
return t.length > n ? t.slice(0, n) + "…" : t;
}
function courseKey(c) {
return String(c.studentChooseId || c.termCourseId || "");
}
function isCourseSelected(c) {
const k = courseKey(c);
if (S.selectMode === "all") return true;
if (S.selectMode === "none") return false;
return S.selectedIds.has(k);
}
function progressNum(v) {
const n = Number(v);
return Number.isFinite(n) ? n : 0;
}
function isActDone(act) {
return progressNum(act.progress) >= 100;
}
function getCloudApiBase() {
return DEFAULT_CLOUD_API_BASE.replace(/\/+$/, "");
}
function isCloudProTier() {
return !S.cloudRevoked && String(S.cloudTier || "").toLowerCase() === "pro";
}
function formatCloudTierText() {
if (S.cloudRevoked || String(S.cloudTier).toLowerCase() === "revoked")
return "\u5df2\u505c\u7528";
if (isCloudProTier()) return "\u4e13\u4e1a\u7248";
if (String(S.cloudTier).toLowerCase() === "free") return "\u4f53\u9a8c\u7248";
return "\u672a\u6821\u9a8c";
}
function _crq(path, method, body, headersExtra, timeoutMs) {
const url = `${getCloudApiBase()}${path}`;
const headers = Object.assign(
{
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
},
headersExtra || {}
);
if (S.cloudToken) headers.Authorization = `Bearer ${S.cloudToken}`;
const luid = String(S.studentId || "").trim();
if (luid) headers["x-learning-user-id"] = luid;
const payload = body == null ? null : JSON.stringify(body);
const timeout = Number(timeoutMs) > 0 ? Number(timeoutMs) : 30000;
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== "function") {
reject(new Error("\u670d\u52a1\u8bf7\u6c42\u4e0d\u53ef\u7528"));
return;
}
GM_xmlhttpRequest({
method: method || "POST",
url,
headers,
data: payload,
timeout,
onload(resp) {
let data = {};
try {
data = JSON.parse(String(resp.responseText || "").trim() || "{}");
} catch (_) {
data = {};
}
const status = Number(resp.status || 0);
if (status < 200 || status >= 300) {
const detail = String(
data.detail || data.message || data.msg || ""
).trim();
if (status === 504 || status === 502) {
reject(
new Error(
detail ||
`http_${status}\uff08\u670d\u52a1\u8d85\u65f6\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\uff09`
)
);
return;
}
reject(new Error(detail || `http_${status}`));
return;
}
resolve(data);
},
onerror() {
reject(new Error("\u7f51\u7edc\u9519\u8bef"));
},
ontimeout() {
reject(new Error("\u8bf7\u6c42\u8d85\u65f6"));
},
});
});
}
function formatProExpireText() {
if (!isCloudProTier()) return "—";
if (S.proExpiresText) return S.proExpiresText;
const ts = Number(S.proExpiresAt || 0);
if (!ts) return "—";
try {
const d = new Date(ts * 1000);
if (Number.isNaN(d.getTime())) return "—";
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(
d.getHours()
)}:${p(d.getMinutes())}`;
} catch (_) {
return "—";
}
}
function renderPanelNotice() {
const text =
String(S.remotePanelNotice || PANEL_NOTICE || "").trim() || PANEL_NOTICE;
const span = $("ytccr-ann-text");
if (span) span.textContent = text;
}
async function _fpn() {
try {
const text = await new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== "function") {
reject(new Error("no_gm"));
return;
}
GM_xmlhttpRequest({
method: "GET",
url: `${getCloudApiBase()}${
S.panelNoticePath || "/api/ytccr/panel-notice"
}`,
headers: { Accept: "text/plain, */*" },
timeout: 15000,
anonymous: true,
onload(resp) {
if (resp.status >= 200 && resp.status < 300) {
resolve(String(resp.responseText || "").trim());
} else reject(new Error("notice_http_" + resp.status));
},
onerror() {
reject(new Error("notice_net"));
},
ontimeout() {
reject(new Error("notice_timeout"));
},
});
});
if (text) S.remotePanelNotice = text;
} catch (e) {
try {
log(`\u516c\u544a\u62c9\u53d6\u5931\u8d25\uff1a${e.message || e}`, "warn");
} catch (_) {}
}
renderPanelNotice();
}
async function _fcc() {
try {
const data = await _crq("/api/ytccr/client-config", "GET");
if (data?.panelNoticePath) {
const p = String(data.panelNoticePath).trim();
if (p) S.panelNoticePath = p.startsWith("/") ? p : `/${p}`;
}
if (data?.panelNotice || data?.panel_notice) {
const n = String(data.panelNotice || data.panel_notice || "").trim();
if (n) S.remotePanelNotice = n;
}
if (data?.freeCourseLimit != null || data?.free_course_limit != null) {
S.freeCourseLimit =
Number(data.freeCourseLimit ?? data.free_course_limit) ||
FREE_COURSE_LIMIT;
}
if (data?.freeAllowsExam != null || data?.free_allows_exam != null) {
S.freeAllowsExam = !!(data.freeAllowsExam ?? data.free_allows_exam);
} else {
S.freeAllowsExam = false;
}
if (data?.examTargetScore != null || data?.exam_target_score != null) {
const t = Number(data.examTargetScore ?? data.exam_target_score);
if (Number.isFinite(t) && t > 0) S.examTargetScore = t;
}
if (data?.proBuyUrl) {
const buy = String(data.proBuyUrl).trim();
if (buy) S.proBuyUrl = buy;
}
if (data?.policy) _arp(data.policy);
if (data?.secrets) _ase(data.secrets);
renderPanelNotice();
} catch (_) {}
await _fpn();
}
async function _cl(force) {
const now = Date.now() / 1000;
if (
!force &&
S.cloudLease &&
S.cloudLeaseExp - now > 60 &&
S.policyReady
) {
return S.cloudLease;
}
const luid = String(S.studentId || "").trim();
if (!luid) throw new Error("\u672a\u83b7\u53d6\u5b66\u5458\u8eab\u4efd\uff0c\u8bf7\u5148\u767b\u5f55");
const data = await _crq("/api/ytccr/lease", "POST", {
learning_user_id: luid,
name: S.studentName || "",
userName: S.studentName || "",
});
S.cloudLease = String(data.lease || "");
S.cloudLeaseExp = Number(data.exp || 0) || 0;
S.cloudTier = String(
data.tier || (S.cloudToken ? "pro" : "free")
).toLowerCase();
if (data.free_used_courses != null)
S.freeUsedCourses = Number(data.free_used_courses) || 0;
if (data.free_course_limit != null)
S.freeCourseLimit =
Number(data.free_course_limit) || FREE_COURSE_LIMIT;
if (data.proBuyUrl)
S.proBuyUrl = String(data.proBuyUrl).trim() || S.proBuyUrl;
const proExp = Number(data.pro_expires_at ?? data.proExpiresAt ?? 0);
S.proExpiresAt = Number.isFinite(proExp) && proExp > 0 ? proExp : 0;
S.proExpiresText = String(
data.pro_expires_at_text || data.proExpiresAtText || ""
).trim();
if (!isCloudProTier()) {
S.proExpiresAt = 0;
S.proExpiresText = "";
}
if (data.policy) _arp(data.policy);
if (data.secrets) _ase(data.secrets);
if (!S.cloudLease) throw new Error("\u6388\u6743\u65e0\u6548\uff0c\u8bf7\u91cd\u65b0\u4fdd\u5b58\u6388\u6743\u7801");
if (!S.policyReady) throw new Error("\u4e91\u7aef\u672a\u8fd4\u56de\u8fd0\u884c\u7b56\u7565");
try {
refreshCloudUi();
} catch (_) {}
return S.cloudLease;
}
function coursePlanId(course) {
return String(
course?.courseId ||
course?.id ||
course?.studentChooseId ||
course?.chooseId ||
""
);
}
async function _fop(mode, courses) {
await _cl(false);
requireCloudPolicy();
const list = (courses || []).map((c) => ({
courseId: coursePlanId(c),
courseName: String(c?.courseName || c?.name || ""),
}));
const data = await _crq("/api/ytccr/orchestrate/plan", "POST", {
lease: S.cloudLease,
mode: mode || "full",
courses: list,
});
if (data?.policy) _arp(data.policy);
if (data?.secrets) _ase(data.secrets);
if (!data?.plan || typeof data.plan !== "object") {
throw new Error("\u4e91\u7aef\u7f16\u6392\u65e0\u6548\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5");
}
if (!Array.isArray(data.plan.tasks) || !data.plan.tasks.length) {
throw new Error("\u4e91\u7aef\u672a\u8fd4\u56de\u5b66\u4e60\u8ba1\u5212");
}
return data;
}
function _rpt(planData, courses) {
const tasks = Array.isArray(planData?.plan?.tasks)
? planData.plan.tasks
: [];
if (!tasks.length) throw new Error("\u4e91\u7aef\u672a\u8fd4\u56de\u8bfe\u7a0b\u4efb\u52a1");
const planSteps = Array.isArray(planData?.plan?.steps)
? planData.plan.steps.map(String).filter(Boolean)
: [];
const byId = new Map();
const byName = new Map();
for (const c of courses || []) {
const id = coursePlanId(c);
if (id) byId.set(id, c);
const n = String(c?.courseName || "").trim();
if (n) byName.set(n, c);
}
const out = [];
for (const t of tasks) {
const id = String(t.courseId || t.course_id || "").trim();
const name = String(t.courseName || t.course_name || "").trim();
const course = (id && byId.get(id)) || (name && byName.get(name)) || null;
if (!course) continue;
const steps =
Array.isArray(t.steps) && t.steps.length
? t.steps.map(String).filter(Boolean)
: planSteps.slice();
if (!steps.length) {
throw new Error("\u4e91\u7aef\u672a\u8fd4\u56de\u5b66\u4e60\u6b65\u9aa4");
}
out.push({ course, steps });
}
if (!out.length) {
throw new Error("\u4e91\u7aef\u8ba1\u5212\u4e0e\u672c\u5730\u8bfe\u7a0b\u65e0\u6cd5\u5339\u914d\uff0c\u8bf7\u5237\u65b0\u540e\u91cd\u8bd5");
}
return out;
}
function createAsyncLock() {
let tail = Promise.resolve();
return function withLock(fn) {
const run = tail.then(() => fn());
tail = run.then(
() => undefined,
() => undefined
);
return run;
};
}
const withExamLock = createAsyncLock();
const withQuotaLock = createAsyncLock();
function refreshActiveCourseLabel() {
const names = Array.from(S.activeCourses || []);
if (!names.length) {
S.currentCourse = "";
return;
}
if (names.length === 1) {
S.currentCourse = names[0];
return;
}
const head = names.slice(0, 2).join("\u3001");
S.currentCourse =
names.length > 2 ? `${head} \u7b49${names.length}\u95e8` : head;
}
async function _ccq(course) {
return withQuotaLock(async () => {
await _cl(false);
const cid = courseKey(course);
const data = await _crq("/api/ytccr/course/consume", "POST", {
lease: S.cloudLease,
course_id: cid,
course_name: course.courseName || "",
});
if (data.free_used_courses != null)
S.freeUsedCourses = Number(data.free_used_courses) || 0;
if (data.free_course_limit != null)
S.freeCourseLimit = Number(data.free_course_limit) || S.freeCourseLimit;
if (data.tier) S.cloudTier = String(data.tier).toLowerCase();
refreshCloudUi();
return data;
});
}
function refreshCloudUi() {
const tierEl = $("ytccr-cloud-tier");
const freeEl = $("ytccr-cloud-free");
const expEl = $("ytccr-cloud-expire");
const tokenEl = $("ytccr-cloud-token");
if (tierEl) {
tierEl.textContent = formatCloudTierText();
tierEl.style.color = S.cloudRevoked ? "#b91c1c" : "#0f172a";
}
if (freeEl) {
freeEl.textContent = isCloudProTier()
? "\u4e0d\u9650"
: `${Math.max(0, Number(S.freeUsedCourses) || 0)} / ${Math.max(
0,
Number(S.freeCourseLimit) || 0
)}`;
}
if (expEl) expEl.textContent = formatProExpireText();
if (tokenEl && document.activeElement !== tokenEl) {
tokenEl.value = S.cloudToken || "";
}
renderPanelNotice();
}
function log(msg, level) {
const row = {
t: new Date().toLocaleTimeString(),
m: String(msg || ""),
l: level || "info",
};
S.logs.unshift(row);
if (S.logs.length > LOG_LIMIT) S.logs.length = LOG_LIMIT;
renderLog();
}
function renderLog() {
const box = $("ytccr-run-log");
if (!box) return;
box.innerHTML = S.logs
.slice(0, 80)
.map((r) => {
const cls =
r.l === "ok"
? "ytccr-log-ok"
: r.l === "warn"
? "ytccr-log-warn"
: r.l === "err"
? "ytccr-log-err"
: "ytccr-log-info";
return `${esc(
r.t
)} ${esc(r.m)}
`;
})
.join("");
}
function renderRunStatus() {
const st = $("ytccr-auto-status");
if (st) st.textContent = S.running ? "\u8fd0\u884c\u4e2d" : "\u5df2\u505c\u6b62";
const done = $("ytccr-queue-done");
const total = $("ytccr-queue-total");
const pct = $("ytccr-queue-percent");
const bar = $("ytccr-queue-progress");
const task = $("ytccr-current-task");
if (done) done.textContent = String(S.queueDone);
if (total) total.textContent = String(S.queueTotal);
const p =
S.queueTotal > 0
? Math.min(100, Math.round((S.queueDone / S.queueTotal) * 100))
: 0;
if (pct) pct.textContent = `${p}%`;
if (bar) bar.style.width = `${p}%`;
if (task) {
const parts = [S.task];
if (S.currentCourse) parts.push(S.currentCourse);
if (S.currentAct) parts.push(S.currentAct);
task.textContent = parts.filter(Boolean).join(" · ");
}
const hint = $("ytccr-panel-hint");
if (hint) hint.textContent = S.panelHint || "";
syncStartStop();
}
function syncStartStop() {
const start = $("ytccr-start");
const stop = $("ytccr-stop");
const examStart = $("ytccr-exam-start");
if (start) {
start.disabled = !!S.running;
start.classList.toggle("ytccr-btn-off", !!S.running);
}
if (examStart) {
examStart.disabled = !!S.running;
examStart.classList.toggle("ytccr-btn-off", !!S.running);
}
if (stop) {
stop.disabled = !S.running;
stop.classList.toggle("ytccr-btn-off", !S.running);
}
}
async function platformRequest(method, path, body, opts) {
const token = getPlatformToken();
if (!token) throw new Error("\u672a\u627e\u5230\u767b\u5f55 token\uff0c\u8bf7\u5148\u767b\u5f55\u5e73\u53f0");
if (!S.studentId && !(opts && opts.skipSign)) {
}
const ts = Date.now();
let url = path.startsWith("http") ? path : API_BASE + path;
const u = new URL(url, ORIGIN);
if (String(method || "GET").toUpperCase() === "GET") {
u.searchParams.set("_t", String(ts));
}
const headers = {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json;charset=UTF-8",
Authorization: `Bearer ${token}`,
};
if (S.studentId) {
headers.sign = makeSign(S.studentId);
} else if (opts && opts.signStudentId) {
headers.sign = makeSign(opts.signStudentId);
}
const init = {
method: method || "GET",
headers,
credentials: "include",
cache: "no-store",
};
if (body != null && String(method).toUpperCase() !== "GET") {
init.body = typeof body === "string" ? body : JSON.stringify(body);
}
const resp = await fetch(u.toString(), init);
let data = null;
const text = await resp.text();
try {
data = JSON.parse(text || "{}");
} catch (_) {
data = { raw: text };
}
if (!resp.ok) {
throw new Error(
String(data?.msg || data?.message || `HTTP ${resp.status}`)
);
}
if (data && data.code != null && Number(data.code) !== 200) {
throw new Error(String(data.msg || data.message || `\u4e1a\u52a1\u7801 ${data.code}`));
}
return data;
}
async function apiGet(path, opts) {
return platformRequest("GET", path, null, opts);
}
async function apiPost(path, body, opts) {
return platformRequest("POST", path, body, opts);
}
function rememberStudentId(id) {
const sid = String(id || "").trim();
if (!/^\d{10,}$/.test(sid)) return;
S.studentId = sid;
try {
GM_setValue(STORAGE.studentId, sid);
} catch (_) {}
}
async function loadMajorInfo() {
const res = await apiGet("/yt/student/myStudy/getMajorInfo");
const d = res?.data || {};
rememberStudentId(d.studentId || S.studentId);
S.majorName = String(d.majorName || "").trim();
S.collegeOrgName = String(d.collegeOrgName || "").trim();
S.studentName = String(
d.studentName || d.realName || d.teacherName || S.studentName || ""
).trim();
return d;
}
async function loadMajorInfoSafe() {
const token = getPlatformToken();
if (!token) throw new Error("\u672a\u767b\u5f55");
if (!S.studentId) {
const found = tryFindStudentIdFromPage();
if (found) rememberStudentId(found);
}
if (S.studentId) return loadMajorInfo();
const ts = Date.now();
const url = `${API_BASE}/yt/student/myStudy/getMajorInfo?_t=${ts}`;
const resp = await fetch(url, {
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${token}`,
sign: makeSign("0"),
},
credentials: "include",
cache: "no-store",
});
const data = await resp.json();
if (data?.code === 200 && data.data?.studentId) {
rememberStudentId(data.data.studentId);
S.majorName = String(data.data.majorName || "");
S.collegeOrgName = String(data.data.collegeOrgName || "");
return loadMajorInfo();
}
throw new Error(data?.msg || "\u83b7\u53d6\u5b66\u5458\u4fe1\u606f\u5931\u8d25\uff08\u8bf7\u5148\u6253\u5f00\u5b66\u4e60\u9875\u5237\u65b0\uff09");
}
async function loadTerms() {
const res = await apiGet("/yt/student/myStudy/getTermSelList");
const list = Array.isArray(res?.data) ? res.data : [];
S.terms = list;
if (!S.termId) {
const cur = list.find((t) => Number(t.isCurrentTerm) === 1);
S.termId = String((cur || list[0] || {}).termId || "");
if (S.termId) GM_setValue(STORAGE.termId, S.termId);
}
return list;
}
async function loadCourses() {
if (!S.termId) await loadTerms();
if (!S.termId) throw new Error("\u65e0\u5b66\u671f\u4fe1\u606f");
const res = await apiGet(
`/yt/student/myStudy/getTermChooseCourseList?termId=${encodeURIComponent(
S.termId
)}`
);
const list = Array.isArray(res?.data?.courseList)
? res.data.courseList
: [];
S.courses = list.map((c) => ({
courseName: String(c.courseName || ""),
studentChooseId: String(c.studentChooseId || ""),
termCourseId: String(c.termCourseId || ""),
progress: progressNum(c.progress),
score: c.score,
studyWeight: c.studyWeight,
testWeight: c.testWeight,
credit: c.credit,
}));
return S.courses;
}
async function loadChapterActs(course) {
const res = await apiGet(
`/yt/student/act/getChapterActList?termCourseId=${encodeURIComponent(
course.termCourseId
)}&studentChooseId=${encodeURIComponent(course.studentChooseId)}`
);
const chapters = Array.isArray(res?.data) ? res.data : [];
const videos = [];
const homeworks = [];
for (const ch of chapters) {
const actList = Array.isArray(ch.actList) ? ch.actList : [];
for (const a of actList) {
const actType = Number(a.actType);
const row = {
actId: String(a.actId || ""),
actName: String(a.actName || ""),
actType,
progress: progressNum(a.progress),
actState: a.actState,
score: a.score,
chapterName: String(ch.chapterName || ""),
};
if (actType === 1) videos.push(row);
else if (actType === 3) homeworks.push(row);
}
}
return { videos, homeworks };
}
async function queryVideo(actId, studentChooseId) {
const res = await apiGet(
`/yt/student/video/queryById?actId=${encodeURIComponent(
actId
)}&studentChooseId=${encodeURIComponent(studentChooseId)}`
);
return res?.data || {};
}
async function addStudyTimeCache(actId, studentChooseId) {
return apiPost("/yt/student/myStudy/addStudyTimeCache", {
objectId: actId,
studentChooseId,
studyType: 3,
});
}
async function saveVideoProgress(actId, studentChooseId, videoTime, videoAllTime) {
const body = _bvs({
actId: String(actId),
studentChooseId: String(studentChooseId),
videoTime: Number(videoTime) || 0,
videoAllTime: Number(videoAllTime) || 0,
});
const { secretKey, data } = body;
return apiPost("/yt/student/video/save", { secretKey, data });
}
function loadHwAnswerBag() {
try {
const raw = GM_getValue(STORAGE.hwAnswers, null);
const obj = typeof raw === "string" ? JSON.parse(raw) : raw;
return obj && typeof obj === "object" ? obj : {};
} catch (_) {
return {};
}
}
function saveHwAnswerBag(map) {
GM_setValue(STORAGE.hwAnswers, map || {});
}
function hwCacheKey(actId) {
return String(actId || "");
}
function flattenQastoreList(data) {
const out = [];
const classifyList = Array.isArray(data?.classifyList)
? data.classifyList
: [];
for (const c of classifyList) {
const list = Array.isArray(c.qastoreList) ? c.qastoreList : [];
for (let i = 0; i < list.length; i++) {
const q = list[i] || {};
out.push({
...q,
classifyId: String(q.classifyId || c.classifyId || ""),
classifyName: String(q.classifyName || c.classifyName || ""),
childIndex: i,
});
}
}
return out;
}
function pickCorrectOptionIds(q) {
const opts = Array.isArray(q?.optList) ? q.optList : [];
return opts
.filter((o) => Number(o.isAnswer) === 1)
.map((o) => String(o.optionId || ""))
.filter(Boolean);
}
function randomOptionResult(q) {
const opts = Array.isArray(q?.optList) ? q.optList : [];
const ids = opts.map((o) => String(o.optionId || "")).filter(Boolean);
if (!ids.length) return "";
return ids[Math.floor(Math.random() * ids.length)];
}
function mergeAnswerMapFromResult(questions, bag) {
const next = { ...(bag || {}) };
for (const q of questions) {
const qid = String(q.qastoreId || "");
if (!qid) continue;
const correct = pickCorrectOptionIds(q);
if (correct.length) next[qid] = correct.join(",");
}
return next;
}
async function getTestInfo(course, act) {
const res = await apiGet(
`/yt/student/test/getTestInfoById?actId=${encodeURIComponent(
act.actId
)}&termCourseId=${encodeURIComponent(
course.termCourseId
)}&studentChooseId=${encodeURIComponent(course.studentChooseId)}`
);
return res?.data || {};
}
async function getTestResultList(course, act) {
const res = await apiGet(
`/yt/student/test/getTestResultList?pageNum=1&pageSize=100&actId=${encodeURIComponent(
act.actId
)}&studentChooseId=${encodeURIComponent(
course.studentChooseId
)}&termCourseId=${encodeURIComponent(course.termCourseId)}`
);
return Array.isArray(res?.rows) ? res.rows : [];
}
async function getTestQastoreList(course, act) {
const res = await apiGet(
`/yt/student/test/getTestqastoreList?actId=${encodeURIComponent(
act.actId
)}&studentChooseId=${encodeURIComponent(
course.studentChooseId
)}&termCourseId=${encodeURIComponent(course.termCourseId)}`
);
return res?.data || {};
}
async function getTestQastoreResult(course, act, studentTestId) {
const res = await apiGet(
`/yt/student/test/getTestQastoreResult?actId=${encodeURIComponent(
act.actId
)}&studentChooseId=${encodeURIComponent(
course.studentChooseId
)}&studentTestId=${encodeURIComponent(studentTestId)}`
);
return res?.data || {};
}
async function saveTest(course, act, questions, testTime) {
const resultList = questions.map((q, idx) => {
const copy = { ...q };
copy.childIndex = idx;
copy.testQastoreId = copy.testQastoreId ?? null;
copy.resultId = copy.resultId ?? null;
copy.resultState = null;
copy.qastoreLabel = copy.qastoreLabel ?? null;
copy.qastoreAnalysis = copy.qastoreAnalysis ?? null;
copy.orderNo = copy.orderNo ?? null;
copy.initQastorePoint = copy.initQastorePoint ?? null;
copy.comment = copy.comment ?? null;
copy.answers = copy.answers ?? null;
if (copy.qastoreResult == null) copy.qastoreResult = "";
return copy;
});
const res = await apiPost("/yt/student/test/save", {
actId: String(act.actId),
termCourseId: String(course.termCourseId),
studentChooseId: String(course.studentChooseId),
testTime: Math.max(1, Math.floor(Number(testTime) || 30)),
resultList,
});
return res?.data || {};
}
function bestResultRow(rows) {
let best = null;
for (const r of rows || []) {
const pts = Number(r.testPoint ?? r.correctRate ?? 0);
if (!best || pts > Number(best.testPoint ?? best.correctRate ?? 0)) {
best = r;
}
}
return best;
}
async function harvestHomeworkAnswers(course, act, studentTestId, bag) {
if (!studentTestId) return bag || {};
const data = await getTestQastoreResult(course, act, studentTestId);
const qs = flattenQastoreList(data);
return mergeAnswerMapFromResult(qs, bag);
}
async function _soh(course, act) {
if (S.stopFlag) return false;
S.currentAct = act.actName;
S.task = "\u505a\u4f5c\u4e1a";
renderRunStatus();
await addStudyTimeCache(act.actId, course.studentChooseId);
let info = await getTestInfo(course, act);
const hwTarget = policyNum("hwTargetScore", HW_TARGET_SCORE) || HW_TARGET_SCORE;
const hwMaxRounds = policyNum("hwMaxRounds", HW_MAX_ROUNDS) || HW_MAX_ROUNDS;
const bestKnown = Number(info.maxTestPoint ?? 0);
if (bestKnown >= hwTarget) {
log(`\u4f5c\u4e1a\u5df2\u6ee1\u5206\uff0c\u8df3\u8fc7\uff1a${act.actName}\uff08${bestKnown}\uff09`, "ok");
return true;
}
const allMap = loadHwAnswerBag();
const key = hwCacheKey(act.actId);
let bag = { ...(allMap[key] || {}) };
try {
const rows = await getTestResultList(course, act);
const best = bestResultRow(rows);
if (best?.studentTestId) {
bag = await harvestHomeworkAnswers(
course,
act,
best.studentTestId,
bag
);
allMap[key] = bag;
saveHwAnswerBag(allMap);
log(
`\u5df2\u7f13\u5b58\u5386\u53f2\u7b54\u6848 ${Object.keys(bag).length} \u9898 · \u5386\u53f2\u6700\u9ad8 ${Number(
best.testPoint ?? best.correctRate ?? 0
)}`,
"info"
);
}
} catch (e) {
log(`\u62c9\u53d6\u5386\u53f2\u6210\u7ee9\uff1a${e.message || e}`, "warn");
}
let lastScore = bestKnown;
for (let round = 1; round <= hwMaxRounds; round++) {
if (S.stopFlag) return false;
const paper = await getTestQastoreList(course, act);
const questions = flattenQastoreList(paper);
if (!questions.length) {
log(`\u4f5c\u4e1a\u65e0\u9898\u76ee\uff1a${act.actName}`, "warn");
return false;
}
let hit = 0;
let miss = 0;
for (const q of questions) {
const qid = String(q.qastoreId || "");
const known = bag[qid];
if (known) {
q.qastoreResult = String(known);
hit += 1;
} else {
q.qastoreResult = randomOptionResult(q);
miss += 1;
}
}
log(
`\u4f5c\u4e1a\u7b2c ${round} \u8f6e\uff1a${act.actName} · \u5df2\u77e5 ${hit} / \u968f\u673a ${miss} / \u5171 ${questions.length}`,
miss ? "warn" : "info"
);
const testTime = Math.max(
20,
Math.min(240, questions.length * 4 + Math.floor(Math.random() * 20))
);
const saved = await saveTest(course, act, questions, testTime);
const studentTestId = String(saved.studentTestId || "");
await sleep(HW_SUBMIT_WAIT_MS);
if (studentTestId) {
try {
bag = await harvestHomeworkAnswers(course, act, studentTestId, bag);
allMap[key] = bag;
saveHwAnswerBag(allMap);
} catch (e) {
log(`\u67e5\u770b\u7b54\u6848\u5931\u8d25\uff1a${e.message || e}`, "warn");
}
}
info = await getTestInfo(course, act);
lastScore = Number(info.maxTestPoint ?? lastScore);
log(`\u4f5c\u4e1a\u5f53\u524d\u6700\u9ad8\u5206\uff1a${lastScore}`, lastScore >= hwTarget ? "ok" : "info");
if (lastScore >= hwTarget) {
log(`\u4f5c\u4e1a\u5b8c\u6210\uff1a${act.actName}`, "ok");
return true;
}
await sleep(policyNum("hwGapMs", HW_GAP_MS));
}
log(`\u4f5c\u4e1a\u672a\u8fbe\u76ee\u6807\u5206\uff08${lastScore}/${hwTarget}\uff09\uff1a${act.actName}`, "warn");
return false;
}
function _eep(plain) {
_rek();
const key = CryptoJS.enc.Utf8.parse(EXAM_AES_KEY);
const data = CryptoJS.enc.Utf8.parse(String(plain || ""));
return CryptoJS.AES.encrypt(data, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
}).toString();
}
function loadExamBag() {
try {
const raw = GM_getValue(STORAGE.examAnswers, null);
const obj = typeof raw === "string" ? JSON.parse(raw) : raw;
return obj && typeof obj === "object" ? obj : {};
} catch (_) {
return {};
}
}
function saveExamBag(map) {
GM_setValue(STORAGE.examAnswers, map || {});
}
function examBagKey(subjectId) {
return String(subjectId || "");
}
function examRequest(method, path, body, opts) {
const url = path.startsWith("http") ? path : EXAM_API + path;
const headers = Object.assign(
{
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json;charset=UTF-8",
},
(opts && opts.headers) || {}
);
if (S.examToken) headers.token = S.examToken;
const payload = body == null ? null : JSON.stringify(body);
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== "function") {
reject(new Error("GM_xmlhttpRequest \u4e0d\u53ef\u7528"));
return;
}
GM_xmlhttpRequest({
method: method || "GET",
url,
headers,
data: payload,
timeout: 60000,
onload(resp) {
let data = {};
try {
data = JSON.parse(String(resp.responseText || "").trim() || "{}");
} catch (_) {
data = { raw: resp.responseText };
}
const status = Number(resp.status || 0);
if (status < 200 || status >= 300) {
reject(
new Error(
String(data.msg || data.message || data.detail || `http_${status}`)
)
);
return;
}
if (data && data.code != null && Number(data.code) !== 1) {
reject(new Error(String(data.msg || `exam_code_${data.code}`)));
return;
}
resolve(data);
},
onerror() {
reject(new Error("\u8003\u8bd5\u5e73\u53f0\u7f51\u7edc\u9519\u8bef"));
},
ontimeout() {
reject(new Error("\u8003\u8bd5\u5e73\u53f0\u8d85\u65f6"));
},
});
});
}
async function loadStudentProfile() {
const res = await apiGet("/yt/student/myStudentInfo/getInfo");
const d = res?.data || {};
S.idCardNo = String(d.idCardNo || S.idCardNo || "").trim();
S.studentName = String(d.realName || S.studentName || "").trim();
if (d.studentId) rememberStudentId(d.studentId);
return d;
}
async function examLogin() {
if (!S.idCardNo) await loadStudentProfile();
if (!S.idCardNo || S.idCardNo.length < 6) {
throw new Error("\u672a\u83b7\u53d6\u8eab\u4efd\u8bc1\u53f7\uff0c\u65e0\u6cd5\u767b\u5f55\u8003\u8bd5\u5e73\u53f0");
}
const pwdPlain = S.idCardNo.slice(-6);
const data = await examRequest(
"POST",
"/api/login",
{
username: S.idCardNo,
password: _eep(pwdPlain),
loginType: "1",
},
{ headers: { Authorization: EXAM_BASIC } }
);
S.examToken = String(data?.data?.token || "").trim();
if (!S.examToken) throw new Error("\u8003\u8bd5\u767b\u5f55\u672a\u8fd4\u56de token");
log(`\u8003\u8bd5\u5e73\u53f0\u5df2\u767b\u5f55\uff1a${data?.data?.userName || S.studentName || ""}`, "ok");
return S.examToken;
}
async function _eel() {
if (S.examToken) return S.examToken;
return examLogin();
}
async function getExamList() {
await _eel();
const res = await examRequest("GET", "/api/client/getExamList");
return Array.isArray(res?.data) ? res.data : [];
}
function mergeExamQuestionLists(primary, extra) {
const byId = new Map();
for (const q of primary || []) {
const id = String(q?.questionId || "").trim();
if (id) byId.set(id, q);
}
for (const q of extra || []) {
const id = String(q?.questionId || "").trim();
if (!id) continue;
const opts = Array.isArray(q.optionList) ? q.optionList : [];
if (!opts.length) continue;
if (!byId.has(id)) {
byId.set(id, q);
continue;
}
const cur = byId.get(id);
if (!cur.questionParent && q.questionParent) {
cur.questionParent = q.questionParent;
}
if (!cur._passage && q._passage) cur._passage = q._passage;
if (!cur.ruleId && q.ruleId) cur.ruleId = q.ruleId;
if (!cur.detailId && q.detailId) cur.detailId = q.detailId;
}
return Array.from(byId.values());
}
async function getExamPaper(subjectId, appointmentId) {
const res = await examRequest(
"GET",
`/api/client/getTestQuestion?subjectId=${encodeURIComponent(
subjectId
)}&appointmentId=${encodeURIComponent(appointmentId)}`
);
const raw = Array.isArray(res?.data) ? res.data : [];
return {
raw,
flat: normalizeExamQuestionList(raw),
};
}
async function getExamQuestions(subjectId, appointmentId) {
const paper = await getExamPaper(subjectId, appointmentId);
return paper.flat;
}
async function enrichExamQuestionsFromDetail(
questions,
historyId,
appointmentId
) {
let list = Array.isArray(questions) ? questions.slice() : [];
if (!historyId) return list;
try {
const detail = await getExamAnswerDetail(historyId, appointmentId);
const fromDetail = normalizeExamQuestionList(
Array.isArray(detail?.questionList) ? detail.questionList : []
);
if (fromDetail.length) {
const before = list.length;
list = mergeExamQuestionLists(list, fromDetail);
if (list.length > before) {
log(
`\u7b54\u5377\u5df2\u8865\u5168\u81f3 ${list.length} \u9898`,
"info"
);
}
}
} catch (_) {
}
return list;
}
function resolveExamAnswerIds(q, bag, cloudMap, { warnEmpty } = {}) {
let ans = pickExamAnswer(q, bag, cloudMap);
if (!ans.length) {
const qid = String(q.questionId || "");
const opts = Array.isArray(q.optionList) ? q.optionList : [];
const banned = new Set(getEliminatedOptionIds(q, bag));
if (!bag?.answers?.[qid]) {
for (const id of bag?.lastSubmit?.[qid] || []) banned.add(String(id));
}
const fallback = opts
.map((o) => String(o.optionId || ""))
.filter((id) => id && !banned.has(id));
if (fallback.length) {
ans =
Number(q.questionType) === 2 ? fallback.slice() : [fallback[0]];
if (warnEmpty) {
log(
`\u9898\u76ee\u5df2\u81ea\u52a8\u4f5c\u7b54\uff1a${stripExamHtml(q.questionTitle).slice(0, 40)}`,
"info"
);
}
} else if (opts.length) {
ans = [String(opts[0].optionId || "")].filter(Boolean);
}
}
return ans;
}
function makeExamSaveLeaf(q, ans, historyId) {
return {
ruleId: q.ruleId,
historyId: q.historyId || historyId,
questionId: q.questionId,
questionType: q.questionType,
detailId: q.detailId,
answer: ans,
subQuestion: [],
};
}
function buildExamAnswerListFromRaw(rawPaper, bag, cloudMap, historyId) {
const answerList = [];
let hit = 0;
let miss = 0;
let leafCount = 0;
if (!bag.lastSubmit || typeof bag.lastSubmit !== "object") bag.lastSubmit = {};
for (const q of rawPaper || []) {
if (!q || typeof q !== "object") continue;
const subs = examNestedSubs(q);
const isReadingParent = subs.length > 0 || Number(q.questionType) === 7;
if (isReadingParent) {
const subItems = [];
for (const sq of subs) {
leafCount += 1;
const child = Object.assign({}, sq, {
_passage: String(q.questionTitle || sq._passage || ""),
});
const ans = resolveExamAnswerIds(child, bag, cloudMap, {
warnEmpty: true,
});
if (ans.length) hit += 1;
else miss += 1;
if (ans.length) bag.lastSubmit[String(child.questionId)] = ans.slice();
subItems.push(makeExamSaveLeaf(child, ans, historyId));
}
answerList.push({
ruleId: q.ruleId,
historyId: q.historyId || historyId,
questionId: q.questionId,
questionType: q.questionType || 7,
detailId: q.detailId,
answer: [],
subQuestion: subItems,
});
} else {
leafCount += 1;
const ans = resolveExamAnswerIds(q, bag, cloudMap, { warnEmpty: true });
if (ans.length) hit += 1;
else miss += 1;
if (ans.length) bag.lastSubmit[String(q.questionId)] = ans.slice();
answerList.push(makeExamSaveLeaf(q, ans, historyId));
}
}
return { answerList, hit, miss, leafCount };
}
async function saveExamAnswerList(answerList) {
for (const item of answerList || []) {
try {
await saveExamOne(item);
} catch (e) {
log(`\u5355\u9898\u4fdd\u5b58\u5931\u8d25\uff1a${e.message || e}`, "warn");
}
if (S.stopFlag) return false;
}
return true;
}
function stripExamHtml(s) {
return String(s || "")
.replace(/<[^>]+>/g, " ")
.replace(/ /gi, " ")
.replace(/</gi, "<")
.replace(/>/gi, ">")
.replace(/&/gi, "&")
.replace(/"/gi, '"')
.replace(/\s+/g, " ")
.trim();
}
function examNestedSubs(q) {
if (!q || typeof q !== "object") return [];
const candidates = [
q.subQuestionList,
q.subQuestion,
q.subQuestions,
q.childList,
q.children,
q.questionList,
];
for (const c of candidates) {
if (Array.isArray(c) && c.length) return c;
}
return [];
}
function normalizeExamQuestionList(raw) {
const list = Array.isArray(raw) ? raw.slice() : [];
const byId = new Map();
const byDetail = new Map();
for (const q of list) {
const id = String(q?.questionId || "").trim();
const did = String(q?.detailId || "").trim();
if (id) byId.set(id, q);
if (did) byDetail.set(did, q);
}
const out = [];
const seen = new Set();
const pushOne = (q, passage) => {
if (!q || typeof q !== "object") return;
const qid = String(q.questionId || "").trim();
if (!qid || seen.has(qid)) return;
const opts = Array.isArray(q.optionList) ? q.optionList : [];
if (!opts.length) return; // \u7eaf\u6750\u6599\u7236\u9898\uff08type7\uff09\u65e0\u53ef\u9009\u9879\uff0c\u8df3\u8fc7
seen.add(qid);
const pid = String(q.questionParent || "").trim();
let pass = String(passage || q._passage || "").trim();
if (!pass && pid) {
const parent = byDetail.get(pid) || byId.get(pid);
if (parent) pass = String(parent.questionTitle || "");
}
out.push(Object.assign({}, q, { _passage: pass }));
};
const walk = (nodes, passage) => {
for (const q of nodes || []) {
if (!q || typeof q !== "object") continue;
const subs = examNestedSubs(q);
const opts = Array.isArray(q.optionList) ? q.optionList : [];
const title = String(q.questionTitle || "");
if (subs.length) {
const nextPass = opts.length ? passage : title || passage;
if (opts.length) pushOne(q, passage);
walk(subs, nextPass || title);
} else {
pushOne(q, passage);
}
}
};
walk(list, "");
if (!out.length && list.length) {
for (const q of list) pushOne(q, "");
}
return out;
}
function examQuestionStemForCloud(q) {
const passage = stripExamHtml(q._passage || "").slice(0, 2500);
const title = stripExamHtml(q.questionTitle || "");
if (passage) return `\u3010\u9605\u8bfb\u6750\u6599\u3011${passage}\n\u3010\u95ee\u9898\u3011${title}`.slice(0, 4000);
return title.slice(0, 4000);
}
async function synExamScoreState(appointmentId, subjectId, historyId) {
return examRequest("POST", "/api/client/synScoreState", {
appointmentId,
examType: 1,
historyId,
subjectId,
});
}
async function saveExamOne(item) {
return examRequest("POST", "/api/client/saveOneQuestionAnswer", [item]);
}
async function submitExamPaper(appointmentId, historyId, answerList) {
return examRequest("POST", "/api/client/saveQuestionAnswer", {
appointmentId,
historyId,
answerList,
submitState: 1,
});
}
async function getExamAnswerDetail(historyId, appointmentId) {
const res = await examRequest(
"GET",
`/api/client/getExamAnswerDetail?historyId=${encodeURIComponent(
historyId
)}&appointmentId=${encodeURIComponent(appointmentId)}`
);
return res?.data || {};
}
function parseExamAnswerField(ans) {
if (Array.isArray(ans)) return ans.map(String).filter(Boolean);
const s = String(ans || "").trim();
if (!s) return [];
return s.split(",").map((x) => x.trim()).filter(Boolean);
}
function hintLetterFromAnalysis(q) {
const blob = `${q?.questionAnalysis || ""} ${q?.optionList?.map((o) => o?.optionContent || "").join(" ") || ""}`;
const m =
blob.match(/\{?\s*\u6b63\u786e\u7b54\u6848\s*\}?\s*[:\uff1a\s]*([A-Da-d])\b/) ||
blob.match(/\u6b63\u786e\u7b54\u6848\s*[:\uff1a]\s*([A-Da-d])\b/);
return m ? String(m[1]).toUpperCase() : "";
}
function cleanExamOptionText(s) {
return stripExamHtml(s)
.replace(/\{\u8d44\u6599\u5e93\u5206\u7c7b\}[\s\S]*$/g, "")
.replace(/\{\u9898\u76ee\u89e3\u6790\}[\s\S]*$/g, "")
.replace(/\{\u6b63\u786e\u7b54\u6848\}[\s\S]*$/g, "")
.replace(/`std::`\u524d\u7f00\u3002?/g, "")
.trim();
}
function isExamQuestionCorrect(q) {
if (Number(q?.answerState) === 2) return false;
if (q?.isRight === true || Number(q?.isRight) === 1) return true;
if (q?.isRight === false || Number(q?.isRight) === 0) return false;
const score = Number(q?.answerScore);
const point = Number(q?.questionPoint);
if (Number.isFinite(point) && point > 0) {
if (!Number.isFinite(score)) return false;
return score + 1e-6 >= point;
}
if (Number(q?.answerState) === 1) {
if (Number.isFinite(score) && score <= 0) return false;
return true;
}
return false;
}
function otherOptionIds(q, usedIds) {
const used = new Set((usedIds || []).map(String));
return (Array.isArray(q?.optionList) ? q.optionList : [])
.map((o) => String(o.optionId || ""))
.filter((id) => id && !used.has(id));
}
function harvestExamDetail(detail, bag, paperQs) {
const next = {
answers: { ...(bag?.answers || {}) },
eliminated: { ...(bag?.eliminated || {}) },
lastSubmit: { ...(bag?.lastSubmit || {}) },
forcedNext: { ...(bag?.forcedNext || {}) },
};
const paperOpts = new Map();
for (const pq of paperQs || []) {
const id = String(pq?.questionId || "");
if (id && Array.isArray(pq.optionList) && pq.optionList.length) {
paperOpts.set(id, pq.optionList);
}
}
const upsert = [];
const wrongTitles = [];
const list = Array.isArray(detail?.questionList) ? detail.questionList : [];
for (const q of list) {
const qid = String(q.questionId || "");
if (!qid) continue;
const submitted = Array.isArray(next.lastSubmit[qid])
? next.lastSubmit[qid].map(String).filter(Boolean)
: [];
let detailIds = parseExamAnswerField(q.answer);
const point = Number(q.questionPoint);
const score = Number(q.answerScore);
const correct = isExamQuestionCorrect(q);
const clearlyWrong =
Number(q.answerState) === 2 ||
(Number.isFinite(point) &&
point > 0 &&
Number.isFinite(score) &&
score + 1e-6 < point);
let ids = clearlyWrong && submitted.length ? submitted.slice() : detailIds;
if (!ids.length && submitted.length) ids = submitted.slice();
if (correct && (detailIds.length || ids.length)) {
const useIds = detailIds.length ? detailIds : ids;
const already = !!bag?.answers?.[qid];
next.answers[qid] = useIds.join(",");
delete next.eliminated[qid];
delete next.forcedNext[qid];
if (already) continue;
const opts =
(Array.isArray(q.optionList) && q.optionList.length
? q.optionList
: paperOpts.get(qid)) || [];
const idset = new Set(useIds.map(String));
const texts = [];
const letters = [];
opts.forEach((o, i) => {
if (idset.has(String(o.optionId || ""))) {
texts.push(cleanExamOptionText(o.optionContent || ""));
letters.push(optionLetter(o, i));
}
});
if (texts.length || letters.length) {
upsert.push({
stem: stripExamHtml(q.questionTitle || ""),
answer_text: texts.join("|||"),
answer_letter: letters.join(""),
paper_name: String(detail.subjectName || ""),
source: "ytccr_exam_ok",
});
}
} else if (clearlyWrong || (!correct && ids.length)) {
delete next.answers[qid];
const qtype = Number(q.questionType);
const opts =
(Array.isArray(q.optionList) && q.optionList.length
? q.optionList
: paperOpts.get(qid)) || [];
const tried = (submitted.length ? submitted : ids).map(String);
const title = stripExamHtml(q.questionTitle || qid).slice(0, 36);
const qForRest = { optionList: opts };
if (qtype === 2) {
const key = tried.slice().sort().join(",");
const arr = Array.isArray(next.eliminated[qid])
? next.eliminated[qid]
: [];
if (key && !arr.includes(key)) arr.push(key);
next.eliminated[qid] = arr;
wrongTitles.push(title);
} else {
next.eliminated[qid] = tried.slice();
const rest = otherOptionIds(qForRest, tried);
const nextId = String(rest[0] || "");
if (nextId) {
next.forcedNext[qid] = nextId;
wrongTitles.push(`${title}\uff08\u5df2\u6362\u9009\u9879\uff09`);
} else {
next.eliminated[qid] = [];
delete next.forcedNext[qid];
wrongTitles.push(`${title}`);
}
}
}
}
if (wrongTitles.length) {
log(
`\u672c\u8f6e\u6709 ${wrongTitles.length} \u9898\u672a\u6ee1\u5206\uff0c\u5c06\u7ee7\u7eed\u91cd\u8bd5\uff1a${wrongTitles[0]}${
wrongTitles.length > 1 ? "…" : ""
}`,
"warn"
);
}
next._upsert = upsert;
return next;
}
function examTarget() {
const t = Number(S.examTargetScore || EXAM_TARGET_SCORE);
return Number.isFinite(t) && t > 0 ? t : EXAM_TARGET_SCORE;
}
function canUseExamFeature() {
if (isCloudProTier()) return true;
return !!S.freeAllowsExam;
}
async function _ces(questions, subjectName, subjectId, bag) {
const known = bag?.answers || {};
const localFilled = {};
const needCloud = [];
let multiProbe = 0;
for (const q of questions) {
const qid = String(q.questionId || "");
if (bag?.forcedNext?.[qid]) {
localFilled[qid] = {
optionIds: [String(bag.forcedNext[qid])],
source: "forced_next",
};
continue;
}
if (known[qid]) continue;
if (Number(q.questionType) === 2) {
const multi = nextMultiCombo(q, bag);
if (multi && multi.length) {
localFilled[qid] = { optionIds: multi, source: "multi_probe" };
multiProbe += 1;
continue;
}
}
const forced = forceAnswerByElimination(q, bag);
if (forced && forced.length) {
localFilled[qid] = { optionIds: forced, source: "elim" };
continue;
}
needCloud.push(q);
}
if (multiProbe) {
}
if (!needCloud.length) {
return {
answers: localFilled,
stats: {
total: Object.keys(localFilled).length,
qbank: 0,
ai: 0,
miss: 0,
elim: Object.keys(localFilled).length,
},
};
}
await _cl(false);
const merged = {
answers: { ...localFilled },
stats: {
total: needCloud.length + Object.keys(localFilled).length,
qbank: 0,
ai: 0,
miss: 0,
elim: Object.keys(localFilled).length,
},
ai_error: "",
ai_ready: true,
model: "",
target_score: 0,
upserted: 0,
};
if (Object.keys(localFilled).length) {
}
const batchSize = policyNum("cloudExamBatch", CLOUD_EXAM_BATCH) || CLOUD_EXAM_BATCH;
const batches = Math.ceil(needCloud.length / batchSize);
if (needCloud.length) {
log(`\u6b63\u5728\u7b54\u9898\uff08${needCloud.length} \u9898\uff09…`, "info");
}
for (let bi = 0; bi < batches; bi++) {
if (S.stopFlag) break;
const slice = needCloud.slice(bi * batchSize, (bi + 1) * batchSize);
let data = null;
let lastErr = null;
for (let attempt = 1; attempt <= 2; attempt++) {
try {
data = await _crq(
"/api/ytccr/exam/solve",
"POST",
{
lease: S.cloudLease,
subject_id: subjectId || "",
subject_name: subjectName || "",
allow_ai: true,
questions: slice.map((q) => ({
questionId: String(q.questionId || ""),
questionTitle: examQuestionStemForCloud(q),
questionType: Number(q.questionType || 1),
eliminatedOptionIds: getEliminatedOptionIds(q, bag),
eliminatedLetters: getEliminatedLetters(q, bag),
optionList: (Array.isArray(q.optionList) ? q.optionList : []).map(
(o) => ({
optionId: String(o.optionId || ""),
optionContent: cleanExamOptionText(o.optionContent || ""),
optionNo: o.optionNo,
})
),
})),
},
null,
CLOUD_EXAM_BATCH_TIMEOUT_MS
);
lastErr = null;
break;
} catch (e) {
lastErr = e;
const msg = String(e.message || e);
if (attempt < 2 && /504|502|\u8d85\u65f6|timeout/i.test(msg)) {
log(`\u7b54\u9898\u8d85\u65f6\uff0c\u91cd\u8bd5\u4e2d…`, "warn");
await _cl(true);
await sleep(800);
continue;
}
throw e;
}
}
if (lastErr) throw lastErr;
const ans = data?.answers || {};
for (const [qid, pack] of Object.entries(ans)) {
const q = slice.find((x) => String(x.questionId) === String(qid));
const ids = Array.isArray(pack?.optionIds) ? pack.optionIds.map(String) : [];
const cleaned = q ? sanitizeExamAnswer(q, bag, ids) : ids;
merged.answers[qid] = {
optionIds: cleaned,
source: pack?.source || "ai",
};
}
for (const q of slice) {
const qid = String(q.questionId || "");
if (!qid) continue;
const cur = merged.answers[qid];
if (cur && Array.isArray(cur.optionIds) && cur.optionIds.length) continue;
if (bag?.forcedNext?.[qid]) {
merged.answers[qid] = {
optionIds: [String(bag.forcedNext[qid])],
source: "forced_next",
};
continue;
}
const forced = forceAnswerByElimination(q, bag);
if (forced && forced.length) {
merged.answers[qid] = { optionIds: forced, source: "elim" };
continue;
}
const opts = Array.isArray(q.optionList) ? q.optionList : [];
let banned = new Set(getEliminatedOptionIds(q, bag));
let pool = opts
.map((o) => String(o.optionId || ""))
.filter((id) => id && !banned.has(id));
if (!pool.length && opts.length) {
delete bag.eliminated[qid];
if (bag.lastSubmit) delete bag.lastSubmit[qid];
banned = new Set();
pool = opts.map((o) => String(o.optionId || "")).filter(Boolean);
log(`\u5df2\u81ea\u52a8\u8c03\u6574\u9009\u9879\uff1a${stripExamHtml(q.questionTitle).slice(0, 36)}`, "info");
}
if (pool.length) {
merged.answers[qid] = {
optionIds:
Number(q.questionType) === 2 ? pool.slice() : [pool[0]],
source: "client_fallback",
};
}
}
const st = data?.stats || {};
let batchMiss = 0;
let batchAi = Number(st.ai || 0);
for (const q of slice) {
const qid = String(q.questionId || "");
const pack = merged.answers[qid];
if (!pack?.optionIds?.length) batchMiss += 1;
else if (pack.source === "client_fallback" || pack.source === "elim") {
}
}
merged.stats.qbank += Number(st.qbank || 0);
merged.stats.ai += batchAi;
merged.stats.miss += batchMiss;
merged.upserted += Number(data?.upserted || 0);
if (data?.model) merged.model = String(data.model);
if (data?.target_score) merged.target_score = Number(data.target_score);
if (data?.ai_ready === false) merged.ai_ready = false;
const err = String(data?.ai_error || st.ai_error || "").trim();
if (err) {
merged.ai_error = merged.ai_error
? `${merged.ai_error} | ${err}`
: err;
log(`\u7b54\u9898\u670d\u52a1\u5f02\u5e38\uff1a${_sue(err)}`, "warn");
}
log(
`\u7b54\u9898\u8fdb\u5ea6 ${bi + 1}/${batches}`,
batchMiss ? "warn" : "ok"
);
if (bi + 1 < batches) await sleep(400);
}
return merged;
}
async function cloudQbankUpsert(items) {
const list = (items || []).filter((it) => String(it.stem || "").trim());
if (!list.length) return;
try {
await _cl(false);
await _crq("/api/ytccr/qbank/upsert", "POST", {
lease: S.cloudLease,
items: list.slice(0, 200),
});
} catch (e) {
log(`\u7b54\u6848\u540c\u6b65\u5931\u8d25\uff1a${_sue(e)}`, "warn");
}
}
function optionLetter(o, idx) {
const no = Number(o?.optionNo);
if (Number.isFinite(no) && no >= 1) return String.fromCharCode(64 + no);
return String.fromCharCode(65 + Math.max(0, idx));
}
function comboKey(ids) {
return (ids || [])
.map(String)
.filter(Boolean)
.slice()
.sort()
.join(",");
}
function listMultiComboCandidates(allIds) {
const ids = (allIds || []).map(String).filter(Boolean);
const n = ids.length;
const out = [];
const push = (arr) => {
if (!arr.length) return;
const k = comboKey(arr);
if (!out.some((x) => comboKey(x) === k)) out.push(arr.slice());
};
if (!n) return out;
push(ids.slice());
for (let i = 0; i < n; i++) push(ids.filter((_, j) => j !== i));
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
push(ids.filter((_, k) => k !== i && k !== j));
}
}
for (const id of ids) push([id]);
if (n >= 5) {
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
for (let k = j + 1; k < n; k++) {
push(ids.filter((_, t) => t !== i && t !== j && t !== k));
}
}
}
}
return out;
}
function nextMultiCombo(q, bag) {
const opts = Array.isArray(q.optionList) ? q.optionList : [];
const allIds = opts.map((o) => String(o.optionId || "")).filter(Boolean);
if (!allIds.length) return null;
const elim = new Set(
(Array.isArray(bag?.eliminated?.[String(q.questionId || "")])
? bag.eliminated[String(q.questionId)]
: []
).map(String)
);
const cands = listMultiComboCandidates(allIds);
for (const combo of cands) {
const k = comboKey(combo);
if (!elim.has(k)) return combo;
}
return null;
}
function getEliminatedOptionIds(q, bag) {
const qid = String(q.questionId || "");
const elim = Array.isArray(bag?.eliminated?.[qid]) ? bag.eliminated[qid] : [];
const qtype = Number(q.questionType);
if (qtype === 2) return [];
const out = elim.map(String).filter(Boolean);
if (!bag?.answers?.[qid]) {
for (const id of bag?.lastSubmit?.[qid] || []) {
const s = String(id || "");
if (s && !out.includes(s)) out.push(s);
}
}
return out;
}
function getEliminatedLetters(q, bag) {
const opts = Array.isArray(q.optionList) ? q.optionList : [];
const ids = new Set(getEliminatedOptionIds(q, bag));
if (!ids.size) return "";
return opts
.map((o, i) => (ids.has(String(o.optionId || "")) ? optionLetter(o, i) : ""))
.filter(Boolean)
.join("");
}
function getEliminatedCombos(q, bag) {
const qid = String(q.questionId || "");
const qtype = Number(q.questionType);
if (qtype !== 2) return [];
return Array.isArray(bag?.eliminated?.[qid])
? bag.eliminated[qid].map(String)
: [];
}
function forceAnswerByElimination(q, bag) {
const qtype = Number(q.questionType);
if (qtype === 2) return nextMultiCombo(q, bag);
const opts = Array.isArray(q.optionList) ? q.optionList : [];
const allIds = opts.map((o) => String(o.optionId || "")).filter(Boolean);
const banned = new Set(getEliminatedOptionIds(q, bag));
if (!banned.size) return null;
const pool = allIds.filter((id) => !banned.has(id));
if (pool.length === 1) return pool;
if (pool.length > 1 && (qtype === 3 || allIds.length <= 4)) return [pool[0]];
return null;
}
function letterToOptionIds(q, letters) {
const want = new Set(
String(letters || "")
.toUpperCase()
.split("")
.filter((c) => c >= "A" && c <= "Z")
);
if (!want.size) return [];
const opts = Array.isArray(q.optionList) ? q.optionList : [];
const ids = [];
opts.forEach((o, i) => {
if (want.has(optionLetter(o, i))) {
const id = String(o.optionId || "");
if (id) ids.push(id);
}
});
return ids;
}
function sanitizeExamAnswer(q, bag, ids) {
const qid = String(q.questionId || "");
const qtype = Number(q.questionType);
const elim = Array.isArray(bag?.eliminated?.[qid]) ? bag.eliminated[qid] : [];
let pick = (ids || []).map(String).filter(Boolean);
if (qtype === 2) {
if (pick.length) {
const key = comboKey(pick);
if (!elim.map(String).includes(key)) return pick;
}
return nextMultiCombo(q, bag) || [];
}
if (!pick.length) {
const forced = forceAnswerByElimination(q, bag);
return forced || [];
}
const banned = new Set(elim.map(String));
if (pick.some((id) => banned.has(id))) {
const forced = forceAnswerByElimination(q, bag);
if (forced) return forced;
const allIds = (q.optionList || [])
.map((o) => String(o.optionId || ""))
.filter(Boolean);
const pool = allIds.filter((id) => !banned.has(id));
return pool.length ? [pool[0]] : [];
}
return pick;
}
function pickExamAnswer(q, bag, cloudMap) {
const qid = String(q.questionId || "");
const forcedId = bag?.forcedNext?.[qid];
if (forcedId) {
const ok = (q.optionList || []).some(
(o) => String(o.optionId || "") === String(forcedId)
);
if (ok) return [String(forcedId)];
}
const known = bag?.answers?.[qid];
if (known) return parseExamAnswerField(known);
if (Number(q.questionType) === 2) {
const multi = nextMultiCombo(q, bag);
if (multi && multi.length) return multi;
}
const forced = forceAnswerByElimination(q, bag);
if (forced) return forced;
const cloud = cloudMap && cloudMap[qid];
if (cloud && Array.isArray(cloud.optionIds) && cloud.optionIds.length) {
return sanitizeExamAnswer(q, bag, cloud.optionIds.map(String));
}
const dirtyOpts = (q.optionList || []).some((o) =>
/\{\u8d44\u6599\u5e93\u5206\u7c7b\}|\{\u9898\u76ee\u89e3\u6790\}|\{\u6b63\u786e\u7b54\u6848\}/.test(String(o?.optionContent || ""))
);
const hint = dirtyOpts ? "" : hintLetterFromAnalysis(q);
if (hint) {
const ids = letterToOptionIds(q, hint);
const cleaned = sanitizeExamAnswer(q, bag, ids);
if (cleaned.length) return cleaned;
}
return [];
}
function normalizeExamName(s) {
return String(s || "")
.replace(/\uff08/g, "(")
.replace(/\uff09/g, ")")
.replace(/\s+/g, "")
.toLowerCase();
}
function examNameMatchesCourse(examName, courseName) {
const a = normalizeExamName(examName);
const b = normalizeExamName(courseName);
if (!a || !b) return false;
return a === b || a.includes(b) || b.includes(a);
}
function findExamsForCourse(course, examList) {
const cname = course?.courseName || "";
return (examList || []).filter((ex) =>
examNameMatchesCourse(ex.subjectName || ex.planName || "", cname)
);
}
async function _eec(force) {
if (!force && Array.isArray(S.examListCache)) return S.examListCache;
await _eel();
S.examListCache = await getExamList();
return S.examListCache;
}
function examNeedsWork(exam) {
const ts = Number(exam.timeState);
if (Number.isFinite(ts) && ts !== 0 && ts !== 3) {
}
return true;
}
async function _sce(course) {
if (!S.autoExam) {
return;
}
if (!canUseExamFeature()) {
log(`\u8003\u8bd5\u9700\u4e13\u4e1a\u7248\uff0c\u8df3\u8fc7\uff1a${course.courseName}`, "warn");
return;
}
let list = [];
try {
list = await _eec(false);
} catch (e) {
log(`\u62c9\u53d6\u8003\u8bd5\u5217\u8868\u5931\u8d25\uff1a${e.message || e}`, "warn");
return;
}
const exams = findExamsForCourse(course, list).filter(examNeedsWork);
if (!exams.length) {
log(`\u672c\u8bfe\u65e0\u5bf9\u5e94\u8003\u8bd5\u79d1\u76ee\uff1a${course.courseName}`, "info");
return;
}
for (const exam of exams) {
if (S.stopFlag) break;
const name = exam.subjectName || exam.planName || "\u8003\u8bd5";
try {
log(`\u672c\u8bfe\u8003\u8bd5\uff1a${name}`);
await _soe(exam);
} catch (e) {
log(`\u8003\u8bd5\u5931\u8d25\uff1a${name} · ${_sue(e)}`, "err");
}
await sleep(EXAM_GAP_MS);
}
}
async function _soe(exam) {
const name = String(exam.subjectName || exam.planName || "\u8003\u8bd5");
const appointmentId = String(exam.appointmentId || "");
const subjectId = String(exam.subjectId || "");
if (!appointmentId || !subjectId) throw new Error("\u8003\u8bd5\u7f3a\u5c11 appointmentId/subjectId");
S.currentAct = name;
S.task = "\u8003\u8bd5";
renderRunStatus();
const allMap = loadExamBag();
const key = examBagKey(subjectId);
let bag = allMap[key] || {
answers: {},
eliminated: {},
lastSubmit: {},
forcedNext: {},
};
if (!bag.answers) bag.answers = {};
if (!bag.eliminated) bag.eliminated = {};
if (!bag.lastSubmit) bag.lastSubmit = {};
if (!bag.forcedNext) bag.forcedNext = {};
const target = examTarget();
let lastScore = -1;
let bestScore = -1;
let stuckRounds = 0;
let bothSidesTried = false;
for (let round = 1; round <= (policyNum("examMaxRounds", EXAM_MAX_ROUNDS) || EXAM_MAX_ROUNDS); round++) {
if (S.stopFlag) return false;
let paper = await getExamPaper(subjectId, appointmentId);
let rawPaper = paper.raw || [];
let questions = paper.flat || [];
if (!questions.length && !rawPaper.length) {
log(`\u8003\u8bd5\u65e0\u9898\u76ee\uff1a${name}`, "warn");
return false;
}
for (const q of questions) {
const qid = String(q.questionId || "");
const n = (q.optionList || []).length;
const elim = bag.eliminated?.[qid];
if (qid && n && Array.isArray(elim) && elim.length >= n && Number(q.questionType) !== 2) {
delete bag.eliminated[qid];
delete bag.lastSubmit[qid];
log(`\u5df2\u91cd\u7f6e\u7b54\u9898\u8bb0\u5f55\uff1a${stripExamHtml(q.questionTitle).slice(0, 36)}`, "info");
}
}
const historyId = String(
questions[0]?.historyId ||
rawPaper[0]?.historyId ||
questions.find((x) => x.historyId)?.historyId ||
rawPaper.find((x) => x.historyId)?.historyId ||
""
);
if (!historyId) throw new Error("\u672a\u83b7\u53d6 historyId");
await synExamScoreState(appointmentId, subjectId, historyId);
questions = await enrichExamQuestionsFromDetail(
questions,
historyId,
appointmentId
);
const readingN = questions.filter(
(q) => q._passage || q.questionParent
).length;
if (round === 1) {
log(
`\u8bd5\u5377 ${questions.length} \u9898`,
"info"
);
}
let cloudMap = {};
let stats = { qbank: 0, ai: 0, miss: 0 };
try {
const solved = await _ces(questions, name, subjectId, bag);
cloudMap = solved.answers || {};
stats = solved.stats || stats;
const aiErr = String(solved.ai_error || stats.ai_error || "").trim();
if (solved.ai_ready === false || /\u4e91\u7aef_not_configured|\u4e91\u7aef_disabled/i.test(aiErr)) {
log("\u7b54\u9898\u670d\u52a1\u672a\u5c31\u7eea\uff0c\u8bf7\u8054\u7cfb\u4f5c\u8005", "err");
}
if (solved.target_score) {
const t = Number(solved.target_score);
if (Number.isFinite(t) && t > 0) S.examTargetScore = t;
}
for (const [qid, pack] of Object.entries(cloudMap)) {
const ids = Array.isArray(pack?.optionIds) ? pack.optionIds : [];
const src = String(pack?.source || "");
if (ids.length && src === "qbank") {
bag.answers[qid] = ids.map(String).join(",");
}
}
} catch (e) {
const msg = String(e.message || e);
if (/exam_requires_pro|pro_only/i.test(msg)) {
throw new Error("\u8003\u8bd5\u9700\u4e13\u4e1a\u7248");
}
throw e;
}
const built = buildExamAnswerListFromRaw(
rawPaper,
bag,
cloudMap,
historyId
);
const answerList = built.answerList;
const hit = built.hit;
const miss = built.miss;
if (!(await saveExamAnswerList(answerList))) return false;
log(
`\u8003\u8bd5\u7b2c ${round} \u8f6e\uff1a${name} · \u4f5c\u7b54 ${hit}/${built.leafCount || questions.length}`,
miss ? "warn" : "info"
);
if (hit === 0) {
log("\u672c\u8f6e\u672a\u80fd\u4f5c\u7b54\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u6216\u8054\u7cfb\u4f5c\u8005", "err");
}
await submitExamPaper(appointmentId, historyId, answerList);
await sleep(EXAM_GAP_MS);
let detail = {};
try {
detail = await getExamAnswerDetail(historyId, appointmentId);
const pending = (Array.isArray(detail.questionList)
? detail.questionList
: []
).filter(
(q) =>
Number(q.answerState) === 0 &&
Array.isArray(q.optionList) &&
q.optionList.length
);
if (pending.length && !S.stopFlag) {
log(
`\u53d1\u73b0\u672a\u4f5c\u7b54 ${pending.length} \u9898\uff0c\u6b63\u5728\u8865\u4ea4…`,
"info"
);
let patchQs = normalizeExamQuestionList(pending);
let patchCloud = {};
let patchStats = { qbank: 0, ai: 0, miss: 0 };
try {
const solved2 = await _ces(
patchQs,
name,
subjectId,
bag
);
patchCloud = solved2.answers || {};
patchStats = solved2.stats || patchStats;
for (const [qid, pack] of Object.entries(patchCloud)) {
const ids = Array.isArray(pack?.optionIds) ? pack.optionIds : [];
if (ids.length && String(pack?.source || "") === "qbank") {
bag.answers[qid] = ids.map(String).join(",");
}
}
} catch (e) {
log(`\u8865\u4ea4\u5931\u8d25\uff1a${_sue(e)}`, "warn");
}
const mergedCloud = Object.assign({}, cloudMap, patchCloud);
const rebuilt = buildExamAnswerListFromRaw(
rawPaper,
bag,
mergedCloud,
historyId
);
if (rebuilt.answerList.length) {
if (!(await saveExamAnswerList(rebuilt.answerList))) return false;
log(
`\u8865\u4ea4\u5b8c\u6210 ${rebuilt.hit}/${rebuilt.leafCount}`,
"info"
);
await submitExamPaper(
appointmentId,
historyId,
rebuilt.answerList
);
await sleep(EXAM_GAP_MS);
detail = await getExamAnswerDetail(historyId, appointmentId);
}
}
bag = harvestExamDetail(detail, bag, questions);
allMap[key] = {
answers: bag.answers,
eliminated: bag.eliminated,
lastSubmit: bag.lastSubmit || {},
forcedNext: bag.forcedNext || {},
};
saveExamBag(allMap);
lastScore = Number(detail.examScore ?? detail.testPoint ?? lastScore);
if (Array.isArray(bag._upsert) && bag._upsert.length) {
log(`\u5df2\u8bb0\u5f55 ${bag._upsert.length} \u9898\u6b63\u786e\u7b54`, "ok");
await cloudQbankUpsert(bag._upsert);
}
} catch (e) {
log(`\u8003\u8bd5\u67e5\u7b54\u6848\u5931\u8d25\uff1a${_sue(e)}`, "warn");
}
const passMark = Number(detail.passMark ?? exam.passMark ?? 60);
const tgt = examTarget();
log(
`\u8003\u8bd5\u5f97\u5206\uff1a${lastScore}\uff08\u53ca\u683c\u7ebf ${passMark}\uff0c\u76ee\u6807 ${tgt}\uff09`,
lastScore >= tgt ? "ok" : "info"
);
if (lastScore >= tgt) {
log(`\u8003\u8bd5\u5b8c\u6210\uff1a${name}`, "ok");
return true;
}
for (const q of questions) {
if (Number(q.questionType) === 2) continue;
const qid = String(q.questionId || "");
const opts = Array.isArray(q.optionList) ? q.optionList : [];
if (opts.length !== 2) continue;
const elim = new Set(
(Array.isArray(bag.eliminated?.[qid]) ? bag.eliminated[qid] : []).map(
String
)
);
const last = new Set(
(Array.isArray(bag.lastSubmit?.[qid]) ? bag.lastSubmit[qid] : []).map(
String
)
);
const hist = bag._binaryTried?.[qid] || [];
const merged = new Set([...hist, ...elim, ...last]);
for (const id of merged) {
if (!hist.includes(id)) hist.push(id);
}
if (!bag._binaryTried) bag._binaryTried = {};
bag._binaryTried[qid] = hist;
if (hist.length >= 2 && !bag.answers?.[qid]) {
bothSidesTried = true;
log(
`\u5224\u65ad\u9898\u5f02\u5e38\uff08\u9009\u9879\u5747\u65e0\u6cd5\u5f97\u5206\uff09\uff1a${stripExamHtml(q.questionTitle).slice(0, 40)}`,
"warn"
);
}
}
if (bothSidesTried) {
log(
`\u9898\u76ee\u5f02\u5e38\u65e0\u6cd5\u7ee7\u7eed\u63d0\u5206\uff08\u5f53\u524d ${lastScore}/${tgt}\uff09\uff0c\u5df2\u8df3\u8fc7\uff1a${name}`,
"warn"
);
return lastScore >= passMark;
}
if (lastScore > bestScore) {
bestScore = lastScore;
stuckRounds = 0;
} else if (Number.isFinite(lastScore) && lastScore >= 0) {
stuckRounds += 1;
const stuckLimit = policyNum("examStuckRounds", EXAM_STUCK_ROUNDS) || EXAM_STUCK_ROUNDS;
if (stuckRounds >= stuckLimit) {
log(
`\u8fde\u7eed ${stuckLimit} \u8f6e\u5206\u6570\u672a\u63d0\u5347\uff08${lastScore}\uff09\uff0c\u5df2\u8df3\u8fc7\uff1a${name}`,
"warn"
);
return lastScore >= passMark;
}
}
await sleep(policyNum("examGapMs", EXAM_GAP_MS));
}
log(`\u8003\u8bd5\u672a\u8fbe\u76ee\u6807\u5206\uff08${lastScore}/${examTarget()}\uff09\uff1a${name}`, "warn");
return false;
}
async function _req() {
if (S.running) return;
S.running = true;
S.stopFlag = false;
S.panelHint = "";
S.queueDone = 0;
renderRunStatus();
try {
await _cl(false);
const selected = (S.courses || []).filter(isCourseSelected);
const planData = await _fop("exam_only", selected);
if (!canUseExamFeature() || !(S.runPolicy.allowExam || canUseExamFeature())) {
throw new Error("\u8003\u8bd5\u9700\u4e13\u4e1a\u7248");
}
const list = await _eec(true);
let queue = (list || []).filter(examNeedsWork);
if (selected.length) {
const matched = [];
for (const course of selected) {
for (const ex of findExamsForCourse(course, queue)) {
if (!matched.some((m) => String(m.appointmentId) === String(ex.appointmentId))) {
matched.push(ex);
}
}
}
if (!matched.length) {
log("\u52fe\u9009\u8bfe\u7a0b\u6ca1\u6709\u5bf9\u5e94\u8003\u8bd5\u79d1\u76ee", "warn");
return;
}
queue = matched;
log(`\u6309\u52fe\u9009\u8bfe\u7a0b\u5339\u914d\u8003\u8bd5 ${queue.length} \u95e8`);
}
if (!queue.length) {
log("\u8003\u8bd5\u5217\u8868\u4e3a\u7a7a", "warn");
return;
}
S.queueTotal = queue.length;
renderRunStatus();
log(`\u8003\u8bd5\u79d1\u76ee ${queue.length} \u95e8\uff08\u76ee\u6807 ${examTarget()} \u5206\uff09`);
const examGap = policyNum("examGapMs", EXAM_GAP_MS);
for (const exam of queue) {
if (S.stopFlag) break;
const name = exam.subjectName || exam.planName || "\u8003\u8bd5";
try {
await _soe(exam);
} catch (e) {
log(`\u8003\u8bd5\u5931\u8d25\uff1a${name} · ${_sue(e)}`, "err");
}
S.queueDone += 1;
renderRunStatus();
await sleep(examGap);
}
log(S.stopFlag ? "\u8003\u8bd5\u5df2\u505c\u6b62" : "\u8003\u8bd5\u961f\u5217\u5904\u7406\u5b8c\u6210", S.stopFlag ? "warn" : "ok");
} catch (e) {
log(`\u8003\u8bd5\u8fd0\u884c\u5931\u8d25\uff1a${_sue(e)}`, "err");
S.panelHint = _sue(e);
} finally {
S.running = false;
S.task = "\u5f85\u673a";
S.currentAct = "";
renderRunStatus();
}
}
async function _soa(course, act) {
if (S.stopFlag) return false;
if (isActDone(act)) {
log(`\u8df3\u8fc7\u5df2\u5b8c\u6210\uff1a${act.actName}`, "info");
return true;
}
S.currentAct = act.actName;
renderRunStatus();
log(`\u5b66\u4e60\uff1a${act.actName}`);
const [, info] = await Promise.all([
addStudyTimeCache(act.actId, course.studentChooseId),
queryVideo(act.actId, course.studentChooseId),
]);
const duration = Math.max(
1,
Math.floor(Number(info.videoTime) || Number(act.duration) || 60)
);
const midCount = policyNum("effMidCount", EFF_MID_COUNT);
const effGap = policyNum("effGapMs", EFF_GAP_MS);
const mids = [];
if (duration > 30 && midCount > 0) {
for (let i = 1; i <= midCount; i++) {
const t = Math.floor((duration * i) / (midCount + 1));
if (t > 0 && t < duration) mids.push(t);
}
}
for (const t of mids) {
if (S.stopFlag) return false;
await saveVideoProgress(
act.actId,
course.studentChooseId,
t,
duration
);
await sleep(effGap);
}
if (S.stopFlag) return false;
await saveVideoProgress(
act.actId,
course.studentChooseId,
duration,
duration
);
log(`\u5b8c\u6210\uff1a${act.actName}\uff08${duration}s\uff09`, "ok");
return true;
}
async function _sc(course, steps) {
if (S.stopFlag) return;
const cname = course.courseName || "";
const stepSet = new Set(
(Array.isArray(steps) && steps.length
? steps
: ["video", "homework", "exam"]
).map(String)
);
S.activeCourses.add(cname);
refreshActiveCourseLabel();
S.task = S.activeCourses.size > 1 ? `\u5b66\u4e60 ${S.activeCourses.size} \u95e8` : "\u5b66\u4e60\u8bfe\u7a0b";
renderRunStatus();
log(`\u5f00\u59cb\u8bfe\u7a0b\uff1a${cname}`);
try {
try {
await _ccq(course);
} catch (e) {
const msg = String(e?.message || e);
if (/free_course_quota_exceeded|quota/i.test(msg)) {
S.panelHint = "\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5f00\u901a\u4e13\u4e1a\u7248";
openProHint("\u514d\u8d39\u989d\u5ea6\u4ec5\u652f\u6301 1 \u95e8\u8bfe\uff0c\u5f00\u901a\u4e13\u4e1a\u7248\u540e\u53ef\u5b66\u4e60\u5168\u90e8\u8bfe\u7a0b\u3002");
}
throw e;
}
const { videos, homeworks } = await loadChapterActs(course);
const todoVideos = videos.filter((a) => !isActDone(a));
log(
`[${cname}] \u89c6\u9891 ${videos.length}\uff08\u5f85\u5b66 ${todoVideos.length}\uff09· \u4f5c\u4e1a ${homeworks.length}`,
todoVideos.length || homeworks.length ? "info" : "ok"
);
if (stepSet.has("video")) {
const actGap = policyNum("actGapMs", ACT_GAP_MS);
for (const act of todoVideos) {
if (S.stopFlag) break;
await _soa(course, act);
await sleep(actGap);
}
}
if (stepSet.has("homework") && S.autoHomework) {
const hwGap = policyNum("hwGapMs", HW_GAP_MS);
for (const hw of homeworks) {
if (S.stopFlag) break;
try {
await _soh(course, hw);
} catch (e) {
log(`\u4f5c\u4e1a\u5931\u8d25\uff1a${hw.actName} · ${e.message || e}`, "err");
}
await sleep(hwGap);
}
}
if (!S.stopFlag && stepSet.has("exam") && S.autoExam) {
await withExamLock(async () => {
if (S.stopFlag) return;
await _sce(course);
});
}
} finally {
S.activeCourses.delete(cname);
refreshActiveCourseLabel();
S.task = S.activeCourses.size > 1 ? `\u5b66\u4e60 ${S.activeCourses.size} \u95e8` : S.running ? "\u5b66\u4e60\u8bfe\u7a0b" : "\u5f85\u673a";
renderRunStatus();
}
}
async function refreshAll() {
S.task = "\u5237\u65b0\u6570\u636e";
renderRunStatus();
if (!getPlatformToken()) {
log("\u672a\u68c0\u6d4b\u5230\u767b\u5f55\uff0c\u8bf7\u5148\u767b\u5f55\u5e73\u53f0", "warn");
showLoginGuide("\u8bf7\u5148\u767b\u5f55\u5e73\u53f0");
return;
}
if (isLikelyLoginPage()) {
log("\u5f53\u524d\u4e3a\u767b\u5f55\u9875\uff0c\u8bf7\u5148\u5b8c\u6210\u767b\u5f55", "warn");
showLoginGuide("\u5f53\u524d\u4e3a\u767b\u5f55\u9875\uff0c\u8bf7\u5148\u5b8c\u6210\u767b\u5f55");
return;
}
try {
await ensureClientBootstrap(true);
await loadMajorInfoSafe();
await loadTerms();
await loadCourses();
disarmLoginWatcher();
renderProfile();
renderTermSelect();
renderCourseList();
try {
await _cl(true);
} catch (e) {
log(`\u6388\u6743\u6821\u9a8c\uff1a${_sue(e)}`, "warn");
S.cloudTier = S.cloudToken ? "unknown" : "free";
refreshCloudUi();
}
log(
`\u5df2\u52a0\u8f7d ${S.courses.length} \u95e8\u8bfe · ${S.collegeOrgName || ""} ${
S.majorName || ""
}`,
"ok"
);
S.panelHint = `\u5df2\u52a0\u8f7d ${S.courses.length} \u95e8\u8bfe`;
S.task = "\u5f85\u673a";
renderRunStatus();
} catch (e) {
if (isAuthErrorMsg(e?.message || e)) {
log(`\u8ba4\u8bc1\u5931\u8d25\uff0c\u8bf7\u5148\u767b\u5f55\uff1a${e.message || e}`, "warn");
showLoginGuide("\u8bf7\u5148\u767b\u5f55\u5e73\u53f0\u540e\u518d\u4f7f\u7528");
return;
}
throw e;
}
}
async function _rq() {
if (S.running) return;
S.running = true;
S.stopFlag = false;
S.panelHint = "";
S.queueDone = 0;
S.examListCache = null;
renderRunStatus();
try {
if (!S.studentId) await loadMajorInfoSafe();
if (!S.courses.length) await loadCourses();
await _cl(true);
const queue = S.courses.filter(isCourseSelected);
if (!queue.length) {
log("\u672a\u9009\u62e9\u8bfe\u7a0b", "warn");
S.panelHint = "\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b";
return;
}
const planData = await _fop("full", queue);
const planned = _rpt(planData, queue);
S.queueTotal = planned.length;
renderRunStatus();
const wantExam = planned.some((p) => p.steps.includes("exam"));
if (wantExam && canUseExamFeature()) {
try {
await _eec(true);
} catch (e) {
log(`\u9884\u62c9\u53d6\u8003\u8bd5\u5217\u8868\u5931\u8d25\uff08\u672c\u8bfe\u4ecd\u4f1a\u5c1d\u8bd5\uff09\uff1a${e.message || e}`, "warn");
}
}
const parallel =
planData?.plan?.parallel != null
? !!planData.plan.parallel
: S.runPolicy.courseParallel !== false;
if (parallel && planned.length > 1) {
log(`\u540c\u65f6\u5b66\u4e60 ${planned.length} \u95e8\u8bfe`, "info");
S.task = `\u5b66\u4e60 ${planned.length} \u95e8`;
renderRunStatus();
let quotaHit = false;
await Promise.all(
planned.map(async ({ course, steps }) => {
if (S.stopFlag || quotaHit) return;
try {
await _sc(course, steps);
} catch (e) {
const msg = String(e?.message || e);
log(`\u8bfe\u7a0b\u5931\u8d25\uff1a${course.courseName} · ${_sue(msg)}`, "err");
if (/free_course_quota_exceeded|quota/i.test(msg)) {
quotaHit = true;
S.stopFlag = true;
}
} finally {
S.queueDone += 1;
renderRunStatus();
}
})
);
} else {
const courseGap = policyNum("courseGapMs", COURSE_GAP_MS);
for (const { course, steps } of planned) {
if (S.stopFlag) break;
try {
await _sc(course, steps);
} catch (e) {
const msg = String(e?.message || e);
log(`\u8bfe\u7a0b\u5931\u8d25\uff1a${course.courseName} · ${_sue(msg)}`, "err");
if (/free_course_quota_exceeded|quota/i.test(msg)) break;
}
S.queueDone += 1;
renderRunStatus();
await sleep(courseGap);
}
}
log(S.stopFlag ? "\u5df2\u505c\u6b62" : "\u5168\u90e8\u5904\u7406\u5b8c\u6210", S.stopFlag ? "warn" : "ok");
try {
await loadCourses();
renderCourseList();
} catch (_) {}
} catch (e) {
log(`\u8fd0\u884c\u5931\u8d25\uff1a${_sue(e)}`, "err");
S.panelHint = _sue(e);
} finally {
S.running = false;
S.task = "\u5f85\u673a";
S.currentCourse = "";
S.currentAct = "";
if (S.activeCourses) S.activeCourses.clear();
renderRunStatus();
}
}
function stopRun() {
S.stopFlag = true;
S.panelHint = "\u6b63\u5728\u505c\u6b62…";
renderRunStatus();
log("\u6536\u5230\u505c\u6b62\u6307\u4ee4", "warn");
}
function openProBuy() {
const url = S.proBuyUrl || PRO_BUY_URL;
try {
if (typeof GM_openInTab === "function")
GM_openInTab(url, { active: true });
else window.open(url, "_blank", "noopener,noreferrer");
} catch (_) {
location.href = url;
}
}
function openProHint(message) {
const old = document.getElementById("ytccr-dialog-root");
if (old) old.remove();
const root = document.createElement("div");
root.id = "ytccr-dialog-root";
root.innerHTML = `
\u5f00\u901a\u4e13\u4e1a\u7248
${esc(
message || "\u5f00\u901a\u4e13\u4e1a\u7248\u540e\u53ef\u5b66\u4e60\u5168\u90e8\u8bfe\u7a0b\u3002"
)}
`;
root.addEventListener("click", (e) => {
const act = e.target?.closest?.("[data-act]")?.getAttribute("data-act");
if (act === "buy") openProBuy();
if (act === "buy" || act === "close") root.remove();
});
document.documentElement.appendChild(root);
}
function renderProfile() {
const el = $("ytccr-profile");
if (!el) return;
el.textContent = [
S.collegeOrgName,
S.majorName,
S.studentId ? `ID:${trunc(S.studentId, 10)}` : "",
]
.filter(Boolean)
.join(" · ");
}
function renderTermSelect() {
const sel = $("ytccr-term-select");
if (!sel) return;
sel.innerHTML = S.terms
.map((t) => {
const id = String(t.termId || "");
const name = String(t.termName || id);
const cur = Number(t.isCurrentTerm) === 1 ? "\uff08\u5f53\u524d\uff09" : "";
return ``;
})
.join("");
sel.style.display = S.terms.length > 1 ? "" : S.terms.length === 1 ? "" : "none";
}
function renderCourseList() {
const box = $("ytccr-course-list");
if (!box) return;
if (!S.courses.length) {
box.innerHTML =
'\u6682\u65e0\u8bfe\u7a0b\uff0c\u53ef\u70b9\u51fb\u5237\u65b0\u91cd\u8bd5
';
return;
}
box.innerHTML = S.courses
.map((c) => {
const k = courseKey(c);
const checked = isCourseSelected(c) ? "checked" : "";
const prog = progressNum(c.progress);
const done = prog >= 100;
return `
`;
})
.join("");
box.querySelectorAll("input[type=checkbox]").forEach((inp) => {
inp.addEventListener("change", () => {
const id = inp.getAttribute("data-cid");
if (inp.checked) S.selectedIds.add(id);
else S.selectedIds.delete(id);
const all = S.courses.every((c) => S.selectedIds.has(courseKey(c)));
const none = S.selectedIds.size === 0;
S.selectMode = all ? "all" : none ? "none" : "partial";
if (S.selectMode === "all") S.selectedIds = new Set();
saveSelected();
renderCourseList();
});
});
}
function injectStyles() {
if ($("ytccr-panel-style")) return;
const st = document.createElement("style");
st.id = "ytccr-panel-style";
st.textContent = `
#ytccr-auto-panel,#ytccr-auto-panel *{box-sizing:border-box}
#ytccr-auto-panel{position:fixed;right:16px;top:64px;z-index:2147483000;width:396px;max-height:min(88vh,700px);display:flex;flex-direction:column;background:#f4f7fb;border:1px solid #d0dae8;border-radius:14px;box-shadow:0 14px 32px rgba(15,23,42,.16);font:12px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:#0f172a;overflow:hidden}
#ytccr-auto-panel.ytccr-min #ytccr-panel-body,#ytccr-auto-panel.ytccr-min #ytccr-panel-actions,#ytccr-auto-panel.ytccr-min #ytccr-panel-footer{display:none!important}
#ytccr-panel-header{padding:8px 11px;background:#f8fafc;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;flex-shrink:0}
#ytccr-panel-brand{display:flex;align-items:center;gap:7px;min-width:0;flex:1}
#ytccr-panel-logo{width:24px;height:24px;border-radius:7px;object-fit:cover;border:1px solid #cbd5e1;background:#fff}
#ytccr-panel-title{font-size:12px;font-weight:900;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ytccr-ver{font-size:10px;font-weight:800;color:#64748b;padding:1px 5px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;margin-left:4px}
.ytccr-ctl{border:1px solid #cbd5e1;background:#fff;color:#334155;width:24px;height:24px;border-radius:8px;cursor:pointer;font-size:16px;font-weight:900;line-height:1;display:inline-flex;align-items:center;justify-content:center;padding:0}
#ytccr-panel-body{padding:8px 9px 6px;overflow:auto;flex:1 1 auto;min-height:0}
.ytccr-card{background:#fff;border:1px solid #d9e2ee;border-radius:10px;padding:8px 9px;margin-bottom:7px}
.ytccr-status-row{display:flex;justify-content:space-between;align-items:center;gap:6px}
#ytccr-auto-status{padding:1px 6px;border-radius:999px;font-weight:900;font-size:10px;border:1px solid #cbd5e1;background:#fff;color:#334155}
.ytccr-progress-bar{height:3px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:3px}
.ytccr-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb)}
.ytccr-tabbar{display:flex;gap:4px;margin-bottom:6px}
.ytccr-tab{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#334155;padding:4px 2px;border-radius:8px;cursor:pointer;font-weight:700;font-size:11px}
.ytccr-tab.ytccr-on{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent}
.ytccr-pane{display:none}.ytccr-pane.ytccr-on{display:block}
#ytccr-course-list,#ytccr-run-log{max-height:168px;overflow:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:8px;padding:4px}
.ytccr-item{display:flex;gap:7px;align-items:flex-start;padding:5px 6px;border-bottom:1px dashed #e2e8f0}
.ytccr-item label{flex:1;cursor:pointer;line-height:1.35;font-size:12px;font-weight:600}
.ytccr-meta{display:block;color:#64748b;font-size:11px;margin-top:2px;font-weight:500}
.ytccr-done{opacity:.65}.ytccr-done-tag{color:#16a34a;font-size:10px}
.ytccr-log-row{padding:3px 4px;border-bottom:1px dashed #e2e8f0;line-height:1.35;font-size:11px}
.ytccr-log-time{color:#64748b}.ytccr-log-ok{color:#166534}.ytccr-log-warn{color:#b45309}.ytccr-log-err{color:#b91c1c}
.ytccr-meta-row{display:flex;align-items:center;gap:8px;margin:5px 0;font-size:12px}
.ytccr-meta-label{flex:0 0 72px;color:#64748b}
.ytccr-meta-value{flex:1;min-width:0;word-break:break-all;font-weight:700}
#ytccr-cloud-token,#ytccr-term-select{flex:1;border:1px solid #cbd5e1;border-radius:8px;padding:5px 7px;font-size:12px;background:#fff}
.ytccr-btn-row{display:flex;gap:8px;margin-top:6px}
.ytccr-btn{border:0;border-radius:10px;padding:8px 10px;font-size:13px;font-weight:800;cursor:pointer}
.ytccr-btn-start{flex:1;background:#2563eb;color:#fff}
.ytccr-btn-stop{flex:1;background:#0f172a;color:#fff}
.ytccr-btn-off,.ytccr-btn:disabled{opacity:.45;cursor:not-allowed}
.ytccr-btn-ghost{background:#f1f5f9;color:#334155;border:1px solid #cbd5e1}
.ytccr-btn-pro{background:linear-gradient(135deg,#f59e0b,#ea580c);color:#fff}
.ytccr-actions{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px}
.ytccr-actions button{font-size:11px;padding:4px 10px;border-radius:8px;border:1px solid #cbd5e1;background:#f8fafc;color:#334155;font-weight:700;cursor:pointer}
#ytccr-panel-actions{padding:0 9px 8px}
#ytccr-panel-hint{color:#b91c1c;font-size:11px;min-height:14px;margin:2px 0 4px}
#ytccr-panel-footer{padding:6px 12px 10px;font-size:11px;color:#64748b;border-top:1px solid #e2e8f0;background:#f8fafc}
.ytccr-ann{background:#fff;border:1px solid #dbe4f0;border-radius:10px;padding:7px 8px;font-size:11px;margin:0 9px 8px}
.ytccr-empty{padding:14px;text-align:center;color:#64748b}
.ytccr-login-guide{padding:18px 12px}
.ytccr-login-title{font-size:14px;font-weight:600;color:#b45309;margin-bottom:6px}
.ytccr-login-sub{font-size:11px;color:#94a3b8;line-height:1.45}
#ytccr-dialog-root{position:fixed;inset:0;z-index:2147483646;display:flex;align-items:center;justify-content:center}
.ytccr-dialog-mask{position:absolute;inset:0;background:rgba(0,0,0,.35)}
.ytccr-dialog-card{position:relative;width:min(360px,92vw);background:#fff;border-radius:14px;padding:14px;border:1px solid #cbd5e1;box-shadow:0 16px 40px rgba(0,0,0,.2)}
.ytccr-dialog-head{font-weight:800;margin-bottom:8px}
.ytccr-dialog-body{font-size:13px;margin-bottom:12px;line-height:1.5}
.ytccr-dialog-actions{display:flex;gap:8px;justify-content:flex-end}
#ytccr-profile{font-size:11px;color:#64748b;margin-top:2px;word-break:break-all}
`;
document.documentElement.appendChild(st);
}
function enableDrag(panel) {
const header = panel.querySelector("#ytccr-panel-header");
if (!header) return;
let dragging = false,
sx = 0,
sy = 0,
ol = 0,
ot = 0;
header.addEventListener("mousedown", (e) => {
if (e.target.closest("button")) return;
dragging = true;
const rect = panel.getBoundingClientRect();
sx = e.clientX;
sy = e.clientY;
ol = rect.left;
ot = rect.top;
panel.style.right = "auto";
panel.style.left = `${ol}px`;
panel.style.top = `${ot}px`;
e.preventDefault();
});
window.addEventListener("mousemove", (e) => {
if (!dragging) return;
panel.style.left = `${Math.max(0, ol + e.clientX - sx)}px`;
panel.style.top = `${Math.max(0, ot + e.clientY - sy)}px`;
});
window.addEventListener("mouseup", () => {
if (!dragging) return;
dragging = false;
const rect = panel.getBoundingClientRect();
GM_setValue(STORAGE.panelPos, { left: rect.left, top: rect.top });
});
}
function switchTab(tab) {
document.querySelectorAll(".ytccr-tab").forEach((b) => {
b.classList.toggle("ytccr-on", b.dataset.tab === tab);
});
document.querySelectorAll(".ytccr-pane").forEach((p) => {
p.classList.toggle("ytccr-on", p.dataset.pane === tab);
});
}
function mountPanel() {
injectStyles();
if ($("ytccr-auto-panel")) return;
const panel = document.createElement("div");
panel.id = "ytccr-auto-panel";
if (GM_getValue(STORAGE.collapsed, false)) panel.classList.add("ytccr-min");
const pos = GM_getValue(STORAGE.panelPos, null);
if (pos && typeof pos.left === "number") {
panel.style.right = "auto";
panel.style.left = `${pos.left}px`;
panel.style.top = `${pos.top}px`;
}
panel.innerHTML = `
运行状态
\u5df2\u505c\u6b62
\u8fdb\u5ea6 0/0
· 0%
待机
学期
授权状态\u672a\u6821\u9a8c
\u514d\u8d39\u989d\u5ea60 / 1
专业版到期—
\u6388\u6743\u7801
\u300c\u5f00\u59cb\u300d\uff1a\u52fe\u9009\u8bfe\u7a0b\u540e\u540c\u65f6\u5b66\u4e60\uff08\u542b\u89c6\u9891\u3001\u4f5c\u4e1a\u4e0e\u8003\u8bd5\uff09\u3002\u300c\u5f00\u59cb\u8003\u8bd5\u300d\uff1a\u4ec5\u5904\u7406\u52fe\u9009\u8bfe\u7a0b\u7684\u8003\u8bd5\u3002
${esc(
PANEL_NOTICE
)}
`;
document.documentElement.appendChild(panel);
enableDrag(panel);
const btnMin = $("ytccr-btn-min");
const btnMax = $("ytccr-btn-max");
btnMin.onclick = () => {
panel.classList.add("ytccr-min");
btnMin.style.display = "none";
btnMax.style.display = "";
GM_setValue(STORAGE.collapsed, true);
};
btnMax.onclick = () => {
panel.classList.remove("ytccr-min");
btnMax.style.display = "none";
btnMin.style.display = "";
GM_setValue(STORAGE.collapsed, false);
};
if (panel.classList.contains("ytccr-min")) {
btnMin.style.display = "none";
btnMax.style.display = "";
}
document.querySelectorAll(".ytccr-tab").forEach((b) => {
b.addEventListener("click", () => switchTab(b.dataset.tab));
});
$("ytccr-sel-all").onclick = () => {
S.selectMode = "all";
S.selectedIds = new Set();
saveSelected();
renderCourseList();
};
$("ytccr-sel-none").onclick = () => {
S.selectMode = "none";
S.selectedIds = new Set();
saveSelected();
renderCourseList();
};
$("ytccr-refresh").onclick = () => {
refreshAll().catch((e) => log(`\u5237\u65b0\u5931\u8d25\uff1a${e.message || e}`, "err"));
};
$("ytccr-log-clear").onclick = () => {
S.logs = [];
renderLog();
};
$("ytccr-term-select").onchange = async (e) => {
S.termId = String(e.target.value || "");
GM_setValue(STORAGE.termId, S.termId);
try {
await loadCourses();
renderCourseList();
log(`\u5df2\u5207\u6362\u5b66\u671f`, "ok");
} catch (err) {
log(`\u5207\u6362\u5b66\u671f\u5931\u8d25\uff1a${err.message || err}`, "err");
}
};
$("ytccr-save-token").onclick = async () => {
S.cloudToken = String($("ytccr-cloud-token")?.value || "").trim();
GM_setValue(STORAGE.cloudToken, S.cloudToken);
try {
await _cl(true);
log("\u6388\u6743\u7801\u5df2\u4fdd\u5b58\u5e76\u6821\u9a8c", "ok");
} catch (e) {
log(`\u6388\u6743\u6821\u9a8c\uff1a${_sue(e)}`, "warn");
}
refreshCloudUi();
};
$("ytccr-buy-pro").onclick = () => openProBuy();
$("ytccr-exam-start").onclick = () => {
if (S.running) {
log("\u5f53\u524d\u6709\u4efb\u52a1\u5728\u8dd1\uff0c\u8bf7\u5148\u505c\u6b62\u6216\u7b49\u5b8c\u6210", "warn");
return;
}
_req();
};
$("ytccr-start").onclick = () => _rq();
$("ytccr-stop").onclick = () => stopRun();
refreshCloudUi();
renderLog();
renderRunStatus();
}
async function boot() {
if (!ensureD9a6()) {
console.warn("[ytccr] d9a6 missing");
}
mountPanel();
log(`${SCRIPT_NAME} v${VER} \u5df2\u52a0\u8f7d`);
try {
await ensureClientBootstrap(true);
} catch (e) {
log(`\u4e91\u7aef\u914d\u7f6e\uff1a${_sue(e)}`, "warn");
}
try {
await refreshAll();
} catch (e) {
if (isAuthErrorMsg(e?.message || e)) {
showLoginGuide("\u8bf7\u5148\u767b\u5f55\u5e73\u53f0\u540e\u518d\u4f7f\u7528");
log(`\u521d\u59cb\u5316\uff1a\u8bf7\u5148\u767b\u5f55\u5e73\u53f0`, "warn");
} else {
log(`\u521d\u59cb\u5316\uff1a${_sue(e)}`, "warn");
S.panelHint = _sue(e);
renderRunStatus();
}
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => boot());
} else {
boot();
}
})();