whistle.mockbubu 2.0.0 → 2.1.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 (56) hide show
  1. package/README.md +38 -0
  2. package/index.js +3 -3
  3. package/lib/config/const.js +138 -0
  4. package/lib/config/rule-collector.js +81 -0
  5. package/lib/constants.js +62 -0
  6. package/lib/core/memory-buffer/index.js +207 -0
  7. package/lib/core/memory-buffer/shared-instance.js +15 -0
  8. package/lib/core/plugin-mode-manager.js +74 -0
  9. package/lib/core/resRulesServer.js +14 -0
  10. package/lib/core/rulesServer.js +31 -0
  11. package/lib/core/server-entry/capture-handler.js +191 -0
  12. package/lib/core/server-entry/request-interceptor.js +82 -0
  13. package/lib/core/server-entry/response-handler.js +147 -0
  14. package/lib/core/server-entry/server.js +230 -0
  15. package/lib/storage/group-manager.js +627 -0
  16. package/lib/storage/storage-adapter.js +712 -0
  17. package/lib/storage/storage-v3.js +1418 -0
  18. package/lib/uiServer/index.js +61 -24
  19. package/lib/uiServer/router/export/import-export-router.js +459 -0
  20. package/lib/uiServer/router/files/api-list-router.js +150 -0
  21. package/lib/uiServer/router/files/batch-operations-router.js +185 -0
  22. package/lib/uiServer/router/files/file-config-router.js +118 -0
  23. package/lib/uiServer/router/files/file-crud-router.js +212 -0
  24. package/lib/uiServer/router/files/file-save-router.js +146 -0
  25. package/lib/uiServer/router/files/version-router.js +260 -0
  26. package/lib/uiServer/router/global/plugin-control.js +135 -0
  27. package/lib/uiServer/router/global/system-info-router.js +386 -0
  28. package/lib/uiServer/router/{group-router.js → groups/group-router.js} +21 -20
  29. package/lib/uiServer/router/index.js +38 -1521
  30. package/lib/uiServer/utils/router-helpers.js +100 -0
  31. package/lib/uiServer/utils/util.js +172 -0
  32. package/lib/uiServer/{validator.js → utils/validator.js} +11 -6
  33. package/lib/utils/archive-utils.js +788 -0
  34. package/lib/utils/error-handler.js +173 -0
  35. package/lib/utils/logger.js +79 -0
  36. package/lib/utils/path-utils.js +147 -0
  37. package/lib/utils/performance.js +265 -0
  38. package/lib/utils/utils.js +541 -0
  39. package/package.json +2 -2
  40. package/public/js/app.js +3707 -1922
  41. package/public/js/app.js.map +1 -1
  42. package/public/js/chunk-vendors.js +5098 -3965
  43. package/public/js/chunk-vendors.js.map +1 -1
  44. package/rules.txt +1 -1
  45. package/CHANGELOG_GROUP_FEATURE.md +0 -468
  46. package/CHANGELOG_P0_FIXES.md +0 -412
  47. package/CHANGELOG_P1_OPTIMIZATIONS.md +0 -292
  48. package/CLAUDE.md +0 -436
  49. package/GROUP_FEATURE_DESIGN.md +0 -520
  50. package/lib/const.js +0 -47
  51. package/lib/group-manager.js +0 -491
  52. package/lib/resRulesServer.js +0 -9
  53. package/lib/server.js +0 -249
  54. package/lib/uiServer/router/version-router.js +0 -205
  55. package/lib/uiServer/util.js +0 -153
  56. package/lib/utils.js +0 -409
