您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
监听Ajax请求,按F3启动/关闭
// ==UserScript== // @name Ajax请求监听器 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 监听Ajax请求,按F3启动/关闭 // @author xiaoma // @match *://*/* // @grant none // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; // 配置参数 const CONFIG = { hotkey: 'F3', // 激活热键 maxRecords: 200, // 最大记录数 autoScroll: true // 自动滚动到最新记录 }; // 状态管理 let isListening = false; let ajaxRecords = []; let recordIndex = 0; let monitorPanel = null; let currentFilters = { searchText: '', method: 'all', status: 'all', searchReverse: false, methodReverse: false, statusReverse: false }; // 原始Ajax方法备份 const originalXHR = window.XMLHttpRequest; const originalFetch = window.fetch; // 创建监控面板 function createMonitorPanel() { if (monitorPanel) return; // 创建面板容器 monitorPanel = document.createElement('div'); monitorPanel.id = 'ajax-monitor-panel'; monitorPanel.innerHTML = ` <div class="ajax-monitor-header"> <h3>🔍 Ajax监听器</h3> <div class="ajax-monitor-controls"> <button id="clear-records" title="清空所有记录">🗑️ 清空</button> <button id="export-records" title="导出为JSON文件">📥 导出</button> <button id="toggle-auto-scroll" title="开启/关闭自动滚动">📜 滚动</button> <button id="close-monitor" title="关闭监听器">❌ 关闭</button> </div> </div> <div class="ajax-monitor-stats"> <span>📊 总请求: <span id="total-count">0</span></span> <span>✅ 成功: <span id="success-count">0</span></span> <span>❌ 失败: <span id="error-count">0</span></span> <span>⏱️ 平均耗时: <span id="avg-time">0</span>ms</span> </div> <div class="ajax-monitor-filters"> <div class="filter-group"> <label>🔍 搜索:</label> <input type="text" id="filter-search" placeholder="搜索URL、状态码..." /> <button id="search-reverse" class="reverse-btn" title="启用反向搜索,排除匹配的记录">🚫 反向</button> </div> <div class="filter-group"> <label>📝 方法:</label> <select id="filter-method"> <option value="all">全部</option> <option value="GET">GET</option> <option value="POST">POST</option> <option value="PUT">PUT</option> <option value="DELETE">DELETE</option> <option value="PATCH">PATCH</option> </select> <button id="method-reverse" class="reverse-btn" title="启用反向方法过滤,排除选中的方法">🚫 反向</button> </div> <div class="filter-group"> <label>📊 状态:</label> <select id="filter-status"> <option value="all">全部</option> <option value="success">成功 (2xx)</option> <option value="error">失败 (4xx/5xx)</option> <option value="pending">进行中</option> </select> <button id="status-reverse" class="reverse-btn" title="启用反向状态过滤,排除选中的状态">🚫 反向</button> </div> <div class="filter-group"> <button id="clear-filters" title="清空所有过滤条件">🗑️ 清空过滤</button> </div> </div> <div class="ajax-monitor-content"> <div class="ajax-records-list" id="ajax-records-list"> <div class="ajax-record-header"> <div class="col-time">时间</div> <div class="col-method">方法</div> <div class="col-url">URL</div> <div class="col-status">状态</div> <div class="col-duration">耗时</div> <div class="col-actions">操作</div> </div> </div> </div> <div class="ajax-monitor-footer"> <span>💡 提示: 按 F3 关闭监听 | 最多显示 ${CONFIG.maxRecords} 条记录</span> </div> `; // 添加样式 const style = document.createElement('style'); style.textContent = ` #ajax-monitor-panel { position: fixed; top: 20px; right: 20px; width: 900px; max-height: 80vh; background: #fff; border: 2px solid #007acc; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,122,204,0.2); z-index: 2147483647; font-family: 'Segoe UI', 'Microsoft YaHei', Arial, sans-serif; font-size: 13px; backdrop-filter: blur(10px); resize: both; overflow: hidden; min-width: 600px; min-height: 400px; } /* 缩放指示器 */ #ajax-monitor-panel::after { content: ''; position: absolute; bottom: 2px; right: 2px; width: 16px; height: 16px; background: linear-gradient(-45deg, transparent 40%, #007acc 40%, #007acc 60%, transparent 60%); cursor: nw-resize; opacity: 0.6; border-radius: 0 0 10px 0; } #ajax-monitor-panel:hover::after { opacity: 1; } .ajax-monitor-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: linear-gradient(135deg, #007acc, #0099ff); color: white; border-radius: 10px 10px 0 0; cursor: move; } .ajax-monitor-header h3 { margin: 0; font-size: 16px; font-weight: 600; } .ajax-monitor-controls button { margin-left: 8px; padding: 6px 12px; border: none; background: rgba(255,255,255,0.2); color: white; border-radius: 6px; cursor: pointer; font-size: 12px; transition: all 0.2s; backdrop-filter: blur(5px); } .ajax-monitor-controls button:hover { background: rgba(255,255,255,0.3); transform: translateY(-1px); } .ajax-monitor-stats { padding: 10px 16px; background: #f8f9fa; border-bottom: 1px solid #e9ecef; font-size: 12px; display: flex; gap: 20px; } .ajax-monitor-stats span { color: #495057; font-weight: 500; } .ajax-monitor-filters { padding: 12px 16px; background: #ffffff; border-bottom: 1px solid #e9ecef; display: flex; gap: 16px; align-items: center; flex-wrap: wrap; } .filter-group { display: flex; align-items: center; gap: 6px; } .filter-group label { font-size: 12px; color: #495057; font-weight: 500; white-space: nowrap; } .filter-group input, .filter-group select { padding: 6px 10px; border: 1px solid #ced4da; border-radius: 4px; font-size: 12px; background: white; min-width: 120px; transition: all 0.2s; } .filter-group input:focus, .filter-group select:focus { outline: none; border-color: #007acc; box-shadow: 0 0 0 2px rgba(0,122,204,0.1); } #clear-filters { padding: 6px 12px; border: 1px solid #6c757d; background: #f8f9fa; color: #495057; border-radius: 4px; cursor: pointer; font-size: 11px; transition: all 0.2s; } #clear-filters:hover { background: #e9ecef; border-color: #495057; } .reverse-btn { padding: 4px 8px; border: 1px solid #6c757d; background: #f8f9fa; color: #495057; border-radius: 4px; cursor: pointer; font-size: 10px; margin-left: 4px; transition: all 0.2s; white-space: nowrap; } .reverse-btn:hover { background: #e9ecef; border-color: #495057; } .reverse-btn.active { background: #fd7e14; color: white; border-color: #fd7e14; box-shadow: 0 0 0 2px rgba(253,126,20,0.2); } .reverse-btn.active:hover { background: #e8690b; border-color: #e8690b; } .ajax-monitor-content { flex: 1; overflow: hidden; display: flex; flex-direction: column; max-height: 400px; } .ajax-records-list { flex: 1; overflow-y: auto; overflow-x:hidden; max-height: calc(80vh - 160px); } .ajax-record-header { display: grid; grid-template-columns: 80px 60px 1fr 80px 80px 160px; gap: 8px; padding: 10px 16px; background: #e9ecef; border-bottom: 2px solid #dee2e6; font-weight: 600; font-size: 12px; color: #495057; position: sticky; top: 0; z-index: 10; } .ajax-record { display: grid; grid-template-columns: 80px 60px 1fr 80px 80px 160px; gap: 8px; padding: 10px 16px; border-bottom: 1px solid #e9ecef; cursor: pointer; font-size: 12px; transition: all 0.2s; position: relative; } .ajax-record:hover { background: #f8f9fa; transform: translateX(2px); } .ajax-record.success { border-left: 4px solid #28a745; } .ajax-record.error { border-left: 4px solid #dc3545; } .ajax-record.pending { border-left: 4px solid #ffc107; } .ajax-record-url { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: 'Courier New', monospace; min-width: 0; /* 确保在flex/grid布局中正确处理文本溢出 */ } .ajax-monitor-footer { padding: 8px 16px; background: #f8f9fa; border-top: 1px solid #e9ecef; font-size: 11px; color: #6c757d; text-align: center; } .ajax-detail-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 2147483648; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(3px); } .ajax-detail-content { background: #fff; width: 95%; max-width: 1200px; max-height: 90%; border-radius: 12px; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } .ajax-detail-header { padding: 16px 20px; background: linear-gradient(135deg, #007acc, #0099ff); color: white; display: flex; justify-content: space-between; align-items: center; } .ajax-detail-header h3 { margin: 0; font-size: 16px; font-weight: 600; } .ajax-detail-body { padding: 20px; overflow-y: auto; flex: 1; background: #f8f9fa; } .ajax-detail-section { margin-bottom: 24px; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .ajax-detail-section h4 { margin: 0; padding: 12px 16px; color: #495057; font-size: 14px; font-weight: 600; background: #e9ecef; border-bottom: 1px solid #dee2e6; } .ajax-detail-code { padding: 16px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 13px; line-height: 1.5; max-height: 300px; overflow-y: auto; background: #f8f9fa; } .close-btn { background: rgba(255,255,255,0.2); color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s; } .close-btn:hover { background: rgba(255,255,255,0.3); } .view-btn { background: #007acc; color: white; border: none; padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 11px; transition: all 0.2s; } .view-btn:hover { background: #0099ff; transform: scale(1.05); } .copy-btn { background: #17a2b8; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 10px; margin-left: 4px; } .resend-btn { background: #28a745; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 10px; margin-left: 4px; } .resend-btn:hover { background: #218838; transform: scale(1.05); } .curl-btn { background: #6f42c1; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 10px; margin-left: 4px; } .curl-btn:hover { background: #5a32a3; transform: scale(1.05); } .status-success { color: #28a745; font-weight: 600; } .status-error { color: #dc3545; font-weight: 600; } .status-pending { color: #ffc107; font-weight: 600; } .duration-fast { color: #28a745; } .duration-medium { color: #ffc107; } .duration-slow { color: #dc3545; } /* 滚动条美化 */ .ajax-records-list::-webkit-scrollbar, .ajax-detail-code::-webkit-scrollbar { width: 8px; } .ajax-records-list::-webkit-scrollbar-track, .ajax-detail-code::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } .ajax-records-list::-webkit-scrollbar-thumb, .ajax-detail-code::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; } .ajax-records-list::-webkit-scrollbar-thumb:hover, .ajax-detail-code::-webkit-scrollbar-thumb:hover { background: #a8a8a8; } /* 编辑请求模态框样式 */ .ajax-edit-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 2147483649; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(3px); } .ajax-edit-content { background: #fff; width: 95%; max-width: 800px; max-height: 90%; border-radius: 12px; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } .ajax-edit-body { padding: 20px; overflow-y: auto; flex: 1; background: #f8f9fa; } .ajax-edit-form { display: flex; flex-direction: column; gap: 16px; } .ajax-edit-group { display: flex; flex-direction: column; gap: 8px; } .ajax-edit-group label { font-weight: 600; color: #495057; font-size: 14px; } .ajax-edit-group input, .ajax-edit-group select, .ajax-edit-group textarea { padding: 10px 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 13px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; } .ajax-edit-group textarea { min-height: 120px; resize: vertical; } .ajax-edit-actions { display: flex; gap: 12px; justify-content: flex-end; padding: 16px 20px; background: #f8f9fa; border-top: 1px solid #dee2e6; } .ajax-edit-actions button { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.2s; } .send-btn { background: #007acc; color: white; } .send-btn:hover { background: #0099ff; } .cancel-btn { background: #6c757d; color: white; } .cancel-btn:hover { background: #5a6268; } `; document.head.appendChild(style); document.body.appendChild(monitorPanel); // 使面板可拖拽 makeDraggable(); // 绑定事件 bindPanelEvents(); } // 使面板可拖拽 function makeDraggable() { const header = monitorPanel.querySelector('.ajax-monitor-header'); let isDragging = false; let currentX; let currentY; let initialX; let initialY; let xOffset = 0; let yOffset = 0; header.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); function dragStart(e) { initialX = e.clientX - xOffset; initialY = e.clientY - yOffset; if (e.target === header || header.contains(e.target)) { isDragging = true; } } function drag(e) { if (isDragging) { e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; xOffset = currentX; yOffset = currentY; monitorPanel.style.transform = `translate(${currentX}px, ${currentY}px)`; } } function dragEnd() { isDragging = false; } } // 绑定面板事件 function bindPanelEvents() { // 清空记录 document.getElementById('clear-records').onclick = () => { ajaxRecords = []; recordIndex = 0; // 重置过滤器 currentFilters = { searchText: '', method: 'all', status: 'all', searchReverse: false, methodReverse: false, statusReverse: false }; // 重置过滤器UI if (document.getElementById('filter-search')) { document.getElementById('filter-search').value = ''; document.getElementById('filter-method').value = 'all'; document.getElementById('filter-status').value = 'all'; // 重置反向按钮状态 const searchReverseBtn = document.getElementById('search-reverse'); const methodReverseBtn = document.getElementById('method-reverse'); const statusReverseBtn = document.getElementById('status-reverse'); if (searchReverseBtn) searchReverseBtn.classList.remove('active'); if (methodReverseBtn) methodReverseBtn.classList.remove('active'); if (statusReverseBtn) statusReverseBtn.classList.remove('active'); } updateRecordsList(); updateStats(); }; // 导出数据 document.getElementById('export-records').onclick = exportRecords; // 切换自动滚动 document.getElementById('toggle-auto-scroll').onclick = () => { CONFIG.autoScroll = !CONFIG.autoScroll; const btn = document.getElementById('toggle-auto-scroll'); btn.style.background = CONFIG.autoScroll ? 'rgba(40,167,69,0.3)' : 'rgba(255,255,255,0.2)'; }; // 关闭监控 document.getElementById('close-monitor').onclick = stopMonitoring; // 绑定过滤器事件 bindFilterEvents(); } // 过滤记录 function filterRecords() { return ajaxRecords.filter(record => { // 搜索文本过滤 if (currentFilters.searchText) { const searchText = currentFilters.searchText.toLowerCase(); const matchUrl = record.url.toLowerCase().includes(searchText); const matchStatus = record.status.toString().includes(searchText); const matchMethod = record.method.toLowerCase().includes(searchText); const hasMatch = matchUrl || matchStatus || matchMethod; if (currentFilters.searchReverse) { // 反向搜索:排除匹配的 if (hasMatch) { return false; } } else { // 正向搜索:包含匹配的 if (!hasMatch) { return false; } } } // 方法过滤 if (currentFilters.method !== 'all') { const methodMatches = record.method === currentFilters.method; if (currentFilters.methodReverse) { // 反向方法过滤:排除选中的方法 if (methodMatches) { return false; } } else { // 正向方法过滤:只显示选中的方法 if (!methodMatches) { return false; } } } // 状态过滤 if (currentFilters.status !== 'all') { let statusMatches = false; switch (currentFilters.status) { case 'success': statusMatches = record.success && record.status >= 200 && record.status < 300; break; case 'error': statusMatches = !record.success && record.status >= 400; break; case 'pending': statusMatches = record.status === 0; break; } if (currentFilters.statusReverse) { // 反向状态过滤:排除选中的状态 if (statusMatches) { return false; } } else { // 正向状态过滤:只显示选中的状态 if (!statusMatches) { return false; } } } return true; }); } // 更新记录列表 function updateRecordsList() { const listContainer = document.getElementById('ajax-records-list'); const filteredRecords = filterRecords(); const recordsHtml = filteredRecords.map(record => { const statusClass = record.success ? 'success' : 'error'; const durationClass = record.duration < 200 ? 'duration-fast' : record.duration < 1000 ? 'duration-medium' : 'duration-slow'; return ` <div class="ajax-record ${statusClass}" onclick="showRecordDetail(${record.id})"> <div>${record.time}</div> <div><span class="method-${record.method.toLowerCase()}">${record.method}</span></div> <div class="ajax-record-url" title="${record.url}">${record.url}</div> <div class="${record.success ? 'status-success' : 'status-error'}">${record.status}</div> <div class="${durationClass}">${record.duration}ms</div> <div> <button class="view-btn" onclick="event.stopPropagation(); showRecordDetail(${record.id})">详情</button> <button class="copy-btn" onclick="event.stopPropagation(); copyUrl('${record.url}')" title="复制URL">📋</button> <button class="resend-btn" onclick="event.stopPropagation(); editAndResendRequest(${record.id})" title="修改并重发">🔄</button> <button class="curl-btn" onclick="event.stopPropagation(); copyCurlCommand(${record.id})" title="复制curl">📜</button> </div> </div> `; }).join(''); const noResultsHtml = filteredRecords.length === 0 && ajaxRecords.length > 0 ? `<div style="padding: 20px; text-align: center; color: #6c757d;"> <div>🔍 没有找到符合条件的请求</div> <div style="font-size: 11px; margin-top: 4px;">尝试调整过滤条件或清空过滤器</div> </div>` : ''; listContainer.innerHTML = ` <div class="ajax-record-header"> <div class="col-time">时间</div> <div class="col-method">方法</div> <div class="col-url">URL</div> <div class="col-status">状态</div> <div class="col-duration">耗时</div> <div class="col-actions">操作</div> </div> ${recordsHtml} ${noResultsHtml} `; if (CONFIG.autoScroll && filteredRecords.length > 0) { listContainer.scrollTop = listContainer.scrollHeight; } // 更新过滤器状态显示 updateFilterStatus(filteredRecords.length); } // 绑定过滤器事件 function bindFilterEvents() { // 搜索框 const searchInput = document.getElementById('filter-search'); searchInput.addEventListener('input', (e) => { currentFilters.searchText = e.target.value; updateRecordsList(); }); // 方法选择器 const methodSelect = document.getElementById('filter-method'); methodSelect.addEventListener('change', (e) => { currentFilters.method = e.target.value; updateRecordsList(); }); // 状态选择器 const statusSelect = document.getElementById('filter-status'); statusSelect.addEventListener('change', (e) => { currentFilters.status = e.target.value; updateRecordsList(); }); // 反向搜索按钮 const searchReverseBtn = document.getElementById('search-reverse'); searchReverseBtn.addEventListener('click', () => { currentFilters.searchReverse = !currentFilters.searchReverse; searchReverseBtn.classList.toggle('active', currentFilters.searchReverse); updateRecordsList(); }); // 反向方法按钮 const methodReverseBtn = document.getElementById('method-reverse'); methodReverseBtn.addEventListener('click', () => { currentFilters.methodReverse = !currentFilters.methodReverse; methodReverseBtn.classList.toggle('active', currentFilters.methodReverse); updateRecordsList(); }); // 反向状态按钮 const statusReverseBtn = document.getElementById('status-reverse'); statusReverseBtn.addEventListener('click', () => { currentFilters.statusReverse = !currentFilters.statusReverse; statusReverseBtn.classList.toggle('active', currentFilters.statusReverse); updateRecordsList(); }); // 清空过滤器 document.getElementById('clear-filters').addEventListener('click', () => { currentFilters = { searchText: '', method: 'all', status: 'all', searchReverse: false, methodReverse: false, statusReverse: false }; // 重置UI searchInput.value = ''; methodSelect.value = 'all'; statusSelect.value = 'all'; // 重置反向按钮状态 searchReverseBtn.classList.remove('active'); methodReverseBtn.classList.remove('active'); statusReverseBtn.classList.remove('active'); updateRecordsList(); }); } // 更新过滤器状态显示 function updateFilterStatus(filteredCount) { const totalCount = ajaxRecords.length; const isFiltered = currentFilters.searchText || currentFilters.method !== 'all' || currentFilters.status !== 'all'; const hasReverse = currentFilters.searchReverse || currentFilters.methodReverse || currentFilters.statusReverse; if (isFiltered && filteredCount !== totalCount) { let filterText = '(已过滤'; if (hasReverse) { filterText += ' 🚫反向'; } filterText += ')'; document.getElementById('total-count').innerHTML = `${filteredCount} / ${totalCount} <span style="color: #007acc; font-size: 10px;">${filterText}</span>`; } else { document.getElementById('total-count').textContent = totalCount; } } // 复制URL到剪贴板 window.copyUrl = function(url) { navigator.clipboard.writeText(url).then(() => { console.log('📋 URL已复制到剪贴板:', url); }).catch(err => { console.error('复制失败:', err); }); }; // 复制curl命令 window.copyCurlCommand = function(recordId) { const record = ajaxRecords.find(r => r.id === recordId); if (!record) return; const curlCommand = generateCurlCommand(record); navigator.clipboard.writeText(curlCommand).then(() => { console.log('📜 cURL命令已复制到剪贴板'); // 显示成功提示 showToast('cURL命令已复制到剪贴板', 'success'); }).catch(err => { console.error('复制失败:', err); showToast('复制失败', 'error'); }); }; // 编辑并重发请求 window.editAndResendRequest = function(recordId) { const record = ajaxRecords.find(r => r.id === recordId); if (!record) return; const bodyFormat = record.bodyFormat || 'text'; const modal = document.createElement('div'); modal.className = 'ajax-edit-modal'; modal.innerHTML = ` <div class="ajax-edit-content"> <div class="ajax-detail-header"> <h3>🔄 修改并重发请求</h3> <button class="close-btn" onclick="this.closest('.ajax-edit-modal').remove()">关闭</button> </div> <div class="ajax-edit-body"> <form class="ajax-edit-form" id="edit-form-${recordId}"> <div class="ajax-edit-group"> <label for="edit-method-${recordId}">请求方法</label> <select id="edit-method-${recordId}" name="method"> <option value="GET" ${record.method === 'GET' ? 'selected' : ''}>GET</option> <option value="POST" ${record.method === 'POST' ? 'selected' : ''}>POST</option> <option value="PUT" ${record.method === 'PUT' ? 'selected' : ''}>PUT</option> <option value="DELETE" ${record.method === 'DELETE' ? 'selected' : ''}>DELETE</option> <option value="PATCH" ${record.method === 'PATCH' ? 'selected' : ''}>PATCH</option> </select> </div> <div class="ajax-edit-group"> <label for="edit-url-${recordId}">请求URL</label> <input type="text" id="edit-url-${recordId}" name="url" value="${record.url}" /> </div> <div class="ajax-edit-group"> <label for="edit-headers-${recordId}">请求头 (JSON格式)</label> <textarea id="edit-headers-${recordId}" name="headers" placeholder='{"Content-Type": "application/json"}'>${JSON.stringify(record.requestHeaders, null, 2)}</textarea> </div> <div class="ajax-edit-group"> <label for="edit-body-format-${recordId}">请求体格式</label> <select id="edit-body-format-${recordId}" name="bodyFormat" onchange="handleBodyFormatChange(${recordId})"> <option value="none" ${bodyFormat === 'none' ? 'selected' : ''}>无请求体</option> <option value="text" ${bodyFormat === 'text' ? 'selected' : ''}>文本/字符串</option> <option value="json" ${bodyFormat === 'json' ? 'selected' : ''}>JSON</option> <option value="form" ${bodyFormat === 'form' ? 'selected' : ''}>表单数据</option> </select> </div> <div class="ajax-edit-group" id="body-group-${recordId}" ${bodyFormat === 'none' ? 'style="display:none"' : ''}> <label for="edit-body-${recordId}">请求体内容</label> <textarea id="edit-body-${recordId}" name="body" placeholder="请求体内容..." rows="6">${formatRequestBodyForEdit(record.requestBody, bodyFormat)}</textarea> <small id="body-hint-${recordId}" style="color: #6c757d; font-size: 11px;"> ${getBodyFormatHint(bodyFormat)} </small> </div> </form> </div> <div class="ajax-edit-actions"> <button class="cancel-btn" onclick="this.closest('.ajax-edit-modal').remove()">取消</button> <button class="send-btn" onclick="sendEditedRequest(${recordId})">发送请求</button> </div> </div> `; document.body.appendChild(modal); // 点击背景关闭 modal.onclick = (e) => { if (e.target === modal) { modal.remove(); } }; }; // 格式化请求体用于编辑 function formatRequestBodyForEdit(body, format) { if (!body) return ''; switch (format) { case 'json': if (typeof body === 'string') { try { return JSON.stringify(JSON.parse(body), null, 2); } catch { return body; } } return JSON.stringify(body, null, 2); case 'form': if (body instanceof FormData) { const entries = []; for (let pair of body.entries()) { entries.push(`${pair[0]}=${pair[1]}`); } return entries.join('&'); } return body.toString(); case 'text': default: return body.toString(); } } // 获取请求体格式提示 function getBodyFormatHint(format) { switch (format) { case 'json': return '请输入有效的JSON格式数据,例如: {"key": "value"}'; case 'form': return '请输入表单数据格式,例如: key1=value1&key2=value2'; case 'text': return '请输入纯文本内容'; case 'none': return '该请求不包含请求体'; default: return ''; } } // 处理请求体格式变化 window.handleBodyFormatChange = function(recordId) { const formatSelect = document.getElementById(`edit-body-format-${recordId}`); const bodyGroup = document.getElementById(`body-group-${recordId}`); const bodyHint = document.getElementById(`body-hint-${recordId}`); const selectedFormat = formatSelect.value; if (selectedFormat === 'none') { bodyGroup.style.display = 'none'; } else { bodyGroup.style.display = 'block'; bodyHint.textContent = getBodyFormatHint(selectedFormat); } }; // 发送编辑后的请求 window.sendEditedRequest = function(recordId) { const form = document.getElementById(`edit-form-${recordId}`); const formData = new FormData(form); const method = formData.get('method'); const url = formData.get('url'); const headersText = formData.get('headers'); const bodyText = formData.get('body'); const bodyFormat = formData.get('bodyFormat'); let headers = {}; try { if (headersText.trim()) { headers = JSON.parse(headersText); } } catch (e) { showToast('请求头格式错误,请使用正确的JSON格式', 'error'); return; } // 根据格式处理请求体 let processedBody = null; if (bodyFormat !== 'none' && bodyText && method !== 'GET') { switch (bodyFormat) { case 'json': try { // 验证JSON格式 JSON.parse(bodyText); processedBody = bodyText; // 确保Content-Type正确 if (!headers['Content-Type'] && !headers['content-type']) { headers['Content-Type'] = 'application/json'; } } catch (e) { showToast('JSON格式错误,请检查请求体内容', 'error'); return; } break; case 'form': processedBody = bodyText; // 确保Content-Type正确 if (!headers['Content-Type'] && !headers['content-type']) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } break; case 'text': default: processedBody = bodyText; break; } } // 发送请求 const requestOptions = { method: method, headers: headers }; if (processedBody !== null) { requestOptions.body = processedBody; } console.log('🚀 发送编辑后的请求:', { method, url, headers, body: processedBody, bodyFormat }); fetch(url, requestOptions) .then(response => { console.log('✅ 请求发送成功,状态:', response.status); showToast(`请求已发送,状态: ${response.status}`, response.ok ? 'success' : 'error'); // 关闭模态框 document.querySelector('.ajax-edit-modal').remove(); }) .catch(error => { console.error('❌ 请求发送失败:', error); showToast(`请求发送失败: ${error.message}`, 'error'); }); }; // 显示提示信息 function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 24px; border-radius: 6px; color: white; font-size: 14px; font-weight: 500; z-index: 2147483650; box-shadow: 0 4px 16px rgba(0,0,0,0.2); transition: all 0.3s ease; backdrop-filter: blur(10px); `; switch (type) { case 'success': toast.style.background = 'linear-gradient(135deg, #28a745, #20c997)'; break; case 'error': toast.style.background = 'linear-gradient(135deg, #dc3545, #fd7e14)'; break; default: toast.style.background = 'linear-gradient(135deg, #007acc, #0099ff)'; } toast.textContent = message; document.body.appendChild(toast); // 3秒后移除 setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateX(-50%) translateY(-20px)'; setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); }, 3000); } // 更新统计数据 function updateStats() { const filteredRecords = filterRecords(); const total = ajaxRecords.length; const success = ajaxRecords.filter(r => r.success).length; const error = total - success; const avgTime = total > 0 ? Math.round(ajaxRecords.reduce((sum, r) => sum + r.duration, 0) / total) : 0; // 检查是否有过滤条件 const isFiltered = currentFilters.searchText || currentFilters.method !== 'all' || currentFilters.status !== 'all'; const hasReverse = currentFilters.searchReverse || currentFilters.methodReverse || currentFilters.statusReverse; if (isFiltered && filteredRecords.length !== total) { let filterText = '(已过滤'; if (hasReverse) { filterText += ' 🚫反向'; } filterText += ')'; document.getElementById('total-count').innerHTML = `${filteredRecords.length} / ${total} <span style="color: #007acc; font-size: 10px;">${filterText}</span>`; } else { document.getElementById('total-count').textContent = total; } document.getElementById('success-count').textContent = success; document.getElementById('error-count').textContent = error; document.getElementById('avg-time').textContent = avgTime; } // 显示记录详情 window.showRecordDetail = function(recordId) { const record = ajaxRecords.find(r => r.id === recordId); if (!record) return; const modal = document.createElement('div'); modal.className = 'ajax-detail-modal'; modal.innerHTML = ` <div class="ajax-detail-content"> <div class="ajax-detail-header"> <h3>🔍 ${record.method} ${record.url}</h3> <button class="close-btn" onclick="this.closest('.ajax-detail-modal').remove()">关闭</button> </div> <div class="ajax-detail-body"> <div class="ajax-detail-section"> <h4>📊 基本信息</h4> <div class="ajax-detail-code">时间: ${record.time} 方法: ${record.method} URL: ${record.url} 状态: ${record.status} ${record.success ? '✅' : '❌'} 耗时: ${record.duration}ms 用户代理: ${navigator.userAgent}</div> </div> <div class="ajax-detail-section"> <h4>📤 请求头 <button class="copy-btn" onclick="copyRequestHeaders(${recordId})">复制</button> </h4> <div class="ajax-detail-code">${JSON.stringify(record.requestHeaders, null, 2)}</div> </div> <div class="ajax-detail-section"> <h4>📝 请求参数 (格式: ${record.bodyFormat || 'auto'}) <button class="copy-btn" onclick="copyRequestBody(${recordId})">复制</button> </h4> <div class="ajax-detail-code">${formatRequestBody(record.requestBody, record.bodyFormat)}</div> </div> <div class="ajax-detail-section"> <h4>📥 响应头 <button class="copy-btn" onclick="copyResponseHeaders(${recordId})">复制</button> </h4> <div class="ajax-detail-code">${JSON.stringify(record.responseHeaders, null, 2)}</div> </div> <div class="ajax-detail-section"> <h4>📋 响应数据 <button class="copy-btn" onclick="copyResponseData(${recordId})">复制</button> </h4> <div class="ajax-detail-code">${formatResponse(record.response)}</div> </div> <div class="ajax-detail-section"> <h4>🔄 cURL命令 <button class="copy-btn" onclick="copyCurlCommand(${recordId})">复制</button> </h4> <div class="ajax-detail-code">${generateCurlCommand(record)}</div> </div> </div> </div> `; document.body.appendChild(modal); // 点击背景关闭 modal.onclick = (e) => { if (e.target === modal) { modal.remove(); } }; }; // 复制到剪贴板 window.copyToClipboard = function(text) { navigator.clipboard.writeText(text).then(() => { console.log('📋 内容已复制到剪贴板'); }).catch(err => { console.error('复制失败:', err); }); }; // 复制响应数据 window.copyResponseData = function(recordId) { const record = ajaxRecords.find(r => r.id === recordId); if (!record) return; const responseText = record.response || ''; navigator.clipboard.writeText(responseText).then(() => { console.log('📋 响应数据已复制到剪贴板'); showToast('响应数据已复制到剪贴板', 'success'); }).catch(err => { console.error('复制失败:', err); showToast('复制失败', 'error'); }); }; window.copyCurlCommand = function(recordId) { const record = ajaxRecords.find(r => r.id === recordId); if (!record) return; const curlCommand = generateCurlCommand(record); navigator.clipboard.writeText(curlCommand).then(() => { console.log('📋 cURL命令已复制到剪贴板'); showToast('cURL命令已复制到剪贴板', 'success'); }).catch(err => { console.error('复制失败:', err); showToast('复制失败', 'error'); }); }; // 复制请求头 window.copyRequestHeaders = function(recordId) { const record = ajaxRecords.find(r => r.id === recordId); if (!record) return; const headersText = JSON.stringify(record.requestHeaders, null, 2); navigator.clipboard.writeText(headersText).then(() => { console.log('📋 请求头已复制到剪贴板'); showToast('请求头已复制到剪贴板', 'success'); }).catch(err => { console.error('复制失败:', err); showToast('复制失败', 'error'); }); }; // 复制请求体 window.copyRequestBody = function(recordId) { const record = ajaxRecords.find(r => r.id === recordId); if (!record) return; const bodyText = formatRequestBodyForCopy(record.requestBody); navigator.clipboard.writeText(bodyText).then(() => { console.log('📋 请求体已复制到剪贴板'); showToast('请求体已复制到剪贴板', 'success'); }).catch(err => { console.error('复制失败:', err); showToast('复制失败', 'error'); }); }; // 复制响应头 window.copyResponseHeaders = function(recordId) { const record = ajaxRecords.find(r => r.id === recordId); if (!record) return; const headersText = JSON.stringify(record.responseHeaders, null, 2); navigator.clipboard.writeText(headersText).then(() => { console.log('📋 响应头已复制到剪贴板'); showToast('响应头已复制到剪贴板', 'success'); }).catch(err => { console.error('复制失败:', err); showToast('复制失败', 'error'); }); }; // 生成cURL命令 function generateCurlCommand(record) { let curl = `curl -X ${record.method} '${record.url}'`; // 添加请求头 Object.entries(record.requestHeaders).forEach(([key, value]) => { curl += ` \\\n -H '${key}: ${value}'`; }); // 添加请求体 if (record.requestBody) { curl += ` \\\n -d '${record.requestBody}'`; } return curl; } // 格式化请求体 function formatRequestBody(body, bodyFormat) { if (!body) return '无请求体'; // 如果有格式信息,按格式处理 if (bodyFormat) { switch (bodyFormat) { case 'json': if (typeof body === 'string') { try { return JSON.stringify(JSON.parse(body), null, 2); } catch { return body; } } return JSON.stringify(body, null, 2); case 'form': if (body instanceof FormData) { const entries = []; for (let pair of body.entries()) { entries.push(`${pair[0]}: ${pair[1]}`); } return entries.join('\n'); } return body.toString(); case 'text': case 'none': default: return body.toString(); } } // 回退到原有逻辑(为了兼容性) if (typeof body === 'string') { // 只在确实是JSON格式时才格式化 try { const parsed = JSON.parse(body); // 检查是否真的是对象/数组 if (typeof parsed === 'object' && parsed !== null) { return JSON.stringify(parsed, null, 2); } return body; } catch { return body; } } if (body instanceof FormData) { const entries = []; for (let pair of body.entries()) { entries.push(`${pair[0]}: ${pair[1]}`); } return entries.join('\n'); } return JSON.stringify(body, null, 2); } // 格式化请求体用于复制 function formatRequestBodyForCopy(body) { if (!body) return ''; return typeof body === 'string' ? body.replace(/'/g, "\\'") : JSON.stringify(body).replace(/'/g, "\\'"); } // 格式化响应数据 function formatResponse(response) { if (!response) return '无响应数据'; if (typeof response === 'string') { try { return JSON.stringify(JSON.parse(response), null, 2); } catch { return response; } } return JSON.stringify(response, null, 2); } // 导出记录 function exportRecords() { const exportData = { exportTime: new Date().toISOString(), environment: { userAgent: navigator.userAgent, url: window.location.href, timestamp: Date.now() }, records: ajaxRecords }; const data = JSON.stringify(exportData, null, 2); const blob = new Blob([data], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `ajax_records_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`; a.click(); URL.revokeObjectURL(url); console.log('📥 导出完成,包含', ajaxRecords.length, '条记录'); } // 检测请求体格式 function detectBodyFormat(body, contentType) { if (!body) return 'none'; contentType = (contentType || '').toLowerCase(); if (contentType.includes('application/json')) { return 'json'; } else if (contentType.includes('application/x-www-form-urlencoded')) { return 'form'; } else if (contentType.includes('multipart/form-data')) { return 'multipart'; } else if (contentType.includes('text/')) { return 'text'; } // 尝试检测是否是JSON字符串 if (typeof body === 'string') { try { JSON.parse(body); return 'json'; } catch { return 'text'; } } if (body instanceof FormData) { return 'form'; } return 'text'; } // 记录Ajax请求 function recordAjaxRequest(data) { if (ajaxRecords.length >= CONFIG.maxRecords) { ajaxRecords.shift(); // 删除最早的记录 } // 检测请求体格式 const contentType = data.requestHeaders['Content-Type'] || data.requestHeaders['content-type'] || ''; const bodyFormat = detectBodyFormat(data.requestBody, contentType); ajaxRecords.push({ id: ++recordIndex, time: new Date().toLocaleTimeString(), timestamp: Date.now(), bodyFormat: bodyFormat, ...data }); if (monitorPanel) { updateRecordsList(); updateStats(); } } // 拦截XMLHttpRequest function interceptXMLHttpRequest() { window.XMLHttpRequest = function() { const xhr = new originalXHR(); const startTime = Date.now(); let requestData = { method: 'GET', url: '', requestHeaders: {}, requestBody: null, responseHeaders: {}, response: '', status: 0, success: false, duration: 0 }; // 拦截open方法 const originalOpen = xhr.open; xhr.open = function(method, url, async, user, password) { requestData.method = method.toUpperCase(); requestData.url = url; return originalOpen.apply(this, arguments); }; // 拦截setRequestHeader方法 const originalSetRequestHeader = xhr.setRequestHeader; xhr.setRequestHeader = function(name, value) { requestData.requestHeaders[name] = value; return originalSetRequestHeader.apply(this, arguments); }; // 拦截send方法 const originalSend = xhr.send; xhr.send = function(body) { requestData.requestBody = body; // 监听状态变化 const originalOnReadyStateChange = xhr.onreadystatechange; xhr.onreadystatechange = function() { if (xhr.readyState === 4) { requestData.duration = Date.now() - startTime; requestData.status = xhr.status; requestData.success = xhr.status >= 200 && xhr.status < 400; requestData.response = xhr.responseText; // 获取响应头 const responseHeadersStr = xhr.getAllResponseHeaders(); if (responseHeadersStr) { responseHeadersStr.split('\r\n').forEach(line => { const parts = line.split(': '); if (parts.length === 2) { requestData.responseHeaders[parts[0]] = parts[1]; } }); } if (isListening) { recordAjaxRequest(requestData); } } if (originalOnReadyStateChange) { originalOnReadyStateChange.apply(this, arguments); } }; return originalSend.apply(this, arguments); }; return xhr; }; // 复制原始构造函数的静态属性 Object.setPrototypeOf(window.XMLHttpRequest, originalXHR); Object.setPrototypeOf(window.XMLHttpRequest.prototype, originalXHR.prototype); } // 拦截fetch请求 function interceptFetch() { window.fetch = function(input, init = {}) { const startTime = Date.now(); const url = typeof input === 'string' ? input : input.url; const method = (init.method || 'GET').toUpperCase(); const requestData = { method: method, url: url, requestHeaders: init.headers || {}, requestBody: init.body || null, responseHeaders: {}, response: '', status: 0, success: false, duration: 0 }; return originalFetch(input, init).then(response => { requestData.duration = Date.now() - startTime; requestData.status = response.status; requestData.success = response.ok; // 获取响应头 response.headers.forEach((value, name) => { requestData.responseHeaders[name] = value; }); // 克隆响应以读取内容 const responseClone = response.clone(); responseClone.text().then(text => { requestData.response = text; if (isListening) { recordAjaxRequest(requestData); } }).catch(() => { // 如果无法读取响应内容,仍然记录请求 if (isListening) { recordAjaxRequest(requestData); } }); return response; }).catch(error => { requestData.duration = Date.now() - startTime; requestData.status = 0; requestData.success = false; requestData.response = error.message; if (isListening) { recordAjaxRequest(requestData); } throw error; }); }; } // 开始监听 function startMonitoring() { if (isListening) return; isListening = true; ajaxRecords = []; recordIndex = 0; // 安装拦截器 interceptXMLHttpRequest(); interceptFetch(); // 创建监控面板 createMonitorPanel(); console.log('🎯 Ajax监听已启动'); console.log('📍 当前URL:', window.location.href); console.log('🔧 按 F3 可关闭监听'); } // 停止监听 function stopMonitoring() { isListening = false; // 恢复原始方法 window.XMLHttpRequest = originalXHR; window.fetch = originalFetch; // 移除监控面板 if (monitorPanel) { monitorPanel.remove(); monitorPanel = null; } console.log('🔴 Ajax监听已停止'); } // 热键监听 function setupHotkey() { document.addEventListener('keydown', function(e) { if (e.key === 'F3') { e.preventDefault(); if (isListening) { stopMonitoring(); } else { startMonitoring(); } } }); } // 初始化 function init() { setupHotkey(); console.log('📡 Ajax监听器已加载'); console.log('🚀 按 F3 启动Ajax请求监听'); console.log('🌐 当前环境:', window.location.href); } // 页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址