From ff4306a310c098e3308b5e33373f57a4f5d08b35 Mon Sep 17 00:00:00 2001 From: JackWang032 <2522134117@qq.com> Date: Mon, 29 Sep 2025 11:25:44 +0800 Subject: [PATCH 01/25] feat: add MCP services --- app.js | 89 +++ app/controller/mcp.js | 413 +++++++++++++ app/mcp/mcpClient.js | 307 ++++++++++ app/mcp/mcpProcessManager.js | 103 ++++ app/mcp/mcpProxy.js | 230 ++++++++ app/mcp/mcpRequestManager.js | 181 ++++++ app/mcp/mcpSessionManager.js | 136 +++++ app/mcp/transportHandlers.js | 972 +++++++++++++++++++++++++++++++ app/model/mcp_server.js | 234 ++++++++ app/router.js | 43 ++ app/schedule/mcpHealthCheck.js | 39 ++ app/service/mcp.js | 1002 ++++++++++++++++++++++++++++++++ config/config.default.js | 19 +- config/config.local.js | 4 +- env.json | 3 +- package.json | 10 +- sql/doraemon.sql | 33 ++ yarn.lock | 900 +++++++++++++++++++++++++++- 18 files changed, 4692 insertions(+), 26 deletions(-) create mode 100644 app/controller/mcp.js create mode 100644 app/mcp/mcpClient.js create mode 100644 app/mcp/mcpProcessManager.js create mode 100644 app/mcp/mcpProxy.js create mode 100644 app/mcp/mcpRequestManager.js create mode 100644 app/mcp/mcpSessionManager.js create mode 100644 app/mcp/transportHandlers.js create mode 100644 app/model/mcp_server.js create mode 100644 app/schedule/mcpHealthCheck.js create mode 100644 app/service/mcp.js diff --git a/app.js b/app.js index f76c343..7162e1e 100644 --- a/app.js +++ b/app.js @@ -1,5 +1,6 @@ const fs = require('fs'); const utils = require('./app/utils'); +const { MCPProxy } = require('./app/mcp/mcpProxy'); module.exports = class AppBootHook { constructor(app) { @@ -13,6 +14,9 @@ module.exports = class AppBootHook { fs.mkdirSync(cacheDirectory); } + // 启动MCP服务 + await this.startMCPServices(); + // 监听 agent 进程发出的信息并作出反应 app.messenger.on('sendArticleSubscription', (id) => { // create an anonymous context to access service @@ -22,4 +26,89 @@ module.exports = class AppBootHook { }); }); } + + /** + * 启动MCP服务 + */ + async startMCPServices() { + const { app } = this; + const ctx = app.createAnonymousContext(); + + try { + app.logger.info('开始启动MCP服务...'); + + // 获取所有启用的MCP服务器 + const enabledServers = await ctx.model.McpServer.findAll({ + where: { + is_delete: 0, + } + }); + + if (enabledServers.length === 0) { + app.logger.info('没有找到需要启动的MCP服务器'); + return; + } + + app.logger.info(`找到 ${enabledServers.length} 个需要启动的MCP服务器`); + + // 启动每个服务器 + const startPromises = enabledServers.map(async (server) => { + await ctx.service.mcp.startMCPServer(server.server_id).catch(err => { + app.logger.error(`MCP服务器启动失败 [${server.server_id}]:`, err.message); + }); + }); + + await Promise.all(startPromises); + } catch (error) { + app.logger.error('MCP服务启动过程中发生错误:', error); + } + } + + /** + * 构建MCP配置对象 + * @param {Object} server - 数据库中的服务器记录 + * @returns {Object} MCP配置对象 + */ + buildMCPConfig(server) { + const config = { + transport: { + type: server.transport + } + }; + + if (server.transport === 'stdio') { + config.command = server.command; + config.args = server.args || []; + config.env = server.env || {}; + config.cwd = server.deploy_path; // 设置工作目录为部署路径 + } else if (server.transport === 'streamable-http') { + config.httpUrl = server.http_url; + } else if (server.transport === 'sse') { + config.sseUrl = server.sse_url; + } + + return config; + } + + /** + * 应用关闭时的清理工作 + */ + async beforeClose() { + const { app } = this; + + try { + app.logger.info('开始清理MCP服务...'); + + // 获取MCP代理实例并清理 + const mcpProxy = MCPProxy.getInstance(); + await mcpProxy.cleanup(); + + // 销毁单例实例 + MCPProxy.destroyInstance(); + + app.logger.info('MCP服务清理完成'); + } catch (error) { + app.logger.error('MCP服务清理过程中发生错误:', error); + } + } }; diff --git a/app/controller/mcp.js b/app/controller/mcp.js new file mode 100644 index 0000000..babd5b8 --- /dev/null +++ b/app/controller/mcp.js @@ -0,0 +1,413 @@ +const Controller = require('egg').Controller; + +const { MCPProxy } = require('../mcp/mcpProxy'); + +// mcpProxy 将在具体方法中获取,以便传递正确的 logger + +class MCPController extends Controller { + async handleMCPEndpointPost() { + const { ctx } = this; + const { serverId } = ctx.params; + const mcpProxy = MCPProxy.getInstance(ctx.logger); + await mcpProxy.forwardRequest(serverId, ctx.request, ctx.response); + await ctx.service.mcp.incrementUseCount(serverId); + } + + async handleMCPEndpointGet() { + const { ctx } = this; + const { serverId } = ctx.params; + const mcpProxy = MCPProxy.getInstance(ctx.logger); + + await mcpProxy.forwardRequest(serverId, ctx.request, ctx.response); + ctx.respond = false; + } + + async handleMCPEndpointDelete() { + const { app, ctx } = this; + const { serverId } = ctx.params; + const { body } = ctx.request; + const mcpProxy = MCPProxy.getInstance(ctx.logger); + + await mcpProxy.forwardRequest( + serverId, + ctx.request, + ctx.response + ); + } + + async getMCPServerList() { + const { app, ctx } = this; + const params = ctx.query; + const data = await ctx.service.mcp.getMCPServerList(params); + ctx.body = app.utils.response(true, data); + } + + async getMCPServerDetail() { + const { app, ctx } = this; + const { serverId } = ctx.query; + const data = await ctx.service.mcp.getMCPServerDetail(serverId); + ctx.body = app.utils.response(true, data); + } + + async registerMCPServer() { + const { app, ctx } = this; + + try { + // 获取表单数据和文件 + const body = ctx.request.body; + + // 在file模式下,文件信息在body中 + let files = []; + if (ctx.request.files) { + // 如果有files属性,使用它 + files = Array.isArray(ctx.request.files) ? ctx.request.files : [ctx.request.files]; + } else if (body.files) { + // 如果files在body中,使用它 + files = Array.isArray(body.files) ? body.files : [body.files]; + delete body.files; // 从body中移除,避免重复处理 + } + + // 调试日志 + ctx.logger.info( + '注册MCP服务器 - 表单数据:', + JSON.stringify(Object.keys(body), null, 2) + ); + ctx.logger.info('注册MCP服务器 - 文件信息:', { + hasFiles: files.length > 0, + filesCount: files.length, + requestFiles: 'files' in ctx.request, + bodyKeys: Object.keys(body), + }); + + // 处理JSON字段 + if (body.tags && typeof body.tags === 'string') { + try { + body.tags = JSON.parse(body.tags); + } catch (e) { + // 如果解析失败,尝试按数组处理 + if (body.tags.includes(',')) { + body.tags = body.tags.split(',').map((item) => item.trim()); + } + } + } + + if (body.env && typeof body.env === 'string') { + try { + body.env = JSON.parse(body.env); + } catch (e) { + // 解析失败时保持原值 + ctx.logger.warn('环境变量解析失败:', body.env); + } + } + + // 准备数据 + const data = { + ...body, + files: Array.isArray(files) ? files : files ? [files] : [], + }; + + const result = await ctx.service.mcp.registerMCPServer(data); + + // 检查是否自动启动了服务器 + let message = '注册成功'; + if (result.status === 1 && result.transport === 'stdio') { + const status = ctx.service.mcp.getMCPServerStatus(result.server_id); + if (status.status === 'running') { + message = '注册成功并已自动启动'; + } else { + message = '注册成功但启动失败,请手动启动'; + } + } + + ctx.body = app.utils.response(true, result, message); + } catch (error) { + ctx.logger.error('MCP服务器注册失败:', error); + ctx.body = app.utils.response(false, null, error.message); + } + } + + async updateMCPServer() { + const { app, ctx } = this; + + try { + // 获取表单数据和文件 + const body = ctx.request.body; + const { serverId } = body; + + // 在file模式下,文件信息在body中 + let files = []; + if (ctx.request.files) { + // 如果有files属性,使用它 + files = Array.isArray(ctx.request.files) ? ctx.request.files : [ctx.request.files]; + } else if (body.files) { + // 如果files在body中,使用它 + files = Array.isArray(body.files) ? body.files : [body.files]; + delete body.files; // 从body中移除,避免重复处理 + } + + // 调试日志 + ctx.logger.info( + '更新MCP服务器 - 表单数据:', + JSON.stringify(Object.keys(body), null, 2) + ); + ctx.logger.info('更新MCP服务器 - 文件信息:', { + hasFiles: files.length > 0, + filesCount: files.length, + requestFiles: 'files' in ctx.request, + bodyKeys: Object.keys(body), + }); + + // 处理JSON字段 + if (body.tags && typeof body.tags === 'string') { + try { + body.tags = JSON.parse(body.tags); + } catch (e) { + // 如果解析失败,尝试按数组处理 + if (body.tags.includes(',')) { + body.tags = body.tags.split(',').map((item) => item.trim()); + } + } + } + + if (body.env && typeof body.env === 'string') { + try { + body.env = JSON.parse(body.env); + } catch (e) { + // 解析失败时保持原值 + ctx.logger.warn('环境变量解析失败:', body.env); + } + } + + // 准备数据 + const data = { + ...body, + files: Array.isArray(files) ? files : files ? [files] : [], + }; + + const result = await ctx.service.mcp.updateMCPServer(serverId, data); + + // 检查是否重新启动了服务器 + let message = '更新成功'; + if (result.status === 1 && result.transport === 'stdio') { + const status = ctx.service.mcp.getMCPServerStatus(result.server_id); + if (status.status === 'running') { + message = '更新成功并已重新启动'; + } else { + message = '更新成功但启动失败,请手动启动'; + } + } + + ctx.body = app.utils.response(true, result, message); + } catch (error) { + ctx.logger.error('MCP服务器更新失败:', error); + ctx.body = app.utils.response(false, null, error.message); + } + } + + async deleteMCPServer() { + const { app, ctx } = this; + const { serverId } = ctx.query; + await ctx.service.mcp.deleteMCPServer(serverId); + ctx.body = app.utils.response(true, null, '删除成功'); + } + + async incrementUseCount() { + const { app, ctx } = this; + const { serverId } = ctx.request.body; + await ctx.service.mcp.incrementUseCount(serverId); + ctx.body = app.utils.response(true, null, '统计成功'); + } + + async getPopularTags() { + const { app, ctx } = this; + const data = await app.service.mcp.getPopularTags(); + ctx.body = app.utils.response(true, data); + } + + async checkMCPServerHealth() { + const { app, ctx } = this; + const { serverId } = ctx.query; + + try { + const isHealthy = await ctx.service.mcp.checkMCPServerHealth(serverId); + ctx.body = app.utils.response(true, { serverId, healthy: isHealthy }); + } catch (error) { + ctx.logger.error('MCP服务器健康检查失败:', error); + ctx.body = app.utils.response(false, null, error.message); + } + } + + async cleanupFile() { + const { app, ctx } = this; + const { filePath } = ctx.request.body; + + try { + ctx.service.mcp.cleanupFile(filePath); + ctx.body = app.utils.response(true, null, '文件清理成功'); + } catch (error) { + ctx.logger.error('文件清理失败:', error); + ctx.body = app.utils.response(false, null, error.message); + } + } + + async startMCPServer() { + const { app, ctx } = this; + const { serverId } = ctx.request.body; + + try { + await ctx.service.mcp.startMCPServer(serverId); + ctx.body = app.utils.response(true, null, '服务器启动成功'); + } catch (error) { + ctx.logger.error('MCP服务器启动失败:', error); + ctx.body = app.utils.response(false, null, error.message); + } + } + + async stopMCPServer() { + const { app, ctx } = this; + const { serverId } = ctx.request.body; + + try { + await ctx.service.mcp.stopMCPServer(serverId); + ctx.body = app.utils.response(true, null, '服务器停止成功'); + } catch (error) { + ctx.logger.error('MCP服务器停止失败:', error); + ctx.body = app.utils.response(false, null, error.message); + } + } + + async restartMCPServer() { + const { app, ctx } = this; + const { serverId } = ctx.request.body; + + try { + await ctx.service.mcp.restartMCPServer(serverId); + ctx.body = app.utils.response(true, null, '服务器重启成功'); + } catch (error) { + ctx.logger.error('MCP服务器重启失败:', error); + ctx.body = app.utils.response(false, null, error.message); + } + } + + async getMCPServerStatus() { + const { app, ctx } = this; + const { serverId } = ctx.query; + + try { + const status = ctx.service.mcp.getMCPServerStatus(serverId); + ctx.body = app.utils.response(true, status); + } catch (error) { + ctx.logger.error('获取MCP服务器状态失败:', error); + ctx.body = app.utils.response(false, null, error.message); + } + } + + async getAllMCPServerStatus() { + const { app, ctx } = this; + + try { + const statuses = ctx.service.mcp.getAllMCPServerStatus(); + ctx.body = app.utils.response(true, statuses); + } catch (error) { + ctx.logger.error('获取所有MCP服务器状态失败:', error); + ctx.body = app.utils.response(false, null, error.message); + } + } + + async syncMCPServerInfo() { + const { app, ctx } = this; + const { serverId } = ctx.request.body; + + try { + const serverInfo = await ctx.service.mcp.syncMCPServerInfo(serverId); + ctx.body = app.utils.response(true, serverInfo, '服务器信息同步成功'); + } catch (error) { + ctx.logger.error('MCP服务器信息同步失败:', error); + ctx.body = app.utils.response(false, null, error.message); + } + } + + /** + * 手动检查指定服务器健康状态 + */ + async checkMCPServerHealth() { + const { app, ctx } = this; + const { serverId } = ctx.params; + + try { + const healthResult = await ctx.service.mcp.checkMCPServerHealth(serverId); + await ctx.service.mcp.updateServerStatus(serverId, healthResult); + + ctx.body = app.utils.response(true, { + serverId, + ...healthResult, + message: '健康检查完成' + }); + } catch (error) { + ctx.logger.error(`手动健康检查失败 [${serverId}]:`, error); + ctx.body = app.utils.response(false, error.message); + } + } + + /** + * 手动检查所有服务器健康状态 + */ + async checkAllMCPServersHealth() { + const { app, ctx } = this; + + try { + await ctx.service.mcp.checkAllServersHealth(); + + ctx.body = app.utils.response(true, { + message: '所有服务器健康检查已启动,请稍后查看状态' + }); + } catch (error) { + ctx.logger.error('批量健康检查失败:', error); + ctx.body = app.utils.response(false, error.message); + } + } + + /** + * 获取服务器运行状态统计 + */ + async getMCPServerStatusStats() { + const { app, ctx } = this; + + try { + const stats = await ctx.model.McpServer.findAll({ + attributes: [ + 'runtime_status', + [ctx.app.Sequelize.fn('COUNT', '*'), 'count'] + ], + where: { + is_delete: 0, + status: 1 + }, + group: ['runtime_status'] + }); + + // 格式化统计数据 + const statusStats = { + running: 0, + stopped: 0, + error: 0, + unknown: 0, + total: 0 + }; + + stats.forEach(stat => { + const status = stat.get('runtime_status'); + const count = parseInt(stat.get('count')); + statusStats[status] = count; + statusStats.total += count; + }); + + ctx.body = app.utils.response(true, statusStats); + } catch (error) { + ctx.logger.error('获取服务器状态统计失败:', error); + ctx.body = app.utils.response(false, error.message); + } + } + +} +module.exports = MCPController; diff --git a/app/mcp/mcpClient.js b/app/mcp/mcpClient.js new file mode 100644 index 0000000..bbf6df4 --- /dev/null +++ b/app/mcp/mcpClient.js @@ -0,0 +1,307 @@ +const { Client } = require('@modelcontextprotocol/sdk/client/index.js'); +const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js'); +const { StreamableHTTPClientTransport } = require('@modelcontextprotocol/sdk/client/streamableHttp.js'); +const { SSEClientTransport } = require('@modelcontextprotocol/sdk/client/sse.js'); + +/** + * MCP客户端封装类 + * 提供与MCP服务器通信的统一接口 + */ +class MCPClient { + constructor(logger) { + this.logger = logger; + this.client = null; + this.transport = null; + } + + /** + * 创建并连接MCP客户端 + * @param {Object} server - 服务器配置 + * @returns {Promise} 连接是否成功 + */ + async connect(server) { + try { + this.transport = await this.createTransport(server); + + this.client = new Client( + { + name: `doraemon-client-${server.server_id}`, + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + prompts: {}, + resources: {}, + }, + } + ); + + await this.client.connect(this.transport); + + this.logger?.info(`MCP客户端连接成功: ${server.server_id}`); + return true; + } catch (error) { + this.logger?.error(`MCP客户端连接失败 [${server.server_id}]:`, error); + await this.close(); + throw error; + } + } + + /** + * 创建传输层 + * @param {Object} server - 服务器配置 + * @returns {Promise} + */ + async createTransport(server) { + let transport; + + if (server.transport === 'stdio') { + transport = new StdioClientTransport({ + command: server.command, + args: server.args || [], + env: { ...process.env, ...server.env || {} }, + cwd: server.deploy_path, + }); + } else if (server.transport === 'streamable-http') { + transport = new StreamableHTTPClientTransport(new URL(server.http_url)); + } else if (server.transport === 'sse') { + transport = new SSEClientTransport(new URL(server.sse_url)); + } else { + throw new Error(`不支持的传输类型: ${server.transport}`); + } + + return transport; + } + + /** + * 发送ping请求检查服务器状态 + * @param {number} timeout - 超时时间(毫秒) + * @returns {Promise<{healthy: boolean, error?: string}>} + */ + async ping(timeout = 15000) { + if (!this.client) { + return { healthy: false, error: '客户端未连接' }; + } + + try { + const pingId = this.generateRequestId(); + const request = { + jsonrpc: '2.0', + id: pingId, + method: 'ping' + }; + + await this.sendRequest(request, timeout); + return { healthy: true }; + } catch (error) { + return { + healthy: false, + error: `Ping失败: ${error.message}` + }; + } + } + + /** + * 获取服务器工具列表 + * @returns {Promise} + */ + async listTools() { + if (!this.client) { + throw new Error('客户端未连接'); + } + + try { + const response = await this.client.listTools(); + return response.tools || []; + } catch (error) { + this.logger?.warn('获取工具列表失败:', error.message); + return []; + } + } + + /** + * 获取服务器提示词列表 + * @returns {Promise} + */ + async listPrompts() { + if (!this.client) { + throw new Error('客户端未连接'); + } + + try { + const response = await this.client.listPrompts(); + return response.prompts || []; + } catch (error) { + this.logger?.warn('获取提示词列表失败:', error.message); + return []; + } + } + + /** + * 获取服务器资源列表 + * @returns {Promise} + */ + async listResources() { + if (!this.client) { + throw new Error('客户端未连接'); + } + + try { + const response = await this.client.listResources(); + return response.resources || []; + } catch (error) { + this.logger?.warn('获取资源列表失败:', error.message); + return []; + } + } + + /** + * 获取服务器能力信息 + * @returns {Promise} + */ + async getServerCapabilities() { + if (!this.client) { + throw new Error('客户端未连接'); + } + + try { + const capabilities = await this.client.getServerCapabilities(); + return capabilities || {}; + } catch (error) { + this.logger?.warn('获取服务器能力信息失败:', error.message); + return {}; + } + } + + /** + * 获取完整的服务器信息 + * @returns {Promise} + */ + async getServerInfo() { + const serverInfo = { + tools: [], + prompts: [], + resources: [], + capabilities: {}, + }; + + try { + // 并行获取所有信息 + const [tools, prompts, resources, capabilities] = await Promise.allSettled([ + this.listTools(), + this.listPrompts(), + this.listResources(), + this.getServerCapabilities() + ]); + + if (tools.status === 'fulfilled') { + serverInfo.tools = tools.value; + this.logger?.info(`获取到 ${serverInfo.tools.length} 个工具`); + } + + if (prompts.status === 'fulfilled') { + serverInfo.prompts = prompts.value; + this.logger?.info(`获取到 ${serverInfo.prompts.length} 个提示词`); + } + + if (resources.status === 'fulfilled') { + serverInfo.resources = resources.value; + this.logger?.info(`获取到 ${serverInfo.resources.length} 个资源`); + } + + if (capabilities.status === 'fulfilled') { + serverInfo.capabilities = capabilities.value; + this.logger?.info('获取服务器能力信息成功'); + } + + return serverInfo; + } catch (error) { + this.logger?.error('获取MCP服务器信息失败:', error); + throw error; + } + } + + /** + * 发送自定义请求 + * @param {Object} request - JSON-RPC请求对象 + * @param {number} timeout - 超时时间 + * @returns {Promise} + */ + async sendRequest(request, timeout = 15000) { + if (!this.client || !this.transport) { + throw new Error('客户端未连接'); + } + + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + reject(new Error('请求时间超时,请重试')); + }, timeout); + + // 设置消息处理器 + const messageHandler = (message) => { + if (message.id === request.id) { + clearTimeout(timeoutHandle); + + if (message.error) { + reject(new Error(message.error.message || '服务器返回错误')); + } else { + resolve(message.result || {}); + } + } + }; + + // 设置临时消息处理器 + if (this.transport.onmessage) { + const originalHandler = this.transport.onmessage; + this.transport.onmessage = (message) => { + messageHandler(message); + originalHandler(message); + }; + } else { + this.transport.onmessage = messageHandler; + } + + // 发送请求 + this.transport.send(request).catch((error) => { + clearTimeout(timeoutHandle); + reject(error); + }); + }); + } + + /** + * 生成请求ID + * @returns {string} + */ + generateRequestId() { + return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + /** + * 关闭客户端连接 + */ + async close() { + if (this.client) { + try { + await this.client.close(); + this.logger?.info('MCP客户端连接已关闭'); + } catch (error) { + this.logger?.warn('关闭MCP客户端连接失败:', error.message); + } finally { + this.client = null; + this.transport = null; + } + } + } + + /** + * 检查客户端是否已连接 + * @returns {boolean} + */ + isConnected() { + return this.client !== null; + } +} + +module.exports = { MCPClient }; diff --git a/app/mcp/mcpProcessManager.js b/app/mcp/mcpProcessManager.js new file mode 100644 index 0000000..5c020da --- /dev/null +++ b/app/mcp/mcpProcessManager.js @@ -0,0 +1,103 @@ +const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js'); + +/** + * MCP进程管理器 + * 基于MCP SDK管理客户端连接的生命周期 + */ +class MCPProcessManager { + constructor(logger) { + this.mcpTransports = new Map(); + this.serverConfigs = new Map(); + this.logger = logger; + } + + async createStdioProcess(serverId, config) { + if (!config.command) { + throw new Error(`服务器 ${serverId} 的 stdio 传输需要指定 command 字段`); + } + + await this.stopStdioProcess(serverId); + + this.logger?.info(`创建MCP客户端连接: ${serverId}`); + + const transport = new StdioClientTransport({ + command: config.command, + args: config.args || [], + env: { ...process.env, ...(config.env || {}) }, + cwd: config.cwd || process.cwd(), + }); + + this.mcpTransports.set(serverId, transport); + this.serverConfigs.set(serverId, config); + + await transport.start(); + + this.logger?.info(`MCP客户端连接成功: ${serverId}`); + + return transport; + } + + async sendMessage(serverId, message) { + const transport = this.mcpTransports.get(serverId); + if (!transport || !transport.pid) { + this.logger?.error(`MCP客户端 ${serverId} 不存在或未连接`); + return false; + } + + try { + // 解析消息数据 + let messageObject; + if (typeof message === 'string') { + messageObject = JSON.parse(message); + } else { + messageObject = message; + } + + // 使用MCP客户端发送请求 + await transport.send(messageObject); + } catch (error) { + this.logger?.error(`向MCP服务器 ${serverId} 发送消息失败:`, error); + throw error; + } + } + + async stopStdioProcess(serverId) { + const transport = this.mcpTransports.get(serverId); + if (!transport) { + return; + } + + this.logger?.info(`断开MCP客户端连接: ${serverId}`); + + try { + await transport.close(); + } catch (error) { + this.logger?.warn(`关闭MCP客户端连接失败 [${serverId}]:`, error.message); + } + + this.mcpTransports.delete(serverId); + this.serverConfigs.delete(serverId); + } + + isProcessRunning(serverId) { + const transport = this.mcpTransports.get(serverId); + return transport ? !!transport.pid : false; + } + + async restartStdioProcess(serverId) { + const config = this.serverConfigs.get(serverId); + if (!config) { + throw new Error(`服务器 ${serverId} 的配置不存在`); + } + + await this.stopStdioProcess(serverId); + await this.createStdioProcess(serverId, config); + } + + async cleanup() { + const serverIds = Array.from(this.mcpTransports.keys()); + await Promise.all(serverIds.map((serverId) => this.stopStdioProcess(serverId))); + } +} + +module.exports = { MCPProcessManager }; diff --git a/app/mcp/mcpProxy.js b/app/mcp/mcpProxy.js new file mode 100644 index 0000000..d632788 --- /dev/null +++ b/app/mcp/mcpProxy.js @@ -0,0 +1,230 @@ +const { MCPRequestManager } = require('./mcpRequestManager.js'); +const { MCPProcessManager } = require('./mcpProcessManager.js'); +const { + StdioTransportHandler, + SSETransportHandler, + StreamableHttpTransportHandler, +} = require('./transportHandlers.js'); + +class MCPProxy { + constructor(logger = null) { + if (MCPProxy.instance) { + return MCPProxy.instance; + } + + this.logger = logger; + this.requestManager = new MCPRequestManager(); + this.processManager = new MCPProcessManager(logger); + this.transportHandlers = new Map(); + this.connections = new Map(); + + // 传输处理器实例 + this.stdioHandler = new StdioTransportHandler(this.processManager, this.requestManager); + this.sseHandler = new SSETransportHandler(); + this.httpHandler = new StreamableHttpTransportHandler(); + + // 保存实例 + MCPProxy.instance = this; + } + + /** + * 获取单例实例 + * @param {Object} logger - 可选的日志记录器 + * @returns {MCPProxy} + */ + static getInstance(logger = null) { + if (!MCPProxy.instance) { + MCPProxy.instance = new MCPProxy(logger); + } + return MCPProxy.instance; + } + + /** + * 销毁单例实例 + */ + static destroyInstance() { + if (MCPProxy.instance) { + MCPProxy.instance.cleanup(); + MCPProxy.instance = null; + } + } + + async startProxy(serverId, config) { + try { + // 停止已存在的代理 + await this.stopProxy(serverId); + + const transport = config.transport || { type: 'stdio' }; + let handler; + + // 选择合适的传输处理器 + switch (transport.type) { + case 'stdio': + handler = this.stdioHandler; + break; + case 'sse': + handler = this.sseHandler; + break; + case 'streamable-http': + handler = this.httpHandler; + break; + default: + throw new Error(`不支持的传输类型: ${transport.type}`); + } + + // 启动传输处理器 + await handler.start(serverId, config); + + // 记录使用的处理器 + this.transportHandlers.set(serverId, handler); + + console.log(`MCP代理已启动: ${serverId} (${transport.type})`); + } catch (error) { + console.error(`启动MCP代理失败 [${serverId}]:`, error); + throw error; + } + } + + /** + * 停止代理 + * @param {string} serverId - 服务器ID + * @returns {Promise} + */ + async stopProxy(serverId) { + try { + // 停止传输处理器 + const handler = this.transportHandlers.get(serverId); + if (handler) { + await handler.stop(serverId); + this.transportHandlers.delete(serverId); + } + + // 清理连接 + this.connections.delete(serverId); + + } catch (error) { + console.error(`停止MCP代理失败 [${serverId}]:`, error); + throw error; + } + } + + /** + * 处理HTTP请求到MCP的转发 + * @param {string} serverId - 服务器ID + * @param {any} request - 请求对象 + * @param {object} response - 响应对象 + */ + async forwardRequest(serverId, request, response) { + const handler = this.transportHandlers.get(serverId); + if (!handler) { + throw new Error(`服务器 ${serverId} 未运行`); + } + + try { + console.log(`转发请求到服务器 [${serverId}]:`, { + method: request.body.method, + id: request.body.id, + hasParams: !!request.body.params, + }); + + await handler.forward(serverId, request, response); + } catch (error) { + console.error(`请求转发失败 [${serverId}]:`, error); + throw error; + } + } + + getProxyStatus(serverId) { + const handler = this.transportHandlers.get(serverId); + const connections = this.connections.get(serverId)?.size || 0; + + const status = { + serverId, + status: handler?.isRunning(serverId) ? 'running' : 'stopped', + connections, + }; + + return status; + } + + getAllProxyStatus() { + const statuses = {}; + + for (const serverId of this.transportHandlers.keys()) { + statuses[serverId] = this.getProxyStatus(serverId); + } + + return statuses; + } + + /** + * 添加客户端连接 + * @param {string} serverId - 服务器ID + * @param {any} connection - 连接对象 + */ + addConnection(serverId, connection) { + if (!this.connections.has(serverId)) { + this.connections.set(serverId, new Set()); + } + this.connections.get(serverId).add(connection); + console.log( + `添加客户端连接 [${serverId}], 当前连接数: ${this.connections.get(serverId).size}` + ); + } + + /** + * 移除客户端连接 + * @param {string} serverId - 服务器ID + * @param {any} connection - 连接对象 + */ + removeConnection(serverId, connection) { + const connections = this.connections.get(serverId); + if (connections) { + connections.delete(connection); + console.log(`移除客户端连接 [${serverId}], 当前连接数: ${connections.size}`); + + if (connections.size === 0) { + this.connections.delete(serverId); + } + } + } + + async restartProxy(serverId) { + const handler = this.transportHandlers.get(serverId); + if (!handler) { + throw new Error(`服务器 ${serverId} 不存在`); + } + + // 对于STDIO类型,可以重启进程 + if (handler === this.stdioHandler) { + try { + await this.processManager.restartStdioProcess( + serverId, + (data) => this.stdioHandler.handleProcessData(serverId, data), + (error) => this.stdioHandler.handleProcessError(serverId, error), + (code, signal) => this.stdioHandler.handleProcessExit(serverId, code, signal) + ); + console.log(`STDIO服务器重启成功: ${serverId}`); + } catch (error) { + console.error(`STDIO服务器重启失败 [${serverId}]:`, error); + throw error; + } + } else { + throw new Error(`服务器类型不支持重启: ${serverId}`); + } + } + + async cleanup() { + console.log('开始清理所有MCP代理...'); + + const serverIds = Array.from(this.transportHandlers.keys()); + await Promise.all(serverIds.map((serverId) => this.stopProxy(serverId))); + + // 清理底层管理器 + await this.processManager.cleanup(); + + console.log('MCP代理清理完成'); + } +} + +module.exports = { MCPProxy }; diff --git a/app/mcp/mcpRequestManager.js b/app/mcp/mcpRequestManager.js new file mode 100644 index 0000000..cf8b592 --- /dev/null +++ b/app/mcp/mcpRequestManager.js @@ -0,0 +1,181 @@ +/** + * 请求处理接口 + * @typedef {Object} PendingRequest + * @property {string} internalId - 内部唯一ID + * @property {string|number} clientId - 客户端原始ID + * @property {function(any): void} resolve - 成功回调 + * @property {function(Error): void} reject - 失败回调 + * @property {NodeJS.Timeout} timeout - 超时定时器 + * @property {string} buffer - 缓冲区 + */ + +/** + * MCP请求管理器 + * 负责管理待处理请求队列和ID映射 + */ +class MCPRequestManager { + constructor() { + // 每个服务器的待处理请求队列(使用内部ID作为key) + this.pendingRequests = new Map(); + } + + /** + * 初始化服务器的请求队列 + * @param {string} serverId - 服务器ID + */ + initializeServerQueue(serverId) { + if (!this.pendingRequests.has(serverId)) { + this.pendingRequests.set(serverId, new Map()); + } + } + + /** + * 添加待处理请求 + * @param {string} serverId - 服务器ID + * @param {string} internalId - 内部ID + * @param {string|number} clientId - 客户端ID + * @param {function(any): void} resolve - 成功回调 + * @param {function(Error): void} reject - 失败回调 + * @param {number} [timeoutMs=30000] - 超时时间 + */ + addPendingRequest(serverId, internalId, clientId, resolve, reject, timeoutMs = 30000) { + const requestMap = this.pendingRequests.get(serverId); + if (!requestMap) { + throw new Error(`服务器 ${serverId} 的请求队列未初始化`); + } + + // 设置超时 + const timeout = setTimeout(() => { + this.removePendingRequest(serverId, internalId); + reject(new Error(`请求超时: 客户端ID=${clientId}, 内部ID=${internalId}`)); + }, timeoutMs); + + // 创建待处理请求 + const pendingRequest = { + internalId, + clientId, + resolve, + reject, + timeout, + buffer: '', + }; + + // 添加到队列(使用内部ID作为key) + requestMap.set(internalId, pendingRequest); + } + + /** + * 获取待处理请求 + * @param {string} serverId - 服务器ID + * @param {string} internalId - 内部ID + * @returns {PendingRequest|undefined} 待处理请求 + */ + getPendingRequest(serverId, internalId) { + const requestMap = this.pendingRequests.get(serverId); + return requestMap?.get(internalId); + } + + /** + * 移除待处理请求 + * @param {string} serverId - 服务器ID + * @param {string} internalId - 内部ID + * @returns {boolean} 是否成功移除 + */ + removePendingRequest(serverId, internalId) { + const requestMap = this.pendingRequests.get(serverId); + if (!requestMap) { + return false; + } + + const pendingRequest = requestMap.get(internalId); + if (pendingRequest) { + clearTimeout(pendingRequest.timeout); + requestMap.delete(internalId); + return true; + } + return false; + } + + /** + * 根据内部ID查找待处理请求 + * @param {string} serverId - 服务器ID + * @param {string} internalId - 内部ID + * @returns {PendingRequest|null} 待处理请求 + */ + findPendingRequest(serverId, internalId) { + const requestMap = this.pendingRequests.get(serverId); + if (!requestMap) { + return null; + } + + const pendingRequest = requestMap.get(internalId); + if (!pendingRequest) { + console.warn(`未找到待处理请求: ${internalId}`); + return null; + } + + return pendingRequest; + } + + /** + * 清理服务器的所有待处理请求 + * @param {string} serverId - 服务器ID + * @param {Error} [error] - 错误信息 + */ + clearServerRequests(serverId, error) { + const requestMap = this.pendingRequests.get(serverId); + if (!requestMap) { + return; + } + + const defaultError = error || new Error('服务器正在停止'); + + for (const [, pendingRequest] of requestMap.entries()) { + clearTimeout(pendingRequest.timeout); + pendingRequest.reject(defaultError); + } + + requestMap.clear(); + } + + /** + * 删除服务器队列 + * @param {string} serverId - 服务器ID + */ + deleteServerQueue(serverId) { + this.clearServerRequests(serverId); + this.pendingRequests.delete(serverId); + } + + /** + * 生成唯一的请求ID + * @returns {string} 唯一ID + */ + generateRequestId() { + return Date.now().toString() + Math.random().toString(36).substr(2, 9); + } + + /** + * 获取服务器的待处理请求数量 + * @param {string} serverId - 服务器ID + * @returns {number} 待处理请求数量 + */ + getPendingRequestCount(serverId) { + const requestMap = this.pendingRequests.get(serverId); + return requestMap ? requestMap.size : 0; + } + + /** + * 获取所有服务器的待处理请求统计 + * @returns {Record} 统计信息 + */ + getAllPendingRequestStats() { + const stats = {}; + for (const [serverId, requestMap] of this.pendingRequests.entries()) { + stats[serverId] = requestMap.size; + } + return stats; + } +} + +module.exports = { MCPRequestManager }; diff --git a/app/mcp/mcpSessionManager.js b/app/mcp/mcpSessionManager.js new file mode 100644 index 0000000..490e58f --- /dev/null +++ b/app/mcp/mcpSessionManager.js @@ -0,0 +1,136 @@ +const { randomUUID } = require('crypto'); + +/** + * MCP会话管理器 + */ +class MCPSessionManager { + constructor() { + this.sessions = new Map(); + this.SESSION_TIMEOUT = 30 * 60 * 1000; // 30分钟超时 + + // 定期清理过期会话 + setInterval(() => { + this.cleanupExpiredSessions(); + }, 5 * 60 * 1000); // 每5分钟检查一次 + } + + createSession(serverId, protocolVersion = '2025-06-18') { + const sessionId = randomUUID(); + const session = { + id: sessionId, + serverId, + createdAt: Date.now(), + lastActivity: Date.now(), + protocolVersion, + }; + + this.sessions.set(sessionId, session); + console.log(`创建MCP会话: ${sessionId} for server: ${serverId}`); + + return session; + } + + getSession(sessionId) { + const session = this.sessions.get(sessionId); + if (!session) { + return null; + } + + // 检查是否过期 + if (Date.now() - session.lastActivity > this.SESSION_TIMEOUT) { + this.sessions.delete(sessionId); + console.log(`会话已过期: ${sessionId}`); + return null; + } + + // 更新最后活动时间 + session.lastActivity = Date.now(); + return session; + } + + updateSessionActivity(sessionId) { + const session = this.sessions.get(sessionId); + if (session) { + session.lastActivity = Date.now(); + return true; + } + return false; + } + + deleteSession(sessionId) { + const deleted = this.sessions.delete(sessionId); + if (deleted) { + console.log(`删除MCP会话: ${sessionId}`); + } + return deleted; + } + + updateSessionId(oldSessionId, newSessionId) { + const session = this.sessions.get(oldSessionId); + if (session) { + session.id = newSessionId; + this.sessions.delete(oldSessionId); + this.sessions.set(newSessionId, session); + console.log(`更新会话ID: ${oldSessionId} -> ${newSessionId}`); + return true; + } + return false; + } + + getSessionsByServerId(serverId) { + return Array.from(this.sessions.values()).filter( + (session) => session.serverId === serverId + ); + } + + cleanupExpiredSessions() { + const now = Date.now(); + const expiredSessions = []; + + for (const [sessionId, session] of this.sessions.entries()) { + if (now - session.lastActivity > this.SESSION_TIMEOUT) { + expiredSessions.push(sessionId); + } + } + + expiredSessions.forEach((sessionId) => { + this.sessions.delete(sessionId); + console.log(`清理过期会话: ${sessionId}`); + }); + + if (expiredSessions.length > 0) { + console.log(`清理了 ${expiredSessions.length} 个过期会话`); + } + } + + getSessionStats() { + const totalSessions = this.sessions.size; + const sessionsByServer = {}; + let oldestSession = null; + + for (const session of this.sessions.values()) { + sessionsByServer[session.serverId] = (sessionsByServer[session.serverId] || 0) + 1; + + if (oldestSession === null || session.createdAt < oldestSession) { + oldestSession = session.createdAt; + } + } + + return { + totalSessions, + sessionsByServer, + oldestSession, + }; + } + + static isValidProtocolVersion(version) { + const supportedVersions = ['2025-06-18', '2025-03-26', '2024-11-05']; + return supportedVersions.includes(version); + } + + static generateSecureSessionId() { + return randomUUID(); + } +} + +module.exports = { MCPSessionManager }; diff --git a/app/mcp/transportHandlers.js b/app/mcp/transportHandlers.js new file mode 100644 index 0000000..a08a25a --- /dev/null +++ b/app/mcp/transportHandlers.js @@ -0,0 +1,972 @@ +const fetch = require('node-fetch'); +const { SSEClientTransport } = require('@modelcontextprotocol/sdk/client/sse.js'); +const { SSEServerTransport } = require('@modelcontextprotocol/sdk/server/sse.js'); + +const MESSAGE_RESPONSE_TIMEOUT = 30000; + +class BaseTransportHandler { + /** + * 统一的响应处理方法 + * @param {any} response - 响应数据 + * @param {Record} headers - 响应头 + * @param {number} status - 状态码 + * @returns {{response: any, headers: Record, status: number}} 格式化的响应 + */ + formatResponse(response, headers, status) { + return { + response, + headers, + status, + }; + } + + /** + * 启动传输 + * @param {string} serverId - 服务器ID + * @param {object} config - 配置 + */ + start(serverId, config) { + throw new Error('Start Method Not Implemented'); + } + + /** + * 关闭传输 + * @param {string} serverId - 服务器ID + */ + stop(serverId) { + throw new Error('Stop Method Not Implemented'); + } + + /** + * 通用请求路由方法 + * @param {string} serverId - 服务器ID + * @param {any} req - 请求对象 + * @param {object} res - 响应对象 + */ + forward(serverId, req, res) { + throw new Error('Forward Method Not Implemented'); + } +} + +/** + * STDIO传输处理器 + */ +class StdioTransportHandler extends BaseTransportHandler { + constructor(processManager, requestManager) { + super(); + this.processManager = processManager; + this.requestManager = requestManager; + } + + async start(serverId, config) { + if (!config.command) { + throw new Error(`服务器 ${serverId} 的 stdio 传输需要指定 command 字段`); + } + + // 初始化请求队列 + this.requestManager.initializeServerQueue(serverId); + + // 创建进程 + const transport = await this.processManager.createStdioProcess( + serverId, + config, + ); + + transport.onmessage = (message) => { + this.handleProcessData(serverId, message); + }; + + transport.onerror = (error) => { + this.handleProcessError(serverId, error); + }; + + transport.onclose = () => { + this.handleProcessExit(serverId); + }; + } + + async stop(serverId) { + await this.processManager.stopStdioProcess(serverId); + this.requestManager.deleteServerQueue(serverId); + console.log(`STDIO传输处理器已停止: ${serverId}`); + } + + async forward(serverId, req, res) { + const method = req.method?.toUpperCase() || 'POST'; + + switch (method) { + case 'POST': + return this.handlePost(serverId, req, res); + case 'GET': + return this.handleGet(serverId, req, res); + case 'DELETE': + return this.handleDelete(serverId, req, res); + default: + throw new Error(`不支持的HTTP方法: ${method}`); + } + } + + /** + * 处理POST请求 + * @param {string} serverId - 服务器ID + * @param {any} req - 请求数据 + * @param {object} res - 响应数据 + */ + async handlePost(serverId, req, res) { + const isRunning = this.processManager.isProcessRunning(serverId); + if (!isRunning) { + throw new Error(`服务器 ${serverId} 的进程未运行`); + } + + // 确保消息符合JSON-RPC 2.0规范 + const mcpMessage = this.formatMCPMessage(req.body); + const clientId = mcpMessage.id; + + // 生成内部唯一ID + const internalId = this.requestManager.generateRequestId(); + const messageWithInternalId = { ...mcpMessage, id: internalId }; + + console.log( + `STDIO POST请求映射 [${serverId}]: 客户端ID=${clientId} -> 内部ID=${internalId}` + ); + + return new Promise(async (resolve, reject) => { + try { + // 添加到待处理请求队列 + this.requestManager.addPendingRequest( + serverId, + internalId, + clientId, + (response) => { + const formattedResponse = this.formatResponse( + response, + { 'Content-Type': 'application/json' }, + 200 + ); + + // 结束请求 + res.set(formattedResponse.headers); + res.status = formattedResponse.status; + res.body = formattedResponse.response; + resolve(formattedResponse); + }, + reject, + MESSAGE_RESPONSE_TIMEOUT + ); + + console.log( + `发送到STDIO进程 [${serverId}]: 客户端ID=${clientId}, 内部ID=${internalId}` + ); + console.log(`发送消息内容:`, JSON.stringify(messageWithInternalId)); + + // 发送请求到MCP服务器 + await this.processManager.sendMessage( + serverId, + messageWithInternalId + ); + } catch (error) { + this.requestManager.removePendingRequest(serverId, internalId); + reject(error); + } + }); + } + + /** + * 处理GET请求 + * @param {string} serverId - 服务器ID + * @param {any} req - 请求数据 + * @param {object} res - 响应数据 + * @returns {Promise<{response: any, headers?: Record, status: number}>} 响应数据 + */ + async handleGet(serverId, req, res) { + const isRunning = this.processManager.isProcessRunning(serverId); + if (!isRunning) { + throw new Error(`服务器 ${serverId} 的进程未运行`); + } + + // GET请求通常用于查询服务器状态或获取资源信息 + const mcpMessage = { + jsonrpc: '2.0', + id: this.requestManager.generateRequestId(), + method: req.query?.method, + params: req.query || {}, + }; + + const clientId = mcpMessage.id; + const internalId = this.requestManager.generateRequestId(); + const messageWithInternalId = { ...mcpMessage, id: internalId }; + + console.log( + `STDIO GET请求映射 [${serverId}]: 客户端ID=${clientId} -> 内部ID=${internalId}` + ); + + return new Promise(async (resolve, reject) => { + try { + this.requestManager.addPendingRequest( + serverId, + internalId, + clientId, + (response) => { + const formattedResponse = this.formatResponse( + response, + { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + }, + 200 + ); + resolve(formattedResponse); + }, + reject, + MESSAGE_RESPONSE_TIMEOUT + ); + + const success = await this.processManager.sendMessage( + serverId, + messageWithInternalId + ); + if (!success) { + this.requestManager.removePendingRequest(serverId, internalId); + reject(new Error('GET请求发送到进程失败')); + } + } catch (error) { + this.requestManager.removePendingRequest(serverId, internalId); + reject(error); + } + }); + } + + /** + * 处理DELETE请求 + * @param {string} serverId - 服务器ID + * @param {any} req - 请求数据 + * @param {object} res - 响应数据 + * @returns {Promise<{response: any, headers?: Record, status: number}>} 响应数据 + */ + async handleDelete(serverId, req, res) { + const isRunning = this.processManager.isProcessRunning(serverId); + if (!isRunning) { + throw new Error(`服务器 ${serverId} 的进程未运行`); + } + + // DELETE请求用于删除资源 + const mcpMessage = { + jsonrpc: '2.0', + id: this.requestManager.generateRequestId(), + method: req.body?.method || 'delete', + params: { + ...req.body?.params, + resourceId: req.params?.id || req.body?.resourceId, + }, + }; + + const clientId = mcpMessage.id; + const internalId = this.requestManager.generateRequestId(); + const messageWithInternalId = { ...mcpMessage, id: internalId }; + + console.log( + `STDIO DELETE请求映射 [${serverId}]: 客户端ID=${clientId} -> 内部ID=${internalId}` + ); + + return new Promise(async (resolve, reject) => { + try { + this.requestManager.addPendingRequest( + serverId, + internalId, + clientId, + (response) => { + const formattedResponse = this.formatResponse( + response, + { 'Content-Type': 'application/json' }, + 204 // DELETE成功通常返回204 No Content + ); + resolve(formattedResponse); + }, + reject, + MESSAGE_RESPONSE_TIMEOUT + ); + + const success = await this.processManager.sendMessage( + serverId, + messageWithInternalId + ); + if (!success) { + this.requestManager.removePendingRequest(serverId, internalId); + reject(new Error(`${serverId}DELETE请求发送到进程失败`)); + } + } catch (error) { + this.requestManager.removePendingRequest(serverId, internalId); + reject(error); + } + }); + } + + /** + * 检查是否运行 + * @param {string} serverId - 服务器ID + * @returns {boolean} 是否运行中 + */ + isRunning(serverId) { + return this.processManager.isProcessRunning(serverId); + } + + /** + * 处理进程数据 + * @param {string} serverId - 服务器ID + * @param {object} data - 数据 + * @private + */ + handleProcessData(serverId, data) { + let pendingRequest; + + const internalId = data.id; + if (internalId === undefined || internalId === null) { + console.warn(`${serverId}响应缺少ID: ${internalId}`); + return; + } + + pendingRequest = this.requestManager.findPendingRequest(serverId, internalId); + if (!pendingRequest) { + return; + } + + // 恢复客户端原始ID并处理响应 + const clientResponse = { ...data, id: pendingRequest.clientId }; + + if (data.jsonrpc === '2.0') { + pendingRequest.resolve(clientResponse); + } else { + pendingRequest.reject(new Error(`无效的JSON-RPC响应格式`)); + } + + this.requestManager.removePendingRequest(serverId, internalId); + } + + /** + * 处理进程错误 + * @param {string} serverId - 服务器ID + * @param {Error} error - 错误信息 + * @private + */ + handleProcessError(serverId, error) { + this.requestManager.clearServerRequests(serverId, new Error(`${serverId}进程错误: ${error.message}`)); + } + + /** + * 处理进程退出 + * @param {string} serverId - 服务器ID + * @param {number|null} code - 退出代码 + * @param {string|null} signal - 退出信号 + * @private + */ + handleProcessExit(serverId) { + this.requestManager.clearServerRequests( + serverId, + new Error(`${serverId}进程退出`) + ); + } + + /** + * 格式化MCP消息 + * @param {any} data - 原始数据 + * @returns {any} 格式化后的消息 + * @private + */ + formatMCPMessage(data) { + // 如果已经是有效的JSON-RPC消息,直接返回 + if (data && data.jsonrpc === '2.0' && data.method && data.id !== undefined) { + return data; + } + + // 构造符合JSON-RPC 2.0规范的消息 + const mcpMessage = { + jsonrpc: '2.0', + id: data.id !== undefined ? data.id : this.requestManager.generateRequestId(), + method: data.method || 'initialize', + }; + + // 如果有params,添加到消息中 + if (data.params !== undefined) { + mcpMessage.params = data.params; + } + + // 如果是响应消息,添加result或error + if (data.result !== undefined) { + mcpMessage.result = data.result; + } + if (data.error !== undefined) { + mcpMessage.error = data.error; + } + + return mcpMessage; + } +} + +/** + * SSE传输处理器 + */ +class SSETransportHandler extends BaseTransportHandler { + constructor() { + super(); + this.sseProxies = new Map(); + this.clientTransports = new Map(); + this.serverTransports = new Map(); + } + + async start(serverId, config) { + if (!config.sseUrl) { + throw new Error('SSE传输需要提供URL'); + } + + // 暂时创建一个简单的占位符对象 + const mockEventSource = { + url: config.sseUrl, + headers: config.headers || {}, + close: () => {}, + }; + + console.log(`SSE传输处理器启动: ${serverId} -> ${config.sseUrl}`); + this.sseProxies.set(serverId, mockEventSource); + } + + async stop(serverId) { + const sseProxy = this.sseProxies.get(serverId); + if (sseProxy) { + sseProxy.close(); + this.sseProxies.delete(serverId); + console.log(`SSE传输处理器已停止: ${serverId}`); + } + } + + async forward(serverId, req, res) { + const method = req.method?.toUpperCase() || 'GET'; + + switch (method) { + case 'POST': + return this.handlePost(serverId, req, res); + case 'GET': + return this.handleGet(serverId, req, res); + default: + throw new Error(`SSE传输不支持的HTTP方法: ${method}`); + } + } + + /** + * 处理POST请求 + * @param {string} serverId - 服务器ID + * @param {any} req - 请求数据 + * @param {object} res - 响应数据 + */ + async handlePost(serverId, req, res) { + const sseProxy = this.sseProxies.get(serverId); + if (!sseProxy) { + throw new Error(`服务器 ${serverId} 的SSE连接未找到`); + } + + const serverTransport = this.serverTransports.get(req.query.sessionId); + + if (!serverTransport) { + throw new Error(`session ${req.query.sessionId} 不存在`); + } + + try { + // 由于Egg.js已经解析了请求体,我们需要将解析好的body传递给handlePostMessage + await serverTransport.handlePostMessage(req.req, res.res, req.body); + } catch (error) { + console.error(`SSE POST请求处理错误:`, error); + throw error; + } + } + + /** + * 处理GET请求 + * @param {string} serverId - 服务器ID + * @param {any} req - 请求数据 + * @param {object} res - 响应数据 + */ + async handleGet(serverId, req, res) { + const sseProxy = this.sseProxies.get(serverId); + if (!sseProxy) { + throw new Error(`服务器 ${serverId} 的SSE连接未找到`); + } + + let transportToClientClosed = false; + let transportToServerClosed = false; + + const headers = { + ...req.headers, + ...sseProxy.headers, + accept: 'text/event-stream', + }; + + const clientTransport = new SSEClientTransport(new URL(sseProxy.url), { + headers, + }); + + await clientTransport.start(); + const messageEndpoint = 'http://localhost:7001/mcp-endpoint/' + serverId + '/messages'; + + const serverTransport = new SSEServerTransport(messageEndpoint, res.res, { + headers, + }); + await serverTransport.start(); + + this.serverTransports.set(serverTransport.sessionId, serverTransport); + this.clientTransports.set(serverTransport.sessionId, clientTransport); + + serverTransport.onmessage = (message) => clientTransport.send(message); + clientTransport.onmessage = (message) => serverTransport.send(message); + serverTransport.onclose = () => { + if (transportToServerClosed) { + return; + } + + transportToClientClosed = true; + clientTransport.close().catch((e) => console.log(e)); + }; + + clientTransport.onclose = () => { + if (transportToClientClosed) { + return; + } + transportToServerClosed = true; + serverTransport.close().catch((e) => console.log(e)); + }; + + } + + /** + * 检查是否运行 + * @param {string} serverId - 服务器ID + * @returns {boolean} 是否运行中 + */ + isRunning(serverId) { + return this.sseProxies.has(serverId); + } +} + +/** + * Streamable HTTP传输处理器 + */ +class StreamableHttpTransportHandler extends BaseTransportHandler { + constructor() { + super(); + this.httpProxies = new Map(); + } + + async start(serverId, config) { + if (!config.httpUrl) { + throw new Error('Streamable HTTP传输需要提供URL'); + } + + // 验证URL格式 + try { + new URL(config.httpUrl); + } catch (error) { + throw new Error(`无效的URL格式: ${config.httpUrl}`); + } + + // 存储HTTP代理信息 + this.httpProxies.set(serverId, { + url: config.httpUrl, + headers: config.headers || {}, + }); + + console.log(`Streamable HTTP传输处理器启动: ${serverId} -> ${config.httpUrl}`); + } + + async stop(serverId) { + this.httpProxies.delete(serverId); + console.log(`Streamable HTTP传输处理器已停止: ${serverId}`); + } + + async forward(serverId, req, res) { + const method = req.method?.toUpperCase() || 'POST'; + + switch (method) { + case 'POST': + return this.handlePost(serverId, req, res); + case 'GET': + return this.handleGet(serverId, req, res); + case 'DELETE': + return this.handleDelete(serverId, req, res); + default: + throw new Error(`Streamable HTTP传输不支持的HTTP方法: ${method}`); + } + } + + /** + * 处理POST请求 + * @param {string} serverId - 服务器ID + * @param {any} req - 请求数据 + * @param {object} res - 响应数据 + * @returns {Promise<{response: any, headers?: Record, status: number, streamPromise?: Promise}>} 响应数据 + */ + async handlePost(serverId, req, res) { + const httpProxy = this.httpProxies.get(serverId); + if (!httpProxy) { + throw new Error(`服务器 ${serverId} 的HTTP代理未启动`); + } + + try { + console.log(`转发POST请求到Streamable HTTP服务器: ${httpProxy.url}`); + + // 设置POST请求头 + const headers = { + ...req.headers, + ...httpProxy.headers, + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + }; + + // 发送HTTP POST请求 + console.log(`发送POST请求头:`, JSON.stringify(headers, null, 2)); + console.log(`发送POST请求体:`, JSON.stringify(req.body)); + + const response = await fetch(httpProxy.url, { + method: 'POST', + headers, + body: JSON.stringify(req.body), + }); + + return this.processHttpResponse(response, 'POST'); + } catch (error) { + console.error( + `Streamable HTTP POST请求错误:`, + error instanceof Error ? error.message : error + ); + throw new Error( + `Streamable HTTP POST请求失败: ${ + error instanceof Error ? error.message : '未知错误' + }` + ); + } + } + + /** + * 处理GET请求 + * @param {string} serverId - 服务器ID + * @param {any} req - 请求数据 + * @param {object} res - 响应数据 + * @returns {Promise<{response: any, headers?: Record, status: number, streamPromise?: Promise}>} 响应数据 + */ + async handleGet(serverId, req, res) { + const httpProxy = this.httpProxies.get(serverId); + if (!httpProxy) { + throw new Error(`服务器 ${serverId} 的HTTP代理未启动`); + } + + try { + console.log(`转发GET请求到Streamable HTTP服务器: ${httpProxy.url}`); + + // 构建查询参数 + const url = new URL(httpProxy.url); + if (req.query) { + Object.keys(req.query).forEach((key) => { + url.searchParams.append(key, req.query[key]); + }); + } + + // 设置GET请求头 + const headers = { + ...req.headers, + ...httpProxy.headers, + Accept: 'application/json, text/event-stream', + }; + + console.log(`发送GET请求到: ${url.toString()}`); + console.log(`发送GET请求头:`, JSON.stringify(headers, null, 2)); + + const response = await fetch(url.toString(), { + method: 'GET', + headers, + }); + + return this.processHttpResponse(response, 'GET'); + } catch (error) { + console.error( + `Streamable HTTP GET请求错误:`, + error instanceof Error ? error.message : error + ); + throw new Error( + `Streamable HTTP GET请求失败: ${ + error instanceof Error ? error.message : '未知错误' + }` + ); + } + } + + /** + * 处理DELETE请求 + * @param {string} serverId - 服务器ID + * @param {any} req - 请求数据 + * @param {object} res - 响应数据 + * @returns {Promise<{response: any, headers?: Record, status: number, streamPromise?: Promise}>} 响应数据 + */ + async handleDelete(serverId, req, res) { + const httpProxy = this.httpProxies.get(serverId); + if (!httpProxy) { + throw new Error(`服务器 ${serverId} 的HTTP代理未启动`); + } + + try { + console.log(`转发DELETE请求到Streamable HTTP服务器: ${httpProxy.url}`); + + // 构建DELETE URL,通常包含资源ID + let url = httpProxy.url; + if (req.params?.id) { + url = `${url}/${req.params.id}`; + } else if (req.body?.resourceId) { + url = `${url}/${req.body.resourceId}`; + } + + // 设置DELETE请求头 + const headers = { + ...req.headers, + ...httpProxy.headers, + Accept: 'application/json', + }; + + // 如果有请求体,添加Content-Type + if (req.body && Object.keys(req.body).length > 0) { + headers['Content-Type'] = 'application/json'; + } + + console.log(`发送DELETE请求到: ${url}`); + console.log(`发送DELETE请求头:`, JSON.stringify(headers, null, 2)); + + const response = await fetch(url, { + method: 'DELETE', + headers, + body: + req.body && Object.keys(req.body).length > 0 + ? JSON.stringify(req.body) + : undefined, + }); + + return this.processHttpResponse(response, 'DELETE'); + } catch (error) { + console.error( + `Streamable HTTP DELETE请求错误:`, + error instanceof Error ? error.message : error + ); + throw new Error( + `Streamable HTTP DELETE请求失败: ${ + error instanceof Error ? error.message : '未知错误' + }` + ); + } + } + + /** + * 处理HTTP响应的通用方法 + * @param {Response} response - HTTP响应 + * @param {string} method - HTTP方法 + * @returns {Promise<{response: any, headers?: Record, status: number, streamPromise?: Promise}>} 响应数据 + * @private + */ + async processHttpResponse(response, method) { + console.log(`收到${method}响应状态: ${response.status} ${response.statusText}`); + console.log(`响应头:`, Object.fromEntries(response.headers.entries())); + + // 立即提取响应头信息 + const responseHeaders = {}; + response.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + + // 立即返回响应状态和头信息 + const responseInfo = { + status: response.status, + headers: responseHeaders, + }; + + if (!response.ok) { + const errorText = await response.text().catch(() => ''); + console.error(`HTTP ${method}请求失败详情: 状态=${response.status}, 响应=${errorText}`); + throw new Error( + `HTTP ${method}请求失败: ${response.status} ${response.statusText}${ + errorText ? ` - ${errorText}` : '' + }` + ); + } + + const contentType = response.headers.get('content-type') || ''; + console.log(`${method}响应Content-Type: ${contentType}`); + + // 处理不同类型的响应 + if (contentType.includes('text/event-stream')) { + // SSE流响应 - 立即返回响应信息,异步处理流 + console.log(`处理${method} SSE流响应`); + + // 流式转发模式 - 异步处理流数据 + const streamPromise = this.processSSEStreamAsync(response); + return { + response: response, + ...responseInfo, + streamPromise, + }; + } else if (contentType.includes('application/json')) { + // JSON响应 - 需要读取完整响应体 + let responseText = ''; + try { + responseText = await response.text(); + console.log(`收到HTTP ${method}原始响应文本:`, responseText); + + if (!responseText.trim()) { + // 对于DELETE请求,空响应体是正常的 + if (method === 'DELETE') { + return this.formatResponse( + { result: 'Resource deleted successfully' }, + responseInfo.headers, + responseInfo.status + ); + } + throw new Error('响应体为空'); + } + + const responseData = JSON.parse(responseText); + console.log( + `成功解析HTTP ${method} JSON响应:`, + JSON.stringify(responseData, null, 2) + ); + + return this.formatResponse(responseData, responseInfo.headers, responseInfo.status); + } catch (parseError) { + console.error(`解析${method} JSON响应失败:`, parseError); + console.error(`原始响应内容:`, responseText); + throw new Error( + `JSON解析失败: ${ + parseError instanceof Error ? parseError.message : '未知错误' + } - 响应内容: ${responseText}` + ); + } + } else { + // 其他类型响应 + const responseText = await response.text(); + console.log(`收到HTTP ${method}文本响应:`, responseText); + try { + const responseData = JSON.parse(responseText); + return this.formatResponse(responseData, responseInfo.headers, responseInfo.status); + } catch { + return this.formatResponse( + { result: responseText }, + responseInfo.headers, + responseInfo.status + ); + } + } + } + + /** + * 检查是否运行 + * @param {string} serverId - 服务器ID + * @returns {boolean} 是否运行中 + */ + isRunning(serverId) { + return this.httpProxies.has(serverId); + } + + /** + * 异步处理SSE流数据 + * @param {Response} response - HTTP响应 + * @returns {Promise} 流处理Promise + * @private + */ + async processSSEStreamAsync(response) { + const stream = response.body; + if (!stream) { + throw new Error('无法读取SSE流响应'); + } + + let buffer = ''; + + return new Promise((resolve, reject) => { + stream.on('data', (chunk) => { + try { + buffer += chunk.toString(); + + // 处理完整的SSE事件 + const events = this.parseSSEEvents(buffer); + + for (const event of events.complete) { + if (event.data) { + try { + const eventData = JSON.parse(event.data); + console.log(`处理SSE事件:`, JSON.stringify(eventData, null, 2)); + } catch (parseError) { + console.warn(`解析SSE事件数据失败:`, event.data); + } + } + } + + // 更新缓冲区,保留不完整的事件 + buffer = events.incomplete; + } catch (error) { + console.error(`处理SSE数据块失败:`, error); + reject(error); + } + }); + + stream.on('end', () => { + console.log(`SSE流结束`); + resolve(); + }); + + stream.on('error', (error) => { + console.error(`SSE流处理失败:`, error); + reject(error); + }); + }); + } + + /** + * 解析SSE事件 + * @param {string} buffer - 缓冲区内容 + * @returns {{complete: Array<{id?: string, event?: string, data?: string}>, incomplete: string}} 解析结果 + * @private + */ + parseSSEEvents(buffer) { + const lines = buffer.split('\n'); + const complete = []; + let current = {}; + let i = 0; + + while (i < lines.length) { + const line = lines[i].trim(); + + if (line === '') { + // 空行表示事件结束 + if (Object.keys(current).length > 0) { + complete.push(current); + current = {}; + } + } else if (line.startsWith('id: ')) { + current.id = line.slice(4); + } else if (line.startsWith('event: ')) { + current.event = line.slice(7); + } else if (line.startsWith('data: ')) { + current.data = line.slice(6); + } + + i++; + } + + // 检查是否有不完整的事件 + let incomplete = ''; + if (Object.keys(current).length > 0) { + // 重建不完整的事件 + if (current.id) incomplete += `id: ${current.id}\n`; + if (current.event) incomplete += `event: ${current.event}\n`; + if (current.data) incomplete += `data: ${current.data}\n`; + } + + return { complete, incomplete }; + } +} + +module.exports = { + StdioTransportHandler, + SSETransportHandler, + StreamableHttpTransportHandler, +}; diff --git a/app/model/mcp_server.js b/app/model/mcp_server.js new file mode 100644 index 0000000..b7081bc --- /dev/null +++ b/app/model/mcp_server.js @@ -0,0 +1,234 @@ +module.exports = (app) => { + const { STRING, TEXT, INTEGER, ENUM, JSON, DATE, TINYINT } = app.Sequelize; + + const McpServer = app.model.define( + 'mcp_server', + { + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '自增主键', + }, + server_id: { + type: STRING(128), + allowNull: false, + unique: true, + comment: '服务器唯一标识/名称', + }, + title: { + type: STRING(200), + allowNull: false, + comment: '显示标题', + }, + description: { + type: TEXT, + comment: '服务器描述(支持Markdown)', + }, + author: { + type: STRING(100), + allowNull: false, + comment: '创建者', + }, + version: { + type: STRING(20), + allowNull: false, + comment: '版本号', + }, + tags: { + type: JSON, + comment: '标签数组', + }, + transport: { + type: ENUM('stdio', 'sse', 'streamable-http'), + allowNull: false, + defaultValue: 'stdio', + comment: '传输协议类型', + }, + command: { + type: STRING(255), + comment: '启动命令(stdio类型)', + }, + args: { + type: JSON, + comment: '命令参数数组(stdio类型)', + }, + env: { + type: JSON, + comment: '环境变量对象(stdio类型)', + }, + http_url: { + type: STRING(500), + comment: 'HTTP访问地址(http类型)', + }, + sse_url: { + type: STRING(500), + comment: 'SSE访问地址(sse类型)', + }, + git_url: { + type: STRING(500), + comment: 'Git源码地址', + }, + deploy_path: { + type: STRING(500), + comment: '托管部署路径', + }, + status: { + type: ENUM('running', 'stopped', 'error'), + allowNull: false, + defaultValue: 'stopped', + comment: '服务器状态 running-运行中 stopped-已停止 error-错误', + }, + is_delete: { + type: TINYINT, + allowNull: false, + defaultValue: 0, + comment: '是否删除 1-已删除 0-未删除', + }, + use_count: { + type: INTEGER, + allowNull: false, + defaultValue: 0, + comment: '使用次数', + }, + tools: { + type: JSON, + comment: '可用工具列表', + }, + prompts: { + type: JSON, + comment: '可用提示词列表', + }, + resources: { + type: JSON, + comment: '可用资源列表', + }, + capabilities: { + type: JSON, + comment: '服务器能力信息', + }, + last_sync_at: { + type: DATE, + comment: '最后同步时间', + }, + last_ping_at: { + type: DATE, + comment: '最后ping检查时间', + }, + ping_error: { + type: TEXT, + comment: '最后ping检查错误信息', + }, + created_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '创建时间', + }, + updated_at: { + type: DATE, + allowNull: false, + defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'), + comment: '更新时间', + }, + }, + { + freezeTableName: true, + tableName: 'mcp_servers', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + indexes: [ + { + unique: true, + fields: ['server_id'], + }, + { + fields: ['name'], + }, + { + fields: ['author'], + }, + { + fields: ['transport'], + }, + { + fields: ['status', 'is_delete'], + }, + { + fields: ['last_ping_at'], + }, + { + fields: ['created_at'], + }, + ], + } + ); + + // 实例方法 + McpServer.prototype.toJSON = function () { + const values = Object.assign({}, this.get()); + + // 解析JSON字段 + if (values.tags && typeof values.tags === 'string') { + try { + values.tags = JSON.parse(values.tags); + } catch (e) { + values.tags = []; + } + } + + if (values.args && typeof values.args === 'string') { + try { + values.args = JSON.parse(values.args); + } catch (e) { + values.args = []; + } + } + + if (values.env && typeof values.env === 'string') { + try { + values.env = JSON.parse(values.env); + } catch (e) { + values.env = {}; + } + } + + if (values.tools && typeof values.tools === 'string') { + try { + values.tools = JSON.parse(values.tools); + } catch (e) { + values.tools = []; + } + } + + if (values.prompts && typeof values.prompts === 'string') { + try { + values.prompts = JSON.parse(values.prompts); + } catch (e) { + values.prompts = []; + } + } + + if (values.resources && typeof values.resources === 'string') { + try { + values.resources = JSON.parse(values.resources); + } catch (e) { + values.resources = []; + } + } + + if (values.capabilities && typeof values.capabilities === 'string') { + try { + values.capabilities = JSON.parse(values.capabilities); + } catch (e) { + values.capabilities = {}; + } + } + + return values; + }; + + + return McpServer; +}; diff --git a/app/router.js b/app/router.js index 347c1c3..af9c795 100644 --- a/app/router.js +++ b/app/router.js @@ -124,6 +124,49 @@ module.exports = (app) => { app.post('/api/tags/update-tag', app.controller.tagManagement.editTag); app.post('/api/tags/delete-tag', app.controller.tagManagement.deleteTag); + /** + * MCP服务器注册中心路由 + */ + app.get('/api/mcp-servers/list', app.controller.mcp.getMCPServerList); + app.get('/api/mcp-servers/detail', app.controller.mcp.getMCPServerDetail); + app.post('/api/mcp-servers/register', app.controller.mcp.registerMCPServer); + app.put('/api/mcp-servers/update', app.controller.mcp.updateMCPServer); + app.delete('/api/mcp-servers/delete', app.controller.mcp.deleteMCPServer); + app.post('/api/mcp-servers/use', app.controller.mcp.incrementUseCount); + app.get('/api/mcp-servers/tags/popular', app.controller.mcp.getPopularTags); + app.get('/api/mcp-servers/health', app.controller.mcp.checkMCPServerHealth); + app.post('/api/mcp-servers/cleanup-file', app.controller.mcp.cleanupFile); + + // MCP服务器生命周期管理 + app.post('/api/mcp-servers/start', app.controller.mcp.startMCPServer); + app.post('/api/mcp-servers/stop', app.controller.mcp.stopMCPServer); + app.post('/api/mcp-servers/restart', app.controller.mcp.restartMCPServer); + app.get('/api/mcp-servers/status', app.controller.mcp.getMCPServerStatus); + app.get('/api/mcp-servers/status/all', app.controller.mcp.getAllMCPServerStatus); + app.post('/api/mcp-servers/sync-info', app.controller.mcp.syncMCPServerInfo); + + // MCP服务器健康检查路由 + app.post('/api/mcp-servers/health/:serverId', app.controller.mcp.checkMCPServerHealth); + app.post('/api/mcp-servers/health/all', app.controller.mcp.checkAllMCPServersHealth); + app.get('/api/mcp-servers/status/stats', app.controller.mcp.getMCPServerStatusStats); + + // MCP服务器状态切换路由 + + /** + * MCP代理端点 + */ + app.post('/mcp-endpoint/:serverId/mcp', app.controller.mcp.handleMCPEndpointPost); + app.get('/mcp-endpoint/:serverId/mcp', app.controller.mcp.handleMCPEndpointGet); + app.delete('/mcp-endpoint/:serverId/mcp', app.controller.mcp.handleMCPEndpointDelete); + + + app.post('/mcp-endpoint/:serverId/messages', app.controller.mcp.handleMCPEndpointPost); + app.get('/mcp-endpoint/:serverId/sse', app.controller.mcp.handleMCPEndpointGet); + + // app.post('/mcp/:serverId/mcp', app.controller.mcp.handleMCPEndpointPost); + // app.get('/mcp/:serverId/mcp', app.controller.mcp.handleMCPEndpointGet); + // app.delete('/mcp/:serverId/mcp', app.controller.mcp.handleMCPEndpointDelete); + // io.of('/').route('getShellCommand', io.controller.home.getShellCommand) // 暂时close Terminal相关功能 // io.of('/').route('loginServer', io.controller.home.loginServer) diff --git a/app/schedule/mcpHealthCheck.js b/app/schedule/mcpHealthCheck.js new file mode 100644 index 0000000..bed4cd4 --- /dev/null +++ b/app/schedule/mcpHealthCheck.js @@ -0,0 +1,39 @@ +const Subscription = require('egg').Subscription; + +/** + * MCP服务器健康检查定时任务 + * 每小时检查一次所有启用的MCP服务器状态 + */ +class MCPHealthCheck extends Subscription { + /** + * 定时任务配置 + */ + static get schedule() { + return { + cron: '0 0 * * * *', // 每小时的0分钟执行 + type: 'worker', // 指定一个 worker 执行 + immediate: false, // 应用启动后不立即执行 + disable: false, + }; + } + + /** + * 执行定时任务 + */ + async subscribe() { + const { ctx } = this; + + try { + ctx.logger.info('开始执行MCP服务器健康检查定时任务'); + + // 调用服务方法检查所有服务器健康状态 + await ctx.service.mcp.checkAllServersHealth(); + + ctx.logger.info('MCP服务器健康检查定时任务执行完成'); + } catch (error) { + ctx.logger.error('MCP服务器健康检查定时任务执行失败:', error); + } + } +} + +module.exports = MCPHealthCheck; diff --git a/app/service/mcp.js b/app/service/mcp.js new file mode 100644 index 0000000..a090e89 --- /dev/null +++ b/app/service/mcp.js @@ -0,0 +1,1002 @@ +const _ = require('lodash'); +const Service = require('egg').Service; +const path = require('path'); +const fs = require('fs'); +const AdmZip = require('adm-zip'); +const tar = require('tar'); +const env = require('../../env.json'); +const { MCPProxy } = require('../mcp/mcpProxy'); +const { MCPClient } = require('../mcp/mcpClient'); + +class MCPService extends Service { + /** + * 获取MCP服务器列表 + * @param {Object} params - 查询参数 + * @param {Number} params.pageNum - 页码 + * @param {Number} params.pageSize - 每页数量 + * @param {String} params.transport - 传输类型 + * @param {String} params.author - 作者 + * @param {Array} params.tags - 标签数组 + * @param {String} params.keyword - 搜索关键词 + */ + async getMCPServerList(params = {}) { + const { + pageNum = 1, + pageSize = 20, + transport, + author, + tags, + keyword, + showAll = false, + } = params; + const offset = (pageNum - 1) * pageSize; + + const where = { + is_delete: 0, + }; + + if (!showAll) { + where.status = { + [this.app.Sequelize.Op.not]: 'stopped', + }; + } + + if (transport) { + where.transport = transport; + } + + if (author) { + where.author = { + [this.app.Sequelize.Op.like]: `%${author}%`, + }; + } + + if (keyword) { + where[this.app.Sequelize.Op.or] = [ + { server_id: { [this.app.Sequelize.Op.like]: `%${keyword}%` } }, + { title: { [this.app.Sequelize.Op.like]: `%${keyword}%` } }, + { description: { [this.app.Sequelize.Op.like]: `%${keyword}%` } }, + ]; + } + + if (tags && tags.length > 0) { + where[this.app.Sequelize.Op.and] = tags.map((tag) => + this.app.Sequelize.literal(`JSON_CONTAINS(tags, '"${tag}"')`) + ); + } + + const result = await this.ctx.model.McpServer.findAndCountAll({ + where, + limit: parseInt(pageSize), + offset: offset, + order: [['created_at', 'DESC']], + attributes: { + // 列表中不返回敏感的环境变量信息 + exclude: ['env'], + }, + }); + + return { + list: result.rows, + total: result.count, + pageNum: parseInt(pageNum), + pageSize: parseInt(pageSize), + }; + } + + /** + * 根据服务器ID获取详情 + * @param {String} serverId - 服务器ID + */ + async getMCPServerDetail(serverId) { + const server = await this.findByServerId(serverId); + if (!server) { + this.ctx.throw(400, 'MCP服务器不存在'); + } + return server; + } + + /** + * 注册MCP服务器 + * @param {Object} data - 服务器数据 + */ + async registerMCPServer(data) { + const { + serverId, + title, + description, + author, + version, + tags, + transport, + command, + args, + env, + httpUrl, + sseUrl, + gitUrl, + files, + } = data; + + // 检查名称是否已存在 + const existingServer = await this.ctx.model.McpServer.findOne({ + where: { + server_id: serverId, + is_delete: 0, + }, + }); + + if (existingServer) { + this.ctx.throw(400, 'MCP服务器名称已存在'); + } + + // 处理文件上传 + let filePath = null; + let deployPath = null; + + if (transport === 'stdio') { + deployPath = this.generateDeployPath(serverId); + + this.ctx.logger.info( + `MCP服务注册 - transport: ${transport}, files数量: ${files ? files.length : 0}` + ); + + if (files && files.length > 0) { + this.ctx.logger.info(`开始处理文件上传,文件信息:`, { + filename: files[0].filename || files[0].originalFilename || 'unknown', + filepath: files[0].filepath || files[0].path || 'unknown', + size: files[0].size || 'unknown', + }); + + filePath = await this.handleFileUpload(files[0], serverId); + + // 文件解压完成后清理临时文件 + setTimeout(() => { + this.cleanupFile(filePath); + }, 5000); // 5秒后清理临时文件 + } else { + this.ctx.logger.info('MCP服务注册 - 没有文件需要处理'); + } + } + + // 处理参数数组 + let parsedArgs = null; + if (args && typeof args === 'string') { + parsedArgs = args.split('\n').filter((arg) => arg.trim()); + } else if (Array.isArray(args)) { + parsedArgs = args; + } + + // 处理环境变量 + let parsedEnv = null; + if (env && Array.isArray(env)) { + parsedEnv = {}; + env.forEach((item) => { + if (item.key && item.value) { + parsedEnv[item.key] = item.value; + } + }); + } + + // 创建服务器记录 + const serverData = { + server_id: serverId, + title, + description, + author, + version, + tags: Array.isArray(tags) ? tags : [], + transport, + git_url: gitUrl, + deploy_path: deployPath, + }; + + // 根据传输类型设置特定字段 + if (transport === 'stdio') { + serverData.command = command; + serverData.args = parsedArgs; + serverData.env = parsedEnv; + } else if (transport === 'streamable-http') { + serverData.http_url = httpUrl; + } else if (transport === 'sse') { + serverData.sse_url = sseUrl; + } + + const server = await this.ctx.model.McpServer.create(serverData); + + try { + await this.startMCPServer(server.server_id); + this.ctx.logger.info(`MCP服务器注册成功并已启动: ${server.server_id}`); + + // 启动成功后,自动获取服务器信息 + setTimeout(async () => { + try { + await this.syncMCPServerInfo(server.server_id); + this.ctx.logger.info(`MCP服务器信息同步完成: ${server.server_id}`); + } catch (syncError) { + this.ctx.logger.error( + `MCP服务器信息同步失败 [${server.server_id}]:`, + syncError.message + ); + } + }, 2000); // 等待2秒后同步,确保服务器已完全启动 + } catch (error) { + this.ctx.logger.error( + `MCP服务器注册成功但启动失败 [${server.server_id}]:`, + error.message + ); + } + + return server; + } + + /** + * 更新MCP服务器 + * @param {String} serverId - 服务器ID + * @param {Object} data - 更新数据 + */ + async updateMCPServer(serverId, data) { + const server = await this.findByServerId(serverId); + if (!server) { + this.ctx.throw(404, 'MCP服务器不存在'); + } + + const { files, ...updateData } = data; + let needsRestart = false; // 标记是否需要重启服务器 + + // 处理文件上传(仅对stdio类型且有文件的情况) + let filePath = null; + let deployPath = server.deploy_path; + + if (server.transport === 'stdio' && files && files.length > 0) { + this.ctx.logger.info( + `MCP服务更新 - transport: ${server.transport}, files数量: ${files.length}` + ); + + // 如果服务器正在运行,先停止它 + if (server.status === 'running') { + try { + await this.stopMCPServer(server.server_id); + this.ctx.logger.info(`MCP服务器更新前已停止: ${server.server_id}`); + needsRestart = true; // 标记需要重启 + } catch (error) { + this.ctx.logger.error( + `MCP服务器更新前停止失败 [${server.server_id}]:`, + error.message + ); + } + } + + // 清理旧的部署目录 + if (deployPath && fs.existsSync(deployPath)) { + try { + fs.rmSync(deployPath, { recursive: true, force: true }); + this.ctx.logger.info(`旧部署目录已清理: ${deployPath}`); + } catch (error) { + this.ctx.logger.error(`清理旧部署目录失败: ${deployPath}`, error); + } + } + + this.ctx.logger.info(`开始处理文件上传,文件信息:`, { + filename: files[0].filename || files[0].originalFilename || 'unknown', + filepath: files[0].filepath || files[0].path || 'unknown', + size: files[0].size || 'unknown', + }); + + filePath = await this.handleFileUpload(files[0], serverId); + + // 文件解压完成后清理临时文件 + setTimeout(() => { + this.cleanupFile(filePath); + }, 5000); // 5秒后清理临时文件 + } + + // 处理特殊字段 + if (updateData.args && typeof updateData.args === 'string') { + updateData.args = updateData.args.split('\n').filter((arg) => arg.trim()); + } + + if (updateData.env && Array.isArray(updateData.env)) { + const envObj = {}; + updateData.env.forEach((item) => { + if (item.key && item.value) { + envObj[item.key] = item.value; + } + }); + updateData.env = envObj; + } + + // 检测关键配置变更,如果有变更且服务器在运行,需要重启 + if (server.status === 'running' && server.transport === 'stdio' && !needsRestart) { + const keyConfigFields = ['command', 'args', 'env']; + needsRestart = keyConfigFields.some((field) => { + if (updateData[field] !== undefined) { + const oldValue = JSON.stringify(server[field]); + const newValue = JSON.stringify(updateData[field]); + return oldValue !== newValue; + } + return false; + }); + + if (needsRestart) { + try { + await this.stopMCPServer(server.server_id); + this.ctx.logger.info(`MCP服务器配置变更前已停止: ${server.server_id}`); + } catch (error) { + this.ctx.logger.error( + `MCP服务器配置变更前停止失败 [${server.server_id}]:`, + error.message + ); + } + } + } + + const oldStatus = server.status; + await server.update(updateData); + + // 重新获取更新后的服务器信息 + await server.reload(); + + // 如果需要重启且是stdio类型,则重启服务器 + if (needsRestart && server.transport === 'stdio') { + try { + await this.startMCPServer(server.server_id); + this.ctx.logger.info(`MCP服务器更新成功并已启动: ${server.server_id}`); + + // 启动成功后,自动获取服务器信息 + setTimeout(async () => { + try { + await this.syncMCPServerInfo(server.server_id); + this.ctx.logger.info(`MCP服务器信息同步完成: ${server.server_id}`); + } catch (syncError) { + this.ctx.logger.error( + `MCP服务器信息同步失败 [${server.server_id}]:`, + syncError.message + ); + } + }, 2000); // 等待2秒后同步,确保服务器已完全启动 + } catch (error) { + this.ctx.logger.error( + `MCP服务器更新成功但启动失败 [${server.server_id}]:`, + error.message + ); + } + } + + return server; + } + + /** + * 删除MCP服务器(软删除) + * @param {String} serverId - 服务器ID + */ + async deleteMCPServer(serverId) { + const server = await this.findByServerId(serverId); + if (!server) { + this.ctx.throw(404, 'MCP服务器不存在'); + } + + // 如果服务器正在运行,先停止它 + if (server.status === 'running') { + try { + await this.stopMCPServer(serverId); + this.ctx.logger.info(`MCP服务器删除时已停止: ${serverId}`); + } catch (error) { + this.ctx.logger.error(`MCP服务器删除时停止失败 [${serverId}]:`, error.message); + } + } + + await server.update({ is_delete: 1 }); + return true; + } + + /** + * 增加使用次数 + * @param {String} serverId - 服务器ID + */ + async incrementUseCount(serverId) { + return await this.ctx.model.McpServer.increment('use_count', { + where: { + server_id: serverId, + is_delete: 0, + }, + }); + } + + /** + * 获取热门标签 + */ + async getPopularTags() { + const servers = await this.ctx.model.McpServer.findAll({ + where: { + is_delete: 0, + status: 1, + }, + attributes: ['tags'], + }); + + const tagCount = {}; + servers.forEach((server) => { + if (server.tags && Array.isArray(server.tags)) { + server.tags.forEach((tag) => { + tagCount[tag] = (tagCount[tag] || 0) + 1; + }); + } + }); + + // 按使用次数排序,返回前10个 + return Object.entries(tagCount) + .sort(([, a], [, b]) => b - a) + .slice(0, 10) + .map(([tag, count]) => ({ tag, count })); + } + + /** + * 生成部署路径 + * @param {String} serverId - 服务器名称 + */ + generateDeployPath(serverId) { + const deployRoot = env.mcpDeployDir || '/opt/doraemon/mcp-servers/'; + return path.join(deployRoot, serverId); + } + + /** + * 处理文件上传 + * @param {Object} file - 上传的文件对象 (egg-multipart file模式) + * @param {String} serverId - 服务器名称 + */ + async handleFileUpload(file, serverId) { + const deployPath = this.generateDeployPath(serverId); + + // 确保部署目录存在 + if (!fs.existsSync(deployPath)) { + fs.mkdirSync(deployPath, { recursive: true }); + } + + // 在file模式下,文件已经保存到临时目录 + const tempFilePath = file.filepath || file.path; + const fileName = file.filename || file.originalFilename || file.name; + + if (!tempFilePath) { + throw new Error('无法获取上传文件的临时路径'); + } + + if (!fs.existsSync(tempFilePath)) { + throw new Error(`临时文件不存在: ${tempFilePath}`); + } + + try { + this.ctx.logger.info( + `处理上传文件: ${fileName}, 临时路径: ${tempFilePath}, 文件大小: ${ + file.size || 'unknown' + } bytes` + ); + + // 解压文件到部署目录 + await this.extractFile(tempFilePath, deployPath); + + this.ctx.logger.info(`文件解压成功: ${tempFilePath} -> ${deployPath}`); + return tempFilePath; + } catch (error) { + this.ctx.logger.error('文件上传处理失败:', error); + throw new Error('文件上传处理失败: ' + error.message); + } + } + + /** + * 解压文件到指定目录 + * @param {String} filePath - 文件路径 + * @param {String} extractPath - 解压目录 + */ + async extractFile(filePath, extractPath) { + const fileExtension = path.extname(filePath).toLowerCase(); + + try { + if (fileExtension === '.zip') { + await this.extractZip(filePath, extractPath); + } else if ( + fileExtension === '.tar' || + fileExtension === '.gz' || + fileExtension === '.tgz' + ) { + await this.extractTar(filePath, extractPath); + } else { + throw new Error(`不支持的文件格式: ${fileExtension}`); + } + + this.ctx.logger.info(`文件解压成功: ${filePath} -> ${extractPath}`); + } catch (error) { + this.ctx.logger.error('文件解压失败:', error); + throw error; + } + } + + /** + * 解压ZIP文件 + * @param {String} filePath - ZIP文件路径 + * @param {String} extractPath - 解压目录 + */ + async extractZip(filePath, extractPath) { + return new Promise((resolve, reject) => { + try { + const zip = new AdmZip(filePath); + zip.extractAllTo(extractPath, true); + resolve(); + } catch (error) { + reject(error); + } + }); + } + + /** + * 解压TAR文件 + * @param {String} filePath - TAR文件路径 + * @param {String} extractPath - 解压目录 + */ + async extractTar(filePath, extractPath) { + return tar.x({ + file: filePath, + cwd: extractPath, + }); + } + + /** + * 清理临时文件 + * @param {String} filePath - 文件路径 + */ + cleanupFile(filePath) { + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + this.ctx.logger.info(`临时文件清理成功: ${filePath}`); + } + } catch (error) { + this.ctx.logger.error('临时文件清理失败:', error); + } + } + + /** + * 检查MCP服务器是否正常运行 + * @param {String} serverId - 服务器ID + */ + async checkMCPServerHealth(serverId) { + const server = await this.findByServerId(serverId); + if (!server) { + throw new Error('MCP服务器不存在'); + } + + // 根据传输类型进行健康检查 + if (server.transport === 'stdio') { + return await this.checkStdioServerHealth(server); + } else if (server.transport === 'streamable-http') { + return await this.checkHttpServerHealth(server); + } else if (server.transport === 'sse') { + return await this.checkSSEServerHealth(server); + } + + return false; + } + + /** + * 检查STDIO类型服务器健康状态 + * @param {Object} server - 服务器配置 + */ + async checkStdioServerHealth(server) { + try { + // 检查部署路径是否存在 + if (!server.deploy_path || !fs.existsSync(server.deploy_path)) { + return { healthy: false, error: '部署路径不存在' }; + } + + // 检查启动命令和参数 + if (!server.command) { + return { healthy: false, error: '启动命令未配置' }; + } + + // 使用ping检查MCP服务器是否响应 + const pingResult = await this.pingMCPServer(server); + return pingResult; + } catch (error) { + this.ctx.logger.error('STDIO服务器健康检查失败:', error); + return { healthy: false, error: error.message }; + } + } + + /** + * 检查HTTP类型服务器健康状态 + * @param {Object} server - 服务器配置 + */ + async checkHttpServerHealth(server) { + try { + if (!server.http_url) { + return { healthy: false, error: 'HTTP URL未配置' }; + } + + // 使用ping检查HTTP MCP服务器 + const pingResult = await this.pingMCPServer(server); + return pingResult; + } catch (error) { + this.ctx.logger.error('HTTP服务器健康检查失败:', error); + return { healthy: false, error: error.message }; + } + } + + /** + * 检查SSE类型服务器健康状态 + * @param {Object} server - 服务器配置 + */ + async checkSSEServerHealth(server) { + try { + if (!server.sse_url) { + return { healthy: false, error: 'SSE URL未配置' }; + } + + // 使用ping检查SSE MCP服务器 + const pingResult = await this.pingMCPServer(server); + return pingResult; + } catch (error) { + this.ctx.logger.error('SSE服务器健康检查失败:', error); + return { healthy: false, error: error.message }; + } + } + + /** + * 获取MCP服务器状态 + * @param {String} serverId - 服务器ID + */ + getMCPServerStatus(serverId) { + const mcpProxy = MCPProxy.getInstance(this.ctx.logger); + return mcpProxy.getProxyStatus(serverId); + } + + /** + * 获取所有MCP服务器状态 + */ + getAllMCPServerStatus() { + const mcpProxy = MCPProxy.getInstance(this.ctx.logger); + return mcpProxy.getAllProxyStatus(); + } + + /** + * 构建MCP配置对象(从app.js中复制的逻辑) + * @param {Object} server - 数据库中的服务器记录 + * @returns {Object} MCP配置对象 + */ + buildMCPConfig(server) { + const config = { + transport: { + type: server.transport, + }, + }; + + if (server.transport === 'stdio') { + config.command = server.command; + config.args = server.args || []; + config.env = server.env || {}; + config.cwd = server.deploy_path; // 设置工作目录为部署路径 + } else if (server.transport === 'streamable-http') { + config.httpUrl = server.http_url; + } else if (server.transport === 'sse') { + config.sseUrl = server.sse_url; + } + + return config; + } + + /** + * 同步MCP服务器信息(tools、prompts、resources) + * @param {String} serverId - 服务器ID + */ + async syncMCPServerInfo(serverId) { + const server = await this.findByServerId(serverId); + if (!server) { + throw new Error('MCP服务器不存在'); + } + + const { healthy } = await this.pingMCPServer(server); + await server.update({ + status: healthy ? 'running' : 'error', + }) + + const mcpClient = new MCPClient(this.ctx.logger); + + try { + // 连接到MCP服务器 + const connected = await mcpClient.connect(server); + if (!connected) { + throw new Error('无法创建MCP客户端连接'); + } + + // 获取服务器信息 + const serverInfo = await mcpClient.getServerInfo(); + + // 更新数据库记录 + await server.update({ + tools: serverInfo.tools || [], + prompts: serverInfo.prompts || [], + resources: serverInfo.resources || [], + capabilities: serverInfo.capabilities || {}, + last_sync_at: new Date(), + }); + + this.ctx.logger.info(`MCP服务器信息同步成功: ${serverId}`, { + toolsCount: serverInfo.tools?.length || 0, + promptsCount: serverInfo.prompts?.length || 0, + resourcesCount: serverInfo.resources?.length || 0, + }); + + return serverInfo; + } catch (error) { + this.ctx.logger.error(`MCP服务器信息同步失败 [${serverId}]:`, error); + throw error; + } finally { + // 关闭客户端连接 + await mcpClient.close(); + } + } + + /** + * 使用MCP ping协议检查服务器状态 + * @param {Object} server - 服务器配置 + * @returns {Promise<{healthy: boolean, error?: string}>} + */ + async pingMCPServer(server) { + const mcpClient = new MCPClient(this.ctx.logger); + + try { + // 连接到MCP服务器 + await mcpClient.connect(server); + // 发送ping请求 + const result = await mcpClient.ping(15000); + // 关闭连接 + await mcpClient.close(); + return result; + } catch (error) { + // 确保连接关闭 + await mcpClient.close(); + return { healthy: false, error: '无法创建MCP客户端连接: ' + error.message } + } + } + + async updateServerStatus(serverId, healthResult) { + const server = await this.findByServerId(serverId); + if (!server) { + return; + } + + const updateData = { + status: healthResult.healthy ? 'running' : healthResult.error ? 'error' : 'stopped', + last_ping_at: new Date(), + ping_error: healthResult.error || null, + }; + + await server.update(updateData); + + this.ctx.logger.info(`服务器状态已更新 [${serverId}]:`, { + status: updateData.status, + error: updateData.ping_error, + }); + } + + /** + * 批量检查所有服务器状态 + */ + async checkAllServersHealth() { + const servers = await this.ctx.model.McpServer.findAll({ + where: { + is_delete: 0, + }, + }); + + this.ctx.logger.info(`开始检查 ${servers.length} 个MCP服务器的健康状态`); + + for (const server of servers) { + try { + const healthResult = await this.checkMCPServerHealth(server.server_id); + await this.updateServerStatus(server.server_id, healthResult); + } catch (error) { + this.ctx.logger.error(`检查服务器状态失败 [${server.server_id}]:`, error); + await this.updateServerStatus(server.server_id, { + healthy: false, + error: error.message, + }); + } + } + + this.ctx.logger.info(`完成所有MCP服务器健康状态检查`); + } + + /** + * 根据服务器ID查找服务器(辅助方法) + * @param {String} serverId - 服务器ID + * @returns {Promise} 服务器对象或null + */ + async findByServerId(serverId) { + return await this.ctx.model.McpServer.findOne({ + where: { + server_id: serverId, + is_delete: 0, + }, + }); + } + + /** + * 启动MCP服务器(合并状态管理) + * @param {string} serverId 服务器ID + */ + async startMCPServer(serverId) { + const server = await this.findByServerId(serverId); + if (!server) { + this.ctx.throw(404, 'MCP服务器不存在'); + } + + this.ctx.logger.info(`开始启动MCP服务器: ${serverId}`); + + // 构建配置对象 + const config = this.buildMCPConfig(server); + + try { + // 获取MCP代理实例并启动服务器 + const mcpProxy = MCPProxy.getInstance(this.ctx.logger); + await mcpProxy.startProxy(serverId, config); + + // 更新状态为运行中 + await this.ctx.model.McpServer.update( + { + status: 'running', + ping_error: null, + last_ping_at: new Date(), + }, + { where: { server_id: serverId } } + ); + + this.ctx.logger.info(`MCP服务器启动成功: ${serverId}`); + + // 检查服务器状态 + setTimeout(async () => { + try { + const healthResult = await this.checkMCPServerHealth(serverId); + if (!healthResult.healthy) { + await this.ctx.model.McpServer.update( + { + status: 'error', + ping_error: healthResult.error, + last_ping_at: new Date(), + }, + { where: { server_id: serverId } } + ); + } + this.ctx.logger.info(`服务器启动后状态检查完成: ${serverId}`); + } catch (error) { + this.ctx.logger.error(`服务器启动后状态检查失败 [${serverId}]:`, error); + } + }, 2000); // 等待2秒后检查,确保服务器已完全启动 + + return true; + } catch (error) { + // 启动失败,更新状态为错误 + await this.ctx.model.McpServer.update( + { + status: 'error', + ping_error: error.message, + last_ping_at: new Date(), + }, + { where: { server_id: serverId } } + ); + + this.ctx.logger.error(`MCP服务器启动失败 [${serverId}]:`, error); + throw error; + } + } + + /** + * 停止MCP服务器(合并状态管理) + * @param {string} serverId 服务器ID + */ + async stopMCPServer(serverId) { + const server = await this.findByServerId(serverId); + if (!server) { + this.ctx.throw(404, 'MCP服务器不存在'); + } + + this.ctx.logger.info(`开始停止MCP服务器: ${serverId}`); + + try { + const mcpProxy = MCPProxy.getInstance(this.ctx.logger); + await mcpProxy.stopProxy(serverId); + + // 更新状态为已停止 + await this.ctx.model.McpServer.update( + { + status: 'stopped', + ping_error: null, + last_ping_at: new Date(), + }, + { where: { server_id: serverId } } + ); + + this.ctx.logger.info(`MCP服务器停止成功: ${serverId}`); + return true; + } catch (error) { + // 停止失败,更新状态为错误 + await this.ctx.model.McpServer.update( + { + status: 'error', + ping_error: error.message, + last_ping_at: new Date(), + }, + { where: { server_id: serverId } } + ); + + this.ctx.logger.error(`MCP服务器停止失败 [${serverId}]:`, error); + throw error; + } + } + + /** + * 重启MCP服务器(合并状态管理) + * @param {string} serverId 服务器ID + */ + async restartMCPServer(serverId) { + const server = await this.findByServerId(serverId); + if (!server) { + this.ctx.throw(404, 'MCP服务器不存在'); + } + + this.ctx.logger.info(`开始重启MCP服务器: ${serverId}`); + + try { + const mcpProxy = MCPProxy.getInstance(this.ctx.logger); + await mcpProxy.restartProxy(serverId); + + // 更新状态为运行中 + await this.ctx.model.McpServer.update( + { + status: 'running', + ping_error: null, + last_ping_at: new Date(), + }, + { where: { server_id: serverId } } + ); + + this.ctx.logger.info(`MCP服务器重启成功: ${serverId}`); + + // 立即检查服务器状态 + setTimeout(async () => { + try { + const healthResult = await this.checkMCPServerHealth(serverId); + if (!healthResult.healthy) { + await this.ctx.model.McpServer.update( + { + status: 'error', + ping_error: healthResult.error || '健康检查失败', + last_ping_at: new Date(), + }, + { where: { server_id: serverId } } + ); + } + this.ctx.logger.info(`服务器重启后状态检查完成: ${serverId}`); + } catch (error) { + this.ctx.logger.error(`服务器重启后状态检查失败 [${serverId}]:`, error); + } + }, 2000); // 等待2秒后检查,确保服务器已完全重启 + + return true; + } catch (error) { + // 重启失败,更新状态为错误 + await this.ctx.model.McpServer.update( + { + status: 'error', + ping_error: error.message, + last_ping_at: new Date(), + }, + { where: { server_id: serverId } } + ); + + this.ctx.logger.error(`MCP服务器重启失败 [${serverId}]:`, error); + throw error; + } + } +} + +module.exports = MCPService; diff --git a/config/config.default.js b/config/config.default.js index dd404c9..558553f 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -16,6 +16,7 @@ module.exports = (app) => { exports.cacheDirectory = path.join(__dirname, '../cache'); exports.bodyParser = { ignore: [/^\/proxy/], + enableTypes: ['json', 'form', 'text'], }; exports.logger = { consoleLevel: 'DEBUG', @@ -55,10 +56,24 @@ module.exports = (app) => { exports.multipart = { // 文件上传 - fileSize: '50mb', - mode: 'stream', + fileSize: '100mb', + mode: 'file', // 使用文件模式,直接保存到临时文件 + fileExtensions: ['.zip', '.tar', '.gz', '.tgz'], // 允许的文件扩展名 + tmpdir: path.join(app.baseDir, 'cache/uploads'), // 临时文件目录 + cleanSchedule: { + // 清理上传的临时文件 + cron: '0 30 4 * * *', // 每天4:30清理 + }, + whitelist: [ + // 允许的文件类型 + '.zip', + '.tar', + '.gz', + '.tgz' + ] }; + exports.io = { init: {}, // passed to engine.io namespace: { diff --git a/config/config.local.js b/config/config.local.js index 0ab31f7..690fc15 100644 --- a/config/config.local.js +++ b/config/config.local.js @@ -34,12 +34,12 @@ module.exports = () => { { delegate: 'model', baseDir: 'model', - database: 'doraemon_test', + database: 'doraemon', dialect: 'mysql', host: '127.0.0.1', port: 3306, username: 'root', - password: '123456', + password: '123456789', }, ], }; diff --git a/env.json b/env.json index 2370b40..017c5a1 100644 --- a/env.json +++ b/env.json @@ -7,5 +7,6 @@ "proxyHelpDocUrl": "https://dtstack.github.io/doraemon/docsify/#/zh-cn/guide/%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1", "mysql": { "prod": {} - } + }, + "mcpDeployDir": "/opt/doraemon/mcp-server/" } diff --git a/package.json b/package.json index 81f550e..4ceb6dd 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "clean": "easy clean", "build": "easy build", "debug": "egg-bin debug", - "dev": "egg-bin dev --daemon", + "dev": "NODE_OPTIONS=--openssl-legacy-provider && egg-bin dev --daemon", "start": "bash start.sh", "start:test": "bash start.sh -t", "server": "egg-scripts start --daemon --workers=4", @@ -30,6 +30,8 @@ }, "dependencies": { "@ant-design/icons": "4.5.0", + "@modelcontextprotocol/sdk": "^1.18.0", + "adm-zip": "^0.5.10", "ant-design-dtinsight-theme": "1.1.3", "antd": "4.15.6", "await-stream-ready": "^1.0.1", @@ -59,6 +61,7 @@ "mockjs": "^1.0.1-beta3", "moment": "^2.17.1", "mysql2": "^1.6.5", + "node-fetch": "2", "node-schedule": "^2.0.0", "node-ssh": "^6.0.0", "react": "16.9.0", @@ -66,6 +69,7 @@ "react-color": "^2.19.3", "react-dom": "^16.9.0", "react-loadable": "^5.5.0", + "react-markdown": "^6.0.3", "react-redux": "^7.1.0", "react-router": "^4.2.0", "react-router-config": "^1.0.0-beta.4", @@ -73,11 +77,15 @@ "react-router-redux": "^4.0.8", "redux": "^4.0.4", "redux-thunk": "^2.3.0", + "remark-gfm": "^1.0.0", "socket.io": "^4.1.0", "socket.io-client": "1.7.0", "ssh2": "^1.4.0", + "stream-buffers": "^3.0.2", + "tar": "^6.1.15", "typescript": "4.7.4", "utf8": "^3.0.0", + "uuid": "^8.1.0", "xterm": "^4.12.0", "xterm-addon-attach": "^0.6.0", "xterm-addon-fit": "^0.5.0" diff --git a/sql/doraemon.sql b/sql/doraemon.sql index aae0843..6727c0f 100644 --- a/sql/doraemon.sql +++ b/sql/doraemon.sql @@ -260,3 +260,36 @@ CREATE TABLE `tag_management` ( ) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; SET FOREIGN_KEY_CHECKS = 1; + +CREATE TABLE IF NOT EXISTS `mcp_servers` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '自增主键', + `server_id` varchar(64) NOT NULL COMMENT '服务器唯一标识/名称', + `title` varchar(200) NOT NULL COMMENT '显示标题', + `description` text COMMENT '服务器描述(支持Markdown)', + `author` varchar(100) NOT NULL COMMENT '创建者', + `version` varchar(20) NOT NULL COMMENT '版本号', + `tags` json COMMENT '标签数组', + `transport` enum('stdio', 'sse', 'streamable-http') NOT NULL DEFAULT 'stdio' COMMENT '传输协议类型', + `command` varchar(255) COMMENT '启动命令(stdio类型)', + `args` json COMMENT '命令参数数组(stdio类型)', + `env` json COMMENT '环境变量对象(stdio类型)', + `http_url` varchar(500) COMMENT 'HTTP访问地址(http类型)', + `sse_url` varchar(500) COMMENT 'SSE访问地址(sse类型)', + `git_url` varchar(500) COMMENT 'Git源码地址', + `deploy_path` varchar(500) COMMENT '托管部署路径', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '服务器状态 1-启用 0-禁用', + `is_delete` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除 1-已删除 0-未删除', + `use_count` int NOT NULL DEFAULT '0' COMMENT '使用次数', + `tools` json COMMENT '可用工具列表', + `prompts` json COMMENT '可用提示词列表', + `resources` json COMMENT '可用资源列表', + `capabilities` json COMMENT '服务器能力信息', + `last_sync_at` datetime COMMENT '最后同步时间', + `runtime_status` enum('running', 'stopped', 'error', 'unknown') NOT NULL DEFAULT 'unknown' COMMENT '运行时状态 running-运行中 stopped-已停止 error-错误 unknown-未知', + `last_ping_at` datetime COMMENT '最后ping检查时间', + `ping_error` text COMMENT '最后ping检查错误信息', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_server_id` (`server_id`), + ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COLLATE = utf8_bin diff --git a/yarn.lock b/yarn.lock index 6fd7888..31dcb88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -733,6 +733,24 @@ mkdirp "^0.5.1" rimraf "^2.5.2" +"@modelcontextprotocol/sdk@^1.18.0": + version "1.18.1" + resolved "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.18.1.tgz#cdc7e0809319b0466599b93fbf655dafa9f49ceb" + integrity sha512-d//GE8/Yh7aC3e7p+kZG8JqqEAwwDUmAfvH1quogtbk+ksS6E0RR6toKKESPYYZVre0meqkJb27zb+dhqE9Sgw== + dependencies: + ajv "^6.12.6" + content-type "^1.0.5" + cors "^2.8.5" + cross-spawn "^7.0.5" + eventsource "^3.0.2" + eventsource-parser "^3.0.0" + express "^5.0.1" + express-rate-limit "^7.5.0" + pkce-challenge "^5.0.0" + raw-body "^3.0.0" + zod "^3.23.8" + zod-to-json-schema "^3.24.1" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.npmmirror.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -934,6 +952,13 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/hast@^2.0.0": + version "2.3.10" + resolved "https://registry.npmmirror.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643" + integrity sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw== + dependencies: + "@types/unist" "^2" + "@types/history@^4.7.11": version "4.7.11" resolved "https://registry.npmmirror.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" @@ -1047,6 +1072,13 @@ resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.190.tgz#d8e99647af141c63902d0ca53cf2b34d2df33545" integrity sha512-5iJ3FBJBvQHQ8sFhEhJfjUP+G+LalhavTkYyrAYqz5MEJG+erSv0k9KJLb6q7++17Lafk1scaTIFXcMJlwK8Mw== +"@types/mdast@^3.0.0": + version "3.0.15" + resolved "https://registry.npmmirror.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" + integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ== + dependencies: + "@types/unist" "^2" + "@types/mime@*": version "3.0.1" resolved "https://registry.npmmirror.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" @@ -1246,6 +1278,11 @@ dependencies: "@types/node" "*" +"@types/unist@^2", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3": + version "2.0.11" + resolved "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + "@types/validator@*": version "13.7.10" resolved "https://registry.npmmirror.com/@types/validator/-/validator-13.7.10.tgz#f9763dc0933f8324920afa9c0790308eedf55ca7" @@ -1773,6 +1810,14 @@ accepts@^1.2.2, accepts@^1.3.5, accepts@~1.3.0, accepts@~1.3.4, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + accepts@~1.2.12, accepts@~1.2.13: version "1.2.13" resolved "https://registry.npmmirror.com/accepts/-/accepts-1.2.13.tgz#e5f1f3928c6d95fd96558c36ec3d9d0de4a6ecea" @@ -1883,6 +1928,11 @@ address@^1.0.1: resolved "https://registry.npmmirror.com/address/-/address-1.2.1.tgz#25bb61095b7522d65b357baa11bc05492d4c8acd" integrity sha512-B+6bi5D34+fDYENiH5qOlA0cV2rAGKuWZ9LeyUUehbXy8e0VS9e498yO0Jeeh+iM+6KbfudHTFjXw2MmJD4QRA== +adm-zip@^0.5.10: + version "0.5.16" + resolved "https://registry.npmmirror.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909" + integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== + after@0.8.1: version "0.8.1" resolved "https://registry.npmmirror.com/after/-/after-0.8.1.tgz#ab5d4fb883f596816d3515f8f791c0af486dd627" @@ -1934,7 +1984,7 @@ ajv@^5.0.0, ajv@^5.2.3, ajv@^5.3.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6: version "6.12.6" resolved "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -3530,6 +3580,11 @@ backo2@1.0.2: resolved "https://registry.npmmirror.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" integrity sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA== +bail@^1.0.0: + version "1.0.5" + resolved "https://registry.npmmirror.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" + integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -3716,6 +3771,21 @@ body-parser@1.20.1: type-is "~1.6.18" unpipe "1.0.0" +body-parser@^2.2.0: + version "2.2.0" + resolved "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa" + integrity sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg== + dependencies: + bytes "^3.1.2" + content-type "^1.0.5" + debug "^4.4.0" + http-errors "^2.0.0" + iconv-lite "^0.6.3" + on-finished "^2.4.1" + qs "^6.14.0" + raw-body "^3.0.0" + type-is "^2.0.0" + body-parser@~1.13.3: version "1.13.3" resolved "https://registry.npmmirror.com/body-parser/-/body-parser-1.13.3.tgz#c08cf330c3358e151016a05746f13f029c97fa97" @@ -3997,7 +4067,7 @@ bytes@2.4.0: resolved "https://registry.npmmirror.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" integrity sha512-SvUX8+c/Ga454a4fprIdIUzUN9xfd1YTvYh7ub5ZPJ+ZJ/+K2Bp6IpWGmnw8r3caLTsmhvJAKZz3qjIo9+XuCQ== -bytes@3.1.2: +bytes@3.1.2, bytes@^3.1.2: version "3.1.2" resolved "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== @@ -4136,6 +4206,14 @@ caching-transform@^3.0.1: package-hash "^3.0.0" write-file-atomic "^2.4.2" +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -4155,6 +4233,14 @@ call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + call-matcher@^1.0.0: version "1.1.0" resolved "https://registry.npmmirror.com/call-matcher/-/call-matcher-1.1.0.tgz#23b2c1bc7a8394c8be28609d77ddbd5786680432" @@ -4300,6 +4386,11 @@ caseless@~0.12.0: resolved "https://registry.npmmirror.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== +ccount@^1.0.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" + integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== + cfork@^1.6.1, cfork@^1.7.1: version "1.8.0" resolved "https://registry.npmmirror.com/cfork/-/cfork-1.8.0.tgz#d0fde2debabbd6e0758ff33f21c2f980ccc05711" @@ -4372,6 +4463,21 @@ change-case@^3.0.2: upper-case "^1.1.1" upper-case-first "^1.1.0" +character-entities-legacy@^1.0.0: + version "1.1.4" + resolved "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" + integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== + +character-entities@^1.0.0: + version "1.2.4" + resolved "https://registry.npmmirror.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" + integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== + +character-reference-invalid@^1.0.0: + version "1.1.4" + resolved "https://registry.npmmirror.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" + integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== + chardet@^0.4.0: version "0.4.2" resolved "https://registry.npmmirror.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" @@ -4471,6 +4577,11 @@ chownr@^1.0.1, chownr@^1.1.1: resolved "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + chrome-trace-event@^1.0.0, chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -4806,6 +4917,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +comma-separated-tokens@^1.0.0: + version "1.0.8" + resolved "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" + integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== + commander@*: version "9.4.1" resolved "https://registry.npmmirror.com/commander/-/commander-9.4.1.tgz#d1dd8f2ce6faf93147295c0df13c7c21141cfbdd" @@ -5147,11 +5263,23 @@ content-disposition@0.5.4, content-disposition@~0.5.0, content-disposition@~0.5. dependencies: safe-buffer "5.2.1" +content-disposition@^1.0.0: + version "1.0.0" + resolved "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" + integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg== + dependencies: + safe-buffer "5.2.1" + content-type@^1.0.0, content-type@^1.0.2, content-type@^1.0.4, content-type@~1.0.1, content-type@~1.0.4: version "1.0.4" resolved "https://registry.npmmirror.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-type@^1.0.5: + version "1.0.5" + resolved "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + conventional-changelog-angular@^1.3.3: version "1.6.6" resolved "https://registry.npmmirror.com/conventional-changelog-angular/-/conventional-changelog-angular-1.6.6.tgz#b27f2b315c16d0a1f23eb181309d0e6a4698ea0f" @@ -5366,6 +5494,11 @@ cookie-signature@1.0.6: resolved "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + cookie@0.1.3: version "0.1.3" resolved "https://registry.npmmirror.com/cookie/-/cookie-0.1.3.tgz#e734a5c1417fce472d5aef82c381cabb64d1a435" @@ -5381,6 +5514,11 @@ cookie@^0.3.1: resolved "https://registry.npmmirror.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" integrity sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw== +cookie@^0.7.1: + version "0.7.2" + resolved "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + cookie@~0.4.1: version "0.4.2" resolved "https://registry.npmmirror.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" @@ -5472,7 +5610,7 @@ core-util-is@^1.0.2, core-util-is@~1.0.0: resolved "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cors@~2.8.5: +cors@^2.8.5, cors@~2.8.5: version "2.8.5" resolved "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== @@ -5663,7 +5801,7 @@ cross-spawn@^7.0.0: shebang-command "^2.0.0" which "^2.0.1" -cross-spawn@^7.0.2: +cross-spawn@^7.0.2, cross-spawn@^7.0.5: version "7.0.6" resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -6127,6 +6265,13 @@ debug@^3.0.1, debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.0.0, debug@^4.3.5, debug@^4.4.0: + version "4.4.3" + resolved "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + debug@^4.3.4: version "4.3.7" resolved "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" @@ -6684,6 +6829,15 @@ draft-js@^0.10.0, draft-js@~0.10.0: immutable "~3.7.4" object-assign "^4.1.0" +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + duplexer3@^0.1.4: version "0.1.5" resolved "https://registry.npmmirror.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" @@ -7340,6 +7494,11 @@ encodeurl@^1.0.2, encodeurl@~1.0.2: resolved "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + encoding@^0.1.11: version "0.1.13" resolved "https://registry.npmmirror.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" @@ -7607,6 +7766,11 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" @@ -7619,6 +7783,13 @@ es-object-atoms@^1.0.0: dependencies: es-errors "^1.3.0" +es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + es-set-tostringtag@^2.0.3: version "2.0.3" resolved "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" @@ -8348,16 +8519,16 @@ esutils@^2.0.2: resolved "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +etag@^1.8.1, etag@~1.8.1: + version "1.8.1" + resolved "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + etag@~1.7.0: version "1.7.0" resolved "https://registry.npmmirror.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" integrity sha512-Mbv5pNpLNPrm1b4rzZlZlfTRpdDr31oiD43N362sIyvSWVNu5Du33EcJGzvEV4YdYLuENB1HzND907cQkFmXNw== -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - event-emitter@^0.3.5, event-emitter@~0.3.5: version "0.3.5" resolved "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" @@ -8394,6 +8565,18 @@ events@^3.0.0: resolved "https://registry.npmmirror.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +eventsource-parser@^3.0.0, eventsource-parser@^3.0.1: + version "3.0.6" + resolved "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz#292e165e34cacbc936c3c92719ef326d4aeb4e90" + integrity sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg== + +eventsource@^3.0.2: + version "3.0.7" + resolved "https://registry.npmmirror.com/eventsource/-/eventsource-3.0.7.tgz#1157622e2f5377bb6aef2114372728ba0c156989" + integrity sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA== + dependencies: + eventsource-parser "^3.0.1" + evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.npmmirror.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" @@ -8464,6 +8647,11 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" +express-rate-limit@^7.5.0: + version "7.5.1" + resolved "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-7.5.1.tgz#8c3a42f69209a3a1c969890070ece9e20a879dec" + integrity sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw== + express-session@~1.11.3: version "1.11.3" resolved "https://registry.npmmirror.com/express-session/-/express-session-1.11.3.tgz#5cc98f3f5ff84ed835f91cbf0aabd0c7107400af" @@ -8516,6 +8704,39 @@ express@^4.16.3: utils-merge "1.0.1" vary "~1.1.2" +express@^5.0.1: + version "5.1.0" + resolved "https://registry.npmmirror.com/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9" + integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA== + dependencies: + accepts "^2.0.0" + body-parser "^2.2.0" + content-disposition "^1.0.0" + content-type "^1.0.5" + cookie "^0.7.1" + cookie-signature "^1.2.1" + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + finalhandler "^2.1.0" + fresh "^2.0.0" + http-errors "^2.0.0" + merge-descriptors "^2.0.0" + mime-types "^3.0.0" + on-finished "^2.4.1" + once "^1.4.0" + parseurl "^1.3.3" + proxy-addr "^2.0.7" + qs "^6.14.0" + range-parser "^1.2.1" + router "^2.2.0" + send "^1.1.0" + serve-static "^2.2.0" + statuses "^2.0.1" + type-is "^2.0.1" + vary "^1.1.2" + ext@^1.1.2: version "1.7.0" resolved "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" @@ -8543,7 +8764,7 @@ extend2@^1.0.0: resolved "https://registry.npmmirror.com/extend2/-/extend2-1.0.1.tgz#ba9eee03bd6608995990c309b8c285316671c221" integrity sha512-ISoKeVhtewd5YHzMo+r9KC3Zx0fdpNBqoRzot+6BeEQ3bWQYQQOt0jkkY5gLveI2e7j+vdCJKeszHJIbg2Uceg== -extend@^3.0.1, extend@~3.0.0, extend@~3.0.2: +extend@^3.0.0, extend@^3.0.1, extend@~3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -8821,6 +9042,18 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +finalhandler@^2.1.0: + version "2.1.0" + resolved "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.0.tgz#72306373aa89d05a8242ed569ed86a1bff7c561f" + integrity sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q== + dependencies: + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" + find-cache-dir@^0.1.1: version "0.1.1" resolved "https://registry.npmmirror.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" @@ -9047,6 +9280,11 @@ fresh@0.5.2, fresh@^0.5.2, fresh@~0.5.2: resolved "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + from2@^2.1.0: version "2.3.0" resolved "https://registry.npmmirror.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" @@ -9113,6 +9351,13 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs-readdir-recursive@^1.0.0: version "1.1.0" resolved "https://registry.npmmirror.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" @@ -9246,6 +9491,22 @@ get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-pkg-repo@^4.0.0: version "4.2.1" resolved "https://registry.npmmirror.com/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz#75973e1c8050c73f48190c52047c4cee3acbf385" @@ -9261,6 +9522,14 @@ get-port@^5.0.0: resolved "https://registry.npmmirror.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-ready@^1.0.0, get-ready@~1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/get-ready/-/get-ready-1.0.0.tgz#f91817f1e9adecfea13a562adfc8de883ab34782" @@ -9591,6 +9860,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.2.4" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + got@^6.7.1: version "6.7.1" resolved "https://registry.npmmirror.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" @@ -9783,6 +10057,11 @@ has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -10057,7 +10336,7 @@ http-cache-semantics@^4.0.0: resolved "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== -http-errors@2.0.0: +http-errors@2.0.0, http-errors@^2.0.0: version "2.0.0" resolved "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== @@ -10201,6 +10480,13 @@ iconv-lite@0.4.24, iconv-lite@^0.4.15, iconv-lite@^0.4.17, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@0.7.0: + version "0.7.0" + resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.0.tgz#c50cd80e6746ca8115eb98743afa81aa0e147a3e" + integrity sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + iconv-lite@^0.2.11: version "0.2.11" resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.2.11.tgz#1ce60a3a57864a292d1321ff4609ca4bb965adc8" @@ -10213,7 +10499,7 @@ iconv-lite@^0.5.0: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: +iconv-lite@^0.6.2, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -10397,6 +10683,11 @@ ini@^1.3.2, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +inline-style-parser@0.1.1: + version "0.1.1" + resolved "https://registry.npmmirror.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" + integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== + inquirer@8.2.4: version "8.2.4" resolved "https://registry.npmmirror.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" @@ -10559,6 +10850,19 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" +is-alphabetical@^1.0.0: + version "1.0.4" + resolved "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" + integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== + +is-alphanumerical@^1.0.0: + version "1.0.4" + resolved "https://registry.npmmirror.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" + integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== + dependencies: + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-arguments@^1.0.4: version "1.1.1" resolved "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" @@ -10631,7 +10935,7 @@ is-buffer@^1.1.5, is-buffer@~1.1.6: resolved "https://registry.npmmirror.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@~2.0.3: +is-buffer@^2.0.0, is-buffer@~2.0.3: version "2.0.5" resolved "https://registry.npmmirror.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== @@ -10714,6 +11018,11 @@ is-date-object@^1.0.1, is-date-object@^1.0.5: dependencies: has-tostringtag "^1.0.0" +is-decimal@^1.0.0: + version "1.0.4" + resolved "https://registry.npmmirror.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" + integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== + is-descriptor@^0.1.0: version "0.1.6" resolved "https://registry.npmmirror.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" @@ -10809,6 +11118,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-hexadecimal@^1.0.0: + version "1.0.4" + resolved "https://registry.npmmirror.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" + integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== + is-installed-globally@^0.1.0: version "0.1.0" resolved "https://registry.npmmirror.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" @@ -10926,6 +11240,11 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== +is-plain-obj@^2.0.0: + version "2.1.0" + resolved "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-plain-obj@^3.0.0: version "3.0.0" resolved "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" @@ -10953,6 +11272,11 @@ is-promise@^2.2.2: resolved "https://registry.npmmirror.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + is-property@^1.0.2: version "1.0.2" resolved "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" @@ -12165,6 +12489,11 @@ long@^4.0.0: resolved "https://registry.npmmirror.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +longest-streak@^2.0.0: + version "2.0.4" + resolved "https://registry.npmmirror.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" + integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== + longest@^2.0.1: version "2.0.1" resolved "https://registry.npmmirror.com/longest/-/longest-2.0.1.tgz#781e183296aa94f6d4d916dc335d0d17aefa23f8" @@ -12307,6 +12636,13 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +markdown-table@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" + integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A== + dependencies: + repeat-string "^1.0.0" + marked@^1.2.9: version "1.2.9" resolved "https://registry.npmmirror.com/marked/-/marked-1.2.9.tgz#53786f8b05d4c01a2a5a76b7d1ec9943d29d72dc" @@ -12324,6 +12660,11 @@ material-colors@^1.2.1: resolved "https://registry.npmmirror.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + mathml-tag-names@^2.1.3: version "2.1.3" resolved "https://registry.npmmirror.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" @@ -12347,6 +12688,106 @@ md5@^2.2.1: crypt "0.0.2" is-buffer "~1.1.6" +mdast-util-definitions@^4.0.0: + version "4.0.0" + resolved "https://registry.npmmirror.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2" + integrity sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ== + dependencies: + unist-util-visit "^2.0.0" + +mdast-util-find-and-replace@^1.1.0: + version "1.1.1" + resolved "https://registry.npmmirror.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-1.1.1.tgz#b7db1e873f96f66588c321f1363069abf607d1b5" + integrity sha512-9cKl33Y21lyckGzpSmEQnIDjEfeeWelN5s1kUW1LwdB0Fkuq2u+4GdqcGEygYxJE8GVqCl0741bYXHgamfWAZA== + dependencies: + escape-string-regexp "^4.0.0" + unist-util-is "^4.0.0" + unist-util-visit-parents "^3.0.0" + +mdast-util-from-markdown@^0.8.0: + version "0.8.5" + resolved "https://registry.npmmirror.com/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz#d1ef2ca42bc377ecb0463a987910dae89bd9a28c" + integrity sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-string "^2.0.0" + micromark "~2.11.0" + parse-entities "^2.0.0" + unist-util-stringify-position "^2.0.0" + +mdast-util-gfm-autolink-literal@^0.1.0: + version "0.1.3" + resolved "https://registry.npmmirror.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-0.1.3.tgz#9c4ff399c5ddd2ece40bd3b13e5447d84e385fb7" + integrity sha512-GjmLjWrXg1wqMIO9+ZsRik/s7PLwTaeCHVB7vRxUwLntZc8mzmTsLVr6HW1yLokcnhfURsn5zmSVdi3/xWWu1A== + dependencies: + ccount "^1.0.0" + mdast-util-find-and-replace "^1.1.0" + micromark "^2.11.3" + +mdast-util-gfm-strikethrough@^0.2.0: + version "0.2.3" + resolved "https://registry.npmmirror.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-0.2.3.tgz#45eea337b7fff0755a291844fbea79996c322890" + integrity sha512-5OQLXpt6qdbttcDG/UxYY7Yjj3e8P7X16LzvpX8pIQPYJ/C2Z1qFGMmcw+1PZMUM3Z8wt8NRfYTvCni93mgsgA== + dependencies: + mdast-util-to-markdown "^0.6.0" + +mdast-util-gfm-table@^0.1.0: + version "0.1.6" + resolved "https://registry.npmmirror.com/mdast-util-gfm-table/-/mdast-util-gfm-table-0.1.6.tgz#af05aeadc8e5ee004eeddfb324b2ad8c029b6ecf" + integrity sha512-j4yDxQ66AJSBwGkbpFEp9uG/LS1tZV3P33fN1gkyRB2LoRL+RR3f76m0HPHaby6F4Z5xr9Fv1URmATlRRUIpRQ== + dependencies: + markdown-table "^2.0.0" + mdast-util-to-markdown "~0.6.0" + +mdast-util-gfm-task-list-item@^0.1.0: + version "0.1.6" + resolved "https://registry.npmmirror.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-0.1.6.tgz#70c885e6b9f543ddd7e6b41f9703ee55b084af10" + integrity sha512-/d51FFIfPsSmCIRNp7E6pozM9z1GYPIkSy1urQ8s/o4TC22BZ7DqfHFWiqBD23bc7J3vV1Fc9O4QIHBlfuit8A== + dependencies: + mdast-util-to-markdown "~0.6.0" + +mdast-util-gfm@^0.1.0: + version "0.1.2" + resolved "https://registry.npmmirror.com/mdast-util-gfm/-/mdast-util-gfm-0.1.2.tgz#8ecddafe57d266540f6881f5c57ff19725bd351c" + integrity sha512-NNkhDx/qYcuOWB7xHUGWZYVXvjPFFd6afg6/e2g+SV4r9q5XUcCbV4Wfa3DLYIiD+xAEZc6K4MGaE/m0KDcPwQ== + dependencies: + mdast-util-gfm-autolink-literal "^0.1.0" + mdast-util-gfm-strikethrough "^0.2.0" + mdast-util-gfm-table "^0.1.0" + mdast-util-gfm-task-list-item "^0.1.0" + mdast-util-to-markdown "^0.6.1" + +mdast-util-to-hast@^10.2.0: + version "10.2.0" + resolved "https://registry.npmmirror.com/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz#61875526a017d8857b71abc9333942700b2d3604" + integrity sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + mdast-util-definitions "^4.0.0" + mdurl "^1.0.0" + unist-builder "^2.0.0" + unist-util-generated "^1.0.0" + unist-util-position "^3.0.0" + unist-util-visit "^2.0.0" + +mdast-util-to-markdown@^0.6.0, mdast-util-to-markdown@^0.6.1, mdast-util-to-markdown@~0.6.0: + version "0.6.5" + resolved "https://registry.npmmirror.com/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz#b33f67ca820d69e6cc527a93d4039249b504bebe" + integrity sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ== + dependencies: + "@types/unist" "^2.0.0" + longest-streak "^2.0.0" + mdast-util-to-string "^2.0.0" + parse-entities "^2.0.0" + repeat-string "^1.0.0" + zwitch "^1.0.0" + +mdast-util-to-string@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b" + integrity sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w== + mdn-data@2.0.14: version "2.0.14" resolved "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" @@ -12357,11 +12798,21 @@ mdn-data@2.0.4: resolved "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== +mdurl@^1.0.0: + version "1.0.1" + resolved "https://registry.npmmirror.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + medium-zoom@^1.0.6: version "1.0.8" resolved "https://registry.npmmirror.com/medium-zoom/-/medium-zoom-1.0.8.tgz#2bd1fbcf2961fa7b0e318fe284462aa9b8608ed2" @@ -12477,6 +12928,11 @@ merge-descriptors@1.0.1: resolved "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + merge-estraverse-visitors@^1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/merge-estraverse-visitors/-/merge-estraverse-visitors-1.0.0.tgz#eb968338b5ded5ceed82cec0307decba2d8ea994" @@ -12521,6 +12977,59 @@ methods@^1.0.1, methods@^1.1.2, methods@~1.1.2: resolved "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== +micromark-extension-gfm-autolink-literal@~0.5.0: + version "0.5.7" + resolved "https://registry.npmmirror.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-0.5.7.tgz#53866c1f0c7ef940ae7ca1f72c6faef8fed9f204" + integrity sha512-ePiDGH0/lhcngCe8FtH4ARFoxKTUelMp4L7Gg2pujYD5CSMb9PbblnyL+AAMud/SNMyusbS2XDSiPIRcQoNFAw== + dependencies: + micromark "~2.11.3" + +micromark-extension-gfm-strikethrough@~0.6.5: + version "0.6.5" + resolved "https://registry.npmmirror.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-0.6.5.tgz#96cb83356ff87bf31670eefb7ad7bba73e6514d1" + integrity sha512-PpOKlgokpQRwUesRwWEp+fHjGGkZEejj83k9gU5iXCbDG+XBA92BqnRKYJdfqfkrRcZRgGuPuXb7DaK/DmxOhw== + dependencies: + micromark "~2.11.0" + +micromark-extension-gfm-table@~0.4.0: + version "0.4.3" + resolved "https://registry.npmmirror.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-0.4.3.tgz#4d49f1ce0ca84996c853880b9446698947f1802b" + integrity sha512-hVGvESPq0fk6ALWtomcwmgLvH8ZSVpcPjzi0AjPclB9FsVRgMtGZkUcpE0zgjOCFAznKepF4z3hX8z6e3HODdA== + dependencies: + micromark "~2.11.0" + +micromark-extension-gfm-tagfilter@~0.3.0: + version "0.3.0" + resolved "https://registry.npmmirror.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-0.3.0.tgz#d9f26a65adee984c9ccdd7e182220493562841ad" + integrity sha512-9GU0xBatryXifL//FJH+tAZ6i240xQuFrSL7mYi8f4oZSbc+NvXjkrHemeYP0+L4ZUT+Ptz3b95zhUZnMtoi/Q== + +micromark-extension-gfm-task-list-item@~0.3.0: + version "0.3.3" + resolved "https://registry.npmmirror.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-0.3.3.tgz#d90c755f2533ed55a718129cee11257f136283b8" + integrity sha512-0zvM5iSLKrc/NQl84pZSjGo66aTGd57C1idmlWmE87lkMcXrTxg1uXa/nXomxJytoje9trP0NDLvw4bZ/Z/XCQ== + dependencies: + micromark "~2.11.0" + +micromark-extension-gfm@^0.3.0: + version "0.3.3" + resolved "https://registry.npmmirror.com/micromark-extension-gfm/-/micromark-extension-gfm-0.3.3.tgz#36d1a4c089ca8bdfd978c9bd2bf1a0cb24e2acfe" + integrity sha512-oVN4zv5/tAIA+l3GbMi7lWeYpJ14oQyJ3uEim20ktYFAcfX1x3LNlFGGlmrZHt7u9YlKExmyJdDGaTt6cMSR/A== + dependencies: + micromark "~2.11.0" + micromark-extension-gfm-autolink-literal "~0.5.0" + micromark-extension-gfm-strikethrough "~0.6.5" + micromark-extension-gfm-table "~0.4.0" + micromark-extension-gfm-tagfilter "~0.3.0" + micromark-extension-gfm-task-list-item "~0.3.0" + +micromark@^2.11.3, micromark@~2.11.0, micromark@~2.11.3: + version "2.11.4" + resolved "https://registry.npmmirror.com/micromark/-/micromark-2.11.4.tgz#d13436138eea826383e822449c9a5c50ee44665a" + integrity sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA== + dependencies: + debug "^4.0.0" + parse-entities "^2.0.0" + micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8: version "3.1.10" resolved "https://registry.npmmirror.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -12569,6 +13078,11 @@ mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + mime-types@^2.0.7, mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.8, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34, mime-types@~2.1.6, mime-types@~2.1.9: version "2.1.35" resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" @@ -12576,6 +13090,13 @@ mime-types@^2.0.7, mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.8, mi dependencies: mime-db "1.52.0" +mime-types@^3.0.0, mime-types@^3.0.1: + version "3.0.1" + resolved "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce" + integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA== + dependencies: + mime-db "^1.54.0" + mime@1.3.4: version "1.3.4" resolved "https://registry.npmmirror.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" @@ -12709,6 +13230,26 @@ minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@^1. resolved "https://registry.npmmirror.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + mississippi@^2.0.0: version "2.0.0" resolved "https://registry.npmmirror.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f" @@ -12763,6 +13304,11 @@ mkdirp@0.5.4: dependencies: minimist "^1.2.6" +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + mocha@^6.0.2: version "6.2.3" resolved "https://registry.npmmirror.com/mocha/-/mocha-6.2.3.tgz#e648432181d8b99393410212664450a4c1e31912" @@ -13027,6 +13573,11 @@ negotiator@0.6.3: resolved "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -13067,6 +13618,13 @@ node-environment-flags@1.0.5: object.getownpropertydescriptors "^2.0.3" semver "^5.7.0" +node-fetch@2: + version "2.7.0" + resolved "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-fetch@^1.0.1: version "1.7.3" resolved "https://registry.npmmirror.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -13545,7 +14103,7 @@ omit.js@^1.0.2: dependencies: babel-runtime "^6.23.0" -on-finished@2.4.1, on-finished@^2.1.0, on-finished@^2.3.0: +on-finished@2.4.1, on-finished@^2.1.0, on-finished@^2.3.0, on-finished@^2.4.1: version "2.4.1" resolved "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== @@ -13940,6 +14498,18 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" +parse-entities@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" + integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + parse-json@^2.2.0: version "2.2.0" resolved "https://registry.npmmirror.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -14026,7 +14596,7 @@ parseuri@0.0.6: resolved "https://registry.npmmirror.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a" integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow== -parseurl@^1.3.0, parseurl@^1.3.2, parseurl@~1.3.0, parseurl@~1.3.1, parseurl@~1.3.2, parseurl@~1.3.3: +parseurl@^1.3.0, parseurl@^1.3.2, parseurl@^1.3.3, parseurl@~1.3.0, parseurl@~1.3.1, parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -14115,6 +14685,11 @@ path-to-regexp@^1.0.1, path-to-regexp@^1.1.1, path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +path-to-regexp@^8.0.0: + version "8.3.0" + resolved "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f" + integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA== + path-type@^1.0.0: version "1.1.0" resolved "https://registry.npmmirror.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -14216,6 +14791,11 @@ pinkie@^2.0.0: resolved "https://registry.npmmirror.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== +pkce-challenge@^5.0.0: + version "5.0.0" + resolved "https://registry.npmmirror.com/pkce-challenge/-/pkce-challenge-5.0.0.tgz#c3a405cb49e272094a38e890a2b51da0228c4d97" + integrity sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ== + pkg-dir@^1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" @@ -14868,12 +15448,19 @@ prop-types@15.x, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, pr object-assign "^4.1.1" react-is "^16.13.1" +property-information@^5.3.0: + version "5.6.0" + resolved "https://registry.npmmirror.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" + integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA== + dependencies: + xtend "^4.0.0" + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.npmmirror.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== -proxy-addr@~2.0.7: +proxy-addr@^2.0.7, proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -14998,6 +15585,13 @@ qs@6.11.0, qs@^6.4.0, qs@^6.5.2: dependencies: side-channel "^1.0.4" +qs@^6.14.0: + version "6.14.0" + resolved "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== + dependencies: + side-channel "^1.1.0" + qs@~6.5.2: version "6.5.3" resolved "https://registry.npmmirror.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" @@ -15068,7 +15662,7 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" -range-parser@^1.0.3, range-parser@~1.2.1: +range-parser@^1.0.3, range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== @@ -15088,6 +15682,16 @@ raw-body@2.5.1, raw-body@^2.2.0, raw-body@^2.3.3: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@^3.0.0: + version "3.0.1" + resolved "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.1.tgz#ced5cd79a77bbb0496d707f2a0f9e1ae3aecdcb1" + integrity sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.7.0" + unpipe "1.0.0" + raw-body@~2.1.2: version "2.1.7" resolved "https://registry.npmmirror.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774" @@ -16003,7 +16607,7 @@ react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.2: +react-is@^17.0.0, react-is@^17.0.2: version "17.0.2" resolved "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== @@ -16035,6 +16639,25 @@ react-loadable@^5.5.0: dependencies: prop-types "^15.5.0" +react-markdown@^6.0.3: + version "6.0.3" + resolved "https://registry.npmmirror.com/react-markdown/-/react-markdown-6.0.3.tgz#625ec767fa321d91801129387e7d31ee0cb99254" + integrity sha512-kQbpWiMoBHnj9myLlmZG9T1JdoT/OEyHK7hqM6CqFT14MAkgWiWBUYijLyBmxbntaN6dCDicPcUhWhci1QYodg== + dependencies: + "@types/hast" "^2.0.0" + "@types/unist" "^2.0.3" + comma-separated-tokens "^1.0.0" + prop-types "^15.7.2" + property-information "^5.3.0" + react-is "^17.0.0" + remark-parse "^9.0.0" + remark-rehype "^8.0.0" + space-separated-tokens "^1.1.0" + style-to-object "^0.3.0" + unified "^9.0.0" + unist-util-visit "^2.0.0" + vfile "^4.0.0" + react-redux@^7.1.0: version "7.2.9" resolved "https://registry.npmmirror.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" @@ -16457,6 +17080,28 @@ release-zalgo@^1.0.0: dependencies: es6-error "^4.0.1" +remark-gfm@^1.0.0: + version "1.0.0" + resolved "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-1.0.0.tgz#9213643001be3f277da6256464d56fd28c3b3c0d" + integrity sha512-KfexHJCiqvrdBZVbQ6RopMZGwaXz6wFJEfByIuEwGf0arvITHjiKKZ1dpXujjH9KZdm1//XJQwgfnJ3lmXaDPA== + dependencies: + mdast-util-gfm "^0.1.0" + micromark-extension-gfm "^0.3.0" + +remark-parse@^9.0.0: + version "9.0.0" + resolved "https://registry.npmmirror.com/remark-parse/-/remark-parse-9.0.0.tgz#4d20a299665880e4f4af5d90b7c7b8a935853640" + integrity sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw== + dependencies: + mdast-util-from-markdown "^0.8.0" + +remark-rehype@^8.0.0: + version "8.1.0" + resolved "https://registry.npmmirror.com/remark-rehype/-/remark-rehype-8.1.0.tgz#610509a043484c1e697437fa5eb3fd992617c945" + integrity sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA== + dependencies: + mdast-util-to-hast "^10.2.0" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.npmmirror.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -16478,7 +17123,7 @@ repeat-element@^1.1.2: resolved "https://registry.npmmirror.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== -repeat-string@^1.6.1: +repeat-string@^1.0.0, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.npmmirror.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== @@ -16758,6 +17403,17 @@ rndm@1.2.0: resolved "https://registry.npmmirror.com/rndm/-/rndm-1.2.0.tgz#f33fe9cfb52bbfd520aa18323bc65db110a1b76c" integrity sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw== +router@^2.2.0: + version "2.2.0" + resolved "https://registry.npmmirror.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== + dependencies: + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" + run-async@^2.2.0, run-async@^2.4.0: version "2.4.1" resolved "https://registry.npmmirror.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -17100,6 +17756,23 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" +send@^1.1.0, send@^1.2.0: + version "1.2.0" + resolved "https://registry.npmmirror.com/send/-/send-1.2.0.tgz#32a7554fb777b831dfa828370f773a3808d37212" + integrity sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw== + dependencies: + debug "^4.3.5" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.0" + mime-types "^3.0.1" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.1" + sendmessage@^1.1.0: version "1.1.0" resolved "https://registry.npmmirror.com/sendmessage/-/sendmessage-1.1.0.tgz#10a245cee2d50c759f1e09a23477b91496d09e35" @@ -17214,6 +17887,16 @@ serve-static@1.15.0, serve-static@^1.12.1: parseurl "~1.3.3" send "0.18.0" +serve-static@^2.2.0: + version "2.2.0" + resolved "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.0.tgz#9c02564ee259bdd2251b82d659a2e7e1938d66f9" + integrity sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ== + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" + serve-static@~1.10.0: version "1.10.3" resolved "https://registry.npmmirror.com/serve-static/-/serve-static-1.10.3.tgz#ce5a6ecd3101fed5ec09827dac22a9c29bfb0535" @@ -17367,6 +18050,35 @@ should-send-same-site-none@^2.0.2: resolved "https://registry.npmmirror.com/should-send-same-site-none/-/should-send-same-site-none-2.0.5.tgz#f710116f7d922ef17a90ecdeb864932e68eff588" integrity sha512-7dig49H7sKnv1v/GPoFQChGgJdEX9s2oy9TQBSD5RbUx7M9CCRjHMaFP06v+DZQNM0K+o8dBhvBAd4eEKirqbQ== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -17386,6 +18098,17 @@ side-channel@^1.0.6: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + sigmund@^1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" @@ -17700,6 +18423,11 @@ source-map@^0.7.3: resolved "https://registry.npmmirror.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== +space-separated-tokens@^1.1.0: + version "1.1.5" + resolved "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" + integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== + spawn-wrap@^1.4.2: version "1.4.3" resolved "https://registry.npmmirror.com/spawn-wrap/-/spawn-wrap-1.4.3.tgz#81b7670e170cca247d80bf5faf0cfb713bdcf848" @@ -17901,6 +18629,11 @@ statuses@2.0.1: resolved "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +statuses@^2.0.1: + version "2.0.2" + resolved "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + statuses@~1.2.1: version "1.2.1" resolved "https://registry.npmmirror.com/statuses/-/statuses-1.2.1.tgz#dded45cc18256d51ed40aec142489d5c61026d28" @@ -17919,6 +18652,11 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" +stream-buffers@^3.0.2: + version "3.0.3" + resolved "https://registry.npmmirror.com/stream-buffers/-/stream-buffers-3.0.3.tgz#9fc6ae267d9c4df1190a781e011634cac58af3cd" + integrity sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw== + stream-combiner@~0.0.4: version "0.0.4" resolved "https://registry.npmmirror.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" @@ -18230,6 +18968,13 @@ style-search@^0.1.0: resolved "https://registry.npmmirror.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" integrity sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg== +style-to-object@^0.3.0: + version "0.3.0" + resolved "https://registry.npmmirror.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" + integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA== + dependencies: + inline-style-parser "0.1.1" + stylehacks@^4.0.0: version "4.0.3" resolved "https://registry.npmmirror.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" @@ -18476,6 +19221,18 @@ tar@^2.2.1: fstream "^1.0.12" inherits "2" +tar@^6.1.15: + version "6.2.1" + resolved "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + tcp-base@^3.1.0: version "3.1.1" resolved "https://registry.npmmirror.com/tcp-base/-/tcp-base-3.1.1.tgz#7daf9599b148919bc12fc3df14f49b89b748cca1" @@ -18846,6 +19603,11 @@ trim-right@^1.0.1: resolved "https://registry.npmmirror.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" integrity sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw== +trough@^1.0.0: + version "1.0.5" + resolved "https://registry.npmmirror.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" + integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== + tryer@^1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" @@ -19001,6 +19763,15 @@ type-is@^1.5.5, type-is@^1.6.15, type-is@^1.6.16, type-is@~1.6.18, type-is@~1.6. media-typer "0.3.0" mime-types "~2.1.24" +type-is@^2.0.0, type-is@^2.0.1: + version "2.0.1" + resolved "https://registry.npmmirror.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" + integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== + dependencies: + content-type "^1.0.5" + media-typer "^1.1.0" + mime-types "^3.0.0" + type-name@^2.0.0, type-name@^2.0.1: version "2.0.2" resolved "https://registry.npmmirror.com/type-name/-/type-name-2.0.2.tgz#efe7d4123d8ac52afff7f40c7e4dec5266008fb4" @@ -19179,6 +19950,18 @@ unescape@^1.0.1: dependencies: extend-shallow "^2.0.1" +unified@^9.0.0: + version "9.2.2" + resolved "https://registry.npmmirror.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975" + integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== + dependencies: + bail "^1.0.0" + extend "^3.0.0" + is-buffer "^2.0.0" + is-plain-obj "^2.0.0" + trough "^1.0.0" + vfile "^4.0.0" + union-value@^1.0.0: version "1.0.1" resolved "https://registry.npmmirror.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -19227,6 +20010,50 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" +unist-builder@^2.0.0: + version "2.0.3" + resolved "https://registry.npmmirror.com/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436" + integrity sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw== + +unist-util-generated@^1.0.0: + version "1.1.6" + resolved "https://registry.npmmirror.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b" + integrity sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg== + +unist-util-is@^4.0.0: + version "4.1.0" + resolved "https://registry.npmmirror.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797" + integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg== + +unist-util-position@^3.0.0: + version "3.1.0" + resolved "https://registry.npmmirror.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47" + integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA== + +unist-util-stringify-position@^2.0.0: + version "2.0.3" + resolved "https://registry.npmmirror.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da" + integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g== + dependencies: + "@types/unist" "^2.0.2" + +unist-util-visit-parents@^3.0.0: + version "3.1.1" + resolved "https://registry.npmmirror.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6" + integrity sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^4.0.0" + +unist-util-visit@^2.0.0: + version "2.0.3" + resolved "https://registry.npmmirror.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c" + integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^4.0.0" + unist-util-visit-parents "^3.0.0" + universal-deep-strict-equal@^1.2.1: version "1.2.2" resolved "https://registry.npmmirror.com/universal-deep-strict-equal/-/universal-deep-strict-equal-1.2.2.tgz#0da4ac2f73cff7924c81fa4de018ca562ca2b0a7" @@ -19497,7 +20324,7 @@ uuid@^3.0.1, uuid@^3.2.1, uuid@^3.3.2: resolved "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.2: +uuid@^8.1.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -19580,6 +20407,24 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vfile-message@^2.0.0: + version "2.0.4" + resolved "https://registry.npmmirror.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" + integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position "^2.0.0" + +vfile@^4.0.0: + version "4.2.1" + resolved "https://registry.npmmirror.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" + integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== + dependencies: + "@types/unist" "^2.0.0" + is-buffer "^2.0.0" + unist-util-stringify-position "^2.0.0" + vfile-message "^2.0.0" + vhost@~3.0.1: version "3.0.2" resolved "https://registry.npmmirror.com/vhost/-/vhost-3.0.2.tgz#2fb1decd4c466aa88b0f9341af33dc1aff2478d5" @@ -20487,3 +21332,18 @@ zlogger@^1.1.0: pumpify "^1.3.5" split2 "^2.1.0" through2 "^2.0.1" + +zod-to-json-schema@^3.24.1: + version "3.24.6" + resolved "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz#5920f020c4d2647edfbb954fa036082b92c9e12d" + integrity sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg== + +zod@^3.23.8: + version "3.25.76" + resolved "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== + +zwitch@^1.0.0: + version "1.0.5" + resolved "https://registry.npmmirror.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" + integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw== From fb697cdcc1d242e0b0dead2d33333e375f1ca73b Mon Sep 17 00:00:00 2001 From: JackWang032 <2522134117@qq.com> Date: Mon, 29 Sep 2025 14:15:47 +0800 Subject: [PATCH 02/25] feat: add MCP web pages --- app/web/api/url.ts | 79 ++ app/web/components/markdownEditor/index.tsx | 106 +++ app/web/components/markdownEditor/style.scss | 52 ++ app/web/components/markdownRenderer/index.tsx | 72 ++ .../components/markdownRenderer/style.scss | 211 +++++ app/web/layouts/header/header.tsx | 12 + .../mcpServer/components/configTab/index.tsx | 75 ++ .../components/inspectorTab/index.tsx | 26 + .../components/overviewTab/index.tsx | 27 + .../mcpServer/components/serverCard/index.tsx | 95 +++ .../components/serverCard/style.scss | 244 ++++++ .../components/serverCardSkeleton/index.tsx | 29 + .../components/serverCardSkeleton/style.scss | 33 + .../components/statusBadge/index.tsx | 38 + .../components/statusBadge/style.scss | 28 + .../components/toolsResourcesTab/index.tsx | 460 ++++++++++ .../components/toolsResourcesTab/style.scss | 94 +++ .../components/transportTag/index.tsx | 66 ++ .../components/transportTag/style.scss | 39 + app/web/pages/mcpServer/detail/index.tsx | 200 +++++ app/web/pages/mcpServer/detail/style.scss | 567 +++++++++++++ app/web/pages/mcpServer/inspector/index.tsx | 82 ++ app/web/pages/mcpServer/inspector/style.scss | 212 +++++ app/web/pages/mcpServer/management/index.tsx | 368 ++++++++ app/web/pages/mcpServer/management/style.scss | 59 ++ app/web/pages/mcpServer/mcpMarket/index.tsx | 152 ++++ app/web/pages/mcpServer/mcpMarket/style.scss | 113 +++ .../pages/mcpServer/registryCenter/index.tsx | 792 ++++++++++++++++++ .../pages/mcpServer/registryCenter/style.scss | 118 +++ app/web/pages/mcpServer/types.ts | 51 ++ app/web/router/index.ts | 27 + app/web/scss/reset.scss | 8 + app/web/utils/copyUtils.ts | 31 + 33 files changed, 4566 insertions(+) create mode 100644 app/web/components/markdownEditor/index.tsx create mode 100644 app/web/components/markdownEditor/style.scss create mode 100644 app/web/components/markdownRenderer/index.tsx create mode 100644 app/web/components/markdownRenderer/style.scss create mode 100644 app/web/pages/mcpServer/components/configTab/index.tsx create mode 100644 app/web/pages/mcpServer/components/inspectorTab/index.tsx create mode 100644 app/web/pages/mcpServer/components/overviewTab/index.tsx create mode 100644 app/web/pages/mcpServer/components/serverCard/index.tsx create mode 100644 app/web/pages/mcpServer/components/serverCard/style.scss create mode 100644 app/web/pages/mcpServer/components/serverCardSkeleton/index.tsx create mode 100644 app/web/pages/mcpServer/components/serverCardSkeleton/style.scss create mode 100644 app/web/pages/mcpServer/components/statusBadge/index.tsx create mode 100644 app/web/pages/mcpServer/components/statusBadge/style.scss create mode 100644 app/web/pages/mcpServer/components/toolsResourcesTab/index.tsx create mode 100644 app/web/pages/mcpServer/components/toolsResourcesTab/style.scss create mode 100644 app/web/pages/mcpServer/components/transportTag/index.tsx create mode 100644 app/web/pages/mcpServer/components/transportTag/style.scss create mode 100644 app/web/pages/mcpServer/detail/index.tsx create mode 100644 app/web/pages/mcpServer/detail/style.scss create mode 100644 app/web/pages/mcpServer/inspector/index.tsx create mode 100644 app/web/pages/mcpServer/inspector/style.scss create mode 100644 app/web/pages/mcpServer/management/index.tsx create mode 100644 app/web/pages/mcpServer/management/style.scss create mode 100644 app/web/pages/mcpServer/mcpMarket/index.tsx create mode 100644 app/web/pages/mcpServer/mcpMarket/style.scss create mode 100644 app/web/pages/mcpServer/registryCenter/index.tsx create mode 100644 app/web/pages/mcpServer/registryCenter/style.scss create mode 100644 app/web/pages/mcpServer/types.ts create mode 100644 app/web/utils/copyUtils.ts diff --git a/app/web/api/url.ts b/app/web/api/url.ts index 5117b86..000643c 100644 --- a/app/web/api/url.ts +++ b/app/web/api/url.ts @@ -273,4 +273,83 @@ export default { method: 'get', url: '/api/article-topic/get-topic-list', }, + + /** + * MCP服务器管理 + */ + // 获取MCP服务器列表 + getMCPServerList: { + method: 'get', + url: '/api/mcp-servers/list', + }, + // 获取MCP服务器详情 + getMCPServerDetail: { + method: 'get', + url: '/api/mcp-servers/detail', + }, + // 注册MCP服务器 + registerMCPServer: { + method: 'post', + url: '/api/mcp-servers/register', + }, + // 更新MCP服务器 + updateMCPServer: { + method: 'put', + url: '/api/mcp-servers/update', + }, + // 删除MCP服务器 + deleteMCPServer: { + method: 'delete', + url: '/api/mcp-servers/delete', + }, + // 增加使用统计 + incrementUseCount: { + method: 'post', + url: '/api/mcp-servers/use', + }, + // 获取MCP服务器健康状态 + checkMCPServerHealth: { + method: 'get', + url: '/api/mcp-servers/health', + }, + // 清理文件 + cleanupFile: { + method: 'post', + url: '/api/mcp-servers/cleanup-file', + }, + // 启动MCP服务器 + startMCPServer: { + method: 'post', + url: '/api/mcp-servers/start', + }, + // 停止MCP服务器 + stopMCPServer: { + method: 'post', + url: '/api/mcp-servers/stop', + }, + // 重启MCP服务器 + restartMCPServer: { + method: 'post', + url: '/api/mcp-servers/restart', + }, + // 获取MCP服务器状态 + getMCPServerStatus: { + method: 'get', + url: '/api/mcp-servers/status', + }, + // 获取所有MCP服务器状态 + getAllMCPServerStatus: { + method: 'get', + url: '/api/mcp-servers/status/all', + }, + // 获取热门标签 + getMCPPopularTags: { + method: 'get', + url: '/api/mcp-servers/tags/popular', + }, + // 同步MCP服务器信息 + syncMCPServerInfo: { + method: 'post', + url: '/api/mcp-servers/sync-info', + }, }; diff --git a/app/web/components/markdownEditor/index.tsx b/app/web/components/markdownEditor/index.tsx new file mode 100644 index 0000000..8003546 --- /dev/null +++ b/app/web/components/markdownEditor/index.tsx @@ -0,0 +1,106 @@ +import React, { useState } from 'react'; +import { Tabs, Input, Card } from 'antd'; +import { EditOutlined, EyeOutlined } from '@ant-design/icons'; +import MarkdownRenderer from '../markdownRenderer'; +import './style.scss'; + +const { TextArea } = Input; +const { TabPane } = Tabs; + +interface MarkdownEditorProps { + value?: string; + onChange?: (value: string) => void; + placeholder?: string; + height?: number; + disabled?: boolean; + className?: string; + style?: React.CSSProperties; +} + +const MarkdownEditor: React.FC = ({ + value = '', + onChange, + placeholder = '请输入Markdown内容...', + height = 300, + disabled = false, + className = '', + style +}) => { + const [activeTab, setActiveTab] = useState('edit'); + + const handleContentChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + onChange?.(newValue); + }; + + return ( +
+ + + + + 编写 + + } + key="edit" + > +
+