JSON Response Capture

Capture and save JSON responses from web requests with URL filtering

当前为 2024-12-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         JSON Response Capture
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Capture and save JSON responses from web requests with URL filtering
// @author       nickm8
// @license      MIT
// @match        *://*/*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js
// ==/UserScript==

(function() {
    'use strict';

    // Additional styles for configuration UI
    const style = document.createElement('style');
    style.textContent = `
        /* Previous styles remain the same */
        .json-capture-panel {
            position: fixed;
            bottom: 1rem;
            right: 1rem;
            background: white;
            border-radius: 0.5rem;
            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
            transition: all 200ms;
            z-index: 10000;
        }
        .json-capture-panel.minimized {
            width: 3rem;
        }
        .json-capture-panel.expanded {
            width: 24rem;
        }
        .json-capture-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 0.5rem;
            border-bottom: 1px solid #e5e7eb;
            background: #f9fafb;
            border-top-left-radius: 0.5rem;
            border-top-right-radius: 0.5rem;
        }
        .json-capture-content {
            max-height: 24rem;
            overflow: auto;
            padding: 1rem;
        }
        .json-capture-item {
            margin-bottom: 1rem;
            padding: 0.5rem;
            border: 1px solid #e5e7eb;
            border-radius: 0.375rem;
        }
        .json-capture-url {
            font-size: 0.875rem;
            color: #4b5563;
            margin-bottom: 0.25rem;
            word-break: break-all;
        }
        .json-capture-timestamp {
            font-size: 0.75rem;
            color: #6b7280;
            margin-bottom: 0.5rem;
        }
        .json-capture-json {
            font-size: 0.75rem;
            background: #f9fafb;
            padding: 0.5rem;
            border-radius: 0.375rem;
            overflow: auto;
            white-space: pre-wrap;
            max-height: 12rem;
        }
        .json-capture-button {
            padding: 0.25rem 0.5rem;
            background: none;
            border: none;
            cursor: pointer;
            color: #4b5563;
        }
        .json-capture-button:hover {
            color: #2563eb;
        }
        .json-capture-button.delete:hover {
            color: #dc2626;
        }
        
        /* New styles for configuration */
        .config-section {
            margin-bottom: 1rem;
            padding: 0.5rem;
            border: 1px solid #e5e7eb;
            border-radius: 0.375rem;
        }
        .config-title {
            font-weight: 600;
            margin-bottom: 0.5rem;
        }
        .config-input {
            display: flex;
            gap: 0.5rem;
            margin-bottom: 0.5rem;
        }
        .config-input input {
            flex: 1;
            padding: 0.25rem 0.5rem;
            border: 1px solid #e5e7eb;
            border-radius: 0.25rem;
        }
        .config-list {
            display: flex;
            flex-wrap: wrap;
            gap: 0.25rem;
        }
        .config-tag {
            background: #f3f4f6;
            padding: 0.25rem 0.5rem;
            border-radius: 0.25rem;
            display: flex;
            align-items: center;
            gap: 0.25rem;
        }
        .config-tag button {
            padding: 0;
            background: none;
            border: none;
            cursor: pointer;
            color: #6b7280;
        }
        .tabs {
            display: flex;
            border-bottom: 1px solid #e5e7eb;
            margin-bottom: 1rem;
        }
        .tab {
            padding: 0.5rem 1rem;
            cursor: pointer;
            border-bottom: 2px solid transparent;
        }
        .tab.active {
            border-bottom-color: #2563eb;
            color: #2563eb;
        }
    `;
    document.head.appendChild(style);

    const { useState, useEffect } = React;

    // Configuration component
    function ConfigSection({ matches, ignores, onUpdateMatches, onUpdateIgnores }) {
        const [newMatch, setNewMatch] = useState('');
        const [newIgnore, setNewIgnore] = useState('');

        const addMatch = () => {
            if (newMatch && !matches.includes(newMatch)) {
                onUpdateMatches([...matches, newMatch]);
                setNewMatch('');
            }
        };

        const addIgnore = () => {
            if (newIgnore && !ignores.includes(newIgnore)) {
                onUpdateIgnores([...ignores, newIgnore]);
                setNewIgnore('');
            }
        };

        const removeMatch = (match) => {
            onUpdateMatches(matches.filter(m => m !== match));
        };

        const removeIgnore = (ignore) => {
            onUpdateIgnores(ignores.filter(i => i !== ignore));
        };

        return React.createElement('div', { className: 'config-content' }, [
            React.createElement('div', { key: 'matches', className: 'config-section' }, [
                React.createElement('div', { className: 'config-title' }, 'URL Matches'),
                React.createElement('div', { className: 'config-input' }, [
                    React.createElement('input', {
                        value: newMatch,
                        onChange: (e) => setNewMatch(e.target.value),
                        placeholder: 'Enter URL keyword to match',
                        onKeyPress: (e) => e.key === 'Enter' && addMatch()
                    }),
                    React.createElement('button', {
                        className: 'json-capture-button',
                        onClick: addMatch
                    }, '+')
                ]),
                React.createElement('div', { className: 'config-list' },
                    matches.map(match => 
                        React.createElement('span', { key: match, className: 'config-tag' }, [
                            match,
                            React.createElement('button', {
                                onClick: () => removeMatch(match)
                            }, '×')
                        ])
                    )
                )
            ]),
            React.createElement('div', { key: 'ignores', className: 'config-section' }, [
                React.createElement('div', { className: 'config-title' }, 'URL Ignores'),
                React.createElement('div', { className: 'config-input' }, [
                    React.createElement('input', {
                        value: newIgnore,
                        onChange: (e) => setNewIgnore(e.target.value),
                        placeholder: 'Enter URL keyword to ignore',
                        onKeyPress: (e) => e.key === 'Enter' && addIgnore()
                    }),
                    React.createElement('button', {
                        className: 'json-capture-button',
                        onClick: addIgnore
                    }, '+')
                ]),
                React.createElement('div', { className: 'config-list' },
                    ignores.map(ignore => 
                        React.createElement('span', { key: ignore, className: 'config-tag' }, [
                            ignore,
                            React.createElement('button', {
                                onClick: () => removeIgnore(ignore)
                            }, '×')
                        ])
                    )
                )
            ])
        ]);
    }

    function JsonCapturePanel() {
        const [captures, setCaptures] = useState([]);
        const [isMinimized, setIsMinimized] = useState(true);
        const [activeTab, setActiveTab] = useState('captures');
        const [matches, setMatches] = useState([]);
        const [ignores, setIgnores] = useState([]);
        const domain = window.location.hostname;

        // Load configuration from localStorage
        useEffect(() => {
            const config = JSON.parse(localStorage.getItem(`jsonCapture_${domain}`) || '{"matches":[],"ignores":[]}');
            setMatches(config.matches);
            setIgnores(config.ignores);
        }, []);

        // Save configuration to localStorage
        useEffect(() => {
            localStorage.setItem(`jsonCapture_${domain}`, JSON.stringify({ matches, ignores }));
        }, [matches, ignores]);

        const shouldCaptureUrl = (url) => {
            if (matches.length === 0) return false;
            return matches.some(match => url.includes(match)) && 
                   !ignores.some(ignore => url.includes(ignore));
        };

        useEffect(() => {
            // Intercept fetch
            const originalFetch = window.fetch;
            window.fetch = async function(...args) {
                const response = await originalFetch.apply(this, args);
                const clone = response.clone();

                try {
                    const contentType = clone.headers.get('content-type');
                    if (contentType && contentType.includes('application/json')) {
                        const url = typeof args[0] === 'string' ? args[0] : args[0].url;
                        if (shouldCaptureUrl(url)) {
                            const json = await clone.json();
                            setCaptures(prev => [...prev, {
                                timestamp: new Date().toISOString(),
                                url,
                                data: json
                            }]);
                        }
                    }
                } catch (err) {
                    console.error('Error processing response:', err);
                }

                return response;
            };

            // Intercept XHR
            const originalXHROpen = XMLHttpRequest.prototype.open;
            const originalXHRSend = XMLHttpRequest.prototype.send;

            XMLHttpRequest.prototype.open = function(...args) {
                this._url = args[1];
                return originalXHROpen.apply(this, args);
            };

            XMLHttpRequest.prototype.send = function(...args) {
                this.addEventListener('load', function() {
                    try {
                        const contentType = this.getResponseHeader('content-type');
                        if (contentType && contentType.includes('application/json')) {
                            if (shouldCaptureUrl(this._url)) {
                                const json = JSON.parse(this.responseText);
                                setCaptures(prev => [...prev, {
                                    timestamp: new Date().toISOString(),
                                    url: this._url,
                                    data: json
                                }]);
                            }
                        }
                    } catch (err) {
                        console.error('Error processing XHR response:', err);
                    }
                });

                return originalXHRSend.apply(this, args);
            };
        }, [matches, ignores]);

        const handleSave = () => {
            const blob = new Blob([JSON.stringify(captures, null, 2)], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `json-captures-${domain}-${new Date().toISOString()}.json`;
            a.click();
            URL.revokeObjectURL(url);
        };

        const handleClear = () => {
            setCaptures([]);
        };

        return React.createElement('div', {
            className: `json-capture-panel ${isMinimized ? 'minimized' : 'expanded'}`
        }, [
            // Header
            React.createElement('div', {
                key: 'header',
                className: 'json-capture-header'
            }, [
                !isMinimized && React.createElement('div', {
                    key: 'title'
                }, `JSON Captures (${captures.length})`),
                React.createElement('div', {
                    key: 'controls',
                    style: { display: 'flex', gap: '0.5rem' }
                }, [
                    !isMinimized && React.createElement('button', {
                        key: 'save',
                        className: 'json-capture-button',
                        onClick: handleSave,
                        title: 'Save captures'
                    }, '💾'),
                    React.createElement('button', {
                        key: 'toggle',
                        className: 'json-capture-button',
                        onClick: () => setIsMinimized(!isMinimized),
                        title: isMinimized ? 'Expand' : 'Minimize'
                    }, isMinimized ? '⤢' : '⤡'),
                    !isMinimized && React.createElement('button', {
                        key: 'clear',
                        className: 'json-capture-button delete',
                        onClick: handleClear,
                        title: 'Clear captures'
                    }, '✕')
                ])
            ]),
            // Content
            !isMinimized && React.createElement('div', { key: 'content' }, [
                // Tabs
                React.createElement('div', { key: 'tabs', className: 'tabs' }, [
                    React.createElement('div', {
                        className: `tab ${activeTab === 'captures' ? 'active' : ''}`,
                        onClick: () => setActiveTab('captures')
                    }, 'Captures'),
                    React.createElement('div', {
                        className: `tab ${activeTab === 'config' ? 'active' : ''}`,
                        onClick: () => setActiveTab('config')
                    }, 'Configuration')
                ]),
                // Tab content
                activeTab === 'captures' ? 
                    React.createElement('div', {
                        key: 'captures',
                        className: 'json-capture-content'
                    },
                        captures.length === 0
                            ? React.createElement('div', {
                                style: { textAlign: 'center', color: '#6b7280' }
                            }, matches.length === 0 ? 'Configure URL matches to start capturing' : 'No JSON responses captured yet')
                            : captures.map((capture, index) =>
                                React.createElement('div', {
                                    key: index,
                                    className: 'json-capture-item'
                                }, [
                                    React.createElement('div', {
                                        key: 'url',
                                        className: 'json-capture-url'
                                    }, capture.url),
                                    React.createElement('div', {
                                        key: 'timestamp',
                                        className: 'json-capture-timestamp'
                                    }, capture.timestamp),
                                    React.createElement('pre', {
                                        key: 'json',
                                        className: 'json-capture-json'
                                    }, JSON.stringify(capture.data, null, 2))
                                ])
                            )
                    )
                    : React.createElement(ConfigSection, {
                        key: 'config',
                        matches,
                        ignores,
                        onUpdateMatches: setMatches,
                        onUpdateIgnores: setIgnores
                    })
            ])
]);
}

const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(React.createElement(JsonCapturePanel), container);
})();