1379 lines
48 KiB
JavaScript
1379 lines
48 KiB
JavaScript
/**
|
||
* 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 = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||
</svg>`;
|
||
|
||
const checkIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<polyline points="20 6 9 17 4 12"></polyline>
|
||
</svg>`;
|
||
|
||
button.innerHTML = `<span class="copy-icon">${copyIcon}</span><span class="copy-text">${label}</span>`;
|
||
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 = `<span class="copy-icon">${checkIcon}</span><span class="copy-text">已复制</span>`;
|
||
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 = `<span class="copy-icon">${copyIcon}</span><span class="copy-text">${label}</span>`;
|
||
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, '<')
|
||
.replace(/>/g, '>');
|
||
|
||
highlighted = highlighted.replace(/([{}[\]])/g, `<span style="color: ${colors.punctuation}">$1</span>`);
|
||
highlighted = highlighted.replace(/(:\s*)("(?:[^"\\]|\\.)*")/g, `$1<span style="color: ${colors.string}">$2</span>`);
|
||
highlighted = highlighted.replace(/(\[\s*)("(?:[^"\\]|\\.)*")/g, `$1<span style="color: ${colors.string}">$2</span>`);
|
||
highlighted = highlighted.replace(/(,\s*)("(?:[^"\\]|\\.)*")/g, (match, p1, p2) => {
|
||
if (match.includes('color')) return match;
|
||
return p1 + `<span style="color: ${colors.string}">${p2}</span>`;
|
||
});
|
||
highlighted = highlighted.replace(/(:\s*)(-?\d+\.?\d*(?:[eE][+-]?\d+)?)(?=\s*[,}\]]|$)/g, (match, p1, p2) => {
|
||
if (match.includes('<span')) return match;
|
||
return p1 + `<span style="color: ${colors.number}">${p2}</span>`;
|
||
});
|
||
highlighted = highlighted.replace(/(\[[^\]]*?)(-?\d+\.?\d*(?:[eE][+-]?\d+)?)(?=\s*[,}\]]|$)/g, (match, p1, p2) => {
|
||
if (match.includes('<span')) return match;
|
||
return p1 + `<span style="color: ${colors.number}">${p2}</span>`;
|
||
});
|
||
highlighted = highlighted.replace(/(,\s*)(-?\d+\.?\d*(?:[eE][+-]?\d+)?)(?=\s*[,}\]]|$)/g, (match, p1, p2) => {
|
||
if (match.includes('<span')) return match;
|
||
return p1 + `<span style="color: ${colors.number}">${p2}</span>`;
|
||
});
|
||
highlighted = highlighted.replace(/(:\s*)(true|false|null)\b/g, (match, p1, p2) => {
|
||
if (match.includes('<span')) return match;
|
||
const color = p2 === 'null' ? colors.null : colors.boolean;
|
||
return p1 + `<span style="color: ${color}">${p2}</span>`;
|
||
});
|
||
highlighted = highlighted.replace(/("(?:[^"\\]|\\.)*")\s*:/g, (match, p1) => {
|
||
if (match.includes('<span')) return match;
|
||
return `<span style="color: ${colors.key}">${p1}</span><span style="color: ${colors.punctuation}">:</span>`;
|
||
});
|
||
|
||
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 = `<span>${themeIcon}</span> 切换主题`;
|
||
themeBtn.onclick = toggleTheme;
|
||
toolbar.appendChild(themeBtn);
|
||
|
||
const clearBtn = document.createElement('button');
|
||
clearBtn.className = 'clear-btn';
|
||
clearBtn.innerHTML = `<span>🗑️</span> 清除`;
|
||
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 = `<span style="color: #68d391;">📍 UUID: </span><code style="color: #4ec9b0;">${escapeHtml(req.uuid)}</code>`;
|
||
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 初始化完成');
|