whistle.mockbubu 1.0.0-dev.4 → 2.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +469 -0
- package/GROUP_FEATURE_DESIGN.md +520 -0
- package/README.md +106 -0
- package/lib/archive-utils.js +332 -0
- package/lib/const.js +19 -0
- package/lib/group-manager.js +660 -0
- package/lib/migration-v3.js +321 -0
- package/lib/server.js +333 -60
- package/lib/storage-adapter.js +518 -0
- package/lib/storage-v3.js +1368 -0
- package/lib/uiServer/index.js +76 -5
- package/lib/uiServer/router/group-router.js +218 -0
- package/lib/uiServer/router/index.js +1074 -51
- package/lib/uiServer/router/version-router.js +208 -63
- package/lib/uiServer/util.js +74 -16
- package/lib/uiServer/validator.js +105 -0
- package/lib/utils.js +107 -171
- package/package.json +1 -1
- package/public/js/app.js +5216 -1379
- package/public/js/app.js.map +1 -1
- package/public/js/chunk-vendors.js +14179 -8217
- package/public/js/chunk-vendors.js.map +1 -1
- package/rules.txt +1 -1
- package//346/212/200/346/234/257/346/226/271/346/241/210.md +452 -0
- package//346/265/213/350/257/225/346/215/225/350/216/267/345/212/237/350/203/275/346/255/245/351/252/244.md +145 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据迁移工具:旧架构 -> 新架构
|
|
3
|
+
*
|
|
4
|
+
* 旧架构(Whistle Storage):
|
|
5
|
+
* - Layer 1: files/{index}.{encodedName} - 物理文件
|
|
6
|
+
* - Layer 2: properties (全局元数据) - 单个 JSON 文件
|
|
7
|
+
* - Layer 3: properties (组配置) - 单个 JSON 文件
|
|
8
|
+
*
|
|
9
|
+
* 新架构(File System):
|
|
10
|
+
* - groups/{groupId}/group.json - 组元数据
|
|
11
|
+
* - groups/{groupId}/index.json - 文件索引
|
|
12
|
+
* - groups/{groupId}/files/{fileId}/file.json - 文件数据
|
|
13
|
+
* - groups/{groupId}/files/{fileId}/versions/*.json - 版本数据
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const FileSystemStorage = require('./storage-v3')
|
|
17
|
+
const GroupManager = require('./group-manager')
|
|
18
|
+
|
|
19
|
+
class DataMigration {
|
|
20
|
+
constructor(whistleStorage, targetBaseDir) {
|
|
21
|
+
this.whistleStorage = whistleStorage
|
|
22
|
+
this.newStorage = new FileSystemStorage(targetBaseDir)
|
|
23
|
+
this.stats = {
|
|
24
|
+
groupCount: 0,
|
|
25
|
+
fileCount: 0,
|
|
26
|
+
versionCount: 0,
|
|
27
|
+
errors: [],
|
|
28
|
+
startTime: 0,
|
|
29
|
+
endTime: 0,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 执行完整迁移
|
|
35
|
+
*/
|
|
36
|
+
async migrate() {
|
|
37
|
+
console.log('='.repeat(60))
|
|
38
|
+
console.log('数据迁移:旧架构 -> 新架构 V3')
|
|
39
|
+
console.log('='.repeat(60))
|
|
40
|
+
console.log()
|
|
41
|
+
|
|
42
|
+
this.stats.startTime = Date.now()
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// 初始化新存储
|
|
46
|
+
await this.newStorage.init()
|
|
47
|
+
console.log('✅ 新存储层已初始化\n')
|
|
48
|
+
|
|
49
|
+
// 创建临时 GroupManager 读取旧数据
|
|
50
|
+
const groupManager = new GroupManager(this.whistleStorage)
|
|
51
|
+
|
|
52
|
+
// 1. 迁移组
|
|
53
|
+
console.log('📦 Phase 1: 迁移组列表...')
|
|
54
|
+
const groups = groupManager.listGroups()
|
|
55
|
+
console.log(` 发现 ${groups.length} 个组\n`)
|
|
56
|
+
|
|
57
|
+
for (const group of groups) {
|
|
58
|
+
await this.migrateGroup(group, groupManager)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. 生成统计报告
|
|
62
|
+
this.stats.endTime = Date.now()
|
|
63
|
+
this.printReport()
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
success: this.stats.errors.length === 0,
|
|
67
|
+
stats: this.stats,
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('❌ 迁移失败:', error.message)
|
|
71
|
+
console.error(error.stack)
|
|
72
|
+
throw error
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 迁移单个组
|
|
78
|
+
*/
|
|
79
|
+
async migrateGroup(oldGroup, groupManager) {
|
|
80
|
+
console.log(`\n📁 迁移组: ${oldGroup.name} (${oldGroup.id})`)
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// 检查组是否已存在(跳过默认组的重复创建)
|
|
84
|
+
try {
|
|
85
|
+
await this.newStorage.getGroup(oldGroup.id)
|
|
86
|
+
console.log(' 组已存在,跳过创建')
|
|
87
|
+
} catch {
|
|
88
|
+
// 组不存在,创建新组
|
|
89
|
+
await this.newStorage.createGroup({
|
|
90
|
+
id: oldGroup.id,
|
|
91
|
+
name: oldGroup.name,
|
|
92
|
+
description: oldGroup.description || '',
|
|
93
|
+
isDefault: oldGroup.isDefault || false,
|
|
94
|
+
})
|
|
95
|
+
console.log(' ✅ 组已创建')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.stats.groupCount++
|
|
99
|
+
|
|
100
|
+
// 获取组内文件列表
|
|
101
|
+
const fileList = await this.getGroupFiles(oldGroup.id, groupManager)
|
|
102
|
+
console.log(` 发现 ${fileList.length} 个文件`)
|
|
103
|
+
|
|
104
|
+
if (fileList.length === 0) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 迁移文件
|
|
109
|
+
for (const file of fileList) {
|
|
110
|
+
await this.migrateFile(oldGroup.id, file, groupManager)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(' ✅ 组迁移完成')
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(` ❌ 组迁移失败: ${error.message}`)
|
|
116
|
+
this.stats.errors.push({
|
|
117
|
+
type: 'group',
|
|
118
|
+
id: oldGroup.id,
|
|
119
|
+
error: error.message,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 获取组内文件列表(从旧架构)
|
|
126
|
+
*/
|
|
127
|
+
async getGroupFiles(groupId, groupManager) {
|
|
128
|
+
const allFiles = this.whistleStorage.getFileList() || []
|
|
129
|
+
const groupPrefix = `${groupId}/`
|
|
130
|
+
const files = []
|
|
131
|
+
|
|
132
|
+
for (const item of allFiles) {
|
|
133
|
+
try {
|
|
134
|
+
// 解码文件名
|
|
135
|
+
const dotIndex = item.name.indexOf('.')
|
|
136
|
+
if (dotIndex === -1) continue
|
|
137
|
+
|
|
138
|
+
const encodedName = item.name.substring(dotIndex + 1)
|
|
139
|
+
const decodedName = decodeURIComponent(encodedName)
|
|
140
|
+
|
|
141
|
+
// 检查是否属于该组
|
|
142
|
+
if (decodedName.startsWith(groupPrefix)) {
|
|
143
|
+
const filename = decodedName.substring(groupPrefix.length)
|
|
144
|
+
|
|
145
|
+
// 获取配置
|
|
146
|
+
const config = await groupManager.getGroupFileConfig(groupId, filename)
|
|
147
|
+
|
|
148
|
+
files.push({
|
|
149
|
+
name: filename,
|
|
150
|
+
config: config || {},
|
|
151
|
+
storageKey: item.name,
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.warn(` ⚠️ 解码文件名失败: ${item.name}`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return files
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 迁移单个文件
|
|
164
|
+
*/
|
|
165
|
+
async migrateFile(groupId, fileInfo, groupManager) {
|
|
166
|
+
const { name, config } = fileInfo
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
// 生成文件ID
|
|
170
|
+
const fileId = this.newStorage.generateFileId(name)
|
|
171
|
+
|
|
172
|
+
// 检查文件是否已存在
|
|
173
|
+
if (await this.newStorage.fileExists(groupId, fileId)) {
|
|
174
|
+
console.log(` ⏭️ 文件已存在,跳过: ${name.substring(0, 60)}...`)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 读取原始 session 数据
|
|
179
|
+
const sessionPath = `${groupId}/${name}`
|
|
180
|
+
const sessionData = this.whistleStorage.readFile(sessionPath)
|
|
181
|
+
|
|
182
|
+
if (!sessionData) {
|
|
183
|
+
console.warn(` ⚠️ 无法读取文件数据: ${name.substring(0, 60)}...`)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let session
|
|
188
|
+
try {
|
|
189
|
+
session = JSON.parse(sessionData)
|
|
190
|
+
} catch {
|
|
191
|
+
console.warn(` ⚠️ 解析JSON失败: ${name.substring(0, 60)}...`)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 构建文件数据
|
|
196
|
+
const fileData = {
|
|
197
|
+
url: config.url || name,
|
|
198
|
+
method: config.method || session.req?.method || 'GET',
|
|
199
|
+
status: config.status || session.res?.statusCode || 200,
|
|
200
|
+
headers: session.res?.headers || {},
|
|
201
|
+
session,
|
|
202
|
+
config: {
|
|
203
|
+
mock: config.mock || false,
|
|
204
|
+
locked: config.locked || false,
|
|
205
|
+
mockVersion: config.mockVersion || 'source',
|
|
206
|
+
rule: config.rule || '',
|
|
207
|
+
ruleValue: config.ruleValue || '',
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 创建文件
|
|
212
|
+
await this.newStorage.createFile(groupId, fileId, fileData)
|
|
213
|
+
this.stats.fileCount++
|
|
214
|
+
|
|
215
|
+
// 迁移版本
|
|
216
|
+
const versions = await groupManager.getGroupVersions(groupId, name)
|
|
217
|
+
if (versions && versions.length > 0) {
|
|
218
|
+
for (const version of versions) {
|
|
219
|
+
await this.migrateVersion(groupId, fileId, version)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 每10个文件输出一次进度
|
|
224
|
+
if (this.stats.fileCount % 10 === 0) {
|
|
225
|
+
console.log(` 📊 进度: ${this.stats.fileCount} 个文件已迁移`)
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error(` ❌ 文件迁移失败: ${name.substring(0, 60)}... - ${error.message}`)
|
|
229
|
+
this.stats.errors.push({
|
|
230
|
+
type: 'file',
|
|
231
|
+
groupId,
|
|
232
|
+
name,
|
|
233
|
+
error: error.message,
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 迁移版本
|
|
240
|
+
*/
|
|
241
|
+
async migrateVersion(groupId, fileId, version) {
|
|
242
|
+
try {
|
|
243
|
+
// 跳过 source 版本(对应物理文件)
|
|
244
|
+
if (version.name === 'source') {
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await this.newStorage.createVersion(groupId, fileId, version.name, {
|
|
249
|
+
description: version.description || '',
|
|
250
|
+
content: version.content || '',
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
this.stats.versionCount++
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error(` ⚠️ 版本迁移失败: ${version.name} - ${error.message}`)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 打印迁移报告
|
|
261
|
+
*/
|
|
262
|
+
printReport() {
|
|
263
|
+
const duration = ((this.stats.endTime - this.stats.startTime) / 1000).toFixed(2)
|
|
264
|
+
|
|
265
|
+
console.log('\n' + '='.repeat(60))
|
|
266
|
+
console.log('迁移完成报告')
|
|
267
|
+
console.log('='.repeat(60))
|
|
268
|
+
console.log()
|
|
269
|
+
console.log('📊 统计信息:')
|
|
270
|
+
console.log(` - 组数量: ${this.stats.groupCount}`)
|
|
271
|
+
console.log(` - 文件数量: ${this.stats.fileCount}`)
|
|
272
|
+
console.log(` - 版本数量: ${this.stats.versionCount}`)
|
|
273
|
+
console.log(` - 耗时: ${duration} 秒`)
|
|
274
|
+
console.log()
|
|
275
|
+
|
|
276
|
+
if (this.stats.errors.length > 0) {
|
|
277
|
+
console.log('⚠️ 错误列表:')
|
|
278
|
+
this.stats.errors.forEach((err, idx) => {
|
|
279
|
+
console.log(` ${idx + 1}. [${err.type}] ${err.id || err.name}`)
|
|
280
|
+
console.log(` 错误: ${err.error}`)
|
|
281
|
+
})
|
|
282
|
+
console.log()
|
|
283
|
+
console.log(`❌ 迁移完成,但有 ${this.stats.errors.length} 个错误`)
|
|
284
|
+
} else {
|
|
285
|
+
console.log('✅ 迁移成功,无错误!')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 验证迁移结果
|
|
293
|
+
*/
|
|
294
|
+
async verify() {
|
|
295
|
+
console.log('\n' + '='.repeat(60))
|
|
296
|
+
console.log('验证迁移结果')
|
|
297
|
+
console.log('='.repeat(60))
|
|
298
|
+
console.log()
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
// 验证组数量
|
|
302
|
+
const groups = await this.newStorage.listGroups()
|
|
303
|
+
console.log(`✅ 组数量: ${groups.length}`)
|
|
304
|
+
|
|
305
|
+
// 验证每个组的文件数量
|
|
306
|
+
for (const group of groups) {
|
|
307
|
+
const files = await this.newStorage.listFiles(group.id)
|
|
308
|
+
console.log(` - ${group.name}: ${files.length} 个文件`)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log()
|
|
312
|
+
console.log('✅ 验证通过')
|
|
313
|
+
return true
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error('❌ 验证失败:', error.message)
|
|
316
|
+
return false
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = DataMigration
|