// ==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 = ` `; document.body.appendChild(root); // ---------- 获取元素 ---------- const fab = root.querySelector('#nw-logger-fab'); const badge = root.querySelector('#nw-logger-badge'); const panel = root.querySelector('#nw-logger-panel'); const list = root.querySelector('#nw-logger-list'); const empty = root.querySelector('#nw-empty'); const footerCnt = root.querySelector('#nw-footer-count'); const chk = root.querySelector('#nw-enabled-chk'); const toggleLbl = root.querySelector('#nw-toggle-label'); const searchEl = root.querySelector('#nw-search'); const kwEl = root.querySelector('#nw-keywords'); const xhrBtn = root.querySelector('#nw-xhr-btn'); const fetchBtn = root.querySelector('#nw-fetch-btn'); const clearBtn = root.querySelector('#nw-clear-btn'); const filterBtns = root.querySelectorAll('.nw-filter-btn'); // ---------- 状态 ---------- let panelOpen = false; let searchQ = ''; let methodFilter = new Set(); // 空 = 全部 // ---------- 初始化按钮状态 ---------- if (config.logXHR) xhrBtn.classList.add('active'); if (config.logFetch) fetchBtn.classList.add('active'); // ---------- 悬浮按钮点击 ---------- fab.addEventListener('click', () => { panelOpen = !panelOpen; panel.classList.toggle('hidden', !panelOpen); if (panelOpen) { badge.style.display = 'none'; renderLogs(); } }); // ---------- 监听开关 ---------- chk.addEventListener('change', () => { config.enabled = chk.checked; toggleLbl.textContent = config.enabled ? '监听中' : '已暂停'; }); // ---------- XHR / Fetch 开关 ---------- xhrBtn.addEventListener('click', () => { config.logXHR = !config.logXHR; xhrBtn.classList.toggle('active', config.logXHR); }); fetchBtn.addEventListener('click', () => { config.logFetch = !config.logFetch; fetchBtn.classList.toggle('active', config.logFetch); }); // ---------- 清空 ---------- clearBtn.addEventListener('click', () => { logs.length = 0; renderLogs(); }); // ---------- 搜索 ---------- searchEl.addEventListener('input', () => { searchQ = searchEl.value.trim().toLowerCase(); renderLogs(); }); // ---------- 关键词---------- kwEl.addEventListener('change', () => { config.keywords = kwEl.value.split(',').map(s => s.trim()).filter(Boolean); }); // ---------- 方法过滤 ---------- filterBtns.forEach(btn => { btn.addEventListener('click', () => { const m = btn.dataset.m; if (methodFilter.has(m)) { methodFilter.delete(m); btn.classList.remove('on'); } else { methodFilter.add(m); btn.classList.add('on'); } renderLogs(); }); }); // ---------- 渲染 ---------- window.__nw_render__ = function () { // 过滤 const filtered = logs.filter(log => { if (methodFilter.size > 0 && !methodFilter.has(log.method)) return false; if (searchQ && !log.url.toLowerCase().includes(searchQ)) return false; return true; }); // 更新徽标 if (!panelOpen && logs.length > 0) { badge.style.display = 'flex'; badge.textContent = logs.length > 99 ? '99+' : logs.length; } footerCnt.textContent = `共 ${filtered.length} 条${searchQ || methodFilter.size ? '(已过滤)' : ''}`; if (!panelOpen) return; if (filtered.length === 0) { empty.style.display = 'flex'; // 移除旧条目 list.querySelectorAll('.nw-log-item').forEach(el => el.remove()); return; } empty.style.display = 'none'; // 构建 id => DOM 映射 const existing = {}; list.querySelectorAll('.nw-log-item').forEach(el => { existing[el.dataset.id] = el; }); // 按顺序重建 const frag = document.createDocumentFragment(); filtered.forEach(log => { let item = existing[log.id]; if (!item) { item = buildLogItem(log); } delete existing[log.id]; frag.appendChild(item); }); // 删除不再显示的 Object.values(existing).forEach(el => el.remove()); list.appendChild(frag); }; // 构建单条日志 DOM function buildLogItem(log) { const item = document.createElement('div'); item.className = 'nw-log-item'; item.dataset.id = log.id; // 状态码颜色 const statusClass = log.status >= 400 ? 'error' : log.status >= 300 ? 'redirect' : 'ok'; // 方法颜色 const methodClass = { GET: 'get', POST: 'post', PUT: 'put', DELETE: 'delete' }[log.method] || 'other'; // body 内容 const bodyStr = log.parsed.type === 'json' ? JSON.stringify(log.parsed.data, null, 2) : String(log.body).substring(0, config.maxBodyLength); item.innerHTML = `
${log.type} ${log.method} ${log.status} ${log.url} ${log.time}
${log.url}
响应体 ${log.parsed.type === 'json' ? '(JSON)' : '(Text)'}
${escHtml(bodyStr)}
`; // 展开/收起 item.querySelector('.nw-log-head').addEventListener('click', () => { item.classList.toggle('expanded'); }); // 复制 const copyBtn = item.querySelector('.nw-copy-btn'); copyBtn.addEventListener('click', (e) => { e.stopPropagation(); navigator.clipboard.writeText(log.body || '').then(() => { copyBtn.textContent = '✅ 已复制'; copyBtn.classList.add('copied'); setTimeout(() => { copyBtn.textContent = '复制'; copyBtn.classList.remove('copied'); }, 1500); }); }); return item; } // HTML 转义 function escHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>'); } }; // end initUI // ============================================================ // renderLogs 代理 // ============================================================ function renderLogs() { if (window.__nw_render__) window.__nw_render__(); } // ============================================================ // DOM 就绪后初始化 UI // ============================================================ if (document.body) { initUI(); } else { document.addEventListener('DOMContentLoaded', initUI); } })();