whistle.mock-plugins 1.0.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.
Files changed (68) hide show
  1. package/API.md +247 -0
  2. package/README.md +91 -0
  3. package/app/cgi-bin/features.js +285 -0
  4. package/app/cgi-bin/index.js +31 -0
  5. package/app/cgi-bin/interfaces.js +409 -0
  6. package/app/cgi-bin/logs.js +151 -0
  7. package/app/cgi-bin/test-interface.js +152 -0
  8. package/app/public/index.html +1 -0
  9. package/app/public/js/index.js +2 -0
  10. package/app/public/js/index.js.LICENSE.txt +150 -0
  11. package/app/util/index.js +24 -0
  12. package/index.html +52 -0
  13. package/initialize.js +32 -0
  14. package/lib/dataManager.js +500 -0
  15. package/lib/index.js +3 -0
  16. package/lib/models.js +64 -0
  17. package/lib/routes/features.js +213 -0
  18. package/lib/routes/interfaces.js +304 -0
  19. package/lib/routes/proxy.js +239 -0
  20. package/lib/ruleManager.js +551 -0
  21. package/lib/rules-server.js +267 -0
  22. package/lib/server.js +805 -0
  23. package/lib/storage.js +16 -0
  24. package/lib/ui-server.js +68 -0
  25. package/package.json +51 -0
  26. package/public/bundle.js +2 -0
  27. package/public/bundle.js.LICENSE.txt +38 -0
  28. package/public/index.html +1 -0
  29. package/public/plugin.mock-plugin.html +14 -0
  30. package/rules.txt +22 -0
  31. package/test/rulesServer.test.js +120 -0
  32. package/ui/src/App.css +173 -0
  33. package/ui/src/App.js +24 -0
  34. package/ui/src/components/AppLayout.js +149 -0
  35. package/ui/src/components/FeatureCard.js +53 -0
  36. package/ui/src/components/Layout.js +94 -0
  37. package/ui/src/components/MainNav.js +42 -0
  38. package/ui/src/components/PageContainer.js +16 -0
  39. package/ui/src/index.html +11 -0
  40. package/ui/src/index.js +43 -0
  41. package/ui/src/pages/FileProxy.js +235 -0
  42. package/ui/src/pages/FilesPage.js +292 -0
  43. package/ui/src/pages/Home.js +128 -0
  44. package/ui/src/pages/HomePage.js +195 -0
  45. package/ui/src/pages/InterfaceManagement.js +829 -0
  46. package/ui/src/pages/InterfacePage.js +342 -0
  47. package/ui/src/pages/MockData.js +472 -0
  48. package/ui/src/pages/Settings.js +192 -0
  49. package/ui/src/pages/SettingsPage.js +165 -0
  50. package/ui/src/pages/UrlRedirect.js +225 -0
  51. package/ui/src/pages/Welcome.js +155 -0
  52. package/ui/src/services/api.js +186 -0
  53. package/ui/src/styles/app-layout.css +337 -0
  54. package/ui/src/styles/feature-card.css +172 -0
  55. package/ui/src/styles/file-proxy.css +301 -0
  56. package/ui/src/styles/global.css +224 -0
  57. package/ui/src/styles/home.css +123 -0
  58. package/ui/src/styles/index.css +419 -0
  59. package/ui/src/styles/interface-management.css +694 -0
  60. package/ui/src/styles/layout.css +159 -0
  61. package/ui/src/styles/logs-page.css +427 -0
  62. package/ui/src/styles/main-nav.css +52 -0
  63. package/ui/src/styles/mock-data.css +850 -0
  64. package/ui/src/styles/page-container.css +11 -0
  65. package/ui/src/styles/settings.css +208 -0
  66. package/ui/src/styles/url-redirect.css +279 -0
  67. package/ui/src/styles/welcome.css +282 -0
  68. package/webpack.config.js +49 -0
