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,386 @@
1
+ /**
2
+ * 文件名: system-info-router.js
3
+ * 功能: 系统信息路由(存储统计、规则信息、内存监控)
4
+ * 依赖: util.js, fs, path, child_process
5
+ *
6
+ * 职责:
7
+ * - 存储统计(文件数量、磁盘占用)
8
+ * - 生效规则列表查询
9
+ * - 内存使用监控(堆内存、GC统计)
10
+ *
11
+ * 路由:
12
+ * - GET /cgi-bin/mockbubu/storage-stats - 存储统计
13
+ * - GET /cgi-bin/mockbubu/active-rules - 生效规则列表
14
+ * - GET /cgi-bin/mockbubu/memory-stats - 内存统计
15
+ */
16
+
17
+ const {
18
+ createErrorResponse,
19
+ createSuccessResponse,
20
+ } = require('../../utils/util')
21
+ const fs = require('fs').promises
22
+ const path = require('path')
23
+ const ruleCollector = require('../../../config/rule-collector')
24
+
25
+ module.exports = (router) => {
26
+ /**
27
+ * 存储统计信息
28
+ *
29
+ * 功能:
30
+ * - 统计所有组的文件数量
31
+ * - 统计 mock 状态、锁定状态
32
+ * - 统计版本数量
33
+ * - 计算磁盘占用(groups, backup, recycle_bin, tmp)
34
+ *
35
+ * 响应:
36
+ * ```javascript
37
+ * {
38
+ * code: 0,
39
+ * data: {
40
+ * // 文件统计
41
+ * totalFiles: 100,
42
+ * mockEnabled: 30,
43
+ * mockDisabled: 70,
44
+ * locked: 5,
45
+ * totalVersions: 50,
46
+ *
47
+ * // 磁盘使用(字节)
48
+ * diskUsage: {
49
+ * total: 10485760,
50
+ * groups: 8388608,
51
+ * backup: 1048576,
52
+ * recycleBin: 1048576,
53
+ * tmp: 0
54
+ * },
55
+ *
56
+ * // 组详情
57
+ * groups: [
58
+ * {
59
+ * id: 'default',
60
+ * name: '默认组',
61
+ * mockEnabled: 30,
62
+ * locked: 5
63
+ * }
64
+ * ],
65
+ *
66
+ * // 元信息
67
+ * timestamp: 1762891525570,
68
+ * baseDir: '/path/to/.whistle/...'
69
+ * },
70
+ * msg: 'success'
71
+ * }
72
+ * ```
73
+ */
74
+ router.get('/cgi-bin/mockbubu/storage-stats', async (ctx) => {
75
+ const timestamp = new Date().toLocaleString('zh-CN', { hour12: false })
76
+ console.log(`[mockbubu ${timestamp}] 开始统计存储信息`)
77
+
78
+ try {
79
+ const { storageAdapter } = ctx
80
+ const v3Storage = storageAdapter.getV3Storage()
81
+
82
+ // 1. 获取所有组
83
+ const groups = await v3Storage.listGroups()
84
+ console.log(`[mockbubu ${timestamp}] 组数量: ${groups.length}`)
85
+
86
+ // 2. 统计文件数据
87
+ let totalFiles = 0
88
+ let mockEnabled = 0
89
+ let mockDisabled = 0
90
+ let locked = 0
91
+ let totalVersions = 0
92
+
93
+ const groupsData = []
94
+
95
+ for (const group of groups) {
96
+ const fileIndexData = await v3Storage.getIndex(group.id)
97
+ totalFiles += fileIndexData.files.length
98
+
99
+ let groupMockEnabled = 0
100
+ let groupLocked = 0
101
+
102
+ // 统计每个文件的状态和版本数
103
+ for (const fileEntry of fileIndexData.files) {
104
+ // 从索引中读取配置信息(V3 架构已包含)
105
+ if (fileEntry.config) {
106
+ if (fileEntry.config.mock) {
107
+ mockEnabled++
108
+ groupMockEnabled++
109
+ } else {
110
+ mockDisabled++
111
+ }
112
+
113
+ if (fileEntry.config.locked) {
114
+ locked++
115
+ groupLocked++
116
+ }
117
+ } else {
118
+ mockDisabled++
119
+ }
120
+
121
+ // 统计版本数(从索引中的 versionCount)
122
+ totalVersions += fileEntry.versionCount || 0
123
+ }
124
+
125
+ groupsData.push({
126
+ id: group.id,
127
+ name: group.name,
128
+ description: group.description,
129
+ mockEnabled: groupMockEnabled,
130
+ locked: groupLocked,
131
+ })
132
+ }
133
+
134
+ // 3. 计算目录大小(包括 groups, .backup, .recycle_bin, .tmp)
135
+ const { exec } = require('child_process')
136
+ const { promisify } = require('util')
137
+ const execAsync = promisify(exec)
138
+
139
+ const getDirectorySize = async (dirPath) => {
140
+ try {
141
+ await fs.access(dirPath)
142
+ const { stdout, stderr } = await execAsync(`du -sk "${dirPath}"`)
143
+ if (stderr) {
144
+ console.error(`[mockbubu ${timestamp}] du stderr: ${stderr}`)
145
+ }
146
+ const sizeKB = parseInt(stdout.trim().split(/\s+/)[0])
147
+ const sizeBytes = sizeKB * 1024
148
+ console.log(`[mockbubu ${timestamp}] 目录大小: ${path.basename(dirPath)} => ${sizeKB} KB (${(sizeBytes / 1024 / 1024).toFixed(2)} MB)`)
149
+ return sizeBytes // 转换为字节
150
+ } catch (error) {
151
+ console.error(`[mockbubu ${timestamp}] 获取目录大小失败: ${dirPath}`, error.message)
152
+ return 0
153
+ }
154
+ }
155
+
156
+ const baseDir = v3Storage.baseDir
157
+ console.log(`[mockbubu ${timestamp}] baseDir: ${baseDir}`)
158
+
159
+ const [groupsSize, backupSize, recycleBinSize, tmpSize] = await Promise.all([
160
+ getDirectorySize(path.join(baseDir, 'groups')),
161
+ getDirectorySize(path.join(baseDir, '.backup')),
162
+ getDirectorySize(path.join(baseDir, '.recycle_bin')),
163
+ getDirectorySize(path.join(baseDir, '.tmp')),
164
+ ])
165
+
166
+ const totalDiskUsage = groupsSize + backupSize + recycleBinSize + tmpSize
167
+
168
+ console.log(`[mockbubu ${timestamp}] 统计完成:`, {
169
+ totalFiles,
170
+ mockEnabled,
171
+ locked,
172
+ totalVersions,
173
+ totalDiskUsage: (totalDiskUsage / 1024 / 1024).toFixed(2) + ' MB',
174
+ })
175
+
176
+ // 4. 返回统计数据
177
+ ctx.body = createSuccessResponse({
178
+ // 文件统计
179
+ totalFiles,
180
+ mockEnabled,
181
+ mockDisabled,
182
+ locked,
183
+ totalVersions,
184
+
185
+ // 磁盘使用统计
186
+ diskUsage: {
187
+ total: totalDiskUsage,
188
+ groups: groupsSize,
189
+ backup: backupSize,
190
+ recycleBin: recycleBinSize,
191
+ tmp: tmpSize,
192
+ },
193
+
194
+ // 组详情
195
+ groups: groupsData,
196
+
197
+ // 元信息
198
+ timestamp: Date.now(),
199
+ baseDir,
200
+ })
201
+ } catch (error) {
202
+ const timestampError = new Date().toLocaleString('zh-CN', { hour12: false })
203
+ console.error(`[mockbubu ${timestampError}] 存储统计失败:`, error)
204
+ ctx.body = createErrorResponse(`存储统计失败: ${error.message}`)
205
+ }
206
+ })
207
+
208
+ /**
209
+ * 获取生效的 Whistle 规则列表
210
+ *
211
+ * 功能:
212
+ * - 获取当前会话中生效的 mockbubu 规则
213
+ * - 规则由 server.js 在捕获请求时记录
214
+ * - 用于帮助用户调试 Whistle 规则配置
215
+ *
216
+ * 响应:
217
+ * ```javascript
218
+ * {
219
+ * code: 0,
220
+ * data: {
221
+ * rules: [
222
+ * '*.example.com mockbubu://pathname',
223
+ * 'https://api.test.com mockbubu://href'
224
+ * ],
225
+ * count: 2,
226
+ * message: '检测到 2 条生效的规则'
227
+ * },
228
+ * msg: 'success'
229
+ * }
230
+ * ```
231
+ *
232
+ * 注意:
233
+ * - 规则列表会在一定时间后自动过期(10秒)
234
+ * - 如果没有规则,提示用户检查 Whistle 配置
235
+ */
236
+ router.get('/cgi-bin/mockbubu/active-rules', async (ctx) => {
237
+ try {
238
+ const rules = ruleCollector.getRules()
239
+ const count = ruleCollector.getCount()
240
+
241
+ ctx.body = createSuccessResponse({
242
+ rules,
243
+ count,
244
+ message: count > 0
245
+ ? `检测到 ${count} 条生效的规则`
246
+ : '尚未检测到生效的规则,请确保 Whistle 规则已配置并有请求触发',
247
+ })
248
+ } catch (error) {
249
+ console.error('[mockbubu] 获取规则列表失败:', error)
250
+ ctx.body = createErrorResponse(`获取规则列表失败: ${error.message}`)
251
+ }
252
+ })
253
+
254
+ /**
255
+ * 内存使用统计
256
+ *
257
+ * 功能:
258
+ * - 监控 Node.js 进程内存使用
259
+ * - 统计堆内存(Heap)使用情况
260
+ * - 统计新生代(New Space)和老生代(Old Space)
261
+ * - 提供 GC 相关统计信息
262
+ *
263
+ * 响应:
264
+ * ```javascript
265
+ * {
266
+ * code: 0,
267
+ * data: {
268
+ * // 总体堆内存(单位:MB)
269
+ * heap: {
270
+ * used: '50.00',
271
+ * total: '100.00',
272
+ * limit: '1024.00',
273
+ * usagePercent: '4.88'
274
+ * },
275
+ *
276
+ * // New Space(新生代,存放新对象)
277
+ * newSpace: {
278
+ * size: '10.00',
279
+ * used: '5.00',
280
+ * usagePercent: '50.00',
281
+ * available: '5.00',
282
+ * physical: '10.00'
283
+ * },
284
+ *
285
+ * // Old Space(老生代,存放长期对象)
286
+ * oldSpace: {
287
+ * size: '90.00',
288
+ * used: '45.00',
289
+ * usagePercent: '50.00',
290
+ * available: '45.00',
291
+ * physical: '90.00'
292
+ * },
293
+ *
294
+ * // 进程总内存
295
+ * process: {
296
+ * rss: '120.00', // 常驻内存
297
+ * heapUsed: '50.00', // 已用堆内存
298
+ * heapTotal: '100.00', // 总堆内存
299
+ * external: '2.00', // C++ 对象内存
300
+ * arrayBuffers: '1.00' // ArrayBuffer 内存
301
+ * },
302
+ *
303
+ * // GC 统计
304
+ * gc: {
305
+ * mallocedMemory: '55.00',
306
+ * peakMalloced: '60.00',
307
+ * nativeContexts: 5,
308
+ * detachedContexts: 0
309
+ * },
310
+ *
311
+ * timestamp: '2025-12-02T10:00:00.000Z',
312
+ * pid: 12345
313
+ * },
314
+ * msg: '内存统计数据获取成功'
315
+ * }
316
+ * ```
317
+ */
318
+ router.get('/cgi-bin/mockbubu/memory-stats', async (ctx) => {
319
+ try {
320
+ const v8 = require('v8')
321
+ const memoryUsage = process.memoryUsage()
322
+ const heapStats = v8.getHeapStatistics()
323
+ const heapSpaces = v8.getHeapSpaceStatistics()
324
+
325
+ // 格式化字节为 MB
326
+ const formatMB = (bytes) => (bytes / 1024 / 1024).toFixed(2)
327
+
328
+ // New Space 和 Old Space
329
+ const newSpace = heapSpaces.find(space => space.space_name === 'new_space')
330
+ const oldSpace = heapSpaces.find(space => space.space_name === 'old_space')
331
+
332
+ // 计算使用率
333
+ const heapUsagePercent = ((heapStats.used_heap_size / heapStats.heap_size_limit) * 100).toFixed(2)
334
+
335
+ ctx.body = createSuccessResponse({
336
+ // 总体堆内存
337
+ heap: {
338
+ used: formatMB(heapStats.used_heap_size),
339
+ total: formatMB(heapStats.total_heap_size),
340
+ limit: formatMB(heapStats.heap_size_limit),
341
+ usagePercent: heapUsagePercent,
342
+ },
343
+ // New Space (Young Generation)
344
+ newSpace: newSpace
345
+ ? {
346
+ size: formatMB(newSpace.space_size),
347
+ used: formatMB(newSpace.space_used_size),
348
+ usagePercent: ((newSpace.space_used_size / newSpace.space_size) * 100).toFixed(2),
349
+ available: formatMB(newSpace.space_available_size),
350
+ physical: formatMB(newSpace.physical_space_size),
351
+ }
352
+ : null,
353
+ // Old Space (Old Generation)
354
+ oldSpace: oldSpace
355
+ ? {
356
+ size: formatMB(oldSpace.space_size),
357
+ used: formatMB(oldSpace.space_used_size),
358
+ usagePercent: ((oldSpace.space_used_size / oldSpace.space_size) * 100).toFixed(2),
359
+ available: formatMB(oldSpace.space_available_size),
360
+ physical: formatMB(oldSpace.physical_space_size),
361
+ }
362
+ : null,
363
+ // 进程总内存
364
+ process: {
365
+ rss: formatMB(memoryUsage.rss),
366
+ heapUsed: formatMB(memoryUsage.heapUsed),
367
+ heapTotal: formatMB(memoryUsage.heapTotal),
368
+ external: formatMB(memoryUsage.external),
369
+ arrayBuffers: formatMB(memoryUsage.arrayBuffers),
370
+ },
371
+ // GC 统计
372
+ gc: {
373
+ mallocedMemory: formatMB(heapStats.malloced_memory),
374
+ peakMalloced: formatMB(heapStats.peak_malloced_memory),
375
+ nativeContexts: heapStats.number_of_native_contexts,
376
+ detachedContexts: heapStats.number_of_detached_contexts,
377
+ },
378
+ // 时间戳
379
+ timestamp: new Date().toISOString(),
380
+ pid: process.pid,
381
+ }, '内存统计数据获取成功')
382
+ } catch (error) {
383
+ ctx.body = createErrorResponse(error.message)
384
+ }
385
+ })
386
+ }
@@ -1,21 +1,21 @@
1
1
  const {
2
2
  createErrorResponse,
3
3
  createSuccessResponse,
4
- } = require('../util')
4
+ } = require('../../utils/util')
5
5
  const {
6
6
  validate,
7
- } = require('../validator')
7
+ } = require('../../utils/validator')
8
8
 
