// ==UserScript==
// @name 【效率版】青书学堂课程学习助手|30分钟学完所有课程
// @namespace https://card.wlxy.live/details/A1D0FEF5
// @version 1.1
// @description 青书学堂(qingshuxuetang.com)学习辅助:登录分/讨论分/作业/课件学时一键勾选全满分完成;30分左右完全全部课程的平时分,免费体验1门课,Pro不限;支持所有青书学堂平台;考试功能后期开放。
// @antifeature payment 体验用户限1门课,升级后不限
// @antifeature membership 需云端授权
// @author 柠檬真酸
// @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png
// @match https://*.qingshuxuetang.com/*
// @noframes
// @connect *.qingshuxuetang.com
// @connect cp.qsxt.info
// @connect content.qingshuxuetang.com
// @connect oa27.ahzsksw.cn
// @connect huaweicloudobs.ahjxjy.cn
// @connect card.wlxy.live
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @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.__QINGSHU_STUDY_HELPER__) return;
window.__QINGSHU_STUDY_HELPER__ = true;
const HOST = location.hostname;
if (!/qingshuxuetang\.com$/i.test(HOST)) return;
const SCRIPT_VERSION = "1.1";
const QUEUE_KEY = "qingshu_course_queue_v1";
const MAX_VIDEOS_KEY = "qingshu_max_videos_v1";
const PANEL_POS_KEY = "qingshu_panel_pos_v1";
const PANEL_COLLAPSED_KEY = "qingshu_panel_collapsed_v1";
const ACTIVE_TAB_KEY = "qingshu_active_tab_v1";
const CLOUD_TOKEN_KEY = "qingshu_cloud_token_v1";
const LEARNING_USER_KEY = "qingshu_learning_user_id_v1";
const CLOUD_LEASE_CACHE_KEY = "qingshu_cloud_lease_cache_v1";
const CLOUD_PRO_EXPIRE_CACHE_KEY = "qingshu_cloud_pro_expire_cache_v1";
const CLOUD_LAST_STATE_KEY = "qingshu_cloud_last_state_v1";
const DEFAULT_CLOUD_API_BASE = "https://oa27.ahzsksw.cn";
const PRO_DAYS = 60;
const FREE_COURSE_LIMIT = 1;
const PRO_BUY_URL = "https://card.wlxy.live/details/A1D0FEF5";
const PANEL_LOGO_URL =
"https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png";
const PANEL_NOTICE_FALLBACK = "\u9752\u4e66\u5b66\u5802\u5b66\u4e60\u52a9\u624b";
const QQ_GROUP_NUMBER = "903117129";
const QQ_GROUP_LINK =
"https://qun.qq.com/universal-share/share?ac=1&authKey=rxdL6YIJ0%2FxOEemjLqTGULvl5aAfJIVQcIvkvnwvmL%2FAmpFZnSafajYHgSXMUXvx&busi_data=eyJncm91cENvZGUiOiI5MDMxMTcxMjkiLCJ0b2tlbiI6IlB2dkFGSm5XRXBrSEhtQVFTUGQzdVNZakhNWDNMbW1kODA2enpoMi9obDh4SWp0YzBDODNFaGtwRU44Z0hyU0siLCJ1aW4iOiIxMjU0MzE1MTQifQ%3D%3D&data=9oyJixSPcigCQW-saV5eXlcMwV9C6J36XySx-rDHwVwNlofvRmd2ze5sLwFtHTbYbG4nAWIUrI0qftC6aTX9xg&svctype=4&tempid=h5_group_info";
const ASSIGNMENT_STATUS_TEXT = {
0: "\u5c1a\u672a\u5f00\u59cb",
1: "\u5c1a\u672a\u63d0\u4ea4",
2: "\u5df2\u63d0\u4ea4\u5f85\u6279",
3: "\u5df2\u81ea\u52a8\u9605\u5377",
4: "\u5df2\u6279\u6539",
};
const EXAM_GROUP_KEYS = [
["onlineFinalExamUIDatas", "\u671f\u672b"],
["onlineOtherExamUIDatas", "\u5176\u4ed6"],
["makeupExamUIDatas", "\u8865\u8003"],
["delayExamUIDatas", "\u7f13\u8003"],
["mockExamUIDatas", "\u6a21\u62df"],
["retakeExamUIDatas", "\u91cd\u4fee"],
];
const CONTENT_TYPE_COURSEWARE = 11;
const REPORT_WAIT_MIN_SEC = 60;
const REPORT_WAIT_MAX_SEC = 480;
const CONTINUE_STEP_MIN_SEC = 180;
const CONTINUE_STEP_MAX_SEC = 360;
const DEFAULT_MAX_VIDEOS = 5;
const BEGIN_STAGGER_MS = 2500;
const COURSE_PREP_GAP_MS = 2000;
const API_MIN_GAP_MS = 1200;
const HTML_RETRY = 4;
const BUSY_RETRY_WAIT_MS = [5000, 10000, 20000];
const LOG_LIMIT = 300;
const STUDY_ROUTE_CAP_MINUTES = 0;
const LOOP_POSITION_MAX = 480;
const POSITION_STEP_MIN_SEC = 60;
const POSITION_STEP_MAX_SEC = 120;
const STUDY_SCORE_CHECK_MS = 120000;
const DEFAULT_LOGIN_PER = 1;
const DEFAULT_LOGIN_MAX = 10;
const DEFAULT_STUDY_CONTINUOUS_MIN = 5;
const DEFAULT_STUDY_POINTS_PER = 2;
const DEFAULT_STUDY_MAX = 60;
const DEFAULT_FORUM_PER = 1;
const DEFAULT_FORUM_MAX = 10;
const LOGIN_GAP_MS = 2500;
const FORUM_GAP_MS = 3000;
const SCORE_COURSE_STAGGER_MS = 1200;
const HOMEWORK_ANSWER_GAP_MS = 400;
const HW_STATUS_DONE_MIN = 2;
let studyScoreWatchTimer = null;
const state = {
enabled: false,
stopFlag: false,
runBusy: false,
scoreTaskBusy: false,
maxVideos: readMaxVideos(),
parallelActive: 0,
school: "",
courses: [],
selected: new Set(loadQueue()),
logLines: [],
currentCourse: "",
currentChapter: "",
currentTask: "\u70b9\u300c\u5237\u65b0\u300d\u540e\u52fe\u9009\u5e76\u5f00\u59cb",
queueDone: 0,
queueTotal: 0,
userInfo: null,
currentPeriodLabel: "",
activeTab: localStorage.getItem(ACTIVE_TAB_KEY) || "courses",
assignments: [],
assignmentsLoaded: false,
exams: [],
examsLoaded: false,
examsError: "",
courseStudyFull: new Set(),
studyCompleteLogged: new Set(),
learningUserId: "",
cloudApiBase: DEFAULT_CLOUD_API_BASE,
cloudToken: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(),
cloudTier: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim()
? "unknown"
: "free",
cloudLease: "",
cloudLeaseExp: 0,
cloudProExpireAt: 0,
cloudRevoked: false,
freeCourseLimit: FREE_COURSE_LIMIT,
freeUsedCourses: 0,
freeBoundCourses: [],
proBuyUrl: PRO_BUY_URL,
panelNoticePath: "/api/qingshu/panel-notice",
remotePanelNotice: PANEL_NOTICE_FALLBACK,
};
function loadQueue() {
try {
const raw = JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]");
return Array.isArray(raw) ? raw.map(String) : [];
} catch (_) {
return [];
}
}
function saveQueue() {
localStorage.setItem(QUEUE_KEY, JSON.stringify([...state.selected]));
}
function _brw(attemptIndex) {
return BUSY_RETRY_WAIT_MS[Math.min(attemptIndex, BUSY_RETRY_WAIT_MS.length - 1)];
}
function randReportWaitSec(rules) {
const contMin = Number(rules?.studyContinuousMin) || DEFAULT_STUDY_CONTINUOUS_MIN;
const contSec = Math.max(60, Math.floor(contMin * 60));
const waitHi = Math.min(
REPORT_WAIT_MAX_SEC,
Math.max(REPORT_WAIT_MIN_SEC, contSec - 15)
);
const waitLo = Math.min(REPORT_WAIT_MIN_SEC, waitHi);
return randInt(waitLo, waitHi);
}
function defaultScoreRules() {
return {
loginPer: DEFAULT_LOGIN_PER,
loginMax: DEFAULT_LOGIN_MAX,
studyContinuousMin: DEFAULT_STUDY_CONTINUOUS_MIN,
studyPointsPer: DEFAULT_STUDY_POINTS_PER,
studyMax: DEFAULT_STUDY_MAX,
forumPer: DEFAULT_FORUM_PER,
forumMax: DEFAULT_FORUM_MAX,
source: "",
};
}
function parseScoreRules(html) {
const rules = defaultScoreRules();
if (!html) return rules;
let text = String(html)
.replace(/<[^>]+>/g, " ")
.replace(/ /gi, " ")
.replace(/\s+/g, " ");
let m = text.match(/\u767b\u5f55\u5f97\u5206[\uff1a:]\s*([\d.]+)\s*\u5206\s*\/\s*\u6b21[\u3002.]?\s*\u5171\s*([\d.]+)\s*\u5206/);
if (m) {
rules.loginPer = Number(m[1]);
rules.loginMax = Number(m[2]);
}
m = text.match(
/\u8bfe\u4ef6\u5b66\u4e60[\uff1a:]\s*\u8fde\u7eed\u5b66\u4e60\s*([\d.]+)\s*\u5206\u949f\u53ef\u5f97\s*([\d.]+)\s*\u5206[\u3002.]?\s*\u5171\s*([\d.]+)\s*\u5206/
);
if (m) {
rules.studyContinuousMin = Number(m[1]);
rules.studyPointsPer = Number(m[2]);
rules.studyMax = Number(m[3]);
}
m = text.match(/\u8bfe\u7a0b\u8ba8\u8bba[\uff1a:]\s*.*?([\d.]+)\s*\u5206\s*\/\s*\u4e2a[\s\S]{0,80}?\u5171\s*([\d.]+)\s*\u5206/);
if (!m) {
m = text.match(/\u8bfe\u7a0b\u8ba8\u8bba[\uff1a:].{0,120}?([\d.]+)\s*\u5206\s*\/\s*\u4e2a.{0,80}?\u5171\s*([\d.]+)\s*\u5206/);
}
if (m) {
rules.forumPer = Number(m[1]);
rules.forumMax = Number(m[2]);
}
rules.source = [
`\u767b\u5f55${rules.loginPer}\u5206/\u6b21·\u5171${rules.loginMax}`,
`\u8bfe\u4ef6\u8fde\u7eed${rules.studyContinuousMin}\u5206\u949f=${rules.studyPointsPer}\u5206·\u5171${rules.studyMax}`,
`\u8ba8\u8bba${rules.forumPer}\u5206/\u4e2a·\u5171${rules.forumMax}`,
].join("\uff1b");
return rules;
}
function mergeRulesMaxFromDetail(rules, detail) {
if (!rules || !detail || detail.error) return rules;
if (detail.login?.max != null && Number.isFinite(Number(detail.login.max))) {
rules.loginMax = Number(detail.login.max);
}
if (detail.study?.max != null && Number.isFinite(Number(detail.study.max))) {
rules.studyMax = Number(detail.study.max);
}
if (detail.forum?.max != null && Number.isFinite(Number(detail.forum.max))) {
rules.forumMax = Number(detail.forum.max);
}
return rules;
}
function courseRules(course) {
return course?.scoreRules || defaultScoreRules();
}
function partCur(course, key) {
const p = course?.scoreDetail?.[key];
const n = Number(p?.score);
return Number.isFinite(n) ? n : 0;
}
function loginNeed(rules, cur) {
const gap = Math.max(0, Number(rules.loginMax) - Number(cur || 0));
const per = Number(rules.loginPer) || 1;
return gap > 1e-9 ? Math.ceil(gap / per - 1e-9) : 0;
}
function forumNeed(rules, cur) {
const gap = Math.max(0, Number(rules.forumMax) - Number(cur || 0));
const per = Number(rules.forumPer) || 1;
return gap > 1e-9 ? Math.ceil(gap / per - 1e-9) : 0;
}
function studyNeedContinuousMinutes(rules, cur) {
const gap = Math.max(0, Number(rules.studyMax) - Number(cur || 0));
if (gap <= 1e-9) return 0;
const pts = Number(rules.studyPointsPer) || DEFAULT_STUDY_POINTS_PER;
const block = Number(rules.studyContinuousMin) || DEFAULT_STUDY_CONTINUOUS_MIN;
const blocks = Math.ceil(gap / pts - 1e-9);
return blocks * block;
}
function studyHangMinutes(course, parallel = 1, fallbackCap = STUDY_ROUTE_CAP_MINUTES) {
const rules = courseRules(course);
const need = studyNeedContinuousMinutes(rules, partCur(course, "study"));
if (need <= 0) return 0;
const block = Number(rules.studyContinuousMin) || DEFAULT_STUDY_CONTINUOUS_MIN;
const n = Math.max(1, Number(parallel) || 1);
let hang = Math.ceil(need / n - 1e-9);
hang = Math.max(hang, block);
const cap = Number(fallbackCap) || 0;
if (cap > 0) hang = Math.min(hang, cap);
return hang;
}
async function _esr(course, { silent = false } = {}) {
if (course.scoreRules?.source) {
mergeRulesMaxFromDetail(course.scoreRules, course.scoreDetail);
return course.scoreRules;
}
const url =
`${courseApiBase()}CourseStudy?courseId=${encodeURIComponent(course.courseId)}` +
`&teachPlanId=${encodeURIComponent(course.teachPlanId)}` +
`&periodId=${encodeURIComponent(course.periodId)}&tabType=score`;
try {
const html = await _whs(() => _ahg(url));
const rules = parseScoreRules(html);
mergeRulesMaxFromDetail(rules, course.scoreDetail);
course.scoreRules = rules;
return rules;
} catch (e) {
const rules = defaultScoreRules();
mergeRulesMaxFromDetail(rules, course.scoreDetail);
course.scoreRules = rules;
if (!silent) {
}
return rules;
}
}
function _cln(course) {
return String(course?.courseName || "\u8bfe\u7a0b").trim() || "\u8bfe\u7a0b";
}
function scoreNum(v) {
const n = Number(v);
return Number.isFinite(n) ? Math.floor(n) : 0;
}
async function _ecr(courses) {
for (const course of courses || []) {
if (state.stopFlag) break;
await _esr(course, { silent: true });
await sleep(200);
}
}
function readMaxVideos() {
return DEFAULT_MAX_VIDEOS;
}
function saveMaxVideos(_n) {
state.maxVideos = DEFAULT_MAX_VIDEOS;
localStorage.setItem(MAX_VIDEOS_KEY, String(DEFAULT_MAX_VIDEOS));
return DEFAULT_MAX_VIDEOS;
}
let apiUploadChain = Promise.resolve();
let apiUploadLastAt = 0;
async function _wus(fn) {
const run = apiUploadChain.then(async () => {
const wait = Math.max(0, API_MIN_GAP_MS - (Date.now() - apiUploadLastAt));
if (wait > 0) await sleep(wait);
const ret = await fn();
apiUploadLastAt = Date.now();
return ret;
});
apiUploadChain = run.catch(() => {});
return run;
}
let htmlFetchChain = Promise.resolve();
function _whs(fn) {
const run = htmlFetchChain.then(() => fn());
htmlFetchChain = run.catch(() => {});
return run;
}
async function runPool(items, concurrency, worker) {
const results = new Array(items.length);
let cursor = 0;
async function runner() {
while (cursor < items.length && !state.stopFlag) {
const i = cursor++;
try {
results[i] = await worker(items[i], i);
} catch (e) {
results[i] = { error: e };
}
}
}
const n = Math.max(1, Math.min(concurrency, items.length));
await Promise.all(Array.from({ length: n }, () => runner()));
return results;
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
function randInt(min, max) {
const lo = Math.min(min, max);
const hi = Math.max(min, max);
return lo + Math.floor(Math.random() * (hi - lo + 1));
}
function randSecMs(minSec, maxSec) {
return randInt(minSec, maxSec) * 1000;
}
function escHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function formatSec(sec) {
const s = Math.max(0, Math.floor(Number(sec) || 0));
const m = Math.floor(s / 60);
const r = s % 60;
return `${m}:${String(r).padStart(2, "0")}`;
}
function _ck(c) {
return `${c.teachPlanId}_${c.courseId}_${c.periodId}`;
}
function detectSchool() {
const parts = location.pathname.split("/").filter(Boolean);
if (parts.length >= 2 && parts[1] === "Student") return parts[0];
const m = location.pathname.match(/^\/([^/]+)\/Student\//);
return m ? m[1] : parts[0] || "";
}
function schoolBase() {
if (!state.school) state.school = detectSchool();
return `${location.origin}/${state.school}`;
}
function courseApiBase() {
return `${schoolBase()}/Student/Course/`;
}
function studentApiBase() {
return `${schoolBase()}/Student/`;
}
function svcBase() {
return `${schoolBase()}/`;
}
function svcApiBase() {
return `${schoolBase()}/Svc/`;
}
function ensureDeviceTraceId() {
let id =
localStorage.getItem("deviceTraceIdQS") ||
localStorage.getItem("Device-Trace-Id-QS") ||
"";
if (!id) {
id =
(typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID()) ||
`qs-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
try {
localStorage.setItem("deviceTraceIdQS", id);
} catch (_) {}
}
return id;
}
function defaultHeaders(json, opts) {
const o = opts || {};
const h = {
Accept: o.accept || "application/json, text/javascript, */*; q=0.01",
"X-Requested-With": "XMLHttpRequest",
};
if (json) h["Content-Type"] = "application/json";
h["Device-Trace-Id-QS"] = ensureDeviceTraceId();
return h;
}
function parseApiOpts(tMsOrOpts) {
if (tMsOrOpts != null && typeof tMsOrOpts === "object") return tMsOrOpts;
return { tMs: tMsOrOpts };
}
function tsUrl(url, tMs) {
const t = tMs == null ? Date.now() : Math.floor(tMs);
const sep = url.includes("?") ? "&" : "?";
return `${url}${sep}_t=${t}`;
}
function courseStudyUrl(course, ctx) {
const courseId = (ctx && ctx.courseId) || course.courseId;
const teachPlanId = (ctx && (ctx.classId || ctx.teachPlanId)) || course.teachPlanId;
const periodId = (ctx && ctx.periodId) || course.periodId;
return (
`${courseApiBase()}CourseStudy?courseId=${encodeURIComponent(courseId)}` +
`&teachPlanId=${encodeURIComponent(teachPlanId)}` +
`&periodId=${encodeURIComponent(periodId)}`
);
}
function courseShowUrl(ctx, course, nodeId) {
const classId = (ctx && (ctx.classId || ctx.teachPlanId)) || course.teachPlanId;
const courseId = (ctx && ctx.courseId) || course.courseId;
const periodId = (ctx && ctx.periodId) || course.periodId;
return (
`${courseApiBase()}CourseShow?teachPlanId=${encodeURIComponent(classId)}` +
`&periodId=${encodeURIComponent(periodId)}` +
`&courseId=${encodeURIComponent(courseId)}` +
`&nodeId=${encodeURIComponent(nodeId)}`
);
}
function logTag(node) {
const name = String(node?.name || node?.id || "").slice(0, 14);
return name ? `[${name}]` : "";
}
function taskTag(task) {
const cn = String(task.course?.courseName || "").slice(0, 6);
return cn ? `[${cn}]${logTag(task.node)}` : logTag(task.node);
}
function _gpr(method, url, opts) {
const o = opts || {};
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== "function") {
reject(new Error("GM_xmlhttpRequest \u4e0d\u53ef\u7528"));
return;
}
const headers = Object.assign({}, o.headers || {});
if (o.referrer) headers.Referer = o.referrer;
const req = {
method: method || "GET",
url,
headers,
anonymous: false,
onload(resp) {
_rlh(resp.responseHeaders);
resolve({
status: Number(resp.status || 0),
text: String(resp.responseText || ""),
});
},
onerror() {
reject(new Error("\u7f51\u7edc\u9519\u8bef"));
},
ontimeout() {
reject(new Error("\u8bf7\u6c42\u8d85\u65f6"));
},
};
if (o.data != null) req.data = o.data;
if (o.binary) req.binary = true;
GM_xmlhttpRequest(req);
});
}
async function _aj(method, url, body, tMsOrOpts) {
const opts = parseApiOpts(tMsOrOpts);
const headers = defaultHeaders(body != null, opts);
headers.Origin = "https://degree.qingshuxuetang.com";
const finalUrl = tsUrl(url, opts.tMs);
const payload = body == null ? undefined : JSON.stringify(body);
let status = 0;
let text = "";
if (opts.referrer && typeof GM_xmlhttpRequest === "function") {
const resp = await _gpr(method, finalUrl, {
headers,
data: payload,
referrer: opts.referrer,
});
status = resp.status;
text = resp.text;
} else {
const fetchOpts = {
method,
credentials: "include",
headers,
body: payload,
};
if (opts.referrer) {
fetchOpts.referrer = opts.referrer;
fetchOpts.referrerPolicy = "unsafe-url";
}
const res = await fetch(finalUrl, fetchOpts);
status = res.status;
_rlh(res.headers);
text = await res.text();
}
if (text.trim().startsWith("<")) {
const err = new Error(`HTTP ${status} \u7f51\u5173\u8fd4\u56de HTML`);
err.status = status;
err.html = true;
throw err;
}
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch (_) {
json = null;
}
if (!status || status < 200 || status >= 300) {
const hint = status === 412 ? "\uff08Referer/\u524d\u7f6e\u6761\u4ef6\u88ab\u62d2\uff0c\u8bf7\u5237\u65b0\u5b66\u4e60\u4e2d\u5fc3\u540e\u91cd\u8bd5\uff09" : "";
const err = new Error(`HTTP ${status}${hint}`);
err.status = status;
throw err;
}
if (!json || json.hr !== 0) {
const err = new Error((json && json.message) || text || "\u63a5\u53e3\u5931\u8d25");
err.hr = json && json.hr;
err.status = status;
throw err;
}
return json.data;
}
async function _afp(url, fields) {
const body = new URLSearchParams();
for (const [k, v] of Object.entries(fields || {})) {
if (v == null || v === "") continue;
body.append(k, String(v));
}
const res = await fetch(tsUrl(url), {
method: "POST",
credentials: "include",
headers: {
Accept: "application/json, text/javascript, */*; q=0.01",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
...defaultHeaders(false),
},
body: body.toString(),
});
const text = await res.text();
if (text.trim().startsWith("<")) {
const err = new Error(`HTTP ${res.status} \u7f51\u5173\u8fd4\u56de HTML`);
err.status = res.status;
err.html = true;
throw err;
}
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch (_) {
throw new Error(`\u54cd\u5e94\u975e JSON\uff1a${text.slice(0, 80)}`);
}
if (!res.ok || !json || json.hr !== 0) {
throw new Error((json && json.message) || text || "\u8bf7\u6c42\u5931\u8d25");
}
return json.data;
}
async function _ajr(method, url, body, retriesOrOpts = HTML_RETRY) {
const opts =
retriesOrOpts != null && typeof retriesOrOpts === "object"
? retriesOrOpts
: { retries: retriesOrOpts };
const retries = Number(opts.retries) > 0 ? Number(opts.retries) : HTML_RETRY;
let lastErr = null;
for (let i = 0; i < retries; i++) {
try {
return await _wus(() => _aj(method, url, body, opts));
} catch (e) {
lastErr = e;
const retryable =
e.html ||
e.hr === -2004318076 ||
[412, 429, 502, 503, 504].includes(e.status);
if (retryable && i < retries - 1) {
const wait = _brw(i);
log(
e.status === 412
? `\u524d\u7f6e\u6761\u4ef6\u5931\u8d25(412)\uff0c${Math.round(wait / 1000)}s \u540e\u91cd\u8bd5…`
: `\u63a5\u53e3\u7e41\u5fd9\uff0c${Math.round(wait / 1000)}s \u540e\u91cd\u8bd5…`
);
await sleep(wait);
continue;
}
throw e;
}
}
throw lastErr;
}
async function _amp(url, fields, tMsOrOpts) {
const opts = parseApiOpts(tMsOrOpts);
const fd = new FormData();
Object.entries(fields).forEach(([k, v]) => fd.append(k, String(v)));
const headers = {
Accept: "application/json",
"X-Requested-With": "XMLHttpRequest",
"Device-Trace-Id-QS": ensureDeviceTraceId(),
Origin: "https://degree.qingshuxuetang.com",
};
const finalUrl = tsUrl(url, opts.tMs);
let status = 0;
let text = "";
if (opts.referrer && typeof GM_xmlhttpRequest === "function") {
const resp = await _gpr("POST", finalUrl, {
headers,
data: fd,
referrer: opts.referrer,
});
status = resp.status;
text = resp.text;
} else {
const fetchOpts = {
method: "POST",
credentials: "include",
headers,
body: fd,
};
if (opts.referrer) {
fetchOpts.referrer = opts.referrer;
fetchOpts.referrerPolicy = "unsafe-url";
}
const res = await fetch(finalUrl, fetchOpts);
status = res.status;
_rlh(res.headers);
text = await res.text();
}
if (text.trim().startsWith("<")) {
const err = new Error(`HTTP ${status} \u7f51\u5173\u8fd4\u56de HTML`);
err.status = status;
err.html = true;
throw err;
}
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch (_) {
throw new Error(`\u54cd\u5e94\u975e JSON\uff1a${text.slice(0, 80)}`);
}
if (!status || status < 200 || status >= 300 || !json || json.hr !== 0) {
const hint = status === 412 ? "\uff08Referer \u88ab\u62d2\uff09" : "";
const err = new Error((json && json.message) || `Continue \u5931\u8d25 HTTP ${status}${hint}`);
err.hr = json && json.hr;
err.status = status;
throw err;
}
return json;
}
async function apiTextGet(url) {
const res = await fetch(tsUrl(url), {
method: "GET",
credentials: "include",
headers: defaultHeaders(false),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.text();
}
async function _ahg(url, retries = HTML_RETRY) {
let lastStatus = 0;
for (let i = 0; i < retries; i++) {
const res = await fetch(url, {
method: "GET",
credentials: "include",
headers: { Accept: "text/html,application/xhtml+xml,*/*" },
});
lastStatus = res.status;
if (res.ok) return res.text();
if ([502, 503, 429, 504].includes(res.status) && i < retries - 1) {
await sleep(1200 * (i + 1));
continue;
}
throw new Error(`HTTP ${res.status}`);
}
throw new Error(`HTTP ${lastStatus}`);
}
function detectLogLevel(msg) {
const s = String(msg || "");
if (/\u5931\u8d25|\u9519\u8bef|\u5f02\u5e38|\u65e0\u6548|\u4e0d\u53ef\u7528|\u7981\u7528|\u8fc7\u671f|HTTP/.test(s)) return "error";
if (/\u505c\u6b62|\u8df3\u8fc7|\u6682\u65e0|\u540e\u671f\u5f00\u653e|\u63d0\u793a|\u4ec5\u53ef/.test(s)) return "warn";
if (/\u5b8c\u6210|\u6210\u529f|\u5f00\u59cb\u5904\u7406|\u5df2\u52a0\u8f7d|\u6b63\u5728\u5904\u7406/.test(s)) return "ok";
return "info";
}
function log(msg, level) {
const lv = level || detectLogLevel(msg);
const line = {
t: Date.now(),
level: lv,
msg: String(msg || ""),
};
state.logLines.push(line);
if (state.logLines.length > LOG_LIMIT) state.logLines.shift();
renderPanelLog();
}
function renderPanelLog() {
const box = document.getElementById("qingshu-log");
if (!box) return;
const rows = state.logLines.slice(-100);
box.innerHTML = rows
.map((row) => {
const t = new Date(row.t).toLocaleTimeString();
return `
[${escHtml(t)}] ${escHtml(row.msg)}
`;
})
.join("");
box.scrollTop = box.scrollHeight;
}
function _st(msg, kind = "info") {
const el = document.getElementById("qingshu-toast");
if (!el) return;
el.className = `qingshu-toast show ${kind}`;
const text = el.querySelector(".qingshu-toast-text");
if (text) text.textContent = String(msg || "");
clearTimeout(_st._t);
_st._t = setTimeout(() => el.classList.remove("show"), 3200);
}
function _ipu() {
if (!String(state.cloudToken || "").trim()) return false;
const tier = String(state.cloudTier || "").toLowerCase();
if (tier === "revoked") return false;
if (tier !== "pro") return false;
const exp = Number(state.cloudProExpireAt || 0);
if (exp > 0 && exp * 1000 < Date.now()) return false;
return true;
}
function _msc() {
if (_ipu()) return Number.POSITIVE_INFINITY;
return Math.max(1, Number(state.freeCourseLimit) || FREE_COURSE_LIMIT);
}
function _esl(preferredKey) {
const maxN = _msc();
if (!Number.isFinite(maxN)) return true;
const bound = (state.freeBoundCourses || []).map(String).filter(Boolean);
if (!_ipu() && bound.length) {
const keep = preferredKey && bound.includes(preferredKey) ? preferredKey : bound[0];
if (keep && (state.selected.size !== 1 || !state.selected.has(keep))) {
state.selected.clear();
state.selected.add(keep);
saveQueue();
_st("\u4f53\u9a8c\u989d\u5ea6\u5df2\u7ed1\u5b9a\u8be5\u8bfe\u7a0b\uff0c\u65e0\u6cd5\u66f4\u6362", "warn");
return false;
}
return true;
}
if (state.selected.size <= maxN) return true;
const keys = [...state.selected];
if (preferredKey && state.selected.has(preferredKey)) {
state.selected.clear();
state.selected.add(preferredKey);
} else {
while (state.selected.size > maxN) {
const drop = keys.shift();
if (drop) state.selected.delete(drop);
}
}
saveQueue();
_st("\u5f53\u524d\u4ec5\u53ef\u9009\u62e9 1 \u95e8\u8bfe\u7a0b", "warn");
return false;
}
function isNumericLearningId(id) {
return /^\d{4,}$/.test(String(id || "").trim());
}
function isLoginNameLearningId(id) {
return /^[a-zA-Z][a-zA-Z0-9._-]{3,47}$/.test(String(id || "").trim());
}
function isValidLearningId(id) {
return isNumericLearningId(id) || isLoginNameLearningId(id);
}
function persistLearningUserId(id) {
try {
if (id) localStorage.setItem(LEARNING_USER_KEY, id);
} catch (_) {}
}
function _rlu(raw) {
const id = String(raw == null ? "" : raw).trim();
if (!isValidLearningId(id)) return "";
const cur = String(state.learningUserId || "").trim();
if (cur && isNumericLearningId(cur) && !isNumericLearningId(id)) return cur;
if (cur && !isNumericLearningId(cur) && isNumericLearningId(id)) {
state.learningUserId = id;
persistLearningUserId(id);
} else if (!cur || cur === id) {
state.learningUserId = id;
persistLearningUserId(id);
} else if (isNumericLearningId(id)) {
state.learningUserId = id;
persistLearningUserId(id);
}
if (!state.userInfo || typeof state.userInfo !== "object") state.userInfo = {};
if (isNumericLearningId(id) && !state.userInfo.userId) state.userInfo.userId = id;
if (isLoginNameLearningId(id) && !state.userInfo.userName) state.userInfo.userName = id;
return String(state.learningUserId || id);
}
function _puo(u) {
if (!u || typeof u !== "object") return "";
const keys = [
"userId",
"UserId",
"uid",
"uId",
"id",
"studentId",
"StudentId",
"globalUserId",
"qsUserId",
];
for (const k of keys) {
const v = u[k];
if (v != null && isNumericLearningId(String(v).trim())) {
return String(v).trim();
}
}
for (const k of ["userName", "UserName", "loginName", "account", "user_name"]) {
const v = u[k];
if (v != null && isLoginNameLearningId(String(v).trim())) {
return String(v).trim();
}
}
return "";
}
function _puh(html) {
const s = String(html || "");
let m = s.match(/Behavior\.init\(\s*\{[\s\S]{0,800}?userId:\s*['"]?(\d{4,})/);
if (m) return m[1];
m = s.match(/\buserId:\s*['"]?(\d{5,})\b/);
if (m) return m[1];
m = s.match(/\buId\s*[:=]\s*['"]?(\d{5,})\b/);
if (m) return m[1];
return "";
}
function _pln(html) {
const s = String(html || "");
let m = s.match(
/\u7528\u6237\u540d[\s\S]{0,160}?class="form-value"[^>]*>\s*([a-zA-Z][a-zA-Z0-9._-]{3,47})\s*
);
if (m) return m[1];
m = s.match(
/