@@ -0,0 +1,627 @@
1
+ /**
2
+ * 文件名: group-manager.js
3
+ * 功能: Mock 数据分组管理器
4
+ * 依赖: storage-adapter.js, logger.js, const.js
5
+ * 更新: 2025-12-04
6
+ *
7
+ * 职责:
8
+ * - 管理多个 Mock 数据组,每个组有独立的 mock 配置
9
+ * - 提供组的 CRUD 操作(创建、读取、更新、删除)
10
+ * - 管理组内文件配置和版本数据
11
+ * - 支持组间配置复制
12
+ *
13
+ * 架构特点:
14
+ * - 使用 V3 文件系统存储
15
+ * - 支持版本管理(通过 V3 Storage)
16
+ *
17
+ * 使用示例:
18
+ * ```javascript
19
+ * const manager = new GroupManager(storage)
20
+ * const newGroup = await manager.createGroup({ name: '测试组' })
21
+ * await manager.setCurrentGroup(newGroup.id)
22
+ * ```
23
+ */
24
+
25
+ const { createLogger } = require('../utils/logger')
26
+ const { SystemConstants, BusinessRules } = require('../config/const')
27
+ const { buildConfigKey } = require('../utils/path-utils')
28
+
29
+ class GroupManager {
30
+ /**
31
+ * 构造函数
32
+ *
33
+ * @param {StorageAdapter} storage - 存储适配器实例
34
+ */
35
+ constructor(storage) {
36
+ this.storage = storage
37
+ this.logger = createLogger('group-manager')
38
+
39
+ // 使用 SystemConstants 中的常量
40
+ this.GROUPS_KEY = SystemConstants.GROUPS_META_KEY
41
+ this.DEFAULT_GROUP_ID = SystemConstants.DEFAULT_GROUP_ID
42
+ this.DEFAULT_GROUP_NAME = SystemConstants.DEFAULT_GROUP_NAME
43
+
44
+ // 初始化:确保默认组存在
45
+ this.ensureDefaultGroup()
46
+ }
47
+
48
+ /**
49
+ * 确保默认组存在
50
+ */
51
+ async ensureDefaultGroup() {
52
+ const groupsData = await this.getGroupsData()
53
+ if (!groupsData || !groupsData.groups || groupsData.groups.length === 0) {
54
+ // 首次使用,创建默认组
55
+ const defaultGroup = {
56
+ id: this.DEFAULT_GROUP_ID,
57
+ name: this.DEFAULT_GROUP_NAME,
58
+ description: '系统默认分组',
59
+ createTime: Date.now(),
60
+ updateTime: Date.now(),
61
+ isDefault: true,
62
+ }
63
+
64
+ await this.setGroupsData({
65
+ groups: [defaultGroup],
66
+ currentGroupId: this.DEFAULT_GROUP_ID,
67
+ })
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 获取组列表数据(内部方法)
73
+ * 从 _meta.json 读取
74
+ *
75
+ * @private
76
+ * @returns {Promise<Object>} 组列表数据
77
+ * @property {Array} groups - 组列表
78
+ * @property {string} currentGroupId - 当前激活的组ID
79
+ */
80
+ async getGroupsData() {
81
+ const v3Storage = this.storage.getV3Storage()
82
+ return await v3Storage.getGroupsMeta()
83
+ }
84
+
85
+ /**
86
+ * 保存组列表数据(内部方法)
87
+ * 写入 _meta.json
88
+ *
89
+ * @private
90
+ * @param {Object} data - 组列表数据
91
+ * @returns {Promise<void>}
92
+ */
93
+ async setGroupsData(data) {
94
+ const v3Storage = this.storage.getV3Storage()
95
+ await v3Storage.setGroupsMeta(data)
96
+ }
97
+
98
+ /**
99
+ * 获取所有组
100
+ */
101
+ async getGroups() {
102
+ const data = await this.getGroupsData()
103
+ return data.groups || []
104
+ }
105
+
106
+ /**
107
+ * 获取当前组ID
108
+ */
109
+ async getCurrentGroupId() {
110
+ const data = await this.getGroupsData()
111
+ return data.currentGroupId || this.DEFAULT_GROUP_ID
112
+ }
113
+
114
+ /**
115
+ * 获取当前组信息
116
+ */
117
+ async getCurrentGroup() {
118
+ const currentId = await this.getCurrentGroupId()
119
+ const groups = await this.getGroups()
120
+ return groups.find(g => g.id === currentId) || groups[0]
121
+ }
122
+
123
+ /**
124
+ * 设置当前组
125
+ */
126
+ async setCurrentGroup(groupId) {
127
+ const groups = await this.getGroups()
128
+ const group = groups.find(g => g.id === groupId)
129
+
130
+ if (!group) {
131
+ throw new Error(`组不存在: ${groupId}`)
132
+ }
133
+
134
+ const data = await this.getGroupsData()
135
+ data.currentGroupId = groupId
136
+ await this.setGroupsData(data)
137
+
138
+ return group
139
+ }
140
+
141
+ /**
142
+ * 生成唯一组ID
143
+ */
144
+ generateGroupId() {
145
+ return 'group_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
146
+ }
147
+
148
+ /**
149
+ * 创建新组
150
+ * @param {Object} params
151
+ * @param {string} params.name - 组名
152
+ * @param {string} params.description - 组描述
153
+ * @param {string} params.copyFromGroupId - 从哪个组复制(可选)
154
+ */
155
+ async createGroup({ name, description = '', copyFromGroupId = null }) {
156
+ if (!name || !name.trim()) {
157
+ throw new Error('组名不能为空')
158
+ }
159
+
160
+ const groups = await this.getGroups()
161
+
162
+ // 检查组名是否重复
163
+ if (groups.some(g => g.name === name)) {
164
+ throw new Error('组名已存在')
165
+ }
166
+
167
+ // 检查组数量限制(使用业务规则常量)
168
+ if (groups.length >= BusinessRules.MAX_GROUPS) {
169
+ throw new Error(`组数量已达上限(${BusinessRules.MAX_GROUPS}个)`)
170
+ }
171
+
172
+ const newGroupId = this.generateGroupId()
173
+
174
+ // V3 架构:直接调用 V3 Storage 的 createGroup
175
+ const v3Storage = this.storage.getV3Storage()
176
+ const newGroup = await v3Storage.createGroup({
177
+ id: newGroupId,
178
+ name: name.trim(),
179
+ description: description.trim(),
180
+ isDefault: false,
181
+ })
182
+
183
+ // 如果指定了复制源,复制配置
184
+ if (copyFromGroupId) {
185
+ await this.copyGroupConfigs(copyFromGroupId, newGroup.id)
186
+ }
187
+
188
+ return newGroup
189
+ }
190
+
191
+ /**
192
+ * 更新组信息
193
+ */
194
+ async updateGroup(groupId, { name, description }) {
195
+ const groups = await this.getGroups()
196
+ const groupIndex = groups.findIndex(g => g.id === groupId)
197
+
198
+ if (groupIndex === -1) {
199
+ throw new Error(`组不存在: ${groupId}`)
200
+ }
201
+
202
+ // 检查新名称是否与其他组重复
203
+ if (name && groups.some((g, idx) => idx !== groupIndex && g.name === name)) {
204
+ throw new Error('组名已存在')
205
+ }
206
+
207
+ // V3 架构:直接调用 V3 Storage 的 updateGroup
208
+ const v3Storage = this.storage.getV3Storage()
209
+ const updates = {}
210
+ if (name) updates.name = name.trim()
211
+ if (description !== undefined) updates.description = description.trim()
212
+
213
+ const updatedGroup = await v3Storage.updateGroup(groupId, updates)
214
+ return updatedGroup
215
+ }
216
+
217
+ /**
218
+ * 删除组
219
+ *
220
+ * @param {Object} options - 删除选项
221
+ * @param {Function} options.memoryBuffer - 内存缓冲区实例(可选)
222
+ */
223
+ async deleteGroup(groupId, options = {}) {
224
+ if (groupId === this.DEFAULT_GROUP_ID) {
225
+ throw new Error('不能删除默认组')
226
+ }
227
+
228
+ const groups = await this.getGroups()
229
+ const groupIndex = groups.findIndex(g => g.id === groupId)
230
+
231
+ if (groupIndex === -1) {
232
+ throw new Error(`组不存在: ${groupId}`)
233
+ }
234
+
235
+ // 如果删除的是当前组,先切换到默认组
236
+ const currentGroupId = await this.getCurrentGroupId()
237
+ if (currentGroupId === groupId) {
238
+ // 使用 setCurrentGroup 同时更新 storage adapter 和 _meta.json
239
+ await this.setCurrentGroup(this.DEFAULT_GROUP_ID)
240
+ }
241
+
242
+ // 清理内存缓冲区中对应组的数据(如果提供了 memoryBuffer)
243
+ if (options.memoryBuffer && typeof options.memoryBuffer.clearGroup === 'function') {
244
+ options.memoryBuffer.clearGroup(groupId)
245
+ this.logger.log(`删除组时清理内存缓冲区: ${groupId}`)
246
+ }
247
+
248
+ // V3 架构:直接调用 V3 Storage 的 deleteGroup
249
+ const v3Storage = this.storage.getV3Storage()
250
+ await v3Storage.deleteGroup(groupId)
251
+ }
252
+
253
+ /**
254
+ * 获取组的配置键名
255
+ * 使用统一的 buildConfigKey 工具函数
256
+ *
257
+ * @param {string} groupId - 组ID
258
+ * @param {string} filename - 文件名
259
+ * @returns {string} 配置键,格式:group.{groupId}.file.{filename}
260
+ */
261
+ getGroupConfigKey(groupId, filename) {
262
+ return buildConfigKey(groupId, filename)
263
+ }
264
+
265
+ /**
266
+ * 获取组的文件配置
267
+ */
268
+ async getGroupFileConfig(groupId, filename) {
269
+ // V3 架构:从索引查找 fileId,再读取 file.json
270
+ const v3Storage = this.storage.getV3Storage()
271
+
272
+ try {
273
+ // ⚠️ 强制刷新索引缓存
274
+ v3Storage.invalidateIndex(groupId)
275
+
276
+ // 从索引查找对应的 fileId(因为 fileId 是随机生成的)
277
+ const index = await v3Storage.getIndex(groupId)
278
+ const indexEntry = index.files.find(f => f.url === filename)
279
+
280
+ if (!indexEntry) {
281
+ // 文件不存在,返回默认配置(首次捕获)
282
+ return {
283
+ url: filename,
284
+ method: 'GET',
285
+ status: 0,
286
+ date: Date.now(),
287
+ mock: false,
288
+ locked: false,
289
+ mockVersion: 'source',
290
+ mockTime: null,
291
+ }
292
+ }
293
+
294
+ // 读取完整的文件数据
295
+ const file = await v3Storage.getFile(groupId, indexEntry.id)
296
+
297
+ // 返回完整的文件信息(包含 config + 元数据 + 版本数据)
298
+ // 提取所有版本相关字段(version.* 和 versionMeta.*)
299
+ const versionFields = {}
300
+ Object.keys(file).forEach(key => {
301
+ if (key.startsWith('version.') || key.startsWith('versionMeta.')) {
302
+ versionFields[key] = file[key]
303
+ }
304
+ })
305
+
306
+ // 从 session.url 提取 query 参数(包含完整 URL 和 query)
307
+ // 注意: file.url 可能被 ruleValue 截断(如 pathname 模式会去掉 query)
308
+ const query = this._extractQueryFromUrl(file.session?.url || file.url)
309
+
310
+ // 从 session 提取 payload
311
+ const payload = this._extractPayloadFromSession(file.session)
312
+
313
+ return {
314
+ url: file.url,
315
+ method: file.method,
316
+ status: file.status,
317
+ date: file.date || file.createTime, // 优先使用 file.date,兼容旧数据使用 createTime
318
+ rule: file.rule || '',
319
+ ruleValue: file.ruleValue || '',
320
+ pattern: file.pattern || '',
321
+ ...file.config,
322
+ ...versionFields, // 包含所有版本数据
323
+ query, // ✨ 从 URL 提取的 query 参数
324
+ payload, // ✨ 从 session 提取的 payload
325
+ }
326
+ } catch (err) {
327
+ // 读取失败,返回默认配置
328
+ return {
329
+ url: filename,
330
+ method: 'GET',
331
+ status: 0,
332
+ date: Date.now(),
333
+ mock: false,
334
+ locked: false,
335
+ mockVersion: 'source',
336
+ mockTime: null,
337
+ }
338
+ }
339
+ }
340
+
341
+ /**
342
+ * 设置组的文件配置
343
+ * V3 架构:从索引查找 fileId,再更新 file.json
344
+ */
345
+ async setGroupFileConfig(groupId, filename, config) {
346
+ const v3Storage = this.storage.getV3Storage()
347
+
348
+ try {
349
+ // ⚠️ 强制刷新索引缓存
350
+ v3Storage.invalidateIndex(groupId)
351
+
352
+ // 从索引查找对应的 fileId
353
+ const index = await v3Storage.getIndex(groupId)
354
+ const indexEntry = index.files.find(f => f.url === filename)
355
+
356
+ let fileId
357
+ let file = null
358
+
359
+ if (indexEntry) {
360
+ // 文件已存在,使用现有的 fileId
361
+ fileId = indexEntry.id
362
+ try {
363
+ file = await v3Storage.getFile(groupId, fileId)
364
+ } catch (err) {
365
+ // 文件读取失败(可能物理文件丢失),忽略
366
+ }
367
+ } else {
368
+ // 文件不存在,生成新的 fileId
369
+ fileId = v3Storage.generateFileId()
370
+ }
371
+
372
+ // 合并配置到 config 字段
373
+ const mergedConfig = {
374
+ mock: config.mock !== undefined ? config.mock : (file?.config?.mock || false),
375
+ locked: config.locked !== undefined ? config.locked : (file?.config?.locked || false),
376
+ mockVersion: config.mockVersion !== undefined ? config.mockVersion : (file?.config?.mockVersion || 'source'),
377
+ mockTime: config.mockTime !== undefined ? config.mockTime : (file?.config?.mockTime || null),
378
+ }
379
+
380
+ // 构建更新数据:仅包含非 config 字段 + config 对象
381
+ const updates = {
382
+ config: mergedConfig,
383
+ updateTime: Date.now(),
384
+ }
385
+
386
+ // 如果 config 包含非配置字段(如 url, method, status等),单独添加
387
+ const configFields = ['mock', 'locked', 'mockVersion', 'mockTime']
388
+ Object.keys(config).forEach(key => {
389
+ if (!configFields.includes(key)) {
390
+ updates[key] = config[key]
391
+ }
392
+ })
393
+
394
+ // 更新文件(如果文件不存在,updateFile会自动创建)
395
+ await v3Storage.updateFile(groupId, fileId, updates)
396
+
397
+ this.logger.log(`setGroupFileConfig: ${file ? '更新' : '创建'} ${groupId}/${fileId}`)
398
+ } catch (err) {
399
+ this.logger.error(`setGroupFileConfig 失败: ${err.message}`)
400
+ throw err
401
+ }
402
+ }
403
+
404
+ /**
405
+ * 复制组配置(从源组到目标组)
406
+ * 包括:配置元数据 + 物理文件数据
407
+ * 深拷贝会自动包含所有嵌套的版本配置(version.xxx、versionMeta.xxx)
408
+ */
409
+ async copyGroupConfigs(sourceGroupId, targetGroupId) {
410
+ // V3 架构:直接调用 V3 Storage 的 copyGroup
411
+ const v3Storage = this.storage.getV3Storage()
412
+ const copiedCount = await v3Storage.copyGroup(sourceGroupId, targetGroupId)
413
+ this.logger.log(`V3架构组复制完成: ${sourceGroupId} -> ${targetGroupId}, 共 ${copiedCount} 个文件`)
414
+ }
415
+
416
+ /**
417
+ * 删除组的文件配置
418
+ * V3 架构:配置存储在 file.json 中,随文件一起删除,无需单独删除
419
+ * @param {string} groupId - 组ID
420
+ * @param {string} filename - 文件名
421
+ */
422
+ async removeGroupFileConfig(groupId, filename) {
423
+ // V3 架构:配置已随文件删除,无需额外操作
424
+ this.logger.log('removeGroupFileConfig (V3): 配置已在 file.json 中,随文件一起删除')
425
+ }
426
+
427
+
428
+ /**
429
+ * 获取组的版本列表
430
+ * V3 架构:从文件系统读取版本列表
431
+ */
432
+ async getGroupVersions(groupId, filename) {
433
+ const v3Storage = this.storage.getV3Storage()
434
+
435
+ try {
436
+ // 从索引查找文件ID
437
+ const index = await v3Storage.getIndex(groupId)
438
+ const fileEntry = index.files.find(f => f.url === filename)
439
+
440
+ if (!fileEntry) {
441
+ return []
442
+ }
443
+
444
+ // 使用 V3 Storage 的 listVersions 方法
445
+ const versions = await v3Storage.listVersions(groupId, fileEntry.id)
446
+
447
+ // 映射字段保持兼容(前端可能使用 filename 字段)
448
+ return versions.map(v => ({
449
+ id: v.id, // ✅ 添加 id 字段
450
+ name: v.name,
451
+ filename: v.name, // 保留 filename 用于兼容
452
+ content: v.content,
453
+ description: v.description || '',
454
+ createTime: v.createTime || 0,
455
+ updateTime: v.updateTime || 0,
456
+ }))
457
+ } catch (err) {
458
+ this.logger.error('获取版本列表失败:', err.message)
459
+ return []
460
+ }
461
+ }
462
+
463
+ /**
464
+ * 获取组的版本内容
465
+ * V3 架构:从文件系统读取版本内容
466
+ */
467
+ async getGroupVersionContent(groupId, filename, versionName) {
468
+ const v3Storage = this.storage.getV3Storage()
469
+
470
+ try {
471
+ // 从索引查找文件ID
472
+ const index = await v3Storage.getIndex(groupId)
473
+ const fileEntry = index.files.find(f => f.url === filename)
474
+
475
+ if (!fileEntry) {
476
+ return null
477
+ }
478
+
479
+ // 通过版本名称获取版本
480
+ const version = await v3Storage.getVersionByName(groupId, fileEntry.id, versionName)
481
+
482
+ return version ? version.content : null
483
+ } catch (err) {
484
+ this.logger.error('获取版本内容失败:', err.message)
485
+ return null
486
+ }
487
+ }
488
+
489
+ /**
490
+ * 从 URL 提取 query 参数
491
+ * @param {string} url - 完整的 URL
492
+ * @returns {Object} query 参数对象
493
+ */
494
+ _extractQueryFromUrl(url) {
495
+ try {
496
+ const urlObj = new URL(url)
497
+ const query = {}
498
+ urlObj.searchParams.forEach((value, key) => {
499
+ query[key] = value
500
+ })
501
+ return query
502
+ } catch (err) {
503
+ // URL 解析失败,返回空对象
504
+ return {}
505
+ }
506
+ }
507
+
508
+ /**
509
+ * 从 session 提取 payload (请求 body)
510
+ * @param {Object} session - Whistle session 对象
511
+ * @returns {Object} 返回 { raw: string, parsed: Object|null, type: string }
512
+ */
513
+ _extractPayloadFromSession(session) {
514
+ if (!session || !session.req) {
515
+ return { raw: '', parsed: null, type: 'empty' }
516
+ }
517
+
518
+ // 从 session.req.body 提取 payload
519
+ let body = session.req.body || ''
520
+
521
+ // 如果 body 是对象,转换为 JSON 字符串
522
+ if (typeof body === 'object') {
523
+ try {
524
+ return {
525
+ raw: JSON.stringify(body, null, 2),
526
+ parsed: body,
527
+ type: 'json',
528
+ }
529
+ } catch (err) {
530
+ return { raw: '', parsed: null, type: 'empty' }
531
+ }
532
+ }
533
+
534
+ // 字符串类型的 body
535
+ if (typeof body !== 'string') {
536
+ body = String(body)
537
+ }
538
+
539
+ // 尝试解析不同格式的 payload
540
+ const parsed = this._parsePayload(body, session)
541
+
542
+ return {
543
+ raw: body,
544
+ parsed,
545
+ type: parsed ? (parsed.type || 'unknown') : 'raw',
546
+ }
547
+ }
548
+
549
+ /**
550
+ * 解析 payload 为键值对
551
+ * @param {string} body - 请求体字符串
552
+ * @param {Object} session - session 对象(用于获取 Content-Type)
553
+ * @returns {Object|null} 解析后的键值对,或 null
554
+ */
555
+ _parsePayload(body, session) {
556
+ if (!body) return null
557
+
558
+ const contentType = session.req?.headers?.[' content-type'] || session.req?.headers?.['content-type'] || ''
559
+
560
+ // 1. multipart/form-data
561
+ if (contentType.includes('multipart/form-data') || body.includes('Content-Disposition: form-data')) {
562
+ return this._parseMultipartFormData(body)
563
+ }
564
+
565
+ // 2. application/x-www-form-urlencoded
566
+ if (contentType.includes('application/x-www-form-urlencoded') || (body.includes('=') && !body.includes('\n'))) {
567
+ return this._parseUrlEncoded(body)
568
+ }
569
+
570
+ // 3. application/json
571
+ if (contentType.includes('application/json')) {
572
+ try {
573
+ return {
574
+ type: 'json',
575
+ data: JSON.parse(body),
576
+ }
577
+ } catch (err) {
578
+ // JSON 解析失败,返回 null
579
+ }
580
+ }
581
+
582
+ return null
583
+ }
584
+
585
+ /**
586
+ * 解析 multipart/form-data 格式
587
+ */
588
+ _parseMultipartFormData(body) {
589
+ const result = {}
590
+ const boundaryMatch = body.match(/------[^\r\n]+/)
591
+ if (!boundaryMatch) return null
592
+
593
+ const boundary = boundaryMatch[0]
594
+ const parts = body.split(boundary).filter(part => part.trim() && part !== '--')
595
+
596
+ parts.forEach(part => {
597
+ const nameMatch = part.match(/name="([^"]+)"/)
598
+ if (nameMatch) {
599
+ const name = nameMatch[1]
600
+ const valueMatch = part.match(/\r\n\r\n([\s\S]*?)\r\n/)
601
+ if (valueMatch) {
602
+ result[name] = valueMatch[1].trim()
603
+ }
604
+ }
605
+ })
606
+
607
+ return Object.keys(result).length > 0 ? { type: 'form-data', data: result } : null
608
+ }
609
+
610
+ /**
611
+ * 解析 application/x-www-form-urlencoded 格式
612
+ */
613
+ _parseUrlEncoded(body) {
614
+ try {
615
+ const params = new URLSearchParams(body)
616
+ const result = {}
617
+ params.forEach((value, key) => {
618
+ result[key] = value
619
+ })
620
+ return Object.keys(result).length > 0 ? { type: 'urlencoded', data: result } : null
621
+ } catch (err) {
622
+ return null
623
+ }
624
+ }
625
+ }
626
+
627
+ module.exports = GroupManager