whistle.mock-plugins 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -71,6 +71,12 @@ const dataManager = {
71
71
  */
72
72
  logRequest(logData) {
73
73
  try {
74
+ // 首先检查参数是否有效,避免不必要的文件操作
75
+ if (!logData || typeof logData !== 'object') {
76
+ this.log(`记录请求日志失败: 无效的日志数据`);
77
+ return false;
78
+ }
79
+
74
80
  // 确保日志文件存在
75
81
  if (!fs.existsSync(this.logsFile)) {
76
82
  fs.writeJsonSync(this.logsFile, { logs: [] }, { spaces: 2 });
@@ -105,8 +105,19 @@ const ruleManager = {
105
105
  return null;
106
106
  }
107
107
 
108
- // 先尝试完全匹配
108
+ // 防止空路径
109
+ if (!requestPath) {
110
+ this.log(`[规则处理器] 错误: 请求路径为空`);
111
+ return null;
112
+ }
113
+
114
+ // 先尝试完全匹配,效率更高
109
115
  let matchedInterface = interfaces.find(intf => {
116
+ // 检查接口对象是否有效
117
+ if (!intf || !intf.urlPattern) {
118
+ return false;
119
+ }
120
+
110
121
  // 检查URL和方法是否匹配
111
122
  const urlMatches = intf.urlPattern === requestPath;
112
123
 
@@ -130,38 +141,59 @@ const ruleManager = {
130
141
  // 如果没有完全匹配,尝试正则表达式匹配
131
142
  this.log(`[规则处理器] 未找到完全匹配,尝试正则表达式匹配...`);
132
143
 
133
- matchedInterface = interfaces.find(intf => {
134
- try {
135
- // 将通配符转换为正则表达式
136
- const pattern = this.convertPatternToRegex(intf.urlPattern);
137
- const regex = new RegExp(pattern);
138
-
139
- // 获取方法值,兼容不同的字段名
140
- const methodField = intf.httpMethod || intf.method;
141
- const methodValue = methodField && methodField.toUpperCase();
142
-
143
- // 只记录尝试匹配的模式,不记录详细过程
144
- this.log(`[规则处理器] 尝试正则匹配,测试 ${interfaces.length} 个模式`);
145
-
146
- // 检查URL和方法是否匹配
147
- const urlMatches = regex.test(requestPath);
148
- const methodMatches = !methodValue ||
149
- methodValue === 'ALL' ||
150
- methodValue === 'ANY' ||
151
- methodValue === method;
152
-
153
- // 移除中间匹配过程的日志
154
- return urlMatches && methodMatches;
155
- } catch (err) {
156
- this.log(`[规则处理器] 无效的URL模式: ${intf.urlPattern}, 错误: ${err.message}`);
157
- return false;
158
- }
159
- });
144
+ // 使用正则表达式缓存优化性能
145
+ if (!this.regexCache) {
146
+ this.regexCache = new Map();
147
+ }
160
148
 
149
+ try {
150
+ matchedInterface = interfaces.find(intf => {
151
+ try {
152
+ // 验证接口对象
153
+ if (!intf || !intf.urlPattern) {
154
+ return false;
155
+ }
156
+
157
+ // 从缓存获取或创建正则表达式
158
+ let regex;
159
+ if (this.regexCache.has(intf.urlPattern)) {
160
+ regex = this.regexCache.get(intf.urlPattern);
161
+ } else {
162
+ // 将通配符转换为正则表达式
163
+ const pattern = this.convertPatternToRegex(intf.urlPattern);
164
+ regex = new RegExp(pattern);
165
+ // 缓存正则表达式,避免重复创建
166
+ this.regexCache.set(intf.urlPattern, regex);
167
+ }
168
+
169
+ // 获取方法值,兼容不同的字段名
170
+ const methodField = intf.httpMethod || intf.method;
171
+ const methodValue = methodField && methodField.toUpperCase();
172
+
173
+ // 检查URL和方法是否匹配
174
+ const urlMatches = regex.test(requestPath);
175
+ const methodMatches = !methodValue ||
176
+ methodValue === 'ALL' ||
177
+ methodValue === 'ANY' ||
178
+ methodValue === method;
179
+
180
+ return urlMatches && methodMatches;
181
+ } catch (err) {
182
+ // 记录特定接口的错误,但继续处理其他接口
183
+ this.log(`[规则处理器] 匹配接口 ${intf?.name || 'unknown'} 时出错: ${err.message}`);
184
+ return false;
185
+ }
186
+ });
187
+ } catch (err) {
188
+ // 记录整体匹配过程的错误
189
+ this.log(`[规则处理器] 正则匹配过程出错: ${err.message}`);
190
+ return null;
191
+ }
192
+
161
193
  if (matchedInterface) {
162
- this.log(`[规则处理器] 找到模式匹配的接口: ${matchedInterface.name}`);
194
+ this.log(`[规则处理器] 找到正则匹配的接口: ${matchedInterface.name}`);
163
195
  } else {
164
- this.log(`[规则处理器] 未找到任何匹配的接口`);
196
+ this.log(`[规则处理器] 未找到匹配的接口`);
165
197
  }
166
198
 
167
199
  return matchedInterface;
@@ -115,10 +115,25 @@ const loadInterfaces = () => {
115
115
  };
116
116
 
117
117
  // 匹配URL是否符合模式
118
- const isUrlMatch = (url, pattern) => {
118
+ const isUrlMatch = (url, pattern, proxyType, fullUrl) => {
119
119
  if (!pattern) return false;
120
120
 
121
121
  try {
122
+ // 针对不同的代理类型采用不同的匹配策略
123
+
124
+ // 对于url_redirect类型,需要完全匹配fullPath才命中
125
+ if (proxyType === 'url_redirect') {
126
+ // 精确匹配完整路径
127
+ return pattern === fullUrl;
128
+ }
129
+
130
+ // 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
131
+ if (proxyType === 'redirect') {
132
+ return fullUrl.indexOf(pattern) === 0;
133
+ }
134
+
135
+ // 以下是默认的匹配逻辑(用于response类型等)
136
+
122
137
  // 精确匹配
123
138
  if (pattern === url) {
124
139
  return true;
@@ -148,6 +163,35 @@ const isUrlMatch = (url, pattern) => {
148
163
  return false;
149
164
  };
150
165
 
166
+ // 验证目标URL是否有效
167
+ const isValidTargetUrl = (url) => {
168
+ if (!url) return false;
169
+
170
+ try {
171
+ // 检查是否是合法的URL格式
172
+ new URL(url);
173
+ return true;
174
+ } catch (e) {
175
+ log(`URL验证错误: ${e.message}`);
176
+ return false;
177
+ }
178
+ };
179
+
180
+ // 验证重定向规则的有效性
181
+ const validateRedirectRule = (interface) => {
182
+ if (!interface) return false;
183
+
184
+ const { proxyType, targetUrl } = interface;
185
+
186
+ // 对于redirect和url_redirect类型,必须有有效的目标URL
187
+ if ((proxyType === 'redirect' || proxyType === 'url_redirect') && !isValidTargetUrl(targetUrl)) {
188
+ log(`接口 ${interface.id || interface.name || '未知'} 的目标URL无效: ${targetUrl}`);
189
+ return false;
190
+ }
191
+
192
+ return true;
193
+ };
194
+
151
195
  // Whistle 规则服务器实现
152
196
  module.exports = (server, options) => {
153
197
  server.on('request', (req, res) => {
@@ -220,21 +264,22 @@ module.exports = (server, options) => {
220
264
  const matchedInterface = enabledInterfaces.find(intf => {
221
265
  // 获取URL模式
222
266
  const pattern = intf.urlPattern || '';
267
+ const proxyType = intf.proxyType || 'response';
223
268
 
224
269
  if (!pattern) {
225
270
  log(`接口 ${intf.id || intf.name || '未知'} 缺少URL模式`);
226
271
  return false;
227
272
  }
228
273
 
229
- // 检查URL模式匹配
230
- const urlMatches = isUrlMatch(path, pattern);
274
+ // 检查URL模式匹配,根据proxyType选择适当的匹配策略
275
+ const urlMatches = isUrlMatch(path, pattern, proxyType, fullUrl);
231
276
 
232
277
  if (!urlMatches) {
233
278
  // 不记录每个失败的匹配,以减少日志量
234
279
  return false;
235
280
  }
236
281
 
237
- log(`URL模式匹配成功: ${path} 匹配 ${pattern}`);
282
+ log(`URL模式匹配成功: ${fullUrl} 匹配 ${pattern} (${proxyType}模式)`);
238
283
 
239
284
  // 检查HTTP方法匹配
240
285
  const methodField = intf.httpMethod || intf.method || '';
@@ -254,9 +299,85 @@ module.exports = (server, options) => {
254
299
  return true;
255
300
  });
256
301
 
257
- // 如果找到匹配的接口,返回对应的规则将请求转发到插件
302
+ // 如果找到匹配的接口,处理对应的规则
258
303
  if (matchedInterface) {
259
304
  log(`找到匹配接口 "${matchedInterface.name || matchedInterface.id || '未知'}" 用于 ${method} ${path}`);
305
+
306
+ // 处理不同的代理类型
307
+ const proxyType = matchedInterface.proxyType || 'response';
308
+
309
+ // 优先级判断:首先处理 response 类型
310
+ if (proxyType === 'response') {
311
+ // 返回指向插件服务的规则
312
+ log(`接口 "${matchedInterface.name}" 使用模拟响应模式`);
313
+ res.end(`${fullUrl} mock-plugins://`);
314
+ return;
315
+ }
316
+
317
+ // 对于redirect和url_redirect类型,验证规则有效性
318
+ if ((proxyType === 'redirect' || proxyType === 'url_redirect') && !validateRedirectRule(matchedInterface)) {
319
+ log(`接口 "${matchedInterface.name}" 的重定向规则无效,跳过处理`);
320
+ res.end('');
321
+ return;
322
+ }
323
+
324
+ // 处理 redirect 类型:将请求的 url 中的源 url 部分替换为目标 url
325
+ if (proxyType === 'redirect') {
326
+ const targetUrl = matchedInterface.targetUrl.trim();
327
+ const pattern = matchedInterface.urlPattern.trim();
328
+ log(`接口 "${matchedInterface.name}" 使用重定向模式,模式: ${pattern}, 目标URL: ${targetUrl}`);
329
+
330
+ // 确保目标URL合法
331
+ if (!isValidTargetUrl(targetUrl)) {
332
+ log(`目标URL不合法: ${targetUrl},跳过处理`);
333
+ res.end('');
334
+ return;
335
+ }
336
+
337
+ // 执行重定向:将fullUrl中匹配的pattern替换为targetUrl
338
+ // 对于redirect类型,我们使用indexOf确认前缀匹配
339
+ if (fullUrl.indexOf(pattern) === 0) {
340
+ // 构建新的目标URL
341
+ const redirectUrl = fullUrl.replace(pattern, targetUrl);
342
+
343
+ // 按照 whistle 规则格式返回重定向规则
344
+ res.end(`${fullUrl} ${redirectUrl}`);
345
+ log(`重定向规则: ${fullUrl} ${redirectUrl}`);
346
+ return;
347
+ } else {
348
+ log(`URL ${fullUrl} 不以 ${pattern} 开头,跳过处理`);
349
+ res.end('');
350
+ return;
351
+ }
352
+ }
353
+
354
+ // 处理 url_redirect 类型:完全匹配 fullPath 并直接返回目标URL
355
+ if (proxyType === 'url_redirect') {
356
+ const targetUrl = matchedInterface.targetUrl.trim();
357
+ log(`接口 "${matchedInterface.name}" 使用URL重定向模式,目标URL: ${targetUrl}`);
358
+
359
+ // 确保目标URL合法
360
+ if (!isValidTargetUrl(targetUrl)) {
361
+ log(`目标URL不合法: ${targetUrl},跳过处理`);
362
+ res.end('');
363
+ return;
364
+ }
365
+
366
+ // 对于url_redirect,我们需要完全匹配
367
+ if (fullUrl === matchedInterface.urlPattern) {
368
+ // 直接返回whistle规则
369
+ res.end(`${fullUrl} redirect://${targetUrl}`);
370
+ log(`URL重定向规则: ${fullUrl} redirect://${targetUrl}`);
371
+ return;
372
+ } else {
373
+ log(`URL ${fullUrl} 不完全匹配 ${matchedInterface.urlPattern},跳过处理`);
374
+ res.end('');
375
+ return;
376
+ }
377
+ }
378
+
379
+ // 默认情况下使用mock-plugins处理
380
+ log(`接口 "${matchedInterface.name}" 使用默认处理模式`);
260
381
  res.end(`${fullUrl} mock-plugins://`);
261
382
  } else {
262
383
  // 如果没有匹配的接口,返回空字符串,Whistle 会继续处理下一个规则
package/lib/server.js CHANGED
@@ -43,6 +43,12 @@ const logMessage = (message) => {
43
43
  // 记录日志到 logs.json 文件
44
44
  const addToJsonLog = (logData) => {
45
45
  try {
46
+ // 验证参数有效性
47
+ if (!logData || typeof logData !== 'object') {
48
+ console.error('无效的日志数据');
49
+ return;
50
+ }
51
+
46
52
  const logsFile = path.join(DATA_DIR, 'logs.json');
47
53
 
48
54
  // 确保日志文件存在
@@ -86,9 +92,9 @@ const addToJsonLog = (logData) => {
86
92
  // 添加新日志
87
93
  logsData.logs.unshift(newLog);
88
94
 
89
- // 限制日志数量,最多保留10000条
90
- if (logsData.logs.length > 10000) {
91
- logsData.logs = logsData.logs.slice(0, 10000);
95
+ // 限制日志数量,最多保留5000条 (减少内存使用)
96
+ if (logsData.logs.length > 5000) {
97
+ logsData.logs = logsData.logs.slice(0, 5000);
92
98
  }
93
99
 
94
100
  fs.writeJsonSync(logsFile, logsData, { spaces: 2 });
@@ -643,6 +649,71 @@ const handleResponse = (interfaceItem, req, res) => {
643
649
  }
644
650
  break;
645
651
 
652
+ case 'redirect':
653
+ // 处理redirect - 完全重定向
654
+ const redirectUrl = interfaceItem.targetUrl || '';
655
+ logMessage(`重定向处理: ${req.url} -> ${redirectUrl}`);
656
+ // 这里不需要实际执行重定向,因为规则服务器已经返回了whistle规则
657
+ // 仅记录日志,并返回信息性消息
658
+ res.status(200).json({
659
+ code: 302,
660
+ message: '请求已被重定向到: ' + redirectUrl,
661
+ data: {
662
+ originalUrl: req.url,
663
+ targetUrl: redirectUrl,
664
+ mode: 'redirect'
665
+ }
666
+ });
667
+
668
+ // 记录重定向日志
669
+ addToJsonLog({
670
+ eventType: 'redirect',
671
+ url: req.url,
672
+ method: req.method,
673
+ targetUrl: redirectUrl,
674
+ message: `请求被重定向: ${req.method} ${req.url} -> ${redirectUrl}`,
675
+ pattern: interfaceItem.urlPattern
676
+ });
677
+ break;
678
+
679
+ case 'url_redirect':
680
+ // 处理url_redirect - URL重定向,保留查询参数
681
+ const baseUrl = interfaceItem.targetUrl || '';
682
+ // 提取原始URL的查询参数,实际重定向在规则服务器中处理
683
+ const reqUrl = req.url || '';
684
+ let finalRedirectUrl = baseUrl;
685
+
686
+ if (req.url.includes('?')) {
687
+ const queryStr = req.url.split('?')[1];
688
+ finalRedirectUrl = baseUrl.includes('?')
689
+ ? `${baseUrl}&${queryStr}`
690
+ : `${baseUrl}?${queryStr}`;
691
+ }
692
+
693
+ logMessage(`URL重定向处理: ${req.url} -> ${finalRedirectUrl}`);
694
+
695
+ // 返回信息性消息
696
+ res.status(200).json({
697
+ code: 302,
698
+ message: '请求已被URL重定向到: ' + finalRedirectUrl,
699
+ data: {
700
+ originalUrl: req.url,
701
+ targetUrl: finalRedirectUrl,
702
+ mode: 'url_redirect'
703
+ }
704
+ });
705
+
706
+ // 记录URL重定向日志
707
+ addToJsonLog({
708
+ eventType: 'url_redirect',
709
+ url: req.url,
710
+ method: req.method,
711
+ targetUrl: finalRedirectUrl,
712
+ message: `请求被URL重定向: ${req.method} ${req.url} -> ${finalRedirectUrl}`,
713
+ pattern: interfaceItem.urlPattern
714
+ });
715
+ break;
716
+
646
717
  case 'url':
647
718
  // URL重定向 - 实际场景中这需要代理到目标URL
648
719
  logMessage(`URL重定向: ${req.url} -> ${interfaceItem.targetUrl}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whistle.mock-plugins",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Whistle 插件,用于快速创建 API 模拟数据",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
package/rules.txt CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # ===== 基本规则配置 =====
5
5
  # 在 Whistle 的规则配置中,使用以下格式添加规则:
6
- * mock-plugins://
6
+ * whistle.mock-plugins://
7
7
 
8
8
  # 该插件现在会自动匹配已配置的接口规则,对于没有在插件中配置的接口,不会进行拦截
9
9