/**
* 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 初始化完成');