// ==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();
}
})();