// ==UserScript== // @name WHUT选课跳过弹窗 + 候选课面板 // @namespace http://tampermonkey.net/ // @version 2.8 // @description 定时抢课主推版:入口自动跳弹窗;候选课面板;Fetch/XHR拦截捕获真实选课API;按服务器时间定时重放API并处理重试/限频/登录失效。 // @author zyard // @match https://jwxk.whut.edu.cn/* // @run-at document-start // @grant unsafeWindow // @license MIT // ==/UserScript== (function() { 'use strict'; const pageWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; // ─── 常量 ──────────────────────────────────────────────────────── const PROFILE_PATH = '/xsxk/profile/index'; const ENTRY_SCAN_INTERVAL = 100; const ENTRY_SCAN_DURATION = 30000; const PROFILE_STUCK_KEY = 'WHUT_SKIP_ENTRY_PROFILE_VISITS_V110'; const COOKIE_PROMPT_KEY = 'WHUT_SKIP_ENTRY_COOKIE_PROMPT_AT_V110'; const COURSE_BASKET_KEY = 'WHUT_COURSE_BASKET_ITEMS_V120'; const COURSE_BASKET_COLLAPSED_KEY = 'WHUT_COURSE_BASKET_COLLAPSED_V120'; const COURSE_BASKET_POS_KEY = 'WHUT_COURSE_BASKET_POS_V120'; const COURSE_BASKET_PENDING_KEY = 'WHUT_COURSE_BASKET_PENDING_V130'; const COURSE_API_CAPTURE_KEY = 'WHUT_COURSE_API_CAPTURES_V200'; const COURSE_AUTO_PROCESS_KEY = 'WHUT_COURSE_AUTO_PROCESS_V230'; const COURSE_API_READ_ALL_KEY = 'WHUT_COURSE_API_READ_ALL_V230'; const SCHEDULE_KEY = 'WHUT_SCHEDULE_V220'; const COURSE_BASKET_PANEL_ID = 'whut-course-basket-panel'; const COURSE_BASKET_ADD_CLASS = 'whut-course-basket-add'; const COURSE_BASKET_SCAN_INTERVAL = 800; const COURSE_ADD_PATH = '/xsxk/elective/clazz/add'; const TRIGGER_RECOVERY_TIMEOUT = 3000; const SCHEDULE_RETRY_WINDOW = 30000; // 触发后最长重试 ms const SCHEDULE_RETRY_FAST = 300; const SCHEDULE_RETRY_JITTER = 40; const SCHEDULE_STAGGER_BASE = 300; const SCHEDULE_STAGGER_JITTER = 40; const SCHEDULE_RATE_LIMIT_BACKOFF = 3500; const SCHEDULE_RATE_LIMIT_MAX = 2; const SCHEDULE_ADVANCE_MS = 100; // 提前量 ms(补偿网络延迟) const AUTO_BLOCKED_COURSE_RE = /课程冲突|冲突|已满|不可选|不能选|限制|不满足|退选|已选(?!人数)/; const DIALOG_BLOCK_RE = /冲突|已满|不可选|限制|不满足|错误/; const OUTCOME_SUCCESS_RE = /选课成功|抢课成功|操作成功|提交成功|保存成功|成功加入|已选上|成功/; const OUTCOME_FAILED_RE = /选课失败|抢课失败|操作失败|提交失败|失败|错误|异常|未成功|不成功|登录失效|登录超时|请登录|重新登录|未登录|无权限|没有权限|已满|人数已满|名额已满|容量不足|不可选|不能选|不允许|无资格|冲突|限制|不满足|未开放|未开始|不在选课时间|时间未到|选课未开始|当前阶段未开放/; const SCHEDULE_RETRYABLE_RE = /未开放|未开始|不在选课时间|时间未到|选课未开始|当前阶段未开放|not\.?open|not\.?start/i; // ─── 状态变量 ───────────────────────────────────────────────────── let entryTimer = null, stopTimer = null, courseBasketTimer = null; let courseBasketPanel = null; let courseBasketItems = []; let courseBasketCollapsed = false; let state = 'idle', lastHref = ''; let confirmClickedAt = 0, enterClickedAt = 0, conflictPromptedAt = 0, patchedAt = 0; let currentlyTriggeringId = null; let currentlyTriggeringAt = 0; let capturedApiRequests = new Map(); let _OriginalFetch = null; let scheduleActive = false; let scheduleTimer = null; let scheduledOpenTime = 0; let serverTimeOffset = 0; let confirmScannerTimer = null; let scheduleExecuting = false; let scheduleArming = false; // FIX: 防止服务器校时 Promise 返回前重复点击开始定时导致重复武装。 let nextScheduleDispatchAt = 0; // FIX: 统一串行化定时首发与重试请求,避免多门课重试时堆叠成过快请求。 let autoProcessActive = false; let apiReadActive = false; let apiReadTimer = null; const clickedElements = new WeakSet(); const courseBasketBindings = new Map(); const ignoredOutcomeElements = new WeakSet(); const nativeWindowAlert = window.alert && window.alert.bind(window); const nativeWindowConfirm = window.confirm && window.confirm.bind(window); const nativePageAlert = pageWindow !== window && pageWindow.alert ? pageWindow.alert.bind(pageWindow) : nativeWindowAlert; const nativePageConfirm = pageWindow !== window && pageWindow.confirm ? pageWindow.confirm.bind(pageWindow) : nativeWindowConfirm; function noteApiReadNativeDialog(kind, msg) { if (!apiReadActive || !currentlyTriggeringId) return false; var item = courseBasketItems.find(function(x) { return x.id === currentlyTriggeringId; }); if (item) updateCBItem(item.id, { lastMessage: '读取API时已自动确认原生弹窗: ' + String(msg || kind || '').slice(0, 160) }); return true; } function wrapNativeDialog(kind, fallback) { return function(msg) { if (noteApiReadNativeDialog(kind, msg)) return kind === 'confirm' ? true : undefined; return fallback ? fallback(msg) : (kind === 'confirm'); }; } try { if (nativeWindowAlert) window.alert = wrapNativeDialog('alert', nativeWindowAlert); if (nativeWindowConfirm) window.confirm = wrapNativeDialog('confirm', nativeWindowConfirm); if (pageWindow !== window) { if (nativePageAlert) pageWindow.alert = wrapNativeDialog('alert', nativePageAlert); if (nativePageConfirm) pageWindow.confirm = wrapNativeDialog('confirm', nativePageConfirm); } } catch (_) {} console.log('%c WHUT选课助手 v2.2 已启动', 'background:#2655c8;color:#fff;font-weight:bold;'); // ─── 工具 ────────────────────────────────────────────────────────── function isProfilePage() { return (pageWindow.location && pageWindow.location.href || location.href).includes(PROFILE_PATH); } function isWhutSameOrigin(url) { try { return new URL(String(url || ''), location.href).origin === 'https://jwxk.whut.edu.cn'; } catch (_) { return false; } } function isCourseSelectionApiUrl(url) { try { return new URL(String(url || ''), location.href).pathname === COURSE_ADD_PATH; } catch (_) { return false; } } function serverNow() { return Date.now() + serverTimeOffset; } function getCurrentHref() { return (pageWindow.location && pageWindow.location.href) || location.href; } function getBatchIdFromUrl(url) { try { return new URL(String(url || ''), location.href).searchParams.get('batchId') || ''; } catch (_) { return ''; } } function findHeaderName(headers, name) { var want = String(name || '').toLowerCase(); return Object.keys(headers || {}).find(function(k) { return String(k).toLowerCase() === want; }) || ''; } function ensureBatchIdHeader(headers, referrer) { headers = headers || {}; var key = findHeaderName(headers, 'batchid'); if (key && headers[key]) return headers; var batchId = getBatchIdFromUrl(getCurrentHref()) || getBatchIdFromUrl(referrer); if (batchId) headers[key || 'batchid'] = batchId; return headers; } function isCourseAddCapture(capture) { return Boolean(capture && String(capture.method || 'POST').toUpperCase() === 'POST' && isCourseSelectionApiUrl(capture.url)); } function getCaptureBatchId(capture) { var headers = Object.assign({}, capture && capture.headers || {}, capture && capture.extraHeaders || {}); var key = findHeaderName(headers, 'batchid'); return normalizeCBText((key && headers[key]) || getBatchIdFromUrl(getCurrentHref()) || getBatchIdFromUrl(capture && capture.referrer)); } function hasUsableCapture(id) { var capture = capturedApiRequests.get(id); return isCourseAddCapture(capture) && Boolean(getCaptureBatchId(capture)); } function looksLikeHtmlResponse(text, resp) { var ct = ''; try { ct = resp && resp.headers && resp.headers.get('content-type') || ''; } catch (_) {} return /text\/html/i.test(ct) || /^\s*(= 0) delete headers[k]; }); return headers; } function setHeader(headers, name, value) { var key = findHeaderName(headers, name) || name; headers[key] = value; return headers; } function isRetryableScheduleMessage(msg) { return SCHEDULE_RETRYABLE_RE.test(String(msg || '')); } function resetScheduleDispatchQueue() { nextScheduleDispatchAt = 0; } function reserveScheduleDispatchDelay(baseDelay, jitter) { var now = Date.now(); var base = Math.max(0, Number(baseDelay) || 0); var jit = Math.max(0, Number(jitter) || 0); if (!nextScheduleDispatchAt || nextScheduleDispatchAt < now) nextScheduleDispatchAt = now; var reservedAt = nextScheduleDispatchAt; nextScheduleDispatchAt += base + (jit > 0 ? Math.floor(Math.random() * jit) : 0); return Math.max(0, reservedAt - now); } function getScheduleRetryDelay(attempt) { // FIX: “未开放/未开始”场景首个重试也额外降速,避免单门课按 300ms 连续重试触发限频。 var extra = 350 + Math.min(1200, Math.max(0, attempt - 1) * 300); return SCHEDULE_RETRY_FAST + extra + Math.floor(Math.random() * (SCHEDULE_RETRY_JITTER || 1)); } // ─── Cookie / Token ──────────────────────────────────────────────── function getCookieValues(name) { const prefix = name + '='; return document.cookie.split(';') .map(function(s) { return s.trim(); }) .filter(function(s) { return s.startsWith(prefix); }) .map(function(s) { try { return decodeURIComponent(s.slice(prefix.length)); } catch (_) { return s.slice(prefix.length); } }).filter(Boolean); } function decodeJwtPayload(token) { try { var p = String(token || '').split('.')[1]; if (!p) return null; var n = p.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - p.length % 4) % 4); return JSON.parse(atob(n)); } catch (_) { return null; } } function getTokenTimestamp(t) { var p = decodeJwtPayload(t); return Number(p && (p.time || p.iat) || 0) || 0; } function pickLatestAuthorizationToken(vals) { if (!vals) vals = getCookieValues('Authorization'); var u = vals.filter(Boolean).filter(function(v, i, a) { return a.indexOf(v) === i; }); if (u.length <= 1) return u[0] || ''; return u.sort(function(a, b) { return getTokenTimestamp(b) - getTokenTimestamp(a); })[0] || u[u.length - 1] || ''; } function getAuthorizationCookieConflict() { var vals = getCookieValues('Authorization'); var u = vals.filter(function(v, i, a) { return a.indexOf(v) === i; }); return { vals: vals, u: u, conflict: u.length > 1 }; } function expireAuthCookieAtPath(path) { var host = location.hostname; ['', host, '.' + host].forEach(function(d) { var dp = d ? '; domain=' + d : ''; document.cookie = 'Authorization=; path=' + path + dp + '; expires=Thu, 01 Jan 1970 00:00:00 GMT; max-age=0; secure; SameSite=Lax'; document.cookie = 'Authorization=; path=' + path + dp + '; expires=Thu, 01 Jan 1970 00:00:00 GMT; max-age=0; SameSite=Lax'; }); } function syncAuthCookiesToLatest(token) { var t = token || pickLatestAuthorizationToken(); if (!t || !/^[A-Za-z0-9_.\-]+$/.test(t)) return false; var et = encodeURIComponent(t), host = location.hostname; ['/', '/xsxk'].forEach(function(path) { ['', host, '.' + host].forEach(function(d) { var dp = d ? '; domain=' + d : ''; document.cookie = 'Authorization=' + et + '; path=' + path + dp + '; secure; SameSite=Lax'; }); }); return true; } function repairAuthCookies() { var u = getCookieValues('Authorization').filter(function(v, i, a) { return a.indexOf(v) === i; }); var t = pickLatestAuthorizationToken(u); if (!t) return false; ['/', '/xsxk', '/xsxk/', '/xsxk/profile', '/xsxk/elective'].forEach(expireAuthCookieAtPath); syncAuthCookiesToLatest(t); console.log('%c WHUT: 已清理重复 Authorization Cookie', 'color:#10b981;font-weight:bold;'); return true; } function promptCookieConflict() { var now = Date.now(); var last = Math.max(conflictPromptedAt, Number(localStorage.getItem(COOKIE_PROMPT_KEY) || 0)); if (now - last < 60000) return; conflictPromptedAt = now; localStorage.setItem(COOKIE_PROMPT_KEY, String(now)); setTimeout(function() { if (!isProfilePage()) return; if (window.confirm('检测到多个 Authorization Cookie,可能导致302卡循环。是否清理并只保留最新token?')) { repairAuthCookies(); window.alert('已同步,请刷新页面再进入选课。'); } }, 300); } function ensureCookieReady() { var vals = getCookieValues('Authorization'); if (!vals.length) return false; var t = pickLatestAuthorizationToken(vals); if (!t) return false; syncAuthCookiesToLatest(t); if (getAuthorizationCookieConflict().conflict) { state = 'cookie_conflict'; stopAutoEntry(); promptCookieConflict(); return false; } if (state === 'idle' || state === 'stopped') state = 'cookie_ready'; return true; } // ─── 入口页自动点击 ──────────────────────────────────────────────── function stopAutoEntry() { if (entryTimer) clearInterval(entryTimer); if (stopTimer) clearTimeout(stopTimer); entryTimer = stopTimer = null; if (['done', 'cookie_conflict', 'entry_timeout'].indexOf(state) < 0) state = 'stopped'; } function markDone() { state = 'done'; stopAutoEntry(); } function isButtonClickable(btn) { return Boolean(btn && btn.isConnected && btn.getClientRects().length > 0 && !btn.disabled && !btn.classList.contains('is-disabled') && btn.getAttribute('aria-disabled') !== 'true'); } function safeClick(btn, nextState) { if (!isButtonClickable(btn) || clickedElements.has(btn)) return false; clickedElements.add(btn); syncAuthCookiesToLatest(); btn.click(); state = nextState || state; console.log('%c WHUT: 自动点击 ' + btn.innerText.trim(), 'color:#17a2b8;'); if (nextState === 'entering') setTimeout(function() { if (!isProfilePage()) markDone(); }, 1200); return true; } function getPrimaryButtons() { return Array.from(document.querySelectorAll('button.el-button--primary')).filter(isButtonClickable) .map(function(btn) { return { btn: btn, text: btn.innerText.replace(/\s+/g, '') }; }); } function scanAutoEntryTargets() { if (!isProfilePage()) { markDone(); return; } var href = (pageWindow.location && pageWindow.location.href) || location.href; if (lastHref && href !== lastHref && !href.includes(PROFILE_PATH)) { markDone(); return; } lastHref = href; if (!ensureCookieReady()) return; if (state === 'entering') { if (enterClickedAt && Date.now() - enterClickedAt > 6000) { state = 'entry_timeout'; stopAutoEntry(); } return; } var btns = getPrimaryButtons(); var confirmBtn = null, enterBtn = null; btns.forEach(function(o) { if (!confirmBtn && ['确定', '确认', '我知道了', '同意'].some(function(k) { return o.text.indexOf(k) >= 0; })) confirmBtn = o.btn; if (!enterBtn && (o.text === '选课' || o.text === '进入选课' || o.text === '进入')) enterBtn = o.btn; }); if (confirmBtn && state === 'cookie_ready' && (!confirmClickedAt || Date.now() - confirmClickedAt > 2500)) { confirmClickedAt = Date.now(); safeClick(confirmBtn, 'confirmed'); return; } if (enterBtn && (state === 'cookie_ready' || state === 'confirmed') && !enterClickedAt) { enterClickedAt = Date.now(); safeClick(enterBtn, 'entering'); } } function recordProfileVisitAndWarn() { if (!isProfilePage()) return; var now = Date.now(); var visits; try { visits = JSON.parse(localStorage.getItem(PROFILE_STUCK_KEY) || '[]'); } catch(_) { visits = []; } visits = visits.map(Number).filter(function(t) { return Number.isFinite(t) && now - t < 60000; }); visits.push(now); localStorage.setItem(PROFILE_STUCK_KEY, JSON.stringify(visits)); if (visits.length < 3) return; var last = Number(localStorage.getItem(COOKIE_PROMPT_KEY) || 0); if (now - last < 60000) return; localStorage.setItem(COOKIE_PROMPT_KEY, String(now)); setTimeout(function() { if (!isProfilePage()) return; if (window.confirm('短时间内多次回到选课首页,常见原因是Cookie冲突。是否清理?')) { repairAuthCookies(); window.alert('已同步,请刷新页面。'); } }, 800); } function startAutoEntry() { if (entryTimer || !isProfilePage()) return; state = 'idle'; confirmClickedAt = enterClickedAt = patchedAt = 0; lastHref = (pageWindow.location && pageWindow.location.href) || location.href; scanAutoEntryTargets(); entryTimer = setInterval(scanAutoEntryTargets, ENTRY_SCAN_INTERVAL); stopTimer = setTimeout(stopAutoEntry, ENTRY_SCAN_DURATION); } // ════════════════════════════════════════════════════════════════ // API 捕获存储 // ════════════════════════════════════════════════════════════════ function loadCapturedApiRequests() { try { capturedApiRequests = new Map(Object.entries(JSON.parse(localStorage.getItem(COURSE_API_CAPTURE_KEY) || '{}'))); } catch (_) { capturedApiRequests = new Map(); } } function saveCapturedApiRequests() { try { localStorage.setItem(COURSE_API_CAPTURE_KEY, JSON.stringify(Object.fromEntries(capturedApiRequests))); } catch (_) {} } function extractHeaders(init) { var r = {}; try { if (!init) return r; if (typeof Headers !== 'undefined' && init instanceof Headers) { init.forEach(function(v, k) { r[k] = v; }); } else if (Array.isArray(init)) { init.forEach(function(pair) { r[pair[0]] = pair[1]; }); } else { Object.assign(r, init); } } catch (_) {} return r; } // 返回 null 表示无法序列化(如 ReadableStream),调用方跳过捕获 function serializeBody(body) { if (!body) return Promise.resolve(''); if (typeof body === 'string') return Promise.resolve(body); if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) return Promise.resolve(body.toString()); if (typeof FormData !== 'undefined' && body instanceof FormData) { var params = new URLSearchParams(); body.forEach(function(v, k) { if (typeof File !== 'undefined' && v instanceof File) return; params.append(k, String(v)); }); return Promise.resolve(params.toString()); } if (body instanceof ArrayBuffer || (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(body))) { try { return Promise.resolve(new TextDecoder().decode(body instanceof ArrayBuffer ? body : body.buffer)); } catch (_) {} } if (typeof Blob !== 'undefined' && body instanceof Blob) return body.text(); if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) return Promise.resolve(null); return Promise.resolve(String(body)); } function recordCapture(captureId, capture) { var existing = capturedApiRequests.get(captureId); if (existing && isCourseAddCapture(existing) && !isCourseAddCapture(capture)) { console.log('%c WHUT: 已有真实选课API,忽略非确认接口覆盖', 'color:#d97706;font-weight:bold;', captureId, capture && capture.url); return; } capture.referrer = capture.referrer || getCurrentHref(); var extraHeaders = Object.assign({}, capture.headers || {}, capture.extraHeaders || {}); ensureBatchIdHeader(extraHeaders, capture.referrer); capture.headers = Object.assign({}, extraHeaders); capture.extraHeaders = Object.assign({}, extraHeaders); capturedApiRequests.set(captureId, capture); saveCapturedApiRequests(); updateCBItem(captureId, { apiReadSkipped: false, lastMessage: '已捕获API: ' + capture.url.replace(/.*\/xsxk/, '/xsxk') }); console.log('%c WHUT: 已捕获选课API', 'color:#10b981;font-weight:bold;', captureId, capture.url); } // ─── Fetch 拦截器 ────────────────────────────────────────────────── function installFetchInterceptor() { // FIX: 防止脚本重复注入时重复包装 fetch,导致同一请求被多层拦截。 if (pageWindow.__WHUT_COURSE_BASKET_FETCH_PATCHED__) { _OriginalFetch = pageWindow.__WHUT_COURSE_BASKET_ORIGINAL_FETCH__ || pageWindow.fetch; return; } var OriginalFetch = pageWindow.fetch; _OriginalFetch = OriginalFetch; pageWindow.__WHUT_COURSE_BASKET_ORIGINAL_FETCH__ = OriginalFetch; pageWindow.__WHUT_COURSE_BASKET_FETCH_PATCHED__ = true; pageWindow.fetch = function(input, init) { var url = typeof input === 'string' ? input : (input && input.url || String(input || '')); var method = ((init && init.method) || (input && input.method) || 'GET').toUpperCase(); if (method === 'POST' && isWhutSameOrigin(url) && isCourseSelectionApiUrl(url)) { var elapsed = Date.now() - currentlyTriggeringAt; var captureId = currentlyTriggeringId; if (captureId && elapsed < TRIGGER_RECOVERY_TIMEOUT) { var rawBody = (init && init.body) || (typeof input !== 'string' && input && input.body) || null; return serializeBody(rawBody).then(function(bodyText) { if (bodyText !== null) { var headers = extractHeaders((init && init.headers) || (typeof input !== 'string' && input && input.headers) || null); var tok = pickLatestAuthorizationToken(); if (tok) setHeader(headers, 'Authorization', tok); var referrer = getCurrentHref(); recordCapture(captureId, { url: new URL(url, location.href).href, method: method, headers: headers, body: bodyText, extraHeaders: headers, referrer: referrer, via: 'fetch', capturedAt: Date.now() }); } return OriginalFetch.call(pageWindow, input, init); }); } } return OriginalFetch.call(pageWindow, input, init); }; } // ─── XHR 拦截器 ──────────────────────────────────────────────────── function installXhrInterceptor() { // FIX: 防止脚本重复注入时重复包装 XMLHttpRequest。 if (pageWindow.__WHUT_COURSE_BASKET_XHR_PATCHED__) return; var OriginalXHR = pageWindow.XMLHttpRequest; pageWindow.__WHUT_COURSE_BASKET_ORIGINAL_XHR__ = OriginalXHR; pageWindow.__WHUT_COURSE_BASKET_XHR_PATCHED__ = true; function InterceptedXHR() { var xhr = new OriginalXHR(); var _m = 'GET', _u = '', _h = {}; var _open = xhr.open.bind(xhr), _send = xhr.send.bind(xhr), _setHdr = xhr.setRequestHeader.bind(xhr); xhr.open = function(m, u) { _m = (m || 'GET').toUpperCase(); _u = String(u || ''); return _open.apply(xhr, arguments); }; xhr.setRequestHeader = function(n, v) { _h[n] = v; return _setHdr(n, v); }; xhr.send = function(body) { if (_m === 'POST' && isWhutSameOrigin(_u) && isCourseSelectionApiUrl(_u)) { var elapsed = Date.now() - currentlyTriggeringAt, captureId = currentlyTriggeringId; if (captureId && elapsed < TRIGGER_RECOVERY_TIMEOUT) { serializeBody(body).then(function(bodyText) { if (bodyText === null) return; var headers = Object.assign({}, _h); var tok = pickLatestAuthorizationToken(); if (tok) setHeader(headers, 'Authorization', tok); var referrer = getCurrentHref(); recordCapture(captureId, { url: new URL(_u, location.href).href, method: _m, headers: headers, body: bodyText, extraHeaders: headers, referrer: referrer, via: 'xhr', capturedAt: Date.now() }); }); } } return _send(body); }; return xhr; } InterceptedXHR.prototype = OriginalXHR.prototype; Object.setPrototypeOf(InterceptedXHR, OriginalXHR); pageWindow.XMLHttpRequest = InterceptedXHR; } // ─── 直接 API 重放 ───────────────────────────────────────────────── function parseRawHeaders(raw) { var map = {}; String(raw || '').split(/\r?\n/).forEach(function(line) { var idx = line.indexOf(':'); if (idx <= 0) return; var name = line.slice(0, idx).trim().toLowerCase(); var value = line.slice(idx + 1).trim(); if (name) map[name] = value; }); return { get: function(name) { return map[String(name || '').toLowerCase()] || ''; } }; } function sendReplayViaFetch(capture, headers) { var doFetch = _OriginalFetch || pageWindow.fetch; return doFetch.call(pageWindow, capture.url, { method: capture.method, headers: headers, body: capture.body || undefined, credentials: 'include' }).then(function(resp) { return resp.text().then(function(text) { return { status: resp.status, type: resp.type || 'fetch', headers: resp.headers, text: text }; }); }); } function sendReplayViaXhr(capture, headers) { return new Promise(function(resolve, reject) { var XHR = pageWindow.__WHUT_COURSE_BASKET_ORIGINAL_XHR__ || pageWindow.XMLHttpRequest; var xhr = new XHR(); xhr.open(String(capture.method || 'POST').toUpperCase(), capture.url, true); xhr.withCredentials = true; Object.keys(headers || {}).forEach(function(k) { try { xhr.setRequestHeader(k, headers[k]); } catch (_) {} }); xhr.onload = function() { resolve({ status: xhr.status, type: 'xhr', headers: parseRawHeaders(xhr.getAllResponseHeaders()), text: xhr.responseText || '' }); }; xhr.onerror = function() { reject(new Error('XHR 网络异常')); }; xhr.ontimeout = function() { reject(new Error('XHR 请求超时')); }; xhr.send(capture.body || undefined); }); } function directApiSelectCourse(id) { var capture = capturedApiRequests.get(id); if (!capture) return Promise.resolve({ ok: false, msg: '未捕获此课程API。请先点一次"点选课"让脚本记录请求格式。' }); if (!isCourseAddCapture(capture)) return Promise.resolve({ ok: false, retryable: false, msg: '已捕获请求不是真实选课接口 /xsxk/elective/clazz/add,请重新点一次"点选课"捕获。' }); try { var headers = Object.assign({}, capture.headers || {}, capture.extraHeaders || {}); ensureBatchIdHeader(headers, capture.referrer); var tok = pickLatestAuthorizationToken(); if (tok) setHeader(headers, 'Authorization', tok); setHeader(headers, 'Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8'); sanitizeReplayHeaders(headers); var sender = String(capture.via || '').toLowerCase() === 'fetch' ? sendReplayViaFetch : sendReplayViaXhr; return sender(capture, headers).then(function(resp) { if (resp.status === 401 || resp.status === 403) { return { ok: false, retryable: false, status: resp.status, msg: '登录失效或无权限' }; } if (resp.status === 429) { return { ok: false, retryable: true, status: resp.status, msg: '请求过快/限频' }; } if (resp.status === 302 || resp.type === 'opaqueredirect') { return { ok: false, retryable: false, status: resp.status, msg: '登录失效或返回登录页' }; } var text = resp.text || ''; if (looksLikeHtmlResponse(text, resp)) { return { ok: false, retryable: false, status: resp.status, msg: '登录失效或返回登录页' }; } var data; try { data = JSON.parse(text); } catch (_) { data = { rawText: text }; } var codeVal = data && (data.code != null ? data.code : data.status); var code = String(codeVal != null ? codeVal : ''); var msg = normalizeCBText((data && (data.msg || data.message || data.info)) || data.rawText || '').slice(0, 260); var explicitFailure = (data && data.success === false) || OUTCOME_FAILED_RE.test(msg); var ok = !explicitFailure && ((data && data.success === true) || code === '200' || code === '0'); if (!msg) msg = ok ? '操作成功' : '操作失败'; var retryable = !ok && isRetryableScheduleMessage(msg); return { ok: ok, retryable: retryable, status: resp.status, msg: msg, data: data, via: resp.type || (sender === sendReplayViaXhr ? 'xhr' : 'fetch') }; }).catch(function(err) { return { ok: false, retryable: false, msg: '请求异常: ' + err.message }; }); } catch (err) { return Promise.resolve({ ok: false, retryable: false, msg: '请求异常: ' + err.message }); } } // ════════════════════════════════════════════════════════════════ // 候选课数据层 // ════════════════════════════════════════════════════════════════ function isCourseBasketPage() { var href = (pageWindow.location && pageWindow.location.href) || location.href; return isWhutSameOrigin(href) && href.indexOf('/xsxk/') >= 0 && !isProfilePage(); } function normalizeCBText(v) { return String(v || '').replace(/ /g, ' ').replace(/\s+/g, ' ').trim(); } function getElemText(el) { return normalizeCBText((el && (el.innerText || el.textContent)) || ''); } function isIgnoredFrag(text) { var c = normalizeCBText(text).replace(/\s+/g, ''); if (!c || c.length > 100) return true; var list = ['选','选课','选择','申请','补选','预选','正选','申请选课','查看','详情','退选', '确定','确认','取消','关闭','加入候选','已加入','候选课','点选课','一键读取API','停止读取API','恢复全部', '自动开','自动关','标记完成','恢复','重试','删除','清空','收起','展开', '直接API选','直接选课','API选课','重新触发']; return list.indexOf(c) >= 0; } function hashText(text) { var h = 2166136261; for (var i = 0; i < text.length; i++) { h ^= text.charCodeAt(i); h = Math.imul(h, 16777619); } return 'course_' + (h >>> 0).toString(36); } function loadCourseBasket() { var items; try { items = JSON.parse(localStorage.getItem(COURSE_BASKET_KEY) || '[]'); } catch (_) { items = []; } var validStatuses = ['done', 'triggered', 'searching', 'failed', 'requesting']; var now = Date.now(); courseBasketItems = Array.isArray(items) ? items.filter(function(x) { return x && x.id && x.title; }).map(function(x) { var status = validStatuses.indexOf(x.status || '') >= 0 ? x.status : ''; if (['triggered', 'searching', 'requesting'].indexOf(status) >= 0 && now - Number(x.updatedAt || 0) > 60000) status = ''; return Object.assign({}, x, { status: status, retryCount: Number(x.retryCount || 0) || 0, lastMessage: normalizeCBText(x.lastMessage || '') }); }).slice(0, 40) : []; courseBasketCollapsed = localStorage.getItem(COURSE_BASKET_COLLAPSED_KEY) === '1'; } function saveCourseBasket() { localStorage.setItem(COURSE_BASKET_KEY, JSON.stringify(courseBasketItems)); } function isCBSelectBtn(btn) { if (!btn || btn.nodeType !== 1) return false; if (btn.closest && btn.closest('#' + COURSE_BASKET_PANEL_ID + ',.' + COURSE_BASKET_ADD_CLASS)) return false; if (!isButtonClickable(btn)) return false; var text = getElemText(btn).replace(/\s+/g, ''); if (!text || text.length > 8) return false; if (['退选','已选','不可选','查看','详情','查询','搜索','确定','确认','取消','关闭'].some(function(k) { return text.indexOf(k) >= 0; })) return false; return ['选','选课','选择','申请','补选','预选','正选','重修选课','申请选课'].indexOf(text) >= 0 || (text.indexOf('选课') >= 0 && text.length <= 8); } function isUsableContainer(el, btn, req) { if (req === undefined) req = true; if (!el || el === document.body) return false; if (req && !el.contains(btn)) return false; var t = getElemText(el); return t.length >= 4 && t.length <= 1800; } function findMirrorRow(btn) { var table = btn.closest && btn.closest('.el-table'); var fc = btn.closest && btn.closest('.el-table__fixed,.el-table__fixed-right'); var fr = btn.closest && btn.closest('.el-table__row,tr'); if (!table || !fc || !fr) return null; var fb = fc.querySelector('.el-table__fixed-body-wrapper,.el-table__body-wrapper') || fc; var frows = Array.from(fb.querySelectorAll('.el-table__row,tbody tr')); var ridx = frows.indexOf(fr); var mb = Array.from(table.querySelectorAll('.el-table__body-wrapper')).find(function(c) { return !(c.closest && c.closest('.el-table__fixed,.el-table__fixed-right')); }); var mrows = mb ? Array.from(mb.querySelectorAll('.el-table__row,tbody tr')) : []; return ridx >= 0 ? mrows[ridx] || null : null; } function findCourseContainer(btn) { var mr = findMirrorRow(btn); if (isUsableContainer(mr, btn, false)) return mr; var sels = ['.el-table__row', 'tr', '[class*="course"]', '[class*="Course"]', '[class*="lesson"]', '[class*="Lesson"]', '[class*="kc"]', '[class*="Kc"]', '.el-card', 'li']; for (var i = 0; i < sels.length; i++) { var m = btn.closest && btn.closest(sels[i]); if (isUsableContainer(m, btn)) return m; } var node = btn.parentElement, d = 0; while (node && node !== document.body && d < 8) { if (isUsableContainer(node, btn)) return node; node = node.parentElement; d++; } return btn.parentElement || btn; } function collectFragments(container) { var frags = [], clone = container.cloneNode(true); clone.querySelectorAll('.' + COURSE_BASKET_ADD_CLASS + ',#' + COURSE_BASKET_PANEL_ID).forEach(function(n) { n.remove(); }); Array.from(clone.querySelectorAll('td,.cell,[class*="name"],[class*="Name"],[class*="course"],[class*="Course"],[class*="lesson"],[class*="Lesson"],[class*="kc"],[class*="Kc"]')) .forEach(function(n) { getElemText(n).split(/[\n\r\t]+| {2,}/).map(normalizeCBText) .filter(function(f) { return !isIgnoredFrag(f); }).forEach(function(f) { frags.push(f); }); }); ((clone.innerText || clone.textContent) || '').split(/[\n\r\t]+| {2,}/).map(normalizeCBText) .filter(function(f) { return !isIgnoredFrag(f); }).forEach(function(f) { frags.push(f); }); return frags.filter(function(v, i, a) { return a.indexOf(v) === i; }).slice(0, 16); } function chooseCourseTitle(frags) { return frags.find(function(f) { return /[一-鿿A-Za-z]/.test(f) && !/^(教师|老师|时间|地点|学分|容量|校区|周次|节次|课程代码|课程序号)[:|:]?/.test(f); }) || frags[0] || '未命名课程'; } function collectOrderedRowCells(container) { if (!container) return []; var row = container; if (!(row.matches && row.matches('tr,.el-table__row'))) { row = (container.closest && container.closest('tr,.el-table__row')) || container; } var cells = Array.from(row.children || []).filter(function(n) { return n.matches && n.matches('td,th'); }); if (!cells.length) cells = Array.from(row.querySelectorAll('td,th')); if (cells.length) { return cells.map(function(cell) { return getElemText(cell.querySelector('.cell') || cell); }) .map(normalizeCBText); } return Array.from(row.querySelectorAll('.cell')).map(getElemText).map(normalizeCBText); } function getAutoBlockedMessage(text) { return AUTO_BLOCKED_COURSE_RE.test(normalizeCBText(text)) ? '检测到冲突/不可选,默认不自动处理' : ''; } function isForceSelectable(item) { return Boolean(item && item.forceSelectable); } function isItemAutoBlocked(item) { return Boolean(item && item.autoBlocked && !isForceSelectable(item)); } function captureCourseTarget(btn) { var container = findCourseContainer(btn), frags = collectFragments(container); if (!frags.length) return null; var cells = collectOrderedRowCells(container); var batchId = getBatchIdFromUrl(getCurrentHref()); var rowText = cells.length ? cells.join(' ') : frags.join(' '); var title = (cells.length >= 2 && cells[1]) || chooseCourseTitle(frags); var detail = frags.filter(function(f) { return f !== title; }).slice(0, 4).join(' / '); var stableKey = ''; if (cells.length >= 5) { var stableParts = [batchId, cells[0], cells[2], cells[3], cells[4]].map(normalizeCBText); if (stableParts.slice(1).some(Boolean)) stableKey = stableParts.join('|'); detail = [cells[0], cells[2], cells[3], cells[4]].filter(Boolean).join(' / '); } var blockMsg = getAutoBlockedMessage(rowText); return { id: hashText(stableKey || frags.join('|')), stableKey: stableKey, title: title, detail: detail, searchText: [rowText, frags.join(' ')].filter(Boolean).join(' '), autoBlocked: Boolean(blockMsg), lastMessage: blockMsg, href: getCurrentHref(), updatedAt: Date.now() }; } function isCourseInBasket(id) { return courseBasketItems.some(function(x) { return x.id === id; }); } function isCBProcessable(item) { return item && !isItemAutoBlocked(item) && ['done', 'triggered', 'searching', 'failed', 'requesting'].indexOf(item.status || '') < 0; } function updateCBItem(id, patch) { var item = courseBasketItems.find(function(x) { return x.id === id; }); if (!item) return null; Object.assign(item, patch, { updatedAt: Date.now() }); saveCourseBasket(); renderPanel(); return item; } function setCBItemStatus(id, status) { updateCBItem(id, { status: status || '' }); } function addCourseToBasket(info) { var ex = courseBasketItems.find(function(x) { return x.id === info.id; }); if (ex) { var forceSelectable = isForceSelectable(ex); Object.assign(ex, info, { addedAt: ex.addedAt || Date.now(), updatedAt: Date.now() }); if (forceSelectable && info && info.autoBlocked) { ex.forceSelectable = true; ex.autoBlocked = false; if (normalizeCBText(ex.lastMessage) === normalizeCBText(info.lastMessage)) ex.lastMessage = ''; } } else courseBasketItems.push(Object.assign({}, info, { addedAt: Date.now(), updatedAt: Date.now() })); if (info && info.autoBlocked && info.lastMessage) { var target = courseBasketItems.find(function(x) { return x.id === info.id; }); if (target && !isForceSelectable(target) && !normalizeCBText(target.lastMessage)) target.lastMessage = info.lastMessage; } courseBasketItems = courseBasketItems.slice(-40); saveCourseBasket(); renderPanel(); } function removeCourseFromBasket(id) { courseBasketItems = courseBasketItems.filter(function(x) { return x.id !== id; }); capturedApiRequests.delete(id); saveCapturedApiRequests(); saveCourseBasket(); renderPanel(); } function restoreAllCourseStatuses() { autoProcessActive = false; localStorage.removeItem(COURSE_AUTO_PROCESS_KEY); stopApiReadAll(); currentlyTriggeringId = null; currentlyTriggeringAt = 0; clearPending(); courseBasketItems = courseBasketItems.map(function(item) { return Object.assign({}, item, { status: '', retryCount: 0, apiReadSkipped: false, autoBlocked: false, forceSelectable: true, lastMessage: '', updatedAt: Date.now() }); }); saveCourseBasket(); renderPanel(); } // ─── 搜索辅助 + 弹窗检测 ────────────────────────────────────────── function getVisibleDialog() { return Array.from(document.querySelectorAll('.el-message-box__wrapper,.el-dialog__wrapper,[role="dialog"],.v-modal')) .find(function(el) { return !(el.closest && el.closest('#' + COURSE_BASKET_PANEL_ID)) && el.getClientRects().length > 0 && getComputedStyle(el).visibility !== 'hidden' && getComputedStyle(el).display !== 'none'; }); } function getVisibleRealDialog() { return Array.from(document.querySelectorAll('.el-message-box__wrapper,.el-dialog__wrapper,[role="dialog"]')) .find(function(el) { return !(el.closest && el.closest('#' + COURSE_BASKET_PANEL_ID)) && el.getClientRects().length > 0 && getComputedStyle(el).visibility !== 'hidden' && getComputedStyle(el).display !== 'none'; }); } function getDialogText(dialog) { return normalizeCBText((dialog && (dialog.innerText || dialog.textContent)) || ''); } function getOutcomeElements() { var sels = [ '.el-message', '.el-notification', '.el-message-box__wrapper', '.el-dialog__wrapper', '[role="alert"]', '[role="status"]', '.ant-message', '.ivu-message', '.toast' ].join(','); return Array.from(document.querySelectorAll(sels)) .filter(function(el) { return !(el.closest && el.closest('#' + COURSE_BASKET_PANEL_ID)) && isVisibleEl(el); }); } function markCurrentOutcomeElementsIgnored() { getOutcomeElements().forEach(function(el) { ignoredOutcomeElements.add(el); }); } function collectOutcomeTexts() { return getOutcomeElements() .filter(function(el) { return !ignoredOutcomeElements.has(el); }) .map(getElemText) .filter(function(text, idx, arr) { return text && text.length <= 320 && arr.indexOf(text) === idx; }); } function classifyOutcomeText(text) { text = normalizeCBText(text); if (!text) return ''; if (OUTCOME_FAILED_RE.test(text)) return 'failed'; if (OUTCOME_SUCCESS_RE.test(text)) return 'done'; return ''; } function findDialogConfirmButton(dialog) { if (!dialog) return null; return Array.from(dialog.querySelectorAll('button,.el-button,[role="button"]')) .find(function(btn) { var text = normalizeCBText((btn && (btn.innerText || btn.textContent)) || '').replace(/\s+/g, ''); return ['确定', '确认', '是', '同意', 'OK'].indexOf(text) >= 0 || (/^(确定|确认)/.test(text) && text.length <= 8); }) || null; } function findDialogAnyButton(dialog) { if (!dialog) return null; return findDialogConfirmButton(dialog) || Array.from(dialog.querySelectorAll('button,.el-button,[role="button"]')) .find(function(btn) { return isVisibleEl(btn) && !btn.disabled; }) || null; } function getDialogClickSignature(dialog, btn, activeId) { return [ activeId || '', getDialogText(dialog).slice(0, 220), normalizeCBText((btn && (btn.innerText || btn.textContent)) || '').slice(0, 30) ].join('|'); } function hasClickedDialogForCurrentTrigger(dialog, btn, activeId, flagName) { if (!dialog || !dialog.dataset) return false; var key = flagName || 'whutDialogClickSig'; var sig = getDialogClickSignature(dialog, btn, activeId); if (dialog.dataset[key] === sig) return true; dialog.dataset[key] = sig; return false; } function isDialogHardBlocked(text) { return DIALOG_BLOCK_RE.test(text) || /登录失效|登录超时|请登录|重新登录|未登录|无权限|没有权限|失败|异常/.test(text); } function clickAnyApiReadDialog(activeId) { if (!apiReadActive) return false; var dialog = getVisibleRealDialog(); if (!dialog) return false; var anyBtn = findDialogAnyButton(dialog); if (!anyBtn) return false; var text = getDialogText(dialog); if (hasClickedDialogForCurrentTrigger(dialog, anyBtn, activeId, 'whutApiReadClickSig')) return false; anyBtn.click(); var item = (activeId && courseBasketItems.find(function(x) { return x.id === activeId; })) || courseBasketItems.find(function(x) { return x.status === 'triggered' || x.status === 'requesting'; }); if (item) updateCBItem(item.id, { lastMessage: '读取API时已自动确认弹窗: ' + text.slice(0, 160) }); return true; } function observeOutcome() { if (!currentlyTriggeringId) return false; var item = courseBasketItems.find(function(x) { return x.id === currentlyTriggeringId; }); if (!item) return false; var outcomeTexts = collectOutcomeTexts(); for (var i = 0; i < outcomeTexts.length; i++) { var kind = classifyOutcomeText(outcomeTexts[i]); if (kind === 'failed') { if (apiReadActive) { clickAnyApiReadDialog(item.id); if (hasUsableCapture(item.id)) { updateCBItem(item.id, { status: '', apiReadSkipped: false, lastMessage: '已捕获真实选课API,系统提示已忽略: ' + outcomeTexts[i].slice(0, 120) }); } else { updateCBItem(item.id, { status: '', apiReadSkipped: true, lastMessage: '读取API未捕获真实接口,已跳过本轮: ' + outcomeTexts[i].slice(0, 120) }); } currentlyTriggeringId = null; continueAutomationSoon(); return true; } var activeDialog = getVisibleRealDialog(); var activeDialogText = getDialogText(activeDialog); var activeConfirmBtn = findDialogConfirmButton(activeDialog); if (activeDialog && activeConfirmBtn && !isDialogHardBlocked(activeDialogText)) { if (hasClickedDialogForCurrentTrigger(activeDialog, activeConfirmBtn, item.id, 'whutConfirmClickSig')) return false; activeConfirmBtn.click(); return true; } updateCBItem(item.id, { status: 'failed', lastMessage: '❌ ' + outcomeTexts[i] }); currentlyTriggeringId = null; continueAutomationSoon(); return true; } if (kind === 'done') { setCBItemStatus(item.id, 'done'); updateCBItem(item.id, { lastMessage: '✅ 页面选课成功: ' + outcomeTexts[i] }); currentlyTriggeringId = null; continueAutomationSoon(); return true; } } var dialog = getVisibleRealDialog(); if (!dialog) return false; var text = getDialogText(dialog); if (!text) return false; if (apiReadActive) { return clickAnyApiReadDialog(item.id); } if (isDialogHardBlocked(text)) { updateCBItem(item.id, { status: 'failed', lastMessage: '检测到不可自动确认弹窗: ' + text.slice(0, 180) }); currentlyTriggeringId = null; continueAutomationSoon(); return true; } var confirmBtn = findDialogConfirmButton(dialog); if (confirmBtn) { if (hasClickedDialogForCurrentTrigger(dialog, confirmBtn, item.id, 'whutConfirmClickSig')) return false; confirmBtn.click(); return true; } if (OUTCOME_FAILED_RE.test(text)) { updateCBItem(item.id, { status: 'failed', lastMessage: '检测到系统提示: ' + text.slice(0, 180) }); currentlyTriggeringId = null; continueAutomationSoon(); return true; } return false; } function startConfirmScanner() { if (confirmScannerTimer) return; confirmScannerTimer = setInterval(function() { if (apiReadActive && clickAnyApiReadDialog(currentlyTriggeringId)) return; if (!currentlyTriggeringId) return; observeOutcome(); }, 120); } function recoverTriggerIfPending(id, triggerAt) { if (currentlyTriggeringAt !== triggerAt) return; if (currentlyTriggeringId === id) currentlyTriggeringId = null; var item = courseBasketItems.find(function(x) { return x.id === id; }); if (!item || item.status !== 'triggered') return; if (hasUsableCapture(id)) { updateCBItem(id, { status: '', apiReadSkipped: false, lastMessage: '已捕获真实选课API,可继续直接API选课或定时抢课' }); } else if (apiReadActive) { updateCBItem(id, { status: '', apiReadSkipped: true, lastMessage: '读取API未捕获真实接口,已跳过本轮(可能未开放时页面未发送请求)' }); } else { updateCBItem(id, { status: 'failed', lastMessage: '页面按钮触发超时,未捕获真实选课API' }); } continueAutomationSoon(); } function isVisibleEl(el) { if (!el || !el.isConnected) return false; var s = getComputedStyle(el); return el.getClientRects().length > 0 && s.visibility !== 'hidden' && s.display !== 'none'; } function getSearchTerms(item) { var text = normalizeCBText([item && item.searchText, item && item.title, item && item.detail].filter(Boolean).join(' ')); var terms = []; var title = normalizeCBText((item && item.title) || '').replace(/^(课程名称|课程名|名称|课程代码|课程序号)[:|:]/, ''); if (title.length >= 2 && title.length <= 40) terms.push(title); (text.match(/[A-Za-z]{0,6}\d{3,}[A-Za-z0-9_\-]*/g) || []).forEach(function(c) { terms.push(c); }); text.split(/[\/|,,;|;\s]+/).map(normalizeCBText) .filter(function(p) { return p.length >= 2 && p.length <= 30 && /[一-鿿A-Za-z0-9]/.test(p) && !isIgnoredFrag(p); }) .forEach(function(p) { terms.push(p); }); return terms.filter(function(v, i, a) { return a.indexOf(v) === i; }).slice(0, 6); } function scoreSearchInput(inp) { var h = [inp.placeholder, inp.name, inp.id, inp.getAttribute('aria-label'), inp.closest && inp.closest('.el-form-item') && inp.closest('.el-form-item').innerText].filter(Boolean).join(' '); var s = 0; if (/课程|课名|名称|关键|关键词|关键字/.test(h)) s += 8; if (/查询|搜索|检索|筛选/.test(h)) s += 5; if (/教师|老师|校区|学院/.test(h)) s += 1; if (inp.closest && inp.closest('.el-form,form,[class*="search"],[class*="query"],[class*="filter"]')) s += 2; return s; } function findSearchInput() { var inputs = Array.from(document.querySelectorAll('input:not([type]),input[type="text"],input[type="search"],textarea')) .filter(function(i) { return !i.disabled && !i.readOnly && !(i.closest && i.closest('#' + COURSE_BASKET_PANEL_ID)) && isVisibleEl(i); }); if (!inputs.length) return null; var scored = inputs.map(function(i) { return { i: i, s: scoreSearchInput(i) }; }) .filter(function(x) { return x.s > 0; }).sort(function(a, b) { return b.s - a.s; }); return scored.length ? scored[0].i : null; } function setInputValue(input, value) { var proto = input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; var desc = Object.getOwnPropertyDescriptor(proto, 'value'); if (desc && desc.set) desc.set.call(input, value); else input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); } function findSearchButton(input) { var root = (input.closest && input.closest('.el-form,form,[class*="search"],[class*="query"],[class*="filter"]')) || document; return Array.from(root.querySelectorAll('button,a[role="button"],a.el-button')) .filter(function(b) { return !(b.closest && b.closest('#' + COURSE_BASKET_PANEL_ID)) && isVisibleEl(b) && !b.disabled; }) .find(function(b) { return /查询|搜索|检索/.test(getElemText(b).replace(/\s+/g, '')); }) || null; } function rememberPending(id) { localStorage.setItem(COURSE_BASKET_PENDING_KEY, JSON.stringify({ id: id, at: Date.now() })); } function readPending() { try { var p = JSON.parse(localStorage.getItem(COURSE_BASKET_PENDING_KEY) || 'null'); if (!p || !p.id || Date.now() - Number(p.at || 0) > 120000) { localStorage.removeItem(COURSE_BASKET_PENDING_KEY); return null; } return p; } catch (_) { localStorage.removeItem(COURSE_BASKET_PENDING_KEY); return null; } } function clearPending() { localStorage.removeItem(COURSE_BASKET_PENDING_KEY); } function openCoursePage(id, remPending) { var item = courseBasketItems.find(function(x) { return x.id === id; }); if (remPending) rememberPending(id); if (item && item.href && item.href !== ((pageWindow.location && pageWindow.location.href) || location.href)) { location.href = item.href; return true; } return false; } function trySearchInPage(item) { if (!isCourseBasketPage() || !item) return false; var inp = findSearchInput(), term = getSearchTerms(item)[0]; if (!inp || !term) return false; rememberPending(item.id); setCBItemStatus(item.id, 'searching'); inp.focus(); setInputValue(inp, term); var sbtn = findSearchButton(inp); if (sbtn) sbtn.click(); else inp.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true })); setTimeout(function() { scanTargets(); var cur = courseBasketItems.find(function(x) { return x.id === item.id; }); if (!cur || cur.status !== 'searching') return; if (courseBasketBindings.has(item.id)) { setCBItemStatus(item.id, ''); triggerItem(item.id); } else { clearPending(); if (apiReadActive) { updateCBItem(item.id, { status: '', apiReadSkipped: true, lastMessage: '读取API时当前页未找到这门课,已跳过本轮' }); continueAutomationSoon(); } else if (autoProcessActive) { updateCBItem(item.id, { status: 'failed', lastMessage: '当前页搜索后未找到这门课,已跳过' }); continueAutomationSoon(); } else { setCBItemStatus(item.id, ''); window.alert('当前页搜索后未找到这门课,请切换分类、分页或调整搜索条件。'); } } }, 2800); return true; } // ─── 面板 CSS ────────────────────────────────────────────────────── function ensurePanelStyle() { if (document.getElementById('whut-cb-style')) return; var s = document.createElement('style'); s.id = 'whut-cb-style'; s.textContent = [ '#' + COURSE_BASKET_PANEL_ID + '{position:fixed;top:84px;right:18px;width:min(340px,calc(100vw - 24px));max-height:min(720px,calc(100vh - 24px));min-height:160px;z-index:2147483646;background:#fff;color:#1f2937;border:1px solid #d7dde8;border-radius:8px;box-shadow:0 14px 42px rgba(15,23,42,.18);font:13px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif;overflow:hidden;display:flex;flex-direction:column}', '#' + COURSE_BASKET_PANEL_ID + ' button,.' + COURSE_BASKET_ADD_CLASS + '{border:1px solid #c7d2fe;background:#eef2ff;color:#1d4ed8;border-radius:6px;padding:4px 8px;font:inherit;cursor:pointer}', '#' + COURSE_BASKET_PANEL_ID + ' button:hover,.' + COURSE_BASKET_ADD_CLASS + ':hover{background:#dbeafe}', '#' + COURSE_BASKET_PANEL_ID + ' button:disabled{border-color:#d1d5db;background:#f3f4f6;color:#9ca3af;cursor:not-allowed}', '#' + COURSE_BASKET_PANEL_ID + ' .btn-api{border-color:#6ee7b7;background:#d1fae5;color:#065f46}', '#' + COURSE_BASKET_PANEL_ID + ' .btn-api:hover{background:#a7f3d0}', '#' + COURSE_BASKET_PANEL_ID + ' .btn-api:disabled{border-color:#d1d5db;background:#f3f4f6;color:#9ca3af}', '#' + COURSE_BASKET_PANEL_ID + ' .cbh{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 12px;border-bottom:1px solid #e5e7eb;background:#f8fafc;cursor:move;user-select:none;flex:0 0 auto}', '#' + COURSE_BASKET_PANEL_ID + ' .cbt{font-weight:700;white-space:nowrap}', '#' + COURSE_BASKET_PANEL_ID + ' .cba{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end}', '#' + COURSE_BASKET_PANEL_ID + ' .sched-sec{border-bottom:1px solid #e5e7eb;flex:0 0 auto}', '#' + COURSE_BASKET_PANEL_ID + ' .sched-hd{padding:8px 12px;font-weight:600;font-size:12px;cursor:pointer;user-select:none;background:#f8fafc;display:flex;align-items:center;gap:6px}', '#' + COURSE_BASKET_PANEL_ID + ' .sched-hd:hover{background:#f1f5f9}', '#' + COURSE_BASKET_PANEL_ID + ' .sched-bd{padding:8px 10px;display:flex;flex-direction:column;gap:6px}', '#' + COURSE_BASKET_PANEL_ID + ' .sched-row{display:flex;align-items:center;gap:6px}', '#' + COURSE_BASKET_PANEL_ID + ' .sched-lbl{font-size:12px;color:#6b7280;white-space:nowrap}', '#' + COURSE_BASKET_PANEL_ID + ' .sched-dt{flex:1;font:inherit;border:1px solid #d1d5db;border-radius:4px;padding:3px 6px;font-size:12px}', '#' + COURSE_BASKET_PANEL_ID + ' .sched-st{font-size:12px;color:#374151;min-height:16px;white-space:pre-line;line-height:1.5}', '#' + COURSE_BASKET_PANEL_ID + ' .sched-st.armed{color:#d97706;font-weight:600}', '#' + COURSE_BASKET_PANEL_ID + ' .sched-st.fired{color:#059669;font-weight:600}', '#' + COURSE_BASKET_PANEL_ID + ' .sched-st.error{color:#dc2626}', '#' + COURSE_BASKET_PANEL_ID + ' .cbb{flex:1 1 auto;min-height:0;overflow:auto;padding:8px;overscroll-behavior:contain}', '#' + COURSE_BASKET_PANEL_ID + ' .cbe{color:#6b7280;padding:14px 8px}', '#' + COURSE_BASKET_PANEL_ID + ' .cbi{border:1px solid #e5e7eb;border-radius:8px;padding:8px;margin-bottom:8px;background:#fff}', '#' + COURSE_BASKET_PANEL_ID + ' .cbi.is-done{background:#f9fafb;opacity:.76}', '#' + COURSE_BASKET_PANEL_ID + ' .cbi.is-triggered{border-color:#fbbf24}', '#' + COURSE_BASKET_PANEL_ID + ' .cbi.is-api-ready{border-color:#6ee7b7}', '#' + COURSE_BASKET_PANEL_ID + ' .cbi.is-requesting{border-color:#93c5fd;background:#eff6ff}', '#' + COURSE_BASKET_PANEL_ID + ' .cbn{font-weight:700;word-break:break-word}', '#' + COURSE_BASKET_PANEL_ID + ' .cbd,#' + COURSE_BASKET_PANEL_ID + ' .cbs{color:#6b7280;margin-top:3px;word-break:break-word}', '#' + COURSE_BASKET_PANEL_ID + ' .cbra{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}', '.' + COURSE_BASKET_ADD_CLASS + '{margin-left:6px;padding:3px 7px;line-height:1.3}', '.' + COURSE_BASKET_ADD_CLASS + '.is-added{border-color:#bbf7d0;background:#ecfdf5;color:#047857}', '.whut-focused{outline:3px solid #f59e0b!important;outline-offset:2px!important;transition:outline-color .2s}', '@media(max-width:640px){#' + COURSE_BASKET_PANEL_ID + '{top:auto;right:8px;left:8px;bottom:8px;width:auto;max-height:54vh}}' ].join(''); document.head.appendChild(s); } function clampPanelPosition(pos) { var w = Math.max(260, Math.min(window.innerWidth - 12, (courseBasketPanel && courseBasketPanel.offsetWidth) || 340)); var h = Math.max(120, Math.min(window.innerHeight - 12, (courseBasketPanel && courseBasketPanel.offsetHeight) || 240)); return { left: Math.max(6, Math.min(Number(pos.left) || 0, window.innerWidth - w - 6)), top: Math.max(6, Math.min(Number(pos.top) || 0, window.innerHeight - h - 6)) }; } function applySavedPanelPosition() { if (!courseBasketPanel) return; try { var pos = JSON.parse(localStorage.getItem(COURSE_BASKET_POS_KEY) || 'null'); if (!pos) return; pos = clampPanelPosition(pos); courseBasketPanel.style.left = pos.left + 'px'; courseBasketPanel.style.top = pos.top + 'px'; courseBasketPanel.style.right = 'auto'; courseBasketPanel.style.bottom = 'auto'; } catch (_) {} } function savePanelPosition(left, top) { var pos = clampPanelPosition({ left: left, top: top }); try { localStorage.setItem(COURSE_BASKET_POS_KEY, JSON.stringify(pos)); } catch (_) {} if (courseBasketPanel) { courseBasketPanel.style.left = pos.left + 'px'; courseBasketPanel.style.top = pos.top + 'px'; courseBasketPanel.style.right = 'auto'; courseBasketPanel.style.bottom = 'auto'; } } function installPanelDrag() { if (!courseBasketPanel || courseBasketPanel.dataset.whutDragInstalled === '1') return; courseBasketPanel.dataset.whutDragInstalled = '1'; var dragging = null; courseBasketPanel.addEventListener('mousedown', function(ev) { var header = ev.target.closest && ev.target.closest('.cbh'); if (!header || !courseBasketPanel.contains(header)) return; if (ev.target.closest && ev.target.closest('button,a,input,textarea,select,[data-action]')) return; var rect = courseBasketPanel.getBoundingClientRect(); dragging = { dx: ev.clientX - rect.left, dy: ev.clientY - rect.top }; ev.preventDefault(); }); document.addEventListener('mousemove', function(ev) { if (!dragging) return; savePanelPosition(ev.clientX - dragging.dx, ev.clientY - dragging.dy); }); document.addEventListener('mouseup', function() { dragging = null; }); courseBasketPanel.addEventListener('dblclick', function(ev) { var header = ev.target.closest && ev.target.closest('.cbh'); if (!header || !courseBasketPanel.contains(header)) return; if (ev.target.closest && ev.target.closest('button,a,input,textarea,select,[data-action]')) return; localStorage.removeItem(COURSE_BASKET_POS_KEY); courseBasketPanel.style.left = ''; courseBasketPanel.style.top = ''; courseBasketPanel.style.right = ''; courseBasketPanel.style.bottom = ''; }); window.addEventListener('resize', applySavedPanelPosition); } function installPanelScrollGuard() { if (!courseBasketPanel) return; var body = courseBasketPanel.querySelector('[data-role="body"]'); if (!body || body.dataset.whutScrollGuardInstalled === '1') return; body.dataset.whutScrollGuardInstalled = '1'; body.addEventListener('wheel', function(ev) { ev.stopPropagation(); }, { passive: true }); body.addEventListener('touchmove', function(ev) { ev.stopPropagation(); }, { passive: true }); } // ─── 面板骨架 ────────────────────────────────────────────────────── function ensurePanel() { if (!document.body) return null; if (courseBasketPanel && courseBasketPanel.isConnected) return courseBasketPanel; ensurePanelStyle(); courseBasketPanel = document.getElementById(COURSE_BASKET_PANEL_ID) || document.createElement('section'); courseBasketPanel.id = COURSE_BASKET_PANEL_ID; courseBasketPanel.innerHTML = '