whistle.mock-plugins 1.0.4 → 1.0.6-beta.0
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.
- package/app/cgi-bin/interfaces.js +3 -0
- package/app/cgi-bin/test-interface.js +279 -230
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +2 -0
- package/lib/rules-server.js +292 -133
- package/package.json +1 -1
- package/ui/src/pages/InterfaceManagement.js +343 -91
- package/ui/src/pages/MockData.js +32 -0
|
@@ -127,6 +127,8 @@
|
|
|
127
127
|
|
|
128
128
|
/** */
|
|
129
129
|
|
|
130
|
+
/** */
|
|
131
|
+
|
|
130
132
|
/** */
|
|
131
133
|
|
|
132
134
|
/** */
|
package/lib/rules-server.js
CHANGED
|
@@ -10,150 +10,207 @@ const INTERFACES_FILE = path.join(DATA_DIR, 'interfaces.json');
|
|
|
10
10
|
// 添加功能模块配置文件
|
|
11
11
|
const FEATURES_FILE = path.join(DATA_DIR, 'features.json');
|
|
12
12
|
|
|
13
|
+
// 配置项
|
|
14
|
+
const CONFIG = {
|
|
15
|
+
// 日志级别:0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG
|
|
16
|
+
LOG_LEVEL: process.env.MOCK_PLUGIN_LOG_LEVEL || 3,
|
|
17
|
+
// 缓存有效期(毫秒),默认 60 秒
|
|
18
|
+
CACHE_INTERVAL: process.env.MOCK_PLUGIN_CACHE_INTERVAL || 60000,
|
|
19
|
+
// 是否在控制台输出详细日志
|
|
20
|
+
VERBOSE_CONSOLE: process.env.MOCK_PLUGIN_VERBOSE === 'true'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// 缓存控制变量
|
|
24
|
+
let enabledInterfacesCache = null; // 仅缓存启用状态的接口
|
|
25
|
+
let cacheTime = 0;
|
|
26
|
+
let cacheVersion = 1;
|
|
27
|
+
|
|
28
|
+
// 正则表达式缓存
|
|
29
|
+
const regexCache = new Map();
|
|
30
|
+
|
|
13
31
|
// 记录日志
|
|
14
|
-
const log = (message) => {
|
|
15
|
-
|
|
32
|
+
const log = (message, level = 3) => {
|
|
33
|
+
// 只记录小于等于当前日志级别的日志
|
|
34
|
+
if (level <= CONFIG.LOG_LEVEL) {
|
|
35
|
+
const prefix = level === 1 ? '[ERROR] ' :
|
|
36
|
+
level === 2 ? '[WARN] ' :
|
|
37
|
+
level === 4 ? '[DEBUG] ' : '';
|
|
38
|
+
|
|
39
|
+
console.log(`[mock-plugin:rulesServer] ${prefix}${message}`);
|
|
40
|
+
}
|
|
16
41
|
};
|
|
17
42
|
|
|
18
|
-
//
|
|
19
|
-
const
|
|
43
|
+
// 错误日志
|
|
44
|
+
const logError = (message) => log(message, 1);
|
|
45
|
+
|
|
46
|
+
// 警告日志
|
|
47
|
+
const logWarn = (message) => log(message, 2);
|
|
48
|
+
|
|
49
|
+
// 信息日志
|
|
50
|
+
const logInfo = (message) => log(message, 3);
|
|
51
|
+
|
|
52
|
+
// 调试日志
|
|
53
|
+
const logDebug = (message) => log(message, 4);
|
|
54
|
+
|
|
55
|
+
// 获取文件最后修改时间
|
|
56
|
+
const getFileModTime = (filePath) => {
|
|
20
57
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return [];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const data = fs.readJsonSync(FEATURES_FILE);
|
|
27
|
-
|
|
28
|
-
// 处理不同的数据结构
|
|
29
|
-
let features = [];
|
|
30
|
-
|
|
31
|
-
if (data && Array.isArray(data)) {
|
|
32
|
-
// 如果直接是数组
|
|
33
|
-
features = data;
|
|
34
|
-
} else if (data && data.features && Array.isArray(data.features)) {
|
|
35
|
-
// 如果是 {features: [...]} 格式
|
|
36
|
-
features = data.features;
|
|
37
|
-
} else {
|
|
38
|
-
log('未能识别功能模块数据结构');
|
|
39
|
-
return [];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
log(`总共发现 ${features.length} 个功能模块配置`);
|
|
43
|
-
|
|
44
|
-
// 过滤出启用状态的功能模块
|
|
45
|
-
const enabledFeatures = features.filter(feature => feature.active === true);
|
|
46
|
-
|
|
47
|
-
log(`找到 ${enabledFeatures.length} 个启用的功能模块`);
|
|
48
|
-
return enabledFeatures;
|
|
58
|
+
const stats = fs.statSync(filePath);
|
|
59
|
+
return stats.mtimeMs;
|
|
49
60
|
} catch (err) {
|
|
50
|
-
|
|
51
|
-
return [];
|
|
61
|
+
return 0;
|
|
52
62
|
}
|
|
53
63
|
};
|
|
54
64
|
|
|
55
|
-
//
|
|
56
|
-
const
|
|
65
|
+
// 加载所有启用的接口(整合了功能模块和接口的加载逻辑)
|
|
66
|
+
const loadEnabledInterfaces = () => {
|
|
57
67
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
68
|
+
const currentTime = Date.now();
|
|
69
|
+
const interfacesFileModTime = getFileModTime(INTERFACES_FILE);
|
|
70
|
+
const featuresFileModTime = getFileModTime(FEATURES_FILE);
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const dataPreview = JSON.stringify(data).substring(0, 100) + (JSON.stringify(data).length > 100 ? '...' : '');
|
|
66
|
-
log(`读取到接口配置数据: ${dataPreview}`);
|
|
72
|
+
// 取两个文件的最后修改时间的最大值
|
|
73
|
+
const lastModTime = Math.max(interfacesFileModTime, featuresFileModTime);
|
|
67
74
|
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
} else if (data && data.interfaces && Array.isArray(data.interfaces)) {
|
|
75
|
-
// 如果是 {interfaces: [...]} 格式
|
|
76
|
-
interfaces = data.interfaces;
|
|
77
|
-
} else {
|
|
78
|
-
log('未能识别接口数据结构');
|
|
79
|
-
return [];
|
|
75
|
+
// 如果缓存有效且文件未被修改,直接返回缓存
|
|
76
|
+
if (enabledInterfacesCache &&
|
|
77
|
+
currentTime - cacheTime < CONFIG.CACHE_INTERVAL &&
|
|
78
|
+
lastModTime <= cacheTime) {
|
|
79
|
+
logDebug('使用已启用接口的缓存');
|
|
80
|
+
return enabledInterfacesCache;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
logDebug('缓存无效或文件已修改,重新加载接口');
|
|
83
84
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
85
|
+
// 1. 读取并解析所有功能模块
|
|
86
|
+
let enabledFeatureIds = [];
|
|
87
|
+
if (fs.existsSync(FEATURES_FILE)) {
|
|
88
|
+
try {
|
|
89
|
+
const featuresData = fs.readJsonSync(FEATURES_FILE);
|
|
90
|
+
let features = [];
|
|
91
|
+
|
|
92
|
+
if (featuresData && Array.isArray(featuresData)) {
|
|
93
|
+
features = featuresData;
|
|
94
|
+
} else if (featuresData && featuresData.features && Array.isArray(featuresData.features)) {
|
|
95
|
+
features = featuresData.features;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 过滤出启用状态的功能ID
|
|
99
|
+
enabledFeatureIds = features
|
|
100
|
+
.filter(feature => feature.active === true)
|
|
101
|
+
.map(feature => feature.id);
|
|
102
|
+
|
|
103
|
+
logDebug(`找到 ${enabledFeatureIds.length} 个启用的功能模块`);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
logError(`读取功能模块失败: ${err.message}`);
|
|
97
106
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2. 读取并解析所有接口
|
|
110
|
+
let enabledInterfaces = [];
|
|
111
|
+
if (fs.existsSync(INTERFACES_FILE)) {
|
|
112
|
+
try {
|
|
113
|
+
const interfacesData = fs.readJsonSync(INTERFACES_FILE);
|
|
114
|
+
let interfaces = [];
|
|
115
|
+
|
|
116
|
+
if (interfacesData && Array.isArray(interfacesData)) {
|
|
117
|
+
interfaces = interfacesData;
|
|
118
|
+
} else if (interfacesData && interfacesData.interfaces && Array.isArray(interfacesData.interfaces)) {
|
|
119
|
+
interfaces = interfacesData.interfaces;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 过滤出启用状态的接口
|
|
123
|
+
enabledInterfaces = interfaces.filter(intf => {
|
|
124
|
+
// 首先检查接口本身是否启用
|
|
125
|
+
const isInterfaceEnabled = intf.active !== false; // 默认为true
|
|
126
|
+
|
|
127
|
+
if (!isInterfaceEnabled) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 如果接口有关联功能,检查功能是否启用
|
|
132
|
+
if (intf.featureId) {
|
|
133
|
+
return enabledFeatureIds.includes(intf.featureId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 如果接口没有关联功能,只要接口本身启用就可以
|
|
137
|
+
return true;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
logInfo(`找到 ${enabledInterfaces.length} 个启用的接口`);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
logError(`读取接口配置失败: ${err.message}`);
|
|
103
143
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 更新缓存
|
|
147
|
+
enabledInterfacesCache = enabledInterfaces;
|
|
148
|
+
cacheTime = currentTime;
|
|
108
149
|
|
|
109
|
-
log(`找到 ${enabledInterfaces.length} 个启用的接口配置`);
|
|
110
150
|
return enabledInterfaces;
|
|
111
151
|
} catch (err) {
|
|
112
|
-
|
|
113
|
-
return [];
|
|
152
|
+
logError(`加载启用接口失败: ${err.message}`);
|
|
153
|
+
return enabledInterfacesCache || [];
|
|
114
154
|
}
|
|
115
155
|
};
|
|
116
156
|
|
|
117
157
|
// 匹配URL是否符合模式
|
|
118
158
|
const isUrlMatch = (url, pattern, proxyType, fullUrl) => {
|
|
119
159
|
if (!pattern) return false;
|
|
120
|
-
|
|
160
|
+
|
|
121
161
|
try {
|
|
122
162
|
// 针对不同的代理类型采用不同的匹配策略
|
|
123
163
|
|
|
124
164
|
// 对于url_redirect类型,需要完全匹配fullPath才命中
|
|
125
165
|
if (proxyType === 'url_redirect') {
|
|
126
|
-
//
|
|
166
|
+
// 精确匹配完整路径,这是最快的比较方式
|
|
127
167
|
return pattern === fullUrl;
|
|
128
168
|
}
|
|
129
169
|
|
|
130
170
|
// 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
|
|
131
171
|
if (proxyType === 'redirect') {
|
|
132
|
-
|
|
172
|
+
// 使用字符串原生的startsWith方法更高效
|
|
173
|
+
return fullUrl.startsWith(pattern);
|
|
133
174
|
}
|
|
134
175
|
|
|
135
176
|
// 以下是默认的匹配逻辑(用于response类型等)
|
|
136
177
|
|
|
137
|
-
// 精确匹配
|
|
178
|
+
// 精确匹配 - 最高效的匹配方式
|
|
138
179
|
if (pattern === url) {
|
|
139
180
|
return true;
|
|
140
181
|
}
|
|
141
182
|
|
|
142
183
|
// 通配符匹配
|
|
143
184
|
if (pattern.includes('*')) {
|
|
144
|
-
//
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
185
|
+
// 使用缓存的正则表达式
|
|
186
|
+
const cacheKey = `wildcard:${pattern}`;
|
|
187
|
+
let regex = regexCache.get(cacheKey);
|
|
188
|
+
|
|
189
|
+
if (!regex) {
|
|
190
|
+
// 需要转换为正则表达式,处理特殊字符
|
|
191
|
+
const regexPattern = pattern
|
|
192
|
+
.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&') // 转义特殊字符
|
|
193
|
+
.replace(/\*/g, '.*'); // 将 * 替换为 .*
|
|
194
|
+
|
|
195
|
+
regex = new RegExp('^' + regexPattern + '$');
|
|
196
|
+
regexCache.set(cacheKey, regex);
|
|
197
|
+
}
|
|
148
198
|
|
|
149
|
-
const regex = new RegExp('^' + regexPattern + '$');
|
|
150
199
|
return regex.test(url);
|
|
151
200
|
}
|
|
152
201
|
|
|
153
202
|
// 正则表达式匹配
|
|
154
|
-
if (pattern.startsWith('/') && pattern.endsWith('/')) {
|
|
155
|
-
|
|
156
|
-
const
|
|
203
|
+
if (pattern.startsWith('/') && pattern.length > 2 && pattern.endsWith('/')) {
|
|
204
|
+
// 使用缓存的正则表达式
|
|
205
|
+
const cacheKey = `regex:${pattern}`;
|
|
206
|
+
let regex = regexCache.get(cacheKey);
|
|
207
|
+
|
|
208
|
+
if (!regex) {
|
|
209
|
+
const regexStr = pattern.slice(1, -1);
|
|
210
|
+
regex = new RegExp(regexStr);
|
|
211
|
+
regexCache.set(cacheKey, regex);
|
|
212
|
+
}
|
|
213
|
+
|
|
157
214
|
return regex.test(url);
|
|
158
215
|
}
|
|
159
216
|
} catch (e) {
|
|
@@ -192,17 +249,111 @@ const validateRedirectRule = (interface) => {
|
|
|
192
249
|
return true;
|
|
193
250
|
};
|
|
194
251
|
|
|
252
|
+
// 生成随机数工具函数
|
|
253
|
+
const generateRandomValue = (pattern) => {
|
|
254
|
+
// 如果不是以@开头的模式,直接返回原值
|
|
255
|
+
if (!pattern || !pattern.startsWith('@')) {
|
|
256
|
+
return pattern;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const formatPattern = pattern.substring(1); // 去掉@前缀
|
|
260
|
+
// 使用单次正则替换代替每个字符的遍历
|
|
261
|
+
return formatPattern.replace(/x/g, () => {
|
|
262
|
+
// 使用一个固定的字符集,避免生成不兼容的字符
|
|
263
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
264
|
+
return chars.charAt(Math.floor(Math.random() * chars.length));
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// 构建重定向规则
|
|
269
|
+
const buildRedirectRule = (fullUrl, targetUrl, customHeaders, pattern, isUrlRedirect = 0) => {
|
|
270
|
+
// 构建目标URL
|
|
271
|
+
let finalTargetUrl;
|
|
272
|
+
if(!isUrlRedirect){
|
|
273
|
+
finalTargetUrl = fullUrl.replace(pattern, targetUrl);
|
|
274
|
+
}else{
|
|
275
|
+
finalTargetUrl = `redirect://${targetUrl}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 定义结果数组,避免多次字符串拼接
|
|
279
|
+
const rules = [`${fullUrl} ${finalTargetUrl}`];
|
|
280
|
+
|
|
281
|
+
// 如果有自定义请求头,添加到规则中
|
|
282
|
+
if (customHeaders && Object.keys(customHeaders).length > 0) {
|
|
283
|
+
// 为每个请求头创建一个规则行
|
|
284
|
+
Object.entries(customHeaders).forEach(([key, value]) => {
|
|
285
|
+
// 处理随机值
|
|
286
|
+
const processedValue = generateRandomValue(value);
|
|
287
|
+
|
|
288
|
+
// 添加到规则数组
|
|
289
|
+
rules.push(`${fullUrl} headerReplace://req.${key}:/(.*)/=${processedValue}`);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 最后才将所有规则合并为一个字符串,减少字符串操作
|
|
294
|
+
return rules.join('\n');
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// 缓存管理接口 - 简化版本
|
|
298
|
+
const cacheManager = {
|
|
299
|
+
// 清除缓存
|
|
300
|
+
clearCache: () => {
|
|
301
|
+
logInfo('主动清除接口缓存');
|
|
302
|
+
enabledInterfacesCache = null;
|
|
303
|
+
cacheTime = 0;
|
|
304
|
+
cacheVersion++;
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
// 获取当前缓存状态
|
|
308
|
+
getStatus: () => {
|
|
309
|
+
return {
|
|
310
|
+
enabled: {
|
|
311
|
+
cached: !!enabledInterfacesCache,
|
|
312
|
+
cacheTime: cacheTime,
|
|
313
|
+
itemCount: enabledInterfacesCache ? enabledInterfacesCache.length : 0
|
|
314
|
+
},
|
|
315
|
+
cacheVersion,
|
|
316
|
+
configInterval: CONFIG.CACHE_INTERVAL
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
195
321
|
// Whistle 规则服务器实现
|
|
196
322
|
module.exports = (server, options) => {
|
|
323
|
+
// 优化:缓存常用路径匹配
|
|
324
|
+
const debugConfigPath = '/_debug/config';
|
|
325
|
+
const debugEnabledInterfacesPath = '/_debug/enabled-interfaces';
|
|
326
|
+
const clearCachePath = '/_flush_cache';
|
|
327
|
+
|
|
197
328
|
server.on('request', (req, res) => {
|
|
198
329
|
const oReq = req.originalReq;
|
|
199
330
|
|
|
200
331
|
// 提取请求相关信息
|
|
201
332
|
const fullUrl = oReq.url;
|
|
202
|
-
|
|
333
|
+
logDebug(`原始URL: ${fullUrl}`);
|
|
203
334
|
|
|
204
|
-
//
|
|
205
|
-
if (fullUrl.
|
|
335
|
+
// 处理缓存刷新请求
|
|
336
|
+
if (fullUrl.startsWith(clearCachePath)) {
|
|
337
|
+
cacheManager.clearCache();
|
|
338
|
+
|
|
339
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
340
|
+
res.end(JSON.stringify({
|
|
341
|
+
success: true,
|
|
342
|
+
message: '缓存已刷新',
|
|
343
|
+
cacheStatus: cacheManager.getStatus()
|
|
344
|
+
}));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 特殊调试路径,查看缓存状态
|
|
349
|
+
if (fullUrl.endsWith('/_cache_status')) {
|
|
350
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
351
|
+
res.end(JSON.stringify(cacheManager.getStatus()));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 特殊调试路径,使用更高效的匹配
|
|
356
|
+
if (fullUrl.endsWith(debugConfigPath)) {
|
|
206
357
|
try {
|
|
207
358
|
const configs = {
|
|
208
359
|
interfaces: fs.existsSync(INTERFACES_FILE) ? fs.readJsonSync(INTERFACES_FILE) : null,
|
|
@@ -211,23 +362,23 @@ module.exports = (server, options) => {
|
|
|
211
362
|
res.end(JSON.stringify(configs, null, 2));
|
|
212
363
|
return;
|
|
213
364
|
} catch (err) {
|
|
214
|
-
|
|
365
|
+
logError(`读取配置文件失败: ${err.message}`);
|
|
215
366
|
res.end(`读取配置文件失败: ${err.message}`);
|
|
216
367
|
return;
|
|
217
368
|
}
|
|
218
369
|
}
|
|
219
370
|
|
|
220
371
|
// 调试路径,查看启用的接口
|
|
221
|
-
if (fullUrl.endsWith(
|
|
372
|
+
if (fullUrl.endsWith(debugEnabledInterfacesPath)) {
|
|
222
373
|
try {
|
|
223
|
-
const enabledInterfaces =
|
|
374
|
+
const enabledInterfaces = loadEnabledInterfaces();
|
|
224
375
|
res.end(JSON.stringify({
|
|
225
376
|
count: enabledInterfaces.length,
|
|
226
377
|
interfaces: enabledInterfaces
|
|
227
378
|
}, null, 2));
|
|
228
379
|
return;
|
|
229
380
|
} catch (err) {
|
|
230
|
-
|
|
381
|
+
logError(`获取启用接口失败: ${err.message}`);
|
|
231
382
|
res.end(`获取启用接口失败: ${err.message}`);
|
|
232
383
|
return;
|
|
233
384
|
}
|
|
@@ -238,7 +389,7 @@ module.exports = (server, options) => {
|
|
|
238
389
|
try {
|
|
239
390
|
parsedUrl = url.parse(fullUrl);
|
|
240
391
|
} catch (err) {
|
|
241
|
-
|
|
392
|
+
logError(`解析URL失败: ${err.message}`);
|
|
242
393
|
// 解析失败时,不处理请求
|
|
243
394
|
res.end('');
|
|
244
395
|
return;
|
|
@@ -248,26 +399,25 @@ module.exports = (server, options) => {
|
|
|
248
399
|
const method = oReq.method;
|
|
249
400
|
const path = parsedUrl.pathname || '';
|
|
250
401
|
|
|
251
|
-
|
|
402
|
+
logDebug(`请求方法: ${method}, 请求路径: ${path}`);
|
|
252
403
|
|
|
253
|
-
//
|
|
254
|
-
const enabledInterfaces =
|
|
255
|
-
log(`已加载 ${enabledInterfaces.length} 个接口配置`);
|
|
404
|
+
// 加载启用的接口配置(使用缓存优化)
|
|
405
|
+
const enabledInterfaces = loadEnabledInterfaces();
|
|
256
406
|
|
|
257
407
|
if (enabledInterfaces.length === 0) {
|
|
258
|
-
|
|
408
|
+
logDebug('没有找到启用的接口配置,跳过处理');
|
|
259
409
|
res.end('');
|
|
260
410
|
return;
|
|
261
411
|
}
|
|
262
412
|
|
|
263
|
-
//
|
|
413
|
+
// 优化:使用 find 查找匹配的接口,只遍历一次数组
|
|
264
414
|
const matchedInterface = enabledInterfaces.find(intf => {
|
|
265
415
|
// 获取URL模式
|
|
266
416
|
const pattern = intf.urlPattern || '';
|
|
267
417
|
const proxyType = intf.proxyType || 'response';
|
|
268
418
|
|
|
269
419
|
if (!pattern) {
|
|
270
|
-
|
|
420
|
+
logDebug(`接口 ${intf.id || intf.name || '未知'} 缺少URL模式`);
|
|
271
421
|
return false;
|
|
272
422
|
}
|
|
273
423
|
|
|
@@ -279,7 +429,7 @@ module.exports = (server, options) => {
|
|
|
279
429
|
return false;
|
|
280
430
|
}
|
|
281
431
|
|
|
282
|
-
|
|
432
|
+
logDebug(`URL模式匹配成功: ${fullUrl} 匹配 ${pattern} (${proxyType}模式)`);
|
|
283
433
|
|
|
284
434
|
// 检查HTTP方法匹配
|
|
285
435
|
const methodField = intf.httpMethod || intf.method || '';
|
|
@@ -290,18 +440,18 @@ module.exports = (server, options) => {
|
|
|
290
440
|
methodValue === method;
|
|
291
441
|
|
|
292
442
|
if (!methodMatches) {
|
|
293
|
-
|
|
443
|
+
logDebug(`方法不匹配: 请求方法 ${method}, 接口方法 ${methodValue}`);
|
|
294
444
|
return false;
|
|
295
445
|
}
|
|
296
446
|
|
|
297
|
-
|
|
447
|
+
logDebug(`方法匹配成功: ${method} 匹配 ${methodValue || 'ALL'}`);
|
|
298
448
|
|
|
299
449
|
return true;
|
|
300
450
|
});
|
|
301
451
|
|
|
302
452
|
// 如果找到匹配的接口,处理对应的规则
|
|
303
453
|
if (matchedInterface) {
|
|
304
|
-
|
|
454
|
+
logInfo(`找到匹配接口 "${matchedInterface.name || matchedInterface.id || '未知'}" 用于 ${method} ${path}`);
|
|
305
455
|
|
|
306
456
|
// 处理不同的代理类型
|
|
307
457
|
const proxyType = matchedInterface.proxyType || 'response';
|
|
@@ -309,14 +459,14 @@ module.exports = (server, options) => {
|
|
|
309
459
|
// 优先级判断:首先处理 response 类型
|
|
310
460
|
if (proxyType === 'response') {
|
|
311
461
|
// 返回指向插件服务的规则
|
|
312
|
-
|
|
462
|
+
logDebug(`接口 "${matchedInterface.name}" 使用模拟响应模式`);
|
|
313
463
|
res.end(`${fullUrl} mock-plugins://`);
|
|
314
464
|
return;
|
|
315
465
|
}
|
|
316
466
|
|
|
317
467
|
// 对于redirect和url_redirect类型,验证规则有效性
|
|
318
468
|
if ((proxyType === 'redirect' || proxyType === 'url_redirect') && !validateRedirectRule(matchedInterface)) {
|
|
319
|
-
|
|
469
|
+
logWarn(`接口 "${matchedInterface.name}" 的重定向规则无效,跳过处理`);
|
|
320
470
|
res.end('');
|
|
321
471
|
return;
|
|
322
472
|
}
|
|
@@ -325,27 +475,30 @@ module.exports = (server, options) => {
|
|
|
325
475
|
if (proxyType === 'redirect') {
|
|
326
476
|
const targetUrl = matchedInterface.targetUrl.trim();
|
|
327
477
|
const pattern = matchedInterface.urlPattern.trim();
|
|
328
|
-
|
|
478
|
+
logDebug(`接口 "${matchedInterface.name}" 使用重定向模式,模式: ${pattern}, 目标URL: ${targetUrl}`);
|
|
329
479
|
|
|
330
480
|
// 确保目标URL合法
|
|
331
481
|
if (!isValidTargetUrl(targetUrl)) {
|
|
332
|
-
|
|
482
|
+
logWarn(`目标URL不合法: ${targetUrl},跳过处理`);
|
|
333
483
|
res.end('');
|
|
334
484
|
return;
|
|
335
485
|
}
|
|
336
486
|
|
|
337
487
|
// 执行重定向:将fullUrl中匹配的pattern替换为targetUrl
|
|
338
|
-
//
|
|
339
|
-
if (fullUrl.
|
|
340
|
-
//
|
|
341
|
-
const
|
|
488
|
+
// 使用startsWith优化匹配效率
|
|
489
|
+
if (fullUrl.startsWith(pattern)) {
|
|
490
|
+
// 获取自定义请求头
|
|
491
|
+
const customHeaders = matchedInterface.customHeaders || {};
|
|
492
|
+
|
|
493
|
+
// 构建重定向规则,包括自定义请求头
|
|
494
|
+
const redirectRule = buildRedirectRule(fullUrl, targetUrl, customHeaders, pattern, 0);
|
|
342
495
|
|
|
343
|
-
//
|
|
344
|
-
res.end(
|
|
345
|
-
|
|
496
|
+
// 返回规则
|
|
497
|
+
res.end(redirectRule);
|
|
498
|
+
logDebug(`重定向规则: ${redirectRule}`);
|
|
346
499
|
return;
|
|
347
500
|
} else {
|
|
348
|
-
|
|
501
|
+
logDebug(`URL ${fullUrl} 不以 ${pattern} 开头,跳过处理`);
|
|
349
502
|
res.end('');
|
|
350
503
|
return;
|
|
351
504
|
}
|
|
@@ -354,34 +507,40 @@ module.exports = (server, options) => {
|
|
|
354
507
|
// 处理 url_redirect 类型:完全匹配 fullPath 并直接返回目标URL
|
|
355
508
|
if (proxyType === 'url_redirect') {
|
|
356
509
|
const targetUrl = matchedInterface.targetUrl.trim();
|
|
357
|
-
|
|
510
|
+
logDebug(`接口 "${matchedInterface.name}" 使用URL重定向模式,目标URL: ${targetUrl}`);
|
|
358
511
|
|
|
359
512
|
// 确保目标URL合法
|
|
360
513
|
if (!isValidTargetUrl(targetUrl)) {
|
|
361
|
-
|
|
514
|
+
logWarn(`目标URL不合法: ${targetUrl},跳过处理`);
|
|
362
515
|
res.end('');
|
|
363
516
|
return;
|
|
364
517
|
}
|
|
365
518
|
|
|
366
519
|
// 对于url_redirect,我们需要完全匹配
|
|
367
520
|
if (fullUrl === matchedInterface.urlPattern) {
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
521
|
+
// 获取自定义请求头
|
|
522
|
+
const customHeaders = matchedInterface.customHeaders || {};
|
|
523
|
+
|
|
524
|
+
// 构建重定向规则,包括自定义请求头
|
|
525
|
+
const redirectRule = buildRedirectRule(fullUrl, targetUrl, customHeaders, pattern, 1);
|
|
526
|
+
|
|
527
|
+
// 返回规则
|
|
528
|
+
res.end(redirectRule);
|
|
529
|
+
logDebug(`URL重定向规则: ${redirectRule}`);
|
|
371
530
|
return;
|
|
372
531
|
} else {
|
|
373
|
-
|
|
532
|
+
logDebug(`URL ${fullUrl} 不完全匹配 ${matchedInterface.urlPattern},跳过处理`);
|
|
374
533
|
res.end('');
|
|
375
534
|
return;
|
|
376
535
|
}
|
|
377
536
|
}
|
|
378
537
|
|
|
379
538
|
// 默认情况下使用mock-plugins处理
|
|
380
|
-
|
|
539
|
+
logDebug(`接口 "${matchedInterface.name}" 使用默认处理模式`);
|
|
381
540
|
res.end(`${fullUrl} mock-plugins://`);
|
|
382
541
|
} else {
|
|
383
542
|
// 如果没有匹配的接口,返回空字符串,Whistle 会继续处理下一个规则
|
|
384
|
-
|
|
543
|
+
logDebug(`未找到匹配接口,跳过处理 ${method} ${path}`);
|
|
385
544
|
res.end('');
|
|
386
545
|
}
|
|
387
546
|
});
|