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
@@ -0,0 +1,712 @@
1
+ /**
2
+ * 文件名: storage-adapter.js
3
+ * 功能: Storage Adapter - V3 存储适配层
4
+ * 依赖: storage-v3.js, logger.js, path-utils.js, error-handler.js
5
+ * 更新: 2025-12-05
6
+ *
7
+ * 职责:
8
+ * - 封装 V3 FileSystemStorage,提供统一的存储接口
9
+ * - 处理路径解析、分组逻辑、错误处理
10
+ * - 为上层路由和中间件提供简洁的 API
11
+ *
12
+ * 设计模式:
13
+ * - 适配器模式: 封装底层存储实现细节
14
+ * - 单例模式: 全局共享存储实例
15
+ *
16
+ * 使用示例:
17
+ * ```javascript
18
+ * const adapter = new StorageAdapter({ baseDir: '~/.whistle' })
19
+ * await adapter.init()
20
+ * await adapter.writeFile('default/https://api.test.com', '{"data": {...}}')
21
+ * const content = await adapter.readFile('default/https://api.test.com')
22
+ * ```
23
+ */
24
+
25
+ const FileSystemStorage = require('./storage-v3')
26
+ const path = require('path')
27
+ const { createLogger } = require('../utils/logger')
28
+ const { parsePath, parseConfigKey } = require('../utils/path-utils')
29
+ const { withErrorHandling } = require('../utils/error-handler')
30
+
31
+ /**
32
+ * StorageAdapter 类
33
+ * V3 文件系统存储的适配层,提供统一的存储接口
34
+ */
35
+ class StorageAdapter {
36
+ /**
37
+ * 构造函数
38
+ *
39
+ * @param {Object} options - 配置选项
40
+ * @param {string} options.baseDir - 基础目录路径(可选)
41
+ *
42
+ * @example
43
+ * const adapter = new StorageAdapter({
44
+ * baseDir: '~/.whistle'
45
+ * })
46
+ */
47
+ constructor(options) {
48
+ // 初始化 logger
49
+ this.logger = createLogger('storage-adapter')
50
+
51
+ // 计算基础目录路径
52
+ let baseDir = path.join(
53
+ process.env.HOME,
54
+ '.WhistleAppData/.whistle/.plugins/whistle.mockbubu',
55
+ )
56
+ if (options.baseDir) {
57
+ baseDir = `${options.baseDir}/.plugins/whistle.mockbubu`
58
+ }
59
+
60
+ // 记录基础目录(生产环境可保留,便于问题排查)
61
+ this.logger.log('基础目录:', baseDir)
62
+
63
+ // 初始化 V3 存储实例
64
+ this.v3Storage = new FileSystemStorage(baseDir)
65
+ this.currentGroupId = 'default'
66
+ this.initialized = false
67
+ }
68
+
69
+ /**
70
+ * 初始化存储适配器
71
+ * 必须在使用前调用,负责初始化底层 V3 存储并加载当前组ID
72
+ *
73
+ * @returns {Promise<void>}
74
+ *
75
+ * @example
76
+ * const adapter = new StorageAdapter(options)
77
+ * await adapter.init() // 必须先初始化
78
+ * await adapter.readFile('default/test.json')
79
+ */
80
+ async init() {
81
+ if (this.initialized) return
82
+
83
+ // 初始化 V3 存储(创建目录结构、加载元数据等)
84
+ await this.v3Storage.init()
85
+
86
+ // 从文件系统加载当前激活的组ID
87
+ this.currentGroupId = await this.v3Storage.getCurrentGroupId()
88
+
89
+ this.initialized = true
90
+ this.logger.log('存储适配器初始化完成,当前组:', this.currentGroupId)
91
+ }
92
+
93
+ /**
94
+ * 确保已初始化(内部方法)
95
+ * 所有对外API调用前都应该先调用此方法
96
+ *
97
+ * @private
98
+ * @returns {Promise<void>}
99
+ */
100
+ async _ensureInit() {
101
+ if (!this.initialized) {
102
+ await this.init()
103
+ }
104
+ }
105
+
106
+ /**
107
+ * 设置当前激活的组ID
108
+ * 会持久化到文件系统,重启后仍然有效
109
+ *
110
+ * @param {string} groupId - 组ID
111
+ * @returns {Promise<void>}
112
+ *
113
+ * @example
114
+ * await adapter.setCurrentGroupId('group_123')
115
+ */
116
+ async setCurrentGroupId(groupId) {
117
+ this.currentGroupId = groupId
118
+
119
+ // V3 架构:持久化到文件系统(写入组元数据)
120
+ if (this.getV3Storage) {
121
+ const v3Storage = this.getV3Storage()
122
+ await v3Storage.setCurrentGroupId(groupId)
123
+ }
124
+
125
+ this.logger.log('切换当前组:', groupId)
126
+ }
127
+
128
+ /**
129
+ * 获取当前激活的组ID
130
+ * 从文件系统读取,确保获取最新值
131
+ *
132
+ * @returns {Promise<string>} 当前组ID
133
+ *
134
+ * @example
135
+ * const currentGroup = await adapter.getCurrentGroupId()
136
+ * console.log('当前组:', currentGroup)
137
+ */
138
+ async getCurrentGroupId() {
139
+ // V3 架构:从文件系统读取(确保获取最新值)
140
+ if (this.getV3Storage) {
141
+ const v3Storage = this.getV3Storage()
142
+ this.currentGroupId = await v3Storage.getCurrentGroupId()
143
+ }
144
+ return this.currentGroupId
145
+ }
146
+
147
+ // ========================================
148
+ // 文件操作 API
149
+ // ========================================
150
+
151
+ /**
152
+ * 读取文件内容
153
+ * 通过 URL 查找文件,返回 session 数据的 JSON 字符串
154
+ *
155
+ * 工作流程:
156
+ * 1. 解析路径得到 groupId 和 filename(URL)
157
+ * 2. 强制刷新索引缓存(避免读到过期数据)
158
+ * 3. 从索引中根据 URL 查找 fileId
159
+ * 4. 使用 fileId 读取完整文件数据
160
+ * 5. 返回 session 数据的 JSON 字符串
161
+ *
162
+ * @param {string} name - 文件路径(格式:{groupId}/{filename})
163
+ * @returns {Promise<string|null>} session 数据的 JSON 字符串,不存在返回 null
164
+ *
165
+ * @example
166
+ * const data = await adapter.readFile('default/https://api.test.com/users')
167
+ * const session = JSON.parse(data)
168
+ * console.log(session.res.body) // 响应内容
169
+ */
170
+ async readFile(name) {
171
+ await this._ensureInit()
172
+
173
+ try {
174
+ // 解析路径:提取 groupId 和 filename(URL)
175
+ const { groupId, filename } = parsePath(name, this.currentGroupId)
176
+
177
+ // 强制刷新索引缓存,确保读取最新数据
178
+ // 这很重要:避免删除文件后立即读取时读到旧缓存
179
+ this.v3Storage.invalidateIndex(groupId)
180
+
181
+ // 从索引查找对应的 fileId
182
+ // 注意:V3 架构中 fileId 是随机生成的,无法从 URL 推导
183
+ const index = await this.v3Storage.getIndex(groupId)
184
+ const entry = index.files.find(f => f.url === filename)
185
+
186
+ if (!entry) {
187
+ this.logger.warn(`文件不存在: ${filename}`)
188
+ return null
189
+ }
190
+
191
+ // 使用索引中的 fileId 读取完整文件数据
192
+ const file = await this.v3Storage.getFile(groupId, entry.id)
193
+
194
+ if (!file) {
195
+ this.logger.warn(`文件不存在: ${groupId}/${entry.id}`)
196
+ return null
197
+ }
198
+
199
+ // ✅ V3 架构:session 数据存储在 file.json 的 session 字段中
200
+ // 直接从文件数据读取 session
201
+ const sessionData = file.session
202
+
203
+ if (!sessionData) {
204
+ this.logger.warn(`session 数据为空: ${groupId}/${entry.id}`)
205
+ return null
206
+ }
207
+
208
+ // 返回 session 数据的 JSON 字符串
209
+ return JSON.stringify(sessionData)
210
+ } catch (err) {
211
+ this.logger.error('readFile 失败:', err.message)
212
+ return null
213
+ }
214
+ }
215
+
216
+ /**
217
+ * 写入文件(创建或更新)
218
+ * 接收 session 数据的 JSON 字符串,自动判断是新建还是更新
219
+ *
220
+ * 工作流程:
221
+ * 1. 解析路径得到 groupId 和 filename
222
+ * 2. 尝试解析 JSON 数据为 session 对象
223
+ * 3. 强制刷新索引缓存
224
+ * 4. 查找是否已存在相同 URL 的文件
225
+ * 5. 存在则更新,不存在则创建新文件
226
+ *
227
+ * 错误处理:
228
+ * - JSON 解析失败时,会构造最小可用的 session 对象并标记错误
229
+ * - 存储失败不会抛出异常,避免影响正常请求流程
230
+ *
231
+ * @param {string} name - 文件路径(格式:{groupId}/{filename})
232
+ * @param {string} data - session 数据的 JSON 字符串
233
+ * @returns {Promise<void>}
234
+ *
235
+ * @example
236
+ * const session = {
237
+ * req: { method: 'GET', url: '/users', headers: {...} },
238
+ * res: { statusCode: 200, headers: {...}, body: '{"data": [...]}' }
239
+ * }
240
+ * await adapter.writeFile('default/https://api.test.com/users', JSON.stringify(session))
241
+ */
242
+ async writeFile(name, data) {
243
+ await this._ensureInit()
244
+
245
+ const { groupId, filename } = parsePath(name, this.currentGroupId)
246
+
247
+ try {
248
+ // 1. 解析 session 数据
249
+ let session
250
+ let hasParseError = false
251
+
252
+ try {
253
+ session = JSON.parse(data)
254
+ this.logger.log(`JSON 解析成功: method=${session.req?.method}, status=${session.res?.statusCode}`)
255
+ } catch (parseErr) {
256
+ // JSON 解析失败 - 可能是非标准格式,但仍然保存(宽容处理)
257
+ this.logger.warn(`JSON 解析失败,将保存原始数据: ${filename}`)
258
+ this.logger.warn(`解析错误: ${parseErr.message}`)
259
+ hasParseError = true
260
+
261
+ // 构造最小可用的 session 对象,保存原始响应数据
262
+ session = {
263
+ req: {
264
+ method: 'UNKNOWN',
265
+ headers: {},
266
+ },
267
+ res: {
268
+ statusCode: 0,
269
+ headers: {},
270
+ body: data, // 保存原始的未解析数据
271
+ },
272
+ _parseError: parseErr.message, // 标记解析错误,便于后续排查
273
+ }
274
+ }
275
+
276
+ // 2. 强制刷新索引缓存(确保查询到最新状态)
277
+ this.v3Storage.invalidateIndex(groupId)
278
+
279
+ // 3. 从索引查找是否已有相同 URL 的文件
280
+ const index = await this.v3Storage.getIndex(groupId)
281
+ const existingEntry = index.files.find(f => f.url === filename)
282
+
283
+ if (existingEntry) {
284
+ // 4a. 更新现有文件(使用已有的 fileId)
285
+ this.logger.log(`更新现有文件: ${existingEntry.id}`)
286
+ await this.v3Storage.updateFile(groupId, existingEntry.id, {
287
+ session,
288
+ updateTime: Date.now(),
289
+ hasParseError, // 标记是否有解析错误
290
+ })
291
+ this.logger.log(`文件更新成功${hasParseError ? ' (包含解析错误)' : ''}`)
292
+ } else {
293
+ // 4b. 创建新文件(生成新的随机 fileId)
294
+ const newFileId = this.v3Storage.generateFileId()
295
+ this.logger.log(`创建新文件: ${newFileId}`)
296
+ await this.v3Storage.createFile(groupId, newFileId, {
297
+ url: filename,
298
+ method: session.req?.method || 'UNKNOWN',
299
+ status: session.res?.statusCode || 0,
300
+ headers: session.res?.headers || {},
301
+ session,
302
+ config: {
303
+ mock: false, // 默认不启用 mock
304
+ locked: false, // 默认不锁定
305
+ mockVersion: 'source', // 默认使用原始版本
306
+ },
307
+ hasParseError, // 标记是否有解析错误
308
+ })
309
+ this.logger.log(`文件创建成功${hasParseError ? ' (包含解析错误)' : ''}`)
310
+ }
311
+ } catch (err) {
312
+ // 存储层错误 - 记录但不抛出,避免影响正常流量
313
+ // 这个设计很重要:存储失败不应该导致用户请求失败
314
+ this.logger.error(`写入文件失败: ${filename}`)
315
+ this.logger.error(`错误详情: ${err.message}`)
316
+
317
+ // 仅在开发环境输出堆栈
318
+ if (process.env.NODE_ENV === 'development') {
319
+ this.logger.error('错误堆栈:', err.stack)
320
+ }
321
+ // 不再抛出错误,避免中断请求流程
322
+ }
323
+ }
324
+
325
+ /**
326
+ * 删除文件
327
+ * 根据 URL 查找文件并删除(包括所有版本数据)
328
+ *
329
+ * 工作流程:
330
+ * 1. 解析路径得到 groupId 和 filename
331
+ * 2. 强制刷新索引缓存
332
+ * 3. 从索引中根据 URL 查找 fileId
333
+ * 4. 使用 fileId 删除文件(包括目录和所有版本)
334
+ *
335
+ * 错误处理:
336
+ * - 文件不存在也视为成功(幂等操作)
337
+ *
338
+ * @param {string} name - 文件路径(格式:{groupId}/{filename})
339
+ * @returns {Promise<void>}
340
+ *
341
+ * @example
342
+ * await adapter.removeFile('default/https://api.test.com/users')
343
+ */
344
+ async removeFile(name) {
345
+ await this._ensureInit()
346
+
347
+ const { groupId, filename } = parsePath(name, this.currentGroupId)
348
+
349
+ try {
350
+ // 强制刷新索引缓存(确保查询到最新状态)
351
+ this.v3Storage.invalidateIndex(groupId)
352
+
353
+ // 从索引查找对应的 fileId
354
+ const index = await this.v3Storage.getIndex(groupId)
355
+ const entry = index.files.find(f => f.url === filename)
356
+
357
+ if (!entry) {
358
+ this.logger.warn(`文件不存在,无需删除: ${filename}`)
359
+ return // 幂等操作:文件不存在也视为成功
360
+ }
361
+
362
+ this.logger.log(`删除文件: ${entry.id} (${filename})`)
363
+ await this.v3Storage.deleteFile(groupId, entry.id)
364
+ this.logger.log('删除成功')
365
+ } catch (err) {
366
+ this.logger.error('删除失败:', err.message)
367
+
368
+ // 仅在开发环境输出堆栈
369
+ if (process.env.NODE_ENV === 'development') {
370
+ this.logger.error('错误堆栈:', err.stack)
371
+ }
372
+ // 文件不存在也视为成功,不抛出异常
373
+ }
374
+ }
375
+
376
+ /**
377
+ * 获取所有组的文件列表
378
+ * 返回所有组中所有文件的列表
379
+ *
380
+ * 工作流程:
381
+ * 1. 获取所有组列表
382
+ * 2. 遍历每个组,获取文件列表
383
+ * 3. 构造文件对象(格式:{groupId}/{url})
384
+ *
385
+ * @returns {Promise<Array>} 文件列表,每个文件包含 name 和 size
386
+ *
387
+ * @example
388
+ * const files = await adapter.getFileList()
389
+ * files.forEach(file => {
390
+ * console.log(file.name, file.size)
391
+ * })
392
+ */
393
+ async getFileList() {
394
+ await this._ensureInit()
395
+
396
+ try {
397
+ const groups = await this.v3Storage.listGroups()
398
+ const allFiles = []
399
+
400
+ for (const group of groups) {
401
+ const files = await this.v3Storage.listFiles(group.id)
402
+
403
+ files.forEach(file => {
404
+ // 构造文件名(格式:{groupId}/{url})
405
+ const name = `${group.id}/${file.url}`
406
+
407
+ allFiles.push({
408
+ name, // 文件路径标识
409
+ size: file.size || 0, // 文件大小(字节)
410
+ })
411
+ })
412
+ }
413
+
414
+ return allFiles
415
+ } catch (err) {
416
+ this.logger.error('getFileList 失败:', err.message)
417
+ return [] // 失败时返回空数组,避免中断业务
418
+ }
419
+ }
420
+
421
+ // ========================================
422
+ // 属性操作 API
423
+ // ========================================
424
+
425
+ /**
426
+ * 获取属性
427
+ * @param {string} key - 属性键
428
+ */
429
+ async getProperty(key) {
430
+ await this._ensureInit()
431
+
432
+ try {
433
+ // 特殊处理:组列表
434
+ if (key === '__groups__') {
435
+ return await this._getGroupsProperty()
436
+ }
437
+
438
+ // 特殊处理:组文件配置
439
+ if (key.startsWith('group.') && key.includes('.file.')) {
440
+ return await this._getFileConfigProperty(key)
441
+ }
442
+
443
+ // 全局配置:从 V3 storage 的 _meta.json.config 读取
444
+ return await this.v3Storage.getConfig(key)
445
+ } catch (err) {
446
+ return null
447
+ }
448
+ }
449
+
450
+ /**
451
+ * 设置属性
452
+ * @param {string} key - 属性键
453
+ * @param {*} value - 属性值
454
+ */
455
+ async setProperty(key, value) {
456
+ await this._ensureInit()
457
+
458
+ try {
459
+ // 特殊处理:组列表
460
+ if (key === '__groups__') {
461
+ // 新架构不需要手动管理组列表
462
+ return
463
+ }
464
+
465
+ // 特殊处理:组文件配置
466
+ if (key.startsWith('group.') && key.includes('.file.')) {
467
+ await this._setFileConfigProperty(key, value)
468
+ return
469
+ }
470
+
471
+ // 全局配置:写入 V3 storage 的 _meta.json.config
472
+ await this.v3Storage.setConfig(key, value)
473
+ } catch (err) {
474
+ console.error('[storage-adapter] setProperty failed:', key, err.message)
475
+ }
476
+ }
477
+
478
+ /**
479
+ * 检查属性是否存在
480
+ * @param {string} key - 属性键
481
+ */
482
+ async hasProperty(key) {
483
+ await this._ensureInit()
484
+
485
+ const value = await this.getProperty(key)
486
+ return value !== null && value !== undefined
487
+ }
488
+
489
+ /**
490
+ * 检查文件是否在索引中(用于 Issue #4 检测)
491
+ *
492
+ * 与 hasProperty 的区别:
493
+ * - hasProperty:检查物理文件是否存在
494
+ * - hasFileInIndex:检查索引中是否有记录(即使物理文件被删除)
495
+ *
496
+ * 使用场景:
497
+ * - 检测删除后立即捕获时,索引可能还有记录但物理文件已删除
498
+ * - 用于诊断文件系统和索引不一致的问题
499
+ *
500
+ * @param {string} key - 配置键,格式:group.{groupId}.file.{filename}
501
+ * @returns {Promise<boolean>} 索引中是否存在该文件
502
+ *
503
+ * @example
504
+ * const exists = await adapter.hasFileInIndex('group.default.file.https://api.test.com/users')
505
+ * if (!exists) {
506
+ * console.log('索引中不存在该文件')
507
+ * }
508
+ */
509
+ async hasFileInIndex(key) {
510
+ await this._ensureInit()
511
+
512
+ try {
513
+ // 1. 验证配置键格式
514
+ if (!key.startsWith('group.') || !key.includes('.file.')) {
515
+ return false
516
+ }
517
+
518
+ // 2. 解析配置键得到 groupId 和 filename
519
+ const { groupId, filename } = parseConfigKey(key)
520
+
521
+ // 3. 强制刷新索引缓存,确保读取最新数据
522
+ // 这很重要:避免删除后立即捕获时读到旧缓存
523
+ this.v3Storage.invalidateIndex(groupId)
524
+
525
+ // 4. 检查索引中是否存在(通过 URL 而不是 fileId)
526
+ // 注意:V3 架构中 fileId 是创建时生成的随机ID
527
+ const index = await this.v3Storage.getIndex(groupId)
528
+ const exists = index.files.some(f => f.url === filename)
529
+
530
+ return exists
531
+ } catch (err) {
532
+ this.logger.error('hasFileInIndex 失败:', err.message)
533
+ return false // 失败时返回 false,视为不存在
534
+ }
535
+ }
536
+
537
+ /**
538
+ * 通过 URL 获取文件索引条目
539
+ *
540
+ * @param {string} groupId - 组ID
541
+ * @param {string} url - 文件URL
542
+ * @returns {Promise<Object|null>} 文件索引条目,不存在返回 null
543
+ */
544
+ async getFileByUrl(groupId, url) {
545
+ await this._ensureInit()
546
+
547
+ try {
548
+ // 强制刷新索引缓存,确保读取最新数据
549
+ this.v3Storage.invalidateIndex(groupId)
550
+
551
+ // 从索引中查找 URL 匹配的文件
552
+ const index = await this.v3Storage.getIndex(groupId)
553
+ const file = index.files.find(f => f.url === url)
554
+
555
+ return file || null
556
+ } catch (err) {
557
+ this.logger.error('getFileByUrl 失败:', err.message)
558
+ return null
559
+ }
560
+ }
561
+
562
+ // ========================================
563
+ // 内部辅助方法
564
+ // ========================================
565
+
566
+ /**
567
+ * 注意:原来的 _parsePath 和 _parseConfigKey 方法已移除
568
+ * 现在统一使用从 path-utils.js 导入的 parsePath 和 parseConfigKey
569
+ */
570
+
571
+ /**
572
+ * 获取组列表属性(兼容旧格式)
573
+ * 内部方法,用于处理 __groups__ 属性查询
574
+ *
575
+ * @private
576
+ * @returns {Promise<Object>} 组列表对象
577
+ * @property {Array} groups - 组列表
578
+ * @property {string} currentGroupId - 当前激活的组ID
579
+ *
580
+ * @example
581
+ * const groupsData = await this._getGroupsProperty()
582
+ * console.log(groupsData.currentGroupId)
583
+ * groupsData.groups.forEach(g => console.log(g.name))
584
+ */
585
+ async _getGroupsProperty() {
586
+ const groups = await this.v3Storage.listGroups()
587
+ const currentGroupId = await this.v3Storage.getCurrentGroupId()
588
+
589
+ return {
590
+ groups: groups.map(g => ({
591
+ id: g.id,
592
+ name: g.name,
593
+ description: g.description,
594
+ createTime: g.createTime,
595
+ updateTime: g.updateTime,
596
+ isDefault: g.isDefault,
597
+ })),
598
+ currentGroupId,
599
+ }
600
+ }
601
+
602
+ /**
603
+ * 获取文件配置属性(兼容旧格式)
604
+ * 内部方法,用于处理 group.{groupId}.file.{filename} 格式的配置查询
605
+ *
606
+ * @private
607
+ * @param {string} key - 配置键,格式:group.{groupId}.file.{filename}
608
+ * @returns {Promise<Object|null>} 文件配置对象,不存在返回 null
609
+ *
610
+ * @example
611
+ * const config = await this._getFileConfigProperty('group.default.file.https://api.test.com/users')
612
+ * if (config) {
613
+ * console.log('Mock状态:', config.mock)
614
+ * console.log('锁定状态:', config.locked)
615
+ * }
616
+ */
617
+ async _getFileConfigProperty(key) {
618
+ const { groupId, filename } = parseConfigKey(key)
619
+
620
+ try {
621
+ // 强制刷新索引缓存
622
+ this.v3Storage.invalidateIndex(groupId)
623
+
624
+ // 从索引查找对应的 fileId
625
+ const index = await this.v3Storage.getIndex(groupId)
626
+ const entry = index.files.find(f => f.url === filename)
627
+
628
+ if (!entry) {
629
+ return null
630
+ }
631
+
632
+ const file = await this.v3Storage.getFile(groupId, entry.id)
633
+
634
+ // 返回兼容旧格式的配置对象
635
+ return {
636
+ ...file.config,
637
+ url: file.url,
638
+ method: file.method,
639
+ status: file.status,
640
+ date: file.createTime,
641
+ }
642
+ } catch (err) {
643
+ this.logger.error('_getFileConfigProperty 失败:', err.message)
644
+ return null
645
+ }
646
+ }
647
+
648
+ /**
649
+ * 设置文件配置属性
650
+ * 内部方法,用于处理 group.{groupId}.file.{filename} 格式的配置更新
651
+ *
652
+ * @private
653
+ * @param {string} key - 配置键,格式:group.{groupId}.file.{filename}
654
+ * @param {Object} value - 配置值(会与现有配置合并)
655
+ * @returns {Promise<void>}
656
+ *
657
+ * @example
658
+ * await this._setFileConfigProperty(
659
+ * 'group.default.file.https://api.test.com/users',
660
+ * { mock: true, locked: true }
661
+ * )
662
+ */
663
+ async _setFileConfigProperty(key, value) {
664
+ const { groupId, filename } = parseConfigKey(key)
665
+
666
+ try {
667
+ // 强制刷新索引缓存
668
+ this.v3Storage.invalidateIndex(groupId)
669
+
670
+ // 从索引查找对应的 fileId
671
+ const index = await this.v3Storage.getIndex(groupId)
672
+ const entry = index.files.find(f => f.url === filename)
673
+
674
+ if (!entry) {
675
+ this.logger.error('_setFileConfigProperty: 文件不存在:', filename)
676
+ return
677
+ }
678
+
679
+ const file = await this.v3Storage.getFile(groupId, entry.id)
680
+
681
+ // 合并配置(保留原有配置,覆盖新值)
682
+ await this.v3Storage.updateFile(groupId, entry.id, {
683
+ config: {
684
+ ...file.config,
685
+ ...value,
686
+ },
687
+ })
688
+ } catch (err) {
689
+ this.logger.error('_setFileConfigProperty 失败:', err.message)
690
+ }
691
+ }
692
+
693
+ // ========================================
694
+ // 新增:直接访问 V3 Storage
695
+ // ========================================
696
+
697
+ /**
698
+ * 获取底层的 V3 Storage 实例
699
+ * 用于需要直接使用 V3 API 的场景(如版本管理、批量操作等)
700
+ *
701
+ * @returns {FileSystemStorage} V3 存储实例
702
+ *
703
+ * @example
704
+ * const v3Storage = adapter.getV3Storage()
705
+ * await v3Storage.createVersion(groupId, fileId, 'v1', {...})
706
+ */
707
+ getV3Storage() {
708
+ return this.v3Storage
709
+ }
710
+ }
711
+
712
+ module.exports = StorageAdapter