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,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件名: utils.js
|
|
3
|
+
* 功能: Whistle.mockbubu 工具函数集
|
|
4
|
+
* 依赖: logger.js, const.js, path-utils.js
|
|
5
|
+
* 更新: 2025-12-02
|
|
6
|
+
*
|
|
7
|
+
* 职责:
|
|
8
|
+
* - 文件操作函数(读取、更新、列表查询)
|
|
9
|
+
* - URL 和请求处理(文件名生成、规则构造)
|
|
10
|
+
* - 响应数据处理(解压缩、类型转换)
|
|
11
|
+
* - API 列表更新标志管理
|
|
12
|
+
*
|
|
13
|
+
* 架构特点:
|
|
14
|
+
* - V3 架构简化版本,移除废弃的版本管理函数
|
|
15
|
+
* - 使用 StorageAdapter 适配层,屏蔽底层存储细节
|
|
16
|
+
* - 支持多种压缩格式(gzip、deflate、brotli)
|
|
17
|
+
*
|
|
18
|
+
* 使用示例:
|
|
19
|
+
* ```javascript
|
|
20
|
+
* const { getFilename, readSession } = require('./utils')
|
|
21
|
+
*
|
|
22
|
+
* // 获取文件名
|
|
23
|
+
* const filename = getFilename(originalReq)
|
|
24
|
+
*
|
|
25
|
+
* // 读取 session 数据
|
|
26
|
+
* const session = await readSession(storage, filename, groupId)
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const zlib = require('zlib')
|
|
31
|
+
const { createLogger } = require('./logger')
|
|
32
|
+
const { RuleValueMap, SystemConstants } = require('../config/const')
|
|
33
|
+
const { buildPath } = require('./path-utils')
|
|
34
|
+
|
|
35
|
+
// 创建 logger 实例
|
|
36
|
+
const logger = createLogger('utils')
|
|
37
|
+
|
|
38
|
+
// ========================================
|
|
39
|
+
// 文件操作函数
|
|
40
|
+
// ========================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 按组获取文件列表
|
|
44
|
+
* 从所有文件中过滤出属于指定组的文件
|
|
45
|
+
*
|
|
46
|
+
* 工作流程:
|
|
47
|
+
* 1. 调用 storage.getFileList() 获取所有文件
|
|
48
|
+
* 2. 过滤出以 "{groupId}/" 开头的文件
|
|
49
|
+
* 3. 去除路径前缀,返回纯文件名列表
|
|
50
|
+
*
|
|
51
|
+
* 文件名格式:
|
|
52
|
+
* - 输入(storage):groupId/filename(例如:default/https://api.test.com/users)
|
|
53
|
+
* - 输出:filename(例如:https://api.test.com/users)
|
|
54
|
+
*
|
|
55
|
+
* @param {StorageAdapter} storage - 存储适配器实例
|
|
56
|
+
* @param {string} groupId - 组ID
|
|
57
|
+
* @returns {Promise<Array>} 文件列表,每个文件包含 name 和 size
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* const files = await getGroupFileList(storage, 'default')
|
|
61
|
+
* files.forEach(file => {
|
|
62
|
+
* console.log(file.name, file.size)
|
|
63
|
+
* })
|
|
64
|
+
* // 输出示例:
|
|
65
|
+
* // https://api.test.com/users 1024
|
|
66
|
+
* // https://api.test.com/posts 2048
|
|
67
|
+
*/
|
|
68
|
+
const getGroupFileList = async (storage, groupId) => {
|
|
69
|
+
// 参数校验
|
|
70
|
+
if (!groupId) {
|
|
71
|
+
logger.warn('getGroupFileList: groupId 为空,返回空数组')
|
|
72
|
+
return []
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// 1. 获取所有文件列表
|
|
77
|
+
// StorageAdapter 返回的格式: groupId/filename(不需要解码)
|
|
78
|
+
const allFiles = await storage.getFileList() || []
|
|
79
|
+
const groupPrefix = `${groupId}/`
|
|
80
|
+
const result = []
|
|
81
|
+
|
|
82
|
+
// 2. 过滤出属于该组的文件
|
|
83
|
+
allFiles.forEach((item) => {
|
|
84
|
+
try {
|
|
85
|
+
// StorageAdapter 返回的文件名格式: groupId/filename
|
|
86
|
+
// 例如: default/https://m.shein.com/us/bff-api/...
|
|
87
|
+
// 不需要任何解码,直接使用
|
|
88
|
+
const name = item.name
|
|
89
|
+
|
|
90
|
+
// 检查是否以 "groupId/" 开头
|
|
91
|
+
if (name.startsWith(groupPrefix)) {
|
|
92
|
+
// 3. 提取纯文件名(去除 groupId/ 前缀)
|
|
93
|
+
const filename = name.substring(groupPrefix.length)
|
|
94
|
+
|
|
95
|
+
result.push({
|
|
96
|
+
name: filename, // 返回纯文件名,不包含 groupId 前缀
|
|
97
|
+
size: item.size || 0, // 文件大小(字节)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
// 处理失败时跳过该文件,不中断整体流程
|
|
102
|
+
logger.warn(`处理文件失败: ${item.name}`, err.message)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return result
|
|
107
|
+
} catch (error) {
|
|
108
|
+
logger.error('getGroupFileList 执行失败:', error.message)
|
|
109
|
+
|
|
110
|
+
// 仅在开发环境输出堆栈
|
|
111
|
+
if (process.env.NODE_ENV === 'development') {
|
|
112
|
+
logger.error('错误堆栈:', error.stack)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [] // 失败时返回空数组,避免中断业务
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 更新文件的响应体
|
|
121
|
+
* 读取现有 session,更新 res.body 字段,然后保存
|
|
122
|
+
*
|
|
123
|
+
* 注意:
|
|
124
|
+
* - 仅更新响应体,不修改其他字段
|
|
125
|
+
* - 如果文件不存在,不会创建新文件
|
|
126
|
+
*
|
|
127
|
+
* @param {StorageAdapter} storage - 存储适配器实例
|
|
128
|
+
* @param {string} filename - 文件名(URL)
|
|
129
|
+
* @param {Object} body - 新的响应体内容(会被 JSON.stringify)
|
|
130
|
+
* @param {string} groupId - 组ID
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* await updateFile(storage, 'https://api.test.com/users', { data: [...] }, 'default')
|
|
135
|
+
*/
|
|
136
|
+
const updateFile = async (storage, filename, body, groupId) => {
|
|
137
|
+
if (!filename) {
|
|
138
|
+
throw new Error('文件名不能为空')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 构造文件路径:groupId/filename
|
|
142
|
+
const filePath = groupId ? buildPath(groupId, filename) : filename
|
|
143
|
+
|
|
144
|
+
// 读取现有 session 数据
|
|
145
|
+
const sourceDataStr = await storage.readFile(filePath)
|
|
146
|
+
if (sourceDataStr) {
|
|
147
|
+
const session = JSON.parse(sourceDataStr)
|
|
148
|
+
// 更新响应体(JSON 序列化)
|
|
149
|
+
session.res.body = JSON.stringify(body)
|
|
150
|
+
// 保存更新后的 session 数据
|
|
151
|
+
await storage.writeFile(filePath, JSON.stringify(session))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 读取文件的响应体
|
|
157
|
+
* 仅返回 res.body 字段,不返回完整 session
|
|
158
|
+
*
|
|
159
|
+
* @param {StorageAdapter} storage - 存储适配器实例
|
|
160
|
+
* @param {string} filename - 文件名(URL)
|
|
161
|
+
* @param {string} groupId - 组ID
|
|
162
|
+
* @returns {Promise<string|undefined>} 响应体内容,文件不存在返回 undefined
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* const body = await readFile(storage, 'https://api.test.com/users', 'default')
|
|
166
|
+
* console.log(body) // '{"data": [...]}'
|
|
167
|
+
*/
|
|
168
|
+
const readFile = async (storage, filename, groupId) => {
|
|
169
|
+
// 构造文件路径:groupId/filename
|
|
170
|
+
const filePath = groupId ? buildPath(groupId, filename) : filename
|
|
171
|
+
|
|
172
|
+
// 读取源数据 (storageAdapter.readFile 返回的是 session 对象的 JSON 字符串)
|
|
173
|
+
const sourceDataStr = await storage.readFile(filePath)
|
|
174
|
+
if (sourceDataStr) {
|
|
175
|
+
try {
|
|
176
|
+
// 解析后得到 session 对象 {req, res, ...}
|
|
177
|
+
const session = JSON.parse(sourceDataStr)
|
|
178
|
+
return session?.res?.body
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.error('[mockbubu] readFile parse error:', err.message)
|
|
181
|
+
return undefined
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 读取完整的 session 数据(包含 req 和 res)
|
|
188
|
+
* 返回完整的 session 对象,包括请求和响应信息
|
|
189
|
+
*
|
|
190
|
+
* Session 结构:
|
|
191
|
+
* ```javascript
|
|
192
|
+
* {
|
|
193
|
+
* req: {
|
|
194
|
+
* method: 'GET',
|
|
195
|
+
* url: 'https://api.test.com/users',
|
|
196
|
+
* headers: {...},
|
|
197
|
+
* body: '...'
|
|
198
|
+
* },
|
|
199
|
+
* res: {
|
|
200
|
+
* statusCode: 200,
|
|
201
|
+
* headers: {...},
|
|
202
|
+
* body: '{"data": [...]}'
|
|
203
|
+
* }
|
|
204
|
+
* }
|
|
205
|
+
* ```
|
|
206
|
+
*
|
|
207
|
+
* @param {StorageAdapter} storage - 存储适配器实例
|
|
208
|
+
* @param {string} filename - 文件名(URL)
|
|
209
|
+
* @param {string} groupId - 组ID
|
|
210
|
+
* @returns {Promise<Object|null>} session 对象,文件不存在返回 null
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* const session = await readSession(storage, 'https://api.test.com/users', 'default')
|
|
214
|
+
* if (session) {
|
|
215
|
+
* console.log('请求方法:', session.req.method)
|
|
216
|
+
* console.log('响应状态:', session.res.statusCode)
|
|
217
|
+
* console.log('响应内容:', session.res.body)
|
|
218
|
+
* }
|
|
219
|
+
*/
|
|
220
|
+
const readSession = async (storage, filename, groupId) => {
|
|
221
|
+
// 构造文件路径:groupId/filename
|
|
222
|
+
const filePath = groupId ? buildPath(groupId, filename) : filename
|
|
223
|
+
|
|
224
|
+
// 读取源数据 (storageAdapter.readFile 返回的就是 session 对象的 JSON 字符串)
|
|
225
|
+
const sourceDataStr = await storage.readFile(filePath)
|
|
226
|
+
if (sourceDataStr) {
|
|
227
|
+
return JSON.parse(sourceDataStr)
|
|
228
|
+
}
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ========================================
|
|
233
|
+
// URL 和请求处理函数
|
|
234
|
+
// ========================================
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 获取 Mock 文件名
|
|
238
|
+
* 根据 Whistle 规则类型(ruleValue)从 URL 生成文件名
|
|
239
|
+
*
|
|
240
|
+
* 规则类型:
|
|
241
|
+
* - **href**: 完整 URL(包含 query 参数)
|
|
242
|
+
* - **pathname**: origin + pathname(默认,不含 query)
|
|
243
|
+
* - **pattern**: 使用 Whistle pattern 作为文件名
|
|
244
|
+
*
|
|
245
|
+
* 示例:
|
|
246
|
+
* - href: `https://api.test.com/users?id=1`
|
|
247
|
+
* - pathname: `https://api.test.com/users`
|
|
248
|
+
* - pattern: `*.test.com`
|
|
249
|
+
*
|
|
250
|
+
* @param {Object} originalReq - Whistle 原始请求对象
|
|
251
|
+
* @param {string} originalReq.url - 完整 URL
|
|
252
|
+
* @param {string} originalReq.ruleValue - 规则类型(href/pathname/pattern)
|
|
253
|
+
* @param {string} originalReq.pattern - Whistle pattern(仅 pattern 模式使用)
|
|
254
|
+
* @returns {string} 文件名
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* const req = {
|
|
258
|
+
* url: 'https://api.test.com/users?id=1',
|
|
259
|
+
* ruleValue: 'pathname',
|
|
260
|
+
* pattern: '*.test.com'
|
|
261
|
+
* }
|
|
262
|
+
* const filename = getFilename(req)
|
|
263
|
+
* console.log(filename) // 'https://api.test.com/users'
|
|
264
|
+
*/
|
|
265
|
+
const getFilename = (originalReq) => {
|
|
266
|
+
const { url, ruleValue, pattern } = originalReq
|
|
267
|
+
const u = new URL(url)
|
|
268
|
+
let filename = url
|
|
269
|
+
|
|
270
|
+
// 根据规则类型生成文件名
|
|
271
|
+
switch (ruleValue) {
|
|
272
|
+
case RuleValueMap.href:
|
|
273
|
+
// 完整 URL(包含 query 参数)
|
|
274
|
+
filename = u.href
|
|
275
|
+
break
|
|
276
|
+
case RuleValueMap.pathname:
|
|
277
|
+
// origin + pathname(不含 query)
|
|
278
|
+
filename = u.origin + u.pathname
|
|
279
|
+
break
|
|
280
|
+
case RuleValueMap.pattern:
|
|
281
|
+
// 使用 Whistle pattern 作为文件名
|
|
282
|
+
filename = pattern
|
|
283
|
+
break
|
|
284
|
+
default:
|
|
285
|
+
// 默认使用 pathname 模式
|
|
286
|
+
filename = u.origin + u.pathname
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return filename
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 获取 API 列表更新标志
|
|
294
|
+
* 检查是否有新的 API 被捕获(需要刷新列表)
|
|
295
|
+
*
|
|
296
|
+
* @param {StorageAdapter} storage - 存储适配器实例
|
|
297
|
+
* @returns {Promise<boolean>} true 表示有更新,false 表示无更新
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* const updated = await getApiListUpdated(storage)
|
|
301
|
+
* if (updated) {
|
|
302
|
+
* console.log('API 列表有更新,需要刷新')
|
|
303
|
+
* }
|
|
304
|
+
*/
|
|
305
|
+
const getApiListUpdated = async (storage) => {
|
|
306
|
+
const value = await storage.getProperty(SystemConstants.API_LIST_UPDATED_KEY)
|
|
307
|
+
return !!value
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 设置 API 列表更新标志
|
|
312
|
+
* 当有新的 API 被捕获时,设置此标志为 true
|
|
313
|
+
*
|
|
314
|
+
* @param {StorageAdapter} storage - 存储适配器实例
|
|
315
|
+
* @param {boolean} updated - 是否有更新
|
|
316
|
+
* @returns {Promise<void>}
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* await setApiListUpdated(storage, true)
|
|
320
|
+
*/
|
|
321
|
+
const setApiListUpdated = async (storage, updated) => {
|
|
322
|
+
return await storage.setProperty(SystemConstants.API_LIST_UPDATED_KEY, updated)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 获取 Whistle 规则字符串
|
|
327
|
+
* 根据 pattern 和 ruleValue 构造完整的规则
|
|
328
|
+
*
|
|
329
|
+
* @param {Object} originalReq - Whistle 原始请求对象
|
|
330
|
+
* @param {string} originalReq.pattern - Whistle pattern
|
|
331
|
+
* @param {string} originalReq.ruleValue - 规则类型
|
|
332
|
+
* @returns {string} 规则字符串
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* const req = { pattern: '*.test.com', ruleValue: 'pathname' }
|
|
336
|
+
* const rule = getRule(req)
|
|
337
|
+
* console.log(rule) // '*.test.com mockbubu://pathname'
|
|
338
|
+
*/
|
|
339
|
+
const getRule = (originalReq) => {
|
|
340
|
+
const { pattern, ruleValue } = originalReq
|
|
341
|
+
return `${pattern} mockbubu://${ruleValue}`
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 判断是否为 JSON 请求
|
|
346
|
+
* 通过请求头判断请求是否期望 JSON 响应
|
|
347
|
+
*
|
|
348
|
+
* 判断依据:
|
|
349
|
+
* 1. `Accept` 头包含 `application/json`
|
|
350
|
+
* 2. `sec-fetch-dest` 或 `Sec-Fetch-Dest` 为 `empty`
|
|
351
|
+
*
|
|
352
|
+
* 注意:
|
|
353
|
+
* - `sec-fetch-dest: empty` 通常表示 fetch/XHR 请求
|
|
354
|
+
* - 用于决定是否捕获并 mock 该请求
|
|
355
|
+
*
|
|
356
|
+
* @param {Object} headers - 请求头对象
|
|
357
|
+
* @param {string} headers.accept - Accept 头
|
|
358
|
+
* @param {string} headers['sec-fetch-dest'] - Fetch Destination(小写)
|
|
359
|
+
* @param {string} headers['Sec-Fetch-Dest'] - Fetch Destination(大写)
|
|
360
|
+
* @returns {boolean} true 表示是 JSON 请求
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* const headers = {
|
|
364
|
+
* accept: 'application/json',
|
|
365
|
+
* 'sec-fetch-dest': 'empty'
|
|
366
|
+
* }
|
|
367
|
+
* const isJson = isJsonReq(headers)
|
|
368
|
+
* console.log(isJson) // true
|
|
369
|
+
*/
|
|
370
|
+
const isJsonReq = (headers) => {
|
|
371
|
+
const result = (
|
|
372
|
+
(headers.accept && headers.accept.includes('application/json')) ||
|
|
373
|
+
headers['sec-fetch-dest'] === 'empty' ||
|
|
374
|
+
headers['Sec-Fetch-Dest'] === 'empty'
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
// 调试日志(仅用于特定域名)
|
|
378
|
+
// 可通过环境变量或配置开关控制
|
|
379
|
+
if (process.env.DEBUG_HOST && headers.host === process.env.DEBUG_HOST) {
|
|
380
|
+
const fullUrl = headers['x-whistle-full-url'] || 'unknown'
|
|
381
|
+
logger.log(`[isJsonReq] ${headers.host} 请求: ${decodeURIComponent(fullUrl)}`)
|
|
382
|
+
logger.log(' - Accept:', headers.accept)
|
|
383
|
+
logger.log(' - sec-fetch-dest:', headers['sec-fetch-dest'])
|
|
384
|
+
logger.log(' - Sec-Fetch-Dest:', headers['Sec-Fetch-Dest'])
|
|
385
|
+
logger.log(' - 判断结果:', result)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return result
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ========================================
|
|
392
|
+
// 工具函数
|
|
393
|
+
// ========================================
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* 为异步函数添加 try-catch 包装
|
|
397
|
+
* 捕获异常并在开发环境输出日志,然后重新抛出
|
|
398
|
+
*
|
|
399
|
+
* 使用场景:
|
|
400
|
+
* - 包装可能抛出异常的函数
|
|
401
|
+
* - 在开发环境提供更好的错误日志
|
|
402
|
+
* - 不改变原函数的返回值和异常行为
|
|
403
|
+
*
|
|
404
|
+
* 注意:
|
|
405
|
+
* - 异常仍会被抛出,只是在抛出前记录日志
|
|
406
|
+
* - 仅在开发环境输出日志,生产环境静默
|
|
407
|
+
*
|
|
408
|
+
* @param {Function} fn - 要包装的异步函数
|
|
409
|
+
* @returns {Function} 包装后的函数
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* const safeReadFile = withTryCatch(async (filename) => {
|
|
413
|
+
* return await fs.readFile(filename, 'utf-8')
|
|
414
|
+
* })
|
|
415
|
+
*
|
|
416
|
+
* try {
|
|
417
|
+
* const content = await safeReadFile('test.json')
|
|
418
|
+
* } catch (err) {
|
|
419
|
+
* // 开发环境会先输出日志,再抛出异常
|
|
420
|
+
* console.error('读取失败:', err)
|
|
421
|
+
* }
|
|
422
|
+
*/
|
|
423
|
+
const withTryCatch = (fn) => {
|
|
424
|
+
return async (...args) => {
|
|
425
|
+
try {
|
|
426
|
+
return await fn(...args)
|
|
427
|
+
} catch (e) {
|
|
428
|
+
// 仅在开发环境输出错误日志
|
|
429
|
+
if (process.env.NODE_ENV === 'development') {
|
|
430
|
+
logger.error('全局捕获异常:', e)
|
|
431
|
+
}
|
|
432
|
+
// 重新抛出异常,不改变原函数行为
|
|
433
|
+
throw e
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ========================================
|
|
439
|
+
// 导出 - V3 架构使用的函数
|
|
440
|
+
// ========================================
|
|
441
|
+
|
|
442
|
+
// 文件操作
|
|
443
|
+
exports.getGroupFileList = getGroupFileList
|
|
444
|
+
exports.updateFile = updateFile
|
|
445
|
+
exports.readFile = readFile
|
|
446
|
+
exports.readSession = readSession
|
|
447
|
+
|
|
448
|
+
// URL 和请求处理
|
|
449
|
+
exports.getFilename = getFilename
|
|
450
|
+
exports.getRule = getRule
|
|
451
|
+
exports.isJsonReq = isJsonReq
|
|
452
|
+
|
|
453
|
+
// API 列表更新标志
|
|
454
|
+
exports.getApiListUpdated = getApiListUpdated
|
|
455
|
+
exports.setApiListUpdated = setApiListUpdated
|
|
456
|
+
|
|
457
|
+
// 工具函数
|
|
458
|
+
exports.withTryCatch = withTryCatch
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* 将响应 Buffer 解压并转换为字符串
|
|
462
|
+
* 支持多种压缩格式和数据类型
|
|
463
|
+
*
|
|
464
|
+
* 支持的压缩格式:
|
|
465
|
+
* - **gzip**: 使用 zlib.gunzipSync() 解压
|
|
466
|
+
* - **deflate**: 使用 zlib.inflateSync() 解压
|
|
467
|
+
* - **br** (brotli): 使用 zlib.brotliDecompressSync() 解压
|
|
468
|
+
*
|
|
469
|
+
* 支持的数据类型:
|
|
470
|
+
* - **String**: 直接返回
|
|
471
|
+
* - **Object**: JSON.stringify() 序列化
|
|
472
|
+
* - **Uint8Array**: toString() 转换
|
|
473
|
+
* - **其他**: 原样返回
|
|
474
|
+
*
|
|
475
|
+
* 工作流程:
|
|
476
|
+
* 1. 根据 encoding 解压缩 body
|
|
477
|
+
* 2. 根据数据类型转换为字符串
|
|
478
|
+
* 3. 返回字符串内容
|
|
479
|
+
*
|
|
480
|
+
* @param {Object} params - 参数对象
|
|
481
|
+
* @param {Buffer|string|Object|Uint8Array} params.body - 响应体数据
|
|
482
|
+
* @param {string} params.encoding - 压缩编码类型(gzip/deflate/br)
|
|
483
|
+
* @returns {string} 转换后的字符串内容
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* // 解压 gzip 数据
|
|
487
|
+
* const result = handleBuffer2String({
|
|
488
|
+
* body: gzipBuffer,
|
|
489
|
+
* encoding: 'gzip'
|
|
490
|
+
* })
|
|
491
|
+
* console.log(result) // '{"data": [...]}'
|
|
492
|
+
*
|
|
493
|
+
* @example
|
|
494
|
+
* // 处理 JSON 对象
|
|
495
|
+
* const result = handleBuffer2String({
|
|
496
|
+
* body: { data: [1, 2, 3] },
|
|
497
|
+
* encoding: undefined
|
|
498
|
+
* })
|
|
499
|
+
* console.log(result) // '{"data":[1,2,3]}'
|
|
500
|
+
*/
|
|
501
|
+
exports.handleBuffer2String = withTryCatch(({ body, encoding }) => {
|
|
502
|
+
// 1. 根据编码类型解压缩
|
|
503
|
+
switch (encoding) {
|
|
504
|
+
case 'gzip':
|
|
505
|
+
body = zlib.gunzipSync(body)
|
|
506
|
+
break
|
|
507
|
+
case 'deflate':
|
|
508
|
+
body = zlib.inflateSync(body)
|
|
509
|
+
break
|
|
510
|
+
case 'br':
|
|
511
|
+
body = zlib.brotliDecompressSync(body)
|
|
512
|
+
break
|
|
513
|
+
default:
|
|
514
|
+
// 无压缩或未知编码,保持原样
|
|
515
|
+
break
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// 2. 根据数据类型转换为字符串
|
|
519
|
+
const type = Object.prototype.toString.call(body)
|
|
520
|
+
let content
|
|
521
|
+
|
|
522
|
+
switch (type) {
|
|
523
|
+
case '[object String]':
|
|
524
|
+
// 已经是字符串,直接返回
|
|
525
|
+
content = body
|
|
526
|
+
break
|
|
527
|
+
case '[object Object]':
|
|
528
|
+
// 对象类型,JSON 序列化
|
|
529
|
+
content = JSON.stringify(body)
|
|
530
|
+
break
|
|
531
|
+
case '[object Uint8Array]':
|
|
532
|
+
// Uint8Array 类型,转换为字符串
|
|
533
|
+
content = body.toString()
|
|
534
|
+
break
|
|
535
|
+
default:
|
|
536
|
+
// 其他类型,原样返回
|
|
537
|
+
content = body
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return content
|
|
541
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whistle.mockbubu",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "mock response data",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"lint": "eslint . --ext .js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
".gitignore",
|
|
26
26
|
"index.js",
|
|
27
27
|
"package.json",
|
|
28
|
-
"
|
|
28
|
+
"README.md",
|
|
29
29
|
"step1.png",
|
|
30
30
|
"step2.png"
|
|
31
31
|
],
|