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.
- package/README.md +38 -0
- package/index.js +3 -3
- package/lib/config/const.js +138 -0
- package/lib/config/rule-collector.js +81 -0
- package/lib/constants.js +62 -0
- package/lib/core/memory-buffer/index.js +207 -0
- package/lib/core/memory-buffer/shared-instance.js +15 -0
- package/lib/core/plugin-mode-manager.js +74 -0
- package/lib/core/resRulesServer.js +14 -0
- package/lib/core/rulesServer.js +31 -0
- package/lib/core/server-entry/capture-handler.js +191 -0
- package/lib/core/server-entry/request-interceptor.js +82 -0
- package/lib/core/server-entry/response-handler.js +147 -0
- package/lib/core/server-entry/server.js +230 -0
- package/lib/storage/group-manager.js +627 -0
- package/lib/storage/storage-adapter.js +712 -0
- package/lib/storage/storage-v3.js +1418 -0
- package/lib/uiServer/index.js +61 -24
- package/lib/uiServer/router/export/import-export-router.js +459 -0
- package/lib/uiServer/router/files/api-list-router.js +150 -0
- package/lib/uiServer/router/files/batch-operations-router.js +185 -0
- package/lib/uiServer/router/files/file-config-router.js +118 -0
- package/lib/uiServer/router/files/file-crud-router.js +212 -0
- package/lib/uiServer/router/files/file-save-router.js +146 -0
- package/lib/uiServer/router/files/version-router.js +260 -0
- package/lib/uiServer/router/global/plugin-control.js +135 -0
- package/lib/uiServer/router/global/system-info-router.js +386 -0
- package/lib/uiServer/router/{group-router.js → groups/group-router.js} +21 -20
- package/lib/uiServer/router/index.js +38 -1521
- package/lib/uiServer/utils/router-helpers.js +100 -0
- package/lib/uiServer/utils/util.js +172 -0
- package/lib/uiServer/{validator.js → utils/validator.js} +11 -6
- package/lib/utils/archive-utils.js +788 -0
- package/lib/utils/error-handler.js +173 -0
- package/lib/utils/logger.js +79 -0
- package/lib/utils/path-utils.js +147 -0
- package/lib/utils/performance.js +265 -0
- package/lib/utils/utils.js +541 -0
- package/package.json +2 -2
- package/public/js/app.js +3707 -1922
- package/public/js/app.js.map +1 -1
- package/public/js/chunk-vendors.js +5098 -3965
- package/public/js/chunk-vendors.js.map +1 -1
- package/rules.txt +1 -1
- package/CHANGELOG_GROUP_FEATURE.md +0 -468
- package/CHANGELOG_P0_FIXES.md +0 -412
- package/CHANGELOG_P1_OPTIMIZATIONS.md +0 -292
- package/CLAUDE.md +0 -436
- package/GROUP_FEATURE_DESIGN.md +0 -520
- package/lib/const.js +0 -47
- package/lib/group-manager.js +0 -491
- package/lib/resRulesServer.js +0 -9
- package/lib/server.js +0 -249
- package/lib/uiServer/router/version-router.js +0 -205
- package/lib/uiServer/util.js +0 -153
- package/lib/utils.js +0 -409
package/lib/uiServer/index.js
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|