whistle.mockbubu 2.2.7 → 2.2.9-beta.1

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.
@@ -1,298 +1,18 @@
1
1
  /**
2
2
  * 文件名: response-handler.js
3
- * 功能: Mock 响应返回逻辑
3
+ * 功能: Mock 响应返回逻辑(薄封装,实现已抽取到 shared)
4
4
  * 职责:
5
- * - 返回 Mock 数据 (源数据或版本数据)
6
- * - 条件匹配模式下匹配规则并返回对应数据
7
- * - 处理版本切换
8
- * - 处理响应头
9
- */
10
-
11
- const { MATCH_MODE_VALUE } = require('../../config/const')
12
-
13
- /**
14
- * 返回 Mock 响应数据
15
- * @param {Object} res - Whistle 响应对象
16
- * @param {string} sourceData - 源数据 (file.json 的字符串内容)
17
- * @param {Object} options - 配置选项
18
- * @param {string} options.mockVersion - Mock 版本名称 (null 表示使用源数据)
19
- * @param {Object} options.groupManager - 组管理器实例
20
- * @param {string} options.currentGroupId - 当前组ID
21
- * @param {string} options.filename - 文件名
22
- * @param {Object} options.originalReq - Whistle 原始请求对象(用于条件匹配)
23
- */
24
- async function sendMockResponse(res, sourceData, options = {}) {
25
- const { mockVersion, groupManager, currentGroupId, filename, originalReq } = options
26
-
27
- // 解析源数据 (storageAdapter.readFile 返回的是 session 对象的 JSON 字符串)
28
- const session = JSON.parse(sourceData)
29
- const sourceResponse = session?.res
30
- const { statusCode, statusMessage } = sourceResponse
31
-
32
- // 浅拷贝 headers,避免直接修改 session 对象(若未来引入缓存会产生副作用)
33
- const headers = { ...sourceResponse.headers }
34
- headers['from-mockbubu-source'] = 'true'
35
- headers['x-mockbubu-mocked'] = 'true'
36
- delete headers['content-encoding']
37
- delete headers['content-length']
38
-
39
- // 【条件匹配模式】
40
- if (mockVersion === MATCH_MODE_VALUE) {
41
- const matchResult = await matchByConditions({
42
- groupManager,
43
- currentGroupId,
44
- filename,
45
- originalReq,
46
- })
47
-
48
- headers['x-mockbubu-match-mode'] = 'match'
49
-
50
- if (matchResult.matched) {
51
- // header 值必须为 ASCII,对中文规则名做 encodeURIComponent 编码
52
- headers['x-mockbubu-match-rule'] = encodeURIComponent(matchResult.ruleName || '')
53
- headers['x-mockbubu-match-params'] = encodeURIComponent(matchResult.paramsSummary || '')
54
- res.writeHead(statusCode, statusMessage, headers)
55
- res.end(JSON.stringify(matchResult.content ?? {}))
56
- return
57
- }
58
-
59
- // 全不命中 → 通知调用方透传请求,不返回 mock 数据
60
- return { action: 'passthrough' }
61
- }
62
-
63
- // 【版本直选模式】先确认版本数据是否存在,再统一调用 writeHead
64
- if (mockVersion && groupManager && currentGroupId && filename) {
65
- const versionData = await groupManager.getGroupVersionContent(currentGroupId, filename, mockVersion)
66
- if (versionData) {
67
- res.writeHead(statusCode, statusMessage, headers)
68
- res.end(JSON.stringify(versionData))
69
- return
70
- }
71
- // 版本不存在时降级到源数据
72
- }
73
-
74
- res.writeHead(statusCode, statusMessage, headers)
75
- res.end(sourceResponse.body)
76
- }
77
-
78
- /**
79
- * 条件匹配核心逻辑
80
- *
81
- * 匹配规则:
82
- * 1. 按条件数量降序(条件越多优先级越高)
83
- * 2. 条件数量相同时按 priority 字段升序(数字越小越优先)
84
- * 3. 空条件规则作为兜底,最后匹配
85
- * 4. 全不命中时返回 matched: false,由调用方降级到 source
5
+ * - 保留 checkShouldCapture(仅 server 钩子使用)
6
+ * - sendMockResponse / matchByConditions / handleConcurrentCapture 重新导出自 shared
86
7
  *
87
- * @param {Object} params
88
- * @param {Object} params.groupManager - 组管理器
89
- * @param {string} params.currentGroupId - 当前组ID
90
- * @param {string} params.filename - 文件名(URL)
91
- * @param {Object} params.originalReq - Whistle 原始请求对象
92
- * @returns {{ matched: boolean, content?, ruleName?, paramsSummary? }}
8
+ * 实现位置:
9
+ * - lib/core/shared/mock-response.js: sendMockResponse + matchByConditions + 辅助函数
10
+ * - lib/core/shared/concurrent.js: handleConcurrentCapture
93
11
  */
