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
|
@@ -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
|
-
//
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
108
|
+
validateArray,
|
|
104
109
|
validate,
|
|
105
110
|
}
|