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.
@@ -0,0 +1,518 @@
1
+ /**
2
+ * Storage Adapter - 适配器模式
3
+ *
4
+ * 将新的 FileSystemStorage 包装成 Whistle Storage 兼容的 API
5
+ * 使得现有代码无需大改即可使用新存储
6
+ */
7
+
8
+ const FileSystemStorage = require('./storage-v3')
9
+ const path = require('path')
10
+
11
+ class StorageAdapter {
12
+ constructor(options) {
13
+ const baseDir = options.baseDir || path.join(
14
+ process.env.HOME,
15
+ '.WhistleAppData/.whistle/.plugins/whistle.mockbubu',
16
+ )
17
+
18
+ this.v3Storage = new FileSystemStorage(baseDir)
19
+ this.currentGroupId = 'default'
20
+ this.initialized = false
21
+ }
22
+
23
+ /**
24
+ * 初始化(必须在使用前调用)
25
+ */
26
+ async init() {
27
+ if (this.initialized) return
28
+ await this.v3Storage.init()
29
+ // 从文件加载当前组ID
30
+ this.currentGroupId = await this.v3Storage.getCurrentGroupId()
31
+ this.initialized = true
32
+ }
33
+
34
+ /**
35
+ * 确保已初始化
36
+ */
37
+ async _ensureInit() {
38
+ if (!this.initialized) {
39
+ await this.init()
40
+ }
41
+ }
42
+
43
+ /**
44
+ * 设置当前组ID
45
+ */
46
+ async setCurrentGroupId(groupId) {
47
+ this.currentGroupId = groupId
48
+ // V3 架构:持久化到文件
49
+ if (this.getV3Storage) {
50
+ const v3Storage = this.getV3Storage()
51
+ await v3Storage.setCurrentGroupId(groupId)
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 获取当前组ID
57
+ */
58
+ async getCurrentGroupId() {
59
+ // V3 架构:从文件读取
60
+ if (this.getV3Storage) {
61
+ const v3Storage = this.getV3Storage()
62
+ this.currentGroupId = await v3Storage.getCurrentGroupId()
63
+ }
64
+ return this.currentGroupId
65
+ }
66
+
67
+ // ========================================
68
+ // 兼容旧 API:文件操作
69
+ // ========================================
70
+
71
+ /**
72
+ * 读取文件
73
+ * @param {string} name - 文件名(格式:{groupId}/{filename})
74
+ */
75
+ async readFile(name) {
76
+ await this._ensureInit()
77
+
78
+ try {
79
+ const { groupId, filename } = this._parsePath(name)
80
+
81
+ // ⚠️ 强制刷新索引缓存,确保读取最新数据
82
+ this.v3Storage.invalidateIndex(groupId)
83
+
84
+ // 从索引查找对应的 fileId(因为 fileId 是随机生成的,无法从 URL 推导)
85
+ const index = await this.v3Storage.getIndex(groupId)
86
+ const entry = index.files.find(f => f.url === filename)
87
+
88
+ if (!entry) {
89
+ console.log(`[storage-adapter] ⚠️ 文件不存在: ${filename}`)
90
+ return null
91
+ }
92
+
93
+ // 使用索引中的 fileId 读取文件
94
+ const file = await this.v3Storage.getFile(groupId, entry.id)
95
+
96
+ if (!file || !file.session) {
97
+ console.log(`[storage-adapter] ⚠️ 未找到 session 数据: ${groupId}/${entry.id}`)
98
+ return null
99
+ }
100
+
101
+ // 返回 session 数据的 JSON 字符串(兼容旧格式)
102
+ return JSON.stringify(file.session)
103
+ } catch (err) {
104
+ console.error('[storage-adapter] ❌ readFile 失败:', err.message)
105
+ return null
106
+ }
107
+ }
108
+
109
+ /**
110
+ * 写入文件
111
+ * @param {string} name - 文件名(格式:{groupId}/{filename})
112
+ * @param {string} data - JSON 字符串
113
+ */
114
+ async writeFile(name, data) {
115
+ await this._ensureInit()
116
+
117
+ const { groupId, filename } = this._parsePath(name)
118
+
119
+ try {
120
+ // 解析 session 数据
121
+ let session
122
+ let hasParseError = false
123
+
124
+ try {
125
+ session = JSON.parse(data)
126
+ console.log(`[storage-adapter] ✅ JSON 解析成功: method=${session.req?.method}, status=${session.res?.statusCode}`)
127
+ } catch (parseErr) {
128
+ // JSON 解析失败 - 可能是非标准格式,但仍然保存
129
+ console.warn(`[storage-adapter] ⚠️ JSON 解析失败,将保存原始数据: ${filename}`)
130
+ console.warn(`[storage-adapter] 解析错误: ${parseErr.message}`)
131
+ hasParseError = true
132
+
133
+ // 构造最小可用的 session 对象,保存原始响应数据
134
+ session = {
135
+ req: {
136
+ method: 'UNKNOWN',
137
+ headers: {},
138
+ },
139
+ res: {
140
+ statusCode: 0,
141
+ headers: {},
142
+ body: data, // 保存原始的未解析数据
143
+ },
144
+ _parseError: parseErr.message, // 标记解析错误
145
+ }
146
+ }
147
+
148
+ // ⚠️ 强制刷新索引缓存
149
+ this.v3Storage.invalidateIndex(groupId)
150
+
151
+ // 从索引查找是否已有相同URL的文件
152
+ const index = await this.v3Storage.getIndex(groupId)
153
+ const existingEntry = index.files.find(f => f.url === filename)
154
+
155
+ if (existingEntry) {
156
+ // 更新现有文件(使用已有的 fileId)
157
+ console.log(`[storage-adapter] 🔄 更新现有文件: ${existingEntry.id}`)
158
+ await this.v3Storage.updateFile(groupId, existingEntry.id, {
159
+ session,
160
+ updateTime: Date.now(),
161
+ hasParseError, // 标记是否有解析错误
162
+ })
163
+ console.log(`[storage-adapter] ✅ 文件更新成功${hasParseError ? ' (包含解析错误)' : ''}`)
164
+ } else {
165
+ // 创建新文件(生成新的随机 fileId)
166
+ const newFileId = this.v3Storage.generateFileId()
167
+ console.log(`[storage-adapter] ➕ 创建新文件: ${newFileId}`)
168
+ await this.v3Storage.createFile(groupId, newFileId, {
169
+ url: filename,
170
+ method: session.req?.method || 'UNKNOWN',
171
+ status: session.res?.statusCode || 0,
172
+ headers: session.res?.headers || {},
173
+ session,
174
+ config: {
175
+ mock: false,
176
+ locked: false,
177
+ mockVersion: 'source',
178
+ },
179
+ hasParseError, // 标记是否有解析错误
180
+ })
181
+ console.log(`[storage-adapter] ✅ 文件创建成功${hasParseError ? ' (包含解析错误)' : ''}`)
182
+ }
183
+ } catch (err) {
184
+ // 存储层错误 - 记录但不抛出,避免影响正常流量
185
+ console.error(`[storage-adapter] ❌ 写入文件失败: ${filename}`)
186
+ console.error(`[storage-adapter] 错误详情: ${err.message}`)
187
+ console.error('[storage-adapter] 错误堆栈:', err.stack)
188
+ // 不再抛出错误,避免中断请求流程
189
+ }
190
+ }
191
+
192
+ /**
193
+ * 删除文件
194
+ * @param {string} name - 文件名(格式:{groupId}/{filename})
195
+ */
196
+ async removeFile(name) {
197
+ await this._ensureInit()
198
+
199
+ const { groupId, filename } = this._parsePath(name)
200
+
201
+ try {
202
+ // ⚠️ 强制刷新索引缓存
203
+ this.v3Storage.invalidateIndex(groupId)
204
+
205
+ // 从索引查找对应的 fileId
206
+ const index = await this.v3Storage.getIndex(groupId)
207
+ const entry = index.files.find(f => f.url === filename)
208
+
209
+ if (!entry) {
210
+ console.log(`[storage-adapter] ⚠️ 文件不存在,无需删除: ${filename}`)
211
+ return
212
+ }
213
+
214
+ console.log(`[storage-adapter] 🗑️ 删除文件: ${entry.id} (${filename})`)
215
+ await this.v3Storage.deleteFile(groupId, entry.id)
216
+ console.log('[storage-adapter] ✅ 删除成功')
217
+ } catch (err) {
218
+ console.error('[storage-adapter] ❌ 删除失败:', err.message, err.stack)
219
+ // 文件不存在也视为成功
220
+ }
221
+ }
222
+
223
+ /**
224
+ * 获取文件列表
225
+ * @returns {Array} - 文件列表(兼容旧格式)
226
+ */
227
+ async getFileList() {
228
+ await this._ensureInit()
229
+
230
+ try {
231
+ const groups = await this.v3Storage.listGroups()
232
+ const allFiles = []
233
+
234
+ for (const group of groups) {
235
+ const files = await this.v3Storage.listFiles(group.id)
236
+
237
+ files.forEach(file => {
238
+ // 构造兼容旧格式的文件名
239
+ const name = `${group.id}/${file.url}`
240
+
241
+ allFiles.push({
242
+ name, // 不带索引前缀(新架构不需要)
243
+ size: file.size,
244
+ })
245
+ })
246
+ }
247
+
248
+ return allFiles
249
+ } catch (err) {
250
+ console.error('[storage-adapter] getFileList failed:', err.message)
251
+ return []
252
+ }
253
+ }
254
+
255
+ // ========================================
256
+ // 兼容旧 API:属性操作
257
+ // ========================================
258
+
259
+ /**
260
+ * 获取属性
261
+ * @param {string} key - 属性键
262
+ */
263
+ async getProperty(key) {
264
+ await this._ensureInit()
265
+
266
+ try {
267
+ // 特殊处理:组列表
268
+ if (key === '__groups__') {
269
+ return await this._getGroupsProperty()
270
+ }
271
+
272
+ // 特殊处理:组文件配置
273
+ if (key.startsWith('group.') && key.includes('.file.')) {
274
+ return await this._getFileConfigProperty(key)
275
+ }
276
+
277
+ // 通用属性:从 V3 storage 的 properties.json 读取
278
+ return await this.v3Storage.getGenericProperty(key)
279
+ } catch (err) {
280
+ return null
281
+ }
282
+ }
283
+
284
+ /**
285
+ * 设置属性
286
+ * @param {string} key - 属性键
287
+ * @param {*} value - 属性值
288
+ */
289
+ async setProperty(key, value) {
290
+ await this._ensureInit()
291
+
292
+ try {
293
+ // 特殊处理:组列表
294
+ if (key === '__groups__') {
295
+ // 新架构不需要手动管理组列表
296
+ return
297
+ }
298
+
299
+ // 特殊处理:组文件配置
300
+ if (key.startsWith('group.') && key.includes('.file.')) {
301
+ await this._setFileConfigProperty(key, value)
302
+ return
303
+ }
304
+
305
+ // 通用属性:写入 V3 storage 的 properties.json
306
+ await this.v3Storage.setGenericProperty(key, value)
307
+ } catch (err) {
308
+ console.error('[storage-adapter] setProperty failed:', key, err.message)
309
+ }
310
+ }
311
+
312
+ /**
313
+ * 删除属性
314
+ * @param {string} key - 属性键
315
+ */
316
+ async removeProperty(key) {
317
+ await this._ensureInit()
318
+
319
+ try {
320
+ // 从 Whistle storage 中删除
321
+ await this.storage.removeProperty(key)
322
+ } catch (err) {
323
+ console.error('[storage-adapter] removeProperty failed:', key, err.message)
324
+ throw err
325
+ }
326
+ }
327
+
328
+ /**
329
+ * 检查属性是否存在
330
+ * @param {string} key - 属性键
331
+ */
332
+ async hasProperty(key) {
333
+ await this._ensureInit()
334
+
335
+ const value = await this.getProperty(key)
336
+ return value !== null && value !== undefined
337
+ }
338
+
339
+ /**
340
+ * 检查文件是否在索引中(用于 Issue #4 检测)
341
+ * 与 hasProperty 的区别:
342
+ * - hasProperty:检查物理文件是否存在
343
+ * - hasFileInIndex:检查索引中是否有记录(即使物理文件被删除)
344
+ *
345
+ * @param {string} key - 配置键,格式:group.{groupId}.file.{filename}
346
+ * @returns {boolean} 索引中是否存在该文件
347
+ */
348
+ async hasFileInIndex(key) {
349
+ await this._ensureInit()
350
+
351
+ try {
352
+ // 解析配置键
353
+ if (!key.startsWith('group.') || !key.includes('.file.')) {
354
+ return false
355
+ }
356
+
357
+ const { groupId, filename } = this._parseConfigKey(key)
358
+
359
+ // ⚠️ 强制刷新索引缓存,确保读取最新数据(避免删除后立即捕获时读到旧缓存)
360
+ this.v3Storage.invalidateIndex(groupId)
361
+
362
+ // 检查索引中是否存在(通过 URL 而不是 fileId,因为 fileId 是创建时生成的随机ID)
363
+ const index = await this.v3Storage.getIndex(groupId)
364
+ const exists = index.files.some(f => f.url === filename)
365
+
366
+ return exists
367
+ } catch (err) {
368
+ return false
369
+ }
370
+ }
371
+
372
+ /**
373
+ * 获取所有属性
374
+ */
375
+ async getProperties() {
376
+ await this._ensureInit()
377
+
378
+ // 新架构不需要这个方法,返回空对象
379
+ return {}
380
+ }
381
+
382
+ // ========================================
383
+ // 内部辅助方法
384
+ // ========================================
385
+
386
+ /**
387
+ * 解析文件路径
388
+ * @param {string} path - 格式:{groupId}/{filename}
389
+ */
390
+ _parsePath(pathStr) {
391
+ const parts = pathStr.split('/')
392
+ if (parts.length < 2) {
393
+ return {
394
+ groupId: this.currentGroupId,
395
+ filename: pathStr,
396
+ }
397
+ }
398
+
399
+ return {
400
+ groupId: parts[0],
401
+ filename: parts.slice(1).join('/'),
402
+ }
403
+ }
404
+
405
+ /**
406
+ * 解析配置键
407
+ * @param {string} key - 格式:group.{groupId}.file.{filename}
408
+ */
409
+ _parseConfigKey(key) {
410
+ const parts = key.split('.file.')
411
+ if (parts.length !== 2) {
412
+ throw new Error(`Invalid config key: ${key}`)
413
+ }
414
+
415
+ const groupId = parts[0].replace('group.', '')
416
+ const filename = parts[1]
417
+
418
+ return { groupId, filename }
419
+ }
420
+
421
+ /**
422
+ * 获取组列表属性(兼容旧格式)
423
+ */
424
+ async _getGroupsProperty() {
425
+ const groups = await this.v3Storage.listGroups()
426
+ const currentGroupId = await this.v3Storage.getCurrentGroupId()
427
+
428
+ return {
429
+ groups: groups.map(g => ({
430
+ id: g.id,
431
+ name: g.name,
432
+ description: g.description,
433
+ createTime: g.createTime,
434
+ updateTime: g.updateTime,
435
+ isDefault: g.isDefault,
436
+ })),
437
+ currentGroupId,
438
+ }
439
+ }
440
+
441
+ /**
442
+ * 获取文件配置属性(兼容旧格式)
443
+ */
444
+ async _getFileConfigProperty(key) {
445
+ const { groupId, filename } = this._parseConfigKey(key)
446
+
447
+ try {
448
+ // ⚠️ 强制刷新索引缓存
449
+ this.v3Storage.invalidateIndex(groupId)
450
+
451
+ // 从索引查找对应的 fileId
452
+ const index = await this.v3Storage.getIndex(groupId)
453
+ const entry = index.files.find(f => f.url === filename)
454
+
455
+ if (!entry) {
456
+ return null
457
+ }
458
+
459
+ const file = await this.v3Storage.getFile(groupId, entry.id)
460
+
461
+ return {
462
+ ...file.config,
463
+ url: file.url,
464
+ method: file.method,
465
+ status: file.status,
466
+ date: file.createTime,
467
+ }
468
+ } catch (err) {
469
+ return null
470
+ }
471
+ }
472
+
473
+ /**
474
+ * 设置文件配置属性
475
+ */
476
+ async _setFileConfigProperty(key, value) {
477
+ const { groupId, filename } = this._parseConfigKey(key)
478
+
479
+ try {
480
+ // ⚠️ 强制刷新索引缓存
481
+ this.v3Storage.invalidateIndex(groupId)
482
+
483
+ // 从索引查找对应的 fileId
484
+ const index = await this.v3Storage.getIndex(groupId)
485
+ const entry = index.files.find(f => f.url === filename)
486
+
487
+ if (!entry) {
488
+ console.error('[storage-adapter] _setFileConfigProperty: 文件不存在:', filename)
489
+ return
490
+ }
491
+
492
+ const file = await this.v3Storage.getFile(groupId, entry.id)
493
+
494
+ await this.v3Storage.updateFile(groupId, entry.id, {
495
+ config: {
496
+ ...file.config,
497
+ ...value,
498
+ },
499
+ })
500
+ } catch (err) {
501
+ console.error('[storage-adapter] _setFileConfigProperty failed:', err.message)
502
+ }
503
+ }
504
+
505
+ // ========================================
506
+ // 新增:直接访问 V3 Storage
507
+ // ========================================
508
+
509
+ /**
510
+ * 获取底层的 V3 Storage 实例
511
+ * 用于需要使用新 API 的场景
512
+ */
513
+ getV3Storage() {
514
+ return this.v3Storage
515
+ }
516
+ }
517
+
518
+ module.exports = StorageAdapter