9
9
  /**
10
10
  * 组管理路由
11
11
  */
12
12
  module.exports = (router) => {
13
13
  // 获取组列表
14
- router.post('/cgi-bin/mockbubu/groups/list', (ctx) => {
14
+ router.post('/cgi-bin/mockbubu/groups/list', async (ctx) => {
15
15
  try {
16
16
  const { groupManager } = ctx
17
- const groups = groupManager.getGroups()
18
- const currentGroupId = groupManager.getCurrentGroupId()
17
+ const groups = await groupManager.getGroups()
18
+ const currentGroupId = await groupManager.getCurrentGroupId()
19
19
 
20
20
  ctx.body = createSuccessResponse({
21
21
  groups,
@@ -27,14 +27,14 @@ module.exports = (router) => {
27
27
  })
28
28
 
29
29
  // 获取当前组
30
- router.post('/cgi-bin/mockbubu/groups/current', (ctx) => {
30
+ router.post('/cgi-bin/mockbubu/groups/current', async (ctx) => {
31
31
  try {
32
32
  const { groupManager } = ctx
33
- const currentGroup = groupManager.getCurrentGroup()
33
+ const currentGroup = await groupManager.getCurrentGroup()
34
34
 
35
35
  ctx.body = createSuccessResponse({
36
36
  currentGroup,
37
- currentGroupId: groupManager.getCurrentGroupId(),
37
+ currentGroupId: await groupManager.getCurrentGroupId(),
38
38
  })
39
39
  } catch (error) {
40
40
  ctx.body = createErrorResponse(error.message)
@@ -65,12 +65,12 @@ module.exports = (router) => {
65
65
  }
66
66
  return null
67
67
  },
68
- }), (ctx) => {
68
+ }), async (ctx) => {
69
69
  try {
70
70
  const { groupManager } = ctx
71
71
  const { name, description, copyFromGroupId } = ctx.request.body
72
72
 
73
- const newGroup = groupManager.createGroup({
73
+ const newGroup = await groupManager.createGroup({
74
74
  name: name.trim(),
75
75
  description: description ? description.trim() : '',
76
76
  copyFromGroupId: copyFromGroupId || null,
@@ -107,12 +107,12 @@ module.exports = (router) => {
107
107
  }
108
108
  return null
109
109
  },
110
- }), (ctx) => {
110
+ }), async (ctx) => {
111
111
  try {
112
112
  const { groupManager } = ctx
113
113
  const { groupId, name, description } = ctx.request.body
114
114
 
115
- const updatedGroup = groupManager.updateGroup(groupId, {
115
+ const updatedGroup = await groupManager.updateGroup(groupId, {
116
116
  name: name ? name.trim() : undefined,
117
117
  description: description !== undefined ? description.trim() : undefined,
118
118
  })
@@ -131,12 +131,13 @@ module.exports = (router) => {
131
131
  }
132
132
  return null
133
133
  },
134
- }), (ctx) => {
134
+ }), async (ctx) => {
135
135
  try {
136
- const { groupManager } = ctx
136
+ const { groupManager, memoryBuffer } = ctx
137
137
  const { groupId } = ctx.request.body
138
138
 
139
- groupManager.deleteGroup(groupId)
139
+ // 删除组时清理内存缓冲区
140
+ await groupManager.deleteGroup(groupId, { memoryBuffer })
140
141
 
141
142
  ctx.body = createSuccessResponse(null, '删除成功')
142
143
  } catch (error) {
@@ -152,12 +153,12 @@ module.exports = (router) => {
152
153
  }
153
154
  return null
154
155
  },
155
- }), (ctx) => {
156
+ }), async (ctx) => {
156
157
  try {
157
158
  const { groupManager } = ctx
158
159
  const { groupId } = ctx.request.body
159
160
 
160
- const currentGroup = groupManager.setCurrentGroup(groupId)
161
+ const currentGroup = await groupManager.setCurrentGroup(groupId)
161
162
 
162
163
  ctx.body = createSuccessResponse({
163
164
  currentGroup,
@@ -191,20 +192,20 @@ module.exports = (router) => {
191
192
  }
192
193
  return null
193
194
  },
194
- }), (ctx) => {
195
+ }), async (ctx) => {
195
196
  try {
196
197
  const { groupManager } = ctx
197
198
  const { sourceGroupId, name, description } = ctx.request.body
198
199
 
199
200
  // 检查源组是否存在
200
- const groups = groupManager.getGroups()
201
+ const groups = await groupManager.getGroups()
201
202
  const sourceGroup = groups.find(g => g.id === sourceGroupId)
202
203
  if (!sourceGroup) {
203
204
  throw new Error(`源组不存在: ${sourceGroupId}`)
204
205
  }
205
206
 
206
207
  // 创建新组并复制配置
207
- const newGroup = groupManager.createGroup({
208
+ const newGroup = await groupManager.createGroup({
208
209
  name: name.trim(),
209
210
  description: description ? description.trim() : '',
210
211
  copyFromGroupId: sourceGroupId,