@@ -0,0 +1,551 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const url = require('url');
4
+ const Mock = require('mockjs');
5
+
6
+ /**
7
+ * 规则管理模块,负责处理 Whistle 规则和代理请求
8
+ */
9
+ const ruleManager = {
10
+ /**
11
+ * 初始化规则管理器
12
+ * @param {object} options 选项对象
13
+ * @param {object} options.server Whistle 服务器对象
14
+ * @param {object} options.config Whistle 配置对象
15
+ * @param {object} options.dataManager 数据管理器对象
16
+ */
17
+ init(options) {
18
+ this.server = options.server;
19
+ this.config = options.config;
20
+ this.dataManager = options.dataManager;
21
+ this.baseDir = options.config.baseDir;
22
+
23
+ // 设置日志函数
24
+ this.log = options.log || console.log;
25
+ this.log('规则管理器初始化完成, baseDir: ' + this.baseDir);
26
+
27
+ // 确保 mock 数据目录存在
28
+ this.mockDataDir = path.join(this.baseDir, 'mock-data');
29
+ if (!fs.existsSync(this.mockDataDir)) {
30
+ fs.mkdirSync(this.mockDataDir, { recursive: true });
31
+ this.log('创建mock数据目录: ' + this.mockDataDir);
32
+ }
33
+ },
34
+
35
+ /**
36
+ * 处理代理请求
37
+ * @param {object} req 请求对象
38
+ * @param {object} res 响应对象
39
+ * @param {string} ruleValue 规则值
40
+ * @param {function} next 下一个中间件
41
+ * @returns {Promise<object>} 处理结果
42
+ */
43
+ async handleRequest(req, res, ruleValue, next) {
44
+ // 只记录请求开始和规则值
45
+ this.log(`[规则处理器] 收到请求: ${req.method} ${req.url}, 规则值: ${ruleValue || '无'}`);
46
+
47
+ // 获取所有启用的接口配置
48
+ const enabledInterfaces = await this.dataManager.getEnabledInterfaces();
49
+
50
+ // 只记录接口数量,不记录详情
51
+ this.log(`[规则处理器] 启用的接口数量: ${enabledInterfaces ? enabledInterfaces.length : 0}`);
52
+
53
+ if (!enabledInterfaces || enabledInterfaces.length === 0) {
54
+ this.log('[规则处理器] 没有启用的接口可用,透传请求');
55
+ return { handled: false };
56
+ }
57
+
58
+ // 解析请求路径
59
+ const parsedUrl = url.parse(req.url);
60
+ const requestPath = parsedUrl.pathname;
61
+
62
+ // 查找匹配的接口
63
+ const matchedInterface = this.findMatchingInterface(enabledInterfaces, requestPath, req.method);
64
+
65
+ if (!matchedInterface) {
66
+ this.log(`[规则处理器] 未找到匹配的接口,请求将透传: ${req.method} ${requestPath}`);
67
+
68
+ // 从请求头获取原始完整URL
69
+ const originalUrl = req.originalReq?.url || req.originalReq?.realUrl;
70
+
71
+ // 如果存在原始URL,确保放回请求头,以便后续处理程序使用
72
+ if (originalUrl && originalUrl.startsWith('http')) {
73
+ this.log(`[规则处理器] 保存原始URL到请求头,确保透传: ${originalUrl}`);
74
+ req.headers['x-whistle-real-url'] = originalUrl;
75
+ }
76
+
77
+ // 标记为未处理,将由下一个中间件处理
78
+ return { handled: false };
79
+ }
80
+
81
+ this.log(`[规则处理器] 找到匹配的接口: ${matchedInterface.name}, 代理类型: ${matchedInterface.proxyType}`);
82
+
83
+ // 根据接口类型处理请求
84
+ this.log(`[规则处理器] 开始处理请求...`);
85
+ const result = await this.processRequest(matchedInterface, req, res);
86
+ this.log(`[规则处理器] 请求处理完成,状态: ${result.error ? '失败' : '成功'}`);
87
+
88
+ return { handled: true, result };
89
+ },
90
+
91
+ /**
92
+ * 根据请求路径和方法查找匹配的接口
93
+ * @param {Array} interfaces 接口列表
94
+ * @param {string} requestPath 请求路径
95
+ * @param {string} method 请求方法
96
+ * @returns {object|null} 匹配的接口对象,如果没有匹配则返回 null
97
+ */
98
+ findMatchingInterface(interfaces, requestPath, method) {
99
+ // 简化日志,只记录正在进行匹配的操作
100
+ this.log(`[规则处理器] 尝试匹配接口,路径: ${requestPath}, 方法: ${method}`);
101
+
102
+ // 确保interfaces是一个数组
103
+ if (!Array.isArray(interfaces)) {
104
+ this.log(`[规则处理器] 错误: interfaces不是一个数组`);
105
+ return null;
106
+ }
107
+
108
+ // 先尝试完全匹配
109
+ let matchedInterface = interfaces.find(intf => {
110
+ // 检查URL和方法是否匹配
111
+ const urlMatches = intf.urlPattern === requestPath;
112
+
113
+ // 修复: 兼容不同的方法名称和值
114
+ const methodField = intf.httpMethod || intf.method;
115
+ const methodValue = methodField && methodField.toUpperCase();
116
+ const methodMatches = !methodValue ||
117
+ methodValue === 'ALL' ||
118
+ methodValue === 'ANY' ||
119
+ methodValue === method;
120
+
121
+ // 移除中间匹配过程的日志
122
+ return urlMatches && methodMatches;
123
+ });
124
+
125
+ if (matchedInterface) {
126
+ this.log(`[规则处理器] 找到完全匹配的接口: ${matchedInterface.name}`);
127
+ return matchedInterface;
128
+ }
129
+
130
+ // 如果没有完全匹配,尝试正则表达式匹配
131
+ this.log(`[规则处理器] 未找到完全匹配,尝试正则表达式匹配...`);
132
+
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
+ });
160
+
161
+ if (matchedInterface) {
162
+ this.log(`[规则处理器] 找到模式匹配的接口: ${matchedInterface.name}`);
163
+ } else {
164
+ this.log(`[规则处理器] 未找到任何匹配的接口`);
165
+ }
166
+
167
+ return matchedInterface;
168
+ },
169
+
170
+ /**
171
+ * 将URL模式中的通配符转换为正则表达式
172
+ * @param {string} pattern URL模式
173
+ * @returns {string} 正则表达式字符串
174
+ */
175
+ convertPatternToRegex(pattern) {
176
+ if (pattern.startsWith('/') && pattern.endsWith('/') && pattern.length > 2) {
177
+ // 已经是正则表达式格式:/pattern/
178
+ return pattern.slice(1, -1);
179
+ }
180
+
181
+ // 转换通配符 * 为 .*
182
+ return pattern.replace(/\*/g, '.*').replace(/\//g, '\\/');
183
+ },
184
+
185
+ /**
186
+ * 根据接口类型处理请求
187
+ * @param {object} interfaceObj 接口对象
188
+ * @param {object} req 请求对象
189
+ * @param {object} res 响应对象
190
+ * @returns {Promise<object>} 处理结果
191
+ */
192
+ async processRequest(interfaceObj, req, res) {
193
+ // 兼容不同的代理类型字段名和值
194
+ const proxyType = interfaceObj.proxyType || '';
195
+
196
+ // 创建统一的配置对象,兼容直接字段访问和config嵌套字段
197
+ const config = interfaceObj.config || interfaceObj;
198
+
199
+ // 只记录关键信息
200
+ this.log(`[规则处理器] 处理请求,接口: ${interfaceObj.name}, 代理类型: ${proxyType}`);
201
+
202
+ // 设置响应头
203
+ let headers = config.headers;
204
+ if (typeof headers === 'string') {
205
+ headers = this.parseHeaders(headers);
206
+ } else if (typeof headers === 'object' && headers !== null) {
207
+ // 已经是对象格式,直接使用
208
+ } else {
209
+ headers = {};
210
+ }
211
+
212
+ Object.entries(headers).forEach(([key, value]) => {
213
+ res.setHeader(key, value);
214
+ });
215
+
216
+ // 设置内容类型
217
+ if (config.contentType && !res.getHeader('Content-Type')) {
218
+ res.setHeader('Content-Type', config.contentType);
219
+ }
220
+
221
+ // 添加延迟
222
+ const delay = parseInt(config.responseDelay || config.delay || 0, 10);
223
+ if (delay > 0) {
224
+ this.log(`[规则处理器] 添加响应延迟: ${delay}ms`);
225
+ await new Promise(resolve => setTimeout(resolve, delay));
226
+ }
227
+
228
+ // 设置状态码
229
+ res.statusCode = parseInt(config.httpStatus || config.statusCode || 200, 10);
230
+
231
+ // 根据代理类型处理
232
+ let result;
233
+ if (proxyType === 'url_redirect' || proxyType === 'redirect') {
234
+ result = await this.handleUrlRedirect(interfaceObj, req, res);
235
+ }
236
+ else if (proxyType === 'data_template' || proxyType === 'response') {
237
+ result = await this.handleDataTemplate(interfaceObj, req, res);
238
+ }
239
+ else if (proxyType === 'file_proxy' || proxyType === 'file') {
240
+ result = await this.handleFileProxy(interfaceObj, req, res);
241
+ }
242
+ else if (proxyType === 'dynamic_data') {
243
+ result = await this.handleDynamicData(interfaceObj, req, res);
244
+ }
245
+ else {
246
+ // 默认使用数据模板处理
247
+ this.log(`[规则处理器] 未知的代理类型 "${proxyType}",默认使用数据模板处理`);
248
+ result = await this.handleDataTemplate(interfaceObj, req, res);
249
+ }
250
+
251
+ // 记录处理结果
252
+ this.log(`[规则处理器] 请求处理完成,状态码: ${res.statusCode}`);
253
+
254
+ // 记录完整的命中规则日志
255
+ if (this.dataManager && typeof this.dataManager.logRequest === 'function') {
256
+ const parsedUrl = url.parse(req.url);
257
+ try {
258
+ this.dataManager.logRequest({
259
+ type: 'mock_hit',
260
+ eventType: 'mock_hit',
261
+ message: `命中接口规则: ${interfaceObj.name}`,
262
+ method: req.method,
263
+ url: req.url,
264
+ path: parsedUrl.pathname,
265
+ status: res.statusCode,
266
+ pattern: interfaceObj.urlPattern,
267
+ proxyType: proxyType,
268
+ interfaceName: interfaceObj.name,
269
+ interfaceId: interfaceObj.id,
270
+ responseData: result,
271
+ responseTime: new Date().toISOString(),
272
+ contentType: res.getHeader('Content-Type') || config.contentType
273
+ });
274
+ } catch (err) {
275
+ this.log(`[规则处理器] 记录命中规则日志失败: ${err.message}`);
276
+ }
277
+ }
278
+
279
+ return result;
280
+ },
281
+
282
+ /**
283
+ * 处理 URL 重定向类型
284
+ * @param {object} interfaceObj 接口对象
285
+ * @param {object} req 请求对象
286
+ * @param {object} res 响应对象
287
+ * @returns {Promise<object>} 处理结果
288
+ */
289
+ async handleUrlRedirect(interfaceObj, req, res) {
290
+ // 兼容不同的字段命名
291
+ const config = interfaceObj.config || interfaceObj;
292
+ const targetUrl = config.targetUrl || '';
293
+ const preserveParams = config.preserveParams || false;
294
+
295
+ if (!targetUrl) {
296
+ res.end(JSON.stringify({ error: 'No target URL specified' }));
297
+ return { error: 'No target URL specified' };
298
+ }
299
+
300
+ // 构建重定向URL
301
+ let redirectUrl = targetUrl;
302
+
303
+ // 如果需要保留参数,则将原请求的查询参数附加到重定向URL
304
+ if (preserveParams) {
305
+ const parsedUrl = url.parse(req.url, true);
306
+ const parsedTargetUrl = url.parse(targetUrl, true);
307
+
308
+ // 合并查询参数
309
+ const mergedQuery = { ...parsedTargetUrl.query, ...parsedUrl.query };
310
+
311
+ // 重建URL
312
+ const redirectUrlObj = new url.URL(targetUrl);
313
+ Object.entries(mergedQuery).forEach(([key, value]) => {
314
+ redirectUrlObj.searchParams.set(key, value);
315
+ });
316
+
317
+ redirectUrl = redirectUrlObj.toString();
318
+ }
319
+
320
+ // 执行重定向
321
+ res.setHeader('Location', redirectUrl);
322
+ res.statusCode = 302;
323
+ res.end();
324
+
325
+ return { redirectUrl };
326
+ },
327
+
328
+ /**
329
+ * 处理数据模板类型
330
+ * @param {object} interfaceObj 接口对象
331
+ * @param {object} req 请求对象
332
+ * @param {object} res 响应对象
333
+ * @returns {Promise<object>} 处理结果
334
+ */
335
+ async handleDataTemplate(interfaceObj, req, res) {
336
+ // 兼容不同的字段命名
337
+ const config = interfaceObj.config || interfaceObj;
338
+ const template = config.responseContent || config.template || '{}';
339
+
340
+ if (!template) {
341
+ res.end(JSON.stringify({ error: 'No template specified' }));
342
+ return { error: 'No template specified' };
343
+ }
344
+
345
+ try {
346
+ // 使用 Mock.js 生成数据
347
+ const mockData = Mock.mock(JSON.parse(template));
348
+
349
+ // 设置内容类型
350
+ if (!res.getHeader('Content-Type')) {
351
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
352
+ }
353
+
354
+ // 发送数据
355
+ res.end(JSON.stringify(mockData));
356
+ return { mockData };
357
+ } catch (err) {
358
+ res.end(JSON.stringify({ error: 'Template parsing error', message: err.message }));
359
+ return { error: 'Template parsing error', message: err.message };
360
+ }
361
+ },
362
+
363
+ /**
364
+ * 处理文件代理类型
365
+ * @param {object} interfaceObj 接口对象
366
+ * @param {object} req 请求对象
367
+ * @param {object} res 响应对象
368
+ * @returns {Promise<object>} 处理结果
369
+ */
370
+ async handleFileProxy(interfaceObj, req, res) {
371
+ // 兼容不同的字段命名
372
+ const config = interfaceObj.config || interfaceObj;
373
+ const filePath = config.filePath || '';
374
+
375
+ // 只记录关键信息
376
+ this.log(`[规则处理器] 处理文件代理,路径: ${filePath}`);
377
+
378
+ if (!filePath) {
379
+ this.log(`[规则处理器] 错误: 未指定文件路径`);
380
+ res.end(JSON.stringify({ error: 'No file path specified' }));
381
+ return { error: 'No file path specified' };
382
+ }
383
+
384
+ // 确定文件路径
385
+ const absFilePath = path.isAbsolute(filePath)
386
+ ? filePath
387
+ : path.join(this.mockDataDir, filePath);
388
+
389
+ try {
390
+ // 检查文件是否存在
391
+ if (!fs.existsSync(absFilePath)) {
392
+ this.log(`[规则处理器] 错误: 文件不存在: ${path.basename(absFilePath)}`);
393
+ res.statusCode = 404;
394
+ res.end(JSON.stringify({ error: 'File not found', path: absFilePath }));
395
+ return { error: 'File not found', path: absFilePath };
396
+ }
397
+
398
+ // 读取文件内容
399
+ const fileContent = fs.readFileSync(absFilePath, 'utf8');
400
+ this.log(`[规则处理器] 成功读取文件: ${path.basename(absFilePath)}`);
401
+
402
+ // 设置内容类型(根据文件扩展名)
403
+ if (!res.getHeader('Content-Type')) {
404
+ const ext = path.extname(absFilePath).toLowerCase();
405
+ const contentType = this.getContentTypeByExt(ext);
406
+ if (contentType) {
407
+ res.setHeader('Content-Type', contentType);
408
+ }
409
+ }
410
+
411
+ // 设置响应头,标识插件处理
412
+ res.setHeader('X-Handled-By', 'whistle.mock-plugin');
413
+
414
+ // 发送文件内容
415
+ res.end(fileContent);
416
+ return { filePath: absFilePath, fileName: path.basename(absFilePath) };
417
+ } catch (err) {
418
+ this.log(`[规则处理器] 读取文件错误: ${err.message}`);
419
+ res.end(JSON.stringify({ error: 'File reading error', message: err.message }));
420
+ return { error: 'File reading error', message: err.message };
421
+ }
422
+ },
423
+
424
+ /**
425
+ * 处理动态数据生成类型
426
+ * @param {object} interfaceObj 接口对象
427
+ * @param {object} req 请求对象
428
+ * @param {object} res 响应对象
429
+ * @returns {Promise<object>} 处理结果
430
+ */
431
+ async handleDynamicData(interfaceObj, req, res) {
432
+ const { config } = interfaceObj;
433
+ const { script } = config;
434
+
435
+ if (!script) {
436
+ res.end(JSON.stringify({ error: 'No script specified' }));
437
+ return { error: 'No script specified' };
438
+ }
439
+
440
+ try {
441
+ // 创建执行环境
442
+ const context = {
443
+ req,
444
+ Mock,
445
+ url: req.url,
446
+ method: req.method,
447
+ headers: req.headers,
448
+ query: url.parse(req.url, true).query,
449
+ };
450
+
451
+ // 从请求中读取 body
452
+ if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
453
+ const body = await this.readRequestBody(req);
454
+ context.body = body;
455
+
456
+ // 尝试解析 JSON
457
+ try {
458
+ context.bodyJson = JSON.parse(body);
459
+ } catch (e) {
460
+ // 忽略解析错误
461
+ }
462
+ }
463
+
464
+ // 执行脚本
465
+ const fn = new Function('ctx', `
466
+ with (ctx) {
467
+ ${script}
468
+ return typeof result !== 'undefined' ? result : {};
469
+ }
470
+ `);
471
+
472
+ const result = fn(context);
473
+
474
+ // 设置内容类型
475
+ if (!res.getHeader('Content-Type')) {
476
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
477
+ }
478
+
479
+ // 发送结果
480
+ res.end(typeof result === 'string' ? result : JSON.stringify(result));
481
+ return { result };
482
+ } catch (err) {
483
+ res.end(JSON.stringify({ error: 'Script execution error', message: err.message }));
484
+ return { error: 'Script execution error', message: err.message };
485
+ }
486
+ },
487
+
488
+ /**
489
+ * 从请求中读取 body
490
+ * @param {object} req 请求对象
491
+ * @returns {Promise<string>} 请求体字符串
492
+ */
493
+ readRequestBody(req) {
494
+ return new Promise((resolve, reject) => {
495
+ let body = '';
496
+ req.on('data', chunk => {
497
+ body += chunk.toString();
498
+ });
499
+ req.on('end', () => {
500
+ resolve(body);
501
+ });
502
+ req.on('error', err => {
503
+ reject(err);
504
+ });
505
+ });
506
+ },
507
+
508
+ /**
509
+ * 解析响应头字符串
510
+ * @param {string} headersStr 响应头字符串
511
+ * @returns {object} 解析后的响应头对象
512
+ */
513
+ parseHeaders(headersStr) {
514
+ if (!headersStr) return {};
515
+
516
+ const headers = {};
517
+ headersStr.split('\n').forEach(line => {
518
+ const [key, ...valueParts] = line.split(':');
519
+ if (key && valueParts.length > 0) {
520
+ const value = valueParts.join(':').trim();
521
+ if (value) {
522
+ headers[key.trim()] = value;
523
+ }
524
+ }
525
+ });
526
+
527
+ return headers;
528
+ },
529
+
530
+ /**
531
+ * 根据文件扩展名获取内容类型
532
+ * @param {string} ext 文件扩展名
533
+ * @returns {string|null} 内容类型
534
+ */
535
+ getContentTypeByExt(ext) {
536
+ const contentTypes = {
537
+ '.json': 'application/json; charset=utf-8',
538
+ '.xml': 'application/xml; charset=utf-8',
539
+ '.html': 'text/html; charset=utf-8',
540
+ '.htm': 'text/html; charset=utf-8',
541
+ '.txt': 'text/plain; charset=utf-8',
542
+ '.js': 'application/javascript; charset=utf-8',
543
+ '.css': 'text/css; charset=utf-8',
544
+ '.csv': 'text/csv; charset=utf-8',
545
+ };
546
+
547
+ return contentTypes[ext] || null;
548
+ }
549
+ };
550
+
551
+ module.exports = ruleManager;