// ==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*]*>\s*\u7528\u6237\u540d\s*<\/label>[\s\S]{0,200}?]*form-value[^>]*>\s*([a-zA-Z][a-zA-Z0-9._-]{3,47})\s* {}); id = _glu(); if (id && isNumericLearningId(id)) return id; const probes = [ `${schoolBase()}/Student/UserInfoBasic`, `${schoolBase()}/Student/Course/CourseList`, `${schoolBase()}/Student/Home`, ]; for (const url of probes) { try { const res = await fetch(url, { method: "GET", credentials: "include", headers: { Accept: "text/html, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", }, }); _rlh(res.headers); id = _glu(); if (id && isNumericLearningId(id)) return id; const html = await res.text(); const fromHtml = _puh(html); if (fromHtml) return _rlu(fromHtml); const loginName = _pln(html); if (loginName) { if (!state.userInfo || typeof state.userInfo !== "object") state.userInfo = {}; state.userInfo.userName = loginName; _rlu(loginName); id = _glu(); if (id) return id; } } catch (_) {} } return _glu(); } function _gca() { return String(state.cloudApiBase || DEFAULT_CLOUD_API_BASE).replace(/\/+$/, ""); } function _crq(path, method, body) { const url = `${_gca()}${path}`; const headers = { Accept: "application/json", "Content-Type": "application/json;charset=UTF-8", }; if (state.cloudToken) headers.Authorization = `Bearer ${state.cloudToken}`; const luid = _glu(); if (luid) headers["x-learning-user-id"] = luid; const payload = body == null ? null : JSON.stringify(body); return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== "function") { reject(new Error("\u4e91\u7aef\u8bf7\u6c42\u4e0d\u53ef\u7528")); return; } GM_xmlhttpRequest({ method: method || "POST", url, headers, data: payload, 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) { reject(new Error(String(data.detail || data.message || data.msg || `http_${status}`))); return; } resolve(data); }, onerror() { reject(new Error("\u4e91\u7aef\u7f51\u7edc\u9519\u8bef")); }, ontimeout() { reject(new Error("\u4e91\u7aef\u8bf7\u6c42\u8d85\u65f6")); }, }); }); } function normalizeUnixSec(v) { const n = Number(v || 0); if (!Number.isFinite(n) || n <= 0) return 0; if (n > 1e12) return Math.floor(n / 1000); return Math.floor(n); } function formatExpireText(expSec) { const sec = normalizeUnixSec(expSec); if (!sec) return "—"; const d = new Date(sec * 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())}`; } function _wcl(lease, exp, extra) { try { localStorage.setItem( CLOUD_LEASE_CACHE_KEY, JSON.stringify(Object.assign({ lease, exp }, extra || {})) ); } catch (_) {} } function _rcl() { try { return JSON.parse(localStorage.getItem(CLOUD_LEASE_CACHE_KEY) || "null"); } catch (_) { return null; } } function _wpe(token, exp) { try { localStorage.setItem( CLOUD_PRO_EXPIRE_CACHE_KEY, JSON.stringify({ token: String(token || ""), exp: Math.floor(Number(exp) || 0) }) ); } catch (_) {} } function _rpe() { try { return JSON.parse(localStorage.getItem(CLOUD_PRO_EXPIRE_CACHE_KEY) || "null"); } catch (_) { return null; } } function _wls(obj) { try { localStorage.setItem(CLOUD_LAST_STATE_KEY, JSON.stringify(obj || {})); } catch (_) {} } function _rls() { try { return JSON.parse(localStorage.getItem(CLOUD_LAST_STATE_KEY) || "null"); } catch (_) { return null; } } function _ald(data) { if (!data || typeof data !== "object") return; if (data.lease) state.cloudLease = String(data.lease); const exp = normalizeUnixSec(data.exp ?? data.expire_at ?? data.expires_at ?? data.lease_exp); if (exp > 0) state.cloudLeaseExp = exp; const tierRaw = data.tier ?? data.Tier ?? data.level ?? data.plan ?? data.license_type ?? data.licenseType ?? ""; if (tierRaw) state.cloudTier = String(tierRaw).trim().toLowerCase(); if (!String(state.cloudToken || "").trim() && state.cloudTier === "pro") { state.cloudTier = "free"; } const lim = data.free_course_limit ?? data.freeCourseLimit ?? data.free_chapter_limit ?? data.freeChapterLimit ?? data.free_limit; const used = data.free_used_courses ?? data.freeUsedCourses ?? data.free_used_chapters ?? data.freeUsedChapters ?? data.free_used; if (lim != null) state.freeCourseLimit = Number(lim) || FREE_COURSE_LIMIT; if (used != null) state.freeUsedCourses = Number(used) || 0; const bound = data.free_bound_courses ?? data.freeBoundCourses ?? data.bound_courses ?? data.boundCourses; if (Array.isArray(bound)) { state.freeBoundCourses = bound.map(String).filter(Boolean); } let proExp = normalizeUnixSec( data.pro_expires_at ?? data.proExpiresAt ?? data.pro_expire_at ?? data.proExpireAt ); if (!proExp && String(state.cloudTier).toLowerCase() === "pro" && state.cloudToken) { const cache = _rpe(); if (cache && String(cache.token || "") === state.cloudToken && cache.exp > 0) { proExp = Number(cache.exp); } else { proExp = Math.floor(Date.now() / 1000) + PRO_DAYS * 86400; } } if (proExp > 0 && String(state.cloudTier).toLowerCase() === "pro" && state.cloudToken) { state.cloudProExpireAt = proExp; _wpe(state.cloudToken, proExp); } if (!state.cloudTier) state.cloudTier = "free"; _wcl(state.cloudLease, state.cloudLeaseExp, { tier: state.cloudTier, freeCourseLimit: state.freeCourseLimit, freeUsedCourses: state.freeUsedCourses, freeBoundCourses: state.freeBoundCourses, proExpireAt: state.cloudProExpireAt, }); _wls({ tier: state.cloudTier, freeCourseLimit: state.freeCourseLimit, freeUsedCourses: state.freeUsedCourses, freeBoundCourses: state.freeBoundCourses, proExpireAt: state.cloudProExpireAt, }); } async function _ecl(force = false) { const now = Math.floor(Date.now() / 1000); if (!force && state.cloudLease && state.cloudLeaseExp - now > 60) { if (!state.cloudToken && state.cloudTier === "pro") state.cloudTier = "free"; _ucp(); return true; } if (!force) { const cached = _rcl(); if (cached && Number(cached.exp || 0) - now > 60) { state.cloudLease = String(cached.lease || ""); state.cloudLeaseExp = Number(cached.exp || 0); if (cached.tier) state.cloudTier = String(cached.tier); if (!state.cloudToken && state.cloudTier === "pro") state.cloudTier = "free"; if (cached.freeCourseLimit) state.freeCourseLimit = Number(cached.freeCourseLimit) || FREE_COURSE_LIMIT; if (cached.freeUsedCourses != null) state.freeUsedCourses = Number(cached.freeUsedCourses) || 0; if (Array.isArray(cached.freeBoundCourses)) { state.freeBoundCourses = cached.freeBoundCourses.map(String).filter(Boolean); } if (cached.proExpireAt && state.cloudToken) state.cloudProExpireAt = Number(cached.proExpireAt) || 0; _ucp(); return true; } } try { const luid = (await _elu()) || _glu(); const data = await _crq("/api/qingshu/lease", "POST", { learning_user_id: luid, school: state.school || detectSchool() || "", userName: String(state.userInfo?.userName || "").trim(), name: String(state.userInfo?.realName || state.userInfo?.name || "").trim(), }); state.cloudRevoked = false; _ald(data); _ucp(); return true; } catch (e) { const em = String(e?.message || e); if (/revoked|\u5df2\u7981\u7528|\u88ab\u7981\u7528/i.test(em)) { state.cloudRevoked = true; state.cloudTier = "revoked"; state.cloudLease = ""; state.cloudLeaseExp = 0; } else { const last = _rls(); if (last?.tier && state.cloudToken) state.cloudTier = last.tier; if (!state.cloudToken) state.cloudTier = "free"; } _ucp(); return false; } } async function _rca(courses, { commit = true } = {}) { const luid = await _elu(); if (!luid) { throw new Error("\u672a\u83b7\u53d6\u5230\u5b66\u4e60\u8d26\u53f7\uff0c\u8bf7\u5148\u8fdb\u5165\u5b66\u4e60\u4e2d\u5fc3\u518d\u8bd5"); } const okLease = await _ecl(true); if (!okLease || !state.cloudLease) { throw new Error("\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc\u540e\u91cd\u8bd5"); } const keys = (courses || []).map((c) => _ck(c)).filter(Boolean); if (!keys.length) throw new Error("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b"); let data; try { data = await _crq("/api/qingshu/study/authorize", "POST", { learning_user_id: luid, lease: state.cloudLease || "", course_keys: keys, commit: !!commit, }); } catch (e) { const msg = String(e?.message || e || ""); if (/free_quota_exhausted|too_many/i.test(msg)) { throw new Error("\u4f53\u9a8c\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5f00\u901a PRO \u540e\u7ee7\u7eed"); } throw new Error(msg || "\u4e91\u7aef\u62d2\u7edd\u6388\u6743"); } _ald(data); if (data?.allowed === false) { throw new Error(String(data.message || "\u4e91\u7aef\u62d2\u7edd\u6388\u6743")); } _ucp(); _esl(); return data; } async function _vct() { try { const data = await _crq("/api/qingshu/license/verify", "GET", null); _ald(data); await _ecl(true); _ucp(); _st("\u6821\u9a8c\u5b8c\u6210", "ok"); return true; } catch (e) { _st(String(e?.message || e || "\u6821\u9a8c\u5931\u8d25"), "error"); _ucp(); return false; } } async function _fcc() { try { const data = await _crq("/api/qingshu/client-config", "GET", null); if (data?.proBuyUrl) state.proBuyUrl = String(data.proBuyUrl).trim() || PRO_BUY_URL; if (data?.freeCourseLimit != null) { state.freeCourseLimit = Number(data.freeCourseLimit) || FREE_COURSE_LIMIT; } if (data?.panelNoticePath) { const p = String(data.panelNoticePath).trim(); if (p) state.panelNoticePath = p.startsWith("/") ? p : `/${p}`; } if (data?.panelNotice) state.remotePanelNotice = String(data.panelNotice); } catch (_) {} _ucp(); const ann = document.getElementById("qingshu-ann-text"); if (ann) ann.textContent = state.remotePanelNotice || PANEL_NOTICE_FALLBACK; } function tierLabel() { const t = String(state.cloudTier || "free").toLowerCase(); if (t === "pro") return "PRO"; if (t === "revoked") return "\u5df2\u7981\u7528"; if (t === "unknown") return "\u6821\u9a8c\u4e2d"; return "\u4f53\u9a8c"; } function _ucp() { const tierEl = document.getElementById("qingshu-cloud-tier"); if (tierEl) { tierEl.textContent = tierLabel(); tierEl.dataset.tier = String(state.cloudTier || "free").toLowerCase(); } const freeEl = document.getElementById("qingshu-cloud-free"); if (freeEl) { if (_ipu()) freeEl.textContent = "\u4e0d\u9650"; else freeEl.textContent = `${Math.min(state.selected.size, state.freeCourseLimit || 1)}/${state.freeCourseLimit || 1} \u95e8`; } const expEl = document.getElementById("qingshu-cloud-expire"); if (expEl) { expEl.textContent = _ipu() ? formatExpireText(state.cloudProExpireAt) : "—"; } const tokenInp = document.getElementById("qingshu-cloud-token"); if (tokenInp && tokenInp !== document.activeElement) { tokenInp.value = state.cloudToken || ""; } const tip = document.getElementById("qingshu-free-tip"); if (tip) { if (_ipu()) { tip.style.display = "none"; } else { const bound = (state.freeBoundCourses || []).filter(Boolean); tip.style.display = ""; tip.textContent = bound.length ? `\u4f53\u9a8c\u989d\u5ea6\u5df2\u7ed1\u5b9a\u4e91\u7aef\u8bfe\u7a0b\uff0c\u65e0\u6cd5\u66f4\u6362\uff1b\u5f00\u901a PRO \u53ef\u5b66\u5168\u90e8` : `\u4f53\u9a8c\u514d\u8d39 ${state.freeCourseLimit || 1} \u95e8\u8bfe\uff0c\u5f00\u8dd1\u540e\u7531\u4e91\u7aef\u9501\u5b9a\u989d\u5ea6`; } } } function _ovm(show) { const modal = document.getElementById("qingshu-vip-modal"); if (!modal) return; modal.classList.toggle("show", !!show); } function openProBuy() { const url = String(state.proBuyUrl || PRO_BUY_URL); try { if (typeof GM_openInTab === "function") GM_openInTab(url, { active: true }); else window.open(url, "_blank"); } catch (_) { location.href = url; } } function studyModeLabel() { const n = state.selected.size || 0; return `\u5df2\u9009 ${n} \u95e8`; } function _rrs() { const el = document.getElementById("qingshu-run-status"); if (!el) return; const parts = [ state.enabled ? "\u8fd0\u884c\u4e2d" : "\u5f85\u673a", studyModeLabel(), state.parallelActive > 0 ? `\u6d3b\u8dc3${state.parallelActive}\u8def` : "", state.currentTask || "", ]; if (state.currentCourse) parts.push(state.currentCourse); if (state.currentChapter) parts.push(state.currentChapter); if (state.queueTotal > 0) parts.push(`${state.queueDone}/${state.queueTotal} \u8282`); el.textContent = parts.filter(Boolean).join(" · "); } function mapCourseRow(c) { return { courseId: String(c.courseId || ""), courseName: String(c.courseName || ""), teachPlanCourseId: String(c.teachingPlanCourseId || c.teachPlanCourseId || ""), teachPlanId: String(c.teachPlanId || ""), periodId: String(c.periodId || ""), periodName: String(c.periodName || ""), isCurrent: c.isCurrent, usualScore: c.usualScore, learnStatus: Number(c.learnStatus), learnStatusName: String(c.learnStatusName || ""), allowLearn: c.allowLearn !== false, score: c.score, }; } function filterCurrentSemesterCourses(list) { const arr = (Array.isArray(list) ? list : []).map(mapCourseRow); const hasCurrentFlag = arr.some((c) => typeof c.isCurrent === "boolean"); if (hasCurrentFlag) return arr.filter((c) => c.isCurrent === true); const allowOnly = arr.filter((c) => c.allowLearn); return allowOnly.length ? allowOnly : arr; } function pruneStaleSelection() { const valid = new Set(state.courses.map((c) => _ck(c))); let removed = 0; for (const key of [...state.selected]) { if (!valid.has(key)) { state.selected.delete(key); removed += 1; } } if (removed > 0) saveQueue(); return removed; } function updateCurrentPeriodLabel(courses) { const cur = courses.find((c) => c.periodName); state.currentPeriodLabel = cur ? cur.periodName : ""; } function parseScoreBlock(html, summaryId) { const re = new RegExp(`id="${summaryId}"[\\s\\S]*?(?=)`, "i"); const block = html.match(re); if (!block) return { label: "", score: null, max: null }; const scoreM = block[0].match(/\s*([^<]+?)\s*<\/span>/); const titleM = block[0].match(/\s*([\s\S]*?)\(/); const maxM = block[0].match(/\((\d+(?:\.\d+)?)\u5206\)/); const scoreRaw = scoreM ? String(scoreM[1]).trim() : ""; const score = scoreRaw && scoreRaw !== "-" ? Number(scoreRaw) : null; const max = maxM ? Number(maxM[1]) : null; const label = titleM ? String(titleM[1]).replace(/\s+/g, "") : ""; return { label, score: Number.isFinite(score) ? score : null, max: Number.isFinite(max) ? max : null, }; } function scoreShortLabel(label, fallback) { const s = String(label || "").trim(); if (!s) return fallback; if (s.includes("\u767b\u5f55")) return "\u767b\u5f55"; if (s.includes("\u8bfe\u4ef6")) return "\u8bfe\u4ef6"; if (s.includes("\u4f5c\u4e1a")) return "\u4f5c\u4e1a"; if (s.includes("\u8ba8\u8bba")) return "\u8ba8\u8bba"; return s.length > 4 ? s.slice(0, 4) : s; } function parseCourseScoreHtml(html) { const usualM = html.match(/\u5e73\u65f6\u6210\u7ee9\uff1a(\d+(?:\.\d+)?)\u5206/); const totalM = html.match(/\u603b\u5206\uff1a(\d+(?:\.\d+)?)\u5206/); return { login: parseScoreBlock(html, "loginSummaryTbl"), study: parseScoreBlock(html, "studySummaryTbl"), quiz: parseScoreBlock(html, "quizSummaryTbl"), forum: parseScoreBlock(html, "forumSummaryTbl"), usual: usualM && Number.isFinite(Number(usualM[1])) ? Number(usualM[1]) : null, total: totalM && Number.isFinite(Number(totalM[1])) ? Number(totalM[1]) : null, }; } function formatScoreChip(label, row, title) { if (!row || row.score == null) { return `${escHtml(label)}-`; } const s = Number.isInteger(row.score) ? row.score : Math.round(row.score * 10) / 10; const max = row.max != null ? `/${Math.round(row.max)}` : ""; const full = row.label || title || label; return `${escHtml(label)}${s}${max}`; } function formatCourseScoreLine(detail) { if (!detail) return `\u5206\u9879\u5f97\u5206\u52a0\u8f7d\u4e2d…`; if (detail.error) return `\u5206\u9879\u5f97\u5206\u52a0\u8f7d\u5931\u8d25`; const chips = [ formatScoreChip(scoreShortLabel(detail.login?.label, "\u767b\u5f55"), detail.login, "\u767b\u5f55\u5f97\u5206"), formatScoreChip(scoreShortLabel(detail.study?.label, "\u8bfe\u4ef6"), detail.study, "\u8bfe\u4ef6\u5b66\u4e60"), formatScoreChip(scoreShortLabel(detail.quiz?.label, "\u4f5c\u4e1a"), detail.quiz, "\u8bfe\u7a0b\u4f5c\u4e1a"), formatScoreChip(scoreShortLabel(detail.forum?.label, "\u8ba8\u8bba"), detail.forum, "\u8bfe\u7a0b\u8ba8\u8bba"), ]; let summary = ""; if (detail.usual != null || detail.total != null) { const bits = []; if (detail.usual != null) bits.push(`\u5e73\u65f6${Math.round(detail.usual)}`); if (detail.total != null) bits.push(`\u603b${Math.round(detail.total)}`); summary = `${escHtml(bits.join(" · "))}`; } return `
${chips.join("")}${summary}
`; } async function _fsd(course) { const tpcId = course.teachPlanCourseId; if (!tpcId || !course.courseId) return { error: true }; const url = `${courseApiBase()}CourseScore?courseId=${encodeURIComponent(course.courseId)}&teachplanCourseId=${encodeURIComponent(tpcId)}`; try { const html = await _whs(() => _ahg(url)); return parseCourseScoreHtml(html); } catch (e) { return { error: true, message: e.message || String(e) }; } } async function fetchAllCourseScores() { for (let i = 0; i < state.courses.length; i++) { if (state.stopFlag) break; const course = state.courses[i]; course.scoreDetail = await _fsd(course); _rcl2(); if (i < state.courses.length - 1) await sleep(300); } } function assignmentStatusText(code) { return ASSIGNMENT_STATUS_TEXT[Number(code)] || `\u72b6\u6001${code}`; } function parseAssignmentListHtml(html) { if (/\u5f53\u524d\u8bfe\u7a0b\u6ca1\u6709\u4f5c\u4e1a/.test(html)) return []; const items = []; const parts = String(html || "").split(/class=["']exercise-body["']/i); for (let i = 1; i < parts.length; i++) { const part = parts[i]; const statusM = part.match(/\s*(\d+)\s*<\/span>/i); if (!statusM) continue; const quizM = part.match(/gotoExercisePaper\(\s*['"]([^'"]+)['"]/); if (!quizM) continue; const titleM = part.match(/]*>\s*([^<]+)/i); const scoreM = part.match(/\u5f97\u5206\uff1a\s*([\d.]+)\s*\u5206/); const totalM = part.match(/\u603b\u5206\uff1a\s*([\d.]+)\s*\u5206/); let title = titleM ? String(titleM[1]).trim() : ""; if (!title) title = `\u4f5c\u4e1a ${quizM[1].slice(0, 8)}…`; items.push({ quizId: quizM[1], title, status: parseInt(statusM[1], 10), statusText: assignmentStatusText(statusM[1]), score: scoreM ? Number(scoreM[1]) : null, totalScore: totalM ? Number(totalM[1]) : null, }); } const seen = new Set(); return items.filter((it) => { if (!it.quizId || seen.has(it.quizId)) return false; seen.add(it.quizId); return true; }); } function isHomeworkPending(it) { if (!it?.quizId) return false; const st = Number(it.status); if (st < HW_STATUS_DONE_MIN) return true; const total = Number(it.totalScore); const score = Number(it.score); if (Number.isFinite(total) && total > 0) { if (!Number.isFinite(score)) return true; return score + 1e-6 < total; } if (Number.isFinite(score)) return score <= 0; return false; } function homeworkNeedsRetest(it) { return Number(it?.status) >= HW_STATUS_DONE_MIN && isHomeworkPending(it); } function pendingHomeworkCount() { return state.assignments.reduce( (n, g) => n + (g.items || []).filter(isHomeworkPending).length, 0 ); } function upsertCourseAssignments(course, items) { const key = _ck(course); const idx = state.assignments.findIndex((g) => _ck(g.course) === key); const row = { course, items: Array.isArray(items) ? items : [] }; if (idx >= 0) state.assignments[idx] = row; else if (row.items.length) state.assignments.push(row); state.assignmentsLoaded = true; } async function refreshAssignmentsForCourses(courses, { retries = 3, gapMs = 800 } = {}) { const list = Array.isArray(courses) ? courses : []; for (const course of list) { if (state.stopFlag) break; let last = null; for (let r = 0; r < retries; r++) { const fetched = await fetchCourseAssignments(course); if (Array.isArray(fetched)) { last = fetched; upsertCourseAssignments(course, fetched); const pendingScore = fetched.some( (it) => Number(it.status) >= HW_STATUS_DONE_MIN && Number(it.totalScore) > 0 && !Number.isFinite(Number(it.score)) ); if (!pendingScore || r >= retries - 1) break; } else if (fetched?.error) { log(`\u300c${course.courseName}\u300d\u4f5c\u4e1a\u5237\u65b0\u5931\u8d25\uff1a${fetched.message}`); break; } await sleep(gapMs); } if (last) { for (const it of last) { if (it.score != null) { log( `\u300c${course.courseName}\u300d·${it.title} → ${it.score}${ it.totalScore != null ? `/${it.totalScore}` : "" }分 · ${it.statusText}` ); } } } } renderAssignmentList(); } function normalizeQuizAnswer(solution) { const s = String(solution ?? "").trim(); if (!s) return ""; if (/^[A-Za-z]([,\s]*[A-Za-z])+$/.test(s) && s.includes(",")) { return s .split(/[,\s]+/) .map((x) => x.trim().toUpperCase()) .filter(Boolean) .join(""); } return s.replace(/\s+/g, ""); } async function fetchQuizDetail(quizId) { return _aj("GET", `${studentApiBase()}DetailData?quizId=${encodeURIComponent(quizId)}`); } async function takeQuizRecord(quizId, referer) { const url = `${svcApiBase()}TakeQuizRecord?quizId=${encodeURIComponent(quizId)}`; const res = await fetch(tsUrl(url), { method: "GET", credentials: "include", headers: { ...defaultHeaders(false), Accept: "application/json, text/javascript, */*; q=0.01", ...(referer ? { Referer: referer } : {}), }, }); const text = await res.text(); if (text.trim().startsWith("<")) { const err = new Error(`TakeQuizRecord \u8fd4\u56de HTML HTTP ${res.status}`); err.html = true; throw err; } let json = null; try { json = text ? JSON.parse(text) : null; } catch (_) { throw new Error(`TakeQuizRecord \u975e JSON\uff1a${text.slice(0, 80)}`); } if (!res.ok || !json || json.hr !== 0) { throw new Error((json && json.message) || "TakeQuizRecord \u5931\u8d25"); } return json.data; } async function saveQuestionAnswer(quizId, questionId, answer, referer) { return _aj( "POST", `${svcApiBase()}SaveQuestionAnswer`, { quizId: String(quizId), questionAnswers: [{ questionId: String(questionId), answer: String(answer) }], }, undefined ).catch(() => { }); } async function submitExercisePaper(payload, referer) { const url = `${studentApiBase()}ExercisePaper`; const res = await fetch(tsUrl(url), { method: "POST", credentials: "include", headers: { ...defaultHeaders(true), Accept: "*/*", ...(referer ? { Referer: referer } : {}), }, body: JSON.stringify(payload), }); const text = await res.text(); if (text.trim().startsWith("<")) { const err = new Error(`\u63d0\u4ea4\u4f5c\u4e1a\u8fd4\u56de HTML HTTP ${res.status}`); err.html = true; throw err; } let json = null; try { json = text ? JSON.parse(text) : null; } catch (_) { throw new Error(`\u63d0\u4ea4\u4f5c\u4e1a\u975e JSON\uff1a${text.slice(0, 80)}`); } if (!res.ok || !json || json.hr !== 0) { throw new Error((json && json.message) || "\u63d0\u4ea4\u4f5c\u4e1a\u5931\u8d25"); } return json.data; } async function doOneHomework(course, item) { if (!item.quizId) { return { skipped: true }; } if (!isHomeworkPending(item)) { return { skipped: true, done: true }; } const retest = homeworkNeedsRetest(item); let paperUrl = `${studentApiBase()}ExercisePaper?courseId=${encodeURIComponent(course.courseId)}&quizId=${encodeURIComponent(item.quizId)}&teachPlanId=${encodeURIComponent(course.teachPlanId)}&periodId=${encodeURIComponent(course.periodId)}`; if (retest) paperUrl += "&isRetest=true"; state.currentTask = `\u4f5c\u4e1a ${item.title}${retest ? "·\u91cd\u505a" : ""}`; state.currentCourse = course.courseName; _rrs(); await _whs(() => _ahg(paperUrl).catch(() => "")); await takeQuizRecord(item.quizId, paperUrl); await sleep(300); let detail = await fetchQuizDetail(item.quizId); let userQuizId = detail?.userQuiz?.userQuizId; if (!userQuizId) { await sleep(500); detail = await fetchQuizDetail(item.quizId); userQuizId = detail?.userQuiz?.userQuizId; } if (!userQuizId) throw new Error("\u672a\u83b7\u53d6\u5230 userQuizId\uff0c\u8bf7\u624b\u52a8\u6253\u5f00\u4e00\u6b21\u4f5c\u4e1a\u9875"); const questions = detail?.paperDetail?.questions || []; if (!questions.length) throw new Error("\u8bd5\u5377\u65e0\u9898\u76ee"); const answered = []; let miss = 0; for (let i = 0; i < questions.length; i++) { if (state.stopFlag) return { stopped: true }; const q = questions[i]; const ans = normalizeQuizAnswer(q.solution); if (!ans) { miss += 1; answered.push({ questionId: String(q.questionId), userAnswer: String(q.userAnswer || ""), sequence: Number(q.sequence) || i + 1, }); continue; } answered.push({ questionId: String(q.questionId), userAnswer: ans, sequence: Number(q.sequence) || i + 1, }); await saveQuestionAnswer(item.quizId, q.questionId, ans, paperUrl); if (i < questions.length - 1) await sleep(HOMEWORK_ANSWER_GAP_MS); } if (miss === questions.length) { return { skipped: true, noAnswer: true }; } const timeSpend = Math.max( 60, Math.min(7200, questions.length * randInt(4, 12) + randInt(20, 90)) ); await submitExercisePaper( { quizId: String(item.quizId), quizType: "exercise", userQuizId: String(userQuizId), status: 2, timeSpend, questions: answered, }, paperUrl ); for (let r = 0; r < 5; r++) { await sleep(r === 0 ? 600 : 1000); const fresh = await fetchCourseAssignments(course); if (!Array.isArray(fresh)) continue; upsertCourseAssignments(course, fresh); const hit = fresh.find((x) => x.quizId === item.quizId); if (hit) { Object.assign(item, hit); if (hit.score != null) break; } renderAssignmentList(); } if (item.score == null) { item.status = Math.max(Number(item.status) || 0, 2); item.statusText = assignmentStatusText(item.status); } renderAssignmentList(); return { done: true, answered: answered.length - miss, miss, retest }; } async function processHomework(courses) { const list = courses && courses.length ? courses : _gsc(); if (!list.length) { log("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b"); return; } log("\u5f00\u59cb\u5904\u7406\u52fe\u9009\u8bfe\u7a0b\u4f5c\u4e1a"); state.currentTask = `\u4f5c\u4e1a ${list.length} \u8bfe`; _rrs(); let anyPending = false; let failN = 0; for (let ci = 0; ci < list.length; ci++) { if (state.stopFlag) break; const course = list[ci]; let items; const cached = state.assignments.find((g) => _ck(g.course) === _ck(course)); if (cached?.items?.length) { items = cached.items; } else { const fetched = await fetchCourseAssignments(course); items = Array.isArray(fetched) ? fetched : []; if (fetched?.error) { failN += 1; continue; } upsertCourseAssignments(course, items); } const todo = items.filter(isHomeworkPending); if (!todo.length && !items.length) { continue; } if (todo.length) { anyPending = true; for (const it of todo) { if (state.stopFlag) break; try { await doOneHomework(course, it); } catch (_) { failN += 1; } await sleep(800); } } await refreshAssignmentsForCourses([course]); const group = state.assignments.find((g) => _ck(g.course) === _ck(course)); const finalItems = group?.items || items || []; if (finalItems.length) { const parts = finalItems.map((it, idx) => { const title = String(it.title || `\u4f5c\u4e1a${idx + 1}`).trim() || `\u4f5c\u4e1a${idx + 1}`; const sc = it.score != null ? scoreNum(it.score) : "—"; return `${title}\u5f97\u5206${sc}`; }); log(`${_cln(course)}\u4f5c\u4e1a\u5904\u7406\u5b8c\u6210 ${parts.join(" ")}`); } if (ci < list.length - 1) await sleep(SCORE_COURSE_STAGGER_MS); } if (!anyPending) { log("\u6240\u52fe\u9009\u8bfe\u7a0b\u6682\u65e0\u4f5c\u4e1a\uff0c\u8df3\u8fc7\u540e\u671f\u5904\u7406"); } if (failN) log(`\u4f5c\u4e1a\u6709 ${failN} \u9879\u5931\u8d25`, "warn"); renderAssignmentList(); } async function fetchCourseAssignments(course) { const url = `${courseApiBase()}AssignmentList?courseId=${encodeURIComponent(course.courseId)}&teachplanCourseId=${encodeURIComponent(course.teachPlanCourseId)}&teachPlanId=${encodeURIComponent(course.teachPlanId)}&periodId=${encodeURIComponent(course.periodId)}`; try { const html = await _whs(() => _ahg(url)); return parseAssignmentListHtml(html); } catch (e) { return { error: true, message: e.message || String(e) }; } } async function fetchAllAssignments() { state.assignments = []; state.assignmentsLoaded = false; renderAssignmentList(); for (let i = 0; i < state.courses.length; i++) { if (state.stopFlag) break; const course = state.courses[i]; const items = await fetchCourseAssignments(course); if (Array.isArray(items)) { if (items.length) state.assignments.push({ course, items }); } else if (items?.error) { } renderAssignmentList(); if (i < state.courses.length - 1) await sleep(300); } state.assignmentsLoaded = true; renderAssignmentList(); } function findCourseForExam(exam) { const tpc = String(exam.teachPlanCourseId || ""); const cid = String(exam.courseId || ""); return state.courses.find( (c) => String(c.teachPlanCourseId) === tpc || String(c.courseId) === cid ); } function filterCurrentSemesterExams(data, courses) { const tpcSet = new Set(courses.map((c) => String(c.teachPlanCourseId))); const out = []; for (const [key, label] of EXAM_GROUP_KEYS) { const arr = data?.[key]; if (!Array.isArray(arr)) continue; for (const e of arr) { const tpc = String(e.teachPlanCourseId || ""); if (e.isCurrent === true || e.current === true || tpcSet.has(tpc)) { out.push({ ...e, examGroup: label }); } } } return out; } async function fetchExams() { state.examsLoaded = false; state.examsError = ""; renderExamList(); try { const data = await _aj("POST", `${studentApiBase()}ExamDatas`, null); state.exams = filterCurrentSemesterExams(data, state.courses); } catch (e) { state.exams = []; state.examsError = e.message || String(e); } finally { state.examsLoaded = true; renderExamList(); } } function assignmentCount() { return state.assignments.reduce((n, g) => n + (g.items?.length || 0), 0); } function switchTab(tabId) { state.activeTab = tabId; localStorage.setItem(ACTIVE_TAB_KEY, tabId); document.querySelectorAll(".qingshu-tab").forEach((btn) => { btn.classList.toggle("active", btn.getAttribute("data-tab") === tabId); }); document.querySelectorAll(".qingshu-tab-panel").forEach((panel) => { panel.classList.toggle("hidden", panel.getAttribute("data-tab") !== tabId); }); const courseTools = document.getElementById("qingshu-course-tools"); const runRow = document.getElementById("qingshu-run-row"); const isCourse = tabId === "courses"; if (courseTools) courseTools.classList.toggle("hidden", !isCourse); if (runRow) runRow.classList.toggle("hidden", !isCourse); } function bindTabEvents() { document.querySelectorAll(".qingshu-tab").forEach((btn) => { btn.addEventListener("click", () => { const tab = btn.getAttribute("data-tab"); if (tab) switchTab(tab); }); }); switchTab(state.activeTab); } async function _fui() { const fromJwt = _pjt(); if (fromJwt) _rlu(fromJwt); const data = await _aj("GET", `${schoolBase()}/Student/Svc/UserInfo`, null).catch( () => null ); if (data) { state.userInfo = data; const id = _puo(data); if (id) _rlu(id); } return data; } async function _fcs() { state.school = detectSchool(); if (!state.school) throw new Error("\u65e0\u6cd5\u8bc6\u522b\u5b66\u6821\u8def\u5f84\uff08\u8bf7\u5728\u5b66\u4e60\u4e2d\u5fc3\u9875\u9762\u4f7f\u7528\uff09"); const list = await _aj("POST", `${courseApiBase()}CourseData`, null); state.courses = filterCurrentSemesterCourses(list); updateCurrentPeriodLabel(state.courses); pruneStaleSelection(); return state.courses; } function parseCourseStudyHtml(html, course) { const teachPlanCourseId = html.match(/teachPlanCourseId:\s*['"](\d+)['"]/)?.[1] || course.teachPlanCourseId; const treeRel = html.match(/Svc\/GetCoursewareTree\?[^'"]+/)?.[0]; const treeUrl = treeRel ? `${svcBase()}${treeRel}` : ""; const classId = html.match(/teachPlanId:\s*['"](\d+)['"]/)?.[1] || course.teachPlanId; const courseId = html.match(/courseId:\s*['"](\d+)['"]/)?.[1] || course.courseId; const periodId = html.match(/periodId:\s*['"](\d+)['"]/)?.[1] || course.periodId; return { teachPlanCourseId, treeUrl, classId, courseId, periodId }; } function isScorePartFull(part, fallbackMax) { if (!part) return false; const max = Number(part.max ?? fallbackMax ?? 0); const score = Number(part.score ?? 0); return max > 0 && score >= max; } function isCourseAllScoresFull(course) { const d = course?.scoreDetail; if (!d || d.error) return false; const r = courseRules(course); return ( isScorePartFull(d.login, r.loginMax) && isScorePartFull(d.forum, r.forumMax) && isScorePartFull(d.study, r.studyMax) ); } function _msf(course) { const ck = _ck(course); if (state.courseStudyFull.has(ck)) return true; const d = course.scoreDetail; const r = courseRules(course); if (d?.study && !d.error && isScorePartFull(d.study, r.studyMax)) { state.courseStudyFull.add(ck); return true; } return false; } function _lsc(course) { const ck = _ck(course); if (!ck || state.studyCompleteLogged.has(ck)) return; state.studyCompleteLogged.add(ck); log(`${_cln(course)}\u8bfe\u4ef6\u5b66\u65f6\u5206\u5b8c\u6210 \u5f97\u5206${scoreNum(partCur(course, "study"))}`); } function _tsw() { if (studyScoreWatchTimer) { clearInterval(studyScoreWatchTimer); studyScoreWatchTimer = null; } } async function _cso(courses) { for (const course of courses) { if (state.stopFlag) break; const ck = _ck(course); if (state.courseStudyFull.has(ck)) continue; const detail = await _fsd(course); course.scoreDetail = detail; mergeRulesMaxFromDetail(courseRules(course), detail); const r = courseRules(course); if (isScorePartFull(detail?.study, r.studyMax)) { state.courseStudyFull.add(ck); _lsc(course); } if (courses.length > 3) await sleep(400); } _rcl2(); } function _ssw(courses) { _tsw(); for (const course of courses) _msf(course); void _cso(courses); studyScoreWatchTimer = setInterval(() => { if (state.stopFlag) { _tsw(); return; } void _cso(courses); }, STUDY_SCORE_CHECK_MS); } function walkVideos(nodes, out = []) { for (const n of nodes || []) { if (n && n.type === "video" && n.id) { out.push({ id: String(n.id), name: String(n.name || n.id) }); } if (n && n.nodes) walkVideos(n.nodes, out); } return out; } async function fetchCourseContext(course) { const url = `${courseApiBase()}CourseStudy?courseId=${encodeURIComponent(course.courseId)}&teachPlanId=${encodeURIComponent(course.teachPlanId)}&periodId=${encodeURIComponent(course.periodId)}`; const html = await _ahg(url); try { const rules = parseScoreRules(html); mergeRulesMaxFromDetail(rules, course.scoreDetail); course.scoreRules = rules; } catch (_) {} const ctx = parseCourseStudyHtml(html, course); if (!ctx.treeUrl) throw new Error("\u672a\u89e3\u6790\u5230\u8bfe\u4ef6\u76ee\u5f55\u5730\u5740\uff0c\u8bf7\u786e\u8ba4\u5df2\u9009\u5b66\u671f\u8bfe\u7a0b"); const tree = await _aj("GET", ctx.treeUrl, null); const nodes = tree && tree.nodes ? tree.nodes : Array.isArray(tree) ? tree : []; return { ...ctx, treeNodes: nodes, treeName: tree && tree.name ? tree.name : "" }; } async function trackCourse(course, ctx) { const studyUrl = courseStudyUrl(course, ctx); await _aj( "POST", `${courseApiBase()}StudentCourseTrack`, { courseId: String(ctx.courseId || course.courseId), teachPlanId: String(ctx.classId || course.teachPlanId), periodId: String(ctx.periodId || course.periodId), }, { referrer: studyUrl, accept: "application/json" } ).catch(() => {}); await _aj( "POST", `${courseApiBase()}LoginDetail`, { teachPlanCourseId: Number(ctx.teachPlanCourseId || course.teachPlanCourseId), }, { referrer: `${studyUrl}&tabType=score`, accept: "application/json" } ).catch(() => {}); } async function _cwr(recordId, fields, retriesOrOpts = HTML_RETRY) { const opts = retriesOrOpts != null && typeof retriesOrOpts === "object" ? retriesOrOpts : { retries: retriesOrOpts }; const retries = Number(opts.retries) > 0 ? Number(opts.retries) : HTML_RETRY; const abortIf = typeof opts.abortIf === "function" ? opts.abortIf : null; let lastErr = null; for (let i = 0; i < retries; i++) { if (abortIf && abortIf()) return; try { await _wus(() => _amp(`${courseApiBase()}UploadStudyRecordContinue`, fields, opts) ); return; } catch (e) { lastErr = e; const retryable = e.html || e.hr === -2004318076 || [412, 429, 502, 503, 504].includes(e.status); if (retryable && i < retries - 1) { if (abortIf && abortIf()) return; await sleep(_brw(i)); continue; } throw e; } } throw lastErr; } async function _bss(ctx, course, node, position = 0, showRef) { const showUrl = showRef || courseShowUrl(ctx, course, node.id); await _whs(() => _ahg(showUrl).catch(() => "")); const beginBody = { classId: String(ctx.classId || course.teachPlanId), courseId: String(ctx.courseId || course.courseId), contentId: node.id, contentType: CONTENT_TYPE_COURSEWARE, periodId: String(ctx.periodId || course.periodId), position: Math.floor(position), detectId: null, }; const recordId = await _ajr( "POST", `${courseApiBase()}UploadStudyRecordBegin`, beginBody, { referrer: showUrl, accept: "application/json" } ); if (!recordId) throw new Error("Begin \u672a\u8fd4\u56de recordId"); return recordId; } async function _ess(recordId, position, rules, showRef) { const waitSec = randReportWaitSec(rules); await sleep(waitSec * 1000); await _cwr( recordId, { recordId, end: "true", position: Math.floor(position), timeOutConfirm: "false", }, { referrer: showRef } ); } async function _sov(task) { const { course, ctx, node, routeIndex } = task; const tag = taskTag(task); const ck = _ck(course); const rules = courseRules(course); const parallel = Math.max(1, Number(task.parallel) || 1); const showUrl = courseShowUrl(ctx, course, node.id); const contOpts = { referrer: showUrl, abortIf: () => state.stopFlag || state.courseStudyFull.has(ck), }; let hangMin = Number(task.hangMin) || studyHangMinutes(course, parallel); if (hangMin <= 0) { return { skipped: true, done: false, course, full: true }; } const contMin = Number(rules.studyContinuousMin) || DEFAULT_STUDY_CONTINUOUS_MIN; const contSec = Math.max(60, Math.floor(contMin * 60)); state.parallelActive += 1; _rrs(); try { await sleep(routeIndex * BEGIN_STAGGER_MS); if (state.stopFlag || state.courseStudyFull.has(ck)) { return { skipped: state.courseStudyFull.has(ck), done: false, course }; } let recordId = await _bss(ctx, course, node, 0, showUrl); let position = 0; let loopCount = 0; const routeStarted = Date.now(); let sessionStarted = routeStarted; let endAt = routeStarted + hangMin * 60 * 1000; const maxEndAt = routeStarted + hangMin * 60 * 1000 * 2.5; while (!state.stopFlag && !state.courseStudyFull.has(ck)) { if (Date.now() >= endAt) { try { course.scoreDetail = await _fsd(course); mergeRulesMaxFromDetail(courseRules(course), course.scoreDetail); } catch (_) {} if (isScorePartFull(course.scoreDetail?.study, courseRules(course).studyMax)) { state.courseStudyFull.add(ck); break; } const remain = studyHangMinutes(course, parallel); if (remain <= 0) { state.courseStudyFull.add(ck); break; } if (Date.now() >= maxEndAt) break; const add = Math.min(remain, Math.max(1, (maxEndAt - Date.now()) / 60000)); endAt = Date.now() + add * 60 * 1000; continue; } const waitSec = randReportWaitSec(rules); const doneM = (Date.now() - routeStarted) / 60000; state.currentTask = `${_cln(course)}\u8bfe\u4ef6 ${doneM.toFixed(0)}/${Math.round(hangMin)}min · ` + `${scoreNum(partCur(course, "study"))}/${scoreNum(rules.studyMax)}`; _rrs(); await sleep(waitSec * 1000); if (state.stopFlag || state.courseStudyFull.has(ck)) break; const stepSec = randInt(POSITION_STEP_MIN_SEC, POSITION_STEP_MAX_SEC); let next = position + stepSec; const sessionElapsed = (Date.now() - sessionStarted) / 1000; if (next > LOOP_POSITION_MAX && sessionElapsed >= contSec) { await _cwr( recordId, { recordId, end: "true", position: LOOP_POSITION_MAX, timeOutConfirm: "false", }, contOpts ).catch(() => {}); if (state.courseStudyFull.has(ck)) break; loopCount += 1; position = 0; recordId = await _bss(ctx, course, node, 0, showUrl); sessionStarted = Date.now(); const remain = studyHangMinutes(course, parallel); if (remain <= 0) { state.courseStudyFull.add(ck); break; } endAt = Math.min(maxEndAt, Date.now() + remain * 60 * 1000); continue; } if (next > LOOP_POSITION_MAX) next = LOOP_POSITION_MAX; position = next; await _cwr( recordId, { recordId, end: "false", position, timeOutConfirm: "false", }, contOpts ); } if (state.courseStudyFull.has(ck)) { try { await _ess(recordId, position, rules, showUrl); } catch (_) {} return { skipped: true, done: false, course, full: true }; } if (!state.stopFlag) { await _ess(recordId, position, rules, showUrl); return { skipped: false, done: true, course, ctx }; } return { skipped: false, done: false, course }; } catch (e) { log(`${_cln(course)}\u8bfe\u4ef6\u5b66\u65f6\u5931\u8d25\uff1a${e.message || e}`, "error"); return { skipped: false, done: false, course, error: e }; } finally { state.parallelActive = Math.max(0, state.parallelActive - 1); _rrs(); } } function pickVideoBatch(videos) { const maxN = state.maxVideos; if (!maxN || maxN <= 0) return videos; return videos.slice(0, maxN); } async function _pct(course, routeBase) { if (_msf(course)) { return []; } if (!course.allowLearn) { return []; } await _esr(course, { silent: true }); const ctx = await fetchCourseContext(course); course.teachPlanCourseId = ctx.teachPlanCourseId || course.teachPlanCourseId; await trackCourse(course, ctx); const allVideos = walkVideos(ctx.treeNodes); if (!allVideos.length) { return []; } const batch = pickVideoBatch(allVideos); const rules = courseRules(course); const cur = partCur(course, "study"); const needMin = studyNeedContinuousMinutes(rules, cur); if (needMin <= 0 || isScorePartFull(course.scoreDetail?.study, rules.studyMax)) { state.courseStudyFull.add(_ck(course)); return []; } const parallel = batch.length; const hangMin = studyHangMinutes(course, parallel); if (hangMin <= 0) return []; log( `${_cln(course)}\u8bfe\u4ef6\u5b66\u65f6\u5206\u6b63\u5728\u5904\u7406\u9884\u8ba1${Math.round(hangMin)}\u5206\u949f\u5b8c\u6210` ); return batch.map((node, i) => ({ course, ctx, node, routeIndex: routeBase + i, hangMin, parallel, })); } function _gsc() { _esl(); return state.courses.filter( (c) => state.selected.has(_ck(c)) && c.allowLearn !== false ); } async function enterCourseOnce(course) { const url = `${courseApiBase()}CourseStudy?courseId=${encodeURIComponent(course.courseId)}&teachPlanId=${encodeURIComponent(course.teachPlanId)}&periodId=${encodeURIComponent(course.periodId)}`; const html = await _whs(() => _ahg(url).catch(() => "")); if (html) { try { const rules = parseScoreRules(html); mergeRulesMaxFromDetail(rules, course.scoreDetail); course.scoreRules = rules; } catch (_) {} } await _aj( "POST", `${courseApiBase()}StudentCourseTrack`, { courseId: String(course.courseId), teachPlanId: String(course.teachPlanId), periodId: String(course.periodId), }, { referrer: url, accept: "application/json" } ).catch(() => {}); const data = await _aj( "POST", `${courseApiBase()}LoginDetail`, { teachPlanCourseId: Number(course.teachPlanCourseId), }, { referrer: `${url}&tabType=score`, accept: "application/json" } ); return Array.isArray(data) ? data.length : 0; } async function fetchForumStats(teachPlanCourseId) { const data = await _aj("POST", `${courseApiBase()}ForumDetail`, { teachPlanCourseId: Number(teachPlanCourseId), }); const total = Number(data?.total) || 0; const reply = Number(data?.reply) || 0; return { total, reply, sum: total + reply }; } async function createForumPost(course, seq) { const name = String(course.courseName || "\u672c\u8bfe\u7a0b").replace(/\(.*?\)/g, "").trim(); const topics = [ `${name}\u7684\u6838\u5fc3\u6982\u5ff5\u5982\u4f55\u7406\u89e3\uff1f`, `\u5b66\u4e60${name}\u6709\u4ec0\u4e48\u65b9\u6cd5\uff1f`, `${name}\u91cd\u70b9\u96be\u70b9\u8bf7\u6559`, `${name}\u7ae0\u8282\u5185\u5bb9\u8ba8\u8bba`, `\u5173\u4e8e${name}\u7684\u8bfe\u540e\u601d\u8003`, `${name}\u77e5\u8bc6\u70b9\u5f52\u7eb3\u5206\u4eab`, `${name}\u5b66\u4e60\u5fc3\u5f97\u4ea4\u6d41`, `\u5927\u5bb6\u5bf9${name}\u6709\u4ec0\u4e48\u7591\u95ee\uff1f`, ]; const title = topics[randInt(0, topics.length - 1)]; const content = `

