// ==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)}
`}
${r.type.toUpperCase()} ${r.timestamp}
${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(); })();