/** * DevTools Panel Script - 使用 DevTools Network API 捕获 XHR 请求 * 纯 JavaScript 实现,完全符合 Manifest V3 CSP 规范 */ // MD5 函数(使用 CryptoJS) function MD5(str) { if (typeof CryptoJS === 'undefined' || !CryptoJS.MD5) { console.warn('CryptoJS.MD5 不可用,请确保已引入 crypto-js.min.js'); return null; } if (!str) return null; try { return CryptoJS.MD5(str); } catch (error) { console.error('MD5 计算失败:', error); return null; } } // 工具函数 function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // 复制到剪贴板(参考 chromeEvent 的成功实现) function copyToClipboard(text) { // 创建一个临时的textarea元素 const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.left = '-9999px'; textarea.style.top = '-9999px'; document.body.appendChild(textarea); try { textarea.select(); textarea.setSelectionRange(0, 99999); // 兼容移动设备 const successful = document.execCommand('copy'); document.body.removeChild(textarea); return successful; } catch (err) { console.error('复制失败:', err); if (textarea.parentNode) { document.body.removeChild(textarea); } return false; } } // 显示复制成功提示 function showCopyNotification(message, duration = 2000) { // 移除已存在的提示 const existing = document.getElementById('copy-notification'); if (existing) { existing.remove(); } const notification = document.createElement('div'); notification.id = 'copy-notification'; notification.textContent = message; Object.assign(notification.style, { position: 'fixed', top: '20px', right: '20px', background: '#4ec9b0', color: 'white', padding: '12px 20px', borderRadius: '6px', fontSize: '14px', fontWeight: 'bold', zIndex: '10000', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)', animation: 'slideIn 0.3s ease-out' }); // 添加动画样式 if (!document.getElementById('copy-notification-style')) { const animationStyle = document.createElement('style'); animationStyle.id = 'copy-notification-style'; animationStyle.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } @keyframes shake { 0%, 100% { transform: translateX(0); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); } 20%, 40%, 60%, 80% { transform: translateX(4px); } } `; document.head.appendChild(animationStyle); } document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease-out'; setTimeout(() => { if (notification.parentNode) { notification.remove(); } }, 300); }, duration); } // 创建复制按钮 function createCopyButton(text, label = '复制') { const button = document.createElement('button'); button.className = 'copy-btn'; const styles = getStyles(); const isLightTheme = state.theme === 'light'; // 主题颜色 const primaryColor = isLightTheme ? '#1890ff' : '#4ec9b0'; const hoverColor = isLightTheme ? '#40a9ff' : '#3fb89a'; const successColor = isLightTheme ? '#52c41a' : '#68d391'; // 图标 SVG(更美观的复制图标) const copyIcon = ` `; const checkIcon = ` `; button.innerHTML = `${copyIcon}${label}`; button.title = '复制到剪贴板'; Object.assign(button.style, { background: primaryColor, color: 'white', border: 'none', padding: '6px 14px', borderRadius: '6px', cursor: 'pointer', fontSize: (state.fontSize - 1) + 'px', fontWeight: '500', marginLeft: '8px', marginTop: '5px', display: 'inline-flex', alignItems: 'center', gap: '6px', transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)', boxShadow: isLightTheme ? '0 2px 4px rgba(24, 144, 255, 0.2)' : '0 2px 6px rgba(78, 201, 176, 0.25)', position: 'relative', overflow: 'hidden', outline: 'none', userSelect: 'none' }); // 图标和文字样式 const iconSpan = button.querySelector('.copy-icon'); const textSpan = button.querySelector('.copy-text'); if (iconSpan) { Object.assign(iconSpan.style, { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '14px', height: '14px', transition: 'transform 0.25s' }); } if (textSpan) { Object.assign(textSpan.style, { display: 'inline-block', lineHeight: '1' }); } // 悬停效果 button.onmouseenter = () => { button.style.background = hoverColor; button.style.transform = 'translateY(-1px)'; button.style.boxShadow = isLightTheme ? '0 4px 8px rgba(64, 169, 255, 0.3)' : '0 4px 10px rgba(63, 184, 154, 0.35)'; if (iconSpan) { iconSpan.style.transform = 'scale(1.1)'; } }; button.onmouseleave = () => { if (!button.dataset.copied) { button.style.background = primaryColor; button.style.transform = 'translateY(0)'; button.style.boxShadow = isLightTheme ? '0 2px 4px rgba(24, 144, 255, 0.2)' : '0 2px 6px rgba(78, 201, 176, 0.25)'; if (iconSpan) { iconSpan.style.transform = 'scale(1)'; } } }; // 点击效果 button.onmousedown = () => { button.style.transform = 'translateY(0) scale(0.98)'; }; button.onmouseup = () => { if (!button.dataset.copied) { button.style.transform = 'translateY(-1px)'; } }; // 复制功能 button.onclick = (e) => { e.stopPropagation(); e.preventDefault(); // 点击动画 button.style.transform = 'scale(0.95)'; setTimeout(() => { button.style.transform = ''; }, 100); const success = copyToClipboard(text); if (success) { showCopyNotification('✅ 已复制到剪贴板'); button.innerHTML = `${checkIcon}已复制`; button.style.background = successColor; button.style.boxShadow = isLightTheme ? '0 2px 4px rgba(82, 196, 26, 0.3)' : '0 2px 6px rgba(104, 211, 145, 0.35)'; button.dataset.copied = 'true'; setTimeout(() => { button.innerHTML = `${copyIcon}${label}`; button.style.background = primaryColor; button.style.boxShadow = isLightTheme ? '0 2px 4px rgba(24, 144, 255, 0.2)' : '0 2px 6px rgba(78, 201, 176, 0.25)'; delete button.dataset.copied; // 重新获取图标和文字元素 const newIconSpan = button.querySelector('.copy-icon'); const newTextSpan = button.querySelector('.copy-text'); if (newIconSpan) { Object.assign(newIconSpan.style, { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '14px', height: '14px', transition: 'transform 0.25s' }); } if (newTextSpan) { Object.assign(newTextSpan.style, { display: 'inline-block', lineHeight: '1' }); } }, 2000); } else { showCopyNotification('❌ 复制失败,请手动复制'); // 失败时添加抖动动画 button.style.animation = 'shake 0.4s'; setTimeout(() => { button.style.animation = ''; }, 400); } }; return button; } // AES 解密函数 function decryptData(word, keyStr) { if (!word || !keyStr) return null; try { if (typeof CryptoJS === 'undefined') return null; const decrypt = CryptoJS.AES.decrypt(word, CryptoJS.enc.Utf8.parse(keyStr), { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7, }); return decrypt.toString(CryptoJS.enc.Utf8); } catch (error) { return null; } } function Decrypt(word, keyStr) { if (!word || !keyStr) return null; try { const cleanData = word.replace(/\s/g, '').replace(/"/g, ''); const cleanKey = keyStr.replace(/\s/g, '').replace(/"/g, ''); const decrypt = CryptoJS.AES.decrypt(cleanData, CryptoJS.enc.Utf8.parse(cleanKey), { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7, }); return decrypt.toString(CryptoJS.enc.Utf8) || null; } catch (error) { return null; } } // JSON 语法高亮 function highlightJson(jsonString) { if (!jsonString) return ''; try { let parsed; try { parsed = JSON.parse(jsonString); } catch (e) { if (typeof JSON5 !== 'undefined') { try { parsed = JSON5.parse(jsonString); } catch (e2) { return escapeHtml(jsonString); } } else { return escapeHtml(jsonString); } } const formatted = JSON.stringify(parsed, null, 2); const isLightTheme = document.body.classList.contains('light-theme'); const colors = { key: isLightTheme ? '#881391' : '#c678dd', string: isLightTheme ? '#0b7500' : '#98c379', number: isLightTheme ? '#1c00cf' : '#61afef', boolean: isLightTheme ? '#0b7500' : '#56b6c2', null: isLightTheme ? '#808080' : '#c678dd', punctuation: isLightTheme ? '#000000' : '#abb2bf' }; let highlighted = formatted .replace(/&/g, '&') .replace(//g, '>'); highlighted = highlighted.replace(/([{}[\]])/g, `$1`); highlighted = highlighted.replace(/(:\s*)("(?:[^"\\]|\\.)*")/g, `$1$2`); highlighted = highlighted.replace(/(\[\s*)("(?:[^"\\]|\\.)*")/g, `$1$2`); highlighted = highlighted.replace(/(,\s*)("(?:[^"\\]|\\.)*")/g, (match, p1, p2) => { if (match.includes('color')) return match; return p1 + `${p2}`; }); highlighted = highlighted.replace(/(:\s*)(-?\d+\.?\d*(?:[eE][+-]?\d+)?)(?=\s*[,}\]]|$)/g, (match, p1, p2) => { if (match.includes('${p2}`; }); highlighted = highlighted.replace(/(\[[^\]]*?)(-?\d+\.?\d*(?:[eE][+-]?\d+)?)(?=\s*[,}\]]|$)/g, (match, p1, p2) => { if (match.includes('${p2}`; }); highlighted = highlighted.replace(/(,\s*)(-?\d+\.?\d*(?:[eE][+-]?\d+)?)(?=\s*[,}\]]|$)/g, (match, p1, p2) => { if (match.includes('${p2}`; }); highlighted = highlighted.replace(/(:\s*)(true|false|null)\b/g, (match, p1, p2) => { if (match.includes('${p2}`; }); highlighted = highlighted.replace(/("(?:[^"\\]|\\.)*")\s*:/g, (match, p1) => { if (match.includes('${p1}:`; }); return highlighted; } catch (error) { return escapeHtml(jsonString); } } // 状态管理(纯 JavaScript) const state = { requests: [], fontSize: 12, theme: 'dark' }; // 样式计算 function getStyles() { const isLightTheme = state.theme === 'light'; return { toolbar: { background: isLightTheme ? '#f8f9fa' : '#1a202c', borderColor: isLightTheme ? '#e9ecef' : '#4a5568', color: isLightTheme ? '#333' : '#e2e8f0' }, item: { background: isLightTheme ? '#ffffff' : '#2d3748', borderColor: isLightTheme ? '#e0e0e0' : '#4a5568', color: isLightTheme ? '#333' : '#e2e8f0' }, subText: { color: isLightTheme ? '#666' : '#a0aec0' }, pre: { background: isLightTheme ? '#f8f9fa' : '#1a202c', color: isLightTheme ? '#333' : '#e2e8f0', borderColor: isLightTheme ? '#e9ecef' : '#4a5568' } }; } // 工具函数 function formatTimestamp(timestamp) { const date = new Date(timestamp); return { date: date.toLocaleDateString(), time: date.toLocaleTimeString() }; } function applyTheme() { const body = document.body; if (state.theme === 'light') { body.classList.add('light-theme'); body.classList.remove('dark-theme'); } else { body.classList.add('dark-theme'); body.classList.remove('light-theme'); } } function saveFontSize() { chrome.storage.local.set({ fontSize: state.fontSize }); } function saveTheme() { chrome.storage.local.set({ theme: state.theme }); } function loadSettings() { chrome.storage.local.get(['fontSize', 'theme'], (result) => { if (result.fontSize) { state.fontSize = Math.max(8, Math.min(24, parseInt(result.fontSize) || 12)); } if (result.theme) { state.theme = result.theme; } applyTheme(); render(); }); } // 字体大小控制 function decreaseFontSize() { state.fontSize = Math.max(8, state.fontSize - 1); saveFontSize(); render(); } function increaseFontSize() { state.fontSize = Math.min(24, state.fontSize + 1); saveFontSize(); render(); } function toggleTheme() { state.theme = state.theme === 'dark' ? 'light' : 'dark'; applyTheme(); saveTheme(); render(); } // 清除所有请求 function clearRequests() { state.requests = []; render(); console.log('✅ 已清除所有请求记录'); } // 请求处理 function processRequest(request) { if (!request.request || !request.request.url || !request.request.method) { return; } const url = request.request.url; const method = request.request.method; // 调试日志 console.log('📡 捕获到请求:', method, url); // 1. 过滤 base64 data URI(如 base64 图片) if (url.startsWith('data:') || url.startsWith('blob:')) { console.log('⏭️ 跳过 Data/Blob URI:', url.substring(0, 50) + '...'); return; } // 2. 过滤静态资源文件扩展名 const urlLower = url.toLowerCase(); const skipExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.ico', '.webp', '.css', '.js', '.woff', '.woff2', '.ttf', '.eot', '.mp4', '.mp3', '.avi', '.mov', '.pdf']; const isResourceFile = skipExtensions.some(ext => urlLower.includes(ext)); if (isResourceFile) { console.log('⏭️ 跳过静态资源:', url); return; } // 3. 检查是否为真实的 XHR/Fetch 请求(参考 chromeEvent 的实现) // 获取 Accept 头 let acceptHeader = null; let contentTypeHeader = null; if (request.request.headers) { request.request.headers.forEach(h => { const headerName = h.name.toLowerCase(); if (headerName === 'accept') { acceptHeader = h.value; } if (headerName === 'content-type') { contentTypeHeader = h.value; } }); } // 判断是否为 XHR 请求: // - Accept 头包含 application/json 或 application/x-www-form-urlencoded // - 或者 Content-Type 是 application/json // - 或者请求方法是 POST/PUT/PATCH/DELETE(通常是 API 请求) const isJsonAccept = acceptHeader && ( acceptHeader.includes('application/json') || acceptHeader.includes('application/x-www-form-urlencoded') ); const isJsonContentType = contentTypeHeader && contentTypeHeader.includes('application/json'); const isApiMethod = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method.toUpperCase()); const isGetMethod = method.toUpperCase() === 'GET'; // 判断逻辑: // 1. 如果有 Accept: application/json 或 Content-Type: application/json,肯定是 XHR // 2. 如果是 POST/PUT/PATCH/DELETE,认为是 API 请求 // 3. 如果是 GET,必须要有 Accept: application/json 才能认为是 XHR let isLikelyXhr = false; if (isJsonAccept || isJsonContentType) { // 有明确的 JSON 请求头,肯定是 XHR isLikelyXhr = true; } else if (isApiMethod) { // POST/PUT/PATCH/DELETE 通常是 API 请求 isLikelyXhr = true; } else if (isGetMethod) { // GET 请求必须有 Accept: application/json 才能认为是 XHR isLikelyXhr = false; } if (!isLikelyXhr) { console.log('⏭️ 跳过非 XHR 请求:', url, { accept: acceptHeader, contentType: contentTypeHeader, method: method }); return; } console.log('✅ 确认为 XHR 请求:', url, { accept: acceptHeader, contentType: contentTypeHeader, method: method }); let requestData = '无请求数据'; if (request.request.postData) { try { requestData = JSON.stringify(JSON.parse(request.request.postData.text), null, 2); } catch (e) { requestData = request.request.postData.text; } } const requestHeaders = {}; const requestHeadersOriginal = {}; if (request.request.headers) { request.request.headers.forEach(h => { const lowerName = h.name.toLowerCase(); requestHeaders[lowerName] = h.value; requestHeadersOriginal[h.name] = h.value; }); } const responseHeaders = {}; if (request.response && request.response.headers) { request.response.headers.forEach(h => { const lowerName = h.name.toLowerCase(); responseHeaders[lowerName] = h.value; }); } request.getContent((content, encoding) => { let responseData = '无响应数据'; let responseDataRaw = null; if (content) { if (typeof content === 'string') { responseDataRaw = content.trim(); } else { try { if (content instanceof ArrayBuffer) { const bytes = new Uint8Array(content); responseDataRaw = String.fromCharCode.apply(null, bytes); } else if (content instanceof Uint8Array) { responseDataRaw = String.fromCharCode.apply(null, content); } else { responseDataRaw = String(content); } responseDataRaw = responseDataRaw.trim(); } catch (e) { responseDataRaw = String(content); } } try { const parsed = JSON.parse(responseDataRaw); responseData = JSON.stringify(parsed, null, 2); } catch (e) { responseData = responseDataRaw; } } let time = requestHeaders['time'] || ''; let traceId = requestHeaders['traceid'] || ''; let uuid = requestHeaders['uuid'] || ''; if (!time) { for (const key in requestHeadersOriginal) { if (key.toLowerCase() === 'time') { time = requestHeadersOriginal[key]; break; } } } if (!traceId) { const possibleKeys = ['TraceId', 'trace-id', 'traceId', 'TRACEID', 'Trace-ID']; for (const key of possibleKeys) { if (requestHeadersOriginal[key]) { traceId = requestHeadersOriginal[key]; break; } } if (!traceId) { for (const key in requestHeadersOriginal) { if (key.toLowerCase().includes('trace')) { traceId = requestHeadersOriginal[key]; break; } } } } if (!uuid) { for (const key in requestHeadersOriginal) { if (key.toLowerCase() === 'uuid') { uuid = requestHeadersOriginal[key]; break; } } } if (!uuid) { uuid = responseHeaders['uuid'] || responseHeaders['UUID'] || ''; } let timestampKey = null; let newKey = null if (time && traceId) { const combined = String(time) + String(traceId); timestampKey = combined.slice(0, 16); newKey = MD5(traceId).toString().substring(8, 24); } let decryptedRequestData = null; if (requestData && requestData !== '无请求数据' && timestampKey) { try { const requestDataObj = JSON.parse(requestData); if (requestDataObj && requestDataObj.data && typeof requestDataObj.data === 'string') { const decrypted = decryptData(requestDataObj.data, timestampKey) || Decrypt(requestDataObj.data, newKey) if (decrypted) { try { if (typeof JSON5 !== 'undefined') { const parsed = JSON5.parse(decrypted); decryptedRequestData = JSON.stringify(parsed, null, 2); } else { const parsed = JSON.parse(decrypted); decryptedRequestData = JSON.stringify(parsed, null, 2); } } catch (e) { decryptedRequestData = decrypted; } } } } catch (e) { // 忽略 } } let decryptedResponseData = null; if (responseDataRaw && responseDataRaw !== '无响应数据' && timestampKey) { const decrypted = Decrypt(responseDataRaw, timestampKey) || Decrypt(responseDataRaw, newKey); if (decrypted) { try { if (typeof JSON5 !== 'undefined') { const parsed = JSON5.parse(decrypted); decryptedResponseData = JSON.stringify(parsed, null, 2); } else { const parsed = JSON.parse(decrypted); decryptedResponseData = JSON.stringify(parsed, null, 2); } } catch (e) { const trimmed = decrypted.trim(); if (trimmed.startsWith('{') || trimmed.startsWith('[')) { try { if (typeof JSON5 !== 'undefined') { const parsed = JSON5.parse(trimmed); decryptedResponseData = JSON.stringify(parsed, null, 2); } else { const parsed = JSON.parse(trimmed); decryptedResponseData = JSON.stringify(parsed, null, 2); } } catch (e2) { decryptedResponseData = trimmed; } } else { decryptedResponseData = decrypted; } } } } const requestItem = { id: Date.now() + Math.random(), url: request.request.url, method: request.request.method, requestData, responseData, decryptedRequestData, decryptedResponseData, timestamp: new Date().toISOString(), duration: request.time ? Math.round(request.time) : 0, uuid }; state.requests.push(requestItem); if (state.requests.length > 100) { state.requests.shift(); } render(); }); } // 渲染函数(纯 DOM 操作) function render() { const app = document.getElementById('app'); if (!app) return; const styles = getStyles(); const sortedRequests = [...state.requests].reverse(); const themeIcon = state.theme === 'light' ? '🌙' : '☀️'; // 工具栏 const toolbar = document.createElement('div'); toolbar.className = 'toolbar'; Object.assign(toolbar.style, styles.toolbar); const toolbarLeft = document.createElement('div'); toolbarLeft.className = 'toolbar-left'; const fontSizeControl = document.createElement('div'); fontSizeControl.className = 'font-size-control'; const fontSizeLabel = document.createElement('span'); fontSizeLabel.textContent = '字体大小:'; fontSizeLabel.style.fontSize = (state.fontSize - 1) + 'px'; const decreaseBtn = document.createElement('button'); decreaseBtn.textContent = 'A-'; decreaseBtn.title = '减小字体'; decreaseBtn.onclick = decreaseFontSize; const fontSizeDisplay = document.createElement('span'); fontSizeDisplay.textContent = state.fontSize + 'px'; fontSizeDisplay.style.fontSize = (state.fontSize - 1) + 'px'; fontSizeDisplay.style.minWidth = '40px'; fontSizeDisplay.style.textAlign = 'center'; const increaseBtn = document.createElement('button'); increaseBtn.textContent = 'A+'; increaseBtn.title = '增大字体'; increaseBtn.onclick = increaseFontSize; fontSizeControl.appendChild(fontSizeLabel); fontSizeControl.appendChild(decreaseBtn); fontSizeControl.appendChild(fontSizeDisplay); fontSizeControl.appendChild(increaseBtn); toolbarLeft.appendChild(fontSizeControl); toolbar.appendChild(toolbarLeft); const themeBtn = document.createElement('button'); themeBtn.className = 'theme-toggle'; themeBtn.innerHTML = `${themeIcon} 切换主题`; themeBtn.onclick = toggleTheme; toolbar.appendChild(themeBtn); const clearBtn = document.createElement('button'); clearBtn.className = 'clear-btn'; clearBtn.innerHTML = `🗑️ 清除`; clearBtn.title = '清除所有请求记录'; clearBtn.onclick = clearRequests; Object.assign(clearBtn.style, { background: '#ff4d4f', color: 'white', border: 'none', padding: '6px 12px', borderRadius: '4px', cursor: 'pointer', fontSize: (state.fontSize - 1) + 'px', fontWeight: 'bold', marginLeft: '8px', transition: 'all 0.2s', display: 'inline-flex', alignItems: 'center', gap: '6px' }); clearBtn.onmouseenter = () => { clearBtn.style.background = '#ff7875'; clearBtn.style.transform = 'translateY(-1px)'; }; clearBtn.onmouseleave = () => { clearBtn.style.background = '#ff4d4f'; clearBtn.style.transform = 'translateY(0)'; }; toolbar.appendChild(clearBtn); // 请求列表 const requestList = document.createElement('div'); requestList.className = sortedRequests.length === 0 ? 'empty-state' : 'requests-list'; if (sortedRequests.length === 0) { requestList.textContent = '暂无请求记录'; } else { sortedRequests.forEach(req => { const item = document.createElement('div'); item.className = 'request-item'; Object.assign(item.style, styles.item); const header = document.createElement('div'); header.className = 'request-header'; header.style.cursor = 'pointer'; // 折叠/展开按钮(默认折叠状态) const collapseBtn = document.createElement('button'); collapseBtn.className = 'collapse-btn'; collapseBtn.innerHTML = '▶'; collapseBtn.dataset.collapsed = 'true'; Object.assign(collapseBtn.style, { background: 'transparent', border: 'none', color: styles.subText.color, cursor: 'pointer', fontSize: (state.fontSize - 1) + 'px', padding: '2px 6px', marginRight: '6px', transition: 'transform 0.2s ease-out', outline: 'none', userSelect: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', minWidth: '20px', transform: 'rotate(-90deg)' // 默认折叠状态,箭头向右 }); // 折叠/展开函数 const toggleCollapse = () => { const isCollapsed = collapseBtn.dataset.collapsed === 'true'; collapseBtn.dataset.collapsed = isCollapsed ? 'false' : 'true'; collapseBtn.innerHTML = isCollapsed ? '▼' : '▶'; collapseBtn.style.transform = isCollapsed ? 'rotate(0deg)' : 'rotate(-90deg)'; // 切换 details 和 uuid 的显示 const details = item.querySelector('.request-details'); const uuidInfo = item.querySelector('.uuid-info'); if (isCollapsed) { // 展开 if (details) { details.style.display = ''; details.style.maxHeight = details.scrollHeight + 'px'; details.style.opacity = '1'; } if (uuidInfo) { uuidInfo.style.display = ''; uuidInfo.style.maxHeight = uuidInfo.scrollHeight + 'px'; uuidInfo.style.opacity = '1'; } // 等待动画完成后移除 maxHeight 限制 setTimeout(() => { if (details) details.style.maxHeight = ''; if (uuidInfo) uuidInfo.style.maxHeight = ''; }, 300); } else { // 折叠 if (details) { details.style.maxHeight = details.scrollHeight + 'px'; // 强制重排 details.offsetHeight; details.style.maxHeight = '0'; details.style.opacity = '0'; setTimeout(() => { details.style.display = 'none'; }, 300); } if (uuidInfo) { uuidInfo.style.maxHeight = uuidInfo.scrollHeight + 'px'; // 强制重排 uuidInfo.offsetHeight; uuidInfo.style.maxHeight = '0'; uuidInfo.style.opacity = '0'; setTimeout(() => { uuidInfo.style.display = 'none'; }, 300); } } }; collapseBtn.onclick = (e) => { e.stopPropagation(); toggleCollapse(); }; // 点击整个 header 也可以折叠 header.onclick = (e) => { // 如果点击的是按钮,不重复触发 if (e.target === collapseBtn || collapseBtn.contains(e.target)) { return; } toggleCollapse(); }; const method = document.createElement('span'); method.className = 'method'; method.textContent = req.method; const url = document.createElement('span'); url.className = 'url'; url.textContent = req.url; const timeInfo = formatTimestamp(req.timestamp); const time = document.createElement('span'); time.className = 'time'; time.textContent = timeInfo.date + ' ' + timeInfo.time; time.style.color = styles.subText.color; time.style.fontSize = state.fontSize + 'px'; header.appendChild(collapseBtn); header.appendChild(method); header.appendChild(url); header.appendChild(time); item.appendChild(header); if (req.uuid) { const uuidInfo = document.createElement('div'); uuidInfo.className = 'uuid-info'; Object.assign(uuidInfo.style, { background: styles.pre.background, borderColor: styles.pre.borderColor, fontSize: state.fontSize + 'px', padding: '8px', borderRadius: '4px', border: '1px solid', marginBottom: '10px', display: 'none', // 默认隐藏(折叠状态) alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '8px', maxHeight: '0', opacity: '0' }); const uuidText = document.createElement('div'); uuidText.innerHTML = `📍 UUID: ${escapeHtml(req.uuid)}`; uuidText.style.flex = '1'; uuidText.style.minWidth = '200px'; const copyBtn = createCopyButton(req.uuid, '复制 UUID'); uuidInfo.appendChild(uuidText); uuidInfo.appendChild(copyBtn); item.appendChild(uuidInfo); } const details = document.createElement('div'); details.className = 'request-details'; // 默认折叠状态:隐藏 Object.assign(details.style, { display: 'none', maxHeight: '0', opacity: '0' }); if (req.decryptedRequestData) { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.style.display = 'flex'; titleContainer.style.alignItems = 'center'; titleContainer.style.marginBottom = '5px'; titleContainer.style.flexWrap = 'wrap'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = '🔓 解密后的请求数据:'; title.style.fontSize = (state.fontSize + 1) + 'px'; title.style.color = '#68d391'; title.style.fontWeight = 'bold'; title.style.flex = '1'; title.style.minWidth = '200px'; const copyBtn = createCopyButton(req.decryptedRequestData, '复制'); titleContainer.appendChild(title); titleContainer.appendChild(copyBtn); const pre = document.createElement('pre'); pre.className = 'code-block'; Object.assign(pre.style, { background: styles.pre.background, color: styles.pre.color, borderColor: styles.pre.borderColor, fontSize: state.fontSize + 'px', padding: '10px', borderRadius: '4px', overflowX: 'auto', maxHeight: '300px', overflowY: 'auto', border: '1px solid', fontFamily: 'Courier New, monospace', whiteSpace: 'pre', margin: '0' }); pre.innerHTML = highlightJson(req.decryptedRequestData); section.appendChild(titleContainer); section.appendChild(pre); details.appendChild(section); } else if (req.requestData && req.requestData !== '无请求数据') { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.style.display = 'flex'; titleContainer.style.alignItems = 'center'; titleContainer.style.marginBottom = '5px'; titleContainer.style.flexWrap = 'wrap'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = '📤 原始请求数据:'; title.style.fontSize = (state.fontSize + 1) + 'px'; title.style.color = styles.subText.color; title.style.fontWeight = 'bold'; title.style.flex = '1'; title.style.minWidth = '200px'; const copyBtn = createCopyButton(req.requestData, '复制'); titleContainer.appendChild(title); titleContainer.appendChild(copyBtn); const pre = document.createElement('pre'); pre.className = 'code-block'; Object.assign(pre.style, { background: styles.pre.background, color: styles.subText.color, borderColor: styles.pre.borderColor, fontSize: state.fontSize + 'px', padding: '10px', borderRadius: '4px', overflowX: 'auto', maxHeight: '300px', overflowY: 'auto', border: '1px solid', fontFamily: 'Courier New, monospace', whiteSpace: 'pre', margin: '0' }); pre.innerHTML = escapeHtml(req.requestData); section.appendChild(titleContainer); section.appendChild(pre); details.appendChild(section); } if (req.decryptedResponseData) { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.style.display = 'flex'; titleContainer.style.alignItems = 'center'; titleContainer.style.marginBottom = '5px'; titleContainer.style.flexWrap = 'wrap'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = '🔓 解密后的响应数据:'; title.style.fontSize = (state.fontSize + 1) + 'px'; title.style.color = '#68d391'; title.style.fontWeight = 'bold'; title.style.flex = '1'; title.style.minWidth = '200px'; const copyBtn = createCopyButton(req.decryptedResponseData, '复制'); titleContainer.appendChild(title); titleContainer.appendChild(copyBtn); const pre = document.createElement('pre'); pre.className = 'code-block'; Object.assign(pre.style, { background: styles.pre.background, color: styles.pre.color, borderColor: styles.pre.borderColor, fontSize: state.fontSize + 'px', padding: '10px', borderRadius: '4px', overflowX: 'auto', maxHeight: '300px', overflowY: 'auto', border: '1px solid', fontFamily: 'Courier New, monospace', whiteSpace: 'pre', margin: '0' }); pre.innerHTML = highlightJson(req.decryptedResponseData); section.appendChild(titleContainer); section.appendChild(pre); details.appendChild(section); } else if (req.responseData && req.responseData !== '无响应数据') { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.style.display = 'flex'; titleContainer.style.alignItems = 'center'; titleContainer.style.marginBottom = '5px'; titleContainer.style.flexWrap = 'wrap'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = '📥 原始响应数据:'; title.style.fontSize = (state.fontSize + 1) + 'px'; title.style.color = styles.subText.color; title.style.fontWeight = 'bold'; title.style.flex = '1'; title.style.minWidth = '200px'; const copyBtn = createCopyButton(req.responseData, '复制'); titleContainer.appendChild(title); titleContainer.appendChild(copyBtn); const pre = document.createElement('pre'); pre.className = 'code-block'; Object.assign(pre.style, { background: styles.pre.background, color: styles.subText.color, borderColor: styles.pre.borderColor, fontSize: state.fontSize + 'px', padding: '10px', borderRadius: '4px', overflowX: 'auto', maxHeight: '300px', overflowY: 'auto', border: '1px solid', fontFamily: 'Courier New, monospace', whiteSpace: 'pre', margin: '0' }); pre.innerHTML = escapeHtml(req.responseData); section.appendChild(titleContainer); section.appendChild(pre); details.appendChild(section); } item.appendChild(details); requestList.appendChild(item); }); } // 清空并重新渲染 app.innerHTML = ''; app.className = `app-container ${state.theme}`; app.appendChild(toolbar); app.appendChild(requestList); } // 添加样式 const style = document.createElement('style'); style.textContent = ` body { overflow: auto !important; height: auto !important; } .app-container { padding: 10px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-height: 100vh; box-sizing: border-box; } .toolbar { display: flex; align-items: center; justify-content: space-between; padding: 10px; margin-bottom: 15px; border-radius: 8px; border: 1px solid; } .toolbar-left { display: flex; align-items: center; gap: 10px; } .font-size-control { display: flex; align-items: center; gap: 8px; } .font-size-control button, .theme-toggle { background: #4ec9b0; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; } .theme-toggle { padding: 6px 12px; } .font-size-control button:hover, .theme-toggle:hover { background: #3fb89a; } .empty-state { padding: 20px; text-align: center; color: #999; } .requests-list { display: flex; flex-direction: column; gap: 15px; overflow: visible; } .request-item { padding: 15px; border-radius: 8px; border: 1px solid; } .request-header { display: flex; align-items: center; margin-bottom: 10px; gap: 10px; } .request-header .method { background: #4ec9b0; color: white; padding: 2px 8px; border-radius: 4px; font-weight: bold; white-space: nowrap; } .request-header .url { flex: 1; font-family: monospace; word-break: break-all; } .request-header .time { white-space: nowrap; } .section { margin-bottom: 15px; } .collapse-btn:hover { opacity: 0.7; transform: scale(1.1) rotate(0deg); } .request-details, .uuid-info { overflow: hidden; transition: max-height 0.3s ease-out, opacity 0.3s ease-out; opacity: 1; } `; document.head.appendChild(style); // 初始化 console.log('🔧 Panel.js 初始化开始...'); console.log('🔍 Chrome DevTools API 检查:', { hasChrome: typeof chrome !== 'undefined', hasDevtools: typeof chrome !== 'undefined' && !!chrome.devtools, hasNetwork: typeof chrome !== 'undefined' && !!chrome.devtools && !!chrome.devtools.network }); // 检查 DevTools API 是否可用并添加监听器 if (typeof chrome !== 'undefined' && chrome.devtools && chrome.devtools.network) { console.log('✅ DevTools Network API 可用'); // 添加监听器(必须在面板上下文中才能工作) try { chrome.devtools.network.onRequestFinished.addListener(processRequest); console.log('✅ 已添加网络请求监听器'); // 测试监听器是否工作 console.log('🔍 监听器测试:添加了一个监听器,等待请求...'); } catch (error) { console.error('❌ 添加监听器失败:', error); } } else { console.error('❌ DevTools Network API 不可用', { chrome: typeof chrome, devtools: typeof chrome !== 'undefined' ? typeof chrome.devtools : 'undefined', network: typeof chrome !== 'undefined' && chrome.devtools ? typeof chrome.devtools.network : 'undefined' }); } // 加载设置并渲染 loadSettings(); render(); console.log('✅ Panel.js 初始化完成');