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.
- package/app/cgi-bin/logs.js +38 -29
- package/app/cgi-bin/test-interface.js +182 -35
- package/app/public/js/index.js +1 -1
- package/lib/dataManager.js +6 -0
- package/lib/ruleManager.js +62 -30
- package/lib/rules-server.js +126 -5
- package/lib/server.js +74 -3
- package/package.json +1 -1
- package/rules.txt +1 -1
- package/ui/src/pages/InterfaceManagement.js +430 -136
package/app/cgi-bin/logs.js
CHANGED
|
@@ -15,6 +15,15 @@ module.exports = function(req, res) {
|
|
|
15
15
|
const page = parseInt(req.query.page) || 1;
|
|
16
16
|
const pageSize = parseInt(req.query.pageSize) || 20;
|
|
17
17
|
|
|
18
|
+
// 检查参数是否合法
|
|
19
|
+
if (isNaN(page) || page < 1 || isNaN(pageSize) || pageSize < 1) {
|
|
20
|
+
return res.status(400).json({
|
|
21
|
+
code: 400,
|
|
22
|
+
message: '无效的分页参数',
|
|
23
|
+
data: null
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
// 确保日志文件存在
|
|
19
28
|
if (!fs.existsSync(logsFile)) {
|
|
20
29
|
fs.writeJsonSync(logsFile, { logs: [] }, { spaces: 2 });
|
|
@@ -31,39 +40,33 @@ module.exports = function(req, res) {
|
|
|
31
40
|
});
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
// 读取日志
|
|
35
|
-
|
|
43
|
+
// 读取日志 - 使用流式处理大文件
|
|
44
|
+
let logsData;
|
|
45
|
+
try {
|
|
46
|
+
logsData = fs.readJsonSync(logsFile, { throws: false }) || { logs: [] };
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error('读取日志文件失败:', err);
|
|
49
|
+
// 文件损坏或格式错误时,重置日志
|
|
50
|
+
fs.writeJsonSync(logsFile, { logs: [] }, { spaces: 2 });
|
|
51
|
+
logsData = { logs: [] };
|
|
52
|
+
}
|
|
36
53
|
|
|
37
54
|
// 确保日志结构正确
|
|
38
55
|
if (!logsData || !logsData.logs || !Array.isArray(logsData.logs)) {
|
|
39
|
-
logsData
|
|
56
|
+
logsData = { logs: [] };
|
|
40
57
|
}
|
|
41
58
|
|
|
42
|
-
//
|
|
43
|
-
let logs = logsData.logs
|
|
44
|
-
if (!log) return null;
|
|
45
|
-
|
|
46
|
-
// 标准化事件类型字段,确保兼容前端过滤
|
|
47
|
-
// 将type转换为eventType确保前端能够正确过滤
|
|
48
|
-
return {
|
|
49
|
-
id: log.id || Date.now().toString(),
|
|
50
|
-
timestamp: log.timestamp || new Date().toISOString(),
|
|
51
|
-
eventType: log.eventType || log.type || 'unknown',
|
|
52
|
-
type: log.type || log.eventType || 'unknown',
|
|
53
|
-
method: log.method || '',
|
|
54
|
-
url: log.url || '',
|
|
55
|
-
status: log.status || '',
|
|
56
|
-
message: log.message || '',
|
|
57
|
-
pattern: log.pattern || '',
|
|
58
|
-
...log // 保留原有其他字段
|
|
59
|
-
};
|
|
60
|
-
}).filter(Boolean); // 过滤掉null
|
|
59
|
+
// 只对需要处理的日志数据进行处理,减少内存使用
|
|
60
|
+
let logs = logsData.logs;
|
|
61
61
|
|
|
62
62
|
// 根据类型过滤
|
|
63
63
|
if (type && type !== 'all') {
|
|
64
64
|
try {
|
|
65
65
|
logs = logs.filter(log =>
|
|
66
|
-
log && (
|
|
66
|
+
log && (
|
|
67
|
+
(log.eventType && log.eventType === type) ||
|
|
68
|
+
(log.type && log.type === type)
|
|
69
|
+
)
|
|
67
70
|
);
|
|
68
71
|
} catch (err) {
|
|
69
72
|
console.error('按类型过滤日志失败:', err);
|
|
@@ -74,14 +77,14 @@ module.exports = function(req, res) {
|
|
|
74
77
|
// 根据关键词过滤
|
|
75
78
|
if (keyword) {
|
|
76
79
|
try {
|
|
80
|
+
const lowercaseKeyword = keyword.toLowerCase();
|
|
77
81
|
logs = logs.filter(log => {
|
|
78
82
|
if (!log) return false;
|
|
79
83
|
|
|
80
84
|
// 检查关键字段是否包含关键词
|
|
81
|
-
const url =
|
|
82
|
-
const pattern =
|
|
83
|
-
const message =
|
|
84
|
-
const lowercaseKeyword = keyword.toLowerCase();
|
|
85
|
+
const url = typeof log.url === 'string' ? log.url.toLowerCase() : '';
|
|
86
|
+
const pattern = typeof log.pattern === 'string' ? log.pattern.toLowerCase() : '';
|
|
87
|
+
const message = typeof log.message === 'string' ? log.message.toLowerCase() : '';
|
|
85
88
|
|
|
86
89
|
return url.includes(lowercaseKeyword) ||
|
|
87
90
|
pattern.includes(lowercaseKeyword) ||
|
|
@@ -97,9 +100,15 @@ module.exports = function(req, res) {
|
|
|
97
100
|
const total = logs.length;
|
|
98
101
|
const totalPages = Math.ceil(total / pageSize);
|
|
99
102
|
|
|
100
|
-
// 分页
|
|
103
|
+
// 分页 - 只返回当前页的数据
|
|
101
104
|
const startIndex = (page - 1) * pageSize;
|
|
102
|
-
|
|
105
|
+
// 确保不会越界
|
|
106
|
+
const endIndex = Math.min(startIndex + pageSize, total);
|
|
107
|
+
const pagedLogs = logs.slice(startIndex, endIndex);
|
|
108
|
+
|
|
109
|
+
// 清除大型对象引用,帮助垃圾回收
|
|
110
|
+
logs = null;
|
|
111
|
+
logsData = null;
|
|
103
112
|
|
|
104
113
|
return res.json({
|
|
105
114
|
code: 0,
|
|
@@ -89,46 +89,193 @@ module.exports = async function(req, res) {
|
|
|
89
89
|
|
|
90
90
|
const startTime = Date.now();
|
|
91
91
|
|
|
92
|
-
//
|
|
93
|
-
let
|
|
92
|
+
// 根据代理类型处理不同的响应
|
|
93
|
+
let responseData;
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
95
|
+
// 检查 URL 是否匹配模式
|
|
96
|
+
const isUrlMatchPattern = (url, pattern, proxyType) => {
|
|
97
|
+
if (!url || !pattern) return false;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// 对于url_redirect类型,需要完全匹配
|
|
101
|
+
if (proxyType === 'url_redirect') {
|
|
102
|
+
return url === pattern;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
|
|
106
|
+
if (proxyType === 'redirect') {
|
|
107
|
+
return url.indexOf(pattern) === 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 默认的匹配逻辑(用于response类型等)
|
|
111
|
+
// 如果是正则表达式
|
|
112
|
+
if (pattern.startsWith('/') && pattern.endsWith('/')) {
|
|
113
|
+
const regex = new RegExp(pattern.slice(1, -1));
|
|
114
|
+
return regex.test(url);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 如果是通配符模式
|
|
118
|
+
if (pattern.includes('*')) {
|
|
119
|
+
const regexPattern = pattern
|
|
120
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
121
|
+
.replace(/\*/g, '.*');
|
|
122
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
123
|
+
return regex.test(url);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 精确匹配
|
|
127
|
+
return url === pattern;
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error('URL匹配检查失败:', e);
|
|
130
|
+
return false;
|
|
104
131
|
}
|
|
105
|
-
}
|
|
106
|
-
// 如果Mock失败,使用原始内容
|
|
107
|
-
}
|
|
132
|
+
};
|
|
108
133
|
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
134
|
+
// 处理不同的代理类型
|
|
135
|
+
switch (targetInterface.proxyType) {
|
|
136
|
+
case 'response':
|
|
137
|
+
// 处理内容,支持 Mock.js 模板语法
|
|
138
|
+
let responseBody = targetInterface.responseContent;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// 如果响应内容是JSON,则尝试应用Mock.js模板
|
|
142
|
+
if (targetInterface.contentType && targetInterface.contentType.includes('json')) {
|
|
143
|
+
// 尝试解析JSON
|
|
144
|
+
const jsonTemplate = JSON.parse(responseBody);
|
|
145
|
+
// 使用Mock.js生成随机数据
|
|
146
|
+
const mockData = Mock.mock(jsonTemplate);
|
|
147
|
+
// 将结果转换回字符串
|
|
148
|
+
responseBody = JSON.stringify(mockData);
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
// 如果Mock失败,使用原始内容
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 模拟接口处理延迟
|
|
155
|
+
const delay = targetInterface.responseDelay || 0;
|
|
156
|
+
if (delay > 0) {
|
|
157
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const endTime = Date.now();
|
|
161
|
+
const duration = endTime - startTime;
|
|
162
|
+
|
|
163
|
+
// 模拟接口处理
|
|
164
|
+
responseData = {
|
|
165
|
+
statusCode: targetInterface.httpStatus || 200,
|
|
166
|
+
contentType: targetInterface.contentType || 'application/json; charset=utf-8',
|
|
167
|
+
responseBody: responseBody,
|
|
168
|
+
mockInfo: {
|
|
169
|
+
matchedPattern: targetInterface.urlPattern,
|
|
170
|
+
requestMethod: targetInterface.httpMethod,
|
|
171
|
+
delay: delay,
|
|
172
|
+
duration: duration,
|
|
173
|
+
timestamp: new Date().toISOString()
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
break;
|
|
177
|
+
|
|
178
|
+
case 'redirect':
|
|
179
|
+
// 处理重定向
|
|
180
|
+
const redirectTarget = targetInterface.targetUrl || '';
|
|
181
|
+
|
|
182
|
+
// 验证目标URL是否有效
|
|
183
|
+
if (!redirectTarget) {
|
|
184
|
+
return res.status(400).json({
|
|
185
|
+
code: 400,
|
|
186
|
+
message: '重定向目标URL不能为空',
|
|
187
|
+
data: null
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
// 验证URL格式
|
|
193
|
+
new URL(redirectTarget);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return res.status(400).json({
|
|
196
|
+
code: 400,
|
|
197
|
+
message: '重定向目标URL格式无效,必须包含http://或https://',
|
|
198
|
+
data: null
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 验证URL匹配(前缀匹配)
|
|
203
|
+
if (url.indexOf(targetInterface.urlPattern) !== 0) {
|
|
204
|
+
return res.status(400).json({
|
|
205
|
+
code: 400,
|
|
206
|
+
message: `测试URL不匹配重定向规则,URL必须以 ${targetInterface.urlPattern} 开头`,
|
|
207
|
+
data: null
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
responseData = {
|
|
212
|
+
statusCode: 302,
|
|
213
|
+
contentType: 'application/json; charset=utf-8',
|
|
214
|
+
proxyType: 'redirect',
|
|
215
|
+
targetUrl: redirectTarget,
|
|
216
|
+
mockInfo: {
|
|
217
|
+
matchedPattern: targetInterface.urlPattern,
|
|
218
|
+
requestMethod: targetInterface.httpMethod,
|
|
219
|
+
timestamp: new Date().toISOString()
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
break;
|
|
223
|
+
|
|
224
|
+
case 'url_redirect':
|
|
225
|
+
// 处理URL重定向
|
|
226
|
+
const baseUrl = targetInterface.targetUrl || '';
|
|
227
|
+
|
|
228
|
+
// 验证基础URL是否有效
|
|
229
|
+
if (!baseUrl) {
|
|
230
|
+
return res.status(400).json({
|
|
231
|
+
code: 400,
|
|
232
|
+
message: 'URL重定向目标URL不能为空',
|
|
233
|
+
data: null
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// 验证URL格式
|
|
239
|
+
new URL(baseUrl);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
return res.status(400).json({
|
|
242
|
+
code: 400,
|
|
243
|
+
message: 'URL重定向目标URL格式无效,必须包含http://或https://',
|
|
244
|
+
data: null
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 验证URL完全匹配
|
|
249
|
+
if (url !== targetInterface.urlPattern) {
|
|
250
|
+
return res.status(400).json({
|
|
251
|
+
code: 400,
|
|
252
|
+
message: `测试URL不匹配URL重定向规则,URL必须完全匹配 ${targetInterface.urlPattern}`,
|
|
253
|
+
data: null
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
responseData = {
|
|
258
|
+
statusCode: 302,
|
|
259
|
+
contentType: 'application/json; charset=utf-8',
|
|
260
|
+
proxyType: 'url_redirect',
|
|
261
|
+
targetUrl: baseUrl,
|
|
262
|
+
mockInfo: {
|
|
263
|
+
matchedPattern: targetInterface.urlPattern,
|
|
264
|
+
requestMethod: targetInterface.httpMethod,
|
|
265
|
+
timestamp: new Date().toISOString()
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
break;
|
|
269
|
+
|
|
270
|
+
default:
|
|
271
|
+
// 不支持的代理类型
|
|
272
|
+
return res.status(400).json({
|
|
273
|
+
code: 400,
|
|
274
|
+
message: `不支持的代理类型: ${targetInterface.proxyType || 'undefined'}`,
|
|
275
|
+
data: null
|
|
276
|
+
});
|
|
113
277
|
}
|
|
114
278
|
|
|
115
|
-
const endTime = Date.now();
|
|
116
|
-
const duration = endTime - startTime;
|
|
117
|
-
|
|
118
|
-
// 模拟接口处理
|
|
119
|
-
const responseData = {
|
|
120
|
-
statusCode: targetInterface.httpStatus || 200,
|
|
121
|
-
contentType: targetInterface.contentType || 'application/json; charset=utf-8',
|
|
122
|
-
responseBody: responseBody,
|
|
123
|
-
mockInfo: {
|
|
124
|
-
matchedPattern: targetInterface.urlPattern,
|
|
125
|
-
requestMethod: targetInterface.httpMethod,
|
|
126
|
-
delay: delay,
|
|
127
|
-
duration: duration,
|
|
128
|
-
timestamp: new Date().toISOString()
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
279
|
return res.json({
|
|
133
280
|
code: 0,
|
|
134
281
|
message: '成功',
|