API信息批量提取器

专为Apifox设计的API接口信息批量提取工具,支持自动监听、数据预览和批量导出

目前為 2025-07-01 提交的版本,檢視 最新版本

// ==UserScript==
// @name         API信息批量提取器
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  专为Apifox设计的API接口信息批量提取工具,支持自动监听、数据预览和批量导出
// @description:en  Batch API information extractor tool designed for Apifox, supports auto-monitoring, data preview and batch export
// @author       xiaoma
// @license      MIT
// @homepage     https://github.com/api-extractor/userscript
// @supportURL   https://github.com/api-extractor/userscript/issues
// @match        https://app.apifox.com/*
// @match        https://*.apifox.com/*
// @icon         data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMTMuMDkgOC4yNkwyMCA5TDEzLjA5IDE1Ljc0TDEyIDIyTDEwLjkxIDE1Ljc0TDQgOUwxMC45MSA4LjI2TDEyIDJaIiBmaWxsPSIjNDA5RUZGIi8+Cjwvc3ZnPgo=
// @grant        none
// @run-at       document-end
// @noframes

// ==/UserScript==

(function() {
    'use strict';
    
    // 全局常量定义
    const STORAGE_KEY = 'api_extractor_data';
    const POSITION_STORAGE_KEY = 'api_extractor_panel_position';
    const UI_CONTAINER_ID = 'api-extractor-control-panel';
    
    // 全局状态管理
    let extractedApiDataCollection = [];
    let isAutoExtracting = false;
    let currentApiUrl = '';
    let apiUrlObserver = null;
    let existDialog = false;
    
    /**
     * 初始化脚本
     */
    function initScript() {
        try {
            loadSavedData();
            createUIControlPanel();
            console.log('API提取器已启动');
        } catch (errorInfo) {
            console.error('脚本初始化失败:', errorInfo);
        }
    }
    
    /**
     * 加载本地存储的数据
     */
    function loadSavedData() {
        try {
            const storedData = localStorage.getItem(STORAGE_KEY);
            extractedApiDataCollection = storedData ? JSON.parse(storedData) : [];
        } catch (errorInfo) {
            console.warn('加载存储数据失败:', errorInfo);
            extractedApiDataCollection = [];
        }
    }
    
    /**
     * 保存数据到本地存储
     */
    function saveDataToLocalStorage() {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(extractedApiDataCollection));
        } catch (errorInfo) {
            console.error('保存数据失败:', errorInfo);
        }
    }
    
    /**
     * 创建UI控制面板
     */
    function createUIControlPanel() {
        // 检查是否已存在控制面板
        if (document.getElementById(UI_CONTAINER_ID)) return;
        
        // 获取保存的位置
        const savedPosition = getSavedPanelPosition();
        
        const controlPanelContainer = document.createElement('div');
        controlPanelContainer.id = UI_CONTAINER_ID;
        controlPanelContainer.style.cssText = `
            position: fixed;
            top: ${savedPosition.top}px;
            left: ${savedPosition.left}px;
            z-index: 9999;
            background: #fafbfc;
            border: 1px solid #e1e8ed;
            border-radius: 6px;
            padding: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.08);
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            width: 300px;
            cursor: move;
            user-select: none;
            transition: opacity 0.2s;
        `;
        
        // 标题栏
        const titleBar = document.createElement('div');
        titleBar.style.cssText = `
            font-weight: 500;
            color: #536471;
            margin-bottom: 8px;
            text-align: center;
            font-size: 13px;
            padding-bottom: 6px;
        `;
        titleBar.textContent = 'API提取器';
        
        // 按钮容器
        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = 'display: flex; flex-direction: column; gap: 6px;';
        
        // 提取当前API按钮
        const extractButton = createButton('提取当前API', '#9373ee', handleExtractCurrentApi);
        
        // 下载全部按钮
        const downloadButton = createButton(`下载全部(${extractedApiDataCollection.length})`, '#9373ee', handleDownloadAllData);
        
        // 预览数据按钮
        const previewButton = createButton('预览数据', '#ef6820', handlePreviewData);
        
        // 清空数据按钮
        const clearButton = createButton('清空数据', '#dc3545', handleClearAllData);
        
        // 自动提取按钮
        const autoExtractButton = createButton('自动提取', '#1890ff', handleAutoExtract);
        
        // 组装UI
        buttonContainer.appendChild(extractButton);
        buttonContainer.appendChild(downloadButton);
        buttonContainer.appendChild(previewButton);
        buttonContainer.appendChild(autoExtractButton);
        buttonContainer.appendChild(clearButton);
        
        // 存储按钮引用
        window.autoExtractButtonRef = autoExtractButton;
        
        // 信息显示区域
        const infoArea = document.createElement('div');
        infoArea.id = 'api-extractor-info';
        infoArea.style.cssText = `
            background: #f8f9fa;
            border: 1px solid #e1e8ed;
            border-radius: 4px;
            padding: 8px;
            margin-top: 8px;
            margin-bottom: 8px;
            font-size: 12px;
            color: #536471;
            min-height: 20px;
            font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
            line-height: 1.3;
            white-space: pre-wrap;
            word-wrap: break-word;
        `;
        infoArea.textContent = '就绪';
        
        controlPanelContainer.appendChild(titleBar);
        controlPanelContainer.appendChild(infoArea);
        controlPanelContainer.appendChild(buttonContainer);
        
        document.body.appendChild(controlPanelContainer);
        
        // 存储按钮引用以便后续更新
        window.downloadButtonRef = downloadButton;
        
        // 添加拖拽功能
        makePanelDraggable(controlPanelContainer);
    }
    
    /**
     * 创建通用按钮
     */
    function createButton(buttonText, backgroundColor, clickHandler) {
        const buttonElement = document.createElement('button');
        buttonElement.textContent = buttonText;
        buttonElement.style.cssText = `
            background: ${backgroundColor};
            color: white;
            border: none;
            border-radius: 4px;
            padding: 6px 10px;
            cursor: pointer;
            font-size: 12px;
            font-weight: 400;
            transition: all 0.2s ease;
            line-height: 1.2;
        `;
        
        // 鼠标悬停效果
        buttonElement.addEventListener('mouseenter', () => {
            buttonElement.style.transform = 'translateY(-1px)';
            buttonElement.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
        });
        
        buttonElement.addEventListener('mouseleave', () => {
            buttonElement.style.transform = 'translateY(0)';
            buttonElement.style.boxShadow = 'none';
        });
        
        buttonElement.addEventListener('click', clickHandler);
        return buttonElement;
    }
    
    /**
     * 获取保存的面板位置
     */
    function getSavedPanelPosition() {
        try {
            const savedPosition = localStorage.getItem(POSITION_STORAGE_KEY);
            if (savedPosition) {
                return JSON.parse(savedPosition);
            }
        } catch (error) {
            console.warn('读取面板位置失败:', error);
        }
        
        // 默认位置:视口正上方居中
        return {
            top: 20,
            left: Math.max(20, (window.innerWidth - 300) / 2)
        };
    }
    
    /**
     * 保存面板位置
     */
    function savePanelPosition(top, left) {
        try {
            const position = { top, left };
            localStorage.setItem(POSITION_STORAGE_KEY, JSON.stringify(position));
        } catch (error) {
            console.warn('保存面板位置失败:', error);
        }
    }
    
    /**
     * 使面板可拖拽
     */
    function makePanelDraggable(panel) {
        let isDragging = false;
        let dragOffsetX = 0;
        let dragOffsetY = 0;
        
        panel.addEventListener('mousedown', function(e) {
            // 防止在按钮上开始拖拽
            if (e.target.tagName === 'BUTTON') return;
            
            isDragging = true;
            
            // 设置拖拽时的半透明效果
            panel.style.opacity = '0.7';
            
            // 计算鼠标相对于面板的偏移
            const rect = panel.getBoundingClientRect();
            dragOffsetX = e.clientX - rect.left;
            dragOffsetY = e.clientY - rect.top;
            
            // 阻止文本选择
            e.preventDefault();
        });
        
        document.addEventListener('mousemove', function(e) {
            if (!isDragging) return;
            
            // 计算新位置
            let newLeft = e.clientX - dragOffsetX;
            let newTop = e.clientY - dragOffsetY;
            
            // 边界检查,确保面板不会超出视口
            const maxLeft = window.innerWidth - panel.offsetWidth;
            const maxTop = window.innerHeight - panel.offsetHeight;
            
            newLeft = Math.max(0, Math.min(newLeft, maxLeft));
            newTop = Math.max(0, Math.min(newTop, maxTop));
            
            // 更新面板位置
            panel.style.left = newLeft + 'px';
            panel.style.top = newTop + 'px';
        });
        
        document.addEventListener('mouseup', function() {
            if (!isDragging) return;
            
            isDragging = false;
            
            // 恢复不透明度
            panel.style.opacity = '1';
            
            // 保存当前位置
            const rect = panel.getBoundingClientRect();
            savePanelPosition(rect.top, rect.left);
        });
    }
    
    /**
     * 处理提取当前API
     */
    function handleExtractCurrentApi() {
        try {
            const apiData = extractCurrentPageApiInfo();
            
            if (!apiData.apiName) {
                showInfoInPanel('未检测到有效的API接口信息');
                return;
            }
            
            // 检查是否已存在相同API
            const existingIndex = extractedApiDataCollection.findIndex(item => 
                item.apiName === apiData.apiName && item.apiUrl === apiData.apiUrl
            );
            
            if (existingIndex !== -1) {
                // 更新现有数据
                extractedApiDataCollection[existingIndex] = apiData;
                showInfoInPanel(`已更新: ${apiData.apiName}`);
            } else {
                // 添加新数据
                extractedApiDataCollection.push(apiData);
                showInfoInPanel(`已提取: ${apiData.apiName}`);
            }
            
            saveDataToLocalStorage();
            updateDownloadButtonText();
            
        } catch (errorInfo) {
            console.error('提取API信息失败:', errorInfo);
            showInfoInPanel('提取失败: ' + errorInfo.message);
        }
    }
    
    /**
     * 提取当前页面的API信息
     */
    function extractCurrentPageApiInfo() {
        const apiInfo = {
            apiName: '',
            apiUrl: '',
            requestMethod: '',
            requestParams: [],
            responseInfo: '暂无响应信息'
        };
        
        // 提取接口名称
        apiInfo.apiName = safeExtractText('.name-text-nO_wGQ .copyable-NYoI4L');
        
        // 提取请求方式和URL
        const pathInfoContainer = document.querySelector('.base-info-path-lbh3Yn');
        if (pathInfoContainer) {
            apiInfo.requestMethod = safeExtractText('.base-info-path-lbh3Yn code', pathInfoContainer);
            apiInfo.apiUrl = safeExtractText('.base-info-path-lbh3Yn .copyable-NYoI4L', pathInfoContainer);
        }
        
        // 提取请求参数
        apiInfo.requestParams = extractRequestParamsStructure();
        
        return apiInfo;
    }
    
    /**
     * 安全提取文本内容
     */
    function safeExtractText(selector, parentElement = document) {
        try {
            const element = parentElement.querySelector(selector);
            return element ? element.textContent.trim() : '';
        } catch (error) {
            console.warn(`提取文本失败 ${selector}:`, error);
            return '';
        }
    }
    
    /**
     * 提取请求参数结构
     */
    function extractRequestParamsStructure() {
        const paramList = [];
        
        try {
            // 查找所有参数节点
            const paramNodeList = document.querySelectorAll('.JsonSchemaViewer .index_node__G6-Qx');
            
            paramNodeList.forEach(node => {
                const paramInfo = parseParamNode(node);
                if (paramInfo.paramName) {
                    paramList.push(paramInfo);
                }
            });
            
        } catch (error) {
            console.warn('提取参数结构失败:', error);
        }
        
        return paramList;
    }
    
    /**
     * 解析单个参数节点
     */
    function parseParamNode(node) {
        const paramInfo = {
            paramName: '',
            paramType: '',
            isRequired: false,
            paramDescription: ''
        };
        
        try {
            // 获取参数名
            const paramNameElement = node.querySelector('.propertyName-Zh4tse .copyable-NYoI4L');
            paramInfo.paramName = paramNameElement ? paramNameElement.textContent.trim() : '';
            
            // 获取参数类型
            const typeElement = node.querySelector('.sl-type span');
            paramInfo.paramType = typeElement ? typeElement.textContent.trim() : '';
            
            // 判断是否必填
            const optionalFlag = node.querySelector('.index_optional__O33wK');
            paramInfo.isRequired = !optionalFlag;
            
            // 获取参数描述
            const descriptionElement = node.querySelector('.json-schema-viewer__description p');
            paramInfo.paramDescription = descriptionElement ? descriptionElement.textContent.trim() : '';
            
        } catch (error) {
            console.warn('解析参数节点失败:', error);
        }
        
        return paramInfo;
    }
    
    /**
     * 处理下载全部数据
     */
    function handleDownloadAllData() {
        if (extractedApiDataCollection.length === 0) {
            showInfoInPanel('暂无数据可下载');
            return;
        }
        
        try {
            const exportData = {
                exportTime: new Date().toLocaleString('zh-CN'),
                dataCount: extractedApiDataCollection.length,
                apiList: extractedApiDataCollection
            };
            
            const dataString = JSON.stringify(exportData, null, 2);
            const fileName = `API接口数据_${new Date().toISOString().slice(0,10)}.json`;
            
            downloadJsonFile(dataString, fileName);
            showInfoInPanel(`已下载 ${extractedApiDataCollection.length} 条API数据`);
            
        } catch (errorInfo) {
            console.error('下载数据失败:', errorInfo);
            showInfoInPanel('下载失败: ' + errorInfo.message);
        }
    }
    
    /**
     * 下载JSON文件
     */
    function downloadJsonFile(dataContent, fileName) {
        const dataUrl = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataContent);
        const downloadLink = document.createElement('a');
        downloadLink.href = dataUrl;
        downloadLink.download = fileName;
        downloadLink.style.display = 'none';
        
        document.body.appendChild(downloadLink);
        downloadLink.click();
        document.body.removeChild(downloadLink);
    }
    
    /**
     * 处理清空所有数据
     */
    function handleClearAllData() {
        if (extractedApiDataCollection.length === 0) {
            showInfoInPanel('暂无数据需要清空');
            return;
        }
        
        if (confirm(`确定要清空所有 ${extractedApiDataCollection.length} 条API数据吗?`)) {
            const clearedCount = extractedApiDataCollection.length;
            extractedApiDataCollection = [];
            saveDataToLocalStorage();
            updateDownloadButtonText();
            showInfoInPanel(`已清空 ${clearedCount} 条API数据`);
        }
    }
    
    /**
     * 处理预览数据
     */
    function handlePreviewData() {
        if (extractedApiDataCollection.length === 0) {
            showInfoInPanel('暂无数据可预览');
            return;
        }
        
        const previewData = {
            exportTime: new Date().toLocaleString('zh-CN'),
            dataCount: extractedApiDataCollection.length,
            apiList: extractedApiDataCollection
        };
        
        showJsonPreviewModal(previewData);
    }
    
    /**
     * 处理自动提取
     */
    function handleAutoExtract() {
        if (!isAutoExtracting) {
            // 开始自动提取
            startAutoExtract();
        } else {
            // 停止自动提取
            stopAutoExtract();
        }
    }
    
    /**
     * 开始自动提取
     */
    function startAutoExtract() {
        isAutoExtracting = true;
        
        // 获取当前接口URL
        const pathInfoContainer = document.querySelector('.base-info-path-lbh3Yn');
        currentApiUrl = pathInfoContainer ? safeExtractText('.base-info-path-lbh3Yn .copyable-NYoI4L', pathInfoContainer) : '';
        
        // 更新按钮状态
        updateAutoExtractButton();
        
        // 先提取当前页面
        setTimeout(() => {
            handleExtractCurrentApi();
        }, 500);
        
        // 开始监听接口URL变化
        startApiUrlMonitoring();
        
        showInfoInPanel('自动提取已开启,正在监听接口变化');
    }
    
    /**
     * 停止自动提取
     */
    function stopAutoExtract() {
        isAutoExtracting = false;
        
        // 更新按钮状态
        updateAutoExtractButton();
        
        // 停止接口URL监听
        stopApiUrlMonitoring();
        
        showInfoInPanel('自动提取已停止');
    }
    
    /**
     * 更新自动提取按钮状态
     */
    function updateAutoExtractButton() {
        if (window.autoExtractButtonRef) {
            if (isAutoExtracting) {
                window.autoExtractButtonRef.textContent = '停止提取';
                window.autoExtractButtonRef.style.background = '#dc3545';
            } else {
                window.autoExtractButtonRef.textContent = '自动提取';
                window.autoExtractButtonRef.style.background = '#1890ff';
            }
        }
    }
    
    /**
     * 开始监听接口URL变化
     */
    function startApiUrlMonitoring() {
        const targetContainer = document.querySelector('.base-info-path-lbh3Yn');
        if (!targetContainer) {
            showInfoInPanel('未找到接口信息容器,无法开启自动提取');
            stopAutoExtract();
            return;
        }
        
        // 使用MutationObserver监听DOM变化
        apiUrlObserver = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'childList' || mutation.type === 'characterData' || mutation.type === 'subtree') {
                    handleApiUrlChange();
                }
            });
        });
        
        // 开始观察目标容器及其子元素
        apiUrlObserver.observe(targetContainer, {
            childList: true,
            subtree: true,
            characterData: true,
            attributes: false
        });
        
        // 备用定时检查
        window.apiUrlCheckInterval = setInterval(() => {
            if (isAutoExtracting) {
                handleApiUrlChange();
            }
        }, 100);
    }
    
    /**
     * 停止监听接口URL变化
     */
    function stopApiUrlMonitoring() {
        // 停止MutationObserver
        if (apiUrlObserver) {
            apiUrlObserver.disconnect();
            apiUrlObserver = null;
        }
        
        // 清除定时器
        if (window.apiUrlCheckInterval) {
            clearInterval(window.apiUrlCheckInterval);
            window.apiUrlCheckInterval = null;
        }
    }
    
    /**
     * 处理接口URL变化
     */
    function handleApiUrlChange() {
        if (!isAutoExtracting) return;
        
        const pathInfoContainer = document.querySelector('.base-info-path-lbh3Yn');
        if (!pathInfoContainer) return;
        
        const newApiUrl = safeExtractText('.base-info-path-lbh3Yn .copyable-NYoI4L', pathInfoContainer);
        
        if (newApiUrl && newApiUrl !== currentApiUrl) {
            currentApiUrl = newApiUrl;
            
            // 延迟提取,确保页面内容完全更新
            setTimeout(() => {
                if (isAutoExtracting) {
                    console.log('检测到接口变化:', newApiUrl);
                    handleExtractCurrentApi();
                }
            }, 800);
        }
    }
    
    /**
     * 更新下载按钮文本
     */
    function updateDownloadButtonText() {
        if (window.downloadButtonRef) {
            window.downloadButtonRef.textContent = `下载全部(${extractedApiDataCollection.length})`;
        }
    }
    
    /**
     * 在面板中显示信息
     */
    function showInfoInPanel(content) {
        const infoArea = document.getElementById('api-extractor-info');
        
        if (!infoArea) return;
        
        // 更新内容
        infoArea.textContent = content;
    }
    
    /**
     * 清除所有模态框
     */
    function clearAllModals() {
        // 移除所有可能的模态框
        const modalSelectors = [
            '#json-preview-modal',
            '[id$="-modal"]',
            '[class*="modal"]',
            '[style*="z-index: 10001"]'
        ];
        
        modalSelectors.forEach(selector => {
            const modals = document.querySelectorAll(selector);
            modals.forEach(modal => modal.remove());
        });
        
        // 重置弹窗状态
        existDialog = false;
    }
    
    /**
     * 显示JSON预览模态框
     */
    function showJsonPreviewModal(data) {
        // 如果已存在弹窗,先销毁
        if (existDialog) {
            clearAllModals();
        }
        
        // 设置弹窗存在标记
        existDialog = true;
        
        // 创建模态框容器
        const modalOverlay = document.createElement('div');
        modalOverlay.id = 'json-preview-modal';
        modalOverlay.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
            z-index: 10001;
            display: flex;
            align-items: center;
            justify-content: center;
            backdrop-filter: blur(2px);
        `;
        
        // 创建模态框内容
        const modalContent = document.createElement('div');
        modalContent.style.cssText = `
            background: #ffffff;
            border-radius: 8px;
            padding: 20px;
            max-width: 90%;
            max-height: 80%;
            overflow: hidden;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
            display: flex;
            flex-direction: column;
            min-width: 600px;
        `;
        
        // 标题栏
        const modalHeader = document.createElement('div');
        modalHeader.style.cssText = `
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 1px solid #e9ecef;
        `;
        modalHeader.innerHTML = `
            <h3 style="margin: 0; color: #536471; font-size: 15px; font-weight: 500;">JSON数据预览 (${data.dataCount}条)</h3>
            <div>
                <button id="copy-json-btn" style="
                    background: #17b26a;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    padding: 6px 12px;
                    cursor: pointer;
                    font-size: 12px;
                    margin-right: 6px;
                    font-weight: 400;
                ">复制</button>
                <button id="close-modal-btn" style="
                    background: #9aa0a6;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    padding: 6px 12px;
                    cursor: pointer;
                    font-size: 12px;
                    font-weight: 400;
                ">关闭</button>
            </div>
        `;
        
        // JSON内容区域
        const jsonContent = document.createElement('pre');
        jsonContent.style.cssText = `
            background: #f8f9fa;
            border: 1px solid #e1e8ed;
            border-radius: 4px;
            padding: 12px;
            margin: 0;
            overflow: auto;
            flex: 1;
            font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
            font-size: 12px;
            line-height: 1.4;
            color: #536471;
            white-space: pre-wrap;
            word-wrap: break-word;
        `;
        jsonContent.textContent = JSON.stringify(data, null, 2);
        
        // 组装模态框
        modalContent.appendChild(modalHeader);
        modalContent.appendChild(jsonContent);
        modalOverlay.appendChild(modalContent);
        document.body.appendChild(modalOverlay);
        
        // 绑定事件
        document.getElementById('close-modal-btn').addEventListener('click', () => {
            modalOverlay.remove();
            existDialog = false;
        });
        
        document.getElementById('copy-json-btn').addEventListener('click', () => {
            navigator.clipboard.writeText(JSON.stringify(data, null, 2)).then(() => {
                showInfoInPanel('JSON数据已复制到剪贴板');
            }).catch(() => {
                showInfoInPanel('复制失败,请手动选择复制');
            });
        });
        
        // 点击背景关闭
        modalOverlay.addEventListener('click', (e) => {
            if (e.target === modalOverlay) {
                modalOverlay.remove();
                existDialog = false;
            }
        });
        
        // ESC键关闭
        const escHandler = (e) => {
            if (e.key === 'Escape') {
                modalOverlay.remove();
                existDialog = false;
                document.removeEventListener('keydown', escHandler);
            }
        };
        document.addEventListener('keydown', escHandler);
    }
    
    // 页面加载完成后初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initScript);
    } else {
        initScript();
    }
    
})();