// ==UserScript== // @name resourceMonitor // @namespace resourceMonitor // @version 1.6.0 // @description 页面资源监控 - 智能检测 JS/CSS 是否返回HTML错误页(基于内容分析),支持异常分级查看、自动刷新、日志归档,并智能忽略常见合法但非传统格式的资源(如CSS变量、Iconfont、Webpack/Vite打包JS、ICE资产清单等)。新增:请求超时记录 + showErrors() 默认显示 warning + 可配置多槽日志存储 + 捕获 ERR_CONNECTION_RESET 等网络层失败。 // @author mozkoe // @copyright 2026, mozkoe (https://github.com/mozkoe) // @match *://*/* // @icon https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org // @grant none // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/565200/resourceMonitor.user.js // @updateURL https://update.greasyfork.icu/scripts/565200/resourceMonitor.meta.js // ==/UserScript== (function () { 'use strict'; const DEFAULT_CONFIG = { enabled: false, refreshInterval: 3000, checkTimeout: 2500, maxRecordsPerSlot: 150, logSlots: 2, monitorJS: true, monitorCSS: true, }; const CONFIG_KEY = 'resourceMonitorConfig_v1_1'; const MAIN_LOG_KEY = 'resourceCheckLog_v1_1'; let CONFIG = { ...DEFAULT_CONFIG }; let isMonitoring = false; // =============== 内容类型辅助判断 =============== function looksLikeHTML(content) { const sample = content.trim().substring(0, 500).toLowerCase(); return //.test(trimmed)) { return true; } if (/^\s*window\.__[A-Z_]+_MANIFEST__\s*=\s*\{/.test(trimmed)) { return true; } if ( /^\s*!(?:async\s+)?(?:function|\(\s*\(\s*\)\s*=>|\(\s*function\b)/.test(trimmed) && (trimmed.includes('e.exports') || /\b\d+\s*:\s*function\s*\([^)]*\)\s*\{/.test(trimmed)) ) { return true; } if ( /^\s*!(?:async\s+)?(?:function|\(\s*function\b)/.test(trimmed) && (trimmed.includes('Object.defineProperty') || trimmed.includes('Object.create')) ) { return true; } const head = trimmed.substring(0, 300).replace(/\s+/g, ' '); if (/^(\s*\/\*|\s*\/\/|\s*var\s+|\s*let\s+|\s*const\s+|\s*function\s+|\s*import\s+|\s*export\s+|\s*\{|\s*\(|\s*"use strict")/.test(head)) { return true; } return false; } function looksLikeCSS(content) { const trimmed = content.trim(); if (!trimmed) return false; if (/:root\s*\{|--\w+\s*:/.test(trimmed)) { return true; } const sample = trimmed.substring(0, 300); return /[\w.#:\[][^{}]*\{[^{}]*\}|@media|@import|@keyframes|@charset|@font-face/.test(sample); } // =============== 配置管理 =============== function loadConfig() { try { const saved = localStorage.getItem(CONFIG_KEY); if (saved) { CONFIG = { ...DEFAULT_CONFIG, ...JSON.parse(saved) }; } else { CONFIG = { ...DEFAULT_CONFIG }; } if (CONFIG.logSlots < 1) CONFIG.logSlots = 1; } catch (e) { console.warn('[⚠️] 配置加载失败,使用默认配置'); CONFIG = { ...DEFAULT_CONFIG }; saveConfig(); } } function saveConfig() { try { localStorage.setItem(CONFIG_KEY, JSON.stringify(CONFIG)); } catch (e) { console.error('[💥] 保存配置失败:', e); } } // =============== 存储工具 =============== function getStorage(key) { try { return JSON.parse(localStorage.getItem(key) || '[]'); } catch (e) { console.warn(`[⚠️] 解析 ${key} 失败,已清除`); localStorage.removeItem(key); return []; } } function safeSetItem(key, data) { try { localStorage.setItem(key, JSON.stringify(data)); return true; } catch (e) { return e.name === 'QuotaExceededError'; } } // =============== 多槽日志存储系统 =============== function getLogSlotKey(index) { if (index === 0) return MAIN_LOG_KEY; return `${MAIN_LOG_KEY}_slot${index}`; } function getAllLogRecords() { let allRecords = []; for (let i = 0; i < CONFIG.logSlots; i++) { allRecords.push(...getStorage(getLogSlotKey(i))); } return allRecords; } function saveFullLog(logEntries) { const logRecord = { timestamp: Date.now(), timeStr: new Date().toLocaleString('zh-CN'), entries: logEntries }; let slots = []; for (let i = 0; i < CONFIG.logSlots; i++) { slots.push(getStorage(getLogSlotKey(i))); } slots[0].unshift(logRecord); for (let i = 0; i < CONFIG.logSlots; i++) { if (slots[i].length > CONFIG.maxRecordsPerSlot) { const overflow = slots[i].splice(CONFIG.maxRecordsPerSlot); if (i + 1 < CONFIG.logSlots && overflow.length > 0) { slots[i + 1] = [...overflow, ...slots[i + 1]]; } } } for (let i = 0; i < CONFIG.logSlots; i++) { if (slots[i].length > CONFIG.maxRecordsPerSlot) { slots[i] = slots[i].slice(0, CONFIG.maxRecordsPerSlot); } } let success = true; for (let i = 0; i < CONFIG.logSlots; i++) { const key = getLogSlotKey(i); if (!safeSetItem(key, slots[i])) { console.warn(`[🔄] 日志槽 ${i} (${key}) 写入失败`); success = false; localStorage.removeItem(key); } } if (!success) { console.warn('[⚠️] 多槽写入失败,回退到仅保存最新记录'); safeSetItem(MAIN_LOG_KEY, [logRecord]); } } // =============== 主检查逻辑(增强:捕获底层网络失败)=============== function runCheckWithOriginalStyle() { const startTime = Date.now(); const jsResources = CONFIG.monitorJS ? Array.from(document.querySelectorAll('script[src]')).map(el => el.src).filter(Boolean) : []; const cssResources = CONFIG.monitorCSS ? Array.from(document.querySelectorAll('link[rel="stylesheet"][href]')).map(el => el.href).filter(Boolean) : []; const logEntries = []; console.log(`\n🔍 开始检查页面资源加载情况 (${new Date().toLocaleTimeString()})`); console.log(`════════════════════════════════════════════════════════════════════════════`); jsResources.forEach(src => { console.log(`[•] 发现JS资源: ${src}`); logEntries.push({ type: 'js', action: 'discovered', url: src }); }); cssResources.forEach(href => { console.log(`[•] 发现CSS资源: ${href}`); logEntries.push({ type: 'css', action: 'discovered', url: href }); }); const allResources = [ ...jsResources.map(url => ({ url, expectedType: 'js' })), ...cssResources.map(url => ({ url, expectedType: 'css' })) ]; const initialEntryNames = new Set( performance.getEntriesByType('resource').map(r => r.name) ); const checkPromises = allResources.map(({ url, expectedType }) => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), CONFIG.checkTimeout); return fetch(url, { credentials: 'omit', signal: controller.signal }) .then(response => { clearTimeout(timeoutId); const contentType = response.headers.get('Content-Type') || ''; return response.text().then(content => ({ contentType, content })); }) .then(({ contentType, content }) => { const isHTMLByHeader = contentType.includes('text/html'); const isHTMLByContent = looksLikeHTML(content); let entry = { url, expectedType, contentType }; logEntries.push(entry); if (isHTMLByHeader || isHTMLByContent) { console.error(`[❌] ${url} → 错误: 返回HTML内容`); entry.result = 'html_error'; } else if (expectedType === 'js') { if (looksLikeJS(content)) { console.log(`[✅] ${url} → 正常: JS资源`); entry.result = 'js_ok'; } else { console.warn(`[⚠️] ${url} → 内容不像JS(但非HTML)`); entry.result = 'js_suspicious'; } } else if (expectedType === 'css') { if (looksLikeCSS(content)) { console.log(`[✅] ${url} → 正常: CSS资源`); entry.result = 'css_ok'; } else { console.warn(`[⚠️] ${url} → 内容不像CSS(但非HTML)`); entry.result = 'css_suspicious'; } } else { console.warn(`[⚠️] ${url} → 未知类型,但非HTML`); entry.result = 'unknown_non_html'; } }) .catch(error => { clearTimeout(timeoutId); let errorMessage = error.message || String(error); let resultType = 'fetch_error'; if (error.name === 'AbortError' || errorMessage.includes('abort')) { errorMessage = 'Request timeout'; resultType = 'timeout_error'; } console.error(`[❌] ${url} → ${resultType === 'timeout_error' ? '超时' : '请求失败'}: ${errorMessage}`); logEntries.push({ url, expectedType, error: errorMessage, result: resultType }); }); }); Promise.race([ Promise.allSettled(checkPromises), new Promise(r => setTimeout(r, CONFIG.checkTimeout + 1000)) ]).finally(() => { // ✅ 补全 fetch 未捕获的网络失败(如 ERR_CONNECTION_RESET) const currentEntries = performance.getEntriesByType('resource'); const monitoredUrls = new Set(allResources.map(r => r.url)); for (const entry of currentEntries) { if (!monitoredUrls.has(entry.name)) continue; if (initialEntryNames.has(entry.name)) continue; // 排除之前已存在的 // 跳过已被 fetch 成功处理的 if (logEntries.some(e => e.url === entry.name && e.result)) continue; // 判断是否为网络层失败 const isNetworkFailure = ( entry.transferSize === 0 && entry.encodedBodySize === 0 && entry.decodedBodySize === 0 && entry.duration > 0 && (entry.responseEnd === 0 || entry.responseStart === 0) ); if (isNetworkFailure) { const expectedType = jsResources.includes(entry.name) ? 'js' : cssResources.includes(entry.name) ? 'css' : 'unknown'; const errorMsg = `Network error (e.g., ERR_CONNECTION_RESET), transferSize=0`; console.error(`[❌] ${entry.name} → 网络层失败: ${errorMsg}`); logEntries.push({ url: entry.name, expectedType, error: errorMsg, result: 'network_error' }); } } saveFullLog(logEntries); if (isMonitoring) { setTimeout(() => { console.log(`\n⏱️ ${CONFIG.refreshInterval}ms 计时结束,正在刷新页面...`); window.location.reload(); }, Math.max(100, CONFIG.refreshInterval - (Date.now() - startTime))); } else { console.log(`\n⏹️ 监控已停止,页面将保持静止。`); } }); } // =============== 控制命令 =============== window.startMonitor = function (cmd) { if (cmd === 'Y' || cmd === 'y') { CONFIG.enabled = true; saveConfig(); isMonitoring = true; console.log('%c🚀 监控已启用!配置已保存。', 'color:#4CAF50;font-weight:bold'); runCheckWithOriginalStyle(); } else if (cmd === 'Q' || cmd === 'q') { CONFIG.enabled = false; saveConfig(); isMonitoring = false; console.log('%c⏹️ 监控已禁用。', 'color:#F44336;font-weight:bold'); } else { console.log('❓ 请输入 Y/y 启动,Q/q 退出'); } }; // =============== 分级错误查看(默认 warning)=============== window.showErrors = function (level = 'warning') { const validLevels = ['error', 'warning']; if (!validLevels.includes(level)) { console.error(`❌ showErrors() 参数无效。支持: ${validLevels.join(', ')}`); return; } const ERROR_TYPES = ['html_error', 'fetch_error', 'timeout_error', 'network_error']; // ✅ 包含 network_error const WARNING_TYPES = ['js_suspicious', 'css_suspicious', 'unknown_non_html']; const targetTypes = level === 'error' ? ERROR_TYPES : [...ERROR_TYPES, ...WARNING_TYPES]; const allRecords = getAllLogRecords(); if (allRecords.length === 0) { console.log('📭 无任何记录'); return; } let hasIssue = false; const title = level === 'warning' ? '🚨⚠️ 所有异常与可疑资源(按时间倒序):' : '🚨 仅严重错误资源(按时间倒序):'; console.log(`\n${title}`); console.log('='.repeat(60)); for (const record of allRecords.sort((a, b) => b.timestamp - a.timestamp)) { const issuesInRecord = record.entries.filter(e => e.result && targetTypes.includes(e.result) ); if (issuesInRecord.length > 0) { hasIssue = true; console.group(`🕒 ${record.timeStr}`); issuesInRecord.forEach(e => { const isWarning = WARNING_TYPES.includes(e.result); const prefix = isWarning ? '[⚠️]' : '[❌]'; const style = isWarning ? 'color:#FF9800;font-weight:bold' : 'color:#F44336;font-weight:bold'; switch (e.result) { case 'html_error': console.log(`%c${prefix} ${e.url} → 返回HTML内容`, style); break; case 'fetch_error': console.log(`%c${prefix} ${e.url} → 请求失败: ${e.error}`, style); break; case 'timeout_error': console.log(`%c${prefix} ${e.url} → 请求超时`, style); break; case 'network_error': // ✅ 新增 console.log(`%c${prefix} ${e.url} → 网络层失败(如连接重置)`, style); break; case 'js_suspicious': console.log(`%c${prefix} ${e.url} → 内容不像JS(但非HTML)`, style); break; case 'css_suspicious': console.log(`%c${prefix} ${e.url} → 内容不像CSS(但非HTML)`, style); break; case 'unknown_non_html': console.log(`%c${prefix} ${e.url} → 未知非HTML类型`, style); break; } }); console.groupEnd(); } } if (!hasIssue) { const msg = level === 'warning' ? '✅ 无异常或可疑资源' : '✅ 暂无严重错误资源'; console.log(msg); } console.log('='.repeat(60)); }; // =============== 调试与维护 =============== window.jsGetConfig = () => { console.log('🔧 当前配置:', CONFIG); return CONFIG; }; window.jsSetConfig = (newConfig) => { if (typeof newConfig !== 'object' || newConfig === null) { console.error('❌ 配置必须是对象'); return; } CONFIG = { ...DEFAULT_CONFIG, ...CONFIG, ...newConfig }; if (CONFIG.logSlots < 1) CONFIG.logSlots = 1; saveConfig(); console.log('✅ 配置已更新:', CONFIG); }; window.jsCheckHistory = function (n = 10) { let allRecords = getAllLogRecords() .sort((a, b) => b.timestamp - a.timestamp) .slice(0, n); if (allRecords.length === 0) { console.log('📭 无历史记录'); return; } allRecords.forEach((record, idx) => { console.groupCollapsed(`📊 记录 #${idx + 1} | ${record.timeStr}`); console.log(`\n🔍 检查时间: ${record.timeStr}`); console.log(`════════════════════════════════════════════════════════════════════════════`); record.entries.forEach(e => { if (e.action === 'discovered') { const prefix = e.type === 'js' ? 'JS' : 'CSS'; console.log(`[•] 发现${prefix}资源: ${e.url}`); } else if (e.result) { switch (e.result) { case 'html_error': console.error(`[❌] ${e.url} → 返回HTML内容`); break; case 'js_ok': console.log(`[✅] ${e.url} → 正常: JS资源`); break; case 'css_ok': console.log(`[✅] ${e.url} → 正常: CSS资源`); break; case 'js_suspicious': console.warn(`[⚠️] ${e.url} → 内容不像JS`); break; case 'css_suspicious': console.warn(`[⚠️] ${e.url} → 内容不像CSS`); break; case 'unknown_non_html': console.warn(`[⚠️] ${e.url} → 未知非HTML类型`); break; case 'fetch_error': console.error(`[❌] ${e.url} → 请求失败: ${e.error}`); break; case 'timeout_error': console.error(`[❌] ${e.url} → 请求超时: ${e.error}`); break; case 'network_error': // ✅ 新增 console.error(`[❌] ${e.url} → 网络层失败: ${e.error}`); break; } } }); console.groupEnd(); }); }; // =============== 存储清理命令 =============== window.clearMonitorStorage = function () { try { localStorage.removeItem(CONFIG_KEY); for (let i = 0; i <= 10; i++) { localStorage.removeItem(getLogSlotKey(i)); } console.log('%c✅ 已成功清除监控脚本的所有本地存储数据', 'color:#4CAF50;font-weight:bold'); console.log(' - 配置已重置为默认值'); console.log(' - 所有历史日志已删除'); } catch (e) { console.error('[💥] 清除存储时发生错误:', e); } }; // =============== 帮助命令 =============== window.monitorHelp = function () { console.log('%c🛠️ 页面资源监控 v1.6.0(多槽日志 + 网络失败捕获)已加载', 'color:#9C27B0;font-weight:bold;font-size:14px;'); console.log('──────────────────────────────────────'); console.log('startMonitor("Y") → 启用自动监控并保存配置'); console.log('startMonitor("q") → 禁用自动监控'); console.log('showErrors([level]) → 查看异常资源(默认 "warning",可选 "error")'); console.log('jsGetConfig() → 查看当前配置'); console.log('jsSetConfig({...}) → 动态更新配置(支持 logSlots, maxRecordsPerSlot 等)'); console.table(DEFAULT_CONFIG); console.log('jsCheckHistory(n) → 查看最近 n 次检查详情'); console.log('clearMonitorStorage() → 清除所有监控相关本地存储(配置+日志)'); console.log('monitorHelp() → 重新显示本帮助信息'); console.log('──────────────────────────────────────'); console.log('💡 提示:监控默认关闭,需手动启用才生效。'); }; // =============== 初始化 =============== function init() { loadConfig(); isMonitoring = CONFIG.enabled; console.log('%cℹ️ 页面资源监控 v1.6.0(多槽日志 + 网络失败捕获)已加载', 'color:#2196F3;font-weight:bold'); if (CONFIG.enabled) { console.log('🔁 检测到已启用监控,自动启动中...'); runCheckWithOriginalStyle(); } else { console.log('👉 在控制台输入以下命令开始使用:'); console.log(''); console.log(' startMonitor("Y") → 启用监控'); console.log(' startMonitor("q") → 关闭监控'); console.log(' showErrors() → 查看所有异常与可疑资源(默认)'); console.log(' showErrors("error") → 仅查看严重错误'); console.log(' jsSetConfig({ logSlots: 3, maxRecordsPerSlot: 200 }) → 扩展日志容量'); console.log(' clearMonitorStorage() → 清除所有监控相关本地存储(配置+日志)'); console.log(''); console.log(' monitorHelp() → 查看完整命令帮助'); } } if (document.readyState === 'complete') { init(); } else { window.addEventListener('load', init, { once: true }); } })();