UNPKG

whistle.mock-plugins

Version:

Whistle 插件,用于快速创建 API 模拟数据

876 lines (768 loc) 28.7 kB
const express = require('express'); const path = require('path'); const fs = require('fs-extra'); const Mock = require('mockjs'); const storage = require('./storage'); // 数据存储目录 const DATA_DIR = path.join(process.env.WHISTLE_PLUGIN_DATA_DIR || storage.DATA_DIR); // 功能和接口配置文件 const FEATURES_FILE = path.join(DATA_DIR, 'features.json'); const INTERFACES_FILE = path.join(DATA_DIR, 'interfaces.json'); // 日志目录 const LOG_DIR = path.join(DATA_DIR, 'logs'); try { fs.ensureDirSync(LOG_DIR); console.log('Log directory created:', LOG_DIR); } catch (err) { console.error('Failed to create log directory:', err.message); } const LOG_FILE = path.join(LOG_DIR, 'plugin.log'); // 记录日志 const logMessage = (message) => { try { // 确保日志目录存在 if (!fs.existsSync(LOG_DIR)) { fs.ensureDirSync(LOG_DIR); } const timestamp = new Date().toISOString(); const logEntry = `[${timestamp}] ${message}\n`; fs.appendFileSync(LOG_FILE, logEntry); // 同时将日志输出到控制台,方便调试 console.log(`[mock-plugin] ${message}`); } catch (err) { console.error('Error writing to log file:', err.message); } }; // 记录日志到 logs.json 文件 const addToJsonLog = (logData) => { try { // 验证参数有效性 if (!logData || typeof logData !== 'object') { console.error('无效的日志数据'); return; } const logsFile = path.join(DATA_DIR, 'logs.json'); // 确保日志文件存在 if (!fs.existsSync(logsFile)) { fs.writeJsonSync(logsFile, { logs: [] }, { spaces: 2 }); } // 读取现有日志 let logsData; try { logsData = fs.readJsonSync(logsFile); if (!logsData.logs) { logsData = { logs: [] }; } } catch (err) { console.error('读取日志文件错误:', err); logsData = { logs: [] }; } // 标准化日志数据,确保同时包含type和eventType字段 const standardizedLogData = { ...logData, // 确保type和eventType都存在,前端可能使用eventType进行过滤 eventType: logData.eventType || 'unknown', type: logData.eventType || 'unknown', // 复制eventType到type字段 // 如果缺少必要字段,提供默认值,防止前端过滤出错 url: logData.url || '', method: logData.method || '', message: logData.message || '', status: logData.status || '', pattern: logData.pattern || '' }; // 添加时间戳和ID const newLog = { ...standardizedLogData, id: Date.now().toString(), timestamp: new Date().toISOString() }; // 添加新日志 logsData.logs.unshift(newLog); // 限制日志数量,最多保留5000条 (减少内存使用) if (logsData.logs.length > 5000) { logsData.logs = logsData.logs.slice(0, 5000); } fs.writeJsonSync(logsFile, logsData, { spaces: 2 }); } catch (err) { console.error('记录日志到JSON文件失败:', err); } }; // 确保数据目录存在 try { fs.ensureDirSync(DATA_DIR); logMessage('数据目录已确认: ' + DATA_DIR); } catch (err) { console.error('创建数据目录失败:', err.message); } // 确保配置文件存在 if (!fs.existsSync(FEATURES_FILE)) { try { fs.writeJsonSync(FEATURES_FILE, { features: [] }, { spaces: 2 }); logMessage('创建了功能配置文件: ' + FEATURES_FILE); } catch (err) { console.error('创建功能配置文件失败:', err.message); } } else { // 验证文件结构 try { const fileData = fs.readJsonSync(FEATURES_FILE); if (!fileData || !fileData.features) { // 如果文件存在但结构无效,重新初始化 fs.writeJsonSync(FEATURES_FILE, { features: [] }, { spaces: 2 }); logMessage('修复了损坏的features文件结构'); } } catch (e) { // JSON解析错误时重新初始化文件 try { fs.writeJsonSync(FEATURES_FILE, { features: [] }, { spaces: 2 }); logMessage('修复了无法解析的features文件'); } catch (err) { console.error('重置功能配置文件失败:', err.message); } } } if (!fs.existsSync(INTERFACES_FILE)) { try { fs.writeJsonSync(INTERFACES_FILE, { interfaces: [] }, { spaces: 2 }); logMessage('创建了接口配置文件: ' + INTERFACES_FILE); } catch (err) { console.error('创建接口配置文件失败:', err.message); } } else { // 验证文件结构 try { const fileData = fs.readJsonSync(INTERFACES_FILE); if (!fileData || !fileData.interfaces) { // 如果文件存在但结构无效,重新初始化 fs.writeJsonSync(INTERFACES_FILE, { interfaces: [] }, { spaces: 2 }); logMessage('修复了损坏的interfaces文件结构'); } } catch (e) { // JSON解析错误时重新初始化文件 try { fs.writeJsonSync(INTERFACES_FILE, { interfaces: [] }, { spaces: 2 }); logMessage('修复了无法解析的interfaces文件'); } catch (err) { console.error('重置接口配置文件失败:', err.message); } } } // API前缀 const RULE_VALUE_HEADER = 'x-whistle-rule-value'; const MOCK_PREFIX = 'mock://'; // 创建 Express 应用 const app = express(); // 解析请求体 app.use(express.json({limit: '100mb'})); app.use(express.urlencoded({ extended: true, limit: '100mb'})); // 加载功能和接口配置 const loadConfigurations = () => { try { let featuresData = { features: [] }; let interfacesData = { interfaces: [] }; try { if (fs.existsSync(FEATURES_FILE)) { featuresData = fs.readJsonSync(FEATURES_FILE); // 确保featuresData和features字段都存在,并且features是数组 if (!featuresData || !featuresData.features || !Array.isArray(featuresData.features)) { logMessage('功能配置数据结构不正确,重新初始化'); featuresData = { features: [] }; } } else { logMessage('功能配置文件不存在,使用空数组'); } } catch (err) { logMessage('加载功能配置失败: ' + err.message); featuresData = { features: [] }; } try { if (fs.existsSync(INTERFACES_FILE)) { interfacesData = fs.readJsonSync(INTERFACES_FILE); // 确保interfacesData和interfaces字段都存在,并且interfaces是数组 if (!interfacesData || !interfacesData.interfaces || !Array.isArray(interfacesData.interfaces)) { logMessage('接口配置数据结构不正确,重新初始化'); interfacesData = { interfaces: [] }; } } else { logMessage('接口配置文件不存在,使用空数组'); } } catch (err) { logMessage('加载接口配置失败: ' + err.message); interfacesData = { interfaces: [] }; } return { features: featuresData.features || [], interfaces: interfacesData.interfaces || [] }; } catch (err) { logMessage('加载配置失败: ' + err.message); return { features: [], interfaces: [] }; } }; // 匹配URL是否符合模式 const isUrlMatch = (url, pattern) => { if (!pattern) return false; try { // 精确匹配 if (pattern === url) { logMessage(`URL精确匹配成功: ${url} === ${pattern}`); return true; } // 通配符匹配 if (pattern.includes('*')) { // 需要转换为正则表达式,处理特殊字符 const regexPattern = pattern .replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&') // 转义特殊字符 .replace(/\*/g, '.*'); // 将 * 替换为 .* const regex = new RegExp('^' + regexPattern + '$'); const result = regex.test(url); logMessage(`URL通配符匹配${result ? '成功' : '失败'}: ${url} ${result ? '匹配' : '不匹配'} 模式 ${pattern}`); return result; } // 正则表达式匹配 if (pattern.startsWith('/') && pattern.endsWith('/')) { const regexStr = pattern.slice(1, -1); const regex = new RegExp(regexStr); const result = regex.test(url); logMessage(`URL正则匹配${result ? '成功' : '失败'}: ${url} ${result ? '匹配' : '不匹配'} 模式 ${pattern}`); return result; } } catch (e) { logMessage('正则表达式匹配错误: ' + e.message); } logMessage(`URL不匹配任何模式: ${url} != ${pattern}`); return false; }; // 解析原始URL const parseOriginalUrl = (req) => { try { logMessage(`开始解析原始URL,请求URL: ${req.url}`); logMessage(`请求方法: ${req.method}`); // 首先尝试从 originalReq 获取 if (req.originalReq) { // 优先使用 realUrl,因为它是真实的 URL if (req.originalReq.realUrl) { logMessage(`从req.originalReq.realUrl获取原始URL: ${req.originalReq.realUrl}`); return req.originalReq.realUrl; } // 如果 realUrl 不存在,则使用 url if (req.originalReq.url) { logMessage(`从req.originalReq.url获取原始URL: ${req.originalReq.url}`); return req.originalReq.url; } } // 作为备选,继续尝试从请求头获取 // 尝试从 x-whistle-real-url 头获取 const whistleRealUrl = req.headers['x-whistle-real-url']; if (whistleRealUrl) { logMessage(`从x-whistle-real-url头获取原始URL: ${whistleRealUrl}`); return whistleRealUrl; } // 尝试从 x-forwarded-url 头获取 const forwardedUrl = req.headers['x-forwarded-url']; if (forwardedUrl) { logMessage(`从x-forwarded-url头获取原始URL: ${forwardedUrl}`); return forwardedUrl; } // 从规则值中获取信息 const ruleValue = req.headers['x-whistle-rule-value']; if (ruleValue) { logMessage(`从规则值获取信息: ${ruleValue}`); // 尝试从规则值中提取完整URL try { const match = /^(?:whistle\.mock-plugin:\/\/)?(.*)$/.exec(ruleValue); if (match && match[1]) { // 如果规则值包含URL,则直接使用 if (match[1].startsWith('http')) { logMessage(`从规则值提取到完整URL: ${match[1]}`); return match[1]; } } } catch (e) { logMessage(`解析规则值出错: ${e.message}`); } } else { logMessage(`请求头中没有x-whistle-rule-value`); } // 如果没有找到原始URL,使用请求URL logMessage(`无法获取原始URL,使用请求URL: ${req.url}`); return req.url; } catch (err) { logMessage(`解析原始URL失败: ${err.message}`); return req.url; } }; // 获取所有请求头信息并记录 const logAllHeaders = (req) => { try { logMessage('===== 请求头信息 ====='); Object.keys(req.headers).forEach(headerName => { logMessage(` ${headerName}: ${req.headers[headerName]}`); }); logMessage('====================='); } catch (err) { logMessage(`记录请求头失败: ${err.message}`); } }; // 添加中间件记录所有请求 app.use((req, res, next) => { try { logMessage(`----- 新请求开始 -----`); logMessage(`收到请求: ${req.method} ${req.url}`); logAllHeaders(req); next(); } catch (err) { logMessage(`请求日志中间件错误: ${err.message}`); next(); } }); // 处理请求 app.use(async (req, res, next) => { try { logMessage(`----- 新请求开始 -----`); logMessage(`收到请求: ${req.method} ${req.url}`); // 记录请求头 logAllHeaders(req); // 获取规则值 const ruleValue = req.headers['x-whistle-rule-value']; logMessage(`规则值: ${ruleValue || '无'}`); // 使用规则管理器处理请求 let result; try { // 使用当前 require 的 ruleManager const ruleManagerModule = require('./ruleManager'); // 传递next参数,确保未匹配的请求能够继续 result = await ruleManagerModule.handleRequest(req, res, ruleValue, next); } catch (err) { logMessage(`规则管理器处理请求失败: ${err.message}`); result = { handled: false }; } if (result && result.handled) { logMessage(`规则管理器成功处理了请求`); return; // 请求已处理,无需继续 } // 如果规则管理器未处理,使用旧的处理方式 logMessage(`规则管理器未处理请求,使用旧的处理方式`); handleLegacyRequest(req, res, next); } catch (err) { logMessage(`请求处理过程中发生错误: ${err.message}`); console.error('请求处理错误:', err); // 返回500错误 res.status(500).json({ code: 500, message: '内部服务器错误: ' + err.message, data: null }); } }); // 旧的请求处理方式,保持向后兼容 const handleLegacyRequest = (req, res, next) => { const http = require('http'); const https = require('https'); const { Readable } = require('stream'); // 获取原始完整URL的所有可能来源 let originalUrl = null; // 首先尝试从 originalReq 获取 (Whistle规范推荐方式) if (req.originalReq) { if (req.originalReq.realUrl) { originalUrl = req.originalReq.realUrl; logMessage(`从req.originalReq.realUrl获取原始URL: ${originalUrl}`); } else if (req.originalReq.url) { originalUrl = req.originalReq.url; logMessage(`从req.originalReq.url获取原始URL: ${originalUrl}`); } } // 如果 originalReq 没有提供有效URL,尝试从请求头获取 if (!originalUrl || !originalUrl.startsWith('http')) { const whistleRealUrl = req.headers['x-whistle-real-url']; if (whistleRealUrl && whistleRealUrl.startsWith('http')) { originalUrl = whistleRealUrl; logMessage(`从x-whistle-real-url头获取原始URL: ${originalUrl}`); } else { const forwardedUrl = req.headers['x-forwarded-url']; if (forwardedUrl && forwardedUrl.startsWith('http')) { originalUrl = forwardedUrl; logMessage(`从x-forwarded-url头获取原始URL: ${originalUrl}`); } } } // 如果仍然没有找到原始URL,尝试从规则值获取 if (!originalUrl || !originalUrl.startsWith('http')) { const ruleValue = req.headers['x-whistle-rule-value']; if (ruleValue) { try { const match = /^(?:whistle\.mock-plugin:\/\/)?(.*)$/.exec(ruleValue); if (match && match[1] && match[1].startsWith('http')) { originalUrl = match[1]; logMessage(`从规则值提取到完整URL: ${originalUrl}`); } } catch (e) { logMessage(`解析规则值出错: ${e.message}`); } } } // 如果没有找到有效的原始URL,则无法继续 if (!originalUrl || !originalUrl.startsWith('http')) { logMessage(`没有找到有效的原始URL,无法转发请求`); res.statusCode = 400; res.end(JSON.stringify({ code: 400, message: '无法确定原始请求URL', data: null })); return; } try { // 解析URL const urlObj = new URL(originalUrl); const isHttps = urlObj.protocol === 'https:'; // 准备请求选项 const options = { protocol: urlObj.protocol, hostname: urlObj.hostname, port: urlObj.port || (isHttps ? 443 : 80), path: urlObj.pathname + urlObj.search, method: req.method, headers: {...req.headers} }; // 删除可能导致问题的请求头 delete options.headers.host; delete options.headers['x-whistle-real-url']; delete options.headers['x-whistle-rule-value']; delete options.headers['x-forwarded-url']; // 设置正确的Host头 options.headers.host = urlObj.host; logMessage(`准备转发请求到: ${originalUrl}, 方法: ${req.method}`); // 选择http或https模块 const httpModule = isHttps ? https : http; // 创建请求并处理响应 const proxyReq = httpModule.request(options, (proxyRes) => { logMessage(`收到来自 ${originalUrl} 的响应,状态码: ${proxyRes.statusCode}`); // 复制响应头 Object.keys(proxyRes.headers).forEach(key => { res.setHeader(key, proxyRes.headers[key]); }); // 设置状态码 res.statusCode = proxyRes.statusCode; // 传输响应体 proxyRes.pipe(res); }); // 错误处理 proxyReq.on('error', (error) => { logMessage(`转发请求到 ${originalUrl} 时出错: ${error.message}`); // 如果响应尚未发送,返回错误信息 if (!res.headersSent) { res.statusCode = 502; res.end(JSON.stringify({ code: 502, message: `转发请求到原始地址失败: ${error.message}`, data: null })); } else { // 如果已经发送了一部分响应,尝试结束响应 try { res.end(); } catch (e) { logMessage(`尝试结束已经部分发送的响应时出错: ${e.message}`); } } }); // 处理超时 proxyReq.setTimeout(30000, () => { logMessage(`转发请求到 ${originalUrl} 超时`); proxyReq.destroy(new Error('请求超时')); }); // 转发请求体 if (req.method !== 'GET' && req.method !== 'HEAD') { // 如果请求有体,则转发 if (req.readable) { req.pipe(proxyReq); } else { // 如果请求体已被读取,尝试重建并发送 if (req.body) { const bodyStr = typeof req.body === 'string' ? req.body : JSON.stringify(req.body); proxyReq.write(bodyStr); proxyReq.end(); } else { proxyReq.end(); } } } else { // 对于GET和HEAD请求,直接结束请求 proxyReq.end(); } } catch (error) { logMessage(`准备转发请求时发生错误: ${error.message}`); res.statusCode = 500; res.end(JSON.stringify({ code: 500, message: `内部服务器错误: ${error.message}`, data: null })); } }; // 处理响应 const handleResponse = (interfaceItem, req, res) => { logMessage(`匹配到接口: ${interfaceItem.name}, ID: ${interfaceItem.id}`); logMessage(`URL匹配规则: ${interfaceItem.urlPattern}`); // 记录请求详情 logMessage(`请求方法: ${req.method}`); logMessage(`原始URL: ${req.url}`); // 设置响应状态码 const statusCode = parseInt(interfaceItem.httpStatus, 10) || 200; // 模拟延迟 const delay = interfaceItem.responseDelay || 0; if (delay > 0) { logMessage(`模拟延迟处理: ${delay}ms`); setTimeout(() => processResponse(), delay); } else { processResponse(); } function processResponse() { // 根据代理类型处理 switch (interfaceItem.proxyType) { case 'response': try { // 设置响应头 if (interfaceItem.contentType) { res.setHeader('Content-Type', interfaceItem.contentType); } else { res.setHeader('Content-Type', 'application/json; charset=utf-8'); } // 添加调试头信息 res.setHeader('X-Mock-Plugin', 'whistle.mock-plugin'); res.setHeader('X-Mock-Interface', interfaceItem.name); res.setHeader('X-Mock-Interface-Id', interfaceItem.id); res.setHeader('X-Mock-Feature-Id', interfaceItem.featureId); // 尝试解析JSON let responseBody = interfaceItem.responseContent; logMessage(`响应内容(前50字符): ${responseBody.substring(0, 50)}...`); try { // 检查内容类型,如果是JSON则应用Mock.js if (interfaceItem.contentType && interfaceItem.contentType.includes('json')) { // 如果是JSON字符串,先解析为对象 const jsonData = JSON.parse(responseBody); logMessage(`成功解析为JSON对象`); // 使用Mock.js处理模板 const mockedData = Mock.mock(jsonData); logMessage(`Mock.js处理完成`); // 返回处理后的JSON res.status(statusCode).json(mockedData); logMessage(`响应JSON数据(状态码: ${statusCode}): ${JSON.stringify(mockedData).substr(0, 200)}...`); } else { // 不是JSON,直接返回文本 res.status(statusCode).send(responseBody); logMessage(`响应文本数据(状态码: ${statusCode}): ${responseBody.substr(0, 200)}...`); } } catch (e) { // JSON解析出错,直接返回原始内容 logMessage(`JSON解析错误,作为文本返回: ${e.message}`); res.status(statusCode).send(responseBody); } } catch (err) { logMessage('处理响应内容错误: ' + err.message); res.status(500).json({ code: 500, message: '处理响应内容错误: ' + err.message, data: null }); } break; case 'redirect': // 处理redirect - 完全重定向 const redirectUrl = interfaceItem.targetUrl || ''; logMessage(`重定向处理: ${req.url} -> ${redirectUrl}`); // 这里不需要实际执行重定向,因为规则服务器已经返回了whistle规则 // 仅记录日志,并返回信息性消息 res.status(200).json({ code: 302, message: '请求已被重定向到: ' + redirectUrl, data: { originalUrl: req.url, targetUrl: redirectUrl, mode: 'redirect' } }); // 记录重定向日志 addToJsonLog({ eventType: 'redirect', url: req.url, method: req.method, targetUrl: redirectUrl, message: `请求被重定向: ${req.method} ${req.url} -> ${redirectUrl}`, pattern: interfaceItem.urlPattern }); break; case 'url_redirect': // 处理url_redirect - URL重定向,保留查询参数 const baseUrl = interfaceItem.targetUrl || ''; // 提取原始URL的查询参数,实际重定向在规则服务器中处理 const reqUrl = req.url || ''; let finalRedirectUrl = baseUrl; if (req.url.includes('?')) { const queryStr = req.url.split('?')[1]; finalRedirectUrl = baseUrl.includes('?') ? `${baseUrl}&${queryStr}` : `${baseUrl}?${queryStr}`; } logMessage(`URL重定向处理: ${req.url} -> ${finalRedirectUrl}`); // 返回信息性消息 res.status(200).json({ code: 302, message: '请求已被URL重定向到: ' + finalRedirectUrl, data: { originalUrl: req.url, targetUrl: finalRedirectUrl, mode: 'url_redirect' } }); // 记录URL重定向日志 addToJsonLog({ eventType: 'url_redirect', url: req.url, method: req.method, targetUrl: finalRedirectUrl, message: `请求被URL重定向: ${req.method} ${req.url} -> ${finalRedirectUrl}`, pattern: interfaceItem.urlPattern }); break; case 'url': // URL重定向 - 实际场景中这需要代理到目标URL logMessage(`URL重定向: ${req.url} -> ${interfaceItem.targetUrl}`); res.status(statusCode).json({ code: 302, message: '需要重定向到: ' + interfaceItem.targetUrl, data: { originalUrl: req.url, targetUrl: interfaceItem.targetUrl } }); break; case 'file': try { // 文件代理 - 读取文件内容并返回 const filePath = interfaceItem.filePath; if (!filePath) { logMessage('文件路径未指定'); return res.status(400).json({ code: 400, message: '文件路径未指定', data: null }); } // 构建绝对路径 - 使用相对插件根目录的路径 const absolutePath = path.resolve(process.cwd(), filePath); logMessage(`读取文件: ${absolutePath}`); if (!fs.existsSync(absolutePath)) { logMessage(`文件不存在: ${absolutePath}`); return res.status(404).json({ code: 404, message: '文件不存在: ' + filePath, data: null }); } const content = fs.readFileSync(absolutePath, 'utf8'); // 尝试解析JSON try { const jsonData = JSON.parse(content); const mockedData = Mock.mock(jsonData); // 设置响应头 res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('X-Mock-Plugin', 'whistle.mock-plugin'); res.setHeader('X-Mock-Interface', interfaceItem.name); res.status(statusCode).json(mockedData); logMessage(`响应文件内JSON数据(状态码: ${statusCode}): ${JSON.stringify(mockedData).substr(0, 200)}...`); } catch (e) { // 不是JSON,直接返回文件内容 res.status(statusCode).send(content); logMessage(`响应文件内文本数据(状态码: ${statusCode}): ${content.substr(0, 200)}...`); } } catch (err) { logMessage('读取文件错误: ' + err.message); res.status(500).json({ code: 500, message: '读取文件错误: ' + err.message, data: null }); } break; default: logMessage(`不支持的代理类型: ${interfaceItem.proxyType}`); res.status(400).json({ code: 400, message: '不支持的代理类型: ' + interfaceItem.proxyType, data: null }); } } // 结束 processResponse 函数 }; // 启动服务器 const server = app.listen(0); // 导出启动服务器的函数 module.exports = function startServer(server, options) { // 确保之前的日志存在 try { logMessage('------------------------------'); logMessage('启动 whistle.mock-plugin 服务器...'); logMessage('数据目录: ' + DATA_DIR); logMessage('日志文件: ' + LOG_FILE); // 加载并初始化规则管理器 try { debugger const ruleManager = require('./ruleManager'); // 初始化数据管理器 const dataManager = require('./dataManager'); dataManager.init({ baseDir: DATA_DIR, log: logMessage }); ruleManager.init({ server: server, config: { baseDir: DATA_DIR }, dataManager: dataManager, log: logMessage // 传递日志函数 }); logMessage('规则管理器初始化完成'); } catch (err) { logMessage('规则管理器初始化失败: ' + err.message); console.error('规则管理器初始化失败:', err); } // 输出插件版本 try { const packagePath = path.resolve(__dirname, '../package.json'); if (fs.existsSync(packagePath)) { const packageJson = require(packagePath); logMessage(`插件版本: ${packageJson.name}@${packageJson.version}`); } } catch (e) { logMessage('获取插件版本失败: ' + e.message); } // 输出Whistle信息 if (options && options.storage) { logMessage(`Whistle存储目录: ${JSON.stringify(options.storage)}`); } if (options && options.type) { logMessage(`Whistle插件类型: ${options.type}`); } } catch (e) { console.error('记录启动日志失败:', e); } // 检查和初始化配置文件 if (!fs.existsSync(FEATURES_FILE) || !fs.existsSync(INTERFACES_FILE)) { logMessage('初始化配置文件...'); if (!fs.existsSync(FEATURES_FILE)) { fs.writeJsonSync(FEATURES_FILE, { features: [] }, { spaces: 2 }); logMessage('创建了features.json文件'); } if (!fs.existsSync(INTERFACES_FILE)) { fs.writeJsonSync(INTERFACES_FILE, { interfaces: [] }, { spaces: 2 }); logMessage('创建了interfaces.json文件'); } } // 记录whistle传入的options logMessage('Whistle插件选项: ' + JSON.stringify(options)); server.on('request', app); logMessage('whistle.mock-plugin 服务器已启动'); };