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.
- package/.gitignore +4 -0
- package/CHANGELOG_GROUP_FEATURE.md +468 -0
- package/CHANGELOG_P0_FIXES.md +412 -0
- package/CHANGELOG_P1_OPTIMIZATIONS.md +292 -0
- package/CLAUDE.md +436 -0
- package/GROUP_FEATURE_DESIGN.md +520 -0
- package/lib/const.js +19 -0
- package/lib/group-manager.js +491 -0
- package/lib/server.js +173 -33
- package/lib/uiServer/index.js +46 -4
- package/lib/uiServer/router/group-router.js +218 -0
- package/lib/uiServer/router/index.js +1393 -38
- package/lib/uiServer/router/version-router.js +131 -53
- package/lib/uiServer/util.js +64 -10
- package/lib/uiServer/validator.js +105 -0
- package/lib/utils.js +149 -27
- package/package.json +1 -1
- package/public/js/app.js +4541 -747
- package/public/js/app.js.map +1 -1
- package/public/js/chunk-vendors.js +12194 -6869
- package/public/js/chunk-vendors.js.map +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const {
|
|
2
2
|
updateFile,
|
|
3
3
|
removeFile,
|
|
4
|
-
writeFile,
|
|
5
4
|
readFile,
|
|
6
|
-
|
|
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', (
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
...
|
|
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', (
|
|
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
|
|
78
|
-
|
|
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
|
-
...
|
|
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', (
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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', (
|
|
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 {
|
|
181
|
+
const { groupManager } = ctx
|
|
113
182
|
const { name, mock } = ctx.request.body
|
|
114
183
|
|
|
115
|
-
|
|
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', (
|
|
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 {
|
|
206
|
+
const { groupManager } = ctx
|
|
126
207
|
const { name, locked } = ctx.request.body
|
|
127
208
|
|
|
128
|
-
|
|
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', (
|
|
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 {
|
|
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
|
-
|
|
278
|
+
// 直接删除物理文件 + 组配置
|
|
279
|
+
removeFile(localStorage, item.name, currentGroupId)
|
|
280
|
+
groupManager.removeGroupFileConfig(currentGroupId, item.name)
|
|
281
|
+
result.success++
|
|
161
282
|
} catch (error) {
|
|
162
|
-
|
|
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
|
-
|
|
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
|
}
|