// ==UserScript== // @name Network Logger // @namespace https://tampermonkey.net/ // @version 1.0 // @description 带悬浮面板的网络请求监听器 // @match *://*/* // @run-at document-start // @grant none // @downloadURL https://update.greasyfork.icu/scripts/574025/Network%20Logger.user.js // @updateURL https://update.greasyfork.icu/scripts/574025/Network%20Logger.meta.js // ==/UserScript== (function () { 'use strict'; if (window.__nw_logger_installed__) return; window.__nw_logger_installed__ = true; // ============================================================ // 配置 // ============================================================ let config = { enabled: true, keywords: [], maxBodyLength: 2000, logXHR: true, logFetch: true, }; // 日志存储 const logs = []; let MAX_LOGS = 200; // ============================================================ // Hook 必须最早执行 // ============================================================ function shouldLog(url) { if (!config.enabled) return false; if (config.keywords.length === 0) return true; return config.keywords.some(kw => kw && url.includes(kw)); } function parseBody(body) { try { return { type: 'json', data: JSON.parse(body) }; } catch { return { type: 'text', data: String(body) }; } } function addLog(entry) { logs.unshift(entry); if (logs.length > MAX_LOGS) logs.pop(); renderLogs(); } // ===== XHR Hook ===== const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this.__nw_method__ = method; this.__nw_url__ = url; return origOpen.apply(this, [method, url, ...rest]); }; XMLHttpRequest.prototype.send = function (...sendArgs) { this.addEventListener('load', function () { const url = this.responseURL || this.__nw_url__ || ''; if (!shouldLog(url)) return; const rawBody = (this.responseText != null && this.responseText !== '') ? this.responseText : (typeof this.response === 'string' ? this.response : JSON.stringify(this.response)); addLog({ id: Date.now() + Math.random(), type: 'XHR', method: (this.__nw_method__ || 'GET').toUpperCase(), url, status: this.status, time: new Date().toLocaleTimeString(), body: rawBody, parsed: parseBody(rawBody), }); }); return origSend.apply(this, sendArgs); }; // ===== Fetch Hook ===== const origFetch = window.fetch; window.fetch = async function (...args) { const req = args[0]; const url = req instanceof Request ? req.url : String(req || ''); const method = (args[1]?.method || (req instanceof Request ? req.method : 'GET')).toUpperCase(); const response = await origFetch.apply(this, args); if (shouldLog(url)) { response.clone().text() .then(text => { addLog({ id: Date.now() + Math.random(), type: 'Fetch', method, url, status: response.status, time: new Date().toLocaleTimeString(), body: text, parsed: parseBody(text), }); }) .catch(() => { }); } return response; }; // ============================================================ // UI 创建 // ============================================================ function initUI() { // ---------- 样式 ---------- const style = document.createElement('style'); style.textContent = ` /* 容器 */ #nw-logger-root * { box-sizing: border-box; font-family: 'Consolas', 'Monaco', monospace; } /* 悬浮按钮 */ #nw-logger-fab { position: fixed; bottom: 24px; right: 24px; z-index: 2147483646; width: 52px; height: 52px; border-radius: 50%; background: linear-gradient(135deg, #0f3460, #e94560); color: #fff; font-size: 22px; border: none; cursor: pointer; box-shadow: 0 4px 16px rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; transition: transform 0.2s, box-shadow 0.2s; user-select: none; } #nw-logger-fab:hover { transform: scale(1.1); box-shadow: 0 6px 24px rgba(233,69,96,0.5); } /* 徽标 */ #nw-logger-badge { position: absolute; top: -4px; right: -4px; background: #ff4757; color: #fff; font-size: 10px; font-family: sans-serif; min-width: 18px; height: 18px; border-radius: 9px; display: flex; align-items: center; justify-content: center; padding: 0 4px; font-weight: bold; display: none; } /* 主面板 */ #nw-logger-panel { position: fixed; bottom: 90px; right: 24px; z-index: 2147483645; width: 700px; max-width: calc(100vw - 48px); height: 520px; max-height: calc(100vh - 120px); background: #0d1117; border: 1px solid #30363d; border-radius: 12px; box-shadow: 0 16px 48px rgba(0,0,0,0.6); display: flex; flex-direction: column; overflow: hidden; transition: opacity 0.2s, transform 0.2s; } #nw-logger-panel.hidden { opacity: 0; transform: translateY(12px) scale(0.98); pointer-events: none; } /* 顶部栏 */ #nw-logger-header { display: flex; align-items: center; gap: 8px; padding: 10px 14px; background: #161b22; border-bottom: 1px solid #30363d; flex-shrink: 0; } #nw-logger-header h3 { margin: 0; font-size: 13px; color: #e6edf3; flex: 1; letter-spacing: 0.5px; } /* 顶部按钮 */ .nw-hbtn { padding: 3px 10px; border-radius: 5px; border: 1px solid #30363d; background: #21262d; color: #c9d1d9; font-size: 11px; cursor: pointer; transition: background 0.15s; white-space: nowrap; } .nw-hbtn:hover { background: #30363d; } .nw-hbtn.danger:hover { background: #5a1a1a; border-color: #e94560; color: #e94560; } .nw-hbtn.active { background: #1f6feb; border-color: #388bfd; color: #fff; } /* 开关 */ #nw-toggle-wrap { display: flex; align-items: center; gap: 5px; font-size: 11px; color: #8b949e; } #nw-toggle { position: relative; width: 32px; height: 17px; flex-shrink: 0; } #nw-toggle input { opacity: 0; width: 0; height: 0; } #nw-toggle-slider { position: absolute; inset: 0; background: #30363d; border-radius: 17px; cursor: pointer; transition: background 0.2s; } #nw-toggle-slider::before { content: ''; position: absolute; width: 11px; height: 11px; left: 3px; top: 3px; background: #fff; border-radius: 50%; transition: transform 0.2s; } #nw-toggle input:checked + #nw-toggle-slider { background: #238636; } #nw-toggle input:checked + #nw-toggle-slider::before { transform: translateX(15px); } /* 过滤栏 */ #nw-logger-toolbar { display: flex; align-items: center; gap: 6px; padding: 7px 14px; background: #161b22; border-bottom: 1px solid #30363d; flex-shrink: 0; flex-wrap: wrap; } #nw-search { flex: 1; min-width: 120px; padding: 4px 8px; background: #0d1117; border: 1px solid #30363d; border-radius: 5px; color: #c9d1d9; font-size: 12px; outline: none; } #nw-search:focus { border-color: #1f6feb; } #nw-keywords { flex: 1.5; min-width: 150px; padding: 4px 8px; background: #0d1117; border: 1px solid #30363d; border-radius: 5px; color: #c9d1d9; font-size: 12px; outline: none; } #nw-keywords:focus { border-color: #e94560; } .nw-filter-btn { padding: 4px 8px; border-radius: 5px; border: 1px solid #30363d; background: #21262d; color: #8b949e; font-size: 11px; cursor: pointer; transition: all 0.15s; } .nw-filter-btn.on { background: #1f3a5f; border-color: #388bfd; color: #79c0ff; } /* 日志列表 */ #nw-logger-list { flex: 1; overflow-y: auto; padding: 6px 0; scrollbar-width: thin; scrollbar-color: #30363d #0d1117; } #nw-logger-list::-webkit-scrollbar { width: 5px; } #nw-logger-list::-webkit-scrollbar-track { background: #0d1117; } #nw-logger-list::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; } /* 空状态 */ #nw-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #484f58; font-size: 13px; gap: 8px; } #nw-empty span { font-size: 36px; } /* 日志条目 */ .nw-log-item { border-bottom: 1px solid #21262d; cursor: pointer; transition: background 0.12s; } .nw-log-item:hover { background: #161b22; } .nw-log-item.expanded { background: #161b22; } /* 条目头部 */ .nw-log-head { display: flex; align-items: center; gap: 6px; padding: 6px 14px; font-size: 12px; } /* 类型标签 */ .nw-badge { padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: bold; flex-shrink: 0; } .nw-badge.xhr { background: #0d419d; color: #79c0ff; } .nw-badge.fetch { background: #3d1d00; color: #ffa657; } /* 方法标签 */ .nw-method { font-size: 10px; font-weight: bold; flex-shrink: 0; width: 38px; text-align: center; } .nw-method.get { color: #3fb950; } .nw-method.post { color: #ffa657; } .nw-method.put { color: #79c0ff; } .nw-method.delete { color: #e94560; } .nw-method.other { color: #8b949e; } /* 状态码 */ .nw-status { font-size: 10px; font-weight: bold; flex-shrink: 0; width: 28px; } .nw-status.ok { color: #3fb950; } .nw-status.redirect { color: #e3b341; } .nw-status.error { color: #e94560; } /* URL */ .nw-url { flex: 1; color: #c9d1d9; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 12px; } /* 时间 */ .nw-time { color: #484f58; font-size: 10px; flex-shrink: 0; } /* 展开箭头 */ .nw-arrow { color: #484f58; font-size: 10px; flex-shrink: 0; transition: transform 0.2s; } .nw-log-item.expanded .nw-arrow { transform: rotate(90deg); } /* 展开内容 */ .nw-log-body { display: none; padding: 0 14px 10px 14px; } .nw-log-item.expanded .nw-log-body { display: block; } /* URL 完整显示 */ .nw-full-url { font-size: 11px; color: #8b949e; word-break: break-all; margin-bottom: 8px; padding: 6px 8px; background: #161b22; border-radius: 4px; border: 1px solid #21262d; } /* body 区域 */ .nw-body-wrap { position: relative; } .nw-body-label { font-size: 10px; color: #484f58; margin-bottom: 4px; display: flex; align-items: center; justify-content: space-between; } .nw-copy-btn { padding: 1px 7px; border-radius: 3px; border: 1px solid #30363d; background: #21262d; color: #8b949e; font-size: 10px; cursor: pointer; transition: all 0.15s; } .nw-copy-btn:hover { background: #30363d; color: #c9d1d9; } .nw-copy-btn.copied { border-color: #238636; color: #3fb950; } pre.nw-pre { margin: 0; padding: 8px; background: #010409; border: 1px solid #21262d; border-radius: 5px; font-size: 11px; color: #c9d1d9; overflow-x: auto; white-space: pre-wrap; word-break: break-all; max-height: 220px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #30363d #010409; line-height: 1.5; } /* 底部状态栏 */ #nw-logger-footer { padding: 5px 14px; background: #161b22; border-top: 1px solid #30363d; font-size: 10px; color: #484f58; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; } `; document.head.appendChild(style); // ---------- 根容器 ---------- const root = document.createElement('div'); root.id = 'nw-logger-root'; // ---------- 悬浮按钮 ---------- root.innerHTML = `
${escHtml(bodyStr)}