whistle.mockbubu 1.0.0-dev.4 → 2.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.
@@ -1,10 +1,10 @@
1
1
  const {
2
2
  updateFile,
3
3
  removeFile,
4
- writeFile,
5
4
  readFile,
6
- setProperty,
5
+ readSession,
7
6
  getProperty,
7
+ getPropertyAttr,
8
8
  getApiListUpdated,
9
9
  setApiListUpdated,
10
10
  } = require('../../utils')
@@ -12,21 +12,37 @@ const {
12
12
  createErrorResponse,
13
13
  createSuccessResponse,
14
14
  handleFilterList,
15
+ filterGroupConfig,
15
16
  getFullDataList,
16
17
  } = require('../util')
18
+ const {
19
+ validateFilename,
20
+ validateMockData,
21
+ validateBoolean,
22
+ validateRuleValue,
23
+ validate,
24
+ } = require('../validator')
17
25
  const versionRouter = require('./version-router')
26
+ const groupRouter = require('./group-router')
18
27
 
19
28
  module.exports = (router) => {
20
29
  // 文件版本路由
21
30
  versionRouter(router)
31
+ // 组管理路由
32
+ groupRouter(router)
22
33
 
23
34
  // 获取列表数据接口
24
35
  router.post('/cgi-bin/mockbubu/api-list', async (ctx) => {
25
36
  try {
26
37
  const { localStorage } = ctx.req
38
+ const { groupManager } = ctx
39
+
40
+ // 获取当前激活的组ID
41
+ const currentGroupId = groupManager.getCurrentGroupId()
27
42
 
43
+ // 从当前组读取数据
28
44
  const filteredList = handleFilterList(
29
- getFullDataList(localStorage),
45
+ getFullDataList(localStorage, groupManager, currentGroupId),
30
46
  ctx.request.body,
31
47
  )
32
48
 
@@ -49,17 +65,23 @@ module.exports = (router) => {
49
65
  })
50
66
 
51
67
  // 更新文件详情接口
52
- router.post('/cgi-bin/mockbubu/update-api-data', (ctx) => {
68
+ router.post('/cgi-bin/mockbubu/update-api-data', validate({
69
+ name: validateFilename,
70
+ data: validateMockData,
71
+ }), (ctx) => {
53
72
  try {
54
73
  const { localStorage } = ctx.req
74
+ const { groupManager } = ctx
55
75
  const { name, data } = ctx.request.body
56
76
 
57
- updateFile(localStorage, name, data)
58
- const file = readFile(localStorage, name)
59
- const props = getProperty(localStorage, name) || {}
77
+ const currentGroupId = groupManager.getCurrentGroupId()
78
+
79
+ updateFile(localStorage, name, data, currentGroupId)
80
+ const file = readFile(localStorage, name, currentGroupId)
81
+ const config = groupManager.getGroupFileConfig(currentGroupId, name)
60
82
 
61
83
  ctx.body = createSuccessResponse({
62
- ...props,
84
+ ...config,
63
85
  name,
64
86
  data: file,
65
87
  })
@@ -69,18 +91,25 @@ module.exports = (router) => {
69
91
  })
70
92
 
71
93
  // 获取文件详情接口
72
- router.post('/cgi-bin/mockbubu/get-api-data', (ctx) => {
94
+ router.post('/cgi-bin/mockbubu/get-api-data', validate({
95
+ name: validateFilename,
96
+ }), (ctx) => {
73
97
  try {
74
98
  const { localStorage } = ctx.req
99
+ const { groupManager } = ctx
75
100
  const { name } = ctx.request.body
76
101
 
77
- const file = readFile(localStorage, name)
78
- const props = getProperty(localStorage, name) || {}
102
+ const currentGroupId = groupManager.getCurrentGroupId()
103
+
104
+ const file = readFile(localStorage, name, currentGroupId)
105
+ const session = readSession(localStorage, name, currentGroupId)
106
+ const config = groupManager.getGroupFileConfig(currentGroupId, name)
79
107
 
80
108
  ctx.body = createSuccessResponse({
81
- ...props,
109
+ ...config,
82
110
  name,
83
111
  data: file,
112
+ session, // 包含完整的 req 和 res 数据
84
113
  })
85
114
  } catch (error) {
86
115
  ctx.body = createErrorResponse(error.message)
@@ -88,17 +117,54 @@ module.exports = (router) => {
88
117
  })
89
118
 
90
119
  // 新增mock接口
91
- router.post('/cgi-bin/mockbubu/create-api-data', (ctx) => {
120
+ router.post('/cgi-bin/mockbubu/create-api-data', validate({
121
+ name: validateFilename,
122
+ content: validateMockData,
123
+ ruleValue: validateRuleValue,
124
+ }), (ctx) => {
92
125
  try {
93
126
  const { localStorage } = ctx.req
127
+ const { groupManager } = ctx
94
128
  const { name, content, ruleValue } = ctx.request.body
95
129
 
96
- writeFile({
97
- storage: localStorage,
98
- filename: name,
99
- body: content,
100
- properties: { ruleValue, mock: true },
101
- })
130
+ // 获取当前组ID
131
+ const currentGroupId = groupManager.getCurrentGroupId()
132
+
133
+ // 完全隔离架构:检查当前组的文件是否存在
134
+ const filePath = `${currentGroupId}/${name}`
135
+ const fileExists = !!localStorage.readFile(filePath)
136
+
137
+ // 创建文件到当前组目录
138
+ if (!fileExists) {
139
+ console.log(`[mockbubu] 创建文件: ${filePath}`)
140
+ const session = {
141
+ req: {
142
+ method: 'GET',
143
+ url: name,
144
+ headers: {},
145
+ },
146
+ res: {
147
+ statusCode: 200,
148
+ headers: { 'content-type': 'application/json' },
149
+ body: content, // content 是 JSON 字符串
150
+ },
151
+ }
152
+ localStorage.writeFile(filePath, JSON.stringify(session))
153
+ console.log(' - 已写入 storage')
154
+ } else {
155
+ console.log(`[mockbubu] 文件已存在,跳过创建: ${filePath}`)
156
+ }
157
+
158
+ // 在当前组配置中添加记录
159
+ const groupConfig = 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
+ 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
+ }), (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 = groupManager.getCurrentGroupId()
186
+ // 获取当前组配置
187
+ const groupConfig = groupManager.getGroupFileConfig(currentGroupId, name)
188
+ // 更新 mock 状态
189
+ groupConfig.mock = mock
190
+ groupConfig.mockTime = Date.now()
191
+ // 写回组配置
192
+ 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,1327 @@ 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
+ }), (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 = groupManager.getCurrentGroupId()
211
+ // 获取当前组配置
212
+ const groupConfig = groupManager.getGroupFileConfig(currentGroupId, name)
213
+ // 更新 locked 状态
214
+ groupConfig.locked = locked
215
+ // 写回组配置
216
+ 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
+ }), (ctx) => {
137
228
  try {
138
229
  const { localStorage } = ctx.req
139
- const { name } = ctx.request.body
230
+ const { groupManager } = ctx
231
+ const { name, groupId } = ctx.request.body
232
+
233
+ // 优先使用传入的 groupId,否则使用当前组ID
234
+ const targetGroupId = groupId || groupManager.getCurrentGroupId()
235
+
236
+ // 完全隔离架构:直接删除物理文件 + 组配置
237
+ removeFile(localStorage, name, targetGroupId)
238
+ groupManager.removeGroupFileConfig(targetGroupId, name)
140
239
 
141
- removeFile(localStorage, name)
142
240
  ctx.body = createSuccessResponse(null, '删除成功')
143
241
  } catch (error) {
144
242
  ctx.body = createErrorResponse(error.message)
145
243
  }
146
244
  })
147
245
 
148
- // 批量删除接口
246
+ // 批量删除接口(按范围)
149
247
  router.post('/cgi-bin/mockbubu/batch-delete-api', (ctx) => {
150
248
  try {
151
249
  const { localStorage } = ctx.req
250
+ const { groupManager } = ctx
251
+
252
+ // 获取当前组ID
253
+ const currentGroupId = groupManager.getCurrentGroupId()
254
+
255
+ const filteredList = handleFilterList(
256
+ getFullDataList(localStorage, groupManager, currentGroupId),
257
+ {
258
+ ...ctx.request.body,
259
+ locked: 'unlocked', // 锁定的文件不可批量删除
260
+ },
261
+ )
262
+
263
+ if (filteredList.length === 0) {
264
+ ctx.body = createErrorResponse('没有符合条件的文件', 400)
265
+ return
266
+ }
267
+
268
+ // 完全隔离架构:直接批量删除
269
+ const result = {
270
+ total: filteredList.length,
271
+ success: 0,
272
+ failed: 0,
273
+ errors: [],
274
+ }
152
275
 
153
- const filteredList = handleFilterList(getFullDataList(localStorage), {
154
- ...ctx.request.body,
155
- locked: 'unlocked', // 锁定的文件不可批量删除
156
- })
157
- // 批量删除
158
276
  filteredList.forEach((item) => {
159
277
  try {
160
- removeFile(localStorage, item.name)
278
+ // 直接删除物理文件 + 组配置
279
+ removeFile(localStorage, item.name, currentGroupId)
280
+ groupManager.removeGroupFileConfig(currentGroupId, item.name)
281
+ result.success++
161
282
  } catch (error) {
162
- console.error(`删除文件 ${item.name} 失败:`, error.message)
283
+ result.failed++
284
+ result.errors.push({
285
+ name: item.name,
286
+ error: error.message,
287
+ })
288
+ if (process.env.NODE_ENV === 'development') {
289
+ console.error(`[mockbubu] 删除文件 ${item.name} 失败:`, error.message)
290
+ }
163
291
  }
164
292
  })
165
293
 
166
- ctx.body = createSuccessResponse(null, '删除成功')
294
+ if (result.failed > 0) {
295
+ ctx.body = createSuccessResponse(
296
+ result,
297
+ `删除完成: 成功 ${result.success}/${result.total},失败 ${result.failed}`,
298
+ )
299
+ } else {
300
+ ctx.body = createSuccessResponse(
301
+ { total: result.total, success: result.success },
302
+ `成功删除 ${result.success} 个文件`,
303
+ )
304
+ }
305
+ } catch (error) {
306
+ ctx.body = createErrorResponse(error.message)
307
+ }
308
+ })
309
+
310
+ // 批量删除预检接口
311
+ router.post('/cgi-bin/mockbubu/batch-delete-preview', validate({
312
+ names: (val) => {
313
+ if (!Array.isArray(val)) {
314
+ throw new Error('names 必须是数组')
315
+ }
316
+ if (val.length === 0) {
317
+ throw new Error('names 不能为空')
318
+ }
319
+ val.forEach(name => validateFilename(name))
320
+ return null
321
+ },
322
+ }), (ctx) => {
323
+ try {
324
+ const { groupManager } = ctx
325
+ const { names } = ctx.request.body
326
+
327
+ // 获取当前组ID
328
+ const currentGroupId = groupManager.getCurrentGroupId()
329
+
330
+ const stats = {
331
+ total: names.length,
332
+ completeDelete: 0,
333
+ configOnlyDelete: 0,
334
+ details: [],
335
+ }
336
+
337
+ names.forEach(name => {
338
+ const isUsedByOthers = groupManager.isFileUsedByOtherGroups(currentGroupId, name)
339
+ const usingGroups = groupManager.getGroupsUsingFile(name)
340
+
341
+ if (isUsedByOthers) {
342
+ stats.configOnlyDelete++
343
+ stats.details.push({
344
+ name,
345
+ deleteMode: 'config-only',
346
+ usingGroups: usingGroups.filter(g => g.id !== currentGroupId),
347
+ })
348
+ } else {
349
+ stats.completeDelete++
350
+ stats.details.push({
351
+ name,
352
+ deleteMode: 'complete',
353
+ usingGroups: [],
354
+ })
355
+ }
356
+ })
357
+
358
+ ctx.body = createSuccessResponse(stats, '预检完成')
359
+ } catch (error) {
360
+ ctx.body = createErrorResponse(error.message)
361
+ }
362
+ })
363
+
364
+ // 批量删除接口(按名称数组) - 智能删除
365
+ router.post('/cgi-bin/mockbubu/batch-delete-by-names', validate({
366
+ names: (val) => {
367
+ if (!Array.isArray(val)) {
368
+ throw new Error('names 必须是数组')
369
+ }
370
+ if (val.length === 0) {
371
+ throw new Error('names 不能为空')
372
+ }
373
+ val.forEach(name => validateFilename(name))
374
+ return null
375
+ },
376
+ }), (ctx) => {
377
+ try {
378
+ const { localStorage } = ctx.req
379
+ const { groupManager } = ctx
380
+ const { names } = ctx.request.body
381
+
382
+ // 获取当前组ID
383
+ const currentGroupId = groupManager.getCurrentGroupId()
384
+
385
+ const result = {
386
+ total: names.length,
387
+ success: 0,
388
+ failed: 0,
389
+ locked: 0,
390
+ errors: [],
391
+ }
392
+
393
+ names.forEach((name) => {
394
+ try {
395
+ // 从当前组配置检查文件是否锁定
396
+ const groupConfig = groupManager.getGroupFileConfig(currentGroupId, name)
397
+ if (groupConfig.locked) {
398
+ result.locked++
399
+ result.errors.push({ name, error: '文件已锁定' })
400
+ return
401
+ }
402
+
403
+ // 完全隔离架构:直接删除物理文件 + 组配置
404
+ removeFile(localStorage, name, currentGroupId)
405
+ groupManager.removeGroupFileConfig(currentGroupId, name)
406
+
407
+ result.success++
408
+ } catch (error) {
409
+ result.failed++
410
+ result.errors.push({ name, error: error.message })
411
+ console.error(`[mockbubu] 删除文件 ${name} 失败:`, error.message)
412
+ }
413
+ })
414
+
415
+ // 返回结果
416
+ if (result.locked > 0 && result.success === 0) {
417
+ ctx.body = createErrorResponse('所有文件都已锁定,无法删除', 403)
418
+ return
419
+ }
420
+
421
+ if (result.failed > 0 || result.locked > 0) {
422
+ const messages = []
423
+ if (result.success > 0) {
424
+ messages.push(`成功 ${result.success}`)
425
+ }
426
+ if (result.locked > 0) {
427
+ messages.push(`锁定 ${result.locked}`)
428
+ }
429
+ if (result.failed > 0) {
430
+ messages.push(`失败 ${result.failed}`)
431
+ }
432
+ ctx.body = createSuccessResponse(
433
+ result,
434
+ `删除完成: ${messages.join(',')}`,
435
+ )
436
+ return
437
+ }
438
+
439
+ ctx.body = createSuccessResponse(
440
+ { total: result.total, success: result.success },
441
+ `成功删除 ${result.success} 个文件`,
442
+ )
443
+ } catch (error) {
444
+ ctx.body = createErrorResponse(error.message)
445
+ }
446
+ })
447
+
448
+ // 批量更新 mock 状态
449
+ router.post('/cgi-bin/mockbubu/batch-update-mock', validate({
450
+ names: (val) => {
451
+ if (!Array.isArray(val)) {
452
+ throw new Error('names 必须是数组')
453
+ }
454
+ if (val.length === 0) {
455
+ throw new Error('names 不能为空')
456
+ }
457
+ val.forEach(name => validateFilename(name))
458
+ return null
459
+ },
460
+ mock: (val) => validateBoolean(val, 'mock'),
461
+ }), (ctx) => {
462
+ try {
463
+ const { groupManager } = ctx
464
+ const { names, mock } = ctx.request.body
465
+
466
+ // 获取当前组ID
467
+ const currentGroupId = groupManager.getCurrentGroupId()
468
+ const errors = []
469
+ const mockTime = Date.now()
470
+
471
+ names.forEach((name) => {
472
+ try {
473
+ // 获取当前组配置
474
+ const groupConfig = groupManager.getGroupFileConfig(currentGroupId, name)
475
+ // 更新 mock 状态
476
+ groupConfig.mock = mock
477
+ groupConfig.mockTime = mockTime
478
+ // 写回组配置
479
+ groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
480
+ } catch (error) {
481
+ errors.push({ name, error: error.message })
482
+ if (process.env.NODE_ENV === 'development') {
483
+ console.error(`[mockbubu] 更新文件 ${name} 的 mock 状态失败:`, error.message)
484
+ }
485
+ }
486
+ })
487
+
488
+ if (errors.length > 0) {
489
+ ctx.body = createSuccessResponse({ errors }, `更新完成,${errors.length} 个文件更新失败`)
490
+ return
491
+ }
492
+
493
+ const action = mock ? '开启' : '关闭'
494
+ ctx.body = createSuccessResponse(null, `成功${action} ${names.length} 个文件的 Mock 状态`)
495
+ } catch (error) {
496
+ ctx.body = createErrorResponse(error.message)
497
+ }
498
+ })
499
+
500
+ // ==================== 分组导出功能 ====================
501
+
502
+ // 导出当前组的配置和数据
503
+ router.post('/cgi-bin/mockbubu/export-group', (ctx) => {
504
+ try {
505
+ const { localStorage } = ctx.req
506
+ const { groupManager } = ctx
507
+
508
+ const currentGroupId = groupManager.getCurrentGroupId()
509
+ const currentGroup = groupManager.getCurrentGroup()
510
+ const fileList = getFullDataList(localStorage, groupManager, currentGroupId)
511
+
512
+ const exportData = {
513
+ exportTime: new Date().toISOString(),
514
+ version: '2.0.0',
515
+ exportType: 'single-group',
516
+ group: {
517
+ id: currentGroup.id,
518
+ name: currentGroup.name,
519
+ description: currentGroup.description,
520
+ createTime: currentGroup.createTime,
521
+ updateTime: currentGroup.updateTime,
522
+ isDefault: currentGroup.isDefault,
523
+ },
524
+ files: fileList.map((item) => {
525
+ const session = readSession(localStorage, item.name)
526
+ const rawGroupConfig = groupManager.getGroupFileConfig(currentGroupId, item.name)
527
+ const groupConfig = filterGroupConfig(rawGroupConfig)
528
+
529
+ return {
530
+ name: item.name,
531
+ session, // 原始抓包数据
532
+ groupConfig, // 组级别的配置(已过滤)
533
+ }
534
+ }),
535
+ }
536
+
537
+ ctx.body = createSuccessResponse(exportData, `成功导出组【${currentGroup.name}】`)
538
+ } catch (error) {
539
+ ctx.body = createErrorResponse(error.message)
540
+ }
541
+ })
542
+
543
+ // 导出所有组的配置和数据
544
+ router.post('/cgi-bin/mockbubu/export-all-groups', (ctx) => {
545
+ try {
546
+ const { localStorage } = ctx.req
547
+ const { groupManager } = ctx
548
+
549
+ const groupsData = groupManager.getGroupsData()
550
+ const allFileNames = localStorage.getFileList().map(item => item.name)
551
+
552
+ const exportData = {
553
+ exportTime: new Date().toISOString(),
554
+ version: '2.0.0',
555
+ exportType: 'all-groups',
556
+ groupsData: {
557
+ groups: groupsData.groups,
558
+ currentGroupId: groupsData.currentGroupId,
559
+ },
560
+ files: allFileNames.map((filename) => {
561
+ const session = readSession(localStorage, filename)
562
+ const groupConfigs = {}
563
+
564
+ // 收集所有组对该文件的配置
565
+ groupsData.groups.forEach(group => {
566
+ const rawConfig = groupManager.getGroupFileConfig(group.id, filename)
567
+ const filteredConfig = filterGroupConfig(rawConfig)
568
+ // 只导出有实际配置的组
569
+ if (Object.keys(filteredConfig).length > 0) {
570
+ groupConfigs[group.id] = filteredConfig
571
+ }
572
+ })
573
+
574
+ return {
575
+ name: filename,
576
+ session, // 原始数据(所有组共享)
577
+ groupConfigs, // 各组的配置(已过滤)
578
+ }
579
+ }),
580
+ }
581
+
582
+ ctx.body = createSuccessResponse(exportData, `成功导出所有组(${groupsData.groups.length} 个组)`)
583
+ } catch (error) {
584
+ ctx.body = createErrorResponse(error.message)
585
+ }
586
+ })
587
+
588
+ // 导出所有 mock 数据(旧版本格式,保持兼容)
589
+ router.post('/cgi-bin/mockbubu/export-all', (ctx) => {
590
+ try {
591
+ const { localStorage } = ctx.req
592
+ const fileList = getFullDataList(localStorage)
593
+
594
+ const exportData = {
595
+ exportTime: new Date().toISOString(),
596
+ version: '1.0',
597
+ count: fileList.length,
598
+ data: fileList.map((item) => {
599
+ // 读取完整的 session 数据(包含 req 和 res)
600
+ const session = readSession(localStorage, item.name)
601
+ const props = getProperty(localStorage, item.name) || {}
602
+
603
+ return {
604
+ name: item.name,
605
+ session, // 导出完整的 session 数据
606
+ properties: props,
607
+ }
608
+ }),
609
+ }
610
+
611
+ ctx.body = createSuccessResponse(exportData, '导出成功(旧版格式)')
612
+ } catch (error) {
613
+ ctx.body = createErrorResponse(error.message)
614
+ }
615
+ })
616
+
617
+ // ==================== 分组导入功能 ====================
618
+
619
+ // 导入到当前组
620
+ router.post('/cgi-bin/mockbubu/import-to-current-group', validate({
621
+ data: (val) => {
622
+ if (!val || typeof val !== 'object') {
623
+ throw new Error('data 必须是对象')
624
+ }
625
+ if (val.exportType !== 'single-group') {
626
+ throw new Error('导入数据格式不正确,必须是单组导出的数据')
627
+ }
628
+ return null
629
+ },
630
+ confirmed: (val) => {
631
+ // confirmed 参数可选,必须是 boolean 或 undefined
632
+ if (val !== undefined && typeof val !== 'boolean') {
633
+ throw new Error('confirmed 参数必须是 boolean 类型')
634
+ }
635
+ return null
636
+ },
637
+ }), (ctx) => {
638
+ try {
639
+ const { localStorage } = ctx.req
640
+ const { groupManager } = ctx
641
+ const { data, confirmed } = ctx.request.body
642
+
643
+ const currentGroupId = groupManager.getCurrentGroupId()
644
+ const currentGroup = groupManager.getCurrentGroup()
645
+
646
+ // 统计结果
647
+ const result = {
648
+ total: data.files.length,
649
+ newFiles: 0,
650
+ existingFiles: 0,
651
+ existingDetails: [],
652
+ imported: 0,
653
+ merged: 0,
654
+ errors: [],
655
+ }
656
+
657
+ // 1. 检测同名文件(检查当前组是否已配置该文件)
658
+ data.files.forEach((item) => {
659
+ const groupConfig = groupManager.getGroupFileConfig(currentGroupId, item.name)
660
+ if (groupConfig) {
661
+ // 当前组已有该文件的配置
662
+ result.existingFiles++
663
+ result.existingDetails.push({
664
+ name: item.name,
665
+ url: item.session?.req?.url || '',
666
+ })
667
+ } else {
668
+ result.newFiles++
669
+ }
670
+ })
671
+
672
+ // 2. 如果有同名文件且未确认,返回统计信息
673
+ if (result.existingFiles > 0 && !confirmed) {
674
+ ctx.body = {
675
+ code: 200,
676
+ needConfirm: true,
677
+ msg: '检测到同名文件,请确认导入',
678
+ data: result,
679
+ }
680
+ return
681
+ }
682
+
683
+ // 3. 执行智能导入
684
+ // 生成版本名称:优先使用导入组名称
685
+ const versionName = data.group?.name || `导入_${new Date().toLocaleString('zh-CN').replace(/[/:]/g, '-').replace(/\s/g, '_')}`
686
+
687
+ data.files.forEach((item) => {
688
+ try {
689
+ const { name, session, groupConfig, versions } = item
690
+ const existingGroupConfig = groupManager.getGroupFileConfig(currentGroupId, name)
691
+
692
+ if (!existingGroupConfig) {
693
+ // 3.1 新文件(当前组没有配置):直接导入
694
+
695
+ // 如果物理文件不存在,创建物理文件
696
+ const existingFile = localStorage.readFile(name)
697
+ if (!existingFile && session) {
698
+ localStorage.writeFile(name, JSON.stringify(session))
699
+ }
700
+
701
+ // 写入到当前组的配置
702
+ if (groupConfig) {
703
+ groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
704
+ }
705
+
706
+ // 导入版本数据(跳过 source 版本)
707
+ if (versions && Array.isArray(versions)) {
708
+ versions.forEach((version) => {
709
+ if (version.name && version.content && version.name !== 'source') {
710
+ groupManager.addGroupVersion(
711
+ currentGroupId,
712
+ name,
713
+ version.name,
714
+ version.content,
715
+ version.description || '',
716
+ )
717
+ }
718
+ })
719
+ }
720
+
721
+ result.imported++
722
+ } else {
723
+ // 3.2 同名文件:智能合并
724
+ // - 保留原始数据(Layer 1 不变)
725
+ // - 导入所有版本数据(跳过 source)
726
+ // - 如果有组名,激活组名版本;否则保持当前版本
727
+
728
+ // 导入版本数据(跳过 source 版本)
729
+ let hasImportedVersions = false
730
+ if (versions && Array.isArray(versions)) {
731
+ versions.forEach((version) => {
732
+ if (version.name && version.content && version.name !== 'source') {
733
+ groupManager.addGroupVersion(
734
+ currentGroupId,
735
+ name,
736
+ version.name,
737
+ version.content,
738
+ version.description || '从导入文件创建',
739
+ )
740
+ hasImportedVersions = true
741
+ }
742
+ })
743
+ }
744
+
745
+ // 更新组配置
746
+ if (hasImportedVersions) {
747
+ // 如果导入了版本数据,激活导入组名称的版本(如果存在)
748
+ const targetVersion = versionName // 使用导入组名称
749
+ groupManager.setGroupFileConfig(currentGroupId, name, {
750
+ ...existingGroupConfig,
751
+ mockVersion: targetVersion,
752
+ mock: groupConfig?.mock ?? existingGroupConfig.mock,
753
+ locked: groupConfig?.locked ?? existingGroupConfig.locked,
754
+ })
755
+ } else {
756
+ // 如果没有版本数据,保持原有配置不变
757
+ // 不做任何更新
758
+ }
759
+
760
+ result.merged++
761
+ }
762
+ } catch (error) {
763
+ result.errors.push({
764
+ name: item.name,
765
+ error: error.message,
766
+ })
767
+ if (process.env.NODE_ENV === 'development') {
768
+ console.error(`[mockbubu] 导入文件 ${item.name} 失败:`, error.message)
769
+ }
770
+ }
771
+ })
772
+
773
+ // 4. 返回结果
774
+ const messages = []
775
+ messages.push(`成功导入到组【${currentGroup.name}】`)
776
+ if (result.imported > 0) {
777
+ messages.push(`新文件 ${result.imported} 个`)
778
+ }
779
+ if (result.merged > 0) {
780
+ messages.push(`合并 ${result.merged} 个`)
781
+ }
782
+ if (result.errors.length > 0) {
783
+ messages.push(`失败 ${result.errors.length} 个`)
784
+ }
785
+
786
+ ctx.body = {
787
+ code: 200,
788
+ needConfirm: false,
789
+ msg: messages.join(','),
790
+ data: result,
791
+ }
792
+ } catch (error) {
793
+ ctx.body = createErrorResponse(error.message)
794
+ }
795
+ })
796
+
797
+ // 导入为新组
798
+ router.post('/cgi-bin/mockbubu/import-as-new-group', validate({
799
+ data: (val) => {
800
+ if (!val || typeof val !== 'object') {
801
+ throw new Error('data 必须是对象')
802
+ }
803
+ if (val.exportType !== 'single-group') {
804
+ throw new Error('导入数据格式不正确,必须是单组导出的数据')
805
+ }
806
+ return null
807
+ },
808
+ groupName: (val) => {
809
+ if (!val || !val.trim()) {
810
+ throw new Error('组名不能为空')
811
+ }
812
+ return null
813
+ },
814
+ }), (ctx) => {
815
+ try {
816
+ const { localStorage } = ctx.req
817
+ const { groupManager } = ctx
818
+ const { data, groupName, groupDescription = '' } = ctx.request.body
819
+
820
+ // 创建新组
821
+ const newGroup = groupManager.createGroup({
822
+ name: groupName,
823
+ description: groupDescription || data.group.description || '',
824
+ })
825
+
826
+ const errors = []
827
+ let imported = 0
828
+
829
+ // 导入数据到新组
830
+ data.files.forEach((item) => {
831
+ try {
832
+ const { name, session, groupConfig, versions } = item
833
+
834
+ // 写入原始 session 数据(如果不存在)
835
+ if (session && !localStorage.readFile(name)) {
836
+ localStorage.writeFile(name, JSON.stringify(session))
837
+ }
838
+
839
+ // 写入到新组的配置
840
+ if (groupConfig) {
841
+ groupManager.setGroupFileConfig(newGroup.id, name, groupConfig)
842
+ }
843
+
844
+ // 导入版本数据(跳过 source 版本,因为 source 对应 Layer 1 的物理文件)
845
+ if (versions && Array.isArray(versions)) {
846
+ versions.forEach((version) => {
847
+ // 跳过 source 版本,source 应该对应原始物理文件
848
+ if (version.name && version.content && version.name !== 'source') {
849
+ groupManager.addGroupVersion(
850
+ newGroup.id,
851
+ name,
852
+ version.name,
853
+ version.content,
854
+ version.description || '',
855
+ )
856
+ }
857
+ })
858
+ }
859
+
860
+ imported++
861
+ } catch (error) {
862
+ errors.push({ name: item.name, error: error.message })
863
+ if (process.env.NODE_ENV === 'development') {
864
+ console.error(`[mockbubu] 导入文件 ${item.name} 失败:`, error.message)
865
+ }
866
+ }
867
+ })
868
+
869
+ // 返回结果
870
+ const messages = []
871
+ messages.push(`成功创建新组【${newGroup.name}】并导入 ${imported} 个文件`)
872
+ if (errors.length > 0) {
873
+ messages.push(`${errors.length} 个文件导入失败`)
874
+ // 添加首个错误详情,便于快速定位问题
875
+ if (errors[0]) {
876
+ messages.push(`首个错误: ${errors[0].error}`)
877
+ }
878
+ }
879
+
880
+ ctx.body = createSuccessResponse({
881
+ newGroup,
882
+ imported,
883
+ errors,
884
+ }, messages.join(','))
885
+ } catch (error) {
886
+ ctx.body = createErrorResponse(error.message)
887
+ }
888
+ })
889
+
890
+ // 导入所有组(全量恢复)
891
+ router.post('/cgi-bin/mockbubu/import-all-groups', validate({
892
+ data: (val) => {
893
+ if (!val || typeof val !== 'object') {
894
+ throw new Error('data 必须是对象')
895
+ }
896
+ if (val.exportType !== 'all-groups') {
897
+ throw new Error('导入数据格式不正确,必须是所有组导出的数据')
898
+ }
899
+ return null
900
+ },
901
+ mode: (val) => {
902
+ // mode 参数可选,默认为 merge
903
+ if (val && !['merge', 'overwrite'].includes(val)) {
904
+ throw new Error('mode 必须是 merge 或 overwrite')
905
+ }
906
+ return null
907
+ },
908
+ }), (ctx) => {
909
+ try {
910
+ const { localStorage } = ctx.req
911
+ const { groupManager } = ctx
912
+ const { data, mode } = ctx.request.body
913
+ const importMode = mode || 'merge' // 默认为 merge
914
+
915
+ const errors = []
916
+ const result = {
917
+ groups: { imported: 0, skipped: 0 },
918
+ files: { imported: 0, skipped: 0 },
919
+ }
920
+
921
+ // 1. overwrite 模式:删除所有非默认组
922
+ if (importMode === 'overwrite') {
923
+ const existingGroups = groupManager.getGroups()
924
+ existingGroups.forEach(group => {
925
+ if (!group.isDefault && group.id !== groupManager.DEFAULT_GROUP_ID) {
926
+ try {
927
+ groupManager.deleteGroup(group.id)
928
+ } catch (error) {
929
+ errors.push({ group: group.name, error: `删除组失败: ${error.message}` })
930
+ }
931
+ }
932
+ })
933
+ }
934
+
935
+ // 2. 导入组元数据
936
+ if (data.groupsData && data.groupsData.groups) {
937
+ const existingGroupIds = groupManager.getGroups().map(g => g.id)
938
+
939
+ data.groupsData.groups.forEach(group => {
940
+ try {
941
+ // 跳过默认组
942
+ if (group.id === groupManager.DEFAULT_GROUP_ID || group.isDefault) {
943
+ result.groups.skipped++
944
+ return
945
+ }
946
+
947
+ if (existingGroupIds.includes(group.id)) {
948
+ if (importMode === 'merge') {
949
+ // merge 模式:更新组信息
950
+ groupManager.updateGroup(group.id, {
951
+ name: group.name,
952
+ description: group.description,
953
+ })
954
+ result.groups.imported++
955
+ } else {
956
+ // overwrite 模式:组已在上面删除,这里跳过
957
+ result.groups.skipped++
958
+ }
959
+ } else {
960
+ // 新组:创建
961
+ const newGroup = {
962
+ id: group.id,
963
+ name: group.name,
964
+ description: group.description,
965
+ createTime: group.createTime,
966
+ updateTime: Date.now(),
967
+ isDefault: false,
968
+ }
969
+ const groupsData = groupManager.getGroupsData()
970
+ groupsData.groups.push(newGroup)
971
+ groupManager.setGroupsData(groupsData)
972
+ result.groups.imported++
973
+ }
974
+ } catch (error) {
975
+ errors.push({ group: group.name, error: error.message })
976
+ }
977
+ })
978
+ }
979
+
980
+ // 3. 导入文件和组配置
981
+ if (data.files) {
982
+ data.files.forEach(item => {
983
+ try {
984
+ const { name, session, groupConfigs } = item
985
+ const existingFile = localStorage.readFile(name)
986
+
987
+ // 处理文件数据
988
+ if (!existingFile) {
989
+ // 新文件:写入
990
+ if (session) {
991
+ localStorage.writeFile(name, JSON.stringify(session))
992
+ }
993
+ result.files.imported++
994
+ } else {
995
+ // 已存在文件
996
+ if (importMode === 'overwrite') {
997
+ // overwrite 模式:覆盖原始数据
998
+ if (session) {
999
+ localStorage.writeFile(name, JSON.stringify(session))
1000
+ }
1001
+ result.files.imported++
1002
+ } else {
1003
+ // merge 模式:保留原始数据,跳过
1004
+ result.files.skipped++
1005
+ }
1006
+ }
1007
+
1008
+ // 写入各组的配置
1009
+ if (groupConfigs) {
1010
+ Object.keys(groupConfigs).forEach(groupId => {
1011
+ const config = groupConfigs[groupId]
1012
+ if (importMode === 'merge') {
1013
+ const existingConfig = groupManager.getGroupFileConfig(groupId, name)
1014
+ // merge 模式:只导入当前没有配置的组
1015
+ if (!existingConfig.mock && !existingConfig.locked && !existingConfig.mockVersion) {
1016
+ groupManager.setGroupFileConfig(groupId, name, config)
1017
+ }
1018
+ } else {
1019
+ // overwrite 模式:覆盖所有配置
1020
+ groupManager.setGroupFileConfig(groupId, name, config)
1021
+ }
1022
+ })
1023
+ }
1024
+ } catch (error) {
1025
+ errors.push({ name: item.name, error: error.message })
1026
+ if (process.env.NODE_ENV === 'development') {
1027
+ console.error(`[mockbubu] 导入文件 ${item.name} 失败:`, error.message)
1028
+ }
1029
+ }
1030
+ })
1031
+ }
1032
+
1033
+ // 返回结果
1034
+ const messages = []
1035
+ messages.push('成功导入')
1036
+ if (result.groups.imported > 0) {
1037
+ messages.push(`${result.groups.imported} 个组`)
1038
+ }
1039
+ if (result.files.imported > 0) {
1040
+ messages.push(`${result.files.imported} 个文件`)
1041
+ }
1042
+ if (errors.length > 0) {
1043
+ messages.push(`${errors.length} 个错误`)
1044
+ }
1045
+
1046
+ ctx.body = createSuccessResponse({
1047
+ ...result,
1048
+ errors,
1049
+ }, messages.join(','))
1050
+ } catch (error) {
1051
+ ctx.body = createErrorResponse(error.message)
1052
+ }
1053
+ })
1054
+
1055
+ // 导入 mock 数据(旧版本格式,保持兼容)
1056
+ router.post('/cgi-bin/mockbubu/import-data', validate({
1057
+ data: (val) => {
1058
+ if (!Array.isArray(val)) {
1059
+ throw new Error('data 必须是数组')
1060
+ }
1061
+ if (val.length === 0) {
1062
+ throw new Error('data 不能为空')
1063
+ }
1064
+ return null
1065
+ },
1066
+ mode: (val) => {
1067
+ if (!['merge', 'overwrite'].includes(val)) {
1068
+ throw new Error('mode 必须是 merge 或 overwrite')
1069
+ }
1070
+ return null
1071
+ },
1072
+ }), (ctx) => {
1073
+ try {
1074
+ const { localStorage } = ctx.req
1075
+ const { groupManager } = ctx
1076
+ const { data, mode } = ctx.request.body
1077
+
1078
+ const currentGroupId = groupManager.getCurrentGroupId()
1079
+ const errors = []
1080
+ const skipped = []
1081
+ let imported = 0
1082
+
1083
+ data.forEach((item) => {
1084
+ try {
1085
+ const { name, session, properties } = item
1086
+
1087
+ // merge 模式下,如果文件已存在则跳过
1088
+ if (mode === 'merge' && localStorage.readFile(name)) {
1089
+ skipped.push(name)
1090
+ return
1091
+ }
1092
+
1093
+ // 直接写入完整的 session 数据
1094
+ if (session) {
1095
+ localStorage.writeFile(name, JSON.stringify(session))
1096
+ }
1097
+
1098
+ // 将旧格式的 properties 迁移到当前组
1099
+ if (properties) {
1100
+ groupManager.setGroupFileConfig(currentGroupId, name, properties)
1101
+ }
1102
+
1103
+ imported++
1104
+ } catch (error) {
1105
+ errors.push({ name: item.name, error: error.message })
1106
+ if (process.env.NODE_ENV === 'development') {
1107
+ console.error(`[mockbubu] 导入文件 ${item.name} 失败:`, error.message)
1108
+ }
1109
+ }
1110
+ })
1111
+
1112
+ // 返回结果
1113
+ const messages = []
1114
+ if (imported > 0) {
1115
+ messages.push(`成功导入 ${imported} 个文件到当前组`)
1116
+ }
1117
+ if (skipped.length > 0) {
1118
+ messages.push(`跳过 ${skipped.length} 个已存在的文件`)
1119
+ }
1120
+ if (errors.length > 0) {
1121
+ messages.push(`${errors.length} 个文件导入失败`)
1122
+ }
1123
+
1124
+ ctx.body = createSuccessResponse({
1125
+ imported,
1126
+ skipped: skipped.length,
1127
+ errors,
1128
+ }, messages.join(','))
1129
+ } catch (error) {
1130
+ ctx.body = createErrorResponse(error.message)
1131
+ }
1132
+ })
1133
+
1134
+ // ==================== 存储管理功能 ====================
1135
+
1136
+ // 获取存储统计信息
1137
+ router.get('/cgi-bin/mockbubu/storage-stats', (ctx) => {
1138
+ try {
1139
+ const { localStorage } = ctx.req
1140
+ const { groupManager } = ctx
1141
+
1142
+ // 获取所有物理文件
1143
+ const allFiles = localStorage.getFileList()
1144
+ const groups = groupManager.getGroups()
1145
+
1146
+ // 初始化统计数据
1147
+ let totalSize = 0
1148
+ let originalDataSize = 0
1149
+ let versionDataSize = 0
1150
+
1151
+ const stats = {
1152
+ mockEnabled: 0,
1153
+ mockDisabled: 0,
1154
+ locked: 0,
1155
+ orphanFiles: 0,
1156
+ unusedFiles: 0,
1157
+ }
1158
+
1159
+ const groupsFileCount = {}
1160
+ groups.forEach(group => {
1161
+ groupsFileCount[group.id] = 0
1162
+ })
1163
+
1164
+ // 遍历所有文件统计
1165
+ allFiles.forEach(item => {
1166
+ const filename = item.name
1167
+ let hasConfig = false
1168
+
1169
+ // 读取文件大小
1170
+ const file = localStorage.readFile(filename)
1171
+ if (file) {
1172
+ const fileSize = Buffer.byteLength(file, 'utf8')
1173
+ totalSize += fileSize
1174
+ originalDataSize += fileSize
1175
+ }
1176
+
1177
+ // 检查各组配置
1178
+ groups.forEach(group => {
1179
+ // 使用 hasGroupConfig 直接检查配置是否存在
1180
+ const hasActualConfig = groupManager.hasGroupConfig(group.id, filename)
1181
+
1182
+ if (hasActualConfig) {
1183
+ hasConfig = true
1184
+ groupsFileCount[group.id]++
1185
+
1186
+ // 获取配置详情用于统计
1187
+ const config = groupManager.getGroupFileConfig(group.id, filename)
1188
+
1189
+ // 统计 mock 状态
1190
+ if (config.mock) {
1191
+ stats.mockEnabled++
1192
+ } else {
1193
+ stats.mockDisabled++
1194
+ }
1195
+
1196
+ // 统计锁定状态
1197
+ if (config.locked) {
1198
+ stats.locked++
1199
+ }
1200
+
1201
+ // 统计版本数据大小
1202
+ const versions = groupManager.getGroupVersions(group.id, filename)
1203
+ versions.forEach(version => {
1204
+ if (version.content) {
1205
+ const versionSize = Buffer.byteLength(JSON.stringify(version.content), 'utf8')
1206
+ versionDataSize += versionSize
1207
+ }
1208
+ })
1209
+ }
1210
+ })
1211
+
1212
+ // 统计孤儿文件
1213
+ if (!hasConfig) {
1214
+ stats.orphanFiles++
1215
+ }
1216
+
1217
+ // 统计未使用文件
1218
+ if (hasConfig) {
1219
+ let allMockDisabled = true
1220
+ let allLocked = false
1221
+ let hasCustomVersions = false
1222
+
1223
+ groups.forEach(group => {
1224
+ if (groupManager.hasGroupConfig(group.id, filename)) {
1225
+ const config = groupManager.getGroupFileConfig(group.id, filename)
1226
+
1227
+ if (config.mock) {
1228
+ allMockDisabled = false
1229
+ }
1230
+
1231
+ if (config.locked) {
1232
+ allLocked = true
1233
+ }
1234
+
1235
+ const versions = groupManager.getGroupVersions(group.id, filename)
1236
+ const customVersions = versions.filter(v => v.filename !== 'source')
1237
+ if (customVersions.length > 0) {
1238
+ hasCustomVersions = true
1239
+ }
1240
+ }
1241
+ })
1242
+
1243
+ // 未使用文件:所有组未开启mock + 所有组未锁定 + 所有组无自定义版本
1244
+ if (allMockDisabled && !allLocked && !hasCustomVersions) {
1245
+ stats.unusedFiles++
1246
+ }
1247
+ }
1248
+ })
1249
+
1250
+ // 构造响应数据
1251
+ const groupsData = groups.map(group => ({
1252
+ id: group.id,
1253
+ name: group.name,
1254
+ fileCount: groupsFileCount[group.id],
1255
+ }))
1256
+
1257
+ ctx.body = createSuccessResponse({
1258
+ totalFiles: allFiles.length,
1259
+ totalSize,
1260
+ originalDataSize,
1261
+ versionDataSize,
1262
+ groups: groupsData,
1263
+ stats,
1264
+ })
1265
+ } catch (error) {
1266
+ ctx.body = createErrorResponse(error.message)
1267
+ }
1268
+ })
1269
+
1270
+ // 预览孤儿文件
1271
+ router.get('/cgi-bin/mockbubu/preview-orphan-files', (ctx) => {
1272
+ try {
1273
+ const { localStorage } = ctx.req
1274
+ const { groupManager } = ctx
1275
+
1276
+ const allFiles = localStorage.getFileList()
1277
+ const orphanFiles = []
1278
+ let totalSize = 0
1279
+
1280
+ allFiles.forEach(item => {
1281
+ const filename = item.name
1282
+ let hasConfig = false
1283
+
1284
+ // 检查是否有任何组配置
1285
+ const groups = groupManager.getGroups()
1286
+ for (const group of groups) {
1287
+ if (groupManager.hasGroupConfig(group.id, filename)) {
1288
+ hasConfig = true
1289
+ break
1290
+ }
1291
+ }
1292
+
1293
+ // 是孤儿文件
1294
+ if (!hasConfig) {
1295
+ const file = localStorage.readFile(filename)
1296
+ const fileSize = file ? Buffer.byteLength(file, 'utf8') : 0
1297
+
1298
+ orphanFiles.push({
1299
+ name: filename,
1300
+ url: getPropertyAttr(localStorage, filename, 'url') || 'N/A',
1301
+ method: getPropertyAttr(localStorage, filename, 'method') || 'N/A',
1302
+ size: fileSize,
1303
+ date: getPropertyAttr(localStorage, filename, 'date') || 'N/A',
1304
+ })
1305
+
1306
+ totalSize += fileSize
1307
+ }
1308
+ })
1309
+
1310
+ ctx.body = createSuccessResponse({
1311
+ files: orphanFiles,
1312
+ totalSize,
1313
+ count: orphanFiles.length,
1314
+ })
1315
+ } catch (error) {
1316
+ ctx.body = createErrorResponse(error.message)
1317
+ }
1318
+ })
1319
+
1320
+ // 清理孤儿文件
1321
+ router.post('/cgi-bin/mockbubu/cleanup-orphan-files', (ctx) => {
1322
+ try {
1323
+ const { localStorage } = ctx.req
1324
+ const { groupManager } = ctx
1325
+
1326
+ const allFiles = localStorage.getFileList()
1327
+ let deleted = 0
1328
+ let freedSpace = 0
1329
+
1330
+ allFiles.forEach(item => {
1331
+ const filename = item.name
1332
+ let hasConfig = false
1333
+
1334
+ // 检查是否有任何组配置
1335
+ const groups = groupManager.getGroups()
1336
+ for (const group of groups) {
1337
+ if (groupManager.hasGroupConfig(group.id, filename)) {
1338
+ hasConfig = true
1339
+ break
1340
+ }
1341
+ }
1342
+
1343
+ // 删除孤儿文件
1344
+ if (!hasConfig) {
1345
+ const file = localStorage.readFile(filename)
1346
+ const fileSize = file ? Buffer.byteLength(file, 'utf8') : 0
1347
+
1348
+ // 删除物理文件
1349
+ removeFile(localStorage, filename)
1350
+
1351
+ deleted++
1352
+ freedSpace += fileSize
1353
+ }
1354
+ })
1355
+
1356
+ const freedSpaceMB = (freedSpace / (1024 * 1024)).toFixed(2)
1357
+ ctx.body = createSuccessResponse({
1358
+ deleted,
1359
+ freedSpace,
1360
+ }, `成功清理 ${deleted} 个孤儿文件,释放 ${freedSpaceMB} MB 存储空间`)
1361
+ } catch (error) {
1362
+ ctx.body = createErrorResponse(error.message)
1363
+ }
1364
+ })
1365
+
1366
+ // 预览未使用文件
1367
+ router.get('/cgi-bin/mockbubu/preview-unused-files', (ctx) => {
1368
+ try {
1369
+ const { localStorage } = ctx.req
1370
+ const { groupManager } = ctx
1371
+
1372
+ const allFiles = localStorage.getFileList()
1373
+ const unusedFiles = []
1374
+ let totalSize = 0
1375
+
1376
+ allFiles.forEach(item => {
1377
+ const filename = item.name
1378
+ const groups = groupManager.getGroups()
1379
+
1380
+ let hasConfig = false
1381
+ let allMockDisabled = true
1382
+ let allLocked = false
1383
+ let allNoCustomVersions = true
1384
+ const groupsStatus = []
1385
+
1386
+ // 检查所有组的配置
1387
+ groups.forEach(group => {
1388
+ if (groupManager.hasGroupConfig(group.id, filename)) {
1389
+ hasConfig = true
1390
+
1391
+ const config = groupManager.getGroupFileConfig(group.id, filename)
1392
+
1393
+ // 检查 mock 状态
1394
+ if (config.mock) {
1395
+ allMockDisabled = false
1396
+ }
1397
+
1398
+ // 检查锁定状态
1399
+ if (config.locked) {
1400
+ allLocked = true
1401
+ }
1402
+
1403
+ // 检查是否有自定义版本
1404
+ const versions = groupManager.getGroupVersions(group.id, filename)
1405
+ const customVersions = versions.filter(v => v.filename !== 'source')
1406
+ const hasCustomVersions = customVersions.length > 0
1407
+
1408
+ if (hasCustomVersions) {
1409
+ allNoCustomVersions = false
1410
+ }
1411
+
1412
+ groupsStatus.push({
1413
+ id: group.id,
1414
+ name: group.name,
1415
+ mock: config.mock,
1416
+ locked: config.locked,
1417
+ hasCustomVersions,
1418
+ })
1419
+ }
1420
+ })
1421
+
1422
+ // 判断是否为未使用文件:所有组未开启mock + 所有组未锁定 + 所有组无自定义版本
1423
+ if (hasConfig && allMockDisabled && !allLocked && allNoCustomVersions) {
1424
+ const file = localStorage.readFile(filename)
1425
+ const fileSize = file ? Buffer.byteLength(file, 'utf8') : 0
1426
+
1427
+ unusedFiles.push({
1428
+ name: filename,
1429
+ url: getPropertyAttr(localStorage, filename, 'url') || 'N/A',
1430
+ method: getPropertyAttr(localStorage, filename, 'method') || 'N/A',
1431
+ size: fileSize,
1432
+ date: getPropertyAttr(localStorage, filename, 'date') || 'N/A',
1433
+ groupsStatus,
1434
+ })
1435
+
1436
+ totalSize += fileSize
1437
+ }
1438
+ })
1439
+
1440
+ ctx.body = createSuccessResponse({
1441
+ files: unusedFiles,
1442
+ totalSize,
1443
+ count: unusedFiles.length,
1444
+ })
1445
+ } catch (error) {
1446
+ ctx.body = createErrorResponse(error.message)
1447
+ }
1448
+ })
1449
+
1450
+ // 清理未使用文件
1451
+ router.post('/cgi-bin/mockbubu/cleanup-unused-files', validate({
1452
+ confirmation: (val) => {
1453
+ if (val !== 'DELETE') {
1454
+ throw new Error('必须输入 DELETE 确认删除操作')
1455
+ }
1456
+ return null
1457
+ },
1458
+ }), (ctx) => {
1459
+ try {
1460
+ const { localStorage } = ctx.req
1461
+ const { groupManager } = ctx
1462
+
1463
+ const allFiles = localStorage.getFileList()
1464
+ const groups = groupManager.getGroups()
1465
+ let deleted = 0
1466
+ let freedSpace = 0
1467
+
1468
+ allFiles.forEach(item => {
1469
+ const filename = item.name
1470
+
1471
+ let hasConfig = false
1472
+ let allMockDisabled = true
1473
+ let allLocked = false
1474
+ let allNoCustomVersions = true
1475
+
1476
+ // 检查所有组的配置
1477
+ groups.forEach(group => {
1478
+ if (groupManager.hasGroupConfig(group.id, filename)) {
1479
+ hasConfig = true
1480
+
1481
+ const config = groupManager.getGroupFileConfig(group.id, filename)
1482
+
1483
+ if (config.mock) {
1484
+ allMockDisabled = false
1485
+ }
1486
+
1487
+ if (config.locked) {
1488
+ allLocked = true
1489
+ }
1490
+
1491
+ const versions = groupManager.getGroupVersions(group.id, filename)
1492
+ const customVersions = versions.filter(v => v.filename !== 'source')
1493
+ if (customVersions.length > 0) {
1494
+ allNoCustomVersions = false
1495
+ }
1496
+ }
1497
+ })
1498
+
1499
+ // 删除未使用文件:所有组未开启mock + 所有组未锁定 + 所有组无自定义版本
1500
+ if (hasConfig && allMockDisabled && !allLocked && allNoCustomVersions) {
1501
+ const file = localStorage.readFile(filename)
1502
+ const fileSize = file ? Buffer.byteLength(file, 'utf8') : 0
1503
+
1504
+ // 删除物理文件(Layer 1)
1505
+ removeFile(localStorage, filename)
1506
+
1507
+ // 删除所有组的配置(Layer 3)
1508
+ groups.forEach(group => {
1509
+ groupManager.removeGroupFileConfig(group.id, filename)
1510
+ })
1511
+
1512
+ deleted++
1513
+ freedSpace += fileSize
1514
+ }
1515
+ })
1516
+
1517
+ const freedSpaceMB = (freedSpace / (1024 * 1024)).toFixed(2)
1518
+ ctx.body = createSuccessResponse({
1519
+ deleted,
1520
+ freedSpace,
1521
+ }, `成功清理 ${deleted} 个未使用文件,释放 ${freedSpaceMB} MB 存储空间`)
167
1522
  } catch (error) {
168
1523
  ctx.body = createErrorResponse(error.message)
169
1524
  }