// ==UserScript== // @name WHUT选课跳过弹窗 // @namespace http://tampermonkey.net/ // @version 1.1 // @description 安全状态机版:仅在选课入口页跳过确认弹窗,先校验/同步 Authorization Cookie,再清洗入口接口并点击入口按钮,进入后立即停止,避免重复 POST 卡死。 // @author 毫厘 // @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'; let entryTimer = null; let stopTimer = null; let state = 'idle'; let lastHref = ''; let patchedAt = 0; let confirmClickedAt = 0; let enterClickedAt = 0; let conflictPromptedAt = 0; const clickedElements = new WeakSet(); console.log('%c WHUT跳过弹窗: v1.1 状态机版已启动', 'background:#2655c8;color:#fff;font-weight:bold;'); function isProfilePage() { const href = pageWindow.location?.href || location.href; return href.includes(PROFILE_PATH); } function isWhutSameOrigin(url) { try { return new URL(String(url || ''), location.href).origin === 'https://jwxk.whut.edu.cn'; } catch (error) { return false; } } function isEntryGateApiUrl(url) { try { const parsed = new URL(String(url || ''), location.href); return parsed.origin === 'https://jwxk.whut.edu.cn' && ( parsed.pathname.includes('/xsxk/elective/user') || parsed.pathname.includes('/web/now') ); } catch (error) { return false; } } function getCookieValues(name) { const prefix = `${name}=`; return document.cookie .split(';') .map(item => item.trim()) .filter(item => item.startsWith(prefix)) .map(item => item.slice(prefix.length)) .map(value => { try { return decodeURIComponent(value); } catch (error) { return value; } }) .filter(Boolean); } function decodeJwtPayload(token) { try { const payload = String(token || '').split('.')[1]; if (!payload) return null; const normalized = payload.replace(/-/g, '+').replace(/_/g, '/'); const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4); return JSON.parse(atob(padded)); } catch (error) { return null; } } function getTokenTimestamp(token) { const payload = decodeJwtPayload(token); return Number(payload?.time || payload?.iat || 0) || 0; } function pickLatestAuthorizationToken(values = getCookieValues('Authorization')) { const uniqueValues = [...new Set(values.filter(Boolean))]; if (uniqueValues.length <= 1) return uniqueValues[0] || ''; return uniqueValues.sort((left, right) => getTokenTimestamp(right) - getTokenTimestamp(left) )[0] || uniqueValues.at(-1) || ''; } function getAuthorizationCookieConflict() { const values = getCookieValues('Authorization'); const uniqueValues = [...new Set(values)]; return { values, uniqueValues, conflict: uniqueValues.length > 1 }; } function expireAuthorizationCookieAtPath(path) { const host = location.hostname; const domains = ['', host, `.${host}`]; domains.forEach(domain => { const domainPart = domain ? `; domain=${domain}` : ''; document.cookie = `Authorization=; path=${path}${domainPart}; expires=Thu, 01 Jan 1970 00:00:00 GMT; max-age=0; secure; SameSite=Lax`; document.cookie = `Authorization=; path=${path}${domainPart}; expires=Thu, 01 Jan 1970 00:00:00 GMT; max-age=0; SameSite=Lax`; }); } function syncAuthorizationCookiesToLatest(token = '') { const latestToken = token || pickLatestAuthorizationToken(); if (!latestToken || !/^[A-Za-z0-9_.-]+$/.test(latestToken)) return false; const encodedToken = encodeURIComponent(latestToken); const host = location.hostname; const domains = ['', host, `.${host}`]; const paths = ['/', '/xsxk']; paths.forEach(path => { domains.forEach(domain => { const domainPart = domain ? `; domain=${domain}` : ''; document.cookie = `Authorization=${encodedToken}; path=${path}${domainPart}; secure; SameSite=Lax`; }); }); return true; } function repairAuthorizationCookies() { const values = getCookieValues('Authorization'); const uniqueValues = [...new Set(values)]; const latestToken = pickLatestAuthorizationToken(uniqueValues); if (!latestToken) return false; ['/', '/xsxk', '/xsxk/', '/xsxk/profile', '/xsxk/elective'].forEach(expireAuthorizationCookieAtPath); syncAuthorizationCookiesToLatest(latestToken); console.log('%c WHUT跳过弹窗: 已清理重复 Authorization Cookie', 'color:#10b981;font-weight:bold;'); return true; } function promptCookieConflict() { const now = Date.now(); const lastPromptAt = Math.max( conflictPromptedAt, Number(localStorage.getItem(COOKIE_PROMPT_KEY) || 0) ); if (now - lastPromptAt < 60000) return; conflictPromptedAt = now; localStorage.setItem(COOKIE_PROMPT_KEY, String(now)); setTimeout(() => { if (!isProfilePage()) return; const confirmed = window.confirm( '检测到多个不同的 Authorization Cookie。\n\n继续自动点击“选课”可能导致服务端 302 跳回首页,形成卡死循环。\n\n是否现在清理重复 Cookie,并只保留时间最新的 token?' ); if (confirmed) { repairAuthorizationCookies(); window.alert('登录 Cookie 已同步。请刷新页面后再进入选课。'); } }, 300); } function ensureCookieReady() { const values = getCookieValues('Authorization'); if (values.length === 0) return false; const latest = pickLatestAuthorizationToken(values); if (!latest) return false; syncAuthorizationCookiesToLatest(latest); const afterSync = getAuthorizationCookieConflict(); if (afterSync.conflict) { state = 'cookie_conflict'; stopAutoEntry(); promptCookieConflict(); return false; } if (state === 'idle' || state === 'stopped') state = 'cookie_ready'; return true; } function cleanEntryGateData(json) { let modified = false; const fixItem = (item) => { if (!item || typeof item !== 'object') return; if ('needConfirm' in item && item.needConfirm != 0 && item.needConfirm !== '0') { item.needConfirm = '0'; modified = true; } if ('isConfirmed' in item && item.isConfirmed != 1 && item.isConfirmed !== '1') { item.isConfirmed = '1'; modified = true; } if ('canSelect' in item && item.canSelect != 1 && item.canSelect !== '1') { item.canSelect = '1'; modified = true; } ['countDown', 'countdown', 'confirmSeconds', 'remainSeconds', 'remainingSeconds', 'waitSeconds'].forEach(key => { if (key in item && Number(item[key]) > 0) { item[key] = 0; modified = true; } }); }; const visit = (value) => { if (Array.isArray(value)) { value.forEach(visit); return; } if (!value || typeof value !== 'object') return; fixItem(value); Object.values(value).forEach(visit); }; visit(json); return modified; } function markEntryGatePatched() { patchedAt = Date.now(); if (!['confirmed', 'entering', 'done'].includes(state)) state = 'patched'; } function createPatchedJsonResponse(response, json) { const HeadersCtor = pageWindow.Headers || Headers; const headers = new HeadersCtor(response.headers || {}); headers.set('content-type', 'application/json;charset=UTF-8'); const ResponseCtor = pageWindow.Response || Response; return new ResponseCtor(JSON.stringify(json), { status: response.status, statusText: response.statusText, headers }); } function patchEntryGateXhrResponse(xhr) { try { const responseType = xhr.responseType || ''; let json = null; let patchedText = ''; let modified = false; if (responseType === '' || responseType === 'text') { const text = xhr.responseText; if (!text) return false; json = JSON.parse(text); modified = cleanEntryGateData(json); if (modified) { patchedText = JSON.stringify(json); try { Object.defineProperty(xhr, 'responseText', { configurable: true, get: () => patchedText }); } catch (error) {} try { Object.defineProperty(xhr, 'response', { configurable: true, get: () => patchedText }); } catch (error) {} } } else if (responseType === 'json' && xhr.response && typeof xhr.response === 'object') { json = xhr.response; modified = cleanEntryGateData(json); if (modified) { try { Object.defineProperty(xhr, 'response', { configurable: true, get: () => json }); } catch (error) {} } } else { return false; } syncAuthorizationCookiesToLatest(); markEntryGatePatched(); if (modified) { console.log('%c WHUT跳过弹窗: XHR入口数据已清洗', 'color:#17a2b8;font-weight:bold;'); } return modified; } catch (error) { return false; } } function installFetchInterceptor() { try { if (typeof pageWindow.fetch !== 'function' || pageWindow.fetch.__whutSkipPatched) return; const originalFetch = pageWindow.fetch.bind(pageWindow); const wrappedFetch = async function(input, init = {}) { const url = typeof input === 'string' ? input : (input?.url || String(input || '')); const response = await originalFetch(input, init); if (isProfilePage() && isEntryGateApiUrl(response.url || url)) { try { const json = await response.clone().json(); const modified = cleanEntryGateData(json); syncAuthorizationCookiesToLatest(); markEntryGatePatched(); if (modified) { console.log('%c WHUT跳过弹窗: Fetch入口数据已清洗', 'color:#17a2b8;font-weight:bold;'); return createPatchedJsonResponse(response, json); } } catch (error) {} } return response; }; Object.defineProperty(wrappedFetch, '__whutSkipPatched', { value: true }); pageWindow.fetch = wrappedFetch; } catch (error) { console.warn('WHUT跳过弹窗: Fetch拦截安装失败', error); } } function installXhrInterceptor() { try { const XHR = pageWindow.XMLHttpRequest; const prototype = XHR?.prototype; if (!prototype || prototype.__whutSkipPatched) return; const originalOpen = prototype.open; const originalSend = prototype.send; prototype.open = function(method, url) { this.__whutSkipUrl = String(url || ''); return originalOpen.apply(this, arguments); }; prototype.send = function(body) { const isEntryGateRequest = isProfilePage() && isWhutSameOrigin(this.__whutSkipUrl) && isEntryGateApiUrl(this.__whutSkipUrl); if (isEntryGateRequest) { const originalReadyStateHandler = this.onreadystatechange; if (typeof originalReadyStateHandler === 'function') { this.onreadystatechange = function(...args) { if (this.readyState === 4) patchEntryGateXhrResponse(this); return originalReadyStateHandler.apply(this, args); }; } this.addEventListener('readystatechange', function() { if (this.readyState === 4) patchEntryGateXhrResponse(this); }, { once: true }); } return originalSend.apply(this, arguments); }; Object.defineProperty(prototype, '__whutSkipPatched', { value: true, configurable: true }); } catch (error) { console.warn('WHUT跳过弹窗: XHR拦截安装失败', error); } } function stopAutoEntry() { if (entryTimer) clearInterval(entryTimer); if (stopTimer) clearTimeout(stopTimer); entryTimer = null; stopTimer = null; if (!['done', 'cookie_conflict', 'entry_timeout'].includes(state)) state = 'stopped'; } function markDone() { state = 'done'; stopAutoEntry(); } function isButtonClickable(button) { return Boolean( button && button.isConnected && button.getClientRects().length > 0 && !button.disabled && !button.classList.contains('is-disabled') && button.getAttribute('aria-disabled') !== 'true' ); } function safeClick(button, nextState) { if (!isButtonClickable(button) || clickedElements.has(button)) return false; clickedElements.add(button); syncAuthorizationCookiesToLatest(); button.click(); state = nextState || state; console.log(`%c WHUT跳过弹窗: 自动点击 ${button.innerText.trim()}`, 'color:#17a2b8;'); if (nextState === 'entering') { setTimeout(() => { if (!isProfilePage()) markDone(); }, 1200); } return true; } function getPrimaryButtons() { return [...document.querySelectorAll('button.el-button--primary')] .filter(isButtonClickable) .map(button => ({ button, text: button.innerText.replace(/\s+/g, '') })); } function scanAutoEntryTargets() { if (!isProfilePage()) { markDone(); return; } const currentHref = pageWindow.location?.href || location.href; if (lastHref && currentHref !== lastHref && !currentHref.includes(PROFILE_PATH)) { markDone(); return; } lastHref = currentHref; if (!ensureCookieReady()) return; if (state === 'entering') { if (enterClickedAt && Date.now() - enterClickedAt > 6000) { state = 'entry_timeout'; stopAutoEntry(); console.warn('WHUT跳过弹窗: 点击进入后仍停留在首页,已暂停自动入口。可尝试修复 Cookie 后刷新。'); } return; } const buttons = getPrimaryButtons(); const confirmButton = buttons.find(({ text }) => ['确定', '确认', '我知道了', '同意'].some(keyword => text.includes(keyword)) )?.button; if ( confirmButton && ['cookie_ready', 'patched'].includes(state) && (!confirmClickedAt || Date.now() - confirmClickedAt > 2500) ) { confirmClickedAt = Date.now(); safeClick(confirmButton, 'confirmed'); return; } const enterButton = buttons.find(({ text }) => text === '选课' || text === '进入选课' || text === '进入' )?.button; if ( enterButton && ['cookie_ready', 'patched', 'confirmed'].includes(state) && !enterClickedAt ) { enterClickedAt = Date.now(); safeClick(enterButton, 'entering'); } } function recordProfileVisitAndWarn() { if (!isProfilePage()) return; const now = Date.now(); const visits = (() => { try { return JSON.parse(localStorage.getItem(PROFILE_STUCK_KEY) || '[]'); } catch (error) { return []; } })() .map(Number) .filter(time => Number.isFinite(time) && now - time < 60000); visits.push(now); localStorage.setItem(PROFILE_STUCK_KEY, JSON.stringify(visits)); if (visits.length < 3) return; const lastPromptAt = Number(localStorage.getItem(COOKIE_PROMPT_KEY) || 0); if (now - lastPromptAt < 60000) return; localStorage.setItem(COOKIE_PROMPT_KEY, String(now)); setTimeout(() => { if (!isProfilePage()) return; const confirmed = window.confirm( '检测到你短时间内多次回到选课首页。\n\n常见原因是 Authorization Cookie 冲突,导致进入选课页时 302 回跳。\n\n是否清理重复 Cookie,并只保留时间最新的 token?' ); if (confirmed) { repairAuthorizationCookies(); window.alert('登录 Cookie 已同步。请刷新页面后再进入选课。'); } }, 800); } function startAutoEntry() { if (entryTimer || !isProfilePage()) return; state = 'idle'; confirmClickedAt = 0; enterClickedAt = 0; patchedAt = 0; lastHref = pageWindow.location?.href || location.href; scanAutoEntryTargets(); entryTimer = setInterval(scanAutoEntryTargets, ENTRY_SCAN_INTERVAL); stopTimer = setTimeout(stopAutoEntry, ENTRY_SCAN_DURATION); } installFetchInterceptor(); installXhrInterceptor(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { recordProfileVisitAndWarn(); startAutoEntry(); }); } else { recordProfileVisitAndWarn(); startAutoEntry(); } })();