94
- async function matchByConditions(params) {
95
- const { groupManager, currentGroupId, filename, originalReq } = params
96
12
 
97
- // 获取所有匹配规则(storage 层已按 priority 排序)
98
- const matchVersions = await groupManager.getMatchVersions(currentGroupId, filename)
99
- if (!matchVersions || matchVersions.length === 0) {
100
- return { matched: false }
101
- }
102
-
103
- // 提取当前请求参数
104
- const queryParams = extractQueryParams(originalReq)
105
- const payloadParams = extractPayloadParams(originalReq)
106
-
107
- // 按条件数量降序 + priority 升序排序(条件越多越具体,优先匹配)
108
- // storage 层已按 priority 排序,这里再按 conditions.length 降序叠加
109
- const sorted = [...matchVersions].sort((a, b) => {
110
- const condDiff = (b.conditions?.length || 0) - (a.conditions?.length || 0)
111
- if (condDiff !== 0) return condDiff
112
- return (a.priority || 0) - (b.priority || 0)
113
- })
114
-
115
- let fallbackRule = null
116
-
117
- // 逐条匹配(AND 关系:所有条件必须同时满足)
118
- for (const rule of sorted) {
119
- if (!rule.conditions || rule.conditions.length === 0) {
120
- // 空条件规则作为兜底,记录备用
121
- if (!fallbackRule) fallbackRule = rule
122
- continue
123
- }
124
-
125
- const allMatch = rule.conditions.every((cond) => {
126
- const sourceParams = cond.source === 'query' ? queryParams : payloadParams
127
- return matchCondition(sourceParams, cond)
128
- })
129
-
130
- if (allMatch) {
131
- return {
132
- matched: true,
133
- content: rule.content ?? {},
134
- ruleName: rule.name,
135
- paramsSummary: rule.conditions
136
- .map(c => `${c.source}.${c.key}[${c.operator || 'eq'}]${c.value ?? ''}`)
137
- .join(', '),
138
- }
139
- }
140
- }
141
-
142
- // 尝试兜底规则(空条件规则)
143
- if (fallbackRule) {
144
- return {
145
- matched: true,
146
- content: fallbackRule.content ?? {},
147
- ruleName: fallbackRule.name,
148
- paramsSummary: '(default)',
149
- }
150
- }
151
-
152
- return { matched: false }
153
- }
154
-
155
- /**
156
- * 单条件匹配:根据 operator 执行对应的比较
157
- * operator 缺省时向后兼容旧数据(等同 eq)
158
- *
159
- * @param {Object} sourceParams - 请求参数 key-value 对
160
- * @param {{ key: string, operator?: string, value?: string }} cond
161
- * @returns {boolean}
162
- */
163
- function matchCondition(sourceParams, cond) {
164
- const { key, value = '' } = cond
165
- const operator = cond.operator || 'eq'
166
-
167
- // 条件不完整时跳过(视为满足),避免空值产生意外的匹配行为
168
- if (!key) return true
169
- const VALUELESS_OPS = new Set(['exists', 'not_exists'])
170
- if (!VALUELESS_OPS.has(operator) && !value) return true
171
-
172
- const exists = Object.prototype.hasOwnProperty.call(sourceParams, key)
173
- const actual = exists ? String(sourceParams[key]) : undefined
174
- const expected = String(value)
175
-
176
- switch (operator) {
177
- case 'eq':
178
- return actual === expected
179
- case 'neq':
180
- return actual !== expected
181
- case 'contains':
182
- return actual !== undefined && actual.includes(expected)
183
- case 'regex': {
184
- if (!actual) return false
185
- try {
186
- return new RegExp(expected).test(actual)
187
- } catch {
188
- return false
189
- }
190
- }
191
- case 'exists':
192
- return exists
193
- case 'not_exists':
194
- return !exists
195
- default:
196
- return actual === expected
197
- }
198
- }
199
-
200
- /**
201
- * 从原始请求中提取 query 参数
202
- * 来源:req.originalReq.url,fallback 到 x-whistle-full-url header
203
- *
204
- * @param {Object} originalReq - Whistle 原始请求对象
205
- * @returns {Object} query 参数 key-value 对
206
- */
207
- function extractQueryParams(originalReq) {
208
- if (!originalReq) return {}
209
-
210
- try {
211
- // 某些 Whistle 代理场景(tunnel/HTTP2)下 originalReq.url 可能不含 query,
212
- // 完整 URL 会在 x-whistle-full-url header 里(URL 编码格式)
213
- const fullUrlHeader = originalReq.headers?.['x-whistle-full-url']
214
- const rawUrl = originalReq.url ||
215
- (fullUrlHeader ? decodeURIComponent(fullUrlHeader) : '') ||
216
- ''
217
- const url = new URL(rawUrl)
218
- const params = {}
219
- url.searchParams.forEach((value, key) => {
220
- params[key] = value
221
- })
222
- return params
223
- } catch {
224
- return {}
225
- }
226
- }
227
-
228
- /**
229
- * 从原始请求中提取 payload 参数(支持 JSON 和 x-www-form-urlencoded,只提取第一层 key-value)
230
- * 来源:req.originalReq.body
231
- *
232
- * @param {Object} originalReq - Whistle 原始请求对象
233
- * @returns {Object} payload 参数 key-value 对(值统一转为字符串)
234
- */
235
- function extractPayloadParams(originalReq) {
236
- if (!originalReq) return {}
237
-
238
- try {
239
- // 优先使用 mock 路径预缓冲的请求体,fallback 到 Whistle 预解析的 body
240
- let body = originalReq._reqBody || originalReq.body || ''
241
-
242
- // Buffer 转字符串
243
- if (Buffer.isBuffer(body)) {
244
- body = body.toString('utf8')
245
- }
246
-
247
- // body 可能已被 Whistle 预解析为对象,直接提取
248
- if (typeof body === 'object' && body !== null) {
249
- return extractFlatParams(body)
250
- }
251
-
252
- if (typeof body !== 'string' || !body.trim()) {
253
- return {}
254
- }
255
-
256
- // 尝试 JSON 格式
257
- try {
258
- const parsed = JSON.parse(body)
259
- if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
260
- return extractFlatParams(parsed)
261
- }
262
- } catch {
263
- // 不是 JSON,继续尝试
264
- }
265
-
266
- // 尝试 application/x-www-form-urlencoded 格式
267
- if (body.includes('=')) {
268
- const result = {}
269
- new URLSearchParams(body).forEach((value, key) => {
270
- result[key] = value
271
- })
272
- if (Object.keys(result).length > 0) return result
273
- }
274
-
275
- return {}
276
- } catch {
277
- return {}
278
- }
279
- }
280
-
281
- /**
282
- * 从对象中提取第一层 string/number/boolean 类型的 key-value(统一转为字符串)
283
- * @param {Object} obj
284
- * @returns {Object}
285
- */
286
- function extractFlatParams(obj) {
287
- const params = {}
288
- Object.keys(obj).forEach((key) => {
289
- const val = obj[key]
290
- if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
291
- params[key] = String(val)
292
- }
293
- })
294
- return params
295
- }
13
+ // 重新导出共用实现(保持 server.js 调用链不变)
14
+ const { sendMockResponse } = require('../shared/mock-response')
15
+ const { handleConcurrentCapture } = require('../shared/concurrent')
296
16
 
