diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 0a38429..ba302f3 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -11,9 +11,11 @@ on: jobs: setup: runs-on: ubuntu-latest + env: + NODE_OPTIONS: --openssl-legacy-provider strategy: matrix: - node-version: [14.x, 16.x] + node-version: [18.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - name: Checkout code diff --git a/agent.js b/agent.js index d58dcad..eb6c0d6 100644 --- a/agent.js +++ b/agent.js @@ -1,3 +1,5 @@ +const http = require('http'); +const env = require('./env.json'); const { createTimedTask, changeTimedTask, @@ -5,9 +7,32 @@ const { timedTaskList, timedTaskResult, } = require('./app/utils/timedTask'); +const { startMcpInspector } = require('./app/agent/mcpInspector'); +const { MCPHttpHandler } = require('./app/agent/mcpHttpHandler'); +const { MCPProxy } = require('./app/mcp/mcpProxy'); +const { buildMCPConfig } = require('./app/utils'); // 接收 app 发送来的消息并作出反应 module.exports = (agent) => { + // 初始化MCP端点HTTP处理器 + const mcpHttpHandler = new MCPHttpHandler(agent); + const mcpProxy = new MCPProxy(agent.logger); + let httpServer = null; + + // 等待Worker进程启动完成后再启动MCP服务 + agent.messenger.once('egg-ready', async () => { + try { + agent.logger.info('Worker进程已就绪,开始启动MCP端点HTTP服务...'); + + httpServer = await createHttpServer(agent, mcpHttpHandler); + + // 启动所有已注册的MCP代理服务 + await startMCPServices(agent); + } catch (error) { + agent.logger.error('MCP端点HTTP服务启动失败:', error); + } + }); + // 创建文章订阅任务 agent.messenger.on('createTimedTask', ({ id, sendCron }) => { createTimedTask(id, sendCron, agent); @@ -32,4 +57,169 @@ module.exports = (agent) => { agent.messenger.on('timedTaskResult', ({ result }) => { timedTaskResult(result, agent); }); + + // 处理MCP服务器启动请求 + agent.messenger.on('mcpStart', async ({ serverId, config }) => { + try { + await mcpProxy.startProxy(serverId, config); + agent.logger.info(`MCP服务器启动成功 [${serverId}]`); + + // 发送成功消息回 worker + agent.messenger.sendRandom('mcpStartResult', { + serverId, + success: true, + }); + } catch (error) { + agent.logger.error(`MCP服务器启动失败 [${serverId}]:`, error); + + // 发送失败消息回 worker + agent.messenger.sendRandom('mcpStartResult', { + serverId, + success: false, + error: error.message, + }); + } + }); + + // 处理MCP服务器停止请求 + agent.messenger.on('mcpStop', async ({ serverId }) => { + try { + await mcpProxy.stopProxy(serverId); + agent.logger.info(`MCP服务器停止成功 [${serverId}]`); + + // 发送成功消息回 worker + agent.messenger.sendToApp('mcpStopResult', { + serverId, + success: true, + }); + } catch (error) { + agent.logger.error(`MCP服务器停止失败 [${serverId}]:`, error); + + // 发送失败消息回 worker + agent.messenger.sendRandom('mcpStopResult', { + serverId, + success: false, + error: error.message, + }); + } + }); + + // 处理MCP服务器重启请求 + agent.messenger.on('mcpRestart', async ({ serverId }) => { + try { + await mcpProxy.restartProxy(serverId); + agent.logger.info(`MCP服务器重启成功 [${serverId}]`); + + // 发送成功消息回 worker + agent.messenger.sendRandom('mcpRestartResult', { + serverId, + success: true, + }); + } catch (error) { + agent.logger.error(`MCP服务器重启失败 [${serverId}]:`, error); + + // 发送失败消息回 worker + agent.messenger.sendRandom('mcpRestartResult', { + serverId, + success: false, + error: error.message, + }); + } + }); + + // 启动MCP Inspector + startMcpInspector(agent); + + // 进程退出前清理 + agent.beforeClose(async () => { + agent.logger.info('Agent进程关闭,清理MCP服务...'); + + try { + // 关闭HTTP服务器 + if (httpServer) { + await new Promise((resolve) => { + httpServer.close(() => { + resolve(); + }); + }); + } + + // 清理MCP代理 + await mcpProxy.cleanup(); + agent.logger.info('MCP服务清理完成'); + } catch (error) { + agent.logger.error('MCP服务清理失败:', error); + } + }); }; + +// 创建HTTP服务(用于处理MCP端点请求) +async function createHttpServer(agent, mcpHttpHandler) { + const port = env.mcpEndpointPort || 7005; + + const server = http.createServer(async (req, res) => { + await mcpHttpHandler.handleRequest(req, res); + }); + + server.listen(port, () => { + agent.logger.info(`MCP端点HTTP服务已启动,监听端口: ${port}`); + }); + + server.on('error', (error) => { + agent.logger.error('MCP端点HTTP服务错误:', error); + }); + + return server; +} + +// 启动所有MCP服务,缓存配置信息 +async function startMCPServices(agent) { + agent.logger.info('开始启动MCP服务...'); + + const ctx = agent.createAnonymousContext(); + + // 获取所有未删除的MCP服务器 + const enabledServers = await ctx.model.McpServer.findAll({ + where: { + is_delete: 0, + }, + }); + + if (enabledServers.length === 0) { + agent.logger.info('无需要启动的MCP服务器'); + return; + } + + agent.logger.info(`启动${enabledServers.length}个MCP服务器...`); + + let successCount = 0; + let failCount = 0; + + // 启动每个服务器 + for (const server of enabledServers) { + try { + // 构建MCP配置对象 + const config = buildMCPConfig(server); + await MCPProxy.getInstance().startProxy(server.server_id, config); + + successCount++; + } catch (error) { + agent.logger.error(`MCP服务器启动失败 [${server.server_id}]:`, error); + + // 更新状态为错误 + await server.update({ + status: 'error', + ping_error: error.message, + last_ping_at: new Date(), + }); + + failCount++; + } + } + + setTimeout(() => { + agent.messenger.sendRandom('mcpCheckAllServersHealth'); + }, 2000); + + agent.logger.info(`MCP服务启动完成: 成功${successCount}个, 失败${failCount}个`); +} diff --git a/app.js b/app.js index f76c343..46a09be 100644 --- a/app.js +++ b/app.js @@ -21,5 +21,49 @@ module.exports = class AppBootHook { await ctx.service.articleSubscription.sendArticleSubscription(id); }); }); + + // 监听 MCP 服务器启动结果 + app.messenger.on('mcpStartResult', (data) => { + const { serverId, success, error } = data; + const ctx = app.createAnonymousContext(); + ctx.runInBackground(async () => { + await ctx.service.mcp.handleMCPStartResult(serverId, success, error); + }); + }); + + // 监听 MCP 服务器停止结果 + app.messenger.on('mcpStopResult', (data) => { + const { serverId, success, error } = data; + const ctx = app.createAnonymousContext(); + ctx.runInBackground(async () => { + await ctx.service.mcp.handleMCPStopResult(serverId, success, error); + }); + }); + + // 监听 MCP 服务器重启结果 + app.messenger.on('mcpRestartResult', (data) => { + const { serverId, success, error } = data; + const ctx = app.createAnonymousContext(); + ctx.runInBackground(async () => { + await ctx.service.mcp.handleMCPRestartResult(serverId, success, error); + }); + }); + + // 监听 MCP 服务器健康检查请求 + app.messenger.on('mcpCheckAllServersHealth', () => { + const ctx = app.createAnonymousContext(); + ctx.runInBackground(async () => { + await ctx.service.mcp.checkAllServersHealth(); + }); + }); + + // 监听 MCP 请求埋点 + app.messenger.on('mcpTraceRequest', (data) => { + const ctx = app.createAnonymousContext(); + ctx.runInBackground(async () => { + const { serverId } = data; + await ctx.service.mcp.incrementUseCount(serverId); + }); + }); } }; diff --git a/app/agent/mcpHttpHandler.js b/app/agent/mcpHttpHandler.js new file mode 100644 index 0000000..9409546 --- /dev/null +++ b/app/agent/mcpHttpHandler.js @@ -0,0 +1,130 @@ +const { MCPProxy } = require('../mcp/mcpProxy'); +const getRawBody = require('raw-body'); +const contentType = require('content-type'); + +/** + * Agent进程的MCP HTTP请求处理器 + */ +class MCPHttpHandler { + constructor(agent) { + this.agent = agent; + this.logger = agent.logger; + } + + /** + * 处理HTTP请求 + * @param {http.IncomingMessage} req - 原生HTTP请求对象 + * @param {http.ServerResponse} res - 原生HTTP响应对象 + */ + async handleRequest(req, res) { + try { + const parsedUrl = new URL(req.url, 'http://localhost'); + const pathname = parsedUrl.pathname; + + // 将查询参数转换为对象 + const query = {}; + parsedUrl.searchParams.forEach((value, key) => { + query[key] = value; + }); + req.query = query; + + // 处理/mcp-endpoint/:serverId/*路径 - MCP代理端点 + if (pathname.startsWith('/mcp-endpoint/')) { + return await this.handleMCPEndpoint(req, res, pathname); + } + + // 其他路径返回404 + this.sendResponse(res, 404, { error: 'Not Found' }); + } catch (error) { + this.logError('HTTP请求处理失败:', error); + this.sendResponse(res, 500, { + error: error.message || 'Internal Server Error', + }); + } + } + + /** + * 处理MCP代理端点请求 + * @param {http.IncomingMessage} req + * @param {http.ServerResponse} res + * @param {string} pathname + */ + async handleMCPEndpoint(req, res, pathname) { + // 解析路径: /mcp-endpoint/:serverId/... + const pathParts = pathname.split('/'); + if (pathParts.length < 3) { + return this.sendResponse(res, 400, { error: 'Invalid endpoint path' }); + } + + const serverId = pathParts[2]; + if (!serverId) { + return this.sendResponse(res, 400, { error: 'Server ID is required' }); + } + + // 转发到MCPProxy + try { + if (req.method === 'POST') { + const body = await getRawBody(req, { + length: req.headers['content-length'], + // 也许存在blob资源,需要设置较大的值 + limit: '100mb', + encoding: + req.method === 'POST' ? contentType.parse(req).parameters.charset : 'utf-8', + }); + req.body = JSON.parse(body); + + // 调用埋点 + this.agent.messenger.sendRandom('mcpTraceRequest', { + serverId, + data: req.body, + }); + } + + const mcpProxy = MCPProxy.getInstance(); + await mcpProxy.forwardRequest(serverId, req, res); + } catch (error) { + this.logError(`MCP请求转发失败 [${serverId}]:`, error); + if (!res.headersSent) { + this.sendResponse(res, 500, { error: error.message }); + } + } + } + + /** + * 发送响应 + * @param {http.ServerResponse} res + * @param {number} statusCode + * @param {any} data + */ + sendResponse(res, statusCode, data) { + if (!res.headersSent) { + res.statusCode = statusCode; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(data)); + } + } + + /** + * 日志输出 + */ + log(...args) { + if (this.logger && this.logger.info) { + this.logger.info(...args); + } else { + console.log('[MCPHttpHandler]', ...args); + } + } + + /** + * 错误日志输出 + */ + logError(...args) { + if (this.logger && this.logger.error) { + this.logger.error(...args); + } else { + console.error('[MCPHttpHandler Error]', ...args); + } + } +} + +module.exports = { MCPHttpHandler }; diff --git a/app/agent/mcpInspector.js b/app/agent/mcpInspector.js new file mode 100644 index 0000000..3ca03b6 --- /dev/null +++ b/app/agent/mcpInspector.js @@ -0,0 +1,63 @@ +const ip = require('ip'); +const { spawn } = require('child_process'); +const env = require('../../env.json'); + +const { mcpInspectorWebPort, mcpInspectorServerPort, mcpInspectorDomainWhiteList } = env; + +const startMcpInspector = (agent) => { + const localIP = ip.address(); + const domainWhiteList = []; + [9000, 9001, 9002, mcpInspectorWebPort].forEach((port) => { + domainWhiteList.push(`http://localhost:${port}`); + domainWhiteList.push(`http://127.0.0.1:${port}`); + domainWhiteList.push(`http://${localIP}:${port}`); + domainWhiteList.push(...mcpInspectorDomainWhiteList.map((domain) => `${domain}:${port}`)); + }); + + const child = spawn( + 'npx', + [ + // 如果fetch failed, 可以尝试切换到国内镜像 + '--registry=https://registry.npmjs.org', + '-y', + '@modelcontextprotocol/inspector@0.17.5', + ], + { + stdio: ['pipe', 'pipe', 'pipe'], + cwd: process.cwd(), + env: { + ...process.env, + CLIENT_PORT: mcpInspectorWebPort, + SERVER_PORT: mcpInspectorServerPort, + MCP_AUTO_OPEN_ENABLED: false, + HOST: '0.0.0.0', + DANGEROUSLY_OMIT_AUTH: true, + ALLOWED_ORIGINS: domainWhiteList.join(','), + }, + } + ); + + agent.logger.info('\n *** MCP INSPECTOR STARTED ***'); + + child.stdout.on('data', (data) => { + agent.logger.info('\n *** MCP INSPECTOR STDOUT ***', data.toString()); + }); + + child.stderr.on('data', (data) => { + agent.logger.info('\n *** MCP INSPECTOR STDERR ***', data.toString()); + }); + + child.on('close', (code) => { + agent.logger.error('\n *** MCP INSPECTOR CLOSED ***', code); + }); + + child.on('error', (error) => { + agent.logger.error('\n *** MCP INSPECTOR ERROR ***', error); + }); + + return child; +}; + +module.exports = { + startMcpInspector, +}; diff --git a/app/controller/mcp.js b/app/controller/mcp.js new file mode 100644 index 0000000..147ba51 --- /dev/null +++ b/app/controller/mcp.js @@ -0,0 +1,270 @@ +const Controller = require('egg').Controller; + +class MCPController extends Controller { + 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; + + let files = []; + if (ctx.request.files) { + files = Array.isArray(ctx.request.files) ? ctx.request.files : [ctx.request.files]; + } else if (body.files) { + 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.args && typeof body.args === 'string') { + try { + body.args = JSON.parse(body.args); + } catch (e) { + // 解析失败时保持原值 + ctx.logger.warn('参数解析失败:', body.args); + } + } + + 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); + ctx.body = app.utils.response(true, result, '注册成功'); + } 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; + + let files = []; + if (ctx.request.files) { + files = Array.isArray(ctx.request.files) ? ctx.request.files : [ctx.request.files]; + } else if (body.files) { + 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.args && typeof body.args === 'string') { + try { + body.args = JSON.parse(body.args); + } catch (e) { + // 解析失败时保持原值 + ctx.logger.warn('参数解析失败:', body.args); + } + } + + 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); + ctx.body = app.utils.response(true, result, '更新成功'); + } 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 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 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); + } + } +} +module.exports = MCPController; diff --git a/app/mcp/mcpClient.js b/app/mcp/mcpClient.js new file mode 100644 index 0000000..b636216 --- /dev/null +++ b/app/mcp/mcpClient.js @@ -0,0 +1,303 @@ +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); + + 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); + 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); + 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); + 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); + 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; + } + + if (prompts.status === 'fulfilled') { + serverInfo.prompts = prompts.value; + } + + if (resources.status === 'fulfilled') { + serverInfo.resources = resources.value; + } + + if (capabilities.status === 'fulfilled') { + serverInfo.capabilities = capabilities.value; + } + + return serverInfo; + } catch (error) { + this.logger?.error('获取服务器信息失败:', 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(); + } catch (error) { + this.logger?.warn('关闭MCP客户端连接失败:', error); + } 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..7f617bf --- /dev/null +++ b/app/mcp/mcpProcessManager.js @@ -0,0 +1,128 @@ +const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js'); + +/** + * MCP进程管理器 + */ +class MCPProcessManager { + constructor(logger) { + this.mcpTransports = new Map(); + this.serverConfigs = new Map(); + this.logger = logger; + } + + async createStdioProcess(serverId, config) { + if (!config.command) { + const error = new Error(`服务器 ${serverId} 的 stdio 传输需要指定 command 字段`); + this.logger?.error(`创建进程失败 [${serverId}]: 缺少command`); + throw error; + } + + await this.stopStdioProcess(serverId); + + try { + 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(); + + return transport; + } catch (error) { + this.logger?.error(`启动进程失败 [${serverId}]:`, error); + throw error; + } + } + + 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') { + try { + messageObject = JSON.parse(message); + } catch (parseError) { + this.logger?.error(`消息格式错误 [${serverId}]`); + throw new Error('消息格式错误,无法解析JSON'); + } + } else { + messageObject = message; + } + + // 使用MCP客户端发送请求 + await transport.send(messageObject); + + return true; + } catch (error) { + this.logger?.error(`向MCP服务器发送消息失败 [${serverId}]:`, error); + throw error; + } + } + + async stopStdioProcess(serverId) { + const transport = this.mcpTransports.get(serverId); + if (!transport) { + return; + } + + try { + await transport.close(); + } catch (error) { + this.logger?.warn(`关闭MCP客户端连接失败 [${serverId}]:`, error); + } + + 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) { + const error = new Error(`服务器 ${serverId} 的配置不存在`); + this.logger?.error(`重启失败 [${serverId}]: 配置不存在`); + throw error; + } + + try { + await this.stopStdioProcess(serverId); + await this.createStdioProcess(serverId, config); + } catch (error) { + this.logger?.error(`重启失败 [${serverId}]:`, error); + throw error; + } + } + + async cleanup() { + const serverIds = Array.from(this.mcpTransports.keys()); + const count = serverIds.length; + + if (count === 0) { + return; + } + + try { + await Promise.all(serverIds.map((serverId) => this.stopStdioProcess(serverId))); + } catch (error) { + this.logger?.error('清理进程失败:', error); + throw error; + } + } +} + +module.exports = { MCPProcessManager }; diff --git a/app/mcp/mcpProxy.js b/app/mcp/mcpProxy.js new file mode 100644 index 0000000..506a317 --- /dev/null +++ b/app/mcp/mcpProxy.js @@ -0,0 +1,173 @@ +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(logger); + this.processManager = new MCPProcessManager(logger); + this.transportHandlers = new Map(); + + // 传输处理器实例 + this.stdioHandler = new StdioTransportHandler( + this.processManager, + this.requestManager, + logger + ); + this.sseHandler = new SSETransportHandler(logger); + this.httpHandler = new StreamableHttpTransportHandler(logger); + + // 保存实例 + 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); + } catch (error) { + this.logger?.error(`启动代理失败 [${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); + } + } catch (error) { + this.logger?.error(`停止代理失败 [${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) { + this.logger?.error(`服务器未运行 [${serverId}]`); + throw new Error(`服务器 ${serverId} 未运行`); + } + + try { + await handler.forward(serverId, request, response); + } catch (error) { + this.logger?.error(`转发请求失败 [${serverId}]:`, error); + throw error; + } + } + + async restartProxy(serverId) { + const handler = this.transportHandlers.get(serverId); + if (!handler) { + this.logger?.error(`重启失败 [${serverId}]: 服务器不存在`); + 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) + ); + } catch (error) { + this.logger?.error(`重启失败 [${serverId}]:`, error); + throw error; + } + } else { + const error = new Error(`服务器类型不支持重启: ${serverId}`); + this.logger?.error(`重启失败 [${serverId}]:`, error); + throw error; + } + } + + async cleanup() { + const serverIds = Array.from(this.transportHandlers.keys()); + + if (serverIds.length === 0) { + return; + } + + try { + await Promise.all(serverIds.map((serverId) => this.stopProxy(serverId))); + await this.processManager.cleanup(); + } catch (error) { + this.logger?.error('清理失败:', error); + throw error; + } + } +} + +module.exports = { MCPProxy }; diff --git a/app/mcp/mcpRequestManager.js b/app/mcp/mcpRequestManager.js new file mode 100644 index 0000000..e2aae28 --- /dev/null +++ b/app/mcp/mcpRequestManager.js @@ -0,0 +1,156 @@ +const { randomUUID } = require('crypto'); + +/** + * MCP请求管理器 + * 负责管理待处理请求队列和ID映射 + */ +class MCPRequestManager { + constructor(logger) { + this.pendingRequests = new Map(); + this.logger = logger; + } + + /** + * 初始化请求队列 + * @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} 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) { + const error = new Error(`服务器 ${serverId} 的请求队列未初始化`); + this.logger?.error(`请求队列未初始化 [${serverId}]`); + throw error; + } + + // 设置超时 + const timeout = setTimeout(() => { + this.logger?.warn(`请求超时 [${serverId}]`); + this.removePendingRequest(serverId, internalId); + reject(new Error(`请求超时: 客户端ID=${clientId}, 内部ID=${internalId}`)); + }, timeoutMs); + + // 创建待处理请求 + const pendingRequest = { + internalId, + clientId, + resolve, + reject, + timeout, + createdAt: Date.now(), + }; + + // 添加到队列(使用内部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) { + 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('服务器正在停止'); + + if (requestMap.size > 0) { + this.logger?.warn(`清理待处理请求 [${serverId}]: ${requestMap.size}个`); + } + + 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 randomUUID(); + } +} + +module.exports = { MCPRequestManager }; diff --git a/app/mcp/transportHandlers.js b/app/mcp/transportHandlers.js new file mode 100644 index 0000000..bd3883a --- /dev/null +++ b/app/mcp/transportHandlers.js @@ -0,0 +1,602 @@ +const { SSEClientTransport } = require('@modelcontextprotocol/sdk/client/sse.js'); +const { SSEServerTransport } = require('@modelcontextprotocol/sdk/server/sse.js'); +const { + StreamableHTTPClientTransport, +} = require('@modelcontextprotocol/sdk/client/streamableHttp.js'); +const { + StreamableHTTPServerTransport, +} = require('@modelcontextprotocol/sdk/server/streamableHttp.js'); +const env = require('../../env.json'); + +/** STDIO 消息响应超时时间 */ +const STDIO_MESSAGE_RESPONSE_TIMEOUT = 60000; + +class BaseTransportHandler { + constructor(logger) { + this.logger = logger; + } + + /** + * 启动服务,记录传输配置 + * @param {string} serverId - 服务器ID + * @param {object} config - 配置 + */ + // @ts-ignore + start() { + throw new Error('Start Method Not Implemented'); + } + + /** + * 关闭传输 + * @param {string} serverId - 服务器ID + */ + // @ts-ignore + stop() { + throw new Error('Stop Method Not Implemented'); + } + + /** + * 请求路由入口 + * @param {string} serverId - 服务器ID + * @param {any} req - 请求对象 + * @param {object} res - 响应对象 + */ + // @ts-ignore + forward() { + throw new Error('Forward Method Not Implemented'); + } + + /** + * 返回405 Method Not Allowed + * @param {object} res - 响应对象 + */ + endWithMethodNotAllowed(res) { + res.writeHead(405, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed.', + }, + id: null, + }) + ); + } +} + +/** + * STDIO传输处理器 + */ +class StdioTransportHandler extends BaseTransportHandler { + constructor(processManager, requestManager, logger) { + super(logger); + this.processManager = processManager; + this.requestManager = requestManager; + } + + async start(serverId, config) { + if (!config.command) { + throw new Error(`服务器 ${serverId} 的 stdio 传输需要指定 command 字段`); + } + + // 初始化请求队列 + this.requestManager.initializeServerQueue(serverId); + + if (this.processManager.isProcessRunning(serverId)) { + await this.processManager.stopStdioProcess(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) { + try { + await this.processManager.stopStdioProcess(serverId); + this.requestManager.deleteServerQueue(serverId); + } catch (error) { + this.logger?.error(`[STDIO] 停止失败 [${serverId}]:`, error); + throw error; + } + } + + 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}`); + } + } + + async handlePost(serverId, req, res) { + const isRunning = this.processManager.isProcessRunning(serverId); + + if (!isRunning) { + const error = new Error(`服务器 ${serverId} 的进程未运行`); + this.logger?.error(`[STDIO] 进程未运行 [${serverId}]`); + throw error; + } + + // 确保消息符合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 }; + + return new Promise(async (resolve, reject) => { + try { + // 添加到待处理请求队列 + this.requestManager.addPendingRequest( + serverId, + internalId, + clientId, + // stdio响应 => http响应 + (response) => { + // 结束请求 + res.writeHead(200, { 'content-type': 'application/json' }); + res.end(JSON.stringify(response)); + resolve({ + response, + headers: { 'content-type': 'application/json' }, + status: 200, + }); + }, + (error) => { + this.logger?.error(`[STDIO] 请求失败 [${serverId}]:`, error); + reject(error); + }, + STDIO_MESSAGE_RESPONSE_TIMEOUT + ); + + // 发送请求到MCP服务器 + await this.processManager.sendMessage(serverId, messageWithInternalId); + } catch (error) { + this.requestManager.removePendingRequest(serverId, internalId); + reject(error); + } + }); + } + + /** + * 创建长连接,暂不支持 + */ + async handleGet(serverId, req, res) { + const isRunning = this.processManager.isProcessRunning(serverId); + if (!isRunning) { + throw new Error(`服务器 ${serverId} 的进程未运行`); + } + + this.endWithMethodNotAllowed(res); + } + + /** + * 删除请求,暂不支持 + */ + async handleDelete(serverId, req, res) { + const isRunning = this.processManager.isProcessRunning(serverId); + if (!isRunning) { + throw new Error(`服务器 ${serverId} 的进程未运行`); + } + + this.endWithMethodNotAllowed(res); + } + + /** + * 处理进程数据 + * @param {string} serverId - 服务器ID + * @param {object} data - 数据 + * @private + */ + handleProcessData(serverId, data) { + const internalId = data.id; + if (internalId === undefined || internalId === null) { + return; + } + + const 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 { + this.logger?.error(`[STDIO] 无效响应格式 [${serverId}]`); + 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(logger) { + super(logger); + this.sseProxies = new Map(); + this.clientTransports = new Map(); + this.serverTransports = new Map(); + } + + async start(serverId, config) { + if (!config.sseUrl) { + const error = new Error('SSE传输需要提供URL'); + this.logger?.error(`[SSE] 启动失败 [${serverId}]:`, error); + throw error; + } + + this.sseProxies.set(serverId, { + url: config.sseUrl, + }); + } + + async stop(serverId) { + const sseProxy = this.sseProxies.get(serverId); + if (sseProxy) { + try { + this.sseProxies.delete(serverId); + } catch (error) { + this.logger?.error(`[SSE] 停止失败 [${serverId}]:`, error); + throw error; + } + } + } + + 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}`); + } + } + + async handlePost(serverId, req, res) { + const sseProxy = this.sseProxies.get(serverId); + + if (!sseProxy) { + this.logger?.error(`[SSE] 连接未找到 [${serverId}]`); + throw new Error(`服务器 ${serverId} 的SSE连接未找到`); + } + + const serverTransport = this.serverTransports.get(req.query.sessionId); + + if (!serverTransport) { + this.logger?.error(`[SSE] Session不存在 [${serverId}]`); + throw new Error(`session ${req.query.sessionId} 不存在`); + } + + try { + await serverTransport.handlePostMessage(req, res, req.body); + } catch (error) { + this.logger?.error(`[SSE] POST失败 [${serverId}]:`, error); + throw error; + } + } + + async handleGet(serverId, req, res) { + const sseProxy = this.sseProxies.get(serverId); + if (!sseProxy) { + this.logger?.error(`[SSE] 连接未找到 [${serverId}]`); + throw new Error(`服务器 ${serverId} 的SSE连接未找到`); + } + + let serverClosed = false; + let clientClosed = false; + + const headers = { + ...req.headers, + accept: 'text/event-stream', + }; + + // 获取原请求的协议和主机名 + const protocol = + req.headers['x-forwarded-proto'] || (req.socket.encrypted ? 'https' : 'http'); + const hostname = (req.headers.host || 'localhost').split(':')[0]; + + try { + const clientTransport = new SSEClientTransport(new URL(sseProxy.url), { + headers, + }); + + await clientTransport.start(); + const mcpEndpointPort = env.mcpEndpointPort || 7005; + const messageEndpoint = `${protocol}://${hostname}:${mcpEndpointPort}/mcp-endpoint/${serverId}/messages`; + + const serverTransport = new SSEServerTransport(messageEndpoint, 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 = () => { + serverClosed = true; + if (!clientClosed) { + clientTransport.close().catch(() => {}); + } + }; + + clientTransport.onclose = () => { + clientClosed = true; + if (!serverClosed) { + serverTransport.close().catch(() => {}); + } + }; + } catch (error) { + this.logger?.error(`[SSE] GET失败 [${serverId}]:`, error); + throw error; + } + } +} + +/** + * Streamable HTTP传输处理器 + */ +class StreamableHttpTransportHandler extends BaseTransportHandler { + constructor(logger) { + super(logger); + this.httpProxies = new Map(); + this.clientTransports = new Map(); + this.serverTransports = new Map(); + } + + async start(serverId, config) { + if (!config.httpUrl) { + const error = new Error('Streamable HTTP传输需要提供URL'); + this.logger?.error(`[HTTP] 启动失败 [${serverId}]:`, error); + throw error; + } + + // 验证URL格式 + try { + new URL(config.httpUrl); + } catch (error) { + this.logger?.error(`[HTTP] URL无效 [${serverId}]:`, error); + throw new Error(`无效的URL格式: ${config.httpUrl}`); + } + + this.httpProxies.set(serverId, { + url: config.httpUrl, + }); + } + + async stop(serverId) { + const httpProxy = this.httpProxies.get(serverId); + if (httpProxy) { + try { + this.httpProxies.delete(serverId); + } catch (error) { + this.logger?.error(`[HTTP] 停止失败 [${serverId}]:`, error); + throw error; + } + } + } + + 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); + case 'DELETE': + return this.handleDelete(serverId, req, res); + default: + throw new Error(`Streamable HTTP传输不支持的HTTP方法: ${method}`); + } + } + + async handlePost(serverId, req, res) { + const httpProxy = this.httpProxies.get(serverId); + + if (!httpProxy) { + this.logger?.error(`[HTTP] 代理未找到 [${serverId}]`); + throw new Error(`服务器 ${serverId} 的HTTP代理未找到`); + } + + const sessionId = req.headers['mcp-session-id']; + + let clientTransport = null; + let serverTransport = null; + + if (sessionId) { + clientTransport = this.clientTransports.get(sessionId); + serverTransport = this.serverTransports.get(sessionId); + if (!clientTransport || !serverTransport) { + this.logger?.error(`[HTTP] Session不存在 [${serverId}]`); + throw new Error(`session ${sessionId} transport 不存在`); + } + } else { + const headers = { + ...req.headers, + accept: 'text/event-stream, application/json', + }; + delete headers['content-length']; + let clientClosed = false; + let serverClosed = false; + + clientTransport = new StreamableHTTPClientTransport(new URL(httpProxy.url), { + requestInit: { headers }, + }); + serverTransport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => require('crypto').randomUUID(), + onsessioninitialized: (newSessionId) => { + this.serverTransports.set(newSessionId, serverTransport); + this.clientTransports.set(newSessionId, clientTransport); + }, + }); + + serverTransport.onmessage = (message) => clientTransport.send(message); + clientTransport.onmessage = (message) => serverTransport.send(message); + serverTransport.onclose = () => { + serverClosed = true; + if (!clientClosed) { + clientTransport.close().catch(() => {}); + } + }; + + clientTransport.onclose = () => { + clientClosed = true; + if (!serverClosed) { + serverTransport.close().catch(() => {}); + } + }; + + await serverTransport.start(); + } + + try { + await serverTransport.handleRequest(req, res, req.body); + } catch (error) { + this.logger?.error(`[HTTP] POST失败 [${serverId}]:`, error); + throw error; + } + } + + async handleGet(serverId, req, res) { + const httpProxy = this.httpProxies.get(serverId); + if (!httpProxy) { + throw new Error(`服务器 ${serverId} 的HTTP代理未找到`); + } + + const sessionId = req.headers['mcp-session-id']; + const serverTransport = this.serverTransports.get(sessionId); + if (!serverTransport) { + throw new Error('session id is required'); + } + + await serverTransport.handleRequest(req, res, req.body); + } + + async handleDelete(serverId, req, res) { + const httpProxy = this.httpProxies.get(serverId); + + if (!httpProxy) { + this.logger?.error(`[HTTP] 代理未找到 [${serverId}]`); + throw new Error(`服务器 ${serverId} 的HTTP代理未找到`); + } + + const sessionId = req.headers['mcp-session-id']; + const serverTransport = this.serverTransports.get(sessionId); + + if (!serverTransport) { + this.logger?.error(`[HTTP] Session不存在 [${serverId}]`); + throw new Error(`session ${sessionId} 不存在`); + } + + try { + await serverTransport.handleRequest(req, res, req.body); + + const clientTransport = this.clientTransports.get(sessionId); + if (clientTransport) { + // 向真实MCP服务器发送DELETE请求 + await clientTransport.terminateSession(); + await clientTransport.close(); + this.clientTransports.delete(sessionId); + } + + this.serverTransports.delete(sessionId); + } catch (error) { + this.logger?.error(`[HTTP] DELETE失败 [${serverId}]:`, error); + throw error; + } + } +} + +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..8d6bab1 --- /dev/null +++ b/app/model/mcp_server.js @@ -0,0 +1,237 @@ +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)', + }, + short_description: { + type: STRING(500), + comment: '简短描述,用于卡片展示', + }, + 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..5622be6 100644 --- a/app/router.js +++ b/app/router.js @@ -124,6 +124,28 @@ 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); + + // 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.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); + // 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..0408fb5 --- /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 */12 * * *', // 每12小时检测一次 + 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..b3254ce --- /dev/null +++ b/app/service/mcp.js @@ -0,0 +1,955 @@ +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 { MCPClient } = require('../mcp/mcpClient'); +const { convertKeysToSnakeCase, buildMCPConfig } = require('../utils'); + +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 - 搜索关键词 + * @param {Boolean} params.showAll - 是否显示所有服务器 + */ + 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, + 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, + shortDescription, + 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 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, + short_description: shortDescription, + author, + version, + tags: Array.isArray(tags) ? tags : [], + transport, + git_url: gitUrl, + deploy_path: deployPath, + }; + + // 根据传输类型设置特定字段 + if (transport === 'stdio') { + serverData.command = command; + serverData.args = args; + 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; + const 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.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.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 + ); + } + } + } + + await server.update(convertKeysToSnakeCase(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 }); + + if (server.deploy_path && fs.existsSync(server.deploy_path)) { + try { + fs.rmSync(server.deploy_path, { recursive: true, force: true }); + this.ctx.logger.info(`部署目录已清理: ${server.deploy_path}`); + } catch (error) { + this.ctx.logger.error(`清理部署目录失败: ${server.deploy_path}`, error); + } + } + + 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服务器信息(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 = buildMCPConfig(server); + + // 通过messenger发送启动请求到agent进程 + // agent 会在完成后发送结果消息,由 app.js 监听并更新数据库 + this.app.messenger.sendToAgent('mcpStart', { serverId, config }); + + this.ctx.logger.info(`MCP服务器启动请求已发送: ${serverId}`); + + return true; + } + + /** + * 停止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}`); + + // 通过messenger发送停止请求到agent进程 + // agent 会在完成后发送结果消息,由 app.js 监听并更新数据库 + this.app.messenger.sendToAgent('mcpStop', { serverId }); + + this.ctx.logger.info(`MCP服务器停止请求已发送: ${serverId}`); + return true; + } + + /** + * 重启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}`); + + // 通过messenger发送重启请求到agent进程 + // agent 会在完成后发送结果消息,由 app.js 监听并更新数据库 + this.app.messenger.sendToAgent('mcpRestart', { serverId }); + + this.ctx.logger.info(`MCP服务器重启请求已发送: ${serverId}`); + return true; + } + + /** + * 处理MCP服务器启动结果(由消息监听器调用) + * @param {string} serverId - 服务器ID + * @param {boolean} success - 是否成功 + * @param {string} error - 错误信息 + */ + async handleMCPStartResult(serverId, success, error) { + try { + if (success) { + try { + const healthResult = await this.checkMCPServerHealth(serverId); + await this.ctx.model.McpServer.update( + { + status: healthResult.healthy ? 'running' : 'error', + ping_error: healthResult.error || null, + last_ping_at: new Date(), + }, + { where: { server_id: serverId } } + ); + this.ctx.logger.info( + `MCP服务器启动后状态已更新 [${serverId}]: ${ + healthResult.healthy ? 'running' : 'error' + }` + ); + } catch (checkError) { + this.ctx.logger.error(`MCP服务器启动后状态检查失败 [${serverId}]:`, checkError); + } + } else { + // 启动失败,更新状态为错误 + await this.ctx.model.McpServer.update( + { + status: 'error', + ping_error: error, + last_ping_at: new Date(), + }, + { where: { server_id: serverId } } + ); + this.ctx.logger.error(`MCP服务器启动失败 [${serverId}]: ${error}`); + } + } catch (updateError) { + this.ctx.logger.error(`更新MCP服务器状态失败 [${serverId}]:`, updateError); + } + } + + /** + * 处理MCP服务器停止结果(由消息监听器调用) + * @param {string} serverId - 服务器ID + * @param {boolean} success - 是否成功 + * @param {string} error - 错误信息 + */ + async handleMCPStopResult(serverId, success, error) { + try { + await this.ctx.model.McpServer.update( + { + status: success ? 'stopped' : 'error', + ping_error: success ? null : error, + last_ping_at: new Date(), + }, + { where: { server_id: serverId } } + ); + this.ctx.logger.info( + `MCP服务器停止后状态已更新 [${serverId}]: ${success ? 'stopped' : 'error'}` + ); + } catch (updateError) { + this.ctx.logger.error(`更新MCP服务器停止状态失败 [${serverId}]:`, updateError); + } + } + + /** + * 处理MCP服务器重启结果(由消息监听器调用) + * @param {string} serverId - 服务器ID + * @param {boolean} success - 是否成功 + * @param {string} error - 错误信息 + */ + async handleMCPRestartResult(serverId, success, error) { + try { + if (success) { + try { + const healthResult = await this.checkMCPServerHealth(serverId); + await this.ctx.model.McpServer.update( + { + status: healthResult.healthy ? 'running' : 'error', + ping_error: healthResult.error || null, + last_ping_at: new Date(), + }, + { where: { server_id: serverId } } + ); + this.ctx.logger.info( + `MCP服务器重启后状态已更新 [${serverId}]: ${ + healthResult.healthy ? 'running' : 'error' + }` + ); + } catch (checkError) { + this.ctx.logger.error(`MCP服务器重启后状态检查失败 [${serverId}]:`, checkError); + } + } else { + // 重启失败,更新状态为错误 + await this.ctx.model.McpServer.update( + { + status: 'error', + ping_error: error, + last_ping_at: new Date(), + }, + { where: { server_id: serverId } } + ); + this.ctx.logger.error(`MCP服务器重启失败 [${serverId}]: ${error}`); + } + } catch (updateError) { + this.ctx.logger.error(`更新MCP服务器重启状态失败 [${serverId}]:`, updateError); + } + } +} + +module.exports = MCPService; diff --git a/app/utils/index.js b/app/utils/index.js index 584da8d..bdffe76 100644 --- a/app/utils/index.js +++ b/app/utils/index.js @@ -83,6 +83,65 @@ const sendMsgAfterSendArticle = async (title, text, webhook) => { feChatRobot.markdown(title, text).catch((ex) => console.error(ex)); }; +/** + * 将驼峰命名转换为下划线命名 + * @param {string} str - 驼峰命名的字符串 + * @returns {string} 下划线命名的字符串 + */ +const camelToSnake = (str) => { + return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); +}; + +/** + * 将对象的驼峰命名键转换为下划线命名 + * @param {Object} obj - 需要转换的对象 + * @param {Array} excludeKeys - 需要排除不转换的键名数组 + * @returns {Object} 转换后的对象 + */ +const convertKeysToSnakeCase = (obj, excludeKeys = []) => { + if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { + return obj; + } + + const result = {}; + Object.keys(obj).forEach((key) => { + if (excludeKeys.includes(key)) { + result[key] = obj[key]; + } else { + const snakeKey = camelToSnake(key); + result[snakeKey] = obj[key]; + } + }); + + return result; +}; + +/** + * 构建MCP配置对象 + * @param {Object} server - 数据库中的MCP配置记录 + * @returns {Object} MCP服务配置对象 + */ +const 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; +}; + module.exports = { createFolder, createFileSync, @@ -90,6 +149,9 @@ module.exports = { sendHostsUpdateMsg, sendArticleMsg, sendMsgAfterSendArticle, + camelToSnake, + convertKeysToSnakeCase, + buildMCPConfig, response: (success, data = null, message) => { if (success) { message = '执行成功'; diff --git a/app/view/app.js b/app/view/app.js index e145135..2de7313 100644 --- a/app/view/app.js +++ b/app/view/app.js @@ -1 +1 @@ -!function(n,t){for(var e in t)n[e]=t[e]}(exports,function(n){var t={};function e(a){if(t[a])return t[a].exports;var o=t[a]={i:a,l:!1,exports:{}};return n[a].call(o.exports,o,o.exports,e),o.l=!0,o.exports}return e.m=n,e.c=t,e.d=function(n,t,a){e.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:a})},e.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},e.t=function(n,t){if(1&t&&(n=e(n)),8&t)return n;if(4&t&&"object"==typeof n&&n&&n.__esModule)return n;var a=Object.create(null);if(e.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:n}),2&t&&"string"!=typeof n)for(var o in n)e.d(a,o,function(t){return n[t]}.bind(null,o));return a},e.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return e.d(t,"a",t),t},e.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},e.p="/public/",e(e.s=68)}([function(n,t){n.exports=require("react")},function(n,t){n.exports=require("antd")},function(n,t,e){"use strict";n.exports=function(n){var t=[];return t.toString=function(){return this.map((function(t){var e=function(n,t){var e=n[1]||"",a=n[3];if(!a)return e;if(t&&"function"==typeof btoa){var o=function(n){var t=btoa(unescape(encodeURIComponent(JSON.stringify(n)))),e="sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(t);return"/*# ".concat(e," */")}(a),i=a.sources.map((function(n){return"/*# sourceURL=".concat(a.sourceRoot||"").concat(n," */")}));return[e].concat(i).concat([o]).join("\n")}return[e].join("\n")}(t,n);return t[2]?"@media ".concat(t[2]," {").concat(e,"}"):e})).join("")},t.i=function(n,e,a){"string"==typeof n&&(n=[[null,n,""]]);var o={};if(a)for(var i=0;i1&&void 0!==arguments[1]?arguments[1]:{},e=t.replace,i=void 0!==e&&e,c=t.prepend,d=void 0!==c&&c,p=[],b=0;b0&&o[o.length-1])||6!==l[0]&&2!==l[0])){r=0;continue}if(3===l[0]&&(!o||l[1]>o[0]&&l[1]0?a:e)(n)}},function(n,t,e){var a=e(46)("keys"),o=e(47);n.exports=function(n){return a[n]||(a[n]=o(n))}},function(n){n.exports=JSON.parse('{"webhookUrls":[],"articleResultWebhook":"","msgSingleUrl":"https://dtstack.github.io/doraemon/docsify/#/","helpDocUrl":"https://dtstack.github.io/doraemon/docsify/#/","articleHelpDocUrl":"https://dtstack.github.io/doraemon/docsify/#/zh-cn/guide/%E6%96%87%E7%AB%A0%E8%AE%A2%E9%98%85","proxyHelpDocUrl":"https://dtstack.github.io/doraemon/docsify/#/zh-cn/guide/%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1","mysql":{"prod":{}}}')},function(n,t){n.exports=require("js-cookie")},function(n,t){n.exports=require("prop-types")},function(n,t){n.exports=require("socket.io-parser")},function(n,t,e){"use strict";var a,o;Object.defineProperty(t,"__esModule",{value:!0}),t.SUBSCRIPTIONSENDTYPECN=t.SUBSCRIPTIONSENDTYPE=t.SUBSCRIPTIONSTATUS=void 0,function(n){n[n.OPEN=1]="OPEN",n[n.CLOSE=2]="CLOSE"}(t.SUBSCRIPTIONSTATUS||(t.SUBSCRIPTIONSTATUS={})),function(n){n[n.FRIDAY=0]="FRIDAY",n[n.WORKDAY=1]="WORKDAY",n[n.EVERYDAY=2]="EVERYDAY",n[n.MONDAY=3]="MONDAY"}(o=t.SUBSCRIPTIONSENDTYPE||(t.SUBSCRIPTIONSENDTYPE={})),t.SUBSCRIPTIONSENDTYPECN=((a={})[o.MONDAY]="每周一",a[o.FRIDAY]="每周五",a[o.WORKDAY]="周一至周五",a[o.EVERYDAY]="每天",a)},function(n,t,e){"use strict";var a=e(15);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var o=a(e(76)),i=a(e(77)),r=a(e(36)),l={lang:(0,o.default)({placeholder:"请选择日期",yearPlaceholder:"请选择年份",quarterPlaceholder:"请选择季度",monthPlaceholder:"请选择月份",weekPlaceholder:"请选择周",rangePlaceholder:["开始日期","结束日期"],rangeYearPlaceholder:["开始年份","结束年份"],rangeMonthPlaceholder:["开始月份","结束月份"],rangeWeekPlaceholder:["开始周","结束周"]},i.default),timePickerLocale:(0,o.default)({},r.default)};l.lang.ok="确 定";var s=l;t.default=s},function(n,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a={placeholder:"请选择时间",rangePlaceholder:["开始时间","结束时间"]};t.default=a},function(n,t,e){"use strict";var a=this&&this.__importDefault||function(n){return n&&n.__esModule?n:{default:n}};Object.defineProperty(t,"__esModule",{value:!0});var o=a(e(79)),i=a(e(118)),r=a(e(122)),l=a(e(133)),s=a(e(139)),c=a(e(154)),d=a(e(163)),p=a(e(190)),b=a(e(194)),u=a(e(198)),f=a(e(204)),m=a(e(205)),h=a(e(207)),g=a(e(214)),x=a(e(222)),w=[{path:"/",component:o.default,routes:[{path:"".concat("/page","/toolbox"),component:s.default},{path:"".concat("/page","/home"),component:i.default},{path:"".concat("/page","/internal-url-navigation"),component:l.default},{path:"".concat("/page","/proxy-server"),component:r.default},{path:"".concat("/page","/mail-sign"),component:c.default},{path:"".concat("/page","/host-management"),component:d.default},{path:"".concat("/page","/env-management"),component:p.default},{path:"".concat("/page","/config-center"),component:b.default},{path:"".concat("/page","/config-detail/:id"),component:u.default},{path:"".concat("/page","/switch-hosts-list"),component:f.default},{path:"".concat("/page","/switch-hosts-edit/:id/:type"),component:m.default},{path:"".concat("/page","/article-subscription-list"),component:h.default},{path:"".concat("/page","/tags"),component:g.default},{path:"*",component:x.default}]}];t.default=w},function(n,t){n.exports=require("classnames")},function(n,t,e){e(87);for(var a=e(11),o=e(14),i=e(13),r=e(8)("toStringTag"),l="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),s=0;s=t.length?{value:void 0,done:!0}:(n=a(t,e),this._i+=n.length,{value:n,done:!1})}))},function(n,t,e){var a=e(40),o=e(8)("toStringTag"),i="Arguments"==a(function(){return arguments}());n.exports=function(n){var t,e,r;return void 0===n?"Undefined":null===n?"Null":"string"==typeof(e=function(n,t){try{return n[t]}catch(n){}}(t=Object(n),o))?e:i?a(t):"Object"==(r=a(t))&&"function"==typeof t.callee?"Arguments":r}},function(n,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.changeLocalIp=void 0;var a=e(1),o=e(53);t.changeLocalIp=function(n){return void 0===n&&(n=!1),function(t,e,i){i.API.getLocalIp().then((function(e){var i=e.success,r=e.data;i&&(console.log(r),n&&a.message.success("刷新成功!"),t({type:o.CHANGE_LOCAL_IP,payload:r}))})).catch((function(){n&&a.message.warning("刷新失败!")}))}}},function(n,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CHANGE_LOCAL_IP=void 0,t.CHANGE_LOCAL_IP="CHANGE_LOCAL_IP"},function(n,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.urlReg=void 0,t.urlReg=new RegExp(/(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/,"i")},function(n,t,e){n.exports=e.p+"img/help-icon.3d0854a5.png"},function(n,t,e){"use strict";n.exports=function(n,t){return t||(t={}),"string"!=typeof(n=n&&n.__esModule?n.default:n)?n:(/^['"].*['"]$/.test(n)&&(n=n.slice(1,-1)),t.hash&&(n+=t.hash),/["'() \t\n]/.test(n)||t.needQuotes?'"'.concat(n.replace(/"/g,'\\"').replace(/\n/g,"\\n"),'"'):n)}},function(n,t,e){var a=e(179),o=e(58),i=e(59),r=e(33),l=e(60),s=e(61),c=e(20)("socket.io-client:manager"),d=e(182),p=e(183),b=Object.prototype.hasOwnProperty;function u(n,t){if(!(this instanceof u))return new u(n,t);n&&"object"==typeof n&&(t=n,n=void 0),(t=t||{}).path=t.path||"/socket.io",this.nsps={},this.subs=[],this.opts=t,this.reconnection(!1!==t.reconnection),this.reconnectionAttempts(t.reconnectionAttempts||1/0),this.reconnectionDelay(t.reconnectionDelay||1e3),this.reconnectionDelayMax(t.reconnectionDelayMax||5e3),this.randomizationFactor(t.randomizationFactor||.5),this.backoff=new p({min:this.reconnectionDelay(),max:this.reconnectionDelayMax(),jitter:this.randomizationFactor()}),this.timeout(null==t.timeout?2e4:t.timeout),this.readyState="closed",this.uri=n,this.connecting=[],this.lastPing=null,this.encoding=!1,this.packetBuffer=[],this.encoder=new r.Encoder,this.decoder=new r.Decoder,this.autoConnect=!1!==t.autoConnect,this.autoConnect&&this.open()}n.exports=u,u.prototype.emitAll=function(){for(var n in this.emit.apply(this,arguments),this.nsps)b.call(this.nsps,n)&&this.nsps[n].emit.apply(this.nsps[n],arguments)},u.prototype.updateSocketIds=function(){for(var n in this.nsps)b.call(this.nsps,n)&&(this.nsps[n].id=this.engine.id)},i(u.prototype),u.prototype.reconnection=function(n){return arguments.length?(this._reconnection=!!n,this):this._reconnection},u.prototype.reconnectionAttempts=function(n){return arguments.length?(this._reconnectionAttempts=n,this):this._reconnectionAttempts},u.prototype.reconnectionDelay=function(n){return arguments.length?(this._reconnectionDelay=n,this.backoff&&this.backoff.setMin(n),this):this._reconnectionDelay},u.prototype.randomizationFactor=function(n){return arguments.length?(this._randomizationFactor=n,this.backoff&&this.backoff.setJitter(n),this):this._randomizationFactor},u.prototype.reconnectionDelayMax=function(n){return arguments.length?(this._reconnectionDelayMax=n,this.backoff&&this.backoff.setMax(n),this):this._reconnectionDelayMax},u.prototype.timeout=function(n){return arguments.length?(this._timeout=n,this):this._timeout},u.prototype.maybeReconnectOnOpen=function(){!this.reconnecting&&this._reconnection&&0===this.backoff.attempts&&this.reconnect()},u.prototype.open=u.prototype.connect=function(n,t){if(c("readyState %s",this.readyState),~this.readyState.indexOf("open"))return this;c("opening %s",this.uri),this.engine=a(this.uri,this.opts);var e=this.engine,o=this;this.readyState="opening",this.skipReconnect=!1;var i=l(e,"open",(function(){o.onopen(),n&&n()})),r=l(e,"error",(function(t){if(c("connect_error"),o.cleanup(),o.readyState="closed",o.emitAll("connect_error",t),n){var e=new Error("Connection error");e.data=t,n(e)}else o.maybeReconnectOnOpen()}));if(!1!==this._timeout){var s=this._timeout;c("connect attempt will timeout after %d",s);var d=setTimeout((function(){c("connect attempt timed out after %d",s),i.destroy(),e.close(),e.emit("error","timeout"),o.emitAll("connect_timeout",s)}),s);this.subs.push({destroy:function(){clearTimeout(d)}})}return this.subs.push(i),this.subs.push(r),this},u.prototype.onopen=function(){c("open"),this.cleanup(),this.readyState="open",this.emit("open");var n=this.engine;this.subs.push(l(n,"data",s(this,"ondata"))),this.subs.push(l(n,"ping",s(this,"onping"))),this.subs.push(l(n,"pong",s(this,"onpong"))),this.subs.push(l(n,"error",s(this,"onerror"))),this.subs.push(l(n,"close",s(this,"onclose"))),this.subs.push(l(this.decoder,"decoded",s(this,"ondecoded")))},u.prototype.onping=function(){this.lastPing=new Date,this.emitAll("ping")},u.prototype.onpong=function(){this.emitAll("pong",new Date-this.lastPing)},u.prototype.ondata=function(n){this.decoder.add(n)},u.prototype.ondecoded=function(n){this.emit("packet",n)},u.prototype.onerror=function(n){c("error",n),this.emitAll("error",n)},u.prototype.socket=function(n,t){var e=this.nsps[n];if(!e){e=new o(this,n,t),this.nsps[n]=e;var a=this;e.on("connecting",i),e.on("connect",(function(){e.id=a.engine.id})),this.autoConnect&&i()}function i(){~d(a.connecting,e)||a.connecting.push(e)}return e},u.prototype.destroy=function(n){var t=d(this.connecting,n);~t&&this.connecting.splice(t,1),this.connecting.length||this.close()},u.prototype.packet=function(n){c("writing packet %j",n);var t=this;n.query&&0===n.type&&(n.nsp+="?"+n.query),t.encoding?t.packetBuffer.push(n):(t.encoding=!0,this.encoder.encode(n,(function(e){for(var a=0;a0&&!this.encoding){var n=this.packetBuffer.shift();this.packet(n)}},u.prototype.cleanup=function(){c("cleanup");for(var n=this.subs.length,t=0;t=this._reconnectionAttempts)c("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var t=this.backoff.duration();c("will wait %dms before reconnect attempt",t),this.reconnecting=!0;var e=setTimeout((function(){n.skipReconnect||(c("attempting reconnect"),n.emitAll("reconnect_attempt",n.backoff.attempts),n.emitAll("reconnecting",n.backoff.attempts),n.skipReconnect||n.open((function(t){t?(c("reconnect attempt error"),n.reconnecting=!1,n.reconnect(),n.emitAll("reconnect_error",t.data)):(c("reconnect success"),n.onreconnect())})))}),t);this.subs.push({destroy:function(){clearTimeout(e)}})}},u.prototype.onreconnect=function(){var n=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",n)}},function(n,t,e){var a=e(33),o=e(59),i=e(180),r=e(60),l=e(61),s=e(20)("socket.io-client:socket"),c=e(181);n.exports=b;var d={connect:1,connect_error:1,connect_timeout:1,connecting:1,disconnect:1,error:1,reconnect:1,reconnect_attempt:1,reconnect_failed:1,reconnect_error:1,reconnecting:1,ping:1,pong:1},p=o.prototype.emit;function b(n,t,e){this.io=n,this.nsp=t,this.json=this,this.ids=0,this.acks={},this.receiveBuffer=[],this.sendBuffer=[],this.connected=!1,this.disconnected=!0,e&&e.query&&(this.query=e.query),this.io.autoConnect&&this.open()}o(b.prototype),b.prototype.subEvents=function(){if(!this.subs){var n=this.io;this.subs=[r(n,"open",l(this,"onopen")),r(n,"packet",l(this,"onpacket")),r(n,"close",l(this,"onclose"))]}},b.prototype.open=b.prototype.connect=function(){return this.connected||(this.subEvents(),this.io.open(),"open"===this.io.readyState&&this.onopen(),this.emit("connecting")),this},b.prototype.send=function(){var n=i(arguments);return n.unshift("message"),this.emit.apply(this,n),this},b.prototype.emit=function(n){if(d.hasOwnProperty(n))return p.apply(this,arguments),this;var t=i(arguments),e=a.EVENT;c(t)&&(e=a.BINARY_EVENT);var o={type:e,data:t,options:{}};return o.options.compress=!this.flags||!1!==this.flags.compress,"function"==typeof t[t.length-1]&&(s("emitting packet with ack id %d",this.ids),this.acks[this.ids]=t.pop(),o.id=this.ids++),this.connected?this.packet(o):this.sendBuffer.push(o),delete this.flags,this},b.prototype.packet=function(n){n.nsp=this.nsp,this.io.packet(n)},b.prototype.onopen=function(){s("transport is open - connecting"),"/"!==this.nsp&&(this.query?this.packet({type:a.CONNECT,query:this.query}):this.packet({type:a.CONNECT}))},b.prototype.onclose=function(n){s("close (%s)",n),this.connected=!1,this.disconnected=!0,delete this.id,this.emit("disconnect",n)},b.prototype.onpacket=function(n){if(n.nsp===this.nsp)switch(n.type){case a.CONNECT:this.onconnect();break;case a.EVENT:case a.BINARY_EVENT:this.onevent(n);break;case a.ACK:case a.BINARY_ACK:this.onack(n);break;case a.DISCONNECT:this.ondisconnect();break;case a.ERROR:this.emit("error",n.data)}},b.prototype.onevent=function(n){var t=n.data||[];s("emitting event %j",t),null!=n.id&&(s("attaching ack callback to event"),t.push(this.ack(n.id))),this.connected?p.apply(this,t):this.receiveBuffer.push(t)},b.prototype.ack=function(n){var t=this,e=!1;return function(){if(!e){e=!0;var o=i(arguments);s("sending ack %j",o);var r=c(o)?a.BINARY_ACK:a.ACK;t.packet({type:r,id:n,data:o})}}},b.prototype.onack=function(n){var t=this.acks[n.id];"function"==typeof t?(s("calling ack %s with %j",n.id,n.data),t.apply(this,n.data),delete this.acks[n.id]):s("bad ack %s",n.id)},b.prototype.onconnect=function(){this.connected=!0,this.disconnected=!1,this.emit("connect"),this.emitBuffered()},b.prototype.emitBuffered=function(){var n;for(n=0;n*\/]/.test(p)?l(null,"select-op"):/[;{}:\[\]]/.test(p)?l(null,p):(n.eatWhile(/[\w\\\-]/),l("variable","variable")):l(null,"compare"):void l(null,"compare")}function c(n,t){for(var e,a=!1;null!=(e=n.next());){if(a&&"/"==e){t.tokenize=s;break}a="*"==e}return l("comment","comment")}function d(n,t){for(var e,a=0;null!=(e=n.next());){if(a>=2&&">"==e){t.tokenize=s;break}a="-"==e?a+1:0}return l("comment","comment")}return{startState:function(n){return{tokenize:s,baseIndent:n||0,stack:[]}},token:function(n,t){if(n.eatSpace())return null;e=null;var a=t.tokenize(n,t),o=t.stack[t.stack.length-1];return"hash"==e&&"rule"==o?a="atom":"variable"==a&&("rule"==o?a="number":o&&"@media{"!=o||(a="tag")),"rule"==o&&/^[\{\};]$/.test(e)&&t.stack.pop(),"{"==e?"@media"==o?t.stack[t.stack.length-1]="@media{":t.stack.push("{"):"}"==e?t.stack.pop():"@media"==e?t.stack.push("@media"):"{"==o&&"comment"!=e&&t.stack.push("rule"),a},indent:function(n,t){var e=n.stack.length;return/^\}/.test(t)&&(e-="rule"==n.stack[n.stack.length-1]?2:1),n.baseIndent+e*r},electricChars:"}"}})),n.defineMIME("text/x-nginx-conf","nginx")}(e(65))},function(n,t,e){n.exports=function(){"use strict";var n=navigator.userAgent,t=navigator.platform,e=/gecko\/\d/i.test(n),a=/MSIE \d/.test(n),o=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(n),i=/Edge\/(\d+)/.exec(n),r=a||o||i,l=r&&(a?document.documentMode||6:+(i||o)[1]),s=!i&&/WebKit\//.test(n),c=s&&/Qt\/\d+\.\d+/.test(n),d=!i&&/Chrome\/(\d+)/.exec(n),p=d&&+d[1],b=/Opera\//.test(n),u=/Apple Computer/.test(navigator.vendor),f=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(n),m=/PhantomJS/.test(n),h=u&&(/Mobile\/\w+/.test(n)||navigator.maxTouchPoints>2),g=/Android/.test(n),x=h||g||/webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(n),w=h||/Mac/.test(t),k=/\bCrOS\b/.test(n),v=/win/i.test(t),y=b&&n.match(/Version\/(\d*\.\d*)/);y&&(y=Number(y[1])),y&&y>=15&&(b=!1,s=!0);var z=w&&(c||b&&(null==y||y<12.11)),_=e||r&&l>=9;function F(n){return new RegExp("(^|\\s)"+n+"(?:$|\\s)\\s*")}var C,S=function(n,t){var e=n.className,a=F(t).exec(e);if(a){var o=e.slice(a.index+a[0].length);n.className=e.slice(0,a.index)+(o?a[1]+o:"")}};function E(n){for(var t=n.childNodes.length;t>0;--t)n.removeChild(n.firstChild);return n}function O(n,t){return E(n).appendChild(t)}function D(n,t,e,a){var o=document.createElement(n);if(e&&(o.className=e),a&&(o.style.cssText=a),"string"==typeof t)o.appendChild(document.createTextNode(t));else if(t)for(var i=0;i=t)return r+(t-i);r+=l-i,r+=e-r%e,i=l+1}}h?T=function(n){n.selectionStart=0,n.selectionEnd=n.value.length}:r&&(T=function(n){try{n.select()}catch(n){}});var H=function(){this.id=null,this.f=null,this.time=0,this.handler=N(this.onTimeout,this)};function U(n,t){for(var e=0;e=t)return a+Math.min(r,t-o);if(o+=i-a,a=i+1,(o+=e-o%e)>=t)return a}}var V=[""];function G(n){for(;V.length<=n;)V.push($(V)+" ");return V[n]}function $(n){return n[n.length-1]}function Q(n,t){for(var e=[],a=0;a"€"&&(n.toUpperCase()!=n.toLowerCase()||tn.test(n))}function an(n,t){return t?!!(t.source.indexOf("\\w")>-1&&en(n))||t.test(n):en(n)}function on(n){for(var t in n)if(n.hasOwnProperty(t)&&n[t])return!1;return!0}var rn=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;function ln(n){return n.charCodeAt(0)>=768&&rn.test(n)}function sn(n,t,e){for(;(e<0?t>0:te?-1:1;;){if(t==e)return t;var o=(t+e)/2,i=a<0?Math.ceil(o):Math.floor(o);if(i==t)return n(i)?t:e;n(i)?e=i:t=i+a}}var dn=null;function pn(n,t,e){var a;dn=null;for(var o=0;ot)return o;i.to==t&&(i.from!=i.to&&"before"==e?a=o:dn=o),i.from==t&&(i.from!=i.to&&"before"!=e?a=o:dn=o)}return null!=a?a:dn}var bn=function(){var n=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,t=/[stwN]/,e=/[LRr]/,a=/[Lb1n]/,o=/[1n]/;function i(n,t,e){this.level=n,this.from=t,this.to=e}return function(r,l){var s="ltr"==l?"L":"R";if(0==r.length||"ltr"==l&&!n.test(r))return!1;for(var c,d=r.length,p=[],b=0;b-1&&(a[t]=o.slice(0,i).concat(o.slice(i+1)))}}}function xn(n,t){var e=hn(n,t);if(e.length)for(var a=Array.prototype.slice.call(arguments,2),o=0;o0}function yn(n){n.prototype.on=function(n,t){mn(this,n,t)},n.prototype.off=function(n,t){gn(this,n,t)}}function zn(n){n.preventDefault?n.preventDefault():n.returnValue=!1}function _n(n){n.stopPropagation?n.stopPropagation():n.cancelBubble=!0}function Fn(n){return null!=n.defaultPrevented?n.defaultPrevented:0==n.returnValue}function Cn(n){zn(n),_n(n)}function Sn(n){return n.target||n.srcElement}function En(n){var t=n.which;return null==t&&(1&n.button?t=1:2&n.button?t=3:4&n.button&&(t=2)),w&&n.ctrlKey&&1==t&&(t=3),t}var On,Dn,In=function(){if(r&&l<9)return!1;var n=D("div");return"draggable"in n||"dragDrop"in n}();function Mn(n){if(null==On){var t=D("span","​");O(n,D("span",[t,document.createTextNode("x")])),0!=n.firstChild.offsetHeight&&(On=t.offsetWidth<=1&&t.offsetHeight>2&&!(r&&l<8))}var e=On?D("span","​"):D("span"," ",null,"display: inline-block; width: 1px; margin-right: -1px");return e.setAttribute("cm-text",""),e}function Ln(n){if(null!=Dn)return Dn;var t=O(n,document.createTextNode("AخA")),e=C(t,0,1).getBoundingClientRect(),a=C(t,1,2).getBoundingClientRect();return E(n),!(!e||e.left==e.right)&&(Dn=a.right-e.right<3)}var Bn,An=3!="\n\nb".split(/\n/).length?function(n){for(var t=0,e=[],a=n.length;t<=a;){var o=n.indexOf("\n",t);-1==o&&(o=n.length);var i=n.slice(t,"\r"==n.charAt(o-1)?o-1:o),r=i.indexOf("\r");-1!=r?(e.push(i.slice(0,r)),t+=r+1):(e.push(i),t=o+1)}return e}:function(n){return n.split(/\r\n?|\n/)},Tn=window.getSelection?function(n){try{return n.selectionStart!=n.selectionEnd}catch(n){return!1}}:function(n){var t;try{t=n.ownerDocument.selection.createRange()}catch(n){}return!(!t||t.parentElement()!=n)&&0!=t.compareEndPoints("StartToEnd",t)},jn="oncopy"in(Bn=D("div"))||(Bn.setAttribute("oncopy","return;"),"function"==typeof Bn.oncopy),Pn=null,Nn={},Rn={};function Yn(n,t){arguments.length>2&&(t.dependencies=Array.prototype.slice.call(arguments,2)),Nn[n]=t}function Hn(n){if("string"==typeof n&&Rn.hasOwnProperty(n))n=Rn[n];else if(n&&"string"==typeof n.name&&Rn.hasOwnProperty(n.name)){var t=Rn[n.name];"string"==typeof t&&(t={name:t}),(n=nn(t,n)).name=t.name}else{if("string"==typeof n&&/^[\w\-]+\/[\w\-]+\+xml$/.test(n))return Hn("application/xml");if("string"==typeof n&&/^[\w\-]+\/[\w\-]+\+json$/.test(n))return Hn("application/json")}return"string"==typeof n?{name:n}:n||{name:"null"}}function Un(n,t){t=Hn(t);var e=Nn[t.name];if(!e)return Un(n,"text/plain");var a=e(n,t);if(Wn.hasOwnProperty(t.name)){var o=Wn[t.name];for(var i in o)o.hasOwnProperty(i)&&(a.hasOwnProperty(i)&&(a["_"+i]=a[i]),a[i]=o[i])}if(a.name=t.name,t.helperType&&(a.helperType=t.helperType),t.modeProps)for(var r in t.modeProps)a[r]=t.modeProps[r];return a}var Wn={};function Xn(n,t){R(t,Wn.hasOwnProperty(n)?Wn[n]:Wn[n]={})}function qn(n,t){if(!0===t)return t;if(n.copyState)return n.copyState(t);var e={};for(var a in t){var o=t[a];o instanceof Array&&(o=o.concat([])),e[a]=o}return e}function Zn(n,t){for(var e;n.innerMode&&(e=n.innerMode(t))&&e.mode!=n;)t=e.state,n=e.mode;return e||{mode:n,state:t}}function Kn(n,t,e){return!n.startState||n.startState(t,e)}var Vn=function(n,t,e){this.pos=this.start=0,this.string=n,this.tabSize=t||8,this.lastColumnPos=this.lastColumnValue=0,this.lineStart=0,this.lineOracle=e};function Gn(n,t){if((t-=n.first)<0||t>=n.size)throw new Error("There is no line "+(t+n.first)+" in the document.");for(var e=n;!e.lines;)for(var a=0;;++a){var o=e.children[a],i=o.chunkSize();if(t=n.first&&te?ot(e,Gn(n,e).text.length):function(n,t){var e=n.ch;return null==e||e>t?ot(n.line,t):e<0?ot(n.line,0):n}(t,Gn(n,t.line).text.length)}function bt(n,t){for(var e=[],a=0;a=this.string.length},Vn.prototype.sol=function(){return this.pos==this.lineStart},Vn.prototype.peek=function(){return this.string.charAt(this.pos)||void 0},Vn.prototype.next=function(){if(this.post},Vn.prototype.eatSpace=function(){for(var n=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>n},Vn.prototype.skipToEnd=function(){this.pos=this.string.length},Vn.prototype.skipTo=function(n){var t=this.string.indexOf(n,this.pos);if(t>-1)return this.pos=t,!0},Vn.prototype.backUp=function(n){this.pos-=n},Vn.prototype.column=function(){return this.lastColumnPos0?null:(a&&!1!==t&&(this.pos+=a[0].length),a)}var o=function(n){return e?n.toLowerCase():n};if(o(this.string.substr(this.pos,n.length))==o(n))return!1!==t&&(this.pos+=n.length),!0},Vn.prototype.current=function(){return this.string.slice(this.start,this.pos)},Vn.prototype.hideFirstChars=function(n,t){this.lineStart+=n;try{return t()}finally{this.lineStart-=n}},Vn.prototype.lookAhead=function(n){var t=this.lineOracle;return t&&t.lookAhead(n)},Vn.prototype.baseToken=function(){var n=this.lineOracle;return n&&n.baseToken(this.pos)};var ut=function(n,t){this.state=n,this.lookAhead=t},ft=function(n,t,e,a){this.state=t,this.doc=n,this.line=e,this.maxLookAhead=a||0,this.baseTokens=null,this.baseTokenPos=1};function mt(n,t,e,a){var o=[n.state.modeGen],i={};_t(n,t.text,n.doc.mode,e,(function(n,t){return o.push(n,t)}),i,a);for(var r=e.state,l=function(a){e.baseTokens=o;var l=n.state.overlays[a],s=1,c=0;e.state=!0,_t(n,t.text,l.mode,e,(function(n,t){for(var e=s;cn&&o.splice(s,1,n,o[s+1],a),s+=2,c=Math.min(n,a)}if(t)if(l.opaque)o.splice(e,s-e,n,"overlay "+t),s=e+2;else for(;en.options.maxHighlightLength&&qn(n.doc.mode,a.state),i=mt(n,t,a);o&&(a.state=o),t.stateAfter=a.save(!o),t.styles=i.styles,i.classes?t.styleClasses=i.classes:t.styleClasses&&(t.styleClasses=null),e===n.doc.highlightFrontier&&(n.doc.modeFrontier=Math.max(n.doc.modeFrontier,++n.doc.highlightFrontier))}return t.styles}function gt(n,t,e){var a=n.doc,o=n.display;if(!a.mode.startState)return new ft(a,!0,t);var i=function(n,t,e){for(var a,o,i=n.doc,r=e?-1:t-(n.doc.mode.innerMode?1e3:100),l=t;l>r;--l){if(l<=i.first)return i.first;var s=Gn(i,l-1),c=s.stateAfter;if(c&&(!e||l+(c instanceof ut?c.lookAhead:0)<=i.modeFrontier))return l;var d=Y(s.text,null,n.options.tabSize);(null==o||a>d)&&(o=l-1,a=d)}return o}(n,t,e),r=i>a.first&&Gn(a,i-1).stateAfter,l=r?ft.fromSaved(a,r,i):new ft(a,Kn(a.mode),i);return a.iter(i,t,(function(e){xt(n,e.text,l);var a=l.line;e.stateAfter=a==t-1||a%5==0||a>=o.viewFrom&&at.start)return i}throw new Error("Mode "+n.name+" failed to advance stream.")}ft.prototype.lookAhead=function(n){var t=this.doc.getLine(this.line+n);return null!=t&&n>this.maxLookAhead&&(this.maxLookAhead=n),t},ft.prototype.baseToken=function(n){if(!this.baseTokens)return null;for(;this.baseTokens[this.baseTokenPos]<=n;)this.baseTokenPos+=2;var t=this.baseTokens[this.baseTokenPos+1];return{type:t&&t.replace(/( |^)overlay .*/,""),size:this.baseTokens[this.baseTokenPos]-n}},ft.prototype.nextLine=function(){this.line++,this.maxLookAhead>0&&this.maxLookAhead--},ft.fromSaved=function(n,t,e){return t instanceof ut?new ft(n,qn(n.mode,t.state),e,t.lookAhead):new ft(n,qn(n.mode,t),e)},ft.prototype.save=function(n){var t=!1!==n?qn(this.doc.mode,this.state):this.state;return this.maxLookAhead>0?new ut(t,this.maxLookAhead):t};var vt=function(n,t,e){this.start=n.start,this.end=n.pos,this.string=n.current(),this.type=t||null,this.state=e};function yt(n,t,e,a){var o,i,r=n.doc,l=r.mode,s=Gn(r,(t=pt(r,t)).line),c=gt(n,t.line,e),d=new Vn(s.text,n.options.tabSize,c);for(a&&(i=[]);(a||d.posn.options.maxHighlightLength?(l=!1,r&&xt(n,t,a,p.pos),p.pos=t.length,s=null):s=zt(kt(e,p,a.state,b),i),b){var u=b[0].name;u&&(s="m-"+(s?u+" "+s:u))}if(!l||d!=s){for(;c=t:i.to>t);(a||(a=[])).push(new St(r,i.from,l?null:i.to))}}return a}(e,o,r),s=function(n,t,e){var a;if(n)for(var o=0;o=t:i.to>t)||i.from==t&&"bookmark"==r.type&&(!e||i.marker.insertLeft)){var l=null==i.from||(r.inclusiveLeft?i.from<=t:i.from0&&l)for(var w=0;wt)&&(!e||Tt(e,i.marker)<0)&&(e=i.marker)}return e}function Yt(n,t,e,a,o){var i=Gn(n,t),r=Ct&&i.markedSpans;if(r)for(var l=0;l=0&&p<=0||d<=0&&p>=0)&&(d<=0&&(s.marker.inclusiveRight&&o.inclusiveLeft?it(c.to,e)>=0:it(c.to,e)>0)||d>=0&&(s.marker.inclusiveRight&&o.inclusiveLeft?it(c.from,a)<=0:it(c.from,a)<0)))return!0}}}function Ht(n){for(var t;t=Pt(n);)n=t.find(-1,!0).line;return n}function Ut(n,t){var e=Gn(n,t),a=Ht(e);return e==a?t:nt(a)}function Wt(n,t){if(t>n.lastLine())return t;var e,a=Gn(n,t);if(!Xt(n,a))return t;for(;e=Nt(a);)a=e.find(1,!0).line;return nt(a)+1}function Xt(n,t){var e=Ct&&t.markedSpans;if(e)for(var a=void 0,o=0;ot.maxLineLength&&(t.maxLineLength=e,t.maxLine=n)}))}var Gt=function(n,t,e){this.text=n,Lt(this,t),this.height=e?e(this):1};function $t(n){n.parent=null,Mt(n)}Gt.prototype.lineNo=function(){return nt(this)},yn(Gt);var Qt={},Jt={};function ne(n,t){if(!n||/^\s*$/.test(n))return null;var e=t.addModeClass?Jt:Qt;return e[n]||(e[n]=n.replace(/\S+/g,"cm-$&"))}function te(n,t){var e=I("span",null,null,s?"padding-right: .1px":null),a={pre:I("pre",[e],"CodeMirror-line"),content:e,col:0,pos:0,cm:n,trailingSpace:!1,splitSpaces:n.getOption("lineWrapping")};t.measure={};for(var o=0;o<=(t.rest?t.rest.length:0);o++){var i=o?t.rest[o-1]:t.line,r=void 0;a.pos=0,a.addToken=ae,Ln(n.display.measure)&&(r=un(i,n.doc.direction))&&(a.addToken=oe(a.addToken,r)),a.map=[],re(i,a,ht(n,i,t!=n.display.externalMeasured&&nt(i))),i.styleClasses&&(i.styleClasses.bgClass&&(a.bgClass=A(i.styleClasses.bgClass,a.bgClass||"")),i.styleClasses.textClass&&(a.textClass=A(i.styleClasses.textClass,a.textClass||""))),0==a.map.length&&a.map.push(0,0,a.content.appendChild(Mn(n.display.measure))),0==o?(t.measure.map=a.map,t.measure.cache={}):((t.measure.maps||(t.measure.maps=[])).push(a.map),(t.measure.caches||(t.measure.caches=[])).push({}))}if(s){var l=a.content.lastChild;(/\bcm-tab\b/.test(l.className)||l.querySelector&&l.querySelector(".cm-tab"))&&(a.content.className="cm-tab-wrap-hack")}return xn(n,"renderLine",n,t.line,a.pre),a.pre.className&&(a.textClass=A(a.pre.className,a.textClass||"")),a}function ee(n){var t=D("span","•","cm-invalidchar");return t.title="\\u"+n.charCodeAt(0).toString(16),t.setAttribute("aria-label",t.title),t}function ae(n,t,e,a,o,i,s){if(t){var c,d=n.splitSpaces?function(n,t){if(n.length>1&&!/ /.test(n))return n;for(var e=t,a="",o=0;oc&&p.from<=c);b++);if(p.to>=d)return n(e,a,o,i,r,l,s);n(e,a.slice(0,p.to-c),o,i,null,l,s),i=null,a=a.slice(p.to-c),c=p.to}}}function ie(n,t,e,a){var o=!a&&e.widgetNode;o&&n.map.push(n.pos,n.pos+t,o),!a&&n.cm.display.input.needsContentAttribute&&(o||(o=n.content.appendChild(document.createElement("span"))),o.setAttribute("cm-marker",e.id)),o&&(n.cm.display.input.setUneditable(o),n.content.appendChild(o)),n.pos+=t,n.trailingSpace=!1}function re(n,t,e){var a=n.markedSpans,o=n.text,i=0;if(a)for(var r,l,s,c,d,p,b,u=o.length,f=0,m=1,h="",g=0;;){if(g==f){s=c=d=l="",b=null,p=null,g=1/0;for(var x=[],w=void 0,k=0;kf||y.collapsed&&v.to==f&&v.from==f)){if(null!=v.to&&v.to!=f&&g>v.to&&(g=v.to,c=""),y.className&&(s+=" "+y.className),y.css&&(l=(l?l+";":"")+y.css),y.startStyle&&v.from==f&&(d+=" "+y.startStyle),y.endStyle&&v.to==g&&(w||(w=[])).push(y.endStyle,v.to),y.title&&((b||(b={})).title=y.title),y.attributes)for(var z in y.attributes)(b||(b={}))[z]=y.attributes[z];y.collapsed&&(!p||Tt(p.marker,y)<0)&&(p=v)}else v.from>f&&g>v.from&&(g=v.from)}if(w)for(var _=0;_=u)break;for(var C=Math.min(u,g);;){if(h){var S=f+h.length;if(!p){var E=S>C?h.slice(0,C-f):h;t.addToken(t,E,r?r+s:s,d,f+E.length==g?c:"",l,b)}if(S>=C){h=h.slice(C-f),f=C;break}f=S,d=""}h=o.slice(i,i=e[m++]),r=ne(e[m++],t.cm.options)}}else for(var O=1;Oe)return{map:n.measure.maps[o],cache:n.measure.caches[o],before:!0}}}function Le(n,t,e,a){return Te(n,Ae(n,t),e,a)}function Be(n,t){if(t>=n.display.viewFrom&&t=e.lineN&&t2&&i.push((s.bottom+c.top)/2-e.top)}}i.push(e.bottom-e.top)}}(n,t.view,t.rect),t.hasHeights=!0),(i=function(n,t,e,a){var o,i=Ne(t.map,e,a),s=i.node,c=i.start,d=i.end,p=i.collapse;if(3==s.nodeType){for(var b=0;b<4;b++){for(;c&&ln(t.line.text.charAt(i.coverStart+c));)--c;for(;i.coverStart+d1}(n))return t;var e=screen.logicalXDPI/screen.deviceXDPI,a=screen.logicalYDPI/screen.deviceYDPI;return{left:t.left*e,right:t.right*e,top:t.top*a,bottom:t.bottom*a}}(n.display.measure,o))}else{var u;c>0&&(p=a="right"),o=n.options.lineWrapping&&(u=s.getClientRects()).length>1?u["right"==a?u.length-1:0]:s.getBoundingClientRect()}if(r&&l<9&&!c&&(!o||!o.left&&!o.right)){var f=s.parentNode.getClientRects()[0];o=f?{left:f.left,right:f.left+la(n.display),top:f.top,bottom:f.bottom}:Pe}for(var m=o.top-t.rect.top,h=o.bottom-t.rect.top,g=(m+h)/2,x=t.view.measure.heights,w=0;wt)&&(o=(i=s-l)-1,t>=s&&(r="right")),null!=o){if(a=n[c+2],l==s&&e==(a.insertLeft?"left":"right")&&(r=e),"left"==e&&0==o)for(;c&&n[c-2]==n[c-3]&&n[c-1].insertLeft;)a=n[2+(c-=3)],r="left";if("right"==e&&o==s-l)for(;c=0&&(e=n[o]).left==e.right;o--);return e}function Ye(n){if(n.measure&&(n.measure.cache={},n.measure.heights=null,n.rest))for(var t=0;t=a.text.length?(s=a.text.length,c="before"):s<=0&&(s=0,c="after"),!l)return r("before"==c?s-1:s,"before"==c);function d(n,t,e){return r(e?n-1:n,1==l[t].level!=e)}var p=pn(l,s,c),b=dn,u=d(s,p,"before"==c);return null!=b&&(u.other=d(s,b,"before"!=c)),u}function $e(n,t){var e=0;t=pt(n.doc,t),n.options.lineWrapping||(e=la(n.display)*t.ch);var a=Gn(n.doc,t.line),o=Zt(a)+Ce(n.display);return{left:e,right:e,top:o,bottom:o+a.height}}function Qe(n,t,e,a,o){var i=ot(n,t,e);return i.xRel=o,a&&(i.outside=a),i}function Je(n,t,e){var a=n.doc;if((e+=n.display.viewOffset)<0)return Qe(a.first,0,null,-1,-1);var o=tt(a,e),i=a.first+a.size-1;if(o>i)return Qe(a.first+a.size-1,Gn(a,i).text.length,null,1,1);t<0&&(t=0);for(var r=Gn(a,o);;){var l=aa(n,r,o,t,e),s=Rt(r,l.ch+(l.xRel>0||l.outside>0?1:0));if(!s)return l;var c=s.find(1);if(c.line==o)return c;r=Gn(a,o=c.line)}}function na(n,t,e,a){a-=qe(t);var o=t.text.length,i=cn((function(t){return Te(n,e,t-1).bottom<=a}),o,0);return{begin:i,end:o=cn((function(t){return Te(n,e,t).top>a}),i,o)}}function ta(n,t,e,a){return e||(e=Ae(n,t)),na(n,t,e,Ze(n,t,Te(n,e,a),"line").top)}function ea(n,t,e,a){return!(n.bottom<=e)&&(n.top>e||(a?n.left:n.right)>t)}function aa(n,t,e,a,o){o-=Zt(t);var i=Ae(n,t),r=qe(t),l=0,s=t.text.length,c=!0,d=un(t,n.doc.direction);if(d){var p=(n.options.lineWrapping?ia:oa)(n,t,e,i,d,a,o);l=(c=1!=p.level)?p.from:p.to-1,s=c?p.to:p.from-1}var b,u,f=null,m=null,h=cn((function(t){var e=Te(n,i,t);return e.top+=r,e.bottom+=r,!!ea(e,a,o,!1)&&(e.top<=o&&e.left<=a&&(f=t,m=e),!0)}),l,s),g=!1;if(m){var x=a-m.left=k.bottom?1:0}return Qe(e,h=sn(t.text,h,1),u,g,a-b)}function oa(n,t,e,a,o,i,r){var l=cn((function(l){var s=o[l],c=1!=s.level;return ea(Ge(n,ot(e,c?s.to:s.from,c?"before":"after"),"line",t,a),i,r,!0)}),0,o.length-1),s=o[l];if(l>0){var c=1!=s.level,d=Ge(n,ot(e,c?s.from:s.to,c?"after":"before"),"line",t,a);ea(d,i,r,!0)&&d.top>r&&(s=o[l-1])}return s}function ia(n,t,e,a,o,i,r){var l=na(n,t,a,r),s=l.begin,c=l.end;/\s/.test(t.text.charAt(c-1))&&c--;for(var d=null,p=null,b=0;b=c||u.to<=s)){var f=Te(n,a,1!=u.level?Math.min(c,u.to)-1:Math.max(s,u.from)).right,m=fm)&&(d=u,p=m)}}return d||(d=o[o.length-1]),d.fromc&&(d={from:d.from,to:c,level:d.level}),d}function ra(n){if(null!=n.cachedTextHeight)return n.cachedTextHeight;if(null==je){je=D("pre",null,"CodeMirror-line-like");for(var t=0;t<49;++t)je.appendChild(document.createTextNode("x")),je.appendChild(D("br"));je.appendChild(document.createTextNode("x"))}O(n.measure,je);var e=je.offsetHeight/50;return e>3&&(n.cachedTextHeight=e),E(n.measure),e||1}function la(n){if(null!=n.cachedCharWidth)return n.cachedCharWidth;var t=D("span","xxxxxxxxxx"),e=D("pre",[t],"CodeMirror-line-like");O(n.measure,e);var a=t.getBoundingClientRect(),o=(a.right-a.left)/10;return o>2&&(n.cachedCharWidth=o),o||10}function sa(n){for(var t=n.display,e={},a={},o=t.gutters.clientLeft,i=t.gutters.firstChild,r=0;i;i=i.nextSibling,++r){var l=n.display.gutterSpecs[r].className;e[l]=i.offsetLeft+i.clientLeft+o,a[l]=i.clientWidth}return{fixedPos:ca(t),gutterTotalWidth:t.gutters.offsetWidth,gutterLeft:e,gutterWidth:a,wrapperWidth:t.wrapper.clientWidth}}function ca(n){return n.scroller.getBoundingClientRect().left-n.sizer.getBoundingClientRect().left}function da(n){var t=ra(n.display),e=n.options.lineWrapping,a=e&&Math.max(5,n.display.scroller.clientWidth/la(n.display)-3);return function(o){if(Xt(n.doc,o))return 0;var i=0;if(o.widgets)for(var r=0;r0&&(s=Gn(n.doc,c.line).text).length==c.ch){var d=Y(s,s.length,n.options.tabSize)-s.length;c=ot(c.line,Math.max(0,Math.round((i-Ee(n.display).left)/la(n.display))-d))}return c}function ua(n,t){if(t>=n.display.viewTo)return null;if((t-=n.display.viewFrom)<0)return null;for(var e=n.display.view,a=0;at)&&(o.updateLineNumbers=t),n.curOp.viewChanged=!0,t>=o.viewTo)Ct&&Ut(n.doc,t)o.viewFrom?ha(n):(o.viewFrom+=a,o.viewTo+=a);else if(t<=o.viewFrom&&e>=o.viewTo)ha(n);else if(t<=o.viewFrom){var i=ga(n,e,e+a,1);i?(o.view=o.view.slice(i.index),o.viewFrom=i.lineN,o.viewTo+=a):ha(n)}else if(e>=o.viewTo){var r=ga(n,t,t,-1);r?(o.view=o.view.slice(0,r.index),o.viewTo=r.lineN):ha(n)}else{var l=ga(n,t,t,-1),s=ga(n,e,e+a,1);l&&s?(o.view=o.view.slice(0,l.index).concat(se(n,l.lineN,s.lineN)).concat(o.view.slice(s.index)),o.viewTo+=a):ha(n)}var c=o.externalMeasured;c&&(e=o.lineN&&t=a.viewTo)){var i=a.view[ua(n,t)];if(null!=i.node){var r=i.changes||(i.changes=[]);-1==U(r,e)&&r.push(e)}}}function ha(n){n.display.viewFrom=n.display.viewTo=n.doc.first,n.display.view=[],n.display.viewOffset=0}function ga(n,t,e,a){var o,i=ua(n,t),r=n.display.view;if(!Ct||e==n.doc.first+n.doc.size)return{index:i,lineN:e};for(var l=n.display.viewFrom,s=0;s0){if(i==r.length-1)return null;o=l+r[i].size-t,i++}else o=l-t;t+=o,e+=o}for(;Ut(n.doc,e)!=e;){if(i==(a<0?0:r.length-1))return null;e+=a*r[i-(a<0?1:0)].size,i+=a}return{index:i,lineN:e}}function xa(n){for(var t=n.display.view,e=0,a=0;a=n.display.viewTo||s.to().line0?r:n.defaultCharWidth())+"px"}if(a.other){var l=e.appendChild(D("div"," ","CodeMirror-cursor CodeMirror-secondarycursor"));l.style.display="",l.style.left=a.other.left+"px",l.style.top=a.other.top+"px",l.style.height=.85*(a.other.bottom-a.other.top)+"px"}}function ya(n,t){return n.top-t.top||n.left-t.left}function za(n,t,e){var a=n.display,o=n.doc,i=document.createDocumentFragment(),r=Ee(n.display),l=r.left,s=Math.max(a.sizerWidth,De(n)-a.sizer.offsetLeft)-r.right,c="ltr"==o.direction;function d(n,t,e,a){t<0&&(t=0),t=Math.round(t),a=Math.round(a),i.appendChild(D("div",null,"CodeMirror-selected","position: absolute; left: "+n+"px;\n top: "+t+"px; width: "+(null==e?s-n:e)+"px;\n height: "+(a-t)+"px"))}function p(t,e,a){var i,r,p=Gn(o,t),b=p.text.length;function u(e,a){return Ve(n,ot(t,e),"div",p,a)}function f(t,e,a){var o=ta(n,p,null,t),i="ltr"==e==("after"==a)?"left":"right";return u("after"==a?o.begin:o.end-(/\s/.test(p.text.charAt(o.end-1))?2:1),i)[i]}var m=un(p,o.direction);return function(n,t,e,a){if(!n)return a(t,e,"ltr",0);for(var o=!1,i=0;it||t==e&&r.to==t)&&(a(Math.max(r.from,t),Math.min(r.to,e),1==r.level?"rtl":"ltr",i),o=!0)}o||a(t,e,"ltr")}(m,e||0,null==a?b:a,(function(n,t,o,p){var h="ltr"==o,g=u(n,h?"left":"right"),x=u(t-1,h?"right":"left"),w=null==e&&0==n,k=null==a&&t==b,v=0==p,y=!m||p==m.length-1;if(x.top-g.top<=3){var z=(c?k:w)&&y,_=(c?w:k)&&v?l:(h?g:x).left,F=z?s:(h?x:g).right;d(_,g.top,F-_,g.bottom)}else{var C,S,E,O;h?(C=c&&w&&v?l:g.left,S=c?s:f(n,o,"before"),E=c?l:f(t,o,"after"),O=c&&k&&y?s:x.right):(C=c?f(n,o,"before"):l,S=!c&&w&&v?s:g.right,E=!c&&k&&y?l:x.left,O=c?f(t,o,"after"):s),d(C,g.top,S-C,g.bottom),g.bottom0?t.blinker=setInterval((function(){n.hasFocus()||Ea(n),t.cursorDiv.style.visibility=(e=!e)?"":"hidden"}),n.options.cursorBlinkRate):n.options.cursorBlinkRate<0&&(t.cursorDiv.style.visibility="hidden")}}function Fa(n){n.hasFocus()||(n.display.input.focus(),n.state.focused||Sa(n))}function Ca(n){n.state.delayingBlurEvent=!0,setTimeout((function(){n.state.delayingBlurEvent&&(n.state.delayingBlurEvent=!1,n.state.focused&&Ea(n))}),100)}function Sa(n,t){n.state.delayingBlurEvent&&!n.state.draggingText&&(n.state.delayingBlurEvent=!1),"nocursor"!=n.options.readOnly&&(n.state.focused||(xn(n,"focus",n,t),n.state.focused=!0,B(n.display.wrapper,"CodeMirror-focused"),n.curOp||n.display.selForContextMenu==n.doc.sel||(n.display.input.reset(),s&&setTimeout((function(){return n.display.input.reset(!0)}),20)),n.display.input.receivedFocus()),_a(n))}function Ea(n,t){n.state.delayingBlurEvent||(n.state.focused&&(xn(n,"blur",n,t),n.state.focused=!1,S(n.display.wrapper,"CodeMirror-focused")),clearInterval(n.display.blinker),setTimeout((function(){n.state.focused||(n.display.shift=!1)}),150))}function Oa(n){for(var t=n.display,e=t.lineDiv.offsetTop,a=Math.max(0,t.scroller.getBoundingClientRect().top),o=t.lineDiv.getBoundingClientRect().top,i=0,s=0;s.005||m<-.005)&&(on.display.sizerWidth){var g=Math.ceil(b/la(n.display));g>n.display.maxLineLength&&(n.display.maxLineLength=g,n.display.maxLine=c.line,n.display.maxLineChanged=!0)}}}Math.abs(i)>2&&(t.scroller.scrollTop+=i)}function Da(n){if(n.widgets)for(var t=0;t=r&&(i=tt(t,Zt(Gn(t,s))-n.wrapper.clientHeight),r=s)}return{from:i,to:Math.max(r,i+1)}}function Ma(n,t){var e=n.display,a=ra(n.display);t.top<0&&(t.top=0);var o=n.curOp&&null!=n.curOp.scrollTop?n.curOp.scrollTop:e.scroller.scrollTop,i=Ie(n),r={};t.bottom-t.top>i&&(t.bottom=t.top+i);var l=n.doc.height+Se(e),s=t.topl-a;if(t.topo+i){var d=Math.min(t.top,(c?l:t.bottom)-i);d!=o&&(r.scrollTop=d)}var p=n.options.fixedGutter?0:e.gutters.offsetWidth,b=n.curOp&&null!=n.curOp.scrollLeft?n.curOp.scrollLeft:e.scroller.scrollLeft-p,u=De(n)-e.gutters.offsetWidth,f=t.right-t.left>u;return f&&(t.right=t.left+u),t.left<10?r.scrollLeft=0:t.leftu+b-3&&(r.scrollLeft=t.right+(f?0:10)-u),r}function La(n,t){null!=t&&(Ta(n),n.curOp.scrollTop=(null==n.curOp.scrollTop?n.doc.scrollTop:n.curOp.scrollTop)+t)}function Ba(n){Ta(n);var t=n.getCursor();n.curOp.scrollToPos={from:t,to:t,margin:n.options.cursorScrollMargin}}function Aa(n,t,e){null==t&&null==e||Ta(n),null!=t&&(n.curOp.scrollLeft=t),null!=e&&(n.curOp.scrollTop=e)}function Ta(n){var t=n.curOp.scrollToPos;t&&(n.curOp.scrollToPos=null,ja(n,$e(n,t.from),$e(n,t.to),t.margin))}function ja(n,t,e,a){var o=Ma(n,{left:Math.min(t.left,e.left),top:Math.min(t.top,e.top)-a,right:Math.max(t.right,e.right),bottom:Math.max(t.bottom,e.bottom)+a});Aa(n,o.scrollLeft,o.scrollTop)}function Pa(n,t){Math.abs(n.doc.scrollTop-t)<2||(e||uo(n,{top:t}),Na(n,t,!0),e&&uo(n),ro(n,100))}function Na(n,t,e){t=Math.max(0,Math.min(n.display.scroller.scrollHeight-n.display.scroller.clientHeight,t)),(n.display.scroller.scrollTop!=t||e)&&(n.doc.scrollTop=t,n.display.scrollbars.setScrollTop(t),n.display.scroller.scrollTop!=t&&(n.display.scroller.scrollTop=t))}function Ra(n,t,e,a){t=Math.max(0,Math.min(t,n.display.scroller.scrollWidth-n.display.scroller.clientWidth)),(e?t==n.doc.scrollLeft:Math.abs(n.doc.scrollLeft-t)<2)&&!a||(n.doc.scrollLeft=t,ho(n),n.display.scroller.scrollLeft!=t&&(n.display.scroller.scrollLeft=t),n.display.scrollbars.setScrollLeft(t))}function Ya(n){var t=n.display,e=t.gutters.offsetWidth,a=Math.round(n.doc.height+Se(n.display));return{clientHeight:t.scroller.clientHeight,viewHeight:t.wrapper.clientHeight,scrollWidth:t.scroller.scrollWidth,clientWidth:t.scroller.clientWidth,viewWidth:t.wrapper.clientWidth,barLeft:n.options.fixedGutter?e:0,docHeight:a,scrollHeight:a+Oe(n)+t.barHeight,nativeBarWidth:t.nativeBarWidth,gutterWidth:e}}var Ha=function(n,t,e){this.cm=e;var a=this.vert=D("div",[D("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),o=this.horiz=D("div",[D("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");a.tabIndex=o.tabIndex=-1,n(a),n(o),mn(a,"scroll",(function(){a.clientHeight&&t(a.scrollTop,"vertical")})),mn(o,"scroll",(function(){o.clientWidth&&t(o.scrollLeft,"horizontal")})),this.checkedZeroWidth=!1,r&&l<8&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")};Ha.prototype.update=function(n){var t=n.scrollWidth>n.clientWidth+1,e=n.scrollHeight>n.clientHeight+1,a=n.nativeBarWidth;if(e){this.vert.style.display="block",this.vert.style.bottom=t?a+"px":"0";var o=n.viewHeight-(t?a:0);this.vert.firstChild.style.height=Math.max(0,n.scrollHeight-n.clientHeight+o)+"px"}else this.vert.scrollTop=0,this.vert.style.display="",this.vert.firstChild.style.height="0";if(t){this.horiz.style.display="block",this.horiz.style.right=e?a+"px":"0",this.horiz.style.left=n.barLeft+"px";var i=n.viewWidth-n.barLeft-(e?a:0);this.horiz.firstChild.style.width=Math.max(0,n.scrollWidth-n.clientWidth+i)+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&n.clientHeight>0&&(0==a&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:e?a:0,bottom:t?a:0}},Ha.prototype.setScrollLeft=function(n){this.horiz.scrollLeft!=n&&(this.horiz.scrollLeft=n),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz,"horiz")},Ha.prototype.setScrollTop=function(n){this.vert.scrollTop!=n&&(this.vert.scrollTop=n),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert,"vert")},Ha.prototype.zeroWidthHack=function(){var n=w&&!f?"12px":"18px";this.horiz.style.height=this.vert.style.width=n,this.horiz.style.visibility=this.vert.style.visibility="hidden",this.disableHoriz=new H,this.disableVert=new H},Ha.prototype.enableZeroWidthBar=function(n,t,e){n.style.visibility="",t.set(1e3,(function a(){var o=n.getBoundingClientRect();("vert"==e?document.elementFromPoint(o.right-1,(o.top+o.bottom)/2):document.elementFromPoint((o.right+o.left)/2,o.bottom-1))!=n?n.style.visibility="hidden":t.set(1e3,a)}))},Ha.prototype.clear=function(){var n=this.horiz.parentNode;n.removeChild(this.horiz),n.removeChild(this.vert)};var Ua=function(){};function Wa(n,t){t||(t=Ya(n));var e=n.display.barWidth,a=n.display.barHeight;Xa(n,t);for(var o=0;o<4&&e!=n.display.barWidth||a!=n.display.barHeight;o++)e!=n.display.barWidth&&n.options.lineWrapping&&Oa(n),Xa(n,Ya(n)),e=n.display.barWidth,a=n.display.barHeight}function Xa(n,t){var e=n.display,a=e.scrollbars.update(t);e.sizer.style.paddingRight=(e.barWidth=a.right)+"px",e.sizer.style.paddingBottom=(e.barHeight=a.bottom)+"px",e.heightForcer.style.borderBottom=a.bottom+"px solid transparent",a.right&&a.bottom?(e.scrollbarFiller.style.display="block",e.scrollbarFiller.style.height=a.bottom+"px",e.scrollbarFiller.style.width=a.right+"px"):e.scrollbarFiller.style.display="",a.bottom&&n.options.coverGutterNextToScrollbar&&n.options.fixedGutter?(e.gutterFiller.style.display="block",e.gutterFiller.style.height=a.bottom+"px",e.gutterFiller.style.width=t.gutterWidth+"px"):e.gutterFiller.style.display=""}Ua.prototype.update=function(){return{bottom:0,right:0}},Ua.prototype.setScrollLeft=function(){},Ua.prototype.setScrollTop=function(){},Ua.prototype.clear=function(){};var qa={native:Ha,null:Ua};function Za(n){n.display.scrollbars&&(n.display.scrollbars.clear(),n.display.scrollbars.addClass&&S(n.display.wrapper,n.display.scrollbars.addClass)),n.display.scrollbars=new qa[n.options.scrollbarStyle]((function(t){n.display.wrapper.insertBefore(t,n.display.scrollbarFiller),mn(t,"mousedown",(function(){n.state.focused&&setTimeout((function(){return n.display.input.focus()}),0)})),t.setAttribute("cm-not-content","true")}),(function(t,e){"horizontal"==e?Ra(n,t):Pa(n,t)}),n),n.display.scrollbars.addClass&&B(n.display.wrapper,n.display.scrollbars.addClass)}var Ka=0;function Va(n){var t;n.curOp={cm:n,viewChanged:!1,startHeight:n.doc.height,forceUpdate:!1,updateInput:0,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++Ka,markArrays:null},t=n.curOp,ce?ce.ops.push(t):t.ownsGroup=ce={ops:[t],delayedCallbacks:[]}}function Ga(n){var t=n.curOp;t&&function(n,t){var e=n.ownsGroup;if(e)try{!function(n){var t=n.delayedCallbacks,e=0;do{for(;e=e.viewTo)||e.maxLineChanged&&t.options.lineWrapping,n.update=n.mustUpdate&&new so(t,n.mustUpdate&&{top:n.scrollTop,ensure:n.scrollToPos},n.forceUpdate)}function Qa(n){n.updatedDisplay=n.mustUpdate&&po(n.cm,n.update)}function Ja(n){var t=n.cm,e=t.display;n.updatedDisplay&&Oa(t),n.barMeasure=Ya(t),e.maxLineChanged&&!t.options.lineWrapping&&(n.adjustWidthTo=Le(t,e.maxLine,e.maxLine.text.length).left+3,t.display.sizerWidth=n.adjustWidthTo,n.barMeasure.scrollWidth=Math.max(e.scroller.clientWidth,e.sizer.offsetLeft+n.adjustWidthTo+Oe(t)+t.display.barWidth),n.maxScrollLeft=Math.max(0,e.sizer.offsetLeft+n.adjustWidthTo-De(t))),(n.updatedDisplay||n.selectionChanged)&&(n.preparedSelection=e.input.prepareSelection())}function no(n){var t=n.cm;null!=n.adjustWidthTo&&(t.display.sizer.style.minWidth=n.adjustWidthTo+"px",n.maxScrollLeft(i.defaultView.innerHeight||i.documentElement.clientHeight)&&(o=!1),null!=o&&!m){var r=D("div","​",null,"position: absolute;\n top: "+(t.top-e.viewOffset-Ce(n.display))+"px;\n height: "+(t.bottom-t.top+Oe(n)+e.barHeight)+"px;\n left: "+t.left+"px; width: "+Math.max(2,t.right-t.left)+"px;");n.display.lineSpace.appendChild(r),r.scrollIntoView(o),n.display.lineSpace.removeChild(r)}}}(t,function(n,t,e,a){var o;null==a&&(a=0),n.options.lineWrapping||t!=e||(e="before"==t.sticky?ot(t.line,t.ch+1,"before"):t,t=t.ch?ot(t.line,"before"==t.sticky?t.ch-1:t.ch,"after"):t);for(var i=0;i<5;i++){var r=!1,l=Ge(n,t),s=e&&e!=t?Ge(n,e):l,c=Ma(n,o={left:Math.min(l.left,s.left),top:Math.min(l.top,s.top)-a,right:Math.max(l.left,s.left),bottom:Math.max(l.bottom,s.bottom)+a}),d=n.doc.scrollTop,p=n.doc.scrollLeft;if(null!=c.scrollTop&&(Pa(n,c.scrollTop),Math.abs(n.doc.scrollTop-d)>1&&(r=!0)),null!=c.scrollLeft&&(Ra(n,c.scrollLeft),Math.abs(n.doc.scrollLeft-p)>1&&(r=!0)),!r)break}return o}(t,pt(a,n.scrollToPos.from),pt(a,n.scrollToPos.to),n.scrollToPos.margin));var o=n.maybeHiddenMarkers,i=n.maybeUnhiddenMarkers;if(o)for(var r=0;r=n.display.viewTo)){var e=+new Date+n.options.workTime,a=gt(n,t.highlightFrontier),o=[];t.iter(a.line,Math.min(t.first+t.size,n.display.viewTo+500),(function(i){if(a.line>=n.display.viewFrom){var r=i.styles,l=i.text.length>n.options.maxHighlightLength?qn(t.mode,a.state):null,s=mt(n,i,a,!0);l&&(a.state=l),i.styles=s.styles;var c=i.styleClasses,d=s.classes;d?i.styleClasses=d:c&&(i.styleClasses=null);for(var p=!r||r.length!=i.styles.length||c!=d&&(!c||!d||c.bgClass!=d.bgClass||c.textClass!=d.textClass),b=0;!p&&be)return ro(n,n.options.workDelay),!0})),t.highlightFrontier=a.line,t.modeFrontier=Math.max(t.modeFrontier,a.line),o.length&&eo(n,(function(){for(var t=0;t=e.viewFrom&&t.visible.to<=e.viewTo&&(null==e.updateLineNumbers||e.updateLineNumbers>=e.viewTo)&&e.renderedView==e.view&&0==xa(n))return!1;go(n)&&(ha(n),t.dims=sa(n));var o=a.first+a.size,i=Math.max(t.visible.from-n.options.viewportMargin,a.first),r=Math.min(o,t.visible.to+n.options.viewportMargin);e.viewFromr&&e.viewTo-r<20&&(r=Math.min(o,e.viewTo)),Ct&&(i=Ut(n.doc,i),r=Wt(n.doc,r));var l=i!=e.viewFrom||r!=e.viewTo||e.lastWrapHeight!=t.wrapperHeight||e.lastWrapWidth!=t.wrapperWidth;!function(n,t,e){var a=n.display;0==a.view.length||t>=a.viewTo||e<=a.viewFrom?(a.view=se(n,t,e),a.viewFrom=t):(a.viewFrom>t?a.view=se(n,t,a.viewFrom).concat(a.view):a.viewFrome&&(a.view=a.view.slice(0,ua(n,e)))),a.viewTo=e}(n,i,r),e.viewOffset=Zt(Gn(n.doc,e.viewFrom)),n.display.mover.style.top=e.viewOffset+"px";var c=xa(n);if(!l&&0==c&&!t.force&&e.renderedView==e.view&&(null==e.updateLineNumbers||e.updateLineNumbers>=e.viewTo))return!1;var d=co(n);return c>4&&(e.lineDiv.style.display="none"),function(n,t,e){var a=n.display,o=n.options.lineNumbers,i=a.lineDiv,r=i.firstChild;function l(t){var e=t.nextSibling;return s&&w&&n.display.currentWheelTarget==t?t.style.display="none":t.parentNode.removeChild(t),e}for(var c=a.view,d=a.viewFrom,p=0;p-1&&(u=!1),ue(n,b,d,e)),u&&(E(b.lineNumber),b.lineNumber.appendChild(document.createTextNode(at(n.options,d)))),r=b.node.nextSibling}else{var f=ke(n,b,d,e);i.insertBefore(f,r)}d+=b.size}for(;r;)r=l(r)}(n,e.updateLineNumbers,t.dims),c>4&&(e.lineDiv.style.display=""),e.renderedView=e.view,function(n){if(n&&n.activeElt&&n.activeElt!=L(n.activeElt.ownerDocument)&&(n.activeElt.focus(),!/^(INPUT|TEXTAREA)$/.test(n.activeElt.nodeName)&&n.anchorNode&&M(document.body,n.anchorNode)&&M(document.body,n.focusNode))){var t=n.activeElt.ownerDocument,e=t.defaultView.getSelection(),a=t.createRange();a.setEnd(n.anchorNode,n.anchorOffset),a.collapse(!1),e.removeAllRanges(),e.addRange(a),e.extend(n.focusNode,n.focusOffset)}}(d),E(e.cursorDiv),E(e.selectionDiv),e.gutters.style.height=e.sizer.style.minHeight=0,l&&(e.lastWrapHeight=t.wrapperHeight,e.lastWrapWidth=t.wrapperWidth,ro(n,400)),e.updateLineNumbers=null,!0}function bo(n,t){for(var e=t.viewport,a=!0;;a=!1){if(a&&n.options.lineWrapping&&t.oldDisplayWidth!=De(n))a&&(t.visible=Ia(n.display,n.doc,e));else if(e&&null!=e.top&&(e={top:Math.min(n.doc.height+Se(n.display)-Ie(n),e.top)}),t.visible=Ia(n.display,n.doc,e),t.visible.from>=n.display.viewFrom&&t.visible.to<=n.display.viewTo)break;if(!po(n,t))break;Oa(n);var o=Ya(n);wa(n),Wa(n,o),mo(n,o),t.force=!1}t.signal(n,"update",n),n.display.viewFrom==n.display.reportedViewFrom&&n.display.viewTo==n.display.reportedViewTo||(t.signal(n,"viewportChange",n,n.display.viewFrom,n.display.viewTo),n.display.reportedViewFrom=n.display.viewFrom,n.display.reportedViewTo=n.display.viewTo)}function uo(n,t){var e=new so(n,t);if(po(n,e)){Oa(n),bo(n,e);var a=Ya(n);wa(n),Wa(n,a),mo(n,a),e.finish()}}function fo(n){var t=n.gutters.offsetWidth;n.sizer.style.marginLeft=t+"px",pe(n,"gutterChanged",n)}function mo(n,t){n.display.sizer.style.minHeight=t.docHeight+"px",n.display.heightForcer.style.top=t.docHeight+"px",n.display.gutters.style.height=t.docHeight+n.display.barHeight+Oe(n)+"px"}function ho(n){var t=n.display,e=t.view;if(t.alignWidgets||t.gutters.firstChild&&n.options.fixedGutter){for(var a=ca(t)-t.scroller.scrollLeft+n.doc.scrollLeft,o=t.gutters.offsetWidth,i=a+"px",r=0;r=105&&(i.wrapper.style.clipPath="inset(0px)"),i.wrapper.setAttribute("translate","no"),r&&l<8&&(i.gutters.style.zIndex=-1,i.scroller.style.paddingRight=0),s||e&&x||(i.scroller.draggable=!0),n&&(n.appendChild?n.appendChild(i.wrapper):n(i.wrapper)),i.viewFrom=i.viewTo=t.first,i.reportedViewFrom=i.reportedViewTo=t.first,i.view=[],i.renderedView=null,i.externalMeasured=null,i.viewOffset=0,i.lastWrapHeight=i.lastWrapWidth=0,i.updateLineNumbers=null,i.nativeBarWidth=i.barHeight=i.barWidth=0,i.scrollbarsClipped=!1,i.lineNumWidth=i.lineNumInnerWidth=i.lineNumChars=null,i.alignWidgets=!1,i.cachedCharWidth=i.cachedTextHeight=i.cachedPaddingH=null,i.maxLine=null,i.maxLineLength=0,i.maxLineChanged=!1,i.wheelDX=i.wheelDY=i.wheelStartX=i.wheelStartY=null,i.shift=!1,i.selForContextMenu=null,i.activeTouch=null,i.gutterSpecs=xo(o.gutters,o.lineNumbers),wo(i),a.init(i)}so.prototype.signal=function(n,t){vn(n,t)&&this.events.push(arguments)},so.prototype.finish=function(){for(var n=0;nc.clientWidth,f=c.scrollHeight>c.clientHeight;if(o&&u||i&&f){if(i&&w&&s)n:for(var m=t.target,h=l.view;m!=c;m=m.parentNode)for(var g=0;g=0&&it(n,a.to())<=0)return e}return-1};var Eo=function(n,t){this.anchor=n,this.head=t};function Oo(n,t,e){var a=n&&n.options.selectionsMayTouch,o=t[e];t.sort((function(n,t){return it(n.from(),t.from())})),e=U(t,o);for(var i=1;i0:s>=0){var c=ct(l.from(),r.from()),d=st(l.to(),r.to()),p=l.empty()?r.from()==r.head:l.from()==l.head;i<=e&&--e,t.splice(--i,2,new Eo(p?d:c,p?c:d))}}return new So(t,e)}function Do(n,t){return new So([new Eo(n,t||n)],0)}function Io(n){return n.text?ot(n.from.line+n.text.length-1,$(n.text).length+(1==n.text.length?n.from.ch:0)):n.to}function Mo(n,t){if(it(n,t.from)<0)return n;if(it(n,t.to)<=0)return Io(t);var e=n.line+t.text.length-(t.to.line-t.from.line)-1,a=n.ch;return n.line==t.to.line&&(a+=Io(t).ch-t.to.ch),ot(e,a)}function Lo(n,t){for(var e=[],a=0;a1&&n.remove(l.line+1,f-1),n.insert(l.line+1,g)}pe(n,"change",n,t)}function No(n,t,e){!function n(a,o,i){if(a.linked)for(var r=0;rl-(n.cm?n.cm.options.historyEventDelay:500)||"*"==t.origin.charAt(0)))&&(i=function(n,t){return t?(Wo(n.done),$(n.done)):n.done.length&&!$(n.done).ranges?$(n.done):n.done.length>1&&!n.done[n.done.length-2].ranges?(n.done.pop(),$(n.done)):void 0}(o,o.lastOp==a)))r=$(i.changes),0==it(t.from,t.to)&&0==it(t.from,r.to)?r.to=Io(t):i.changes.push(Uo(n,t));else{var s=$(o.done);for(s&&s.ranges||Zo(n.sel,o.done),i={changes:[Uo(n,t)],generation:o.generation},o.done.push(i);o.done.length>o.undoDepth;)o.done.shift(),o.done[0].ranges||o.done.shift()}o.done.push(e),o.generation=++o.maxGeneration,o.lastModTime=o.lastSelTime=l,o.lastOp=o.lastSelOp=a,o.lastOrigin=o.lastSelOrigin=t.origin,r||xn(n,"historyAdded")}function qo(n,t,e,a){var o=n.history,i=a&&a.origin;e==o.lastSelOp||i&&o.lastSelOrigin==i&&(o.lastModTime==o.lastSelTime&&o.lastOrigin==i||function(n,t,e,a){var o=t.charAt(0);return"*"==o||"+"==o&&e.ranges.length==a.ranges.length&&e.somethingSelected()==a.somethingSelected()&&new Date-n.history.lastSelTime<=(n.cm?n.cm.options.historyEventDelay:500)}(n,i,$(o.done),t))?o.done[o.done.length-1]=t:Zo(t,o.done),o.lastSelTime=+new Date,o.lastSelOrigin=i,o.lastSelOp=e,a&&!1!==a.clearRedo&&Wo(o.undone)}function Zo(n,t){var e=$(t);e&&e.ranges&&e.equals(n)||t.push(n)}function Ko(n,t,e,a){var o=t["spans_"+n.id],i=0;n.iter(Math.max(n.first,e),Math.min(n.first+n.size,a),(function(e){e.markedSpans&&((o||(o=t["spans_"+n.id]={}))[i]=e.markedSpans),++i}))}function Vo(n){if(!n)return null;for(var t,e=0;e-1&&($(l)[p]=c[p],delete c[p])}}}return a}function Qo(n,t,e,a){if(a){var o=n.anchor;if(e){var i=it(t,o)<0;i!=it(e,o)<0?(o=t,t=e):i!=it(t,e)<0&&(t=e)}return new Eo(o,t)}return new Eo(e||t,t)}function Jo(n,t,e,a,o){null==o&&(o=n.cm&&(n.cm.display.shift||n.extend)),oi(n,new So([Qo(n.sel.primary(),t,e,o)],0),a)}function ni(n,t,e){for(var a=[],o=n.cm&&(n.cm.display.shift||n.extend),i=0;i=t.ch:l.to>t.ch))){if(o&&(xn(s,"beforeCursorEnter"),s.explicitlyCleared)){if(i.markedSpans){--r;continue}break}if(!s.atomic)continue;if(e){var p=s.find(a<0?1:-1),b=void 0;if((a<0?d:c)&&(p=pi(n,p,-a,p&&p.line==t.line?i:null)),p&&p.line==t.line&&(b=it(p,e))&&(a<0?b<0:b>0))return ci(n,p,t,a,o)}var u=s.find(a<0?-1:1);return(a<0?c:d)&&(u=pi(n,u,a,u.line==t.line?i:null)),u?ci(n,u,t,a,o):null}}return t}function di(n,t,e,a,o){var i=a||1,r=ci(n,t,e,i,o)||!o&&ci(n,t,e,i,!0)||ci(n,t,e,-i,o)||!o&&ci(n,t,e,-i,!0);return r||(n.cantEdit=!0,ot(n.first,0))}function pi(n,t,e,a){return e<0&&0==t.ch?t.line>n.first?pt(n,ot(t.line-1)):null:e>0&&t.ch==(a||Gn(n,t.line)).text.length?t.line0)){var d=[s,1],p=it(c.from,l.from),b=it(c.to,l.to);(p<0||!r.inclusiveLeft&&!p)&&d.push({from:c.from,to:l.from}),(b>0||!r.inclusiveRight&&!b)&&d.push({from:l.to,to:c.to}),o.splice.apply(o,d),s+=d.length-3}}return o}(n,t.from,t.to);if(a)for(var o=a.length-1;o>=0;--o)mi(n,{from:a[o].from,to:a[o].to,text:o?[""]:t.text,origin:t.origin});else mi(n,t)}}function mi(n,t){if(1!=t.text.length||""!=t.text[0]||0!=it(t.from,t.to)){var e=Lo(n,t);Xo(n,t,e,n.cm?n.cm.curOp.id:NaN),xi(n,t,e,Dt(n,t));var a=[];No(n,(function(n,e){e||-1!=U(a,n.history)||(yi(n.history,t),a.push(n.history)),xi(n,t,null,Dt(n,t))}))}}function hi(n,t,e){var a=n.cm&&n.cm.state.suppressEdits;if(!a||e){for(var o,i=n.history,r=n.sel,l="undo"==t?i.done:i.undone,s="undo"==t?i.undone:i.done,c=0;c=0;--u){var f=b(u);if(f)return f.v}}}}function gi(n,t){if(0!=t&&(n.first+=t,n.sel=new So(Q(n.sel.ranges,(function(n){return new Eo(ot(n.anchor.line+t,n.anchor.ch),ot(n.head.line+t,n.head.ch))})),n.sel.primIndex),n.cm)){fa(n.cm,n.first,n.first-t,t);for(var e=n.cm.display,a=e.viewFrom;an.lastLine())){if(t.from.linei&&(t={from:t.from,to:ot(i,Gn(n,i).text.length),text:[t.text[0]],origin:t.origin}),t.removed=$n(n,t.from,t.to),e||(e=Lo(n,t)),n.cm?function(n,t,e){var a=n.doc,o=n.display,i=t.from,r=t.to,l=!1,s=i.line;n.options.lineWrapping||(s=nt(Ht(Gn(a,i.line))),a.iter(s,r.line+1,(function(n){if(n==o.maxLine)return l=!0,!0}))),a.sel.contains(t.from,t.to)>-1&&kn(n),Po(a,t,e,da(n)),n.options.lineWrapping||(a.iter(s,i.line+t.text.length,(function(n){var t=Kt(n);t>o.maxLineLength&&(o.maxLine=n,o.maxLineLength=t,o.maxLineChanged=!0,l=!1)})),l&&(n.curOp.updateMaxLine=!0)),function(n,t){if(n.modeFrontier=Math.min(n.modeFrontier,t),!(n.highlightFrontiere;a--){var o=Gn(n,a).stateAfter;if(o&&(!(o instanceof ut)||a+o.lookAhead1||!(this.children[0]instanceof _i))){var l=[];this.collapse(l),this.children=[new _i(l)],this.children[0].parent=this}},collapse:function(n){for(var t=0;t50){for(var r=o.lines.length%25+25,l=r;l10);n.parent.maybeSpill()}},iterN:function(n,t,e){for(var a=0;a0||0==r&&!1!==i.clearWhenEmpty)return i;if(i.replacedWith&&(i.collapsed=!0,i.widgetNode=I("span",[i.replacedWith],"CodeMirror-widget"),a.handleMouseEvents||i.widgetNode.setAttribute("cm-ignore-events","true"),a.insertLeft&&(i.widgetNode.insertLeft=!0)),i.collapsed){if(Yt(n,t.line,t,e,i)||t.line!=e.line&&Yt(n,e.line,t,e,i))throw new Error("Inserting collapsed marker partially overlapping an existing one");Ct=!0}i.addToHistory&&Xo(n,{from:t,to:e,origin:"markText"},n.sel,NaN);var l,s=t.line,c=n.cm;if(n.iter(s,e.line+1,(function(a){c&&i.collapsed&&!c.options.lineWrapping&&Ht(a)==c.display.maxLine&&(l=!0),i.collapsed&&s!=t.line&&Jn(a,0),function(n,t,e){var a=e&&window.WeakSet&&(e.markedSpans||(e.markedSpans=new WeakSet));a&&n.markedSpans&&a.has(n.markedSpans)?n.markedSpans.push(t):(n.markedSpans=n.markedSpans?n.markedSpans.concat([t]):[t],a&&a.add(n.markedSpans)),t.marker.attachLine(n)}(a,new St(i,s==t.line?t.ch:null,s==e.line?e.ch:null),n.cm&&n.cm.curOp),++s})),i.collapsed&&n.iter(t.line,e.line+1,(function(t){Xt(n,t)&&Jn(t,0)})),i.clearOnEnter&&mn(i,"beforeCursorEnter",(function(){return i.clear()})),i.readOnly&&(Ft=!0,(n.history.done.length||n.history.undone.length)&&n.clearHistory()),i.collapsed&&(i.id=++Ei,i.atomic=!0),c){if(l&&(c.curOp.updateMaxLine=!0),i.collapsed)fa(c,t.line,e.line+1);else if(i.className||i.startStyle||i.endStyle||i.css||i.attributes||i.title)for(var d=t.line;d<=e.line;d++)ma(c,d,"text");i.atomic&&li(c.doc),pe(c,"markerAdded",c,i)}return i}Oi.prototype.clear=function(){if(!this.explicitlyCleared){var n=this.doc.cm,t=n&&!n.curOp;if(t&&Va(n),vn(this,"clear")){var e=this.find();e&&pe(this,"clear",e.from,e.to)}for(var a=null,o=null,i=0;in.display.maxLineLength&&(n.display.maxLine=c,n.display.maxLineLength=d,n.display.maxLineChanged=!0)}null!=a&&n&&this.collapsed&&fa(n,a,o+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,n&&li(n.doc)),n&&pe(n,"markerCleared",n,this,a,o),t&&Ga(n),this.parent&&this.parent.clear()}},Oi.prototype.find=function(n,t){var e,a;null==n&&"bookmark"==this.type&&(n=1);for(var o=0;o=0;s--)fi(this,a[s]);l?ai(this,l):this.cm&&Ba(this.cm)})),undo:io((function(){hi(this,"undo")})),redo:io((function(){hi(this,"redo")})),undoSelection:io((function(){hi(this,"undo",!0)})),redoSelection:io((function(){hi(this,"redo",!0)})),setExtending:function(n){this.extend=n},getExtending:function(){return this.extend},historySize:function(){for(var n=this.history,t=0,e=0,a=0;a=n.ch)&&t.push(o.marker.parent||o.marker)}return t},findMarks:function(n,t,e){n=pt(this,n),t=pt(this,t);var a=[],o=n.line;return this.iter(n.line,t.line+1,(function(i){var r=i.markedSpans;if(r)for(var l=0;l=s.to||null==s.from&&o!=n.line||null!=s.from&&o==t.line&&s.from>=t.ch||e&&!e(s.marker)||a.push(s.marker.parent||s.marker)}++o})),a},getAllMarks:function(){var n=[];return this.iter((function(t){var e=t.markedSpans;if(e)for(var a=0;an)return t=n,!0;n-=i,++e})),pt(this,ot(e,t))},indexFromPos:function(n){var t=(n=pt(this,n)).ch;if(n.linet&&(t=n.from),null!=n.to&&n.to-1)return t.state.draggingText(n),void setTimeout((function(){return t.display.input.focus()}),20);try{var p=n.dataTransfer.getData("Text");if(p){var b;if(t.state.draggingText&&!t.state.draggingText.copy&&(b=t.listSelections()),ii(t.doc,Do(e,e)),b)for(var u=0;u=0;t--)wi(n.doc,"",a[t].from,a[t].to,"+delete");Ba(n)}))}function er(n,t,e){var a=sn(n.text,t+e,e);return a<0||a>n.text.length?null:a}function ar(n,t,e){var a=er(n,t.ch,e);return null==a?null:new ot(t.line,a,e<0?"after":"before")}function or(n,t,e,a,o){if(n){"rtl"==t.doc.direction&&(o=-o);var i=un(e,t.doc.direction);if(i){var r,l=o<0?$(i):i[0],s=o<0==(1==l.level)?"after":"before";if(l.level>0||"rtl"==t.doc.direction){var c=Ae(t,e);r=o<0?e.text.length-1:0;var d=Te(t,c,r).top;r=cn((function(n){return Te(t,c,n).top==d}),o<0==(1==l.level)?l.from:l.to-1,r),"before"==s&&(r=er(e,r,1))}else r=o<0?l.to:l.from;return new ot(a,r,s)}}return new ot(a,o<0?e.text.length:0,o<0?"before":"after")}Zi.basic={Left:"goCharLeft",Right:"goCharRight",Up:"goLineUp",Down:"goLineDown",End:"goLineEnd",Home:"goLineStartSmart",PageUp:"goPageUp",PageDown:"goPageDown",Delete:"delCharAfter",Backspace:"delCharBefore","Shift-Backspace":"delCharBefore",Tab:"defaultTab","Shift-Tab":"indentAuto",Enter:"newlineAndIndent",Insert:"toggleOverwrite",Esc:"singleSelection"},Zi.pcDefault={"Ctrl-A":"selectAll","Ctrl-D":"deleteLine","Ctrl-Z":"undo","Shift-Ctrl-Z":"redo","Ctrl-Y":"redo","Ctrl-Home":"goDocStart","Ctrl-End":"goDocEnd","Ctrl-Up":"goLineUp","Ctrl-Down":"goLineDown","Ctrl-Left":"goGroupLeft","Ctrl-Right":"goGroupRight","Alt-Left":"goLineStart","Alt-Right":"goLineEnd","Ctrl-Backspace":"delGroupBefore","Ctrl-Delete":"delGroupAfter","Ctrl-S":"save","Ctrl-F":"find","Ctrl-G":"findNext","Shift-Ctrl-G":"findPrev","Shift-Ctrl-F":"replace","Shift-Ctrl-R":"replaceAll","Ctrl-[":"indentLess","Ctrl-]":"indentMore","Ctrl-U":"undoSelection","Shift-Ctrl-U":"redoSelection","Alt-U":"redoSelection",fallthrough:"basic"},Zi.emacsy={"Ctrl-F":"goCharRight","Ctrl-B":"goCharLeft","Ctrl-P":"goLineUp","Ctrl-N":"goLineDown","Ctrl-A":"goLineStart","Ctrl-E":"goLineEnd","Ctrl-V":"goPageDown","Shift-Ctrl-V":"goPageUp","Ctrl-D":"delCharAfter","Ctrl-H":"delCharBefore","Alt-Backspace":"delWordBefore","Ctrl-K":"killLine","Ctrl-T":"transposeChars","Ctrl-O":"openLine"},Zi.macDefault={"Cmd-A":"selectAll","Cmd-D":"deleteLine","Cmd-Z":"undo","Shift-Cmd-Z":"redo","Cmd-Y":"redo","Cmd-Home":"goDocStart","Cmd-Up":"goDocStart","Cmd-End":"goDocEnd","Cmd-Down":"goDocEnd","Alt-Left":"goGroupLeft","Alt-Right":"goGroupRight","Cmd-Left":"goLineLeft","Cmd-Right":"goLineRight","Alt-Backspace":"delGroupBefore","Ctrl-Alt-Backspace":"delGroupAfter","Alt-Delete":"delGroupAfter","Cmd-S":"save","Cmd-F":"find","Cmd-G":"findNext","Shift-Cmd-G":"findPrev","Cmd-Alt-F":"replace","Shift-Cmd-Alt-F":"replaceAll","Cmd-[":"indentLess","Cmd-]":"indentMore","Cmd-Backspace":"delWrappedLineLeft","Cmd-Delete":"delWrappedLineRight","Cmd-U":"undoSelection","Shift-Cmd-U":"redoSelection","Ctrl-Up":"goDocStart","Ctrl-Down":"goDocEnd",fallthrough:["basic","emacsy"]},Zi.default=w?Zi.macDefault:Zi.pcDefault;var ir={selectAll:bi,singleSelection:function(n){return n.setSelection(n.getCursor("anchor"),n.getCursor("head"),X)},killLine:function(n){return tr(n,(function(t){if(t.empty()){var e=Gn(n.doc,t.head.line).text.length;return t.head.ch==e&&t.head.line0)o=new ot(o.line,o.ch+1),n.replaceRange(i.charAt(o.ch-1)+i.charAt(o.ch-2),ot(o.line,o.ch-2),o,"+transpose");else if(o.line>n.doc.first){var r=Gn(n.doc,o.line-1).text;r&&(o=new ot(o.line,1),n.replaceRange(i.charAt(0)+n.doc.lineSeparator()+r.charAt(r.length-1),ot(o.line-1,r.length-1),o,"+transpose"))}e.push(new Eo(o,o))}n.setSelections(e)}))},newlineAndIndent:function(n){return eo(n,(function(){for(var t=n.listSelections(),e=t.length-1;e>=0;e--)n.replaceRange(n.doc.lineSeparator(),t[e].anchor,t[e].head,"+input");t=n.listSelections();for(var a=0;a-1&&(it((o=c.ranges[o]).from(),t)<0||t.xRel>0)&&(it(o.to(),t)>0||t.xRel<0)?function(n,t,e,a){var o=n.display,i=!1,c=ao(n,(function(t){s&&(o.scroller.draggable=!1),n.state.draggingText=!1,n.state.delayingBlurEvent&&(n.hasFocus()?n.state.delayingBlurEvent=!1:Ca(n)),gn(o.wrapper.ownerDocument,"mouseup",c),gn(o.wrapper.ownerDocument,"mousemove",d),gn(o.scroller,"dragstart",p),gn(o.scroller,"drop",c),i||(zn(t),a.addNew||Jo(n.doc,e,null,null,a.extend),s&&!u||r&&9==l?setTimeout((function(){o.wrapper.ownerDocument.body.focus({preventScroll:!0}),o.input.focus()}),20):o.input.focus())})),d=function(n){i=i||Math.abs(t.clientX-n.clientX)+Math.abs(t.clientY-n.clientY)>=10},p=function(){return i=!0};s&&(o.scroller.draggable=!0),n.state.draggingText=c,c.copy=!a.moveOnDrag,mn(o.wrapper.ownerDocument,"mouseup",c),mn(o.wrapper.ownerDocument,"mousemove",d),mn(o.scroller,"dragstart",p),mn(o.scroller,"drop",c),n.state.delayingBlurEvent=!0,setTimeout((function(){return o.input.focus()}),20),o.scroller.dragDrop&&o.scroller.dragDrop()}(n,a,t,i):function(n,t,e,a){r&&Ca(n);var o=n.display,i=n.doc;zn(t);var l,s,c=i.sel,d=c.ranges;if(a.addNew&&!a.extend?(s=i.sel.contains(e),l=s>-1?d[s]:new Eo(e,e)):(l=i.sel.primary(),s=i.sel.primIndex),"rectangle"==a.unit)a.addNew||(l=new Eo(e,e)),e=ba(n,t,!0,!0),s=-1;else{var p=vr(n,e,a.unit);l=a.extend?Qo(l,p.anchor,p.head,a.extend):p}a.addNew?-1==s?(s=d.length,oi(i,Oo(n,d.concat([l]),s),{scroll:!1,origin:"*mouse"})):d.length>1&&d[s].empty()&&"char"==a.unit&&!a.extend?(oi(i,Oo(n,d.slice(0,s).concat(d.slice(s+1)),0),{scroll:!1,origin:"*mouse"}),c=i.sel):ti(i,s,l,q):(s=0,oi(i,new So([l],0),q),c=i.sel);var b=e;function u(t){if(0!=it(b,t))if(b=t,"rectangle"==a.unit){for(var o=[],r=n.options.tabSize,d=Y(Gn(i,e.line).text,e.ch,r),p=Y(Gn(i,t.line).text,t.ch,r),u=Math.min(d,p),f=Math.max(d,p),m=Math.min(e.line,t.line),h=Math.min(n.lastLine(),Math.max(e.line,t.line));m<=h;m++){var g=Gn(i,m).text,x=K(g,u,r);u==f?o.push(new Eo(ot(m,x),ot(m,x))):g.length>x&&o.push(new Eo(ot(m,x),ot(m,K(g,f,r))))}o.length||o.push(new Eo(e,e)),oi(i,Oo(n,c.ranges.slice(0,s).concat(o),s),{origin:"*mouse",scroll:!1}),n.scrollIntoView(t)}else{var w,k=l,v=vr(n,t,a.unit),y=k.anchor;it(v.anchor,y)>0?(w=v.head,y=ct(k.from(),v.anchor)):(w=v.anchor,y=st(k.to(),v.head));var z=c.ranges.slice(0);z[s]=function(n,t){var e=t.anchor,a=t.head,o=Gn(n.doc,e.line);if(0==it(e,a)&&e.sticky==a.sticky)return t;var i=un(o);if(!i)return t;var r=pn(i,e.ch,e.sticky),l=i[r];if(l.from!=e.ch&&l.to!=e.ch)return t;var s,c=r+(l.from==e.ch==(1!=l.level)?0:1);if(0==c||c==i.length)return t;if(a.line!=e.line)s=(a.line-e.line)*("ltr"==n.doc.direction?1:-1)>0;else{var d=pn(i,a.ch,a.sticky),p=d-r||(a.ch-e.ch)*(1==l.level?-1:1);s=d==c-1||d==c?p<0:p>0}var b=i[c+(s?-1:0)],u=s==(1==b.level),f=u?b.from:b.to,m=u?"after":"before";return e.ch==f&&e.sticky==m?t:new Eo(new ot(e.line,f,m),a)}(n,new Eo(pt(i,y),w)),oi(i,Oo(n,z,s),q)}}var f=o.wrapper.getBoundingClientRect(),m=0;function h(t){n.state.selectingText=!1,m=1/0,t&&(zn(t),o.input.focus()),gn(o.wrapper.ownerDocument,"mousemove",g),gn(o.wrapper.ownerDocument,"mouseup",x),i.history.lastSelOrigin=null}var g=ao(n,(function(t){0!==t.buttons&&En(t)?function t(e){var r=++m,l=ba(n,e,!0,"rectangle"==a.unit);if(l)if(0!=it(l,b)){n.curOp.focus=L(j(n)),u(l);var s=Ia(o,i);(l.line>=s.to||l.linef.bottom?20:0;c&&setTimeout(ao(n,(function(){m==r&&(o.scroller.scrollTop+=c,t(e))})),50)}}(t):h(t)})),x=ao(n,h);n.state.selectingText=x,mn(o.wrapper.ownerDocument,"mousemove",g),mn(o.wrapper.ownerDocument,"mouseup",x)}(n,a,t,i)}(t,a,i,n):Sn(n)==e.scroller&&zn(n):2==o?(a&&Jo(t.doc,a),setTimeout((function(){return e.input.focus()}),20)):3==o&&(_?t.display.input.onContextMenu(n):Ca(t)))}}function vr(n,t,e){if("char"==e)return new Eo(t,t);if("word"==e)return n.findWordAt(t);if("line"==e)return new Eo(ot(t.line,0),pt(n.doc,ot(t.line+1,0)));var a=e(n,t);return new Eo(a.from,a.to)}function yr(n,t,e,a){var o,i;if(t.touches)o=t.touches[0].clientX,i=t.touches[0].clientY;else try{o=t.clientX,i=t.clientY}catch(n){return!1}if(o>=Math.floor(n.display.gutters.getBoundingClientRect().right))return!1;a&&zn(t);var r=n.display,l=r.lineDiv.getBoundingClientRect();if(i>l.bottom||!vn(n,e))return Fn(t);i-=l.top-r.viewOffset;for(var s=0;s=o)return xn(n,e,n,tt(n.doc,i),n.display.gutterSpecs[s].className,t),Fn(t)}}function zr(n,t){return yr(n,t,"gutterClick",!0)}function _r(n,t){Fe(n.display,t)||function(n,t){return!!vn(n,"gutterContextMenu")&&yr(n,t,"gutterContextMenu",!1)}(n,t)||wn(n,t,"contextmenu")||_||n.display.input.onContextMenu(t)}function Fr(n){n.display.wrapper.className=n.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+n.options.theme.replace(/(^|\s)\s*/g," cm-s-"),Ue(n)}wr.prototype.compare=function(n,t,e){return this.time+400>n&&0==it(t,this.pos)&&e==this.button};var Cr={toString:function(){return"CodeMirror.Init"}},Sr={},Er={};function Or(n,t,e){if(!t!=!(e&&e!=Cr)){var a=n.display.dragFunctions,o=t?mn:gn;o(n.display.scroller,"dragstart",a.start),o(n.display.scroller,"dragenter",a.enter),o(n.display.scroller,"dragover",a.over),o(n.display.scroller,"dragleave",a.leave),o(n.display.scroller,"drop",a.drop)}}function Dr(n){n.options.lineWrapping?(B(n.display.wrapper,"CodeMirror-wrap"),n.display.sizer.style.minWidth="",n.display.sizerWidth=null):(S(n.display.wrapper,"CodeMirror-wrap"),Vt(n)),pa(n),fa(n),Ue(n),setTimeout((function(){return Wa(n)}),100)}function Ir(n,t){var e=this;if(!(this instanceof Ir))return new Ir(n,t);this.options=t=t?R(t):{},R(Sr,t,!1);var a=t.value;"string"==typeof a?a=new Ai(a,t.mode,null,t.lineSeparator,t.direction):t.mode&&(a.modeOption=t.mode),this.doc=a;var o=new Ir.inputStyles[t.inputStyle](this),i=this.display=new vo(n,a,o,t);for(var c in i.wrapper.CodeMirror=this,Fr(this),t.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap"),Za(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:-1,cutIncoming:-1,selectingText:!1,draggingText:!1,highlight:new H,keySeq:null,specialChars:null},t.autofocus&&!x&&i.input.focus(),r&&l<11&&setTimeout((function(){return e.display.input.reset(!0)}),20),function(n){var t=n.display;mn(t.scroller,"mousedown",ao(n,kr)),mn(t.scroller,"dblclick",r&&l<11?ao(n,(function(t){if(!wn(n,t)){var e=ba(n,t);if(e&&!zr(n,t)&&!Fe(n.display,t)){zn(t);var a=n.findWordAt(e);Jo(n.doc,a.anchor,a.head)}}})):function(t){return wn(n,t)||zn(t)}),mn(t.scroller,"contextmenu",(function(t){return _r(n,t)})),mn(t.input.getField(),"contextmenu",(function(e){t.scroller.contains(e.target)||_r(n,e)}));var e,a={end:0};function o(){t.activeTouch&&(e=setTimeout((function(){return t.activeTouch=null}),1e3),(a=t.activeTouch).end=+new Date)}function i(n,t){if(null==t.left)return!0;var e=t.left-n.left,a=t.top-n.top;return e*e+a*a>400}mn(t.scroller,"touchstart",(function(o){if(!wn(n,o)&&!function(n){if(1!=n.touches.length)return!1;var t=n.touches[0];return t.radiusX<=1&&t.radiusY<=1}(o)&&!zr(n,o)){t.input.ensurePolled(),clearTimeout(e);var i=+new Date;t.activeTouch={start:i,moved:!1,prev:i-a.end<=300?a:null},1==o.touches.length&&(t.activeTouch.left=o.touches[0].pageX,t.activeTouch.top=o.touches[0].pageY)}})),mn(t.scroller,"touchmove",(function(){t.activeTouch&&(t.activeTouch.moved=!0)})),mn(t.scroller,"touchend",(function(e){var a=t.activeTouch;if(a&&!Fe(t,e)&&null!=a.left&&!a.moved&&new Date-a.start<300){var r,l=n.coordsChar(t.activeTouch,"page");r=!a.prev||i(a,a.prev)?new Eo(l,l):!a.prev.prev||i(a,a.prev.prev)?n.findWordAt(l):new Eo(ot(l.line,0),pt(n.doc,ot(l.line+1,0))),n.setSelection(r.anchor,r.head),n.focus(),zn(e)}o()})),mn(t.scroller,"touchcancel",o),mn(t.scroller,"scroll",(function(){t.scroller.clientHeight&&(Pa(n,t.scroller.scrollTop),Ra(n,t.scroller.scrollLeft,!0),xn(n,"scroll",n))})),mn(t.scroller,"mousewheel",(function(t){return Co(n,t)})),mn(t.scroller,"DOMMouseScroll",(function(t){return Co(n,t)})),mn(t.wrapper,"scroll",(function(){return t.wrapper.scrollTop=t.wrapper.scrollLeft=0})),t.dragFunctions={enter:function(t){wn(n,t)||Cn(t)},over:function(t){wn(n,t)||(function(n,t){var e=ba(n,t);if(e){var a=document.createDocumentFragment();va(n,e,a),n.display.dragCursor||(n.display.dragCursor=D("div",null,"CodeMirror-cursors CodeMirror-dragcursors"),n.display.lineSpace.insertBefore(n.display.dragCursor,n.display.cursorDiv)),O(n.display.dragCursor,a)}}(n,t),Cn(t))},start:function(t){return function(n,t){if(r&&(!n.state.draggingText||+new Date-Ti<100))Cn(t);else if(!wn(n,t)&&!Fe(n.display,t)&&(t.dataTransfer.setData("Text",n.getSelection()),t.dataTransfer.effectAllowed="copyMove",t.dataTransfer.setDragImage&&!u)){var e=D("img",null,null,"position: fixed; left: 0; top: 0;");e.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",b&&(e.width=e.height=1,n.display.wrapper.appendChild(e),e._top=e.offsetTop),t.dataTransfer.setDragImage(e,0,0),b&&e.parentNode.removeChild(e)}}(n,t)},drop:ao(n,ji),leave:function(t){wn(n,t)||Pi(n)}};var s=t.input.getField();mn(s,"keyup",(function(t){return mr.call(n,t)})),mn(s,"keydown",ao(n,fr)),mn(s,"keypress",ao(n,hr)),mn(s,"focus",(function(t){return Sa(n,t)})),mn(s,"blur",(function(t){return Ea(n,t)}))}(this),Yi(),Va(this),this.curOp.forceUpdate=!0,Ro(this,a),t.autofocus&&!x||this.hasFocus()?setTimeout((function(){e.hasFocus()&&!e.state.focused&&Sa(e)}),20):Ea(this),Er)Er.hasOwnProperty(c)&&Er[c](this,t[c],Cr);go(this),t.finishInit&&t.finishInit(this);for(var d=0;d150)){if(!a)return;e="prev"}}else c=0,e="not";"prev"==e?c=t>i.first?Y(Gn(i,t-1).text,null,r):0:"add"==e?c=s+n.options.indentUnit:"subtract"==e?c=s-n.options.indentUnit:"number"==typeof e&&(c=s+e),c=Math.max(0,c);var p="",b=0;if(n.options.indentWithTabs)for(var u=Math.floor(c/r);u;--u)b+=r,p+="\t";if(br,s=An(t),c=null;if(l&&a.ranges.length>1)if(Br&&Br.text.join("\n")==t){if(a.ranges.length%Br.text.length==0){c=[];for(var d=0;d=0;b--){var u=a.ranges[b],f=u.from(),m=u.to();u.empty()&&(e&&e>0?f=ot(f.line,f.ch-e):n.state.overwrite&&!l?m=ot(m.line,Math.min(Gn(i,m.line).text.length,m.ch+$(s).length)):l&&Br&&Br.lineWise&&Br.text.join("\n")==s.join("\n")&&(f=m=ot(f.line,0)));var h={from:f,to:m,text:c?c[b%c.length]:s,origin:o||(l?"paste":n.state.cutIncoming>r?"cut":"+input")};fi(n.doc,h),pe(n,"inputRead",n,h)}t&&!l&&Pr(n,t),Ba(n),n.curOp.updateInput<2&&(n.curOp.updateInput=p),n.curOp.typing=!0,n.state.pasteIncoming=n.state.cutIncoming=-1}function jr(n,t){var e=n.clipboardData&&n.clipboardData.getData("Text");if(e)return n.preventDefault(),t.isReadOnly()||t.options.disableInput||!t.hasFocus()||eo(t,(function(){return Tr(t,e,0,null,"paste")})),!0}function Pr(n,t){if(n.options.electricChars&&n.options.smartIndent)for(var e=n.doc.sel,a=e.ranges.length-1;a>=0;a--){var o=e.ranges[a];if(!(o.head.ch>100||a&&e.ranges[a-1].head.line==o.head.line)){var i=n.getModeAt(o.head),r=!1;if(i.electricChars){for(var l=0;l-1){r=Lr(n,o.head.line,"smart");break}}else i.electricInput&&i.electricInput.test(Gn(n.doc,o.head.line).text.slice(0,o.head.ch))&&(r=Lr(n,o.head.line,"smart"));r&&pe(n,"electricInput",n,o.head.line)}}}function Nr(n){for(var t=[],e=[],a=0;a0?0:-1));if(isNaN(d))r=null;else{var p=e>0?d>=55296&&d<56320:d>=56320&&d<57343;r=new ot(t.line,Math.max(0,Math.min(l.text.length,t.ch+e*(p?2:1))),-e)}}else r=o?function(n,t,e,a){var o=un(t,n.doc.direction);if(!o)return ar(t,e,a);e.ch>=t.text.length?(e.ch=t.text.length,e.sticky="before"):e.ch<=0&&(e.ch=0,e.sticky="after");var i=pn(o,e.ch,e.sticky),r=o[i];if("ltr"==n.doc.direction&&r.level%2==0&&(a>0?r.to>e.ch:r.from=r.from&&b>=d.begin)){var u=p?"before":"after";return new ot(e.line,b,u)}}var f=function(n,t,a){for(var i=function(n,t){return t?new ot(e.line,s(n,1),"before"):new ot(e.line,n,"after")};n>=0&&n0==(1!=r.level),c=l?a.begin:s(a.end,-1);if(r.from<=c&&c0?d.end:s(d.begin,-1);return null==h||a>0&&h==t.text.length||!(m=f(a>0?0:o.length-1,a,c(h)))?null:m}(n.cm,l,t,e):ar(l,t,e);if(null==r){if(i||(c=t.line+s)=n.first+n.size||(t=new ot(c,t.ch,t.sticky),!(l=Gn(n,c))))return!1;t=or(o,n.cm,l,t.line,s)}else t=r;return!0}if("char"==a||"codepoint"==a)c();else if("column"==a)c(!0);else if("word"==a||"group"==a)for(var d=null,p="group"==a,b=n.cm&&n.cm.getHelper(t,"wordChars"),u=!0;!(e<0)||c(!u);u=!1){var f=l.text.charAt(t.ch)||"\n",m=an(f,b)?"w":p&&"\n"==f?"n":!p||/\s/.test(f)?null:"p";if(!p||u||m||(m="s"),d&&d!=m){e<0&&(e=1,c(),t.sticky="after");break}if(m&&(d=m),e>0&&!c(!u))break}var h=di(n,t,i,r,!0);return rt(i,h)&&(h.hitSide=!0),h}function Ur(n,t,e,a){var o,i,r=n.doc,l=t.left;if("page"==a){var s=Math.min(n.display.wrapper.clientHeight,P(n).innerHeight||r(n).documentElement.clientHeight),c=Math.max(s-.5*ra(n.display),3);o=(e>0?t.bottom:t.top)+e*c}else"line"==a&&(o=e>0?t.bottom+3:t.top-3);for(;(i=Je(n,l,o)).outside;){if(e<0?o<=0:o>=r.height){i.hitSide=!0;break}o+=5*e}return i}var Wr=function(n){this.cm=n,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new H,this.composing=null,this.gracePeriod=!1,this.readDOMTimeout=null};function Xr(n,t){var e=Be(n,t.line);if(!e||e.hidden)return null;var a=Gn(n.doc,t.line),o=Me(e,a,t.line),i=un(a,n.doc.direction),r="left";i&&(r=pn(i,t.ch)%2?"right":"left");var l=Ne(o.map,t.ch,r);return l.offset="right"==l.collapse?l.end:l.start,l}function qr(n,t){return t&&(n.bad=!0),n}function Zr(n,t,e){var a;if(t==n.display.lineDiv){if(!(a=n.display.lineDiv.childNodes[e]))return qr(n.clipPos(ot(n.display.viewTo-1)),!0);t=null,e=0}else for(a=t;;a=a.parentNode){if(!a||a==n.display.lineDiv)return null;if(a.parentNode&&a.parentNode==n.display.lineDiv)break}for(var o=0;o=t.display.viewTo||i.line=t.display.viewFrom&&Xr(t,o)||{node:s[0].measure.map[2],offset:0},d=i.linea.firstLine()&&(r=ot(r.line-1,Gn(a.doc,r.line-1).length)),l.ch==Gn(a.doc,l.line).text.length&&l.lineo.viewTo-1)return!1;r.line==o.viewFrom||0==(n=ua(a,r.line))?(t=nt(o.view[0].line),e=o.view[0].node):(t=nt(o.view[n].line),e=o.view[n-1].node.nextSibling);var s,c,d=ua(a,l.line);if(d==o.view.length-1?(s=o.viewTo-1,c=o.lineDiv.lastChild):(s=nt(o.view[d+1].line)-1,c=o.view[d+1].node.previousSibling),!e)return!1;for(var p=a.doc.splitLines(function(n,t,e,a,o){var i="",r=!1,l=n.doc.lineSeparator(),s=!1;function c(){r&&(i+=l,s&&(i+=l),r=s=!1)}function d(n){n&&(c(),i+=n)}function p(t){if(1==t.nodeType){var e=t.getAttribute("cm-text");if(e)return void d(e);var i,b=t.getAttribute("cm-marker");if(b){var u=n.findMarks(ot(a,0),ot(o+1,0),(h=+b,function(n){return n.id==h}));return void(u.length&&(i=u[0].find(0))&&d($n(n.doc,i.from,i.to).join(l)))}if("false"==t.getAttribute("contenteditable"))return;var f=/^(pre|div|p|li|table|br)$/i.test(t.nodeName);if(!/^br$/i.test(t.nodeName)&&0==t.textContent.length)return;f&&c();for(var m=0;m1&&b.length>1;)if($(p)==$(b))p.pop(),b.pop(),s--;else{if(p[0]!=b[0])break;p.shift(),b.shift(),t++}for(var u=0,f=0,m=p[0],h=b[0],g=Math.min(m.length,h.length);ur.ch&&x.charCodeAt(x.length-f-1)==w.charCodeAt(w.length-f-1);)u--,f++;p[p.length-1]=x.slice(0,x.length-f).replace(/^\u200b+/,""),p[0]=p[0].slice(u).replace(/\u200b+$/,"");var v=ot(t,u),y=ot(s,b.length?$(b).length-f:0);return p.length>1||p[0]||it(v,y)?(wi(a.doc,p,v,y,"+input"),!0):void 0},Wr.prototype.ensurePolled=function(){this.forceCompositionEnd()},Wr.prototype.reset=function(){this.forceCompositionEnd()},Wr.prototype.forceCompositionEnd=function(){this.composing&&(clearTimeout(this.readDOMTimeout),this.composing=null,this.updateFromDOM(),this.div.blur(),this.div.focus())},Wr.prototype.readFromDOMSoon=function(){var n=this;null==this.readDOMTimeout&&(this.readDOMTimeout=setTimeout((function(){if(n.readDOMTimeout=null,n.composing){if(!n.composing.done)return;n.composing=null}n.updateFromDOM()}),80))},Wr.prototype.updateFromDOM=function(){var n=this;!this.cm.isReadOnly()&&this.pollContent()||eo(this.cm,(function(){return fa(n.cm)}))},Wr.prototype.setUneditable=function(n){n.contentEditable="false"},Wr.prototype.onKeyPress=function(n){0==n.charCode||this.composing||(n.preventDefault(),this.cm.isReadOnly()||ao(this.cm,Tr)(this.cm,String.fromCharCode(null==n.charCode?n.keyCode:n.charCode),0))},Wr.prototype.readOnlyChanged=function(n){this.div.contentEditable=String("nocursor"!=n)},Wr.prototype.onContextMenu=function(){},Wr.prototype.resetPosition=function(){},Wr.prototype.needsContentAttribute=!0;var Vr=function(n){this.cm=n,this.prevInput="",this.pollingFast=!1,this.polling=new H,this.hasSelection=!1,this.composing=null,this.resetting=!1};Vr.prototype.init=function(n){var t=this,e=this,a=this.cm;this.createField(n);var o=this.textarea;function i(n){if(!wn(a,n)){if(a.somethingSelected())Ar({lineWise:!1,text:a.getSelections()});else{if(!a.options.lineWiseCopyCut)return;var t=Nr(a);Ar({lineWise:!0,text:t.text}),"cut"==n.type?a.setSelections(t.ranges,null,X):(e.prevInput="",o.value=t.text.join("\n"),T(o))}"cut"==n.type&&(a.state.cutIncoming=+new Date)}}n.wrapper.insertBefore(this.wrapper,n.wrapper.firstChild),h&&(o.style.width="0px"),mn(o,"input",(function(){r&&l>=9&&t.hasSelection&&(t.hasSelection=null),e.poll()})),mn(o,"paste",(function(n){wn(a,n)||jr(n,a)||(a.state.pasteIncoming=+new Date,e.fastPoll())})),mn(o,"cut",i),mn(o,"copy",i),mn(n.scroller,"paste",(function(t){if(!Fe(n,t)&&!wn(a,t)){if(!o.dispatchEvent)return a.state.pasteIncoming=+new Date,void e.focus();var i=new Event("paste");i.clipboardData=t.clipboardData,o.dispatchEvent(i)}})),mn(n.lineSpace,"selectstart",(function(t){Fe(n,t)||zn(t)})),mn(o,"compositionstart",(function(){var n=a.getCursor("from");e.composing&&e.composing.range.clear(),e.composing={start:n,range:a.markText(n,a.getCursor("to"),{className:"CodeMirror-composing"})}})),mn(o,"compositionend",(function(){e.composing&&(e.poll(),e.composing.range.clear(),e.composing=null)}))},Vr.prototype.createField=function(n){this.wrapper=Yr(),this.textarea=this.wrapper.firstChild},Vr.prototype.screenReaderLabelChanged=function(n){n?this.textarea.setAttribute("aria-label",n):this.textarea.removeAttribute("aria-label")},Vr.prototype.prepareSelection=function(){var n=this.cm,t=n.display,e=n.doc,a=ka(n);if(n.options.moveInputWithCursor){var o=Ge(n,e.sel.primary().head,"div"),i=t.wrapper.getBoundingClientRect(),r=t.lineDiv.getBoundingClientRect();a.teTop=Math.max(0,Math.min(t.wrapper.clientHeight-10,o.top+r.top-i.top)),a.teLeft=Math.max(0,Math.min(t.wrapper.clientWidth-10,o.left+r.left-i.left))}return a},Vr.prototype.showSelection=function(n){var t=this.cm.display;O(t.cursorDiv,n.cursors),O(t.selectionDiv,n.selection),null!=n.teTop&&(this.wrapper.style.top=n.teTop+"px",this.wrapper.style.left=n.teLeft+"px")},Vr.prototype.reset=function(n){if(!(this.contextMenuPending||this.composing&&n)){var t=this.cm;if(this.resetting=!0,t.somethingSelected()){this.prevInput="";var e=t.getSelection();this.textarea.value=e,t.state.focused&&T(this.textarea),r&&l>=9&&(this.hasSelection=e)}else n||(this.prevInput=this.textarea.value="",r&&l>=9&&(this.hasSelection=null));this.resetting=!1}},Vr.prototype.getField=function(){return this.textarea},Vr.prototype.supportsTouch=function(){return!1},Vr.prototype.focus=function(){if("nocursor"!=this.cm.options.readOnly&&(!x||L(this.textarea.ownerDocument)!=this.textarea))try{this.textarea.focus()}catch(n){}},Vr.prototype.blur=function(){this.textarea.blur()},Vr.prototype.resetPosition=function(){this.wrapper.style.top=this.wrapper.style.left=0},Vr.prototype.receivedFocus=function(){this.slowPoll()},Vr.prototype.slowPoll=function(){var n=this;this.pollingFast||this.polling.set(this.cm.options.pollInterval,(function(){n.poll(),n.cm.state.focused&&n.slowPoll()}))},Vr.prototype.fastPoll=function(){var n=!1,t=this;t.pollingFast=!0,t.polling.set(20,(function e(){t.poll()||n?(t.pollingFast=!1,t.slowPoll()):(n=!0,t.polling.set(60,e))}))},Vr.prototype.poll=function(){var n=this,t=this.cm,e=this.textarea,a=this.prevInput;if(this.contextMenuPending||this.resetting||!t.state.focused||Tn(e)&&!a&&!this.composing||t.isReadOnly()||t.options.disableInput||t.state.keySeq)return!1;var o=e.value;if(o==a&&!t.somethingSelected())return!1;if(r&&l>=9&&this.hasSelection===o||w&&/[\uf700-\uf7ff]/.test(o))return t.display.input.reset(),!1;if(t.doc.sel==t.display.selForContextMenu){var i=o.charCodeAt(0);if(8203!=i||a||(a="​"),8666==i)return this.reset(),this.cm.execCommand("undo")}for(var s=0,c=Math.min(a.length,o.length);s1e3||o.indexOf("\n")>-1?e.value=n.prevInput="":n.prevInput=o,n.composing&&(n.composing.range.clear(),n.composing.range=t.markText(n.composing.start,t.getCursor("to"),{className:"CodeMirror-composing"}))})),!0},Vr.prototype.ensurePolled=function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},Vr.prototype.onKeyPress=function(){r&&l>=9&&(this.hasSelection=null),this.fastPoll()},Vr.prototype.onContextMenu=function(n){var t=this,e=t.cm,a=e.display,o=t.textarea;t.contextMenuPending&&t.contextMenuPending();var i=ba(e,n),c=a.scroller.scrollTop;if(i&&!b){e.options.resetSelectionOnContextMenu&&-1==e.doc.sel.contains(i)&&ao(e,oi)(e.doc,Do(i),X);var d,p=o.style.cssText,u=t.wrapper.style.cssText,f=t.wrapper.offsetParent.getBoundingClientRect();if(t.wrapper.style.cssText="position: static",o.style.cssText="position: absolute; width: 30px; height: 30px;\n top: "+(n.clientY-f.top-5)+"px; left: "+(n.clientX-f.left-5)+"px;\n z-index: 1000; background: "+(r?"rgba(255, 255, 255, .05)":"transparent")+";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);",s&&(d=o.ownerDocument.defaultView.scrollY),a.input.focus(),s&&o.ownerDocument.defaultView.scrollTo(null,d),a.input.reset(),e.somethingSelected()||(o.value=t.prevInput=" "),t.contextMenuPending=g,a.selForContextMenu=e.doc.sel,clearTimeout(a.detectingSelectAll),r&&l>=9&&h(),_){Cn(n);var m=function(){gn(window,"mouseup",m),setTimeout(g,20)};mn(window,"mouseup",m)}else setTimeout(g,50)}function h(){if(null!=o.selectionStart){var n=e.somethingSelected(),i="​"+(n?o.value:"");o.value="⇚",o.value=i,t.prevInput=n?"":"​",o.selectionStart=1,o.selectionEnd=i.length,a.selForContextMenu=e.doc.sel}}function g(){if(t.contextMenuPending==g&&(t.contextMenuPending=!1,t.wrapper.style.cssText=u,o.style.cssText=p,r&&l<9&&a.scrollbars.setScrollTop(a.scroller.scrollTop=c),null!=o.selectionStart)){(!r||r&&l<9)&&h();var n=0,i=function(){a.selForContextMenu==e.doc.sel&&0==o.selectionStart&&o.selectionEnd>0&&"​"==t.prevInput?ao(e,bi)(e):n++<10?a.detectingSelectAll=setTimeout(i,500):(a.selForContextMenu=null,a.input.reset())};a.detectingSelectAll=setTimeout(i,200)}}},Vr.prototype.readOnlyChanged=function(n){n||this.reset(),this.textarea.disabled="nocursor"==n,this.textarea.readOnly=!!n},Vr.prototype.setUneditable=function(){},Vr.prototype.needsContentAttribute=!1,function(n){var t=n.optionHandlers;function e(e,a,o,i){n.defaults[e]=a,o&&(t[e]=i?function(n,t,e){e!=Cr&&o(n,t,e)}:o)}n.defineOption=e,n.Init=Cr,e("value","",(function(n,t){return n.setValue(t)}),!0),e("mode",null,(function(n,t){n.doc.modeOption=t,Ao(n)}),!0),e("indentUnit",2,Ao,!0),e("indentWithTabs",!1),e("smartIndent",!0),e("tabSize",4,(function(n){To(n),Ue(n),fa(n)}),!0),e("lineSeparator",null,(function(n,t){if(n.doc.lineSep=t,t){var e=[],a=n.doc.first;n.doc.iter((function(n){for(var o=0;;){var i=n.text.indexOf(t,o);if(-1==i)break;o=i+t.length,e.push(ot(a,i))}a++}));for(var o=e.length-1;o>=0;o--)wi(n.doc,t,e[o],ot(e[o].line,e[o].ch+t.length))}})),e("specialChars",/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9-\ufffc]/g,(function(n,t,e){n.state.specialChars=new RegExp(t.source+(t.test("\t")?"":"|\t"),"g"),e!=Cr&&n.refresh()})),e("specialCharPlaceholder",ee,(function(n){return n.refresh()}),!0),e("electricChars",!0),e("inputStyle",x?"contenteditable":"textarea",(function(){throw new Error("inputStyle can not (yet) be changed in a running editor")}),!0),e("spellcheck",!1,(function(n,t){return n.getInputField().spellcheck=t}),!0),e("autocorrect",!1,(function(n,t){return n.getInputField().autocorrect=t}),!0),e("autocapitalize",!1,(function(n,t){return n.getInputField().autocapitalize=t}),!0),e("rtlMoveVisually",!v),e("wholeLineUpdateBefore",!0),e("theme","default",(function(n){Fr(n),ko(n)}),!0),e("keyMap","default",(function(n,t,e){var a=nr(t),o=e!=Cr&&nr(e);o&&o.detach&&o.detach(n,a),a.attach&&a.attach(n,o||null)})),e("extraKeys",null),e("configureMouse",null),e("lineWrapping",!1,Dr,!0),e("gutters",[],(function(n,t){n.display.gutterSpecs=xo(t,n.options.lineNumbers),ko(n)}),!0),e("fixedGutter",!0,(function(n,t){n.display.gutters.style.left=t?ca(n.display)+"px":"0",n.refresh()}),!0),e("coverGutterNextToScrollbar",!1,(function(n){return Wa(n)}),!0),e("scrollbarStyle","native",(function(n){Za(n),Wa(n),n.display.scrollbars.setScrollTop(n.doc.scrollTop),n.display.scrollbars.setScrollLeft(n.doc.scrollLeft)}),!0),e("lineNumbers",!1,(function(n,t){n.display.gutterSpecs=xo(n.options.gutters,t),ko(n)}),!0),e("firstLineNumber",1,ko,!0),e("lineNumberFormatter",(function(n){return n}),ko,!0),e("showCursorWhenSelecting",!1,wa,!0),e("resetSelectionOnContextMenu",!0),e("lineWiseCopyCut",!0),e("pasteLinesPerSelection",!0),e("selectionsMayTouch",!1),e("readOnly",!1,(function(n,t){"nocursor"==t&&(Ea(n),n.display.input.blur()),n.display.input.readOnlyChanged(t)})),e("screenReaderLabel",null,(function(n,t){t=""===t?null:t,n.display.input.screenReaderLabelChanged(t)})),e("disableInput",!1,(function(n,t){t||n.display.input.reset()}),!0),e("dragDrop",!0,Or),e("allowDropFileTypes",null),e("cursorBlinkRate",530),e("cursorScrollMargin",0),e("cursorHeight",1,wa,!0),e("singleCursorHeightPerLine",!0,wa,!0),e("workTime",100),e("workDelay",100),e("flattenSpans",!0,To,!0),e("addModeClass",!1,To,!0),e("pollInterval",100),e("undoDepth",200,(function(n,t){return n.doc.history.undoDepth=t})),e("historyEventDelay",1250),e("viewportMargin",10,(function(n){return n.refresh()}),!0),e("maxHighlightLength",1e4,To,!0),e("moveInputWithCursor",!0,(function(n,t){t||n.display.input.resetPosition()})),e("tabindex",null,(function(n,t){return n.display.input.getField().tabIndex=t||""})),e("autofocus",null),e("direction","ltr",(function(n,t){return n.doc.setDirection(t)}),!0),e("phrases",null)}(Ir),function(n){var t=n.optionHandlers,e=n.helpers={};n.prototype={constructor:n,focus:function(){P(this).focus(),this.display.input.focus()},setOption:function(n,e){var a=this.options,o=a[n];a[n]==e&&"mode"!=n||(a[n]=e,t.hasOwnProperty(n)&&ao(this,t[n])(this,e,o),xn(this,"optionChange",this,n))},getOption:function(n){return this.options[n]},getDoc:function(){return this.doc},addKeyMap:function(n,t){this.state.keyMaps[t?"push":"unshift"](nr(n))},removeKeyMap:function(n){for(var t=this.state.keyMaps,e=0;ee&&(Lr(this,o.head.line,n,!0),e=o.head.line,a==this.doc.sel.primIndex&&Ba(this));else{var i=o.from(),r=o.to(),l=Math.max(e,i.line);e=Math.min(this.lastLine(),r.line-(r.ch?0:1))+1;for(var s=l;s0&&ti(this.doc,a,new Eo(i,c[a].to()),X)}}})),getTokenAt:function(n,t){return yt(this,n,t)},getLineTokens:function(n,t){return yt(this,ot(n),t,!0)},getTokenTypeAt:function(n){n=pt(this.doc,n);var t,e=ht(this,Gn(this.doc,n.line)),a=0,o=(e.length-1)/2,i=n.ch;if(0==i)t=e[2];else for(;;){var r=a+o>>1;if((r?e[2*r-1]:0)>=i)o=r;else{if(!(e[2*r+1]i&&(n=i,o=!0),a=Gn(this.doc,n)}else a=n;return Ze(this,a,{top:0,left:0},t||"page",e||o).top+(o?this.doc.height-Zt(a):0)},defaultTextHeight:function(){return ra(this.display)},defaultCharWidth:function(){return la(this.display)},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(n,t,e,a,o){var i,r,l,s=this.display,c=(n=Ge(this,pt(this.doc,n))).bottom,d=n.left;if(t.style.position="absolute",t.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(t),s.sizer.appendChild(t),"over"==a)c=n.top;else if("above"==a||"near"==a){var p=Math.max(s.wrapper.clientHeight,this.doc.height),b=Math.max(s.sizer.clientWidth,s.lineSpace.clientWidth);("above"==a||n.bottom+t.offsetHeight>p)&&n.top>t.offsetHeight?c=n.top-t.offsetHeight:n.bottom+t.offsetHeight<=p&&(c=n.bottom),d+t.offsetWidth>b&&(d=b-t.offsetWidth)}t.style.top=c+"px",t.style.left=t.style.right="","right"==o?(d=s.sizer.clientWidth-t.offsetWidth,t.style.right="0px"):("left"==o?d=0:"middle"==o&&(d=(s.sizer.clientWidth-t.offsetWidth)/2),t.style.left=d+"px"),e&&(i=this,r={left:d,top:c,right:d+t.offsetWidth,bottom:c+t.offsetHeight},null!=(l=Ma(i,r)).scrollTop&&Pa(i,l.scrollTop),null!=l.scrollLeft&&Ra(i,l.scrollLeft))},triggerOnKeyDown:oo(fr),triggerOnKeyPress:oo(hr),triggerOnKeyUp:mr,triggerOnMouseDown:oo(kr),execCommand:function(n){if(ir.hasOwnProperty(n))return ir[n].call(null,this)},triggerElectric:oo((function(n){Pr(this,n)})),findPosH:function(n,t,e,a){var o=1;t<0&&(o=-1,t=-t);for(var i=pt(this.doc,n),r=0;r0&&r(t.charAt(e-1));)--e;for(;a.5||this.options.lineWrapping)&&pa(this),xn(this,"refresh",this)})),swapDoc:oo((function(n){var t=this.doc;return t.cm=null,this.state.selectingText&&this.state.selectingText(),Ro(this,n),Ue(this),this.display.input.reset(),Aa(this,n.scrollLeft,n.scrollTop),this.curOp.forceScroll=!0,pe(this,"swapDoc",this,t),t})),phrase:function(n){var t=this.options.phrases;return t&&Object.prototype.hasOwnProperty.call(t,n)?t[n]:n},getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},yn(n),n.registerHelper=function(t,a,o){e.hasOwnProperty(t)||(e[t]=n[t]={_global:[]}),e[t][a]=o},n.registerGlobalHelper=function(t,a,o,i){n.registerHelper(t,a,i),e[t]._global.push({pred:o,val:i})}}(Ir);var Gr="iter insert remove copy getEditor constructor".split(" ");for(var $r in Ai.prototype)Ai.prototype.hasOwnProperty($r)&&U(Gr,$r)<0&&(Ir.prototype[$r]=function(n){return function(){return n.apply(this.doc,arguments)}}(Ai.prototype[$r]));return yn(Ai),Ir.inputStyles={textarea:Vr,contenteditable:Wr},Ir.defineMode=function(n){Ir.defaults.mode||"null"==n||(Ir.defaults.mode=n),Yn.apply(this,arguments)},Ir.defineMIME=function(n,t){Rn[n]=t},Ir.defineMode("null",(function(){return{token:function(n){return n.skipToEnd()}}})),Ir.defineMIME("text/plain","null"),Ir.defineExtension=function(n,t){Ir.prototype[n]=t},Ir.defineDocExtension=function(n,t){Ai.prototype[n]=t},Ir.fromTextArea=function(n,t){if((t=t?R(t):{}).value=n.value,!t.tabindex&&n.tabIndex&&(t.tabindex=n.tabIndex),!t.placeholder&&n.placeholder&&(t.placeholder=n.placeholder),null==t.autofocus){var e=L(n.ownerDocument);t.autofocus=e==n||null!=n.getAttribute("autofocus")&&e==document.body}function a(){n.value=l.getValue()}var o;if(n.form&&(mn(n.form,"submit",a),!t.leaveSubmitMethodAlone)){var i=n.form;o=i.submit;try{var r=i.submit=function(){a(),i.submit=o,i.submit(),i.submit=r}}catch(n){}}t.finishInit=function(e){e.save=a,e.getTextArea=function(){return n},e.toTextArea=function(){e.toTextArea=isNaN,a(),n.parentNode.removeChild(e.getWrapperElement()),n.style.display="",n.form&&(gn(n.form,"submit",a),t.leaveSubmitMethodAlone||"function"!=typeof n.form.submit||(n.form.submit=o))}},n.style.display="none";var l=Ir((function(t){return n.parentNode.insertBefore(t,n.nextSibling)}),t);return l},function(n){n.off=gn,n.on=mn,n.wheelEventPixels=Fo,n.Doc=Ai,n.splitLines=An,n.countColumn=Y,n.findColumn=K,n.isWordChar=en,n.Pass=W,n.signal=xn,n.Line=Gt,n.changeEnd=Io,n.scrollbarModel=qa,n.Pos=ot,n.cmpPos=it,n.modes=Nn,n.mimeModes=Rn,n.resolveMode=Hn,n.getMode=Un,n.modeExtensions=Wn,n.extendMode=Xn,n.copyState=qn,n.startState=Kn,n.innerMode=Zn,n.commands=ir,n.keyMap=Zi,n.keyName=Ji,n.isModifierKey=$i,n.lookupKey=Gi,n.normalizeKeyMap=Vi,n.StringStream=Vn,n.SharedTextMarker=Ii,n.TextMarker=Oi,n.LineWidget=Ci,n.e_preventDefault=zn,n.e_stopPropagation=_n,n.e_stop=Cn,n.addClass=B,n.contains=M,n.rmClass=S,n.keyNames=Ui}(Ir),Ir.version="5.65.10",Ir}()},function(n,t,e){!function(n){"use strict";n.defineMode("shell",(function(){var t={};function e(n,e){for(var a=0;a1&&n.eat("$");var e=n.next();return/['"({]/.test(e)?(t.tokens[0]=l(e,"("==e?"quote":"{"==e?"def":"string"),d(n,t)):(/\d/.test(e)||n.eatWhile(/\w/),t.tokens.shift(),"def")};function d(n,t){return(t.tokens[0]||r)(n,t)}return{startState:function(){return{tokens:[]}},token:function(n,t){return d(n,t)},closeBrackets:"()[]{}''\"\"``",lineComment:"#",fold:"brace"}})),n.defineMIME("text/x-sh","shell"),n.defineMIME("application/x-sh","shell")}(e(65))},function(n,t,e){var a=e(203),o=e(3);"string"==typeof a&&(a=[[n.i,a,""]]),n.exports=a.locals||{},n.exports._getContent=function(){return a},n.exports._getCss=function(){return a.toString()},n.exports._insertCss=function(n){return o(a,n)}},function(n,t,e){"use strict";var a=this&&this.__createBinding||(Object.create?function(n,t,e,a){void 0===a&&(a=e);var o=Object.getOwnPropertyDescriptor(t,e);o&&!("get"in o?!t.__esModule:o.writable||o.configurable)||(o={enumerable:!0,get:function(){return t[e]}}),Object.defineProperty(n,a,o)}:function(n,t,e,a){void 0===a&&(a=e),n[a]=t[e]}),o=this&&this.__setModuleDefault||(Object.create?function(n,t){Object.defineProperty(n,"default",{enumerable:!0,value:t})}:function(n,t){n.default=t}),i=this&&this.__importStar||function(n){if(n&&n.__esModule)return n;var t={};if(null!=n)for(var e in n)"default"!==e&&Object.prototype.hasOwnProperty.call(n,e)&&a(t,n,e);return o(t,n),t},r=this&&this.__importDefault||function(n){return n&&n.__esModule?n:{default:n}};Object.defineProperty(t,"__esModule",{value:!0});var l=i(e(0)),s=(i(e(69)),e(9)),c=e(7),d=e(22),p=(e(70),r(e(71))),b=r(e(72)),u=e(234),f=r(e(37));e(238),e(240);t.default=function(n,t){var e=n.state.url,a=(0,d.matchRoutes)(f.default,e).map((function(n){var t=n.route,e=t.component&&t.component.fetch;return e instanceof Function?e():Promise.resolve(null)}));return Promise.all(a).then((function(t){var a=n.state;t.forEach((function(n){Object.assign(a,n)})),n.state=Object.assign({},n.state,a);var o=(0,u.create)(a);return function(){return l.createElement(p.default,null,l.createElement("div",{style:{height:"100%"}},l.createElement(s.Provider,{store:o},l.createElement(c.StaticRouter,{location:e,context:{}},l.createElement(b.default,null)))))}}))}},function(n,t){n.exports=require("react-dom")},function(n,t){n.exports=require("react-hot-loader")},function(n,t,e){"use strict";var a,o=this&&this.__extends||(a=function(n,t){return(a=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(n,t){n.__proto__=t}||function(n,t){for(var e in t)Object.prototype.hasOwnProperty.call(t,e)&&(n[e]=t[e])})(n,t)},function(n,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function e(){this.constructor=n}a(n,t),n.prototype=null===t?Object.create(t):(e.prototype=t.prototype,new e)}),i=this&&this.__createBinding||(Object.create?function(n,t,e,a){void 0===a&&(a=e);var o=Object.getOwnPropertyDescriptor(t,e);o&&!("get"in o?!t.__esModule:o.writable||o.configurable)||(o={enumerable:!0,get:function(){return t[e]}}),Object.defineProperty(n,a,o)}:function(n,t,e,a){void 0===a&&(a=e),n[a]=t[e]}),r=this&&this.__setModuleDefault||(Object.create?function(n,t){Object.defineProperty(n,"default",{enumerable:!0,value:t})}:function(n,t){n.default=t}),l=this&&this.__importStar||function(n){if(n&&n.__esModule)return n;var t={};if(null!=n)for(var e in n)"default"!==e&&Object.prototype.hasOwnProperty.call(n,e)&&i(t,n,e);return r(t,n),t};Object.defineProperty(t,"__esModule",{value:!0});var s=l(e(0)),c=function(n){function t(){return null!==n&&n.apply(this,arguments)||this}return o(t,n),t.prototype.render=function(){return s.createElement("html",null,s.createElement("head",null,s.createElement("title",null,this.props.title),s.createElement("meta",{charSet:"utf-8"}),s.createElement("meta",{name:"viewport",content:"initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"}),s.createElement("meta",{name:"keywords",content:this.props.keywords}),s.createElement("meta",{name:"description",content:this.props.description}),s.createElement("link",{rel:"shortcut icon",href:"/favicon.ico",type:"image/x-icon"}),s.createElement("link",null),s.createElement("link",{href:"https://cdn.jsdelivr.net/npm/codemirror@5.48.4/lib/codemirror.min.css",rel:"stylesheet"}),s.createElement("link",{href:"https://cdn.jsdelivr.net/npm/codemirror@5.48.4/theme/dracula.min.css",rel:"stylesheet"}),s.createElement("script",{src:"https://cdn.jsdelivr.net/npm/codemirror@5.48.4/lib/codemirror.min.js"}),s.createElement("script",{src:"https://cdn.jsdelivr.net/npm/codemirror@5.48.4/mode/nginx/nginx.min.js"})),s.createElement("body",null,s.createElement("div",{id:"app"},this.props.children)))},t}(s.Component);t.default=c},function(n,t,e){"use strict";var a=this&&this.__createBinding||(Object.create?function(n,t,e,a){void 0===a&&(a=e);var o=Object.getOwnPropertyDescriptor(t,e);o&&!("get"in o?!t.__esModule:o.writable||o.configurable)||(o={enumerable:!0,get:function(){return t[e]}}),Object.defineProperty(n,a,o)}:function(n,t,e,a){void 0===a&&(a=e),n[a]=t[e]}),o=this&&this.__setModuleDefault||(Object.create?function(n,t){Object.defineProperty(n,"default",{enumerable:!0,value:t})}:function(n,t){n.default=t}),i=this&&this.__importStar||function(n){if(n&&n.__esModule)return n;var t={};if(null!=n)for(var e in n)"default"!==e&&Object.prototype.hasOwnProperty.call(n,e)&&a(t,n,e);return o(t,n),t},r=this&&this.__importDefault||function(n){return n&&n.__esModule?n:{default:n}};Object.defineProperty(t,"__esModule",{value:!0});var l=i(e(0)),s=e(7),c=e(23),d=e(9),p=e(1),b=r(e(73)),u=r(e(37)),f=e(22),m=i(e(52));e(228),e(230),e(232);t.default=function(){var n=(0,c.bindActionCreators)(m,(0,d.useDispatch)()).changeLocalIp;return(0,l.useEffect)((function(){var t,e,a,o;t=window,e=document,t.hj=t.hj||function(){(t.hj.q=t.hj.q||[]).push(arguments)},t._hjSettings={hjid:2133522,hjsv:6},a=e.getElementsByTagName("head")[0],(o=e.createElement("script")).async=1,o.src="https://static.hotjar.com/c/hotjar-"+t._hjSettings.hjid+".js?sv="+t._hjSettings.hjsv,a.appendChild(o),n()}),[]),l.default.createElement("div",{style:{height:"100%"}},l.default.createElement(p.ConfigProvider,{locale:b.default},l.default.createElement(s.Switch,null,(0,f.renderRoutes)(u.default))))}},function(n,t,e){"use strict";var a=e(15);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var o=a(e(74)).default;t.default=o},function(n,t,e){"use strict";var a=e(15);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var o=a(e(75)),i=a(e(35)),r=a(e(36)),l=a(e(78)),s="${label}不是一个有效的${type}",c={locale:"zh-cn",Pagination:o.default,DatePicker:i.default,TimePicker:r.default,Calendar:l.default,global:{placeholder:"请选择"},Table:{filterTitle:"筛选",filterConfirm:"确定",filterReset:"重置",filterEmptyText:"无筛选项",selectAll:"全选当页",selectInvert:"反选当页",selectNone:"清空所有",selectionAll:"全选所有",sortTitle:"排序",expand:"展开行",collapse:"关闭行",triggerDesc:"点击降序",triggerAsc:"点击升序",cancelSort:"取消排序"},Modal:{okText:"确定",cancelText:"取消",justOkText:"知道了"},Popconfirm:{cancelText:"取消",okText:"确定"},Transfer:{searchPlaceholder:"请输入搜索内容",itemUnit:"项",itemsUnit:"项",remove:"删除",selectCurrent:"全选当页",removeCurrent:"删除当页",selectAll:"全选所有",removeAll:"删除全部",selectInvert:"反选当页"},Upload:{uploading:"文件上传中",removeFile:"删除文件",uploadError:"上传错误",previewFile:"预览文件",downloadFile:"下载文件"},Empty:{description:"暂无数据"},Icon:{icon:"图标"},Text:{edit:"编辑",copy:"复制",copied:"复制成功",expand:"展开"},PageHeader:{back:"返回"},Form:{optional:"(可选)",defaultValidateMessages:{default:"字段验证错误${label}",required:"请输入${label}",enum:"${label}必须是其中一个[${enum}]",whitespace:"${label}不能为空字符",date:{format:"${label}日期格式无效",parse:"${label}不能转换为日期",invalid:"${label}是一个无效日期"},types:{string:s,method:s,array:s,object:s,number:s,date:s,boolean:s,integer:s,float:s,regexp:s,email:s,url:s,hex:s},string:{len:"${label}须为${len}个字符",min:"${label}最少${min}个字符",max:"${label}最多${max}个字符",range:"${label}须在${min}-${max}字符之间"},number:{len:"${label}必须等于${len}",min:"${label}最小值为${min}",max:"${label}最大值为${max}",range:"${label}须在${min}-${max}之间"},array:{len:"须为${len}个${label}",min:"最少${min}个${label}",max:"最多${max}个${label}",range:"${label}数量须在${min}-${max}之间"},pattern:{mismatch:"${label}与模式不匹配${pattern}"}}},Image:{preview:"预览"}};t.default=c},function(n,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;t.default={items_per_page:"条/页",jump_to:"跳至",jump_to_confirm:"确定",page:"页",prev_page:"上一页",next_page:"下一页",prev_5:"向前 5 页",next_5:"向后 5 页",prev_3:"向前 3 页",next_3:"向后 3 页",page_size:"页码"}},function(n,t){function e(){return n.exports=e=Object.assign?Object.assign.bind():function(n){for(var t=1;t=n.length?(this._t=void 0,o(1)):o(0,"keys"==t?e:"values"==t?n[e]:[e,n[e]])}),"values"),i.Arguments=i.Array,a("keys"),a("values"),a("entries")},function(n,t){n.exports=function(){}},function(n,t){n.exports=function(n,t){return{value:t,done:!!n}}},function(n,t,e){var a=e(40);n.exports=Object("z").propertyIsEnumerable(0)?Object:function(n){return"String"==a(n)?n.split(""):Object(n)}},function(n,t,e){var a=e(11),o=e(10),i=e(92),r=e(14),l=e(18),s=function(n,t,e){var c,d,p,b=n&s.F,u=n&s.G,f=n&s.S,m=n&s.P,h=n&s.B,g=n&s.W,x=u?o:o[t]||(o[t]={}),w=x.prototype,k=u?a:f?a[t]:(a[t]||{}).prototype;for(c in u&&(e=t),e)(d=!b&&k&&void 0!==k[c])&&l(x,c)||(p=d?k[c]:e[c],x[c]=u&&"function"!=typeof k[c]?e[c]:h&&d?i(p,a):g&&k[c]==p?function(n){var t=function(t,e,a){if(this instanceof n){switch(arguments.length){case 0:return new n;case 1:return new n(t);case 2:return new n(t,e)}return new n(t,e,a)}return n.apply(this,arguments)};return t.prototype=n.prototype,t}(p):m&&"function"==typeof p?i(Function.call,p):p,m&&((x.virtual||(x.virtual={}))[c]=p,n&s.R&&w&&!w[c]&&r(w,c,p)))};s.F=1,s.G=2,s.S=4,s.P=8,s.B=16,s.W=32,s.U=64,s.R=128,n.exports=s},function(n,t,e){var a=e(93);n.exports=function(n,t,e){if(a(n),void 0===t)return n;switch(e){case 1:return function(e){return n.call(t,e)};case 2:return function(e,a){return n.call(t,e,a)};case 3:return function(e,a,o){return n.call(t,e,a,o)}}return function(){return n.apply(t,arguments)}}},function(n,t){n.exports=function(n){if("function"!=typeof n)throw TypeError(n+" is not a function!");return n}},function(n,t,e){n.exports=!e(17)&&!e(43)((function(){return 7!=Object.defineProperty(e(44)("div"),"a",{get:function(){return 7}}).a}))},function(n,t,e){var a=e(27);n.exports=function(n,t){if(!a(n))return n;var e,o;if(t&&"function"==typeof(e=n.toString)&&!a(o=e.call(n)))return o;if("function"==typeof(e=n.valueOf)&&!a(o=e.call(n)))return o;if(!t&&"function"==typeof(e=n.toString)&&!a(o=e.call(n)))return o;throw TypeError("Can't convert object to primitive value")}},function(n,t,e){n.exports=e(14)},function(n,t,e){"use strict";var a=e(98),o=e(45),i=e(49),r={};e(14)(r,e(8)("iterator"),(function(){return this})),n.exports=function(n,t,e){n.prototype=a(r,{next:o(1,e)}),i(n,t+" Iterator")}},function(n,t,e){var a=e(16),o=e(99),i=e(48),r=e(29)("IE_PROTO"),l=function(){},s=function(){var n,t=e(44)("iframe"),a=i.length;for(t.style.display="none",e(105).appendChild(t),t.src="javascript:",(n=t.contentWindow.document).open(),n.write("