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,100 @@
1
+ /**
2
+ * 文件名: router-helpers.js
3
+ * 功能: 路由公共辅助函数(减少子路由中的重复逻辑)
4
+ * 依赖: logger.js, util.js
5
+ */
6
+
7
+ const { createLogger } = require('../../utils/logger')
8
+ const { createErrorResponse } = require('./util')
9
+
10
+ const logger = createLogger()
11
+
12
+ /**
13
+ * 获取当前组的V3 Storage和索引
14
+ *
15
+ * @param {Object} ctx - Koa上下文
16
+ * @returns {Promise<{currentGroupId: string, v3Storage: Object, fileIndexData: Object}>}
17
+ */
18
+ async function getCurrentGroupContext(ctx) {
19
+ const { groupManager, storageAdapter } = ctx
20
+ const currentGroupId = await groupManager.getCurrentGroupId()
21
+ const v3Storage = storageAdapter.v3Storage
22
+ const fileIndexData = await v3Storage.getIndex(currentGroupId)
23
+
24
+ return { currentGroupId, v3Storage, fileIndexData }
25
+ }
26
+
27
+ /**
28
+ * 通过URL查找文件条目
29
+ *
30
+ * @param {Object} fileIndexData - 文件索引数据对象(包含files数组)
31
+ * @param {string} url - 文件URL
32
+ * @returns {Object} 文件条目
33
+ * @throws {Error} 文件不存在
34
+ */
35
+ function findFileByUrl(fileIndexData, url) {
36
+ const fileEntry = fileIndexData.files.find(f => f.url === url)
37
+ if (!fileEntry) {
38
+ throw new Error('文件不存在')
39
+ }
40
+ return fileEntry
41
+ }
42
+
43
+ /**
44
+ * 获取文件ID(通过URL查找)
45
+ *
46
+ * @param {Object} ctx - Koa上下文
47
+ * @param {string} url - 文件URL
48
+ * @returns {Promise<{fileId: string, currentGroupId: string, v3Storage: Object}>}
49
+ */
50
+ async function getFileIdByUrl(ctx, url) {
51
+ const { currentGroupId, v3Storage, fileIndexData } = await getCurrentGroupContext(ctx)
52
+ const fileEntry = findFileByUrl(fileIndexData, url)
53
+
54
+ return {
55
+ fileId: fileEntry.id,
56
+ currentGroupId,
57
+ v3Storage,
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 路由处理器包装器(统一错误处理)
63
+ *
64
+ * @param {Function} handler - 异步路由处理函数
65
+ * @param {string} operation - 操作名称(用于日志)
66
+ * @returns {Function} 包装后的处理器
67
+ */
68
+ function wrapRouteHandler(handler, operation = '操作') {
69
+ return async (ctx) => {
70
+ try {
71
+ await handler(ctx)
72
+ } catch (error) {
73
+ logger.error(`${operation}失败:`, error.message)
74
+ ctx.body = createErrorResponse(error.message)
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 验证必需参数
81
+ *
82
+ * @param {Object} params - 参数对象
83
+ * @param {string[]} requiredFields - 必需字段名称数组
84
+ * @throws {Error} 缺少必需参数
85
+ */
86
+ function validateRequiredParams(params, requiredFields) {
87
+ for (const field of requiredFields) {
88
+ if (!params[field]) {
89
+ throw new Error(`缺少必需参数: ${field}`)
90
+ }
91
+ }
92
+ }
93
+
94
+ module.exports = {
95
+ getCurrentGroupContext,
96
+ findFileByUrl,
97
+ getFileIdByUrl,
98
+ wrapRouteHandler,
99
+ validateRequiredParams,
100
+ }
@@ -0,0 +1,172 @@
1
+ const {
2
+ getGroupFileList,
3
+ } = require('../../utils/utils')
4
+ const { RangeFilterMap, RuleFilterMap, LockedFilterMap } = require('../../config/const')
5
+
6
+ // 统一错误响应格式
7
+ const createErrorResponse = (message, code = 500) => ({
8
+ code,
9
+ msg: message || '操作失败',
10
+ })
11
+
12
+ // 统一成功响应格式
13
+ const createSuccessResponse = (data = null, message = '操作成功') => ({
14
+ code: 200,
15
+ msg: message,
16
+ ...(data !== null && data !== undefined && { data }),
17
+ })
18
+
19
+ // 执行筛选
20
+ const execFilter = (result, filter) => {
21
+ const { key, value, method } = filter
22
+ const list = result.filter((item) => {
23
+ const type = Object.prototype.toString.call(value)
24
+
25
+ switch (method) {
26
+ case 'indexOf':
27
+ if (type === '[object String]') {
28
+ return ~item[key].indexOf(value)
29
+ }
30
+ break
31
+ case 'equal':
32
+ return item[key] === value
33
+ case 'unequal':
34
+ return item[key] !== value
35
+ case 'range':
36
+ // 范围筛选,value 是 [min, max] 数组
37
+ if (Array.isArray(value) && value.length === 2) {
38
+ const itemValue = item[key]
39
+ return itemValue >= value[0] && itemValue <= value[1]
40
+ }
41
+ break
42
+ }
43
+
44
+ return true
45
+ })
46
+
47
+ return list
48
+ }
49
+
50
+ // 处理筛选条件
51
+ const handleFilterList = (list, filterOptions) => {
52
+ if (!Array.isArray(list) || !filterOptions) return []
53
+
54
+ const filters = []
55
+ const { name, rule, range, ruleValue, locked } = filterOptions
56
+ if (name) {
57
+ filters.push({
58
+ key: 'name',
59
+ value: name.trim(),
60
+ method: 'indexOf',
61
+ })
62
+ }
63
+
64
+ if (rule) {
65
+ filters.push({
66
+ key: 'rule',
67
+ value: rule.trim(),
68
+ method: 'equal',
69
+ })
70
+ }
71
+
72
+ // 范围筛选
73
+ RangeFilterMap[range] && filters.push(RangeFilterMap[range])
74
+ // 规则值筛选
75
+ RuleFilterMap[ruleValue] && filters.push(RuleFilterMap[ruleValue])
76
+ // 锁定筛选
77
+ LockedFilterMap[locked] && filters.push(LockedFilterMap[locked])
78
+
79
+ // 执行筛选
80
+ const filteredList = filters.reduce((result, filter) => {
81
+ return execFilter(result, filter)
82
+ }, list)
83
+
84
+ return filteredList
85
+ }
86
+
87
+ /**
88
+ * 过滤文件配置,只保留文件级别的字段
89
+ * 移除全局元数据字段 (query, payload, method, rule, status, pattern, ruleValue, url, date)
90
+ * 移除版本字段 (version.*, versionMeta.*) - 版本通过单独的 versions 数组导出
91
+ */
92
+ const filterFileConfig = (fileConfig) => {
93
+ if (!fileConfig) return {}
94
+ // 保留基础配置 + 元数据字段
95
+ // 元数据字段(url, method, date, status等)对于文件列表显示是必需的
96
+ const validKeys = ['mock', 'locked', 'mockVersion', 'mockTime', 'url', 'method', 'date', 'status', 'rule', 'ruleValue']
97
+ const filtered = {}
98
+
99
+ Object.keys(fileConfig).forEach(key => {
100
+ // 仅保留基础配置字段和元数据字段
101
+ if (validKeys.includes(key)) {
102
+ filtered[key] = fileConfig[key]
103
+ }
104
+ // 版本字段通过 versions 数组单独导出,不在 fileConfig 中保留
105
+ })
106
+
107
+ return filtered
108
+ }
109
+
110
+ // 获取文件列表(包含完整 session 数据)
111
+ // 完全隔离架构:直接获取当前组的文件列表
112
+ const getFullDataList = async (storageAdapter, groupManager, groupId) => {
113
+ // 获取组的物理文件列表并合并配置
114
+ const list = await getGroupFileList(storageAdapter, groupId) || []
115
+ const result = []
116
+ const v3Storage = storageAdapter.getV3Storage()
117
+
118
+ // ⚠️ 强制刷新索引缓存(只刷新一次)
119
+ v3Storage.invalidateIndex(groupId)
120
+
121
+ // 获取索引以查找真实的 fileId(只读取一次)
122
+ const fileIndexData = await v3Storage.getIndex(groupId)
123
+
124
+ // ✅ 读取完整数据(包含 session),支持 Payload/Headers 视图
125
+ for (let idx = 0; idx < list.length; idx++) {
126
+ const item = list[idx]
127
+
128
+ // 从索引查找对应的文件条目
129
+ const indexEntry = fileIndexData.files.find(f => f.url === item.name)
130
+
131
+ if (indexEntry) {
132
+ const fileId = indexEntry.id
133
+
134
+ // 读取 file.json (包含 session 数据和配置)
135
+ const fileData = await v3Storage.getFile(groupId, fileId)
136
+
137
+ // 返回完整数据(包含 config 配置字段 + session 数据)
138
+ result.push({
139
+ name: item.name,
140
+ id: fileId,
141
+ url: indexEntry.url,
142
+ method: indexEntry.method,
143
+ status: indexEntry.status,
144
+ // ✅ 从 fileData.config 读取配置(而非 indexEntry)
145
+ mock: fileData.config?.mock || false,
146
+ locked: fileData.config?.locked || false,
147
+ mockVersion: fileData.config?.mockVersion || null,
148
+ mockTime: fileData.config?.mockTime || null,
149
+ date: indexEntry.createTime,
150
+ rule: indexEntry.rule || '',
151
+ ruleValue: indexEntry.ruleValue || 'pathname',
152
+ pattern: indexEntry.pattern || '',
153
+ domain: indexEntry.domain || null,
154
+ pathname: indexEntry.pathname || null,
155
+ urlHash: indexEntry.urlHash || null,
156
+ // ✅ 添加 captureTime 字段(与 api-list 保持一致)
157
+ captureTime: indexEntry.createTime,
158
+ // ✅ 直接从 file.json 读取 session 数据
159
+ session: fileData.session || null,
160
+ })
161
+ }
162
+ }
163
+
164
+ return result
165
+ }
166
+
167
+ exports.createErrorResponse = createErrorResponse
168
+ exports.createSuccessResponse = createSuccessResponse
169
+ exports.execFilter = execFilter
170
+ exports.handleFilterList = handleFilterList
171
+ exports.filterFileConfig = filterFileConfig
172
+ exports.getFullDataList = getFullDataList
@@ -57,11 +57,16 @@ const validateBoolean = (value, fieldName) => {
57
57
  return null
58
58
  }
59
59
 
60
- // 验证 ruleValue
61
- const validateRuleValue = (ruleValue) => {
62
- const validValues = ['pathname', 'href', 'pattern']
63
- if (!ruleValue || !validValues.includes(ruleValue)) {
64
- return `ruleValue 必须是 ${validValues.join(', ')} 之一`
60
+ // 验证数组
61
+ const validateArray = (value, fieldName = 'urls') => {
62
+ if (!Array.isArray(value)) {
63
+ return `${fieldName} 必须是数组`
64
+ }
65
+ if (value.length === 0) {
66
+ return `${fieldName} 不能为空数组`
67
+ }
68
+ if (value.length > 1000) {
69
+ return `${fieldName} 长度不能超过 1000`
65
70
  }
66
71
  return null
67
72
  }
@@ -100,6 +105,6 @@ module.exports = {
100
105
  validateVersionName,
101
106
  validateMockData,
102
107
  validateBoolean,
103
- validateRuleValue,
108
+ validateArray,
104
109
  validate,
105
110
  }