whistle.mockbubu 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +38 -0
  2. package/index.js +3 -3
  3. package/lib/config/const.js +138 -0
  4. package/lib/config/rule-collector.js +81 -0
  5. package/lib/constants.js +62 -0
  6. package/lib/core/memory-buffer/index.js +207 -0
  7. package/lib/core/memory-buffer/shared-instance.js +15 -0
  8. package/lib/core/plugin-mode-manager.js +74 -0
  9. package/lib/core/resRulesServer.js +14 -0
  10. package/lib/core/rulesServer.js +31 -0
  11. package/lib/core/server-entry/capture-handler.js +191 -0
  12. package/lib/core/server-entry/request-interceptor.js +82 -0
  13. package/lib/core/server-entry/response-handler.js +147 -0
  14. package/lib/core/server-entry/server.js +230 -0
  15. package/lib/storage/group-manager.js +627 -0
  16. package/lib/storage/storage-adapter.js +712 -0
  17. package/lib/storage/storage-v3.js +1418 -0
  18. package/lib/uiServer/index.js +61 -24
  19. package/lib/uiServer/router/export/import-export-router.js +459 -0
  20. package/lib/uiServer/router/files/api-list-router.js +150 -0
  21. package/lib/uiServer/router/files/batch-operations-router.js +185 -0
  22. package/lib/uiServer/router/files/file-config-router.js +118 -0
  23. package/lib/uiServer/router/files/file-crud-router.js +212 -0
  24. package/lib/uiServer/router/files/file-save-router.js +146 -0
  25. package/lib/uiServer/router/files/version-router.js +260 -0
  26. package/lib/uiServer/router/global/plugin-control.js +135 -0
  27. package/lib/uiServer/router/global/system-info-router.js +386 -0
  28. package/lib/uiServer/router/{group-router.js → groups/group-router.js} +21 -20
  29. package/lib/uiServer/router/index.js +38 -1521
  30. package/lib/uiServer/utils/router-helpers.js +100 -0
  31. package/lib/uiServer/utils/util.js +172 -0
  32. package/lib/uiServer/{validator.js → utils/validator.js} +11 -6
  33. package/lib/utils/archive-utils.js +788 -0
  34. package/lib/utils/error-handler.js +173 -0
  35. package/lib/utils/logger.js +79 -0
  36. package/lib/utils/path-utils.js +147 -0
  37. package/lib/utils/performance.js +265 -0
  38. package/lib/utils/utils.js +541 -0
  39. package/package.json +2 -2
  40. package/public/js/app.js +3707 -1922
  41. package/public/js/app.js.map +1 -1
  42. package/public/js/chunk-vendors.js +5098 -3965
  43. package/public/js/chunk-vendors.js.map +1 -1
  44. package/rules.txt +1 -1
  45. package/CHANGELOG_GROUP_FEATURE.md +0 -468
  46. package/CHANGELOG_P0_FIXES.md +0 -412
  47. package/CHANGELOG_P1_OPTIMIZATIONS.md +0 -292
  48. package/CLAUDE.md +0 -436
  49. package/GROUP_FEATURE_DESIGN.md +0 -520
  50. package/lib/const.js +0 -47
  51. package/lib/group-manager.js +0 -491
  52. package/lib/resRulesServer.js +0 -9
  53. package/lib/server.js +0 -249
  54. package/lib/uiServer/router/version-router.js +0 -205
  55. package/lib/uiServer/util.js +0 -153
  56. package/lib/utils.js +0 -409
@@ -6,7 +6,9 @@ const path = require('path')
6
6
  const router = require('koa-router')()
7
7
  const setupRouter = require('./router')
8
8
  const cors = require('koa2-cors')
9
- const GroupManager = require('../group-manager')
9
+ const GroupManager = require('../storage/group-manager')
10
+ const StorageAdapter = require('../storage/storage-adapter')
11
+ const memoryBuffer = require('../core/memory-buffer/shared-instance')
10
12
  const MAX_AGE = 1000 * 60 * 5
11
13
 
