Files
chromeEvent/panel.js
2026-01-29 12:03:28 +08:00

1379 lines
48 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
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 初始化完成');