resourceMonitor

页面资源监控 - 智能检测 JS/CSS 是否返回HTML错误页(基于内容分析),支持异常分级查看、自动刷新、日志归档,并智能忽略常见合法但非传统格式的资源(如CSS变量、Iconfont、Webpack/Vite打包JS、ICE资产清单等)。新增:请求超时记录 + showErrors() 默认显示 warning + 可配置多槽日志存储 + 捕获 ERR_CONNECTION_RESET 等网络层失败。

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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
// ==/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 /<!doctype|<html|<head|<body|<meta\s+|<title|<script\s+(?!type=['"]?module['"]?)|<link\s+rel=|<div\s+class=/.test(sample);
    }

    function looksLikeJS(content) {
        const trimmed = content.trim();
        if (!trimmed) return false;

        if (/window\._iconfont_svg_string_\d+\s*=\s*'<svg>/.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 });
    }
})();