// ==UserScript== // @name 网络请求记录器 // @namespace request-logger // @version 1.0 // @description 拦截并记录网页中的 fetch / XHR 请求,支持导出为 JSON 文件 // @author You // @match *://*/* // @grant GM_registerMenuCommand // @run-at document-start // ==/UserScript== (function() { 'use strict'; // 存储所有请求记录 const requests = []; // 将拦截逻辑注入页面真实上下文(才能抓到页面自身的请求) function injectInterceptor() { const script = document.createElement('script'); script.textContent = '(' + function() { // 拦截 fetch const origFetch = window.fetch; window.fetch = async function(input, init) { const url = input instanceof Request ? input.url : input; const options = input instanceof Request ? input : (init || {}); const record = { type: 'fetch', url: String(url), method: (options.method || 'GET').toUpperCase(), headers: {}, body: null, timestamp: new Date().toISOString() }; if (options.headers) { if (options.headers instanceof Headers) { options.headers.forEach((v, k) => record.headers[k] = v); } else { record.headers = { ...options.headers }; } } if (options.body && typeof options.body === 'string') { record.body = options.body; } else if (options.body) { record.body = '[Binary/Object Body]'; } window.postMessage({ source: 'REQUEST_LOGGER', data: record }, '*'); return origFetch.apply(this, arguments); }; // 拦截 XMLHttpRequest const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; const origSetHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.open = function(method, url) { this._reqLog = { type: 'xhr', method: method.toUpperCase(), url: String(url), headers: {}, body: null, timestamp: new Date().toISOString() }; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function(header, value) { if (this._reqLog) this._reqLog.headers[header] = value; return origSetHeader.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(body) { if (this._reqLog) { if (body && typeof body === 'string') this._reqLog.body = body; else if (body) this._reqLog.body = '[Binary/Object Body]'; window.postMessage({ source: 'REQUEST_LOGGER', data: this._reqLog }, '*'); } return origSend.apply(this, arguments); }; }.toString() + ')();'; document.documentElement.appendChild(script); script.remove(); } // 接收页面传来的请求数据 window.addEventListener('message', (e) => { if (e.data && e.data.source === 'REQUEST_LOGGER') { requests.push(e.data.data); } }); // 导出为 JSON 文件 function exportJson() { if (requests.length === 0) { alert('暂无记录的请求,请先操作页面触发网络请求'); return; } const payload = { exportTime: new Date().toISOString(), pageUrl: location.href, userAgent: navigator.userAgent, total: requests.length, requests: requests }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `requests_${location.hostname}_${Date.now()}.json`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); console.log(`[请求记录器] 已导出 ${requests.length} 条请求`); } // 查看统计 function showStats() { const typeCount = {}; requests.forEach(r => { typeCount[r.type] = (typeCount[r.type] || 0) + 1; }); alert(`📊 当前已记录 ${requests.length} 条请求\n` + Object.entries(typeCount).map(([k, v]) => ` · ${k.toUpperCase()}: ${v} 条`).join('\n')); } // 清空记录 function clearLogs() { requests.length = 0; console.log('[请求记录器] 记录已清空'); } // 注册 ScriptCat / 油猴 右键菜单命令 if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand('📥 导出请求记录 (JSON)', exportJson); GM_registerMenuCommand('📊 查看记录统计', showStats); GM_registerMenuCommand('🗑️ 清空记录', clearLogs); } // 同时暴露到控制台,方便手动调用 window.exportRequests = exportJson; window.getRequestLogs = () => requests; // 页面开始时注入拦截器 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', injectInterceptor); } else { injectInterceptor(); } console.log('[请求记录器] 已启动,菜单或控制台(window.exportRequests)可导出JSON'); })();