// ==UserScript== // @name 华夏系统增强工具 // @namespace hxxy-enhancer // @version 5.6.4 // @description 华夏系统增强工具 // @author Zhang // @license MIT // @match https://me.hxxy.edu.cn/* // @match https://plat.hxxy.edu.cn/* // @match https://*.hxxy.edu.cn/* // @connect me.hxxy.edu.cn // @connect plat.hxxy.edu.cn // @connect *.hxxy.edu.cn // @run-at document-start // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // ==/UserScript== (function () { 'use strict'; const VERSION = '5.6.4'; const STORAGE_KEY = 'hxxy-enhancer-config-v3'; const LOG_STORAGE_KEY = 'hxxy-enhancer-api-logs-v1'; const EVENT_CONFIG = 'hxxy-enhancer-config'; const EVENT_LOG = 'hxxy-enhancer-log'; const EVENT_STATE = 'hxxy-enhancer-state'; const INSTANCE_LOCK = '__HX_ENHANCER_INSTANCE__'; const WATCHDOG_STORAGE_KEY = 'hxxy-enhancer-watchdog-v1'; const WATCHDOG_TIMEOUT_MS = 3000; const TARGET_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0'; // Capture the real userscript environment before the optional page UA lock runs. const RUNTIME_ENVIRONMENT = (() => { const userAgent = navigator.userAgent || ''; const platform = navigator.platform || ''; const maxTouchPoints = Number(navigator.maxTouchPoints) || 0; const isIOS = /iPad|iPhone|iPod/i.test(userAgent) || (platform === 'MacIntel' && maxTouchPoints > 1); return Object.freeze({ userAgent, platform, vendor: navigator.vendor || '', language: navigator.language || '', maxTouchPoints, isIOS }); })(); const fieldsMap = { ClassCodes: '', counselorClzz: '0', classcode: '0', College: '0' }; // ScriptCat may evaluate overlapping @match entries as separate instances. // Keep one owner per top window and let a stale owner expire naturally. const topWindow = window.top || window; const nowMs = () => Date.now(); if (window.top === window.self) { try { const previous = topWindow[INSTANCE_LOCK]; if (previous && previous.owner && nowMs() - previous.startedAt < 15000) return; topWindow[INSTANCE_LOCK] = { owner: Math.random().toString(36).slice(2), startedAt: nowMs() }; } catch (e) {} } const defaultRules = [ { id: 'builtin-check-info-activity', enabled: true, name: '(内置)解除综测活动未开始限制', mode: 'modifyResponse', match: { url: '/studentwork/HXAssessmentAppraise/CheckInfoActivity', regex: false }, modify: { pattern: '("isok"\\s*:\\s*)false', replacement: '$1true', regex: true } }, { id: 'builtin-Plat-App-info', enabled: true, name: '(内置)解除Plat应用访问限制', mode: 'modifyResponse', match: { url: '/QY/CheckMyApp', regex: false }, modify: { pattern: '("isok"\\s*:\\s*)false', replacement: '$1true', regex: true } }, { id: 'builtin-Assessment-timeout-activity', enabled: true, name: '(内置)解除综测申请过期限制', mode: 'modifyResponse', match: { url: '/studentwork/HXAssessmentActivity/actVerifyInfo', regex: false }, modify: { pattern: '("isok"\\s*:\\s*)false', replacement: '$1true', regex: true } }, { id: 'builtin-activity-year-zero', enabled: true, name: '(内置)综测总是加载全部活动', mode: 'modifyRequest', match: { url: '/studentwork/hxassessmentactivity/_activitypagelist', regex: false }, modify: { pattern: '(^|&)Year=[^&]*', replacement: '$1Year=0', regex: true } }, { id: 'builtin-verify-role-true', enabled: true, name: '(内置)解除学生活动审核限制', mode: 'replaceResponse', match: { url: '/studentwork/LessonActivityMobile/VerifyCurUserRole', regex: false }, response: { lineEnabled: false, headersEnabled: false, bodyEnabled: true, body: 'true' } } ]; function normalizeRule(rule) { if (!rule || typeof rule !== 'object') return null; if (rule.mode) { return Object.assign({ enabled: true, match: { url: '', regex: false }, request: { lineEnabled: false, headersEnabled: false, bodyEnabled: false, method: '', url: '', headers: '{}', body: '' }, response: { lineEnabled: false, headersEnabled: false, bodyEnabled: false, status: 200, statusText: '', headers: '{}', body: '' }, modify: { pattern: '', replacement: '', regex: true }, redirect: { url: '' } }, rule, { match: Object.assign({ url: '', regex: false }, rule.match || {}), request: Object.assign({ lineEnabled: false, headersEnabled: false, bodyEnabled: false, method: '', url: '', headers: '{}', body: '' }, rule.request || {}), response: Object.assign({ lineEnabled: false, headersEnabled: false, bodyEnabled: false, method: '', url: '', headers: '{}', body: '' }, rule.response || {}), modify: Object.assign({ pattern: '', replacement: '', regex: true }, rule.modify || {}), redirect: Object.assign({ url: '' }, rule.redirect || {}) }); } const phase = rule.match && (rule.match.phase || rule.match.scope) || 'response'; const action = rule.action || {}; const migrated = Object.assign({}, rule, { mode: phase === 'request' ? 'modifyRequest' : 'modifyResponse', match: { url: rule.match && rule.match.value || '', regex: rule.match && rule.match.type === 'regex' }, modify: { pattern: action.search || '', replacement: action.replace == null ? '' : String(action.replace), regex: !!action.regex } }); if (action.type === 'json') migrated.legacyAction = { type: 'json', path: action.path, value: action.value }; return migrated; } const defaultConfig = { hookEnabled: true, domPatchEnabled: true, apiEnabled: true, panelEnabled: true, logEnabled: true, maxLogs: 200, rules: defaultRules, savedApis: [], uaEnabled: true }; const clone = value => JSON.parse(JSON.stringify(value)); function loadConfig() { let saved = {}; try { saved = GM_getValue(STORAGE_KEY, {}) || {}; } catch (e) { console.warn('[Zhang华夏系统增强] 配置读取失败', e); } const savedRules = Array.isArray(saved.rules) ? saved.rules.map(normalizeRule).filter(Boolean) : []; const savedRuleById = new Map(savedRules.map(rule => [rule.id, rule])); const builtInRuleIds = new Set(defaultRules.map(rule => rule.id)); const rules = clone(defaultRules).map(defaultRule => { const savedRule = savedRuleById.get(defaultRule.id); if (!savedRule) return defaultRule; return Object.assign({}, defaultRule, savedRule, { enabled: savedRule.enabled !== false, mode: defaultRule.mode, match: clone(defaultRule.match), request: Object.assign({}, defaultRule.request || {}, savedRule.request || {}), response: Object.assign({}, defaultRule.response || {}, savedRule.response || {}, defaultRule.response || {}), modify: Object.assign({}, defaultRule.modify || {}, savedRule.modify || {}), redirect: Object.assign({}, defaultRule.redirect || {}, savedRule.redirect || {}) }); }).concat(savedRules.filter(rule => !builtInRuleIds.has(rule.id))); return Object.assign({}, clone(defaultConfig), saved, { rules, savedApis: Array.isArray(saved.savedApis) ? saved.savedApis : [], uaEnabled: saved.uaEnabled !== false }); } let config = loadConfig(); const truncateLogText = (value, limit = 32768) => { const text = value == null ? '' : String(value); return text.length > limit ? text.slice(0, limit) + '\n...[已截断]' : text; }; function loadPersistedLogs() { try { const saved = GM_getValue(LOG_STORAGE_KEY, []); if (!Array.isArray(saved)) return []; return saved.slice(0, Math.max(20, Number(config.maxLogs) || 200)); } catch (e) { console.warn('[Zhang华夏系统增强] API日志读取失败', e); return []; } } function persistLogs() { try { const maxLogs = Math.max(20, Number(config.maxLogs) || 200); const persisted = logs.slice(0, maxLogs).map(entry => Object.assign({}, entry, { requestBody: truncateLogText(entry.requestBody), originalResponse: truncateLogText(entry.originalResponse), modifiedResponse: truncateLogText(entry.modifiedResponse) })); GM_setValue(LOG_STORAGE_KEY, persisted); } catch (e) { console.warn('[Zhang华夏系统增强] API日志保存失败', e); } } let logs = loadPersistedLogs(); const seenLogEntries = new WeakSet(); const recentLogKeys = new Map(); function saveConfig() { try { GM_setValue(STORAGE_KEY, config); } catch (e) { console.warn('[Zhang华夏系统增强] 配置保存失败', e); } } function emitConfig() { const detail = { config: clone(config) }; window.dispatchEvent(new CustomEvent(EVENT_CONFIG, { detail })); const broadcast = doc => { try { doc.querySelectorAll('iframe').forEach(frame => { const child = frame.contentWindow; if (child) child.dispatchEvent(new CustomEvent(EVENT_CONFIG, { detail })); if (frame.contentDocument) broadcast(frame.contentDocument); }); } catch (e) {} }; broadcast(document); } function localLog(level, ...args) { (console[level] || console.log).call(console, '[Zhang华夏系统增强]', ...args); } function addLog(entry) { if (!config.logEnabled || !entry) return; if (typeof entry === 'object') { if (seenLogEntries.has(entry)) return; seenLogEntries.add(entry); } const logKey = entry.id || [entry.method, entry.url, entry.status, entry.requestBody || '', entry.modified, (entry.matchedRules || []).join(',')].join('|'); const now = Date.now(); const previous = recentLogKeys.get(logKey); if (previous && now - previous < 1000) return; recentLogKeys.set(logKey, now); recentLogKeys.forEach((time, key) => { if (now - time > 5000) recentLogKeys.delete(key); }); logs.unshift(entry); logs = logs.slice(0, Math.max(20, Number(config.maxLogs) || 200)); persistLogs(); } const patchedFields = new WeakMap(); function patchField(el, fieldId, targetValue) { if (!el || !('value' in el) || el.value === targetValue) return; let patched = patchedFields.get(el); if (!patched) { patched = new Map(); patchedFields.set(el, patched); } if (patched.get(fieldId) === targetValue) return; // Mark before dispatching events because page handlers can synchronously restore the value. patched.set(fieldId, targetValue); el.value = targetValue; el.setAttribute('value', targetValue); try { el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} localLog('log', `字段 ${fieldId} 已修改为 "${targetValue}"`); } function patchDocument(doc, root = doc) { if (!config.domPatchEnabled || !doc || !root) return; for (const [fieldId, targetValue] of Object.entries(fieldsMap)) { let nodes = []; try { const selector = `#${CSS.escape(fieldId)}, [id="${fieldId}"], .${CSS.escape(fieldId)}`; if (root.nodeType === 1 && root.matches(selector)) nodes = [root]; if (root.querySelectorAll) nodes.push(...root.querySelectorAll(selector)); } catch (e) { const selector = `[id="${fieldId}"], .${fieldId}`; if (root.nodeType === 1 && root.matches && root.matches(selector)) nodes = [root]; if (root.querySelectorAll) nodes.push(...root.querySelectorAll(selector)); } nodes.forEach(el => patchField(el, fieldId, targetValue)); } } const observedDocs = new WeakSet(); function injectUserAgentIntoDocument(doc) { if (!config.uaEnabled || !doc) return; const win = doc.defaultView; if (!win || win.__HX_ENHANCER_UA_LOCKED__) return; const parent = doc.documentElement || doc.head || doc.body; if (!parent) return; const script = doc.createElement('script'); script.textContent = `(() => { 'use strict'; if (window.__HX_ENHANCER_UA_LOCKED__) return; const target = ${JSON.stringify(TARGET_USER_AGENT)}; const define = (key, value) => { try { Object.defineProperty(navigator, key, { configurable: true, get: () => value }); } catch (e) {} }; define('userAgent', target); define('appVersion', target.replace('Mozilla/5.0 ', '')); define('platform', 'Win32'); define('vendor', 'Google Inc.'); define('productSub', '20030107'); define('maxTouchPoints', 0); try { Object.defineProperty(navigator, 'userAgentData', { configurable: true, get: () => ({ brands: [ { brand: 'Not:A-Brand', version: '99' }, { brand: 'Microsoft Edge', version: '145' }, { brand: 'Chromium', version: '145' } ], mobile: false, platform: 'Windows', getHighEntropyValues: async () => ({ architecture: 'x86', bitness: '64', mobile: false, model: '', platform: 'Windows', platformVersion: '10.0.0', uaFullVersion: '145.0.0.0' }) }) }); } catch (e) {} window.__HX_ENHANCER_UA_LOCKED__ = true; })();`; parent.appendChild(script); script.remove(); } function injectPageHookIntoDocument(doc) { if (!config.hookEnabled || !config.apiEnabled || !doc) return; const win = doc.defaultView; if (!win || win.__HX_ENHANCER_PAGE_HOOK__) return; const parent = doc.documentElement || doc.head || doc.body; if (!parent) return; const script = doc.createElement('script'); script.textContent = pageHookSource(config); parent.appendChild(script); script.remove(); } function bridgeFrameEvents(doc) { const win = doc && doc.defaultView; if (!win || win === window || win.__HX_ENHANCER_EVENT_BRIDGE__) return; win.__HX_ENHANCER_EVENT_BRIDGE__ = true; win.addEventListener(EVENT_LOG, event => { window.dispatchEvent(new CustomEvent(EVENT_LOG, { detail: event.detail })); }); } function observeDocument(doc) { if (!doc || observedDocs.has(doc)) return; observedDocs.add(doc); patchDocument(doc); injectUserAgentIntoDocument(doc); injectPageHookIntoDocument(doc); bridgeFrameEvents(doc); const root = doc.documentElement || doc; if (typeof MutationObserver !== 'undefined') { const observer = new MutationObserver(mutations => mutations.forEach(mutation => { if (mutation.type === 'attributes') { patchDocument(doc, mutation.target); return; } mutation.addedNodes.forEach(node => { if (node.nodeType === 1) patchDocument(doc, node); }); })); observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['id', 'class'] }); } doc.querySelectorAll('iframe').forEach(bindFrame); } function bindFrame(frame) { if (!frame || frame.__HX_ENHANCER_BOUND__) return; frame.__HX_ENHANCER_BOUND__ = true; const load = () => { try { if (frame.contentDocument) observeDocument(frame.contentDocument); } catch (e) {} }; frame.addEventListener('load', load); load(); } function startWatchdog() { if (window.top !== window.self) return; let recoveryScheduled = false; let lastTick = performance.now(); const scheduleRecovery = reason => { if (recoveryScheduled) return; recoveryScheduled = true; const stamp = String(Date.now()); try { const previous = sessionStorage.getItem(WATCHDOG_STORAGE_KEY); if (previous && Date.now() - Number(previous) < 15000) { localLog('warn', '检测到重复卡顿恢复,停止连续刷新', reason); return; } sessionStorage.setItem(WATCHDOG_STORAGE_KEY, stamp); } catch (e) {} localLog('warn', `页面连续无响应超过 ${WATCHDOG_TIMEOUT_MS}ms,准备刷新恢复`, reason); const recover = () => { try { window.stop(); } catch (e) {} window.location.reload(); }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', recover, { once: true }); else setTimeout(recover, 0); }; const workerSource = `setInterval(() => postMessage({ type: 'heartbeat', sentAt: Date.now() }), 500);`; try { const blob = new Blob([workerSource], { type: 'text/javascript' }); const worker = new Worker(URL.createObjectURL(blob)); worker.onmessage = event => { if (!event.data) return; if (event.data.type === 'heartbeat') { const delay = Date.now() - Number(event.data.sentAt || Date.now()); if (delay >= WATCHDOG_TIMEOUT_MS) scheduleRecovery(`主线程消息延迟约 ${delay}ms`); } }; window.addEventListener('beforeunload', () => worker.terminate(), { once: true }); } catch (e) { localLog('warn', 'Worker看门狗启动失败,使用主线程长任务监测', e); } if (typeof PerformanceObserver !== 'undefined') { try { const observer = new PerformanceObserver(list => list.getEntries().forEach(entry => { if (entry.duration >= WATCHDOG_TIMEOUT_MS) scheduleRecovery(`Long Task ${Math.round(entry.duration)}ms`); })); observer.observe({ type: 'longtask', buffered: true }); } catch (e) {} } const heartbeat = () => { const current = performance.now(); if (current - lastTick >= WATCHDOG_TIMEOUT_MS) scheduleRecovery(`心跳间隔 ${Math.round(current - lastTick)}ms`); lastTick = current; setTimeout(heartbeat, 500); }; setTimeout(heartbeat, 500); } function startDomPatch() { const start = () => { if (!document.documentElement) return false; observeDocument(document); const observer = new MutationObserver(mutations => mutations.forEach(m => m.addedNodes.forEach(node => { if (node.nodeType !== 1) return; if (node.matches && node.matches('iframe')) bindFrame(node); if (node.querySelectorAll) node.querySelectorAll('iframe').forEach(bindFrame); }))); observer.observe(document.documentElement, { childList: true, subtree: true }); return true; }; if (!start()) document.addEventListener('DOMContentLoaded', start, { once: true }); } function pageHookSource(initialConfig) { return `(${function (EVENT_CONFIG, EVENT_LOG, EVENT_STATE, initialConfig) { 'use strict'; if (window.__HX_ENHANCER_PAGE_HOOK__) return; window.__HX_ENHANCER_PAGE_HOOK__ = true; let state = { config: initialConfig }; const nativeOpen = XMLHttpRequest.prototype.open; const nativeSend = XMLHttpRequest.prototype.send; const nativeFetch = window.fetch; const nativeResponseDescriptor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'response'); const nativeResponseTextDescriptor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'responseText'); if (nativeResponseDescriptor && nativeResponseDescriptor.get && nativeResponseDescriptor.configurable) { try { Object.defineProperty(XMLHttpRequest.prototype, 'response', { configurable: nativeResponseDescriptor.configurable, enumerable: nativeResponseDescriptor.enumerable, get() { return Object.prototype.hasOwnProperty.call(this, '__HX_RESPONSE_OVERRIDE__') ? this.__HX_RESPONSE_OVERRIDE__ : nativeResponseDescriptor.get.call(this); } }); } catch (e) {} } if (nativeResponseTextDescriptor && nativeResponseTextDescriptor.get && nativeResponseTextDescriptor.configurable) { try { Object.defineProperty(XMLHttpRequest.prototype, 'responseText', { configurable: nativeResponseTextDescriptor.configurable, enumerable: nativeResponseTextDescriptor.enumerable, get() { return Object.prototype.hasOwnProperty.call(this, '__HX_RESPONSE_TEXT_OVERRIDE__') ? this.__HX_RESPONSE_TEXT_OVERRIDE__ : nativeResponseTextDescriptor.get.call(this); } }); } catch (e) {} } const now = () => performance && performance.now ? performance.now() : Date.now(); const getUrl = input => typeof input === 'string' ? input : (input && input.url ? input.url : String(input || '')); function matches(rule, url) { if (!rule || !rule.enabled || !rule.match || !rule.match.url) return false; try { return rule.match.regex ? new RegExp(rule.match.url).test(url) : url.indexOf(String(rule.match.url)) >= 0; } catch (e) { return false; } } function applyText(text, rule) { if (!rule || !rule.modify || !rule.modify.pattern) return { text, modified: false }; try { const next = rule.modify.regex ? text.replace(new RegExp(rule.modify.pattern, 'g'), String(rule.modify.replacement == null ? '' : rule.modify.replacement)) : text.split(rule.modify.pattern).join(String(rule.modify.replacement == null ? '' : rule.modify.replacement)); return { text: next, modified: next !== text }; } catch (e) { return { text, modified: false }; } } function applyLegacyJson(text, rule) { if (!rule.legacyAction || rule.legacyAction.type !== 'json') return { text, modified: false }; try { const data = JSON.parse(text); const parts = String(rule.legacyAction.path || '').replace(/^\\$\\.?/, '').split('.').filter(Boolean); if (!parts.length) return { text, modified: false }; let cursor = data; for (let i = 0; i < parts.length - 1; i += 1) { if (!cursor || typeof cursor !== 'object') return { text, modified: false }; cursor = cursor[parts[i]]; } if (!cursor || typeof cursor !== 'object') return { text, modified: false }; cursor[parts[parts.length - 1]] = rule.legacyAction.value; const next = JSON.stringify(data); return { text: next, modified: next !== text }; } catch (e) { return { text, modified: false }; } } function modifyBody(url, originalText, phase) { if (!state.config.apiEnabled || typeof originalText !== 'string') return { text: originalText, modified: false, matchedRules: [] }; let text = originalText; let modified = false; const matchedRules = []; (state.config.rules || []).forEach(rule => { const expectedMode = phase === 'request' ? 'modifyRequest' : 'modifyResponse'; if (!matches(rule, url) || rule.mode !== expectedMode) return; matchedRules.push(rule.name || rule.id || 'unnamed'); const result = rule.legacyAction ? applyLegacyJson(text, rule) : applyText(text, rule); if (result.modified) { text = result.text; modified = true; } if (phase === 'response' && rule.mode === 'replaceResponse' && rule.response && rule.response.bodyEnabled) { text = String(rule.response.body == null ? '' : rule.response.body); modified = text !== originalText; } }); return { text, modified, matchedRules }; } function patchXhrResponse(xhr, responseType, text) { try { if (responseType === 'json') { xhr.__HX_RESPONSE_OVERRIDE__ = JSON.parse(text); } else { xhr.__HX_RESPONSE_TEXT_OVERRIDE__ = text; xhr.__HX_RESPONSE_OVERRIDE__ = text; } return true; } catch (e) { return false; } } function parseHeaderObject(value) { try { return value && typeof value === 'object' ? value : JSON.parse(value || '{}'); } catch (e) { return {}; } } function applyRequestHeaders(url, headers) { const next = new Headers(headers || {}); const matchedRules = []; (state.config.rules || []).forEach(rule => { if (!rule.enabled || !matches(rule, url) || rule.mode !== 'replaceRequest' || !rule.request || !rule.request.headersEnabled) return; matchedRules.push(rule.name || rule.id || 'unnamed'); const replacement = parseHeaderObject(rule.request.headers); Object.keys(replacement).forEach(key => { try { next.set(key, String(replacement[key])); } catch (e) {} }); }); return { headers: next, modified: matchedRules.length > 0, matchedRules }; } function applyResponseHeaders(url, headers) { const next = new Headers(headers || {}); const matchedRules = []; (state.config.rules || []).forEach(rule => { if (!rule.enabled || !matches(rule, url) || rule.mode !== 'replaceResponse' || !rule.response || !rule.response.headersEnabled) return; matchedRules.push(rule.name || rule.id || 'unnamed'); const replacement = parseHeaderObject(rule.response.headers); Object.keys(replacement).forEach(key => { try { next.set(key, String(replacement[key])); } catch (e) {} }); }); return { headers: next, modified: matchedRules.length > 0, matchedRules }; } function transformRequest(url, method, body, headers) { let nextUrl = url; let nextMethod = method; let nextBody = body; let nextHeaders = headers || {}; let modified = false; const matchedRules = []; (state.config.rules || []).forEach(rule => { if (!rule.enabled || !matches(rule, nextUrl)) return; if (rule.mode === 'redirect' && rule.redirect && rule.redirect.url) { nextUrl = rule.redirect.url; modified = true; matchedRules.push(rule.name || rule.id || 'unnamed'); return; } if (rule.mode !== 'replaceRequest') return; matchedRules.push(rule.name || rule.id || 'unnamed'); const request = rule.request || {}; if (request.lineEnabled) { if (request.method) nextMethod = request.method; if (request.url) nextUrl = request.url; modified = true; } if (request.bodyEnabled && typeof nextBody === 'string') { nextBody = String(request.body == null ? '' : request.body); modified = true; } }); const headerResult = applyRequestHeaders(nextUrl, nextHeaders); const bodyResult = modifyBody(nextUrl, nextBody, 'request'); return { url: nextUrl, method: nextMethod, body: bodyResult.text, headers: headerResult.headers, modified: modified || bodyResult.modified || headerResult.matchedRules.length > 0, matchedRules: matchedRules.concat(headerResult.matchedRules, bodyResult.matchedRules) }; } function transformResponse(url, originalText) { const result = modifyBody(url, originalText, 'response'); let text = result.text; let modified = result.modified; const matchedRules = result.matchedRules.slice(); (state.config.rules || []).forEach(rule => { if (!rule.enabled || !matches(rule, url) || rule.mode !== 'replaceResponse') return; const response = rule.response || {}; if (!response.bodyEnabled) return; matchedRules.push(rule.name || rule.id || 'unnamed'); text = String(response.body == null ? '' : response.body); modified = text !== originalText; }); return { text, modified, matchedRules }; } function sendLog(start, method, url, status, originalText, result, requestBody, requestResult) { if (!state.config.logEnabled) return; const requestMatchedRules = requestResult && requestResult.matchedRules ? requestResult.matchedRules : []; window.dispatchEvent(new CustomEvent(EVENT_LOG, { detail: { id: ` $ { Date.now() } - $ { Math.random().toString(36).slice(2) } `, time: new Date().toLocaleTimeString(), timestamp: Date.now(), method: method || 'GET', url, status: status == null ? 0 : status, duration: Math.round(now() - start), modified: !!result.modified || !!(requestResult && requestResult.modified), matchedRules: requestMatchedRules.concat(result.matchedRules || []), requestBody: requestBody == null ? '' : String(requestBody), originalResponse: originalText, modifiedResponse: result.text } })); } function processXhrResponse(xhr, meta) { if (xhr.readyState !== 4 || meta.done) return; meta.done = true; try { const responseType = xhr.responseType || 'text'; const nativeResponse = responseType === 'json' ? xhr.response : null; const originalText = responseType === 'json' ? (nativeResponse == null ? '' : JSON.stringify(nativeResponse)) : (responseType === 'text' || responseType === '' ? xhr.responseText : ''); const result = transformResponse(meta.url, originalText); if (result.modified && responseType !== 'document' && responseType !== 'arraybuffer' && responseType !== 'blob') { if (!patchXhrResponse(xhr, responseType, result.text)) console.warn('[Zhang华夏系统增强] XHR响应回写失败'); } sendLog(meta.start, meta.method, meta.url, xhr.status, originalText, result, meta.requestBody, meta.requestResult); } catch (e) { console.warn('[Zhang华夏系统增强] XHR处理失败', e); } } XMLHttpRequest.prototype.open = function (method, url) { const transformed = transformRequest(getUrl(url), method || 'GET', '', {}); this.__HX_META__ = { method: transformed.method || method || 'GET', url: transformed.url, start: 0, done: false, requestHeaders: {}, requestHeaderResult: transformed }; const xhr = this; const meta = this.__HX_META__; xhr.addEventListener('readystatechange', () => processXhrResponse(xhr, meta), false); xhr.addEventListener('load', () => processXhrResponse(xhr, meta), false); return nativeOpen.call(this, transformed.method || method, transformed.url, ...Array.prototype.slice.call(arguments, 2)); }; const nativeSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.setRequestHeader = function (name, value) { const meta = this.__HX_META__; if (meta) { meta.requestHeaders[name] = value; if (meta.requestHeaderResult && meta.requestHeaderResult.headers) { const replacement = meta.requestHeaderResult.headers.get(name); if (replacement != null) value = replacement; } } return nativeSetRequestHeader.call(this, name, value); }; XMLHttpRequest.prototype.send = function () { const xhr = this; const meta = xhr.__HX_META__ || { method: 'GET', url: '', done: false }; meta.start = now(); const originalBody = arguments[0]; const requestResult = typeof originalBody === 'string' ? transformRequest(meta.url, meta.method, originalBody) : { url: meta.url, method: meta.method, body: originalBody, modified: false, matchedRules: [] }; meta.requestResult = requestResult; meta.requestBody = requestResult.body == null ? '' : requestResult.body; if (requestResult.headers) requestResult.headers.forEach((value, name) => { if (!Object.prototype.hasOwnProperty.call(meta.requestHeaders, name)) { try { nativeSetRequestHeader.call(xhr, name, value); } catch (e) {} } }); const sendArgs = requestResult.modified ? [requestResult.body] : arguments; return nativeSend.apply(this, sendArgs); }; window.fetch = function () { const args = Array.prototype.slice.call(arguments); const input = args[0]; const init = Object.assign({}, args[1] || {}); const originalUrl = getUrl(input); const originalMethod = init.method || (input && input.method) || 'GET'; const originalBody = init.body; const requestHeaders = new Headers(init.headers || (input && input.headers) || {}); const requestResult = transformRequest(originalUrl, originalMethod, originalBody, requestHeaders); const start = now(); if (requestResult.url !== originalUrl || requestResult.method !== originalMethod) { if (input instanceof Request) args[0] = new Request(requestResult.url, { method: requestResult.method, headers: requestResult.headers, body: requestResult.body, credentials: input.credentials, mode: input.mode, cache: input.cache, redirect: input.redirect, referrer: input.referrer, referrerPolicy: input.referrerPolicy, integrity: input.integrity }); else { args[0] = requestResult.url; init.method = requestResult.method; init.body = requestResult.body; init.headers = requestResult.headers; } } else if (requestResult.modified) { init.body = requestResult.body; init.headers = requestResult.headers; } args[1] = init; return nativeFetch.apply(this, args).then(async response => { try { const originalText = await response.clone().text(); const result = transformResponse(requestResult.url, originalText); sendLog(start, requestResult.method, requestResult.url, response.status, originalText, result, requestResult.body || '', requestResult); const headerResult = applyResponseHeaders(requestResult.url, response.headers); if (!result.modified && !headerResult.modified) return response; headerResult.headers.delete('content-length'); return new Response(result.text, { status: response.status, statusText: response.statusText, headers: headerResult.headers }); } catch (e) { console.warn('[Zhang华夏系统增强] fetch处理失败', e); return response; } }); }; window.addEventListener(EVENT_CONFIG, event => { if (event.detail && event.detail.config) state.config = event.detail.config; }); window.dispatchEvent(new CustomEvent(EVENT_STATE, { detail: { hooked: true } })); }.toString()})(${JSON.stringify(EVENT_CONFIG)}, ${JSON.stringify(EVENT_LOG)}, ${JSON.stringify(EVENT_STATE)}, ${JSON.stringify(initialConfig)});`; } function formatResponse(text) { if (!text) return ''; try { return JSON.stringify(JSON.parse(text), null, 2); } catch (e) { return text; } } function parseHeaders(text) { try { return JSON.parse(text || '{}'); } catch (e) { return null; } } function detectBodyType(body) { return /(^|&)\w+=[^&]*/.test(body) && !/^\s*[\[{]/.test(body); } function parseRawBody(body) { const params = new URLSearchParams(); body.split('&').forEach(part => { if (!part) return; const i = part.indexOf('='); const key = i < 0 ? part : part.slice(0, i); const value = i < 0 ? '' : part.slice(i + 1); params.append(decodeURIComponent(key.replace(/\+/g, ' ')), decodeURIComponent(value.replace(/\+/g, ' '))); }); return params; } function normalizeBody(body, headers) { if (!body) return { body: '', headers }; const nextHeaders = Object.assign({}, headers); if (detectBodyType(body) && !Object.keys(nextHeaders).some(k => k.toLowerCase() === 'content-type')) nextHeaders['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; if (detectBodyType(body)) return { body: parseRawBody(body), headers: nextHeaders }; return { body, headers: nextHeaders }; } function esc(value) { return String(value == null ? '' : value).replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' } [ch])); } function setInput(container, id, value) { const el = container.querySelector('#' + id); if (el) el.value = value == null ? '' : value; } const pythonApiCatalog = [ {name:'请假记录', method:'POST', path:'/studentwork/VApply/GetVList', body:{page:1,rows:1000,askLeaveStatus:-10}}, {name:'提交销假', method:'POST', path:'/studentwork/VApply/SubmitSignin', body:{id:'',address:''}, write:true}, {name:'晚寝活动列表', method:'POST', path:'/studentwork/PunchMStudent/GetActivityList', body:{page:1,size:1000}}, {name:'返校确认', method:'POST', path:'/studentwork/vHStudent/SubmitSignin', body:{id:'',address:'',longitudeGaoDe:'',latitudeGaoDe:''}, write:true}, ]; function renderPythonApiCatalog(container, context) { const domainFor = item => item.domain === 'plat' ? 'https://plat.hxxy.edu.cn' : 'https://me.hxxy.edu.cn'; container.innerHTML = `
请选择接口`; container.querySelector('.tool-back').onclick = context.back; const select = container.querySelector('#pythonApiSelect'); pythonApiCatalog.forEach((item, index) => { const option = document.createElement('option'); option.value = index; option.textContent = `${item.domain === 'plat' ? 'plat' : 'me'} · ${item.name}${item.write ? ' · 写入' : ''}`; select.appendChild(option); }); const fill = () => { const item = pythonApiCatalog[Number(select.value)]; container.querySelector('#pythonApiUrl').value = domainFor(item) + item.path; container.querySelector('#pythonApiMethod').value = item.method; container.querySelector('#pythonApiBody').value = JSON.stringify(item.body || {}, null, 2); container.querySelector('#pythonApiNote').textContent = (item.write ? '写入接口:请确认参数和账号权限后再发送。' : '查询接口') + (item.note ? ` ${item.note}` : ''); }; select.onchange = fill; fill(); container.querySelector('#pythonApiSend').onclick = async () => { const result = container.querySelector('#pythonApiResult'); const method = container.querySelector('#pythonApiMethod').value; const item = pythonApiCatalog[Number(select.value)]; if (item.write && !confirm(`这是写入接口:${item.name}\n确认使用当前登录身份发送?`)) return; let bodyText = container.querySelector('#pythonApiBody').value.trim(); let body = bodyText; if (bodyText) { try { const parsed = JSON.parse(bodyText); body = method === 'GET' ? undefined : new URLSearchParams(Object.entries(parsed).map(([key, value]) => [key, value == null ? '' : String(value)])).toString(); } catch (e) { body = bodyText; } } let requestUrl = container.querySelector('#pythonApiUrl').value.trim(); if (method === 'GET' && bodyText) { try { const parsed = JSON.parse(bodyText); const url = new URL(requestUrl, window.location.href); Object.entries(parsed).forEach(([key, value]) => url.searchParams.set(key, value == null ? '' : String(value))); requestUrl = url.href; } catch (e) { requestUrl += (requestUrl.indexOf('?') >= 0 ? '&' : '?') + bodyText; } } result.textContent = '正在请求...'; const response = await context.request({method, url:requestUrl, headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'}, body:method === 'GET' ? undefined : body}); context.showResult(result, response); }; container.querySelector('#pythonApiSave').onclick = () => { const item = pythonApiCatalog[Number(select.value)]; config.savedApis.push({id:'api-python-' + Date.now(), name:'Python-' + item.name, method:container.querySelector('#pythonApiMethod').value, url:container.querySelector('#pythonApiUrl').value, headers:'{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"}', body:container.querySelector('#pythonApiBody').value}); saveConfig(); container.querySelector('#pythonApiResult').textContent = '已保存到 API调试。'; }; } // 工具箱注册表:新增工具时只需增加一个 { id, name, description, render } 项。 // render(container, context) 可自行创建独立界面;context 统一提供 request、showResult 与 back。 const toolboxTools = [ { id: 'python-api-catalog', name: '(学工系统)移植接口', description: '移植自动化中的已知接口,支持参数编辑、查询和保存。', render: renderPythonApiCatalog }, //一键销假功能 { id: 'leave-list', name: '(学工系统)请假销假', description: '查询当前请假记录并支持直接销假。', render(container, context) { container.innerHTML = `
${JSON.stringify(json, null, 2)}等待提交`; container.querySelector('.tool-back').onclick = context.back; container.querySelector('#submitWebvpnChangeName').onclick = async () => { const fullName = container.querySelector('#webvpnFullName').value.trim(); const nickname = container.querySelector('#webvpnNickname').value.trim(); const result = container.querySelector('#webvpnChangeResult'); if (!fullName || !nickname) { result.textContent = '请填写姓名和昵称'; return; } result.textContent = '正在提交...'; const response = await context.request({ method: 'POST', url: 'https://webvpn.hxxy.edu.cn/api/access/user/change-info', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fullName, nickname }) }); context.showResult(result, response); }; } }, ]; // 主动API请求层,绕过页面CORS限制;页面Hook仍然使用原生XHR/fetch。 function requestWithCurrentIdentity(options) { const start = performance.now(); const method = String(options.method || 'GET').toUpperCase(); const withCredentials = options.withCredentials !== false; let requestUrl; try { requestUrl = new URL(String(options.url || ''), window.location.href).href; } catch (error) { return Promise.resolve({ ok: false, status: 0, statusText: '', duration: 0, text: '', error: 'URL格式无效' }); } let data; if (options.body != null) { if (typeof options.body === 'string') data = options.body; else if (options.body instanceof URLSearchParams) data = options.body.toString(); else data = JSON.stringify(options.body); } return new Promise(resolve => { const finish = (response, error) => { const status = Number(response && response.status) || 0; resolve({ ok: status >= 200 && status < 300, status, statusText: response && response.statusText ? response.statusText : '', duration: Math.round(performance.now() - start), text: response && response.responseText != null ? String(response.responseText) : '', responseHeaders: response && response.responseHeaders ? String(response.responseHeaders) : '', ...(error ? { error } : {}) }); }; try { GM_xmlhttpRequest({ method, url: requestUrl, headers: Object.assign({}, options.headers || {}), data, responseType: 'text', withCredentials, anonymous: !withCredentials, onload: response => finish(response), onerror: response => finish(response, '网络请求失败'), ontimeout: response => finish(response, '请求超时'), onabort: response => finish(response, '请求已取消') }); } catch (error) { finish(null, String(error)); } }); } function inspectDiagnosticResponse(response) { const text = response && response.text != null ? String(response.text) : ''; const trimmed = text.trim(); const headers = response && response.responseHeaders ? String(response.responseHeaders) : ''; const contentTypeMatch = headers.match(/^content-type\s*:\s*([^\r\n]+)/im); const contentType = contentTypeMatch ? contentTypeMatch[1].trim() : ''; const htmlByHeader = /(?:text\/html|application\/xhtml\+xml)/i.test(contentType); const htmlByMarkup = /^\s*(?:\s*)?(?:= 400) || /(?:错误|异常|无权限|拒绝访问|error|exception|access denied|forbidden|not found)/i.test(text.slice(0, 5000))); let isJson = false; let jsonError = ''; if (trimmed && !isHtml) { try { JSON.parse(trimmed); isJson = true; } catch (error) { jsonError = error && error.message ? error.message : String(error); } } const statusOk = !!response && response.status >= 200 && response.status < 300; const transportError = response && response.error ? String(response.error) : ''; let reason = ''; if (transportError) reason = transportError; else if (!statusOk) reason = `HTTP状态异常:${response ? response.status : 0}`; else if (isHtml) reason = loginPage ? '返回HTML登录页,当前请求未携带有效登录状态' : (errorPage ? '返回HTML错误页' : '返回HTML页面'); else if (!isJson) reason = trimmed ? `返回内容不是有效JSON${jsonError ? `:${jsonError}` : ''}` : '响应内容为空,不是有效JSON'; return { valid: !transportError && statusOk && isJson && !isHtml, status: response ? response.status : 0, statusText: response && response.statusText ? response.statusText : '', duration: response && response.duration != null ? response.duration : 0, contentType, isJson, isHtml, loginPage, errorPage, reason, preview: text.slice(0, 500) }; } async function requestWithFetchCurrentIdentity(options) { const start = performance.now(); const timeoutMs = 20000; let requestUrl; try { requestUrl = new URL(String(options.url || ''), window.location.href).href; } catch (error) { return { ok: false, status: 0, statusText: '', duration: 0, text: '', responseHeaders: '', error: 'URL格式无效' }; } const method = String(options.method || 'GET').toUpperCase(); let body; if (options.body != null) { if (typeof options.body === 'string') body = options.body; else if (options.body instanceof URLSearchParams) body = options.body.toString(); else body = JSON.stringify(options.body); } try { const controller = typeof AbortController !== 'undefined' ? new AbortController() : null; const timeoutId = controller ? setTimeout(() => controller.abort(), timeoutMs) : null; let response; try { response = await fetch(requestUrl, { method, headers: Object.assign({}, options.headers || {}), body: method === 'GET' || method === 'HEAD' ? undefined : body, credentials: 'include', cache: 'no-store', ...(controller ? { signal: controller.signal } : {}) }); } finally { if (timeoutId) clearTimeout(timeoutId); } const text = await response.text(); const responseHeaders = Array.from(response.headers.entries()).map(([key, value]) => `${key}: ${value}`).join('\n'); return { ok: response.ok, status: response.status, statusText: response.statusText || '', duration: Math.round(performance.now() - start), text, responseHeaders }; } catch (error) { return { ok: false, status: 0, statusText: '', duration: Math.round(performance.now() - start), text: '', responseHeaders: '', error: error && error.name === 'AbortError' ? `fetch请求超时(${timeoutMs}ms)` : (error && error.message ? error.message : String(error)) }; } } async function runIOSRequestDiagnostic(options) { const attempts = []; const gmTimeoutMs = 20000; const gmResponse = await Promise.race([ requestWithCurrentIdentity(Object.assign({}, options, { withCredentials: true })), new Promise(resolve => setTimeout(() => resolve({ ok: false, status: 0, statusText: '', duration: gmTimeoutMs, text: '', responseHeaders: '', error: `GM_xmlhttpRequest未在${gmTimeoutMs}ms内返回` }), gmTimeoutMs)) ]); const gmInspection = inspectDiagnosticResponse(gmResponse); attempts.push({ transport: 'GM_xmlhttpRequest (withCredentials)', response: gmResponse, inspection: gmInspection }); if (gmInspection.valid) return { success: true, fallbackUsed: false, attempts }; const fetchResponse = await requestWithFetchCurrentIdentity(options); const fetchInspection = inspectDiagnosticResponse(fetchResponse); attempts.push({ transport: 'fetch (credentials: include)', response: fetchResponse, inspection: fetchInspection }); return { success: fetchInspection.valid, fallbackUsed: true, attempts }; } function formatIOSDiagnosticReport(result, options) { const environment = [ `脚本版本:${VERSION}`, `当前地址:${window.location.href}`, `User-Agent:${RUNTIME_ENVIRONMENT.userAgent}`, `Platform:${RUNTIME_ENVIRONMENT.platform || '(空)'}`, `Vendor:${RUNTIME_ENVIRONMENT.vendor || '(空)'}`, `Language:${RUNTIME_ENVIRONMENT.language || '(空)'}`, `MaxTouchPoints:${RUNTIME_ENVIRONMENT.maxTouchPoints}`, `是否iOS:${RUNTIME_ENVIRONMENT.isIOS ? '是' : '否'}`, `测试请求:${options.method} ${new URL(options.url, window.location.href).href}` ]; const attempts = result.attempts.map((attempt, index) => { const item = attempt.inspection; return [ `\n方案 ${index === 0 ? 'A' : 'B'}:${attempt.transport}`, `HTTP状态:${item.status} ${item.statusText || ''}`.trimEnd(), `耗时:${item.duration}ms`, `Content-Type:${item.contentType || '(未提供)'}`, `是否JSON:${item.isJson ? '是' : '否'}`, `是否HTML:${item.isHtml ? '是' : '否'}`, `HTML登录页:${item.loginPage ? '是' : '否'}`, `HTML错误页:${item.errorPage ? '是' : '否'}`, `校验结果:${item.valid ? '通过' : `失败 - ${item.reason || '未知原因'}`}`, '返回内容前500字符:', item.preview || '(空响应)' ].join('\n'); }); const outcome = result.success ? `\n最终结果:成功,使用 ${result.attempts[result.attempts.length - 1].transport}` : `\n最终结果:失败,GM_xmlhttpRequest 与 fetch 均未返回有效JSON;请保留本报告用于定位 Cookie、CORS 或登录状态问题。`; return environment.concat(attempts, outcome).join('\n'); } function renderInternalPage(container, title, url, back, fallback) { container.innerHTML = ''; const head = document.createElement('div'); head.className = 'tool-head'; const backButton = document.createElement('button'); backButton.className = 'secondary'; backButton.textContent = '返回'; backButton.onclick = back; const heading = document.createElement('h4'); heading.textContent = title; head.appendChild(backButton); head.appendChild(heading); if (typeof fallback === 'function') { const fallbackButton = document.createElement('button'); fallbackButton.className = 'secondary'; fallbackButton.textContent = '改用二维码接口'; fallbackButton.onclick = fallback; head.appendChild(fallbackButton); } const frameViewport = document.createElement('div'); frameViewport.className = 'internal-page-viewport'; frameViewport.style.cssText = ` position:relative; width:100%; height:min(62vh,560px); min-height:260px; overflow:hidden; border:1px solid #cbd5e1; border-radius:6px; background:#fff; contain:strict; margin:0 auto; `; const frame = document.createElement('iframe'); frame.className = 'internal-page-frame'; frame.src = new URL(url, window.location.href).href; frame.title = title; frame.setAttribute('scrolling', 'yes'); frame.style.cssText = ` display:block; position:absolute; top:0; left:50%; width:160%; height:160%; max-width:none; border:0; background:#fff; transform:translateX(-50%) scale(.625); transform-origin:top center; `; if (typeof fallback === 'function') frame.addEventListener('error', fallback, { once: true }); frameViewport.appendChild(frame); container.appendChild(head); container.appendChild(frameViewport); } function renderToolbox(container) { container.innerHTML = '
等待请求`; setInput(container, 'apiName', item.name); setInput(container, 'apiMethod', item.method); setInput(container, 'apiUrl', item.url); setInput(container, 'apiHeaders', item.headers || '{}'); setInput(container, 'apiBody', item.body || ''); container.querySelector('#saveApi').onclick = () => { const form = getApiForm(container); if (!form.name) { alertInPanel(container, '请填写API名称'); return; } const saved = Object.assign({ id: 'api-' + Date.now() }, form); const index = config.savedApis.findIndex(x => x.name === form.name); if (index >= 0) config.savedApis[index] = Object.assign(config.savedApis[index], saved); else config.savedApis.push(saved); saveConfig(); renderSavedApis(container); alertInPanel(container, 'API已保存'); }; container.querySelector('#loadApi').onclick = () => renderSavedApis(container); container.querySelector('#sendApi').onclick = async () => { const form = getApiForm(container); const result = container.querySelector('#apiResult'); if (!form.url) { result.textContent = '请输入URL'; return; } const headers = parseHeaders(form.headers); if (!headers) { result.textContent = 'Headers不是有效JSON'; return; } const normalized = normalizeBody(form.body, headers); const method = String(form.method || 'GET').toUpperCase(); let actualUrl = form.url; const options = { method, url: form.url, headers: normalized.headers, withCredentials: true }; if (form.body && method === 'GET') { try { actualUrl = appendQueryToUrl(form.url, normalized.body); options.url = actualUrl; } catch (e) { result.textContent = `GET 参数转换失败:${e.message}`; return; } } else if (form.body) { options.body = normalized.body; } result.textContent = `正在请求...\n${method} ${actualUrl}`; const response = await requestWithCurrentIdentity(options); if (response.error) { result.textContent = `请求失败\n${response.error}`; return; } result.textContent = `实际请求:${method} ${actualUrl}\nStatus: ${response.status} ${response.statusText || ''}\nTime: ${response.duration}ms\n\n${formatResponse(response.text)}`; }; renderSavedApis(container); } function alertInPanel(container, text) { const result = container.querySelector('#apiResult'); if (result) { result.textContent = text; return; } let notice = container.querySelector('.panel-notice'); if (!notice) { notice = document.createElement('div'); notice.className = 'panel-notice'; notice.style.cssText = 'margin:6px 0;padding:7px 8px;border:1px solid #93c5fd;border-radius:6px;background:#eff6ff;white-space:pre-wrap;word-break:break-word;font-size:12px;'; container.insertBefore(notice, container.firstChild); } notice.textContent = text; } async function copyCurrentCookies(container) { const cookies = (document.cookie || '').split(';').map(item => item.trim()).filter(Boolean).join(';') + (document.cookie ? ';' : ''); if (!cookies) { alertInPanel(container, '当前页面没有可读取的 Cookies(HttpOnly Cookie 无法被脚本读取)'); return; } try { await navigator.clipboard.writeText(cookies); alertInPanel(container, `Cookies 已复制(${cookies.split(';').filter(Boolean).length} 项)`); } catch (e) { const input = document.createElement('textarea'); input.value = cookies; input.style.position = 'fixed'; input.style.opacity = '0'; document.body.appendChild(input); input.focus(); input.select(); let copied = false; try { copied = document.execCommand('copy'); } catch (error) {} input.remove(); alertInPanel(container, copied ? 'Cookies 已复制' : `Cookies:\n${cookies}`); } } function renderSavedApis(container) { const list = container.querySelector('#savedApiList'); if (!list) return; list.innerHTML = config.savedApis.length ? '
原始响应:\n${esc(formatResponse(item.originalResponse))}\n\n修改后响应:\n${esc(formatResponse(item.modifiedResponse))}`;
detail.querySelector('.replay-method').value = item.method || 'GET';
detail.querySelector('.replay-url').value = splitRequest.url;
detail.querySelector('.replay-body').value = initialBody;
detail.querySelector('.replay-request').onclick = async () => {
const result = detail.querySelector('.replay-result');
const headers = parseHeaders(detail.querySelector('.replay-headers').value);
if (!headers) {
result.textContent = 'Headers 不是有效 JSON';
return;
}
const method = detail.querySelector('.replay-method').value;
const rawBody = detail.querySelector('.replay-body').value.trim();
let body = rawBody;
if (rawBody) {
try {
const data = JSON.parse(rawBody);
if (data && typeof data === 'object' && !Array.isArray(data)) {
body = new URLSearchParams(Object.entries(data).flatMap(([key, value]) => {
const values = Array.isArray(value) ? value : [value];
return values.map(item => [key, item == null ? '' : String(item)]);
})).toString();
}
} catch (e) {}
}
const requestUrl = detail.querySelector('.replay-url').value.trim();
let replayUrl = requestUrl;
let replayBody = body;
if (method === 'GET' && body) {
try {
const url = new URL(requestUrl, window.location.href);
const params = new URLSearchParams(body);
params.forEach((value, key) => url.searchParams.append(key, value));
replayUrl = url.href;
replayBody = undefined;
} catch (e) {
result.textContent = `GET 参数转换失败:${e.message}`;
return;
}
}
result.textContent = `正在重放请求...\n${method} ${replayUrl}`;
const response = await requestWithCurrentIdentity({
method,
url: replayUrl,
headers: method === 'GET' ? headers : addFormContentType(headers, body),
body: replayBody,
withCredentials: true
});
result.textContent = response.error ? `请求失败\n${response.error}` : `实际请求:${method} ${replayUrl}\nStatus: ${response.status} ${response.statusText || ''}\nTime: ${response.duration}ms\n\n${formatResponse(response.text)}`;
};
detail.querySelector('.save-detail-api').onclick = () => {
const method = detail.querySelector('.replay-method').value;
const url = detail.querySelector('.replay-url').value.trim();
config.savedApis.push({
id: 'api-' + Date.now(),
name: `${method} ${url.split('?')[0]}`,
method,
url,
headers: detail.querySelector('.replay-headers').value,
body: detail.querySelector('.replay-body').value
});
saveConfig();
detail.querySelector('.replay-result').textContent = '已保存到 API调试。';
};
row.appendChild(detail);
}
function renderLogs(container) {
const expandedId = container.dataset.expandedLogId || '';
container.dataset.view = 'logs';
container.innerHTML = '环境:${RUNTIME_ENVIRONMENT.isIOS ? 'iOS' : '非 iOS'}\n等待测试