${title}\uff08\u6253\u5361${seq}\uff09\u6b22\u8fce\u540c\u5b66\u4e00\u8d77\u8ba8\u8bba\u3002

`; await _afp(`${courseApiBase()}EditPost`, { courseId: course.courseId, teachplanCourseId: course.teachPlanCourseId, title, contentTxt: content, }); } async function _rts(courses) { _ssw(courses); const allTasks = []; let routeBase = 0; try { for (let ci = 0; ci < courses.length; ci++) { if (state.stopFlag) break; if (ci > 0) await sleep(COURSE_PREP_GAP_MS); state.currentCourse = courses[ci].courseName; state.currentTask = `\u51c6\u5907\u8bfe\u65f6\u5206 ${ci + 1}/${courses.length}…`; _rrs(); const tasks = await _pct(courses[ci], routeBase); allTasks.push(...tasks); routeBase += tasks.length; } if (!allTasks.length) { return; } state.currentTask = `\u8bfe\u4ef6\u5206 ${allTasks.length} \u8def\u5e76\u884c`; _rrs(); const results = await runPool(allTasks, allTasks.length, (task) => _sov(task) ); const studyDetailDone = new Set(); for (let i = 0; i < results.length; i++) { const r = results[i]; if (!r?.done && !r?.full) continue; const t = allTasks[i]; const ck = _ck(t.course); if (!studyDetailDone.has(ck) && t.ctx) { studyDetailDone.add(ck); await _aj("POST", `${courseApiBase()}StudyDetail`, { teachPlanCourseId: Number(t.ctx.teachPlanCourseId || t.course.teachPlanCourseId), }).catch(() => {}); } } } finally { _tsw(); } } async function _pss(courses) { log("\u5f00\u59cb\u5904\u7406\u52fe\u9009\u8bfe\u7a0b\u8bfe\u4ef6\u5b66\u65f6\u5206"); state.studyCompleteLogged = new Set(); await _ecr(courses); const needCourses = []; for (const course of courses) { if (state.stopFlag) break; let detail = course.scoreDetail || (await _fsd(course)); course.scoreDetail = detail; mergeRulesMaxFromDetail(courseRules(course), detail); const rules = courseRules(course); const current = partCur(course, "study"); const max = Number(rules.studyMax) || DEFAULT_STUDY_MAX; if (current >= max) { _msf(course); _lsc(course); } else { needCourses.push(course); } } if (needCourses.length && !state.stopFlag) { await _rts(needCourses); for (const course of needCourses) { if (state.stopFlag) break; try { course.scoreDetail = await _fsd(course); } catch (_) {} _lsc(course); } } } function summarizeScoreResults(_results, _label) { } async function loginScoreForCourse(course, courseIndex) { await sleep((courseIndex || 0) * SCORE_COURSE_STAGGER_MS); if (state.stopFlag) return { course, skipped: true }; let detail = course.scoreDetail || (await _fsd(course)); course.scoreDetail = detail; await _esr(course, { silent: true }); const rules = courseRules(course); const need = loginNeed(rules, partCur(course, "login")); if (need <= 0) { log(`${_cln(course)}\u767b\u5f55\u5206\u5b8c\u6210\u5f97\u5206${scoreNum(partCur(course, "login"))}`); return { course, skipped: true, full: true }; } for (let i = 0; i < need && !state.stopFlag; i++) { await enterCourseOnce(course); if (i < need - 1) await sleep(LOGIN_GAP_MS); } try { course.scoreDetail = await _fsd(course); } catch (_) {} log(`${_cln(course)}\u767b\u5f55\u5206\u5b8c\u6210\u5f97\u5206${scoreNum(partCur(course, "login"))}`); return { course, done: true }; } async function forumScoreForCourse(course, courseIndex) { await sleep((courseIndex || 0) * SCORE_COURSE_STAGGER_MS); if (state.stopFlag) return { course, skipped: true }; let detail = course.scoreDetail || (await _fsd(course)); course.scoreDetail = detail; await _esr(course, { silent: true }); const rules = courseRules(course); const need = forumNeed(rules, partCur(course, "forum")); let stats = { total: 0, reply: 0, sum: 0 }; try { stats = await fetchForumStats(course.teachPlanCourseId); } catch (_) {} if (need <= 0) { log(`${_cln(course)}\u8ba8\u8bba\u5206\u5b8c\u6210 \u5f97\u5206${scoreNum(partCur(course, "forum"))}`); return { course, skipped: true, full: true }; } for (let i = 0; i < need && !state.stopFlag; i++) { try { await createForumPost(course, stats.total + i + 1); } catch (e) { log(`${_cln(course)}\u8ba8\u8bba\u53d1\u5e16\u5931\u8d25\uff1a${e.message || e}`, "error"); return { course, error: e }; } if (i < need - 1) await sleep(FORUM_GAP_MS); } try { course.scoreDetail = await _fsd(course); } catch (_) {} log(`${_cln(course)}\u8ba8\u8bba\u5206\u5b8c\u6210 \u5f97\u5206${scoreNum(partCur(course, "forum"))}`); return { course, done: true }; } async function _pls(courses) { await _ecr(courses); log("\u5f00\u59cb\u5904\u7406\u52fe\u9009\u8bfe\u7a0b\u767b\u5f55\u5206"); state.currentTask = `\u767b\u5f55\u5206 ${courses.length} \u8bfe`; _rrs(); const results = await runPool(courses, courses.length, (course, i) => loginScoreForCourse(course, i) ); summarizeScoreResults(results, "\u767b\u5f55\u5206"); } async function _pfs(courses) { await _ecr(courses); log("\u5f00\u59cb\u5904\u7406\u52fe\u9009\u8bfe\u7a0b\u8ba8\u8bba\u5206"); state.currentTask = `\u8ba8\u8bba\u5206 ${courses.length} \u8bfe`; _rrs(); const results = await runPool(courses, courses.length, (course, i) => forumScoreForCourse(course, i) ); summarizeScoreResults(results, "\u8ba8\u8bba\u5206"); } async function refreshSelectedScores(courses) { for (const course of courses) { course.scoreDetail = await _fsd(course); _rcl2(); await sleep(300); } } async function _pes(_courses) { log("\u8003\u8bd5\u529f\u80fd\u540e\u671f\u5f00\u653e\uff0c\u6682\u8df3\u8fc7"); } async function _rst(label, worker) { if (state.runBusy || state.scoreTaskBusy) { log("\u5df2\u6709\u4efb\u52a1\u8fd0\u884c\u4e2d"); return; } const courses = _gsc(); if (!courses.length) { log("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b"); return; } state.scoreTaskBusy = true; state.stopFlag = false; state.currentTask = `${label}\u5904\u7406\u4e2d…`; syncScoreButtons(); _rrs(); try { await _rca(courses, { commit: true }); await worker(courses); if (!state.stopFlag) { await refreshSelectedScores(courses); if (label === "\u4f5c\u4e1a") { await refreshAssignmentsForCourses(courses, { retries: 4, gapMs: 1000 }); } } else { log("\u5df2\u505c\u6b62"); } } catch (e) { log(`${label}\u5931\u8d25\uff1a${e.message || e}`, "error"); _st(String(e?.message || e), "error"); } finally { state.scoreTaskBusy = false; state.currentTask = "\u5f85\u673a"; state.currentCourse = ""; syncScoreButtons(); _rrs(); } } async function _rq() { if (state.runBusy) return; if (!state.selected.size) { log("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b"); return; } if (state.scoreTaskBusy) { log("\u5df2\u6709\u5206\u9879\u4efb\u52a1\u8fd0\u884c\u4e2d"); return; } state.runBusy = true; state.scoreTaskBusy = false; state.enabled = true; state.stopFlag = false; state.queueDone = 0; state.queueTotal = 0; state.courseStudyFull.clear(); state.studyCompleteLogged = new Set(); syncStartStop(); syncScoreButtons(); _rrs(); try { if (!state.courses.length) await _fcs(); await _fui().catch(() => {}); const queue = _gsc(); if (!queue.length) { log("\u8bf7\u5148\u52fe\u9009\u8bfe\u7a0b", "warn"); return; } await _rca(queue, { commit: true }); if (!_ipu() && queue.length > _msc()) { _st("\u5f53\u524d\u4ec5\u53ef\u9009\u62e9 1 \u95e8\u8bfe\u7a0b", "warn"); return; } await refreshSelectedScores(queue); if (state.stopFlag) { log("\u5df2\u505c\u6b62"); return; } await _ecr(queue); if (state.stopFlag) { log("\u5df2\u505c\u6b62"); return; } const todo = []; for (const course of queue) { if (isCourseAllScoresFull(course)) { state.courseStudyFull.add(_ck(course)); } else { todo.push(course); } } if (!todo.length) { log("\u52fe\u9009\u8bfe\u7a0b\u4e09\u9879\u5f97\u5206\u5747\u5df2\u6ee1\uff0c\u65e0\u9700\u5904\u7406"); return; } state.currentTask = "① \u767b\u5f55\u5206…"; _rrs(); await _pls(todo); if (state.stopFlag) { log("\u5df2\u505c\u6b62"); return; } await refreshSelectedScores(todo); state.currentTask = "② \u8ba8\u8bba\u5206…"; _rrs(); await _pfs(todo); if (state.stopFlag) { log("\u5df2\u505c\u6b62"); return; } await refreshSelectedScores(todo); state.currentTask = "③ \u4f5c\u4e1a…"; _rrs(); await processHomework(todo); if (state.stopFlag) { log("\u5df2\u505c\u6b62"); return; } await refreshSelectedScores(todo); const needStudy = todo.filter((c) => { if (isCourseAllScoresFull(c)) { state.courseStudyFull.add(_ck(c)); return false; } const r = courseRules(c); return !isScorePartFull(c.scoreDetail?.study, r.studyMax); }); if (needStudy.length && !state.stopFlag) { state.currentTask = "④ \u8bfe\u65f6\u5206…"; _rrs(); await _pss(needStudy); } if (!state.stopFlag) { state.currentTask = "⑤ \u8003\u8bd5…"; _rrs(); await _pes(todo); } if (!state.stopFlag) { await refreshSelectedScores(todo); for (const course of todo) { if (isCourseAllScoresFull(course)) { state.courseStudyFull.add(_ck(course)); } } log("\u5168\u90e8\u5904\u7406\u5b8c\u6210"); } else { log("\u5df2\u505c\u6b62"); } } catch (e) { log(`\u8fd0\u884c\u5931\u8d25\uff1a${e.message || e}`, "error"); _st(String(e?.message || e), "error"); } finally { state.enabled = false; state.runBusy = false; state.parallelActive = 0; state.currentTask = "\u5f85\u673a"; state.currentCourse = ""; state.currentChapter = ""; _tsw(); syncStartStop(); syncScoreButtons(); _rrs(); } } function stopRun() { state.stopFlag = true; _tsw(); state.currentTask = "\u6b63\u5728\u505c\u6b62…"; _rrs(); log("\u505c\u6b62\u4fe1\u53f7\u5df2\u53d1\u9001"); } async function refreshCourseList() { if (state.scoreTaskBusy || state.runBusy) { log("\u4efb\u52a1\u8fd0\u884c\u4e2d\uff0c\u8bf7\u7ed3\u675f\u540e\u518d\u5237\u65b0"); return; } const listEl = document.getElementById("qingshu-course-list"); if (!listEl) return; listEl.innerHTML = `
\u52a0\u8f7d\u4e2d…
`; const assignEl = document.getElementById("qingshu-assignment-list"); const examEl = document.getElementById("qingshu-exam-list"); if (assignEl) assignEl.innerHTML = `
\u52a0\u8f7d\u4e2d…
`; if (examEl) examEl.innerHTML = `
\u52a0\u8f7d\u4e2d…
`; try { await _fcs(); _rcl2(); await fetchAllCourseScores(); await fetchAllAssignments(); await fetchExams(); log( `\u5df2\u52a0\u8f7d ${state.courses.length} \u95e8\u8bfe · \u4f5c\u4e1a${assignmentCount()}\u9879 · \u8003\u8bd5${state.exams.length}\u573a` ); } catch (e) { listEl.innerHTML = `
${escHtml(e.message || e)}
`; log(`\u5237\u65b0\u5931\u8d25\uff1a${e.message || e}`); } } function _rcl2() { const listEl = document.getElementById("qingshu-course-list"); if (!listEl) return; if (!state.courses.length) { listEl.innerHTML = `
\u5f53\u524d\u5b66\u671f\u6682\u65e0\u8bfe\u7a0b
`; return; } listEl.innerHTML = state.courses .map((c) => { const key = _ck(c); const checked = state.selected.has(key) ? "checked" : ""; const status = c.learnStatusName || (c.learnStatus === 1 ? "\u5df2\u901a\u8fc7" : "\u5728\u4fee"); const disabled = c.allowLearn === false ? "disabled" : ""; return ``; }) .join(""); listEl.querySelectorAll('input[type="checkbox"]').forEach((inp) => { inp.addEventListener("change", () => { const key = inp.getAttribute("data-key"); if (!key) return; if (inp.checked) { state.selected.add(key); if (!_esl(key)) { inp.checked = state.selected.has(key); _rcl2(); _ucp(); return; } } else { state.selected.delete(key); } saveQueue(); _ucp(); _rrs(); }); }); } function renderAssignmentList() { const el = document.getElementById("qingshu-assignment-list"); const badge = document.getElementById("qingshu-tab-assignments-badge"); if (!el) return; const total = assignmentCount(); const pending = pendingHomeworkCount(); if (badge) badge.textContent = pending > 0 ? String(pending) : total > 0 ? String(total) : ""; if (!state.courses.length) { el.innerHTML = `
\u8bf7\u5148\u5237\u65b0\u8bfe\u7a0b
`; return; } if (!state.assignmentsLoaded && !state.assignments.length) { el.innerHTML = `
\u52a0\u8f7d\u4e2d…
`; return; } const groups = state.assignments .map((group) => { const c = group.course; const itemsHtml = group.items .map((it) => { const done = !isHomeworkPending(it); const scoreTxt = it.score != null ? `${it.score}${it.totalScore != null ? `/${it.totalScore}` : ""}\u5206` : ""; const mark = done ? "\u5df2\u6ee1\u5206" : it.status >= HW_STATUS_DONE_MIN ? "\u672a\u6ee1\u5206·\u5c06\u91cd\u505a" : it.statusText; const url = it.quizId ? `${studentApiBase()}ExercisePaper?courseId=${encodeURIComponent(c.courseId)}&quizId=${encodeURIComponent(it.quizId)}&teachPlanId=${encodeURIComponent(c.teachPlanId)}&periodId=${encodeURIComponent(c.periodId)}` : ""; return `
${escHtml(it.title)} ${escHtml(mark)}${scoreTxt ? ` · ${scoreTxt}` : ""} ${url ? `\u6253\u5f00` : ""}
`; }) .join(""); return `
${escHtml(c.courseName)}
${itemsHtml}
`; }) .join(""); el.innerHTML = groups || `
\u5f53\u524d\u5b66\u671f\u6682\u65e0\u4f5c\u4e1a
`; } function renderExamList() { const el = document.getElementById("qingshu-exam-list"); const badge = document.getElementById("qingshu-tab-exams-badge"); if (!el) return; if (badge) badge.textContent = state.exams.length > 0 ? String(state.exams.length) : ""; if (state.examsError) { el.innerHTML = `
${escHtml(state.examsError)}
`; return; } if (!state.examsLoaded) { el.innerHTML = `
\u52a0\u8f7d\u4e2d…
`; return; } if (!state.exams.length) { el.innerHTML = `
\u5f53\u524d\u5b66\u671f\u6682\u65e0\u8003\u8bd5
`; return; } el.innerHTML = state.exams .map((e) => { const course = findCourseForExam(e); const status = e.studentExamStatusStr || e.examStatusStr || ""; const score = e.scoreDetail ? String(e.scoreDetail) : ""; const time = e.startTimeStr && e.endTimeStr ? `${e.startTimeStr} ~ ${e.endTimeStr}` : ""; const listUrl = `${studentApiBase()}ExamArrange/ExamList?tab=finalExam`; const paperUrl = e.id && course ? `${studentApiBase()}ExamPaper?examId=${encodeURIComponent(e.id)}&courseId=${encodeURIComponent(course.courseId)}&teachPlanId=${encodeURIComponent(course.teachPlanId)}&periodId=${encodeURIComponent(course.periodId)}` : listUrl; const canTake = e.ableToParticipate === true; const href = canTake ? paperUrl : listUrl; return `
${escHtml(e.courseName || e.examName || "\u8003\u8bd5")} ${escHtml(e.examGroup || "\u8003\u8bd5")}
${escHtml(e.examName || e.quizTitle || "")}
${escHtml(status)}${score ? " · " + escHtml(score) : ""}${time ? " · " + escHtml(time) : ""}
${canTake ? "\u8fdb\u5165\u8003\u8bd5" : "\u67e5\u770b"}
`; }) .join(""); } function selectIncomplete() { const maxN = _msc(); state.selected.clear(); for (const c of state.courses) { if (c.allowLearn === false || c.learnStatus === 1) continue; if (Number.isFinite(maxN) && state.selected.size >= maxN) break; state.selected.add(_ck(c)); } saveQueue(); _rcl2(); _ucp(); log(_ipu() ? "\u5df2\u52fe\u9009\u672a\u901a\u8fc7\u8bfe\u7a0b" : "\u5df2\u52fe\u9009 1 \u95e8\u672a\u901a\u8fc7\u8bfe\u7a0b"); } function clearSelection() { state.selected.clear(); saveQueue(); _rcl2(); } function syncStartStop() { const btnStart = document.getElementById("qingshu-btn-start"); const btnStop = document.getElementById("qingshu-btn-stop"); if (btnStart) btnStart.disabled = state.runBusy || state.scoreTaskBusy; if (btnStop) btnStop.disabled = !(state.runBusy || state.scoreTaskBusy); syncScoreButtons(); const st = document.getElementById("qingshu-auto-status"); if (st) { const busy = state.runBusy || state.scoreTaskBusy; st.textContent = busy ? (state.stopFlag ? "\u505c\u6b62\u4e2d" : "\u8fd0\u884c\u4e2d") : "\u5f85\u673a"; st.classList.toggle("running", !!busy && !state.stopFlag); } } function syncScoreButtons() { const busy = state.runBusy || state.scoreTaskBusy; const btn = document.getElementById("qingshu-btn-homework-tab"); if (btn) btn.disabled = busy; } function readPanelPos() { try { return JSON.parse(localStorage.getItem(PANEL_POS_KEY) || "null"); } catch (_) { return null; } } function writePanelPos(left, top) { localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left, top })); } function _epd(panel) { const header = panel.querySelector("#qingshu-panel-header"); if (!header) return; let dragging = false; let sx = 0; let sy = 0; let ol = 0; let 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; const nl = Math.max(0, ol + e.clientX - sx); const nt = Math.max(0, ot + e.clientY - sy); panel.style.left = `${nl}px`; panel.style.top = `${nt}px`; }); window.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; const rect = panel.getBoundingClientRect(); writePanelPos(rect.left, rect.top); }); } function mountPanel() { if (document.getElementById("qingshu-auto-panel")) return; const style = document.createElement("style"); style.id = "qingshu-panel-style-v2"; style.textContent = ` #qingshu-auto-panel{position:fixed;right:16px;top:72px;z-index:2147483640;width:min(420px,calc(100vw - 24px));background:#f4f7fb;border:1px solid #dbe4f0;border-radius:18px;box-shadow:0 18px 44px rgba(15,23,42,.18);font:12px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:#0f172a;overflow:hidden} #qingshu-auto-panel.collapsed #qingshu-panel-body,#qingshu-auto-panel.collapsed .qingshu-footer-extra{display:none} #qingshu-panel-header{padding:10px 12px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none} #qingshu-panel-brand{display:flex;align-items:center;gap:10px;min-width:0} #qingshu-panel-logo{width:34px;height:34px;border-radius:10px;object-fit:cover;border:1px solid rgba(148,163,184,.45);background:#fff;flex:0 0 auto} #qingshu-panel-title{font-size:15px;font-weight:900;color:#b45309;line-height:1.25;display:flex;align-items:center;gap:8px;flex-wrap:wrap} #qingshu-panel-sub{font-size:11px;color:#475569;margin-top:2px} .qingshu-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 8px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0} #qingshu-toggle{border:none;background:#fff;color:#64748b;width:30px;height:30px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1);flex:0 0 auto} #qingshu-panel-body{padding:9px;position:relative;max-height:min(78vh,640px);overflow:auto} .qingshu-card{background:#fff;border:1px solid #d9e2ee;border-radius:14px;padding:9px 10px;margin-bottom:9px} .qingshu-progress-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;font-size:12px;color:#334155} #qingshu-auto-status{padding:5px 10px;border-radius:999px;border:1px solid rgba(148,163,184,.45);font-weight:900;font-size:12px;background:#fff} #qingshu-auto-status.running{background:rgba(16,185,129,.12);border-color:rgba(16,185,129,.45);color:#059669} .qingshu-meta-row{display:flex;justify-content:space-between;gap:8px;font-size:12px;margin-bottom:5px;align-items:center} .qingshu-meta-label{color:#64748b;flex:0 0 auto} .qingshu-meta-value{font-weight:700;color:#0f172a;text-align:right;min-width:0} .qingshu-auth-badge{display:inline-flex;padding:2px 8px;border-radius:999px;font-weight:900;font-size:11px;border:1px solid rgba(148,163,184,.45)} .qingshu-auth-badge[data-tier="pro"]{background:#fff7ed;border-color:#fdba74;color:#c2410c} .qingshu-auth-badge[data-tier="free"]{background:#f8fafc;color:#475569} .qingshu-auth-badge[data-tier="revoked"]{background:#fef2f2;border-color:#fecaca;color:#dc2626} .qingshu-btn-row{display:flex;gap:8px;margin-bottom:6px;flex-wrap:wrap} .qingshu-btn{flex:1;border:none;color:#fff;padding:8px 10px;border-radius:11px;cursor:pointer;font-weight:800;min-width:72px;font-size:12px} .qingshu-btn-ghost{flex:0 0 auto;border:1px solid rgba(148,163,184,.45);background:#fff;color:#0f172a} .qingshu-btn-start{background:#16a34a} .qingshu-btn-stop{background:#ef4444} .qingshu-btn-refresh{background:#0284c7} .qingshu-btn-pro{flex:0 0 auto;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;border:none} .qingshu-btn:disabled{opacity:.55;cursor:not-allowed} .qingshu-btn-exam-soon{background:#94a3b8!important;opacity:1!important;cursor:not-allowed;width:100%;flex:1 1 100%;border-radius:999px;padding:10px 14px} .qingshu-btn-score{font-size:11px;padding:6px 8px} .qingshu-tabbar{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap} .qingshu-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:6px 8px;border-radius:10px;cursor:pointer;font-weight:700;font-size:12px;min-width:64px} .qingshu-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent} .qingshu-pane{display:none} .qingshu-pane.active{display:block} .qingshu-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px} .qingshu-list-title{font-size:12px;color:#64748b;font-weight:700} .qingshu-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 8px} #qingshu-course-list,#qingshu-assignment-list,#qingshu-exam-list,#qingshu-log{max-height:190px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:12px;padding:6px} #qingshu-log{max-height:160px;font-family:ui-monospace,Consolas,monospace;font-size:11px;background:#f8fafc;border:1px solid #dbe4f0;color:#334155} .qingshu-log-row{padding:2px 0;line-height:1.45;word-break:break-word} .qingshu-log-time{color:#94a3b8;margin-right:4px} .qingshu-log-info{color:#334155} .qingshu-log-ok{color:#15803d} .qingshu-log-warn{color:#b45309} .qingshu-log-error{color:#dc2626} .qingshu-course-item{display:flex;gap:8px;align-items:flex-start;background:linear-gradient(180deg,#fff,#f8fbff);border:1px solid #dbe4f0;border-radius:11px;padding:8px 9px;margin-bottom:6px;font-size:13px} .qingshu-course-item.done{opacity:.65} .qingshu-course-body{flex:1;min-width:0} .qingshu-course-row{display:flex;justify-content:space-between;gap:6px} .qingshu-course-name{font-weight:700;min-width:0} .qingshu-course-meta{font-size:10px;color:#64748b;white-space:nowrap} .qingshu-course-scores{display:flex;flex-wrap:wrap;gap:3px 5px;margin-top:3px} .qingshu-score-chip{font-size:9px;color:#64748b;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:1px 4px} .qingshu-score-chip b{margin-left:1px;color:#1e40af;font-weight:700} .qingshu-score-summary{font-size:9px;color:#94a3b8} .qingshu-score-pending,.qingshu-muted{color:#94a3b8;font-size:10px} .qingshu-score-err,.qingshu-err{color:#b91c1c;font-size:10px} .qingshu-task-group{margin-bottom:6px} .qingshu-task-course{font-size:11px;font-weight:700;color:#1e40af;margin:2px 0 4px} .qingshu-task-item{display:flex;flex-wrap:wrap;gap:4px 8px;padding:4px 2px;border-bottom:1px dashed #e8ecf1;font-size:11px} .qingshu-task-item.done{opacity:.65} .qingshu-task-row{display:flex;justify-content:space-between;gap:6px;width:100%} .qingshu-task-name{font-weight:600;color:#334155} .qingshu-task-sub,.qingshu-task-meta{width:100%;font-size:10px;color:#64748b} .qingshu-task-tag{font-size:9px;background:#dbeafe;color:#1d4ed8;border-radius:4px;padding:1px 4px} .qingshu-task-link{font-size:10px;color:#2563eb;text-decoration:none;margin-left:auto} .qingshu-mode{display:flex;gap:8px;align-items:center;font-size:11px;margin:6px 0;flex-wrap:wrap} .qingshu-settings-group{margin-bottom:10px} .qingshu-token-row{display:flex;gap:6px;align-items:center;flex-wrap:wrap} #qingshu-cloud-token{flex:1 1 140px;min-width:120px;border:1px solid #cbd5e1;border-radius:8px;padding:5px 8px;font-size:12px} #qingshu-toast{display:none;position:absolute;left:10px;right:10px;top:8px;z-index:5;background:#0f172a;color:#fff;border-radius:10px;padding:8px 10px;box-shadow:0 8px 24px rgba(15,23,42,.25)} #qingshu-toast.show{display:block} #qingshu-toast.ok{background:#166534} #qingshu-toast.warn{background:#b45309} #qingshu-toast.error{background:#b91c1c} .qingshu-toast-text{font-size:12px;font-weight:700} .qingshu-footer-extra{padding:0 10px 8px} .qingshu-ann{background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;padding:6px 8px;margin-bottom:8px;color:#9a3412;font-size:11px} .qingshu-qq-row{display:flex;justify-content:space-between;align-items:center;gap:8px;background:#fff;border:1px solid #dbe4f0;border-radius:12px;padding:8px 10px} .qingshu-qq-text{font-weight:800;font-size:12px} .qingshu-qq-title{font-size:10px;color:#64748b} .qingshu-qq-btn{border:0;background:#2563eb;color:#fff;border-radius:8px;padding:6px 10px;font-weight:800;cursor:pointer} #qingshu-panel-footer{padding:8px 12px;background:#eef2f7;border-top:1px solid #dbe4f0;color:#334155;font-size:11px} #qingshu-vip-modal{display:none;position:fixed;inset:0;z-index:2147483646;background:rgba(15,23,42,.45);align-items:center;justify-content:center;padding:16px} #qingshu-vip-modal.show{display:flex} .qingshu-vip-card{width:min(420px,100%);background:#fff;border-radius:16px;padding:16px;box-shadow:0 20px 50px rgba(15,23,42,.3)} .qingshu-vip-title{font-size:18px;font-weight:900;color:#b45309} .qingshu-vip-sub{font-size:12px;color:#64748b;margin:6px 0 12px} .qingshu-vip-card ul{margin:0;padding-left:18px;font-size:13px;line-height:1.6} .qingshu-vip-actions{margin-top:14px;display:flex;gap:8px;justify-content:flex-end} .qingshu-free-tip{font-size:11px;color:#b45309;background:#fff7ed;border:1px solid #fed7aa;border-radius:8px;padding:6px 8px;margin:6px 0} .hidden{display:none!important} `; document.documentElement.appendChild(style); const panel = document.createElement("div"); panel.id = "qingshu-auto-panel"; panel.innerHTML = `
\u9752\u4e66\u5b66\u5802\u5b66\u4e60\u52a9\u624b v${SCRIPT_VERSION}
${escHtml(HOST)}
\u72b6\u6001\u5f85\u673a
\u5f85\u673a
\u8bfe\u7a0b\u9009\u62e9 \u5f53\u524d\u5b66\u671f
\u52a0\u8f7d\u4e2d…
\u70b9\u51fb\u300c\u5237\u65b0\u300d\u52a0\u8f7d\u4f5c\u4e1a
\u70b9\u51fb\u300c\u5237\u65b0\u300d\u52a0\u8f7d\u8003\u8bd5
\u8fd0\u884c\u65e5\u5fd7
\u6682\u65e0\u65e5\u5fd7
\u7528\u6237\u7c7b\u578b\u4f53\u9a8c
\u4f53\u9a8c\u989d\u5ea60/1 \u95e8
\u5230\u671f\u65f6\u95f4
`; document.documentElement.appendChild(panel); const vip = document.createElement("div"); vip.id = "qingshu-vip-modal"; vip.innerHTML = `
\u5f00\u901a PRO
\u5f00\u901a\u540e\u53ef\u591a\u95e8\u8bfe\u7a0b\u4e00\u5e76\u5904\u7406\uff0c\u6fc0\u6d3b\u540e ${PRO_DAYS} \u5929\u6709\u6548
  • \u4e0d\u9650\u8bfe\u7a0b\u95e8\u6570
  • \u767b\u5f55\u5206 / \u8ba8\u8bba\u5206 / \u4f5c\u4e1a / \u8bfe\u65f6\u5206 \u4e00\u5e76\u5904\u7406
  • \u6743\u9650\u7ed1\u5b9a\u8d26\u53f7\uff0c\u8bf7\u52ff\u6cc4\u9732 Token