12
14
  module.exports = (server, options) => {
@@ -15,37 +17,67 @@ module.exports = (server, options) => {
15
17
 
16
18
  console.log('[mockbubu] 插件开始初始化...')
17
19
 
18
- // 初始化组管理器
20
+ // 使用 StorageAdapter 包装 Whistle storage
21
+ // Whistle storage 对象直接有 baseDir 属性
22
+ const actualBaseDir = options.config.baseDir
23
+ console.log('[mockbubu] 🔍 UI Server storage.baseDir:', storage.baseDir)
24
+ console.log('[mockbubu] 🔍 UI Server 使用 baseDir:', actualBaseDir)
25
+ const storageAdapter = new StorageAdapter({ baseDir: actualBaseDir })
26
+
27
+ // 同步初始化:确保初始化完成后再处理请求
28
+ let isInitialized = false
19
29
  let groupManager
20
- try {
21
- console.log('[mockbubu] 创建 GroupManager...')
22
- groupManager = new GroupManager(storage)
23
- console.log('[mockbubu] GroupManager 创建成功')
24
30
 
25
- // 首次启动时迁移现有数据到默认组(已添加大文件保护)
26
- console.log('[mockbubu] 开始数据迁移...')
27
- groupManager.migrateExistingData()
28
- console.log('[mockbubu] 数据迁移完成')
29
- } catch (error) {
30
- console.error('[mockbubu] 初始化失败:', error)
31
- console.error('[mockbubu] 错误堆栈:', error.stack)
32
- // 如果初始化失败,创建一个简单的 mock 对象
33
- groupManager = {
34
- getCurrentGroupId: () => 'default',
35
- getCurrentGroup: () => ({ id: 'default', name: '默认组' }),
36
- getGroups: () => [{ id: 'default', name: '默认组' }],
37
- }
38
- }
31
+ ;(async () => {
32
+ try {
33
+ // 等待 storage 初始化完成
34
+ await storageAdapter.init()
35
+ console.log('[mockbubu] V3 Storage (UI Server) 已初始化')
36
+
37
+ // 创建组管理器
38
+ console.log('[mockbubu] 创建 GroupManager...')
39
+ groupManager = new GroupManager(storageAdapter)
40
+ console.log('[mockbubu] GroupManager 创建成功')
41
+
42
+ // 确保默认组存在
43
+ console.log('[mockbubu] 确保默认组存在...')
44
+ await groupManager.ensureDefaultGroup()
45
+ console.log('[mockbubu] 默认组已准备就绪')
46
+
39
47
 
40
- // groupManager 挂载到 app.context,让所有中间件都能访问
41
- app.context.groupManager = groupManager
48
+ isInitialized = true
49
+ console.log('[mockbubu] 插件初始化完成')
50
+ } catch (error) {
51
+ console.error('[mockbubu] 初始化失败:', error)
52
+ console.error('[mockbubu] 错误堆栈:', error.stack)
53
+ // 如果初始化失败,创建一个简单的 mock 对象
54
+ groupManager = {
55
+ getCurrentGroupId: async () => 'default',
56
+ getCurrentGroup: async () => ({ id: 'default', name: '默认组' }),
57
+ getGroups: async () => [{ id: 'default', name: '默认组' }],
58
+ }
59
+ isInitialized = true
60
+ }
61
+ })()
42
62
 
43
- console.log('[mockbubu] 插件初始化完成')
63
+ // 中间件:等待初始化完成
64
+ app.use(async (ctx, next) => {
65
+ // eslint-disable-next-line no-unmodified-loop-condition
66
+ while (!isInitialized) {
67
+ await new Promise(resolve => setTimeout(resolve, 100))
68
+ }
69
+ // 将 groupManager、storageAdapter、memoryBuffer 挂载到 ctx,让所有路由都能访问
70
+ ctx.groupManager = groupManager
71
+ ctx.storageAdapter = storageAdapter
72
+ ctx.memoryBuffer = memoryBuffer
73
+ await next()
74
+ })
44
75
 
45
76
  app.proxy = true
46
77
  app.silent = true
47
78
  onerror(app)
48
- setupRouter(router, options)
79
+
80
+ // CORS 中间件必须在最前面
49
81
  app.use(
50
82
  cors({
51
83
  origin: (ctx) => {
@@ -65,11 +97,16 @@ module.exports = (server, options) => {
65
97
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
66
98
  }),
67
99
  )
100
+
101
+ // bodyParser 必须在路由注册之前
68
102
  app.use(bodyParser({
69
103
  jsonLimit: '100mb',
70
104
  formLimit: '100mb',
71
105
  textLimit: '100mb',
72
106
  }))
107
+
108
+ // 注册路由
109
+ setupRouter(router, { ...options, storage: storageAdapter })
73
110
  app.use(router.routes())
74
111
  app.use(router.allowedMethods())
75
112
  app.use(serve(path.join(__dirname, '../../public'), { maxage: MAX_AGE }))
@@ -0,0 +1,459 @@
1
+ /**
2
+ * 文件名: import-export-router.js
3
+ * 功能: 导入导出路由(组数据打包/解包)
4
+ * 依赖: archive-utils.js, util.js, fs, path
5
+ *
6
+ * 职责:
7
+ * - 导出组数据为 tar.gz 压缩包
8
+ * - 从 tar.gz 压缩包导入组数据
9
+ * - 支持选择性导出(所有文件/含版本/不含版本)
10
+ * - 自动处理组名冲突
11
+ *
12
+ * 路由:
13
+ * - POST /cgi-bin/mockbubu/export-group-archive - 导出压缩包
14
+ * - POST /cgi-bin/mockbubu/import-group-archive - 导入压缩包
15
+ */
16
+
17
+ const {
18
+ createTarGz,
19
+ extractTarGz,
20
+ parseMultipartFile,
21
+ sanitizeFilename,
22
+ } = require('../../../utils/archive-utils')
23
+ const {
24
+ createErrorResponse,
25
+ createSuccessResponse,
26
+ } = require('../../utils/util')
27
+ const { createLogger } = require('../../../utils/logger')
28
+ const { wrapRouteHandler } = require('../../utils/router-helpers')
29
+ const fs = require('fs').promises
30
+ const path = require('path')
31
+
32
+ const logger = createLogger()
33
+
34
+ module.exports = (router) => {
35
+ /**
36
+ * 导出多个组数据为压缩包
37
+ *
38
+ * 功能:
39
+ * - 支持同时导出多个组
40
+ * - 包含完整的组目录结构
41
+ * - 智能冲突处理
42
+ *
43
+ * 请求体:
44
+ * ```javascript
45
+ * {
46
+ * groupIds: ['default', 'dev-env', 'test-env']
47
+ * }
48
+ * ```
49
+ *
50
+ * 响应:
51
+ * - Content-Type: application/gzip
52
+ * - Content-Disposition: attachment; filename="导出-时间戳.tar.gz"
53
+ * - Body: tar.gz 压缩包二进制数据
54
+ *
55
+ * 压缩包结构:
56
+ * ```
57
+ * export/
58
+ * ├── _meta.json # 导出组的元数据
59
+ * └── groups/
60
+ * ├── {groupId1}/
61
+ * │ ├── index.json
62
+ * │ └── files/
63
+ * │ └── {fileId}/...
64
+ * └── {groupId2}/...
65
+ * ```
66
+ */
67
+ router.post('/cgi-bin/mockbubu/export-groups-archive', async (ctx) => {
68
+ const timestamp = new Date().toLocaleString('zh-CN', { hour12: false })
69
+ console.log(`[mockbubu ${timestamp}] 开始导出多组压缩包`)
70
+
71
+ try {
72
+ const { groupManager, storageAdapter } = ctx
73
+ const v3Storage = storageAdapter.v3Storage
74
+
75
+ // 获取要导出的组ID列表
76
+ const groupIds = ctx.request.body?.groupIds || []
77
+
78
+ if (!Array.isArray(groupIds) || groupIds.length === 0) {
79
+ ctx.body = createErrorResponse('请至少选择一个组进行导出')
80
+ return
81
+ }
82
+
83
+ console.log(`[mockbubu ${timestamp}] 要导出的组: ${groupIds.join(', ')}`)
84
+
85
+ // 1. 验证所有组是否存在,并读取组信息
86
+ const allGroups = await groupManager.getGroups()
87
+ const selectedGroups = []
88
+
89
+ for (const groupId of groupIds) {
90
+ try {
91
+ const group = allGroups.find(g => g.id === groupId)
92
+
93
+ if (!group) {
94
+ ctx.body = createErrorResponse(`组不存在: ${groupId}`)
95
+ return
96
+ }
97
+
98
+ selectedGroups.push(group)
99
+ } catch (err) {
100
+ ctx.body = createErrorResponse(`读取组失败: ${groupId} - ${err.message}`)
101
+ return
102
+ }
103
+ }
104
+
105
+ console.log(`[mockbubu ${timestamp}] 已验证 ${selectedGroups.length} 个组`)
106
+
107
+ // 2. 创建临时目录
108
+ const pluginTmpBase = path.join(v3Storage.baseDir, '.tmp', 'export-')
109
+ await fs.mkdir(path.dirname(pluginTmpBase), { recursive: true })
110
+ const tmpDir = await fs.mkdtemp(pluginTmpBase)
111
+ const exportDir = path.join(tmpDir, 'export')
112
+ const groupsDir = path.join(exportDir, 'groups')
113
+ await fs.mkdir(groupsDir, { recursive: true })
114
+
115
+ console.log(`[mockbubu ${timestamp}] 临时目录: ${tmpDir}`)
116
+
117
+ try {
118
+ // 3. 创建完整的 _meta.json(与系统 _meta.json 格式一致)
119
+ // 读取当前系统的 _meta.json 以获取 config 配置
120
+ const systemMeta = await v3Storage.getGroupsMeta()
121
+
122
+ const exportMeta = {
123
+ version: '3.0',
124
+ groups: selectedGroups.map(g => ({
125
+ id: g.id,
126
+ name: g.name,
127
+ description: g.description || '',
128
+ isDefault: g.isDefault || false,
129
+ createTime: g.createTime,
130
+ updateTime: g.updateTime,
131
+ })),
132
+ currentGroupId: groupIds[0], // 使用第一个导出的组作为当前组
133
+ updateTime: Date.now(),
134
+ config: systemMeta.config || { mode: 'default' }, // 包含全局配置
135
+ }
136
+
137
+ await fs.writeFile(
138
+ path.join(exportDir, '_meta.json'),
139
+ JSON.stringify(exportMeta, null, 2),
140
+ 'utf8',
141
+ )
142
+
143
+ console.log(`[mockbubu ${timestamp}] 元数据已写入`)
144
+
145
+ // 4. 复制所有选中组的目录
146
+ for (const groupId of groupIds) {
147
+ const sourceGroupDir = path.join(v3Storage.groupsDir, groupId)
148
+ const targetGroupDir = path.join(groupsDir, groupId)
149
+
150
+ // 递归复制整个组目录
151
+ await v3Storage._copyDirectory(sourceGroupDir, targetGroupDir)
152
+ console.log(`[mockbubu ${timestamp}] 已复制组: ${groupId}`)
153
+ }
154
+
155
+ // 5. 创建压缩包
156
+ const archiveName = sanitizeFilename(`whistle-mockbubu-${Date.now()}.tar.gz`)
157
+ const archivePath = path.join(tmpDir, archiveName)
158
+
159
+ await createTarGz(exportDir, archivePath)
160
+ console.log(`[mockbubu ${timestamp}] 压缩包已创建: ${archivePath}`)
161
+
162
+ // 6. 读取压缩包内容
163
+ const archiveBuffer = await fs.readFile(archivePath)
164
+ console.log(`[mockbubu ${timestamp}] 压缩包大小: ${(archiveBuffer.length / 1024).toFixed(2)} KB`)
165
+
166
+ // 7. 返回压缩包
167
+ ctx.set('Content-Type', 'application/gzip')
168
+ ctx.set('Content-Disposition', `attachment; filename="${encodeURIComponent(archiveName)}"`)
169
+ ctx.body = archiveBuffer
170
+
171
+ // 8. 清理临时目录(异步执行)
172
+ const cleanupTmpDir = async (retries = 3) => {
173
+ for (let i = 0; i < retries; i++) {
174
+ try {
175
+ await fs.rm(tmpDir, { recursive: true, force: true })
176
+ console.log(`[mockbubu ${timestamp}] ✓ 临时目录已清理: ${tmpDir}`)
177
+ return
178
+ } catch (err) {
179
+ if (i === retries - 1) {
180
+ console.error(`[mockbubu ${timestamp}] ✗ 清理临时目录失败(已重试${retries}次): ${err.message}`)
181
+ } else {
182
+ await new Promise(resolve => setTimeout(resolve, 100))
183
+ }
184
+ }
185
+ }
186
+ }
187
+ cleanupTmpDir().catch(() => {})
188
+
189
+ console.log(`[mockbubu ${timestamp}] 导出完成`)
190
+ } catch (err) {
191
+ // 出错时清理临时目录
192
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
193
+ throw err
194
+ }
195
+ } catch (error) {
196
+ const timestampError = new Date().toLocaleString('zh-CN', { hour12: false })
197
+ console.error(`[mockbubu ${timestampError}] 导出失败:`, error)
198
+ ctx.set('Content-Type', 'application/json')
199
+ ctx.body = createErrorResponse(`导出失败: ${error.message}`)
200
+ }
201
+ })
202
+
203
+ /**
204
+ * 从压缩包导入多组数据
205
+ *
206
+ * 功能:
207
+ * - 支持同时导入多个组
208
+ * - 智能冲突检测和处理
209
+ * - 默认组自动转为普通组
210
+ * - 如果本地没有 _meta.json,直接使用压缩包内的
211
+ * - 如果本地有 _meta.json,进行冲突检测后合并
212
+ *
213
+ * 请求:
214
+ * - Content-Type: multipart/form-data
215
+ * - Body: 包含 tar.gz 文件的 multipart 数据
216
+ *
217
+ * 响应:
218
+ * ```javascript
219
+ * {
220
+ * code: 200,
221
+ * msg: '成功导入 3 个分组',
222
+ * data: {
223
+ * importedGroups: [...],
224
+ * conflictResolutions: [...]
225
+ * }
226
+ * }
227
+ * ```
228
+ */
229
+ router.post('/cgi-bin/mockbubu/import-groups-archive', async (ctx) => {
230
+ const timestampStart = new Date().toLocaleString('zh-CN', { hour12: false })
231
+ console.log(`[mockbubu ${timestampStart}] 开始导入多组压缩包`)
232
+
233
+ try {
234
+ const { storageAdapter, groupManager } = ctx
235
+ const v3Storage = storageAdapter.v3Storage
236
+
237
+ // 1. 解析上传的文件
238
+ const file = await parseMultipartFile(ctx)
239
+ if (!file || !file.buffer) {
240
+ ctx.body = createErrorResponse('未检测到上传文件')
241
+ return
242
+ }
243
+
244
+ const timestamp = new Date().toLocaleString('zh-CN', { hour12: false })
245
+ console.log(`[mockbubu ${timestamp}] 上传文件: ${file.filename}, 大小: ${(file.buffer.length / 1024).toFixed(2)} KB`)
246
+
247
+ // 2. 创建临时目录
248
+ const pluginTmpBase = path.join(v3Storage.baseDir, '.tmp', 'import-')
249
+ await fs.mkdir(path.dirname(pluginTmpBase), { recursive: true })
250
+ const tmpDir = await fs.mkdtemp(pluginTmpBase)
251
+ const extractDir = path.join(tmpDir, 'extract')
252
+ await fs.mkdir(extractDir, { recursive: true })
253
+
254
+ console.log(`[mockbubu ${timestamp}] 临时目录: ${tmpDir}`)
255
+
256
+ try {
257
+ // 3. 保存并解压
258
+ const uploadPath = path.join(tmpDir, 'upload.tar.gz')
259
+ await fs.writeFile(uploadPath, file.buffer)
260
+ await extractTarGz(uploadPath, extractDir)
261
+ console.log(`[mockbubu ${timestamp}] 解压完成`)
262
+
263
+ // 4. 读取压缩包中的 _meta.json
264
+ const exportMetaPath = path.join(extractDir, '_meta.json')
265
+ const exportMetaContent = await fs.readFile(exportMetaPath, 'utf8')
266
+ const exportMeta = JSON.parse(exportMetaContent)
267
+
268
+ console.log(`[mockbubu ${timestamp}] 压缩包元数据: ${exportMeta.groups.length} 个组`)
269
+
270
+ // 5. 检查本地是否存在 _meta.json
271
+ let localMeta
272
+ try {
273
+ localMeta = await v3Storage.getGroupsMeta()
274
+ console.log(`[mockbubu ${timestamp}] 本地已有 ${localMeta.groups.length} 个组,执行合并逻辑`)
275
+ } catch (err) {
276
+ console.log(`[mockbubu ${timestamp}] 本地无 _meta.json,直接使用压缩包数据`)
277
+ localMeta = null
278
+ }
279
+
280
+ const importedGroups = []
281
+ const conflictResolutions = []
282
+
283
+ if (!localMeta) {
284
+ // 5.1 本地没有 _meta.json,先创建默认组,再导入压缩包中的组
285
+ console.log(`[mockbubu ${timestamp}] 本地无组,先创建默认组`)
286
+
287
+ // 先创建系统默认组
288
+ await groupManager.ensureDefaultGroup()
289
+ localMeta = await v3Storage.getGroupsMeta()
290
+ console.log(`[mockbubu ${timestamp}] 默认组已创建: ${localMeta.groups[0].id}`)
291
+
292
+ // 现在有了默认组,继续执行合并逻辑(将导入的组都当作普通组处理)
293
+ }
294
+
295
+ // 5.2 执行冲突检测和合并(统一处理逻辑)
296
+ {
297
+ console.log(`[mockbubu ${timestamp}] 执行冲突检测`)
298
+
299
+ const sourceGroupsDir = path.join(extractDir, 'groups')
300
+ const existingNames = localMeta.groups.map(g => g.name)
301
+ const existingIds = localMeta.groups.map(g => g.id)
302
+
303
+ for (const importGroup of exportMeta.groups) {
304
+ let resolvedGroup = { ...importGroup }
305
+ let conflictType = null
306
+
307
+ // 导入的默认组强制转为普通组
308
+ if (resolvedGroup.isDefault) {
309
+ const originalName = resolvedGroup.name
310
+ const originalId = resolvedGroup.id
311
+ resolvedGroup.isDefault = false
312
+ resolvedGroup.id = groupManager.generateGroupId()
313
+ conflictType = 'default-group-converted'
314
+
315
+ // 检查名称冲突
316
+ let suffix = 1
317
+ let newName = `${resolvedGroup.name} (${suffix})`
318
+ while (existingNames.includes(newName)) {
319
+ suffix++
320
+ newName = `${resolvedGroup.name} (${suffix})`
321
+ }
322
+ resolvedGroup.name = newName
323
+
324
+ conflictResolutions.push({
325
+ type: conflictType,
326
+ groupId: importGroup.id,
327
+ oldId: originalId,
328
+ newId: resolvedGroup.id,
329
+ oldName: originalName,
330
+ newName: resolvedGroup.name,
331
+ message: `默认组【${originalName}】已转为普通组【${resolvedGroup.name}】`,
332
+ })
333
+ console.log(`[mockbubu ${timestamp}] ⚠ 默认组转换: ${originalName} → ${resolvedGroup.name} (${originalId} → ${resolvedGroup.id})`)
334
+ }
335
+
336
+ // 检查组名冲突(非默认组)
337
+ if (!importGroup.isDefault && existingNames.includes(resolvedGroup.name)) {
338
+ const originalName = resolvedGroup.name
339
+ let suffix = 1
340
+ let newName = `${resolvedGroup.name} (${suffix})`
341
+ while (existingNames.includes(newName)) {
342
+ suffix++
343
+ newName = `${resolvedGroup.name} (${suffix})`
344
+ }
345
+ resolvedGroup.name = newName
346
+ conflictType = 'name-conflict'
347
+ conflictResolutions.push({
348
+ type: conflictType,
349
+ groupId: importGroup.id,
350
+ oldName: originalName,
351
+ newName: resolvedGroup.name,
352
+ message: `组名冲突: ${originalName} → ${resolvedGroup.name}`,
353
+ })
354
+ console.log(`[mockbubu ${timestamp}] ⚠ 组名冲突: ${originalName} → ${resolvedGroup.name}`)
355
+ }
356
+
357
+ // 检查组ID冲突(非默认组,因为默认组已经生成新ID)
358
+ if (!importGroup.isDefault && existingIds.includes(resolvedGroup.id)) {
359
+ const originalId = resolvedGroup.id
360
+ resolvedGroup.id = groupManager.generateGroupId()
361
+ if (!conflictType) {
362
+ conflictResolutions.push({
363
+ type: 'id-conflict',
364
+ oldId: originalId,
365
+ newId: resolvedGroup.id,
366
+ message: `组ID冲突: ${originalId} → ${resolvedGroup.id}`,
367
+ })
368
+ }
369
+ console.log(`[mockbubu ${timestamp}] ⚠ 组ID冲突: ${originalId} → ${resolvedGroup.id}`)
370
+ }
371
+
372
+ // 复制组目录(使用解决后的ID)
373
+ const sourceGroupDir = path.join(sourceGroupsDir, importGroup.id)
374
+ const targetGroupDir = path.join(v3Storage.groupsDir, resolvedGroup.id)
375
+ await v3Storage._copyDirectory(sourceGroupDir, targetGroupDir)
376
+ console.log(`[mockbubu ${timestamp}] ✓ 复制组: ${resolvedGroup.name} (${resolvedGroup.id})`)
377
+
378
+ // 更新 group.json 文件中的组ID和 isDefault(如果发生变化)
379
+ if (resolvedGroup.id !== importGroup.id || resolvedGroup.name !== importGroup.name || resolvedGroup.isDefault !== importGroup.isDefault) {
380
+ const groupJsonPath = path.join(targetGroupDir, 'group.json')
381
+ try {
382
+ const groupJsonContent = await fs.readFile(groupJsonPath, 'utf8')
383
+ const groupJson = JSON.parse(groupJsonContent)
384
+ groupJson.id = resolvedGroup.id
385
+ groupJson.name = resolvedGroup.name
386
+ groupJson.isDefault = resolvedGroup.isDefault
387
+ groupJson.updateTime = Date.now()
388
+ await fs.writeFile(groupJsonPath, JSON.stringify(groupJson, null, 2), 'utf8')
389
+ console.log(`[mockbubu ${timestamp}] ✓ 更新 group.json: ${resolvedGroup.id}`)
390
+ } catch (err) {
391
+ console.error(`[mockbubu ${timestamp}] ✗ 更新 group.json 失败: ${err.message}`)
392
+ }
393
+ }
394
+
395
+ // 追加到本地元数据
396
+ localMeta.groups.push({
397
+ id: resolvedGroup.id,
398
+ name: resolvedGroup.name,
399
+ description: resolvedGroup.description || '',
400
+ isDefault: resolvedGroup.isDefault || false,
401
+ createTime: resolvedGroup.createTime || Date.now(),
402
+ updateTime: Date.now(),
403
+ })
404
+
405
+ existingNames.push(resolvedGroup.name)
406
+ existingIds.push(resolvedGroup.id)
407
+
408
+ importedGroups.push({
409
+ originalId: importGroup.id,
410
+ originalName: importGroup.name,
411
+ newId: resolvedGroup.id,
412
+ newName: resolvedGroup.name,
413
+ })
414
+ }
415
+
416
+ // 写回合并后的 _meta.json
417
+ await v3Storage.setGroupsMeta(localMeta)
418
+ console.log(`[mockbubu ${timestamp}] ✓ 更新 _meta.json`)
419
+ }
420
+
421
+ // 6. 清理临时目录
422
+ const cleanupTmpDir = async (retries = 3) => {
423
+ for (let i = 0; i < retries; i++) {
424
+ try {
425
+ await fs.rm(tmpDir, { recursive: true, force: true })
426
+ console.log(`[mockbubu ${timestamp}] ✓ 临时目录已清理: ${tmpDir}`)
427
+ return
428
+ } catch (err) {
429
+ if (i === retries - 1) {
430
+ console.error(`[mockbubu ${timestamp}] ✗ 清理临时目录失败(已重试${retries}次): ${err.message}`)
431
+ } else {
432
+ await new Promise(resolve => setTimeout(resolve, 100))
433
+ }
434
+ }
435
+ }
436
+ }
437
+ await cleanupTmpDir()
438
+
439
+ console.log(`[mockbubu ${timestamp}] 导入完成`)
440
+
441
+ ctx.body = createSuccessResponse(
442
+ {
443
+ importedGroups,
444
+ conflictResolutions,
445
+ },
446
+ `成功导入 ${importedGroups.length} 个分组`,
447
+ )
448
+ } catch (err) {
449
+ // 出错时清理临时目录
450
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
451
+ throw err
452
+ }
453
+ } catch (error) {
454
+ const timestampError = new Date().toLocaleString('zh-CN', { hour12: false })
455
+ console.error(`[mockbubu ${timestampError}] 导入失败:`, error)
456
+ ctx.body = createErrorResponse(`导入失败: ${error.message}`)
457
+ }
458
+ })
459
+ }