whistle.mockbubu 1.0.0-dev.4 → 2.0.0-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.
@@ -1,10 +1,7 @@
1
1
  const {
2
2
  updateFile,
3
- removeFile,
4
- writeFile,
5
3
  readFile,
6
- setProperty,
7
- getProperty,
4
+ readSession,
8
5
  getApiListUpdated,
9
6
  setApiListUpdated,
10
7
  } = require('../../utils')
@@ -14,23 +11,46 @@ const {
14
11
  handleFilterList,
15
12
  getFullDataList,
16
13
  } = require('../util')
14
+ const {
15
+ validateFilename,
16
+ validateMockData,
17
+ validateBoolean,
18
+ validateRuleValue,
19
+ validate,
20
+ } = require('../validator')
17
21
  const versionRouter = require('./version-router')
22
+ const groupRouter = require('./group-router')
23
+ const {
24
+ createTarGz,
25
+ extractTarGz,
26
+ parseMultipartFile,
27
+ sanitizeFilename,
28
+ } = require('../../archive-utils')
29
+ const fs = require('fs').promises
30
+ const path = require('path')
31
+ const os = require('os')
18
32
 
19
33
  module.exports = (router) => {
20
34
  // 文件版本路由
21
35
  versionRouter(router)
36
+ // 组管理路由
37
+ groupRouter(router)
22
38
 
23
39
  // 获取列表数据接口
24
40
  router.post('/cgi-bin/mockbubu/api-list', async (ctx) => {
25
41
  try {
26
- const { localStorage } = ctx.req
42
+ const { storageAdapter, groupManager } = ctx
27
43
 
44
+ // 获取当前激活的组ID
45
+ const currentGroupId = await groupManager.getCurrentGroupId()
46
+
47
+ // 从当前组读取数据
28
48
  const filteredList = handleFilterList(
29
- getFullDataList(localStorage),
49
+ await getFullDataList(storageAdapter, groupManager, currentGroupId),
30
50
  ctx.request.body,
31
51
  )
32
52
 
33
- setApiListUpdated(localStorage, false)
53
+ await setApiListUpdated(storageAdapter, false)
34
54
  ctx.body = createSuccessResponse(filteredList || [])
35
55
  } catch (error) {
36
56
  ctx.body = createErrorResponse(error.message)
@@ -38,10 +58,10 @@ module.exports = (router) => {
38
58
  })
39
59
 
40
60
  // 检测接口是否更新
41
- router.get('/cgi-bin/mockbubu/check-api-list', (ctx) => {
61
+ router.get('/cgi-bin/mockbubu/check-api-list', async (ctx) => {
42
62
  try {
43
- const { localStorage } = ctx.req
44
- const updated = getApiListUpdated(localStorage)
63
+ const { storageAdapter } = ctx
64
+ const updated = await getApiListUpdated(storageAdapter)
45
65
  ctx.body = createSuccessResponse(updated)
46
66
  } catch (error) {
47
67
  ctx.body = createErrorResponse(error.message)
@@ -49,17 +69,23 @@ module.exports = (router) => {
49
69
  })
50
70
 
51
71
  // 更新文件详情接口
52
- router.post('/cgi-bin/mockbubu/update-api-data', (ctx) => {
72
+ router.post('/cgi-bin/mockbubu/update-api-data', validate({
73
+ name: validateFilename,
74
+ data: validateMockData,
75
+ }), async (ctx) => {
53
76
  try {
54
- const { localStorage } = ctx.req
77
+ const { storageAdapter } = ctx
78
+ const { groupManager } = ctx
55
79
  const { name, data } = ctx.request.body
56
80
 
57
- updateFile(localStorage, name, data)
58
- const file = readFile(localStorage, name)
59
- const props = getProperty(localStorage, name) || {}
81
+ const currentGroupId = await groupManager.getCurrentGroupId()
82
+
83
+ updateFile(storageAdapter, name, data, currentGroupId)
84
+ const file = await readFile(storageAdapter, name, currentGroupId)
85
+ const config = await groupManager.getGroupFileConfig(currentGroupId, name)
60
86
 
61
87
  ctx.body = createSuccessResponse({
62
- ...props,
88
+ ...config,
63
89
  name,
64
90
  data: file,
65
91
  })
@@ -69,18 +95,25 @@ module.exports = (router) => {
69
95
  })
70
96
 
71
97
  // 获取文件详情接口
72
- router.post('/cgi-bin/mockbubu/get-api-data', (ctx) => {
98
+ router.post('/cgi-bin/mockbubu/get-api-data', validate({
99
+ name: validateFilename,
100
+ }), async (ctx) => {
73
101
  try {
74
- const { localStorage } = ctx.req
102
+ const { storageAdapter } = ctx
103
+ const { groupManager } = ctx
75
104
  const { name } = ctx.request.body
76
105
 
77
- const file = readFile(localStorage, name)
78
- const props = getProperty(localStorage, name) || {}
106
+ const currentGroupId = await groupManager.getCurrentGroupId()
107
+
108
+ const file = await readFile(storageAdapter, name, currentGroupId)
109
+ const session = await readSession(storageAdapter, name, currentGroupId)
110
+ const config = await groupManager.getGroupFileConfig(currentGroupId, name)
79
111
 
80
112
  ctx.body = createSuccessResponse({
81
- ...props,
113
+ ...config,
82
114
  name,
83
115
  data: file,
116
+ session, // 包含完整的 req 和 res 数据
84
117
  })
85
118
  } catch (error) {
86
119
  ctx.body = createErrorResponse(error.message)
@@ -88,17 +121,50 @@ module.exports = (router) => {
88
121
  })
89
122
 
90
123
  // 新增mock接口
91
- router.post('/cgi-bin/mockbubu/create-api-data', (ctx) => {
124
+ router.post('/cgi-bin/mockbubu/create-api-data', validate({
125
+ name: validateFilename,
126
+ content: validateMockData,
127
+ ruleValue: validateRuleValue,
128
+ }), async (ctx) => {
92
129
  try {
93
- const { localStorage } = ctx.req
130
+ const { storageAdapter } = ctx
131
+ const { groupManager } = ctx
94
132
  const { name, content, ruleValue } = ctx.request.body
95
133
 
96
- writeFile({
97
- storage: localStorage,
98
- filename: name,
99
- body: content,
100
- properties: { ruleValue, mock: true },
101
- })
134
+ // 获取当前组ID
135
+ const currentGroupId = await groupManager.getCurrentGroupId()
136
+
137
+ // 完全隔离架构:检查当前组的文件是否存在
138
+ const filePath = `${currentGroupId}/${name}`
139
+ const fileExists = !!await storageAdapter.readFile(filePath)
140
+
141
+ // 创建文件到当前组目录
142
+ if (!fileExists) {
143
+ const session = {
144
+ req: {
145
+ method: 'GET',
146
+ url: name,
147
+ headers: {},
148
+ },
149
+ res: {
150
+ statusCode: 200,
151
+ headers: { 'content-type': 'application/json' },
152
+ body: content, // content 是 JSON 字符串
153
+ },
154
+ }
155
+ await storageAdapter.writeFile(filePath, JSON.stringify(session))
156
+ }
157
+
158
+ // 在当前组配置中添加记录
159
+ const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
160
+ groupConfig.ruleValue = ruleValue
161
+ groupConfig.mock = true
162
+ groupConfig.mockTime = Date.now()
163
+ groupConfig.url = name
164
+ groupConfig.method = 'GET'
165
+ groupConfig.date = Date.now()
166
+ groupConfig.status = 200
167
+ await groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
102
168
 
103
169
  ctx.body = createSuccessResponse(null, '创建成功')
104
170
  } catch (error) {
@@ -107,12 +173,24 @@ module.exports = (router) => {
107
173
  })
108
174
 
109
175
  // 修改接口mock开关
110
- router.post('/cgi-bin/mockbubu/update-api-mock', (ctx) => {
176
+ router.post('/cgi-bin/mockbubu/update-api-mock', validate({
177
+ name: validateFilename,
178
+ mock: (val) => validateBoolean(val, 'mock'),
179
+ }), async (ctx) => {
111
180
  try {
112
- const { localStorage } = ctx.req
181
+ const { groupManager } = ctx
113
182
  const { name, mock } = ctx.request.body
114
183
 
115
- setProperty(localStorage, name, { mock })
184
+ // 获取当前组ID
185
+ const currentGroupId = await groupManager.getCurrentGroupId()
186
+ // 获取当前组配置
187
+ const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
188
+ // 更新 mock 状态
189
+ groupConfig.mock = mock
190
+ groupConfig.mockTime = Date.now()
191
+ // 写回组配置
192
+ await groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
193
+
116
194
  ctx.body = createSuccessResponse(null, '更新成功')
117
195
  } catch (error) {
118
196
  ctx.body = createErrorResponse(error.message)
@@ -120,50 +198,995 @@ module.exports = (router) => {
120
198
  })
121
199
 
122
200
  // 修改接口lock开关
123
- router.post('/cgi-bin/mockbubu/update-api-lock', (ctx) => {
201
+ router.post('/cgi-bin/mockbubu/update-api-lock', validate({
202
+ name: validateFilename,
203
+ locked: (val) => validateBoolean(val, 'locked'),
204
+ }), async (ctx) => {
124
205
  try {
125
- const { localStorage } = ctx.req
206
+ const { groupManager } = ctx
126
207
  const { name, locked } = ctx.request.body
127
208
 
128
- setProperty(localStorage, name, { locked })
209
+ // 获取当前组ID
210
+ const currentGroupId = await groupManager.getCurrentGroupId()
211
+ // 获取当前组配置
212
+ const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
213
+ // 更新 locked 状态
214
+ groupConfig.locked = locked
215
+ // 写回组配置
216
+ await groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
217
+
129
218
  ctx.body = createSuccessResponse(null, '更新成功')
130
219
  } catch (error) {
131
220
  ctx.body = createErrorResponse(error.message)
132
221
  }
133
222
  })
134
223
 
135
- // 删除接口数据
136
- router.post('/cgi-bin/mockbubu/delete-api', (ctx) => {
224
+ // 删除接口数据(智能删除)
225
+ router.post('/cgi-bin/mockbubu/delete-api', validate({
226
+ name: validateFilename,
227
+ }), async (ctx) => {
137
228
  try {
138
- const { localStorage } = ctx.req
139
- const { name } = ctx.request.body
229
+ const { storageAdapter } = ctx
230
+ const { groupManager } = ctx
231
+ const { name, groupId } = ctx.request.body
232
+
233
+ // 优先使用传入的 groupId,否则使用当前组ID
234
+ const targetGroupId = groupId || await groupManager.getCurrentGroupId()
235
+
236
+ // [mockbubu] 单文件删除日志 (delete-api)
237
+ console.log('[mockbubu] delete-api 开始', new Date().toLocaleString('zh-CN', { hour12: false }))
238
+ console.log('[mockbubu] 待删除文件:', name)
239
+ console.log('[mockbubu] 目标组ID:', targetGroupId)
240
+
241
+ // ✅ 检查文件是否被锁定
242
+ const fileConfig = await groupManager.getGroupFileConfig(targetGroupId, name)
243
+ if (fileConfig && fileConfig.locked) {
244
+ console.log('[mockbubu] delete-api 失败: 文件已锁定', name)
245
+ ctx.body = createErrorResponse('文件已锁定,无法删除')
246
+ return
247
+ }
248
+
249
+ // 完全隔离架构:直接删除物理文件 + 组配置
250
+ // 注意:storageAdapter.removeFile 是 async 方法,必须 await
251
+ await storageAdapter.removeFile(`${targetGroupId}/${name}`)
252
+ await groupManager.removeGroupFileConfig(targetGroupId, name)
253
+
254
+ console.log('[mockbubu] delete-api 删除成功:', name)
140
255
 
141
- removeFile(localStorage, name)
142
256
  ctx.body = createSuccessResponse(null, '删除成功')
143
257
  } catch (error) {
144
258
  ctx.body = createErrorResponse(error.message)
145
259
  }
146
260
  })
147
261
 
148
- // 批量删除接口
149
- router.post('/cgi-bin/mockbubu/batch-delete-api', (ctx) => {
262
+ // 批量删除接口(按范围)
263
+ router.post('/cgi-bin/mockbubu/batch-delete-api', async (ctx) => {
150
264
  try {
151
- const { localStorage } = ctx.req
265
+ const { storageAdapter } = ctx
266
+ const { groupManager } = ctx
267
+
268
+ // 获取当前组ID
269
+ const currentGroupId = await groupManager.getCurrentGroupId()
270
+
271
+ const filteredList = handleFilterList(
272
+ await getFullDataList(storageAdapter, groupManager, currentGroupId),
273
+ {
274
+ ...ctx.request.body,
275
+ locked: 'unlocked', // 锁定的文件不可批量删除
276
+ },
277
+ )
278
+
279
+ if (filteredList.length === 0) {
280
+ ctx.body = createErrorResponse('没有符合条件的文件', 400)
281
+ return
282
+ }
283
+
284
+ // 完全隔离架构:直接批量删除
285
+ const result = {
286
+ total: filteredList.length,
287
+ success: 0,
288
+ failed: 0,
289
+ errors: [],
290
+ }
152
291
 
153
- const filteredList = handleFilterList(getFullDataList(localStorage), {
154
- ...ctx.request.body,
155
- locked: 'unlocked', // 锁定的文件不可批量删除
292
+ // [mockbubu] 批量删除日志
293
+ console.log('[mockbubu] 批量删除开始', new Date().toLocaleString('zh-CN', { hour12: false }))
294
+ console.log('[mockbubu] 待删除文件数量:', filteredList.length)
295
+ console.log('[mockbubu] 待删除文件列表:', filteredList.map(f => f.name))
296
+
297
+ for (const item of filteredList) {
298
+ try {
299
+ console.log('[mockbubu] 正在删除:', item.name)
300
+ // 直接删除物理文件 + 组配置
301
+ // 注意:storageAdapter.removeFile 是 async 方法,必须 await
302
+ await storageAdapter.removeFile(`${currentGroupId}/${item.name}`)
303
+ await groupManager.removeGroupFileConfig(currentGroupId, item.name)
304
+ result.success++
305
+ console.log('[mockbubu] 删除成功:', item.name)
306
+ } catch (error) {
307
+ result.failed++
308
+ result.errors.push({
309
+ name: item.name,
310
+ error: error.message,
311
+ })
312
+ console.error(`[mockbubu] 删除文件 ${item.name} 失败:`, error.message, error.stack)
313
+ }
314
+ }
315
+
316
+ console.log('[mockbubu] 批量删除结束', new Date().toLocaleString('zh-CN', { hour12: false }), {
317
+ total: result.total,
318
+ success: result.success,
319
+ failed: result.failed,
156
320
  })
157
- // 批量删除
158
- filteredList.forEach((item) => {
321
+
322
+ if (result.failed > 0) {
323
+ ctx.body = createSuccessResponse(
324
+ result,
325
+ `删除完成: 成功 ${result.success}/${result.total},失败 ${result.failed}`,
326
+ )
327
+ } else {
328
+ ctx.body = createSuccessResponse(
329
+ { total: result.total, success: result.success },
330
+ `成功删除 ${result.success} 个文件`,
331
+ )
332
+ }
333
+ } catch (error) {
334
+ ctx.body = createErrorResponse(error.message)
335
+ }
336
+ })
337
+
338
+
339
+ // 批量删除接口(按名称数组) - 智能删除
340
+ router.post('/cgi-bin/mockbubu/batch-delete-by-names', validate({
341
+ names: (val) => {
342
+ if (!Array.isArray(val)) {
343
+ throw new Error('names 必须是数组')
344
+ }
345
+ if (val.length === 0) {
346
+ throw new Error('names 不能为空')
347
+ }
348
+ val.forEach(name => validateFilename(name))
349
+ return null
350
+ },
351
+ }), async (ctx) => {
352
+ try {
353
+ const { storageAdapter } = ctx
354
+ const { groupManager } = ctx
355
+ const { names } = ctx.request.body
356
+
357
+ // 获取当前组ID
358
+ const currentGroupId = await groupManager.getCurrentGroupId()
359
+
360
+ const result = {
361
+ total: names.length,
362
+ success: 0,
363
+ failed: 0,
364
+ locked: 0,
365
+ errors: [],
366
+ }
367
+
368
+ // [mockbubu] 批量删除日志 (batch-delete-by-names)
369
+ console.log('[mockbubu] batch-delete-by-names 开始', new Date().toLocaleString('zh-CN', { hour12: false }))
370
+ console.log('[mockbubu] 待删除文件数量:', names.length)
371
+ console.log('[mockbubu] 待删除文件列表:', names)
372
+
373
+ for (const name of names) {
159
374
  try {
160
- removeFile(localStorage, item.name)
375
+ console.log('[mockbubu] 正在处理:', name)
376
+ // 从当前组配置检查文件是否锁定
377
+ const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
378
+ if (groupConfig.locked) {
379
+ console.log('[mockbubu] 文件已锁定,跳过:', name)
380
+ result.locked++
381
+ result.errors.push({ name, error: '文件已锁定' })
382
+ continue
383
+ }
384
+
385
+ // 完全隔离架构:直接删除物理文件 + 组配置
386
+ // 注意:storageAdapter.removeFile 是 async 方法,必须 await
387
+ await storageAdapter.removeFile(`${currentGroupId}/${name}`)
388
+ await groupManager.removeGroupFileConfig(currentGroupId, name)
389
+
390
+ result.success++
391
+ console.log('[mockbubu] 删除成功:', name)
161
392
  } catch (error) {
162
- console.error(`删除文件 ${item.name} 失败:`, error.message)
393
+ result.failed++
394
+ result.errors.push({ name, error: error.message })
395
+ console.error(`[mockbubu] 删除文件 ${name} 失败:`, error.message, error.stack)
163
396
  }
397
+ }
398
+
399
+ console.log('[mockbubu] batch-delete-by-names 结束', new Date().toLocaleString('zh-CN', { hour12: false }), {
400
+ total: result.total,
401
+ success: result.success,
402
+ failed: result.failed,
403
+ locked: result.locked,
164
404
  })
165
405
 
166
- ctx.body = createSuccessResponse(null, '删除成功')
406
+ // 返回结果
407
+ if (result.locked > 0 && result.success === 0) {
408
+ ctx.body = createErrorResponse('所有文件都已锁定,无法删除', 403)
409
+ return
410
+ }
411
+
412
+ if (result.failed > 0 || result.locked > 0) {
413
+ const messages = []
414
+ if (result.success > 0) {
415
+ messages.push(`成功 ${result.success}`)
416
+ }
417
+ if (result.locked > 0) {
418
+ messages.push(`锁定 ${result.locked}`)
419
+ }
420
+ if (result.failed > 0) {
421
+ messages.push(`失败 ${result.failed}`)
422
+ }
423
+ ctx.body = createSuccessResponse(
424
+ result,
425
+ `删除完成: ${messages.join(',')}`,
426
+ )
427
+ return
428
+ }
429
+
430
+ ctx.body = createSuccessResponse(
431
+ { total: result.total, success: result.success },
432
+ `成功删除 ${result.success} 个文件`,
433
+ )
434
+ } catch (error) {
435
+ ctx.body = createErrorResponse(error.message)
436
+ }
437
+ })
438
+
439
+ // 批量更新 mock 状态
440
+ router.post('/cgi-bin/mockbubu/batch-update-mock', validate({
441
+ names: (val) => {
442
+ if (!Array.isArray(val)) {
443
+ throw new Error('names 必须是数组')
444
+ }
445
+ if (val.length === 0) {
446
+ throw new Error('names 不能为空')
447
+ }
448
+ val.forEach(name => validateFilename(name))
449
+ return null
450
+ },
451
+ mock: (val) => validateBoolean(val, 'mock'),
452
+ }), async (ctx) => {
453
+ try {
454
+ const { groupManager } = ctx
455
+ const { names, mock } = ctx.request.body
456
+
457
+ // 获取当前组ID
458
+ const currentGroupId = await groupManager.getCurrentGroupId()
459
+ const errors = []
460
+ const mockTime = Date.now()
461
+
462
+ for (const name of names) {
463
+ try {
464
+ // 获取当前组配置
465
+ const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
466
+ // 更新 mock 状态
467
+ groupConfig.mock = mock
468
+ groupConfig.mockTime = mockTime
469
+ // 写回组配置
470
+ await groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
471
+ } catch (error) {
472
+ errors.push({ name, error: error.message })
473
+ if (process.env.NODE_ENV === 'development') {
474
+ console.error(`[mockbubu] 更新文件 ${name} 的 mock 状态失败:`, error.message)
475
+ }
476
+ }
477
+ }
478
+
479
+ if (errors.length > 0) {
480
+ ctx.body = createSuccessResponse({ errors }, `更新完成,${errors.length} 个文件更新失败`)
481
+ return
482
+ }
483
+
484
+ const action = mock ? '开启' : '关闭'
485
+ ctx.body = createSuccessResponse(null, `成功${action} ${names.length} 个文件的 Mock 状态`)
486
+ } catch (error) {
487
+ ctx.body = createErrorResponse(error.message)
488
+ }
489
+ })
490
+
491
+ // ==================== 压缩包导出/导入功能 ====================
492
+
493
+ /**
494
+ * 导出当前组为压缩包(仅导出含版本的文件)
495
+ * POST /cgi-bin/mockbubu/export-group-archive
496
+ */
497
+ router.post('/cgi-bin/mockbubu/export-group-archive', async (ctx) => {
498
+ console.log('[mockbubu 2025/11/19 16:30:00] 开始导出压缩包')
499
+
500
+ try {
501
+ const { groupManager, storageAdapter } = ctx
502
+ const currentGroupId = await groupManager.getCurrentGroupId()
503
+ const currentGroup = await groupManager.getCurrentGroup()
504
+ const v3Storage = storageAdapter.v3Storage
505
+
506
+ console.log(`[mockbubu 2025/11/19 16:30:00] 当前组: ${currentGroup.name} (${currentGroupId})`)
507
+
508
+ // 1. 读取索引
509
+ const index = await v3Storage.getIndex(currentGroupId)
510
+ console.log(`[mockbubu 2025/11/19 16:30:00] 索引文件总数: ${index.files.length}`)
511
+
512
+ // 2. 筛选含版本的文件
513
+ const filesWithVersions = []
514
+ for (const fileEntry of index.files) {
515
+ const versions = await v3Storage.listVersions(currentGroupId, fileEntry.id)
516
+ if (versions.length > 0) {
517
+ filesWithVersions.push({
518
+ ...fileEntry,
519
+ versionCount: versions.length,
520
+ })
521
+ }
522
+ }
523
+
524
+ console.log(`[mockbubu 2025/11/19 16:30:00] 含版本的文件数: ${filesWithVersions.length}`)
525
+
526
+ if (filesWithVersions.length === 0) {
527
+ ctx.body = createErrorResponse('当前组没有包含版本的文件,无法导出')
528
+ return
529
+ }
530
+
531
+ // 3. 创建临时目录
532
+ const tmpDir = path.join(os.tmpdir(), `mockbubu-export-${Date.now()}-${Math.random().toString(36).slice(2)}`)
533
+ const exportDir = path.join(tmpDir, 'export')
534
+ const filesDir = path.join(exportDir, 'files')
535
+ await fs.mkdir(filesDir, { recursive: true })
536
+
537
+ console.log(`[mockbubu 2025/11/19 16:30:00] 临时目录: ${tmpDir}`)
538
+
539
+ try {
540
+ // 4. 复制文件
541
+ for (const fileEntry of filesWithVersions) {
542
+ const sourceFileDir = path.join(v3Storage.groupsDir, currentGroupId, 'files', fileEntry.id)
543
+ const targetFileDir = path.join(filesDir, fileEntry.id)
544
+
545
+ // 递归复制文件目录(包括 file.json 和 versions/)
546
+ await v3Storage._copyDirectory(sourceFileDir, targetFileDir)
547
+ console.log(`[mockbubu 2025/11/19 16:30:00] 已复制文件: ${fileEntry.url} (${fileEntry.id})`)
548
+ }
549
+
550
+ // 5. 创建导出元数据
551
+ const exportMeta = {
552
+ exportTime: new Date().toISOString(),
553
+ exportVersion: '3.0.0',
554
+ groupName: currentGroup.name,
555
+ groupDescription: currentGroup.description || '',
556
+ fileCount: filesWithVersions.length,
557
+ files: filesWithVersions.map(f => ({
558
+ id: f.id,
559
+ url: f.url,
560
+ method: f.method,
561
+ versionCount: f.versionCount,
562
+ })),
563
+ }
564
+
565
+ await fs.writeFile(
566
+ path.join(exportDir, 'export-meta.json'),
567
+ JSON.stringify(exportMeta, null, 2),
568
+ 'utf8',
569
+ )
570
+
571
+ console.log('[mockbubu 2025/11/19 16:30:00] 元数据已写入')
572
+
573
+ // 6. 创建压缩包
574
+ const archiveName = sanitizeFilename(`${currentGroup.name}-${Date.now()}.tar.gz`)
575
+ const archivePath = path.join(tmpDir, archiveName)
576
+
577
+ await createTarGz(exportDir, archivePath)
578
+ console.log(`[mockbubu 2025/11/19 16:30:00] 压缩包已创建: ${archivePath}`)
579
+
580
+ // 7. 读取压缩包内容
581
+ const archiveBuffer = await fs.readFile(archivePath)
582
+ console.log(`[mockbubu 2025/11/19 16:30:00] 压缩包大小: ${(archiveBuffer.length / 1024).toFixed(2)} KB`)
583
+
584
+ // 8. 返回压缩包
585
+ ctx.set('Content-Type', 'application/gzip')
586
+ ctx.set('Content-Disposition', `attachment; filename="${encodeURIComponent(archiveName)}"`)
587
+ ctx.body = archiveBuffer
588
+
589
+ // 9. 清理临时目录(异步执行,不阻塞响应)
590
+ fs.rm(tmpDir, { recursive: true, force: true }).catch(err => {
591
+ console.error(`[mockbubu 2025/11/19 16:30:00] 清理临时目录失败: ${err.message}`)
592
+ })
593
+
594
+ console.log('[mockbubu 2025/11/19 16:30:00] 导出完成')
595
+ } catch (err) {
596
+ // 出错时清理临时目录
597
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
598
+ throw err
599
+ }
600
+ } catch (error) {
601
+ console.error('[mockbubu 2025/11/19 16:30:00] 导出失败:', error.message)
602
+ ctx.set('Content-Type', 'application/json')
603
+ ctx.body = createErrorResponse(`导出失败: ${error.message}`)
604
+ }
605
+ })
606
+
607
+ /**
608
+ * 导入压缩包到新组
609
+ * POST /cgi-bin/mockbubu/import-group-archive
610
+ * Content-Type: multipart/form-data
611
+ */
612
+ router.post('/cgi-bin/mockbubu/import-group-archive', async (ctx) => {
613
+ console.log('[mockbubu 2025/11/19 16:30:00] 开始导入压缩包')
614
+
615
+ try {
616
+ const { storageAdapter } = ctx
617
+ const v3Storage = storageAdapter.v3Storage
618
+
619
+ // 1. 解析上传的文件
620
+ const file = await parseMultipartFile(ctx)
621
+ if (!file || !file.buffer) {
622
+ ctx.body = createErrorResponse('未检测到上传文件')
623
+ return
624
+ }
625
+
626
+ console.log(`[mockbubu 2025/11/19 16:30:00] 上传文件: ${file.filename}, 大小: ${(file.buffer.length / 1024).toFixed(2)} KB`)
627
+
628
+ // 2. 创建临时目录
629
+ const tmpDir = path.join(os.tmpdir(), `mockbubu-import-${Date.now()}-${Math.random().toString(36).slice(2)}`)
630
+ const extractDir = path.join(tmpDir, 'extract')
631
+ await fs.mkdir(extractDir, { recursive: true })
632
+
633
+ console.log(`[mockbubu 2025/11/19 16:30:00] 临时目录: ${tmpDir}`)
634
+
635
+ try {
636
+ // 3. 保存上传的压缩包
637
+ const uploadPath = path.join(tmpDir, 'upload.tar.gz')
638
+ await fs.writeFile(uploadPath, file.buffer)
639
+
640
+ // 4. 解压
641
+ await extractTarGz(uploadPath, extractDir)
642
+ console.log('[mockbubu 2025/11/19 16:30:00] 解压完成')
643
+
644
+ // 5. 读取导出元数据
645
+ const exportMetaPath = path.join(extractDir, 'export-meta.json')
646
+ const exportMetaContent = await fs.readFile(exportMetaPath, 'utf8')
647
+ const exportMeta = JSON.parse(exportMetaContent)
648
+
649
+ console.log(`[mockbubu 2025/11/19 16:30:00] 导出元数据: 组名=${exportMeta.groupName}, 文件数=${exportMeta.fileCount}`)
650
+
651
+ // 6. 生成新组名(处理冲突)
652
+ let newGroupName = exportMeta.groupName
653
+ let suffix = 1
654
+ const allGroups = await v3Storage.listGroups()
655
+ const existingNames = allGroups.map(g => g.name)
656
+
657
+ while (existingNames.includes(newGroupName)) {
658
+ newGroupName = `${exportMeta.groupName} (${suffix})`
659
+ suffix++
660
+ }
661
+
662
+ console.log(`[mockbubu 2025/11/19 16:30:00] 新组名: ${newGroupName}`)
663
+
664
+ // 7. 生成新组ID(确保唯一)
665
+ let newGroupId = newGroupName.toLowerCase().replace(/[^a-z0-9]/g, '-')
666
+ const existingIds = allGroups.map(g => g.id)
667
+
668
+ if (existingIds.includes(newGroupId)) {
669
+ newGroupId = `${newGroupId}-${Date.now()}`
670
+ }
671
+
672
+ console.log(`[mockbubu 2025/11/19 16:30:00] 新组ID: ${newGroupId}`)
673
+
674
+ // 8. 创建新组
675
+ await v3Storage.createGroup({
676
+ id: newGroupId,
677
+ name: newGroupName,
678
+ description: exportMeta.groupDescription || `从【${exportMeta.groupName}】导入`,
679
+ })
680
+
681
+ console.log('[mockbubu 2025/11/19 16:30:00] 新组已创建')
682
+
683
+ // 9. 复制文件到新组
684
+ const sourceFilesDir = path.join(extractDir, 'files')
685
+ const targetFilesDir = path.join(v3Storage.groupsDir, newGroupId, 'files')
686
+ await fs.mkdir(targetFilesDir, { recursive: true })
687
+
688
+ const importedFiles = []
689
+ for (const fileMeta of exportMeta.files) {
690
+ const sourceFileDir = path.join(sourceFilesDir, fileMeta.id)
691
+ const targetFileDir = path.join(targetFilesDir, fileMeta.id)
692
+
693
+ // 检查源文件是否存在
694
+ try {
695
+ await fs.access(sourceFileDir)
696
+ await v3Storage._copyDirectory(sourceFileDir, targetFileDir)
697
+
698
+ importedFiles.push(fileMeta)
699
+ console.log(`[mockbubu 2025/11/19 16:30:00] 已导入文件: ${fileMeta.url} (${fileMeta.id})`)
700
+ } catch (err) {
701
+ console.error(`[mockbubu 2025/11/19 16:30:00] 导入文件失败: ${fileMeta.id}`, err.message)
702
+ }
703
+ }
704
+
705
+ // 10. 重建索引
706
+ await v3Storage.rebuildIndex(newGroupId)
707
+ console.log('[mockbubu 2025/11/19 16:30:00] 索引已重建')
708
+
709
+ // 11. 清理临时目录
710
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
711
+
712
+ console.log('[mockbubu 2025/11/19 16:30:00] 导入完成')
713
+
714
+ ctx.body = createSuccessResponse(
715
+ {
716
+ groupId: newGroupId,
717
+ groupName: newGroupName,
718
+ fileCount: importedFiles.length,
719
+ },
720
+ `成功导入到新组【${newGroupName}】,共 ${importedFiles.length} 个文件`,
721
+ )
722
+ } catch (err) {
723
+ // 出错时清理临时目录
724
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
725
+ throw err
726
+ }
727
+ } catch (error) {
728
+ console.error('[mockbubu 2025/11/19 16:30:00] 导入失败:', error.message)
729
+ ctx.body = createErrorResponse(`导入失败: ${error.message}`)
730
+ }
731
+ })
732
+
733
+ // ==================== 存储管理功能 ====================
734
+
735
+ // 获取存储统计信息
736
+ router.get('/cgi-bin/mockbubu/storage-stats', async (ctx) => {
737
+ try {
738
+ const { storageAdapter } = ctx
739
+ const { groupManager } = ctx
740
+
741
+ // 获取所有物理文件
742
+ const allFiles = storageAdapter.getFileList()
743
+ const groups = groupManager.getGroups()
744
+
745
+ // 初始化统计数据
746
+ let totalSize = 0
747
+ let originalDataSize = 0
748
+ let versionDataSize = 0
749
+
750
+ const stats = {
751
+ mockEnabled: 0,
752
+ mockDisabled: 0,
753
+ locked: 0,
754
+ orphanFiles: 0,
755
+ unusedFiles: 0,
756
+ }
757
+
758
+ const groupsFileCount = {}
759
+ for (const group of groups) {
760
+ groupsFileCount[group.id] = 0
761
+ }
762
+
763
+ // 遍历所有文件统计
764
+ for (const item of allFiles) {
765
+ const filename = item.name
766
+ let hasConfig = false
767
+
768
+ // 读取文件大小
769
+ const file = await storageAdapter.readFile(filename)
770
+ if (file) {
771
+ const fileSize = Buffer.byteLength(file, 'utf8')
772
+ totalSize += fileSize
773
+ originalDataSize += fileSize
774
+ }
775
+
776
+ // 检查各组配置
777
+ for (const group of groups) {
778
+ // 使用 hasGroupConfig 直接检查配置是否存在
779
+ const hasActualConfig = await groupManager.hasGroupConfig(group.id, filename)
780
+
781
+ if (hasActualConfig) {
782
+ hasConfig = true
783
+ groupsFileCount[group.id]++
784
+
785
+ // 获取配置详情用于统计
786
+ const config = await groupManager.getGroupFileConfig(group.id, filename)
787
+
788
+ // 统计 mock 状态
789
+ if (config.mock) {
790
+ stats.mockEnabled++
791
+ } else {
792
+ stats.mockDisabled++
793
+ }
794
+
795
+ // 统计锁定状态
796
+ if (config.locked) {
797
+ stats.locked++
798
+ }
799
+
800
+ // 统计版本数据大小
801
+ const versions = await groupManager.getGroupVersions(group.id, filename)
802
+ for (const version of versions) {
803
+ if (version.content) {
804
+ const versionSize = Buffer.byteLength(JSON.stringify(version.content), 'utf8')
805
+ versionDataSize += versionSize
806
+ }
807
+ }
808
+ }
809
+ }
810
+
811
+ // 统计孤儿文件
812
+ if (!hasConfig) {
813
+ stats.orphanFiles++
814
+ }
815
+
816
+ // 统计未使用文件
817
+ if (hasConfig) {
818
+ let allMockDisabled = true
819
+ let allLocked = false
820
+ let hasCustomVersions = false
821
+
822
+ for (const group of groups) {
823
+ if (await groupManager.hasGroupConfig(group.id, filename)) {
824
+ const config = await groupManager.getGroupFileConfig(group.id, filename)
825
+
826
+ if (config.mock) {
827
+ allMockDisabled = false
828
+ }
829
+
830
+ if (config.locked) {
831
+ allLocked = true
832
+ }
833
+
834
+ const versions = await groupManager.getGroupVersions(group.id, filename)
835
+ const customVersions = versions.filter(v => v.filename !== 'source')
836
+ if (customVersions.length > 0) {
837
+ hasCustomVersions = true
838
+ }
839
+ }
840
+ }
841
+
842
+ // 未使用文件:所有组未开启mock + 所有组未锁定 + 所有组无自定义版本
843
+ if (allMockDisabled && !allLocked && !hasCustomVersions) {
844
+ stats.unusedFiles++
845
+ }
846
+ }
847
+ }
848
+
849
+ // 构造响应数据
850
+ const groupsData = groups.map(group => ({
851
+ id: group.id,
852
+ name: group.name,
853
+ fileCount: groupsFileCount[group.id],
854
+ }))
855
+
856
+ ctx.body = createSuccessResponse({
857
+ totalFiles: allFiles.length,
858
+ totalSize,
859
+ originalDataSize,
860
+ versionDataSize,
861
+ groups: groupsData,
862
+ stats,
863
+ })
864
+ } catch (error) {
865
+ ctx.body = createErrorResponse(error.message)
866
+ }
867
+ })
868
+
869
+ // 预览孤儿文件
870
+ router.get('/cgi-bin/mockbubu/preview-orphan-files', async (ctx) => {
871
+ try {
872
+ const { storageAdapter } = ctx
873
+ const { groupManager } = ctx
874
+
875
+ const allFiles = storageAdapter.getFileList()
876
+ const orphanFiles = []
877
+ let totalSize = 0
878
+
879
+ for (const item of allFiles) {
880
+ try {
881
+ // 解码文件名: {index}.{encodedName} -> groupId/filename
882
+ const decodedName = decodeURIComponent(item.name)
883
+
884
+ // 提取 groupId 和纯文件名
885
+ const slashIndex = decodedName.indexOf('/')
886
+ if (slashIndex === -1) {
887
+ // 文件名格式不正确,跳过
888
+ continue
889
+ }
890
+
891
+ const groupId = decodedName.substring(0, slashIndex)
892
+ const pureFilename = decodedName.substring(slashIndex + 1)
893
+
894
+ // 检查该组是否有配置
895
+ const hasConfig = await groupManager.hasGroupConfig(groupId, pureFilename)
896
+
897
+ // 是孤儿文件:物理文件存在但没有组配置
898
+ if (!hasConfig) {
899
+ const file = await storageAdapter.readFile(item.name)
900
+ const fileSize = file ? Buffer.byteLength(file, 'utf8') : 0
901
+
902
+ orphanFiles.push({
903
+ name: item.name, // 原始编码文件名
904
+ pureFilename, // 纯文件名
905
+ groupId, // 所属组
906
+ url: pureFilename, // URL 就是纯文件名
907
+ size: fileSize,
908
+ })
909
+
910
+ totalSize += fileSize
911
+ }
912
+ } catch (err) {
913
+ console.warn(`[mockbubu] 处理文件 ${item.name} 失败:`, err.message)
914
+ }
915
+ }
916
+
917
+ ctx.body = createSuccessResponse({
918
+ files: orphanFiles,
919
+ totalSize,
920
+ count: orphanFiles.length,
921
+ })
922
+ } catch (error) {
923
+ ctx.body = createErrorResponse(error.message)
924
+ }
925
+ })
926
+
927
+ // 清理孤儿文件
928
+ router.post('/cgi-bin/mockbubu/cleanup-orphan-files', async (ctx) => {
929
+ try {
930
+ const { storageAdapter } = ctx
931
+ const { groupManager } = ctx
932
+
933
+ const allFiles = storageAdapter.getFileList()
934
+ let deleted = 0
935
+ let freedSpace = 0
936
+
937
+ for (const item of allFiles) {
938
+ try {
939
+ // 解码文件名: {index}.{encodedName} -> groupId/filename
940
+ const decodedName = decodeURIComponent(item.name)
941
+
942
+ // 提取 groupId 和纯文件名
943
+ const slashIndex = decodedName.indexOf('/')
944
+ if (slashIndex === -1) {
945
+ // 文件名格式不正确,跳过
946
+ continue
947
+ }
948
+
949
+ const groupId = decodedName.substring(0, slashIndex)
950
+ const pureFilename = decodedName.substring(slashIndex + 1)
951
+
952
+ // 检查该组是否有配置
953
+ const hasConfig = await groupManager.hasGroupConfig(groupId, pureFilename)
954
+
955
+ // 删除孤儿文件:物理文件存在但没有组配置
956
+ if (!hasConfig) {
957
+ const file = await storageAdapter.readFile(item.name)
958
+ const fileSize = file ? Buffer.byteLength(file, 'utf8') : 0
959
+
960
+ // 删除物理文件(使用原始编码文件名)
961
+ await storageAdapter.removeFile(item.name)
962
+
963
+ deleted++
964
+ freedSpace += fileSize
965
+ }
966
+ } catch (err) {
967
+ console.warn(`[mockbubu] 处理文件 ${item.name} 失败:`, err.message)
968
+ }
969
+ }
970
+
971
+ const freedSpaceMB = (freedSpace / (1024 * 1024)).toFixed(2)
972
+ ctx.body = createSuccessResponse({
973
+ deleted,
974
+ freedSpace,
975
+ }, `成功清理 ${deleted} 个孤儿文件,释放 ${freedSpaceMB} MB 存储空间`)
976
+ } catch (error) {
977
+ ctx.body = createErrorResponse(error.message)
978
+ }
979
+ })
980
+
981
+ // 预览未使用文件
982
+ router.get('/cgi-bin/mockbubu/preview-unused-files', async (ctx) => {
983
+ try {
984
+ const { storageAdapter } = ctx
985
+ const { groupManager } = ctx
986
+
987
+ const allFiles = storageAdapter.getFileList()
988
+ const unusedFiles = []
989
+ let totalSize = 0
990
+
991
+ for (const item of allFiles) {
992
+ try {
993
+ // 解码文件名: {index}.{encodedName} -> groupId/filename
994
+ const decodedName = decodeURIComponent(item.name)
995
+
996
+ // 提取 groupId 和纯文件名
997
+ const slashIndex = decodedName.indexOf('/')
998
+ if (slashIndex === -1) {
999
+ continue
1000
+ }
1001
+
1002
+ const groupId = decodedName.substring(0, slashIndex)
1003
+ const pureFilename = decodedName.substring(slashIndex + 1)
1004
+
1005
+ // 检查该文件所属的组是否有配置
1006
+ if (!await groupManager.hasGroupConfig(groupId, pureFilename)) {
1007
+ // 没有配置,是孤儿文件,不是未使用文件
1008
+ continue
1009
+ }
1010
+
1011
+ const config = await groupManager.getGroupFileConfig(groupId, pureFilename)
1012
+
1013
+ // 检查是否为未使用文件:未开启mock + 未锁定 + 无自定义版本
1014
+ const isMockDisabled = !config.mock
1015
+ const isNotLocked = !config.locked
1016
+ const versions = await groupManager.getGroupVersions(groupId, pureFilename)
1017
+ const customVersions = versions.filter(v => v.filename !== 'source')
1018
+ const hasNoCustomVersions = customVersions.length === 0
1019
+
1020
+ if (isMockDisabled && isNotLocked && hasNoCustomVersions) {
1021
+ const file = await storageAdapter.readFile(item.name)
1022
+ const fileSize = file ? Buffer.byteLength(file, 'utf8') : 0
1023
+
1024
+ unusedFiles.push({
1025
+ name: item.name, // 原始编码文件名
1026
+ pureFilename, // 纯文件名
1027
+ groupId, // 所属组
1028
+ url: pureFilename,
1029
+ size: fileSize,
1030
+ mock: config.mock,
1031
+ locked: config.locked,
1032
+ })
1033
+
1034
+ totalSize += fileSize
1035
+ }
1036
+ } catch (err) {
1037
+ console.warn(`[mockbubu] 处理文件 ${item.name} 失败:`, err.message)
1038
+ }
1039
+ }
1040
+
1041
+ ctx.body = createSuccessResponse({
1042
+ files: unusedFiles,
1043
+ totalSize,
1044
+ count: unusedFiles.length,
1045
+ })
1046
+ } catch (error) {
1047
+ ctx.body = createErrorResponse(error.message)
1048
+ }
1049
+ })
1050
+
1051
+ // 清理未使用文件
1052
+ router.post('/cgi-bin/mockbubu/cleanup-unused-files', validate({
1053
+ confirmation: (val) => {
1054
+ if (val !== 'DELETE') {
1055
+ throw new Error('必须输入 DELETE 确认删除操作')
1056
+ }
1057
+ return null
1058
+ },
1059
+ }), async (ctx) => {
1060
+ try {
1061
+ const { storageAdapter } = ctx
1062
+ const { groupManager } = ctx
1063
+
1064
+ const allFiles = storageAdapter.getFileList()
1065
+ let deleted = 0
1066
+ let freedSpace = 0
1067
+
1068
+ for (const item of allFiles) {
1069
+ try {
1070
+ // 解码文件名: {index}.{encodedName} -> groupId/filename
1071
+ const decodedName = decodeURIComponent(item.name)
1072
+
1073
+ // 提取 groupId 和纯文件名
1074
+ const slashIndex = decodedName.indexOf('/')
1075
+ if (slashIndex === -1) {
1076
+ continue
1077
+ }
1078
+
1079
+ const groupId = decodedName.substring(0, slashIndex)
1080
+ const pureFilename = decodedName.substring(slashIndex + 1)
1081
+
1082
+ // 检查该文件所属的组是否有配置
1083
+ if (!await groupManager.hasGroupConfig(groupId, pureFilename)) {
1084
+ // 没有配置,是孤儿文件,不是未使用文件
1085
+ continue
1086
+ }
1087
+
1088
+ const config = await groupManager.getGroupFileConfig(groupId, pureFilename)
1089
+
1090
+ // 检查是否为未使用文件:未开启mock + 未锁定 + 无自定义版本
1091
+ const isMockDisabled = !config.mock
1092
+ const isNotLocked = !config.locked
1093
+ const versions = await groupManager.getGroupVersions(groupId, pureFilename)
1094
+ const customVersions = versions.filter(v => v.filename !== 'source')
1095
+ const hasNoCustomVersions = customVersions.length === 0
1096
+
1097
+ if (isMockDisabled && isNotLocked && hasNoCustomVersions) {
1098
+ const file = await storageAdapter.readFile(item.name)
1099
+ const fileSize = file ? Buffer.byteLength(file, 'utf8') : 0
1100
+
1101
+ // 删除物理文件(使用原始编码文件名)
1102
+ await storageAdapter.removeFile(item.name)
1103
+
1104
+ // 删除该组的配置(Layer 3)
1105
+ await groupManager.removeGroupFileConfig(groupId, pureFilename)
1106
+
1107
+ deleted++
1108
+ freedSpace += fileSize
1109
+ }
1110
+ } catch (err) {
1111
+ console.warn(`[mockbubu] 处理文件 ${item.name} 失败:`, err.message)
1112
+ }
1113
+ }
1114
+
1115
+ const freedSpaceMB = (freedSpace / (1024 * 1024)).toFixed(2)
1116
+ ctx.body = createSuccessResponse({
1117
+ deleted,
1118
+ freedSpace,
1119
+ }, `成功清理 ${deleted} 个未使用文件,释放 ${freedSpaceMB} MB 存储空间`)
1120
+ } catch (error) {
1121
+ ctx.body = createErrorResponse(error.message)
1122
+ }
1123
+ })
1124
+
1125
+ // 内存监控接口
1126
+ router.get('/cgi-bin/mockbubu/memory-stats', async (ctx) => {
1127
+ try {
1128
+ const v8 = require('v8')
1129
+ const memoryUsage = process.memoryUsage()
1130
+ const heapStats = v8.getHeapStatistics()
1131
+ const heapSpaces = v8.getHeapSpaceStatistics()
1132
+
1133
+ // 格式化字节为 MB
1134
+ const formatMB = (bytes) => (bytes / 1024 / 1024).toFixed(2)
1135
+
1136
+ // New Space 和 Old Space
1137
+ const newSpace = heapSpaces.find(space => space.space_name === 'new_space')
1138
+ const oldSpace = heapSpaces.find(space => space.space_name === 'old_space')
1139
+
1140
+ // 计算使用率
1141
+ const heapUsagePercent = ((heapStats.used_heap_size / heapStats.heap_size_limit) * 100).toFixed(2)
1142
+
1143
+ ctx.body = createSuccessResponse({
1144
+ // 总体堆内存
1145
+ heap: {
1146
+ used: formatMB(heapStats.used_heap_size),
1147
+ total: formatMB(heapStats.total_heap_size),
1148
+ limit: formatMB(heapStats.heap_size_limit),
1149
+ usagePercent: heapUsagePercent,
1150
+ },
1151
+ // New Space (Young Generation)
1152
+ newSpace: newSpace
1153
+ ? {
1154
+ size: formatMB(newSpace.space_size),
1155
+ used: formatMB(newSpace.space_used_size),
1156
+ usagePercent: ((newSpace.space_used_size / newSpace.space_size) * 100).toFixed(2),
1157
+ available: formatMB(newSpace.space_available_size),
1158
+ physical: formatMB(newSpace.physical_space_size),
1159
+ }
1160
+ : null,
1161
+ // Old Space (Old Generation)
1162
+ oldSpace: oldSpace
1163
+ ? {
1164
+ size: formatMB(oldSpace.space_size),
1165
+ used: formatMB(oldSpace.space_used_size),
1166
+ usagePercent: ((oldSpace.space_used_size / oldSpace.space_size) * 100).toFixed(2),
1167
+ available: formatMB(oldSpace.space_available_size),
1168
+ physical: formatMB(oldSpace.physical_space_size),
1169
+ }
1170
+ : null,
1171
+ // 进程总内存
1172
+ process: {
1173
+ rss: formatMB(memoryUsage.rss),
1174
+ heapUsed: formatMB(memoryUsage.heapUsed),
1175
+ heapTotal: formatMB(memoryUsage.heapTotal),
1176
+ external: formatMB(memoryUsage.external),
1177
+ arrayBuffers: formatMB(memoryUsage.arrayBuffers),
1178
+ },
1179
+ // GC 统计
1180
+ gc: {
1181
+ mallocedMemory: formatMB(heapStats.malloced_memory),
1182
+ peakMalloced: formatMB(heapStats.peak_malloced_memory),
1183
+ nativeContexts: heapStats.number_of_native_contexts,
1184
+ detachedContexts: heapStats.number_of_detached_contexts,
1185
+ },
1186
+ // 时间戳
1187
+ timestamp: new Date().toISOString(),
1188
+ pid: process.pid,
1189
+ }, '内存统计数据获取成功')
167
1190
  } catch (error) {
168
1191
  ctx.body = createErrorResponse(error.message)
169
1192
  }