Greasy Fork

请求监听过滤器

监听所有HTTP和HTTPS请求

// ==UserScript==
// @name         请求监听过滤器
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  监听所有HTTP和HTTPS请求
// @author       晚风
// @match        http://*/*
// @match        https://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-start
// @inject-into  page
// @license MIT
// ==/UserScript==

// 立即执行重定向检查,在任何其他代码执行之前
(function() {
    if (window.location.href.includes('cryptbox.sankuai.com/file/')) {
        // 阻止页面继续加载
        window.stop();

        const currentUrl = window.location.href;
        const newUrl = currentUrl.replace(
            /^https:\/\/cryptbox\.sankuai\.com\/file\/(.+)$/,
            'https://distribute-platform-pub.sankuai.com/distribute/download/v1/$1'
        );

        if (currentUrl !== newUrl) {
            // 创建一个隐藏的 iframe 来触发下载
            const iframe = document.createElement('iframe');
            iframe.style.display = 'none';
            iframe.src = newUrl;
            document.body.appendChild(iframe);

            // 等待一段时间后关闭页面
            setTimeout(() => {
                window.close();
            }, 1000); // 给予1秒时间让下载开始

            return;
        }
    }
})();

class RequestMonitor {
    constructor() {
        if (window._RequestMonitor) {
            return window._RequestMonitor;
        }
        window._RequestMonitor = this;

        // 初始化状态
        this.logs = [];
        this.filterKeyword = '';
        this.initialized = false;
        this.requestCount = {
            total: 0,
            xhr: 0,
            fetch: 0
        };

        // 重定向规则
        this.redirectRules = [
            {
                pattern: /^https:\/\/cryptbox\.sankuai\.com\/file\/(.+)$/,
                replacement: 'https://distribute-platform-pub.sankuai.com/distribute/download/v1/$1'
            }
        ];

        // SVG 图标
        this.icons = {
            copy: `<svg viewBox="64 64 896 896" width="14" height="14" fill="currentColor">
                <path d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z" />
            </svg>`,
            success: `<svg viewBox="64 64 896 896" width="14" height="14" fill="currentColor">
                <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
            </svg>`
        };

        // 设置默认状态为折叠
        this.isPanelExpanded = false;

        this.init();
    }

    init() {
        if (this.initialized) return;
        this.setupRequestInterceptors();
        this.createUI();
        this.initialized = true;
    }

