// ==UserScript==
// @name 加载日记 - 悦览专版
// @namespace yuelan-resource-diary
// @version 2.5.2
// @description 利用AI模仿并生成M浏览器样式的网页资源加载日记
// @author 一程丶
// @match *://*/*
// @grant none
// @run-at document-end
// @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBdPSJNMTIgMkM2LjQ4IDIgMiA2LjQ4IDIgMTJTMi42NDggMjIgMTIgMjJTMjIgMTcuNTIgMjIgMTJTMTcuNTIgMiAxMiAyWk0xNyAxM0gxM1Y3SDExVjEzSDdWMTVIMTFWjFIMTNWMTVIMTdaIiBmaWxsPSIjNDI4NWY0Ii8+PC9zdmc+
// ==/UserScript==
(function() {
'use strict';
const ResourceDiary = {
resources: [],
filteredResources: [],
currentFilter: 'all',
searchKeyword: '',
isInitialized: false,
pendingUpdate: null,
lastRenderTime: 0,
// 资源类型图标映射
previewIcons: {
image: '🖼️',
script: '📜',
media: '🎬',
video: '🎬',
audio: '🎵',
stylesheet: '🎨',
font: '🔤',
XHR: '📡',
fetch: '📡',
other: '📄'
},
// 获取资源类型
getResourceType(url, contentType) {
const ext = url.split('.').pop().split('?')[0].toLowerCase();
const mime = contentType || '';
if (mime.includes('script') || ext === 'js' || ext === 'mjs') return 'script';
if (mime.includes('css') || ext === 'css') return 'stylesheet';
if (mime.includes('image') || ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'ico', 'bmp'].includes(ext)) return 'image';
if (mime.includes('font') || ['woff', 'woff2', 'ttf', 'otf', 'eot'].includes(ext)) return 'font';
if (mime.includes('video') || ['mp4', 'webm', 'ogg', 'mov', 'avi'].includes(ext)) return 'media';
if (mime.includes('audio') || ['mp3', 'wav', 'flac', 'aac'].includes(ext)) return 'media';
if (mime.includes('json') || ext === 'json') return 'XHR';
if (mime.includes('xml') || ext === 'xml') return 'XHR';
return 'other';
},
// 获取预览图URL
getPreviewUrl(resource) {
try {
const urlObj = new URL(resource.url);
if (resource.type === 'image') {
return resource.url;
}
return `${urlObj.origin}/favicon.ico`;
} catch (e) {
return '';
}
},
// 获取备用图标
getFallbackIcon(type) {
return this.previewIcons[type] || this.previewIcons.other;
},
// 初始化入口
init() {
if (this.isInitialized) {
console.log('[加载日记] 已初始化,跳过');
return;
}
console.log('[加载日记] 开始初始化');
try {
// 步骤1:立即注入按钮样式和创建按钮
this.injectButtonStyles();
this.createFloatingButton();
// 步骤2:等待DOM加载完成后创建主界面
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.createMainUI();
this.setupResourceCapture();
this.setupSPASupport();
});
} else {
this.createMainUI();
this.setupResourceCapture();
this.setupSPASupport();
}
this.isInitialized = true;
console.log('[加载日记] 初始化完成');
} catch (e) {
console.error('[加载日记] 初始化失败:', e);
// 5秒后重试
setTimeout(() => {
this.isInitialized = false;
this.init();
}, 5000);
}
},
// 注入按钮样式(最优先)
injectButtonStyles() {
// 移除旧样式
const oldStyle = document.getElementById('rd-button-styles');
if (oldStyle) oldStyle.remove();
const style = document.createElement('style');
style.id = 'rd-button-styles';
// 使用!important确保优先级最高
style.textContent = `
#rd-floating-btn {
position: fixed !important;
bottom: 100px !important;
right: 16px !important;
width: 32px !important;
height: 32px !important;
background: #4285f4 !important;
border-radius: 12px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 16px !important;
color: white !important;
cursor: pointer !important;
z-index: 999999 !important;
box-shadow: 0 2px 10px rgba(0,0,0,0.2) !important;
visibility: visible !important;
opacity: 1 !important;
}
`;
// 插入到head最前面
const head = document.head;
if (head) {
head.insertBefore(style, head.firstChild);
console.log('[加载日记] 按钮样式已注入');
} else {
console.warn('[加载日记] 未找到head元素,延迟注入样式');
setTimeout(() => this.injectButtonStyles(), 100);
}
},
// 创建浮动按钮
createFloatingButton() {
// 移除旧按钮
const oldBtn = document.getElementById('rd-floating-btn');
if (oldBtn) oldBtn.remove();
const btn = document.createElement('div');
btn.id = 'rd-floating-btn';
btn.title = '加载日记';
btn.innerHTML = '📊';
btn.onclick = () => this.togglePanel();
// 立即添加到body
const addButton = () => {
if (document.body) {
document.body.appendChild(btn);
console.log('[加载日记] 浮动按钮已添加到body');
// 强制显示
setTimeout(() => {
btn.style.visibility = 'visible';
btn.style.opacity = '1';
}, 10);
} else {
console.warn('[加载日记] body不存在,等待100ms后重试');
setTimeout(addButton, 100);
}
};
addButton();
},
// 创建主UI
createMainUI() {
// 移除旧容器
const oldContainer = document.getElementById('resource-diary-container');
if (oldContainer) oldContainer.remove();
const container = document.createElement('div');
container.id = 'resource-diary-container';
// 使用innerHTML一次性创建所有UI
container.innerHTML = `
所有: 0
媒体: 0
图片: 0
脚本: 0
其他: 0
已过滤: 0
`;
document.body.appendChild(container);
this.loadMainStyles();
this.bindEvents();
console.log('[加载日记] 主UI已创建');
},
// 加载主界面样式
loadMainStyles() {
const oldStyle = document.getElementById('rd-main-styles');
if (oldStyle) oldStyle.remove();
const style = document.createElement('style');
style.id = 'rd-main-styles';
style.textContent = `
#rd-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 700px;
height: 80vh;
background: #fff;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
z-index: 999998;
display: none;
flex-direction: column;
font-family: Arial, sans-serif;
}
#rd-panel.active { display: flex; }
.rd-header { padding: 15px; background: #4285f4; color: white; border-radius: 12px 12px 0 0; display: flex; justify-content: space-between; align-items: center; }
.rd-header h3 { margin: 0; font-size: 16px; }
.rd-controls button { background: none; border: none; color: white; font-size: 18px; cursor: pointer; margin-left: 10px; }
.rd-stats { padding: 10px 15px; background: #f5f5f5; display: flex; gap: 10px; font-size: 12px; color: #666; overflow-x: auto; }
.rd-stats span { white-space: nowrap; }
.rd-filters { padding: 10px 15px; border-bottom: 1px solid #e0e0e0; display: flex; gap: 8px; }
.rd-filter {
padding: 6px 12px;
border: 1px solid #ddd;
background: #fff;
border-radius: 15px;
cursor: pointer;
font-size: 12px;
color: #000;
}
.rd-filter.active {
background: #4285f4;
color: #000;
border-color: #4285f4;
}
.rd-search { padding: 10px 15px; border-bottom: 1px solid #e0e0e0; }
#rd-search { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
.rd-list { flex: 1; overflow-y: auto; padding: 0 15px 15px; }
.rd-entry {
display: flex;
gap: 10px;
padding: 10px;
margin-top: 10px;
background: #f9f9f9;
border-radius: 6px;
font-size: 12px;
border-left: 3px solid #4285f4;
}
.rd-entry-thumb {
width: 32px;
height: 32px;
flex-shrink: 0;
border-radius: 6px;
object-fit: cover;
background: #e8f0fe;
}
.rd-entry-fallback {
width: 32px;
height: 32px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
background: #e8f0fe;
border-radius: 6px;
}
.rd-entry-content {
flex: 1;
min-width: 0;
}
.rd-entry-header { display: flex; justify-content: space-between; margin-bottom: 5px; }
.rd-entry-type {
background: #4285f4;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
}
.rd-entry-time { color: #999; }
.rd-entry-url {
word-break: break-all;
color: #333;
margin-bottom: 5px;
font-family: monospace;
}
.rd-entry-details { display: flex; gap: 10px; font-size: 11px; color: #666; }
.rd-empty { text-align: center; color: #999; padding: 40px 20px; }
`;
document.head.appendChild(style);
},
// SPA支持
setupSPASupport() {
let lastUrl = location.href;
const checkUrlChange = () => {
const currentUrl = location.href;
if (currentUrl !== lastUrl) {
console.log('[加载日记] SPA导航:', lastUrl, '->', currentUrl);
lastUrl = currentUrl;
this.resources = [];
this.filteredResources = [];
if (this.isInitialized) {
this.scheduleUpdate();
}
}
};
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = (...args) => {
originalPushState.apply(history, args);
setTimeout(checkUrlChange, 100);
};
history.replaceState = (...args) => {
originalReplaceState.apply(history, args);
setTimeout(checkUrlChange, 100);
};
window.addEventListener('popstate', () => setTimeout(checkUrlChange, 100));
document.addEventListener('click', () => setTimeout(checkUrlChange, 500), true);
},
// 资源捕获
setupResourceCapture() {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
const that = this;
XMLHttpRequest.prototype.open = function(method, url) {
this._rd_url = url;
this._rd_method = method;
this._rd_startTime = Date.now();
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
this.addEventListener('load', function() {
that.addResource({
url: this._rd_url,
method: this._rd_method,
type: 'XHR',
status: this.status,
size: this.responseText ? this.responseText.length : 0,
duration: Date.now() - this._rd_startTime
});
});
return originalSend.apply(this, arguments);
};
const originalFetch = window.fetch;
window.fetch = function(input, init) {
const url = typeof input === 'string' ? input : input.url;
const method = (init && init.method) || 'GET';
const startTime = Date.now();
return originalFetch.apply(this, arguments).then(response => {
response.clone().text().then(text => {
that.addResource({
url: url,
method: method,
type: 'fetch',
status: response.status,
size: text.length,
duration: Date.now() - startTime
});
});
return response;
});
};
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.entryType === 'resource') {
that.addResource({
url: entry.name,
type: that.getResourceType(entry.name, entry.responseContentType),
status: 200,
size: entry.transferSize || 0,
duration: entry.duration || 0
});
}
});
});
observer.observe({ entryTypes: ['resource'] });
},
// 添加资源(去重)
addResource(resource) {
const fullResource = {
id: Date.now() + Math.random(),
url: resource.url,
type: resource.type || this.getResourceType(resource.url, ''),
status: resource.status || 0,
size: resource.size || 0,
duration: resource.duration || 0,
method: resource.method || 'GET',
timestamp: new Date().toLocaleTimeString('zh-CN'),
date: new Date().toLocaleDateString('zh-CN')
};
// 去重
const exists = this.resources.some(r =>
r.url === fullResource.url &&
r.type === fullResource.type &&
r.status === fullResource.status &&
Math.abs(r.duration - fullResource.duration) < 1000
);
if (exists) return;
this.resources.unshift(fullResource);
if (this.resources.length > 500) {
this.resources = this.resources.slice(0, 500);
}
this.filterResources();
this.scheduleUpdate();
},
// 防抖更新
scheduleUpdate() {
if (this.pendingUpdate) clearTimeout(this.pendingUpdate);
this.pendingUpdate = setTimeout(() => {
const now = Date.now();
if (now - this.lastRenderTime > 100) {
this.lastRenderTime = now;
this.updateUI();
this.updateStats();
}
}, 100);
},
// 绑定事件
bindEvents() {
const btn = document.getElementById('rd-floating-btn');
if (btn) btn.onclick = () => this.togglePanel();
const closeBtn = document.getElementById('rd-close');
if (closeBtn) closeBtn.onclick = () => this.hidePanel();
const exportBtn = document.getElementById('rd-export');
if (exportBtn) exportBtn.onclick = () => this.exportDiaries();
const clearBtn = document.getElementById('rd-clear');
if (clearBtn) clearBtn.onclick = () => this.clearAllDiaries();
const searchInput = document.getElementById('rd-search');
if (searchInput) searchInput.onkeyup = () => this.searchDiaries();
// 过滤按钮事件
document.querySelectorAll('.rd-filter').forEach(btn => {
btn.onclick = () => {
document.querySelectorAll('.rd-filter').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
this.currentFilter = btn.dataset.filter;
this.scheduleUpdate();
};
});
},
// 其余方法(filterResources, searchDiaries等)...
filterResources() {
this.filteredResources = this.resources.filter(r => {
const typeMatch = this.currentFilter === 'all' ||
(this.currentFilter === 'media' && ['media', 'video', 'audio'].includes(r.type)) ||
r.type === this.currentFilter;
const searchMatch = !this.searchKeyword ||
r.url.toLowerCase().includes(this.searchKeyword.toLowerCase());
return typeMatch && searchMatch;
});
},
searchDiaries() {
this.searchKeyword = document.getElementById('rd-search').value;
this.filterResources();
this.scheduleUpdate();
},
updateUI() {
this.filterResources();
const list = document.getElementById('rd-list');
if (!list) return;
if (this.filteredResources.length === 0) {
list.innerHTML = '暂无资源记录
';
return;
}
list.innerHTML = this.filteredResources.map(r => {
const previewUrl = this.getPreviewUrl(r);
return `
${previewUrl ? `
${this.getFallbackIcon(r.type)}
` : `
${this.getFallbackIcon(r.type)}
`}
${r.url}
状态: ${r.status || '-'}
大小: ${r.size > 0 ? (r.size / 1024).toFixed(2) + 'KB' : '-'}
耗时: ${r.duration ? r.duration.toFixed(2) + 'ms' : '-'}
`;
}).join('');
},
getFallbackIcon(type) {
const icons = {
image: '🖼️',
script: '📜',
media: '🎬',
video: '🎬',
audio: '🎵',
stylesheet: '🎨',
font: '🔤',
XHR: '📡',
fetch: '📡',
other: '📄'
};
return icons[type] || '📄';
},
updateStats() {
const stats = {
all: this.resources.length,
media: this.resources.filter(r => ['media', 'video', 'audio'].includes(r.type)).length,
image: this.resources.filter(r => r.type === 'image').length,
script: this.resources.filter(r => ['script', 'javascript'].includes(r.type)).length,
other: this.resources.filter(r => !['media', 'video', 'audio', 'image', 'script', 'javascript'].includes(r.type)).length,
filtered: this.resources.filter(r => r.url.includes('filter')).length
};
const allEl = document.getElementById('rd-all');
if (allEl) allEl.textContent = `所有: ${stats.all}`;
const mediaEl = document.getElementById('rd-media');
if (mediaEl) mediaEl.textContent = `媒体: ${stats.media}`;
const imageEl = document.getElementById('rd-image');
if (imageEl) imageEl.textContent = `图片: ${stats.image}`;
const scriptEl = document.getElementById('rd-script');
if (scriptEl) scriptEl.textContent = `脚本: ${stats.script}`;
const otherEl = document.getElementById('rd-other');
if (otherEl) otherEl.textContent = `其他: ${stats.other}`;
const filteredEl = document.getElementById('rd-filtered');
if (filteredEl) filteredEl.textContent = `已过滤: ${stats.filtered}`;
},
exportDiaries() {
let content = `加载日记 - 资源加载报告\n`;
content += `页面: ${window.location.href}\n`;
content += `导出时间: ${new Date().toLocaleString('zh-CN')}\n\n`;
content += `=${'='.repeat(80)}\n\n`;
this.resources.forEach(r => {
content += `[${r.type.toUpperCase()}] ${r.url}\n`;
content += `时间: ${r.date} ${r.timestamp} | 状态: ${r.status} | 大小: ${r.size > 0 ? (r.size/1024).toFixed(2)+'KB' : '-'} | 耗时: ${r.duration ? r.duration.toFixed(2)+'ms' : '-'}\n`;
content += `-`.repeat(80) + '\n\n';
});
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `加载日记_${window.location.hostname}_${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
},
clearAllDiaries() {
if (confirm('清空所有资源记录?')) {
this.resources = [];
this.filteredResources = [];
this.scheduleUpdate();
}
},
togglePanel() {
const panel = document.getElementById('rd-panel');
if (!panel) return;
const isActive = panel.style.display === 'flex';
panel.style.display = isActive ? 'none' : 'flex';
if (!isActive) {
this.scheduleUpdate();
}
},
hidePanel() {
const panel = document.getElementById('rd-panel');
if (panel) panel.style.display = 'none';
}
};
// 立即执行
ResourceDiary.init();
})();