`; document.documentElement.appendChild(vip); const pos = readPanelPos(); if (pos && Number.isFinite(pos.left) && Number.isFinite(pos.top)) { panel.style.right = "auto"; panel.style.left = `${pos.left}px`; panel.style.top = `${pos.top}px`; } if (localStorage.getItem(PANEL_COLLAPSED_KEY) === "1") { panel.classList.add("collapsed"); } _epd(panel); document.getElementById("qingshu-toggle").addEventListener("click", () => { const collapsed = panel.classList.toggle("collapsed"); localStorage.setItem(PANEL_COLLAPSED_KEY, collapsed ? "1" : "0"); }); panel.querySelectorAll(".qingshu-tab-btn").forEach((btn) => { btn.addEventListener("click", () => { const tab = btn.getAttribute("data-tab"); state.activeTab = tab; localStorage.setItem(ACTIVE_TAB_KEY, tab); panel.querySelectorAll(".qingshu-tab-btn").forEach((b) => b.classList.toggle("active", b === btn)); panel.querySelectorAll(".qingshu-pane").forEach((p) => { p.classList.toggle("active", p.getAttribute("data-pane") === tab); }); if (tab === "log") renderPanelLog(); }); }); { const tab = state.activeTab || "courses"; const btn = panel.querySelector(`.qingshu-tab-btn[data-tab="${tab}"]`); if (btn) btn.click(); } document.getElementById("qingshu-btn-sel").addEventListener("click", selectIncomplete); document.getElementById("qingshu-btn-clear").addEventListener("click", () => { clearSelection(); _ucp(); }); const clearLogBtn = document.getElementById("qingshu-btn-clear-log"); if (clearLogBtn) { clearLogBtn.addEventListener("click", () => { state.logLines = []; renderPanelLog(); }); } document.getElementById("qingshu-btn-start").addEventListener("click", () => { void _rq(); }); document.getElementById("qingshu-btn-stop").addEventListener("click", stopRun); const hwTabBtn = document.getElementById("qingshu-btn-homework-tab"); if (hwTabBtn) { hwTabBtn.addEventListener("click", () => { void _rst("\u4f5c\u4e1a", processHomework); }); } document.getElementById("qingshu-cloud-save").addEventListener("click", async () => { const inp = document.getElementById("qingshu-cloud-token"); const token = String(inp?.value || "").trim(); state.cloudToken = token; localStorage.setItem(CLOUD_TOKEN_KEY, token); if (!token) { state.cloudTier = "free"; _ucp(); _st("\u5df2\u5207\u6362\u4e3a\u4f53\u9a8c\u7528\u6237", "info"); return; } await _vct(); _esl(); _rcl2(); }); document.getElementById("qingshu-open-vip").addEventListener("click", () => _ovm(true)); document.getElementById("qingshu-vip-close").addEventListener("click", () => _ovm(false)); document.getElementById("qingshu-vip-buy").addEventListener("click", () => { openProBuy(); }); document.getElementById("qingshu-join-qq").addEventListener("click", () => { try { if (typeof GM_openInTab === "function") GM_openInTab(QQ_GROUP_LINK, { active: true }); else window.open(QQ_GROUP_LINK, "_blank"); } catch (_) { location.href = QQ_GROUP_LINK; } }); vip.addEventListener("click", (e) => { if (e.target === vip) _ovm(false); }); _rrs(); _ucp(); void (async () => { await _fcc(); await _ecl(false); _esl(); _ucp(); })(); if (detectSchool()) { void refreshCourseList(); } else { log("\u8bf7\u8fdb\u5165\u5b66\u4e60\u4e2d\u5fc3\u540e\u5237\u65b0\u9875\u9762", "warn"); } } mountPanel(); })();