whistle.mock-plugins
Version:
Whistle 插件,用于快速创建 API 模拟数据
1,135 lines (981 loc) • 40 kB
JavaScript
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;