您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Capture and save JSON responses from web requests with URL filtering
- // ==UserScript==
- // @name JSON Response Capture
- // @namespace http://tampermonkey.net/
- // @version 1.3
- // @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';
- const style = document.createElement('style');
- style.textContent = `
- :root {
- --bg-primary: #ffffff;
- --bg-secondary: #f9fafb;
- --text-primary: #1f2937;
- --text-secondary: #4b5563;
- --text-tertiary: #6b7280;
- --border-color: #e5e7eb;
- --shadow-color: rgba(0, 0, 0, 0.1);
- --hover-color: #2563eb;
- --danger-color: #dc2626;
- --tag-bg: #f3f4f6;
- }
- @media (prefers-color-scheme: dark) {
- :root {
- --bg-primary: #1f2937;
- --bg-secondary: #374151;
- --text-primary: #f9fafb;
- --text-secondary: #d1d5db;
- --text-tertiary: #9ca3af;
- --border-color: #4b5563;
- --shadow-color: rgba(0, 0, 0, 0.3);
- --hover-color: #60a5fa;
- --danger-color: #ef4444;
- --tag-bg: #374151;
- }
- }
- .json-capture-panel {
- position: fixed;
- bottom: 1rem;
- right: 1rem;
- background: var(--bg-primary);
- color: var(--text-primary);
- border-radius: 0.5rem;
- box-shadow: 0 4px 6px -1px var(--shadow-color);
- 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 var(--border-color);
- background: var(--bg-secondary);
- 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 var(--border-color);
- border-radius: 0.375rem;
- }
- .json-capture-url {
- font-size: 0.875rem;
- color: var(--text-secondary);
- margin-bottom: 0.25rem;
- word-break: break-all;
- }
- .json-capture-timestamp {
- font-size: 0.75rem;
- color: var(--text-tertiary);
- margin-bottom: 0.5rem;
- }
- .json-capture-json {
- font-size: 0.75rem;
- background: var(--bg-secondary);
- padding: 0.5rem;
- border-radius: 0.375rem;
- overflow: auto;
- white-space: pre-wrap;
- max-height: 12rem;
- color: var(--text-primary);
- }
- .json-capture-button {
- padding: 0.25rem 0.5rem;
- background: none;
- border: none;
- cursor: pointer;
- color: var(--text-secondary);
- }
- .json-capture-button:hover {
- color: var(--hover-color);
- }
- .json-capture-button.delete:hover {
- color: var(--danger-color);
- }
- .config-section {
- margin-bottom: 1rem;
- padding: 0.5rem;
- border: 1px solid var(--border-color);
- border-radius: 0.375rem;
- }
- .config-title {
- font-weight: 600;
- margin-bottom: 0.5rem;
- color: var(--text-primary);
- }
- .config-input {
- display: flex;
- gap: 0.5rem;
- margin-bottom: 0.5rem;
- }
- .config-input input {
- flex: 1;
- padding: 0.25rem 0.5rem;
- border: 1px solid var(--border-color);
- border-radius: 0.25rem;
- background: var(--bg-primary);
- color: var(--text-primary);
- }
- .config-list {
- display: flex;
- flex-wrap: wrap;
- gap: 0.25rem;
- }
- .config-tag {
- background: var(--tag-bg);
- color: var(--text-primary);
- 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: var(--text-tertiary);
- }
- .tabs {
- display: flex;
- border-bottom: 1px solid var(--border-color);
- margin-bottom: 1rem;
- }
- .tab {
- padding: 0.5rem 1rem;
- cursor: pointer;
- border-bottom: 2px solid transparent;
- color: var(--text-secondary);
- }
- .tab.active {
- border-bottom-color: var(--hover-color);
- color: var(--hover-color);
- }
- `;
- 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);
- })();