    // UI 相关方法
    createUI() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => this.renderUI(), { once: true });
        } else {
            this.renderUI();
        }
    }

    renderUI() {
        // 创建主容器
        const container = this.createMainContainer();

        // 创建头部
        const header = this.createHeader();
        container.appendChild(header);

        // 创建统计栏
        const stats = this.createStatsBar();
        container.appendChild(stats);

        // 创建请求列表
        const content = this.createRequestList();
        container.appendChild(content);

        // 创建详情面板
        this.createDetailPanel();

        // 添加到页面
        document.body.appendChild(container);
    }

    // 创建主容器
    createMainContainer() {
        const container = document.createElement('div');
        container.id = 'requestMonitor';
        container.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            width: ${this.isPanelExpanded ? '400px' : '40px'};
            height: ${this.isPanelExpanded ? '90vh' : '40px'};
            background: rgba(255, 255, 255, 0.95);
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            z-index: 999999;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(0, 0, 0, 0.1);
            transition: all 0.3s ease;
            overflow: hidden;
            transform-origin: right center;
            display: ${this.isPanelExpanded ? 'flex' : 'block'};
            flex-direction: column;
        `;

        // 创建折叠按钮
        const toggleButton = document.createElement('div');
        toggleButton.style.cssText = `
            position: ${this.isPanelExpanded ? 'absolute' : 'relative'};
            top: ${this.isPanelExpanded ? '12px' : '0'};
            right: ${this.isPanelExpanded ? '12px' : '0'};
            width: ${this.isPanelExpanded ? '24px' : '40px'};
            height: ${this.isPanelExpanded ? '24px' : '40px'};
            background: ${this.isPanelExpanded ? 'rgba(24, 144, 255, 0.1)' : '#1890ff'};
            border-radius: ${this.isPanelExpanded ? '4px' : '8px'};
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            color: ${this.isPanelExpanded ? '#1890ff' : 'white'};
            font-size: 18px;
            transition: all 0.3s;
            z-index: 1;
        `;
        toggleButton.innerHTML = this.isPanelExpanded ? '−' : '+';

        toggleButton.onmouseover = () => {
            toggleButton.style.background = this.isPanelExpanded ?
                'rgba(24, 144, 255, 0.2)' :
                'rgba(24, 144, 255, 0.8)';
        };

        toggleButton.onmouseout = () => {
            toggleButton.style.background = this.isPanelExpanded ?
                'rgba(24, 144, 255, 0.1)' :
                '#1890ff';
        };

        toggleButton.onclick = (e) => {
            e.stopPropagation();
            this.togglePanel(!this.isPanelExpanded);
        };

        container.appendChild(toggleButton);
        return container;
    }

    // 创建头部
    createHeader() {
        const header = document.createElement('div');
        header.style.cssText = `
            padding: 12px 16px;
            padding-right: 48px;
            background: rgba(245, 245, 245, 0.95);
            border-radius: 8px 8px 0 0;
            display: flex;
            justify-content: space-between;
            align-items: center;
            cursor: move;
            user-select: none;
            border-bottom: 1px solid rgba(0, 0, 0, 0.05);
        `;

        // 标题
        const title = document.createElement('span');
        title.textContent = `${document.title} - 请求监听器`;
        title.style.cssText = `
            font-weight: 600;
            color: #333;
            font-size: 14px;
        `;

        // 搜索
        const filterInput = document.createElement('input');
        filterInput.placeholder = '搜索请求...';
        filterInput.style.cssText = `
            margin-left: 12px;
            padding: 6px 12px;
            border: 1px solid rgba(0, 0, 0, 0.1);
            border-radius: 4px;
            flex-grow: 1;
            font-size: 12px;
            outline: none;
            transition: all 0.3s;
            background: rgba(255, 255, 255, 0.9);
        `;

        // 添加拖拽功能
        this.setupDrag(header);

        // 添加事件监听
        filterInput.oninput = () => {
            this.filterKeyword = filterInput.value.toLowerCase();
            this.updateList();
        };

        header.appendChild(title);
        header.appendChild(filterInput);
        return header;
    }

    // 创建统计栏
    createStatsBar() {
        const stats = document.createElement('div');
        stats.id = 'requestStats';
        stats.style.cssText = `
            padding: 8px 16px;
            background: rgba(250, 250, 250, 0.95);
            border-bottom: 1px solid rgba(0, 0, 0, 0.05);
            font-size: 12px;
            color: #666;
            display: flex;
            gap: 16px;
        `;
        stats.innerHTML = `
            <span>总请求: <b id="totalCount">0</b></span>
            <span>XHR: <b id="xhrCount">0</b></span>
            <span>Fetch: <b id="fetchCount">0</b></span>
        `;
        return stats;
    }

    // 创建请求列表
    createRequestList() {
        const content = document.createElement('div');
        content.style.cssText = `
            flex: 1;
            overflow-y: auto;
            display: ${this.isPanelExpanded ? 'block' : 'none'};
            background: rgba(255, 255, 255, 0.95);
            padding: 0;
        `;

        const requestList = document.createElement('div');
        requestList.id = 'requestList';
        content.appendChild(requestList);

        return content;
    }

    // 创建详情面板
    createDetailPanel() {
        const detailPanel = document.createElement('div');
        detailPanel.id = 'requestDetail';
        detailPanel.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 800px;
            max-height: 80vh;
            background: rgba(255, 255, 255, 0.98);
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            z-index: 1000000;
            display: none;
            overflow: hidden;
            border: 1px solid rgba(0, 0, 0, 0.1);
            backdrop-filter: blur(10px);
        `;

        const header = document.createElement('div');
        header.style.cssText = `
            padding: 16px;
            background: rgba(245, 245, 245, 0.95);
            border-bottom: 1px solid rgba(0, 0, 0, 0.05);
            display: flex;
            justify-content: space-between;
            align-items: center;
        `;

        const title = document.createElement('h3');
        title.style.cssText = `
            margin: 0;
            font-size: 16px;
            color: #333;
        `;
        title.textContent = '请求详情';

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '×';
        closeBtn.style.cssText = `
            background: none;
            border: none;
            font-size: 24px;
            color: #999;
            cursor: pointer;
            padding: 0 8px;
            line-height: 1;
        `;

        const content = document.createElement('div');
        content.style.cssText = `
            padding: 16px;
            overflow-y: auto;
            max-height: calc(80vh - 60px);
        `;

        closeBtn.onclick = () => detailPanel.style.display = 'none';

        header.appendChild(title);
        header.appendChild(closeBtn);
        detailPanel.appendChild(header);
        detailPanel.appendChild(content);
        document.body.appendChild(detailPanel);
    }

    // 设置拖拽功能
    setupDrag(header) {
        let isDragging = false;
        let currentX;
        let currentY;
        let initialX;
        let initialY;

        header.onmousedown = (e) => {
            isDragging = true;
            const container = document.getElementById('requestMonitor');
            const rect = container.getBoundingClientRect();
            initialX = e.clientX - rect.left;
            initialY = e.clientY - rect.top;
        };

        document.onmousemove = (e) => {
            if (isDragging) {
                const container = document.getElementById('requestMonitor');
                const viewportWidth = window.innerWidth;

                // 算新位置
                currentX = e.clientX - initialX;
                currentY = e.clientY - initialY;

                // 计算距离右侧的距离
                const rightDistance = viewportWidth - (currentX + container.offsetWidth);

                // 更新位置,使用 right 而不是 left
                container.style.right = `${rightDistance}px`;
                container.style.top = `${currentY}px`;
                container.style.left = 'auto';
            }
        };

        document.onmouseup = () => isDragging = false;
    }

    // 请求拦截相关方法
    setupRequestInterceptors() {
        this.interceptXHR();
        this.interceptFetch();
    }

    // XHR拦截实现
    interceptXHR() {
        const originalXHR = XMLHttpRequest.prototype;
        const originalOpen = originalXHR.open;
        const originalSend = originalXHR.send;
        const self = this;

        originalXHR.open = function(method, url) {
            if (self.shouldSkipRequest(url)) return;

            this._requestData = {
                method,
                url: url instanceof URL ? url.href : url,
                status: null,
                response: null,
                type: 'xhr',
                requestData: null,
                urlParams: self.getUrlParams(url),
                timestamp: new Date().toLocaleTimeString()
            };
            return originalOpen.apply(this, arguments);
        };

        originalXHR.send = function(data) {
            if (this._requestData) {
                if (data) {
                    try {
                        this._requestData.requestData = typeof data === 'string'
                            ? JSON.parse(data) : data;
                    } catch (e) {
                        this._requestData.requestData = data;
                    }
                }

                this.addEventListener('load', () => {
                    this._requestData.status = this.status;
                    try {
                        this._requestData.response = this.responseText;
                    } catch (e) {
                        this._requestData.response = '[无法读取响应内容]';
                    }

                    self.logs.push(this._requestData);
                    self.requestCount.total++;
                    self.requestCount.xhr++;
                    self.updateStats();
                    self.updateList();
                });
            }
            return originalSend.apply(this, arguments);
        };
    }

    // Fetch拦截实现
    interceptFetch() {
        const originalFetch = window.fetch;
        const self = this;

        window.fetch = async function(input, init = {}) {
            const url = input instanceof Request ? input.url : input;
            if (self.shouldSkipRequest(url)) return;

            const method = init.method || (input instanceof Request ? input.method : 'GET');
            const logEntry = {
                method,
                url: url instanceof URL ? url.href : url,
                status: null,
                response: null,
                type: 'fetch',
                requestData: init.body || null,
                urlParams: self.getUrlParams(url),
                timestamp: new Date().toLocaleTimeString()
            };

            try {
                const response = await originalFetch.apply(this, arguments);
                const clone = response.clone();
                logEntry.status = clone.status;

                try {
                    const responseText = await clone.text();
                    logEntry.response = responseText;
                } catch (e) {
                    logEntry.response = '[无法读取响应内容]';
                }

                self.logs.push(logEntry);
                self.requestCount.total++;
                self.requestCount.fetch++;
                self.updateStats();
                self.updateList();
                return response;
            } catch (error) {
                logEntry.status = 'ERROR';
                logEntry.response = error.message;
                self.logs.push(logEntry);
                self.updateStats();
                self.updateList();
                throw error;
            }
        };
    }

    // 更新统计信息
    updateStats() {
        const totalEl = document.getElementById('totalCount');
        const xhrEl = document.getElementById('xhrCount');
        const fetchEl = document.getElementById('fetchCount');

        if (totalEl) totalEl.textContent = this.requestCount.total;
        if (xhrEl) xhrEl.textContent = this.requestCount.xhr;
        if (fetchEl) fetchEl.textContent = this.requestCount.fetch;
    }

    // 更新请求列表
    updateList() {
        const requestList = document.getElementById('requestList');
        if (!requestList) return;

        const filteredLogs = this.filterKeyword ?
            this.logs.filter(log => {
                const searchStr = this.filterKeyword.toLowerCase();
                return (
                    log.url.toLowerCase().includes(searchStr) ||
                    (log.response && log.response.toLowerCase().includes(searchStr))
                );
            }) :
            this.logs;

        requestList.innerHTML = '';
        filteredLogs.forEach(log => this.createRequestItem(log, requestList));
    }

    // 创建请求列表项
    createRequestItem(log, container) {
        const item = document.createElement('div');
        item.style.cssText = `
            padding: 12px 16px;
            border-bottom: 1px solid rgba(0, 0, 0, 0.05);
            cursor: pointer;
            font-size: 12px;
            transition: all 0.2s;
            position: relative;
            background: rgba(255, 255, 255, 0.95);
        `;

        item.onmouseover = () => {
            item.style.backgroundColor = 'rgba(24, 144, 255, 0.05)';
        };

        item.onmouseout = () => {
            item.style.backgroundColor = 'rgba(255, 255, 255, 0.95)';
        };

        const statusColor = log.status >= 200 && log.status < 300 ? '#52c41a' : '#f5222d';

        item.innerHTML = `
            <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
                <span style="
                    font-weight: 600;
                    color: #1890ff;
                    padding: 2px 8px;
                    background: rgba(24, 144, 255, 0.1);
                    border-radius: 4px;
                ">${log.method}</span>
                <span style="
                    color: ${statusColor};
                    padding: 2px 8px;
                    background: ${log.status >= 200 && log.status < 300 ? 'rgba(82, 196, 26, 0.1)' : 'rgba(245, 34, 45, 0.1)'};
                    border-radius: 4px;
                ">${log.status || 'pending'}</span>
                <span style="color: #999; font-size: 11px;">${log.timestamp}</span>
            </div>
            <div style="
                word-break: break-all;
                color: #666;
                padding: 4px 8px;
                background: rgba(0, 0, 0, 0.02);
                border-radius: 4px;
                font-family: monospace;
            ">${log.url}</div>
        `;

        item.onclick = () => this.showRequestDetail(log);
        container.appendChild(item);
    }

    // 工具方法
    shouldSkipRequest(url) {
        return url.includes('cryptbox.sankuai.com/api_sdk/file_download/url_generate?current_url=') ||
               url.includes('cryptbox.sankuai.com/api_sdk/wenshu_url/judge_and_generate?current_url=');
    }

    getUrlParams(url) {
        try {
            const urlObj = new URL(url);
            const params = {};
            for (const [key, value] of urlObj.searchParams) {
                params[key] = value;
            }
            return Object.keys(params).length > 0 ? params : null;
        } catch (e) {
            return null;
        }
    }

    handleRedirect(url) {
        for (const rule of this.redirectRules) {
            if (rule.pattern.test(url)) {
                const newUrl = url.replace(rule.pattern, rule.replacement);
                console.log('重定向到:', newUrl);
                window.location.replace(newUrl);
                return true;
            }
        }
        return false;
    }

    // 显示请求详情
    showRequestDetail(log) {
        const detailPanel = document.getElementById('requestDetail');
        const detailContent = detailPanel.querySelector('div:last-child');

        // 创建复制按钮
        const createCopyButton = (text) => {
            const buttonContainer = document.createElement('div');
            buttonContainer.style.cssText = `
                position: absolute;
                right: 8px;
                top: 8px;
            `;

            const button = document.createElement('button');
            button.innerHTML = this.icons.copy;
            button.style.cssText = `
                padding: 4px 8px;
                background: rgba(24, 144, 255, 0.1);
                border: 1px solid rgba(24, 144, 255, 0.2);
                border-radius: 4px;
                color: #1890ff;
                cursor: pointer;
                font-size: 14px;
                transition: all 0.3s;
                display: flex;
                align-items: center;
                justify-content: center;
                min-width: 28px;
                height: 28px;
                opacity: 0.8;
            `;

            button.onmouseover = () => {
                button.style.background = 'rgba(24, 144, 255, 0.2)';
                button.style.opacity = '1';
            };

            button.onmouseout = () => {
                button.style.background = 'rgba(24, 144, 255, 0.1)';
                button.style.opacity = '0.8';
            };

            button.onclick = (e) => {
                e.stopPropagation();
                navigator.clipboard.writeText(text).then(() => {
                    button.innerHTML = this.icons.success;
                    setTimeout(() => button.innerHTML = this.icons.copy, 1000);
                });
            };

            buttonContainer.appendChild(button);
            return buttonContainer;
        };

        // 格式化数据显示
        const formatData = (data, type = '') => {
            try {
                if (!data) return '';
                let formattedData = data;
                if (typeof data === 'string') {
                    try {
                        formattedData = JSON.stringify(JSON.parse(data), null, 2);
                    } catch {
                        if (data.trim().startsWith('<')) {
                            formattedData = data.replace(/</g, '&lt;').replace(/>/g, '&gt;');
                        }
                    }
                } else {
                    formattedData = JSON.stringify(data, null, 2);
                }

                const container = document.createElement('div');
                container.style.cssText = 'margin-bottom: 16px; position: relative;';

                const title = document.createElement('div');
                title.style.cssText = 'font-weight: 600; color: #1890ff; margin-bottom: 4px;';
                title.textContent = `${type}:`;

                const pre = document.createElement('pre');
                pre.style.cssText = 'margin: 0; padding: 8px; background: rgba(0,0,0,0.02); border-radius: 4px; overflow-x: auto; max-height: 400px;';
                pre.textContent = formattedData;

                const copyButton = createCopyButton(formattedData);

                container.appendChild(title);
                container.appendChild(pre);
                pre.appendChild(copyButton);

                return container;
            } catch (e) {
                console.warn(`格式化${type}失败:`, e);
                return document.createElement('div');
            }
        };

        // 清空现内容
        detailContent.innerHTML = '';

        // 创建基本信息区
        const basicInfo = document.createElement('div');
        basicInfo.style.cssText = 'margin-bottom: 16px;';
        basicInfo.innerHTML = `
            <div style="display: flex; margin-bottom: 8px;">
                <span style="font-weight: 600; color: #1890ff; width: 80px;">方法:</span>
                <span>${log.method}</span>
            </div>
            <div style="display: flex; margin-bottom: 8px;">
                <span style="font-weight: 600; color: #1890ff; width: 80px;">状态:</span>
                <span style="color: ${log.status >= 200 && log.status < 300 ? '#52c41a' : '#f5222d'}">${log.status || 'pending'}</span>
            </div>
        `;

        // URL 部分
        const urlContainer = document.createElement('div');
        urlContainer.style.cssText = 'margin-bottom: 8px; position: relative;';
        const urlTitle = document.createElement('div');
        urlTitle.style.cssText = 'font-weight: 600; color: #1890ff; margin-bottom: 4px;';
        urlTitle.textContent = 'URL:';
        const urlContent = document.createElement('div');
        urlContent.style.cssText = 'word-break: break-all; padding: 8px; background: rgba(0,0,0,0.02); border-radius: 4px;';
        urlContent.textContent = log.url;
        urlContainer.appendChild(urlTitle);
        urlContainer.appendChild(urlContent);
        urlContent.appendChild(createCopyButton(log.url));

        // 添加所有内容
        detailContent.appendChild(basicInfo);
        detailContent.appendChild(urlContainer);

        // URL参数
        if (log.urlParams) {
            detailContent.appendChild(formatData(log.urlParams, 'URL参数'));
        }

        // 请求数据
        if (log.requestData) {
            detailContent.appendChild(formatData(log.requestData, '请求数据'));
        }

        // 响应数据
        if (log.response) {
            detailContent.appendChild(formatData(log.response, '响应数据'));
        }

        detailPanel.style.display = 'block';
    }

    // 修改 togglePanel 方法
    togglePanel(expand) {
        this.isPanelExpanded = expand;
        const container = document.getElementById('requestMonitor');
        const toggleButton = container.querySelector('div:first-child');
        const content = container.querySelector('#requestList')?.parentElement;
        const stats = container.querySelector('#requestStats');
        const header = container.querySelector('div:nth-child(2)');

        // 更新容器样式
        container.style.width = expand ? '400px' : '40px';
        container.style.height = expand ? '90vh' : '40px';
        container.style.display = expand ? 'flex' : 'block';

        // 更新按钮样式
        toggleButton.style.position = expand ? 'absolute' : 'relative';
        toggleButton.style.top = expand ? '12px' : '0';
        toggleButton.style.right = expand ? '12px' : '0';
        toggleButton.style.width = expand ? '24px' : '40px';
        toggleButton.style.height = expand ? '24px' : '40px';
        toggleButton.style.background = expand ? 'rgba(24, 144, 255, 0.1)' : '#1890ff';
        toggleButton.style.borderRadius = expand ? '4px' : '8px';
        toggleButton.style.color = expand ? '#1890ff' : 'white';
        toggleButton.innerHTML = expand ? '−' : '+';

        // 显示/隐藏其他元素
        if (header) header.style.display = expand ? 'flex' : 'none';
        if (stats) stats.style.display = expand ? 'flex' : 'none';
        if (content) content.style.display = expand ? 'block' : 'none';

        // 展开时重新计算内容高度
        if (expand) {
            // 触发内容重新渲染
            this.updateList();
        }
    }
}

// 只有在不需要重定向时才初始化监控器
if (!window.location.href.includes('cryptbox.sankuai.com/file/')) {
    new RequestMonitor();
}