UNPKG

whistle.mock-plugins

Version:

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

1,135 lines (981 loc) 40 kB
const fs = require('fs'); const fse = require('fs-extra'); const path = require('path'); const url = require('url'); const Mock = require('mockjs'); const storage = require('./storage'); const SETTINGS_FILE = path.join(storage.DATA_DIR, 'settings.json'); const getSettings = () => { try { if (!fse.existsSync(SETTINGS_FILE)) return { interfaceParamMatcherEnabled: true, responseParamMatcherEnabled: true }; const s = fse.readJsonSync(SETTINGS_FILE); return { interfaceParamMatcherEnabled: s.interfaceParamMatcherEnabled !== false, responseParamMatcherEnabled: s.responseParamMatcherEnabled !== false, }; } catch (e) { return { interfaceParamMatcherEnabled: true, responseParamMatcherEnabled: true }; } }; // 获取嵌套对象属性值的工具函数 const getNestedValue = (obj, path) => { return path.split('.').reduce((current, key) => { return current && current[key] !== undefined ? current[key] : undefined; }, obj); }; // 参数匹配功能 const isParamMatch = (req, paramMatchers, log) => { if (!paramMatchers || !Array.isArray(paramMatchers) || paramMatchers.length === 0) { return true; // 如果没有参数匹配规则,视为匹配成功 } try { // 合并所有可能的参数来源 let requestParams = {}; // URL查询参数 if (req.query) { requestParams = { ...requestParams, ...req.query }; } // POST请求体参数 if (req.body && typeof req.body === 'object') { requestParams = { ...requestParams, ...req.body }; } // URL路径参数(如果有的话) if (req.params) { requestParams = { ...requestParams, ...req.params }; } if (log) { log(`参数匹配检查 - 请求参数: ${JSON.stringify(requestParams)}`); log(`参数匹配检查 - 匹配规则: ${JSON.stringify(paramMatchers)}`); } // 检查每个匹配规则 for (const matcher of paramMatchers) { const { paramPath, paramValue, matchType = 'exact' } = matcher; if (!paramPath) { continue; // 跳过无效的匹配规则 } // 获取实际参数值 const actualValue = getNestedValue(requestParams, paramPath); if (log) { log(`参数匹配检查 - 路径: ${paramPath}, 期望值: ${paramValue}, 实际值: ${actualValue}, 匹配类型: ${matchType}`); } // 如果参数不存在,视为不匹配 if (actualValue === undefined || actualValue === null) { if (log) { log(`参数匹配失败 - 参数 ${paramPath} 不存在`); } return false; } // 根据匹配类型进行比较 let isMatch = false; const actualStr = String(actualValue); const expectedStr = String(paramValue); switch (matchType) { case 'exact': isMatch = actualStr === expectedStr; break; case 'contains': isMatch = actualStr.includes(expectedStr); break; case 'regex': try { const regex = new RegExp(expectedStr); isMatch = regex.test(actualStr); } catch (e) { if (log) { log(`参数匹配失败 - 正则表达式无效: ${expectedStr}`); } return false; } break; default: isMatch = actualStr === expectedStr; } if (!isMatch) { if (log) { log(`参数匹配失败 - ${paramPath}: ${actualStr} 不匹配 ${expectedStr} (${matchType})`); } return false; } } if (log) { log('参数匹配成功 - 所有规则都匹配'); } return true; } catch (error) { if (log) { log(`参数匹配检查出错: ${error.message}`); } return false; } }; /** * 规则管理模块,负责处理 Whistle 规则和代理请求 */ const ruleManager = { /** * 初始化规则管理器 * @param {object} options 选项对象 * @param {object} options.server Whistle 服务器对象 * @param {object} options.config Whistle 配置对象 * @param {object} options.dataManager 数据管理器对象 */ init(options) { this.server = options.server; this.config = options.config; this.dataManager = options.dataManager; this.baseDir = options.config.baseDir; // 设置日志函数 this.log = options.log || console.log; this.log('规则管理器初始化完成, baseDir: ' + this.baseDir); // 确保 mock 数据目录存在 this.mockDataDir = path.join(this.baseDir, 'mock-data'); if (!fs.existsSync(this.mockDataDir)) { fs.mkdirSync(this.mockDataDir, { recursive: true }); this.log('创建mock数据目录: ' + this.mockDataDir); } }, /** * 处理代理请求 * @param {object} req 请求对象 * @param {object} res 响应对象 * @param {string} ruleValue 规则值 * @param {function} next 下一个中间件 * @returns {Promise<object>} 处理结果 */ async handleRequest(req, res, ruleValue, next) { // 只记录请求开始和规则值 this.log(`[规则处理器] 收到请求: ${req.method} ${req.url}, 规则值: ${ruleValue || '无'}`); // 获取所有启用的接口配置 const enabledInterfaces = await this.dataManager.getEnabledInterfaces(); // 只记录接口数量,不记录详情 this.log(`[规则处理器] 启用的接口数量: ${enabledInterfaces ? enabledInterfaces.length : 0}`); if (!enabledInterfaces || enabledInterfaces.length === 0) { this.log('[规则处理器] 没有启用的接口可用,透传请求'); return { handled: false }; } // 解析请求路径 const parsedUrl = url.parse(req.url); const requestPath = parsedUrl.pathname; // 查找匹配的接口 const matchedInterface = this.findMatchingInterface(enabledInterfaces, requestPath, req.method, req); if (!matchedInterface) { this.log(`[规则处理器] 未找到匹配的接口,请求将透传: ${req.method} ${requestPath}`); // 从请求头获取原始完整URL const originalUrl = req.originalReq?.url || req.originalReq?.realUrl; // 如果存在原始URL,确保放回请求头,以便后续处理程序使用 if (originalUrl && originalUrl.startsWith('http')) { this.log(`[规则处理器] 保存原始URL到请求头,确保透传: ${originalUrl}`); req.headers['x-whistle-real-url'] = originalUrl; } // 标记为未处理,将由下一个中间件处理 return { handled: false }; } this.log(`[规则处理器] 找到匹配的接口: ${matchedInterface.name}, 代理类型: ${matchedInterface.proxyType}`); // 根据接口类型处理请求 this.log(`[规则处理器] 开始处理请求...`); const result = await this.processRequest(matchedInterface, req, res); this.log(`[规则处理器] 请求处理完成,状态: ${result.error ? '失败' : '成功'}`); return { handled: true, result, matchedInterface: matchedInterface // 返回匹配到的接口信息 }; }, /** * 根据请求路径和方法查找匹配的接口 * @param {Array} interfaces 接口列表 * @param {string} requestPath 请求路径 * @param {string} method 请求方法 * @param {object} req 完整的请求对象,用于参数匹配 * @returns {object|null} 匹配的接口对象,如果没有匹配则返回 null */ findMatchingInterface(interfaces, requestPath, method, req) { // 简化日志,只记录正在进行匹配的操作 this.log(`[规则处理器] 尝试匹配接口,路径: ${requestPath}, 方法: ${method}`); // 确保interfaces是一个数组 if (!Array.isArray(interfaces)) { this.log(`[规则处理器] 错误: interfaces不是一个数组`); return null; } // 防止空路径 if (!requestPath) { this.log(`[规则处理器] 错误: 请求路径为空`); return null; } // 收集所有URL匹配的接口 const urlMatchedInterfaces = []; // 先收集所有URL匹配的接口(根据proxyType采用不同匹配策略) for (const intf of interfaces) { if (!intf || !intf.urlPattern) { continue; } // 检查HTTP方法是否匹配 const methodField = intf.httpMethod || intf.method; const methodValue = methodField && methodField.toUpperCase(); const methodMatches = !methodValue || methodValue === 'ALL' || methodValue === 'ANY' || methodValue === method; if (!methodMatches) { continue; } // 根据proxyType选择匹配策略 const proxyType = intf.proxyType || 'response'; let urlMatches = false; try { urlMatches = this.isUrlMatchByProxyType(requestPath, intf.urlPattern, proxyType, req); } catch (err) { this.log(`[规则处理器] 匹配接口 ${intf.name} 时出错: ${err.message}`); continue; } if (urlMatches) { this.log(`[规则处理器] 接口 ${intf.name} URL匹配成功 (${proxyType}模式)`); urlMatchedInterfaces.push(intf); } } this.log(`[规则处理器] 找到 ${urlMatchedInterfaces.length} 个URL匹配的接口`); if (urlMatchedInterfaces.length === 0) { this.log(`[规则处理器] 未找到匹配的接口`); return null; } // 在URL匹配的接口中,查找参数也匹配的接口(受接口入参匹配开关控制) const settings = getSettings(); if (settings.interfaceParamMatcherEnabled) { for (const intf of urlMatchedInterfaces) { if (isParamMatch(req, intf.paramMatchers, this.log)) { this.log(`[规则处理器] 找到完全匹配的接口: ${intf.name} (URL + 参数匹配)`); return intf; } else { this.log(`[规则处理器] 接口 ${intf.name} URL匹配但参数不匹配,继续查找...`); } } // 如果没有参数匹配的接口,返回第一个没有参数匹配规则的接口 const fallbackInterface = urlMatchedInterfaces.find(intf => !intf.paramMatchers || !Array.isArray(intf.paramMatchers) || intf.paramMatchers.length === 0 ); if (fallbackInterface) { this.log(`[规则处理器] 使用回退接口: ${fallbackInterface.name} (无参数匹配规则)`); return fallbackInterface; } this.log(`[规则处理器] 所有URL匹配的接口都有参数匹配规则且都不满足,未找到合适的接口`); return null; } else { this.log('[规则处理器] 接口入参匹配已关闭,直接返回第一个URL匹配的接口'); return urlMatchedInterfaces[0]; } }, /** * 根据代理类型进行URL匹配 * @param {string} requestPath 请求路径 * @param {string} pattern URL匹配模式 * @param {string} proxyType 代理类型 * @param {object} req 请求对象(用于获取完整URL) * @returns {boolean} 是否匹配 */ isUrlMatchByProxyType(requestPath, pattern, proxyType, req) { if (!pattern) return false; // 获取完整URL(如果需要) const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl; switch (proxyType) { case 'url_redirect': // URL重定向:需要完全匹配完整URL this.log(`[规则处理器] url_redirect模式 - 完全匹配: ${fullUrl} === ${pattern}`); return pattern === fullUrl; case 'redirect': // 重定向:前缀匹配 this.log(`[规则处理器] redirect模式 - 前缀匹配: ${fullUrl}.startsWith(${pattern})`); return fullUrl.startsWith(pattern); case 'response': case 'file': default: // 模拟响应和文件代理:支持多种匹配方式 return this.isUrlMatchForResponse(requestPath, pattern); } }, /** * 响应类型的URL匹配(支持精确匹配、通配符、正则表达式) * @param {string} requestPath 请求路径 * @param {string} pattern URL匹配模式 * @returns {boolean} 是否匹配 */ isUrlMatchForResponse(requestPath, pattern) { // 精确匹配 if (pattern === requestPath) { this.log(`[规则处理器] response模式 - 精确匹配成功: ${requestPath}`); return true; } // 通配符匹配 if (pattern.includes('*')) { try { // 从缓存获取或创建正则表达式 if (!this.regexCache) { this.regexCache = new Map(); } const cacheKey = `wildcard:${pattern}`; let regex = this.regexCache.get(cacheKey); if (!regex) { // 转换通配符为正则表达式 const regexPattern = pattern .replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&') // 转义特殊字符 .replace(/\*/g, '.*'); // 将 * 替换为 .* regex = new RegExp('^' + regexPattern + '$'); this.regexCache.set(cacheKey, regex); } const matches = regex.test(requestPath); this.log(`[规则处理器] response模式 - 通配符匹配${matches ? '成功' : '失败'}: ${requestPath} ~ ${pattern}`); return matches; } catch (err) { this.log(`[规则处理器] 通配符匹配错误: ${err.message}`); return false; } } // 正则表达式匹配 if (pattern.startsWith('/') && pattern.length > 2 && pattern.endsWith('/')) { try { // 从缓存获取或创建正则表达式 if (!this.regexCache) { this.regexCache = new Map(); } const cacheKey = `regex:${pattern}`; let regex = this.regexCache.get(cacheKey); if (!regex) { const regexStr = pattern.slice(1, -1); regex = new RegExp(regexStr); this.regexCache.set(cacheKey, regex); } const matches = regex.test(requestPath); this.log(`[规则处理器] response模式 - 正则匹配${matches ? '成功' : '失败'}: ${requestPath} ~ ${pattern}`); return matches; } catch (err) { this.log(`[规则处理器] 正则表达式匹配错误: ${err.message}`); return false; } } // 都不匹配 this.log(`[规则处理器] response模式 - 所有匹配方式都失败: ${requestPath} != ${pattern}`); return false; }, /** * 根据接口类型处理请求 * @param {object} interfaceObj 接口对象 * @param {object} req 请求对象 * @param {object} res 响应对象 * @returns {Promise<object>} 处理结果 */ async processRequest(interfaceObj, req, res) { // 兼容不同的代理类型字段名和值 const proxyType = interfaceObj.proxyType || ''; // 创建统一的配置对象,兼容直接字段访问和config嵌套字段 const config = interfaceObj.config || interfaceObj; // 只记录关键信息 this.log(`[规则处理器] 处理请求,接口: ${interfaceObj.name}, 代理类型: ${proxyType}`); // 添加Whistle Mock插件标识响应头(检查环境变量控制开关,默认启用) const enableMockHeaders = process.env.WHISTLE_MOCK_HEADERS !== 'false'; if (enableMockHeaders) { res.setHeader('X-Whistle-Mock', 'true'); res.setHeader('X-Whistle-Mock-Mode', this.normalizeProxyType(proxyType)); res.setHeader('X-Whistle-Mock-Rule', interfaceObj.name || 'unknown'); res.setHeader('X-Whistle-Mock-Interface', interfaceObj.id || 'unknown'); // 添加功能模块信息 if (interfaceObj.featureId) { try { const features = await this.dataManager.getFeatures(); const feature = features.find(f => f.id === interfaceObj.featureId); if (feature && feature.name) { res.setHeader('X-Whistle-Mock-Feature', feature.name); } } catch (err) { this.log(`[规则处理器] 获取功能模块信息失败: ${err.message}`); } } // 对于有多个响应的接口,添加当前响应标识(优先展示实际命中的响应,在 handleDataTemplate 前此处只能用 activeResponseId 作为预估) if (config.responses && config.responses.length > 0) { const activeResponse = config.responses.find(r => r.id === config.activeResponseId) || config.responses[0]; if (activeResponse && activeResponse.name) { res.setHeader('X-Whistle-Mock-Response', activeResponse.name); } } } // 设置响应头 let headers = config.headers; if (typeof headers === 'string') { headers = this.parseHeaders(headers); } else if (typeof headers === 'object' && headers !== null) { // 已经是对象格式,直接使用 } else { headers = {}; } Object.entries(headers).forEach(([key, value]) => { res.setHeader(key, value); }); // 设置内容类型 if (config.contentType && !res.getHeader('Content-Type')) { res.setHeader('Content-Type', config.contentType); } // 添加延迟 const delay = parseInt(config.responseDelay || config.delay || 0, 10); if (delay > 0) { this.log(`[规则处理器] 添加响应延迟: ${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); } // 设置状态码 res.statusCode = parseInt(config.httpStatus || config.statusCode || 200, 10); // 根据代理类型处理 let result; if (proxyType === 'url_redirect' || proxyType === 'redirect') { result = await this.handleUrlRedirect(interfaceObj, req, res); } else if (proxyType === 'data_template' || proxyType === 'response') { // 添加CORS支持 - 设置跨域相关的响应头 const origin = req.headers.origin || req.headers.host; res.setHeader('Access-Control-Allow-Origin', origin || '*'); res.setHeader('Access-Control-Allow-Credentials', 'true'); result = await this.handleDataTemplate(interfaceObj, req, res); } else if (proxyType === 'file_proxy' || proxyType === 'file') { result = await this.handleFileProxy(interfaceObj, req, res); } else if (proxyType === 'dynamic_data') { result = await this.handleDynamicData(interfaceObj, req, res); } else { // 默认使用数据模板处理 this.log(`[规则处理器] 未知的代理类型 "${proxyType}",默认使用数据模板处理`); result = await this.handleDataTemplate(interfaceObj, req, res); } // 记录处理结果 this.log(`[规则处理器] 请求处理完成,状态码: ${res.statusCode}`); // 记录完整的命中规则日志 if (this.dataManager && typeof this.dataManager.logRequest === 'function') { const parsedUrl = url.parse(req.url); try { // 获取功能模块名称 let featureName = ''; if (interfaceObj.featureId) { try { const features = await this.dataManager.getFeatures(); const feature = features.find(f => f.id === interfaceObj.featureId); if (feature && feature.name) { featureName = feature.name; } } catch (err) { this.log(`[规则处理器] 获取功能模块信息失败: ${err.message}`); } } // 获取实际命中的响应名称 let responseName = ''; if (result && result.mockInfo && result.mockInfo.responseName) { responseName = result.mockInfo.responseName; } else { const headerResponse = res.getHeader('X-Whistle-Mock-Response'); if (headerResponse) { responseName = headerResponse; } } this.dataManager.logRequest({ type: 'mock_hit', eventType: 'mock_hit', message: `命中接口规则: ${interfaceObj.name}`, method: req.method, url: req.url, path: parsedUrl.pathname, status: res.statusCode, pattern: interfaceObj.urlPattern, proxyType: proxyType, interfaceName: interfaceObj.name, interfaceId: interfaceObj.id, featureName: featureName, responseName: responseName, responseData: result, responseTime: new Date().toISOString(), contentType: res.getHeader('Content-Type') || config.contentType }); } catch (err) { this.log(`[规则处理器] 记录命中规则日志失败: ${err.message}`); } } return result; }, /** * 处理 URL 重定向类型 * @param {object} interfaceObj 接口对象 * @param {object} req 请求对象 * @param {object} res 响应对象 * @returns {Promise<object>} 处理结果 */ async handleUrlRedirect(interfaceObj, req, res) { // 兼容不同的字段命名 const config = interfaceObj.config || interfaceObj; const targetUrl = config.targetUrl || ''; const preserveParams = config.preserveParams || false; const proxyType = interfaceObj.proxyType || config.proxyType || ''; if (!targetUrl) { res.end(JSON.stringify({ error: 'No target URL specified' })); return { error: 'No target URL specified' }; } let redirectUrl = targetUrl; if (proxyType === 'url_redirect') { // url_redirect 只跳转到 targetUrl,不附加原始请求的 query 参数 redirectUrl = targetUrl; } else if (proxyType === 'redirect') { // redirect 类型:将 pattern 部分替换为 targetUrl const pattern = config.urlPattern || interfaceObj.urlPattern || ''; const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl; this.log(`[规则处理器] fullUrl : ${fullUrl}`); if (pattern && fullUrl && fullUrl.startsWith(pattern)) { redirectUrl = targetUrl + fullUrl.slice(pattern.length); this.log(`[规则处理器] redirectUrl : ${redirectUrl}`); // 保留参数 if (preserveParams) { const parsedReqUrl = url.parse(fullUrl, true); const parsedTargetUrl = url.parse(redirectUrl, true); const mergedQuery = { ...parsedTargetUrl.query, ...parsedReqUrl.query }; const redirectUrlObj = new url.URL(redirectUrl); Object.entries(mergedQuery).forEach(([key, value]) => { redirectUrlObj.searchParams.set(key, value); }); redirectUrl = redirectUrlObj.toString(); } } else { // pattern 不匹配,直接跳转到 targetUrl redirectUrl = targetUrl; } } else { // 其他类型,默认直接跳转到 targetUrl if (preserveParams) { const parsedReqUrl = url.parse(req.url, true); const parsedTargetUrl = url.parse(targetUrl, true); const mergedQuery = { ...parsedTargetUrl.query, ...parsedReqUrl.query }; const redirectUrlObj = new url.URL(targetUrl); Object.entries(mergedQuery).forEach(([key, value]) => { redirectUrlObj.searchParams.set(key, value); }); redirectUrl = redirectUrlObj.toString(); } } this.log(`[规则处理器] 准备模拟请求到: ${redirectUrl}`); // 调用模拟请求方法 await this.simulateRequest(redirectUrl, req, res); return { redirectUrl }; }, /** * 模拟请求到目标URL并返回响应数据 * @param {string} targetUrl 目标URL * @param {object} req 原始请求对象 * @param {object} res 响应对象 * @returns {Promise<object>} 请求结果 */ async simulateRequest(targetUrl, req, res) { try { const http = require('http'); const https = require('https'); // 解析目标URL const urlObj = new URL(targetUrl); 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; this.log(`[规则处理器] 发送 ${req.method} 请求到: ${targetUrl}`); // 选择http或https模块 const httpModule = isHttps ? https : http; // 创建请求并返回Promise return new Promise((resolve, reject) => { const proxyReq = httpModule.request(options, (proxyRes) => { this.log(`[规则处理器] 收到响应,状态码: ${proxyRes.statusCode}`); // 复制响应头 Object.keys(proxyRes.headers).forEach(key => { res.setHeader(key, proxyRes.headers[key]); }); // 添加重定向特定的标识头部 const enableMockHeaders = process.env.WHISTLE_MOCK_HEADERS !== 'false'; if (enableMockHeaders) { res.setHeader('X-Whistle-Mock-Data-Type', 'redirect'); res.setHeader('X-Whistle-Mock-Target-Url', targetUrl); } // 设置状态码 res.statusCode = proxyRes.statusCode; // 传输响应体 proxyRes.pipe(res); // 收集响应数据用于返回结果 let responseData = ''; proxyRes.on('data', chunk => { responseData += chunk; }); proxyRes.on('end', () => { this.log(`[规则处理器] 请求完成,响应大小: ${responseData.length} 字节`); resolve({ statusCode: proxyRes.statusCode, responseSize: responseData.length, contentType: proxyRes.headers['content-type'] }); }); }); // 错误处理 proxyReq.on('error', (error) => { this.log(`[规则处理器] 请求 ${targetUrl} 时出错: ${error.message}`); // 如果响应尚未发送,返回错误信息 if (!res.headersSent) { res.statusCode = 502; res.end(JSON.stringify({ error: '转发请求失败', message: error.message, targetUrl: targetUrl })); } reject(error); }); // 处理超时 proxyReq.setTimeout(30000, () => { this.log(`[规则处理器] 请求 ${targetUrl} 超时`); const timeoutError = new Error('请求超时'); proxyReq.destroy(timeoutError); reject(timeoutError); }); // 转发请求体 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) { this.log(`[规则处理器] 准备请求时发生错误: ${error.message}`); res.statusCode = 500; res.end(JSON.stringify({ error: '内部服务器错误', message: error.message, targetUrl: targetUrl })); return { error: 'Internal server error', message: error.message }; } }, /** * 处理数据模板类型 * @param {object} interfaceObj 接口对象 * @param {object} req 请求对象 * @param {object} res 响应对象 * @returns {Promise<object>} 处理结果 */ async handleDataTemplate(interfaceObj, req, res) { // 兼容不同的字段命名 const config = interfaceObj.config || interfaceObj; // 支持多响应系统 let template = ''; let hitResponse = null; // 检查是否有多响应配置 if (config.responses && Array.isArray(config.responses) && config.responses.length > 0) { let selectedResponse = null; const settings = getSettings(); // 1. 优先检查接口级 paramMatchers 中带 targetResponseId 的规则(受开关控制) if (settings.interfaceParamMatcherEnabled) { const interfaceMatchers = config.paramMatchers; if (interfaceMatchers && Array.isArray(interfaceMatchers) && interfaceMatchers.length > 0) { for (const matcher of interfaceMatchers) { if (!matcher.targetResponseId) continue; if (isParamMatch(req, [matcher], this.log)) { const target = config.responses.find(r => r.id === matcher.targetResponseId); if (target) { selectedResponse = target; this.log(`[规则处理器] 接口级规则指定响应匹配成功,使用响应: ${target.name || '未命名'}`); break; } } } } } else { this.log('[规则处理器] 接口入参匹配已关闭,跳过接口级匹配'); } // 2. 按顺序查找第一个响应级入参匹配的响应(受开关控制) if (!selectedResponse) { if (settings.responseParamMatcherEnabled) { for (const resp of config.responses) { if (resp.paramMatchers && Array.isArray(resp.paramMatchers) && resp.paramMatchers.length > 0) { if (isParamMatch(req, resp.paramMatchers, this.log)) { selectedResponse = resp; this.log(`[规则处理器] 响应级入参匹配成功,使用响应: ${resp.name || '未命名'}`); break; } else { this.log(`[规则处理器] 响应 "${resp.name || '未命名'}" 入参不匹配,继续检查下一个`); } } } } else { this.log('[规则处理器] 响应入参匹配已关闭,跳过响应级匹配'); } } // 3. 无匹配则回退到 activeResponseId 指定的默认响应 if (!selectedResponse) { const activeResponseId = config.activeResponseId; selectedResponse = (activeResponseId && config.responses.find(r => r.id === activeResponseId)) || config.responses[0]; this.log(`[规则处理器] 无响应级匹配,使用默认响应: ${selectedResponse ? (selectedResponse.name || '未命名') : '无'}`); } if (selectedResponse) { hitResponse = selectedResponse; template = selectedResponse.content; } } // 如果没有找到响应内容,回退到传统字段 if (!template) { template = config.responseContent || config.template || '{}'; this.log(`[规则处理器] 使用传统响应内容`); } if (!template) { res.end(JSON.stringify({ error: 'No template specified' })); return { error: 'No template specified' }; } try { // 使用 Mock.js 生成数据 const mockData = Mock.mock(JSON.parse(template)); // 设置内容类型 if (!res.getHeader('Content-Type')) { res.setHeader('Content-Type', 'application/json; charset=utf-8'); } // 添加mock数据特定的标识头部 const enableMockHeaders = process.env.WHISTLE_MOCK_HEADERS !== 'false'; if (enableMockHeaders) { res.setHeader('X-Whistle-Mock-Data-Type', 'template'); if (hitResponse) { res.setHeader('X-Whistle-Mock-Template', hitResponse.name || 'unnamed'); // 覆盖 processRequest 中预设的响应头,确保反映实际命中的响应 res.setHeader('X-Whistle-Mock-Response', hitResponse.name || 'unnamed'); } } // 发送数据 res.end(JSON.stringify(mockData)); // 返回处理结果,包含响应信息 return { mockData, mockInfo: { delay: parseInt(config.responseDelay || config.delay || 0, 10), timestamp: new Date().toISOString(), responseName: hitResponse ? (hitResponse.name || '未命名') : '默认响应' } }; } catch (err) { this.log(`[规则处理器] 模板解析错误: ${err.message}`); res.end(JSON.stringify({ error: 'Template parsing error', message: err.message })); return { error: 'Template parsing error', message: err.message }; } }, /** * 处理文件代理类型 * @param {object} interfaceObj 接口对象 * @param {object} req 请求对象 * @param {object} res 响应对象 * @returns {Promise<object>} 处理结果 */ async handleFileProxy(interfaceObj, req, res) { // 兼容不同的字段命名 const config = interfaceObj.config || interfaceObj; const filePath = config.filePath || ''; // 只记录关键信息 this.log(`[规则处理器] 处理文件代理,路径: ${filePath}`); if (!filePath) { this.log(`[规则处理器] 错误: 未指定文件路径`); res.end(JSON.stringify({ error: 'No file path specified' })); return { error: 'No file path specified' }; } // 确定文件路径 const absFilePath = path.isAbsolute(filePath) ? filePath : path.join(this.mockDataDir, filePath); try { // 检查文件是否存在 if (!fs.existsSync(absFilePath)) { this.log(`[规则处理器] 错误: 文件不存在: ${path.basename(absFilePath)}`); res.statusCode = 404; res.end(JSON.stringify({ error: 'File not found', path: absFilePath })); return { error: 'File not found', path: absFilePath }; } // 读取文件内容 const fileContent = fs.readFileSync(absFilePath, 'utf8'); this.log(`[规则处理器] 成功读取文件: ${path.basename(absFilePath)}`); // 设置内容类型(根据文件扩展名) if (!res.getHeader('Content-Type')) { const ext = path.extname(absFilePath).toLowerCase(); const contentType = this.getContentTypeByExt(ext); if (contentType) { res.setHeader('Content-Type', contentType); } } // 设置响应头,标识插件处理 res.setHeader('X-Handled-By', 'whistle.mock-plugin'); // 添加文件代理特定的标识头部 const enableMockHeaders = process.env.WHISTLE_MOCK_HEADERS !== 'false'; if (enableMockHeaders) { res.setHeader('X-Whistle-Mock-Data-Type', 'file'); res.setHeader('X-Whistle-Mock-File', path.basename(absFilePath)); res.setHeader('X-Whistle-Mock-File-Path', filePath); } // 发送文件内容 res.end(fileContent); return { filePath: absFilePath, fileName: path.basename(absFilePath) }; } catch (err) { this.log(`[规则处理器] 读取文件错误: ${err.message}`); res.end(JSON.stringify({ error: 'File reading error', message: err.message })); return { error: 'File reading error', message: err.message }; } }, /** * 处理动态数据生成类型 * @param {object} interfaceObj 接口对象 * @param {object} req 请求对象 * @param {object} res 响应对象 * @returns {Promise<object>} 处理结果 */ async handleDynamicData(interfaceObj, req, res) { const { config } = interfaceObj; const { script } = config; if (!script) { res.end(JSON.stringify({ error: 'No script specified' })); return { error: 'No script specified' }; } try { // 创建执行环境 const context = { req, Mock, url: req.url, method: req.method, headers: req.headers, query: url.parse(req.url, true).query, }; // 从请求中读取 body if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') { const body = await this.readRequestBody(req); context.body = body; // 尝试解析 JSON try { context.bodyJson = JSON.parse(body); } catch (e) { // 忽略解析错误 } } // 执行脚本 const fn = new Function('ctx', ` with (ctx) { ${script} return typeof result !== 'undefined' ? result : {}; } `); const result = fn(context); // 设置内容类型 if (!res.getHeader('Content-Type')) { res.setHeader('Content-Type', 'application/json; charset=utf-8'); } // 添加动态数据特定的标识头部 const enableMockHeaders = process.env.WHISTLE_MOCK_HEADERS !== 'false'; if (enableMockHeaders) { res.setHeader('X-Whistle-Mock-Data-Type', 'dynamic'); res.setHeader('X-Whistle-Mock-Script-Engine', 'function'); } // 发送结果 res.end(typeof result === 'string' ? result : JSON.stringify(result)); return { result }; } catch (err) { res.end(JSON.stringify({ error: 'Script execution error', message: err.message })); return { error: 'Script execution error', message: err.message }; } }, /** * 从请求中读取 body * @param {object} req 请求对象 * @returns {Promise<string>} 请求体字符串 */ readRequestBody(req) { return new Promise((resolve, reject) => { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { resolve(body); }); req.on('error', err => { reject(err); }); }); }, /** * 解析响应头字符串 * @param {string} headersStr 响应头字符串 * @returns {object} 解析后的响应头对象 */ parseHeaders(headersStr) { if (!headersStr) return {}; const headers = {}; headersStr.split('\n').forEach(line => { const [key, ...valueParts] = line.split(':'); if (key && valueParts.length > 0) { const value = valueParts.join(':').trim(); if (value) { headers[key.trim()] = value; } } }); return headers; }, /** * 根据文件扩展名获取内容类型 * @param {string} ext 文件扩展名 * @returns {string|null} 内容类型 */ getContentTypeByExt(ext) { const contentTypes = { '.json': 'application/json; charset=utf-8', '.xml': 'application/xml; charset=utf-8', '.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8', '.txt': 'text/plain; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.csv': 'text/csv; charset=utf-8', }; return contentTypes[ext] || null; }, /** * 规范化代理类型名称 * @param {string} proxyType 原始代理类型 * @returns {string} 规范化后的代理类型 */ normalizeProxyType(proxyType) { const typeMap = { 'response': 'mock', 'data_template': 'mock', 'redirect': 'redirect', 'url_redirect': 'redirect', 'file': 'file', 'file_proxy': 'file', 'dynamic_data': 'dynamic', }; return typeMap[proxyType] || 'proxy'; } }; module.exports = ruleManager;