297
17
  /**
298
18
  * 检查是否需要捕获源数据
@@ -329,65 +49,6 @@ async function checkShouldCapture(params) {
329
49
  }
330
50
  }
331
51
 
332
- /**
333
- * 处理并发捕获时的等待逻辑
334
- * @param {Map} capturingFiles - 捕获中的文件 Map
335
- * @param {string} captureKey - 捕获 key
336
- * @param {Object} params - 参数对象
337
- * @returns {Promise<Object>} { handled, response }
338
- */
339
- async function handleConcurrentCapture(capturingFiles, captureKey, params) {
340
- const {
341
- res,
342
- storageAdapter,
343
- groupManager,
344
- currentGroupId,
345
- filename,
346
- mockVersion,
347
- filePath,
348
- originalReq,
349
- } = params
350
-
351
- if (!capturingFiles.has(captureKey)) {
352
- return { handled: false }
353
- }
354
-
355
- // 正在捕获源数据中,等待捕获完成
356
- console.log(`[mockbubu] ⏳ 检测到并发请求,等待源数据捕获完成: ${filename}`)
357
-
358
- try {
359
- // 等待第一个请求捕获完成(最多等待10秒)
360
- await Promise.race([
361
- capturingFiles.get(captureKey),
362
- new Promise((_resolve, reject) => setTimeout(() => reject(new Error('捕获超时')), 10000)),
363
- ])
364
-
365
- // 捕获完成,重新读取源数据
366
- const freshSourceData = await storageAdapter.readFile(filePath)
367
- if (freshSourceData) {
368
- console.log(`[mockbubu] ✅ 等待成功,返回捕获的源数据: ${filename}`)
369
-
370
- await sendMockResponse(res, freshSourceData, {
371
- mockVersion,
372
- groupManager,
373
- currentGroupId,
374
- filename,
375
- originalReq,
376
- })
377
-
378
- return { handled: true, response: 'success' }
379
- } else {
380
- // 等待后仍无源数据,降级到透传
381
- console.warn(`[mockbubu] ⚠️ 等待后仍无源数据,降级透传: ${filename}`)
382
- return { handled: true, response: 'passthrough' }
383
- }
384
- } catch (err) {
385
- // 等待超时或失败,降级到透传
386
- console.error(`[mockbubu] ❌ 等待捕获失败,降级透传: ${filename}`, err.message)
387
- return { handled: true, response: 'passthrough' }
388
- }
389
- }
390
-
391
52
  module.exports = {
392
53
  sendMockResponse,
393
54
  checkShouldCapture,
@@ -22,19 +22,7 @@ const {
22
22
  captureResponse,
23
23
  } = require('./capture-handler')
24
24
  const pluginModeManager = require('../plugin-mode-manager')
25
-
26
- /**
27
- * 从 req 流缓冲请求体,供条件匹配使用
28
- * 仅在 mock 路径调用,此时 req 流未被 pipe,可安全读取
29
- */
30
- function bufferReqBody(req) {
31
- return new Promise((resolve) => {
32
- const chunks = []
33
- req.on('data', chunk => chunks.push(chunk))
34
- req.on('end', () => resolve(Buffer.concat(chunks)))
35
- req.on('error', () => resolve(Buffer.alloc(0)))
36
- })
37
- }
25
+ const { bufferReqBody } = require('../shared/buffer')
38
26
 
39
27
  module.exports = (server, options) => {
40
28
  console.log('[mockbubu] 🚀 server.js 模块已加载!')
@@ -69,6 +57,10 @@ module.exports = (server, options) => {
69
57
  await new Promise(resolve => setTimeout(resolve, 100))
70
58
  }
71
59
 
60
+ // 【互斥标志】标记此请求已由 server 钩子处理
61
+ // resWrite 钩子见此标志直接 passThrough,不重复处理
62
+ req._mockbubuServerHandled = true
63
+
72
64
  // 判断是否需要拦截该请求(使用模式管理器获取当前模式)
73
65
  const interceptResult = shouldInterceptRequest(req, storage, pluginModeManager.getMode())
74
66
 
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 文件名: buffer.js
3
+ * 功能: 请求体缓冲工具(共用模块)
4
+ * 职责:
5
+ * - 从 req 流缓冲请求体,供条件匹配使用
6
+ * - 仅在 mock 路径调用,此时 req 流未被 pipe,可安全读取
7
+ */
8
+
9
+ /**
10
+ * 从 req 流缓冲请求体
11
+ * @param {Object} req - Whistle 请求对象
12
+ * @returns {Promise<Buffer>} 请求体 Buffer
13
+ */
14
+ function bufferReqBody(req) {
15
+ return new Promise((resolve) => {
16
+ const chunks = []
17
+ req.on('data', chunk => chunks.push(chunk))
18
+ req.on('end', () => resolve(Buffer.concat(chunks)))
19
+ req.on('error', () => resolve(Buffer.alloc(0)))
20
+ })
21
+ }
22
+
23
+ module.exports = { bufferReqBody }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * 文件名: capture.js
3
+ * 功能: 源数据捕获逻辑(共用模块)
4
+ * 职责:
5
+ * - 捕获真实 API 响应数据
6
+ * - 处理响应体解压缩
7
+ * - 保存源数据到存储 (file.json)
8
+ * - 更新文件配置
9
+ * - 并发控制
10
+ *
11
+ * 从 lib/core/server-entry/capture-handler.js 抽取,供 server / resWrite 钩子共用
12
+ *
13
+ * 注意:
14
+ * - captureResponse 用于「server 钩子接管请求」场景:主动 req.request 发起上游请求
15
+ * - resWrite 钩子「不接管请求」场景应使用 captureAndPassthrough(见 res-write/response-rewriter.js)
16
+ */
17
+
18
+ const {
19
+ setApiListUpdated,
20
+ handleBuffer2String,
21
+ withTryCatch,
22
+ } = require('../../utils/utils')
23
+
24
+ /**
25
+ * 捕获真实 API 响应数据
26
+ *
27
+ * @param {Object} params - 参数对象
28
+ * @param {Object} params.req - Whistle 请求对象
29
+ * @param {Object} params.res - Whistle 响应对象
30
+ * @param {Object} params.storageAdapter - 存储适配器
31
+ * @param {Object} params.groupManager - 组管理器
32
+ * @param {Object} params.memoryBuffer - 内存缓冲区
33
+ * @param {string} params.currentGroupId - 当前组ID
34
+ * @param {string} params.filename - 文件名
35
+ * @param {boolean} params.isFirstCapture - 是否首次捕获
36
+ * @param {boolean} params.mock - Mock 状态
37
+ * @param {Object} params.requestInfo - 请求信息 { method, rule, pattern, ruleValue, url }
38
+ * @param {Map} params.capturingFiles - 捕获中的文件 Map
39
+ * @param {Object} params.storage - Whistle storage 对象
40
+ */
41
+ async function captureResponse(params) {
42
+ const {
43
+ req,
44
+ res,
45
+ storageAdapter,
46
+ groupManager,
47
+ memoryBuffer,
48
+ currentGroupId,
49
+ filename,
50
+ isFirstCapture,
51
+ mock,
52
+ requestInfo,
53
+ capturingFiles,
54
+ storage,
55
+ } = params
56
+
57
+ const { method, rule, pattern, ruleValue, url } = requestInfo
58
+ const captureKey = `${currentGroupId}/${filename}`
59
+
60
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 🚀 captureResponse 函数开始执行: ${filename}`)
61
+
62
+ // 设置捕获标志
63
+ let resolveCapture
64
+ const capturePromise = new Promise(resolve => { resolveCapture = resolve })
65
+ capturingFiles.set(captureKey, capturePromise)
66
+
67
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 🌐 准备调用 req.request(): ${filename}`)
68
+
69
+ // 发起真实请求
70
+ const client = req.request((svrRes) => {
71
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 🔵 req.request 回调已执行: ${filename}`)
72
+ const encoding = svrRes.headers['content-encoding']
73
+ let body
74
+
75
+ svrRes.on('data', (data) => {
76
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 📥 接收数据块: ${filename}, 大小: ${data?.length || 0}`)
77
+ body = body ? Buffer.concat([body, data]) : data
78
+ })
79
+
80
+ svrRes.on('end', withTryCatch(async () => {
81
+ try {
82
+ if (!body) {
83
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ⚠️ 响应体为空,跳过捕获: ${filename}`)
84
+ return
85
+ }
86
+
87
+ const resContentType = svrRes.headers['content-type'] || ''
88
+ if (!resContentType.includes('application/json')) {
89
+ console.log(`[mockbubu] ⏭️ 非 JSON 响应,跳过捕获: ${filename} (Content-Type: ${resContentType})`)
90
+ return
91
+ }
92
+
93
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 📦 响应接收完成,开始解压: ${filename}`)
94
+ // 解压响应体
95
+ const content = await handleBuffer2String({ body, encoding })
96
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ✅ 解压完成,调用 req.getSession(): ${filename}`)
97
+
98
+ // 获取完整的请求响应数据 (用于构建源数据)
99
+ req.getSession(async (session) => {
100
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 🔍 getSession 回调已触发: ${filename}`)
101
+ console.log('[mockbubu DEBUG] session.req.body存在:', !!session?.req?.body)
102
+ console.log('[mockbubu DEBUG] session.req.body类型:', typeof session?.req?.body)
103
+ if (session?.req?.body) {
104
+ console.log('[mockbubu DEBUG] session.req.body前200字符:', String(session.req.body).substring(0, 200))
105
+ }
106
+ try {
107
+ // 如果设置了 enable://hide 会获取到空数据
108
+ if (!session) {
109
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ⚠️ session 为空(可能设置了 enable://hide): ${filename}`)
110
+ return
111
+ }
112
+
113
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 💾 开始保存源数据: ${filename}`)
114
+ // 保存源数据和文件配置
115
+ await saveSourceData({
116
+ session,
117
+ content,
118
+ storageAdapter,
119
+ groupManager,
120
+ memoryBuffer,
121
+ currentGroupId,
122
+ filename,
123
+ isFirstCapture,
124
+ mock,
125
+ method,
126
+ rule,
127
+ pattern,
128
+ ruleValue,
129
+ url,
130
+ storage,
131
+ })
132
+
133
+ console.log(`[mockbubu] ✅ 捕获完成: ${filename}`)
134
+ } finally {
135
+ // 捕获完成,清除标志并通知等待的请求
136
+ resolveCapture()
137
+ capturingFiles.delete(captureKey)
138
+ }
139
+ })
140
+ } catch (err) {
141
+ console.error(`[mockbubu] ❌ 捕获过程出错: ${filename}`, err)
142
+ // 出错也要清除标志
143
+ resolveCapture()
144
+ capturingFiles.delete(captureKey)
145
+ }
146
+ }))
147
+
148
+ // 将响应透传给客户端
149
+ svrRes.pipe(res)
150
+ })
151
+
152
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ✅ req.request() 已调用,准备 pipe 数据: ${filename}`)
153
+
154
+ req.pipe(client)
155
+
156
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ✅ req.pipe(client) 已完成: ${filename}`)
157
+ }
158
+
159
+ /**
160
+ * 保存源数据到内存缓冲区
161
+ *
162
+ * 职责:
163
+ * - 不写入文件系统(文件仅在用户点击"保存"时创建)
164
+ * - 将完整会话数据添加到内存缓冲区(供前端轮询拉取)
165
+ * - 刷新页面后,memoryBuffer 自动清空,未保存的数据消失
166
+ *
167
+ * @param {Object} params - 参数对象
168
+ */
169
+ async function saveSourceData(params) {
170
+ const {
171
+ session,
172
+ content,
173
+ memoryBuffer,
174
+ currentGroupId,
175
+ url,
176
+ method,
177
+ storage,
178
+ } = params
179
+
180
+ // 1. 构建完整会话数据
181
+ const sourceDataToSave = JSON.parse(JSON.stringify(session))
182
+ sourceDataToSave.res.body = content
183
+
184
+ // 2. 添加到内存缓冲区(供前端轮询拉取)
185
+ // MemoryBuffer 内部会检查暂存区是否已存在,已存在则跳过
186
+ memoryBuffer.add(currentGroupId, url, {
187
+ method,
188
+ status: session.res.statusCode,
189
+ session: sourceDataToSave,
190
+ })
191
+
192
+ // 3. ⚠️ 不要在捕获时创建文件配置
193
+ // 只有用户明确点击"保存"按钮后,才会创建 file.json + versions/
194
+ // 捕获的数据仅保存在 memoryBuffer 中,刷新后自动清空
195
+
196
+ // 4. 标记 API 列表已更新
197
+ await setApiListUpdated(storage, true)
198
+ }
199
+
200
+ module.exports = {
201
+ captureResponse,
202
+ saveSourceData,
203
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 文件名: concurrent.js
3
+ * 功能: 并发捕获控制(共用模块)
4
+ * 职责:
5
+ * - 处理并发捕获时的等待逻辑
6
+ * - 同一文件并发请求时,后续请求等待首个捕获完成
7
+ *
8
+ * 从 lib/core/server-entry/response-handler.js 抽取,供 server / resWrite 钩子共用
9
+ */
10
+
11
+ const { sendMockResponse } = require('./mock-response')
12
+
13
+ /**
14
+ * 处理并发捕获时的等待逻辑
15
+ * @param {Map} capturingFiles - 捕获中的文件 Map
16
+ * @param {string} captureKey - 捕获 key
17
+ * @param {Object} params - 参数对象
18
+ * @returns {Promise<Object>} { handled, response }
19
+ */
20
+ async function handleConcurrentCapture(capturingFiles, captureKey, params) {
21
+ const {
22
+ res,
23
+ storageAdapter,
24
+ groupManager,
25
+ currentGroupId,
26
+ filename,
27
+ mockVersion,
28
+ filePath,
29
+ originalReq,
30
+ } = params
31
+
32
+ if (!capturingFiles.has(captureKey)) {
33
+ return { handled: false }
34
+ }
35
+
36
+ // 正在捕获源数据中,等待捕获完成
37
+ console.log(`[mockbubu] ⏳ 检测到并发请求,等待源数据捕获完成: ${filename}`)
38
+
39
+ try {
40
+ // 等待第一个请求捕获完成(最多等待10秒)
41
+ await Promise.race([
42
+ capturingFiles.get(captureKey),
43
+ new Promise((_resolve, reject) => setTimeout(() => reject(new Error('捕获超时')), 10000)),
44
+ ])
45
+
46
+ // 捕获完成,重新读取源数据
47
+ const freshSourceData = await storageAdapter.readFile(filePath)
48
+ if (freshSourceData) {
49
+ console.log(`[mockbubu] ✅ 等待成功,返回捕获的源数据: ${filename}`)
50
+
51
+ await sendMockResponse(res, freshSourceData, {
52
+ mockVersion,
53
+ groupManager,
54
+ currentGroupId,
55
+ filename,
56
+ originalReq,
57
+ })
58
+
59
+ return { handled: true, response: 'success' }
60
+ } else {
61
+ // 等待后仍无源数据,降级到透传
62
+ console.warn(`[mockbubu] ⚠️ 等待后仍无源数据,降级透传: ${filename}`)
63
+ return { handled: true, response: 'passthrough' }
64
+ }
65
+ } catch (err) {
66
+ // 等待超时或失败,降级到透传
67
+ console.error(`[mockbubu] ❌ 等待捕获失败,降级透传: ${filename}`, err.message)
68
+ return { handled: true, response: 'passthrough' }
69
+ }
70
+ }
71
+
72
+ module.exports = { handleConcurrentCapture }